.NET Core 3.0之深入原始碼理解Kestrel的整合與應用(二)

艾心❤發表於2019-07-15
 

前言

前一篇文章主要介紹了.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生命週期處理、錯誤編碼、連線控制等,限於篇幅此處不做其他說明,有興趣的朋友可以自己檢視原始碼。

相關文章