前言

 TCP\IP已成为业界通讯标准。现在越来越多的程序需要联网。网络系统分为服务端和客户端,也就是c\s模式(client \ server)。client一般有一个或少数几个连接;server则需要处理大量连接。大部分情况下,只有服务端才特别考虑性能问题。本文主要介绍服务端处理方法,当然也可以用于客户端。

  我也发表过c#版网络库。其实,我最早是从事c++开发,多年前就实现了对完成端口的封装。最近又把以前的代码整理一下,做了测试,也和c#版网络库做了粗略对比。总体上,还是c++性能要好一些。c#网络库见文章《一个高性能异步socket封装库的实现思路》。

Windows平台下处理socket通讯有多种方式;大体可以分为阻塞模式和非阻塞模式。阻塞模式下send和recv都是阻塞的。简单讲一下这两种模式处理思路。

  阻塞模式:比如调用send时,把要发送的数据放到网络发送缓冲区才返回。如果这时,网络发送缓冲区满了,则需要等待更久的时间。socket的收发其实也是一种IO,和读写硬盘数据有些类似。一般来讲,IO处理速度总是慢的,不要和内存处理并列。对于调用recv,至少读取一个字节数据,函数才会返回。所以对于recv,一般用一个单独的线程处理。

非阻塞模式:send和recv都是非阻塞的;比如调用send,函数会立马返回。真正的发送结果,需要等待操作系统的再次通知。阻塞模式下一步可以完成的处理,在非阻塞模式下需要两步。就是多出的这一步,导致开发难度大大增加。高性能大并发网络服务器必须采用非阻塞模式。完成端口(IOCP)是非阻塞模式中性能最好的一种。

  作者多年以前,就开始从事winsocket开发,最开始是采用c++、后来采用c#。对高性能服务器设计的体会逐步加深。人要在一定的压力下才能有所成就。最开始的一个项目是移动信令分析,所处理的消息量非常大;高峰期,每秒要处理30万条信令,占用带宽500M。无论是socket通讯还是后面的数据处理,都必须非常优化。所以从项目的开始,我就谨小慎微,对性能特别在意。项目实施后,程序的处理性能出乎意料。一台服务器可以轻松处理一个省的信令数据(项目是08年开始部署,现在的硬件性能远超当时)。程序界面如下:

题外话 通过这个项目我也有些体会:1)不要怀疑Windows的性能,不要怀疑微软的实力。有些人遇到性能问题,或是遇到奇怪的bug,总是把责任推给操作系统;这是不负责任的表现。应该反思自己的开发水平、设计思路。2)开发过程中,需要把业务吃透;业务是开发的基石。不了解业务,不可能开发出高性能的程序。所有的处理都有取舍,每个函数都有他的适应场合。有时候需要拿来主义,有时候需要从头开发一个函数。

目标

  开发出一个完善的IOCP程序是非常困难的。怎么才能化繁为简?需要把IOCP封装;同时这个封装库要有很好的适应性,能满足各种应用场景。一个好的思路就能事半功倍。我就是围绕这两个目标展开设计。

  1 程序开发接口

  socket处理本质上可以分为:读、写、accept、socket关闭等事件。把这些事件分为两类:a)读、accept、socket关闭 b)写;a类是从库中获取消息,b类是程序主动调用函数。对于a类消息可以调用如下函数:

//消息事件
enum Enum_MessageType :char
{
EN_Accept = ,
EN_Read,
EN_Close,
EN_Connect
}; //返回的数据结构
class SocketMessage
{
public:
SOCKET Socket;
Enum_MessageType MessageType;
//当MessageType为EN_Connect时,BufferLen为EasyIocpLib_Connect函数的tag参数
INT32 BufferLen;
char *Buffer;
}; //不停的调用此函数,返回数据
SocketMessage* EasyIocpLib_GetMessage(UINT64 handle);

对于b类,就是发送数据。当调用发送时,数据被放到库的发送缓冲中,函数里面返回。接口如下:

enum EN_SEND_BUFFER_RESULT
{
en_send_buffer_ok = , //放入到发送缓冲
en_not_validate_socket, //无效的socket句柄
en_send_buffer_full //发送缓冲区满
}; EN_SEND_BUFFER_RESULT EasyIocpLib_SendMessage(UINT64 handle, SOCKET socket,
char* buffer, int offset, int len, BOOL mustSend = FALSE);

  总的思路是接收时,放到接收缓冲;发送时,放到发送缓冲。外部接口只对内存中数据操作,没有任何阻塞。

    2)具有广泛的适应性

如果网络库可以用到各种场景,所处理的逻辑必须与业务无关。所以本库接收和发送的都是字节流。包协议一般有长度指示或有开始结束符。需要把字节流分成一个个完整的数据包。这就与业务逻辑有关了。所以要有分层处理思想:

库性能测试

  首先对库的性能做测试,使大家对库的性能有初步印象。这些测试都不是很严格,大体能反映程序的性能。IOCP是可扩展的,就是同时处理10个连接与同时处理1000个连接,性能上没有差别。

我的机器配置不高,cup为酷睿2 双核 E7500,相当于i3低端。

  1)两台机器测试,一个发送,一个接收:带宽占用40M,整体cpu占用10%,程序占用cpu不超过3%。

  2)单台机器,两个程序互发:收发数据达到30M字节,相当于300M带宽,cpu占用大概25%。

  

  3)采用更高性能机器测试,两个程序对发数据:cpu为:i5-7500 CPU @ 3.40GHz

  

   收发数据总和80M字节每秒,接近1G带宽。cpu占用25%。

   测试程序下载地址 :《完成端口(IOCP)性能测试程序(c++版本 64位程序)。只有exe程序,不包括代码。

网络库设计思路

  服务器要启动监听,当有客户端连接时,生成新的socket句柄;该socket句柄与完成端口关联,后续读写都通过完成端口完成。

1 socket监听(Accept处理)

  关于监听处理,参考我另一篇文章《单线程实现同时监听多个端口》

2 数据接收

  收发数据要用到类型OVERLAPPED。需要对该类型进一步扩充,这样当从完成端口返回时,可以获取具体的数据和操作类型。这是处理完成端口一个非常重要的技巧。

//完成端口操作类型
typedef enum
{
POST_READ_PKG, //读
POST_SEND_PKG, //写
POST_CONNECT_PKG,
POST_CONNECT_RESULT
}OPERATION_TYPE; struct PER_IO_OPERATION_DATA
{
WSAOVERLAPPED overlap; //第一个变量,必须是操作系统定义的结构
OPERATION_TYPE opType;
SOCKET socket;
WSABUF buf; //要读取或发送的数据
};

  发送处理:overlap包含要发送的数据。调用此函数会立马返回;当有数据到达时,会有通知。

BOOL NetServer::PostRcvBuffer(SOCKET socket, PER_IO_OPERATION_DATA *overlap)
{
DWORD flags = MSG_PARTIAL;
DWORD numToRecvd = ; overlap->opType = OPERATION_TYPE::POST_READ_PKG;
overlap->socket = socket; int ret = WSARecv(socket,
&overlap->buf,
,
&numToRecvd,
&flags,
&(overlap->overlap),
NULL); if (ret != )
{
if (WSAGetLastError() == WSA_IO_PENDING)
{
ret = NO_ERROR;
}
else
{
ret = SOCKET_ERROR;
}
} return (ret == NO_ERROR);
}

从完成端口获取读数据事件通知:

DWORD NetServer::Deal_CompletionRoutine()
{
DWORD dwBytesTransferred;
PER_IO_OPERATION_DATA *lpPerIOData = NULL; ULONG_PTR Key;
BOOL rc;
int error; while (m_bServerStart)
{
error = NO_ERROR;
//从完成端口获取事件
rc = GetQueuedCompletionStatus(
m_hIocp,
&dwBytesTransferred,
&Key,
(LPOVERLAPPED *)&lpPerIOData,
INFINITE); if (rc == FALSE)
{
error = ;
if (lpPerIOData == NULL)
{
DWORD lastError = GetLastError();
if (lastError == WAIT_TIMEOUT)
{
continue;
}
else
{
//continue;
//程序结束
assert(false);
return lastError;
}
}
else
{
if (GetNetResult(lpPerIOData, dwBytesTransferred) == FALSE)
{
error = WSAGetLastError();
}
}
}
if (lpPerIOData != NULL)
{
switch (lpPerIOData->opType)
{
case POST_READ_PKG: //读函数返回
{
OnIocpReadOver(*lpPerIOData, dwBytesTransferred, error);
}
break; case POST_SEND_PKG:
{
OnIocpWriteOver(*lpPerIOData, dwBytesTransferred, error);
}
break;
}
} }
return ;
} void NetServer::OnIocpReadOver(PER_IO_OPERATION_DATA& opData,
DWORD nBytesTransfered, DWORD error)
{
if (error != NO_ERROR || nBytesTransfered == )//socket出错
{
Net_CloseSocket(opData.socket);
NetPool::PutIocpData(&opData);//数据缓冲处理
}
else
{
OnRcvBuffer(opData, nBytesTransfered);//处理接收到的数据
BOOL post = PostRcvBuffer(opData.socket, &opData); //再次读数据
if (!post)
{
Net_CloseSocket(opData.socket);
NetPool::PutIocpData(&opData);
}
}
}

3 数据发送

  数据发送时,先放到发送缓冲,再发送。向完成端口投递时,每个连接同时只能有一个正在投递的操作。

BOOL NetServer::PostSendBuffer(SOCKET socket)
{
if (m_clientManage.IsPostSendBuffer(socket)) //如果有正在执行的投递,不能再次投递
return FALSE; //获取要发送的数据
PER_IO_OPERATION_DATA *overlap = NetPool::GetIocpData(FALSE);
int sendCount = m_clientManage.GetSendBuf(socket, overlap->buf);
if (sendCount == )
{
NetPool::PutIocpData(overlap);
return FALSE;
} overlap->socket = socket;
overlap->opType = POST_SEND_PKG;
BOOL post = PostSendBuffer(socket, overlap);
if (!post)
{
Net_CloseSocket(socket);
NetPool::PutIocpData(overlap);
return FALSE;
}
else
{
m_clientManage.SetPostSendBuffer(socket, TRUE);
return TRUE;
}
}

 总结:开发一个好的封装库必须有的好的思路。对复杂问题要学会分解,每个模块功能合理,适应性要强;要有模块化、层次化处理思路。如果网络库也处理业务逻辑,处理具体包协议,它就无法做到通用性。一个通用性好的库,才值得我们花费大气力去做好。我设计的这个库,用在了公司多个系统上;以后无论遇到任何网络协议,这个库都可以用得上,一劳永逸的解决网络库封装问题。

采用完成端口(IOCP)实现高性能网络服务器(Windows c++版)的更多相关文章

  1. 基于 IOCP 的通用异步 Windows Socket TCP 高性能服务端组件的设计与实现

    设计概述 服务端通信组件的设计是一项非常严谨的工作,其中性能.伸缩性和稳定性是必须考虑的硬性质量指标,若要把组件设计为通用组件提供给多种已知或未知的上层应用使用,则设计的难度更会大大增加,通用性.可用 ...

  2. 套接字I/O模型-完成端口IOCP

    “完成端口”模型是迄今为止最为复杂的一种I/O模型.然而,假若一个应用程序同时需要管理为数众多的套接字,那么采用这种模型,往往可以达到最佳的系统性能!但不幸的是,该模型只适用于Windows NT和W ...

  3. nginx+ftp搭建图片服务器(Windows Server服务器环境下)

    几种图片服务器的对比 1.直接使用ftp服务器,访问图片路径为 ftp://账户:密码@192.168.0.106/31275-105.jpg 不采用这种方式,不安全容易暴露ftp账户信息 2.直接使 ...

  4. Android 采用post方式提交数据到服务器

    接着上篇<Android 采用get方式提交数据到服务器>,本文来实现采用post方式提交数据到服务器 首先对比一下get方式和post方式: 修改布局: <LinearLayout ...

  5. 如何在IIS上发布网站 在阿里云服务器windows server2012r iis上部署.net网站

    如何在IIS上发布网站   本片博客记录一下怎么用IIS发布一个网站,以我自己电脑上一个已经开发完成的网站为例: 1.打开项目 这是我电脑上的一个项目,现在我记录一下将这个项目发布到iis上的整个过程 ...

  6. 阿里云ECS服务器windows环境下配置redis

    一.下载解压redis github下载地址:https://github.com/MSOpenTech/redis/tags 下载的是Redis-x64-3.2.100版本,Redis-x64-3. ...

  7. 基于svnserve的SVN服务器(windows下安装与配置)

    基于svnserve的SVN服务器(windows下安装与配置) 基于svnserve的SVN服务器(windows下安装与配置)关键字: svn 安装SVNserve 从http://subvers ...

  8. 无法向会话状态服务器发出会话状态请求。请确保 ASP.NET State Service (ASP.NET 状态服务)已启动,并且客户端端口与服务器端口相同。如果服务器位于远程计算机上,请检查。。。

    异常处理汇总-服 务 器 http://www.cnblogs.com/dunitian/p/4522983.html 无法向会话状态服务器发出会话状态请求.请确保 ASP.NET State Ser ...

  9. 阿里云服务器Windows Server 2008/2012部署Office Web Server 2013

    以前成功将Office Web Server 2013部署在了本地服务器上,此次是将Office Web Server 2013部署在阿里云服务器Windows Server 2008和2012上,中 ...

  10. DELPHI中完成端口(IOCP)的简单分析(4)

    DELPHI中完成端口(IOCP)的简单分析(4)   在我以前写的文章中,一直说的是如何接收数据.但是对于如何发送数据却一点也没有提到.因为从代码量上来说接收的代码要比发送多很多.今天我就来写一下如 ...

随机推荐

  1. JS生成PDF文件

    代码: var pdf = new jsPDF('p','pt','a4'); pdf.internal.scaleFactor = 1; //可以调整缩放比例 var options = { //p ...

  2. Python中单引号,双引号,3个单引号及3个双引号的区别

    单引号和双引号在Python中我们都知道单引号和双引号都可以用来表示一个字符串,比如 str1 = 'python' str2 = "python" str1和str2是没有任何区 ...

  3. spring mvc+mybatis 构建 cms + 实现UC浏览器文章功能

    最近公司在模拟UC浏览器做一个简单的cms系统,主要针对于企业内部的文章浏览需求,这边考虑用户大多用mobile浏览文章内容,故使用原生的ios和android进行开发,后面也会集成html5. 1. ...

  4. win7 home 提升 admin, pip

    以管理员权限运行cmd cmd   右击‘以管理员身份运行’ 输入net user administrator /active:yes 取消为 net user administrator /acti ...

  5. 深入理解JVM(六)类文件结构

    6.1 关于类文件 1.class文件的一次编译,到处运行的跨平台性: 2.JVM不止有跨平台性,还有跨语言性,不管是JRuby还是Groovy写出来的程序,只要编译出符合JVM规范的class文件就 ...

  6. java多线程系列15 设计模式 生产者 - 消费者模式

    生产者-消费者 生产者消费者模式是一个非常经典的多线程模式,比如我们用到的Mq就是其中一种具体实现 在该模式中 通常会有2类线程,消费者线程和生产者线程 生产者提交用户请求 消费者负责处理生产者提交的 ...

  7. [c#.net]未能加载文件或程序集“”或它的某一个依赖项。系统找不到指定的文件

    问题是这样嘀: 项目采用了三层架构和工厂模式,并借鉴了PetShop的架构,因为这个项目也是采用分布式的数据库,目前只有三个数据库,主要出于提高访问性能考虑. 原来是按照网上对PetShop的介绍来给 ...

  8. 开启hadoop集群

    首先开启zookeeper zkServer.sh start start-all

  9. 2019.02.19 bzoj2655: calc(生成函数+拉格朗日插值)

    传送门 题意简述:问有多少数列满足如下条件: 所有数在[1,A][1,A][1,A]之间. 没有相同的数 数列长度为nnn 一个数列的贡献是所有数之积,问所有满足条件的数列的贡献之和. A≤1e9,n ...

  10. ABP框架系列之三十一:(Localization-本地化)

    Introduction Any application has at least one language for user interface. Many applications have mo ...