前言
http協議是互聯網上使用最廣泛的通訊協議了。Web通訊也是基于http協議;對應c#開發(fā)者來說ASP.NET Core是最新的開發(fā)Web應用平臺。
由于最近要開發(fā)一套人臉識別系統(tǒng),對通訊效率的要求很高。雖然.NET Core對http處理很優(yōu)化了,但是我決定開發(fā)一個輕量級http服務器;不求功能多強大,只求能滿足需求,性能優(yōu)越。本文以c#開發(fā)windows下http服務器為例。
經過多年的完善、優(yōu)化,我積累了一個非常高效的網絡庫《.NET中高性能、高可用性Socket通訊庫》以此庫為基礎,開發(fā)一套輕量級的http服務器難度并不大。花了兩天的時間完成http服務器開發(fā),并做了測試。
同時與ASP.NET Core處理效率做了對比,結果出乎意料。我的服務器性能是ASP.NET Core的10倍。對于此結果一開始我也是不相信,經過多次反復測試,事實卻是如此。此結果并不能說明我寫的服務器優(yōu)于ASP.NET Core,只是說明一個道理:合適的就是最好,高大上的東西并不是最好的。
1、HTTP協議特點

HTTP協議是基于TCP/IP之上的文本交換協議。對于開發(fā)者而言,也屬于socket通訊處理范疇。只是http協議是請求應答模式,一次請求處理完成,則立即斷開。http這種特點對sokcet通訊提出幾個要求:
a)、能迅速接受TCP連接請求。TCP是面向連接的,在建立連接時,需要三次握手。這就要求socket處理accept事件要迅速,要能短時間處理大量連接請求。
b)、服務端必須采用異步通訊模式。對windows而言,底層通訊就要采取IOCP,這樣才能應付成千上萬的socket請求。
c)、快速的處理讀取數據。tcp是流傳輸協議,而http傳輸的是文本協議;客戶端向服務端發(fā)送的數據,服務端可能需要讀取多次,服務端需要快速判斷數據是否讀取完畢。
以上幾點只是處理http必須要考慮的問題,如果需要進一步優(yōu)化,必須根據自身的業(yè)務特點來處理。
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、快速分析從客戶端收到的數據
比如客戶端發(fā)送1M數據到服務端,服務端收到1M數據,需要讀取的次數是不確定的。怎么樣才能知道數據是否讀取完?
這個細節(jié)處理不好,會嚴重影響服務器的性能。畢竟服務器要對大量這樣的數據進行分析。
http包頭舉例
POST / HTTP/1.1
Accept: */*
Content-Type: application/x-www-from-urlencoded
Host: www.163.com
Content-Length: 7
Connection: Keep-Alive
body
分析讀取數據,常規(guī)、直觀的處理方式如下:
1) 、將收到的多個buffer合并成一個buffer。如果讀取10次才完成,則需要合并9次。
2) 、將buffer數據轉成文本。
3) 、找到文本中的http包頭結束標識("\r\n\r\n") 。
4) 、找到Content-Length,根據此值判斷是否接收完成。
采用上述處理方法,將嚴重影響處理性能。必須另辟蹊徑,采用更優(yōu)化的處理方法。
優(yōu)化后的處理思路
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文本的解析。
public class HttpReadParse
{
BufferManage _bufferManage = new BufferManage();
public void AddBuffer(byte[] buffer)
{
_bufferManage.AddBuffer(buffer);
}
public int HeaderByteCount { get; private set; } = -1;
string _httpHeaderText = string.Empty;
public string HttpHeaderText
{
get
{
if (_httpHeaderText != string.Empty)
return _httpHeaderText;
if (!IsHttpHeadOver)
return _httpHeaderText;
byte[] buffer = _bufferManage.GetSubBuffer(0, HeaderByteCount);
_httpHeaderText = Encoding.UTF8.GetString(buffer);
return _httpHeaderText;
}
}
string _httpHeaderFirstLine = string.Empty;
public string HttpHeaderFirstLine
{
get
{
if (_httpHeaderFirstLine != string.Empty)
return _httpHeaderFirstLine;
if (HttpHeaderText == string.Empty)
return string.Empty;
int index = HttpHeaderText.IndexOf(HttpConst.Flag_Return);
if (index < 0)
return string.Empty;
_httpHeaderFirstLine = HttpHeaderText.Substring(0, index);
return _httpHeaderFirstLine;
}
}
public string HttpRequestUrl
{
get
{
if (HttpHeaderFirstLine == string.Empty)
return string.Empty;
string[] items = HttpHeaderFirstLine.Split(' ');
if (items.Length < 2)
return string.Empty;
return items[1];
}
}
public bool IsHttpHeadOver
{
get
{
if (HeaderByteCount > 0)
return true;
byte[] headOverFlag = HttpConst.Flag_DoubleReturnByte;
if (_bufferManage.FindBuffer(headOverFlag, out int count))
{
HeaderByteCount = count;
return true;
}
return false;
}
}
int _httpContentLen = -1;
public int HttpContentLen
{
get
{
if (_httpContentLen >= 0)
return _httpContentLen;
if (HttpHeaderText == string.Empty)
return -1;
int start = HttpHeaderText.IndexOf(HttpConst.Flag_HttpContentLenth);
if (start < 0) //http請求沒有包體
return 0;
start += HttpConst.Flag_HttpContentLenth.Length;
int end = HttpHeaderText.IndexOf(HttpConst.Flag_Return, start);
if (end < 0)
return -1;
string intValue = HttpHeaderText.Substring(start, end - start).Trim();
if (int.TryParse(intValue, out _httpContentLen))
return _httpContentLen;
return -1;
}
}
public string HttpAllText
{
get
{
byte[] textBytes = _bufferManage.GetAllByte();
string text = Encoding.UTF8.GetString(textBytes);
return text;
}
}
public int TotalByteLength => _bufferManage.TotalByteLength;
public bool IsReadEnd
{
get
{
if (!IsHttpHeadOver)
return false;
if (HttpContentLen == -1)
return false;
int shouldLenth = HeaderByteCount + HttpContentLen;
bool result = TotalByteLength >= shouldLenth;
return result;
}
}
public List<HttpByteValueKey> GetBodyParamBuffer()
{
List<HttpByteValueKey> result = new List<HttpByteValueKey>();
if (HttpContentLen < 0)
return result;
Debug.Assert(IsReadEnd);
if (HttpContentLen == 0)
return result;
byte[] bodyBytes = _bufferManage.GetSubBuffer(HeaderByteCount, HttpContentLen);
//獲取key value對應的byte
int start = 0;
int current = 0;
HttpByteValueKey item = null;
foreach (byte b in bodyBytes)
{
if (item == null)
item = new HttpByteValueKey();
current++;
if (b == '=')
{
byte[] buffer = new byte[current - start - 1];
Buffer.BlockCopy(bodyBytes, start, buffer, 0, buffer.Length);
item.Key = buffer;
start = current;
}
else if (b == '&')
{
byte[] buffer = new byte[current - start - 1];
Buffer.BlockCopy(bodyBytes, start, buffer, 0, buffer.Length);
item.Value = buffer;
start = current;
result.Add(item);
item = null;
}
}
if (item != null && item.Key != null)
{
byte[] buffer = new byte[bodyBytes.Length - start];
Buffer.BlockCopy(bodyBytes, start, buffer, 0, buffer.Length);
item.Value = buffer;
result.Add(item);
}
return result;
}
public string HttpBodyText
{
get
{
if (HttpContentLen < 0)
return string.Empty;
Debug.Assert(IsReadEnd);
if (HttpContentLen == 0)
return string.Empty;
byte[] bodyBytes = _bufferManage.GetSubBuffer(HeaderByteCount, HttpContentLen);
string bodyString = Encoding.UTF8.GetString(bodyBytes);
return bodyString;
}
}
}
4、性能測試
采用模擬客戶端持續(xù)發(fā)送http請求測試,每個http請求包含兩個圖片。一次http請求大概發(fā)送70K數據。服務端解析數據后,立即發(fā)送應答。
注:所有測試都在本機,客戶端無法模擬大量http請求,只能做簡單壓力測試。
1)本人所寫的服務器,測試結果如下

每秒可發(fā)送300次請求,每秒發(fā)送數據25M,服務器cpu占有率為4%。
2)ASP.NET Core 服務器性能測試

每秒發(fā)送30次請求,服務器cpu占有率為12%。
測試對比
本人開發(fā)的服務端處理速度為ASP.NET Core的10倍,cpu占用為對方的三分之一。ASP.NET Core處理慢,有可能實現了更多的功能;只是這些隱藏的功能,對我們也沒用。
后記
如果沒有開發(fā)經驗,沒有清晰的處理思路,開發(fā)一個高效的http服務器還有很困難的。
本人也一直以來都是采用ASP.NET Core作為http服務器。因為工作中需要高效的http服務器,就嘗試寫一個。
不可否認,ASP.NET Core各方面肯定優(yōu)化的很好;但是ASP.NET Core 提供的某些功能是多余的。如果化繁為簡,根據業(yè)務特點開發(fā),性能未必不能更優(yōu)。
該文章在 2025/7/2 16:58:15 編輯過