在掌握了socket相關的一些函式後,套接字程式設計還是比較簡單的,日常工作中碰到很多的問題就是客戶端/伺服器模型中,如何讓服務端在同一時間高效的處理多個客戶端的連線,我們的處理辦法可能會是在服務端不停的監聽客戶端的請求,有新的請求到達時,開闢一個新的執行緒去和該客戶端進行後續處理,但是這樣針對每一個客戶端都需要去開闢一個新的執行緒,效率必定底下。
其實,socket程式設計提供了很多的模型來處理這種情形,我們只要按照模型去實現我們的程式碼就可以解決這個問題。主要有select模型和重疊I/o模型,以及完成埠模型。這次,我們主要介紹下select模型,該模型又分為普通select模型,wsaasyncselect模型,wsaeventselect模型。我們將通過樣例程式碼的方式逐一介紹。
一、select模型
使用該模型時,在服務端我們可以開闢兩個執行緒,一個執行緒用來監聽客戶端的連線
請求,另一個用來處理客戶端的請求。主要用到的函式為select函式。如:
全域性變數:
fd_set g_fdClientSock;
執行緒1處理函式:
SOCKET listenSock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP ); sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(7788); sin.sin_addr.S_un.S_addr = INADDR_ANY; int nRet = bind( listenSock, (sockaddr*)&sin, (int)(sizeof(sin))); if ( nRet == SOCKET_ERROR ) { DWORD errCode = GetLastError();
return; } listen( listenSock, 5); int clientNum = 0; sockaddr_in clientAddr; int nameLen = sizeof( clientAddr ); while( clientNum < FD_SETSIZE ) { SOCKET clientSock = accept( listenSock, (sockaddr*)&clientAddr, &nameLen );
FD_SET( clientSock, &g_fdClientSock); clientNum++;
}
執行緒2處理函式:
fd_set fdRead; FD_ZERO( &fdRead ); int nRet = 0;
char* recvBuffer =(char*)malloc( sizeof(char) * 1024 ); if ( recvBuffer == NULL ) { return;
} memset( recvBuffer, 0, sizeof(char) * 1024 ); while ( true ) { fdRead = g_fdClientSock; nRet = select( 0, &fdRead, NULL, NULL, NULL ); if ( nRet != SOCKET_ERROR ) { for ( int i = 0; i < g_fdClientSock.fd_count; i++ ) { if ( FD_ISSET(g_fdClientSock.fd_array[i],&fdRead) ) { memset( recvBuffer, 0, sizeof(char) * 1024 ); nRet = recv( g_fdClientSock.fd_array[i], recvBuffer, 1024, 0); if ( nRet == SOCKET_ERROR ) { closesocket( g_fdClientSock.fd_array[i] ); FD_CLR( g_fdClientSock.fd_array[i], &g_fdClientSock ); } else { //todo:後續處理 } } } } } if ( recvBuffer != NULL ) {
free( recvBuffer );
}
該模型有個最大的缺點就是,它需要一個死迴圈不停的去遍歷所有的客戶端套接字集合,詢問是否有資料到來,這樣,如果連線的客戶端很多,勢必會影響處理客戶端請求的效率,但它的優點就是解決了每一個客戶端都去開闢新的執行緒與其通訊的問題。如果有一個模型,可以不用去輪詢客戶端套接字集合,而是等待系統通知,當有客戶端資料到來時,系統自動的通知我們的程式,這就解決了select模型帶來的問題了。
二、WsaAsyncSelect模型
WsaAsyncSelect模型就是這樣一個解決了普通select模型問題的socket程式設計模型。它是在有客戶端資料到來時,系統傳送訊息給我們的程式,我們的程式只要定義好訊息的處理方法就可以了,用到的函式只要是WSAAsyncSelect,如:
首先,我們定義一個Windows訊息,告訴系統,當有客戶端資料到來時,傳送該訊息給我們。
#define UM_SOCK_ASYNCRECVMSG WM_USER + 1
在我們的處理函式中可以如下監聽客戶端的連線:
SOCKET listenSock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP ); sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(7788); sin.sin_addr.S_un.S_addr = INADDR_ANY; int nRet = bind( listenSock, (sockaddr*)&sin, (int)(sizeof(sin))); if ( nRet == SOCKET_ERROR ) { DWORD errCode = GetLastError(); return;
} listen( listenSock, 5); int clientNum = 0;
sockaddr_in clientAddr; int nameLen = sizeof( clientAddr ); while( clientNum < FD_SETSIZE )
{ SOCKET clientSock = accept( listenSock, (sockaddr*)&clientAddr, &nameLen ); //hWnd為接收系統傳送的訊息的視窗控制程式碼 WSAAsyncSelect( clientSock, hWnd, UM_SOCK_ASYNCRECVMSG, FD_READ | FD_CLOSE ); clientNum++; }
接下來,我們需要在我們的視窗新增對UM_SOCK_ASYNCRECVMSG訊息的處理函式,在該函式中真正接收客戶端傳送過來的資料,在這個訊息處理函式中的wparam參數列示的是客戶端套接字,lparam參數列示的是發生的網路事件如:
SOCKET clientSock = (SOCKET)wParam; if ( WSAGETSELECTERROR( lParam ) ) {
closesocket( clientSock ); return;
} switch ( WSAGETSELECTEVENT( lParam ) )
{ case FD_READ:
{
char recvBuffer[1024] = {'\0'}; int nRet = recv( clientSock, recvBuffer, 1024, 0 ); if ( nRet > 0 ) { szRecvMsg.AppendFormat(_T("Client %d Say:%s\r\n"), clientSock, recvBuffer ); } else { //client disconnect szRecvMsg.AppendFormat(_T("Client %d Disconnect!\r\n"), clientSock ); }
} break; case FD_CLOSE: {
closesocket( clientSock ); szRecvMsg.AppendFormat(_T("Client %d Disconnect!\r\n"), clientSock ); } break; }
可以看到WsaAsyncSelect模型是非常簡單的模型,它解決了普通select模型的問題,但是它最大的缺點就是它只能用在windows程式上,因為它需要一個接收系統訊息的視窗控制程式碼,那麼有沒有一個模型既可以解決select模型的問題,又不限定只能是windows程式才能用呢?下面我們來看看WsaEventSelect模型。
三、WsaEventSelect模型
WsaEventSelect模型是一個不用主動去輪詢所有客戶端套接字是否有資料到來的模型,它也是在客戶端有資料到來時,系統傳送通知給我們的程式,但是,它不是傳送訊息,而是通過事件的方式來通知我們的程式,這就解決了WsaAsyncSelect模型只能用在windows程式的問題。
該模型的實現,我們也可以開闢兩個執行緒來進行處理,一個用來接收客戶端的連線請求,一個用來與客戶端進行通訊,用到的主要函式有WSAEventSelect,WSAWaitForMultipleEvents,WSAEnumNetworkEvents實現方式如下:
首先定義三個全域性陣列
SOCKET g_SockArray[MAX_NUM_SOCKET];//存放客戶端套接字 WSAEVENT g_EventArray[MAX_NUM_SOCKET];//存放該客戶端有資料到來時,觸發的事件 UINT32 g_totalEvent = 0;//記錄客戶端的連線數
執行緒1處理函式如下:
SOCKET listenSock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP ); sockaddr_in sin; sin.sin_family = AF_INET; sin.sin_port = htons(7788); sin.sin_addr.S_un.S_addr = INADDR_ANY; int nRet = bind( listenSock, (sockaddr*)&sin, (int)(sizeof(sin))); if ( nRet == SOCKET_ERROR ) { DWORD errCode = GetLastError(); return; } listen( listenSock, 5); sockaddr_in clientAddr; int nameLen = sizeof( clientAddr ); while( g_totalEvent < MAX_NUM_SOCKET ) { SOCKET clientSock = accept( listenSock, (sockaddr*)&clientAddr, &nameLen ); if ( clientSock == INVALID_SOCKET ) { continue; } g_SockArray[g_totalEvent] = clientSock; if( (g_EventArray[g_totalEvent] = WSACreateEvent()) == WSA_INVALID_EVENT ) { continue; } WSAEventSelect( clientSock, g_EventArray[g_totalEvent],FD_READ | FD_CLOSE ); g_totalEvent++; }
執行緒2的處理函式如下:
int nIndex = 0; char* recvBuffer =(char*)malloc( sizeof(char) * 1024 ); if ( recvBuffer == NULL ) { return; } memset( recvBuffer, 0, sizeof(char) * 1024 ); while( true ) { nIndex = WSAWaitForMultipleEvents( g_totalEvent, g_EventArray, FALSE, WSA_INFINITE,FALSE ); if ( nIndex == WSA_WAIT_FAILED ) { continue; } else { WSAResetEvent( g_EventArray[ nIndex - WSA_WAIT_EVENT_0]); SOCKET clientSock = g_SockArray[ nIndex - WSA_WAIT_EVENT_0 ]; WSANETWORKEVENTS wsaNetWorkEvent; int nRet = WSAEnumNetworkEvents( clientSock, g_EventArray[nIndex - WSA_WAIT_EVENT_0], &wsaNetWorkEvent ); if ( SOCKET_ERROR == nRet ) { continue; } else if ( wsaNetWorkEvent.lNetworkEvents & FD_READ ) { if ( wsaNetWorkEvent.iErrorCode[FD_READ_BIT] != 0 ) { //occur error closesocket( clientSock ); } else { memset( recvBuffer, 0, sizeof(char) * 1024 ); nRet = recv( clientSock, recvBuffer, 1024, 0); if ( nRet == SOCKET_ERROR ) { closesocket( clientSock ); } else { //todo:對接收到的客戶端資料進行處理 } } } else if( wsaNetWorkEvent.lNetworkEvents & FD_CLOSE ) { if ( wsaNetWorkEvent.iErrorCode[FD_CLOSE_BIT] != 0 ) { //occur error closesocket( clientSock ); } else { closesocket( clientSock ); } } } } if ( recvBuffer != NULL ) { free( recvBuffer ); }
該模型通過一個死迴圈裡面呼叫WSAWaitForMultipleEvents函式來等待客戶端套接字對應的Event的到來,一旦事件通知到達,就通過該套接字去接收資料。雖然WsaEventSelect模型的實現較前兩種方法複雜,但它在效率和相容性方面是最好的。
以上三種模型雖然在效率方面有了不少的提升,但它們都存在一個問題,就是都預設了只能接收64個客戶端連線,雖然我們在實現時可以不受這個限制,但是那樣,它們所帶來的效率提升又將打折扣,那又有沒有什麼模型可以解決這個問題呢?我們的下一篇重疊I/0模型將解決這個問題