NIUCLOUD是一款SaaS管理后台框架多应用插件+云编译。上千名开发者、服务商正在积极拥抱开发者生态。欢迎开发者们免费入驻。一起助力发展! 广告
# 2.1关键概念 **2.1.1接收模型** * * * * * 如表1.2-1所示,HP-Socket的TCP组件支持PUSH、PULL和PACK三种接收模型: **PUSH模型**:组件接收到数据时会触发监听器对象的OnReceive(pSender, dwConnID, pData, iLength) 事件,把数据“推”给应用程序。 **PULL模型**:组件接收到数据时会触发监听器对象的OnReceive(pSender, dwConnID, iTotalLength) 事件,告诉应用程序当前已经接收到多少数据,应用程序检查数据的长度,如果满足需要则调用组件的Fetch(dwConnID, pData, iDataLength) 方法把需要的数据“拉”出来。 **PACK模型**:PACK模型系列组件是 PUSH和PULL 模型的结合体,应用程序不必处理分包(如:PUSH)与数据抓取(如:PULL),组件保证每个 OnReceive 事件都向应用程序提供一个完整数据包。 三种模型的比较如图2.1.1-1所示: ![](https://box.kancloud.cn/c7c97b30b5903e074edf1ad0e6fa06af_846x1519.png) PUSH模型组件触发监听器对象的OnReceive(pSender, dwConnID, pData, iLength) 事件时,应用程序需要立即处理接收到的数据,如:粘包处理、协议解析等。组件不会对应用层的数据处理工作提供任何协助。 PULL模型组件触发监听器对象的OnReceive(pSender, dwConnID, iTotalLength) 事件时,应用程序根据应用层协议检测接收到的数据长度(iTotalLength)是否满足处理条件,选择性地进行处理。当iTotalLength小于当前期望的长度时可以忽略本次事件;当iTotalLength大于或等于当前期望的长度时,循环调用组件的Fetch(dwConnID, pData, iDataLength) 方法把需要的数据拉取出来,直到剩余的数据长度小于当前期望的长度。 Fetch(dwConnID, pData, iDataLength) 方法返回值的类型为EnFetchResult: ![](https://box.kancloud.cn/1a7cd07ed49ab7d41b3a1a3ee21182ef_321x207.png) **FR_OK :成功拉取 HR_LENGTH_TOO_LONG :拉取的长度超过实际数据长度 HR_DATA_NOT_FOUND :没有数据可拉取,可能连接已被关闭** * * * * * > 注意:只有当Fetch(dwConnID, pData, iDataLength)方法返回FR_OK时,数据才会被拉取出来。另外,PULL模型组件还提供Peek(dwConnID, pData, iDataLength)方法用于窥探接收缓冲区,该方法不会移除缓冲区数据。 > PULL模型适用于完全清楚应用层协议,并且应用层协议可以根据当前数据包得知下一个数据包长度的场景。典型的场景如Head + Body,Head长度固定,第一个数据包为Head,通过Head得知Body的长度,接收完Body之后下一个数据包一定为Head。 > 注意:通过PULL模型与应用层协议的相互配合,使得应用程序可以免除粘包处理和分拆包工作,从而减少应用程序的负担。 > PACK模型组件触发监听器对象的OnReceive(pSender, dwConnID, pData, iLength) 事件时,会保证pData是一个完整的数据包。PACK模型组件会对应用程序发送的每个数据包自动加上4字节(32位)的包头,组件接收到数据时根据包头信息自动分包,每个完整数据包通过OnReceive事件发送给应用程序。 PACK包头格式: **XXXXXXXXXX** YYYYYYYYYYYYYYYYYYYYYY 前10位X为包头标识位,用于数据包校验。有效包头标识取值范围0 ~ 1023(0x3FF),当包头标识等于 0 时不校验包头。后22位Y为长度位,记录包体长度。有效数据包最大长度不能超过4194303(0x3FFFFF)字节,默认长度限制为:262144(0x40000)字节。应用程序可以通过SetPackHeaderFlag() 和SetMaxPackSize() 分别设置包头标识与最大包长限制。 **2.1.2发送策略** 对于IClient系列组件,当应用程序调用组件的Send()、SendPackets()、SendSmallFile() 方法发送数据时,组件内部会把数据缓存起来,在适当的时机再发送出去。 对于IServer和IAgent系列组件,当应用程序调用组件的Send()、SendPackets()、SendSmallFile() 方法发送数据时,根据不同的发送策略会有不同的处理方式。 (发送策略通过SetSendPolicy(enSendPolicy) 方法进行设置) ![](https://box.kancloud.cn/0c3defab6c6fe0681f864842ba6574ab_248x207.png) **SP_PACK :打包策略(默认) 尽量把多个发送操作的数据组合在一起发送,增加传输效率。 SP_SAFE :安全策略 尽量把多个发送操作的数据组合在一起发送,并尽量避免缓冲区溢出。 SP_DIRECT :直接策略 对每一个发送操作都直接投递,适用于负载不高但要求实时性较高的场合。 注:SP_DIRECT通常与TCP_NODELAY Socket选项配合使用来获得最低延时。TCP_NODELAY Socket选项可以通过HP-Socket导出函数SYS_SSO_NoDelay()设置。** 对于SP_PACK和SP_SAFE策略,组件内部会缓存待发送的数据,应用程序可以调用组件的GetPendingDataLength(dwConnID, iPending) 方法获取指定连接的未发出数据量,实现流量控制;对于SP_DIRECT策略,组件不会缓存待发送数据,如果要实现流量控制则需要比较组件通过Send()、SendPackets()、 SendSmallFile() 方法提交的数据量与OnSend(pSender, dwConnID, pData, iLength) 事件报告的实际发出的数据量。 * * * * * **注意:基于Linux的Socket模型特点,Linux平台的通信组件不支持发送策略设置,所有Linux通信组件的发送策略均为SP_PACK。也就是说,SetSendPolicy(enSendPolicy)方法对Linux通信组件无效。** **2.1.3接收策略** 对于IClient系列组件,每个组件对象都在单独的通信线程执行所有通信工作,OnReceive与Onclose事件不可能同时触发,因此不必担心在处理OnReceive事件时某些共享数据被Onclose事件处理代码意外修改或释放的情形。 对于IServer和IAgent系列组件,由于有多个通信线程同时工作,对于同一连接,在触发OnReceive事件时可能同时触发了OnClose事件,因此,共享数据存在被意外修改或释放的可能。为了应对这个问题,HP-Socket v3.3及其之前的版本提供了以下两种接收策略(通过SetRecvPolicy(enRecvPolicy) 方法设置): ![](https://box.kancloud.cn/120bfeac0f2e797cae0044168b9d3aaf_248x181.png) **RP_SERIAL :串行策略(默认) 对于单个连接,顺序触发OnReceive和OnClose等事件。降低应用程序处理的复杂度,增强安全性;但同时损失一些并发性能。 RP_PARALLEL :并行策略 对于单个连接,同时收到OnReceive和OnClose事件时,会在不同的通信线程中同时触发这些事件,使并发性能得到提升,但应用程序需要考虑在OnReceive的事件处理代码中,某些公共数据可能被 OnClose的事件处理代码修改或释放的情形,程序代码逻辑会变得复杂,处理不好时将会产生代码缺陷。除非有充足的理由并且完全能避免RP_PARALLEL策略所带来的隐患,否则不建议应用程序使用RP_PARALLEL策略。** HP-Socket v3.4版本开始,废弃了RP_PARALLEL接收策略。HP-Socket保证除了OnSend事件以外(因为OnSend事件通常用处不大)其它所有事件都是线程安全的。 * * * * * **注意:对于HP-Socket的所有组件(IServer / IAgent / IClient),当连接触发了OnClose事件时,表示连接已被关闭。并且OnClose事件只会触发一次,也就是说:同一个连接不可能收到两个或多个OnClose事件。 HP-Socket v3.3及其之前版本中提供了OnClose和OnError两个事件,分别表示正常关闭和异常关闭连接。HP-Socket v3.4开始,这两个事件已合并为一个事件,应用程序可根据OnClose事件的iErrorCode参数判断是正常关闭还是异常关闭。** **2.1.4连接方式** HP-Socket所有组件的通信过程都是异步的,如:调用组件的Send() 方法会立即返回,稍后监听器会接收到OnSend() 事件获知发送了多少数据,或者会接收到OnClose() 事件可获知发送失败原因。 但HP-Socket的IClient和IAgent组件向服务器发起连接的过程可以是同步或异步的。同步是指组件的连接方法(IClient - Start(),IAgent - Connect())等到建立连接成功或失败了再返回(返回TRUE或FALSE)。 异步连接是指组件的连接方法Start() / Connect() 会立即返回,如果Start() / Connect() 返回成功(TRUE)则稍后会接收到OnConnect() 或OnClose() 事件,收到前者则说明连接成功,收到后者则说明连接失败。注意:如果Start() / Connect() 返回失败(FALSE)则稍后不一定能接收到OnClose() 事件。因此,对于异步连接也必须检查Start() / Connect() 的返回值,当返回失败(FALSE)则立即可以断定连接失败。 **IClient建立连接方法:** `BOOL Start(lpszRemoteAddress, usPort, bAsyncConnect = TRUE, lpszBindAddress = nullptr)` 参数bAsyncConnect指示是否采用异步连接方式(默认:TRUE),如果Start()方法返回失败可以调用组件的GetLastError() 和GetLastErrorDesc() 方法获取错误代码和错误描述。如果Start()方法返回成功可以调用组件的GetConnectionID() 方法获取当前连接的Connection ID。 * * * * * **注意:IUdpCast组件的Star()方法忽略bAsyncConnect参数。** ~~~ IAgent建立连接方法: BOOL Start(lpszBindAddress = nullptr, bAsyncConnect = TRUE) BOOL Connect(lpszRemoteAddress, usPort, pdwConnID = nullptr, pExtra = nullptr) ~~~ Start() 方法启动IAgent组件并指定连接方式,参数bAsyncConnect指示是否采用异步连接方式(默认:TRUE),如果Start() 方法返回失败可以调用组件的GetLastError() 和GetLastErrorDesc() 方法获取错误代码和错误描述。**注意:Start() 方法在整个通信周期中只需调用1次。** Connect() 方法与指定服务器建立连接,参数pdwConnID用来获取本连接的Connection ID(默认:nullptr,不获取),参数pExtra设置“连接绑定”数据(默认:nullptr,不设置),如果Connect() 方法返回失败可以调用Windows API函数 ::GetLastError() 获取Windows错误代码;如果设置了pExtra,这时也要手工释放。 无论是同步或异步连接,成功完成连接的过程中都会先后触发监听器的两个事件: **OnPrepareConnect(pSender, dwConnID, socket) OnConnect pSender, dwConnID)** 其中OnPrepareConnect(pSender, dwConnID, socket) 在发起连接前触发,socket是本地SOCKET句柄,可以在该事件中通过setsockopt() / WSAIoctl() 等方法设置SOCKET选项。OnConnect(pSender, dwConnID) 则在连接建立成功后触发。 **2.1.5连接绑定** 对于IClient系列组件,一个组件对象对应一个Connection ID和一个通信连接,因此很容易把通信连接与应用层数据关联起来。应用程序与组件交互时,直接通知组件处理数据即可(如:Send( pData, iLength))。如图2.1.5-1所示: ![](https://box.kancloud.cn/2e6955ac400f6a95ee276b2a874aa2fd_634x385.png) * * * * * > **注意:对于IClient组件,可以通过SetExtra() / GetExtra() 方法绑定、获取附加数据** * * * * * 对于IServer和IAgent系列组件,一个组件对象管理多个通信连接,HP-Socket把通信连接抽象为Connection ID,应用程序与组件交互时,需要指定Connection ID来告知组件处理哪个连接(如:Send(dwConnID, pData, iLength) )。如图2.1.5-2示: ![](https://box.kancloud.cn/38e856e17a105c9e9dc41315432d99ea_702x263.png) 应用程序为了建立Connection ID与应用层数据的对应关系通常需要维护一张映射表(如:map<CONNID, TMyAppData*>),从而不但增加了应用程序的负担;另外,由于运行在多线程环境下,对映射表的读写操作需要进行同步处理,从而降低了应用程序的并发性能。 HP-Socket为IServer和IAgent系列组件提供以下方法组绑定Connection ID和应用层数据,尽量避免让应用程序维护映射表。 * * * * * ~~~ BOOL SetConnectionExtra(CONNID dwConnID, PVOID pExtra) BOOL GetConnectionExtra(CONNID dwConnID, PVOID* ppExtra) ~~~ * * * * * 通常的应用情景如下: 1)在OnAccept() / OnConnect() 事件中调用SetConnectionExtra(dwConnID, pExtra) 把Connection ID和应用层数据进行绑定。 2)在OnReceive() / OnSend() 事件中调用GetConnectionExtra(dwConnID, ppExtra) 取出与Connection ID绑定的应用层数据,执行相应业务逻辑处理。 3)在OnClose() 事件中取消Connection ID和应用层数据的绑定,清除应用层数据并释放资源。 > 注意:由于HP-Socket已经确保了OnReceive() / OnClose() 等事件的线程安全,因此应用程序可以放心使用连接绑定机制,不用担心同步问题。