這一期全是乾貨。幹得你口渴想喝水。html
# bash shell
mkdir gethy
cd gethy
複製代碼
在 Windows 上的同窗不用擔憂,本教程的一切操做都是能夠在 Windows、Linux 和 Mac 上完成的。 4. 建立測試路徑和源代碼路徑python
mkdir gethy # Python 界的約定俗成是在項目根目錄下建立一個同名的路徑來放源代碼
mkdir test
複製代碼
pip install h2
複製代碼
咱們要用到 Lukasa 大神寫的 hyper-h2 庫:https://github.com/python-hyper/hyper-h2git
這個庫實現了 h2 協議的底層部分,包括:編碼解碼 TCP 層字節串(hpack),創建並管理 HTTP 鏈接。 可是,這個庫並無實現 HTTP 應用層的方法(GET、POST)和語義(Requset & Response),也沒有實現 Flow Control(流量管理)和 Server Push(服務器推送)。這些也是咱們要實現的部分(除了 Server Push)github
咱們能夠看到,HTTP協議是五、六、7層的協議,可是 hyper-h2 只實現了五、6層的功能。Web 應用是沒有辦法直接使用 hyper-h2 。因此咱們要在 hyper-h2 的基礎上,實現完整的 h2 協議。shell
關於網絡協議和架構,請參考 What’s The Difference Between The OSI Seven-Layer Network Model And TCP/IP?編程
咱們遵循自上而下的設計。先設計API,再實現函數。設計模式
touch http2protocol.py event.py
複製代碼
用你最喜歡的編輯器打開http2protocol.py
,加入如下代碼bash
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
除了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。
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 及應用場景。接下來就是要實現它了。咱們下一期再見!