WSGI剖析

  在一个 HTTP 请求到达服务器时, 服务器接收并调用 web 应用程序解析请求, 产生响应数据并返回给服务器. 这里涉及了两个方面的东西: 服务器(server)和应用程序(application). 势必要有一个合约要求服务器和应用程序都去遵守, 如此按照此合约开发的无论是服务器还是应用程序都会具有较大的普遍性. 而这就好像在计算机通信的早期, 各大公司都有属于自己的通信协议, 如此只会让市场杂乱无章, 宁愿只要一种通信协议.而针对 python 的合约是 WSGI(Python Web Server Gateway Interface).

一、WSGI

  WSGI,全称 Web Server Gateway Interface,或者 Python Web Server Gateway Interface ,是为 Python 语言定义的 Web 服务器和 Web 应用程序或框架之间的一种简单而通用的接口。

  WSGI 的官方定义是,the Python Web Server Gateway Interface。从名字就可以看出来,这东西是一个Gateway,也就是网关。网关的作用就是在协议之间进行转换。

  • 也就是说,WSGI就像是一座桥梁,一边连着web服务器,另一边连着用户的应用。WSGI将web组件分为三部分:WSGI 的作用如图所示:

  WSGI作用

  WSGI有两方:“服务器”或“网关”一方,以及“应用程序”或“应用框架”一方。服务方调用应用方,提供环境信息,以及一个回调函数(提供给应用程序用来将消息头传递给服务器方),并接收Web内容作为返回值。

  WSGI将Web组件分成了三类:Web 服务器(WSGI Server)、Web中间件(WSGI Middleware)与Web应用程序(WSGI Application)

  WSGI server所做的工作仅仅是将从客户端收到的请求传递给WSGI application,然后将WSGI application的返回值作为响应传给客户端。WSGI applications 可以是栈式的,这个栈的中间部分叫做中间件,两端是必须要实现的application和server。

  WSGI application接口应该实现为一个可调用对象,例如函数、方法、类、含__call__方法的实例。这个可调用对象可以接收2个参数:

  • 一个字典,该字典可以包含了客户端请求的信息以及其他信息,可以认为是请求上下文,一般叫做environment(编码中多简写为environ、env);
  • 一个用于发送HTTP响应状态(HTTP status )、响应头(HTTP headers)的回调函数。

  同时,可调用对象的返回值是响应正文(response body),响应正文是可迭代的、并包含了多个字符串。

  Web Server接收HTTP请求,封装一系列环境变量,按照WSGI接口标准调用注册的WSGI Application,最后将响应返回给客户端

  WSGI Application结构如下:

def application (environ, start_response):
 
    response_body = 'Request method: %s' % environ['REQUEST_METHOD']
 
    # HTTP响应状态
    status = '200 OK'
 
    # HTTP响应头,注意格式
    response_headers = [
        ('Content-Type', 'text/plain'),
        ('Content-Length', str(len(response_body)))
    ]
 
    # 将响应状态和响应头交给WSGI server
    start_response(status, response_headers)
 
    # 返回响应正文
    return [response_body]

  下面的程序可以将environment字典的内容返回给客户端:

# -*- coding: utf-8 -*- 
 
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)  # 由于下面将Content-Type设置为text/plain,所以`\n`在浏览器中会起到换行的作用
 
    status = '200 OK'
    response_headers = [
        ('Content-Type', 'text/plain'),
        ('Content-Length', str(len(response_body)))
    ]
    start_response(status, response_headers)
 
    return [response_body]
 
# 实例化WSGI server
httpd = make_server (  
    '127.0.0.1', 
    8051, # port
    application # WSGI application,此处就是一个函数
)
 
# handle_request函数只能处理一次请求,之后就在控制台`print 'end'`了
httpd.handle_request()
 

  MiddleWare:

     WSGI 的宽松耦合的特性,我们可以轻松的在 Application 和 Server 之前插入任何的中间插件,在不需要改动 Server 和 Application 的前提下,实现一些特殊功能。

class Router():
    
    def __init__(self):
        self.path_info = {}

    def route(self, environ, start_response):
        application = self.path_info[environ['PATH_INFO']]
        return application(environ, start_response)

    def __call__(self, path):
        def wrapper(application):
            self.path_info[path] = application
        return wrapper

""" The above is the middleware"""

router = Router()

@router('/world')
def world(environ, start_response):
    status = '200 OK'
    output = 'World!'start_response(status, response_headers)  
    return [output] 

@router('/hello') 
def hello(environ, start_response):
    status = '200 OK'
    output = 'Hello'
    response_headers = [('Content-type', 'text/plain'), ('Content-Length', str(len(output)))]
    start_response(status, response_headers)  
    return [output] 

  作为 Application 时,我们用 Router 实例化一个对象。然后对 “ PATH-APP “ 进行注册,根据不同的 PATH,我们要进一步选择哪个 App。接着,就是把 router.route() 喂给          Server ,作为 Application 侧的可调用实体。有请求到来时,根据已经注册的 “PATH-APP” 对选择应用并执行。

-     Server 端类似,我们要先实例化并完成注册。然后,比如,拿我们上一小节实现的 WSGI 容器为例,我们需要修改 result = router.route(environ, start_response),同样完成了router的功能。

      下面是另外一个,实现了 postprocessor 的一个例子,在 Application 返回的 HTTP Header 里面再加一个 Header。

def myapp(environ, start_response):
    response_headers = [('content-type', 'text/plain')]
    start_response('200 OK', response_headers)
    return ['Check the headers!']

class Middleware:
    def __init__(self, app):
        self.wrapped_app = app

    def __call__(self, environ, start_response):
        def custom_start_response(status, headers, exc_info=None):
            headers.append(('X-A-SIMPLE-TOKEN', "1234567890"))
            return start_response(status, headers, exc_info)
        return self.wrapped_app(environ, custom_start_response)

app = Middleware(myapp)

  Web应用的本质:

         1)、浏览器发送HTTP请求                 

    2)、服务器接收到请求,生成HTML文档

         3)、服务器把HTML文档作为HTTP响应的Body发送给浏览器

         4)、浏览器收到HTTP响应,从HTTP Body取出HTML文档进行显示

  接受HTTP请求、解析HTTP请求、发送HTTP响应都是重复的苦力活,如果我们自己来写这些底层代码,还没开始写HTML,先要花半把个月研读HTTP规范。所以底层的代码应该由专门的服务器软件实现,我们用Python专注于生成HTML文档。

  因为我们不想要接触TCP连接、HTTP原始请求和响应格式。所以需要一个统一的接口,专心用python编写Web业务

  这个接口就是 WSGI:(Web 服务器网关接口)。

二、WSGI接口

  WSGI接口定义非常简单,它只要求Web开发者实现一个函数,就可以响应HTTP请求

def applicaion(environ,start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    return [b'hello world']

  上面的application()函数就是符合WSGI标准的一个HTTP处理函数,它接收两个参数:

    • environ:一个包含所有HTTP请求信息dict对象(后面详细介绍environ是什么东西,有哪些内容);

    • start_response:一个发送HTTP响应的函数。

三、python-wsgiref源码分析

  simple_server.py

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)
            # WSGIServer((host, port), handler_class)
    server.set_app(app)
    return server

  监听在本地的端口上,接受来自客户端的请求,通过 WSGIServer 和 WSGIRequestHandler 处理后,把请求交给程序的的可调用对象 app,然后返回 app 的结果给客户端。

  这里有两个重要的类:WSGIServer, WSGIRequestHandler

  先来看看WSGIServer源码:

class WSGIServer(HTTPServer):
    """BaseHTTPServer that implements the Python WSGI protocol"""

    application = None

    def server_bind(self):
        """Override server_bind to store the server name."""
        HTTPServer.server_bind(self)
        self.setup_environ()

    def setup_environ(self):
        # Set up base environment
        env = self.base_environ = {}
        env['SERVER_NAME'] = self.server_name
        env['GATEWAY_INTERFACE'] = 'CGI/1.1'
        env['SERVER_PORT'] = str(self.server_port)
        env['REMOTE_HOST']=''
        env['CONTENT_LENGTH']=''
        env['SCRIPT_NAME'] = ''

    def get_app(self):
        return self.application

    def set_app(self,application):
        self.application = application

  WSGIServer 在原来的 HTTPServer 上面封装了一层,在原来的 HTTPServer 的基础上又额外做了下面的事情: 

  •  覆写原来的 server_bind 函数,添加初始化 environ 变量的动作
  •  添加了处理满足 wsgi 的 app 函数:set_app 和 get_app

  另外,WSGIRequestHandler

class WSGIRequestHandler(BaseHTTPRequestHandler):

    server_version = "WSGIServer/" + __version__

    def get_environ(self):
        env = self.server.base_environ.copy()
        env['SERVER_PROTOCOL'] = self.request_version
        env['REQUEST_METHOD'] = self.command
        if '?' in self.path:
            path,query = self.path.split('?',1)
        else:
            path,query = self.path,''

        env['PATH_INFO'] = urllib.unquote(path)
        env['QUERY_STRING'] = query

        host = self.address_string()
        if host != self.client_address[0]:
            env['REMOTE_HOST'] = host
        env['REMOTE_ADDR'] = self.client_address[0]

        if self.headers.typeheader is None:
            env['CONTENT_TYPE'] = self.headers.type
        else:
            env['CONTENT_TYPE'] = self.headers.typeheader

        length = self.headers.getheader('content-length')
        if length:
            env['CONTENT_LENGTH'] = length

        for h in self.headers.headers:
            k,v = h.split(':',1)
            k=k.replace('-','_').upper(); v=v.strip()
            if k in env:
                continue                    # skip content length, type,etc.
            if 'HTTP_'+k in env:
                env['HTTP_'+k] += ','+v     # comma-separate multiple headers
            else:
                env['HTTP_'+k] = v
        return env

    def get_stderr(self):
        return sys.stderr

    def handle(self):
        """Handle a single HTTP request"""

        self.raw_requestline = self.rfile.readline()
        if not self.parse_request(): # An error code has been sent, just exit
            return

        handler = ServerHandler(self.rfile, self.wfile, self.get_stderr(), self.get_environ()) # 注意,这里实例化了ServerHandler
     handler.request_handler = self # backpointer for logging 
     handler.run(self.server.get_app())

   这个类处理客户端的 HTTP 请求,它也是在原来处理 http 请求的BaseHTTPRequestHandler 类上添加了 wsgi 规范相关的内容。

  •  get_environ:解析environ变量
  •  handle: 解析验证客户端的http的request是否合法,把封装的环境变量交给 ServerHandler,然后由 ServerHandler 调用 wsgi app。

   我们之前定义的app,恰恰就是传给run方法,通过run方法的包装,实现wsgi协议的通信。

  handle.py

   主要是wsgi server 的处理过程,定义 start_response、调用 wsgi app 、处理set_content_length等等。

  该文件下存在五个类:   

  

  ServerHandler

class ServerHandler(SimpleHandler):  # 该类继承关系:ServerHandler---->SimpleHandler------>BaseHandler

    server_software = software_version

    def close(self):
        try:
            self.request_handler.log_request(
                self.status.split(' ',1)[0], self.bytes_sent
            )
        finally:
            SimpleHandler.close(self)

  而BaseHandler主要用于操作WSGI app。其类下面定义的run:

    def run(self, application):
        """Invoke the application"""
        # Note to self: don't move the close()!  Asynchronous servers shouldn't
        # call close() from finish_response(), so if you close() anywhere but
        # the double-error branch here, you'll break asynchronous servers by
        # prematurely closing.  Async servers must return from 'run()' without
        # closing if there might still be output to iterate over.
        try:
            self.setup_environ() //获取环境变量
            self.result = application(self.environ, self.start_response) //传递进来的app
            self.finish_response()
        except:
            try:
                self.handle_error()
            except:
                # If we get an error handling an error, just give up already!
                self.close()
                raise   # ...and let the actual server figure it out.

  run方法最重要的就是调用自定义的wsgi app,并把在finish_reponse方法中把结果send给客户端。

  下面来看看finish_reponse方法:

    def finish_response(self):
        if not self.result_is_file() or not self.sendfile():
            for data in self.result: //遍历self.result里面的数据
                self.write(data)
            self.finish_content()
        self.close()
self.result = application(self.environ, self.start_response) //也就是上面run方法里面的self.result
    def write(self, data):
        """'write()' callable as specified by PEP 333"""

        assert type(data) is StringType,"write() argument must be string"

        if not self.status:
            raise AssertionError("write() before start_response()")

        elif not self.headers_sent:
            # Before the first output, send the stored headers
            self.bytes_sent = len(data)    # make sure we know content-length
            self.send_headers()
        else:
            self.bytes_sent += len(data)

        # XXX check Content-Length and truncate if too many bytes written?
        self._write(data)
        self._flush()
Write---发送数据长度
    def finish_content(self):
        """Ensure headers and content have both been sent"""
        if not self.headers_sent:
            self.headers['Content-Length'] = "0"
            self.send_headers() //将头信息发送出去
        else:
            pass

  finish_response返回http的body是一方面,其中还需要返回http的headers。这个操作分别在wirte方法和finish_content中,它们都调用了send_headers方法,send_headers方法由通过调用send_preamble构造header数据,并最终通过_write 方法写入到缓冲可写文件中。完成服务器对客户端的响应。

  其中send_header()方法如下:

def send_headers(self):
        """Transmit headers to the client, via self._write()"""
        self.cleanup_headers() //统计头长度
        self.headers_sent = True  //修改头状态
        if not self.origin_server or self.client_is_modern():
            self.send_preamble() //version/status/date/server 等信息写入缓冲文件
     self._write(str(self.headers)) //将头信息写入缓冲文件
    def send_preamble(self):
        """Transmit version/status/date/server, via self._write()"""
        if self.origin_server:
            if self.client_is_modern():
                self._write('HTTP/%s %s\r\n' % (self.http_version,self.status)) //写入协议和状态
                if 'Date' not in self.headers: //写入日期信息
                    self._write(
                        'Date: %s\r\n' % format_date_time(time.time())
                    )
                if self.server_software and 'Server' not in self.headers:
                    self._write('Server: %s\r\n' % self.server_software)
        else:
            self._write('Status: %s\r\n' % self.status)

四、一条Http请求的旅程

  服务器端启动服务,等到客户端输入 http://localhost:8000/ 命令,摁下回车键,看到终端上的输出,整个过程中,wsgi 的服务器端发生了什么呢?

  1. 服务器程序创建 socket,并监听在特定的端口,等待客户端的连接
  2. 客户端发送 http 请求
  3. socket server 读取请求的数据,交给 http server
  4. http server 根据 http 的规范解析请求,然后把请求交给 WSGIServer
  5. WSGIServer 把客户端的信息存放在 environ 变量里,然后交给绑定的 handler 处理请求
  6. HTTPHandler 解析请求,把 method、path 等放在 environ,然后 WSGIRequestHandler 把服务器端的信息也放到 environ 里
  7. WSGIRequestHandler 调用绑定的 wsgi ServerHandler,把上面包含了服务器信息,客户端信息,本次请求信息得 environ 传递过去
  8. wsgi ServerHandler 调用注册的 wsgi app,把 environ 和 start_response 传递过去
  9. wsgi app 将reponse header、status、body 回传给 wsgi handler
  10. 然后 handler 逐层传递,最后把这些信息通过 socket 发送到客户端
  11. 客户端的程序接到应答,解析应答,并把结果打印出来。

 

posted @ 2017-08-21 20:05  看雪。  阅读(699)  评论(0编辑  收藏  举报