自己动手开发网络服务器(二):实现WSGI服务
首先来介绍下WSGI.我们在写django或者flask程序的时候,可以通过request直接将客户端浏览器上的信息取下来.这也省去了我们自己去解析HTTP协议的时间.这其中的就是python自己实现的WSGI解析程序.
WSGI全称是Web Service Gateway Interface, WEB服务器网关接口.这个是python语音中所定义的web服务器和web应用程序之间或框架之间的通用接口标准.
WSGI就是一座桥梁,桥梁的一端成为服务器或网关端,另一端称为应用端或者框架端,WSGI的作用就是在协议之间相互转化.WSIG将web组建分成了三类,WEB服务器,WEB中间件与web应用程序.
接受HTTP请求、解析HTTP请求、发送HTTP响应都是重复的苦力活,如果我们自己来写这些底层代码,还没开始写HTML,先要花时间研读HTTP规范。所以底层的代码应该由专门的服务器软件实现,我们用python专注于生成HTML文档。
因为我们不想要接触TCP连接、HTTP原始请求和响应格式。所以需要一个统一的接口,专心用python编写Web业务。这个接口就是 WSGI:(Web 服务器网关接口)。
在python中内置了一个WSGI服务器,这个模块叫wsgiref, 它是用纯python编写的WSGI服务器的参考实现,我们来看一个具体的例子:
from wsgiref.simple_server import make_server
def application(environ,start_response):
print environ
start_response('200 OK',[('Content-type','text/html')])
return '<h1>hello world\n</>'
httpd=make_server('127.0.0.1',8888,application)
httpd.serve_forever()
运行该程序并在浏览器中输入http://127.0.0.1:8888/.可以看到返回的结果
在这个程序中,首先定义了一个函数application.其中有2个参数,一个是environ,一个是start_response.
environ: 一个包含所有HTTP请求消息的dict对象,
start_response:一个发送HTTP响应的函数
那么这个appliation是如何被调用的呢,如果自己调用肯定拿不到environ和start_response这两个参数,因为这2个参数我们无法自己提供,所以application函数必须由WSGI服务器来调用.在这个程序中是被make_server调用的.我们来看下make_server的代码.
def make_server(
host, port, app, server_class=WSGIServer, handler_class=WSGIRequestHandler
):
"""Create a new WSGI server listening on `host` and `port` for `app`"""
server = server_class((host, port), handler_class)
server.set_app(app)
return server_server的代码:
头两个参数分别是地址和端口,第三个参数app也就是我们传入的application.另外还有两个参数是WSGIServer和WSGIRequestHandler.在代码中返回一个server_class也就是WSGIServer实例,这个初始化的过程中就是获取客户端参数并设置environ的过程.最后通过set_app将application函数注册到这个实例中去.
/usr/bin/python2.7 /home/zhf/py_prj/web_server/webserver2.py
我们在代码中打印了print environ可以看到如下获取的各种类型参数
127.0.0.1 - - [19/Feb/2018 15:20:34] "GET / HTTP/1.1" 200 19
{'SERVER_SOFTWARE': 'WSGIServer/0.1 Python/2.7.14', 'SCRIPT_NAME': '', 'XDG_SESSION_TYPE': 'x11', 'REQUEST_METHOD': 'GET', 'SERVER_PROTOCOL': 'HTTP/1.1', 'CONTENT_LENGTH': '', 'SHELL': '/bin/bash', 'XDG_DATA_DIRS': '/usr/share/ukui:/usr/share/ukui:/usr/local/share:/usr/share:/var/lib/snapd/desktop', 'MANDATORY_PATH': '/usr/share/gconf/ukui.mandatory.path', 'CLUTTER_IM_MODULE': 'xim', 'TEXTDOMAIN': 'im-config', 'XMODIFIERS': '@im=fcitx', 'LIBVIRT_DEFAULT_URI': 'qemu:///system', 'JAVA_HOME': '/usr/lib/jvm/jdk1.8.0_151', 'XDG_RUNTIME_DIR': '/run/user/1000', 'PYTHONPATH': '/home/zhf/py_prj/web_server', 'HTTP_UPGRADE_INSECURE_REQUESTS': '1', 'XDG_SESSION_ID': 'c2', 'DBUS_SESSION_BUS_ADDRESS': 'unix:path=/run/user/1000/bus', 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'DESKTOP_SESSION': 'ukui', 'wsgi.version': (1, 0), 'GTK_MODULES': 'gail:atk-bridge', 'wsgi.multiprocess': False, 'PYCHARM_HOSTED': '1', 'GNOME_DESKTOP_SESSION_ID': 'this-is-deprecated', 'XDG_CURRENT_DESKTOP': 'UKUI', 'USER': 'zhf', 'XDG_VTNR': '7', 'PYTHONUNBUFFERED': '1', 'HTTP_USER_AGENT': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0', 'HTTP_CONNECTION': 'keep-alive', 'XAUTHORITY': '/home/zhf/.Xauthority', 'LANGUAGE': 'zh_CN:', 'SESSION_MANAGER': 'local/zhf-maple:@/tmp/.ICE-unix/2271,unix/zhf-maple:/tmp/.ICE-unix/2271', 'SHLVL': '0', 'DISPLAY': ':0', 'wsgi.url_scheme': 'http', 'QT_ACCESSIBILITY': '1', 'GTK_OVERLAY_SCROLLING': '0', 'LANG': 'zh_CN.UTF-8', 'CLASSPATH': '/home/zhf/pycharm-2017.2.4/lib/bootstrap.jar:/home/zhf/pycharm-2017.2.4/lib/extensions.jar:/home/zhf/pycharm-2017.2.4/lib/util.jar:/home/zhf/pycharm-2017.2.4/lib/jdom.jar:/home/zhf/pycharm-2017.2.4/lib/log4j.jar:/home/zhf/pycharm-2017.2.4/lib/trove4j.jar:/home/zhf/pycharm-2017.2.4/lib/jna.jar', 'GDMSESSION': 'ukui', 'wsgi.multithread': True, 'XDG_SEAT_PATH': '/org/freedesktop/DisplayManager/Seat0', 'GTK_IM_MODULE': 'fcitx', 'XDG_CONFIG_DIRS': '/etc/xdg/xdg-ukui:/etc/xdg', 'wsgi.file_wrapper': <class wsgiref.util.FileWrapper at 0x7fefc5ff3598>, 'REMOTE_HOST': 'localhost', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate', 'XDG_GREETER_DATA_DIR': '/var/lib/lightdm-data/zhf', 'QT4_IM_MODULE': 'fcitx', 'HOME': '/home/zhf', 'LD_LIBRARY_PATH': '/home/zhf/pycharm-2017.2.4/bin:', 'XDG_SESSION_DESKTOP': 'ukui', 'UNZIP': '-O GBK', 'SERVER_PORT': '8888', 'HTTP_HOST': '127.0.0.1:8888', 'DEFAULTS_PATH': '/usr/share/gconf/ukui.default.path', 'wsgi.run_once': False, 'wsgi.errors': <open file '<stderr>', mode 'w' at 0x7fefc82561e0>, 'HTTP_ACCEPT_LANGUAGE': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2', 'JRE_HOME': '/usr/lib/jvm/jdk1.8.0_151/jre', 'PATH_INFO': '/', 'PYTHONIOENCODING': 'UTF-8', 'QUERY_STRING': '', 'QT_IM_MODULE': 'fcitx', 'LOGNAME': 'zhf', 'XDG_SEAT': 'seat0', 'PATH': '{JAVA_HOME}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin', 'SSH_AGENT_PID': '2354', 'XDG_SESSION_PATH': '/org/freedesktop/DisplayManager/Session0', 'SERVER_NAME': 'localhost', 'IM_CONFIG_PHASE': '2', 'GIO_LAUNCHED_DESKTOP_FILE_PID': '2986', 'GIO_LAUNCHED_DESKTOP_FILE': '/home/zhf/\xe6\xa1\x8c\xe9\x9d\xa2/Pycharm.desktop', 'SSH_AUTH_SOCK': '/run/user/1000/keyring/ssh', 'wsgi.input': <socket._fileobject object at 0x7fefc811e7d0>, 'TEXTDOMAINDIR': '/usr/share/locale/', 'GATEWAY_INTERFACE': 'CGI/1.1', 'OLDPWD': '/home/zhf/pycharm-2017.2.4/bin', 'REMOTE_ADDR': '127.0.0.1', 'GDM_LANG': 'zh_CN', 'PWD': '/home/zhf/py_prj/web_server', 'DESKTOP_STARTUP_ID': 'peony-2395-zhf-maple-sh-0_TIME90535', 'CONTENT_TYPE': 'text/plain', 'ZIPINFO': '-O GBK'}
WSGI的出现,让开发者可以将网络框架与网络服务器的选择分隔开来,不再相互限制。现在,你可以真正地将不同的网络服务器与网络开发框架进行混合搭配,选择满足自己需求的组合。例如,你可以使用Gunicorn或Nginx/uWSGI或Waitress服务器来运行Django、Flask或Pyramid应用。正是由于服务器和框架均支持WSGI,才真正得以实现二者之间的自由混合搭配
那么接下来我们继续深入了解WSGI的原理,我们自己来做一个简单的WSGI.代码如下:
class WSGIServer(object):
address_family=socket.AF_INET
socket_type=socket.SOCK_STREAM
request_queue_size=1
def __init__(self,server_address):
self.lisen_socket=listen_socket=socket.socket(self.address_family,self.socket_type)
listen_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
listen_socket.bind(server_address)
listen_socket.listen(self.request_queue_size)
host,port=self.lisen_socket.getsockname()[:2]
self.server_name=socket.getfqdn(host)
self.server_port=port
self.headers_set=[]
def set_app(self,application):
self.application=application
def server_forever(self):
listen_socket=self.lisen_socket
while True:
self.client_connection,client_address=listen_socket.accept()
self.hand_one_request()
def hand_one_request(self):
self.request_data=request_data=self.client_connection.recv(1024)
print ''.join('<{line}\n'.format(line=line) for line in request_data.splitlines())
self.parse_request(request_data)
env=self.get_environ()
result=self.application(env,self.start_response)
self.finish_response(result)
def parse_request(self,text):
request_line=text.splitlines()[0]
request_line=request_line.rstrip('\r\n')
(self.request_method,self.path,self.request_version)=request_line.split()
def get_environ(self):
env={}
env['wsgi.version']=(1,0)
env['wsgi.url_scheme'] = 'http'
env['wsgi.input'] = StringIO.StringIO(self.request_data)
env['wsgi.errors'] = sys.stderr
env['wsgi.multithread'] = False
env['wsgi.multiprocess'] = False
env['wsgi.run_once'] = False
env['REQUEST_METHOD'] = self.request_method
env['PATH_INFO'] = self.path
env['SERVER_NAME'] = self.server_name
env['SERVER_PORT'] = str(self.server_port)
return env
def start_response(self,status,response_headers,exc_info=None):
server_headers=[('Date','Tue,20 Feb 2018 07:30:30 GMT'),('Server','WSGIServer 0.2')]
self.headers_set=[status,response_headers+server_headers]
def finish_response(self,result):
try:
status,response_headers=self.headers_set
response='HTTP/1.1 {status}\r\n'.format(status=status)
for header in response_headers:
response+='{0}: {1}\r\n'.format(*header)
response+='\r\n'
for data in result:
response+=data
print ''.join('>{line}\n'.format(line=line) for line in response.splitlines())
self.client_connection.sendall(response)
finally:
self.client_connection.close()\
SERVER_ADDRESS = (HOST, PORT) = '', 8888
def make_server(server_address,application):
server=WSGIServer(server_address)
server.set_app(application)
return server
if __name__=="__main__":
if len(sys.argv) < 2:
sys.exit('Provide a WSGI application object as module:callable')
app_path=sys.argv[1]
module,application=app_path.split(':')
module=__import__(module)
application=getattr(module,application)
httpd=make_server(SERVER_ADDRESS,application)
print 'WSGIServer: Serving HTTP on port {port} ...\n'.format(port=PORT)
httpd.server_forever()
再另外创建一个flask的应用,保存为flaskapp文件
from flask import Flask
from flask import Response
flask_app=Flask('flaskapp')
@flask_app.route('/hello')
def hello_world():
return Response('Hello world from Flask!\n',mimetype='text/plain')
app=flask_app.wsgi_app
通过命令行运行
zhf@zhf-maple:~/py_prj/web_server$ python webserver2.py flaskapp:app
WSGIServer: Serving HTTP on port 8888 ...
此时在浏览器中输入http://127.0.0.1:8888/hello
可以看到反馈的响应.
下面给大家解释一下上述代码的工作原理:
- 网络框架提供一个命名为application的可调用对象(WSGI协议并没有指定如何实现这个对象)。在这里我们通过创建一个flask应用,并传入flask中的application. 当然这个application我们也可以按照之前的方法自己定义一个.
- 服务器每次从HTTP客户端接收请求之后,调用application。它会向可调用对象传递一个名叫environ的字典作为参数,其中包含了WSGI/CGI的诸多变量,以及一个名为start_response的可调用对象。
- 框架/应用生成HTTP状态码以及HTTP响应报头(HTTP response headers),然后将二者传递至start_response,等待服务器保存。此外,框架/应用还将返回响应的正文。
- 服务器将状态码、响应报头和响应正文组合成HTTP响应,并返回给客户端(这一步并不属于WSGI协议)
流程图如下所示
截至目前,我们已经成功创建了自己的支持WSGI协议的网络服务器,还利用不同的网络框架开发了多个网络应用。另外,还自己开发了一个极简的网络框架。本文介绍的内容不可谓不丰富。我们接下来回顾一下WSGI网络服务器如何处理HTTP请求:
· 首先,服务器启动并加载网络框架/应用提供的application可调用对象
· 然后,服务器读取一个请求信息
· 然后,服务器对请求进行解析
· 然后,服务器使用请求数据创建一个名叫environ的字典
· 然后,服务器以environ字典和start_response可调用对象作为参数,调用application,并获得应用生成的响应正文。
· 然后,服务器根据调用application对象后返回的数据,以及start_response设置的状态码和响应标头,构建一个HTTP响应。
· 最后,服务器将HTTP响应返回至客户端
整个流程如下:
我们已经实现了一个简单的WSGI, 具体WSGI内部规范可以参考PEP333文档.链接为http://legacy.python.org/dev/peps/pep-0333/#rationale-and-goals