Foreword

The previous article mainly introduced the purpose, operation mode and related use of .NET Core to inherit Kestrel. Next, we will further explore other aspects of Kestrel in .NET Core 3.0 from the source code. This part of the content, we do not need to master, still You can use Kestrel. This article just reveals some internal technical points for you and everyone to have a deeper understanding.

Kestrel provides support for HTTP 1.X and HTTP 2.0. There is a lot of content. From the trend point of view, Http2.0 has improved many defects of HTTP 1.X, so this article mainly focuses on Kestrel’s support for HTTP 2.0.

HTTP 2.0

Flow control

Before discussing flow control, let’s take a look at the overall structure of flow control:


DotNET_Core_3.0_source_-_understanding_of_Kestrel_integration_and_application(2)_0.png

 

Next, we discuss the flow control in detail, in which there is a structure implementation inside: FlowControl, FlowControl sets the amount of data that can be received or output at the time of initialization, and will dynamically control according to the input and output. After all, the resource is Limited, under the limitation of limited resources, it is necessary to flexibly handle the resource occupation of data packets. The call to the FlowControl.Advance method will make room, and FlowControl.TryUpdateWindow will take up space. The following is the source code of 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:       }
  twenty one:      
  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:   }

In the control flow, it mainly includes FlowControl and StreamFlowControl. StreamFlowControl depends on FlowControl ( Http2Stream refers to the read and write implementation of StreamFlowControl ). We know that in computer networks, both Flow and Stream refer to the concept of flow. Flow focuses on the two-way transmission of data between hosts or networks. Stream focuses on the conversation between pairs of IPs.

In FlowControl’s input and output control, OutFlowControl adds a reference to OutputFlowControlAwaitable and uses a queue.

The relevant use is as follows:

   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:   }

Head compression algorithm

The head compression algorithm involves moving/static tables, Huffman encoding/decoding, integer encoding/decoding, and so on.

The header field is maintained in the HeaderField. The source code is as follows:

   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:   }

The static table is implemented by StaticTable. It maintains a read-only HeaderField array. The dynamic table is implemented by DynamicTable. It can be regarded as a dynamic array implementation of HeaderField. Its initial size is input when instantiated and divided by 32 ( HeaderField .RfcOverhead ).

Huffman encoding/decoding and integer encoding/decoding are referenced by HPackDecoder and HPackEncoder.

HPackDecoder provides three public methods. These three methods will eventually call EncodeString for final encoding. At present, we can see that there is only integer encoding inside. I believe that Huffman encoding will be added in the future. The following is the EncodeString source. Friends can pay attention to the use of 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;
  twenty one:   
  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 has only one public method, Decode, but its internal implementation is very complex, it implements the processing, size control and multiplexing of different frames of the stream.

HTTP frame processing

We know that EndPoints can exchange frames after establishing an HTTP2.X connection. In .NET Core, there are mainly ten kinds of frame processing. In the code implementation, these ten kinds of frames are put into a large class, that is, Http2Frame. The .NET Core will preprocess it in a specific usage scenario. The main purpose is to determine the stream size, StreamId, the type of the frame, and the assignment of special attributes in a specific scene. (For the knowledge of HTTP frames, you can click on the 
link to

 view the detailed information.)
The Http2Frame source code is as follows:
   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:   }
The distinction between frame types allows .NET Core to better handle different frames, such as reading and writing.
The write function is mainly implemented in Http2FrameWriter. In addition to processing specific frames, it also includes updating packet size, completion, suspend, and refresh operations. Internally, lock is used to achieve thread safety. Some source code is as follows:
   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:           }
  twenty one:          
  22:           var bytesWritten = _unflushedBytes;
  23:           _unflushedBytes = 0;
  twenty four:   
  25:           return _flusher.FlushAsync(_minResponseDataRate, bytesWritten, outputAborter, cancellationToken);
  26:       }
  27:   }

The read function is mainly implemented by Http2FrameReader and has four constants inside, as shown below:

  • HeaderLength = 9: Header length
  • TypeOffset = 3: type offset
  • FlagsOffset = 4: Mark Offset
  • StreamIdOffset = 5: StreamId offset
  • SettingSize = 6: Id takes 2 bytes, and the value takes up 4 bytes.
In addition to the processing of different frame types, the internal method includes obtaining the payload length and reading the configuration information. The configuration information here mainly refers to the protocol default value, instead of the Kestrel default value.

The Http2PeerSettings implementation internally provides an Update method for updating configuration information.

In addition to this, it also includes Stream life cycle processing, error coding, connection control, etc., due to space limitations, no other instructions here, interested friends can view the source code.

Orignal link:https://www.cnblogs.com/edison0621/p/11186153.html