diocp运作核心探密

朝闻道 朝闻道     2022-08-20     278

关键词:

来自网友天地弦的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官方网站,现在转回家里来)

http://www.cnblogs.com/yangyxd/p/5146798.html

diocp从零开始1

diocp是windows下最复杂的通讯技术也是效率最高的,之前一直想学但都没学会。因手上有一个项目是很老的delphi项目,想用diocp改造成三层的,所以这次一定要好好学习,争取把这个技术拿下。基础知识想学好diocp就得先打好基础... 查看详情

diocp数据包太大,请在业务层分拆发送

DIOCP数据包太大,请在业务层分拆发送DIOCP日志记录异常:数据包太大,请在业务层分拆发送。。。。。。跟踪发现,原因在下图:  查看详情

diocp_tcp_client单元源码与注释

(** Unitowner:d10.天地弦* blog:http://www.cnblogs.com/dksoft*homePage:www.diocp.org**2015-02-2208:29:43*DIOCP-V5发布**1.修复ex.tcpclient编码问题,发送大数据时,无法解码的bug*2015-08-1714:25:56*)unitdiocp_tcp_client;{$I‘diocp 查看详情

diocp小白精简版

DIOCP修改版(YangYxd)【当前版本】2015.05.27.001【主要更改】–———————————––2015.05.27–—————— 查看详情

深入浅出spring原理及实战「原理分析专题」不看源码就带你剖析ioc容器核心流程以及运作原理

这是史上最全面的Spring的核心流程以及运作原理的分析指南🍃【Spring核心专题】「IOC容器篇」不看繁琐的源码就带你浏览Spring的核心流程以及运作原理🍃【Spring核心专题】「AOP容器篇」不看繁琐的源码就带你浏览Spring的... 查看详情

diocp转发型中间网关

处理部分关键代码:中转类网关处理部分:procedureDiocpExecute(pvCMDObject:TSimpleMsgPack;pvContext:TDiocpStreamClientContext);varlvCMDIndex:Integer;lvSession:TCHATSession;ToUserID:string;DataDBID:string;beginifpvCMDObject 查看详情

diocp之编写第一个应用程序

...on(创建一个标准VCL程序)第二步:引用必要的单元文件:diocp_coder_tcpServer,diocp_tcp_server, Sim 查看详情

diocp之demo-登陆验证设计

ECHOServer代码(不考虑粘包的处理):unitufrmMain;interfaceusesWindows,Messages,SysUtils,Variants,Classes,Graphics,Controls,Forms,Dialogs,StdCtrls,ActnList,diocp_tcp_server,ExtCtrls,ComCtrls,utils_safeLogger,utils_ 查看详情

diocp之获取在线用户列表

通过获取tcpserver.getonlinecontextlist来得到在线列表procedureTfrmMain.btn_refreshClick(Sender:TObject);varlvList:TList;i:Integer;lvClient:TMyClientContext;lvItem:TListItem;beginlstClientINfo.Items.Clear;lvList:= 查看详情

diocp之数据接收事件

一、不引用编码器与解码器的情况下(ECHO的DEMO)  类TIOCPtcpclient,接收服务器的数据事件:OnRecvBuffer  类TDiocpTcpServer,接收客户端数据事件:OnRecvBuffer  TIocpTcpServer  propertyOnDataReceived:TOnDataRec 查看详情

diocp之demo学习顺序及达到要求

...解粘包的理方式,即数据类型+数据长度+数据流第四个:diocp_utils_demo,理解字符处理函数,快速编程处理字符第五个:DiocpTask,任务投递,Task多线程使用方法以上只是个人学习的历程与体会 查看详情

「spring」「aop容器」不看源码就带你认识核心流程以及运作原理

...篇文章主要介绍了spring核心特性机制的IOC容器机制和核心运作原理,接下来我们去介绍另外一个较为核心的功能,那就是AOP容器机制,主要负责承接前一篇代理模式机制中动态代理:JDKProxy和CglibProxy的功能机制之后,我们开始研... 查看详情

diocp之编写第一个应用程序

构建client界面:构建界面要比写代码更难爱,不是专业UI设计太丑,先有个界面,好写代码,客户端代码与界面设计思想:界面与数据之间分离处理,不能要接收数据的地方写代码,不然以后修改程序会死人的。 查看详情

diocp之开发流程图之client

本次分析开发流程图采用的是DIOCP群里的群友[彩蛋]所给的DEMO,依然是win7的画图作品。本人分析认为:学习网络开发不同本地开发,首先你应该知道完整的开发流程即网络程序运行的先后顺序,有个整体感知,不能一头深入到代... 查看详情

diocp之demo-登陆验证设计

登陆设计原理:(一)client在与服务器建立连接后向服务器发送含有"USER=XXXXX&PWD=XXXXX"或者用JSON字符串。   client的代码需要写在OnContextConnected事件中,注{"cmd":"login","User":"admin","Pwd":"admin888"}(二)服务器收到客户端传来的... 查看详情

diocp知识库连接上下文tiocpclientcontext

来自弦子介绍【概述】    该类管理远程连接,每一个远程连接会对应一个该类的实例,开发时可以通过继承该类,扩展属性,可以存储更多的连接信息,可以重写OnRecvBuffer方法进行数据逻辑的处理 【字段/属... 查看详情

diocp之编写第一个应用程序

Client设计功能如下:  1、建立与服务器连接  2、请求连接时,加密密码,采用Base64编码  3、时时发送心跳告诉服务器在线  4、进行相关的数据处理与交互第一步:创建一个VCL-FormsApplication(创建一个... 查看详情

区块链原理及核心技术

文章目录前言一、运作机制1.1比特币系统运作机制1.1.1比特币核心数据结构——账本1.1.2比特币运作机制1.2区块链系统运作机制1.2.1区块链核心数据结构1.2.1.1链式结构1.2.1.2树状结构1.2.2区块链系统角色1.2.3区块链运作机制二、核心... 查看详情