還記得嗎?在本系列第一部分我問過你:“怎樣在你的剛完成的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 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行不到)。上面的伺服器還做了別的事 – 它可以執行你喜歡的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 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() |
現在你已經準備好用完全屬於自己的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地址,按Enter鍵,觀察結果:
你也可以在命令列下使用‘curl’工具來測試伺服器:
1 2 |
$ curl -v http://localhost:8888/hello ... |
檢查伺服器和curl輸出了什麼到標準輸出。
現在弄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,它是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 ... |
輸入下面的地址,然後按Enter鍵:
雖然你已經做過兩次啦,你還是可以再在命令列測試一下,確認一下,這次是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伺服器,還更詳細地講解了我上面提到的話題。訂閱郵件組來獲取關於書籍和釋出時間和最近更新。