網路伺服器程式設計——完成埠
4.3.5完成埠模型(IOCP)
選擇模型是5種模型中效率最低的,而完成埠則是5種模型中效率最高的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;
UINT WINAPI WorkerThread(LPVOID lpParameter);
int main(int argc,char ** argv)
{
//步驟1:當前應用程式和相應的socket庫繫結
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
cout << "WSAStartup Failed!" << endl;
return -1;
}
//步驟2:建立完成埠
HANDLE h_CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
//步驟3:根據系統中CPU核心的數量建立對應的Worker執行緒
SYSTEM_INFO systeminfo;
GetSystemInfo(&systeminfo);
unsigned int thread_id = 0;
for (int i = 0; i < systeminfo.dwNumberOfProcessors*2; i++)//建立CPU核心數量*2的執行緒,可以充分利用CPU資源
{
_beginthreadex(NULL, 0, WorkerThread, h_CompletionPort, 0, &thread_id);
}
//步驟4:建立監聽套接字和伺服器端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
//步驟5:套接字繫結和監聽
bind(sockListen, (SOCKADDR*)&addr_server, sizeof(addr_server));
listen(sockListen, 5);
cout << "Start Listen..." << endl;
SOCKADDR_IN addr_client;
int len = sizeof(SOCKADDR);
SOCKET sockClient;
LPMY_WSAOVERLAPPED lp_OVERLAPPED;
while (1)
{
//步驟6:等待客戶端連線
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));
//步驟7:完成埠和套接字繫結
CreateIoCompletionPort((HANDLE)sockClient, h_CompletionPort, (DWORD)sockClient, 0);
//步驟8:分配一個單IO資料結構
lp_OVERLAPPED = (LPMY_WSAOVERLAPPED)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(MY_WSAOVERLAPPED));
//步驟9:初始化單IO資料結構
lp_OVERLAPPED->Buffer.len = SIZE;//接收緩衝區的長度
lp_OVERLAPPED->Buffer.buf = lp_OVERLAPPED->szMessage;//接收緩衝區
//步驟10:接收資料
WSARecv(
sockClient,//接收套接字
&lp_OVERLAPPED->Buffer,//接收緩衝區
1,
&lp_OVERLAPPED->NumberOfBytesRecvd,//操作完成,接收資料的位元組數
&lp_OVERLAPPED->Flags,
&lp_OVERLAPPED->overlap,//指向WSAOVERLAPPED結構指標
NULL);
}
//步驟13:喚醒工作者執行緒
PostQueuedCompletionStatus(h_CompletionPort, 0xFFFFFFFF, 0, NULL);
//步驟14:關閉套接字和庫解綁
CloseHandle(h_CompletionPort);
closesocket(sockListen);
WSACleanup();
return 0;
}
UINT WINAPI WorkerThread(LPVOID lpParameter)
{
HANDLE h_CompletionPort = (HANDLE)lpParameter;
DWORD NumberOfBytesTransferred;
SOCKET sockClient;
LPMY_WSAOVERLAPPED lpWSAOVERLAPPED = NULL;
while (1)
{
//步驟11:獲取完成埠的狀態。若完成端口出現已完成的IO請求,則執行緒被喚醒;否則繼續睡眠
GetQueuedCompletionStatus(
h_CompletionPort,
&NumberOfBytesTransferred,
(LPDWORD)&sockClient,
(LPOVERLAPPED*)&lpWSAOVERLAPPED,
INFINITE);
//步驟12:資料處理
if (NumberOfBytesTransferred == 0xFFFFFFFF)
{
return 0;
}
if (NumberOfBytesTransferred == 0)
{
cout << "客戶端斷開連線" << endl;
closesocket(sockClient);
HeapFree(GetProcessHeap(), 0, lpWSAOVERLAPPED);
}
else
{
//二次開發
char Buf[SIZE] = "\0";
strcpy_s(Buf, 1024, lpWSAOVERLAPPED->szMessage);
cout << Buf << endl;
strcat(Buf, ":Server Received");
send(sockClient, Buf, strlen(Buf) + 1, 0);
memset(lpWSAOVERLAPPED, 0, sizeof(MY_WSAOVERLAPPED));
lpWSAOVERLAPPED->Buffer.len = SIZE;
lpWSAOVERLAPPED->Buffer.buf = lpWSAOVERLAPPED->szMessage;
WSARecv(sockClient,
&lpWSAOVERLAPPED->Buffer,
1,
&lpWSAOVERLAPPED->NumberOfBytesRecvd,
&lpWSAOVERLAPPED->Flags,
&lpWSAOVERLAPPED->overlap,
NULL);
}
}
return 0;
}
//完成埠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;
}
HANDLE CreateIoCompletionPort(HANDLE FileHandle,HANDLE ExistingCompletionPort,ULONG_PTR CompletionKey,DWORD NumberOfConcurrentThreads):建立完成埠、或完成埠和套接字繫結
引數FileHandle:開啟重疊IO完成埠的檔案控制代碼。如果設定這個引數為INVALID_HANDLE_VALUE,CreateIoCompletionPort會建立一個不關聯任何檔案的完成埠,而且ExistingCompletionPort必須設定為NULL,CompletionKey也將被忽略;
引數ExistingCompletionPort:完成埠控制代碼。如果指定一個已經存在的完成埠,函式將關聯FileHandle指定的檔案;
引數CompletionKey:單檔案控制代碼,包含指定檔案每次IO完成資料包資訊;
引數NumberOfConcurrentThreads:系統允許在完成埠上併發處理IO完成包的最大執行緒數量。
BOOL GetQueuedCompletionStatus(HANDLE CompletionPort,LPDWORD lpNumberOfBytesTransferred,PULONG_PTR lpCompletionKey,LPOVERLAPPED* lpOverlapped,DWORD dwMilliseconds):查詢完成埠狀態
引數CompletionPort:指定的完成埠(IOCP),由CreateIoCompletionPort函式建立;
引數lpNumberOfBytesTransferred:一次I/O操作完成後,所傳送資料的位元組數;
引數lpCompletionKey:當檔案I/O操作完成後,用於存放與完成埠關聯的socket;
引數lpOverlapped:為呼叫IOCP機制所引用的OVERLAPPED結構;
引數dwMilliseconds: 等待完成埠的超時時間,INFINITE表示一直等待。
BOOL PostQueuedCompletionStatus(HANDLE CompletionPort,DWORD dwNumberOfBytesTransferred,ULONG_PTR dwCompletionKey,LPOVERLAPPED lpOverlapped):喚醒工作者執行緒
引數CompletionPort:指定想向其傳送一個完成資料包的完成埠物件;
引數dwNumberOfBytesTransferred:指定—個值,直接傳遞給GetQueuedCompletionStatus函式中對應的引數 ;
引數dwCompletionKey:指定—個值,直接傳遞給GetQueuedCompletionStatus函式中對應的引數;
引數lpOverlapped:指定—個值,直接傳遞給GetQueuedCompletionStatus函式中對應的引數。
當呼叫GetQueuedCompletionStatus,若完成埠無IO請求,則執行緒睡眠。如果完成埠上一直都沒有已經完成的I/O請求,那麼這些執行緒將無法被喚醒,這也意味著執行緒沒法正常退出。PostQueuedCompletionStatus函式讓我們手動的新增一個完成埠I/O操作,這樣處於睡眠等待的狀態的執行緒就會有一個被喚醒,如果為我們每一個工作執行緒都呼叫一次PostQueuedCompletionStatus(),那麼所有的執行緒也就會因此而被喚醒了。
總結:完成埠是C/S通訊模式中最好的I/O模型,也是最複雜的I/O模型。
重點1:一般情況下,我們只需要建立一個完成埠,儲存好它的控制代碼,後面會經常用到。
重點2:系統中有多少個處理器,可以建立處理器數量*2個執行緒。
重點3:可以用accept來接收客戶端的連線請求,也可以使用效能更好的AcceptEx來接收客戶端連線請求。accept阻塞;AcceptEx非阻塞。
重點4:客戶端連線後,就可以在socket上提交網路請求,如WSARecv。
推薦小豬的部落格:https://blog.csdn.net/piggyxp/article/details/6922277,這是國內對完成埠解釋最詳細的部落格。
4.3.6總結
該如何挑選最適合自己應用程式的 I / O模型?
1. 客戶機的開發
若打算開發一個客戶機應用,令其同時管理一個或多個套接字,那麼建議採用重疊I/O或WSAEventSelect模型,以便在一定程度上提升效能。然而,假如開發的是一個以Windows為基礎的應用程式,要進行視窗訊息的管理,那麼WSAAsyncSelect模型恐怕是一種最好的選擇,因為WSAAsyncSelect本身便是從Windows訊息模型借鑑來的。若採用這種模型,我們的程式一開始便具備了處理訊息的能力。
2. 伺服器的開發
若開發的是一個伺服器應用,要在一個給定的時間,同時控制幾個套接字,建議大家採用重疊I/O模型,這同樣是從效能出發點考慮的。但是,如果預計到自己的伺服器在任何給定的時間,都會為大量I/O請求提供服務,便應考慮使用I/O完成埠模型,從而獲得更好的效能。
節選自《網路程式設計》第8章。