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

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

通常要開發網絡應用程序並不是一件輕鬆的事情,不過,實際上只要掌握幾個關鍵的原則也就可以了——

創建和連接一個套接字,嘗試進行連接,然後收發數據。真正難的是要寫出一個可以接納少則一個,多則

數千個連接的網絡應用程序。本文將討論如何通過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

宏取出其中指向擴展結構的指針(譯者注:以上兩小段一會是OVERLAPPEDPLUS結構,一會是OVERLAPPED結構

,本人也看不太懂,請高手賜教)。

OVERLAPPED 結構的定義如下:

typedef struct _OVERLAPPEDPLUS {
  OVERLAPPED    ol;
  SOCKET      s, sclient;
  int        OpCode;
  WSABUF      wbuf;
  DWORD       dwBytes, dwFlags;
  // other useful information
} OVERLAPPEDPLUS;

#define OP_READ   0
#define OP_WRITE  1
#define OP_ACCEPT  2


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


Figure 2 Worker Thread 

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 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()

直接從底層的提供者那裏取得函數的指針。

參見Figure 3 套接字架構:

application
 ||
/||/
 //
winsock 2.0 dll (ws2_32.dll)
 ||
/||/
 //
layered/Base Providers
RSVP | Proxy | Default Microsoft Providers (mswsock.dll/msafd.dll)
 ||
/||/
 //
Windows Sockets kernel-mode driver (afd.sys)
 ||
/||/
 //
Tramsport Protocols
TCP/IP | ATM | Other

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服務器並不是很可怕,其實也就是設置一個監聽套接字、接受連接請求和進行重

疊收發調用。通過設置合理的進行守候的重疊調用的數量,防止出現未分頁內存池被耗盡,這纔是最主要

的挑戰。按照我們前面討論的一些原則,你就可以開發出大響應規模的服務器應用程序。

發佈了71 篇原創文章 · 獲贊 0 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章