前言 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. Appcan 自定义星星评价

    注意要先有图片:     放在 css/icons/下 HTML代码: <div class="ub ub-f1"> <input id="bz-0&q ...

  2. PATB 1019. 数字黑洞 (20)

    一个神奇的数字. 时间限制 100 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Standard 作者 CHEN, Yue 给定任一个各位数字不完全相同的4位正整数,如果我 ...

  3. 机器学习经典算法之Apriori

    一. 搞懂关联规则中的几个概念 关联规则这个概念,最早是由 Agrawal 等人在 1993 年提出的.在 1994 年 Agrawal 等人又提出了基于关联规则的 Apriori 算法,至今 Apr ...

  4. c++ 子类,基类 中this指针 虚函数使用

    笔记: 子类和基类 构造函数不显式时,的this指针相同..在QT中,如果父类基于QObject,那么构造子类时传入this指针,这样所有子类,父类,基类都是同一地址.delelater(),会del ...

  5. ES6_08_Iterator遍历器

    Iterator遍历器: 概念: iterator是一种接口机制,为各种不同的数据结构提供统一的访问机制 作用: 1.为各种数据结构,提供一个统一的.简便的访问接口: 2.使得数据结构的成员能够按某种 ...

  6. Codeforces 755D:PolandBall and Polygon(思维+线段树)

    http://codeforces.com/problemset/problem/755/D 题意:给出一个n正多边形,还有k,一开始从1出发,向第 1 + k 个点连一条边,然后以此类推,直到走完 ...

  7. 01(a)一元函数_多元函数_无约束极值问题的求解

    1. 一元函数的极值问题  (函数光滑) 对于一个一元函数$f(x)$,怎么才能找出它的极值呢? 1.1根据定义:如果存在一点${{x}_{0}}$,在点${{x}_{0}}$的某个领域$U({{x} ...

  8. MMM 状压dp学习记

    状压dp学习记 by scmmm 开始日期 2019/7/17 前言 状压dp感觉很好理解(本质接近于爆搜但是又有广搜的感觉),综合了dp的高效性(至少比dfs,bfs优),又能解决普通dp难搞定的问 ...

  9. SCADA开源项目lite版本

    一.引子 自从看了老坏猫(江湖人称猫总)的SharpSCADA项目后,让我感觉耳目一新同时也对自动化数据采集有了更深入的认识,我相信有不少做上位机的朋友和我一样对这个项目非常好奇.我们做上位机的应用场 ...

  10. python模块导入-软件开发目录规范-01

    模块 模块的基本概念 模块: # 一系列功能的结合体 模块的三种来源 """ 模块的三种来源 1.python解释器内置的模块(os.sys....) 2.第三方的别人写 ...