使用python来编写http服务器的超级指南

首先,到底什么是网络服务器?

2016218142846088.png (1024×499)

简而言之,它是在物理服务器上搭建的一个网络连接服务器(networking server),永久地等待客户端发送请求。当服务器收到请求之后,它会生成响应并将 其返回至客户端。客户端与服务器之间的通信,是以http协议进行的。客户端可以是浏览器,也可以是任何支持http协议的软件。

那么,网络服务器的简单实现形式会是怎样的呢?下面是我对此的理解。示例代码使用python语言实现,不过即使你不懂python语言,你应该也可以从代码和下面的 解释中理解相关的概念:

import socket
host, port = ”, 8888
listen_socket = socket.socket(socket.af_inet, socket.sock_stream)
listen_socket.setsockopt(socket.sol_socket, socket.so_reuseaddr, 1)
listen_socket.bind((host, port))
listen_socket.listen(1)
print ‘serving http on port %s …’ % port
while true:
client_connection, client_address = listen_socket.accept()
request = client_connection.recv(1024)
print request
http_response = “””\
http/1.1 200 ok
hello, world!
“””
client_connection.sendall(http_response)
client_connection.close()

将上面的代码保存为webserver1.py,或者直接从我的[github仓库](https://github.com/rspivak/lsbaws/b lob/master/part1/webserver1.py)下载,然后通过命令行运行该文件:

$ python webserver1.py
serving http on port 8888 …

接下来,在浏览器的地址栏输入这个链接http://localhost:8888/hello, 然后按下回车键,你就会看见神奇的一幕。在浏览器中,应该会出现“ hello, world!”这句话:

2016218142925558.png (278×100)

是不是很神奇?接下来,我们来分析背后的实现原理。

首先,我们来看你所输入的网络地址。它的名字叫url(uniform resource locator,统一资源定位符),其基本结构如下:

2016218142947477.png (602×500)

通过url,你告诉了浏览器它所需要发现并连接的网络服务器地址,以及获取服务器上的页面路径。不过在浏览器发送http请求之前,它首先要与目标网络服务器建立tc p连接。然后,浏览器再通过tcp连接发送http请求至服务器,并等待服务器返回http响应。当浏览器收到响应的时候,就会在页面上显示响应的内容,而在上面的例
子中,浏览器显示的就是“hello, world!”这句话。

那么,在客户端发送请求、服务器返回响应之前,二者究竟是如何建立起tcp连接的呢?要建立起tcp连接,服务器和客户端都使用了所谓的套接字(socket)。接下 来,我们不直接使用浏览器,而是在命令行使用telnet手动模拟浏览器。

在运行网络服务器的同一台电脑商,通过命令行开启一次telnet会话,将需要连接的主机设置为localhost,主机的连接端口设置为8888,然后 按回车键:

$ telnet localhost 8888
trying 127.0.0.1 …
connected to localhost.

完成这些操作之后,你其实已经与本地运行的网络服务器建立了tcp连接,随时可以发送和接收http信息。在下面这张图片里,展示的是服务器接受新tcp连接所需要完 成的标准流程。

2016218143013389.png (1024×673)

在上面那个telnet会话中,我们输入get /hello http/1.1,然后按下回车:

$ telnet localhost 8888
trying 127.0.0.1 …
connected to localhost.
get /hello http/1.1
http/1.1 200 ok
hello, world!

你成功地手动模拟了浏览器!你手动发送了一条http请求,然后收到了http响应。下面这幅图展示的是http请求的基本结构:

2016218143046068.png (1024×468)

http请求行包括了http方法(这里使用的是get方法,因为我们希望从服务器获取内容),服务器页面路径(/hello)以及http协议的版本。

为了尽量简化,我们目前实现的网络服务器并不会解析上面的请求,你完全可以输入一些没有任何意义的代码,也一样可以收到”hello, world!”响应。

在你输入请求代码并按下回车键之后,客户端就将该请求发送至服务器了,服务器则会解析你发送的请求,并返回相应的http响应。

下面这张图显示的是服务器返回至客户端的http响应详情:

2016218143103071.png (844×600)

我们来分析一下。响应中包含了状态行http/1.1 200 ok,之后是必须的空行,然后是http响应的正文。

响应的状态行http/1.1 200 ok中,包含了http版本、http状态码以及与状态码相对应的原因短语(reason phrase)。浏览器收到响应之后,会显示响应的正文,这就是为什么你会在浏览器中看到“hello, world!”这句话。

这就是网络服务器基本的工作原理了。简单回顾一下:网络服务器首先创建一个侦听套接字(listening socket),并开启一个永续循环接收新连接;客户端启 动一个与服务器的tcp连接,成功建立连接之后,向服务器发送http请求,之后服务器返回http响应。要建立tcp连接,客户端和服务器都使用了套接字。

现在,你已经拥有了一个基本可用的简易网络服务器,你可以使用浏览器或其他http客户端进行测试。正如上文所展示的,通过telnet命令并手动输入http请 求,你自己也可以成为一个http客户端。

下面大家思考一下:如何在不对服务器代码作任何修改的情况下,通过该服务器运行djando应用、flask应用和pyramid应用,同时满足这些不同网络 框架的要求?
以前,你选择的python网络框架将会限制所能够使用的网络服务器,反之亦然。如果框架和服务器在设计时就是可以相互匹配的,那你就不会面临这个问题:

2016218143122394.png (1024×671)

但是如果你试图将设计不相匹配的服务器与框架相结合,那么你肯定就会碰到下面这张图所展示的这个问题:

2016218143140296.png (968×768)

这就意味着,你基本上只能使用能够正常运行的服务器与框架组合,而不能选择你希望使用的服务器或框架。

那么,你怎样确保可以在不修改网络服务器代码或网络框架代码的前提下,使用自己选择的服务器,并且匹配多个不同的网络框架呢?为了解决这个问题,就出现了python web服务器网关接口(python web server gateway interface,简称“wsgi”)。

2016218143211755.png (1024×536)

wsgi的出现,让开发者可以将网络框架与网络服务器的选择分隔开来,不再相互限制。现在,你可以真正地将不同的网络服务器与网络开发框架进行混合搭配,选择满足自己需求的组合。例如,你可以使用gunicorn或nginx/uwsgi或waitress服务器来运行django、flask或pyramid应用。正是由于服务器和框架均支持wsgi,才真正得以实现二者之间的自由混合搭配。

所以,wsgi就是我在上一篇文章中所留问题的答案。你的网络服务器必须实现一个服务器端的wsgi接口,而目前所有现代python网络框架都已经实现了框架端的wsgi接口,这样开发者不需要修改服务器的代码,就可以支持某个网络框架。

网络服务器和网络框架支持wsgi协议,不仅让应用开发者选择符合自己需求的组合,同时也有利于服务器和框架的开发者,因为他们可以将注意力集中在自己擅长的领域,而不是相互倾轧。其他编程语言也拥有类似的接口:例如java的servlet api和ruby的rack。

口说无凭,我猜你肯定在想:“无代码无真相!”既然如此,我就在这里给出一个非常简单的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()

上面的代码比第一部分的服务器实现代码要长的多,但是这些代码实际也不算太长,只有不到150行,大家理解起来并不会太困难。上面这个服务器的功能也更多——它可以运行你使用自己喜欢的框架所写出来的网络应用,无论你选择pyramid、flask、django或是其他支持wsgi协议的框架。

你不信?你可以自己测试一下,看看结果如何。将上述代码保存为webserver2.py,或者直接从我的github仓库下载。如果你运行该文件时没有提供任何参数,那么程序就会报错并退出。

$ python webserver2.py
provide a wsgi application object as module:callable

上述程序设计的目的,就是运行你开发的网络应用,但是你还需要满足一些它的要求。要运行服务器,你只需要安装python即可。但是要运行使用pyramid、flask和django等框架开发的网络应用,你还需要先安装这些框架。我们接下来安装这三种框架。我倾向于使用virtualenv安装。请按照下面的提示创建并激活一个虚拟环境,然后安装这三个网络框架。

$ [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

接下来,你需要创建一个网络应用。我们首先创建pyramid应用。将下面的代码保存为pyramidapp.py文件,放至webserver2.py所在的文件夹中,或者直接从我的github仓库下载该文件:

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应用。

(lsbaws) $ python webserver2.py pyramidapp:app
wsgiserver: serving http on port 8888 …

在运行webserver2.py时,你告诉自己的服务器去加载pyramidapp模块中的app可调用对象(callable)。你的服务器现在可以接收http请求,并将请求中转至你的pyramid应用。应用目前只能处理一个路由(route):/hello。在浏览器的地址栏输入http://localhost:8888/hello,按下回车键,观察会出现什么情况:

2016218143311914.png (318×122)

你还可以在命令行使用curl命令,来测试服务器运行情况:

$ curl -v http://localhost:8888/hello

接下来我们创建flask应用。重复上面的步骤。

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

将上面的代码保存为flaskapp.py,或者直接从我的github仓库下载文件,并运行:

(lsbaws) $ python webserver2.py flaskapp:app
wsgiserver: serving http on port 8888 …

然后在浏览器地址栏输入http://localhost:8888/hello,并按下回车:

2016218143352301.png (344×148)

同样,在命令行使用curl命令,看看服务器是否会返回flask应用生成的信息:

$ curl -v http://localhost:8888/hello

这个服务器是不是也能支持django应用?试一试就知道了!不过接下来的操作更为复杂一些,我建议大家克隆整个仓库,并使用其中的djangoapp.py文件。下面的代码将一个名叫helloworld的django应用添加至当前的python路径中,然后导入了该项目的wsgi应用。

import sys
sys.path.insert(0, ‘./helloworld’)
from helloworld import wsgi
app = wsgi.application

将上面的代码保存为djangoapp.py,并使用你开发的服务器运行这个django应用。

(lsbaws) $ python webserver2.py djangoapp:app
wsgiserver: serving http on port 8888 …

同样,在浏览器中输入http://localhost:8888/hello,并按下回车键:

2016218143421440.png (340×135)

接下来,和前面几次一样,你通过命令行使用curl命令进行测试,确认了这个djando应用成功处理了你发出的请求:

$ curl -v http://localhost:8888/hello

你有没有按照上面的步骤测试?你做到了让服务器支持全部三种框架吗?如果没有,请尽量自己动手操作。阅读代码很重要,但这系列文章的目的在于重新开发,而这意味着你需要自己亲自动手。最好是你自己重新输入所有的代码,并确保代码运行结果符合预期。

经过上面的介绍,你应该已经认识到了wsgi的强大之处:它可以让你自由混合搭配网络服务器和框架。wsgi为python网络服务器与python网络框架之间的交互提供了一个极简的接口,而且非常容易在服务器端和框架端实现。下面的代码段分别展示了服务器端和框架端的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协议并没有指定如何实现这个对象)。
服务器每次从http客户端接收请求之后,调用application。它会向可调用对象传递一个名叫environ的字典作为参数,其中包含了wsgi/cgi的诸多变量,以及一个名为start_response的可调用对象。
框架/应用生成http状态码以及http响应报头(http response headers),然后将二者传递至start_response,等待服务器保存。此外,框架/应用还将返回响应的正文。
服务器将状态码、响应报头和响应正文组合成http响应,并返回给客户端(这一步并不属于wsgi协议)。
下面这张图直观地说明了wsgi接口的情况:

2016218143448615.png (1024×689)

有一点要提醒大家,当你使用上述框架开发网络应用的时候,你处理的是更高层级的逻辑,并不会直接处理wsgi协议相关的要求,但是我很清楚,既然你正在看这篇文章,你一定对框架端的wsgi接口很感兴趣。所以,我们接下来在不使用pyramid、flask或djando框架的前提下,自己开发一个极简的wsgi网络应用/网络框架,并使用wsgi服务器运行该应用:

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’]

将上述代码保存为wsgiapp.py文件,或者直接从我的github仓库下载,然后利用网络服务器运行该应用:

(lsbaws) $ python webserver2.py wsgiapp:app
wsgiserver: serving http on port 8888 …

在浏览器中输入下图中的地址,然后按回车键。结果应该是这样的:

2016218143519369.png (374×156)

你刚刚自己编写了一个极简的wsgi网络框架!太不可思议了。

接下来,我们重新分析服务器返回给客户端的对象。下面这张图展示的是你通过http客户端调用pyramid应用后,服务器生成的http响应:

2016218143535297.png (1024×519)

上图中的响应与你在第一篇中看到的有些类似,但是也有明显不同之处。举个例子,其中就出现了你之前没有看到过的4歌http报头:content-type,content-length,date和server。这些事网络服务器返回的响应对象通常都会包含的报头。不过,这四个都不是必须的。报头的目的是传递有关http请求/响应的额外信息。

既然你已经对wsgi接口有了更深的理解,下面这张图对响应对象的内容进行了更详细的解释,说明了每条内容是如何产生的。

2016218143552797.png (1024×500)

到目前为止,我还没有介绍过environ字典的具体内容,但简单来说,它是一个必须包含着wsgi协议所指定的某些wsgi和cgi变量。服务器从http请求中获取字典所需的值。下面这张图展示的是字典的详细内容:

2016218143611629.png (1024×669)

网络框架通过该字典提供的信息,根据指定的路由和请求方法等参数来决定使用哪个视图(views),从哪里读取请求正文,以及如何输出错误信息。

截至目前,你已经成功创建了自己的支持wsgi协议的网络服务器,还利用不同的网络框架开发了多个网络应用。另外,你还自己开发了一个极简的网络框架。本文介绍的内容不可谓不丰富。我们接下来回顾一下wsgi网络服务器如何处理http请求:

首先,服务器启动并加载网络框架/应用提供的application可调用对象
然后,服务器读取一个请求信息
然后,服务器对请求进行解析
然后,服务器使用请求数据创建一个名叫environ的字典
然后,服务器以environ字典和start_response可调用对象作为参数,调用application,并获得应用生成的响应正文。
然后,服务器根据调用application对象后返回的数据,以及start_response设置的状态码和响应标头,构建一个http响应。
最后,服务器将http响应返回至客户端。

2016218143633907.png (2979×4734)

以上就是第二部分的所有内容。你现在已经拥有了一个正常运行的wsgi服务器,可以支持通过遵守wsgi协议的网络框架所写的网络应用。最棒的是,这个服务器可以不需要作任何代码修改,就可以与多个网络框架配合使用。

Posted in 未分类

发表评论