還記得嗎?在本系列第一部分我問過你:“怎樣在你的剛完成的WEB服務器下運行 Django 應用、Flask 應用和 Pyramid 應用?在不單獨修改服務器來適應這些不同的WEB框架的情況下。”往下看,來找出答案。
過去,你所選擇的一個Python Web框架會限制你選擇可用的Web服務器,反之亦然。如果框架和服務器設計的是可以一起工作的,那就很好:
但是,當你試着結合沒有設計成可以一起工作的服務器和框架時,你可能要面對(可能你已經面對了)下面這種問題:
基本上,你只能用可以在一起工作的部分,而不是你想用的部分。
那麼,怎樣確保在不修改Web服務器和Web框架下,用你的Web服務器運行不同的Web框架?答案就是Python Web服務器網關接口(或者縮寫爲WSGI,讀作“wizgy”)。
WSGI允許開發者把框架的選擇和服務器的選擇分開。現在你可以真正地混合、匹配Web服務器和Web框架了。例如,你可以在Gunicorn或者Nginx/uWSGI或者Waitress上面運行Django,Flask,或Pyramid。真正的混合和匹配喲,感謝WSGI服務器和框架兩者都支持:
就這樣,WSGI成了我在本系列第一部分和本文開頭重複問的問題的答案。你的Web服務器必須實現WSGI接口的服務器端,所有的現代Python Web框架已經實現 了WSGI接口的框架端了,這就讓你可以不用修改服務器代碼,適應某個框架。
現在你瞭解了Web服務器和WEb框架支持的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 | # 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('rn') # 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}rn'.format(status=status) for header in response_headers: response += '{0}: {1}rn'.format(*header) response += 'rn' 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行不到)。上面的服務器還做了別的事 – 它可以運行你喜歡的Web框架寫的基本的Web應用,可以是Pyramid,Flask,Django,或者其他的Python WSGI框架。
不信?自己試試看。把上面的代碼保存成webserver2.py或者直接從Github上下載。如果你不帶參數地直接運行它,它就會報怨然後退出。
1
2
|
$
python
webserver2.py
Provide
a
WSGI
application
object
as
module:callable
|
它真的想給Web框架提供服務,從這開始有趣起來。要運行服務器你唯一需要做的是安裝Python。但是要運行使用Pyramid,Flask,和Django寫的應用,你得先安裝這些框架。一起安裝這三個吧。我比較喜歡使用virtualenv。跟着以下步驟來創建和激活一個虛擬環境,然後安裝這三個Web框架。
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 |
此時你需要創建一個Web應用。我們先拿Pyramid開始吧。保存以下代碼到保存webserver2.py時相同的目錄。命名爲pyramidapp.py。或者直接從Github上下載:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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()
|
現在你已經準備好用完全屬於自己的Web服務器來運行Pyramid應用了:
1 2 | (lsbaws) $ python webserver2.py pyramidapp:app WSGIServer: Serving HTTP on port 8888 ... |
剛纔你告訴你的服務器從python模塊‘pyramidapp’中加載可調用的‘app’,現在你的服務器準備好了接受請求然後轉發它們給你的Pyramid應用。目前應用只處理一個路由:/hello 路由。在瀏覽器裏輸入http://localhost:8888/hello地址,按回車鍵,觀察結果:
你也可以在命令行下使用‘curl’工具來測試服務器:
1
2
|
$
curl
-v
http://localhost:8888/hello
...
|
檢查服務器和curl輸出了什麼到標準輸出。
現在弄Flask。按照相同的步驟。
1 2 3 4 5 6 7 8 9 10 11 12 | 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,它是GitHub倉庫的一部分。以下的源碼,簡單地把Django ‘helloworld’ 工程(使用Django的django-admin.py啓動項目預創建的)添加到當前Python路徑,然後導入了工程的WSGI應用。
1
2
3
4
5
|
import
sys
sys.path.insert(0,
'./helloworld')
from
helloworld
import
wsgi
app
=
wsgi.application
|
把以上代碼保存爲djangoapp.py,然後用你的Web服務器運行Django應用:
1 2 | (lsbaws) $ python webserver2.py djangoapp:app WSGIServer: Serving HTTP on port 8888 ... |
輸入下面的地址,然後按回車鍵:
雖然你已經做過兩次啦,你還是可以再在命令行測試一下,確認一下,這次是Django應用處理了請求。
1
2
|
$
curl
-v
http://localhost:8888/hello
...
|
你試了吧?你確定服務器可以和這三個框架一起工作吧?如果沒試,請試一下。閱讀挺重要,但這個系列是關於重建的,也就是說,你要自己動手。去動手試試吧。別擔心,我等你喲。你必須試下,最好呢,你親自輸入所有的東西,確保它工作起來像你期望的那樣。
很好,你已經體驗到了WSGI的強大:它可以讓你把Web服務器和Web框架結合起來。WSGI提供了Python Web服務器和Python Web框架之間的一個最小接口。它非常簡單,在服務器和框架端都可以輕易實現。下面的代碼片段展示了(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) |
以下是它如何工作的:
- 1.框架提供一個可調用的’應用’(WSGI規格並沒有要求如何實現)
- 2.服務器每次接收到HTTP客戶端請求後,執行可調用的’應用’。服務器把一個包含了WSGI/CGI變量的字典和一個可調用的’start_response’做爲參數給可調用的’application’。
- 3.框架/應用生成HTTP狀態和HTTP響應頭,然後把它們傳給可調用的’start_response’,讓服務器保存它們。框架/應用也返回一個響應體。
- 4.服務器把狀態,響應頭,響應體合併到HTTP響應裏,然後傳給(HTTP)客戶端(這步不是(WSGI)規格里的一部分,但它是後面流程中的一步,爲了解釋清楚我加上了這步)
以下是接口的視覺描述:
目前爲止,你已經瞭解了Pyramid,Flask,和Django Web應用,你還了解了實現了WSGI規範服務器端的服務器代碼。你甚至已經知道了不使用任何框架的基本的WSGI應用代碼片段。
問題就在於,當你使用這些框架中的一個來寫Web應用時,你站在一個比較高的層次,並不直接和WSGI打交道,但我知道你對WSGI接口的框架端好奇,因爲你在讀本文。所以,咱們一起寫個極簡的WSGI Web應用/Web框架吧,不用Pyramid,Flask,或者Django,然後用你的服務器運行它:
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上下載,然後像下面這樣使用你的Web服務器運行應用:
1
2
|
(lsbaws)
$
python
webserver2.py
wsgiapp:app
WSGIServer:
Serving
HTTP
on
port
8888
...
|
輸入下面地址,敲回車。你應該就看到下面結果了:
在你學習怎樣寫一個Web服務器時,你剛剛寫了一個你自己的極簡的WSGI Web框架!棒極啦。
現在,讓我們回頭看看服務器傳輸了什麼給客戶端。以下就是使用HTTP客戶端調用Pyramid應用時生成的HTTP響應:
這個響應跟你在本系列第一部分看到的有一些相近的部分,但也有一些新東西。例如,你以前沒見過的4個HTTP頭:Content-Type, Content-Length, Date, 和Servedr。這些頭是Web服務器生成的響應應該有的。雖然他們並不是必須的。頭的目的傳輸HTTP請求/響應的額外信息。
現在你對WSGI接口瞭解的更多啦,同樣,以下是帶有更多信息的HTTP響應,這些信息表示了哪些部件產生的它(響應):
我還沒有介紹’environ’字典呢,但它基本上就是一個Python字典,必須包含WSGI規範規定的必要的WSGI和CGI變量。服務器在解析請求後,從HTTP請求拿到了字典的值,字典的內容看起來像下面這樣:
Web框架使用字典裏的信息來決定使用哪個視圖,基於指定的路由,請求方法等,從哪裏讀請求體,錯誤寫到哪裏去,如果有的話。
現在你已經創建了你自己的WSGI Web服務器,使用不同的Web框架寫Web應用。還有,你還順手寫了個簡單的Web應用/Web框架。真是段難忘的旅程。咱們簡要重述下WSGI Web服務器必須做哪些工作才能處理髮給WSGI應用的請求吧:
- 首先,服務器啓動並加載一個由Web框架/應用提供的可調用的’application’
- 然後,服務器讀取請求
- 然後,服務器解析它
- 然後,服務器使用請求的數據創建了一個’environ’字典
- 然後,服務器使用’environ’字典和’start_response’做爲參數調用’application’,並拿到返回的響應體。
- 然後,服務器使用調用’application’返回的數據,由’start_response’設置的狀態和響應頭,來構造HTTP響應。
- 最終,服務器把HTTP響應傳回給戶端。
這就是全部啦。現在你有了一個可工作的WSGI服務器,它可以處理使用像Django,Flask,Pyramid或者 你自己的WSGI框架這樣的兼容WSGI的Web框架寫的基本的Web應用。最優秀的地方是,服務器可以在不修改代碼的情況下,使用不同的Web框架。
在你離開之前,還有個問題請你想一下,“該怎麼做才能讓服務器同一時間處理多個請求呢?”
保持關注,我會在本系列第三部分秀給你看實現它的一種方式。歡呼!
順便說下,我在寫一本書《一起構建WEB服務器:第一步》,它解釋了從零開始寫一個基本的WEB服務器,還更詳細地講解了我上面提到的話題。訂閱郵件組來獲取關於書籍和發佈時間和最近更新。