第 1 樓:細(xì)說WinSock IOCP
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.詳細(xì)解釋一下阻塞和非阻塞. 我們那最常用的send和recv兩個(gè)函數(shù)來說吧... 比如你調(diào)用send函數(shù)發(fā)送一定的Byte,在系統(tǒng)內(nèi)部send做的工作其實(shí)只是把數(shù)據(jù)傳輸(Copy)到TCP/IP協(xié)議棧的輸出緩沖區(qū),它執(zhí)行成功并不代表數(shù)據(jù)已經(jīng)成功的發(fā)送出去了,如果TCP/IP協(xié)議棧沒有足夠的可用緩沖區(qū)來保存你Copy過來的數(shù)據(jù)的話...這時(shí)候就體現(xiàn)出阻塞和非阻塞的不同之處了:對(duì)于阻塞模式的socket send函數(shù)將不返回直到系統(tǒng)緩沖區(qū)有足夠的空間把你要發(fā)送的數(shù)據(jù)Copy過去以后才返回,而對(duì)于阻塞的socket來說send會(huì)立即返回WSAEWOULDDBLOCK告訴調(diào)用者說:"發(fā)送操作被阻塞了!!!你想辦法處理吧..." 對(duì)于recv函數(shù),同樣道理,該函數(shù)的內(nèi)部工作機(jī)制其實(shí)是在等待TCP/IP協(xié)議棧的接收緩沖區(qū)通知它說:嗨,你的數(shù)據(jù)來了.對(duì)于阻塞模式的socket來說如果TCP/IP協(xié)議棧的接收緩沖區(qū)沒有通知一個(gè)結(jié)果給它它就一直不返回:耗費(fèi)著系統(tǒng)資源....對(duì)于阻塞模式的socket該函數(shù)會(huì)馬上返回,然后告訴你:WSAEWOULDDBLOCK.
3.對(duì)于阻塞模式(Blocking Mode)我采取開線程的方式不就搞定了嗎,而且還不用我去判斷基本的收/發(fā)什么時(shí)候完成,多省心? 沒錯(cuò),這位大蝦說的開線程的方式具體的做法如下:我們將應(yīng)用劃分成一個(gè)讀線程(Reading Thread)一個(gè)工作線程(Working Thread),兩個(gè)線程*同步對(duì)象(Synchronization Object,可以是Event,Mutex...)來共享一個(gè)數(shù)據(jù)緩沖區(qū),讀線程(Reading Thread)負(fù)責(zé)從網(wǎng) 絡(luò)讀取數(shù)據(jù)并將其放到共享緩沖區(qū)中,在適當(dāng)?shù)臅r(shí)機(jī)(比如說讀線程將工作線程需要的數(shù)據(jù)接收下來之后)觸發(fā)一個(gè)事件,通知/喚醒工作線程(Working Thread),下面是上述文字的簡(jiǎn)單代碼實(shí)現(xiàn): |
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)
{
// 因?yàn)樯婕熬€程和全局變量所以說需要用到關(guān)鍵代碼段(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);
}
// 從網(wǎng)絡(luò)接收完指定的(NUM_BYTES_REQUIRED)Bytes之后通知WorkThread啟動(dòng)
SetEvent(g_hEvent);
}
}
void WorkThread(void)
{
WaitForSingleObject(g_hEvent);
EnterCriticalSection(&g_data);
// 主要的處理工作...
DoSomeWorkOnData(g_szBuffer);
g_nBytes -= NUM_BYTE_REQUIRED;
LeaveCriticalSelection(&g_data);
}
這種方式的一個(gè)缺點(diǎn)是:應(yīng)用程序很難同時(shí)通過多個(gè)建立好連接的socket通信.當(dāng)然話說回來我們可以為每一個(gè)連接好的socket都開一個(gè)Reading Thread and WorkThread,但是呢這會(huì)帶來很大的開銷,而且擴(kuò)展性很差,試想:如果用這樣的模型建立一個(gè)服務(wù)器端來一個(gè)連接開至少兩個(gè)線程...天..要是業(yè)務(wù)負(fù)擔(dān)很大/連接的socket很多的話怎么辦??所以說別多想了...
這樣的方式是很不適合做服務(wù)器端的.(注意:我說的服務(wù)器端是指C/S中的S端邏輯).
從編寫代碼的角度解讀一下IO中最復(fù)雜的IOCP模型
下面的一段代碼是Microsoft Win32 SDK里面的例子,這是一個(gè)Echo服務(wù)器程序:
//
// 這就不說了,為了使用Winsock2必須包含的頭文件...
//
#include
#include
#include
#define PORT 5150 // 這個(gè)服務(wù)器選擇5150端口
#define DATA_BUFSIZE 8192 // 把Buffer定義為8K是一個(gè)比較被Microsoft推薦的大小
//
// 這個(gè)結(jié)構(gòu)體很重要,請(qǐng)一定看清楚,到后面用到的時(shí)候別糊涂了
// 特別注意:把Overlapped放最開頭是有原因的,你應(yīng)該明確的是
// 如果有:
// PER_IO_OPERATION_DATA PerIoData;
// &PerIoData->Overlapped 和 &PerIoData是一樣的/等價(jià)的/
// 指向同一塊內(nèi)存區(qū)域的.
//
typedef struct
{
OVERLAPPED Overlapped;
WSABUF DataBuf;
CHAR Buffer[DATA_BUFSIZE];
DWORD BytesSEND; // 下面兩個(gè)參數(shù)是用來記錄每個(gè)connected socket上面的收發(fā)的Bytes的
DWORD BytesRECV;
} PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;
// 別把這個(gè)結(jié)構(gòu)體和上面的結(jié)構(gòu)體混淆了,名字很像的...其實(shí)它在這個(gè)程序中就相當(dāng)于是那個(gè)和Client端連接的socket
typedef struct
{
SOCKET Socket;
} PER_HANDLE_DATA, * LPPER_HANDLE_DATA;
DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID);
void main(void)
{
SOCKADDR_IN InternetAddr;
//
// 一個(gè)listen和一個(gè)Accept socket是服務(wù)器程序的老套路了...
//
SOCKET Listen;
SOCKET Accept;
//
// IO完成端口映射到程序就是一個(gè)HANDLE
//
HANDLE CompletionPort;
SYSTEM_INFO SystemInfo;
//
// 看清楚,這是指針...不怕你笑話我們處還真有分不清和指針相關(guān)的一切概念的人,比如下面的代碼:
// 我寫了一個(gè)DLL,里面有一個(gè)這樣的函數(shù):
// BOOL GetPlayingPosition(DWORD * dwPos);
// 結(jié)果大蝦調(diào)用的時(shí)候是這樣的....
// DWORD * p;
// GetPlayingPostion(p);
// 它是調(diào)用爽了...我呢...:-(
// 當(dāng)然了,也不好定義說它就是分不清指針,也許人家就不會(huì)寫程序呢..對(duì)吧..
//
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服務(wù)啟動(dòng)起來...
if ((Ret = WSAStartup(0x0202, &wsaData)) != 0)
{
printf("WSAStartup failed with error %d\n", Ret);
return;
}
// CreateIoCompletionPort有兩個(gè)不同的用途:1.創(chuàng)建完成端口;2.把已經(jīng)創(chuàng)建好的完成端口和一個(gè)Socket關(guān)聯(lián)/綁定起來.
// 下面這句話是它的第一個(gè)用途:創(chuàng)建完成端口
if ((CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)) == NULL)
{
printf( "CreateIoCompletionPort failed with error: %d\n", GetLastError());
return;
}
//
// 下面這一些代碼說得是:根據(jù)該服務(wù)器跑的機(jī)器上有幾個(gè)CPU建立2*CPU數(shù)量的Thread.
// 這涉及到完成端口工作線程應(yīng)該創(chuàng)建多少個(gè)的問題,在早期(MSDN 2000)Microsoft說
// 是WorkThreadNums = 2 * CPU + 2,現(xiàn)在的MSDN 2003說是WorkThreadNums = 2 * CPU;
//
GetSystemInfo(&SystemInfo);
for(i = 0; i < SystemInfo.dwNumberOfProcessors * 2; i++)
{
HANDLE ThreadHandle;
// 創(chuàng)建一個(gè)線程并把上面創(chuàng)建的完成端口傳給線程
if ((ThreadHandle = CreateThread(NULL, 0, ServerWorkerThread, CompletionPort,
0, &ThreadID)) == NULL)
{
printf("CreateThread() failed with error %d\n", GetLastError());
return;
}
// 這個(gè)風(fēng)格我喜歡,創(chuàng)建完Thread就把人家的Handle給release,省得要是線程異常退出搞得資源泄漏之類的麻煩事情.
CloseHandle(ThreadHandle);
}
// 開始創(chuàng)建監(jiān)聽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);
// 把監(jiān)聽socket和綁定TCP/IP協(xié)議棧做一個(gè)綁定...
if (bind(Listen, (PSOCKADDR) &InternetAddr, sizeof(InternetAddr)) == SOCKET_ERROR)
{
printf("bind() failed with error %d\n", WSAGetLastError());
return;
}
// 設(shè)置一次能接受5個(gè)連接
if (listen(Listen, 5) == SOCKET_ERROR)
{
printf("listen() failed with error %d\n", WSAGetLastError());
return;
}
//
// 下面是服務(wù)器程序標(biāo)準(zhǔn)的循環(huán)了...
//
while(TRUE)
{
if ((Accept = WSAAccept(Listen, NULL, NULL, NULL, 0)) == SOCKET_ERROR)
{
printf("WSAAccept() failed with error %d\n", WSAGetLastError());
return;
}
//
// 接收到一個(gè)連接就new一個(gè)新的socket并承攬下連接進(jìn)來的socket的接下來的接待/處理工作
// 為什么不用new呢?我想是因?yàn)橛肳indows提供的內(nèi)存管理函數(shù),這樣的話在業(yè)務(wù)量大的情況下
// 出現(xiàn)內(nèi)存匱乏的情況下不至于直接就異常了,怎么著也就是該次的內(nèi)存分配失敗...
//
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;
//
// 把a(bǔ)ccept socket和IO完成端口關(guān)聯(lián)/綁定起來,并把a(bǔ)ccept socket傳給IOCP機(jī)制這樣的話
// 當(dāng)調(diào)用GetQueuedCompletionStatus的時(shí)候就可以得到該值.
//
if (CreateIoCompletionPort((HANDLE) Accept, CompletionPort, (DWORD) PerHandleData,
0) == NULL)
{
printf("CreateIoCompletionPort failed with error %d\n", GetLastError());
return;
}
// 下面這個(gè)結(jié)構(gòu)體也是會(huì)被傳到工作線程中去的,怎么傳呢?注意看下面紅色部分
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上面發(fā)起IO請(qǐng)求:WSARecv()以此激發(fā)IOCP工作機(jī)制..
// 注意WSARecv的第五個(gè)參數(shù),也就是說我這時(shí)候告訴WSARecv的不僅僅是
// PerIoData->Overlapped的地址同時(shí)也是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)
{
//
// 這個(gè)函數(shù)解釋清楚了整個(gè)機(jī)制也就明白了...
// 首先我們看CompletionPort應(yīng)該沒有問題,是從創(chuàng)建線程的主線程傳過來的.
// BytesTransferred指明了這次IO傳輸了多少Bytes...
// PerHandleData是CreateIoCompletionPort的時(shí)候穿過來的,MSDN里面叫CompletionKey.
// 請(qǐng)注意PerIoData這個(gè)參數(shù),本來按照MSDN的解釋如下:
// "Pointer to a variable that receives the address of the OVERLAPPED structure that was
// specified when the completed I/O operation was started."
// 其實(shí)在這這個(gè)lpOverlapped傳過來的就是上面WSARecv傳入的overlapped的地址...
// 這下明白了吧:主線程借著overlapped的名聲傳了一個(gè)結(jié)構(gòu)體指針過來..:-)
//
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;
}
}
}
}
}
posted on 2007-10-09 11:57
聶文龍 閱讀(1729)
評(píng)論(0) 編輯 收藏 引用 所屬分類:
net work