重疊I/O之完成例程
阿新 • • 發佈:2019-01-31
這個模型中有兩個函式可以交換著用,那就是WSAWaitForMultipleEvents()和SleepEx()函式,前者需要一個事件驅動,後者則不需要。是不是聽起來後者比較厲害,當然不是,簡單肯定是拿某種效能換來的,那就是當多client同時發出請求的時候,SleepEx如果等候時間設定成比較大的話,會造成client連線不上的現象。具體可以執行一下示例程式碼體會一下。
示例程式碼1(WSAWaitForMultipleEvents()版本)
示例程式碼2(SleepEx()版本)
使用該模型的步驟如下:
一、開啟伺服器(和事件通知那裡一樣)
二、建立ThreadAccept執行緒
這裡要先建立一個事件物件,然後把該事件物件作為引數傳入ThreadBind執行緒中。之後就不斷的等待client的請求,一有新的請求立即用WSASetEvent函式將該事件物件狀態設定為有訊號。
虛擬碼如下:
create a event object; //WSACreateEvent()
...
call ThreadAccept and use this event object as param;
...
while(1)
{
accept new client request;
...
set the event has single; //WSASetEvent()
}
如圖:
三、建立ThreadBind執行緒
這個主要用來為new client繫結一個完成例程,然後再投遞一個WSARecv。
虛擬碼如下:
while(1)
{
while(1)
{
Wait for accept() to signal an event and also process CompletionRoutine ;//WSAWaitForMultipleEvents()
...
reset the event object;//WSAResetEvent()
...
alloc a global mem for save client information;//GlobalAlloc()
...
post a WSARecv;
}
}
如圖:
四、建立完成例程函式(回撥函式)
其實這個就相當於是寫一個自定義的回撥函式給系統呼叫。
該函式的引數一定,不能更改。名字隨便起。
如下:
void CALLBACK CompeletRoutine(DWORD dwError, DWORD dwBytesTransferred,
LPWSAOVERLAPPED Overlapped, DWORD dwFlags)
{
......
}
其主要功能如下所描述
虛擬碼:
get the client information;
...
error handle;
...
data handle;
...
post a WSARecv
如圖:
SleepEx版本的基本差不多,就是把事件去掉,改為用一個變數判斷有無new client以及用SleepEx等待完成例程的操作。
如圖:
示例程式碼1
#include <WinSock2.h>
#include <process.h>
#include <stdio.h>
#pragma comment(lib,"ws2_32.lib")
#define PORT 18000
#define MAXBUF 128
//自定義一個存放socket資訊的結構體,用於完成例程中對OVERLAPPED的轉換
typedef struct _SOCKET_INFORMATION {
OVERLAPPED Overlapped; //這個欄位一定要放在第一個,否則轉換的時候,資料的賦值會出錯
SOCKET Socket; //後面的欄位順序可打亂並且不限制欄位數,也就是說你還可以多定義幾個欄位
CHAR Buffer[MAXBUF];
WSABUF wsaBuf;
} SOCKET_INFORMATION, *LPSOCKET_INFORMATION;
SOCKET g_sClient; //不斷新加進來的client
//開啟伺服器
BOOL OpenServer(SOCKET* sServer)
{
BOOL bRet = FALSE;
WSADATA wsaData = { 0 };
SOCKADDR_IN addrServer = { 0 };
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(PORT);
addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
do
{
if (!WSAStartup(MAKEWORD(2, 2), &wsaData))
{
if (LOBYTE(wsaData.wVersion) == 2 || HIBYTE(wsaData.wVersion) == 2)
{
//在套接字上使用重疊I/O模型,必須使用WSA_FLAG_OVERLAPPED標誌建立套接字
//g_sServer = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);
*sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (*sServer != INVALID_SOCKET)
{
if (SOCKET_ERROR != bind(*sServer, (SOCKADDR*)&addrServer, sizeof(addrServer)))
{
if (SOCKET_ERROR != listen(*sServer, SOMAXCONN))
{
bRet = TRUE;
break;
}
closesocket(*sServer);
}
closesocket(*sServer);
}
}
}
} while (FALSE);
return bRet;
}
//完成例程
void CALLBACK CompeletRoutine(DWORD dwError, DWORD dwBytesTransferred, LPWSAOVERLAPPED Overlapped, DWORD dwFlags)
{
DWORD dwSendBytes, dwRecvBytes;
DWORD dwFlag;
//強制轉換為我們自定義的結構,這裡就解釋了為什麼第一個欄位要是OVERLAPPED
//因為轉換後首地址肯定會相同,讀取的資料一定會是Overlapped的資料
//所以要先把Overlapped的資料儲存下來,接下來記憶體中的資料再由系統分配到各個欄位中
LPSOCKET_INFORMATION pSi = (LPSOCKET_INFORMATION)Overlapped;
if (dwError != 0) //錯誤顯示
printf("I/O operation failed with error %d\n", dwError);
if (dwBytesTransferred == 0)
printf("Closing socket %d\n\n", pSi->Socket);
if (dwError != 0 || dwBytesTransferred == 0) //錯誤處理
{
closesocket(pSi->Socket);
GlobalFree(pSi);
return;
}
//如果已經發送完成了,接著投遞下一個WSARecv
printf("Recv%d:%s\n", pSi->Socket, pSi->wsaBuf.buf);
dwFlag = 0;
ZeroMemory(&(pSi->Overlapped), sizeof(WSAOVERLAPPED));
pSi->wsaBuf.len = MAXBUF;
pSi->wsaBuf.buf = pSi->Buffer;
if (WSARecv(pSi->Socket, &(pSi->wsaBuf), 1, &dwRecvBytes, &dwFlag, &(pSi->Overlapped), CompeletRoutine) == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("WSARecv() failed with error %d\n", WSAGetLastError());
return;
}
}
}
//把client和完成例程繫結起來
unsigned int __stdcall ThreadBind(void* lparam)
{
DWORD dwFlags;
LPSOCKET_INFORMATION pSi;
DWORD dwIndex;
DWORD dwRecvBytes;
WSAEVENT eventArry[1];
eventArry[0] = (WSAEVENT)lparam;
while (1)
{
//等待一個完成例程返回
while (TRUE)
{
dwIndex = WSAWaitForMultipleEvents(1, eventArry, FALSE, WSA_INFINITE, TRUE);
if (dwIndex == WSA_WAIT_FAILED)
{
printf("WSAWaitForMultipleEvents() failed with error %d\n", WSAGetLastError());
return FALSE;
}
if (dwIndex != WAIT_IO_COMPLETION)
break;
}
//重設事件
WSAResetEvent(eventArry[0]);
//為SOCKET_INFORMATION分配一個全域性記憶體空間,相當於全域性變量了
//這裡為什麼要分配全域性的呢?因為我們要在完成例程中引用socket的資料
if ((pSi = (LPSOCKET_INFORMATION)GlobalAlloc(GPTR, sizeof(SOCKET_INFORMATION))) == NULL)
{
printf("GlobalAlloc() failed with error %d\n", GetLastError());
return 1;
}
//填充各個欄位
pSi->Socket = g_sClient;
ZeroMemory(&(pSi->Overlapped), sizeof(WSAOVERLAPPED));
pSi->wsaBuf.len = MAXBUF;
pSi->wsaBuf.buf = pSi->Buffer;
dwFlags = 0;
//投遞一個WSARecv
if (WSARecv(pSi->Socket, &(pSi->wsaBuf), 1, &dwRecvBytes, &dwFlags,
&(pSi->Overlapped), CompeletRoutine) == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("WSARecv() failed with error %d\n", WSAGetLastError());
return 1;
}
}
printf("Socket %d got connected...\n", g_sClient);
}
return 0;
}
//接受client請求執行緒
unsigned int __stdcall ThreadAccept(void* lparam)
{
SOCKET sServer = *(SOCKET*)lparam;
WSAEVENT event = WSACreateEvent();
_beginthreadex(NULL, 0, ThreadBind, event, 0, NULL);
while (TRUE)
{
g_sClient = accept(sServer, NULL, NULL);
if (g_sClient != INVALID_SOCKET)
WSASetEvent(event);
}
return 0;
}
int main(int argc, char **argv)
{
SOCKET sServer = INVALID_SOCKET;
if (OpenServer(&sServer))
_beginthreadex(NULL, 0, ThreadAccept, &sServer, 0, NULL);
Sleep(10000000);
return 0;
}
示例程式碼2
因為只是ThreadAccept和ThreadBind有變動,所以只貼出這兩段程式碼
//接受client請求執行緒
unsigned int __stdcall ThreadAccept(void* lparam)
{
SOCKET sServer = *(SOCKET*)lparam;
while (TRUE)
{
g_sClient = accept(sServer, NULL, NULL);
if (g_sClient != INVALID_SOCKET)
g_bNewClient = TRUE;
}
return 0;
}
//把client和完成例程繫結起來
unsigned int __stdcall ThreadBind(void* lparam)
{
DWORD dwFlags;
LPSOCKET_INFORMATION pSi;
DWORD dwIndex;
DWORD dwRecvBytes;
while (1)
{
//等待一個完成例程返回
while (TRUE)
{
dwIndex = SleepEx(10, TRUE); //這裡等待10ms,如果等待時間越大,越容易出現記憶體讀取錯誤
if (dwIndex == WSA_WAIT_FAILED) //如果用事件通知,則不用考慮這個,不會衝突的。
{
printf("SleepEx() failed with error %d\n", WSAGetLastError());
return 1;
}
if (dwIndex != WAIT_IO_COMPLETION)
break; //有新的client加入,跳出迴圈,繼續為新的client繫結例程
}
if (g_bNewClient)//如果有new client 才為它繫結一個完成例程
{
//為SOCKET_INFORMATION分配一個全域性記憶體空間,相當於全域性變量了
//這裡為什麼要分配全域性的呢?因為我們要在完成例程中引用socket的資料
if ((pSi = (LPSOCKET_INFORMATION)GlobalAlloc(GPTR, sizeof(SOCKET_INFORMATION))) == NULL)
{
printf("GlobalAlloc() failed with error %d\n", GetLastError());
return 1;
}
//填充各個欄位
pSi->Socket = g_sClient;
ZeroMemory(&(pSi->Overlapped), sizeof(WSAOVERLAPPED));
pSi->wsaBuf.len = MAXBUF;
pSi->wsaBuf.buf = pSi->Buffer;
dwFlags = 0;
//投遞一個WSARecv
if (WSARecv(pSi->Socket, &(pSi->wsaBuf), 1, &dwRecvBytes, &dwFlags,
&(pSi->Overlapped), CompeletRoutine) == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("WSARecv() failed with error %d\n", WSAGetLastError());
return 1;
}
}
printf("Socket %d got connected...\n", g_sClient);
g_bNewClient = FALSE;
}
}
return 0;
}
關於完成例程如何同時投遞WSARecv和WSASend下一篇講。