在掌握了socket相關的一些函數後,套接字編程仍是比較簡單的,平常工做中碰到不少的問題就是客戶端/服務器模型中,如何讓服務端在同一時間高效的處理多個客戶端的鏈接,咱們的處理辦法可能會是在服務端不停的監聽客戶端的請求,有新的請求到達時,開闢一個新的線程去和該客戶端進行後續處理,可是這樣針對每個客戶端都須要去開闢一個新的線程,效率一定底下。編程
其實,socket編程提供了不少的模型來處理這種情形,咱們只要按照模型去實現咱們的代碼就能夠解決這個問題。主要有select模型和重疊I/o模型,以及完成端口模型。此次,咱們主要介紹下select模型,該模型又分爲普通select模型,wsaasyncselect模型,wsaeventselect模型。咱們將經過樣例代碼的方式逐一介紹。windows
1、select模型數組
使用該模型時,在服務端咱們能夠開闢兩個線程,一個線程用來監聽客戶端的鏈接服務器
請求,另外一個用來處理客戶端的請求。主要用到的函數爲select函數。如:網絡
全局變量:
fd_set g_fdClientSock;
線程1處理函數:socket
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處理函數:async
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模型帶來的問題了。函數
2、WsaAsyncSelect模型spa
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模型。
3、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模型將解決這個問題