首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >.NET Core 3.0之深入源码理解Kestrel的集成与应用(二)

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

作者头像
Edison.Ma
发布2019-07-19 18:26:24
6590
发布2019-07-19 18:26:24
举报
文章被收录于专栏:DotNet Core圈圈DotNet Core圈圈

前言

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet技术平台 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • HTTP 2.X
    • 流控制
      • 头部压缩算法
        • HTTP帧处理
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档