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

HTTP協(xié)議是基于TCP/IP之上的文本交換協(xié)議。對(duì)于開發(fā)者而言,也屬于socket通訊處理范疇。只是http協(xié)議是請(qǐng)求應(yīng)答模式,一次請(qǐng)求處理完成,則立即斷開。http這種特點(diǎn)對(duì)sokcet通訊提出幾個(gè)要求:
a)、能迅速接受TCP連接請(qǐng)求。TCP是面向連接的,在建立連接時(shí),需要三次握手。這就要求socket處理accept事件要迅速,要能短時(shí)間處理大量連接請(qǐng)求。
b)、服務(wù)端必須采用異步通訊模式。對(duì)windows而言,底層通訊就要采取IOCP,這樣才能應(yīng)付成千上萬的socket請(qǐng)求。
c)、快速的處理讀取數(shù)據(jù)。tcp是流傳輸協(xié)議,而http傳輸?shù)氖俏谋緟f(xié)議;客戶端向服務(wù)端發(fā)送的數(shù)據(jù),服務(wù)端可能需要讀取多次,服務(wù)端需要快速判斷數(shù)據(jù)是否讀取完畢。
以上幾點(diǎn)只是處理http必須要考慮的問題,如果需要進(jìn)一步優(yōu)化,必須根據(jù)自身的業(yè)務(wù)特點(diǎn)來處理。
2、快速接受客戶端的連接請(qǐng)求
采用異步Accept接受客戶端請(qǐng)求。這樣的好處是:可以同時(shí)投遞多個(gè)連接請(qǐng)求。當(dāng)有大量客戶端請(qǐng)求時(shí),能快速建立連接。
異步連接請(qǐng)求代碼如下:
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;
}
可以設(shè)置同時(shí)投遞的個(gè)數(shù),比如此值為10。當(dāng)異步連接投遞個(gè)數(shù)小于10時(shí),立馬再次增加投遞。有一個(gè)線程專門負(fù)責(zé)投遞。
_acceptAsyncCount記錄當(dāng)前正在投遞的個(gè)數(shù),MaxAcceptInPool表示同時(shí)投遞的個(gè)數(shù);一旦_acceptAsyncCount小于MaxAcceptInPool,立即增加一次投遞。
private void DealNewAccept()
{
try
{
if (_acceptAsyncCount <= MaxAcceptInPool)
{
StartAccept();
}
}
catch (Exception ex)
{
_log.LogException(0, "DealNewAccept 異常", ex);
}
}
3、快速分析從客戶端收到的數(shù)據(jù)
比如客戶端發(fā)送1M數(shù)據(jù)到服務(wù)端,服務(wù)端收到1M數(shù)據(jù),需要讀取的次數(shù)是不確定的。怎么樣才能知道數(shù)據(jù)是否讀取完?
這個(gè)細(xì)節(jié)處理不好,會(huì)嚴(yán)重影響服務(wù)器的性能。畢竟服務(wù)器要對(duì)大量這樣的數(shù)據(jù)進(jìn)行分析。
http包頭舉例
POST / HTTP/1.1
Accept: */*
Content-Type: application/x-www-from-urlencoded
Host: www.163.com
Content-Length: 7
Connection: Keep-Alive
body
分析讀取數(shù)據(jù),常規(guī)、直觀的處理方式如下:
1) 、將收到的多個(gè)buffer合并成一個(gè)buffer。如果讀取10次才完成,則需要合并9次。
2) 、將buffer數(shù)據(jù)轉(zhuǎn)成文本。
3) 、找到文本中的http包頭結(jié)束標(biāo)識(shí)("\r\n\r\n") 。
4) 、找到Content-Length,根據(jù)此值判斷是否接收完成。
采用上述處理方法,將嚴(yán)重影響處理性能。必須另辟蹊徑,采用更優(yōu)化的處理方法。
優(yōu)化后的處理思路
1、多緩沖處理
基本思路是:收到所有的buffer之前,不進(jìn)行buffer合并。將緩沖存放在List<byte[]> listBuffer中。通過遍歷listBuffer來查找http包頭結(jié)束標(biāo)識(shí),來判斷是否接收完成。
類BufferManage負(fù)責(zé)管理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)
{
//找到起始復(fù)制點(diǎn)
int indexOfItem = 0;
if (indexOfBufferPool < start)
{
int left = start - indexOfBufferPool;
if (buffer.Length <= left)
{
indexOfBufferPool += buffer.Length;
continue;
}
else
{
indexOfItem = left;
indexOfBufferPool = start;
}
}
//復(fù)制數(shù)據(jù)
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("沒有足夠的數(shù)據(jù)!");
// return result;
}
}
類HttpReadParse借助BufferManage類,實(shí)現(xiàn)對(duì)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請(qǐng)求沒有包體
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對(duì)應(yīng)的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、性能測(cè)試
采用模擬客戶端持續(xù)發(fā)送http請(qǐng)求測(cè)試,每個(gè)http請(qǐng)求包含兩個(gè)圖片。一次http請(qǐng)求大概發(fā)送70K數(shù)據(jù)。服務(wù)端解析數(shù)據(jù)后,立即發(fā)送應(yīng)答。
注:所有測(cè)試都在本機(jī),客戶端無法模擬大量http請(qǐng)求,只能做簡(jiǎn)單壓力測(cè)試。
1)本人所寫的服務(wù)器,測(cè)試結(jié)果如下

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

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