前言
前一篇文章主要介绍了. NET Core 继承 Kestrel 的目的, 运行方式以及相关的使用, 接下来将进一步从源码角度探讨. NET Core 3.0 中关于 Kestrel 的其他内容, 该部分内容, 我们无需掌握, 依然可以用好 Kestrel, 本文只是将一些内部的技术点揭露出来, 供自己及大家有一个较深的认识.
Kestrel 提供了 HTTP 1.X 及 HTTP 2.X 的支持, 内容比较多, 从趋势上看, Http2.X 针对 HTTP 1.X 的众多缺陷进行了改进, 所以这篇文章主要关注 Kestrel 对 HTTP 2.X 的支持.
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 中, 源码如下:
- internal readonly struct HeaderField
- {
- public const int RfcOverhead = 32;
- public HeaderField(Span<byte> name, Span<byte> value)
- {
- Name = new byte[name.Length];
- name.CopyTo(Name);
- Value = new byte[value.Length];
- value.CopyTo(Value);
- } 13:
- public byte[] Name { get; }
- public byte[] Value { get; }
- public int Length => GetLength(Name.Length, Value.Length);
- public static int GetLength(int nameLength, int valueLength) => nameLength + valueLength + 32;
- }
静态表由 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 源码如下:
- internal enum Http2FrameType : byte
- {
- DATA = 0x0,
- HEADERS = 0x1,
- PRIORITY = 0x2,
- RST_STREAM = 0x3,
- SETTINGS = 0x4,
- PUSH_PROMISE = 0x5,
- PING = 0x6,
- GOAWAY = 0x7,
- WINDOW_UPDATE = 0x8,
- CONTINUATION = 0x9
- }
帧类型的区分, 可以使得. NET Core 更好的处理不同的帧, 比如读取和写入.
写入功能主要在 Http2FrameWriter 中实现, 内部除了对特定帧的处理外, 还包括更新数据包大小, 完成, 挂起以及刷新操作, 内部都用到了 lock 以实现线程安全. 部分源码如下:
- public void UpdateMaxFrameSize(uint maxFrameSize)
- {
- lock (_writeLock)
- {
- if (_maxFrameSize != maxFrameSize)
- {
- _maxFrameSize = maxFrameSize;
- _headerEncodingBuffer = new byte[_maxFrameSize];
- } 10: } 11: } 12:
- public ValueTask<FlushResult> FlushAsync(IHttpOutputAborter outputAborter, CancellationToken cancellationToken)
- {
- lock (_writeLock)
- {
- if (_completed)
- {
- return default;
- } 21:
- var bytesWritten = _unflushedBytes;
- _unflushedBytes = 0;
- return _flusher.FlushAsync(_minResponseDataRate, bytesWritten, outputAborter, cancellationToken);
- } 27: }
读取功能主要由 Http2FrameReader 实现, 内部有四个常数, 如下所示:
HeaderLength = 9:Header 长度
TypeOffset = 3: 类型偏移量
FlagsOffset = 4: 标记偏移量
StreamIdOffset = 5:StreamId 偏移量
SettingSize = 6:Id 占用 2 bytes, 值占用了 4 bytes
其内部方法除了有不同帧类型的处理外, 还包括获取有效负荷长度, 读取配置信息, 这里的配置信息主要指的是协议默认值, 而不是 Kestrel 默认值, 该功能由
Http2PeerSettings 实现, 内部提供了一个 Update 方法用于更新配置信息.
除此以外还包括 Stream 生命周期处理, 错误编码, 连接控制等, 限于篇幅此处不做其他说明, 有兴趣的朋友可以自己查看源代码.
来源: https://www.cnblogs.com/edison0621/p/11186153.html