IOCP异步TCP服务器模型讲解
注意:阅读本文章需要有一定的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:CompletionPortIOCP
参数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模型仅仅只需要一个或几个线程就可以完美的解决!
页:
[1]