WinSocket下编程,总共有7种套接字I/O模型可供选择,分别是阻塞(block)、非阻塞(nonblock)、选择(I/O复用)(select)、异步选择(WSAAsyncSelect)、事件选择(WSAEventSelect)、重叠I/O(Overlapped )以及完成端口(CompletionPort)模型。
其中,默认情况下使用WinSocket进行编程,都属于阻塞模型,详细编程过程参考这里(点击打开链接)。
下面对WinSocket下,各种套接字IO模型的编程步骤进行详细的说明,以服务端编程为例,客户端使用阻塞模型。所有I/O模型的完整代码可以到这里下载(点击打开链接)。
一、非阻塞I/O模型
相比默认情况下的阻塞模型编程,非阻塞模型编程仅仅调用了ioctlsocket()函数将socket设置成了非阻塞模式,然后原本要阻塞主线程的accept()、send()、recv()等函数,将变成非阻塞的,即调用该函数后,无论操作能否完成都立即返回,不再等待。那如何知道操作是否完成呐?其实,如果操作不能完成,将返回一个WSAEWOULDBLOCK错误,便可以依据这条错误消息,来判断是否需要进行重新进行该操作。
大部分编程步骤和阻塞模型一样,下面仅对不同的关键步骤进行说明。
1、将套接字设置为非阻塞模式
// 设置套接字为非阻塞模式
int iMode = 1;
iResult = ioctlsocket(ServSocket, FIONBIO, (ULONG*)&iMode);
if (iResult == SOCKET_ERROR)
{
printf("ioctlsocket faield with error: %d\n", WSAGetLastError());
closesocket(ServSocket);
WSACleanup();
return 1;
}
2、根据WSAEWOULDBLOCK错误判断是否需要重新进行操作,以accept()操作为例
// 循环处理客户端的连接请求
while (true)
{
// 接收客户端的连接请求,并且返回已连接套接字,负责与本次连接的客户端通信
SOCKET AcceSocket = INVALID_SOCKET;
SOCKADDR_IN addrClient;
int addrClienLen = sizeof(SOCKADDR_IN);
// 非阻塞模式下,这一步将不再阻塞主线程
AcceSocket = accept(ServSocket, (SOCKADDR*)&addrClient, &addrClienLen);
if (AcceSocket == INVALID_SOCKET)
{
int err = WSAGetLastError();
// 当前无法立即完成非阻塞套接字上的操作,即此时没有连接请求需要处理,等待一段时间后返回继续处理
if (err == WSAEWOULDBLOCK)
{
Sleep(1000);
continue;
}
else
{
printf("accept failed with error: %d\n", WSAGetLastError());
break;
}
}
printf("a cilent has connect successful, its ip: %s\n", inet_ntoa(addrClient.sin_addr));
}
3、send()、recv()操作编程类似步骤2
非阻塞I/O模型下,服务端、客户端最终的运行效果如下:
二、选择I/O模型
选择I/O模型最主要解决的问题就是如何管理多个套接字状态,它使用fd_set结构来实现这项功能。选择I/O模型允许用户选择感兴趣的一组套接字,然后将这组套接字以fd_set的形式交给select()函数进行处理,select()函数会将fd_set结构中不存在I/O操作的套接字删除,那么剩下的套接字就是需要进行处理的了。这样select模型就以单线程的方式,模拟了多线程并发的效果。
这里同样只列出关键步骤,其余编程步骤均类似于一般情况下的socket编程。
1、定义套接字集合,用于保存所有的套接字,或保存感兴趣的套接字。
// select模型,管理一组套接字
fd_set fdSocket; // socket集合
fd_set fdRead; // 可读集
FD_ZERO(&fdSocket);
FD_SET(ServSocket, &fdSocket);
2、循环调用select()函数,找出具有未决I/O操作的套接字
// 循环等待事件的发生
while (TRUE)
{
// 选择感兴趣的socket集合
fdRead = fdSocket;
// just care read event,block until there are some event happening
iResult = select(0, &fdRead, NULL, NULL, NULL);
}
3、根据select()函数的返回结果,对发生I/O操作的socket进行相应的处理
if (iResult > 0)
{
// 确定哪些套接字有需要处理的I/O操作
for (int i = 0; i < fdSocket.fd_count; ++i)
{
// 如果该套接字存在读事件
if (FD_ISSET(fdSocket.fd_array[i], &fdRead))
{
// 有连接请求到来
if (fdSocket.fd_array[i] == ServSocket)
{
if (fdSocket.fd_count < FD_SETSIZE)
{
// 接收客户端的连接请求,并且返回已连接套接字,负责与本次连接的客户端通信
SOCKET AcceSocket = INVALID_SOCKET;
SOCKADDR_IN addrClient;
int addrClienLen = sizeof(SOCKADDR_IN);
// 这一步将阻塞主线程,直到有连接请求的到来
AcceSocket = accept(ServSocket, (SOCKADDR*)&addrClient, &addrClienLen);
if (AcceSocket == INVALID_SOCKET)
{
printf("accept failed with error: %d\n", WSAGetLastError());
continue;
}
// 将新连接的套接字加入socket集合中,进行select管理
FD_SET(AcceSocket, &fdSocket);
printf("a cilent has connect successful, its ip: %s\n", inet_ntoa(addrClient.sin_addr));
}
else
{
puts("超过连接的最大个数");
continue;
}
}
// 有数据到达
else
{
// 接收客户端发送的数据,阻塞,直到接收缓冲区中有数据
char recvBuf[DEFAULT_BUF_LEN];
memset(recvBuf, 0, sizeof(recvBuf));
iResult = recv(fdSocket.fd_array[i], recvBuf, DEFAULT_BUF_LEN, 0);
// 成功接收到数据
if (iResult > 0)
{
printf("%d bytes data received: ", iResult);
printf("%s\n", recvBuf);
// 回射一个消息,阻塞,直到数据成功写入发送缓冲区
char sendBuf[DEFAULT_BUF_LEN] = "this is server";
iResult = send(fdSocket.fd_array[i], sendBuf, strlen(sendBuf), 0);
if (iResult == SOCKET_ERROR)
{
printf("send faield with error: %d\n", WSAGetLastError());
closesocket(fdSocket.fd_array[i]);
FD_CLR(fdSocket.fd_array[i], &fdSocket);
}
}
// 连接关闭
else if (iResult == 0)
{
printf("current connection closing\n");
closesocket(fdSocket.fd_array[i]);
FD_CLR(fdSocket.fd_array[i], &fdSocket);
}
// 接收发生错误
else
{
printf("recv failed with error: %d\n", WSAGetLastError());
closesocket(fdSocket.fd_array[i]);
FD_CLR(fdSocket.fd_array[i], &fdSocket);
}
}
}
// 其它类型的事件判断
// ......
}
}
else
{
printf("select faield with error: %d\n", WSAGetLastError());
break;
}
选择I/O模型下,服务端、客户端最终的运行效果如下:
三、异步选择I/O模型
该I/O模型是为了适应win32 sdk下,窗体应用程序的。有过纯win32窗体API开发经验的都知道,整个程序的运行动力就是windows定义的各种消息事件,如键盘按下、单击鼠标等,程序本身就是对这些消息的响应。其实套接字的网络操作,也算是一种响应事件,所以,为了统一windows的消息响应机制,自然套接字的各种网络事件也就需要合并到windows消息响应中了。
1、创建win32窗体应用程序
既然要和win32窗体的消息机制进行整合,首先就得利用win32窗体API创建窗体,这一步编程可以去这里了解()。
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
char szAppName[] = "AsyncSelect Model";
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ; // 定义窗口类
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass(&wndclass)) // 注册窗口类
{
MessageBox (NULL, "This program requires Windows NT!", szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, // window class name // 创建窗口
"AsyncSelect Model", // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT, // initial x position
CW_USEDEFAULT, // initial y position
CW_USEDEFAULT, // initial x size
CW_USEDEFAULT, // initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL) ; // creation parameters
ShowWindow(hwnd, iCmdShow); // 显示窗口
UpdateWindow(hwnd); // 更新窗口
while (GetMessage(&msg, NULL, 0, 0)) // 获取消息
{
TranslateMessage(&msg) ; // 转换消息
DispatchMessage(&msg) ; // 分发消息
}
return msg.wParam;
}
2、初始化套接字,进行常规服务端套接字编程
每当窗体有消息产生时,都会调用与该窗体进行绑定的窗口例程,进行消息事件的响应。当窗口被第一次创建时,会发送一个WM_CREATE消息给窗口例程,所以套接字的初始化操作将在这个消息到来时进行。并且这一步中,还要调用WSAAsyncSelect()函数,将套接字、需要接受消息的窗体、用户自定义消息以及感兴趣的网络事件进行绑定。
switch (message)
{
case WM_CREATE: // 窗口创建消息
{
// Initialize Windows Socket library
WSADATA wsaData;
int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0)
{
sprintf(errorBuf, "WSAStartup failed with error: %d", WSAGetLastError());
MessageBox(hwnd, errorBuf, "error", MB_OK);
return 0;
}
// Create listening socket
SOCKET ServSocket = INVALID_SOCKET;
ServSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (ServSocket == INVALID_SOCKET)
{
sprintf(errorBuf, "socket failed with error: %d", WSAGetLastError());
MessageBox(hwnd, errorBuf, "error", MB_OK);
WSACleanup();
return 0;
}
// 为监听套接字绑定地址和端口号
SOCKADDR_IN addrServ;
addrServ.sin_family = AF_INET;
addrServ.sin_addr.S_un.S_addr = INADDR_ANY;
addrServ.sin_port = htons(DEFAULT_PORT);
iResult = bind(ServSocket, (SOCKADDR*)&addrServ, sizeof(SOCKADDR_IN));
if (iResult == SOCKET_ERROR)
{
sprintf(errorBuf, "bind faield with error: %d", WSAGetLastError());
MessageBox(hwnd, errorBuf, "error", MB_OK);
closesocket(ServSocket);
WSACleanup();
return 0;
}
// 开启监听状态
iResult = listen(ServSocket, SOMAXCONN);
if (iResult == SOCKET_ERROR)
{
sprintf(errorBuf, "listen faield with error: %d", WSAGetLastError());
MessageBox(hwnd, errorBuf, "error", MB_OK);
closesocket(ServSocket);
WSACleanup();
return 0;
}
char buf[] = "server is listening ......";
TextOut(hdc, 0, MESSAGE_LINE += 20, buf, strlen(buf)); // 显示信息
// Associate listening socket with FD_ACCEPT event
WSAAsyncSelect(ServSocket, hwnd, WM_SOCKET, FD_ACCEPT);
return 0;
}
<span style="white-space:pre"> </span>}
3、对网络事件进行处理
调用WSAAsyncSelect()函数后,当某一个套接字上产生网络事件时,会向应用程序发送用户自定义的消息,窗口过程函数的lParam参数保存了具体的网络事件,可以使用宏WSAGETSELECTEVENT提取具体的网络事件,wParam参数保存了发生该网络事件的套接字句柄。
case WM_SOCKET: // 自定义套接字消息
if (WSAGETSELECTERROR(lParam))
{
sprintf(errorBuf, "套接字错误,错误号: %d", WSAGetLastError());
MessageBox(hwnd, errorBuf, "error", MB_OK);
closesocket(wParam);
break;
}
// 判断具体的套接字网络事件
switch (WSAGETSELECTEVENT(lParam))
{
case FD_ACCEPT:
{
// Accept a connection from client
SOCKET AcceSocket = INVALID_SOCKET;
SOCKADDR_IN addrClient;
int addrClienLen = sizeof(SOCKADDR_IN);
// 这一步将阻塞主线程,直到有连接请求的到来
AcceSocket = accept(wParam, (SOCKADDR*)&addrClient, &addrClienLen);
if (AcceSocket == INVALID_SOCKET)
{
sprintf(errorBuf, "accept failed with error : %d", WSAGetLastError());
MessageBox(hwnd, errorBuf, "error", MB_OK);
break;
}
char buf[DEFAULT_BUF_LEN];
sprintf(buf, "a cilent has connect successful, its ip: %s\n", inet_ntoa(addrClient.sin_addr));
TextOut(hdc, 0, MESSAGE_LINE += 20, buf, strlen(buf)); // 显示信息
// Associate client socket with FD_READ and FD_CLOSE event
WSAAsyncSelect(AcceSocket, hwnd, WM_SOCKET, FD_READ | FD_CLOSE);
break;
}
case FD_READ:
// 接收客户端发送的数据,阻塞,直到接收缓冲区中有数据
char recvBuf[DEFAULT_BUF_LEN];
memset(recvBuf, 0, sizeof(recvBuf));
iResult = recv(wParam, recvBuf, DEFAULT_BUF_LEN, 0);
// 成功接收到数据
if (iResult > 0)
{
char buf[DEFAULT_BUF_LEN];
sprintf(buf, "%d bytes data received: ", iResult);
TextOut(hdc, 0, MESSAGE_LINE += 20, buf, strlen(buf));
TextOut(hdc, 0, MESSAGE_LINE += 20, recvBuf, strlen(recvBuf));
// 回射一个消息,阻塞,直到数据成功写入发送缓冲区
char sendBuf[DEFAULT_BUF_LEN] = "this is server";
iResult = send(wParam, sendBuf, strlen(sendBuf), 0);
if (iResult == SOCKET_ERROR)
{
sprintf(errorBuf, "send faield with error: %d", WSAGetLastError());
MessageBox(hwnd, errorBuf, "error", MB_OK);
}
}
// 连接关闭
else if (iResult == 0)
{
char buf[] = "current connection closing\n";
TextOut(hdc, 0, MESSAGE_LINE += 20, buf, strlen(buf));
closesocket(wParam);
}
// 接收发生错误
else
{
sprintf(errorBuf, "recv failed with error: %d", WSAGetLastError());
MessageBox(hwnd, errorBuf, "error", MB_OK);
closesocket(wParam);
break;
}
case FD_CLOSE:
closesocket(wParam);
break;
}
return 0;
异步选择I/O模型下,服务端、客户端的运行效果如下:
四、事件选择I/O模型
异步选择I/O模型需要依靠win32窗体程序的消息回调系统,可是某些情况下可能并不需要创建窗体,并且将窗体的消息系统与网络事件结合起来的做法也会造成性能问题。事件选择I/O模型优化了这一问题,允许在不建立win32窗体的情况下也能使用网络事件通知机制。所以,可以知道,异步选择I/O模型与事件选择I/O模型的最大不同,也就是当网络事件发生时,系统的通知方式不同,前者依靠windows本身的消息回调机制,后者则利用轮询的机制来检查网络事件的到来。
1、创建监听套接字、绑定端口、监听链接请求等步骤,如默认情况下阻塞模型编程
2、利用WSAEventSelect()函数注册感兴趣的网络事件
这一步需要建立套接字表和对应的网络事件表,如果某一套接字发生网络事件,对应的消息将会被投递至对应的网络事件句柄。
// 创建时间对象
WSAEVENT Event = WSACreateEvent();
// 为套接字注册感兴趣的网络事件,并将事件对象关联到指定的网络事件集合
// WSAEventSelect()函数会将套接字设置为非阻塞模式
WSAEventSelect(ServSocket, Event, FD_ACCEPT);
// 将新建的事件保存到事件表中
eventArray[iConnecTotal] = Event;
// 将套接字保存到套接字表中
socketArray[iConnecTotal] = ServSocket;
++iConnecTotal;
3、利用WSAWaitForMultipleEvents()函数轮询发生网络事件的套接字
// 循环处理发生的网络事件
while (TRUE)
{
// 在所有事件对象上等待,设置只要有一个事件对象变为已授信状态便返回
int iIndex = WSAWaitForMultipleEvents(iConnecTotal, eventArray, FALSE, WSA_INFINITE, FALSE);
// 一般是句柄数组中最前面一个发生事件
// 然后循环对依次处理后面的事件对象,对每个事件调用WSAWaitForMultipleEvents()函数,以便确定它的状态
iIndex = iIndex - WAIT_OBJECT_0;
for (int i = iIndex; i < iConnecTotal; ++i)
{
iResult = WSAWaitForMultipleEvents(1, &eventArray[i], TRUE, 1000, FALSE);
if (iResult == WSA_WAIT_FAILED || iResult == WSA_WAIT_TIMEOUT) continue;
else
{
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>}
4、利用WSAEnumNetworkEvents()函数获取到具体发生的网络事件
// 获取并处理具体发生的网络事件,调用WSAEnumNetworkEvents(),它会自动重置授信事件
<span style="white-space:pre"> </span>WSANETWORKEVENTS newevent;
<span style="white-space:pre"> </span>WSAEnumNetworkEvents(socketArray[i], eventArray[i], &newevent);
5、对具体发生的网络事件进行处理
// 处理FD_ACCEPT事件
if (newevent.lNetworkEvents & FD_ACCEPT)
{
// 处理连接请求事件时没有错误
if (newevent.iErrorCode[FD_ACCEPT_BIT] == 0)
{
// 连接过多
if (iConnecTotal > WSA_MAXIMUM_WAIT_EVENTS)
{
printf("too many connections\n");
continue;
}
// 接收客户端的连接请求,并且返回已连接套接字,负责与本次连接的客户端通信
SOCKET AcceSocket = INVALID_SOCKET;
SOCKADDR_IN addrClient;
int addrClienLen = sizeof(SOCKADDR_IN);
AcceSocket = accept(ServSocket, (SOCKADDR*)&addrClient, &addrClienLen);
if (AcceSocket == INVALID_SOCKET)
{
printf("accept failed with error: %d\n", WSAGetLastError());
break;
}
printf("a cilent has connect successful, its ip: %s\n", inet_ntoa(addrClient.sin_addr));
// 为新套接字创建事件对象,并关联事件集合
WSAEVENT EventClient = WSACreateEvent();
WSAEventSelect(AcceSocket, EventClient, FD_READ | FD_WRITE | FD_CLOSE);
// 将新建的对象加入表中保存
eventArray[iConnecTotal] = EventClient;
socketArray[iConnecTotal] = AcceSocket;
++iConnecTotal;
}
}
// 处理FD_READ事件
if (newevent.lNetworkEvents & FD_READ)
{
// 处理接收消息事件时没有错误
if (newevent.iErrorCode[FD_READ_BIT] == 0)
{
// 接收客户端发送的数据
char recvBuf[DEFAULT_BUF_LEN];
memset(recvBuf, 0, sizeof(recvBuf));
iResult = recv(socketArray[i], recvBuf, DEFAULT_BUF_LEN, 0);
// 成功接收到数据
if (iResult > 0)
{
printf("%d bytes data received: ", iResult);
printf("%s\n", recvBuf);
// 回射一个消息,阻塞,直到数据成功写入发送缓冲区
char sendBuf[DEFAULT_BUF_LEN] = "this is server";
iResult = send(socketArray[i], sendBuf, strlen(sendBuf), 0);
if (iResult == SOCKET_ERROR)
{
printf("send faield with error: %d\n", WSAGetLastError());
}
cleanup(i);
}
// 连接关闭
else if (iResult == 0)
{
printf("current connection closing\n");
cleanup(i);
}
// 接收发生错误
else
{
printf("recv failed with error: %d\n", WSAGetLastError());
cleanup(i);
break;
}
}
}
// 处理FD_CLOSE事件
if (newevent.lNetworkEvents & FD_CLOSE)
{
// 处理连接关闭事件时没有错误
if (newevent.iErrorCode[FD_CLOSE_BIT] == 0)
{
printf("current connection closing...\n");
cleanup(i);
}
}
// 处理FD_WRITE事件
if (newevent.lNetworkEvents & FD_WRITE)
{
// 处理连接关闭事件时没有错误
if (newevent.iErrorCode[FD_WRITE_BIT] == 0)
{
// 进行数据发送
}
}
事件选择I/O模型下,服务端、客户端的最终运行效果如下:
五、重叠I/O模型
重叠I/O模型下将允许用户和系统共用一个网络数据结构,即重叠数据结构WSAOVERLAPPED,这样该模型的效率优势就体现在减少了一次将网络数据从I/O缓冲区到用户缓冲区的拷贝。同时该模型也允许用户提交异步网络操作,即WSARecv()、WSASend()等,这些操作调用后将立即返回,所以可以将其看成是用户提交的一个异步操作请求,这些请求将在满足条件时由系统通知用户处理。也因此,在windows下用户通知机制的不同,实现重叠I/O模型也有不同的方法。一般可以采用两种通知机制实现对用户异步请求的响应,一是事件选择模型中的事件通知方式,二是类似于异步选择模型中消息回调的完成例程方式。
1、事件通知方式下的草重叠I/O模型
在这种方式中,将事件对象与重叠I/O结构进行关联,那么当事件对象变为授信状态时,用户就知道对应的重叠结构也发生了网络事件,此时就可以对前面提交的异步网络请求进行响应操作了。
1) 设计重叠I/O数据结构,以保存网络数据等相关信息
typedef struct // 自定义重叠I/O结构体
{
WSAOVERLAPPED overlap; // 重叠数据结构,用于事件的关联
WSABUF Buffer; // 发送和接收数据的缓冲区结构
char szMessage[DEFAULT_BUF_LEN]; // 记录事件对象数组中的数据
DWORD NumberOfBytesRecvd; // 接收数据的字节数
DWORD NumberOfBytesSend; // 发送数据的字节数
DWORD Flags; // 标志位
}PER_IO_OPERATION_DATA, *LPPER_IO_OPERATION_DATA;
2) 套接字的初始化操作如同阻塞模型,只是在创建监听套接字时,需要使用WSASocket()函数,并指定WSA_FLAG_OVERLAPPED参数,以支持重叠I/O
// Create listening socket,using WSASocket()
SOCKET ServSocket = INVALID_SOCKET;
ServSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_IP, NULL, 0, WSA_FLAG_OVERLAPPED);
3) 创建工作线程,等待网络事件的发生
// 创建工作线程
CreateThread(NULL, 0, WorkerThread, NULL, 0, NULL);
4) 循环接受客户端的连接请求,并为其创建事件对象以及重叠数据结构
// 处理客户端的连接请求
while (TRUE)
{
// Accept a connection
SOCKET AcceSocket = INVALID_SOCKET;
SOCKADDR_IN addrClient;
int addrClienLen = sizeof(SOCKADDR_IN);
AcceSocket = accept(ServSocket, (SOCKADDR*)&addrClient, &addrClienLen);
if (AcceSocket == INVALID_SOCKET)
{
printf("accept failed with error: %d\n", WSAGetLastError());
break;
}
printf("a cilent has connect successful, its ip: %s\n", inet_ntoa(addrClient.sin_addr));
socketArray[iConnecTotal] = AcceSocket;
// Allocate a PER_IO_OPERATION_DATA structure
pPerIODataArray[iConnecTotal] = (LPPER_IO_OPERATION_DATA)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(PER_IO_OPERATION_DATA));
// 填充结构体内容
pPerIODataArray[iConnecTotal]->Buffer.len = DEFAULT_BUF_LEN;
pPerIODataArray[iConnecTotal]->Buffer.buf = pPerIODataArray[iConnecTotal]->szMessage;
ZeroMemory(pPerIODataArray[iConnecTotal]->Buffer.buf, DEFAULT_BUF_LEN);
ZeroMemory(&(pPerIODataArray[iConnecTotal]->overlap), sizeof(WSAOVERLAPPED));
// 将套接字及关联的事件进行绑定,这样当hEvent被授信时,eventArray中的事件也会被授信
pPerIODataArray[iConnecTotal]->overlap.hEvent = WSACreateEvent();
eventArray[iConnecTotal] = pPerIODataArray[iConnecTotal]->overlap.hEvent;
// 提交一个异步接收请求,这样当在有数据到达之后,对应的事件对象将会变为被授信状态
iResult = WSARecv(socketArray[iConnecTotal], &pPerIODataArray[iConnecTotal]->Buffer, 1,
&pPerIODataArray[iConnecTotal]->NumberOfBytesRecvd,
&pPerIODataArray[iConnecTotal]->Flags,
&pPerIODataArray[iConnecTotal]->overlap,
NULL);
if (iResult == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("error occured at wsarecv():%d\n", WSAGetLastError());
}
}
++iConnecTotal;
}
5) 实现工作线程,循环轮询有网络事件发生的事件对象,进而得到相应的重叠结构,获取到相关信息
DWORD WINAPI WorkerThread(LPVOID lpParam)
{
int iResult, index;
DWORD cbTransferred; // 在读、写操作中实际传输的字节数
// 等待投递的异步I/O操作请求授信
while (TRUE)
{
// 等待网络事件的发生
iResult = WSAWaitForMultipleEvents(iConnecTotal, eventArray, FALSE, WSA_INFINITE, FALSE);
if (iResult == WSA_WAIT_FAILED || iResult == WSA_WAIT_TIMEOUT)
{
continue;
}
index = iResult - WSA_WAIT_EVENT_0;
WSANETWORKEVENTS newevent;
WSAEnumNetworkEvents(socketArray[index], eventArray[index], &newevent);
// 处理发送数据事件的异步I/O请求
//if (newevent.lNetworkEvents & FD_WRITE)
{
// ......
}
// 处理接收数据事件的异步I/O请求
//else if (newevent.lNetworkEvents & FD_READ)
{
// 确定重叠事件的状态,获取接受到的数据
WSAGetOverlappedResult(socketArray[index], &pPerIODataArray[index]->overlap,
&cbTransferred,
TRUE,
&pPerIODataArray[iConnecTotal]->Flags);
if (cbTransferred == 0)
{
// The connection was closed by client
cleanup(index);
}
else
{
// g_pPerIODataArr[index]->szMessage contains the received data
pPerIODataArray[index]->szMessage[cbTransferred] = '\0';
printf("%d bytes data received: ", cbTransferred);
printf("%s\n", pPerIODataArray[index]->szMessage);
// 提交一个异步发送请求
strcpy(pPerIODataArray[index]->szMessage, "this is server");
iResult = WSASend(socketArray[index], &pPerIODataArray[index]->Buffer, 1,
&pPerIODataArray[index]->NumberOfBytesSend,
pPerIODataArray[index]->Flags,
&pPerIODataArray[index]->overlap,
NULL);
if (iResult == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("error occured at wsasend():%d\n", WSAGetLastError());
}
}
// Launch another asynchronous recv operation
iResult = WSARecv(socketArray[index], &pPerIODataArray[index]->Buffer, 1,
&pPerIODataArray[index]->NumberOfBytesRecvd,
&pPerIODataArray[index]->Flags,
&pPerIODataArray[index]->overlap,
NULL);
if (iResult == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("error occured at wsarecv():%d\n", WSAGetLastError());
}
}
}
}
// 重置已授信的事件对象
WSAResetEvent(eventArray[index]);
}
return 0;
}
事件通知方式下的重叠I/O模型,最终服务端、客户端的运行效果如下:
2、完成例程方式下的重叠I/O模型
利用完成例程方式实现重叠I/O模型,与事件通知方式下的重叠I/O模型不同,当用户提交的异步请求完成时,并不是利用对应的事件对象来判断的,而是预先设置一个回调函数CompletionROUTINE(),由系统直接调用。而对于异步请求的提交,几乎是相同的,都是使用WSARecv()、WSASend()等函数完成。
1) 设计重叠I/O数据结构,以保存网络数据等相关信息
typedef struct
{
WSAOVERLAPPED overlap;
WSABUF Buffer;
char szMessage[DEFAULT_BUF_LEN];
DWORD NumberOfBytesRecvd;
DWORD NumberOfBytesSend;
DWORD Flags;
SOCKET sClient; // 客户端套接字
}PER_IO_OPERATION_DATA, *LPPER_IO_OPERATION_DATA;
2) 套接字的初始化操作如同阻塞模型,只是在创建监听套接字时,需要使用WSASocket()函数,并指定WSA_FLAG_OVERLAPPED参数,以支持重叠I/O
// Create listening socket,using WSASocket()
SOCKET ServSocket = INVALID_SOCKET;
ServSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_IP, NULL, 0, WSA_FLAG_OVERLAPPED);
4) 循环接受客户端的连接请求,创建重叠结构,并利用WSA*()函数的最后一个参数设置对应异步请求完成时的回调函数
// 处理客户端的连接请求
while (TRUE)
{
// Accept a connection
SOCKET AcceSocket = INVALID_SOCKET;
SOCKADDR_IN addrClient;
int addrClienLen = sizeof(SOCKADDR_IN);
AcceSocket = accept(ServSocket, (SOCKADDR*)&addrClient, &addrClienLen);
if (AcceSocket == INVALID_SOCKET)
{
printf("accept failed with error: %d\n", WSAGetLastError());
break;
}
printf("a cilent has connect successful, its ip: %s\n", inet_ntoa(addrClient.sin_addr));
// Allocate a PER_IO_OPERATION_DATA structure
LPPER_IO_OPERATION_DATA lpPerIOData = NULL;
lpPerIOData = (LPPER_IO_OPERATION_DATA)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(PER_IO_OPERATION_DATA));
// 填充结构体内容
lpPerIOData->Buffer.len = DEFAULT_BUF_LEN;
lpPerIOData->Buffer.buf = lpPerIOData->szMessage;
lpPerIOData->sClient = AcceSocket;
ZeroMemory(lpPerIOData->Buffer.buf, DEFAULT_BUF_LEN);
ZeroMemory(&(lpPerIOData->overlap), sizeof(WSAOVERLAPPED));
// 提交一个异步接收请求,这样当在有数据到达之后,对应的事件对象将会变为被授信状态,并回调CompletionROUTINE()函数
iResult = WSARecv(AcceSocket, &lpPerIOData->Buffer, 1,
&lpPerIOData->NumberOfBytesRecvd,
&lpPerIOData->Flags,
&lpPerIOData->overlap,
CompletionROUTINE);
if (iResult == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("error occured at wsarecv():%d\n", WSAGetLastError());
}
}
// 以便系统调用回调函数CompletionROUTINE
SleepEx(1000, TRUE);
}
5) 实现完成例程回调函数,处理异步请求完成事件
// 处理每个客户端的数据
void CALLBACK CompletionROUTINE(DWORD dwError,
DWORD cbTransferred,
LPWSAOVERLAPPED lpOverlapped,
DWORD dwFlags)
{
int iResult;
// 保存I/O操作的数据
LPPER_IO_OPERATION_DATA lpPerIOData = (LPPER_IO_OPERATION_DATA)lpOverlapped;
// 如果发生错误,或者没有传输数据,则关闭套接字,释放资源
if (dwError != 0 || cbTransferred == 0)
{
closesocket(lpPerIOData->sClient);
HeapFree(GetProcessHeap(), 0, lpPerIOData);
}
else
{
// g_pPerIODataArr[index]->szMessage contains the received data
lpPerIOData->szMessage[cbTransferred] = '\0';
printf("%d bytes data received: ", cbTransferred);
printf("%s\n", lpPerIOData->szMessage);
// 提交一个异步发送请求
strcpy(lpPerIOData->szMessage, "this is server");
iResult = WSASend(lpPerIOData->sClient, &lpPerIOData->Buffer, 1,
&lpPerIOData->NumberOfBytesSend,
lpPerIOData->Flags,
&lpPerIOData->overlap,
NULL);
if (iResult == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("error occured at wsasend():%d\n", WSAGetLastError());
}
}
// Launch another asynchronous recv operation
ZeroMemory(lpPerIOData->Buffer.buf, DEFAULT_BUF_LEN);
ZeroMemory(&(lpPerIOData->overlap), sizeof(WSAOVERLAPPED));
lpPerIOData->Buffer.len = DEFAULT_BUF_LEN;
lpPerIOData->Buffer.buf = lpPerIOData->szMessage;
iResult = WSARecv(lpPerIOData->sClient,
&lpPerIOData->Buffer,
1,
&lpPerIOData->NumberOfBytesRecvd,
&lpPerIOData->Flags,
&lpPerIOData->overlap,
CompletionROUTINE);
if (iResult == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("error occured at wsasend():%d\n", WSAGetLastError());
}
}
}
}
完成例程方式下的重叠I/O模型,服务端、客户端的最终运行效果如下:
六、完成端口模型
完成端口模型是在重叠I/O模型上的进一步优化。在重叠I/O模型中,处理异步请求的线程只有一个,在处理多个并发异步请求时就会导致效率下降,或者可以I/O请求时创建多个线程,但是这样的处理方式会在线程的创建以及切换操作下花费比较多的事件,影响了程序整体的性能。所以,完成端口模型针对这种情况,允许用户提前创建一定数量的线程,具体线程数量与cpu数目有关,然后在完成端口内部提供了线程池的管理,对多个重叠I/O操作完成的事件通知建立消息队列,并唤醒多个线程进行并发处理,这样既避免了反复创建线程的花费,也减轻了频繁切换线程的开销,同时也做到了高并发。
1、定义单I/O操作数据和单句柄数据结构
单I/O操作数据包含了重叠结构以及传输的数据等,单句柄数据是与单I/O操作数据对应的套接字句柄。
typedef enum // 操作类型
{
OP_RECV,
OP_SEND
}OPERATION_TYPE;
typedef struct // 自定义单I/O操作相关数据
{
WSAOVERLAPPED overlap;
WSABUF Buffer;
char szMessage[DEFAULT_BUF_LEN];
DWORD NumberOfBytesRecvd;
DWORD NumberOfBytesSend;
DWORD Flags;
OPERATION_TYPE OperationType;
}PER_IO_DATA, *LPPER_IO_DATA;
typedef struct // 自定义单句柄数据
{
SOCKET Socket;
}PER_HANDLE_DATA, *LPPER_HANDLE_DATA;
2、套接字的初始化操作如同阻塞模型,只是在创建监听套接字时,需要使用WSASocket()函数,并指定WSA_FLAG_OVERLAPPED参数,以支持重叠I/O
// Create listening socket,using WSASocket()
SOCKET ServSocket = INVALID_SOCKET;
ServSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_IP, NULL, 0, WSA_FLAG_OVERLAPPED);
3、利用CreateIoCompletionPort()函数创建完成端口对象,注意参数类型
// Create completion port
HANDLE CompletionPort = NULL;
CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
if (CompletionPort == NULL)
{
printf("CreateIoCompletionPort failed\n");
return 1;
}
4、依据机器cpu的数量,合理的创建线程
// Create worker thread, by the numbers of cpu
DWORD dwThreadId;
SYSTEM_INFO systeminfo;
GetSystemInfo(&systeminfo);
for (int i = 0; i < systeminfo.dwNumberOfProcessors; i++)
{
HANDLE ThreadHandle;
ThreadHandle = CreateThread(NULL, 0, WorkerThread, CompletionPort, 0, &dwThreadId);
if (ThreadHandle == NULL)
{
printf("CreateThread failed with error %d\n", GetLastError());
return 1;
}
CloseHandle(ThreadHandle);
}
5、循环处理客户端连接,如同阻塞模型
6、 利用CreateIoCompletionPort()函数,注意参数类型,将单句柄数据,也就是客户端套接字句柄,与完成端口对象进行绑定
// 分配并设置套接字句柄结构
LPPER_HANDLE_DATA PerHandleData = NULL;
PerHandleData = (LPPER_HANDLE_DATA)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(PER_HANDLE_DATA));
if (PerHandleData == NULL)
{
printf("GlobalAlloc failed with error %d\n", GetLastError());
return 1;
}
PerHandleData->Socket = AcceSocket;
// Associate the newly arrived client socket with completion port
if (CreateIoCompletionPort((HANDLE)AcceSocket,
CompletionPort,
(DWORD)PerHandleData, 0) == NULL)
{
printf("CreateIoCompletionPort failed\n");
return 1;
}
7、创建单I/O操作数据结构,并利用WSA*()函数提交异步操作请求
// Launch an asynchronous operation for new arrived connection
LPPER_IO_DATA lpPerIOData = NULL;
lpPerIOData = (LPPER_IO_DATA)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(PER_IO_DATA));
if (lpPerIOData == NULL)
{
printf("GlobalAlloc failed with error %d\n", GetLastError());
return 1;
}
// 初始化I/O操作结构
ZeroMemory(&(lpPerIOData->overlap), sizeof(WSAOVERLAPPED));
lpPerIOData->NumberOfBytesRecvd = 0;
lpPerIOData->Buffer.len = DEFAULT_BUF_LEN;
lpPerIOData->Buffer.buf = lpPerIOData->szMessage;
lpPerIOData->OperationType = OP_RECV;
// 提交一个异步接收数据请求,数据放到lpPerIOData中,由工作线程取出
iResult = WSARecv(AcceSocket,
&lpPerIOData->Buffer,
1,
&lpPerIOData->NumberOfBytesRecvd,
&lpPerIOData->Flags,
&lpPerIOData->overlap,
NULL);
if (iResult == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("error occured at wsarecv():%d\n", WSAGetLastError());
}
}
8、实现工作线程,并发处理多个异步请求完成事件、
DWORD WINAPI WorkerThread(LPVOID CompletionPortID)
{
// 获取完成端口句柄
HANDLE CompletionPort = (HANDLE)CompletionPortID;
DWORD dwBytesTransferred; // I/O操作中传输的字节数
LPPER_HANDLE_DATA PreHandleData; // 单句柄数据
LPPER_IO_DATA lpPerIOData; // I/O操作结构
// 循环等待I/O操作完成结果
while (TRUE)
{
// 获取本次I/O的相关信息
int iResult = GetQueuedCompletionStatus(
CompletionPort,
&dwBytesTransferred,
(PULONG_PTR)&PreHandleData,
(LPOVERLAPPED *)&lpPerIOData,
INFINITE);
if (iResult == 0)
{
printf("GetQueuedCompletionStatus failed");
return 0;
}
if (lpPerIOData->OperationType == OP_RECV)
{
if (dwBytesTransferred == 0)
{
// Connection was closed by client
closesocket(PreHandleData->Socket);
HeapFree(GetProcessHeap(), 0, PreHandleData);
HeapFree(GetProcessHeap(), 0, lpPerIOData);
}
else
{
// g_pPerIODataArr[index]->szMessage contains the received data
lpPerIOData->szMessage[dwBytesTransferred] = '\0';
printf("%d bytes data received: ", dwBytesTransferred);
printf("%s\n", lpPerIOData->szMessage);
// 提交一个异步发送请求
ZeroMemory(&(lpPerIOData->overlap), sizeof(WSAOVERLAPPED));
lpPerIOData->NumberOfBytesRecvd = 0;
lpPerIOData->Buffer.len = DEFAULT_BUF_LEN;
lpPerIOData->Buffer.buf = lpPerIOData->szMessage;
lpPerIOData->OperationType = OP_SEND;
strcpy(lpPerIOData->szMessage, "this is server");
iResult = WSASend(PreHandleData->Socket, &lpPerIOData->Buffer, 1,
&lpPerIOData->NumberOfBytesSend,
lpPerIOData->Flags,
&lpPerIOData->overlap,
NULL);
if (iResult == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("error occured at wsasend():%d\n", WSAGetLastError());
}
}
Sleep(1000);
// Launch another asynchronous recv operation
ZeroMemory(&(lpPerIOData->overlap), sizeof(WSAOVERLAPPED));
lpPerIOData->NumberOfBytesRecvd = 0;
lpPerIOData->Buffer.len = DEFAULT_BUF_LEN;
lpPerIOData->Buffer.buf = lpPerIOData->szMessage;
lpPerIOData->OperationType = OP_RECV;
iResult = WSARecv(PreHandleData->Socket,
&lpPerIOData->Buffer,
1,
&lpPerIOData->NumberOfBytesRecvd,
&lpPerIOData->Flags,
&lpPerIOData->overlap,
NULL);
if (iResult == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("error occured at wsarecv():%d\n", WSAGetLastError());
}
}
}
}
}
return 0;
}
完成端口I/O模式下,服务端、客户端的最终运行效果如下: