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)
?{
??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);
}
總結
以上是生活随笔為你收集整理的WinSocket模型的探讨——完成端口模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 结束查找到的所有飞鸽当前步骤(飞鸽传书2
- 下一篇: 原HP大中华区总裁孙振耀的退休感言