通信編程:WSAAsyncSelect 模型通信


Windows 程序工作原理

Windows 程序設計完全不同於 DOS 程序設計方法,采用的是基於事件驅動方式的程序設計模式。Windows 系統是通過事件驅動的,事件驅動也就是程序的事件是圍繞着消息的產生與處理展開,一條消息是關於發生的事件的消息,事件驅動是靠消息循環機制來實現的。
在 Windows 編程中,所有的程序都是一個個窗口。在整個系統中,使用窗口句柄唯一標識一個窗口,每個窗口都有自己的窗口過程來處理消息。當 Windows 程序開始運行時,首先先初始化程序,然后初始化並創建一個窗口。窗口進入消息循環等待消息的到來,接收到消息的時候先看看是不是退出程序,如果是就終止程序。如果不是則判斷是否是自己感興趣的消息,如果是就進行相應的響應和處理。對於窗口不感興趣的消息,則使用默認的消息處理過程。

Windows 為每一個應用程序維護相應的消息隊列,應用程序的任務就是處理消息循環。16 位的操作系統中只有一個消息隊列,所以系統必須等待當前任務處理消息后才可以發送下一消息到相應程序。32 位的系統中每一個運行的程序都會有一個消息隊列,所以系統可以在多個消息隊列中轉換。Windows 應用程序的消息來源有輸入消息、控制消息、系統消息、用戶消息 4 種,這些消息會被窗口綁定的回調函數(Cal lback Function)處理。

Windows 程序樣例

運行 Windows 應用程序在桌面顯示 Windows 窗口,且窗口中居中顯示“大家好,這是我的第一個 Windows API 程序!”同時播放背景音樂。一個簡單的 Windows API 程序由 2 部分組成,分別是 WinMain 函數CALLBACK 函數,Windows 程序以 WinMain 函數作為進入程序的初始人口點。Windows 系統是通過事件驅動的,事件驅動圍繞着消息的產生與處理展開,一條消息是關於發生的事件的消息。CALLBACK 函數就是當窗口接收到消息時,負責對對應的消息做出動作和響應。

WinMain 函數

WinMain 函數的原型為 int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR IpCmdLine, int nCmdshow),一般情況下我們應該在 WinMain 函數中完成下面的操作:

首先先解釋一下 Main 函數的各個參數的含義:

參數 說明
hinstance 應用程序當前實例的句柄
hPrevlnstance 應用程序的先前實例的句柄
szCmdLine 指向應用程序命令行的指針
iCmdShow 指明窗口如何顯示

實例化窗口類

接着初始化一些變量,例如窗口名、消息句柄和消息:

static TCHAR szAppName[] = TEXT("HelloWorld!");
HWND hwnd;
MSG msg;

接着需要實例化窗口類,並且設置窗口的一些基本屬性,包括窗口的樣式、圖標、背景和鼠標樣式等,還要綁定 callback 函數。

WNDCLASS wndclass;    //WNDCLASS是一個由系統支持的結構,用來儲存某一類窗口的信息
wndclass.style = CS_HREDRAW | CS_VREDRAW;    // 窗口類的風格
wndclass.lpfnWndProc = WndProc;    //窗口處理的回調函數
wndclass.cbClsExtra = 0;    //窗口擴展
wndclass.cbWndExtra = 0;    //窗口實例擴展
wndclass.hInstance = hInstance;    //實例句柄
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);    //窗口的圖標
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);   //窗口鼠標光標
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);   //窗口背景色
wndclass.lpszMenuName = NULL;   //窗口菜單

注冊窗口

實例化窗口類后,我們需要將這個窗口對象注冊,只有注冊好的窗口對象才能夠被顯示。注冊窗口需要使用 RegisterClass(&wndclass) 方法,注冊成功返回 true,此時使用一個 if 判斷一下是否注冊成功,注冊失敗顯示提示信息並返回。

wndclass.lpszClassName = szAppName;   //窗口類名
if(!RegisterClass(&wndclass))    //注冊窗口
{
	//顯示一個模態對話框
	MessageBox(NULL,TEXT("This program RegisterClass is error!"), szAppName, MB_ICONERROR);
	return 0;
}

顯示並更新窗口

接下來需要在句柄中設置窗口顯示的相關信息,並利用該句柄和 ShowWindow(hwnd, iCmdShow) 方法顯示窗口,顯示后用 UpdateWindow(hwnd) 更新窗口完成顯示。

hwnd = CreateWindow(szAppName,                    //lpClassName:窗口類名 
                    TEXT("The Hello Program"),    //lpWindowName:窗口標題 
                    WS_OVERLAPPEDWINDOW,          //dwStyle:指定創建窗口的風格
                    CW_USEDEFAULT,                //X:指定窗口的初始水平位置
                    CW_USEDEFAULT,                //Y:指定窗口的初始垂直位置
                    CW_USEDEFAULT,                //nWidth:以設備單元指明窗口的寬度
                    CW_USEDEFAULT,                //nHeight:以設備單元指明窗口的高度
                    NULL,                         //hWndParent:指向被創建窗口的父窗口或所有者窗口的句柄
                    NULL,                         //hMenu:菜單句柄 
                    hInstance,                    //hlnstance:與窗口相關聯的模塊實例的句柄
                    NULL);                        //lpParam:該值傳遞給窗口WM_CREATE消息
ShowWindow(hwnd, iCmdShow);    //該函數設置指定窗口的顯示狀態
UpdateWindow(hwnd);    //更新指定窗口的客戶區

消息循環

窗口顯示在桌面后,就需要循環偵聽傳來的消息,轉換成相應的消息標識后做相應的處理即可。

while(GetMessage(&msg, NULL, 0, 0))    //從調用線程的消息隊列里取得一個消息並將其放於指定的結構
{ 
	TranslateMessage(&msg);    //用於將虛擬鍵消息轉換為字符消息
	DispatchMessage(&msg);    //函數分發一個消息給窗口程序
} 
return msg.wParam;

CALLBACK 函數

回調函數並不由開發者直接調用執行,只是使用系統接口 API 函數作為起點,回調函數通常作為參數傳遞給系統 API,由該 API 來調用。回調函數可能被系統 API 調用一次,也可能被循環調用多次。此處就是當窗口接收到一個消息時,將調用 CALLBACK 函數對消息進行處理。代碼如下:

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM IParam)
{ 
	HDC hdc;    //HDC:設備場景句柄
	PAINTSTRUCT ps;    //PAINTSTRUCT:繪圖信息結構
	RECT rect;    //rect:存儲成對出現的參數,比如一個矩形框的左上角坐標、寬度和高度
	switch(message)
	{
		case WM_CREATE:
			PlaySound(TEXT("hellowin.wav"), NULL, SND_FILENAME|SND_ASYNC);    //播放音頻
			return 0;
		case WM_PAINT:
			hdc = BeginPaint(hwnd, &ps);    //為指定窗口進行繪圖工作的准備
			GetClientRect(hwnd, &rect);    //函數獲取窗口客戶區的大小
			//在指定的矩形里寫入格式化的正文
			DrawText(hdc, TEXT("大家好,這是我的第一個Windows API 程序!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
			EndPaint(hwnd, &ps);
			return 0;
		case WM_DESTROY:    //該函數向系統表明有個線程有終止請求
			PostQuitMessage(0);
			return 0;
	}
	return DefWindowProc(hwnd, message, wParam, IParam);    //調用缺省的窗口過程來為應用程序沒有處理的任何窗口消息提供缺省的處理
}

運行效果

如果使用 DEV-C++ 來編譯 Windows 程序,需要在編譯時加入“-mwindows -lwinmm”命令,首先先打開工具中的“編譯選項”。

選中“編譯時加入以下命令”,並且寫上“-mwindows -lwinmm”命令,保存並編譯。

通過編譯后運行的效果如下:

完整代碼

#include <windows.h>
//LRESULT就是long,CALLBACK是回調函數,參數分別是窗口句柄,消息,消息參數,消息參數
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
/*
hinstance:應用程序當前實例的句柄
hPrevlnstance:應用程序的先前實例的句柄
szCmdLine:指向應用程序命令行的指針
iCmdShow:指明窗口如何顯示
*/
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
	static TCHAR szAppName[] = TEXT("HelloWorld!");
	HWND hwnd;
	MSG msg;
	WNDCLASS wndclass;    //WNDCLASS是一個由系統支持的結構,用來儲存某一類窗口的信息
	wndclass.style = CS_HREDRAW | CS_VREDRAW;    // 窗口類的風格
	wndclass.lpfnWndProc = WndProc;    //窗口處理的回調函數
	wndclass.cbClsExtra = 0;    //窗口擴展
	wndclass.cbWndExtra = 0;    //窗口實例擴展
	wndclass.hInstance = hInstance;    //實例句柄
	wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);    //窗口的圖標
	wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);   //窗口鼠標光標
	wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);   //窗口背景色
        wndclass.lpszMenuName = NULL;   //窗口菜單

	wndclass.lpszClassName = szAppName;   //窗口類名
	if(!RegisterClass(&wndclass))
	{
		//顯示一個模態對話框
		MessageBox(NULL,TEXT("This program RegisterClass is error!"), szAppName, MB_ICONERROR);
		return 0;
	}
	
	hwnd = CreateWindow(szAppName,                    //lpClassName:窗口類名 
			    TEXT("The Hello Program"),    //lpWindowName:窗口標題 
			    //WS_POPUP | WS_BORDER | WS_THICKFRAME, 
			    WS_OVERLAPPEDWINDOW,          //dwStyle:指定創建窗口的風格
			    CW_USEDEFAULT,                //X:指定窗口的初始水平位置
			    CW_USEDEFAULT,                //Y:指定窗口的初始垂直位置
			    CW_USEDEFAULT,                //nWidth:以設備單元指明窗口的寬度
			    CW_USEDEFAULT,                //nHeight:以設備單元指明窗口的高度
			    NULL,                         //hWndParent:指向被創建窗口的父窗口或所有者窗口的句柄
			    NULL,                         //hMenu:菜單句柄 
			    hInstance,                    //hlnstance:與窗口相關聯的模塊實例的句柄
			    NULL);                        //lpParam:該值傳遞給窗口WM_CREATE消息
	ShowWindow(hwnd, iCmdShow);    //該函數設置指定窗口的顯示狀態
	UpdateWindow(hwnd);    //更新指定窗口的客戶區
	while(GetMessage(&msg, NULL, 0, 0))    //從調用線程的消息隊列里取得一個消息並將其放於指定的結構
	{ 
		TranslateMessage(&msg);    //用於將虛擬鍵消息轉換為字符消息
		DispatchMessage(&msg);    //函數分發一個消息給窗口程序
	} 
	return msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM IParam)
{ 
	HDC hdc;    //HDC:設備場景句柄
	PAINTSTRUCT ps;    //PAINTSTRUCT:繪圖信息結構
	RECT rect;    //rect:存儲成對出現的參數,比如一個矩形框的左上角坐標、寬度和高度
	switch(message)
	{
		case WM_CREATE:
			PlaySound(TEXT("hellowin.wav"), NULL, SND_FILENAME|SND_ASYNC);
			return 0;
		case WM_PAINT:
			hdc = BeginPaint(hwnd, &ps);    //為指定窗口進行繪圖工作的准備
			GetClientRect(hwnd, &rect);    //函數獲取窗口客戶區的大小
			//在指定的矩形里寫入格式化的正文
			DrawText(hdc, TEXT("大家好,這是我的第一個Windows API 程序!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
			EndPaint(hwnd, &ps);
			return 0;
		case WM_DESTROY:    //該函數向系統表明有個線程有終止請求
			PostQuitMessage(0);
			return 0;
	}
	return DefWindowProc(hwnd, message, wParam, IParam);    //調用缺省的窗口過程來為應用程序沒有處理的任何窗口消息提供缺省的處理
}

WSAAsyncSelect 模型

WSAAsyncSelect 模型允許應用程序以 Windows 消息的形式接收網絡事件通知,它為了適應 Windows 的消息驅動環境而設置的。WSAAsyncSelect 模型最突出的特點是與 Windows 的消息驅動機制融在了一起,這使得開發帶 GUI 界面的網絡程序變得很簡單。但是如果連接增加,單個 Windows 函數處理上千個客戶請求時,服務器性能勢必會受到影響。
WSAAsyncSelect 模型使用 WSAAsyncSelect 函數自動把套接字設為非阻塞模式,並且為套接字綁定一個窗口句柄,當有網絡事件發生時,便向這個窗口發送消息。

int
WSAAPI
WSAAsyncSelect(
    _In_ SOCKET s,
    _In_ HWND hWnd,
    _In_ u_int wMsg,
    _In_ long lEvent
    );
參數 熟悉
s 需要設置的套接字句柄
hWnd 指定一個窗口句柄,通知消息將被發送到與其對應的窗口過程中
wMsg 網絡事件到來時接收到的消息 ID,可以在 WM USER 以上的數值中任意選擇一個
IEvent 指定哪些通知碼需要發送消息
參數 IEvent 指定了要發送的通知碼,可以是如下取值的組合:
通知碼 說明
--- ---
FD_READ 套接字接收到對方發送過來的數據包,此時可以讀取數據
FD_WRITE 數據緩沖區滿后再次變空時,通知應用程序表示可以繼續發送數據了
FD_ACCEPT 監聽中的套接字檢測到有連接進入
FD_CONNECT 連接其他的主機,連接完成以后會接收到這個通知碼
FD_CLOSE 檢測到套接字對應的連接被關閉

如果需要指定多個通知碼就用 “|” 連接,例如下面這個調用表示當接收數據、可以發送數據、套接字關閉時發送消息。

::WSAAsyncSelect(client, hWnd, WM_SOCKET, FD_READ | FD_WRITE | FD_CLOSE);

成功調用 WSAAsyncSelect 之后,應用程序便開始以 Windows 消息的形式在 CALLBACK 函數接收網絡事件通知。

WSAAsvncSelect 模型樣例

注意無論是客戶端還是服務器,都需要包含頭文件 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();
    }
};

服務器

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

#define WM_SOCKET WM_USER + 101     // 自定義消息
CInitSock theSock;

LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int main()
{
    static TCHAR szClassName[] = TEXT("MainWClass");
    WNDCLASSEX wndclass;
    // 用描述主窗口的參數填充WNDCLASSEX結構
    wndclass.cbSize = sizeof(wndclass);
    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = WindowProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = NULL;
    wndclass.hIcon = ::LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor = ::LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)::GetStockObject(WHITE_BRUSH);
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = szClassName;
    wndclass.hIconSm = NULL;
    ::RegisterClassEx(&wndclass);
    
    // 創建主窗口
    HWND hWnd = ::CreateWindowEx(
        0,
        szClassName,
        TEXT(""),
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        NULL,
        NULL,
        NULL,
        NULL);
    if (hWnd == NULL)
    {
        ::MessageBox(NULL, TEXT("創建窗口出錯!"), TEXT("error"), MB_OK);
        return -1;
    }

    // 創建監聽套接字
    SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    sockaddr_in sin;
    USHORT nPort = 4567;    // 此服務器監聽的端口號
    sin.sin_family = AF_INET;
    sin.sin_port = htons(nPort);
    sin.sin_addr.S_un.S_addr = INADDR_ANY;
    // 綁定套接字到本地機器
    if (::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
    {
        cout << " Failed bind()" << endl;
        return -1;
    }

    // 將套接字設為窗口通知消息類型。
    ::WSAAsyncSelect(sListen, hWnd, WM_SOCKET, FD_ACCEPT | FD_CLOSE);

    // 進入監聽模式
    if (::listen(sListen, 5) == SOCKET_ERROR)
    {
        cout << " Failed listen()" << endl;
        return 0;
    }
    cout << "服務器已啟動監聽,可以接收連接!" << endl;

    // 從消息隊列中取出消息
    MSG msg;
    while (::GetMessage(&msg, NULL, 0, 0))
    {
        // 轉化鍵盤消息
        ::TranslateMessage(&msg);
        // 將消息發送到相應的窗口函數
        ::DispatchMessage(&msg);
    }
    // 當GetMessage返回0時程序結束
    return msg.wParam;
}


LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
        case WM_SOCKET:
        {
            // 取得有事件發生的套接字句柄
            SOCKET s = wParam;
            // 查看是否出錯
            if (WSAGETSELECTERROR(lParam))
            {
                ::closesocket(s);
                return 0;
            }
            // 處理發生的事件
            switch (WSAGETSELECTEVENT(lParam))
            {
                case FD_ACCEPT:     // 監聽中的套接字檢測到有連接進入
                {
                    sockaddr_in addrRemote;
                    int nAddrLen = sizeof(addrRemote);
                    SOCKET client = ::accept(s, (SOCKADDR*)&addrRemote, &nAddrLen);
                    ::WSAAsyncSelect(client, hWnd, WM_SOCKET, FD_READ | FD_CLOSE);
                    cout << "\n與主機" << ::inet_ntoa(addrRemote.sin_addr) << "建立連接" << endl;
                }
                break;
            
                case FD_READ:
                {
                    char szText[1024] = { 0 };
                    char sendText[] = "你好,客戶端!";
                    if (::recv(s, szText, 1024, 0) == -1)
                    {
                        ::closesocket(s);
                    }
                    else
                    {
                        cout << "  接收到數據:" << szText << endl;
                        // 向客戶端發送數據
                        if (::send(s, sendText, strlen(szText), 0) > 0)
                        {
                            cout << "  向客戶端發送數據:" << sendText << endl;
                        }
                    }
                }
                break;
        
                case FD_CLOSE:
                {
                    ::closesocket(s);
                }
                break;
            }
        }
        return 0;
    
        case WM_DESTROY:
        {
            ::PostQuitMessage(0);
        }
        return 0;
    }

    // 將我們不處理的消息交給系統做默認處理
    return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}

客戶端

#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, strlen(buff), 0);
    if (nRecv > 0)
    {
        buff[nRecv] = '\0';
        cout << "  接收到數據:" << buff << endl;
    }
    
    // 關閉套接字
    ::closesocket(s);
    return 0;
}

運行效果

參考資料

《Windows 網絡與通信編程》,陳香凝 王燁陽 陳婷婷 張錚 編著,人民郵電出版社


免責聲明!

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



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