眾所皆知,完成端口是在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)
{
OutErr("WSAStartup()");
}
}
///////////////////////////////////////////////////////////////////////
//
// 函數名 : 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)
{
CloseHandle(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);
}
好了,本篇文章就到此為止,
回復 更多評論