本文主要是介绍【翻译】一步步开发一个Web服务器.Part 2.,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
原文地址: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 sysclass WSGIServer(object):address_family = socket.AF_INETsocket_type = socket.SOCK_STREAMrequest_queue_size = 1def __init__(self, server_address):# Create a listening socketself.listen_socket = listen_socket = socket.socket(self.address_family,self.socket_type)# Allow to reuse the same addresslisten_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)# Bindlisten_socket.bind(server_address)# Activatelisten_socket.listen(self.request_queue_size)# Get server host name and porthost, port = self.listen_socket.getsockname()[:2]self.server_name = socket.getfqdn(host)self.server_port = port# Return headers set by Web framework/Web applicationself.headers_set = []def set_app(self, application):self.application = applicationdef serve_forever(self):listen_socket = self.listen_socketwhile True:# New client connectionself.client_connection, client_address = listen_socket.accept()# Handle one request and close the client connection. Then# loop over to wait for another client connectionself.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 dataenv = self.get_environ()# It's time to call our application callable and get# back a result that will become HTTP response bodyresult = self.application(env, self.start_response)# Construct a response and send it back to the clientself.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, # GETself.path, # /helloself.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 variablesenv['wsgi.version'] = (1, 0)env['wsgi.url_scheme'] = 'http'env['wsgi.input'] = StringIO.StringIO(self.request_data)env['wsgi.errors'] = sys.stderrenv['wsgi.multithread'] = Falseenv['wsgi.multiprocess'] = Falseenv['wsgi.run_once'] = False# Required CGI variablesenv['REQUEST_METHOD'] = self.request_method # GETenv['PATH_INFO'] = self.path # /helloenv['SERVER_NAME'] = self.server_name # localhostenv['SERVER_PORT'] = str(self.server_port) # 8888return envdef start_response(self, status, response_headers, exc_info=None):# Add necessary server headersserver_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_responsedef finish_response(self, result):try:status, response_headers = self.headers_setresponse = '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) = '', 8888def make_server(server_address, application):server = WSGIServer(server_address)server.set_app(application)return serverif __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 Responsedef 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 wsgiapp = 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 clientheaders_set = []# Environment dictionary with WSGI/CGI variablesenviron = {}def start_response(status, response_headers, exc_info=None):headers_set[:] = [status, response_headers]# Server invokes the ‘application' callable and gets back the# response bodyresult = 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中讲解。
译者注:
文章中的图大家不要只看看热闹就过去了,作者在图中放了很多干货,大家看仔细点儿
这篇关于【翻译】一步步开发一个Web服务器.Part 2.的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!