自己动手开发网络服务器(二):实现WSGI服务

2018/02/20 08:39
阅读数 18

首先来介绍下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是如何被调用的呢,如果自己调用肯定拿不到environstart_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_appapplication函数注册到这个实例中去.

 

/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的出现,让开发者可以将网络框架与网络服务器的选择分隔开来,不再相互限制。现在,你可以真正地将不同的网络服务器与网络开发框架进行混合搭配,选择满足自己需求的组合。例如,你可以使用GunicornNginx/uWSGIWaitress服务器来运行DjangoFlaskPyramid应用。正是由于服务器和框架均支持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

可以看到反馈的响应.

下面给大家解释一下上述代码的工作原理:

  1. 网络框架提供一个命名为application的可调用对象(WSGI协议并没有指定如何实现这个对象)。在这里我们通过创建一个flask应用,并传入flask中的application. 当然这个application我们也可以按照之前的方法自己定义一个.
  2. 服务器每次从HTTP客户端接收请求之后,调用application。它会向可调用对象传递一个名叫environ的字典作为参数,其中包含了WSGI/CGI的诸多变量,以及一个名为start_response的可调用对象。
  3. 框架/应用生成HTTP状态码以及HTTP响应报头(HTTP response headers),然后将二者传递至start_response,等待服务器保存。此外,框架/应用还将返回响应的正文。
  4. 服务器将状态码、响应报头和响应正文组合成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

 

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部