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