Python WSGI 介绍

一、写在前面

        这篇文章主要介绍一下Python WSGI, 学习python wsgi规范的时候读到了几篇介绍的很好的入门教程,内容不长,整理了一下。由于能力和时间有限,错误之处在所难免,欢迎指正!     

         如果转载,请保留作者信息。

         邮箱地址:[email protected]

二、WSGI 介绍

Python中的WSGI (Web Server Gateway Interface), 是Python应用程序(或者框架、套件)与服务器之间的一种接口,其定义了两者进行通信的接口规范。服务器端和应用端都必须遵循这套规范。当一个应用程序是按照WSGI规范开发的,那么它可以在任意遵循该规范的服务器上运行。那这是怎样一套接口呢?在wsgi规范里,一个web服务流程如下图所示:

Python WSGI 介绍        

显而易见,wsgi把web组件分成三个部分:wsgi server (服务器端),wsgi middleware (中间件),wsgi application (应用端)。

应用端只需要实现一个接受两个参数的,含有__call__方法并返回一个可遍历的含有零个或多个string结果的Python对象即可。当然传入参数的名字可以任意取,但习惯把第一个参数命名为“environ”,第二个为“start_response”。 中间件是一个两头都要兼顾的部件。在上面所示的服务栈中,一个中间件对于位于其顶部的中间件或者应用充当着服务器,而对于其下面的中间件或者服务器,它又扮演着应用端的角色。所以,中间件需要同时实现服务器端与应用端的规范。

服务器端也不复杂,对于每个http请求,调用一次应用端“注册”的那个协议规定应用端必须要实现的对象,然后返回相应的响应消息。所有的处理细节都在应用或者中间件中完成。这样一次应用端与服务器的通信就完成了,即完成了一次对用户请求的处理。

WSGI1不是服务器,不是python模块,不是框架或API或任何一种软件。WSGI是一套服务器和应用程序共同遵守以供通讯的接口规范。服务器端和应用程序端的接口都在WSGI被规范定义了。WSGI的唯一官方描述仅以PEP 3333提案为准。如果一个应用程序(或框架,工具包)是遵从WSGI规范编写的,则它可以运行于同样遵从WSGI规范的任何服务器之上。 

WSGI应用(遵守它的程序)是可以堆叠的(stacked)。在堆叠时处于中间位置的成为中间件,必须同时遵循WSIG规范的应用端和服务端的两个接口。在其上层的应用看来,中间件等同于服务器。在其下层的服务器(或其它低层应用)看来,中间件等同于应用。

一个遵循WSGI规范的服务器做的全部工作可以理解为从客户端接收一个请求,传递给应用程序,然后将应用返回的响应发送回客户端。其抽象的工作逻辑就是这么简单,而所有复杂的细节工作应交由接口另一侧的应用程序或是中间件去完成。

对于仅仅是使用框架或者工具包而言,学习WSGI规范不是必须的。但是,除非应用程序已经完全集成在框架内,为了使用中间件还是应有一些关于如何堆叠不同层次的基本知识。

Python 2.5及之后的版本都内置了一个WSGI服务器,在这个教程中我们将使用它。2.4及之前的版本也可以手动安装它。而除了学习以外的任何实用场景,我都推荐使用Apache服务器配合以mod_wsgi模块。

该教程的所有代码都是较为底层的,并且唯一的目的只是以边写边运行的教学方式来展示WSGI规范。它不是为真实使用情景写的。对于生产代码,建议直接使用工具包,框架和中间件。


三、应用程序接口


应用程序接口:Application Interface 

WSGI应用接口实现为一个可调用对象:函数,方法,类或者任何一个实现了__call__方法的对象。该可调用对象应满足下述条件:

必须接收两个位置参数:

一个包含了CGI环境变量的字典

一个可调用函数对象,让应用程序调用来向服务器提交HTTP状态码和HTTP响应头。

必须向服务器Return包装在可迭代对象中的字符串(一个或多个)。 

应用程序的基本骨架大致如下:

[python] view plain copy
  1. # 这是我们的应用对象。 它可以采用任何名字来命名,  
  2. # 但当在Apache中使用mod_wsgi来运行时则必须命名为application  
  3. # 必须接收两个参数:  
  4. #   一为包含了CGI环境变量的环境字典,由服务器对来自客户端的每一次请求而生成。  
  5. #   二为由服务器提供的一个回调函数,可以调用以向服务器返回HTTP状态码和HTTP响应头。  
  6. def application(environ, start_response):  
  7.   
  8.    # 在这里我们利用环境字典中的信息生成一个简单的响应体。  
  9.    response_body = 'The request method was %s' % environ['REQUEST_METHOD']  
  10.   
  11.    # HTTP 响应吗  
  12.    status = '200 OK'  
  13.   
  14.    # 这里是客户端需要的HTTP响应头,必须按一个个元组对的形式存放在一个列表中。  
  15.    # 如:[(响应头名称, 响应头值)].  
  16.    response_headers = [('Content-Type''text/plain'),  
  17.                        ('Content-Length', str(len(response_body)))]  
  18.   
  19.    # 使用由服务器提供的这个start_resposne回调函数来提交状态码和响应头  
  20.    start_response(status, response_headers)  
  21.   
  22.    # 在这里利用return来返回真正的响应体  
  23.    # 请注意响应体在这里被包装在一个列表中。实际上WSGI要求它存放在一个可迭代对象中即可,不一定要是列表。  
  24.    return [response_body]</span>  


四、环境字典


环境字典:Environment dictionary

环境字典会包含一些CGI环境变量,由服务器在接收到每一次来自客户端的请求时产生。下面的脚本代码会输出整个字典的内容。

[python] view plain copy
  1. #! /usr/bin/env python  
  2.   
  3. # 使用python内置的WSGI服务器  
  4. from wsgiref.simple_server import make_server  
  5.   
  6. def application(environ, start_response):  
  7.    # 排序并将环境字典的键值对转换为字符串  
  8.    response_body = ['%s: %s' % (key, value)  
  9.                     for key, value in sorted(environ.items())]  
  10.    response_body = '\n'.join(response_body)  
  11.   
  12.    status = '200 OK'  
  13.    response_headers = [('Content-Type''text/plain'),  
  14.                   ('Content-Length', str(len(response_body)))]  
  15.    start_response(status, response_headers)  
  16.   
  17.    return [response_body]  
  18.   
  19. # 实例化一个WSGI服务器对象。  
  20. # 该服务器对象可以接收来自客户端(我们的浏览器)的请求,将它传给应用程序,  
  21. # 并且将应用程序返回过来的响应再发送给客户端。  
  22. httpd = make_server(  
  23.    'localhost'# 主机名。  
  24.    8051# 监听请求的端口号。  
  25.    application # 我们的可调用应用对象,在这里是一个函数。  
  26.    )  
  27.   
  28. # 在这里简单地一次性监听,得到响应后处理完则直接退出。  
  29. httpd.handle_request()  

要运行这段脚本代码的话,将其保存为environment.py,然后打开终端,进入脚本所在的目录下,输入:

[python] view plain copy
  1. >>python environment.py

如果是在Windows系统的话,需要将python.exe的路径添加到系统环境变量路径中。最后我们打开浏览器,在地址栏输入我们的服务器运行的地址和端口号即可看到结果:http://localhost:8051/


五、解析Get请求


解析Get请求:Parsing the Request – Get 

再次运行environment.py,这一次在浏览器中输入下面的地址:

http://localhost:8051/?age=10&hobbies=software&hobbies=tunning

请注意这一次在环境字典中输出的变量:QUERY_STRING 和 REQUEST_METHOD 。当请求方法是GET时,表单的变量都会附加在URL中被称为查询字符串的部分来发送。所谓查询字符串即URL中问号(?)之后的全部内容。在此例中查询字符串的值即为age=10&hobbies=software&hobbies=tunning。请注意hobbies变量出现了两次,这是有可能的,比如表单中存在复选框或者用户直接在URL中故意输入重复的查询键。 

我们可以通过硬编码的形式来从环境变量中取出查询字符串,获取参数值。不过更简单的方法是使用CGI模块的parse_qs函数来直接获得一个字典,其中键为查询字符串中的键,值为列表,存储了查询字符串中对应某个键的所有值。

永远要当心用户的输入,处理用户输入来避免潜在的脚本注入。CGI的转义(escape)函数可以达到此目的。

如下所示的代码展示了对GET请求的解析。为了让它正常工作,请保存文件名为parsingget.wsgi,因为这是我们编写的html表单中action的值。对于运行在使用了modwsgi模块的apache服务器上的主程序(main)脚本来说,wsgi文件扩展名是非常常用的。

[python] view plain copy
  1. #!/usr/bin/env python  
  2.   
  3. from wsgiref.simple_server import make_server  
  4. from cgi import parse_qs, escape  
  5.   
  6. html = """ 
  7. <html> 
  8. <body> 
  9.    <form method="get" action="parsing_get.wsgi"> 
  10.       <p> 
  11.          Age: <input type="text" name="age"> 
  12.          </p> 
  13.       <p> 
  14.          Hobbies: 
  15.          <input name="hobbies" type="checkbox" value="software"> Software 
  16.          <input name="hobbies" type="checkbox" value="tunning"> Auto Tunning 
  17.          </p> 
  18.       <p> 
  19.          <input type="submit" value="Submit"> 
  20.          </p> 
  21.       </form> 
  22.    <p> 
  23.       Age: %s<br> 
  24.       Hobbies: %s 
  25.       </p> 
  26.    </body> 
  27. </html>"""  
  28.   
  29. def application(environ, start_response):  
  30.   
  31.    # 解析后直接返回一个字典,每个值都是一个列表,包含了查询字符串中所有对应于该键的值  
  32.    d = parse_qs(environ['QUERY_STRING'])  
  33.   
  34.    # 调用字典的get方法并传入一个key不存在时返回的默认值,这样可以在第一次显示表单时也给出合理的值  
  35.    age = d.get('age', [''])[0# 返回第一个age值.  
  36.    hobbies = d.get('hobbies', []) # 返回一个hobbies列表.  
  37.   
  38.    # 总是对用户输入进行转义来避免脚本注入  
  39.    age = escape(age)  
  40.    hobbies = [escape(hobby) for hobby in hobbies]  
  41.   
  42.    response_body = html % (age or 'Empty',  
  43.                ', '.join(hobbies or ['No Hobbies']))  
  44.   
  45.    status = '200 OK'  
  46.   
  47.    # 修改 content type 为 text/html  
  48.    response_headers = [('Content-Type''text/html'),  
  49.                   ('Content-Length', str(len(response_body)))]  
  50.    start_response(status, response_headers)  
  51.   
  52.    return [response_body]  
  53.   
  54. httpd = make_server('localhost'8051, application)  
  55. # 这里我们改为调用serve_forever()而不是原来的handle_request(),以保持服务器不停监听。  
  56. # 在Windows中你可以在任务管理器中kill掉python.exe进程来结束运行。  
  57. # 在Linux/Mac OS系统中按下CTRL+C来终止运行。  
  58. httpd.serve_forever()  

六、解析Post请求


解析Post请求:Parsing the Request – Post 

当请求类型是POST时,查询字符串不再通过URL传递,而是包含在HTTP请求体之中来传递。请求体在环境字典中保存为键为“wsgi.input”对应的一个类文件变量。

为了从wsgi.input中读出请求体,有必要先知道请求体的长度,即CONTENTLENGTH变量。WSGI规范中指出,存放有请求体大小的这一CONTENTLENGTH变量是不可靠的,有可能是空值,或者直接缺失,所以获取时应采用try/except语法块来进行异常防错。 

如下所示的脚本应保存为parsing_post.wsgi,因为这是表单action属性的值。

[python] view plain copy
  1. #!/usr/bin/env python  
  2.   
  3. from wsgiref.simple_server import make_server  
  4. from cgi import parse_qs, escape  
  5.   
  6. html = """ 
  7. <html> 
  8. <body> 
  9.    <form method="post" action="parsing_post.wsgi"> 
  10.       <p> 
  11.          Age: <input type="text" name="age"> 
  12.          </p> 
  13.       <p> 
  14.          Hobbies: 
  15.          <input name="hobbies" type="checkbox" value="software"> Software 
  16.          <input name="hobbies" type="checkbox" value="tunning"> Auto Tunning 
  17.          </p> 
  18.       <p> 
  19.          <input type="submit" value="Submit"> 
  20.          </p> 
  21.       </form> 
  22.    <p> 
  23.       Age: %s<br> 
  24.       Hobbies: %s 
  25.       </p> 
  26.    </body> 
  27. </html> 
  28. """  
  29.   
  30. def application(environ, start_response):  
  31.   
  32.    # 环境变量CONTENT_LENGTH可能为空值或缺失,采用try/except来防错  
  33.    try:  
  34.       request_body_size = int(environ.get('CONTENT_LENGTH'0))  
  35.    except (ValueError):  
  36.       request_body_size = 0  
  37.   
  38.    # 当请求方法为POST时查询字符串被放在HTTP请求体中进行传递。它被WSGI服务器具体存放在名为wsgi.input的一个类文件环境变量中。  
  39.    request_body = environ['wsgi.input'].read(request_body_size)  
  40.    d = parse_qs(request_body)  
  41.   
  42.    age = d.get('age', [''])[0# 返回第一个age值。  
  43.    hobbies = d.get('hobbies', []) # 返回一个hobbies列表。  
  44.   
  45.    # 总是对用户输入进行转义来避免脚本注入。  
  46.    age = escape(age)  
  47.    hobbies = [escape(hobby) for hobby in hobbies]  
  48.   
  49.    response_body = html % (age or 'Empty',  
  50.                ', '.join(hobbies or ['No Hobbies']))  
  51.   
  52.    status = '200 OK'  
  53.   
  54.    response_headers = [('Content-Type''text/html'),  
  55.                   ('Content-Length', str(len(response_body)))]  
  56.    start_response(status, response_headers)  
  57.   
  58.    return [response_body]  
  59.   
  60. httpd = make_server('localhost'8051, application)  
  61. httpd.serve_forever()