4.3.4重叠I/O模型
异步IO和同步IO的区别:
同步IO中,线程启动一个IO操作然后就立即进入等待状态,直到IO操作完成后才醒来继续执行。
异步IO中,线程发送一个IO请求到内核,然后继续处理其他的事情,内核完成IO请求后,将会通知线程IO操作完成了。重叠IO属于异步IO。
在Windows socket中,接收数据分为2步:等待数据传输;将数据从系统复制到用户空间。
第一阶段(等待数据传输):select模型利用select函数主动检查系统中套接字是否满足可读条件;而WSAAsyncSelect模型和WSAEventSelect模型则被动等待系统的通知。
第二阶段:前三个模型在数据从系统复制到用户缓冲区时,线程阻塞(recv)。重叠IO的应用程序在调用输入函数(WSARecv)后继续执行,直到系统完成IO操作后发出通知。
总结:由于IO操作虽然耗时但并不占CPU资源,因此将IO操作交给操作系统来完成,等操作系统完成IO操作后,发送通知给应用程序。操作系统内部采用线程的方式来实现重叠IO,它可以同时接收多个客户端传来的数据。
重叠IO模型分为2种:事件通知、完成例程。
//重叠IO:事件通知TCP服务器端
#include <iostream>
#include <winsock2.h>
#include <windows.h>
#include <process.h>
#pragma comment (lib, "Ws2_32.lib")
using namespace std;
#define PORT 6000
#define SIZE 1024
//创建单IO结构体
typedef struct
{
WSAOVERLAPPED overlap; //每一个socket连接需要关联一个WSAOVERLAPPED对象
WSABUF Buffer; //与WSAOVERLAPPED对象绑定的缓冲区
char szMessage[SIZE]; //初始化buffer的缓冲区
DWORD NumberOfBytesRecvd; //指定接收到的字符的数目
DWORD Flags;
}MY_WSAOVERLAPPED, *LPMY_WSAOVERLAPPED;
SOCKET g_ClientSocketArr[WSA_MAXIMUM_WAIT_EVENTS];
WSAEVENT g_ClientEventArr[WSA_MAXIMUM_WAIT_EVENTS];
LPMY_WSAOVERLAPPED g_pWSAOVERLAPPED_Arr[WSA_MAXIMUM_WAIT_EVENTS];
int g_EvenCount = 0;
UINT WINAPI WorkerThread(LPVOID lpParameter);
int main(int argc,char ** argv)
{
//步骤1:当前应用程序和相应的socket库绑定
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
cout << "WSAStartup Failed!" << endl;
return -1;
}
//步骤2:创建监听套接字和服务器端IP/PORT
//SOCKET sockListen = WSASocket(AF_INET, SOCK_STREAM, 0,NULL,0, WSA_FLAG_OVERLAPPED);
SOCKET sockListen = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addr_server;
memset(&addr_server, 0, sizeof(addr_server));
addr_server.sin_family = AF_INET;
addr_server.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示绑定电脑上所有网卡IP
addr_server.sin_port = htons(PORT);//不能使用公认端口,即端口>= 1024
//步骤3:套接字绑定和监听
bind(sockListen, (SOCKADDR*)&addr_server, sizeof(addr_server));
listen(sockListen, 5);
cout << "Start Listen..." << endl;
//步骤4:创建线程
unsigned int thread_id = 0;
_beginthreadex(NULL, 0, WorkerThread, NULL, 0, &thread_id);
SOCKADDR_IN addr_client;
int len = sizeof(SOCKADDR);
SOCKET sockClient;
while (1)
{
//步骤5:等待客户端连接
sockClient = accept(sockListen, (struct sockaddr *)&addr_client, &len);
printf("Accepted Client IP:%s,PORT:%d\n", inet_ntoa(addr_client.sin_addr), ntohs(addr_client.sin_port));
g_ClientSocketArr[g_EvenCount] = sockClient;
//步骤6:分配一个单IO数据结构
g_pWSAOVERLAPPED_Arr[g_EvenCount] = (LPMY_WSAOVERLAPPED)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(MY_WSAOVERLAPPED));
//步骤7:初始化单IO数据结构
g_pWSAOVERLAPPED_Arr[g_EvenCount]->Buffer.len = SIZE;//接收缓冲区的长度
g_pWSAOVERLAPPED_Arr[g_EvenCount]->Buffer.buf = g_pWSAOVERLAPPED_Arr[g_EvenCount]->szMessage;//接收缓冲区
WSAEVENT newEvent = WSACreateEvent();
g_ClientEventArr[g_EvenCount] = newEvent;
g_pWSAOVERLAPPED_Arr[g_EvenCount]->overlap.hEvent = g_ClientEventArr[g_EvenCount];//事件和WSAOVERLAPPED绑定
//步骤8:接收数据
WSARecv(
g_ClientSocketArr[g_EvenCount],//接收套接字
&g_pWSAOVERLAPPED_Arr[g_EvenCount]->Buffer,//接收缓冲区
1,
&g_pWSAOVERLAPPED_Arr[g_EvenCount]->NumberOfBytesRecvd,//操作完成,接收数据的字节数
&g_pWSAOVERLAPPED_Arr[g_EvenCount]->Flags,
&g_pWSAOVERLAPPED_Arr[g_EvenCount]->overlap,//指向WSAOVERLAPPED结构指针
NULL);
g_EvenCount++;
}
//步骤14:关闭套接字和库解绑
closesocket(sockListen);
WSACleanup();
return 0;
}
UINT WINAPI WorkerThread(LPVOID lpParameter)
{
while (1)
{
//步骤9:等待事件发生
//int nIndex = WSAWaitForMultipleEvents(g_EvenCount, g_ClientEventArr, false, WSA_INFINITE, false);
int nIndex = WSAWaitForMultipleEvents(g_EvenCount, g_ClientEventArr, false, 1000, false);
if (nIndex == WSA_WAIT_FAILED || nIndex == WSA_WAIT_TIMEOUT)
continue;
//步骤10:重置触发的事件
nIndex = nIndex - WSA_WAIT_EVENT_0;
WSAResetEvent(g_ClientEventArr[nIndex]);
//步骤11:查询重叠操作的结果
DWORD cbTransferred;//接收的字节数
WSAGetOverlappedResult(
g_ClientSocketArr[nIndex],
&g_pWSAOVERLAPPED_Arr[nIndex]->overlap,
&cbTransferred,
TRUE,
&g_pWSAOVERLAPPED_Arr[g_EvenCount]->Flags);
//步骤12:若接收字节为0,则表示客户端断开连接
if (cbTransferred == 0)
{
closesocket(g_ClientSocketArr[nIndex]);
WSACloseEvent(g_ClientEventArr[nIndex]);
HeapFree(GetProcessHeap(), 0, g_pWSAOVERLAPPED_Arr[nIndex]);
//删除套接字数组、事件数组、WSAOVERLAPPED数组中对应的客户端数据
if (nIndex < g_EvenCount - 1)
{
//用最后一个数据来替换当前的数据
g_ClientSocketArr[nIndex] = g_ClientSocketArr[g_EvenCount - 1];
g_ClientEventArr[nIndex] = g_ClientEventArr[g_EvenCount - 1];
g_pWSAOVERLAPPED_Arr[nIndex] = g_pWSAOVERLAPPED_Arr[g_EvenCount - 1];
}
g_EvenCount--;
g_pWSAOVERLAPPED_Arr[g_EvenCount] = NULL;
}
else
{
//步骤13:二次开发
//数据保存在szMessage
char Buf[SIZE] = "\0";
strcpy_s(Buf,1024, g_pWSAOVERLAPPED_Arr[nIndex]->szMessage);
cout << Buf << endl;
strcat(Buf, ":Server Received");
send(g_ClientSocketArr[nIndex], Buf, strlen(Buf) + 1, 0);
//继续接收数据
WSARecv(g_ClientSocketArr[nIndex],
&g_pWSAOVERLAPPED_Arr[nIndex]->Buffer,
1,
&g_pWSAOVERLAPPED_Arr[nIndex]->NumberOfBytesRecvd,
&g_pWSAOVERLAPPED_Arr[nIndex]->Flags,
&g_pWSAOVERLAPPED_Arr[nIndex]->overlap,
NULL);
}
}
return 0;
}
//重叠IO:事件通知TCP客户端:
#include <WinSock2.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
using namespace std;
int main()
{
// Initialize Windows socket library
WSADATA wsaData;
WSAStartup(0x0202, &wsaData);
// Create client socket
SOCKET sockClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Connect to server
SOCKADDR_IN server;
memset(&server, 0, sizeof(SOCKADDR_IN));
server.sin_family = AF_INET;
server.sin_addr.S_un.S_addr = inet_addr("192.168.137.144");
server.sin_port = htons(6000);
connect(sockClient, (struct sockaddr *)&server, sizeof(SOCKADDR_IN));
while (1)
{
cout << "send:";
char Buf[1024] = "\0";
cin.getline(Buf, 1024);
// Send message
send(sockClient, Buf, strlen(Buf) + 1, 0);
// Receive message
recv(sockClient, Buf, 1024, 0);
printf("Received: '%s'\n", Buf);
}
// Clean up
closesocket(sockClient);
WSACleanup();
return 0;
}
重叠IO模型有以下相关函数:
(1)SOCKET WSASocket(int af,int type,int protocol,LPWSAPROTOCOL_INFOW lpProtocolInfo,GROUP g,DWORD dwFlags):创建套接字
参数dwFlags:要想在一个套接字上使用重叠IO模型,则此参数必须为WSA_FLAG_OVERLAPPED。
socket区别:使用socket时,系统默认设置WSA_FLAG_OVERLAPPED标志。因此可以使用socket代替WSASocket。
(2)SOCKET WSAAccept(SOCKET s,(*addrlen,*addrlen) struct sockaddr FAR * addr,LPINT addrlen,LPCONDITIONPROC lpfnCondition,DWORD_PTR dwCallbackData):等待客户端连接
accept区别:WSAAccept、accept是同步操作,而WSAAccept函数在accept函数基础上添加了条件函数判断是否接受客户端连接。因此可以使用accept代替WSAAccept。
(3)int WSASend(SOCKET s,(dwBufferCount) LPWSABUF lpBuffers,DWORD dwBufferCount,LPDWORD lpNumberOfBytesSent,DWORD dwFlags,LPWSAOVERLAPPED lpOverlapped,LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine):发送数据
send区别:对于使用WSASend争议颇多,因为使用WSASend太容易出问题。比如同时投递WSASend和WSARecv;一个socket上投递多个WSASend;连续投递WSASend却不检查等等。建议不熟悉的暂时不使用WSASend。
重叠IO模型重点知识:
(1)WSAOVERLAPPED结构体:这个结构是重叠IO模型的核心。通过其成员WSAEVENT hEvent来绑定事件对象,而事件对象用来通知应用程序操作完成。
(2)WSABUF结构体:
typedef struct _WSABUF {
ULONG len; //缓冲区长度
CHAR *buf; //缓冲区
} WSABUF,* LPWSABUF;
(3)int WSARecv(SOCKET s,LPWSABUF lpBuffers,DWORD dwBufferCount,LPDWORD lpNumberOfBytesRecvd,LPDWORD lpFlags,LPWSAOVERLAPPED lpOverlapped,LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine):接收数据
参数s:接收套接字;
参数lpBuffers:接收缓冲区;
参数dwBufferCount:数组中WSABUF结构的数量;
参数lpNumberOfBytesRecvd:如果接收操作立即完成,该参数指明接收数据的字节数;
参数lpFlags:标志位,一般为0;
参数lpOverlapped:指向WSAOVERLAPPED结构指针;
参数lpCompletionRoutine:完成例程。
recv区别:recv阻塞;WSARecv非阻塞。
(4)DWORD WSAWaitForMultipleEvents(DWORD cEvents,const WSAEVENT * lphEvents,BOOL fWaitAll,DWORD dwTimeout,BOOL fAlertable):等待事件触发
参数cEvents:等待事件的总数量;
参数lphEvents:事件数组的指针;
参数fWaitAll:设置为FALSE,则当任何一个事件被通知时,函数就会返回;
参数dwTimeout:超时时间,设置为 WSA_INFINITE表示一直等待,一直到有事件被通知(传信)才会返回;
参数fAlertable:在完成例程中会用到这个参数,这里先设置为FALSE。
(5)WSAResetEvent(WSAEVENT hEvent):重置当前这个用完的事件对象。
(6)BOOL WSAGetOverlappedResult(SOCKET s,LPWSAOVERLAPPED lpOverlapped,LPDWORD lpcbTransfer,BOOL fWait,LPDWORD lpdwFlags):查询重叠操作的结果
参数s:发起重叠操作的套接字;
参数lpOverlapped:发起重叠操作的WSAOVERLAPPED结构指针;
参数lpcbTransfer:实际发送或接收的字节数;
参数fWait:设置为TRUE,除非重叠操作完成,否则函数不会返回;设置FALSE,当操作处于挂起状态,那么函数就会返回FALSE;
参数lpdwFlags:指向DWORD的指针,负责接收结果标志。
(7)LPVOID HeapAlloc(HANDLE hHeap,DWORD dwFlags,SIZE_T dwBytes):分配堆内存
参数hHeap:堆句柄,表示从该堆分配内存,这个参数是函数HeapCreate或GetProcessHeap的返回值;
参数dwFlags:堆分配选项,HEAP_ZERO_MEMERY指明分配的内存将会被初始化为0;
参数dwBytes:分配的空间大小,单位为Byte。
malloc是C标准提供的API,而HeadAlloc是windows自身的API。在windows系统中使用malloc,它实际调用的就是HeadAlloc。
BOOL HeapFree(HANDLE hHeap,DWORD dwFlags,LPVOID lpMem):释放堆内存
为什么要在服务器端使用HeapAlloc,而不使用malloc和new。因为HeapAlloc分配内存的速度是malloc和new的5-10倍。
完成例程的重叠IO,大家可以尝试自己去完成。