前言 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(, "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 = -;
int flagIndex = ; int count = ;
foreach (byte[] buffer in _listBuffer)
{
foreach (byte ch in buffer)
{
count++;
if (ch == destBuffer[flagIndex])
{
flagIndex++;
}
else
{
flagIndex = ;
} if (flagIndex >= destBuffer.Length)
{
index = count;
return true;
}
}
} return false;
} public int TotalByteLength
{
get
{
int count = ;
foreach (byte[] item in _listBuffer)
{
count += item.Length;
}
return count;
}
} public byte[] GetAllByte()
{
if (_listBuffer.Count == )
return new byte[];
if (_listBuffer.Count == )
return _listBuffer[]; int byteLen = ;
_listBuffer.ForEach(o => byteLen += o.Length);
byte[] result = new byte[byteLen]; int index = ;
foreach (byte[] item in _listBuffer)
{
Buffer.BlockCopy(item, , result, index, item.Length);
index += item.Length;
}
return result;
} public byte[] GetSubBuffer(int start, int countTotal)
{
if (countTotal == )
return new byte[]; byte[] result = new byte[countTotal];
int countCopyed = ; int indexOfBufferPool = ;
foreach (byte[] buffer in _listBuffer)
{
//找到起始复制点
int indexOfItem = ;
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; } = -; string _httpHeaderText = string.Empty;
public string HttpHeaderText
{
get
{
if (_httpHeaderText != string.Empty)
return _httpHeaderText; if (!IsHttpHeadOver)
return _httpHeaderText; byte[] buffer = _bufferManage.GetSubBuffer(, 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 < )
return string.Empty; _httpHeaderFirstLine = HttpHeaderText.Substring(, index);
return _httpHeaderFirstLine;
}
} public string HttpRequestUrl
{
get
{
if (HttpHeaderFirstLine == string.Empty)
return string.Empty; string[] items = HttpHeaderFirstLine.Split(' ');
if (items.Length < )
return string.Empty; return items[];
}
} public bool IsHttpHeadOver
{
get
{
if (HeaderByteCount > )
return true; byte[] headOverFlag = HttpConst.Flag_DoubleReturnByte; if (_bufferManage.FindBuffer(headOverFlag, out int count))
{
HeaderByteCount = count;
return true;
}
return false;
}
} int _httpContentLen = -;
public int HttpContentLen
{
get
{
if (_httpContentLen >= )
return _httpContentLen; if (HttpHeaderText == string.Empty)
return -; int start = HttpHeaderText.IndexOf(HttpConst.Flag_HttpContentLenth);
if (start < ) //http请求没有包体
return ; start += HttpConst.Flag_HttpContentLenth.Length; int end = HttpHeaderText.IndexOf(HttpConst.Flag_Return, start);
if (end < )
return -; string intValue = HttpHeaderText.Substring(start, end - start).Trim();
if (int.TryParse(intValue, out _httpContentLen))
return _httpContentLen;
return -;
}
} 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 == -)
return false; int shouldLenth = HeaderByteCount + HttpContentLen;
bool result = TotalByteLength >= shouldLenth;
return result;
}
} public List<HttpByteValueKey> GetBodyParamBuffer()
{
List<HttpByteValueKey> result = new List<HttpByteValueKey>(); if (HttpContentLen < )
return result;
Debug.Assert(IsReadEnd); if (HttpContentLen == )
return result; byte[] bodyBytes = _bufferManage.GetSubBuffer(HeaderByteCount, HttpContentLen); //获取key value对应的byte
int start = ;
int current = ;
HttpByteValueKey item = null;
foreach (byte b in bodyBytes)
{
if (item == null)
item = new HttpByteValueKey(); current++;
if (b == '=')
{
byte[] buffer = new byte[current - start - ];
Buffer.BlockCopy(bodyBytes, start, buffer, , buffer.Length);
item.Key = buffer;
start = current;
}
else if (b == '&')
{
byte[] buffer = new byte[current - start - ];
Buffer.BlockCopy(bodyBytes, start, buffer, , 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, , buffer.Length);
item.Value = buffer;
result.Add(item);
} return result;
} public string HttpBodyText
{
get
{
if (HttpContentLen < )
return string.Empty;
Debug.Assert(IsReadEnd); if (HttpContentLen == )
return string.Empty; byte[] bodyBytes = _bufferManage.GetSubBuffer(HeaderByteCount, HttpContentLen);
string bodyString = Encoding.UTF8.GetString(bodyBytes);
return bodyString;
}
} }

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 提供的某些功能是多余的。如果化繁为简,根据业务特点开发,性能未必不能更优。

自己动手,开发轻量级,高性能http服务器。的更多相关文章

  1. HttpServer: 基于IOCP模型且集成Openssl的轻量级高性能web服务器

    2021年4月写过一个轻量级的web服务器HttpServer,见文章: <HttpServer:一款Windows平台下基于IOCP模型的高并发轻量级web服务器>,但一直没有时间添加O ...

  2. 谈谈如何使用Netty开发实现高性能的RPC服务器

    RPC(Remote Procedure Call Protocol)远程过程调用协议,它是一种通过网络,从远程计算机程序上请求服务,而不必了解底层网络技术的协议.说的再直白一点,就是客户端在不必知道 ...

  3. Netty开发实现高性能的RPC服务器

    Netty开发实现高性能的RPC服务器 RPC(Remote Procedure Call Protocol)远程过程调用协议,它是一种通过网络,从远程计算机程序上请求服务,而不必了解底层网络技术的协 ...

  4. Nginx:轻量级高性能的Web服务器

    Nginx ("engine x") 是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP服务器.Nginx是由Igor Sysoev为俄罗斯访问量第二的R ...

  5. <摘录>详谈高性能UDP服务器的开发

    上一篇文章我详细介绍了如何开发一款高性能的TCP服务器的网络传输层.本章我将谈谈如何开发一个高性能的UDP服务器的网络层.UDP服务器的网络层开 发相对与TCP服务器来说要容易和简单的多,UDP服务器 ...

  6. 《高性能Linux服务器构建实战》——第1章轻量级HTTP服务器Nginx

    第1章 轻量级HTTP服务器Nginx本章主要介绍Nginx的配置管理和使用.作为一个轻量级的HTTP服务器,Nginx与Apache相比有以下优势:在性能上,它占用很少的系统资源,能支持更多的并发连 ...

  7. 高性能Web服务器Nginx的配置与部署研究系列(1)-- 入门 hello work

    简介: Nginx 是一个基于 BSD-like 协议.开源.高性能.轻量级的HTTP服务器.反向代理服务器以及电子邮件(SMTP.POP3.IMAP)服务器.Nginx 是由一个俄罗斯的名叫“Igo ...

  8. Netty实现高性能IOT服务器(Groza)之精尽代码篇中

    运行环境: JDK 8+ Maven 3.0+ Redis 技术栈: SpringBoot 2.0+ Redis (Lettuce客户端,RedisTemplate模板方法) Netty 4.1+ M ...

  9. 高性能Nginx服务器-反向代理

    Nginx Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,并在一个BSD-like 协议下发行.由俄罗斯的程序设计师Igor Sysoev所开发,供 ...

随机推荐

  1. docker相关使用

    安装docker 在CentOS 7上安装docker-ce,首先检查系统中是否已经安装过docker及相关依赖: $ sudo yum remove docker docker-client doc ...

  2. PyCharm问题-ModuleNotFoundError: No module named 'pymysql'

    在使用PyCharm时遇到pymysql模块导入有问题,本人使用的是Windows,但解决问题的方法是一致的,先来安装pymysql: 用管理员身份运行CMD.exe,然后查看python的安装路径 ...

  3. Analysis of requirement specification of parking management system

    Analysis of requirement specification of parking management system PURPOSE OF THE SYSTEM The parking ...

  4. 关于Lombok和自动生成get set方法

    在Java开发的项目里面免不了要用很多的get set 以及toString之类的方法,有时候确实是很繁琐而且做着重复共同工作,我们有没有办法来简化这个过程呢,当然有. Lombok就可以很好的解决这 ...

  5. c语言:链表

    1.链表概述: 链表是一种数据结构,它采用动态分配存储单元方式.它能够有效地节省存储空间(同数组比较). 由于链表中的节点是一个结构体类型,并且结点中有一个成员用于指向下一个结点.所以定义作为结点的格 ...

  6. [WPF自定义控件库]好用的VisualTreeExtensions

    1. 前言 A long time ago in a galaxy far, far away....微软在Silverlight Toolkit里提供了一个好用的VisualTreeExtensio ...

  7. 【无线安全实践入门】网络扫描和ARP欺骗

    文中可能存在错误操作或错误理解,望大家不吝指正. 同时也希望可以帮助到想要学习接触此方面.或兴趣使然的你,让你有个大概的印象. !阅前须知! 本文是基于我几年前的一本笔记本,上面记录了我学习网络基础时 ...

  8. 在?MySQL事务隔离级别了解一下?

    事务的四大ACID 属性:Atomicity 原子性.Consistency 一致性.Isolation 隔离性.Durability 持久性. 原子性: 事务是最小的执行单位不可分割,强调事务的不可 ...

  9. Node.js热部署代码,实现修改代码后自动重启服务方便实时调试

    写PHP等脚本语言的时候,已经习惯了修改完代码直接打开浏览器去查看最新的效果.而Node.js 只有在第一次引用时才会去解析脚本文件,以后都会直接访问内存,避免重复载入,这种设计虽然有利于提高性能,却 ...

  10. java Springboot 生成 二维码 +logo

    上码,如有问题或者优化,劳请广友下方留言 1.工具类 import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHint ...