前言
前一篇文章主要介紹了.NET Core繼承Kestrel的目的、執行方式以及相關的使用,接下來將進一步從原始碼角度探討.NET Core 3.0中關於Kestrel的其他內容,該部分內容,我們無需掌握,依然可以用好Kestrel,本文只是將一些內部的技術點揭露出來,供自己及大家有一個較深的認識。
Kestrel提供了HTTP 1.X及HTTP 2.0的支援,內容比較多,從趨勢上看,Http2.0針對HTTP 1.X的眾多缺陷進行了改進,所以這篇文章主要關注Kestrel對HTTP 2.0的支援。
HTTP 2.X
流控制
在討論流控制之前,我們先看一下流控制的整體結構圖:
接下來,我們詳細討論一下流控制,其中內部有一個結構體的實現:FlowControl,FlowControl在初始化的時候設定了所能接收或者輸出的資料量大小,並會根據輸入出入進行動態控制,畢竟資源是有限的,在有限資源的限制下,需要靈活處理資料包對資源的佔用。FlowControl.Advance方法的呼叫會騰出空間,FlowControl.TryUpdateWindow會佔用空間,以下是FlowControl的原始碼:
1: internal struct FlowControl
2: {
3: public FlowControl(uint initialWindowSize)
4: {
5: Debug.Assert(initialWindowSize <= Http2PeerSettings.MaxWindowSize, $"{nameof(initialWindowSize)} too large.");
6:
7: Available = (int)initialWindowSize;
8: IsAborted = false;
9: }
10:
11: public int Available { get; private set; }
12: public bool IsAborted { get; private set; }
13:
14: public void Advance(int bytes)
15: {
16: Debug.Assert(!IsAborted, $"({nameof(Advance)} called after abort.");
17: Debug.Assert(bytes == 0 || (bytes > 0 && bytes <= Available), $"{nameof(Advance)}({bytes}) called with {Available} bytes available.");
18:
19: Available -= bytes;
20: }
21:
22: public bool TryUpdateWindow(int bytes)
23: {
24: var maxUpdate = Http2PeerSettings.MaxWindowSize - Available;
25:
26: if (bytes > maxUpdate)
27: {
28: return false;
29: }
30:
31: Available += bytes;
32:
33: return true;
34: }
35:
36: public void Abort()
37: {
38: IsAborted = true;
39: }
40: }
在控制流中,主要包括FlowControl和StreamFlowControl,StreamFlowControl依賴於FlowControl(Http2Stream引用了StreamFlowControl的讀寫實現)。我們知道,在計算機網路中,Flow和Stream都是指流的概念,Flow側重於主機或者網路之間的雙向傳輸的資料包,Stream側重於成對的IP之間的會話。
在FlowControl的輸入輸出控制中,OutFlowControl增加了對OutputFlowControlAwaitable的引用,並採用了佇列的方式。
相關使用如下:
1: public OutputFlowControlAwaitable AvailabilityAwaitable
2: {
3: get
4: {
5: Debug.Assert(!_flow.IsAborted, $"({nameof(AvailabilityAwaitable)} accessed after abort.");
6: Debug.Assert(_flow.Available <= 0, $"({nameof(AvailabilityAwaitable)} accessed with {Available} bytes available.");
7:
8: if (_awaitableQueue == null)
9: {
10: _awaitableQueue = new Queue<OutputFlowControlAwaitable>();
11: }
12:
13: var awaitable = new OutputFlowControlAwaitable();
14: _awaitableQueue.Enqueue(awaitable);
15: return awaitable;
16: }
17: }
頭部壓縮演算法
頭部壓縮演算法這塊涉及到動/靜態表、哈夫曼編/解碼、整型編/解碼等。
頭部欄位維護在HeaderField中,原始碼如下:
1: internal readonly struct HeaderField
2: {
3: public const int RfcOverhead = 32;
4:
5: public HeaderField(Span<byte> name, Span<byte> value)
6: {
7: Name = new byte[name.Length];
8: name.CopyTo(Name);
9:
10: Value = new byte[value.Length];
11: value.CopyTo(Value);
12: }
13:
14: public byte[] Name { get; }
15:
16: public byte[] Value { get; }
17:
18: public int Length => GetLength(Name.Length, Value.Length);
19:
20: public static int GetLength(int nameLength, int valueLength) => nameLength + valueLength + 32;
21: }
靜態表由StaticTable實現,內部維護了一個只讀的HeaderField陣列,動態表由DynamicTable實現,可以視為是HeaderField的一個動態陣列的實現,其初始大小在例項化的時候輸入,併除以32(HeaderField.RfcOverhead)。
哈夫曼編/解碼和整型編/解碼會被HPackDecoder和HPackEncoder引用。
HPackDecoder提供了三個公共方法,這三個方法最終都會呼叫EncodeString進行最終的編碼,目前可以看到其內部只有整形編碼,我相信在未來會增加哈夫曼編碼,以下是EncodeString原始碼(有興趣的朋友可以關注下Span<>的使用):
1: private bool EncodeString(string s, Span<byte> buffer, out int length, bool lowercase)
2: {
3: const int toLowerMask = 0x20;
4:
5: var i = 0;
6: length = 0;
7:
8: if (buffer.Length == 0)
9: {
10: return false;
11: }
12:
13: buffer[0] = 0;
14:
15: if (!IntegerEncoder.Encode(s.Length, 7, buffer, out var nameLength))
16: {
17: return false;
18: }
19:
20: i += nameLength;
21:
22: for (var j = 0; j < s.Length; j++)
23: {
24: if (i >= buffer.Length)
25: {
26: return false;
27: }
28:
29: buffer[i++] = (byte)(s[j] | (lowercase && s[j] >= (byte)'A' && s[j] <= (byte)'Z' ? toLowerMask : 0));
30: }
31:
32: length = i;
33: return true;
34: }
HPackEncoder只有一個公共方法Decode,不過其內部實現非常複雜,它實現了流的不同幀的處理、大小的控制以及多路複用。
HTTP幀處理
我們知道,在建立HTTP2.X連線後,EndPoints就可以交換幀了。.NET Core中,主要有十種幀的處理,程式碼實現上,將這十種幀放到了一個大的類中,也就是Http2Frame,.NET Core在具體的使用場景中會對其進行一次預處理,主要是為了確定流大小、StreamId、幀的型別以及特定場景下的特殊屬性的賦值。(關於HTTP幀的知識點,大家可以點選連結檢視詳細的資訊。)
Http2Frame原始碼如下:
1: internal enum Http2FrameType : byte
2: {
3: DATA = 0x0,
4: HEADERS = 0x1,
5: PRIORITY = 0x2,
6: RST_STREAM = 0x3,
7: SETTINGS = 0x4,
8: PUSH_PROMISE = 0x5,
9: PING = 0x6,
10: GOAWAY = 0x7,
11: WINDOW_UPDATE = 0x8,
12: CONTINUATION = 0x9
13: }
幀型別的區分,可以使得.NET Core更好的處理不同的幀,比如讀取和寫入。
寫入功能主要在Http2FrameWriter中實現,內部除了對特定幀的處理外,還包括更新資料包大小、完成、掛起以及重新整理操作,內部都用到了lock以實現執行緒安全。部分原始碼如下:
1: public void UpdateMaxFrameSize(uint maxFrameSize)
2: {
3: lock (_writeLock)
4: {
5: if (_maxFrameSize != maxFrameSize)
6: {
7: _maxFrameSize = maxFrameSize;
8: _headerEncodingBuffer = new byte[_maxFrameSize];
9: }
10: }
11: }
12:
13: public ValueTask<FlushResult> FlushAsync(IHttpOutputAborter outputAborter, CancellationToken cancellationToken)
14: {
15: lock (_writeLock)
16: {
17: if (_completed)
18: {
19: return default;
20: }
21:
22: var bytesWritten = _unflushedBytes;
23: _unflushedBytes = 0;
24:
25: return _flusher.FlushAsync(_minResponseDataRate, bytesWritten, outputAborter, cancellationToken);
26: }
27: }
讀取功能主要由Http2FrameReader實現,內部有四個常數,如下所示:
- HeaderLength = 9:Header長度
- TypeOffset = 3:型別偏移量
- FlagsOffset = 4:標記偏移量
- StreamIdOffset = 5:StreamId偏移量
- SettingSize = 6:Id佔用2 bytes, 值佔用了4 bytes
其內部方法除了有不同幀型別的處理外,還包括獲取有效負荷長度、讀取配置資訊,這裡的配置資訊主要指的是協議預設值,而不是Kestrel預設值,該功能由
Http2PeerSettings實現,內部提供了一個Update方法用於更新配置資訊。
除此以外還包括Stream生命週期處理、錯誤編碼、連線控制等,限於篇幅此處不做其他說明,有興趣的朋友可以自己檢視原始碼。