网络服务器编程——重叠IO模型

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,大家可以尝试自己去完成。

猜你喜欢

转载自blog.csdn.net/Bing_bing_bing_/article/details/83656029