一起寫一個 Web 服務器
還記的么,在第一部分Part 1我問過一個問題,“怎樣在你的剛完成的WEB服務器下運行 Django 應用、Flask 應用和 Pyramid 應用?在不單獨修改服務器來適應這些不同的 WEB 框架的情況下。”
繼續讀,下面將會給出答案。在過去,你選擇一個python web 框架將會限制web服務器的使用,反之亦然。 如果web框架和服務器被設計的一起工作,那么他們將沒問題:
但是,當你試圖組合不是被設計的一起工作的一個web框架和一個web服務器時你可能已經遇到下面的問題:
基本上,你必須使用一起工作的而不是你想要用的組合。
所以,你怎么能確定你能跑你的web服務器兼容多個web框架的同時而又不用寫代碼來改變web服務器或者web框架?答案就是**Python Web Server Gateway Interface **(或者[WSGI] (https://www.python.org/dev/peps/pep-0333/) 作為簡寫, 發音 “wizgy”).
WSGI 允許開發者分別選擇web框架和web服務器。現在你們混合使用匹配的web框架和服務器來滿足你的需求。你能跑 Django, Flask, 或者 Pyramid, 例如, 使用 Gunicorn 或者 Nginx/uWSGI 又或者 Waitress. 真的混合且匹配這要歸功于 WSGI 既支持服務器有支持框架:
所以, WSGI 是第一部分我問的問題的答案 Part 1 也在文章最開始提到。你的web服務器必須實現WSGI的服務端接口,現在所有的python web 框架已經實現了WSGI的框架端接口 , 這允許你使用它們而不需修改代碼來適配一個特殊的web框架。 現在你知道了WSGI 支持 Web servers 和 Web frameworks 允許你選擇一個匹配的組合,這也得利于服務端和框架開發者因為它們能集中于它們想關注的方面.其他語言有類似的接口 : 例如Java, 有 Servlet API 同時Ruby 有 Rack. 這都沒問題,你可能會說: “給我展示你的代碼!”好的,看一下這個完美的最小 WSGI 服務器實現:
#!python # 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()
這比第一部分 Part 1打多了, 但是 他也足夠小(僅僅150行) 讓你們能理解而又不用陷入細節中 .
上面的代碼實現了更多--他能跑基本的web框架 ,不論他是 Pyramid, Flask, Django,或者其他 PythonWSGI 框架. 不信?自己試試保存上面的代碼 webserver2.py 或者直接從GitHub下載.如果你跑它而不用參數,他將報錯然后退出 .
#!bash $ python webserver2.pyProvide a WSGI application object as module:callable
它真的想服務你的web應用 , 這也是它有趣的開始。你只需要安裝python就可以運行起來。 但是為了運行使用 Pyramid, Flask,和 Django 框架的應用,你需要先安裝這些框架。接下來我們安裝這三個框架,我更喜歡使用 virtualenv.只需要按下面的步驟來創建和激活一個虛擬環境和安裝這三個web框架
#!bash $ [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 開始. 保存下面的代碼 pyramidapp.py 到你保存webserver2.py的目錄或者下載它從 GitHub:
#!bash 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 應用了 :
#!bash (lsbaws) $ python webserver2.py pyramidapp:app WSGIServer: Serving HTTP on port 8888 ...
你只需要告訴你的服務器加載那個應用 ‘app’ ,他可以從python模塊調用。你的服務器現在已經可以接受請求了,他會轉給你的 Pyramid 應用 這個應用只處理一個路由 :那個/hello 路由. 在瀏覽器里輸入 http://localhost:8888/hello 可以看到下面的變化: 你也可以在命令行使用curl 調用服務器
#!bash $ curl -v http://localhost:8888/hello...
檢查服務器和curl 的標準打印,現在看Flask. 按下面的步驟.
#!bash 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下載,在服務器上運行:
#!bash (lsbaws) $ python webserver2.py flaskapp:app WSGIServer: Serving HTTP on port 8888 ...
在瀏覽器輸入 http://localhost:8888/hello : 再次使用 ‘curl’ 來看服務器返回信息 :
#!bash $ curl -v http://localhost:8888/hello...
服務器也能處理一個 Django 應用。試一試t! 這需要多一點的處理,所以我建議克隆整個repo并使用 djangoapp.py, 這是 GitHub repository的部分內容. 下面是源碼,他基本的添加了 Django ‘helloworld’ 工程 到當前的python 目錄并導入 WSGI 應用.
#!bash import sys sys.path.insert(0, './helloworld') from helloworld import wsgi app = wsgi.application
保存上面的代碼 djangoapp.py ,運行 Django 應用 :
#!bash (lsbaws) $ python webserver2.py djangoapp:app WSGIServer: Serving HTTP on port 8888 ...
在瀏覽器輸入 :
確認是 Django 應用處理 的請求
#!bash $ curl -v http://localhost:8888/hello...
你試了嗎?你確定這個服務器在這三個框架下能工作嗎?如果沒有,試試吧 .閱讀很重要,但是這個系列是關于重建的,這意味著你需要去嘗試 。大膽的試試吧,我將等你,不用擔心 。你需要嘗試確保他可以按預先想的那樣運行。 好的,你已經體驗了 WSGI的威力:他運行你混合選擇web服務器和web框架 . WSGI 提供了最小的服務器和框架間的接口 .這很重要且容易實現兩邊 .下面的代碼展示了服務器和框架的接口 :
#!python 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)
他是這樣工作的:
-
框架提供可調用的應用 ( WSGI 沒有描述需要如何實現)
-
服務器調用可調用的應用給他接收到的每一個http請求 . 他傳遞一個 字典 ‘environ’ 包含 WSGI/CGI變量和一個 ‘start_response’ 可調用接口
-
框架/應用產生 HTTP 狀態和 HTTP 響應頭傳遞給 ‘start_response’ 服務器. 框架/應用也產生響應體
-
服務器組裝狀態,響應頭和響應體為一個 HTTP 響應并傳輸到客戶端
下面是一個接口的簡單圖示:
目前,你已經看來 Pyramid, Flask, and Django Web 應用你也看來服務端的代碼實現 WSGI . 你已經知道了 WSGI 的最難的部分而且他并沒有使用任何框架.
但你使用框架寫一個web應用時,你在一個更高的水平工作,并沒有直接使用 WSGI , 但是你好奇框架端的 WSGI 接口, 因為你在讀這篇文章.
所以,我們創建一個最小的 WSGI Web 應用/Web 框架不使用 Pyramid, Flask, 或者 Django 并在服務器上運行:
#!python 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 下載并部署在服務器下
#!bash (lsbaws) $ python webserver2.py wsgiapp:app WSGIServer: Serving HTTP on port 8888 ...
在瀏覽器輸入:
你剛才在學習寫服務器的同時已經寫了一個自己的最小 WSGI Web 框架 ! .現在我們看服務器給客戶端傳輸了什么?這是當你調用Pyramid應用時服務器產生的 HTTP 響應 :
響應有一些相似于第一部分 Part 1 但是他也有一些新東西 .例如他包含四個HTTP headers :Content-Type, Content-Length, Date, and Server. 這些頭部是一個web服務器應該生產的 .盡管沒有一個是嚴格必須的。
這些頭部的目的是為了添加額外的信息給htpp 請求/應答的.
現在你知道了 WSGI 接口, 下面是同樣的 HTTP 需要和更多的生成信息 :
我還沒有說任何關于e ‘environ’ 字典的信息 , 但是基本上他是一個 Python 字典,他必須包含 WSGI 和 CGI 變量 . 服務器從字典中取http請求值 .這是字典內容像這樣的 :
一個web框架使用字典中的信息決定使用哪一個view 和路由,請求方法等. 哪里讀取請求主體和哪里寫錯誤信息,如果有 .到現在你已經創建了你自己的 WSGI Web 服務器和你的web應用. .這是見鬼了的過程 .我們來回顧 WSGI Web server 需要做些什來吸納關于一個 WSGI 應用:
- 首先,服務器開始并加載應用
- 然后, 服務器讀取請求
- 然后, 服務器解析他
- 然后, 服務器使用請求數據創建 ‘environ’ 字典
- 然后,服務器使用 ‘environ’ 字典調用應用并添加一個 *‘start_response’ 獲取一個阻塞的 響應體.
- 然后, 服務器創建http響應
- 最后,服務器傳輸http 響應給客戶端
這就是所有的,你已經有一個可以工作的 WSGI 服務器,服務基本的web應用基于 WSGI 實現,兼容 Web 框架 如 Django, Flask,Pyramid, 或者你自己的WSGI 框架. 最好的是服務器可以運行多種框架而不必修改服務端代碼 .
在你繼續之前,考慮下面的問題 , “怎么才能使你的服務器同時處理多個請求 ?” 繼續關注在第三部分我將繼續解答 Cheers!
來自:http://drops.wooyun.org/tips/6089