前言

前一篇文章主要介绍了.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

流控制

在讨论流控制之前,我们先看一下流控制的整体结构图:


<https://img2018.cnblogs.com/blog/533598/201907/533598-20190714222137763-15532691.png>


接下来,我们详细讨论一下流控制,其中内部有一个结构体的实现: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帧的知识点,大家可以点击
链接 <https://tools.ietf.org/html/rfc7540>查看详细的信息。) 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生命周期处理、错误编码、连接控制等,限于篇幅此处不做其他说明,有兴趣的朋友可以自己查看源代码。