WinSocket I/O Methods

1.首先你需要知道什麼是"Socket Modes",什麼是"Socket I/O Models"?
Socket Modes : Determines how winsock functions behave when called with a socket.
Socket I/O Models : Describes how an application manages and processes I/O on a socket.

 

2.那麼Winsock提供了哪幾種"Socket Modes"給程序員用呢?
    a.Blocking Mode(阻塞式的)
    b.Noblocking Mode(非阻塞式的)

 

3.詳細解釋一下阻塞和非阻塞.
我們那最常用的send和recv兩個函數來說吧...
比如你調用send函數發送一定的Byte,在系統內部send做的工作其實只是把數據傳輸(Copy)到TCP/IP協議棧的輸出緩衝區,它執行成功並不代表數據已經成功的發送出去了,如果TCP/IP協議棧沒有足夠的可用緩衝區來保存你Copy過來的數據的話...這時候就體現出阻塞和非阻塞的不同之處了:對於阻塞模式的socket send函數將不返回直到系統緩衝區有足夠的空間把你要發送的數據Copy過去以後才返回,而對於非阻塞的socket來說send會立即返回WSAEWOULDDBLOCK告訴調用者說:"發送操作被阻塞了!!!你想辦法處理吧..."
對於recv函數,同樣道理,該函數的內部工作機制其實是在等待TCP/IP協議棧的接收緩衝區通知它說:嗨,你的數據來了.對於阻塞模式的socket來說如果TCP/IP協議棧的接收緩衝區沒有通知一個結果給它它就一直不返回:耗費着系統資源....對於非阻塞模式的socket該函數會馬上返回,然後告訴你:WSAEWOULDDBLOCK.

4.對於阻塞模式(Blocking Mode)我採取開線程的方式不就搞定了嗎,而且還不用我去判斷基本的收/發什麼時候完成,多省心?
    沒錯,這位大蝦說的開線程的方式具體的做法如下:我們將應用劃分成一個讀線程(Reading Thread)一個工作線程(Working Thread),兩個線程靠同步對象(Synchronization Object,可以是Event,Mutex...)來共享一個數據緩衝區,讀線程(Reading Thread)負責從網
絡讀取數據並將其放到共享緩衝區中,在適當的時機(比如說讀線程將工作線程需要的數據接收下來之後)觸發一個事件,通知/喚醒工作線程(Working Thread),下面是上述文字的簡單代碼實現:

CRITICAL_SECTION g_data;
HANDLE                    g_hEvent;
TCHAR                      g_szBuffer[MAX_BUFFER_SIZE];
int                              g_nBytes;

void ReadThread(void)
{
     int nTotle = nRead = nLeft = nBytes = 0;

     while(!done)
    {
       nTotal = 0;
       nLeft = NUM_BYTES_REQUIRED; //要讀多少的Bytes

       while( nTotal != NUM_BYTE_REQUIRED)
      {
         // 因爲涉及線程和全局變量所以說需要用到關鍵代碼段(Critical Section)
         EnterCriticalSection(&g_data);

         nRead = recv(sock,&(g_szBuffer[MAX_BUFFER_SIZE] - nBytes),nLeft);

         if( -1 == nread )
        {
            printf("socket %d error./n",WSAGetLasError());
            ExitThread();
         }

          nTotal += nRead;
          nLeft -= nRead;

          nBytes += nRead;

          LeaveCriticalSelection(&g_data);
     }

    // 從網絡接收完指定的(NUM_BYTES_REQUIRED)Bytes之後通知WorkThread啓動
     SetEvent(g_hEvent);
   }
}


void WorkThread(void)
{
    WaitForSingleObject(g_hEvent);

    EnterCriticalSection(&g_data);

    // 主要的處理工作...
    DoSomeWorkOnData(g_szBuffer);

   g_nBytes -= NUM_BYTE_REQUIRED;

   LeaveCriticalSelection(&g_data);
}

    這種方式的一個缺點是:應用程序很難同時通過多個建立好連接的socket通信.當然話說回來我們可以爲每一個連接好的socket都開一個Reading Thread and WorkThread,但是呢這會帶來很大的開銷,而且擴展性很差,試想:如果用這樣的模型建立一個服務器端來一個連接開至少兩個線程...天..要是業務負擔很大/連接的socket很多的話怎麼辦??所以說別多想了...
這樣的方式是很不適合做服務器端的.(注意:我說的服務器端是指C/S中的S端邏輯).

從編寫代碼的角度解讀一下IO中最複雜的IOCP模型

下面的一段代碼是Microsoft Win32 SDK裏面的例子,這是一個Echo服務器程序:

//
// 這就不說了,爲了使用Winsock2必須包含的頭文件...
//
#include <winsock2.h>
#include <windows.h>
#include <stdio.h>

#define PORT 5150 // 這個服務器選擇5150端口
#define DATA_BUFSIZE 8192 // 把Buffer定義爲8K是一個比較被Microsoft推薦的大小

//
// 這個結構體很重要,請一定看清楚,到後面用到的時候別糊塗了
// 特別注意:把Overlapped放最開頭是有原因的,你應該明確的是
// 如果有:
// PER_IO_OPERATION_DATA PerIoData;
// &PerIoData->Overlapped 和 &PerIoData是一樣的/等價的/
// 指向同一塊內存區域的.
//
typedef struct
{
    OVERLAPPED Overlapped;
    WSABUF DataBuf;
    CHAR Buffer[DATA_BUFSIZE];
    DWORD BytesSEND; // 下面兩個參數是用來記錄每個connected socket上面的收發的Bytes的
    DWORD BytesRECV;
} PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;

// 別把這個結構體和上面的結構體混淆了,名字很像的...其實它在這個程序中就相當於是那個和Client端連接的socket
typedef struct
{
SOCKET Socket;
} PER_HANDLE_DATA, * LPPER_HANDLE_DATA;


DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID);

void main(void)
{
SOCKADDR_IN InternetAddr;
//
// 一個listen和一個Accept socket是服務器程序的老套路了...
//
SOCKET Listen;
SOCKET Accept;

//
// IO完成端口映射到程序就是一個HANDLE
//
HANDLE CompletionPort;
SYSTEM_INFO SystemInfo;

//
// 看清楚,這是指針...不怕你笑話我們處還真有分不清和指針相關的一切概念的人,比如下面的代碼:
// 我寫了一個DLL,裏面有一個這樣的函數:
// BOOL GetPlayingPosition(DWORD * dwPos);
// 結果大蝦調用的時候是這樣的....
// DWORD * p;
// GetPlayingPostion(p);
// 它是調用爽了...我呢...:-(
// 當然了,也不好定義說它就是分不清指針,也許人家就不會寫程序呢..對吧..
//
LPPER_HANDLE_DATA PerHandleData;
LPPER_IO_OPERATION_DATA PerIoData;

int i;
DWORD RecvBytes;
DWORD Flags;
DWORD ThreadID;
WSADATA wsaData;
DWORD Ret;

// 就像CoInitialize()一樣的把Winsock 2.2服務啓動起來...
if ((Ret = WSAStartup(0x0202, &wsaData)) != 0)
{
printf("WSAStartup failed with error %d/n", Ret);
return;
}

// CreateIoCompletionPort有兩個不同的用途:1.創建完成端口;2.把已經創建好的完成端口和一個Socket關聯/綁定起來.
// 下面這句話是它的第一個用途:創建完成端口
if ((CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)) == NULL)
{
printf( "CreateIoCompletionPort failed with error: %d/n", GetLastError());
return;
}

//
// 下面這一些代碼說得是:根據該服務器跑的機器上有幾個CPU建立2*CPU數量的Thread.
// 這涉及到完成端口工作線程應該創建多少個的問題,在早期(MSDN 2000)Microsoft說
// 是WorkThreadNums = 2 * CPU + 2,現在的MSDN 2003說是WorkThreadNums = 2 * CPU;
//
GetSystemInfo(&SystemInfo);

for(i = 0; i < SystemInfo.dwNumberOfProcessors * 2; i++)
{
HANDLE ThreadHandle;

// 創建一個線程並把上面創建的完成端口傳給線程
if ((ThreadHandle = CreateThread(NULL, 0, ServerWorkerThread, CompletionPort,
0, &ThreadID)) == NULL)
{
printf("CreateThread() failed with error %d/n", GetLastError());
return;
}

// 這個風格我喜歡,創建完Thread就把人家的Handle給release,省得要是線程異常退出搞得資源泄漏之類的麻煩事情.
CloseHandle(ThreadHandle);
}

// 開始創建監聽Socket...
if ((Listen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0,WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET)
{
printf("WSASocket() failed with error %d/n", WSAGetLastError());
return;
}

InternetAddr.sin_family = AF_INET;
InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
InternetAddr.sin_port = htons(PORT);

// 把監聽socket和綁定TCP/IP協議棧做一個綁定...
if (bind(Listen, (PSOCKADDR) &InternetAddr, sizeof(InternetAddr)) == SOCKET_ERROR)
{
printf("bind() failed with error %d/n", WSAGetLastError());
return;
}

// 設置一次能接受5個連接
if (listen(Listen, 5) == SOCKET_ERROR)
{
printf("listen() failed with error %d/n", WSAGetLastError());
return;
}

//
// 下面是服務器程序標準的循環了...
//
while(TRUE)
{
if ((Accept = WSAAccept(Listen, NULL, NULL, NULL, 0)) == SOCKET_ERROR)
{
printf("WSAAccept() failed with error %d/n", WSAGetLastError());
return;
}

//
// 接收到一個連接就new一個新的socket並承攬下連接進來的socket的接下來的接待/處理工作
// 爲什麼不用new呢?我想是因爲用Windows提供的內存管理函數,這樣的話在業務量大的情況下
// 出現內存匱乏的情況下不至於直接就異常了,怎麼着也就是該次的內存分配失敗...
//
if ((PerHandleData = (LPPER_HANDLE_DATA) GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA))) == NULL)
{
printf("GlobalAlloc() failed with error %d/n", GetLastError());
return;
}

// Associate the accepted socket with the original completion port.

printf("Socket number %d connected/n", Accept);
PerHandleData->Socket = Accept;

//
// 把accept socket和IO完成端口關聯/綁定起來,並把accept socket傳給IOCP機制這樣的話
// 當調用GetQueuedCompletionStatus的時候就可以得到該值.
//
if (CreateIoCompletionPort((HANDLE) Accept, CompletionPort, (DWORD) PerHandleData,
0) == NULL)
{
printf("CreateIoCompletionPort failed with error %d/n", GetLastError());
return;
}

// 下面這個結構體也是會被傳到工作線程中去的,怎麼傳呢?注意看下面紅色部分
if ((PerIoData = (LPPER_IO_OPERATION_DATA) GlobalAlloc(GPTR,sizeof(PER_IO_OPERATION_DATA))) == NULL)
{
printf("GlobalAlloc() failed with error %d/n", GetLastError());
return;
}

ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));
PerIoData->BytesSEND = 0;
PerIoData->BytesRECV = 0;
PerIoData->DataBuf.len = DATA_BUFSIZE;
PerIoData->DataBuf.buf = PerIoData->Buffer;

Flags = 0;

//
// 在accept socket上面發起IO請求:WSARecv()以此激發IOCP工作機制..
// 注意WSARecv的第五個參數,也就是說我這時候告訴WSARecv的不僅僅是
// PerIoData->Overlapped的地址同時也是PerIoData的地址...
//
if (WSARecv(Accept, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,
&(PerIoData->Overlapped), NULL) == SOCKET_ERROR)
{
if (WSAGetLastError() != ERROR_IO_PENDING)
{
printf("WSARecv() failed with error %d/n", WSAGetLastError());
return;
}
}
}
}

//
// 工作線程
//
DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID)
{
HANDLE CompletionPort = (HANDLE) CompletionPortID;
DWORD BytesTransferred;
LPOVERLAPPED Overlapped;
LPPER_HANDLE_DATA PerHandleData;
LPPER_IO_OPERATION_DATA PerIoData;
DWORD SendBytes, RecvBytes;
DWORD Flags;

while(TRUE)
{

//
// 這個函數解釋清楚了整個機制也就明白了...
// 首先我們看CompletionPort應該沒有問題,是從創建線程的主線程傳過來的.
// BytesTransferred指明瞭這次IO傳輸了多少Bytes...
// PerHandleData是CreateIoCompletionPort的時候穿過來的,MSDN裏面叫CompletionKey.
// 請注意PerIoData這個參數,本來按照MSDN的解釋如下:
// "Pointer to a variable that receives the address of the OVERLAPPED structure that was
// specified when the completed I/O operation was started."
// 其實在這這個lpOverlapped傳過來的就是上面WSARecv傳入的overlapped的地址...
// 這下明白了吧:主線程藉着overlapped的名聲傳了一個結構體指針過來..:-)
//
if (GetQueuedCompletionStatus(CompletionPort, &BytesTransferred,
(LPDWORD)&PerHandleData, (LPOVERLAPPED *) &PerIoData, INFINITE) == 0)
{
printf("GetQueuedCompletionStatus failed with error %d/n", GetLastError());
return 0;
}


//
// First check to see if an error has occured on the socket and if so
// then close the socket and cleanup the SOCKET_INFORMATION structure
// associated with the socket.
//

if (BytesTransferred == 0)
{
printf("Closing socket %d/n", PerHandleData->Socket);

if (closesocket(PerHandleData->Socket) == SOCKET_ERROR)
{
printf("closesocket() failed with error %d/n", WSAGetLastError());
return 0;
}

GlobalFree(PerHandleData);
GlobalFree(PerIoData);
continue;
}

//
// Check to see if the BytesRECV field equals zero. If this is so, then
// this means a WSARecv call just completed so update the BytesRECV field
// with the BytesTransferred value from the completed WSARecv() call.
//

if (PerIoData->BytesRECV == 0)
{
PerIoData->BytesRECV = BytesTransferred;
PerIoData->BytesSEND = 0;
}
else
{
PerIoData->BytesSEND += BytesTransferred;
}

if (PerIoData->BytesRECV > PerIoData->BytesSEND)
{

//
// Post another WSASend() request.
// Since WSASend() is not gauranteed to send all of the bytes requested,
// continue posting WSASend() calls until all received bytes are sent.
//
ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));

PerIoData->DataBuf.buf = PerIoData->Buffer + PerIoData->BytesSEND;
PerIoData->DataBuf.len = PerIoData->BytesRECV - PerIoData->BytesSEND;

if (WSASend(PerHandleData->Socket, &(PerIoData->DataBuf), 1, &SendBytes, 0,
&(PerIoData->Overlapped), NULL) == SOCKET_ERROR)
{
if (WSAGetLastError() != ERROR_IO_PENDING)
{
printf("WSASend() failed with error %d/n", WSAGetLastError());
return 0;
}
}
}
else
{
PerIoData->BytesRECV = 0;

// Now that there are no more bytes to send post another WSARecv() request.
Flags = 0;
ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));

PerIoData->DataBuf.len = DATA_BUFSIZE;
PerIoData->DataBuf.buf = PerIoData->Buffer;

if (WSARecv(PerHandleData->Socket, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,
&(PerIoData->Overlapped), NULL) == SOCKET_ERROR)
{
if (WSAGetLastError() != ERROR_IO_PENDING)
{
printf("WSARecv() failed with error %d/n", WSAGetLastError());
return 0;
}
}
}
}
}

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