痛入爽出 HTTP/2:程式碼實戰2

哲的王發表於2017-12-12

一個寫文件的開發者,其實就是個 Docker


正文

上一期我們熟悉了應用場景和測試,這一期我們實現receive函式。

先重溫一下 API:

class HTTP2Protocol:
    def receive(self, data: bytes):
        pass    
    def send(self, stream: Stream):
        pass
複製程式碼

我們的整體設計思路是 Event Driven + Mutable State.

Event Drivengethy 內部自定義一些事件(Event),HTTP2Protocol的 Public API 只會返回這些 Event 而已。

Mutable StateHTTP2Protocol內部會管理兩個緩衝(Buffer),一個inbound_buffer儲存接收的資料,一個outbound_buffer儲存需要傳送的資料。這兩個 Buffer 都是私有的,使用者不應該使用。根據不同的事件,HTTP2Protocol會向 Buffer 新增資料或者清除資料。

HTTP2Protocol 類

現在,我們來看更具體的函式簽名:

# http2protocol.py
from typing import List

import h2.config
import h2.connection
import h2.events
import h2.exceptions
from h2.events import (
	RequestReceived,
	DataReceived,
	WindowUpdated,
	StreamEnded
)

from gethy.event import H2Event


class HTTP2Protocol:
    def __init__(self):
        self.current_events = []
        
        self.request_buffer = {}   # input buffer
        self.response_buffer = {}  # output buffer, not used in this tutorial
        
        config = h2.config.H2Configuration(client_side=False, header_encoding='utf-8')
        self.http2_connection = h2.connection.H2Connection(config=config)

    def receive(self, data: bytes) -> List[H2Event]:
        pass
複製程式碼

current_events:顧名思義,用來存放目前已知的事件。
request_buffer:存放沒有接收完整的 Request Stream。
response_buffer:存放沒有完全傳送的 Response Stream。

Stream 類

當然,我們還需要一個Stream來表示一個資料流。

class Stream:
    def __init__(self, stream_id: int, headers):
    	self.stream_id = stream_id
    	self.headers = headers  # as the name indicates
    
    	# when stream_ended is True
    	# buffered_data has to be None
    	# and data has to be a bytes
    	#
    	# if buffered_data is empty
    	# then both buffered_data and data have to be None when stream_ended is True
    	#
    	# should write a value enforcement contract decorator for it
    	self.stream_ended = False
    	self.buffered_data = []
    	self.data = None
複製程式碼

流程圖

在實現之前,我們先來看看流程圖。

Receive 邏輯
如圖所示,我們的工作流程是純線性的,所以也使其邏輯簡明,容易實現。

receive

def receive(self, data: bytes):
    """
    receive bytes, return HTTP Request object if any stream is ready
    else return None
    
    :param data: bytes, received from a socket
    :return: list, of Request
    """
    # First, proceed incoming data
    # handle any events emitted from h2
    events = self.http2_connection.receive_data(data)
    for event in events:
    	self._handle_event(event)
    
    self._parse_request_buffer()
    
    events = self.current_events    # assign all current events to an events variable and return this variable
    self.current_events = []        # empty current event list by assign a newly allocated list
    
    return events
複製程式碼

這裡就將receive函式寫好了,接下來實現_handle_event_parse_request_buffer

_handle_event

Handle events 的部分由幾個重要的函式組成。

def _handle_event(self, event: h2.events.Event):
    # RequestReceived 的命名可能產生誤解。
    # 這裡不是說一個完整的 Request 收到了。
    # 而是說,Headers 收到了。
    if isinstance(event, h2.events.RequestReceived):
    	self._request_headers_received(event)
    
    elif isinstance(event, h2.events.DataReceived):
    	self._data_received(event)
    
    else:
    	logging.info("Has not implement %s handler" % type(event))
複製程式碼

首先_handle_event要判斷是哪種 h2 事件。我們用if/else來將事件導流到相應的函式去。本期只關心 Request(Headers&Data),其餘事件簡單地列印出來。

注:這裡的 h2 事件其實和 HTTP/2 的 frame 有直接的關係。一個 Request 事件其實就是一個 Request Frame。一個 Data 事件其實就是一個 Data Frame。

參考文件:
Hyper-h2 API
http2 FramingLayer

_request_headers_received

def _request_headers_received(self, event: RequestReceived):
    self.request_buffer[event.stream_id] = Stream(event.stream_id, event.headers)
    
    if event.priority_updated:
    	logging.warning("RequestReceived.priority_updated is not handled")
    
    if event.stream_ended:
	    self._stream_ended(event.stream_ended)
複製程式碼

這個 event 裡有stream_id&headers,將其拿到並構造一個Stream例項。如果資料流結束,則呼叫_stream_ended。這裡stream_ended == True的意思就是這個 Request 只有 Headers。通常的GET或者POST url param encoded就屬於這個型別。很多框架甚至不允許GET帶有 Request Body/Data。

_data_received

def _data_received(self, event: DataReceived):
    self.request_buffer[event.stream_id].buffered_data.append(event.data)
    
    if event.stream_ended:
    	self._stream_ended(event.stream_ended)
複製程式碼

Request 也可以帶有 Data,所以就會觸發這個事件。這裡request_buffer[event.stream_id]是一定不能觸發KeyError的,因為只有可能先接收 Headers,再接收 Data。如果有 KeyError,那麼八阿哥一定潛伏於某處。這裡stream_ended == True就說明 Request 完整接收了。

_stream_ended

def _stream_ended(self, event: StreamEnded):
    stream = self.request_buffer[event.stream_id]
    stream.stream_ended = True
    stream.data = b''.join(stream.buffered_data)
    stream.buffered_data = None
複製程式碼

當接收完一個 Request 資料流後,將Stream例項的狀態做一些調整。

_parse_request_buffer

這樣,我們就將所有資料都處理好了。現在的任務就是將緩衝掃描一遍,看有沒有指的返回的東西。

def _parse_request_buffer(self):
    """
    exercise all inbound streams
    """
    # This is a list of stream ids
    streams_to_delete_from_request_buffer = []
    
    # inbound_streams is a dictionary with schema {stream_id: stream_obj}
    # therefore use .values()
    for stream in self.request_buffer.values():
        if stream.stream_ended:
            # create a HTTP Request event, add it to current event list
            event = RequestEvent(stream)
            self.current_events.append(event)
            
            # Emitting an event means to clear the cached inbound data
            # The caller has to handle all returned events. Otherwise bad
            streams_to_delete_from_request_buffer.append(stream.stream_id)
    
    # clear the inbound cache
    for stream_id in streams_to_delete_from_request_buffer:
    	del self.request_buffer[stream_id]
複製程式碼

這裡的邏輯也簡單明瞭,檢查有沒有完整的 Request,有的話就構造一個完整的RequestEvent,然後將其放到self.current_events中。最後從緩衝中刪除相應的Stream

RequestEvent 類

RequestEvent定義如下:

# events.py
class H2Event:
    pass

class RequestEvent(H2Event):
    def __init__(self, stream):
    	self.stream = stream
複製程式碼

純粹為了程式碼可讀性而定義的。

仔細的同學可能會看到兩點:

  • _stream_ended中就可以完成這個函式中的所有操作,沒有必要再 loop 一遍浪費時間。
  • 如果非要再 loop 一遍,可以寫成函式式的,returncurrent_events,而不是更改物件的值。

完全正確,這裡我為了大家看得簡單明瞭,所以選擇了更簡潔,但是效率稍微慢一點的實現。

結語

到這裡你就實現了一個完全正確可用的 HTTP/2 伺服器端的接收功能。下一期就要實現傳送了。

視訊對文章進行補充,感興趣就去看看吧!程式碼在 GitHub,喜歡給個?唄!

程式碼

GitHub

視訊

B 站
油膩的管道(你留言我就上傳)

文章

上期
下期(還沒寫)

相關文章