【引自selfboot的博客】在从零开始搭建一个论坛(一):Web服务器和Web框架中,搞清楚了Web服务器,Web应用,的概念网络框架。对于Python来说,越来越多的web框架问世,这不仅给了我们更多的选择,也限制了我们对web服务器的选择。也是Java有很多web框架,因为servletAPI的存在,任何用Javaweb框架写的应用程序都可以运行在任何web服务器上。当然,Python社区也需要这样一套API来适配Web服务器和应用程序。这套API就是WSGI(PythonWebServerGatewayInterface),在PEP3333中有详细的描述。简单来说,WSGI是连接web服务器和web应用的桥梁。一方面,它从Web服务器获取原始的HTTP数据,处理成统一的格式,交给Web应用程序。另一方面,它从应用程序/框架进行业务逻辑处理,生成响应内容并发送给服务器。通过WSGI耦合Web服务器和框架的详细过程如下图:这个对象)。每次服务器收到来自HTTP客户端的请求时,它都会调用可调用对象应用程序,传递一个名为environ的字典作为参数和一个名为start_response的可调用对象。框架/应用生成HTTP状态码和HTTP响应头,然后传递给start_response,等待服务器保存。此外,框架/应用程序将返回响应文本。服务端将状态码、响应头和响应文本组合成一个HTTP响应返回给客户端(这一步不属于WSGI协议)。下面我们从服务端和应用端来看一下WSGI是如何适配的。在服务器端,我们知道客户端(通常是浏览器)发出的每一个HTTP请求都由三部分组成:请求行、消息头和请求文本,其中包含了本次请求的相关细节。例如:Method:表示在Request-URI标识的资源上执行的方法,包括GET、POST等。User-Agent:允许客户端告诉服务器它的操作系统、浏览器等属性;服务器收到来自客户端的HTTP请求后,WSGI接口必须统一处理这些请求字段,这样才能传递给应用服务器接口(其实就是传递给框架)。Web服务器传递给应用程序的具体数据已经在CGI(CommonGatewayInterface,公共网关接口)中指定,这些数据称为CGI环境变量。WSGI遵循CGI环境变量的内容,要求Web服务器创建一个字典来保存这些环境变量(通常命名为environ)。除了CGI定义的变量,environ还必须保存一些WSGI定义的变量。此外,它还可以保存客户端系统的一些环境变量。可以参考environVariables看看有哪些变量。然后WSGI接口必须把environ交给应用程序处理。这里WSGI规定应用程序提供一个可调用的对象application,然后服务器调用应用程序获取返回值作为HTTP响应文本。服务端在调用应用时,需要提供两个变量,一个是上面提到的变量字典environ,另一个是可调用对象start_response,生成状态码和响应头,让我们得到一个完整的HTTP响应。Web服务器将响应返回给客户端,一个完整的HTTP请求-响应过程就完成了。wsgiref分析Python有一个内置的Web服务器,实现了WSGI接口。wsgiref模块中,是纯Python编写的WSGI服务器的参考实现。下面一起简单分析一下它的实现。首先假设我们用下面的代码启动一个web服务器:#Instantiatetheserverhttpd=make_server('localhost',#Thehostname8051,#Aportnumberwheretowaitfortherequestapplication#Theapplicationobjectname,inthiscasefunction)#Waitforasinglerequest,serveitandquithttpd.handle_request()然后我们接收到web服务器的请求并生成一个environ,然后调用应用主线处理请求来分析源码的调用过程,简化如下图所示:WSGIServer调用过程这里主要有三个类,WSGIServer,WSGIRequestHandler,和服务器句柄。WSGIServer是一个web服务器类,可以提供server_address(IP:Port)和WSGIRequestHandler类来初始化和获取一个服务器对象。该对象监视响应的端口。收到HTTP请求后,通过finish_request创建RequestHandler类的实例。在实例的初始化过程中,会生成一个Handle类实例,然后调用它的run(application)函数,然后在函数中调用application提供的application对象来生成response。这三个类的继承关系如下图:WSGI类继承关系图。其中,TCPServer使用socket完成TCP通信,HTTPServer用于HTTP层面的处理。同样,StreamRequestHandler用于处理流套接字,BaseHTTPRequestHandler用于处理HTTP级别的内容。这部分和WSGI接口关系不大,更多的是Web服务器的具体实现,可以忽略。微型服务器示例如果上面的wsgiref太复杂,我们一起来实现一个微型的web服务器,这样我们就可以了解web服务器端WSGI接口的实现。代码摘自自研web服务器(二),放在gist上。主要结构如下:classWSGIServer(object):#Socket参数address_family,socket_type=socket.AF_INET,socket.SOCK_STREAMrequest_queue_size=1def__init__(self,server_address):#TCP服务器初始化:创建socket,绑定地址,监听端口#获取服务器地址,端口defset_app(self,application):#获取框架提供的应用self.application=applicationdefserve_forever(self):#处理TCP连接:获取请求内容,调用处理函数defhandle_request(self):#解析HTTP请求,获取environ,处理请求内容,返回HTTP响应结果env=self.get_environ()result=self.application(env,self.start_response)self.finish_response(result)defparse_request(self,text):#解析HTTP请求defget_environ(self):#分析environ参数,这里只是举个例子,实际情况还有很多参数。env['wsgi.url_scheme']='http'...env['REQUEST_METHOD']=self.request_method#GET...returnenvdefstart_response(self,status,response_headers,exc_info=None):#添加响应头,状态码self.headers_set=[status,response_headers+server_headers]definish_response(self,result):#返回HTTP响应信息SERVER_ADDRESS=(HOST,PORT)='',8888#创建服务器实例defmake_server(server_address,application):server=WSGIServer(server_address)server.set_app(application)returnserver目前有很多成熟的web服务器都支持WSGI,Gunicorn就是一个不错的。它脱胎于ruby社区的Unicorn,并成功移植到python成为一个WSGIHTTPServer。它有以下优点:配置方便可以自动管理多个worker进程选择不同的后台扩展接口(sync、gevent、tornado等)相对于server端,应用端(framework)需要做的事情简单多了,它只需要提供一个可调用对象(通常命名为application),这个对象接收服务器传来的两个参数environ和start_response。这里的可调用对象不仅可以是一个函数,也可以是一个类(下面第二个例子)或者一个带有__call__方法的实例。总之,只要能接受上面提到的两个参数,并且返回值可以被服务端迭代。.Application需要做的是根据environ提供的HTTP请求的信息进行一定的业务处理,返回一个可迭代的对象。服务器端通过迭代这个对象来获取HTTP响应的文本。如果没有响应文本,可能会返回None。同时,应用程序还会调用服务端提供的start_response,生成HTTP响应的状态码和响应头。原型如下:defstart_response(self,status,headers,exc_info=None):应用需要提供status:表示HTTP响应字符串状态的字符串,以及response_headers:包含形式为元组的列表:(header_name,header_value),用于表示HTTP响应的标头。同时exc_info是可选的,用于发生错误时服务器需要返回给浏览器的信息。至此,我们可以实现一个简单的应用,如下:defsimple_app(environ,start_response):"""Simplestpossibleapplicationfunction"""HELLO_WORLD="Helloworld!\n"status='200OK'response_headers=[('Content-type','text/plain')]start_response(status,response_headers)return[HELLO_WORLD]或者使用类实现如下。classAppClass:"""Producethesameoutput,butusingaclass"""def__init__(self,environ,start_response):self.environ=environself.start=start_responsedef__iter__(self):...HELLO_WORLD="Helloworld!\n"yieldHELLO_WORLD注意这里的AppClass类本身就是application,被environ和start_response调用(实例化),返回一个实例对象。这个实例对象本身是可迭代的,符合WSGI对应用的要求。如果要将AppClass类的对象作为应用使用,必须在类中添加__call__方法,接受environ和start_response作为参数,返回一个可迭代对象,如下:classAppClass:"""Producethesameoutput,butusinganobject"""def__call__(self,environ,start_response):这部分涉及到python的一些高级特性,比如yield和magicmethod,大家可以参考我总结的python语言要点来理解。Flask中的WSGIflask是一个符合WSGI规范的轻量级PythonWeb框架。它的初始版本只有600多行,因此相对容易理解。让我们看一下原始版本中关于WSGI接口的部分。defwsgi_app(self,environ,start_response):"""TheactualWSGIapplication.Thisisnotimplementedin`__call__`sothatmiddlewarescanbeapplied:app.wsgi_app=MyMiddleware(app.wsgi_app)"""withself.request_context(environ):rv=self.preprocess_request()ifrvisN=self.dispatch_request()response=self.make_response(rv)response=self.process_response(response)returnresponse(environ,start_response)def__call__(self,environ,start_response):"""Shortcutfor:attr:`wsgi_app`"""返回自我。wsgi_app(environ,start_response)这里的wsgi_app实现了我们所说的应用函数,rv是对request的封装,response是框架用来处理业务逻辑的具体函数。关于flask的源码这里就不过多解释了。有兴趣的可以去github下载,然后去原版查看。在中间件前面的flask代码的wsgi_app函数的注释中提到application部分并没有直接在__call__中实现,这样才能使用中间件。那么为什么要使用中间件,什么是中间件呢?回顾之前的应用/服务器接口,对于一个HTTP请求,服务器总会调用一个应用进行处理,应用处理后返回结果。这足以应对一般场景,但还不够完美。考虑以下应用场景:针对不同的请求(比如不同的URL),服务端需要调用不同的应用,那么如何选择调用哪一个;对于负载均衡或远程处理,需要使用运行在网络上其他主机上的应用程序来做处理;您需要对应用程序返回的内容进行一些处理,然后才能将其用作HTTP响应;上述场景的一个共同点是,无论是在服务器端还是应用程序(框架)端,都有一些必要的操作是不合适的。对于应用程序端来说,这些操作应该由服务器端来完成,而对于服务器端来说,这些操作应该由应用程序端来完成。为了处理这种情况,引入了中间件。中间件就像是应用程序端和服务器端之间的桥梁,用于与双方进行通信。对于服务器端,中间件就像应用程序端,对于应用程序端,它就像服务器端。如下图:middleware中间件实现Flask框架在Flask类的初始化代码中使用了中间件:self.wsgi_app=SharedDataMiddleware(self.wsgi_app,{self.static_path:target})这里的功能和那个一样在python中就像装饰器一样,就是在执行self.wsgi_app前后执行SharedDataMiddleware中的一些内容。中间件的作用与python中装饰器的作用非常相似。SharedDataMiddleware中间件由werkzeug库提供,用于支持托管静态内容的站点。另外还有DispatcherMiddleware中间件,用来支持根据不同的请求调用不同的应用,这样就可以解决前面场景1和2的问题。我们来看看DispatcherMiddleware的实现:classDispatcherMiddleware(object):"""AllowsonetomountmiddlewaresorapplicationsinWSGIapplication.ThisisusefulifyouwanttocombinemultipleWSGIapplications::app=DispatcherMiddleware(app,{'/app2':app2,'/app3':app3})"""def__init__(self,app,mounts=None):self.app=appself.mounts=mountsor{}def__call__(self,environ,start_response):script=environ.get('PATH_INFO','')path_info=''while'/'inscript:ifscriptinself.mounts:app=self.mounts[script]breakscript,last_item=script.rsplit('/',1)path_info='/%s%s'%(last_item,path_info)else:app=self.mounts.get(script,self.app)original_script_name=environ.get('SCRIPT_NAME','')environ['SCRIPT_NAME']=original_script_name+scriptenviron['PATH_INFO']=path_inforeturnapp(environ,start_response)需要提供一个mount时初始化中间件Dictionary,用于指定不同URL路径与应用的映射关系。这样,对于一个请求,中间件会检查它的路径,然后选择合适的应用程序进行处理。WSGI的原理部分基本就结束了。在下一篇文章中,我将介绍我对flask框架的理解。
