1. 程式人生 > >自己動手,開發輕量級,高效能http伺服器。

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

前言 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包頭結束標識("\r\n\r\n") 。

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 提供的某些功能是多餘的。如果化繁為簡,根據業務特點開發,效能未必不能更