程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 用完成端口開發大響應規模的Winsock應用程序

用完成端口開發大響應規模的Winsock應用程序

編輯:關於VC++

通常要開發網絡應用程序並不是一件輕松的事情,不過,實際上只要掌握幾個關鍵的原則 也就可以了——創建和連接一個套接字,嘗試進行連接,然後收發數據。真正難 的是要寫出一個可以接納少則一個,多則數千個連接的網絡應用程序。本文將討論如何通過 Winsock2在Windows NT 和 Windows 2000上開發高擴展能力的Winsock應用程序。文章主要的 焦點在客戶機/服務器模型的服務器這一方,當然,其中的許多要點對模型的雙方都適用。

API與響應規模

通過Win32的重疊I/O機制,應用程序可以提請一項I/O操作,重疊 的操作請求在後台完成,而同一時間提請操作的線程去做其他的事情。等重疊操作完成後線 程收到有關的通知。這種機制對那些耗時的操作而言特別有用。不過,像Windows 3.1上的 WSAAsyncSelect()及Unix下的select()那樣的函數雖然易於使用,但是它們不能滿足響應規 模的需要。而完成端口機制是針對操作系統內部進行了優化,在Windows NT 和 Windows 2000上,使用了完成端口的重疊I/O機制才能夠真正擴大系統的響應規模。

完成端口

一個完成端口其實就是一個通知隊列,由操作系統把已經完成的重疊I/O請求的通知 放入其中。當某項I/O操作一旦完成,某個可以對該操作結果進行處理的工作者線程就會收到 一則通知。而套接字在被創建後,可以在任何時候與某個完成端口進行關聯。

通常情 況下,我們會在應用程序中創建一定數量的工作者線程來處理這些通知。線程數量取決於應 用程序的特定需要。理想的情況是,線程數量等於處理器的數量,不過這也要求任何線程都 不應該執行諸如同步讀寫、等待事件通知等阻塞型的操作,以免線程阻塞。每個線程都將分 到一定的CPU時間,在此期間該線程可以運行,然後另一個線程將分到一個時間片並開始執行 。如果某個線程執行了阻塞型的操作,操作系統將剝奪其未使用的剩余時間片並讓其它線程 開始執行。也就是說,前一個線程沒有充分使用其時間片,當發生這樣的情況時,應用程序 應該准備其它線程來充分利用這些時間片。

完成端口的使用分為兩步。首先創建完成 端口,如以下代碼所示:

HANDLE  hIocp;
hIocp = CreateIoCompletionPort(
  INVALID_HANDLE_VALUE,
  NULL,
   (ULONG_PTR)0,
  0);
if (hIocp == NULL) {
  // Error
}

完成端口創建後,要把將使用該完成端口的套接字與之關聯起來。方法是再次調用 CreateIoCompletionPort ()函數,第一個參數FileHandle設為套接字的句柄,第二個參數 ExistingCompletionPort 設為剛剛創建的那個完成端口的句柄。

以下代碼創建了一 個套接字,並把它和前面創建的完成端口關聯起來:SOCKET  s;
s = socket (AF_INET, SOCK_STREAM, 0);
if (s == INVALID_SOCKET) {
  // Error
if (CreateIoCompletionPort((HANDLE)s,
              hIocp,
              (ULONG_PTR)0,
              0) == NULL)
{
// Error
}
...
}

這時就完成了套接字 與完成端口的關聯操作。在這個套接字上進行的任何重疊操作都將通過完成端口發出完成通 知。注意,CreateIoCompletionPort()函數中的第三個參數用來設置一個與該套接字相關的 “完成鍵(completion key)”(譯者注:完成鍵可以是任何數據類型)。每當完成 通知到來時,應用程序可以讀取相應的完成鍵,因此,完成鍵可用來給套接字傳遞一些背景 信息。

在創建了完成端口、將一個或多個套接字與之相關聯之後,我們就要創建若干個線 程來處理完成通知。這些線程不斷循環調用GetQueuedCompletionStatus ()函數並返回完成 通知。

下面,我們先來看看應用程序如何跟蹤這些重疊操作。當應用程序調用一個重 疊操作函數時,要把指向一個overlapped結構的指針包括在其參數中。當操作完成後,我們 可以通過GetQueuedCompletionStatus()函數中拿回這個指針。不過,單是根據這個指針所指 向的overlapped結構,應用程序並不能分辨究竟完成的是哪個操作。要實現對操作的跟蹤, 你可以自己定義一個OVERLAPPED結構,在其中加入所需的跟蹤信息。

無論何時調用重 疊操作函數時,總是會通過其lpOverlapped參數傳遞一個OVERLAPPEDPLUS結構(例如WSASend 、 WSARecv等函數)。這就允許你為每一個重疊調用操作設置某些操作狀態信息,當操作結束 後,你可以通過GetQueuedCompletionStatus()函數獲得你自定義結構的指針。注意 OVERLAPPED字段不要求一定是這個擴展後的結構的第一個字段。當得到了指向OVERLAPPED結 構的指針以後,可以用CONTAINING_RECORD宏取出其中指向擴展結構的指針。

OVERLAPPED 結構的定義如下:

typedef struct _OVERLAPPEDPLUS {
   OVERLAPPED    ol;
   SOCKET      s, sclient;
   int         OpCode;
   WSABUF      wbuf;
   DWORD       dwBytes, dwFlags;
   // 其它有用的信息
} OVERLAPPEDPLUS;
#define OP_READ   0
#define OP_WRITE  1
#define OP_ACCEPT  2

下面讓 我們來看看工作者線程的情況。

工作線程WorkerThread代碼:

DWORD WINAPI WorkerThread(LPVOID lpParam)
{  
  ULONG_PTR    *PerHandleKey;
  OVERLAPPED   *Overlap;
  OVERLAPPEDPLUS  *OverlapPlus,
          *newolp;
  DWORD      dwBytesXfered;
  while (1)
  {
    ret = GetQueuedCompletionStatus(
      hIocp,
       &dwBytesXfered,
      (PULONG_PTR)&PerHandleKey,
       &Overlap,
      INFINITE);
    if (ret == 0)
     {
      // Operation failed
      continue;
     }
    OverlapPlus = CONTAINING_RECORD(Overlap, OVERLAPPEDPLUS, ol);
  
  switch (OverlapPlus->OpCode)
  {
  case OP_ACCEPT:
    // Client socket is contained in OverlapPlus.sclient
    // Add client to completion port
       CreateIoCompletionPort(
        (HANDLE)OverlapPlus->sclient,
        hIocp,
        (ULONG_PTR)0,
         0);
    // Need a new OVERLAPPEDPLUS structure
    // for the newly accepted socket. Perhaps
    // keep a look aside list of free structures.
    newolp = AllocateOverlappedPlus();
    if (! newolp)
    {
      // Error
    }
     newolp->s = OverlapPlus->sclient;
    newolp->OpCode = OP_READ;
    // This function prepares the data to be sent
     PrepareSendBuffer(&newolp->wbuf);
 
    ret = WSASend(
        newolp->s,
        &newolp->wbuf,
        1,
        &newolp->dwBytes,
         0,
        &newolp.ol,
        NULL);
     
    if (ret == SOCKET_ERROR)
    {
      if (WSAGetLastError() != WSA_IO_PENDING)
      {
      // Error
      }
    }
    // Put structure in look aside list for later use
    FreeOverlappedPlus(OverlapPlus);
     // Signal accept thread to issue another AcceptEx
    SetEvent (hAcceptThread);
    break;
  case OP_READ:
    // Process the data read
    // ...
    // Repost the read if necessary, reusing the same
    // receive buffer as before
     memset(&OverlapPlus->ol, 0, sizeof(OVERLAPPED));
    ret = WSARecv(
       OverlapPlus->s,
        &OverlapPlus->wbuf,
       1,
        &OverlapPlus->dwBytes,
       &OverlapPlus->dwFlags,
       &OverlapPlus->ol,
       NULL);
     if (ret == SOCKET_ERROR)
    {
      if (WSAGetLastError() != WSA_IO_PENDING)
      {
        // Error
       }
    }
    break;
  case OP_WRITE:
    // Process the data sent, etc.
    break;
  } // switch
  } // while
} // WorkerThread

其中每句柄鍵(PerHandleKey)變量的內容,是在 把完成端口與套接字進行關聯時所設置的完成鍵參數;Overlap參數返回的是一個指向發出重 疊操作時所使用的那個OVERLAPPEDPLUS結構的指針。

要記住,如果重疊操作調用失敗時( 也就是說,返回值是SOCKET_ERROR,並且錯誤原因不是WSA_IO_PENDING),那麼完成端口將不 會收到任何完成通知。如果重疊操作調用成功,或者發生原因是WSA_IO_PENDING的錯誤時, 完成端口將總是能夠收到完成通知。

Windows NT和Windows 2000的套接字架構

對於開發大響應規模的Winsock應用程序而言,對Windows NT和Windows 2000的套接 字架構有基本的了解是很有幫助的。下圖是Windows 2000中的Winsock架構:

與其它類型操作系 統不同,Windows NT和Windows 2000的傳輸協議沒有一種風格像套接字那樣的、可以和應用 程序直接交談的界面,而是采用了一種更為底層的API,叫做傳輸驅動程序界面(Transport Driver Interface,TDI)。Winsock的核心模式驅動程序負責連接和緩沖區管理,以便向應用 程序提供套接字仿真(在AFD.SYS文件中實現),同時負責與底層傳輸驅動程序對話。

誰來負責管理緩沖區?

正如上面所說的,應用程序通過Winsock來和傳輸協議驅動程 序交談,而AFD.SYS負責為應用程序進行緩沖區管理。也就是說,當應用程序調用send()或 WSASend()函數來發送數據時,AFD.SYS將把數據拷貝進它自己的內部緩沖區(取決於 SO_SNDBUF設定值),然後send()或WSASend()函數立即返回。也可以這麼說,AFD.SYS在後台 負責把數據發送出去。不過,如果應用程序要求發出的數據超過了SO_SNDBUF設定的緩沖區大 小,那麼WSASend()函數會阻塞,直至所有數據發送完畢。

從遠程客戶端接收數據的 情況也類似。只要不用從應用程序那裡接收大量的數據,而且沒有超出SO_RCVBUF設定的值, AFD.SYS將把數據先拷貝到其內部緩沖區中。當應用程序調用recv()或WSARecv()函數時,數 據將從內部緩沖拷貝到應用程序提供的緩沖區。

多數情況下,這樣的架構運行良好, 特別在是應用程序采用傳統的套接字下非重疊的send()和receive()模式編寫的時候。不過程 序員要小心的是,盡管可以通過setsockopt()這個API來把SO_SNDBUF和SO_RCVBUF選項值設成 0(關閉內部緩沖區),但是程序員必須十分清楚把AFD.SYS的內部緩沖區關掉會造成什麼後果 ,避免收發數據時有關的緩沖區拷貝可能引起的系統崩潰。

舉例來說,一個應用程序 通過設定SO_SNDBUF為0把緩沖區關閉,然後發出一個阻塞send()調用。在這樣的情況下,系 統內核會把應用程序的緩沖區鎖定,直到接收方確認收到了整個緩沖區後send()調用才返回 。似乎這是一種判定你的數據是否已經為對方全部收到的簡潔的方法,實際上卻並非如此。 想想看,即使遠端TCP通知數據已經收到,其實也根本不代表數據已經成功送給客戶端應用程 序,比如對方可能發生資源不足的情況,導致AFD.SYS不能把數據拷貝給應用程序。另一個更 要緊的問題是,在每個線程中每次只能進行一次發送調用,效率極其低下。

把 SO_RCVBUF設為0,關閉AFD.SYS的接收緩沖區也不能讓性能得到提升,這只會迫使接收到的數 據在比Winsock更低的層次進行緩沖,當你發出receive調用時,同樣要進行緩沖區拷貝,因 此你本來想避免緩沖區拷貝的陰謀不會得逞。

現在我們應該清楚了,關閉緩沖區對於 多數應用程序而言並不是什麼好主意。只要要應用程序注意隨時在某個連接上保持幾個 WSARecvs重疊調用,那麼通常沒有必要關閉接收緩沖區。如果AFD.SYS總是有由應用程序提供 的緩沖區可用,那麼它將沒有必要使用內部緩沖區。

高性能的服務器應用程序可以關 閉發送緩沖區,同時不會損失性能。不過,這樣的應用程序必須十分小心,保證它總是發出 多個重疊發送調用,而不是等待某個重疊發送結束了才發出下一個。如果應用程序是按一個 發完再發下一個的順序來操作,那浪費掉兩次發送中間的空檔時間,總之是要保證傳輸驅動 程序在發送完一個緩沖區後,立刻可以轉向另一個緩沖區。

資源的限制條件

在設計任何服務器應用程序時,其強健性是主要的目標。也就是說,

你的應用程序要 能夠應對任何突發的問題,例如並發客戶請求數達到峰值、可用內存臨時出現不足、以及其 它短時間的現象。這就要求程序的設計者注意Windows NT和2000系統下的資源限制條件的問 題,從容地處理突發性事件。

你可以直接控制的、最基本的資源就是網絡帶寬。通常 ,使用用戶數據報協議(UDP)的應用程序都可能會比較注意帶寬方面的限制,以最大限度地減 少包的丟失。然而,在使用TCP連接時,服務器必須十分小心地控制好,防止網絡帶寬過載超 過一定的時間,否則將需要重發大量的包或造成大量連接中斷。關於帶寬管理的方法應根據 不同的應用程序而定,這超出了本文討論的范圍。

虛擬內存的使用也必須很小心地管 理。通過謹慎地申請和釋放內存,或者應用lookaside lists(一種高速緩存)技術來重新使用 已分配的內存,將有助於控制服務器應用程序的內存開銷(原文為“讓服務器應用程序 留下的腳印小一點”),避免操作系統頻繁地將應用程序申請的物理內存交換到虛擬內 存中(原文為“讓操作系統能夠總是把更多的應用程序地址空間更多地保留在內存中 ”)。你也可以通過SetWorkingSetSize()這個Win32 API讓操作系統分配給你的應用程 序更多的物理內存。

在使用Winsock時還可能碰到另外兩個非直接的資源不足情況。 一個是被鎖定的內存頁面的極限。如果你把AFD.SYS的緩沖關閉,當應用程序收發數據時,應 用程序緩沖區的所有頁面將被鎖定到物理內存中。這是因為內核驅動程序需要訪問這些內存 ,在此期間這些頁面不能交換出去。如果操作系統需要給其它應用程序分配一些可分頁的物 理內存,而又沒有足夠的內存時就會發生問題。我們的目標是要防止寫出一個病態的、鎖定 所有物理內存、讓系統崩潰的程序。也就是說,你的程序鎖定內存時,不要超出系統規定的 內存分頁極限。

在Windows NT和2000系統上,所有應用程序總共可以鎖定的內存大約 是物理內存的1/8(不過這只是一個大概的估計,不是你計算內存的依據)。如果你的應用程序 不注意這一點,當你的發出太多的重疊收發調用,而且I/O沒來得及完成時,就可能偶爾發生 ERROR_INSUFFICIENT_RESOURCES的錯誤。在這種情況下你要避免過度鎖定內存。同時要注意 ,系統會鎖定包含你的緩沖區所在的整個內存頁面,因此緩沖區靠近頁邊界時是有代價的(譯 者理解,緩沖區如果正好超過頁面邊界,那怕是1個字節,超出的這個字節所在的頁面也會被 鎖定)。

另外一個限制是你的程序可能會遇到系統未分頁池資源不足的情況。所謂未 分頁池是一塊永遠不被交換出去的內存區域,這塊內存用來存儲一些供各種內核組件訪問的 數據,其中有的內核組件是不能訪問那些被交換出去的頁面空間的。Windows NT和2000的驅 動程序能夠從這個特定的未分頁池分配內存。

當應用程序創建一個套接字(或者是類 似的打開某個文件)時,內核會從未分頁池中分配一定數量的內存,而且在綁定、連接套接字 時,內核又會從未分頁池中再分配一些內存。當你注意觀察這種行為時你將發現,如果你發 出某些I/O請求時(例如收發數據),你會從未分頁池裡再分配多一些內存(比如要追蹤某個待 決的I/O操作,你可能需要給這個操作添加一個自定義結構,如前文所提及的)。最後這就可 能會造成一定的問題,操作系統會限制未分頁內存的用量。

在Windows NT和2000這兩 種操作系統上,給每個連接分配的未分頁內存的具體數量是不同的,未來版本的Windows很可 能也不同。為了使應用程序的生命期更長,你就不應該計算對未分頁池內存的具體需求量。

你的程序必須防止消耗到未分頁池的極限。當系統中未分頁池剩余空間太小時,某些 與你的應用程序毫無關系的內核驅動就會發瘋,甚至造成系統崩潰,特別是當系統中有第三 方設備或驅動程序時,更容易發生這樣的慘劇(而且無法預測)。同時你還要記住,同一台電 腦上還可能運行有其它同樣消耗未分頁池的其它應用程序,因此在設計你的應用程序時,對 資源量的預估要特別保守和謹慎。

處理資源不足的問題是十分復雜的,因為發生上述 情況時你不會收到特別的錯誤代碼,通常你只能收到一般性的WSAENOBUFS或者 ERROR_INSUFFICIENT_RESOURCES 錯誤。要處理這些錯誤,首先,把你的應用程序工作配置調 整到合理的最大值(譯者注:所謂工作配置,是指應用程序各部分運行中所需的內存用量,請 參考 http://msdn.microsoft.com/msdnmag/issues/1000/Bugslayer/Bugslayer1000.asp , 關於內存優化,譯者另有譯文),如果錯誤繼續出現,那麼注意檢查是否是網絡帶寬不足的問 題。之後,請確認你沒有同時發出太多的收發調用。最後,如果還是收到資源不足的錯誤, 那就很可能是遇到了未分頁內存池不足的問題了。要釋放未分頁內存池空間,請關閉應用程 序中相當部分的連接,等待系統自行渡過和修正這個瞬時的錯誤。

接受連接請求

服務器要做的最普通的事情之一就是接受來自客戶端的連接請求。在套接字上使用重 疊I/O接受連接的惟一API就是AcceptEx()函數。有趣的是,通常的同步接受函數accept()的 返回值是一個新的套接字,而AcceptEx()函數則需要另外一個套接字作為它的參數之一。這 是因為AcceptEx()是一個重疊操作,所以你需要事先創建一個套接字(但不要綁定或連接它) ,並把這個套接字通過參數傳給AcceptEx()。以下是一小段典型的使用AcceptEx()的偽代碼 :

do {
   -等待上一個 AcceptEx 完成
   -創建一個新套接字並 與完成端口進行關聯
   -設置背景結構等等
   -發出一個 AcceptEx 請求
}while(TRUE);

作為一個高響應能力的服務器,它必須發出足夠的AcceptEx調用, 守候著,一旦出現客戶端連接請求就立刻響應。至於發出多少個AcceptEx才夠,就取決於你 的服務器程序所期待的通信交通類型。比如,如果進入連接率高的情況(因為連接持續時間較 短,或者出現交通高峰),那麼所需要守候的AcceptEx當然要比那些偶爾進入的客戶端連接的 情況要多。聰明的做法是,由應用程序來分析交通狀況,並調整AcceptEx守候的數量,而不 是固定在某個數量上。

對於Windows2000,Winsock提供了一些機制,幫助你判定AcceptEx 的數量是否足夠。這就是,在創建監聽套接字時創建一個事件,通過WSAEventSelect()這個 API並注冊FD_ACCEPT事件通知來把套接字和這個事件關聯起來。一旦系統收到一個連接請求 ,如果系統中沒有AcceptEx()正在等待接受連接,那麼上面的事件將收到一個信號。通過這 個事件,你就可以判斷你有沒有發出足夠的AcceptEx(),或者檢測出一個非正常的客戶請求( 下文述)。這種機制對Windows NT 4.0不適用。

使用AcceptEx()的一大好處是,你可 以通過一次調用就完成接受客戶端連接請求和接受數據(通過傳送lpOutputBuffer參數)兩件 事情。也就是說,如果客戶端在發出連接的同時傳輸數據,你的AcceptEx()調用在連接創建 並接收了客戶端數據後就可以立刻返回。這樣可能是很有用的,但是也可能會引發問題,因 為AcceptEx()必須等全部客戶端數據都收到了才返回。具體來說,如果你在發出AcceptEx() 調用的同時傳遞了lpOutputBuffer參數,那麼AcceptEx()不再是一項原子型的操作,而是分 成了兩步:接受客戶連接,等待接收數據。當缺少一種機制來通知你的應用程序所發生的這 種情況:“連接已經建立了,正在等待客戶端數據”,這將意味著有可能出現客 戶端只發出連接請求,但是不發送數據。如果你的服務器收到太多這種類型的連接時,它將 拒絕連接更多的合法客戶端請求。這就是黑客進行“拒絕服務”攻擊的常見手法 。

要預防此類攻擊,接受連接的線程應該不時地通過調用getsockopt()函數(選項參 數為SO_CONNECT_TIME)來檢查AcceptEx()裡守候的套接字。getsockopt()函數的選項值將被 設置為套接字被連接的時間,或者設置為-1(代表套接字尚未建立連接)。這時, WSAEventSelect()的特性就可以很好地利用來做這種檢查。如果發現連接已經建立,但是很 久都沒有收到數據的情況,那麼就應該終止連接,方法就是關閉作為參數提供給AcceptEx() 的那個套接字。注意,在多數非緊急情況下,如果套接字已經傳遞給AcceptEx()並開始守候 ,但還未建立連接,那麼你的應用程序不應該關閉它們。這是因為即使關閉了這些套接字, 出於提高系統性能的考慮,在連接進入之前,或者監聽套接字自身被關閉之前,相應的內核 模式的數據結構也不會被干淨地清除。

發出AcceptEx()調用的線程,似乎與那個進行 完成端口關聯操作、處理其它I/O完成通知的線程是同一個,但是,別忘記線程裡應該盡力避 免執行阻塞型的操作。Winsock2分層結構的一個副作用是調用socket()或WSASocket() API的 上層架構可能很重要(譯者不太明白原文意思,抱歉)。每個AcceptEx()調用都需要創建一個 新套接字,所以最好有一個獨立的線程專門調用AcceptEx(),而不參與其它I/O處理。你也可 以利用這個線程來執行其它任務,比如事件記錄。

有關AcceptEx()的最後一個注意事 項:要實現這些API,並不需要其它提供商提供的Winsock2實現。這一點對微軟特有的其它 API也同樣適用,比如TransmitFile()和GetAcceptExSockAddrs(),以及其它可能會被加入到 新版Windows的API. 在Windows NT和2000上,這些API是在微軟的底層提供者DLL (mswsock.dll)中實現的,可通過與mswsock.lib編譯連接進行調用,或者通過WSAIoctl() ( 選項參數為SIO_GET_EXTENSION_FUNCTION_POINTER)動態獲得函數的指針。

如果在沒 有事先獲得函數指針的情況下直接調用函數(也就是說,編譯時靜態連接mswsock.lib,在程 序中直接調用函數),那麼性能將很受影響。因為AcceptEx()被置於Winsock2架構之外,每次 調用時它都被迫通過WSAIoctl()取得函數指針。要避免這種性能損失,需要使用這些API的應 用程序應該通過調用WSAIoctl()直接從底層的提供者那裡取得函數的指針。

參見下圖 套接字架構:

TransmitFile 和 TransmitPackets

Winsock 提供兩個專門為文件和內存數據傳輸進行了優化的函數。 其中TransmitFile()這個API函數在Windows NT 4.0 和 Windows 2000上都可以使用,而 TransmitPackets()則將在未來版本的Windows中實現。

TransmitFile()用來把文件內 容通過Winsock進行傳輸。通常發送文件的做法是,先調用CreateFile()打開一個文件,然後 不斷循環調用ReadFile() 和WSASend ()直至數據發送完畢。但是這種方法很沒有效率,因為 每次調用ReadFile() 和 WSASend ()都會涉及一次從用戶模式到內核模式的轉換。如果換成 TransmitFile(),那麼只需要給它一個已打開文件的句柄和要發送的字節數,而所涉及的模 式轉換操作將只在調用CreateFile()打開文件時發生一次,然後TransmitFile()時再發生一 次。這樣效率就高多了。

TransmitPackets()比TransmitFile()更進一步,它允許用 戶只調用一次就可以發送指定的多個文件和內存緩沖區。函數原型如下:

BOOL TransmitPackets(
 SOCKET hSocket,
 LPTRANSMIT_PACKET_ELEMENT lpPacketArray,
 DWORD nElementCount,
 DWORD nSendSize,
  LPOVERLAPPED lpOverlapped,
 DWORD dwFlags
);

其中,lpPacketArray是一個結構的數組,其中的每個元素既可以是一個文件句 柄或者內存緩沖區,該結構定義如下:

typedef struct _TRANSMIT_PACKETS_ELEMENT {
  DWORD dwElFlags;
  DWORD cLength;
  union {
    struct {
      LARGE_INTEGER   nFileOffset;
      HANDLE      hFile;
      };
      PVOID       pBuffer;
  };
} TRANSMIT_FILE_BUFFERS;

其中各字段是自描述型的(self explanatory)。

dwElFlags字段:指定當前元素是一個文件句柄還是內存緩沖區(分別通過常量 TF_ELEMENT_FILE 和TF_ELEMENT_MEMORY指定);

cLength字段:指定將從數據源發送 的字節數(如果是文件,這個字段值為0表示發送整個文件);

結構中的無名聯合體: 包含文件句柄的內存緩沖區(以及可能的偏移量)。

使用這兩個API的另一個好處,是可以 通過指定TF_REUSE_SOCKET和TF_DISCONNECT標志來重用套接字句柄。每當API完成數據的傳輸 工作後,就會在傳輸層級別斷開連接,這樣這個套接字就又可以重新提供給AcceptEx()使用 。采用這種優化的方法編程,將減輕那個專門做接受操作的線程創建套接字的壓力(前文述及 )。

這兩個API也都有一個共同的弱點:Windows NT Workstation 或 Windows 2000 專業版中,函數每次只能處理兩個調用請求,只有在Windows NT、Windows 2000服務器版、 Windows 2000高級服務器版或 Windows 2000 Data Center中才獲得完全支持。

放在 一起看看

以上各節中,我們討論了開發高性能的、大響應規模的應用程序所需的函數 、方法和可能遇到的資源瓶頸問題。這些對你意味著什麼呢?其實,這取決於你如何構造你 的服務器和客戶端。當你能夠在服務器和客戶端設計上進行更好地控制時,那麼你越能夠避 開瓶頸問題。

來看一個示范的環境。我們要設計一個服務器來響應客戶端的連接、發 送請求、接收數據以及斷開連接。那麼,服務器將需要創建一個監聽套接字,把它與某個完 成端口進行關聯,為每顆CPU創建一個工作線程。再創建一個線程專門用來發出AcceptEx()。 我們知道客戶端會在發出連接請求後立刻傳送數據,所以如果我們准備好接收緩沖區會使事 情變得更為容易。當然,不要忘記不時地輪詢AcceptEx()調用中使用的套接字(使用 SO_CONNECT_TIME選項參數)來確保沒有惡意超時的連接。

該設計中有一個重要的問題 要考慮,我們應該允許多少個AcceptEx()進行守候。這是因為,每發出一個AcceptEx()時我 們都同時需要為它提供一個接收緩沖區,那麼內存中將會出現很多被鎖定的頁面(前文說過了 ,每個重疊操作都會消耗一小部分未分頁內存池,同時還會鎖定所有涉及的緩沖區)。這個問 題很難回答,沒有一個確切的答案。最好的方法是把這個值做成可以調整的,通過反復做性 能測試,你就可以得出在典型應用環境中最佳的值。

好了,當你測算清楚後,下面就 是發送數據的問題了,考慮的重點是你希望服務器同時處理多少個並發的連接。通常情況下 ,服務器應該限制並發連接的數量以及等候處理的發送調用。因為並發連接數量越多,所消 耗的未分頁內存池也越多;等候處理的發送調用越多,被鎖定的內存頁面也越多(小心別超過 了極限)。這同樣也需要反復測試才知道答案。

對於上述環境,通常不需要關閉單個 套接字的緩沖區,因為只在AcceptEx()中有一次接收數據的操作,而要保證給每個到來的連 接提供接收緩沖區並不是太難的事情。但是,如果客戶機與服務器交互的方式變一變,客戶 機在發送了一次數據之後,還需要發送更多的數據,在這種情況下關閉接收緩沖就不太妙了 ,除非你想辦法保證在每個連接上都發出了重疊接收調用來接收更多的數據。

結論

開發大響應規模的Winsock服務器並不是很可怕,其實也就是設置一個監聽套接字、 接受連接請求和進行重疊收發調用。通過設置合理的進行守候的重疊調用的數量,防止出現 未分頁內存池被耗盡,這才是最主要的挑戰。按照我們前面討論的一些原則,你就可以開發 出大響應規模的服務器應用程序。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved