關於IOCP網上到處都是資料,說的也很詳細。我在這裏就不再多說了,這只是本人在學習IOCP時的筆記,和配合AcceptEx寫的一個極小的服務端程序。由於剛剛接觸ICOP加上本人剛畢業不到一年,所以裏面的理解或觀點可能有誤,還請大家多多批評!
VC6.0開發,旨在體現IOCP的架構,忽略細節,服務程序的功能只是接收客戶連接,接着接收到客戶數據,然後原封不動的返回給客戶!
下面這段話,如果不感興趣,可以跳過去,直接看代碼...
先說IOCP,其實思路很清晰:
1.聲明一個數據結構,用來存放客戶套接字和客戶信息
2.聲明一個包含OVERLAPPED字段的I/O結構
3.創建完成端口
4.創建服務線程
5.接收客戶端連接請求
6.關聯這個套接字到完成端口中
7.服務線程中不斷的等待I/O結果,在結果中提供服務和根據需要發起另一個異步操作。
按照這個思路很快的寫出了一個服務程序,但是遇到了下面的問題:
1.WSAGetLastError()返回10045,找了半天才發現發起重疊操作時候,WSARecv中flag參數沒有初始化,需要初始化賦值爲0。
2.在GetQueuedCompletionStatus中,沒有錯誤,但總是返回讀取的字數爲0。I/O重疊結構中也收不到任何字符。這時候我就在這裏用了一下recv()函數,在recv中卻可以收到來自客戶端發送的數據。難道每次都要自己recv()?肯定不是!如果那樣還用擴展的I/O結果何用。一定是哪裏指定了接收的數目,而自己不小心指定爲0了,所以沒有接收數據。找了半天果然如此。在發起重疊操作時候,擴展的I/O中WSABUF的賦值有問題。
我的錯誤:wsaBuf.len = (I/O結構).len;
改爲: wsaBuf.len = (I/O結構).len = DATABUF_SIZE;
修改之後終於可以接收和發送數據了。
爲什麼要用AcceptEx?
在學習IOCP時,看到一位大神寫的文章,他用客戶端開了3W個線程同時連接服務端和發送數據,我好奇就也開了3W個線程去同時連接服務端,結果很多都printf連接失敗的信息!再看看大神的文章,再搜一下AcceptEx。對比accept,覺得AcceptEx確實很強大。AcceptEx和accept主要的區別就在於接收套接字:
accept函數是等待客戶連接進來之後才創建套接字,雖然在我們看到的就是一個socket函數,但是在函數背後,系統應該會消耗不少資源,因爲它要打通一個和外界通訊的路。如果大量套接字併發接入,難免有的套接字不能及時創建和接收。
AcceptEx則是事先創建好套接字,坐等客戶端的連接就行了。
但是,AcceptEx相比accept確實複雜了很多。原來一句accept就可以解決的,現在卻要爲AcceptEx做很多服務,但是隻要理清思路,這個做起來也是很從容的。
1.創建一個監聽套接字
2.將監聽套接字關聯到完成端口中
3.對監聽套接字調用bind()、listen()
4.通過WSAIoctl獲取AcceptEx、GetAcceptExSockaddrs函數的指針
5.創建一個用於接收客戶連接的套接字
6.用獲取到的AcceptEx函數指針發起用於接收連接的異步操作
7.服務器接收到連接的套接字,設置一下它的屬性(有人說沒有必要)。用這個接收到的套接字去發起重疊的I/O操作。
8.多次重複5,6就是多次發起接收連接的異步操作的過程。
對於第4步,爲什麼要獲取AcceptEx的指針,而不是直接就調用AcceptEx這個函數呢?網上找到的資料是這麼說的:
Winsock2的其他供應商不一定會實現AcceptEx函數。同樣情況也包括的其他Microsoft的特定APIs如TransmitFile,GetAcceptExSockAddrs以及其他Microsoft將在以後版本的windows裏。
在運行WinNT和Win2000的系統上,這些APIs在Microsoft提供的DLL(mswsock.dll)裏實現,可以通過鏈接mswsock.lib或者通過WSAioctl的SIO_GET_EXTENSION_FUNCTION_POINTER操作動態調用這些擴展APIs.
未獲取函數指針就調用函數(如直接連接mswsock.lib並直接調用AcceptEx)的消耗是很大的,因爲AcceptEx實際上是存在於Winsock2結構體系之外的。每次應用程序常試在服務提供層上(mswsock之上)調用AcceptEx時,都要先通過WSAIoctl獲取該函數指針。如果要避免這個很影響性能的操作,應用程序最好是直接從服務提供層通過WSAIoctl先獲取這些APIs的指針。
這樣一來,大家就不覺得這個複雜的函數WSAloctl那麼讓人心煩了吧!至於調用失敗後所返回的錯誤代碼,百度百科中介紹的很詳細!
使用AcceptEx後:
在使用AcceptEx後,併發2000個套接字去連接客戶端,不再出現連接失敗的消息了。
但是,你肯定會說人家3W個,你這2000個不能說明問題。開始我也一直在嘗試同時併發3W個線程,可是發現公司機器最多時候也就1573個連接,家裏筆記本差不多2000個。這是怎麼會事呢?於是搜資料查到一個進程最多可以開啓的理論線程數是2048個線程,而且實際情況下通常小於這個值,這樣在一個進程裏面怎麼可能有3W個連接啊!忍不住好奇就下了http://blog.csdn.net/piggyxp/article/details/6922277大神的IOCP客戶端demo,發現並不是同時併發3W個,用任務管理器看併發最多時候線程數並沒有超過1K(無意冒犯大神,只是個人的愚見,我學習IOCP也是大部分都是從大神的文章中學習到的,所以先要感謝大神的奉獻,同時如果(不是如果,是肯定)我的理解有錯誤,希望大家不吝賜教,多多批評,鄙人一定感激萬分)。
爲了驗證IOCP是否有那麼強的能力,我的客戶端沒有做成連接到服務端一個套接字,再創建一個線程,傳遞套接字到線程的方式。而是,主線程直接創建2000個線程,在每個線程中去連接服務器(覺得這樣更能體現併發連接),多開幾個客戶端,每個客戶端的連接數爲最大線程數,服務端同時處理的連接數爲12562(開更多的線程連接數更多,有興趣的可以試一下)。下面是360的流量管理下面的截圖:
我註釋掉了接收數據後printf接收到的數據,因爲發現如果連接過多,一直printf服務器就掛掉了,不知道改成mfc會不會好點...
下面是服務器代碼:
[cpp] view plain copy
#include "stdafx.h"
#include <Afx.h>
#include <Windows.h>
#include <Winsock2.h>
#pragma comment(lib, "WS2_32.lib")
#include <mswsock.h> //微軟擴展的類庫
#define DATA_BUFSIZE 100
#define READ 0
#define WRITE 1
#define ACCEPT 2
DWORD g_count = 0;
//擴展的輸入輸出結構
typedef struct _io_operation_data
{
OVERLAPPED overlapped;
WSABUF databuf;
CHAR buffer[DATA_BUFSIZE];
BYTE type;
DWORD len;
SOCKET sock;
}IO_OPERATION_DATA, *LP_IO_OPERATION_DATA;
//完成鍵
typedef struct _completion_key
{
SOCKET sock;
char sIP[30]; //本機測試,IP都是127.0.0.1,沒啥意思,實際寫時候這個值填的是端口號
}COMPLETION_KEY, *LP_COMPLETION_KEY;
///////////////////////////////////////////////////
//完成端口句柄
HANDLE g_hComPort = NULL;
BOOL g_bRun = FALSE;
BOOL AcceptClient(SOCKET sListen); //發起接收連接操作
BOOL Recv(COMPLETION_KEY *pComKey, IO_OPERATION_DATA *pIO); //發起接收操作
BOOL Send(COMPLETION_KEY *pComKey, IO_OPERATION_DATA *pIO); //發起發送操作
//處理重疊結果
BOOL ProcessIO(IO_OPERATION_DATA *pIOdata, COMPLETION_KEY *pComKey);
//////////////////////////////////////////////////
//服務線程
DWORD WINAPI ServerWorkerThread( LPVOID pParam );
//////////////////////////////////////////////////
LPFN_ACCEPTEX lpfnAcceptEx = NULL; //AcceptEx函數指針
LPFN_GETACCEPTEXSOCKADDRS lpfnGetAcceptExSockaddrs; //加載GetAcceptExSockaddrs函數指針
///////////////////////////////////////////////////
//監聽套接字,其實也不一定要是全局的。用於接收到連接後繼續發起等待連接操作。
SOCKET g_sListen;
int main(int argc, char* argv[])
{
g_bRun = TRUE;
//創建完成端口
g_hComPort = CreateIoCompletionPort( INVALID_HANDLE_VALUE, NULL, 0, 0 );
if( g_hComPort == NULL )
{
printf("Create completionport error! %d\n", WSAGetLastError() );
return 0;
}
//創建服務線程
SYSTEM_INFO sysInfor;
GetSystemInfo( &sysInfor );
int i=0;
for(i = 0; i < sysInfor.dwNumberOfProcessors * 2; i++)
// if(true)
{
HANDLE hThread;
DWORD dwThreadID;
hThread = CreateThread( NULL, 0, ServerWorkerThread, g_hComPort, 0, &dwThreadID );
CloseHandle( hThread );
}
//加載套接字庫
WSADATA wsData;
if( 0 != WSAStartup( 0x0202, &wsData ) )
{
printf("加載套接字庫失敗! %d\n", WSAGetLastError() );
g_bRun = FALSE;
return 0;
}
////////////////////////////////////////////////////////////////////
//等待客戶端連接
//先創建一個套接字用於監聽
SOCKET sListen = WSASocket( AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED );
g_sListen = sListen;
//將監聽套接字與完成端口綁定
LP_COMPLETION_KEY pComKey; //完成鍵
pComKey = (LP_COMPLETION_KEY) GlobalAlloc ( GPTR, sizeof(COMPLETION_KEY) );
pComKey->sock = sListen;
CreateIoCompletionPort( (HANDLE)sListen, g_hComPort, (DWORD)pComKey, 0 );
//監聽套接字綁定監聽
SOCKADDR_IN serAdd;
serAdd.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
serAdd.sin_family = AF_INET;
serAdd.sin_port = htons( 6000 );
bind( sListen, (SOCKADDR*)&serAdd, sizeof(SOCKADDR) );
listen( sListen, 5 );
if( sListen == SOCKET_ERROR )
{
goto STOP_SERVER;
}
/////////////////////////////////////////////////////////////////////
//使用WSAIoctl獲取AcceptEx函數指針
if( true )
{
DWORD dwbytes = 0;
//Accept function GUID
GUID guidAcceptEx = WSAID_ACCEPTEX;
if( 0 != WSAIoctl( sListen, SIO_GET_EXTENSION_FUNCTION_POINTER,
&guidAcceptEx, sizeof(guidAcceptEx),
&lpfnAcceptEx, sizeof(lpfnAcceptEx),
&dwbytes, NULL, NULL) )
{
//百度百科,有關該函數的所有返回值都有!
}
// 獲取GetAcceptExSockAddrs函數指針,也是同理
GUID guidGetAcceptExSockaddrs = WSAID_GETACCEPTEXSOCKADDRS;
if( 0 != WSAIoctl( sListen, SIO_GET_EXTENSION_FUNCTION_POINTER,
&guidGetAcceptExSockaddrs,
sizeof(guidGetAcceptExSockaddrs),
&lpfnGetAcceptExSockaddrs,
sizeof(lpfnGetAcceptExSockaddrs),
&dwbytes, NULL, NULL) )
{
}
}
//發起接收的異步操作
for(i=0; i<2000; i++ )
{
AcceptClient(sListen);
}
//不讓主線程退出
while( g_bRun )
{
Sleep(1000);
}
STOP_SERVER:
closesocket( sListen );
g_bRun = FALSE;
WSACleanup();
return 0;
}
/////////////////////////////////////////////////////////////////////////
//服務線程
DWORD WINAPI ServerWorkerThread( LPVOID pParam )
{
HANDLE completionPort = (HANDLE)pParam;
DWORD dwIoSize;
COMPLETION_KEY *pComKey; //完成鍵
LP_IO_OPERATION_DATA lpIOoperData; //I/O數據
//用於發起接收重疊操作
BOOL bRet;
while( g_bRun )
{
bRet = FALSE;
dwIoSize = -1;
pComKey = NULL;
lpIOoperData = NULL;
bRet = GetQueuedCompletionStatus( g_hComPort, &dwIoSize, (LPDWORD)&pComKey, (LPOVERLAPPED*)&lpIOoperData,INFINITE );
if( !bRet )
{
DWORD dwIOError = GetLastError();
if( WAIT_TIMEOUT == dwIOError )
{
continue;
}
else if( NULL != lpIOoperData )
{
CancelIo( (HANDLE)pComKey->sock ); //取消等待執行的異步操作
closesocket(pComKey->sock);
GlobalFree( pComKey );
}
else
{
g_bRun = FALSE;
break;
}
}
else
{
if( 0 == dwIoSize && (READ==lpIOoperData->type || WRITE==lpIOoperData->type) )
{
printf("客戶斷開了連接!\n");
CancelIo( (HANDLE)pComKey->sock ); //取消等待執行的異步操作
closesocket(pComKey->sock);
GlobalFree( pComKey );
GlobalFree( lpIOoperData );
continue;
}
else
{
ProcessIO( lpIOoperData, pComKey );
}
}
}
return 0;
}
BOOL ProcessIO(IO_OPERATION_DATA *pIOoperData, COMPLETION_KEY *pComKey)
{
if( pIOoperData->type == READ )
{
//打印接收到的內容
// char ch[100] = { 0 };
// sprintf(ch, "%s : %s", pComKey->sIP, pIOoperData->buffer);
// printf( ch );
Send( pComKey, pIOoperData ); //將接收到的內容原封不動的發送回去
}
else if( pIOoperData->type == WRITE )
{
Recv( pComKey, pIOoperData ); //發起接收操作
}
else if( pIOoperData->type == ACCEPT )
{ //使用GetAcceptExSockaddrs函數 獲得具體的各個地址參數.
printf("accept sucess!\n");
setsockopt( pIOoperData->sock, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT, (char*)&(pComKey->sock), sizeof(pComKey->sock) );
LP_COMPLETION_KEY pClientComKey = (LP_COMPLETION_KEY) GlobalAlloc ( GPTR, sizeof(COMPLETION_KEY) );
pClientComKey->sock = pIOoperData->sock;
SOCKADDR_IN *addrClient = NULL, *addrLocal = NULL;
int nClientLen = sizeof(SOCKADDR_IN), nLocalLen = sizeof(SOCKADDR_IN);
lpfnGetAcceptExSockaddrs(pIOoperData->buffer, 0,
sizeof(SOCKADDR_IN)+16, sizeof(SOCKADDR_IN)+16,
(LPSOCKADDR*)&addrLocal, &nLocalLen,
(LPSOCKADDR*)&addrClient, &nClientLen);
sprintf(pClientComKey->sIP, "%d", addrClient->sin_port ); //cliAdd.sin_port );
printf(pClientComKey->sIP );
CreateIoCompletionPort( (HANDLE)pClientComKey->sock, g_hComPort, (DWORD)pClientComKey, 0 ); //將監聽到的套接字關聯到完成端口
Recv( pClientComKey, pIOoperData );
// char s[30] = {0};
// sprintf( s, "%d\n", g_count++ );
// printf(s);
//接收到一個連接,就再發起一個異步操作!
AcceptClient( g_sListen );
}
return TRUE;
}
BOOL AcceptClient(SOCKET sListen)
{
DWORD dwBytes;
LP_IO_OPERATION_DATA pIO;
pIO = (LP_IO_OPERATION_DATA) GlobalAlloc (GPTR, sizeof(IO_OPERATION_DATA));
pIO->databuf.buf = pIO->buffer;
pIO->databuf.len = pIO->len = DATA_BUFSIZE;
pIO->type = ACCEPT;
//先創建一個套接字(相比accept有點就在此,accept是接收到連接才創建出來套接字,浪費時間. 這裏先準備一個,用於接收連接)
pIO->sock = WSASocket( AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED );
//調用AcceptEx函數,地址長度需要在原有的上面加上16個字節
//向服務線程投遞一個接收連接的的請求
BOOL rc = lpfnAcceptEx( sListen, pIO->sock,
pIO->buffer, 0,
sizeof(SOCKADDR_IN)+16, sizeof(SOCKADDR_IN)+16,
&dwBytes, &(pIO->overlapped) );
if( FALSE == rc )
{
if( WSAGetLastError() != ERROR_IO_PENDING )
{
printf("%d", WSAGetLastError() );
return false;
}
}
return true;
}
BOOL Recv(COMPLETION_KEY *pComKey, IO_OPERATION_DATA *pIOoperData)
{
DWORD flags = 0;
DWORD recvBytes = 0;
ZeroMemory( &pIOoperData->overlapped, sizeof(OVERLAPPED) );
pIOoperData->type = READ;
pIOoperData->databuf.buf = pIOoperData->buffer;
pIOoperData->databuf.len = pIOoperData->len = DATA_BUFSIZE;
if( SOCKET_ERROR == WSARecv( pComKey->sock, &pIOoperData->databuf, 1, &recvBytes, &flags, &pIOoperData->overlapped, NULL) )
{
if( ERROR_IO_PENDING != WSAGetLastError() )
{
printf("發起重疊接收失敗! %d\n", GetLastError() );
return FALSE;
}
}
return TRUE;
}
BOOL Send(COMPLETION_KEY *pComKey, IO_OPERATION_DATA *pIOoperData)
{
DWORD flags = 0;
DWORD recvBytes = 0;
ZeroMemory( &pIOoperData->overlapped, sizeof(OVERLAPPED) );
pIOoperData->type = WRITE;
pIOoperData->databuf.len = 100;
if( SOCKET_ERROR == WSASend( pComKey->sock, &pIOoperData->databuf, 1, &recvBytes, flags, &pIOoperData->overlapped , NULL) )
{
if( ERROR_IO_PENDING != WSAGetLastError() )
{
printf("發起發送重疊接收失敗!\n");
return FALSE;
}
}
return TRUE;
}
對於客戶端就更簡單了,只是創建線程,請求連接,發送數據,接收數據
[cpp] view plain copy
#include "stdafx.h"
#include <Afx.h>
#include <Windows.h>
#include <Winsock2.h>
#pragma comment(lib, "WS2_32.lib")
DWORD WINAPI Thread(LPVOID lParam);
int main(int argc, char* argv[])
{
WSADATA dwData;
WSAStartup( 0x0202, &dwData );
for( int i = 0; i < 2000; i++ )
{
HANDLE hThread = NULL;
hThread = CreateThread(NULL, 0, Thread, NULL, 0, 0);
CloseHandle(hThread);
hThread = NULL;
}
while( true )
{
Sleep(100);
}
return 0;
}
DWORD WINAPI Thread(LPVOID lParam)
{
SOCKET sock = socket( AF_INET, SOCK_STREAM, 0 );
SOCKADDR_IN serAddr;
serAddr.sin_family = AF_INET;
serAddr.sin_port = htons(6000);
serAddr.sin_addr.S_un.S_addr = inet_addr(_T("127.0.0.1"));
int reVal = connect( sock, (SOCKADDR*)&serAddr, sizeof(SOCKADDR) );
if( reVal==SOCKET_ERROR )
{
printf("cannot client SERVER! %d\n", WSAGetLastError());
return 0;
}
int i=0;
char buf[100] = _T("光陰的故事!\n");
while( true )
{
if( SOCKET_ERROR == send( sock, buf, 100, 0 ) )
{
printf("cannot SEND message to server! %d\n", WSAGetLastError());
break;
}
memset( buf, 0, strlen(buf) ); //清空一下,體現是接收到的數據
if( SOCKET_ERROR == recv( sock, buf, 100, 0 ) )
{
printf("cannot RECV message to server! %d\n", WSAGetLastError());
break;
}
// printf( buf );
Sleep(3000);
}
closesocket(sock);
return 0;
}
[cpp] view plain copy
將代碼貼到編譯器中即可,也可以下載這個demo http://download.csdn.net/detail/u010025913/7250965
代碼漏洞百出,希望大家多多批評指教!