通信編程:WSAEventSelect 模型通信


信號量機制

進程同步機制是對多個相關進程在執行次序上進行協調,使並發執行的諸進程之間能按照一定的規則(或時序)共享系統資源。信號量就是用一個變量來表示系統中某種資源的數量,可以利用這種機制來實現同步。
整型信號量定義為一個用於表示資源數目的整型量 S,S 除了初始化外僅能通過兩個標准的原子操作 wait(S)signal(S)來訪問,這兩個操作也可以被稱為 P、V 操作。wait 操作的偽代碼如下:

wait(S){
    while(S <= 0);
    S--;
}

signal 操作的偽代碼如下:

signal(S){
    S++;
}

當一個進程在修改某信號量時,由於 P、v 操作是原子操作,因此沒有其它進程可同時對該信號量進行修改。

WSAEventSelect 模型

WSAEventSelect 模型也是 Winsock 提供的異步事件通知I/O模型,與 WSAAsyncSelect 模型類似,允許應用程序在一個或者多個套接字上接收基於事件的網絡通知。不過 WSAEventSelect 模型不是依靠 Windows 的消息驅動機制,而是經由事件對象句柄通知,其機理類似於信號量機制。

創建事件對象

WSAEventSelect 模型的基本思路是為需要響應的一組網絡事件創建一個事件對象,創建事件對象的函數是 WSACreateEvent(),返回值是一個事件對象句柄。

WSAEVENT
WSAAPI
WSACreateEvent(
    void
    );

接着再調用 WSAEventSelect() 函數將網絡事件和事件對象關聯起來。當網絡事件發生時,Winsock 使相應的事件對象受信,在事件對象上的等待函數就會返回。

int
WSAAPI
WSAEventSelect(
    _In_ SOCKET s,
    _In_opt_ WSAEVENT hEventObject,
    _In_ long lNetworkEvents
    );
參數 說明
s 套接字句柄
hEventObject 事件對象句柄
lNetworkEvents 需要相應的 FD_XXX 網絡事件組合

事件受信

網絡事件與事件對象關聯之后,應用程序就可以在事件對象上等待事件了。WSAWaitForMultipleEvents() 函數用於在一個或多個事件對象上等待,當所等待的事件對象受信或者指定的時間過去時,此函數返回。

DWORD
WSAAPI
WSAWaitForMultipleEvents(
    _In_ DWORD cEvents,
    _In_reads_(cEvents) const WSAEVENT FAR * lphEvents,
    _In_ BOOL fWaitAll,
    _In_ DWORD dwTimeout,
    _In_ BOOL fAlertable
    );

WSAWaitForMultipleEvents 最多支持 64 個對象,因此這個 I/O 模型在一個線程中同一時間最多能支持 64 個套接字。如果需要使用這個模型管理更多套接字,就需要創建額外的工作線程。WSAWaitForlMultipleEvents 函數會等待網絡事件的發生,如果在指定時間內有網絡事件發生,函數的返回值會指明是哪一個事件對象促使函數返回的。

參數 說明
cEvents 指定 lphEvents 中事件對象句柄的個數
lphEvents 指向一個事件對象句柄數組
fWaitAll 指定是否等待所有事件對象都變成受信狀態
dwTimeout 指定要等待的時間,WSA INFINITE 為無窮大
fAlertable 忽略,設為 FALSE
  • 將 fWaitAll 參數設為 FALSE 以后,如果同時有幾個事件對象受信,WSAWaitForMultipleEvents 函數的返回值也僅能指明數組中較前面的事件對象。這樣就無法讓這些事件對象受信,解決辦法是 WSAWaitForMultipleEvents 函數返回后,對每個事件都再次調用 WSAWaitForMultipleEvents 函數,使所有受信的事件對象得到處理。

查看網絡事件

一旦事件對象受信,就找到與之對應的套接字,然后調用 WSAEnumNetworkEvents() 函數查看發生了什么網絡事件。

int
WSAAPI
WSAEnumNetworkEvents(
    _In_ SOCKET s,
    _In_ WSAEVENT hEventObject,
    _Out_ LPWSANETWORKEVENTS lpNetworkEvents
    );
參數 說明
s 套接字句柄
hEventObject 事件對象句柄
lpNetworkEvents 套接字上的網絡事件和報錯信息

lpNetworkEvents 是指向一個 WSANETWORKEVENTS 結構的數組,保存了在套接字上發生的網絡事件和相關的出錯代碼。

typedef struct _WSANETWORKEVENTS {
       long lNetworkEvents;
       int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;

iErrorCode 參數是一個數組,數組的每個成員對應着一個網絡事件的出錯代碼。

參數 說明
lNetworkEvents 指定發生的網絡事件
iErrorCode lNetworkEvents 的出錯代碼

WSAEventSelect 模型樣例

注意無論是客戶端還是服務器,都需要包含頭文件 initsock.h 來載入 Winsock。

功能設計

模擬實現 TCP 協議通信過程,要求編程實現服務器端與客戶端之間雙向數據傳遞。也就是在一條 TCP 連接中,客戶端和服務器相互發送一條數據即可。

initsock.h

#include <winsock2.h>
#pragma comment(lib, "WS2_32")  // 鏈接到 WS2_32.lib

class CInitSock
{
public:
    /*CInitSock 的構造器*/
    CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)
    {
        // 初始化WS2_32.dll
        WSADATA wsaData;
        WORD sockVersion = MAKEWORD(minorVer, majorVer);
        if (::WSAStartup(sockVersion, &wsaData) != 0)
        {
            exit(0);
        }
    }

    /*CInitSock 的析構器*/
    ~CInitSock()
    {
        ::WSACleanup();
    }
};

服務器

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

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

// 初始化Winsock庫
CInitSock theSock;

int main()
{
    // 事件句柄和套節字句柄表
    WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS];
    SOCKET    sockArray[WSA_MAXIMUM_WAIT_EVENTS];
    int nEventTotal = 0;

    // 創建監聽套節字
    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;

    // 創建事件對象,並關聯到新的套節字
    WSAEVENT event = ::WSACreateEvent();
    ::WSAEventSelect(sListen, event, FD_ACCEPT | FD_CLOSE);
    // 添加到表中
    eventArray[nEventTotal] = event;
     sockArray[nEventTotal] = sListen;
    nEventTotal++;

    // 處理網絡事件
    while (TRUE)
    {
        // 在所有事件對象上等待
        int nIndex = ::WSAWaitForMultipleEvents(nEventTotal, eventArray, FALSE, WSA_INFINITE, FALSE);
        // 對每個事件調用WSAWaitForMultipleEvents函數,以便確定它的狀態
        nIndex = nIndex - WSA_WAIT_EVENT_0;
        for (int i = nIndex; i < nEventTotal; i++)
        {
            nIndex = ::WSAWaitForMultipleEvents(1, &eventArray[i], TRUE, 1000, FALSE);
            if (nIndex == WSA_WAIT_FAILED || nIndex == WSA_WAIT_TIMEOUT)
            {
                continue;
            }
            else
            {
                // 獲取到來的通知消息,WSAEnumNetworkEvents函數會自動重置受信事件
                WSANETWORKEVENTS event;
                ::WSAEnumNetworkEvents(sockArray[i], eventArray[i], &event);
                if (event.lNetworkEvents & FD_ACCEPT)                // 處理FD_ACCEPT通知消息
                {
                    if (event.iErrorCode[FD_ACCEPT_BIT] == 0)
                    {
                        if (nEventTotal > WSA_MAXIMUM_WAIT_EVENTS)
                        {
                            cout << " Too many connections!" << endl;
                            continue;
                        }
                        sockaddr_in addrRemote;
                        int nAddrLen = sizeof(addrRemote);
                        SOCKET sNew = ::accept(sockArray[i], (SOCKADDR*)&addrRemote, &nAddrLen);
                        cout << "\n與主機" << ::inet_ntoa(addrRemote.sin_addr) << "建立連接" << endl;
                        WSAEVENT event = ::WSACreateEvent();
                        ::WSAEventSelect(sNew, event, FD_READ | FD_CLOSE | FD_WRITE);
                        // 添加到表中
                        eventArray[nEventTotal] = event;
                         sockArray[nEventTotal] = sNew;
                        nEventTotal++;
                    }
                }
                else if (event.lNetworkEvents & FD_READ)         // 處理FD_READ通知消息
                {
                    if (event.iErrorCode[FD_READ_BIT] == 0)
                    {
                        char szText[256];
                        int nRecv = ::recv(sockArray[i], szText, strlen(szText), 0);
                        if (nRecv > 0)
                        {
                            szText[nRecv] = '\0';
                            cout << "  接收到數據:" << szText << endl;
                        }
                        // 向客戶端發送數據
                        char sendText[] = "你好,客戶端!";
                        if (::send(sockArray[i], sendText, strlen(sendText), 0) > 0)
                        {
                            cout << "  向客戶端發送數據:" << sendText << endl;
                        }
                    }
                }
                else if (event.lNetworkEvents & FD_CLOSE)        // 處理FD_CLOSE通知消息
                {
                    if (event.iErrorCode[FD_CLOSE_BIT] == 0)
                    {
                        ::closesocket(sockArray[i]);
                        for (int j = i; j < nEventTotal - 1; j++)
                        {
                            eventArray[j] = eventArray[j + 1];
                             sockArray[j] =  sockArray[j + 1];
                        }
                        nEventTotal--;
                    }
                }
            }
        }
    }
    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 網絡與通信編程》,陳香凝 王燁陽 陳婷婷 張錚 編著,人民郵電出版社


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM