天马阁

 找回密码
 立即注册
                                        →→→→→→→→→→→→ 1点击查看所有VIP教程目录长列表(总教程数269个) 2办理VIP详情进入 ←←←←←←←←←←←←
1 x64CE与x64dbg入门基础教程 7课 已完结 2 x64汇编语言基础教程 16课 已完结 3 x64辅助入门基础教程 9课 已完结 4 C++x64内存辅助实战技术教程 149课 已完结
5 C++x64内存检测与过检测技术教程 10课 已完结 6 C+x64二叉树分析遍历与LUA自动登陆教程 19课已完结 7 C++BT功能原理与x64实战教程 29课 已完结 8 C+FPS框透视与自瞄x64实现原理及防护思路 30课完结
64驱?封? 9 64反驱? 10 64位V? 11 绝? 12 ???课?
13 64透 ? 14 64U ? 15 64Q ? 16 64功 ?
17 64U ? 18 64模 ? 19 64多 ? 20 64网 ?
21 64注 ? 22 64火 ? 23 64棋 ? 24 64自二链L?
25 64破 ? VIP会员办理QQ: 89986068   
【请先加好友,然后到好友列表双击联系客服办理,不然可能无法接受到信息。】
27 加入2000人交流群637034024 3 28 免责声明?
查看: 6307|回复: 0

IOCP异步TCP服务器模型讲解

[复制链接]

13

主题

0

回帖

16

积分

编程入门

Rank: 1

天马币
26
发表于 2024-3-1 14:07:12 | 显示全部楼层 |阅读模式
注意:阅读本文章需要有一定的winsock开发基础。

例程源码在最下面在众多的服务器模型中IOCP异步模型无疑是最高效的服务器模型,是其他模型无法比拟的,一些高效稳定的服务器基本都是用IOCP模型的 例如 Apache。

什么是IOCP?IOCP我们可以将它看成一个队列  I/O是一个硬件设备详情介绍请看百度百科。

我们先用普通同步模型的服务器结构做对比

同步模型流程:

1.WSAStartup() 加载winsock服务
2.Socket()/WSASocket() 创建一个套接字
3.Listen() 设置监听模式
4.建立一个线程进行无限循环的Accept()
5.Accept得到连接后建立一个线程与客户通讯

这样的流程很明显并不高效,因为每个客户都要创建一个线程与之通讯,当接受到大量的客户进入的时候就挂了。

那么IOCP模型是怎样的呢? 。

IOCP模型:

1.WSAStartup() 加载winsock服务
2.Socket()/WSASocket() 创建一个套接字
3.CreateIoCompletionPort() 创建一个完成端口
4.Listen() 设置监听模式
5.CreateIoCompletionPort() 将sock与IOCP绑定
6.建立Worker线程(具体这个线程中是干什么的看下面)
7.建立Accept线程(为了更好的讲解暂时使用Accept,下面将介绍扩展的 AcceptEx)

这样看起来两个模型并没有什么区别,但重点在下面

同步模型的Accept线程在得到一个客户连接后将会建立与其通讯的线程;
与同步模型的Accept线程不同的是IOCP模型的在得到客户连接后将会把客户的Socket绑定到IOCP然后投递一个WSARecv操作

IOCP的重点来了 同步模型的Recv操作是同步的,需要等待接到消息或者超时后才返回,但IOCP模型的是投递一个Recv操作  注意 是投递!

那么为什么叫投递呢?  我们来对比上面说了同步模型的Recv需要等待接到消息或者超时后才返回,而IOCP模型的则是投递一个WSARecv操作,这个WSARecv操作将会由系统来帮你完成;当系统接收到消息后将会把消息放到IOCP里面,我们只需要调用 GetQueuedCompletionStatus()来获取消息。

那么我们来看看Worker线程都干了些什么?

很简单 Worker线程只不过是在不断的调用GetQueuedCompletionStatus() 获取来自IOCP的消息
然后根据消息做出不同操作。这里我们暂时不提,因为我们还不知道他是如何获取消息的。

下面讲解 WSARecv() 和 GetQueuedCompletionStatus() 以及IOCP模型最重要的重叠结构!

WSARecv()  用于接收套接口的消息
参数1:s 客户的套接字
参数2:pBuffers  一个WSABUF结构 WSABUF 有2个成员 成员1:len 表明缓冲区长度 成员2:一个指针地址 指向接收数据的缓冲区  这个参数可以是一个数组 因为他可以一次接收多个消息。或者你也可以不传入数组到那下面的参数将要设置为1
参数3:wBufferCount WSABUF数组的数量
参数4:lpFlags  和用来控制套接字的行为 通常设置为0
参数5:lpOverlapped 重叠结构指针地址 重要! 下面讲解
参数6:lpCompletionRoutine 这个我们之间设置0 不管他

GetQueuedCompletionStatus() 用于接收IOCP的消息
参数1:CompletionPort  IOCP
参数2:NumberOfBytesTransferred  整数型传址 用于接收IOCP得到的消息的长度
参数3:CompletionKey  这类似一个线程中的参数 是由 PostQueuedCompletionStatus()传递过来的
参数4:lpOverlapped   重叠结构指针地址
参数5:dwMilliseconds 超时时间 -1表示无限等待

重叠结构:

.数据类型 OVERLAPPED
    .成员 Internal, 整数型
    .成员 InternalHigh, 整数型
    .成员 offset, 整数型
    .成员 OffsetHigh, 整数型
    .成员 hEvent, 整数型

这是基本的重叠结构 这些我们都不需要管,重叠结构之所以最重要是因为我们传递很多东西都要靠它了,例如接收的数据是 WSARecv操作还是AcceptEx操作或者WSASend操作,还有接收数据的缓冲区指针地址啊之类的,很多东西我们可以靠它来传送。

首先我们看 WSARecv() 他的参数5是一个重叠结构的指针地址,而我们使用的GetQueuedCompletionStatus()参数4
就是用来接收这个指针地址的,那么我们这样想只要这个内存区没被释放,我们就可以操作他,那么我们可以在基本的重叠结构后面加上一些我们自己的数据,这样我们在使用GetQueuedCompletionStatus()得到重叠结构地址的时候那么我们的数据不都一样全都传过来了! 这是个非常好的东西。那么我们怎么用呢?

首先我们使用HeapAlloc来为程序分配一块内存,这块内存的长度我们根据要传入的数据来做决定 这里我们示例分配48字节的内存。

其结构:

OVERLAPPED 结构 20字节 这是固定的必须的 留给操作系统 我们不管
MsgType    消息类型,用于我们区分是AcceptEx投递的操作还是WSARecv等投递的操作 4字节 整数型
C_Socket   存放客户套接字 4字节 整数型
PeerAddr   远端客户的地址信息 16字节
Buf_Ptr    缓冲区的内存指针地址  4字节 整数型

好了,我们该投递WSARecv操作了。

WSARecv (C_Sock, WSABuf, 1, NumberOfBytesRecvd, Flags, Overlapped, 0)

我们这样的调用WSARecv来投递一个Recv操作,WSABUF结构中的 缓冲区指针地址成员我们可以使用HeapAlloc来分配一块内存 这里我们给他分配4096字节(即4KB)

我们再创建一个48字节的内存块 并写入 以上介绍的数据结构中的所需数据 再将这个内存块的地址放入WSARecv的参数lpOverlapped 这样我们就投递了一个Recv操作,接下来他将由系统完成。

在Worker线程中我们使用GetQueuedCompletionStatus()来获取IOCP中的消息,它得到的重叠结构的指针地址将与我们创建的一样 所以我们可以从这块内存中取出 我们事先写入的数据 我们先取出 PeerAddr 和C_Socket得到这个客户的地址信息和sock,接下来我们取出 Buf_Ptr 我们之前在这里写入了一块内存的指针地址并作为WSABUF中的成员传入了WSARecv的参数 这时 WSARecv所接收到的数据将写入这块内存。
然后我们需要再次投递一个WSARecv操作以便接收下个来自此客户的数据。

关于Worker线程的数量 一般认为最合适的是 CPU核心数×2 因为CPU只有那么些核心太多了Worker线程也没用,乘以2是因为如果某个Worker线程处在Sleep状态那么另一个便可以代替他,从而更好的利用CPU。

AcceptEx的使用:

Accept()与AcceptEx()最大的区别在于AcceptEx可以像WSARecv一样投递给IOCP 并且还能更好的利用Socket资源减少写创建Socket所浪费的时间。

AcceptEx()
参数1:sListenSocket  服务监听的套接字
参数2:sAcceptSocket  一个将用于新客户的套接字
参数3:lpOutputBuffer 一个内存缓冲区 这个内存缓冲区将用于接收客户的地址信息 和接收客户发来的第一条消息 (如果指定了的话) 这个参数必须指定 否则将返回错误
参数4:dwReceiveDataLength  指定欲接收来自新客户的第一条消息的缓冲区长度 若此值是零 则windows不会等待新客户的第一条消息 而是立即建立连接
参数5:dwLocalAddressLength  为本地地址信息保留的字节数。此值必须比所用传输协议的最大地址大小长16个字节。
参数6:dwRemoteAddressLength为远程地址的信息保留的字节数。此值必须比所用传输协议的最大地址大小长16个字节。 该值不能为0。
参数7:dwBytesReceived 此参数只在同步模式下起作用,所以我们直接给个0
参数8:lpOverlapped    重叠结构指针地址

使用了AcceptEx的话,我们就不必再去创建Accept的线程而是直接投递一个Accept操作。当有客户连接的时候Windows会将消息写入IOCP。我们便可以从调用AcceptEx时指定的缓冲区使用GetAcceptExSockaddrs()取出客户的地址信息。在从重叠结构冲取出我们所需的信息即可。

当Worker接收到AcceptEx消息时,我们需要再次使用同样的方法投递一个Accept操作。
那么为什么说AcceptEx()能更好的利用Socket资源呢?
注意看AcceptEx的第二的参数 与Accept不同的是 Accept接受到连接系统会自动的为客户创建一个Socket 而AcceptEx则是由自己传入一个已创建的Socket 那么我们就可以使用Socket池技术 以减少Socket创建所浪费的时间
可惜的是本文章中的例程并未用到此技术。

PostQueuedCompletionStatus()的使用

PostQueuedCompletionStatus()可以向IOCP发送一条消息以便Worker线程做出相应的处理,例如我们要退出程序了而Worker线程还是处在无限循环状态,我们就可以通过这个函数发送消息给Worker让他退出。

PostQueuedCompletionStatus()
参数1:CompletionPort IOCP
参数2:dwNumberOfBytesTransferred 指定发送的消息的字节数
参数3:dwCompletionKey   这类似于线程的参数,将会传递到GetQueuedCompletionStatus()中的参数CompletionKey
参数4:lpOverlapped  重叠结构指针地址

同样的是Send操作也可以投递给系统去完成 函数声明如下

WSASend()

参数

s:标识一个已连接套接口的描述字。
lpBuffers:一个指向WSABUF结构数组的指针。每个WSABUF结构包含缓冲区的指针和缓冲区的大小。
dwBufferCount:lpBuffers数组中WSABUF结构的数目。
lpNumberOfBytesSent:如果发送操作立即完成,则为一个指向所发送数据字节数的指针。
dwFlags:标志位。
lpOverlapped:重叠结构指针
lpCompletionRoutine:一个指向发送操作完成后调用的完成例程的指针。(对于非重叠套接口则忽略)。

现在我们再回观全文 你是否发现了IOCP模型与同步模型的差距? 同步模型需要为每个客户都建立一个线程,这将极大的消耗系统资源! 而IOCP模型仅仅只需要一个或几个线程就可以完美的解决!

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

天马阁|C/C++辅助教程|安卓逆向安全| 论坛导航|免责申明|Archiver||网站地图
拒绝任何人以任何形式在本论坛发表与中华人民共和国法律相抵触的言论,本站内容均为会员发表,并不代表天马阁立场!
任何人不得以任何方式翻录、盗版或出售本站视频,一经发现我们将追究其相关责任!
我们一直在努力成为最好的编程论坛!
Copyright© 2010-2021 All Right Reserved.
快速回复 返回顶部 返回列表