自己動手,開發輕量級,高性能http服務器。

  • 2019 年 10 月 3 日
  • 筆記

前言 http協議是互聯網上使用最廣泛的通訊協議了。web通訊也是基於http協議;對應c#開發者來說,asp.net core是最新的開發web應用平台。由於最近要開發一套人臉識別系統,對通訊效率的要求很高。雖然.net core對http處理很優化了,但是我決定開發一個輕量級http服務器;不求功能多強大,只求能滿足需求,性能優越。本文以c#開發windows下http服務器為例。

  經過多年的完善、優化,我積累了一個非常高效的網絡庫(參見我的文章:高性能通訊庫)。以此庫為基礎,開發一套輕量級的http服務器難度並不大。我花了兩天的時間完成http服務器開發,並做了測試。同時與asp.net core處理效率做了對比,結果出乎意料。我的服務器性能是asp.net core的10。對於此結果,一開始我也是不相信,經過多次反覆測試,事實卻是如此。此結果並不能說明我寫的服務器優於asp.net core,只是說明一個道理:合適的就是最好,高大上的東西並不是最好的。

 

1 HTTP協議特點

HTTP協議是基於TCP/IP之上的文本交換協議。對於開發者而言,也屬於socket通訊處理範疇。只是http協議是請求應答模式,一次請求處理完成,則立即斷開。http這種特點對sokcet通訊提出幾個要求:

a) 能迅速接受TCP連接請求。TCP是面向連接的,在建立連接時,需要三次握手。這就要求socket處理accept事件要迅速,要能短時間處理大量連接請求。

b) 服務端必須採用異步通訊模式。對windows而言,底層通訊就要採取IOCP,這樣才能應付成千上萬的socket請求。

c) 快速的處理讀取數據。tcp是流傳輸協議,而http傳輸的是文本協議;客戶端向服務端發送的數據,服務端可能需要讀取多次,服務端需要快速判斷數據是否讀取完畢。

以上幾點只是處理http必須要考慮的問題,如果需要進一步優化,必須根據自身的業務特點來處理。

 

 2 快速接受客戶端的連接請求

  採用異步Accept接受客戶端請求。這樣的好處是:可以同時投遞多個連接請求。當有大量客戶端請求時,能快速建立連接。

 異步連接請求代碼如下:

   public bool StartAccept()          {              SocketAsyncEventArgs acceptEventArgs = new SocketAsyncEventArgs();              acceptEventArgs.Completed += AcceptEventArg_Completed;                bool willRaiseEvent = listenSocket.AcceptAsync(acceptEventArgs);              Interlocked.Increment(ref _acceptAsyncCount);                if (!willRaiseEvent)              {                  Interlocked.Decrement(ref _acceptAsyncCount);                  _acceptEvent.Set();                  acceptEventArgs.Completed -= AcceptEventArg_Completed;                  ProcessAccept(acceptEventArgs);              }              return true;          }

可以設置同時投遞的個數,比如此值為10。當異步連接投遞個數小於10時,立馬再次增加投遞。有一個線程專門負責投遞。

_acceptAsyncCount記錄當前正在投遞的個數,MaxAcceptInPool表示同時投遞的個數;一旦_acceptAsyncCount小於MaxAcceptInPool,立即增加一次投遞。

 private void DealNewAccept()          {              try              {                  if (_acceptAsyncCount <= MaxAcceptInPool)                  {                      StartAccept();                  }              }              catch (Exception ex)              {                  _log.LogException(0, "DealNewAccept 異常", ex);              }          }

 

3 快速分析從客戶端收到的數據

比如客戶端發送1M數據到服務端,服務端收到1M數據,需要讀取的次數是不確定的。怎麼樣才能知道數據是否讀取完?

這個細節處理不好,會嚴重影響服務器的性能。畢竟服務器要對大量這樣的數據進行分析。

http包頭舉例

POST / HTTP/1.1  Accept: */*  Content-Type: application/x-www-from-urlencoded  Host: www.163.com  Content-Length: 7  Connection: Keep-Alive
body

分析讀取數據,常規、直觀的處理方式如下:

1) 將收到的多個buffer合併成一個buffer。如果讀取10次才完成,則需要合併9次。

2) 將buffer數據轉成文本。

3) 找到文本中的http包頭結束標識(“rnrn”) 。

4) 找到Content-Length,根據此值判斷是否接收完成。

採用上述處理方法,將嚴重影響處理性能。必須另闢蹊徑,採用更優化的處理方法。

優化後的處理思路

1)多緩衝處理

基本思路是:收到所有的buffer之前,不進行buffer合併。將緩衝存放在List<byte[]> listBuffer中。通過遍歷listBuffer來查找http包頭結束標識,來判斷是否接收完成。

類BufferManage負責管理buffer。

 public class BufferManage      {          List<byte[]> _listBuffer = new List<byte[]>();            public void AddBuffer(byte[] buffer)          {              _listBuffer.Add(buffer);          }            public bool FindBuffer(byte[] destBuffer, out int index)          {              index = -1;              int flagIndex = 0;                int count = 0;              foreach (byte[] buffer in _listBuffer)              {                  foreach (byte ch in buffer)                  {                      count++;                      if (ch == destBuffer[flagIndex])                      {                          flagIndex++;                      }                      else                      {                          flagIndex = 0;                      }                        if (flagIndex >= destBuffer.Length)                      {                          index = count;                          return true;                      }                  }              }                return false;          }            public int TotalByteLength          {              get              {                  int count = 0;                  foreach (byte[] item in _listBuffer)                  {                      count += item.Length;                  }                  return count;              }          }            public byte[] GetAllByte()          {              if (_listBuffer.Count == 0)                  return new byte[0];              if (_listBuffer.Count == 1)                  return _listBuffer[0];                int byteLen = 0;              _listBuffer.ForEach(o => byteLen += o.Length);              byte[] result = new byte[byteLen];                int index = 0;              foreach (byte[] item in _listBuffer)              {                  Buffer.BlockCopy(item, 0, result, index, item.Length);                  index += item.Length;              }              return result;          }            public byte[] GetSubBuffer(int start, int countTotal)          {              if (countTotal == 0)                  return new byte[0];                byte[] result = new byte[countTotal];              int countCopyed = 0;                int indexOfBufferPool = 0;              foreach (byte[] buffer in _listBuffer)              {                  //找到起始複製點                  int indexOfItem = 0;                  if (indexOfBufferPool < start)                  {                      int left = start - indexOfBufferPool;                      if (buffer.Length <= left)                      {                          indexOfBufferPool += buffer.Length;                          continue;                      }                      else                      {                          indexOfItem = left;                          indexOfBufferPool = start;                      }                  }                    //複製數據                  int dataLeft = buffer.Length - indexOfItem;                  int dataNeed = countTotal - countCopyed;                  if (dataNeed >= dataLeft)                  {                      Buffer.BlockCopy(buffer, indexOfItem, result, countCopyed, dataLeft);                      countCopyed += dataLeft;                  }                  else                  {                      Buffer.BlockCopy(buffer, indexOfItem, result, countCopyed, dataNeed);                      countCopyed += dataNeed;                  }                  if (countCopyed >= countTotal)                  {                      Debug.Assert(countCopyed == countTotal);                      return result;                  }              }              throw new Exception("沒有足夠的數據!");              // return result;          }      }

類HttpReadParse藉助BufferManage類,實現對http文本的解析。

  1   public class HttpReadParse    2     {    3    4         BufferManage _bufferManage = new BufferManage();    5    6         public void AddBuffer(byte[] buffer)    7         {    8             _bufferManage.AddBuffer(buffer);    9         }   10   11         public int HeaderByteCount { get; private set; } = -1;   12   13         string _httpHeaderText = string.Empty;   14         public string HttpHeaderText   15         {   16             get   17             {   18                 if (_httpHeaderText != string.Empty)   19                     return _httpHeaderText;   20   21                 if (!IsHttpHeadOver)   22                     return _httpHeaderText;   23   24                 byte[] buffer = _bufferManage.GetSubBuffer(0, HeaderByteCount);   25                 _httpHeaderText = Encoding.UTF8.GetString(buffer);   26                 return _httpHeaderText;   27             }   28         }   29   30         string _httpHeaderFirstLine = string.Empty;   31         public string HttpHeaderFirstLine   32         {   33             get   34             {   35                 if (_httpHeaderFirstLine != string.Empty)   36                     return _httpHeaderFirstLine;   37   38                 if (HttpHeaderText == string.Empty)   39                     return string.Empty;   40                 int index = HttpHeaderText.IndexOf(HttpConst.Flag_Return);   41                 if (index < 0)   42                     return string.Empty;   43   44                 _httpHeaderFirstLine = HttpHeaderText.Substring(0, index);   45                 return _httpHeaderFirstLine;   46             }   47         }   48   49         public string HttpRequestUrl   50         {   51             get   52             {   53                 if (HttpHeaderFirstLine == string.Empty)   54                     return string.Empty;   55   56                 string[] items = HttpHeaderFirstLine.Split(' ');   57                 if (items.Length < 2)   58                     return string.Empty;   59   60                 return items[1];   61             }   62         }   63   64         public bool IsHttpHeadOver   65         {   66             get   67             {   68                 if (HeaderByteCount > 0)   69                     return true;   70   71                 byte[] headOverFlag = HttpConst.Flag_DoubleReturnByte;   72   73                 if (_bufferManage.FindBuffer(headOverFlag, out int count))   74                 {   75                     HeaderByteCount = count;   76                     return true;   77                 }   78                 return false;   79             }   80         }   81   82         int _httpContentLen = -1;   83         public int HttpContentLen   84         {   85             get   86             {   87                 if (_httpContentLen >= 0)   88                     return _httpContentLen;   89   90                 if (HttpHeaderText == string.Empty)   91                     return -1;   92   93                 int start = HttpHeaderText.IndexOf(HttpConst.Flag_HttpContentLenth);   94                 if (start < 0) //http請求沒有包體   95                     return 0;   96   97                 start += HttpConst.Flag_HttpContentLenth.Length;   98   99                 int end = HttpHeaderText.IndexOf(HttpConst.Flag_Return, start);  100                 if (end < 0)  101                     return -1;  102  103                 string intValue = HttpHeaderText.Substring(start, end - start).Trim();  104                 if (int.TryParse(intValue, out _httpContentLen))  105                     return _httpContentLen;  106                 return -1;  107             }  108         }  109  110         public string HttpAllText  111         {  112             get  113             {  114                 byte[] textBytes = _bufferManage.GetAllByte();  115                 string text = Encoding.UTF8.GetString(textBytes);  116                 return text;  117             }  118         }  119  120         public int TotalByteLength => _bufferManage.TotalByteLength;  121  122         public bool IsReadEnd  123         {  124             get  125             {  126                 if (!IsHttpHeadOver)  127                     return false;  128  129                 if (HttpContentLen == -1)  130                     return false;  131  132                 int shouldLenth = HeaderByteCount + HttpContentLen;  133                 bool result = TotalByteLength >= shouldLenth;  134                 return result;  135             }  136         }  137  138         public List<HttpByteValueKey> GetBodyParamBuffer()  139         {  140             List<HttpByteValueKey> result = new List<HttpByteValueKey>();  141  142             if (HttpContentLen < 0)  143                 return result;  144             Debug.Assert(IsReadEnd);  145  146             if (HttpContentLen == 0)  147                 return result;  148  149             byte[] bodyBytes = _bufferManage.GetSubBuffer(HeaderByteCount, HttpContentLen);  150  151             //獲取key value對應的byte  152             int start = 0;  153             int current = 0;  154             HttpByteValueKey item = null;  155             foreach (byte b in bodyBytes)  156             {  157                 if (item == null)  158                     item = new HttpByteValueKey();  159  160                 current++;  161                 if (b == '=')  162                 {  163                     byte[] buffer = new byte[current - start - 1];  164                     Buffer.BlockCopy(bodyBytes, start, buffer, 0, buffer.Length);  165                     item.Key = buffer;  166                     start = current;  167                 }  168                 else if (b == '&')  169                 {  170                     byte[] buffer = new byte[current - start - 1];  171                     Buffer.BlockCopy(bodyBytes, start, buffer, 0, buffer.Length);  172                     item.Value = buffer;  173                     start = current;  174                     result.Add(item);  175                     item = null;  176                 }  177             }  178  179             if (item != null && item.Key != null)  180             {  181                 byte[] buffer = new byte[bodyBytes.Length - start];  182                 Buffer.BlockCopy(bodyBytes, start, buffer, 0, buffer.Length);  183                 item.Value = buffer;  184                 result.Add(item);  185             }  186  187             return result;  188         }  189  190         public string HttpBodyText  191         {  192             get  193             {  194                 if (HttpContentLen < 0)  195                     return string.Empty;  196                 Debug.Assert(IsReadEnd);  197  198                 if (HttpContentLen == 0)  199                     return string.Empty;  200  201                 byte[] bodyBytes = _bufferManage.GetSubBuffer(HeaderByteCount, HttpContentLen);  202                 string bodyString = Encoding.UTF8.GetString(bodyBytes);  203                 return bodyString;  204             }  205         }  206  207     }

4 性能測試

採用模擬客戶端持續發送http請求測試,每個http請求包含兩個圖片。一次http請求大概發送70K數據。服務端解析數據後,立即發送應答。

註:所有測試都在本機,客戶端無法模擬大量http請求,只能做簡單壓力測試。

1)本人所寫的服務器,測試結果如下

 每秒可發送300次請求,每秒發送數據25M,服務器cpu佔有率為4%。

2)asp.net core 服務器性能測試

 

每秒發送30次請求,服務器cpu佔有率為12%。

測試對比:本人開發的服務端處理速度為asp.net core的10倍,cpu佔用為對方的三分之一。asp.net core處理慢,有可能實現了更多的功能;只是這些隱藏的功能,對我們也沒用。

後記: 如果沒有開發經驗,沒有清晰的處理思路,開發一個高效的http服務器還有很困難的。本人也一直以來都是採用asp.net core作為http服務器。因為工作中需要高效的http服務器,就嘗試寫一個。不可否認,asp.net core各方面肯定優化的很好;但是,asp.net core 提供的某些功能是多餘的。如果化繁為簡,根據業務特點開發,性能未必不能更優。