• WSGI协议的原理及实现


    本文将介绍WSGI协议的原理,并亲自实现一个符合协议规范的WSGI server

    WSGI,是Python Web Server Gateway Interface的简称,是web底层跟application解耦的协议,我们的web服务器使用WSGI协议来调用application称为WSGI server。为了各类web server和web application之间能够相互兼容,常见的web server(如Nginx,Apache)无法与web application(如Flask,django、tornado)直接通信,需要WSGI server作为桥梁。如今WSGI已经成为Python的一种标准协议PEP333

     

    WSGI的工作原理

    WSGI的工作原理分为服务器层和应用程序层:

    1. 服务器层:将来自socket的数据包解析为http,调用application,给application提供环境信息environ,这个environ包含wsgi自身的信息(host,post,进程模式等),还有client的header和body信息。同时还给application提供一个start_response的回调函数,这个回调函数主要在应用程序层进行响应信息处理。
    2. 应用程序层:在WSGI提供的start_response,生成header,body和status后将这些信息socket send返回给客户端。

    上面的工作原理通过流程图可以表示如下:

    可以看到,wsgi server已经完成了底层http的解析和数据转发等一系列网络底层的实现,开发者可以更加专注于开发web application。

    使用wsgiref模块理解WSGI工作流程

    Python自带模块wsgiref可以实现上述WSGI工作流程,这里使用wsgiref实现一个wsgi server的例子,以对wsgi的流程有感性的认识。

    from wsgiref.simple_server import make_server
    def application(environ, start_response):
        start_response('200 OK', [('Content-Type', 'text/html')])
        return [b'<h1>Hello, web!</h1>']
    if __name__ == '__main__':
        httpd = make_server('', 9999, application)
        print("Serving HTTP on port 9999...")
        httpd.serve_forever()

    该例子中application就是web application,这里是定义为一个函数,在实际的Flask和bottle中,就是一个类,这些类实现__call__方法,且该方法中带参数environstart_response即可,运行server时只需调用self即可,例如:

    class Flask:
      def __init__(self):
        # init params
        pass
      
      def __call__(self, environ, start_response):
        start_response('200 OK', [('Content-Type', 'text/html')])
        return [b'<h1>Hello, web!</h1>']
      
      def listen(self, port)
          from wsgiref.simple_server import make_server
            server = make_server('127.0.0.1', 9999, self)
            print('serve on 127.0.0.1:9999')
            server.serve_forever()

    因此,在这里例子里,可以具体看到wsgi server调用application后,在start_response中application返回headercontent-typebody,最后在浏览器打开127.0.0.1:9999即可看到由wigs server返回的内容。

    自己实现一个wsgi server

    了解了wsgi的工作原理和具体的使用例子,我们自己来实现一个wsgi服务器,可以进行请求解析,并可以调用Flask暴露出来的wsgi_app参数来将Flask运行起来。

    首先组织代码结构:

    import socket
    import StringIO
    import sys
    
    class WSGIserver(object):
      def __init__(self):
        pass
      
      def serve_forever(self):
        pass
      
      def handle_one_request(self):
        request_data = '' # 暂不处理
        self.parse_request(request_data)
        env = self.get_environ()
        result = self.application(env, self.start_response)
        self.finish_response(result)
        
      def parse_request(self, data):
          pass
      
      def get_environ(self):
        pass
      
      def start_response(self, status, response_headers, exc_info=None):
        pass
      
      def finish_response(self, result):
        pass
      
    def make_server(server_address, application):
        server = WSGIServer(server_address)
        server.set_app(application)
        return server
      
    if __name__ == '__main__':
      httpd = make_server('', 8888, application)
      httpd.serve_forever()

    分析如下:

    1. __init__:wsgi server的初始化操作
    2. serve_forever:可以让wsgi server一直监听客户端请求
    3. handle_one_request:对每一次请求进行参数解析,包括parse_requestget_environ
    4. start_response:传递给application的回调函数,根据PEP333start_response需要包含statusresponse_headersexc_info三个参数。status是http的状态码,如“200 OK”,”404 Not Found”。response_headers是一个(header_name, header_value) 元组的列表,在进行application开发时需要注意这点。exc_info通常不需要,直接设置为None即可。具体的一些参数的解释可以参考协议的详细解释。
    5. finish_response:解析一次请求后,需要关闭socket端口,同时将application返回的data返回至客户端。

    有了代码结构,根据PEP333标准协议,那么具体可以完善上述代码,如下(wsgi.py):

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    import socket
    import StringIO
    import sys
    
    class WSGIServer(object):
    
        address_family = socket.AF_INET
        socket_type = socket.SOCK_STREAM
        request_queue_size = 1
    
        def __init__(self, server_address):
    
            # 创建socket,利用socket获取客户端的请求
            self.listen_socket = listen_socket = socket.socket(self.address_family, self.socket_type)
            # 设置socket的工作模式
            listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            # 绑定socket地址
            listen_socket.bind(server_address)
            # socket active, 监听文件描述符
            listen_socket.listen(self.request_queue_size)
    
            # 获得serve的host name和port
            host, port = self.listen_socket.getsockname()[:2]
            self.server_name = socket.getfqdn(host)
            self.server_port = port
    
            self.headers_set = []
    
        def set_app(self, application):
            self.application = application 
    
        #启动WSGI server服务,不停的监听并获取socket数据。
        def serve_forever(self):
            listen_socket = self.listen_socket
            while True:
                self.client_connection, client_address = listen_socket.accept() #接受客户端请求
                #处理请求
                self.handle_one_request()
    
        def handle_one_request(self):
            self.request_data = request_data = self.client_connection.recv(1024)
            self.parse_request(request_data)
            # print(''.join(
      # '< {line}
    '.format(line=line)
      # for line in request_data.splitlines()
      # ))
              # Construct environment dictionary using request data
            env = self.get_environ()
          
            #给flask	ornado传递两个参数,environ,start_response
            result = self.application(env, self.start_response)
            self.finish_response(result)
    
        #处理socket的http协议
        def parse_request(self, data):
            format_data = data.splitlines()
            if len(format_data):
                request_line = data.splitlines()[0]
                request_line = request_line.rstrip('
    ')
                (self.request_method, self.path, self.request_version) = request_line.split() ## ['GET', '/', 'HTTP/1.1']
    
        # 获取environ数据并设置当前server的工作模式
        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
            # Required CGI variables
            env['REQUEST_METHOD']    = self.request_method    # GET
            env['PATH_INFO']         = self.path              # /hello
            env['SERVER_NAME']       = self.server_name       # localhost
            env['SERVER_PORT']       = str(self.server_port)  # 8888
            return env
    
        def start_response(self, status, response_headers, exc_info=None):
            server_headers = [('Date', 'Tue, 31 Mar 2015 12:54:48 GMT'), ('Server', 'WSGIServer 0.2')]
            self.headers_set = [status, response_headers + server_headers]
    
        #把application返回给WSGI的数据返回给客户端。
        def finish_response(self, result):
            try:
                status, response_headers = self.headers_set
                response = 'HTTP/1.1 {status}
    '.format(status=status)
                for header in response_headers:
                    response += '{0}: {1}
    '.format(*header)
                response += '
    '
                for data in result:
                    response += data
                self.client_connection.sendall(response)
                print(''.join(
                    '> {line}
    '.format(line=line)
                    for line in response.splitlines()
                ))
            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(':') # 第一个参数是文件名,第二个参数时长文件内app的命名
        module = __import__(module)
        application = getattr(module, application) # getattr(object, name[, default]) -> value
        httpd = make_server(SERVER_ADDRESS, application)
        print('WSGIServer: Serving HTTP on port {port} ...
    '.format(port=PORT))
        httpd.serve_forever()

    至此,wsgi server的代码已完成,下面写一个简单的Flask实例(flaskapp.py)来验证这个server是否可用:

    from flask import Flask
    from flask import Response
    
    flask_app = Flask(__name__)
    
    @flask_app.route('/hello')
    def hello_world():
        return Response(
            'hello',
            mimetype='text/plain'
        )
    
    app = flask_app.wsgi_app # 调用Flask暴露的wsgi_app,提供给wsgi server作为application,而不是直接run(port)

    在终端输入:

    $python wsgi.py flaskapp:app
    WSGIServer: Serving HTTP on port 8888 ...

    打开浏览器,输入http://localhost:8888/hello可看到hello的信息,同时,终端输出一些request和response的信息:

    > HTTP/1.1 200 OK
    > Content-Type: text/plain; charset=utf-8
    > Content-Length: 5
    > Date: Tue, 31 Mar 2015 12:54:48 GMT
    > Server: WSGIServer 0.2
    > > hello

    总结

    本文主要从WSGI的原理入手,同时通过原生wsgiref模块了解了WSGI的工作流程,最后利用socket模块自己实现了一个WSGI server,我们可以通过Flask这类web框架暴露出来的WSGI接口启动应用。

    由于WSGI实现了web server和web application的高度解耦,在具体的使用中可以使用一些比较成熟的WSGI server如uwsgi和gunicorn对任何一个符合WSGI规范的app进行部署,这给Python Web应用的部署带来了极大的灵活性。

    http://geocld.github.io/2017/08/14/wsgi/

  • 相关阅读:
    鸿蒙的js开发模式19:鸿蒙手机下载python服务器端文件的实现
    【资源下载】Linux下的Hi3861一站式鸿蒙开发烧录(附工具)
    并发编程大扫盲:带你了解何为线程上下文切换
    内存溢出 MAT 排查工具,它真香香香
    面试官问:如何排除GC引起的CPU飙高?我脱口而出5个步骤
    小学妹问我:如何利用可视化工具排查问题?
    你不得不知的6个JDK自带JVM调优工具
    那个小白还没搞懂内存溢出,只能用案例说给他听了
    听说同学你搞不懂Spring Boot集成Mybatis的玩法,田哥来教你
    手把手教你设置JVM调优参数
  • 原文地址:https://www.cnblogs.com/leijiangtao/p/4378078.html
Copyright © 2020-2023  润新知