本文針對 python 2.7 版本,介紹了 BaseHTTPServer 這個庫的使用方法。
這個庫是 python 自帶的標準庫的一部分,不需要額外安裝,在 linux 系統下,位置在 /usr/lib/python2.7/BaseHTTPServer.py
。
HTTP 協議
HTTP 請求(request)
http 請求分為三個部分:
- 第一行:請求型別、地址和版本號
- 頭部資訊:HTTP header
- 資料部分
標準的 HTTP 請求是:
1 2 3 4 5 6 7 8 9 10 |
GET / HTTP/1.1 Host: cizixs.com User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:42.0) Gecko/20100101 Firefox/42.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Connection: keep-alive If-Modified-Since: Thu, 25 Feb 2016 16:00:57 GMT Cache-Control: max-age=0 |
標準的 HTTP 響應頭部:
1 2 3 4 5 6 7 8 9 10 11 |
HTTP/1.1 304 Not Modified Server: GitHub.com Date: Thu, 24 Mar 2016 06:21:25 GMT Last-Modified: Thu, 25 Feb 2016 16:00:57 GMT access-control-allow-origin: * Expires: Thu, 24 Mar 2016 06:31:25 GMT Cache-Control: max-age=600 X-GitHub-Request-Id: 3AF60A59:7CE3:1C889201:56F38765 data... |
使用 BaseHTTPServer 寫一個簡單的 web server
這個類可以幫助你快速編寫一個 HTTP 服務端 server。別說了,先上程式碼:
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 |
from BaseHTTPServer import BaseHTTPRequestHandler import cgi import json class TodoHandler(BaseHTTPRequestHandler): """A simple TODO server which can display and manage todos for you. """ # Global instance to store todos. You should use a database in reality. TODOS = [] def do_GET(self): # return all todos if self.path != '/': self.send_error(404, "File not found.") return # Just dump data to json, and return it message = json.dumps(self.TODOS) self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(message) def do_POST(self): """Add a new todo Only json data is supported, otherwise send a 415 response back. Append new todo to class variable, and it will be displayed in following get request """ ctype, pdict = cgi.parse_header(self.headers['content-type']) if ctype == 'application/json': length = int(self.headers['content-length']) post_values = json.loads(self.rfile.read(length)) self.TODOS.append(post_values) else: self.send_error(415, "Only json data is supported.") return self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() self.wfile.write(post_values) if __name__ == '__main__': # Start a simple server, and loop forever from BaseHTTPServer import HTTPServer server = HTTPServer(('localhost', 8888), TodoHandler) print("Starting server, use <Ctrl-C> to stop") server.serve_forever() |
這段程式碼實現的功能很簡單,就是一個簡單的 Todo 管理:你可以新增 todo,也可以查詢 todo。更新和刪除 todo 可以根據上面的程式碼自行新增。
程式碼也不難理解,在關鍵的步驟我已經新增了註釋,在這裡就不再解釋了。
好了,我們用 httpie 來和它互動一下,最開始的時候返回的資料是空的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
➜ ~ http --verbose http://localhost:8888 GET / HTTP/1.1 Accept: */* Accept-Encoding: gzip, deflate Host: localhost:8888 User-Agent: HTTPie/0.8.0 HTTP/1.0 200 OK Content-type: application/json Date: Fri, 25 Mar 2016 09:35:08 GMT Server: BaseHTTP/0.3 Python/2.7.10 [] |
然後,新增幾條試試:
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 |
➜ ~ http --verbose POST http://localhost:8888 content="buy a beer" finished:=false POST / HTTP/1.1 Accept: application/json Accept-Encoding: gzip, deflate Content-Length: 44 Content-Type: application/json; Host: localhost:8888 User-Agent: HTTPie/0.8.0 { "content": "buy a beer", "finished": false } HTTP/1.0 200 OK Content-type: application/json Date: Fri, 25 Mar 2016 09:36:08 GMT Server: BaseHTTP/0.3 Python/2.7.10 {u'content': u'buy a beer', u'finished': False} ➜ ~ http --verbose POST http://localhost:8888 content="learn HTTP" finished:=false POST / HTTP/1.1 Accept: application/json Accept-Encoding: gzip, deflate Content-Length: 44 Content-Type: application/json; charset=utf-8 Host: localhost:8888 User-Agent: HTTPie/0.8.0 { "content": "learn HTTP", "finished": false } HTTP/1.0 200 OK Content-type: application/json Date: Fri, 25 Mar 2016 09:36:24 GMT Server: BaseHTTP/0.3 Python/2.7.10 {u'content': u'learn HTTP', u'finished': False} |
這個時候,再來看一下內容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
➜ ~ http --verbose http://localhost:8888 GET / HTTP/1.1 Accept: */* Accept-Encoding: gzip, deflate Host: localhost:8888 User-Agent: HTTPie/0.8.0 HTTP/1.0 200 OK Content-type: application/json Date: Fri, 25 Mar 2016 09:36:58 GMT Server: BaseHTTP/0.3 Python/2.7.10 [ { "content": "buy a beer", "finished": false }, { "content": "learn HTTP", "finished": false } ] |
我們剛剛建立的 todo 就出現了!前面也說了,這段程式碼不支援更新和刪除功能,而且資料也沒有落地,關閉程式之後,資料就消失了。
BaseHTTPServer 原始碼解析
BaseHTTPServer 這個模組提供了兩個類讓開發者實現 HTTP server:HTTPServer
和 BaseHTTPRequestHandler
。
HTTPServer
繼承了 SocketServer.BaseServer
,主要功能是:建立和監聽 socket,把請求轉發給 handler 去處理。主要的工作都是在 BaseHTTPRequestHandler
中處理的,它把和請求有關的資訊都封裝成自己的例項變數,可以在子類中直接使用。這些變數包括:
- client_address:客戶端的地址,存放在一個 tuple 裡 (host, port)
- server: server 例項
- command:請求型別,比如,GET、POST 等
- path:請求路徑,比如
/index.html
- request_version: 請求版本號,比如
HTTP/1.0
- headers:
mimetools.Message
的例項物件,包含了頭部資訊 - rfile:rfile 是一個輸入流,用來讀取請求的資料
- wfile:wfile 是一個輸出流,用來回寫響應,回寫的資料必須遵守 HTTP 協議的格式
除了這些例項變數之外,還有其他的類變數:
- server_version:伺服器的版本號,比如
BaseHTTP/0.2
- sys_version:python 的版本號,比如
Python/1.4
- error_message_format:錯誤 response 的格式
- error_content_type:錯誤 response 的 Content-Type,預設是
text/html
- protocol_version:HTTP 協議版本號
- responses:error code 對應錯誤訊息的匹配關係
當然,還有一些方法可以使用:
- handle():呼叫底層的實現來處理一次請求
- send_response():傳送應答訊息和狀態碼
更多的內容可以檢視文末的連結。
為什麼這個類不會被廣泛使用?
寫了這麼多,我們也看到這個類缺點很多,比如:
- 不支援 url 解析和轉發,如果有多個 endpoint,需要使用者自己解析
- 回寫的響應也需要使用者自己維護格式,容易出錯
- 沒有模板支援,如果要寫 HTML 頁面,也需要自己維護
所以在正式的工作中,編寫 HTTP server 端應用的時候,都是使用 web 框架的。因為 web 框架幫你封裝了底層的這些細節,還提供了很多便利的功能,讓開發者把中心更多地放到業務邏輯的實現。
如果有時間,以後講講怎麼自己寫一個簡單的 HTTP Server。