WinSocket模型的探討——完成連接埠模型(一)

來源:互聯網
上載者:User

眾所皆知,完成連接埠是在WINDOWS平台下效率最高,擴充性最好的IO模型,特別針對於WINSOCK的海量串連時,更能顯示出其威力。其實建立一個完成連接埠的伺服器也很簡單,只要注意幾個函數,瞭解一下關鍵的步驟也就行了。

這是篇完成連接埠入門級的文章,分為以下幾步來說明完成連接埠: 

  1. 函數
  2. 常見問題以及解答
  3. 步驟
  4. 常式

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);
}

好了,本篇文章就到此為止,以後有時間再繼續寫下一篇,下篇就不是這種步驟和函數的說明了,而是包括了封裝、架構、應用等等方面的技術。OK,下次再會!

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.