DIOCP 运作核心探密
来自网友天地弦的DIOCP早已经广为人知了,有很多的同学都用上了它,甚至各种变异、修改版本也出了不少。我最近也在学习DIOCP,打算将它用于自己的服务端,今天让我们来一起探密它(DIOCP)的运作核心吧。
DIOCP作为对Windows的IOCP完成端口封装,拥有了很高的性能,经过对ECHO示例的测试,它能轻松应对几万连接和并发。网络通讯一般分为6大阶段:请求连接、接受连接、接收数据、处理数据、回复数据、断开连接,下面我就从这6大阶段入手,来看看DIOCP是如何实现的。
一、 请求连接
实际上这第一阶段由客户端发起,指定HOST和Port请求连接我们的DIOCP服务。
二、 接受连接
在第一阶段,客户端请求连接后,我们的DIOCP服务会收到一个连接请求,这时默认会接受连接。在iocp.Sockets中,可以看到我们的服务类TIocpCustomTcpServer,它继承自TIocpCustom,就是TIocpCustomTcpServer完成了这整个网络通讯的各种请求的管理。
TIocpCustomTcpServer是一个用户能直接使用的DIOCP服务端类,在TIocpCustomTcpServer被调用Open(或Start)方法后,它先是开启IOCP任务引擎IocpEngine,初始化监听Socket,绑定监听端口,开始监听并将Socket绑定到IOCP句柄。接下来它会初始化指定数量的请求接受对象,然后再调用TiocpAcceptExRequest.PostRequest(内部调用IocpAcceptEx),像望夫石一样的守候着监听端口,等着客户端的连接。有人可能会问了,任务引擎怎么知道任务是什么,让谁来处理?好吧,我们可以看看TiocpRequest,它内部有一个Foverlapped,在Create时,Foverlapped.iocpRequest被设定为Self, TiocpAcceptExRequest是继承自TiocpRequest的。在PostRequest方法中调用AcceptEx时,有一个参数就是@FOverlapped,在任务引擎中GetQueuedCompletionStatus函数会返回Overlapped,这下明白了吧。
监听Socket:用于监听客户连接请求,开启指定的端口进入侦听状态,并调用任务引擎中的IocpCore对象Bind自己的Socket句柄到任务引擎的IOCP句柄(实际就是调用CreateIoCompletionPort函数来实现),这样监听Socket就可以在接收到IO请求时,由内核将请求加入IOCP任务队列,在IOCP引擎的工作线程中就可以通过GetQueuedCompletionStatus函数来直接取到任务进行处理了。请求响应、分配工作线程都是由任务引擎完成的。
在监听Socket收到连接请求时,会对Request进行必要的初始化(如状态设置、记录工作开始时间等),然后调用Request的FonResponse或HandleResponse。这里会优先调用FonResponse,目的是如果有指定外部的响应函数,就完全由外部接管,这样的封装增加了整体的灵活性。
在TIocpAcceptExRequest.HandleResponse中,会调用getPeerInfo函数获取远程客户端的IP地址和当前连接通讯端口,再产生一个OnAcceptedEx事件。接着调用Owner(TiocpCustomTcpServer)的DoAcceptExResponse方法,这时如果设置了OnContextAccept事件,则会产生此事件,你可以在这个事件中确定是否接受连接,默认会接受连接。接受连接后,根据KeepAlive开关判断是否设置TCP心跳。再调用IocpCore.Bind将当前连接的SocketHandle绑定到IOCP端口,如果成功会调用Context的DoConnected方法,在DoConnected里面会为当前连接分配一个标识句柄(实际上是一个计数器),设置Active状态为True,添加到在线列表,然后产生OnContextConnected事件,并调用OnConnected方法(你可以在子类中在这个地方做额外的处理),Context将状态设置为连接成功状态,并请求接收数据。如果在建立连接的过程中发生了错误,会关闭当前连接,产生OnContextError事件。
另外,在TiocpCustomTcpServer中内设了一个连接请求管理器IocpAcceptorMgr,它内部有一个TIocpAcceptExRequest对象池,目的是为了提升性能。IocpAcceptorMgr还能控制并发的最大请求数,超过上限时不再立即接受连接,而是等待对像池有空闲的对象时才返回。这里实际上就是一个排队效果了。
三、 接收数据
在第二阶段,连接成功后会马上调用当前连接的PostWSARecvRequest方法,请求接受数据。在TiocpCustomContext中,本身会初始化一个TIocpRecvRequest对象,它的作用就是产生一个数据接收请求,并在收到请求时,在HandleResponse方法中进行初步处理。
先来看看PostWSARecvRequest,它的实现很简单,就是直接去调用TiocpRecvRequest. PostRequest。 PostRequest函数会调用系统WSARecv函数产生一个接收数据的请求。这个请求当然也是异步的。由于TiocpRecvRequest也是继承于TiocpRequest,所以在调用WSARecv时也通过Foverlapped参数将自己与一次IO事件绑定了,在任务引擎中接收到数据时,会自动进入TiocpRecvRequest的HandleResponse方法。如果PostRequest失败,会触发OnContextError事件。
在TiocpRecvRequest. HandleResponse中,如果前面没有出错,会调用DoReceiveData,触发Context.OnRecvBuffer虚方法和IocpTcpServer的OnDataReceived事件,在这两个地方,用户可以对数据进行处理。紧接着,HandleResponse函数会再次产生一个接收请求,用于接收新的数据。(必须的哦,比如1G的文件,显然不能一个包就发送完^_^)
四、 处理数据
我们在使用TiocpCustomTcpServer时,可以通过注册一个被重载OnRecvBuffer的Context类来处理数据,也可以在OnDataReceived事件中处理。整体来说这个封装还是很自由的。
五、 回复数据
在处理数据时,我们经常需要回复一些东西给客户端。以使用OnDataReceived处理数据为例,我们先看看这个事件的声明:
TOnBufferReceived = procedure(Context: TIocpCustomContext; buf: Pointer; len: Cardinal; ErrorCode: Integer) of object;
在事件中,有当前接收的数据缓冲区地址、长度,还有一个Context。我们要回复数据或是查看远程客户端的IP端口等信息,就需要用到它。使用Context.Send()函数就可以发送我们的数据了。Send函数有几个重载版本,其中有一个里面包含BufReleaseType参数的,是指定正要被发送的数据缓冲区释放方式,默认dtNone,即不管它。如果使用dtFreeMem和dtDispose,则分别是调用FreeMem或Dispose来自动释放缓冲区内存。
Send函数同样也是异步的,会立即返回。在内部实际上是产生一个PostWSASendRequest请求。当然你也可以直接调用PostWSASendRequest请求,Send只是一个简化使用的封装。在PostWSASendRequest函数中,首先通过调用Owner(TIocpTcpServer).GetSendRequest函数从FsendRequestPool池中获取一个请求对象,将数据与这个请求对象绑定,即调用SendRequest. SetBuffer函数来初始化。在初始化完成后,将这个请求加入待发送队列中,成功后立即调用CheckNextSendRequest函数一次。
为何要调用CheckNextSendRequest? 其实CheckNextSendRequest是一个触发函数,它会从队列中Pop一个发送请求,成功后再调用TiocpSendRequest. ExecuteSend函数,在ExecuteSend里面会再次判断要发送的数据长度是否为0,然后再通过内部的InnerPostRequest来真正产生一个发送IO请求。这里面是通过WSASend来产生IO请求的,由于TiocpSendRequest也是基于TiocpRequest,所以WSASend时使用的Foverlapped参数就将对象与本次IO事件绑定了。在系统内核发送完数据或出错时,任务引擎会自动调用这个请求的HandleResponse方法。
在TIocpSendRequest.HandleResponse中,如果数据发送失败会产生OnContextError事件,成功则调用Context的虚方法DoSendRequestCompleted。然后立即调用Conetext. PostNextSendRequest方法,处理队列中的下一个发送请求。从这里可以看出,我们在Send之后调用CheckNextSendRequest时,可能并不是当前投递的待发送请求被响应。我们Send的请求可能会在前面的请求HandleResponse后才真正发送。
在Send数据时,我们用到了队列,其目的一是保证数据的发送顺序,二是能通过设置队列的大小来增强系统的稳定性,三是我们随时能观测到服务的状态。另外我们还使用了发送请求对象池,用来提升性能。
六、断开连接
IOCP服务在与客户建立连接后,内部只在发生错误或系统退出的时候才主动断开连接。平常时候默认由客户端来断开。在处理数据时,我们也可以直接调用Context. Disconnect来断开当前连接。在调用Disconnect时,会关闭当前连接的Socket,产生OnContextDisconnected事件,调用Context. OnDisconnected虚方法,并从在线列表中删除这个连接。在删除时Context会被还回到ContextPool中。
我们可以在OnContextDisconnected事件,或重载的Context的OnDisconnected方法对断开连接作额外的处理。
需要注意的是,前面的接受连接、接收数据、回发数据等都是异步的,只有断开连接是立即的。
七、结束语
至此,本文已经差不多结束了。在上面我们分析了DIOCP整个网络通讯的运作流程和基本使用方法。通过这些分析,你会发现,DIOCP到现在的V5阶段,整体流程已经很合理高效了,至少我暂时没发现明显的沉余。至于优化我想的是,在Send拷贝时,可以将GetMem换成环形内存,降低内存碎片的产生。
另外,本文讲的DIOCP为自己修改后的版本,已经将原来diocp-v5中的diocp.sockets.pas和diocp.tcp.server、diocp.tcp.client合并,部分类名有些细小的改变。将在线列表、HashTable换成了我自己的TYXDHashMapLinkTable,它是一个将HashTable和双向链表综合起来的怪物,个人感觉还是比较好用的。
本文只是我个人的一些初步理解,必竞才学习Diocp三天(三天打鱼两天晒网 :( ),很可能存在一些错误,欢迎大家指正。
最后感谢弦弦哥,能将这么好的东西奉献给大家,真是辛苦了。
有兴趣了解更多DIOCP的同学还可以直接访问它的官方网站: www.diocp.org
也可以直接加入QQ群: 320641073
(此文最初发表于QDAC官方网站,现在转回家里来)
DIOCP 运作核心探密的更多相关文章
- Android之EACCES (Permission denied)与Permission denied异常探密
话说,Accipiter君,最近又开始怒学Android了,记得刚开始还是09年学的,现在的手机还是华为出的最早的一款Android手机C8500,那时候就想好好学习Android,赚点小钱,可是~~ ...
- 第48篇 字符编码探密--ASCII,UTF8,GBK,Unicode
原文地址:http://blog.laofu.online/2017/08/22/encode-string/ ASCII 的由来 在计算机的“原始社会”,有人想把日常的使用的语言使用计算机来表示, ...
- DIOCP网络通讯流程
DIOCP 运作核心探密 原文连接: http://blog.qdac.cc/?p=2362 原作者: BB 天地弦的DIOCP早已经广为人知了,有很多的同学都用上了它,甚至各种变异.修改版本也出 ...
- DIOCP开源项目-DIOCP3的重生和稳定版本发布
DIOCP3的重生 从开始写DIOCP到现在已经有一年多的时间了,最近两个月以来一直有个想法做个 30 * 24 稳定的企业服务端架构,让程序员专注于逻辑实现就好.虽然DIOCP到现在通讯层已经很稳定 ...
- 图像分类丨浅析轻量级网络「SqueezeNet、MobileNet、ShuffleNet」
前言 深度卷积网络除了准确度,计算复杂度也是考虑的重要指标.本文列出了近年主流的轻量级网络,简单地阐述了它们的思想.由于本人水平有限,对这部分的理解还不够深入,还需要继续学习和完善. 最后我参考部分列 ...
- 【Soul源码探秘】插件链实现
引言 插件是 Soul 的灵魂. Soul 使用了插件化设计思想,实现了插件的热插拔,且极易扩展.内置丰富的插件支持,鉴权,限流,熔断,防火墙等等. Soul 是如何实现插件化设计的呢? 一切还得从插 ...
- Syscan360会议胸牌破解揭秘
Syscan360会议胸牌破解揭秘 背景 有幸参加今年11月份的上海Syscan360安全会议,会议期间有一个亮点就是360的独角兽团队设计了一款电子badge(胸牌)供参加人员进行破解尝试,类似于美 ...
- [ZigBee] 5、ZigBee基础实验——图文与代码详解定时器1(16位定时器)(长文)
1.定时器1概述 定时器1 是一个支持典型的定时/计数功能的独立16 位定时器,支持输入捕获,输出比较和PWM等功能.定时器有五个独立的捕获/比较通道.每个通道定时器要使用一个I/O 引脚.定时器用于 ...
- Ajax学习资源大全[本来是转载的,但是现在我增加了很多]
本欲放转载区,但是这样一文章放那里基本是没有用的,帮助不了任何人!所以放新手了!! 我一般非经典或者自己用不上不转载,所以如果你不幸看见了的话,恰恰你又对AJAX有兴趣的话不防看下,也许对你有用的!! ...
随机推荐
- java-URLConnection网络数据收取
通过url创建connection方式收取 import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import ja ...
- 【HDU 2874】Connections between cities(LCA)
dfs找出所有节点所在树及到树根的距离及深度及父亲. i和j在一棵树上,则最短路为dis[i]+dis[j]-dis[LCA(i,j)]*2. #include <cstring> #in ...
- 【转】Apache的Order Allow,Deny 详解
Apache的Order Allow,Deny 详解 Allow和Deny可以用于apache的conf文件或者.htaccess文件中(配合Directory, Location, Files等 ...
- 【BZOJ-1017】魔兽地图DotR 树形DP + 背包
1017: [JSOI2008]魔兽地图DotR Time Limit: 30 Sec Memory Limit: 162 MBSubmit: 1566 Solved: 705[Submit][S ...
- WPF弹出取消确定框
MessageBoxResult dr = MessageBox.Show("是否在"+ConfigHelper.GetAppSetting("SourceDBName& ...
- [iOS 主要框架的总结]
原文地址:http://blog.csdn.net/GooHong/article/details/28911301 在iOS中框架是一个目录,包含了共享资源库,用于访问该资源库中储存的代码的头文件, ...
- oracle建表并设置ID为自动增长
CREATE TABLESPACE shopping DATAFILE 'D:\oracle\mypc\oradata\orcl\shopping.dbf' SIZE 20M AUTOEXTEND O ...
- Java Code Examples for javax.servlet.http.Part
http://www.programcreek.com/java-api-examples/index.php?api=javax.servlet.http.Part The following ar ...
- linux下git的简单运用
linux下git的简单运用 windows下也有git,是git公司出的bash,基本上模拟了linux下命令行.许多常用的命令和linux下操作一样.也就是说,windows下的git命令操作和l ...
- Git连接到Git@OSC
1.配置本地git $git config --global user.name "xxx" $git config --global user.email "xxxxx ...