前言
前一篇文章主要介绍了.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.0
流控制
在讨论流控制之前,我们先看一下流控制的整体结构图:
接下来,我们详细讨论一下流控制,其中内部有一个结构体的实现: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();
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 ValueTaskFlushAsync(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生命周期处理、错误编码、连接控制等,限于篇幅此处不做其他说明,有兴趣的朋友可以自己查看源代码。