.NET Core 3.0之深入源码理解Kestrel的集成与应用(二)

 

前言

前一篇文章主要介绍了.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<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生命周期处理、错误编码、连接控制等,限于篇幅此处不做其他说明,有兴趣的朋友可以自己查看源代码。

posted @ 2019-07-15 09:26 艾心❤ 阅读(...) 评论(...) 编辑 收藏