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