重叠IO模型
基于Windows提供的一种异步读写文件机制,注意socket
的本质就是一个文件描述符;所以异步读写机制也适用于读写其他文件、串口等。
所谓重叠IO模型
中的重叠
指的是该机制在使用时所需要的一个结构体:WSAOVERLAPPED
,在使用重叠IO模型
时我们主要用了该结构体中的第5个成员
struct _WSAOVERLAPPED {
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
WSAEVENT hEvent; //前四个成员用不到,主要用的是hEvent成员,也即事件对象
};
如何理解异步读写文件呢?
①与异步读写相对的就是同步读写:例如recv()
函数要把协议缓冲区中的1000个字节读到程序中的buffer[]
中。这个过程需要花费一定的时间,而在这段时间里,程序会阻塞在recv()
函数位置,等待到recv()
将1000字节数据全部读完并返回,这就是所谓的同步读;同步写也是同样的道理。
②异步读写则是将读写任务投递给操作系统完成,等到操作系统完成后以某种方式通知我们,我们再回来处理。因此程序代码不必阻塞在读写操作
上,也就提高了程序的执行效率。
上边所说的某种方式,在重叠IO网络模型
中就是事件event
,通知方式就是将事件设置为有信号。
重叠IO模型到底解决了什么问题呢?
基本的C/S
模型,到处都是阻塞的,accept()
函数傻等阻塞、recv()
函数傻等阻塞、recv()
和send()
函数均存在的执行阻塞
select模型
通过FD_SET
这样的socket集合,主动查询哪些socket是有信号、有动作的,它解决了accept()
和recv()
傻等阻塞的问题,但是select()
函数本身的执行阻塞也是一个大问题,如果socket非常多,访问量非常高,select模型就非常糟糕了
事件选择模型
是基于事件机制的select模型的升级版,其通过将事件对象event
、socket
、实际的动作
绑定在一起,然后投递给操作系统。并由操作系统监测事件的发生,程序员定时去检查有信号的事件,然后分类处理即可。这就解决了select模型的执行阻塞问题。
异步选择模型
是基于消息机制的select模型的升级版,其通过将消息message
、socket
、实际的动作
绑定在一起,基于win窗口的消息队列,由操作系统监测消息并将消息装进消息队列,程序员只需挨个取出消息message
并分类处理即可,这同样也解决了select模型的执行阻塞问题。
但是无论上述怎么优化,都存在一个非常关键的问题:
recv()
和send()
函数的执行阻塞问题一直都存在。我们又该怎么解决呢?
此时设想一下我们是怎么解决掉select()函数的执行阻塞问题呢?
答案就是:靠
操作系统
托管;靠将任务丢给操作系统
;最终还是操作系统承担了一切!专业一点就是异步解决阻塞这就类似于: 下班回家后,将脏衣服丢进全自动洗衣机里,然后去快乐的打王者;等衣服洗好了,洗衣机发出提示音(嘀嘀嘀~或音乐),我们就可以选择立马放下手机或者打完这局游戏,然后把衣服取出来,这就是一个异步的、由洗衣机托管了洗衣任务的类比!
同样,那思考一下:recv()
和send()
函数可不可以由操作系统托管呢?实现收发数据由操作系统完成,程序不至于阻塞呢?
答案就是: 可以!重叠IO模型解决的就是
recv()
、send()
、以及accept()
的阻塞问题;直接交给操作系统托管。操作系统完成之后用OVERLAPPED
结构实现结果反馈;然后程序员再分类处理即可。该模型使用
AcceptEx()
、WSARecv()
、WSASend()
函数实现接收连接、收发数据的异步性
重叠IO模型的使用逻辑(事件通知反馈结果)
OVERLAPPED
结构体与socket绑定AcceptEx()
、WSARecv()
、WSASend()
将接收连接任务、收发数据任务等投递给操作系统完成WSAWaitForMultipleEvents()
查询是否有事件被置位有信号的WSAGetOverlappedResult()
查询信号结果,根据传出参数以及返回值确定事件类型,然后分类处理。除了事件通知反馈结果,还可以使用 回调函数反馈结果
,即调用AcceptEx()
、WSARecv()
、WSASend()
将接收连接任务、收发数据任务等投递给操作系统完成,完成后直接自动调用回调函数处理
AcceptEx()
函数的原型以及执行逻辑:
// 投递给操作系统异步接收连接请求函数
BOOL AcceptEx(
SOCKET sListenSocket, // 服务器socket
SOCKET sAcceptSocket, // 客户端通信socket,传进来的是一个未绑定任何信息的socket,传出的是一个绑定了客户端信息的socket
PVOID lpOutputBuffer, // 缓冲区指针,接收新连接上发来的第一组数据,不能填NULL
DWORD dwReceiveDataLength, // 设置0可取消参数3的功能;设置成参数3的长度,可接受数据;并且接收完数据之后OVERLAPPED才会有信号
DWORD dwLocalAddressLength, // sizeof(struct sockaddr_in) + 16
DWORD dwRemoteAddressLength, // sizeof(struct sockaddr_in) + 16
LPDWORD lpdwBytesReceived, // 同步接收到数据时,该参数会装着接收到数据的字节数,其他时候没有用
LPOVERLAPPED lpOverlapped // 参数1 socket对应的重叠结构的地址
);
AcceptEx()
函数调用有两种情况:
WSARecv()
以及WSASend()
WSARecv()
与WSASend()
函数的原型与执行逻辑:
// 返回值:0 表示立即完成; SOCKET_ERROR 表示出错(注意客户端强制退出也属于错误)
int WSAAPI WSARecv(
SOCKET s, // 客户端通信socket
LPWSABUF lpBuffers, // 接收到的数据存储位置
DWORD dwBufferCount, // 参数2 WSABUF的个数
LPDWORD lpNumberOfBytesRecvd, // 接收成功时,这里存储着接收到的字节数
LPDWORD lpFlags, // 指向用于修改WSARecv()函数行为的标志的指针 可以填0
LPWSAOVERLAPPED lpOverlapped, // 参数1socket 对应的重叠结构
LPWSAOVERLAPPED_COMPLETION_ROUTINE
lpCompletionRoutine // 回调函数:用于完成例程模型;事件通知时填NULL
);
// 返回值:0 表示立即完成; SOCKET_ERROR 表示出错(注意客户端强制退出也属于错误)
int WSAAPI WSASend(
SOCKET s, // 客户端通信socket
LPWSABUF lpBuffers, // 发送数据缓冲区
DWORD dwBufferCount, // 参数2的WSABUF的个数
LPDWORD lpNumberOfBytesSent, // 发送成功时,这里存储着发送成功的字节数
DWORD dwFlags, // 指向用于修改WSASend()函数行为的标志的指针 可以填0
LPWSAOVERLAPPED lpOverlapped, // 参数1 socket对应的重叠结构
LPWSAOVERLAPPED_COMPLETION_ROUTINE
lpCompletionRoutine // 回调函数
);
WSARecv()
函数调用有两种情况:
WSARecv()
函数WSASend()
函数调用有两种情况:
WSASend()
函数WSAWaitForMultipleEvents()
函数的原型与使用逻辑:
DWORD WSAAPI WSAWaitForMultipleEvents(
DWORD cEvents, // 参数1:事件个数
const WSAEVENT *lphEvents, // 参数2:事件列表(集合数组)
BOOL fWaitAll, // 参数3:true 等待所有事件均产生信号才返回; false 任意一个事件产生信号就返回
DWORD dwTimeout, // 参数4:超时时间
BOOL fAlertable // 参数5:在重叠IO模型的完成例程模型中填true, 其他模型false
)
// 返回值:事件集合数组中有信号事件的下表值
// 如果参数3为true,表示所有事件均有信号
// 如果参数3为false,表示有信号的事件中下表索引最小的那个事件的索引的运算值,实际的索引 == 返回值 - WSA_WAIT_EVENT_0
// 返回值为WSA_WAIT_IO_COMPLETION,表示在完成重叠IO模型中的完成例程(回调函数时)
// 返回值为WSA_WAIT_TIMEOUT,超时了
使用逻辑:
重叠IO模型
中使用这句代码WSAWaitForMultipleEvents(1, &(g_allOlp[i].hEvent), FALSE, 0, FALSE);
每次查看一个事件。WSAGetOverlappedResult()
函数的原型与使用逻辑:
BOOL WSAAPI WSAGetOverlappedResult(
SOCKET s, // 参数1[in]:socket
LPWSAOVERLAPPED lpOverlapped, // 参数2[in]:重叠结构
LPDWORD lpcbTransfer, // 参数3[out]:发送或接收到的实际字节个数;0表示客户端正常下线
//(在调用该函数时,必然是事件有信号,因此应该先判断是否是有连接事件ACCEPT;如果确定是客户端事件,那么基本就是3种情况:发送数据、接收数据、ClLOSE也即正常下线)
BOOL fWait, // 参数4[in]:仅当重叠IO模型中选择事件通知时,才填true,其余填写false
LPDWORD lpdwFlags // 参数5[out]:装WSARecv的参数5 lpflags
);
// 返回值:true 表示执行成功 false 表示执行失败
通常可以根据参数3、socket类型以及接收缓冲区等,区分事件类型并分类处理。
上述所讲基于事件通知,利用 WSAWaitForMultipleEvents()
函数 和 WSAGetOverlappedResult()
函数等待事件并解析,然后分类处理的情况是属于重叠IO的事件通知机制
,此外还存在一种重叠IO的更高效的处理方式----完成例程。
重叠IO模型之完成例程
所谓的完成例程
,其实就是利用回调函数
完成数据收发后的工作
是 WSARecv()
与WSASend()
函数的最后一个参数,回调函数必须按照一定的规则声明并编写实现:
void CALLBACK funcName( // CALLBACK 调用约定 funcName可以随意书写
DWORD dwError, // 错误码,也即在调用WSARecv()与WSASend()函数时出现错误了,可以使用该参数判断客户端强制退出
DWORD cbTransferred, // 发送或接收到的字节数
LPWSAOVERLAPPED lpOverlapped, // 重叠结构
DWORD dwFlags // WSARecv()与WSASend()这个函数的执行方式,也即其参数5
);
AcceptEx()
没有回调函数的参数,因此在投递服务器socket和重叠结构之后,还需要使用WSAWaitForMultipleEvents()
函数去等待服务器socket
上发生的事件,也即有客户端请求连接。
如何回答:讲讲你所理解的重叠IO网络模型?
重叠IO网络模型
基于异步文件读写机制,socket本质也属于一种特殊文件。他解决的是例如recv函数,send()函数的执行阻塞问题。
将socket与重叠结构绑定,并通过异步函数WSARecv()
、WSASend()
、AcceptEx()
等函数将接收连接、收发数据等任务投递给操作系统,操作系统会帮我们完成相应的任务,之后再通过将重叠结构OVERLAPPED中的事件置成有信号的方式通知应用程序
WSARecv()
或WSASend()
;为WSARecv()
和WSASend()
函数编写回调函数,用于告诉操作系统在收发完成数据之后要做什么工作。实际上,事件通知时程序员要做的事情更多,且效率要低于完成例程。
异步选择模型与重叠IO模型的流程图