高性能的socket通讯IOCP服务器源码

ygluu 2009-07-21 12:14:10
加精
关键词:delphi socket通讯服务器例程(完成端口模型--IOCP)、I/O Completion Port、socket通讯登峰造极、IO重叠、 IOCP客户服务端


高性能的socket通讯IOCP服务器源码


很多人费尽心思,都没有找到一个完美的 I/O CP 例程,甚至跟人于误解,先将本人编写的例程公布出来,希望对那些苦苦寻觅的人带来收获。本例程可以作为初学者的学习之用,亦可以作为大型服务程序的通讯模块。其处理速度可以说,优化到了极点。如果理解了本例程的精髓,加上一个高效的通讯协议,你完全可以用它来构建一个高性能的通讯服务器。

在公布代码前,先谈谈I/O CP。对I/O CP的函数不多做说明了,网上很多,都一样。在此本人仅说一些技术上要注意的问题。


一、如何管理内存
1、IO数据缓冲管理
动态分配内存,是一种灵活的方式。但对于系统资源浪费是巨大的。因此本人采用的是预先分配服务器最大需要的内存,用链表来管理。任何时候分配交还都不需要遍历,仅需要互斥而已。
更巧妙的是,将IO发送信息和内存块有机的结合在一起,减少了链表的管理工作。

//IO操作标志
TIOFlag = (IO_ACCEPT, IO_READ, IO_WRITE);
//IO操作信息
PIOInfo =^ TIOInfo;
TIOInfo = packed record
Overlapped: TOverlapped; //重叠结构
DataBuf: TWSABUF; //IO数据信息
Socket: TSocket;
Flag: TIOFlag;
TickCountSend: DWord;
Next: PIOInfo;
Prior: PIOInfo;
end;

PUNode =^ TUNode;
TUNode = record
Next: Pointer;
end;

PIOMem =^ TIOMem;
TIOMem = packed record
IOInfo: TIOInfo;
Data: array[1..IO_MEM_SIZE] of Byte;
//申请内存的时候,返回的是Data的地址
end;


2、链路数据管理
采用双向链表结构,减少删除节点时遍历消耗的时间

//每个连接的信息
PLink =^ TLink;
TLink = record
Socket: TSocket;
RemoteIP: string[30];
RemotePort: DWord;
//最后收到数据时的系统节拍
TickCountActive: DWord;
//处理该连接的当前线程的信息
Worker: PWorker;
Data: Pointer; //应用层可以设置这个成员,当OnReceive的时候,就不要每次遍历每个连接对应的数据区了
Section: TRTLCriticalSection;
Next: PLink;
Prior: PLink;
end;

二、如何管理线程
每个工作线程创建的时候,调用:OnWorkerThreadCreateEvt,该函数可以返回这个线程对应的信息,比如为该线程创建的数据库连接控件或对应的类等,在OnReceive的可以从Link的Worker访问该成员Worker^.Data。

//工作线程信息
PWorker =^ TWorker;
TWorker = record
ID: THandle;
CompletionPort: THandle;
Data: Pointer; //调用OnWorkerThreadCreateEvt返回的值
//用于反应工作情况的数据
TickCountLong,
TickCountActive: DWord;
ExecCount: Integer;
//线程完成后设置
Finished: THandle;
Next: PWorker;
end;

同理,服务线程也是具有一样的特点。相见源码。

关于线程同步,一直是众多程序头疼的问题。在本例程中,尽量避免了过多的互斥,并有效地防止了死锁现象。用RTLCriticalSection,稍微不注意,就会造成死锁的灾难。哪怕是两行代码的差别,对多线程而言都是灾难的。在本例程中,对数据同步需要操作的是在维护链路链表方面上。服务线程需要计算哪个连接空闲超时了,工作线程需要处理断线情况,应用层主动发送数据时需要对该链路独占,否则一个在发送,一个在处理断线故障,就会发送冲突,导致灾难后果。

在本人的压力测试中,已经有效的解决了这个问题,应用层部分不需要做什么同步工作,可以安心的收发数据了。同时每个线程都支持了数据库连接。


三、到底要创建多少个工作线程合适
很多文章说,有N个CPU就创建N个线程,也有说N*2+2。最不喜欢说话不负责任的人了,本例程可以让刚入门 I/O CP 的人对它有更深入的了解。
例程测试结果:




四、该不该使用类
  有人说,抛弃一切类,对于服务器而言,会为类付出很多代价,从我的观点看,为类付出代价的,主要是动态创建的原因。其实,类成员访问和结构成员访问一样,需要相对地址。如果都是预先创建的,两者没有多大的差别。本例程采用裸奔函数的方式,当然在应用层可以采用类来管理,很难想象,如果没有没有类,需要多做多少工作。

五、缺点
  不能发大数据包,只能发不超过固定数的数据包。但对于小数据报而言,它将是优秀的。


  时间原因,不能做太多的解释和对代码做太多的注释,需要例程源码的可以和本人联系,免费提供。QQ:48092788


本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/GuestCode/archive/2009/07/20/4365243.aspx
...全文
9229 133 打赏 收藏 转发到动态 举报
写回复
用AI写文章
133 条回复
切换为时间正序
请发表友善的回复…
发表回复
liuzhigang_0625 2011-11-17
  • 打赏
  • 举报
回复
太好了啊,就是不能下载!
shen1234 2011-09-28
  • 打赏
  • 举报
回复
同求个vc版的 ,sch19831106@163.com
可怕的人 2011-09-25
  • 打赏
  • 举报
回复
太感谢了哈
gavin_lyx 2011-06-15
  • 打赏
  • 举报
回复
不错,学习学习
a1140012035 2011-05-06
  • 打赏
  • 举报
回复
学习,看看代码
mmx566 2010-10-15
  • 打赏
  • 举报
回复
学习中,这个太难了,感觉
OwenYang_cn 2010-09-14
  • 打赏
  • 举报
回复
今天下了一个 没有源码的...唉 。。。服了,还扣我点了..

每天回帖即可获得10分可用分!
tj_swjtu 2010-05-10
  • 打赏
  • 举报
回复
辛苦了 学习下
DarkZol_Huai 2010-05-01
  • 打赏
  • 举报
回复
拿走学习下
最近在研究Iocp
woainihuajia 2010-03-18
  • 打赏
  • 举报
回复
太难了 还是学习吧
badbadbad 2010-03-14
  • 打赏
  • 举报
回复
感觉还不错,但是对于要进行大量数据库操作的情况没什么参考价值啊!
oushengfen 2009-12-25
  • 打赏
  • 举报
回复
楼主如果是原创,则水平确实不一般了,本人自叹不如。则希望楼主公开源码,让更多的人来了解一下,嘿嘿。希望楼主无私奉献。
ygluu 2009-09-21
  • 打赏
  • 举报
回复
[Quote=引用 118 楼 gaozb 的回复:]
呵呵。。。早已阅读过大哥这的篇文章,精典。有没有新的Delphi版程序呀。
[/Quote]

有新的VC版:

三、功能说明
1、可以关闭Socket的Buffer;
2、可以关闭MTU(不等待MTU满才发送);
3、可以多IP或多端口监听;
4、可以重用socket(主动关闭除外);
5、可以0缓冲接收(Socket的Buffe = 0时,避免过多的锁定内存页);
6、可以0缓冲连接(客户端仅连接,不一定立即发数据);
7、可以条件编译:
a、是否使用内核Singly-linked lists;
b、是否使用处理线程(工作线程和处理线程分开);
c、是否使用内核锁来同步链表。
8、可以实现集群服务器模式的通讯(有客户端socket);
9、可以单独设置每个连接的Data项来实现连接和Usernfo的关联;
10、每个线程有OnBegin和OnEnd,用于设置线程独立的对象(数据库会话对象);
11、可以提供详细的运行情况,便于了解IOCP下的机制,以及进行调试分析;
12、可以发起巨量连接和数据(需要硬件配置来支持)。

四、缺陷
1、不支持UDP;
2、不兼容IPv6;
3、不带通讯协议,无法处理粘包;
4、工作线程和处理线程隔离还不是很明确;
5、设计尚需再完善和优化。

五、通讯速率测试部分截图
A机:单核台式机(服务端)
B机:双核笔记本(客户端)
网络:本地100M路由
由于台式机太老,用尽CPU还是不能用完带宽(其中大部分被“system”进程使用),因此改做服务器,由笔记本做客户端,发起密集数据,以堵塞的情况来满负荷使用网络,收发接近:10MB。
注意:由于测试条件太差,下面截图不能说明任何权威结果。

服务器端设置:


服务器端运行信息:



客户端设置:



客户端运行信息:


下载请移步:
http://topic.csdn.net/u/20090921/11/69fe4623-0fd6-46df-9b6d-feaa8257beca.html
gaozb 2009-09-10
  • 打赏
  • 举报
回复
呵呵。。。早已阅读过大哥这的篇文章,精典。有没有新的Delphi版程序呀。
xiezechang 2009-09-05
  • 打赏
  • 举报
回复
wzyzb 2009-08-31
  • 打赏
  • 举报
回复
同求个vc版本的 wzyzb@qq.com
ygluu 2009-08-29
  • 打赏
  • 举报
回复
[Quote=引用 114 楼 xwsn007 的回复:]
呵呵,凑个热闹
[/Quote]

都来凑个热闹
xwsn007 2009-08-28
  • 打赏
  • 举报
回复
呵呵,凑个热闹
ygluu 2009-08-27
  • 打赏
  • 举报
回复
[Quote=引用 67 楼 cjianwen 的回复:]
if GetQueuedCompletionStatus(CompletionPort, Bytes, DWORD(Link), POverlapped(IOInfo), INFINITE) = False then
        begin
          if (Link <> nil) then
          with Link^ do
          begin
            EnterCriticalSection(LinkSec);
            EnterCriticalSection(Section);

这么多锁。。。性能怎么样啊~?
[/Quote]


任何一个系统,都需要一定机制来维护基本数据的。诸如游戏类的,用的锁更多。

我可以做一个不用任何锁的通讯模块给你,但你用起来就更费劲了。
如何结合系统需求,才是性能的关键。




天剑68 2009-08-26
  • 打赏
  • 举报
回复
可以在此源码基础上加上自已经做的通信协议
加载更多回复(109)
1、在C#中,不用去面对完成端口的操作系统内核对象,Microsoft已经为我们提供了SocketAsyncEventArgs类,它封装了IOCP的使用。请参考:http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socketasynceventargs.aspx?cs-save-lang=1&cs-lang=cpp#code-snippet-1。 2、我的SocketAsyncEventArgsPool类使用List对象来存储对客户端来通信的SocketAsyncEventArgs对象,它相当于直接使用内核对象时的IoContext。我这样设计比用堆栈来实现的好处理是,我可以在SocketAsyncEventArgsPool池中找到任何一个与服务器连接的客户,主动向它发信息。而用堆栈来实现的话,要主动给客户发信息,则还要设计一个结构来存储已连接上服务器的客户。 3、对每一个客户端不管还发送还是接收,我使用同一个SocketAsyncEventArgs对象,对每一个客户端来说,通信是同步进行的,也就是说服务器高度保证同一个客户连接上要么在投递发送请求,并等待;或者是在投递接收请求,等待中。本例只做echo服务器,还未考虑由服务器主动向客户发送信息。 4、SocketAsyncEventArgs的UserToken被直接设定为被接受的客户端Socket。 5、没有使用BufferManager 类,因为我在初始化时给每一个SocketAsyncEventArgsPool中的对象分配一个缓冲区,发送时使用Arrary.Copy来进行字符拷贝,不去改变缓冲区的位置,只改变使用的长度,因此在下次投递接收请求时恢复缓冲区长度就可以了!如果要主动给客户发信息的话,可以new一个SocketAsyncEventArgs对象,或者在初始化中建立几个来专门用于主动发送信息,因为这种需求一般是进行信息群发,建立一个对象可以用于很多次信息发送,总体来看,这种花销不大,还减去了字符拷贝和消耗。
有牛人曾经说过,服务器玩的就是内存。仔细想想,确实是如此。服务器对内存的需求是巨大的,对内存的要求也是苛刻的。如何在内存管理上下功夫使服务器性能达到一个质的飞跃,是服务器设计中的首要解决的问题。 说到内存,我想刚开始设计服务器的人会说,不就申请释放吗,有什么难呢。从操作步骤来说,确实就这么两个,没有再多了的工作了。当我们采用虚拟内存分配或堆分配从操作系统获取内存的时候,总以为我们获得了足够的内存就可以让服务器安心工作了。但事情并未就这么简单,操作系统在一定条件下,还可以征用已经分配给你的物理内存,它会将你的物理内存数据复制到页交换文件中,然后把本来给你的物理内存再分配给别的进程,当你的进程访问你所获得的虚拟地址集的数据时,它会再找个空(或许也是从别的进程征用)的物理内存,再从页交换文件里面调出你原来的数据放回到新的物理内存里面,并将这个物理内存映射到你申请的虚拟内存地址集内(有关这项内容请参考操作系统的内存管理)。这个过程是相当耗费CPU资源且十分缓慢的,尤其是对硬盘虚拟内存文件的读写。其它大道理本文不多说,关于操作系统内存管理的原理可以从《Windows核心编程》、《Windows操作系统》、《操作系统》等书籍上了解。 我们可以使用lookaside lists技术来重新使用已经分配的内存的,或者使用SetWorkingSetSize来设置标志告知操作系统不要交换我的内存,但不外乎多一次操作而已。这个操作到底消耗多少的CPU资源,本人也没有考究过,但从性能要求的角度来说,多一事不如少一事。本文讨论的内存管理,将采用AWE(地址窗口化扩展)的技术,将申请到的物理内存保留为非分页内存,这部分的内存不会被页交换文件所交换,关于AWE请参阅以上提到的书籍。(下面提到的“内存管理”,将仅针对应用程序自己的内存管理功能模块(下文称之为内存管理器)而言,已非上面提到的操作系统的内存管理。) 衡量内存管理器性能的有两个,一个是内存分配时的效率(分配效率),另一个内存交还时的效率(释放效率),亦即二者操作的时间性,这个时间越短那么可以认为它的效率越高。下面的讨论,假定内存管理器是以页为最小分配单位,至于页的大小是多少才合适,稍后再说。
结构层次及相互联系 (1)、工作线程:响应连接的IO投递返回并负责投递读请求,并将IO返回结果投递给处理线程,可设定参数决定工作线程数量; (2)、处理线程:处理线程调用回调函数将信息传递给应用层或协议栈,可设定参数决定工作处理数量; (3)、看守线程:响应Accept事件调用AcceptEx,检测连接和心跳超时 ,将信息投递给工作线程,模块仅有一个看守线程。 1. 技术要求 (1)、线程同步:Lock指令、临界段; (2)、主要Socket API:WSASend、WSARecv、AcceptEx、DisconnectEx; (3)、内存管理:连接池(句柄重用)、内存池; (4)、数据0拷贝:通过内置处理线程,上层应用可以避免自建线程池及复制数据的过程。同时提供GBuf内存分配功能,应用层获得分配地址及填充数据之后亦可直接投递给内核/驱动层; (5)、数据顺序同步:同一个连接同时只有一个处理线程响应其IO事件; (6)、IO请求投递:单投递读、多投递写; (7)、0缓冲读投递:可条件编译实现,以适用大规模连接要求。 (8)、超时机制:可设置空连接(连接不发送数据)超时时间以防止DOS攻击,也可设置心跳超时时间防止网络故障导致的现有连接成为虚连接避免耗尽系统资源。 (9)、接口技术:API、回调函数、客户句柄(客户连接句柄)。 (10)、主、被动发送:不使用HASH、MAP及LIST技术,即可提供安全可靠高效的客户连接句柄,以实现服务器端主被动发送数据功能; (11)、PerHandleData的回收不以IO投递的计数器或链表来做依据但仍能安全回收,同时尽量避免在高频的读写操作时做其他无关的操作以提高读写效率。 (12)、处理线程和工作线程有着良好分工界限,繁重的工作交给处理线程完成,工作线程工作量最大限度的减少,仅响应投递返回及读投递的操作; (13)、支持AWE,模块自动识别AWE是否开启(需手动开启),“否”则使用虚拟内存机制。 2. 功能要求 (1)、多IP多端口监听,每个监听可设置不同的回调函数,以高效的区别处理数据 (2)、可设置每秒最大的连接并发量和空连接(连接不发数据)超时时间以防止DOS攻击造成的服务瘫痪、具有心跳处理(防网络异常造成的虚连接)功能 (3)、不加协议的透明传输,可适用广泛的网络通讯环境 (4)、可现实主、被动发送数据,但不会因兼顾主动发送而额外增加降低效率的工作 (5)、内置处理线程,上层应用可不必自建线程池处理数据,所有IO事件按顺序调用回调函数并可以在回调函数内直接处理数据,不必担心多线程造成的接收数据乱序的问题。 (6)、高效率的数据对应关联机制,在初次连接并根据登录数据设置每个连接对应的宿主(Owner)之后,再接收的数据即可立即获得该连接对应的宿主,而不必再做额外的查询工作,并且模块内部采用的是指针关联方式,对于长连接、主动发送的服务器系统而言是高效率的。 (7)、可兼容IPv6 3. 注意事项 因硬件环境和应用环境不同,不合理的配置会出现效率及性能上的问题,因此以下情况出现时,请务必与作者联系以确保获得更好的参数配置: (1)、连接量超过1000个的。超过的应结合具体硬件配置和网络带宽等因素综合设定运行参数。 (2)、带宽使用率超过20%的。工作线程和处理线程数量的设置也是综合考虑数据吞吐量和数据处理负载的因素来设置的,过多的线程会在调度上浪费时间,同时也应该综合考虑线程优先级别来设置工作线程和处理线程数量,两者的设置也不一定能相等。 (3)、服务器端有主动发送需求的、短连接(含网络故障造成的连接断开)出现频率高的。 压力测试工具介绍: 一、 使用G-TcpClient模块 二、 可以设定间隔时间发起大规模长、短连接 三、 可以发起密集数据包,包括即时和定时发送,1M的光纤带宽最大可以达到100K/S(单向)以上,100M本地网最大可以达到10M/S(单向)以上 四、 数据发送仅由一个独立线程但当,每点击一次Connect就创建一个线程根据当前参数发起连接。 五、 测试前提:服务器接收客户端数据后立即原样返回给客户端

1,593

社区成员

发帖
与我相关
我的任务
社区描述
Delphi 网络通信/分布式开发
社区管理员
  • 网络通信/分布式开发社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧