首先,到底什么是网络服务器?
简而言之,它是在物理服务器上搭建的一个网络连接服务器(networking server),永久地等待客户端发送请求。当服务器收到请求之后,它会生成响应并将 其返回至客户端。客户端与服务器之间的通信,是以HTTP协议进行的。客户端可以是浏览器,也可以是任何支持HTTP协议的软件。
那么,网络服务器的简单实现形式会是怎样的呢?下面是我对此的理解。示例代码使用Python语言实现,不过即使你不懂Python语言,你应该也可以从代码和下面的 解释中理解相关的概念:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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)下载,然后通过命令行运行该文件:
1
2
|
$ python webserver1.py Serving HTTP on port 8888 … |
接下来,在浏览器的地址栏输入这个链接http://localhost:8888/hello, 然后按下回车键,你就会看见神奇的一幕。在浏览器中,应该会出现“ Hello, World!”这句话:
是不是很神奇?接下来,我们来分析背后的实现原理。
首先,我们来看你所输入的网络地址。它的名字叫URL(Uniform Resource Locator,统一资源定位符),其基本结构如下:
通过URL,你告诉了浏览器它所需要发现并连接的网络服务器地址,以及获取服务器上的页面路径。不过在浏览器发送HTTP请求之前,它首先要与目标网络服务器建立TC P连接。然后,浏览器再通过TCP连接发送HTTP请求至服务器,并等待服务器返回HTTP响应。当浏览器收到响应的时候,就会在页面上显示响应的内容,而在上面的例
子中,浏览器显示的就是“Hello, World!”这句话。
那么,在客户端发送请求、服务器返回响应之前,二者究竟是如何建立起TCP连接的呢?要建立起TCP连接,服务器和客户端都使用了所谓的套接字(socket)。接下 来,我们不直接使用浏览器,而是在命令行使用telnet手动模拟浏览器。
在运行网络服务器的同一台电脑商,通过命令行开启一次telnet会话,将需要连接的主机设置为localhost,主机的连接端口设置为8888,然后 按回车键:
1
2
3
|
$ telnet localhost 8888 Trying 127.0.0.1 … Connected to localhost. |
完成这些操作之后,你其实已经与本地运行的网络服务器建立了TCP连接,随时可以发送和接收HTTP信息。在下面这张图片里,展示的是服务器接受新TCP连接所需要完 成的标准流程。
在上面那个telnet会话中,我们输入GET /hello HTTP/1.1,然后按下回车:
1
2
3
4
5
6
7
|
$ 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请求的基本结构:
HTTP请求行包括了HTTP方法(这里使用的是GET方法,因为我们希望从服务器获取内容),服务器页面路径(/hello)以及HTTP协议的版本。
为了尽量简化,我们目前实现的网络服务器并不会解析上面的请求,你完全可以输入一些没有任何意义的代码,也一样可以收到"Hello, World!"响应。
在你输入请求代码并按下回车键之后,客户端就将该请求发送至服务器了,服务器则会解析你发送的请求,并返回相应的HTTP响应。
下面这张图显示的是服务器返回至客户端的HTTP响应详情:
我们来分析一下。响应中包含了状态行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网络框架将会限制所能够使用的网络服务器,反之亦然。如果框架和服务器在设计时就是可以相互匹配的,那你就不会面临这个问题:
但是如果你试图将设计不相匹配的服务器与框架相结合,那么你肯定就会碰到下面这张图所展示的这个问题:
这就意味着,你基本上只能使用能够正常运行的服务器与框架组合,而不能选择你希望使用的服务器或框架。
那么,你怎样确保可以在不修改网络服务器代码或网络框架代码的前提下,使用自己选择的服务器,并且匹配多个不同的网络框架呢?为了解决这个问题,就出现了Python Web服务器网关接口(Python Web Server Gateway Interface,简称“WSGI”)。
WSGI的出现,让开发者可以将网络框架与网络服务器的选择分隔开来,不再相互限制。现在,你可以真正地将不同的网络服务器与网络开发框架进行混合搭配,选择满足自己需求的组合。例如,你可以使用Gunicorn或Nginx/uWSGI或Waitress服务器来运行Django、Flask或Pyramid应用。正是由于服务器和框架均支持WSGI,才真正得以实现二者之间的自由混合搭配。
所以,WSGI就是我在上一篇文章中所留问题的答案。你的网络服务器必须实现一个服务器端的WSGI接口,而目前所有现代Python网络框架都已经实现了框架端的WSGI接口,这样开发者不需要修改服务器的代码,就可以支持某个网络框架。
网络服务器和网络框架支持WSGI协议,不仅让应用开发者选择符合自己需求的组合,同时也有利于服务器和框架的开发者,因为他们可以将注意力集中在自己擅长的领域,而不是相互倾轧。其他编程语言也拥有类似的接口:例如Java的Servlet API和Ruby的Rack。
口说无凭,我猜你肯定在想:“无代码无真相!”既然如此,我就在这里给出一个非常简单的WSGI服务器实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
# 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仓库下载。如果你运行该文件时没有提供任何参数,那么程序就会报错并退出。
1
2
|
$ python webserver2.py Provide a WSGI application object as module:callable |
上述程序设计的目的,就是运行你开发的网络应用,但是你还需要满足一些它的要求。要运行服务器,你只需要安装Python即可。但是要运行使用Pyramid、Flask和Django等框架开发的网络应用,你还需要先安装这些框架。我们接下来安装这三种框架。我倾向于使用virtualenv安装。请按照下面的提示创建并激活一个虚拟环境,然后安装这三个网络框架。
1
2
3
4
5
6
7
8
9
10
|
$ [ 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仓库下载该文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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应用。
1
2
|
(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,按下回车键,观察会出现什么情况:
你还可以在命令行使用curl命令,来测试服务器运行情况:
1
2
|
$ curl - v http: //localhost :8888 /hello ... |
接下来我们创建Flask应用。重复上面的步骤。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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仓库下载文件,并运行:
1
2
|
(lsbaws) $ python webserver2.py flaskapp:app WSGIServer: Serving HTTP on port 8888 ... |
然后在浏览器地址栏输入http://localhost:8888/hello,并按下回车:
同样,在命令行使用curl命令,看看服务器是否会返回Flask应用生成的信息:
1
2
|
$ curl - v http: //localhost :8888 /hello ... |
这个服务器是不是也能支持Django应用?试一试就知道了!不过接下来的操作更为复杂一些,我建议大家克隆整个仓库,并使用其中的djangoapp.py文件。下面的代码将一个名叫helloworld的Django应用添加至当前的Python路径中,然后导入了该项目的WSGI应用。
1
2
3
4
5
6
|
import sys sys.path.insert( 0 , './helloworld' ) from helloworld import wsgi app = wsgi.application |
将上面的代码保存为djangoapp.py,并使用你开发的服务器运行这个Django应用。
1
2
|
(lsbaws) $ python webserver2.py djangoapp:app WSGIServer: Serving HTTP on port 8888 ... |
同样,在浏览器中输入http://localhost:8888/hello,并按下回车键:
接下来,和前面几次一样,你通过命令行使用curl命令进行测试,确认了这个Djando应用成功处理了你发出的请求:
1
2
|
$ curl - v http: //localhost :8888 /hello ... |
你有没有按照上面的步骤测试?你做到了让服务器支持全部三种框架吗?如果没有,请尽量自己动手操作。阅读代码很重要,但这系列文章的目的在于重新开发,而这意味着你需要自己亲自动手。最好是你自己重新输入所有的代码,并确保代码运行结果符合预期。
经过上面的介绍,你应该已经认识到了WSGI的强大之处:它可以让你自由混合搭配网络服务器和框架。WSGI为Python网络服务器与Python网络框架之间的交互提供了一个极简的接口,而且非常容易在服务器端和框架端实现。下面的代码段分别展示了服务器端和框架端的WSGI接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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接口的情况:
有一点要提醒大家,当你使用上述框架开发网络应用的时候,你处理的是更高层级的逻辑,并不会直接处理WSGI协议相关的要求,但是我很清楚,既然你正在看这篇文章,你一定对框架端的WSGI接口很感兴趣。所以,我们接下来在不使用Pyramid、Flask或Djando框架的前提下,自己开发一个极简的WSGI网络应用/网络框架,并使用WSGI服务器运行该应用:
1
2
3
4
5
6
7
8
9
|
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仓库下载,然后利用网络服务器运行该应用:
1
2
|
(lsbaws) $ python webserver2.py wsgiapp:app WSGIServer: Serving HTTP on port 8888 ... |
在浏览器中输入下图中的地址,然后按回车键。结果应该是这样的:
你刚刚自己编写了一个极简的WSGI网络框架!太不可思议了。
接下来,我们重新分析服务器返回给客户端的对象。下面这张图展示的是你通过HTTP客户端调用Pyramid应用后,服务器生成的HTTP响应:
上图中的响应与你在第一篇中看到的有些类似,但是也有明显不同之处。举个例子,其中就出现了你之前没有看到过的4歌HTTP报头:Content-Type,Content-Length,Date和Server。这些事网络服务器返回的响应对象通常都会包含的报头。不过,这四个都不是必须的。报头的目的是传递有关HTTP请求/响应的额外信息。
既然你已经对WSGI接口有了更深的理解,下面这张图对响应对象的内容进行了更详细的解释,说明了每条内容是如何产生的。
到目前为止,我还没有介绍过environ字典的具体内容,但简单来说,它是一个必须包含着WSGI协议所指定的某些WSGI和CGI变量。服务器从HTTP请求中获取字典所需的值。下面这张图展示的是字典的详细内容:
网络框架通过该字典提供的信息,根据指定的路由和请求方法等参数来决定使用哪个视图(views),从哪里读取请求正文,以及如何输出错误信息。
截至目前,你已经成功创建了自己的支持WSGI协议的网络服务器,还利用不同的网络框架开发了多个网络应用。另外,你还自己开发了一个极简的网络框架。本文介绍的内容不可谓不丰富。我们接下来回顾一下WSGI网络服务器如何处理HTTP请求:
- 首先,服务器启动并加载网络框架/应用提供的application可调用对象
- 然后,服务器读取一个请求信息
- 然后,服务器对请求进行解析
- 然后,服务器使用请求数据创建一个名叫environ的字典
- 然后,服务器以environ字典和start_response可调用对象作为参数,调用application,并获得应用生成的响应正文。
- 然后,服务器根据调用application对象后返回的数据,以及start_response设置的状态码和响应标头,构建一个HTTP响应。
- 最后,服务器将HTTP响应返回至客户端。
以上就是第二部分的所有内容。你现在已经拥有了一个正常运行的WSGI服务器,可以支持通过遵守WSGI协议的网络框架所写的网络应用。最棒的是,这个服务器可以不需要作任何代码修改,就可以与多个网络框架配合使用。