目录
1. 什么是重叠I/O模型
2. 重叠I/O模型的实现
2.1 创建重叠非阻塞I/O模式的套接字
2.2 执行重叠I/O的Send函数
2.3 执行重叠I/O的Recv函数
2.4 获取执行I/O重叠的函数的执行结果
2.5 重叠I/O的I/O完成确认
2.5.1 使用事件对象(使用重叠I/O函数的第六个参数)
2.5.2 使用Completion Routine函数(使用重叠I/O的第七个参数)
3. 用重叠I/O实现回声服务器端
1. 什么是重叠I/O模型
重叠I/O模型:
重叠I/O:同一线程内部向多个目标传输(或从多个目标接收)数据引起的I/O重叠现象。
所以为了完成这一功能,要求套接字的I/O函数要立即返回,以便于后面的套接字的I/O处理。这就有点像是异步I/O模型。如图:
异步I/O模型:
所以,从结果上来看,重叠I/O的前提条件就是异步I/O。
非阻塞I/O、异步I/O、重叠I/O之间的关系:重叠I/O离不开异步I/O,异步I/O离不开非阻塞I/O,三者之间应该是层层递进的关系。
2. 重叠I/O模型的实现
重叠I/O的重点不在于I/O,因为只要是非阻塞I/O就都能调用并立即返回,我们要关注的是在I/O返回后,我们怎么确认它的执行结果,怎么知道它什么时候读取/发送数据结束,怎么知道它读取/发送了多少数据?这些问题。
2.1 创建重叠非阻塞I/O模式的套接字
#include<winsock2.h>
SOCKET WSASocket(
int af, //协议族信息
int type, //套接字数据传输方式
int protocol, //使用的协议信息
LPWSAPROTOCOL_INFO lpProtocolInfo, //包含创建的套接字信息的WSAPROTOCOL_INFO结构体变量地址值
//不需要时传NULL
GROUP g, //为扩展函数而预约的参数,可以使用0
DWORD dwFlags //套接字属性信息
);
成功返回套接字句柄
失败返回INVALID_SOCKET
创建进行重叠I/O模式的套接字:
hSocket=WSASocket(PF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);
第五个参数传WSA_FLAG_OVERLAPPED。
将套接字改为非阻塞I/O模式:
int mode=1;
ioctlsocket(hSocket,FIONBIO,(u_long*)&mode); //非阻塞I/O的设置
将hSocket句柄引用的套接字I/O模式(FIONBIO)改为变量mode中指定的形式。
当设置为非阻塞模式后:
- 如果在没有客户端请求连接的情况下,调用accpet函数,将直接返回INVALID_SOCKET,调用WSAGetLastError函数,将返回WSAEWOULDBLOCK
- 调用accpet函数时创建的套接字同样具有非阻塞属性。
所以如果针对非阻塞套接字调用accept函数时,要判断返回INVALID_SOCKET的理由。有可能是accpet函数未成功,也有可能是没有客户端请求连接。
2.2 执行重叠I/O的Send函数
#include<winsock2.h>
int WSASend(
SOCKET s, //套接字句柄
LPWSABUF lpBuffers, //WSABUF结构体变量数组的地址值
DWORD dwBufferCount, //第二个参数中数组长度
LPDWORD lpNumberOfBytesSent, //保存实际发送字节数的变量地址值
DWORD dwFlags, //用于更改数据传输特性,如传递MSG_OOB时发送OOB模式的数据
LPWSAOVERLAPPED lpOverlapped, //WSAOVERLAPPED结构体变量地址值,使用事件对象,用于确认完成数据传输
LPWSAOVERLAPPED_COMPLETION_ROUTING lpCompletionRoutine //传入Completion Routine函数的入口
//地址值,可以通过该函数确认是否完成数据传输
);
成功返回0
失败返回SOCKET_ERROR
第二个参数,lpBuffers:
struct __WSABUF
{
u_long len; //待传输数据大小
char FAR* buf; //缓冲地址值
}WSABUF,*LPWSABUF;
第四个参数,lpNumberOfBytesSent:
填写了第四个参数会有如下两种情况:
1.当传输数据不大,函数调用后可以立即完成数据传输时,WSASend函数将返回0,lpNumberOfBytesSent中保存实际传输的数据大小。
2.当传输数据过大,函数调用后不能立即完成数据传输时,WSASend函数将返回SOCKET_ERROR,并将WSA_IO_PENDING注册为错误代码。该代码通过函数WSAGetLastError函数得到:
#include<winsock2.h>
int WSAGetLastError(void);
返回错误代码(表示错误原因)
第六个参数,lpOverlapped:
struct __WSAOVERLAPPED
{
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD offsetHigh;
WSAEVENT hEvent;
}WSAOVERLAPPED,*LPWSAOVERLAPPED;
其中Internal、InternalHigh成员是进行重叠I/O时操作系统内部使用成员,Offset、OffsetHigh是属于具有特殊用途的成员。所以只需关注hEvent成员,前四个成员置零即可。
注意:
1.为了进行重叠I/O,WSASend函数的lpOverlapped参数中应该传递有效的结构体变量地址值,而不是NULL。否则,SOCKET s将以阻塞模式工作。
2.向多个目标传输数据时,要分别构建lpOverlapped参数。但如果是同一个目标的接收/发送,就只需构建一次lpOvrelapped参数即可。
第七个参数, lpCompletionRoutine:
这是传入lpCompletionRoutine的函数原型:
void CALLBACK CompletionROUTING(
DWORD dwError, //写入错误信息,正常结束写入0
DWORD bdTransferred, //写入实际收发的字节数
LPWSAOVERLAPPED lpOverlapped, //写入WSASend\WSARecv函数的参数lpOverlapped
DWORD dwFlags //写入调用I/O时传入的特性信息或0
);
其中void返回值类型后面必须要有CALLBACK关键字。
2.3 执行重叠I/O的Recv函数
#include<winsock2.h>
int WSARecv(
SOCKET s, //套接字句柄
LPWSABUF lpBuffers, //WSABUF结构体变量数组的地址值
DWORD dwBufferCount, //第二个参数中数组长度
LPDWORD lpNumberOfBytesSent, //保存实际接收字节数的变量地址值
LPDWORD dwFlags, //用于设置或读取数据传输特性,如接收MSG_OOB时发送的OOB模式的数据
LPWSAOVERLAPPED lpOverlapped, //WSAOVERLAPPED结构体变量地址值,使用事件对象,用于确认完成数据接收
LPWSAOVERLAPPED_COMPLETION_ROUTING lpCompletionRoutine //传入Completion Routine函数的入口
//地址值,可以通过该函数确认是否完成数据接收
);
成功返回0
失败返回SOCKET_ERROR
这个和WSASend函数没什么区别。
2.4 获取执行I/O重叠的函数的执行结果
#include<winsock2.h>
BOOL WSAGetOverlappedResult(
SOCKET s, //进行重叠I/O的套接字句柄
LPWSAOVERLAPPED lpOverlapped, //进行重叠I/O时传递的WSAOVERLAPPED结构体变量的地址值
LPDWORD lpcbTransger, //保存实际传输的字节数的变量地址值
BOOL fWait, //如果调用该函数仍在进行I/O,则
//填TRUE时,等待I/O完成
//填FALSE时,函数退出并返回FALSE
LPDWORD lpdwFlags //调用WSARecv函数时,用于获取附加信息(如OOB消息)。
//不需要,可以传NULL
);
成功返回TRUE
失败返回FALSE
可以获取实际的传输数据大小。同时还可以通过第四个参数验证接收数据的状态。
2.5 重叠I/O的I/O完成确认
2.5.1 使用事件对象(使用重叠I/O函数的第六个参数)
第六个参数:WSAOVERLAPPED结构体。
当重叠I/O完成时:
- WSAOVERLAPPED结构体里的事件对象将变为signaled状态。
- 验证I/O的完成结果需要调用WSAGetOverlappedResult函数。
如:
if(SOCKET_ERROR==WSASend(hSocket,&dataBuf,1,&sendBytes,0,&overlapped,NULL))
{
if(WSAGetLastError()==WSA_IO_PENDING) //说明数据还未传输完成
{
WSAWaitForMultipleEvents(1,&evObj,TRUE,WSA_INFINITE,FALSE); //等待事件对象结束
WSAGetOverlappedResult(hSocket,&overlapped,&sendBytes,FALSE,NULL); //得到结果
}
else
{
......
}
}
//说明数据传输完成
......
2.5.2 使用Completion Routine函数(使用重叠I/O的第七个参数)
规则:只有请求I/O的线程处于alertable wait状态时才能调用Completion Routine函数。
alertable wait状态指:等待接收操作系统消息的线程状态。
调用以下函数将进入alertable wait状态:
- WaitForSingleObjectEx
- WaitForMultipleObjectsEx
- WSAWaitForMultipleEvents
- SleepEx
- WSA为前缀的上述函数
上述函数和去掉Ex的函数相同,只是上述函数增加了一个参数,为TURE那么就进入alertable wait状态,反之,则不进入。
为什么设定了这个规则?
因为:如果在执行重要任务时,突然调用Completion Routine函数,将破坏程序的正常执行流,所以要定义这个规则。
所以你可以在执行完重要任务后,调用上述任一函数,验证I/O完成与否,如果有已完成的I/O,则操作系统会调用响应的Completion Routine函数。调用结束后,上述函数会返回WAIT_IO_COMPLETION,并继续执行。
int main()
{
......
//进入alertable wait状态,调用CompRoutine函数
int idx=WSAWaitForMultipleEvents(1,&evObj,FALSE,WSA_INFINITE,TRUE);
if(inx==WAIT_TO_COMPLETION)
{
}
......
}
void CALLBACK CompRoutine(......)
{
......
}
使用Completion Routine方式的一个小知识点:
使用Completion Routine就可以无需事件对象了,所以在WSASend/WSARecv的第六个参数填写WSAOVERLAPPED结构体时,里面的事件对象(hEvent),可以存储写入其他信息,这个数据类型会被传送到CALLBACK的函数里的第三个参数,此时直接对hEvent进行强制转换,就可以得到相应的信息了。
struct message { ......; } message msg; overlapped.hEvent=(HANDLE)&msg; //HANDLE是指针类型 //记住使用的是Completion Routine方式哦 void CALLBACK Completion(DWORD error, DWORD transfer, LPWSAOVERLAPPED lpOverlapped, DWORD flags) { message mesg=(message)(lpOverlapped->hEvent); ...... }
3. 用重叠I/O实现回声服务器端
实现一:使用事件对象的方式来完成确认
这个比较简单,故省略。
实现二:使用Completion Routine的方式来完成确认
变量:
EventMessage结构体:用以存储连接的SOCKET套接字和对应客户端发送过来的消息内容。
为什么要创建一个这样的结构体?
因为:程序的运行是异步的,在while循环里,每连接一个客户端,SOCKET对应的客户端套接字变量,就会被重新赋值,就会导致写的RecvCompletion和SendCompletion函数里的SOCKET值会变化,这样发送的套接字就不正确了。所以必须要一个套接字对应一个客户端发送来的内容。
思路要点:
- 要保证客户端与服务器之间不只是发送一次数据,就要在RecvCompletion里面调用WSASend函数,在SendCompletion里面调用WSARecv函数,来达成循环。
- 使用EventMessage结构体,存储套接字和消息内容,把结构体地址值写入WSAOVERLAPPED的hEvent变量里,就可以在RecvCompletion和SendComplition里传递套接字和消息内容信息。
void CALLBACK RecvCompletion(DWORD error, DWORD transfer, LPWSAOVERLAPPED lpOverlapped, DWORD flags);
void CALLBACK SendCompletion(DWORD error, DWORD transfer, LPWSAOVERLAPPED lpOverlapped, DWORD flags);
struct EventMessage
{
WSABUF recvBuf;
SOCKET client;
};
int main()
{
......//这里和方式一都是一样的
while (1)
{
SleepEx(100, TRUE); //进入alertable wait状态
sockaddr_in clientAddr;
memset(&clientAddr, 0, sizeof(clientAddr));
int clientAddrLen = sizeof(clientAddr);
SOCKET client = accept(server, (sockaddr*)&clientAddr, &clientAddrLen);
if (INVALID_SOCKET == client)
{
if (WSAGetLastError() == WSAEWOULDBLOCK)
{
std::cout << "没有客户端连接" << std::endl;
}
else
{
std::cout << "accept fail!" << std::endl;
}
continue;
}
EventMessage eventMsg;
char buff[1024];
eventMsg.recvBuf.buf = buff;
eventMsg.recvBuf.len = sizeof(buff);
eventMsg.client = client;
WSAOVERLAPPED recvOverlapeed;
memset(&recvOverlapeed, 0, sizeof(recvOverlapeed));
recvOverlapeed.hEvent = (HANDLE)&eventMsg;
DWORD recvLen;
DWORD recvFlag=0;
WSARecv(client, &eventMsg.recvBuf, 1, &recvLen, &recvFlag, &recvOverlapeed, RecvCompletion);
}
}
void CALLBACK RecvCompletion(DWORD error, DWORD transfer, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
if (error == 0) //说明是正确结束
{
EventMessage eventMsg =*(EventMessage*)lpOverlapped->hEvent;
int recvLen = transfer; //获取接收的字节数
if (recvLen == 0)
{
std::cout << "客户端已断开!" << std::endl;
closesocket(eventMsg.client);
return;
}
std::cout << "客户端发来的信息:" << eventMsg.recvBuf.buf << std::endl;
WSASend(eventMsg.client, &eventMsg.recvBuf, 1, &transfer, flags, lpOverlapped, SendCompletion);
}
}
void CALLBACK SendCompletion(DWORD error, DWORD transfer, LPWSAOVERLAPPED lpOverlapped, DWORD flags)
{
if (error == 0)
{
EventMessage eventMsg = *(EventMessage*)lpOverlapped->hEvent;
char buff[1024];
eventMsg.recvBuf.buf = buff;
eventMsg.recvBuf.len = sizeof(buff);
DWORD recvLen;
DWORD recvFlag = 0;
WSARecv(eventMsg.client, &eventMsg.recvBuf, 1, &recvLen, &recvFlag, lpOverlapped, RecvCompletion);
}
}
执行结果:
因为是异步执行的,所以线程不会等待,会持续往下执行,当有消息传来时,就会执行Complition函数进行处理,线程不会阻塞住。