自己动手开发网络服务器(二):实现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