1. 程式人生 > >網路伺服器程式設計——重疊IO模型

網路伺服器程式設計——重疊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,大家可以嘗試自己去完成。