web框架本质--学Django、flask、tornado框架前必读

小咪咪 2021-12-15 03:47 409阅读 0赞

Web框架本质

众所周知,对于所有的Web应用,本质上其实就是一个socket服务端,用户的浏览器其实就是一个socket客户端。

上述通过socket来实现了其本质,而对于真实开发中的python web程序来说,一般会分为两部分:服务器程序和应用程序。服务器程序负责对socket服务器进行封装,并在请求到来时,对请求的各种数据进行整理。应用程序则负责具体的逻辑处理。为了方便应用程序的开发,就出现了众多的Web框架,例如:Django、Flask、web.py 等。不同的框架有不同的开发方式,但是无论如何,开发出的应用程序都要和服务器程序配合,才能为用户提供服务。这样,服务器程序就需要为不同的框架提供不同的支持。这样混乱的局面无论对于服务器还是框架,都是不好的。对服务器来说,需要支持各种不同框架,对框架来说,只有支持它的服务器才能被开发出的应用使用。这时候,标准化就变得尤为重要。我们可以设立一个标准,只要服务器程序支持这个标准,框架也支持这个标准,那么他们就可以配合使用。一旦标准确定,双方各自实现。这样,服务器可以支持更多支持标准的框架,框架也可以使用更多支持标准的服务器。

什么是 Web 服务器

平时我们都是通过浏览器(比如Chrome,Firefox等)上网的,当我们在浏览器的地址栏里输入地址后,会得到一个网页。

这个网页就是 web 服务器返回给我们的,而浏览器就称为客户端,当我们输入网址并按下回车后,就像web服务器发送了一个web请求。

这种模式成为 B/S 模式 ,即 Browse / Server 模式

  1. 建立连接:客户机通过TCP/IP协议建立到服务器的TCP连接。
  2. 请求过程:客户端向服务器发送HTTP协议请求包(Request),请求服务器里的资源文档。
  3. 应答过程:服务器向客户机发送HTTP协议应答包(Response),如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端。由客户端解释HTML文档,在客户端屏幕上渲染图形结果。
  4. 关闭连接:客户机与服务器断开。

这里Request 和 Response 都需要遵守 HTTP 协议,关于 HTTP 协议的详细内容,可以读读《HTTP 权威指南》

但是实际中的 Web 服务器远远比上面示例的复杂的多,因为要考虑的因素实在是太多了,比如:

  • 缓存机制:讲一些经常被访问的页面缓存起来,提高响应速度;
  • 安全:防止黑客的各种攻击,比如 SYN Flood 攻击;
  • 并发处理:如何响应不同客户端同时发起的请求;
  • 日志:记录访问日至,方便做一些分析。

目前在UNIX和LINUX平台下使用最广泛的免费 Web 服务器有Apache和 Nginx 。而这些软件都是遵循遵守 HTTP 协议的。

所以可以称他们为HTTP服务器,只是可以通过HTTP协议语言的解析转换。

Web 框架(framework)

框架,即framework,特指为解决一个开放性问题而设计的具有一定约束性的支撑结构,使用框架可以帮你快速开发特定的系统,简单地说,就是你用别人搭建好的舞台来做表演。

  1. 1、以 python web 框架** flask** 为例,框架本身并不限定我们用哪种架构来组织我们的应用,不过其中一种比较经典的Web框架 Flask 采用了 MCV 架构,可以很好地支持以 MVC 方式组织应用。
  1. 用户输入 URL,客户端发送请求
  2. 控制器(Controller)首先会拿到请求
  3. 然后用模型(Models)从数据库取出所有需要的数据,进行必要的处理,将处理后的结果发送给 视图(View)
  4. 视图利用获取到的数据,进行渲染生成 Html Response返回给客户端。

    2、还有一种同样热门且强大的 Web 框架: Django

它的模式是:MTV。Django的MTV模式本质是各组件之间为了保持松耦合关系,Django的MTV分别代表:

  1. Model(模型):负责业务对象与数据库的对象(ORM)
  2. Template(模版):负责如何把页面展示给用户
  3. View(视图):负责业务逻辑,并在适当的时候调用Model和Template

    此外,Django还有一个url分发器,它的作用是将一个个URL的页面请求分发给不同的view处理,view再调用相应的Model和Template

第一个Web框架

半成品自定义Web框架

  1. import socket
  2. sk = socket.socket()
  3. sk.bind(("127.0.0.1", 80))
  4. sk.listen()
  5. while True:
  6. conn, addr = sk.accept()
  7. data = conn.recv(8096)
  8. conn.send(b"OK")
  9. conn.close()
  10. 可以说Web服务本质上都是在这十几行代码基础上扩展出来的。这段代码就是它们的祖宗。

用户的浏览器一输入网址,会给服务端发送数据,那浏览器会发送什么数据?怎么发?这个谁来定? 你这个网站是这个规定,他那个网站按照他那个规定,这互联网还能玩么?

所以,必须有一个统一的规则,让大家发送消息、接收消息的时候有个格式依据,不能随便写。

这个规则就是HTTP协议,以后浏览器发送请求信息也好,服务器回复响应信息也罢,都要按照这个规则来。

HTTP协议主要规定了客户端和服务器之间的通信格式,那HTTP协议是怎么规定消息格式的呢?

让我们首先打印下我们在服务端接收到的消息是什么。

  1. import socket
  2. sk = socket.socket()
  3. sk.bind(("127.0.0.1", 80))
  4. sk.listen()
  5. while True:
  6. conn, addr = sk.accept()
  7. data = conn.recv(8096)
  8. print(data) # 将浏览器发来的消息打印出来
  9. conn.send(b"OK")
  10. conn.close()

输出:

  1. b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\nConnection: keep-
  2. alive\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0
  3. (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like
  4. Gecko) Chrome/64.0.3282.186 Safari/537.36\r\nAccept:
  5. text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,
  6. image/apng,*/*;q=0.8\r\nDNT: 1\r\nAccept-Encoding: gzip, deflate,
  7. br\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie:
  8. csrftoken=RKBXh1d3M97iz03Rpbojx1bR6mhHudhyX5PszUxxG3bOEw
  9. h1lxFpGOgWN93ZH3zv\r\n\r\n'

最简单版本自定义Web框架

经过上面的补充学习,我们知道了要想让我们自己写的web server端正经起来,必须要让我们的Web server在给客户端回复消息的时候按照HTTP协议的规则加上响应状态行,这样我们就实现了一个正经的Web框架了。

  1. import socket
  2. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  3. sock.bind(('127.0.0.1', 8000))
  4. sock.listen()
  5. while True:
  6. conn, addr = sock.accept()
  7. data = conn.recv(8096)
  8. # 给回复的消息加上响应状态行
  9. conn.send(b"HTTP/1.1 200 OK\r\n\r\n")
  10. conn.send(b"OK")
  11. conn.close()

我们通过十几行代码简单地演示了web 框架的本质。

接下来就让我们继续完善我们的自定义web框架吧!

根据路径的不同返回不同内容

这样就结束了吗? 如何让我们的Web服务根据用户请求的URL不同而返回不同的内容呢?

小事一桩,我们可以从请求相关数据里面拿到请求URL的路径,然后拿路径做一个判断…

  1. """
  2. 根据URL中不同的路径返回不同的内容
  3. """
  4. import socket
  5. sk = socket.socket()
  6. sk.bind(("127.0.0.1", 8080)) # 绑定IP和端口
  7. sk.listen() # 监听
  8. while 1:
  9. # 等待连接
  10. conn, add = sk.accept()
  11. data = conn.recv(8096) # 接收客户端发来的消息
  12. # 从data中取到路径
  13. data = str(data, encoding="utf8") # 把收到的字节类型的数据转换成字符串
  14. # 按\r\n分割
  15. data1 = data.split("\r\n")[0]
  16. url = data1.split()[1] # url是我们从浏览器发过来的消息中分离出的访问路径
  17. conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 因为要遵循HTTP协议,所以回复的消息也要加状态行
  18. # 根据不同的路径返回不同内容
  19. if url == "/index/":
  20. response = b"index"
  21. elif url == "/home/":
  22. response = b"home"
  23. else:
  24. response = b"404 not found!"
  25. conn.send(response)
  26. conn.close()

根据不同的路径返回不同的内容—-函数版

上面的代码解决了不同URL路径返回不同内容的需求。

但是问题又来了,如果有很多很多路径要判断怎么办?难道要挨个写if判断? 当然不用,我们有更聪明的办法。

  1. """
  2. 根据URL中不同的路径返回不同的内容--函数版
  3. """
  4. import socket
  5. sk = socket.socket()
  6. sk.bind(("127.0.0.1", 8080)) # 绑定IP和端口
  7. sk.listen() # 监听
  8. # 将返回不同的内容部分封装成函数
  9. def index(url):
  10. s = "这是{}页面!".format(url)
  11. return bytes(s, encoding="utf8")
  12. def home(url):
  13. s = "这是{}页面!".format(url)
  14. return bytes(s, encoding="utf8")
  15. while 1:
  16. # 等待连接
  17. conn, add = sk.accept()
  18. data = conn.recv(8096) # 接收客户端发来的消息
  19. # 从data中取到路径
  20. data = str(data, encoding="utf8") # 把收到的字节类型的数据转换成字符串
  21. # 按\r\n分割
  22. data1 = data.split("\r\n")[0]
  23. url = data1.split()[1] # url是我们从浏览器发过来的消息中分离出的访问路径
  24. conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 因为要遵循HTTP协议,所以回复的消息也要加状态行
  25. # 根据不同的路径返回不同内容,response是具体的响应体
  26. if url == "/index/":
  27. response = index(url)
  28. elif url == "/home/":
  29. response = home(url)
  30. else:
  31. response = b"404 not found!"
  32. conn.send(response)
  33. conn.close()

根据不同的路径返回不同的内容—-函数进阶版

看起来上面的代码还是要挨个写if判断,怎么办?我们还是有办法!(只要思想不滑坡,方法总比问题多!)

  1. """
  2. 根据URL中不同的路径返回不同的内容--函数进阶版
  3. """
  4. import socket
  5. sk = socket.socket()
  6. sk.bind(("127.0.0.1", 8080)) # 绑定IP和端口
  7. sk.listen() # 监听
  8. # 将返回不同的内容部分封装成函数
  9. def index(url):
  10. s = "这是{}页面!".format(url)
  11. return bytes(s, encoding="utf8")
  12. def home(url):
  13. s = "这是{}页面!".format(url)
  14. return bytes(s, encoding="utf8")
  15. # 定义一个url和实际要执行的函数的对应关系
  16. list1 = [
  17. ("/index/", index),
  18. ("/home/", home),
  19. ]
  20. while 1:
  21. # 等待连接
  22. conn, add = sk.accept()
  23. data = conn.recv(8096) # 接收客户端发来的消息
  24. # 从data中取到路径
  25. data = str(data, encoding="utf8") # 把收到的字节类型的数据转换成字符串
  26. # 按\r\n分割
  27. data1 = data.split("\r\n")[0]
  28. url = data1.split()[1] # url是我们从浏览器发过来的消息中分离出的访问路径
  29. conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 因为要遵循HTTP协议,所以回复的消息也要加状态行
  30. # 根据不同的路径返回不同内容
  31. func = None # 定义一个保存将要执行的函数名的变量
  32. for i in list1:
  33. if i[0] == url:
  34. func = i[1]
  35. break
  36. if func:
  37. response = func(url)
  38. else:
  39. response = b"404 not found!"
  40. # 返回具体的响应消息
  41. conn.send(response)
  42. conn.close()

返回具体的HTML文件

完美解决了不同URL返回不同内容的问题。 但是我不想仅仅返回几个字符串,我想给浏览器返回完整的HTML内容,这又该怎么办呢?

没问题,不管是什么内容,最后都是转换成字节数据发送出去的。 我们可以打开HTML文件,读取出它内部的二进制数据,然后再发送给浏览器。

  1. """
  2. 根据URL中不同的路径返回不同的内容--函数进阶版
  3. 返回独立的HTML页面
  4. """
  5. import socket
  6. sk = socket.socket()
  7. sk.bind(("127.0.0.1", 8080)) # 绑定IP和端口
  8. sk.listen() # 监听
  9. # 将返回不同的内容部分封装成函数
  10. def index(url):
  11. # 读取index.html页面的内容
  12. with open("index.html", "r", encoding="utf8") as f:
  13. s = f.read()
  14. # 返回字节数据
  15. return bytes(s, encoding="utf8")
  16. def home(url):
  17. with open("home.html", "r", encoding="utf8") as f:
  18. s = f.read()
  19. return bytes(s, encoding="utf8")
  20. # 定义一个url和实际要执行的函数的对应关系
  21. list1 = [
  22. ("/index/", index),
  23. ("/home/", home),
  24. ]
  25. while 1:
  26. # 等待连接
  27. conn, add = sk.accept()
  28. data = conn.recv(8096) # 接收客户端发来的消息
  29. # 从data中取到路径
  30. data = str(data, encoding="utf8") # 把收到的字节类型的数据转换成字符串
  31. # 按\r\n分割
  32. data1 = data.split("\r\n")[0]
  33. url = data1.split()[1] # url是我们从浏览器发过来的消息中分离出的访问路径
  34. conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 因为要遵循HTTP协议,所以回复的消息也要加状态行
  35. # 根据不同的路径返回不同内容
  36. func = None # 定义一个保存将要执行的函数名的变量
  37. for i in list1:
  38. if i[0] == url:
  39. func = i[1]
  40. break
  41. if func:
  42. response = func(url)
  43. else:
  44. response = b"404 not found!"
  45. # 返回具体的响应消息
  46. conn.send(response)
  47. conn.close()

在返回具体HTML文件同时让网页动起来

这网页能够显示出来了,但是都是静态的啊。页面的内容都不会变化的,我想要的是动态网站。

没问题,我也有办法解决。我选择使用字符串替换来实现这个需求。(这里使用时间戳来模拟动态的数据)

  1. """
  2. 根据URL中不同的路径返回不同的内容--函数进阶版
  3. 返回HTML页面
  4. 让网页动态起来
  5. """
  6. import socket
  7. import time
  8. sk = socket.socket()
  9. sk.bind(("127.0.0.1", 8080)) # 绑定IP和端口
  10. sk.listen() # 监听
  11. # 将返回不同的内容部分封装成函数
  12. def index(url):
  13. with open("index.html", "r", encoding="utf8") as f:
  14. s = f.read()
  15. now = str(time.time())
  16. s = s.replace("@@oo@@", now) # 在网页中定义好特殊符号,用动态的数据去替换提前定义好的特殊符号
  17. return bytes(s, encoding="utf8")
  18. def home(url):
  19. with open("home.html", "r", encoding="utf8") as f:
  20. s = f.read()
  21. return bytes(s, encoding="utf8")
  22. # 定义一个url和实际要执行的函数的对应关系
  23. list1 = [
  24. ("/index/", index),
  25. ("/home/", home),
  26. ]
  27. while 1:
  28. # 等待连接
  29. conn, add = sk.accept()
  30. data = conn.recv(8096) # 接收客户端发来的消息
  31. # 从data中取到路径
  32. data = str(data, encoding="utf8") # 把收到的字节类型的数据转换成字符串
  33. # 按\r\n分割
  34. data1 = data.split("\r\n")[0]
  35. url = data1.split()[1] # url是我们从浏览器发过来的消息中分离出的访问路径
  36. conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 因为要遵循HTTP协议,所以回复的消息也要加状态行
  37. # 根据不同的路径返回不同内容
  38. func = None # 定义一个保存将要执行的函数名的变量
  39. for i in list1:
  40. if i[0] == url:
  41. func = i[1]
  42. break
  43. if func:
  44. response = func(url)
  45. else:
  46. response = b"404 not found!"
  47. # 返回具体的响应消息
  48. conn.send(response)
  49. conn.close()

服务器程序和应用程序

对于真实开发中的python web程序来说,一般会分为两部分:服务器程序和应用程序。

服务器程序负责对socket服务器进行封装,并在请求到来时,对请求的各种数据进行整理。

应用程序则负责具体的逻辑处理。为了方便应用程序的开发,就出现了众多的Web框架,例如:Django、Flask、web.py 等。不同的框架有不同的开发方式,但是无论如何,开发出的应用程序都要和服务器程序配合,才能为用户提供服务。

这样,服务器程序就需要为不同的框架提供不同的支持。这样混乱的局面无论对于服务器还是框架,都是不好的。对服务器来说,需要支持各种不同框架,对框架来说,只有支持它的服务器才能被开发出的应用使用。

这时候,标准化就变得尤为重要。我们可以设立一个标准,只要服务器程序支持这个标准,框架也支持这个标准,那么他们就可以配合使用。一旦标准确定,双方各自实现。这样,服务器可以支持更多支持标准的框架,框架也可以使用更多支持标准的服务器。

WSGI(Web Server Gateway Interface)就是一种规范,它定义了使用Python编写的web应用程序与web服务器程序之间的接口格式,实现web应用程序与web服务器程序间的解耦。

常用的WSGI服务器有uwsgi、Gunicorn。而Python标准库提供的独立WSGI服务器叫wsgiref,Django开发环境用的就是这个模块来做服务器。

Wsgiref

我们利用wsgiref模块来替换我们自己写的web框架的socket server部分:

  1. """
  2. 根据URL中不同的路径返回不同的内容--函数进阶版
  3. 返回HTML页面
  4. 让网页动态起来
  5. wsgiref模块版
  6. """
  7. import time
  8. from wsgiref.simple_server import make_server
  9. # 将返回不同的内容部分封装成函数
  10. def index(url):
  11. with open("index.html", "r", encoding="utf8") as f:
  12. s = f.read()
  13. now = str(time.time())
  14. s = s.replace("@@oo@@", now)
  15. return bytes(s, encoding="utf8")
  16. def home(url):
  17. with open("home.html", "r", encoding="utf8") as f:
  18. s = f.read()
  19. return bytes(s, encoding="utf8")
  20. # 定义一个url和实际要执行的函数的对应关系
  21. list1 = [
  22. ("/index/", index),
  23. ("/home/", home),
  24. ]
  25. def run_server(environ, start_response):
  26. start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'), ]) # 设置HTTP响应的状态码和头信息
  27. url = environ['PATH_INFO'] # 取到用户输入的url
  28. func = None
  29. for i in list1:
  30. if i[0] == url:
  31. func = i[1]
  32. break
  33. if func:
  34. response = func(url)
  35. else:
  36. response = b"404 not found!"
  37. return [response, ]
  38. if __name__ == '__main__':
  39. httpd = make_server('127.0.0.1', 8090, run_server)
  40. print("我在8090等你哦...")
  41. httpd.serve_forever()

jinja2

上面的代码实现了一个简单的动态,我完全可以从数据库中查询数据,然后去替换我html中的对应内容,然后再发送给浏览器完成渲染。 这个过程就相当于HTML模板渲染数据。 本质上就是HTML内容中利用一些特殊的符号来替换要展示的数据。 我这里用的特殊符号是我定义的,其实模板渲染有个现成的工具: jinja2

下载jinja2:

  1. pip install jinja2

index2.html文件

  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="x-ua-compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1">
  7. <title>Title</title>
  8. </head>
  9. <body>
  10. <h1>姓名:{
  11. {name}}</h1>
  12. <h1>爱好:</h1>
  13. <ul>
  14. {% for hobby in hobby_list %}
  15. <li>{
  16. {hobby}}</li>
  17. {% endfor %}
  18. </ul>
  19. </body>
  20. </html>

使用jinja2渲染index2.html文件:

  1. from wsgiref.simple_server import make_server
  2. from jinja2 import Template
  3. def index():
  4. with open("index2.html", "r") as f:
  5. data = f.read()
  6. template = Template(data) # 生成模板文件
  7. ret = template.render({"name": "Alex", "hobby_list": ["烫头", "泡吧"]}) # 把数据填充到模板里面
  8. return [bytes(ret, encoding="utf8"), ]
  9. def home():
  10. with open("home.html", "rb") as f:
  11. data = f.read()
  12. return [data, ]
  13. # 定义一个url和函数的对应关系
  14. URL_LIST = [
  15. ("/index/", index),
  16. ("/home/", home),
  17. ]
  18. def run_server(environ, start_response):
  19. start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'), ]) # 设置HTTP响应的状态码和头信息
  20. url = environ['PATH_INFO'] # 取到用户输入的url
  21. func = None # 将要执行的函数
  22. for i in URL_LIST:
  23. if i[0] == url:
  24. func = i[1] # 去之前定义好的url列表里找url应该执行的函数
  25. break
  26. if func: # 如果能找到要执行的函数
  27. return func() # 返回函数的执行结果
  28. else:
  29. return [bytes("404没有该页面", encoding="utf8"), ]
  30. if __name__ == '__main__':
  31. httpd = make_server('', 8000, run_server)
  32. print("Serving HTTP on port 8000...")
  33. httpd.serve_forever()

现在的数据是我们自己手写的,那可不可以从数据库中查询数据,来填充页面呢?

使用pymysql连接数据库:

  1. conn = pymysql.connect(host="127.0.0.1", port=3306, user="root", passwd="xxx", db="xxx", charset="utf8")
  2. cursor = conn.cursor(cursor=pymysql.cursors.DictCursor)
  3. cursor.execute("select name, age, department_id from userinfo")
  4. user_list = cursor.fetchall()
  5. cursor.close()
  6. conn.close()

创建一个测试的user表:

  1. CREATE TABLE user(
  2. id int auto_increment PRIMARY KEY,
  3. name CHAR(10) NOT NULL,
  4. hobby CHAR(20) NOT NULL
  5. )engine=innodb DEFAULT charset=UTF8;

模板的原理就是字符串替换,我们只要在HTML页面中遵循jinja2的语法规则写上,其内部就会按照指定的语法进行相应的替换,从而达到动态的返回内容。

结束语:

web框架就是按照上述过程搭建和运行的,Django,Flask,Tornado等框架只不过是将功能代码进行封装而已。掌握了上述的流程,之后再学上述web框架将会事半功倍。

发表评论

表情:
评论列表 (有 0 条评论,409人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Django基础之web框架本质

    一 web框架的本质及自定义web框架   可以这样理解:所有的Web应用本质上就是一个socket服务端,而用户的浏览器就是一个socket客户端,基于请求做出响应,客