web 框架的本质
web框架的本质——通俗的说,web框架封装了socket、数据库操作、路由分发、模板配置等,留给我们现成的接口,让我们更专注业务逻辑本身。
一个简单的web示例
import socket # 1. 创建socket对象 sk = socket.socket() # 2. 绑定IP和端口 sk.bind(('127.0.0.1', 8888)) # 3. 监听 sk.listen(5) # 4. 循环监听 while 1: conn, addr = sk.accept() # 等待连接 received_data = conn.recv(8192) # 接收数据 print(received_data) print(received_data.decode(encoding="utf-8")) conn.send(b'HTTP/1.1 200 OK\r\n\r\n') # 接收数据 conn.send(b'<h1>Successful</h1>') conn.close() # 关闭连接
现在,使用浏览器地址栏访问http://127.0.0.1:8888/
,就会得到Successful
。
用户的浏览器一输入网址,会给服务端发送数据,那浏览器会发送什么数据?怎么发?这个谁来定? 你这个网站是这个规定,他那个网站按照他那个规定,这互联网还能玩么?
所以,必须有一个统一的规则,让大家发送消息、接收消息的时候有个格式依据,不能随便写。
这个规则就是HTTP协议,以后浏览器发送请求信息也好,服务器回复响应信息也罢,都要按照这个规则来。
HTTP协议主要规定了客户端和服务器之间的通信格式,那HTTP协议是怎么规定消息格式的呢?
让我们首先打印下我们在服务端接收到的消息是什么。
b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8888\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nsec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"\r\nsec-ch-ua-mobile: ?0\r\nsec-ch-ua-platform: "Windows"\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nSec-Fetch-Site: none\r\nSec-Fetch-Mode: navigate\r\nSec-Fetch-User: ?1\r\nSec-Fetch-Dest: document\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9\r\n\r\n'
GET / HTTP/1.1
Host: 127.0.0.1:8888
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
b'GET /favicon.ico HTTP/1.1\r\nHost: 127.0.0.1:8888\r\nConnection: keep-alive\r\nsec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"\r\nsec-ch-ua-mobile: ?0\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36\r\nsec-ch-ua-platform: "Windows"\r\nAccept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8\r\nSec-Fetch-Site: same-origin\r\nSec-Fetch-Mode: no-cors\r\nSec-Fetch-Dest: image\r\nReferer: http://127.0.0.1:8888/\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9\r\n\r\n'
GET /favicon.ico HTTP/1.1
Host: 127.0.0.1:8888
Connection: keep-alive
sec-ch-ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
sec-ch-ua-platform: "Windows"
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: http://127.0.0.1:8888/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
根据不同路径返回不同内容
第一版·普通版
import socket # 创建socket对象 sk = socket.socket() # 绑定IP和端口 sk.bind(('127.0.0.1', 8888)) # 监听 sk.listen() while True: # 等待连接 conn, addr = sk.accept() # 接收数据 data = conn.recv(8192) data = data.decode('utf-8') url = data.split()[1] print(url) # 返回状态行 conn.send(b'HTTP/1.1 200 OK\r\n\r\n') if url == '/oumei': conn.send(b'<h1>oumei</h1>') elif url == '/rihan': conn.send(b'<h1>rihan</h1>') else: conn.send(b'<h1>404</h1>') # 关闭连接 conn.close()
第二版·函数版
import socket # 创建socket对象 sk = socket.socket() # 绑定IP和端口 sk.bind(('127.0.0.1', 8888)) # 监听 sk.listen() def oumei(url): ret = 'oumei - {}'.format(url) return ret.encode('utf-8') def rihan(url): ret = 'rihan//// - {}'.format(url) return ret.encode('utf-8') while True: # 等待连接 conn, addr = sk.accept() # 接收数据 data = conn.recv(8192) data = data.decode('utf-8') url = data.split()[1] print(url) # 返回状态行 conn.send(b'HTTP/1.1 200 OK\r\n\r\n') if url == '/oumei': ret = oumei(url) elif url == '/rihan': ret = rihan(url) else: ret = b'404 not found' conn.send(ret) # 关闭连接 conn.close()
第三版·函数进阶版
import socket # 创建socket对象 sk = socket.socket() # 绑定IP和端口 sk.bind(('127.0.0.1', 8888)) # 监听 sk.listen() def oumei(url): ret = 'oumei - {}'.format(url) return ret.encode('utf-8') def rihan(url): ret = 'rihan//// - {}'.format(url) return ret.encode('utf-8') def guochan(url): ret = 'guochan - {}'.format(url) return ret.encode('utf-8') list = [ ('/oumei', oumei), ('/rihan', rihan), ('/guochan', guochan), ] while True: # 等待连接 conn, addr = sk.accept() # 接收数据 data = conn.recv(8192) data = data.decode('utf-8') url = data.split()[1] print(url) # 返回状态行 conn.send(b'HTTP/1.1 200 OK\r\n\r\n') func = None for i in list: if i[0] == url: func = i[1] break if func: ret = func(url) else: ret = b'404 not found' conn.send(ret) # 关闭连接 conn.close()
第四版·反射版
ps:仅针对当前脚本使用的反射。
import socket sk = socket.socket() sk.bind(("127.0.0.1", 8888)) sk.listen() class Server(object): """ 模拟多个页面 """ def europe(self): return "<h1>europe</h1>".encode('utf8') def kkc(self): return "<h1>kkc</h1>".encode('utf8') def notfond(self): return "<h1>404 not fond</h1>".encode('utf8') while 1: # 等待请求 conn, address = sk.accept() # 获取数据 data = conn.recv(8192).decode('utf8') # 处理请求拿到请求路径 # print(data) url = data.split(' ', 2)[1][1:] # print(url, data) # 返回数据 server_obj = Server() if hasattr(server_obj, url): res = getattr(server_obj, url)() else: res = server_obj.notfond() conn.send(b'HTTP/1.1 200 OK\r\n\r\n') conn.send(res) conn.close()
返回HTML页面
首先在同级目录有个index.html
页面:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> </head> <body> <h1>index page</h1> </body> </html>
然后写server
端:
import socket # 创建socket对象 sk = socket.socket() # 绑定IP和端口 sk.bind(('127.0.0.1', 8888)) # 监听 sk.listen() def oumei(url): ret = 'oumei - {}'.format(url) return ret.encode('utf-8') def rihan(url): ret = 'rihan//// - {}'.format(url) return ret.encode('utf-8') def guochan(url): ret = 'guochan - {}'.format(url) return ret.encode('utf-8') def index(url): with open('index.html', 'rb') as f: ret = f.read() return ret list = [ ('/oumei', oumei), ('/rihan', rihan), ('/guochan', guochan), ('/index', index), ] while True: # 等待连接 conn, addr = sk.accept() # 接收数据 data = conn.recv(8192) data = data.decode('utf-8') url = data.split()[1] print(url) # 返回状态行 conn.send(b'HTTP/1.1 200 OK\r\n\r\n') func = None for i in list: if i[0] == url: func = i[1] break if func: ret = func(url) else: ret = b'404 not found' conn.send(ret) # 关闭连接 conn.close()
返回动态页面
在同级目录有个tiem.html
页面:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>当前时间是: @@time@@ </h1> </body> </html>
在来实现代码:
import socket
import time
# 创建socket对象
sk = socket.socket()
# 绑定IP和端口
sk.bind(('0.0.0.0', 8000))
# 监听
sk.listen()
def oumei(url):
ret = 'oumei - {}'.format(url)
return ret.encode('utf-8')
def rihan(url):
ret = 'rihan//// - {}'.format(url)
return ret.encode('utf-8')
def guochan(url):
ret = 'guochan - {}'.format(url)
return ret.encode('utf-8')
def index(url):
with open('index.html', 'rb') as f:
ret = f.read()
return ret
def timer(url):
now = time.strftime('%Y-%m-%d %H:%M:%S')
with open('time.html', 'r', encoding='utf-8') as f:
data = f.read()
data = data.replace('@@time@@', now)
return data.encode('utf-8')
list = [
('/oumei', oumei),
('/rihan', rihan),
('/guochan', guochan),
('/index', index),
('/time', timer),
]
while True:
# 等待连接
conn, addr = sk.accept()
# 接收数据
data = conn.recv(8096)
data = data.decode('utf-8')
url = data.split()[1]
print(url)
# 返回状态行
conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
func = None
for i in list:
if i[0] == url:
func = i[1]
break
if func:
ret = func(url)
else:
ret = b'404 not found'
conn.send(ret)
# 关闭连接
conn.close()
服务器程序和应用程序
正如文章开始所示,web框架本质就是一个socket服务端。
web框架功能:
- socket收发消息
- 根据不同的路径返回不同的内容
- 可以返回动态页面(使用字符串替换等方式进行模板渲染)
对于真实开发中的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开发环境用的就是这个模块来做服务器。
为什么需要WSGI
在Web部署的方案上,有一个方案是目前应用最广泛的:
首先,部署一个Web服务器专门用来处理HTTP协议层面相关的事情,比如如何在一个物理机上提供多个不同的Web服务(单IP多域名,单IP多端口等)这种事情。
然后,部署一个用各种语言编写(Java, PHP, Python, Ruby等)的应用程序,这个应用程序会从Web服务器上接收客户端的请求,处理完成后,再返回响应给Web服务器,最后由Web服务器返回给客户端。
那么 Web Server 和 Application 之间就要知道如何进行交互。为了定义Web服务器和应用程序之间的交互过程,就形成了很多不同的规范。比如改进CGI性能的FasgCGI,Java专用的Servlet规范,还有Python专用的WSGI规范等。提出这些规范的目的就是为了定义统一的标准,提升程序的可移植性。
WSGI原理
WSGI 相当于 Web Server 和 Python Application 之间的桥梁,隐藏了很多HTTP相关的细节。其存在的目的有两个:
让 Web Server 知道如何调用 Python Application,并且将 Client Request 传递给 Application。
让 Python Application 能理解 Client Request 并执行对应操作,以及将执行结果返回给 Web Server,最终响应到Client。
Server 调用 Application
从上图可知,Server端调用Application端必须以WSGI为桥梁,因此WSGI定义了application可调用对象以提供Server端和Application端通信,这个可调用对象既可以是函数也可以是类。
# 函数形式 def application(environ, start_response): ''' doing something ''' return [response_body] # 类形式 class Application: def __init__(self, environ, start_response): self.environ = environ self.start = start_response def __iter__(self): status = ‘200 OK’ response_headers = [(‘Content-type’, ‘text/plain’)] self.start(status, response_headers) yield HELLO_WORLD # 上面的类形式是将“Application”类作为服务端调用的application, # 调用这个类会返回它的实例,其结果会返回规范中要求的可迭代响应值。 # 如果要使用这个类的实例作为application对象,就需要实现__call__方法,服务端会调用这个实例去执行应用。 # 下面是pecan的实现方法。 class PecanBase(object): def __init__(self, root, default_renderer='mako', template_path='templates', hooks=lambda: [], custom_renderers={}, extra_template_vars={}, force_canonical=True, guess_content_type_from_ext=True, context_local_factory=None, request_cls=Request, response_cls=Response, **kw): ''' 省略 ''' def __call__(self, environ, start_response): ''' Implements the WSGI specification for Pecan applications, utilizing ``WebOb``. ''' # create the request and response object req = self.request_cls(environ) resp = self.response_cls() state = RoutingState(req, resp, self) environ['pecan.locals'] = { 'request': req, 'response': resp } controller = None # track internal redirects internal_redirect = False # handle the request try: # add context and environment to the request req.context = environ.get('pecan.recursive.context', {}) req.pecan = dict(content_type=None) controller, args, kwargs = self.find_controller(state) self.invoke_controller(controller, args, kwargs, state) except Exception as e: # if this is an HTTP Exception, set it as the response if isinstance(e, exc.HTTPException): # if the client asked for JSON, do our best to provide it accept_header = acceptparse.create_accept_header( getattr(req.accept, 'header_value', '*/*') or '*/*') offers = accept_header.acceptable_offers( ('text/plain', 'text/html', 'application/json')) best_match = offers[0][0] if offers else None state.response = e if best_match == 'application/json': json_body = dumps({ 'code': e.status_int, 'title': e.title, 'description': e.detail }) if isinstance(json_body, six.text_type): e.text = json_body else: e.body = json_body state.response.content_type = best_match environ['pecan.original_exception'] = e # note if this is an internal redirect internal_redirect = isinstance(e, ForwardRequestException) # if this is not an internal redirect, run error hooks on_error_result = None if not internal_redirect: on_error_result = self.handle_hooks( self.determine_hooks(state.controller), 'on_error', state, e ) # if the on_error handler returned a Response, use it. if isinstance(on_error_result, WebObResponse): state.response = on_error_result else: if not isinstance(e, exc.HTTPException): raise # if this is an HTTP 405, attempt to specify an Allow header if isinstance(e, exc.HTTPMethodNotAllowed) and controller: allowed_methods = _cfg(controller).get('allowed_methods', []) if allowed_methods: state.response.allow = sorted(allowed_methods) finally: # if this is not an internal redirect, run "after" hooks if not internal_redirect: self.handle_hooks( self.determine_hooks(state.controller), 'after', state ) self._handle_empty_response_body(state) # get the response return state.response(environ, start_response)
这个可调用对象需要满足两个条件:
两个参数
一个dict对象,Web Server会将HTTP请求相关的信息添加到这个字典中,供Web application使用
一个callback函数,Web application通过这个函数将HTTP status code和headers发送给Web Server
以字符串的形式返回response,并且包含在可迭代的list中
Server端将http请求相关信息、wsgi变量以及一些服务端环境变量添加到environ传给Application端,Application端处理完所需信息后将http状态码和header通过start_response回调函数传给Server端,而http响应body则以返回值的形式传给服务端。
可以看出,仅仅一个application(environ, start_response)仍然显得太底层,在web应用开发过程中效率不高,因此衍生出各种 Web 框架来帮助开发人员快速的开发Web应用,开发人员只需要关注业务层逻辑,不需要过多的处理http相关信息。
示例
在Python中就有一个WSGI server,提供给开发人员测试使用。
# WSGI server in Python from wsgiref.simple_server import make_server def application(environ, start_response): response_body = ['%s: %s' % (key, value) for key, value in sorted(environ.items())] response_body = '\n'.join(response_body) status = '200 OK' response_headers = [('Content-Type', 'text/plain'), ('Content-Length', str(len(response_body)))] start_response(status, response_headers) return [response_body.encode('utf8')] httpd = make_server( 'localhost', 8080, application ) # 请求处理完退出 httpd.handle_request()
访问http://localhost:8080
返回结果:
ADSK_3DSMAX_X64_2018: C:\Program Files\Autodesk\3ds Max 2018\ ADSK_CLM_WPAD_PROXY_CHECK: FALSE ALLUSERSPROFILE: C:\ProgramData ANDROID_HOME: C:\Program Files (x86)\Android\android-sdk APPDATA: C:\Users\ryan\AppData\Roaming COMMONPROGRAMFILES: C:\Program Files\Common Files COMMONPROGRAMFILES(X86): C:\Program Files (x86)\Common Files COMMONPROGRAMW6432: C:\Program Files\Common Files COMPUTERNAME: LAPTOP-11PD23IA COMSPEC: C:\WINDOWS\system32\cmd.exe CONTENT_LENGTH: CONTENT_TYPE: text/plain DRIVERDATA: C:\Windows\System32\Drivers\DriverData FPS_BROWSER_APP_PROFILE_STRING: Internet Explorer FPS_BROWSER_USER_PROFILE_STRING: Default GATEWAY_INTERFACE: CGI/1.1 HOMEDRIVE: C: HOMEPATH: \Users\ryan HTTP_ACCEPT: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 HTTP_ACCEPT_ENCODING: gzip, deflate, br HTTP_ACCEPT_LANGUAGE: zh-CN,zh;q=0.9 HTTP_CONNECTION: keep-alive HTTP_HOST: 127.0.0.1:8080 HTTP_SEC_CH_UA: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105" HTTP_SEC_CH_UA_MOBILE: ?0 HTTP_SEC_CH_UA_PLATFORM: "Windows" HTTP_SEC_FETCH_DEST: document HTTP_SEC_FETCH_MODE: navigate HTTP_SEC_FETCH_SITE: none HTTP_SEC_FETCH_USER: ?1 HTTP_UPGRADE_INSECURE_REQUESTS: 1 HTTP_USER_AGENT: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 IDEA_INITIAL_DIRECTORY: C:\Users\ryan\Desktop JAVA_HOME: C:\Program Files\Java\jdk1.8.0_151 LOCALAPPDATA: C:\Users\ryan\AppData\Local LOGONSERVER: \\LAPTOP-11PD23IA NUMBER_OF_PROCESSORS: 12 ONEDRIVE: C:\Users\ryan\OneDrive ONLINESERVICES: Online Services OS: Windows_NT PATH: C:\Program Files\ImageMagick-7.1.0-Q16-HDRI;C:\Program Files\PlasticSCM5\server;C:\Program Files\PlasticSCM5\client;C:\ProgramData\Oracle\Java\javapath;C:\windows\system32;C:\windows;C:\windows\System32\Wbem;C:\windows\System32\WindowsPowerShell\v1.0\;C:\windows\System32\OpenSSH\;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files\NVIDIA Corporation\NVIDIA NvDLISR;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python39_64;C:\Program Files (x86)\Microsoft Visual Studio\Shared\Python39_64\Scripts;C:\Users\ryan\AppData\Roaming\Python\Python39\Scripts;C:\Program Files\Java\jdk1.8.0_151\bin;C:\Program Files\Java\jdk1.8.0_151\lib\tools.jar;C:\Program Files\Java\jdk1.8.0_151\jre\bin;C:\Program Files (x86)\Android\android-sdk\tools;C:\Program Files (x86)\Android\android-sdk\platform-tools;C:\Program Files\Common Files\Autodesk Shared\;C:\Program Files (x86)\Autodesk\Backburner\;C:\Program Files\nodejs\;C:\Program Files\dotnet\;C:\Program Files\Microsoft SQL Server\130\Tools\Binn\;C:\Program Files\Microsoft SQL Server\Client SDK\ODBC\170\Tools\Binn\;C:\Program Files\MySQL\MySQL Shell 8.0\bin\;C:\Users\ryan\AppData\Local\Microsoft\WindowsApps;;C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\bin;;C:\Users\ryan\AppData\Roaming\npm;C:\Users\ryan\.dotnet\tools PATHEXT: .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC PATH_INFO: / PLATFORMCODE: KV PROCESSOR_ARCHITECTURE: AMD64 PROCESSOR_IDENTIFIER: Intel64 Family 6 Model 158 Stepping 13, GenuineIntel PROCESSOR_LEVEL: 6 PROCESSOR_REVISION: 9e0d PROGRAMDATA: C:\ProgramData PROGRAMFILES: C:\Program Files PROGRAMFILES(X86): C:\Program Files (x86) PROGRAMW6432: C:\Program Files PSMODULEPATH: C:\Program Files\WindowsPowerShell\Modules;C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules PUBLIC: C:\Users\Public PYCHARM COMMUNITY EDITION: C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\bin; PYCHARM_HOSTED: 1 PYTHONIOENCODING: UTF-8 PYTHONPATH: D:\Develop\BackEnd\workkspace PYTHONUNBUFFERED: 1 QUERY_STRING: REGIONCODE: APJ REMOTE_ADDR: 127.0.0.1 REMOTE_HOST: REQUEST_METHOD: GET SCRIPT_NAME: SERVER_NAME: LAPTOP-11PD23IA SERVER_PORT: 8080 SERVER_PROTOCOL: HTTP/1.1 SERVER_SOFTWARE: WSGIServer/0.2 SESSIONNAME: Console SSLKEYLOGFILE: D:\Develop\log SYSTEMDRIVE: C: SYSTEMROOT: C:\WINDOWS TEMP: C:\Users\ryan\AppData\Local\Temp TMP: C:\Users\ryan\AppData\Local\Temp USERDOMAIN: LAPTOP-11PD23IA USERDOMAIN_ROAMINGPROFILE: LAPTOP-11PD23IA USERNAME: ryan USERPROFILE: C:\Users\ryan WINDIR: C:\WINDOWS wsgi.errors: <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'> wsgi.file_wrapper: <class 'wsgiref.util.FileWrapper'> wsgi.input: <_io.BufferedReader name=604> wsgi.multiprocess: False wsgi.multithread: False wsgi.run_once: False wsgi.url_scheme: http wsgi.version: (1, 0)
environ参数
environ字典包含了一些CGI规范要求的数据,以及WSGI规范新增的数据,还可能包含一些操作系统的环境变量以及Web服务器相关的环境变量。
CGI规范中要求的变量:
REQUEST_METHOD: 请求方法,是个字符串,‘GET’, 'POST’等
SCRIPT_NAME: HTTP请求的path中的用于查找到application对象的部分,比如Web服务器可以根据path的一部分来决定请求由哪个virtual host处理
PATH_INFO: HTTP请求的path中剩余的部分,也就是application要处理的部分
QUERY_STRING: HTTP请求中的查询字符串,URL中?后面的内容
CONTENT_TYPE: HTTP headers中的content-type内容
CONTENT_LENGTH: HTTP headers中的content-length内容
SERVER_NAME和SERVER_PORT: 服务器名和端口,这两个值和前面的SCRIPT_NAME, PATH_INFO拼起来可以得到完整的URL路径
SERVER_PROTOCOL: HTTP协议版本,HTTP/1.0或者HTTP/1.1
HTTP_: 和HTTP请求中的headers对应。
WSGI规范中相关变量:
wsgi.version:表示WSGI版本,一个元组(1, 0),表示版本1.0
wsgi.url_scheme:http或者https
wsgi.input:一个类文件的输入流,application可以通过这个获取HTTP request body
wsgi.errors:一个输出流,当应用程序出错时,可以将错误信息写入这里
wsgi.multithread:当application对象可能被多个线程同时调用时,这个值需要为True
wsgi.multiprocess:当application对象可能被多个进程同时调用时,这个值需要为True
wsgi.run_once:当server期望application对象在进程的生命周期内只被调用一次时,该值为True
start_resposne参数
start_response是一个可调用对象,接收两个必选参数和一个可选参数:
status: 一个字符串,表示HTTP响应状态字符串
response_headers: 一个列表,包含有如下形式的元组:(header_name, header_value),用来表示HTTP响应的headers
exc_info(可选): 用于出错时,server需要返回给浏览器的信息
application对象的返回值
application对象的返回值用于为HTTP响应提供body,如果没有body,那么可以返回None。如果有body,那么需要返回一个可迭代的对象。server端通过遍历这个可迭代对象可以获得body的全部内容。
WSGI的实现和部署
要使用WSGI,需要分别实现server端和application端。
Application端的实现一般是由Python的各种框架来实现的,比如Django, web.py等,一般开发者不需要关心WSGI的实现,框架会会提供接口让开发者获取HTTP请求的内容以及发送HTTP响应。
Server端的实现会比较复杂一点,这个主要是因为软件架构的原因。一般常用的Web服务器,如Apache和nginx,都不会内置WSGI的支持,而是通过扩展来完成。比如Apache服务器,会通过扩展模块mod_wsgi来支持WSGI。Apache和mod_wsgi之间通过程序内部接口传递信息,mod_wsgi会实现WSGI的server端、进程管理以及对application的调用。Nginx上一般是用proxy的方式,用nginx的协议将请求封装好,发送给应用服务器,比如uWSGI,应用服务器会实现WSGI的服务端、进程管理以及对application的调用。
基于wsgiref模块的web框架
import time from wsgiref.simple_server import make_server # 将返回不同的内容部分封装成函数 def index(url): with open("index.html", "r", encoding="utf8") as f: s = f.read() now = str(time.time()) s = s.replace("@@oo@@", now) return bytes(s, encoding="utf8") def home(url): with open("home.html", "r", encoding="utf8") as f: s = f.read() return bytes(s, encoding="utf8") # 定义一个url和实际要执行的函数的对应关系 list1 = [ ("/index/", index), ("/home/", home), ] def run_server(environ, start_response): start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'), ]) # 设置HTTP响应的状态码和头信息 url = environ['PATH_INFO'] # 取到用户输入的url func = None for i in list1: if i[0] == url: func = i[1] break if func: response = func(url) else: response = b"404 not found!" return [response, ] if __name__ == '__main__': httpd = make_server('127.0.0.1', 8090, run_server) print("我在8090等你哦...") httpd.serve_forever()
jinja2
上面的代码实现了一个简单的动态,我完全可以从数据库中查询数据,然后去替换我html中的对应内容,然后再发送给浏览器完成渲染。 这个过程就相当于HTML模板渲染数据。 本质上就是HTML内容中利用一些特殊的符号来替换要展示的数据。 我这里用的特殊符号是我定义的,其实模板渲染有个现成的工具: jinja2。 安装:pip install jinja2
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> </head> <body> <h1>姓名:{{name}}</h1> <h1>爱好:</h1> <ul> {% for hobby in hobby_list %} <li>{{hobby}}</li> {% endfor %} </ul> </body> </html>
使用jinja2渲染index2.html文件:
from wsgiref.simple_server import make_server from jinja2 import Template def index(): with open("index2.html", "r") as f: data = f.read() template = Template(data) # 生成模板文件 ret = template.render({"name": "Alex", "hobby_list": ["烫头", "泡吧"]}) # 把数据填充到模板里面 return [bytes(ret, encoding="utf8"), ] def home(): with open("home.html", "rb") as f: data = f.read() return [data, ] # 定义一个url和函数的对应关系 URL_LIST = [ ("/index/", index), ("/home/", home), ] def run_server(environ, start_response): start_response('200 OK', [('Content-Type', 'text/html;charset=utf8'), ]) # 设置HTTP响应的状态码和头信息 url = environ['PATH_INFO'] # 取到用户输入的url func = None # 将要执行的函数 for i in URL_LIST: if i[0] == url: func = i[1] # 去之前定义好的url列表里找url应该执行的函数 break if func: # 如果能找到要执行的函数 return func() # 返回函数的执行结果 else: return [bytes("404没有该页面", encoding="utf8"), ] if __name__ == '__main__': httpd = make_server('', 8000, run_server) print("Serving HTTP on port 8000...") httpd.serve_forever()
现在的数据是我们自己手写的,那可不可以从数据库中查询数据,来填充页面呢?
使用pymysql连接数据库:
conn = pymysql.connect(host="127.0.0.1", port=3306, user="root", passwd="xxx", db="xxx", charset="utf8") cursor = conn.cursor(cursor=pymysql.cursors.DictCursor) cursor.execute("select name, age, department_id from userinfo") user_list = cursor.fetchall() cursor.close() conn.close()
创建一个测试的user表:
CREATE TABLE user( id int auto_increment PRIMARY KEY, name CHAR(10) NOT NULL, hobby CHAR(20) NOT NULL )engine=innodb DEFAULT charset=UTF8;
模板的原理就是字符串替换,我们只要在HTML页面中遵循jinja2的语法规则写上,其内部就会按照指定的语法进行相应的替换,从而达到动态的返回内容。