WinSocket模型的探討——完成埠模型
眾所皆知,完成埠是在WINDOWS平臺下效率最高,擴充套件性最好的IO模型,特別針對於WINSOCK的海量連線時,更能顯示出其威力。其實建立一個完成埠的伺服器也很簡單,只要注意幾個函式,瞭解一下關鍵的步驟也就行了。
這是篇完成埠入門級的文章,分為以下幾步來說明完成埠:
函式
常見問題以及解答
步驟
例程
1、函式:
我們在完成埠模型下會使用到的最重要的兩個函式是:
CreateIoCompletionPort、GetQueuedCompletionStatus
CreateIoCompletionPort 的作用是建立一個完成埠和把一個IO控制代碼和完成埠關聯起來:
// 建立完成埠
HANDLE CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
// 把一個IO控制代碼和完成埠關聯起來,這裡的控制代碼是一個socket 控制代碼
CreateIoCompletionPort((HANDLE)sClient, CompletionPort, (DWORD)PerHandleData, 0);
其中第一個引數是控制代碼,可以是檔案控制代碼、SOCKET控制代碼。
第二個就是我們上面創建出來的完成埠,這裡就把兩個東西關聯在一起了。
第三個引數很關鍵,叫做PerHandleData,就是對應於每個控制代碼的資料塊。我們可以使用這個引數在後面取到與這個SOCKET對應的資料。
最後一個引數給0,意思就是根據CPU的個數,允許儘可能多的執行緒併發執行。
GetQueuedCompletionStatus 的作用就是取得完成埠的結果:
// 從完成埠中取得結果
GetQueuedCompletionStatus(CompletionPort, &BytesTransferred, (LPDWORD)&PerHandleData, (LPOVERLAPPED*)&PerIoData, INFINITE)
第一個引數是完成埠
第二個引數是表明這次的操作傳遞了多少個位元組的資料
第三個引數是OUT型別的引數,就是前面CreateIoCompletionPort傳進去的單控制代碼資料,這裡就是前面的SOCKET控制代碼以及與之相對應的資料,這裡作業系統給我們返回,讓我們不用自己去做列表查詢等操作了。
第四個引數就是進行IO操作的結果,是我們在投遞 WSARecv / WSASend 等操作時傳遞進去的,這裡作業系統做好準備後,給我們返回了。非常省事!!
個人感覺完成埠就是作業系統為我們包裝了很多重疊IO的不爽的地方,讓我們可以更方便的去使用.
2、常見問題和解答
a、什麼是單控制代碼資料(PerHandle)和單IO資料(PerIO)
單控制代碼資料就是和控制代碼對應的資料,像socket控制代碼,檔案控制代碼這種東西。
單IO資料,就是對應於每次的IO操作的資料。例如每次的WSARecv/WSASend等等
其實我覺得PER是每次的意思,翻譯成每個控制代碼資料和每次IO資料還比較清晰一點。
在完成埠中,單控制代碼資料直接通過GetQueuedCompletionStatus 返回,省去了我們自己做容器去管理。單IO資料也容許我們自己擴充套件OVERLAPPED結構,所以,在這裡所有與應用邏輯有關的東西都可以在此擴充套件。
b、如何判斷客戶端的斷開
我們要處理幾種情況
1) 如果客戶端呼叫了closesocket,我們就可以這樣判斷他的斷開:
if(0 == GetQueuedCompletionStatus(CompletionPort, &BytesTransferred, 。。。)
if(BytesTransferred == 0)
{
// 客戶端斷開,釋放資源
}
2) 如果是客戶端直接退出,那就會出現64錯誤,指定的網路名不可再用。這種情況我們也要處理的:
if(0 == GetQueuedCompletionStatus(。。。))
{
if( (GetLastError() == WAIT_TIMEOUT) || (GetLastError() == ERROR_NETNAME_DELETED) )
{
// 客戶端斷開,釋放資源
}
}
3、步驟
編寫完成埠服務程式,無非就是以下幾個步驟:
1、建立一個完成埠
2、根據CPU個數建立工作者執行緒,把完成埠傳進去執行緒裡
3、建立偵聽SOCKET,把SOCKET和完成埠關聯起來
4、建立PerIOData,向連線進來的SOCKET投遞WSARecv操作
5、執行緒裡所做的事情:
a、GetQueuedCompletionStatus,在退出的時候就可以使用PostQueudCompletionStatus使執行緒退出
b、取得資料並處理
4、例程
下面是服務端的例程,可以使用《WinSocket模型的探討——Overlapped模型(一)》中的客戶端程式來測試次服務端。稍微研究一下,也就會對完成埠模型有個大概的瞭解了。
/*
完成埠伺服器
接收到客戶端的資訊,直接顯示出來
*/
#include "winerror.h"
#include "Winsock2.h"
#pragma comment(lib, "ws2_32")
#include "windows.h"
#include <iostream>
using namespace std;
/// 巨集定義
#define PORT 5050
#define DATA_BUFSIZE 8192
#define OutErr(a) cout << (a) << endl
<< "出錯程式碼:" << WSAGetLastError() << endl
<< "出錯檔案:" << __FILE__ << endl
<< "出錯行數:" << __LINE__ << endl
#define OutMsg(a) cout << (a) << endl;
/// 全域性函式定義
///////////////////////////////////////////////////////////////////////
//
// 函式名 : InitWinsock
// 功能描述 : 初始化WINSOCK
// 返回值 : void
//
///////////////////////////////////////////////////////////////////////
void InitWinsock()
{
// 初始化WINSOCK
WSADATA wsd;
if( WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
}
///////////////////////////////////////////////////////////////////////
//
// 函式名 : BindServerOverlapped
// 功能描述 : 繫結埠,並返回一個 Overlapped 的Listen Socket
// 引數 : int nPort
// 返回值 : SOCKET
//
///////////////////////////////////////////////////////////////////////
SOCKET BindServerOverlapped(int nPort)
{
// 建立socket
SOCKET sServer = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
// 繫結埠
struct sockaddr_in servAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(nPort);
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sServer, (struct sockaddr *)&servAddr, sizeof(servAddr)) < 0)
{
OutErr("bind Failed!");
return NULL;
}
// 設定監聽佇列為200
if(listen(sServer, 200) != 0)
{
OutErr("listen Failed!");
return NULL;
}
return sServer;
}
/// 結構體定義
typedef struct
{
OVERLAPPED Overlapped;
WSABUF DataBuf;
CHAR Buffer[DATA_BUFSIZE];
} PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;
typedef struct
{
SOCKET Socket;
} PER_HANDLE_DATA, * LPPER_HANDLE_DATA;
DWORD WINAPI ProcessIO(LPVOID lpParam)
{
HANDLE CompletionPort = (HANDLE)lpParam;
DWORD BytesTransferred;
LPPER_HANDLE_DATA PerHandleData;
LPPER_IO_OPERATION_DATA PerIoData;
while(true)
{
if(0 == GetQueuedCompletionStatus(CompletionPort, &BytesTransferred, (LPDWORD)&PerHandleData, (LPOVERLAPPED*)&PerIoData, INFINITE))
{
if( (GetLastError() == WAIT_TIMEOUT) || (GetLastError() == ERROR_NETNAME_DELETED) )
{
cout << "closing socket" << PerHandleData->Socket << endl;
closesocket(PerHandleData->Socket);
delete PerIoData;
delete PerHandleData;
continue;
}
else
{
OutErr("GetQueuedCompletionStatus failed!");
}
return 0;
}
// 說明客戶端已經退出
if(BytesTransferred == 0)
{
cout << "closing socket" << PerHandleData->Socket << endl;
closesocket(PerHandleData->Socket);
delete PerIoData;
delete PerHandleData;
continue;
}
// 取得資料並處理
cout << PerHandleData->Socket << "傳送過來的訊息:" << PerIoData->Buffer << endl;
// 繼續向 socket 投遞WSARecv操作
DWORD Flags = 0;
DWORD dwRecv = 0;
ZeroMemory(PerIoData, sizeof(PER_IO_OPERATION_DATA));
PerIoData->DataBuf.buf = PerIoData->Buffer;
PerIoData->DataBuf.len = DATA_BUFSIZE;
WSARecv(PerHandleData->Socket, &PerIoData->DataBuf, 1, &dwRecv, &Flags, &PerIoData->Overlapped, NULL);
}
return 0;
}
void main()
{
InitWinsock();
HANDLE CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
// 根據系統的CPU來建立工作者執行緒
SYSTEM_INFO SystemInfo;
GetSystemInfo(&SystemInfo);
for(int i = 0; i < SystemInfo.dwNumberOfProcessors * 2; i++)
{
HANDLE hProcessIO = CreateThread(NULL, 0, ProcessIO, CompletionPort, 0, NULL);
if(hProcessIO)
}
// 建立偵聽SOCKET
SOCKET sListen = BindServerOverlapped(PORT);
SOCKET sClient;
LPPER_HANDLE_DATA PerHandleData;
LPPER_IO_OPERATION_DATA PerIoData;
while(true)
{
// 等待客戶端接入
//sClient = WSAAccept(sListen, NULL, NULL, NULL, 0);
sClient = accept(sListen, 0, 0);
cout << "Socket " << sClient << "連線進來" << endl;
PerHandleData = new PER_HANDLE_DATA();
PerHandleData->Socket = sClient;
// 將接入的客戶端和完成埠聯絡起來
CreateIoCompletionPort((HANDLE)sClient, CompletionPort, (DWORD)PerHandleData, 0);
// 建立一個Overlapped,並使用這個Overlapped結構對socket投遞操作
PerIoData = new PER_IO_OPERATION_DATA();
ZeroMemory(PerIoData, sizeof(PER_IO_OPERATION_DATA));
PerIoData->DataBuf.buf = PerIoData->Buffer;
PerIoData->DataBuf.len = DATA_BUFSIZE;
// 投遞一個WSARecv操作
DWORD Flags = 0;
DWORD dwRecv = 0;
WSARecv(sClient, &PerIoData->DataBuf, 1, &dwRecv, &Flags, &PerIoData->Overlapped, NULL);
}
DWORD dwByteTrans;
PostQueuedCompletionStatus(CompletionPort, dwByteTrans, 0, 0);
closesocket(sListen);
}