原文地址:Let’s Build A Web Server. Part 2.
译文地址:【翻译】一步步开发一个Web服务器.Part 2.
原文代码基于python2.x本系列其他文章:
【翻译】一步步开发一个Web服务器.Part 1.
【翻译】一步步开发一个Web服务器.Part 2.
【翻译】一步步开发一个Web服务器.Part 3.
记得,我在Part 1 中留了个作业题:不改变服务器代码的情况下,怎样在你刚完成的服务器上运行一个Django、Flask、Pyramid应用,适应不同的web框架? 阅读完这篇文章你就能得到答案。
在过去,你选择一个python web框架往往受限于可用的web服务器的选择,反之亦然。如果你选择的框架和你的服务器恰好能在一起工作,这时候你就乐了:
你或许要面对(或者已经遇到了)这样一个问题:当你想要把一个框架和一个服务器结合在一起时,你发现它们并不能很好的结合在一起。
从根本上来讲,你不得不用那些能一起工作的却不是你想用的框架和服务器。
所以,在不改变web服务器和web框架的情况下,你要怎样才能确保你的web服务器能够适应不同的web框架?答案就是Python 服务器网关接口(Web Server Gateway Interface, 缩写为WSGI,读作wizgy)
WSGI 允许开发者在web框架和web服务器上做出不同选择。你可以根据你的需要搭配不同的web框架和web服务器了。可以使用Django、Flask或者Pyramid,举个栗子,Gunicorn或者Nginx/uWSGI又或者Waitress。它做到了真正的混合和搭配,同时支持服务器和框架,WSGI谢天谢地你来了。
所以,WSGI就是我一遍又一遍问你的那个问题的答案。你的web服务器必须实现一个WSGI的服务器接口,当下的Python Web框架都已经实现了WSGI的框架接口。如此一来,不用修改任何代码,你的web服务器可以使用接口适用于一个特定的web框架。
现在你已经知道了WSGI同时支持服务器和框架来满足你不同选择的需求,这也使得服务器和框架的开发者们收益,它们能专注于他们擅长的领域而不必涉足对方的领域。其他语言也有类似的接口,又举个栗子:Java有Servlet API、Ruby有Rack。
这些都挺不错的(你心里或许正嘀咕:别逼逼了,上代码!)。OK,这下面是一个最简单的WSGI服务器的实现:
# Tested with Python 2.7.9, Linux & Mac OS X
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):
# Create a listening socket
self.listen_socket = listen_socket = socket.socket(
self.address_family,
self.socket_type
)
# Allow to reuse the same address
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Bind
listen_socket.bind(server_address)
# Activate
listen_socket.listen(self.request_queue_size)
# Get server host name and port
host, port = self.listen_socket.getsockname()[:2]
self.server_name = socket.getfqdn(host)
self.server_port = port
# Return headers set by Web framework/Web application
self.headers_set = []
def set_app(self, application):
self.application = application
def serve_forever(self):
listen_socket = self.listen_socket
while True:
# New client connection
self.client_connection, client_address = listen_socket.accept()
# Handle one request and close the client connection. Then
# loop over to wait for another client connection
self.handle_one_request()
def handle_one_request(self):
self.request_data = request_data = self.client_connection.recv(1024)
# Print formatted request data a la 'curl -v'
print(''.join(
'< {line}\n'.format(line=line)
for line in request_data.splitlines()
))
self.parse_request(request_data)
# Construct environment dictionary using request data
env = self.get_environ()
# It's time to call our application callable and get
# back a result that will become HTTP response body
result = self.application(env, self.start_response)
# Construct a response and send it back to the client
self.finish_response(result)
def parse_request(self, text):
request_line = text.splitlines()[0]
request_line = request_line.rstrip('\r\n')
# Break down the request line into components
(self.request_method, # GET
self.path, # /hello
self.request_version # HTTP/1.1
) = request_line.split()
def get_environ(self):
env = {}
# The following code snippet does not follow PEP8 conventions
# but it's formatted the way it is for demonstration purposes
# to emphasize the required variables and their values
#
# Required WSGI variables
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):
# Add necessary server headers
server_headers = [
('Date', 'Tue, 31 Mar 2015 12:54:48 GMT'),
('Server', 'WSGIServer 0.2'),
]
self.headers_set = [status, response_headers + server_headers]
# To adhere to WSGI specification the start_response must return
# a 'write' callable. We simplicity's sake we'll ignore that detail
# for now.
# return self.finish_response
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 formatted response data a la 'curl -v'
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.serve_forever()
你肯定在想,什么鬼?说好的简单实现呢?(译者:我也在想什么鬼!这么多!)但是它足够小到能让你理解却不会陷入到细节上。上面最简单的服务器也能做到很多,他可以运行最基本的Pyramid、Flask、Django或者其他的PythonWSGI框架。
还别不信,咱们来试试。看见上面的代码了吗?直接拷下来或者从Github上down下来,保存为webserver2.py文件。如果不加参数直接运行会报错并退出。
$ python webserver2.py
Provide a WSGI application object as module:callable
它真想为你的web应用服务,却无从下手。想让这个服务器运行起来只需要安装python就行了,但是想要运行Pyramid、Flask、Django的应用,还得先安装这些框架。我推荐的方法是使用虚拟环境(virtualenv)。你只要跟着下面的步骤一步步来就OK,首先安装虚拟环境,然后安装这三个框架。(译者认为可以不用虚拟环境,直接安装三个框架)
$ [sudo] pip install virtualenv
$ mkdir ~/envs
$ virtualenv ~/envs/lsbaws/
$ cd ~/envs/lsbaws/
$ ls
bin include lib
$ source bin/activate
(lsbaws) $ pip install pyramid
(lsbaws) $ pip install flask
(lsbaws) $ pip install django
这时候要创建一个web应用。这里以Pyramid为例。同样的,保存下面的代码或者从Github直接down下来,命名为pyramidapp.py。跟你的webserver2.py保存到同一目录下:
from pyramid.config import Configurator
from pyramid.response import Response
def hello_world(request):
return Response(
'Hello world from Pyramid!\n',
content_type='text/plain',
)
config = Configurator()
config.add_route('hello', '/hello')
config.add_view(hello_world, route_name='hello')
app = config.make_wsgi_app()
现在你可以写一个Pyramid程序并且用你刚才的web服务器运行了。
(lsbaws) $ python webserver2.py pyramidapp:app
WSGIServer: Serving HTTP on port 8888 ...
你只是告诉你的服务器从python的pyramidapp模块中加载这个‘app’。你的服务器已经准备好接收请求并且转发给你的Pyramid程序。现在你的web应用只处理/hello这一个路由路径。在你的浏览器中输入http://localhost:8888/hello 按下回车,你会发现,神奇的操作又一次发生了。
同样你也可以使用curl命令在命令行中测试你的服务器。
$ curl -v http://localhost:8888/hello
...
查看一下使用curl命令,你的服务器输出了什么。
现在我们使用Flask创建一个同样web应用。创建、启动、运行、测试都与上面相同。
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
保存上面的代码或者直接从Github上下载,保存为flaskapp.py 也是放在同一文件夹。在命令行中输入:
(lsbaws) $ python webserver2.py flaskapp:app
WSGIServer: Serving HTTP on port 8888 ...
再次在浏览器中输入 http://localhost:8888/hello 回车,神奇的操作又来了!(大家别嫌烦,后面Django还要来一遍…)
同样使用curl命令在命令行中测试web服务器,Flask应用回产生。。。
$ curl -v http://localhost:8888/hello
...
这个服务器能处理Django应用吗?(真的又要来一遍。。。)来试试!但是这次有点儿复杂,我建议从Github直接把整个项目down下来,只使用djangoapp.py这部分。这是工程的源代码,只需要将‘helloworld’这个Django工程添加到当前路径,并且将工程的WSGI应用导入。
import sys
sys.path.insert(0, './helloworld')
from helloworld import wsgi
app = wsgi.application
保存上面的代码,命名为djangoapp.py然后用你的web服务器运行这个程序。
(lsbaws) $ python webserver2.py djangoapp:app
WSGIServer: Serving HTTP on port 8888 ...
然后再浏览器中输入http://localhost:8888/hello
前面你已经做过两次了,这次你也可以在命令行中测试,以进一步确认你的Django程序能正确处理的请求:
$ curl -v http://localhost:8888/hello
...
你确定你试过了吗?你确定你的服务器支持这几种框架吗?如果没有,请认真做一遍。读过文章和代码固然重要,但这一系列文章是动手开发自己的web服务器,这意味着你必须自己亲自动手写一遍。撸起袖子加油干~你必须自己亲自动手把代码敲一遍以确保能按照预期运行。
相信你已经见识到WSGI的强大了。它能让你随意搭配服务器和web框架。WSGI在Python Web服务器和Python Web框架之间提供了一个极简的接口。不管是对于框架还是服务器,WSGI都非常容易实现。下面的代码片段展示了服务器和框架接口的代码实现。
def run_application(application):
"""Server code."""
# This is where an application/framework stores
# an HTTP status and HTTP response headers for the server
# to transmit to the client
headers_set = []
# Environment dictionary with WSGI/CGI variables
environ = {}
def start_response(status, response_headers, exc_info=None):
headers_set[:] = [status, response_headers]
# Server invokes the ‘application' callable and gets back the
# response body
result = application(environ, start_response)
# Server builds an HTTP response and transmits it to the client
…
def app(environ, start_response):
"""A barebones WSGI app."""
start_response('200 OK', [('Content-Type', 'text/plain')])
return ['Hello world!']
run_application(app)
接下来讲讲它的工作原理:
- 框架提供一个可供调用的‘application’对象。(WSGI规范里并没有规定这个application要怎么实现)
2.对于每个来自HTTP客户端的请求,服务器都会调用这个可调用的‘application’对象。服务器会向可调用对象‘application’传递一个名为‘environ’字典类型的参数,字典里包含WSGI/SGI的一些变量和一个‘start_response’可调用对象。
3.框架/应用程序会产生一个HTPP状态和HTTP响应报头,传递给可调用对象‘start_response’以供服务器储存。框架/应用程序也会返回一个响应的主体。
4.服务器将状态码、响应头部、响应主体组合成HTTP响应,将它传递给客户端(这一部没有在规范里写出来,但为了下一步的逻辑能清晰点儿,我才把它加上)。
下面是接口的一个可视化表示:
目前为止,你已经见识了Pyramid、Flask、Django的Web应用,服务器的WSGI接口实现,没用任何框架的WSGI应用的代码片段。
还有一件事,当你使用任何一种框架写一个web应用时,你的思想在一个更高的逻辑,不会与WSGI接口直接接触,我知道你会好奇框架的WSGI接口是什么样的,因为你在读这篇文章啊。所以我早就准备好了一个不用各种框架就能在服务器上运行的WSGI Web应用/Web框架。
def app(environ, start_response):
"""A barebones WSGI application.
This is a starting point for your own Web framework :)
"""
status = '200 OK'
response_headers = [('Content-Type', 'text/plain')]
start_response(status, response_headers)
return ['Hello world from a simple WSGI application!\n']
保存上面的代码或者直接从Github上down下来,保存为wsgiapp.py文件,与你的服务器保存在同一路径。这样就可以运行了:
(lsbaws) $ python webserver2.py wsgiapp:app
WSGIServer: Serving HTTP on port 8888 ...
在浏览器地址栏中输入http://localhost:8888/hello,你就能看见如下结果:
学习开发web服务器的时候,你还写了个最简单的WSGI Web框架,惊不惊喜?意不意外?
接下来,我们转向服务器传递给客户端的HTPP报文。下面是你使用HTTP客户端调用Pyramid应用时,服务器产生的HTTP响应报文:
这个响应与你再Part1中见到的有相似之处,也有一些新东西。再举个栗子,它有四个你以前没见过的HTTP头部:Content-Type、Content-Length、Date和Server。这些头部是一个web服务器产生的响应应该有的,尽管并没有严格要求。这些头部的目的是传递一些关于HTTP请求/响应的附加信息。
既然你已经对WSGI接口有了进一步的了解,下图还是那个HTTP响应,不过多了些内容,告诉你响应的每条信息是从哪里产生的。
目前为止我还没有提及‘environ’这个字典,简单来说‘environ’是个标准的Python字典类型,它必须包含WSGI规范里规定的一些特定的WSGI和SGI变量。服务器解析请求之后从HTTP请求中获取字典所需要的值。下图是字典内容:
web框架从字典中获取信息,根据特定的路由、请求方法等来决定使用哪个页面、从哪里读取请求主体,或者写入错误信息。
目前为止,写过了WSGI Web服务器,使用各种框架写过Web应用,写过最基本的web应用/web框架,真真是可怕的噩梦。一起来概括下你的WSGIweb服务器针对一个WSGI应用都要处理哪些请求。
1.服务器启动并且加载一个web应用/web框架提供的‘application’可调用对象。
2.服务器读取一个请求
3.服务器解析这个请求
4.服务器创建使用请求的数据创建一个‘environ’的字典
5.调用可调用对象‘application’,使用‘environ’字典和可调用对象‘start_response’作为参数,得到一个响应主体。
6.使用可调用对象‘application’返回的数据、状态码、可调用对象‘start_response’设置的响应头部。
7.最后服务器把这个HTTP响应发送回客户端。
上面就是所有的内容了,你已经有一个可以工作的WSGI服务器了,他可以运行与WSGI兼容的web框架例如Flask、Django、Pyramid或者你自己的WSGI框架。最棒的是不用修改任何代码就可以使用不同的web框架。哎哟,不错哟!
留一个课后思考题,“怎样保证你的服务器能在同一时间处理多个请求?”
敬请关注,我会在Part 3中讲解。
译者注:
文章中的图大家不要只看看热闹就过去了,作者在图中放了很多干货,大家看仔细点儿