這一期全是乾貨。幹得你口渴想喝水。
環境搭建
- 安裝 Python。你可以選擇官網安裝、Anaconda安裝或者你已經有了 Python3.5 以上的版本。PyPy也可以的。
- 可選:建立一個 Python 虛擬環境(不知所云的直接忽略這一步)
- 建立我們的專案資料夾
# bash shell
mkdir gethy
cd gethy
複製程式碼
在 Windows 上的同學不用擔心,本教程的一切操作都是可以在 Windows、Linux 和 Mac 上完成的。
4. 建立測試路徑和原始碼路徑
mkdir gethy # Python 界的約定俗成是在專案根目錄下建立一個同名的路徑來放原始碼
mkdir test
複製程式碼
- 安裝依賴
pip install h2
複製程式碼
我們要用到 Lukasa 大神寫的 hyper-h2 庫:https://github.com/python-hyper/hyper-h2
這個庫實現了 h2 協議的底層部分,包括:編碼解碼 TCP 層位元組串(hpack),建立並管理 HTTP 連線。
但是,這個庫並沒有實現 HTTP 應用層的方法(GET、POST)和語義(Requset & Response),也沒有實現 Flow Control(流量管理)和 Server Push(伺服器推送)。這些也是我們要實現的部分(除了 Server Push)
我們可以看到,HTTP協議是5、6、7層的協議,但是 hyper-h2 只實現了5、6層的功能。Web 應用是沒有辦法直接使用 hyper-h2 。所以我們要在 hyper-h2 的基礎上,實現完整的 h2 協議。
關於網路協議和架構,請參考 What’s The Difference Between The OSI Seven-Layer Network Model And TCP/IP?
開始程式設計
定義 API
我們遵循自上而下的設計。先設計API,再實現函式。
touch http2protocol.py event.py
複製程式碼
用你最喜歡的編輯器開啟http2protocol.py
,加入以下程式碼
class HTTP2Protocol:
"""
A pure in-memory H2 implementation for application level development.
It does not do IO.
"""
def __init__(self):
pass
def receive(self, data: bytes):
pass
def send(self, stream: Stream):
pass
複製程式碼
我們的庫只有 2 個公開API,receive
和send
。
receive
用來從 TCP 層獲取資料。send
將一個完整的 Stream
編碼為 TCP 可以直接接收的資料。
值得強調的是,這個庫不做任何 I/O。這種開發正規化叫做 I/O 獨立正規化。庫的使用者應該自己決定使用哪一種 IO 方式。這給予了開發者最大的靈活性。也符合 Clean Architecture 的原則。
hyper-h2 本身也是不做任何 IO 的,所以我們保留這個優良傳統。
英文裡叫 sans-IO model,請參考:http://sans-io.readthedocs.io
定義 Stream
除了HTTP2Protocol
類,Stream
類也是使用者會直接使用的類。
class Stream:
def __init__(self, stream_id: int, headers: iterable):
self.stream_id = stream_id
self.headers = headers
self.stream_ended = False
self.buffered_data = []
self.data = None
複製程式碼
看到這裡大家可能就會覺得很親切了。一個 Stream 其實就代表了一個常規的 HTTP Request 或者 Response。我們有常規的 headers,常規的 data(有些人叫 body)。與 HTTP/1.x 時代唯一不同的是,多了一個 stream id。
寫測試 TDD
Test Driven Development 與自上而下得到設計模式是密不可分的。現在我們有了API,寫測試同時也交代了 API 的使用方法。
cd ../test
touch test_all.py
複製程式碼
我們的庫很小,一個測試檔案就夠了。我們還需要一個幫助模組
wget https://raw.githubusercontent.com/CreatCodeBuild/gethy/master/test/helpers.py
複製程式碼
這個幫助模組是 Lusaka 大神在 hyper-h2 的測試中提供的。
我們現在來想象一下 gethy 的用法
# 虛擬碼
from gethy import HTTP2Protocol
import Socket
import SomeWebFramework
protocol = HTTP2Protocol()
socket = Socket()
while True:
if socket.accept():
while True:
bytes = socket.receive()
if bytes:
requests = protocol.receive(bytes)
for request in requests:
response = SomeWebFramework.handle(request)
bytes_to_send = protocol.send(response)
socket.send(bytes_to_send)
else:
break
複製程式碼
大家可以看到,我在這裡寫了一個虛擬碼的單執行緒阻塞式同步伺服器。我們的庫是完全不做 IO 的。一切IO都直接交給 Server 去完成。gethy 僅僅是在記憶體裡處理資料而已。上面的程式碼例子也清楚地展示了API的使用方式。
測試網路協議的實現的一大難點就在於 IO。如果類庫沒有 IO,那麼測試其實變得簡單了。那麼,我們來看看具體的測試怎麼寫吧。
# test_all.py
def test_receive_headers_only():
pass
def test_receive_headers_and_data():
pass
def test_send_headers_only():
pass
def test_send_headers_and_data():
pass
def test_send_huge_data():
pass
def test_receive_huge_data():
pass
複製程式碼
六個測試案例,測試了傳送接收與回覆請求。最後兩個測試使用巨大資料量,是為了測試 Flow Control 的正確性。我們目前可以不管。
先實現第一個
# test_all.py
from gethy import HTTP2Protocol
from gethy.event import RequestEvent
from helpers import FrameFactory
# 因為我們的測試很少,所以全域性變數也OK
frame_factory = FrameFactory()
protocol = HTTP2Protocol()
protocol.receive(frame_factory.preamble()) # h2 建立連線時要有的欄位
headers = [
(`:method`, `GET`),
(`:path`, `/`),
(`:scheme`, `https`), # scheme 和 schema 在英文中是同一個詞的不同寫法
# 不過,一般在 h2 中用 shceme,說到資料模型時用 schema
(`:authority`, `example.com`),
]
def test_receive_headers_only():
"""
able to receive headers with no data
"""
# 客戶端發起的 session 的 stream id 是單數
# 伺服器發起的 session 的 stream id 是雙數
# 一個 session 包含一對 request/response
# id 0 代表整個 connection
stream_id = 1
# 在這裡手動生成一個 client 的 request frame,來模擬客戶端請求
frame_from_client = frame_factory.build_headers_frame(headers,
stream_id=stream_id,
flags=[`END_STREAM`])
# 將資料結構序列化為 TCP 可接受的 bytes
data = frame_from_client.serialize()
# 伺服器端接收請求,得到一些 gethy 定義的事件
events = protocol.receive(data)
# 因為請求只有一個請求,所以僅可能有一個事件,且為 RequestEvent 事件
assert len(events) == 1
assert isinstance(events[0], RequestEvent)
event = events[0]
assert event.stream.stream_id == stream_id
assert event.stream.headers == headers # 驗證 Headers
assert event.stream.data == b`` # 驗證沒有任何資料
assert event.stream.buffered_data is None # 驗證沒有任何資料
assert event.stream.stream_ended is True # 驗證請求完整(Stream 結束)
複製程式碼
閱讀上面的測試,大家可以基本上知道 gethy 的用法和 http2 的基本語義。大家可以發現,http2 的語義和 http1 基本沒有變化。唯一需要注意的就是 headers 裡4個:xxx
字樣的 header。:
冒號是協議使用的 header 符號。應用自定義的 header 不應該使用冒號。然後,雖然 http2 協議本身是允許大寫字母,並且是大小寫敏感的,但是 gethy 的依賴庫 hyper-h2 只允許小寫。
現在來實現
def test_receive_headers_and_data():
stream_id = 3
client_headers_frame = frame_factory.build_headers_frame(headers, stream_id=stream_id)
headers_bytes = client_headers_frame.serialize()
data = b`some amount of data`
client_data_frame = frame_factory.build_data_frame(data, stream_id=stream_id, flags=[`END_STREAM`])
data_bytes = client_data_frame.serialize()
events = protocol.receive(headers_bytes+data_bytes)
assert len(events) == 1
assert isinstance(events[0], RequestEvent)
event = events[0]
assert event.stream.stream_id == stream_id
assert event.stream.headers == headers # 驗證 Headers
assert event.stream.data == data # 驗證沒有任何資料
assert event.stream.buffered_data is None # 驗證沒有任何資料
assert event.stream.stream_ended is True # 驗證請求完整(Stream 結束)
複製程式碼
帶資料的請求也很簡單,加上DATA
frame即可。
好的,我們再來看看如何傳送回覆。
def test_send_headers_only():
stream_id = 1
response_headers = [(`:status`, `200`)]
stream = Stream(stream_id, response_headers)
stream.stream_ended = True
stream.buffered_data = None
stream.data = None
events = protocol.send(stream)
assert len(events) == 2
for event in events:
assert isinstance(event, MoreDataToSendEvent)
複製程式碼
只傳送 Headers 很簡單,建立一個Stream
,然後傳送就行了。目前大家可以忽略MoreDataToSendEvent
。我會在視訊和後續文章中娓娓道來。
def test_send_headers_and_data():
"""
able to receive headers and small amount data.
able to send headers and small amount of data
"""
stream_id = 3
response_headers = [(`:status`, `400`)]
size = 1024 * 64 - 2 # default flow control window size per stream is 64 KB - 1 byte
stream = Stream(stream_id, response_headers)
stream.stream_ended = True
stream.buffered_data = None
stream.data = bytes(size)
events = protocol.send(stream)
assert len(events) == size // protocol.block_size + 3
for event in events:
assert isinstance(event, MoreDataToSendEvent)
assert not protocol.outbound_streams
assert not protocol.inbound_streams
複製程式碼
如果要傳送資料,只需要將stream.data
賦值。注意,一定要是bytes
型別。以上測試也涉及到了 Flow Control(流量控制),我會在視訊和後續文章中講解。
結語
好啦,想必到這裡你一定對 GetHy 有了大局觀的認識,也熟悉了 API 及應用場景。接下來就是要實現它了。我們下一期再見!