当前位置:   article > 正文

C#开发轻量级 高性能HTTP服务器

c# http服务器

转自:源之缘

cnblogs.com/yuanchenhui/p/httpserver.html

前言 

http协议是互联网上使用最广泛的通讯协议了。Web通讯也是基于http协议;对应c#开发者来说ASP.NET Core是最新的开发Web应用平台。

由于最近要开发一套人脸识别系统,对通讯效率的要求很高。虽然.NET Core对http处理很优化了,但是我决定开发一个轻量级http服务器;不求功能多强大,只求能满足需求,性能优越。本文以c#开发windows下http服务器为例。

经过多年的完善、优化,我积累了一个非常高效的网络库《.NET中高性能、高可用性Socket通讯库》以此库为基础,开发一套轻量级的http服务器难度并不大。花了两天的时间完成http服务器开发,并做了测试。

同时与ASP.NET Core处理效率做了对比,结果出乎意料。我的服务器性能是ASP.NET Core的10倍。对于此结果一开始我也是不相信,经过多次反复测试,事实却是如此。此结果并不能说明我写的服务器优于ASP.NET Core,只是说明一个道理:合适的就是最好,高大上的东西并不是最好的。

1、HTTP协议特点

55fc1f371700286909babb7d2f0cc4f9.jpeg

HTTP协议是基于TCP/IP之上的文本交换协议。对于开发者而言,也属于socket通讯处理范畴。只是http协议是请求应答模式,一次请求处理完成,则立即断开。http这种特点对sokcet通讯提出几个要求:

a)、能迅速接受TCP连接请求。TCP是面向连接的,在建立连接时,需要三次握手。这就要求socket处理accept事件要迅速,要能短时间处理大量连接请求。

b)、服务端必须采用异步通讯模式。对windows而言,底层通讯就要采取IOCP,这样才能应付成千上万的socket请求。

c)、快速的处理读取数据。tcp是流传输协议,而http传输的是文本协议;客户端向服务端发送的数据,服务端可能需要读取多次,服务端需要快速判断数据是否读取完毕。

以上几点只是处理http必须要考虑的问题,如果需要进一步优化,必须根据自身的业务特点来处理。

2、快速接受客户端的连接请求

采用异步Accept接受客户端请求。这样的好处是:可以同时投递多个连接请求。当有大量客户端请求时,能快速建立连接。

异步连接请求代码如下:

  1. public bool StartAccept()
  2. {
  3. SocketAsyncEventArgs acceptEventArgs = new SocketAsyncEventArgs();
  4. acceptEventArgs.Completed += AcceptEventArg_Completed;
  5. bool willRaiseEvent = listenSocket.AcceptAsync(acceptEventArgs);
  6. Interlocked.Increment(ref _acceptAsyncCount);
  7. if (!willRaiseEvent)
  8. {
  9. Interlocked.Decrement(ref _acceptAsyncCount);
  10. _acceptEvent.Set();
  11. acceptEventArgs.Completed -= AcceptEventArg_Completed;
  12. ProcessAccept(acceptEventArgs);
  13. }
  14. return true;
  15. }

可以设置同时投递的个数,比如此值为10。当异步连接投递个数小于10时,立马再次增加投递。有一个线程专门负责投递。

_acceptAsyncCount记录当前正在投递的个数,MaxAcceptInPool表示同时投递的个数;一旦_acceptAsyncCount小于MaxAcceptInPool,立即增加一次投递。

  1. private void DealNewAccept()
  2. {
  3. try
  4. {
  5. if (_acceptAsyncCount <= MaxAcceptInPool)
  6. {
  7. StartAccept();
  8. }
  9. }
  10. catch (Exception ex)
  11. {
  12. _log.LogException(0, "DealNewAccept 异常", ex);
  13. }
  14. }

3、快速分析从客户端收到的数据

比如客户端发送1M数据到服务端,服务端收到1M数据,需要读取的次数是不确定的。怎么样才能知道数据是否读取完?

这个细节处理不好,会严重影响服务器的性能。毕竟服务器要对大量这样的数据进行分析。

http包头举例

  1. POST / HTTP/1.1
  2. Accept: */*
  3. Content-Type: application/x-www-from-urlencoded
  4. Host: www.163.com
  5. Content-Length: 7
  6. Connection: Keep-Alive
  7. 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。

  1. public class BufferManage
  2. {
  3. List<byte[]> _listBuffer = new List<byte[]>();
  4. public void AddBuffer(byte[] buffer)
  5. {
  6. _listBuffer.Add(buffer);
  7. }
  8. public bool FindBuffer(byte[] destBuffer, out int index)
  9. {
  10. index = -1;
  11. int flagIndex = 0;
  12. int count = 0;
  13. foreach (byte[] buffer in _listBuffer)
  14. {
  15. foreach (byte ch in buffer)
  16. {
  17. count++;
  18. if (ch == destBuffer[flagIndex])
  19. {
  20. flagIndex++;
  21. }
  22. else
  23. {
  24. flagIndex = 0;
  25. }
  26. if (flagIndex >= destBuffer.Length)
  27. {
  28. index = count;
  29. return true;
  30. }
  31. }
  32. }
  33. return false;
  34. }
  35. public int TotalByteLength
  36. {
  37. get
  38. {
  39. int count = 0;
  40. foreach (byte[] item in _listBuffer)
  41. {
  42. count += item.Length;
  43. }
  44. return count;
  45. }
  46. }
  47. public byte[] GetAllByte()
  48. {
  49. if (_listBuffer.Count == 0)
  50. return new byte[0];
  51. if (_listBuffer.Count == 1)
  52. return _listBuffer[0];
  53. int byteLen = 0;
  54. _listBuffer.ForEach(o => byteLen += o.Length);
  55. byte[] result = new byte[byteLen];
  56. int index = 0;
  57. foreach (byte[] item in _listBuffer)
  58. {
  59. Buffer.BlockCopy(item, 0, result, index, item.Length);
  60. index += item.Length;
  61. }
  62. return result;
  63. }
  64. public byte[] GetSubBuffer(int start, int countTotal)
  65. {
  66. if (countTotal == 0)
  67. return new byte[0];
  68. byte[] result = new byte[countTotal];
  69. int countCopyed = 0;
  70. int indexOfBufferPool = 0;
  71. foreach (byte[] buffer in _listBuffer)
  72. {
  73. //找到起始复制点
  74. int indexOfItem = 0;
  75. if (indexOfBufferPool < start)
  76. {
  77. int left = start - indexOfBufferPool;
  78. if (buffer.Length <= left)
  79. {
  80. indexOfBufferPool += buffer.Length;
  81. continue;
  82. }
  83. else
  84. {
  85. indexOfItem = left;
  86. indexOfBufferPool = start;
  87. }
  88. }
  89. //复制数据
  90. int dataLeft = buffer.Length - indexOfItem;
  91. int dataNeed = countTotal - countCopyed;
  92. if (dataNeed >= dataLeft)
  93. {
  94. Buffer.BlockCopy(buffer, indexOfItem, result, countCopyed, dataLeft);
  95. countCopyed += dataLeft;
  96. }
  97. else
  98. {
  99. Buffer.BlockCopy(buffer, indexOfItem, result, countCopyed, dataNeed);
  100. countCopyed += dataNeed;
  101. }
  102. if (countCopyed >= countTotal)
  103. {
  104. Debug.Assert(countCopyed == countTotal);
  105. return result;
  106. }
  107. }
  108. throw new Exception("没有足够的数据!");
  109. // return result;
  110. }
  111. }

类HttpReadParse借助BufferManage类,实现对http文本的解析。

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

4、性能测试

采用模拟客户端持续发送http请求测试,每个http请求包含两个图片。一次http请求大概发送70K数据。服务端解析数据后,立即发送应答。

注:所有测试都在本机,客户端无法模拟大量http请求,只能做简单压力测试。

1)本人所写的服务器,测试结果如下

f11820e810bf3a3a5cce445b0c7fd6ce.jpeg

每秒可发送300次请求,每秒发送数据25M,服务器cpu占有率为4%。

2)ASP.NET Core 服务器性能测试

b8142a0b8671ae72cd8311edad7269cb.jpeg

每秒发送30次请求,服务器cpu占有率为12%。

测试对比

本人开发的服务端处理速度为ASP.NET Core的10倍,cpu占用为对方的三分之一。ASP.NET Core处理慢,有可能实现了更多的功能;只是这些隐藏的功能,对我们也没用。

后记

如果没有开发经验,没有清晰的处理思路,开发一个高效的http服务器还有很困难的。

本人也一直以来都是采用ASP.NET Core作为http服务器。因为工作中需要高效的http服务器,就尝试写一个。

不可否认,ASP.NET Core各方面肯定优化的很好;但是ASP.NET Core 提供的某些功能是多余的。如果化繁为简,根据业务特点开发,性能未必不能更优。

- EOF -

技术群:添加小编微信dotnet999

公众号:dotnet讲堂

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/252165
推荐阅读
相关标签
  

闽ICP备14008679号