重叠I/O 模型提供了更好的系统性能。这个模型的基本设计思想是允许应用程序使用重叠数据结构一次投递一个或者多个异步I/O 请求(即所谓的重叠I/O)。提交的I/O 请求完成之后,与之关联的重叠数据结构中的事件对象受信,应用程序便可使用WSAGetOverlappedResult 函数获取重叠操作结果。这和使用重叠结构调用ReadFile和WriteFile 函数操作文件类似。
1. 创建套接字
SOCKET WSASocket(int af, int type, int protocol, // 前3 个参数与socket 函数相同
LPWSAPROTOCOL_INFO lpProtocolInfo, // 指定下层服务提供者,可以是NULL
GROUP g, // 保留
DWORD dwFlags // 指定套接字属性。要使用重叠I/O 模型,必须指定WSA_FLAG_OVERLAPPED
);
2. 传输数据
在重叠I/O 模型中,传输数据的函数是WSASend、WSARecv(TCP)和WSASendTo, WSARecvFrom 等。
int WSASend(
SOCKET s, // 套接字句柄
LPWSABUF lpBuffers, // WSABUF 结构的数组,每个WSABUF 结构包含一个缓冲区指针和对应缓冲区的长度
DWORD dwBufferCount, // 上面WSABUF 数组的大小
LPDWORD lpNumberOfBytesSent, // 如果I/O 操作立即完成的话,此参数取得实际传输数据的字节数
DWORD dwFlags, // 标志
LPWSAOVERLAPPED lpOverlapped, // 与此I/O 操作关联的WSAOVERLAPPED 结构
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 指定一个完成例程
);
这些函数与Winsock1 中的send、recv 等函数相比,都多了如下两个参数。
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
I/O 操作函数都接收一个WSAOVERLAPPED 结构类型的参数。这些函数被调用之后会立即返回,它们依靠应用程序传递的WSAOVERLAPPED 结构管理I/O 请求的完成。应用程序有两种方法可以接收到重叠I/O 请求操作完成的通知:
(1)在与WSAOVERLAPPED 结构关联的事件对象上等待,I/O 操作完成后,此事件对象受信,这是最经常使用的方法。
(2)使用lpCompletionRoutine 指向的完成例程。完成例程是一个自定义的函数,I/O 操作完成后,Winsock 便去调用它。这种方法很少使用,将lpCompletionRoutine 设为NULL 即可
3. 接受连接
可以异步接受连接请求的函数是AcceptEx。这是一个Microsoft 扩展函数,它接受一个新的连接,返回本地和远程地址,取得客户程序发送的第一块数据。
BOOL AcceptEx(
SOCKET sListenSocket, // 监听套接字句柄
SOCKET sAcceptSocket, // 指定一个未被使用的套接字,在这个套接字上接受新的连接
PVOID lpOutputBuffer, // 指定一个缓冲区,用来取得在新连接上接收到的第一块数据,服务器的本地地址和客户端地址
DWORD dwReceiveDataLength, // 上面lpOutputBuffer 所指缓冲区的大小
DWORD dwLocalAddressLength, // 缓冲区中,为本地地址预留的长度。必须比最大地址长度多16
DWORD dwRemoteAddressLength, // 缓冲区中,为远程地址预留的长度。必须比最大地址长度多16
LPDWORD lpdwBytesReceived, // 用来取得接收到数据的长度
LPOVERLAPPED lpOverlapped // 指定用来处理本请求的OVERLAPPED 结构,不能为NULL
); // 声明在Mswsock.h 中,需要添加到Mswsock.lib 库的链接
AcceptEx 函数将几个套接字函数的功能集合在了一起。如果它投递的请求成功完成,则执行了如下3 个操作:
1) 接受了新的连接。
2) 新连接的本地地址和远程地址都会返回。
3) 接收到了远程主机发来的第一块数据。
AcceptEx 函数需要调用者提供两个套接字,一个指定了在哪个套接字上监听(sListenSocket 参数),另一个指定了在哪个套接字上接受连接(sAcceptSocket 参数)。也就是说,AcceptEx 不会像accept 函数一样为新连接创建套接字。如果提供了接收缓冲区,AcceptEx 投递的重叠操作直到接受到连接并且读到数据之后才会返回。以SO_CONNECT_TIME 为参数调用getsockopt 函数可以检查到是否接受了连接。如果接受了连接,这个调用还可以取得连接已经建立了多长时间。
AcceptEx 函数(Microsoft 扩展函数都是这样)是从Mswsock.lib 库中导出的。为了能够直接调用它,而不用链接到Mswsock.lib 库,需要使用WSAIoctl 函数将AcceptEx 函数加载到内存。WSAIoctl 函数是ioctlsocket 函数的扩展,它可以使用重叠I/O。函数的第3 个到第6个参数是输入和输出缓冲区,在这里传递AcceptEx 函数的指针。具体加载代码如下。
// 加载扩展函数AcceptEx
GUID GuidAcceptEx = WSAID_ACCEPTEX;
DWORD dwBytes;
WSAIoctl(pListen->s,
SIO_GET_EXTENSION_FUNCTION_POINTER,
&GuidAcceptEx,
sizeof(GuidAcceptEx),
&pListen->lpfnAcceptEx,
sizeof(pListen->lpfnAcceptEx),
&dwBytes,
NULL,
NULL);
4. 事件通知方式
typedef struct _WSAOVERLAPPED {
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
WSAEVENT hEvent; // 在此为这个操作关联一个事件对象句柄
} WSAOVERLAPPED, *LPWSAOVERLAPPED;
前 4 个域Internal、InternalHigh、Offset 和OffsetHigh 由系统内部使用,应用程序不应该操作或者直接使用它们。hEvent 域允许应用程序为这个操作关联一个事件对象句柄。重叠I/O的事件通知方法需要将Windows 事件对象关联到上面的WSAOVERLAPPED 结构。
当使用WSAOVERLAPPED 结构进行I/O 调用时,如调用WSASend 和WSARecv,这些函数立即返回。通常情况下,这些I/O 调用会失败,返回值是SOCKET_ERROR,并且3.5 重叠(Overlapped)I/O 模型WSAGetLastError 函数报告了WSA_IO_PENDING 出错状态。这个出错状态表示I/O 操作正在进行。在以后的一个时间,应用程序将需要通过在关联到WSAOVERLAPPED 结构的事件对象上等待以确定什么时候一个重叠I/O 请求完成。就这样,WSAOVERLAPPED 结构在重叠I/O 请求的初始化和随后的完成之间提供了交流媒介。
当重叠 I/O 请求最终完成以后,与之关联的事件对象受信,等待函数返回,应用程序可以使用WSAGetOverlappedResult 函数取得重叠操作的结果。
BOOL WSAGetOverlappedResult(
SOCKET s, // 套接字句柄
LPWSAOVERLAPPED lpOverlapped, // 重叠操作启动时指定的WSAOVERLAPPED 结构
LPDWORD lpcbTransfer, // 用来取得实际传输字节的数量
BOOL fWait, // 指定是否要等待未决的重叠操作
LPDWORD lpdwFlags // 用于取得完成状态
);
函数调用成功时返回值是TRUE,这说明重叠操作成功完成了,lpcbTransfer 参数将返回I/O 操作实际传输字节的数量。如果传递的参数无误,但返回值是FALSE,则说明在套接字s上有错误发生。
// 例程1 使用事件对象通知方式
#include <WINSOCK2.H>
#include <STDIO.H>
#pragma comment(lib, "Ws2_32.lib")
#define DATA_BUFSIZE 1024
void main()
{
WSABUF DataBuf;
char buffer[DATA_BUFSIZE];
DWORD EventTotoal = 0, RecvBytes = 0, Flags = 0;
WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS];
WSAOVERLAPPED AcceptOverlapped;
SOCKET ListenSocket, AcceptSocket;
// 1. 启动Winsock, 建立监听套接字
WSADATA wsaData;
WSAStartup(MAKEWORD(2,2), &wsaData);
ListenSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9000);
addr.sin_addr.S_un.S_addr = INADDR_ANY;
bind(ListenSocket, (sockaddr*)&addr, sizeof(addr));
listen(ListenSocket, 5);
// 2. 接受一个入站连接
AcceptSocket = accept(ListenSocket, NULL, NULL);
// 3. 建立一个重叠结构
EventArray[EventTotoal] = WSACreateEvent();
memset(&AcceptOverlapped, 0, sizeof(AcceptOverlapped));
AcceptOverlapped.hEvent = EventArray[EventTotoal];
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = buffer;
EventTotoal++;
// 4. 投递一个WSARecv请求, 以便开始在套接字上接受数据
if ( WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, &AcceptOverlapped, NULL) == SOCKET_ERROR )
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
return; // 出错
}
}
// 处理套接字上的重叠接收
while (TRUE)
{
DWORD index;
// 5. 等候重叠IO调用结束
index = WSAWaitForMultipleEvents(EventTotoal, EventArray, FALSE, WSA_INFINITE, FALSE);
if (WSA_WAIT_FAILED == index)
{
// 出错
}
else
{
// 6. 重置已传信事件
WSAResetEvent(EventArray[index - WSA_WAIT_EVENT_0]);
// 7. 确定重叠请求的状态
DWORD BytesTransferred;
WSAGetOverlappedResult(AcceptSocket, &AcceptOverlapped,&BytesTransferred,FALSE,&Flags);
// 先检查看通信对方是否已关闭连接, 如果已关闭, 则关闭套接字
if (BytesTransferred == 0)
{
printf("close socket %d\n", AcceptSocket);
closesocket(AcceptSocket);
WSACloseEvent(EventArray[index - WSA_WAIT_EVENT_0]);
return;
}
else
{
DataBuf.buf[BytesTransferred] = '\0';
printf("recv data: %s\n", DataBuf.buf);
}
// 8. 在套接字上投递另一个WSARecv请求
Flags = 0;
memset(&AcceptOverlapped, 0, sizeof(WSAOVERLAPPED));
AcceptOverlapped.hEvent = EventArray[index - WSA_WAIT_EVENT_0];
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = buffer;
if (WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags, &AcceptOverlapped, NULL) == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("Unknown error \n");
closesocket(AcceptSocket);
WSACloseEvent(EventArray[index - WSA_WAIT_EVENT_0]);
return;
}
}
}
}
}