1. 程式人生 > 其它 >通訊程式設計:Select 模型通訊

通訊程式設計:Select 模型通訊

目錄

非阻塞模式

Winsock 可以在阻塞和非阻塞模式下執行 I/O 操作,套接字建立時預設工作在阻塞模式下。也就是說當某個操作不能執行時,程式會先阻塞,等待操作可以被執行時才繼續程式。例如對 recv 函式的呼叫會使程式進入等待狀態,直到接收到資料才返回。
阻塞套接字的好處是使用簡單,但是當需要處理多個套接字連線時,就必須建立多個執行緒,給程式設計帶來了許多不便。所以實際開發中使用最多的還是非阻塞模式,它使用起來比較複雜,但是處理髮送和接收資料或者管理連線的 Winsock 呼叫將會立即返回,效率很高。
不過如果系統輸入緩衝區中沒有待處理的資料,那麼對 recv 的呼叫將返回 WSAEWOULDBLOCK 錯誤。關鍵的問題在於如何確定套接字什麼時候可讀/可寫,如果需要不斷呼叫函式去測試的話,程式的效能勢必會受到影響,解決的辦法就是使用 Windows 提供的不同的 I/O 模型。

Select 模型

select 模型的設計源於 UNIX 系統,主要實現的原理是 IO 多路複用。select 模型的優勢是程式能夠在單個執行緒內同時處理多個套接字連線,這避免了阻塞模式下的執行緒膨脹問題。但是新增到 fd_set 結構的套接字數量是有限制的,如果能能新增的 socket 太多的話,伺服器效能就會受到影響。

select 函式

模型通過使用 select 函式來管理 I/O,函式可以確定一個或者多個套接字的狀態。如果套接字上沒有網路事件發生,便進入等待狀態,以便執行同步 I/O。

int
WSAAPI
select(
    _In_ int nfds,
    _Inout_opt_ fd_set FAR * readfds,
    _Inout_opt_ fd_set FAR * writefds,
    _Inout_opt_ fd_set FAR * exceptfds,
    _In_opt_ const struct timeval FAR * timeout
    );

函式呼叫成功返回發生網路事件的所有 socket 數量的綜合,超過時間限制就返回 0.

引數 說明
nfds 忽略,為了與 Berkeley 套接字相容
readfds 指向一個套接字集合,用來檢查其可讀性
writefds 指向一個套接字集合,用來檢查其可寫性
exceptfds 指向一個套接字集合,用來檢查錯誤
timeout 指定此函式等待的最長時間,為 NULL 時最長時間為無限大

套接字集合

fd_set 結構是 socket 集合,它可以把多個套接字連在一起,select 函式可以測試這個集合中哪些套接字有事件發生。

typedef struct fd_set {
        u_int fd_count;               /* how many are SET? */
        SOCKET  fd_array[FD_SETSIZE];   /* an array of SOCKETs */
} fd_set;

WINSOCK 定義了 4 個操作 fd_set 的巨集。

巨集 功能
FD_ZERO(*set) 初始化 set 為空集合,集合在使用前應該總是清空
FD_CLR(s, *set) 從 set 移除套接字 s
FD_ISSET(s, *set) 檢查 s 是不是 set 的成員,如果是返回 TRUE
FD_SET(s, *set) 新增套接字到集合

網路事件

傳遞給 select 函式的 3 個 fd_set 結構分別用於為了檢查可讀性(readfds)檢查可寫性(writefds)檢查錯誤(exceptfds)。當我們想要測試某個 socket 的某種狀態是,就把它放入對應的 fd_set 中,等待 select 函式返回。select 函式呼叫完成後,若 socket 還在 fd_set 中,就說明該 socket 滿足可讀、可寫或者出錯了。

設定超時

timeout 是 timeval 結構的指標,它指定了 select 函式等待的最長時間。

/*
 * Structure used in select() call, taken from the BSD file sys/time.h.
 */
struct timeval {
        long    tv_sec;         /* seconds */
        long    tv_usec;        /* and microseconds */
};
引數 說明
tv_sec 等待多少秒
tv_usec 等待多少毫秒

如果 timeout 設為 NULL,select 將會無限阻塞。

Select 模型樣例

注意無論是客戶端還是伺服器,都需要包含標頭檔案 initsock.h 來載入 Winsock。

功能設計

模擬實現 TCP 協議通訊過程,要求程式設計實現伺服器端與客戶端之間雙向資料傳遞。也就是在一條 TCP 連線中,客戶端和伺服器相互發送一條資料即可。

伺服器

使用 Select 模型實現的伺服器需要按照如圖所示的步驟進行程式設計,具體編碼如下所示。

#include "initsock.h"
#include <iostream>
using namespace std;

CInitSock theSock;      // 初始化Winsock庫
int main()
{
    // 建立監聽套接字
    SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_port = htons(4567);
    sin.sin_addr.S_un.S_addr = INADDR_ANY;
    // 繫結套接字到本地機器
    if (::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
    {
        cout << " Failed bind()" << endl;
        return -1;
    }
    // 進入監聽模式
    if (::listen(sListen, 5) == SOCKET_ERROR)
    {
        cout << " Failed listen()" << endl;
        return 0;
    }
    cout << "伺服器已啟動監聽,可以接收連線!" << endl;

    // select模型處理過程
    // 1)初始化一個套接字集合fdSocket,新增監聽套接字控制代碼到這個集合
    fd_set fdSocket;        // 所有可用套接字集合
    FD_ZERO(&fdSocket);
    FD_SET(sListen, &fdSocket);
    while (TRUE)
    {
        // 2)將fdSocket集合的一個拷貝fdRead傳遞給select函式,
        // 當有事件發生時,select函式移除fdRead集合中沒有未決I/O操作的套接字控制代碼,然後返回。
        fd_set fdRead = fdSocket;
        int nRet = ::select(0, &fdRead, NULL, NULL, NULL);
        if (nRet > 0)
        {
            // 3)通過將原來fdSocket集合與select處理過的fdRead集合比較,
            // 確定都有哪些套接字有未決I/O,並進一步處理這些I/O。
            for (int i = 0; i < (int)fdSocket.fd_count; i++)
            {
                if (FD_ISSET(fdSocket.fd_array[i], &fdRead))
                {
                    if (fdSocket.fd_array[i] == sListen)    // (1)監聽套接字接收到新連線
                    {
                        if (fdSocket.fd_count < FD_SETSIZE)
                        {
                            sockaddr_in addrRemote;
                            int nAddrLen = sizeof(addrRemote);
                            //接收客戶端的連線請求
                            SOCKET sNew = ::accept(sListen, (SOCKADDR*)&addrRemote, &nAddrLen);
                            FD_SET(sNew, &fdSocket);
                            cout << "\n與主機" << ::inet_ntoa(addrRemote.sin_addr) << "建立連線" << endl;
                        }
                        else
                        {
                            cout << " Too much connections!" << endl;
                            continue;
                        }
                    }
                    else
                    {
                        char szText[256];
                        int nRecv = ::recv(fdSocket.fd_array[i], szText, strlen(szText), 0);
                        if (nRecv > 0)    // (2)可讀
                        {
                            //接收資料
                            szText[nRecv] = '\0';
                            cout << "  接收到資料:" << szText << endl;
                            //傳送資料
                            char result[20];
                            char sendText[] = "你好,客戶端!";
                            if(::send(fdSocket.fd_array[i], sendText, strlen(sendText), 0) > 0)
                            {
                                cout << "  向客戶端傳送資料:" << sendText << endl;
                            }
                        }
                        else    // (3)連線關閉、重啟或者中斷
                        {
                            ::closesocket(fdSocket.fd_array[i]);
                            FD_CLR(fdSocket.fd_array[i], &fdSocket);
                        }
                    }
                }
            }
        }
        else
        {
            cout << " Failed select()" << endl;
            break;
        }
    }
    return 0;
}

客戶端

#include "InitSock.h"
#include <iostream>
using namespace std;

CInitSock initSock;     // 初始化Winsock庫

int main()
{
    // 建立套節字
    SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (s == INVALID_SOCKET)
    {
        cout << " Failed socket()" << endl;
        return 0;
    }

    // 也可以在這裡呼叫bind函式繫結一個本地地址
    // 否則系統將會自動安排
    char address[20] = "127.0.0.1";
    // 填寫遠端地址資訊
    sockaddr_in servAddr;
    servAddr.sin_family = AF_INET;
    servAddr.sin_port = htons(4567);
    // 注意,這裡要填寫伺服器程式(TCPServer程式)所在機器的IP地址
    // 如果你的計算機沒有聯網,直接使用127.0.0.1即可
    servAddr.sin_addr.S_un.S_addr = inet_addr(address);

    if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
    {
        cout << " Failed connect() " << endl;
        return 0;
    }
    else 
    {
        cout << "與伺服器 " << address << "建立連線" << endl;
    }

    char szText[] = "你好,伺服器!";
    if (::send(s, szText, strlen(szText), 0) > 0)
    {
        cout << "  傳送資料:" << szText << endl;
    }

    // 接收資料
    char buff[256];
    int nRecv = ::recv(s, buff, 256, 0);
    if (nRecv > 0)
    {
        buff[nRecv] = '\0';
        cout << "  接收到資料:" << buff << endl;
    }
    
    // 關閉套節字
    ::closesocket(s);
    return 0;
}

執行效果

參考資料

《Windows 網路與通訊程式設計》,陳香凝 王燁陽 陳婷婷 張錚 編著,人民郵電出版社
UNIX再學習 -- 函式 select、poll、epoll