socket編程的select模型

該文章轉自 博客園 : socket編程的select模型

在掌握了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模型將解決這個問題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章