Microsoft Word - CH07

Similar documents
Microsoft PowerPoint - gp2.ppt

d2.doc

Microsoft PowerPoint - gp3.ppt

WinSockÍøÂç±à³Ì

BOOL EnumWindows(WNDENUMPROC lparam); lpenumfunc, LPARAM (Native Interface) PowerBuilder PowerBuilder PBNI 2

FY.DOC


資料結構之C語言重點複習

untitled

Microsoft PowerPoint - directx01.ppt

概述

mfc.doc

int *p int a 0x00C7 0x00C7 0x00C int I[2], *pi = &I[0]; pi++; char C[2], *pc = &C[0]; pc++; float F[2], *pf = &F[0]; pf++;

第三章 Windows Sockets 1

MFC 2/e PDF GBK mirror - anyway solution MFC 1/e MFC 2/e

CC213

0 0 = 1 0 = 0 1 = = 1 1 = 0 0 = 1

用手機直接傳值不透過網頁連接, 來當作搖控器控制家電 ( 電視遙控器 ) 按下按鍵發送同時會回傳值來確定是否有送出 問題 :1. 應該是使用了太多 thread 導致在傳值上有問題 2. 一次按很多次按鈕沒辦法即時反應

主程式 : public class Main3Activity extends AppCompatActivity { ListView listview; // 先整理資料來源,listitem.xml 需要傳入三種資料 : 圖片 狗狗名字 狗狗生日 // 狗狗圖片 int[] pic =new

投影片 1

ebook12-1

新版 明解C++入門編


The golden pins of the PCI card can be oxidized after months or years

/ / (FC 3)...

新・明解C言語入門編『索引』

Microsoft PowerPoint - VB14.ppt

Python a p p l e b e a r c Fruit Animal a p p l e b e a r c 2-2

C 1

Microsoft Word - 01.DOC

(Methods) Client Server Microsoft Winsock Control VB 1 VB Microsoft Winsock Control 6.0 Microsoft Winsock Control 6.0 1(a). 2

概述

PowerPoint 簡報

任務二 : 產生 20 個有炸彈的磚塊, 放在隨機的位置編輯 Block 類別的程式碼 import greenfoot.; // (World, Actor, GreenfootImage, Greenfoot and MouseInfo) Write a description of class

<4D F736F F D E4345C6BDCCA84323B1E0B3CCD2AAB5E3D6AED2BB2E646F63>

车载实时图像全景拼接系统.doc

Microsoft PowerPoint - Introduction to Windows Programming and MFC

Slide 1

提问袁小兵:

PowerPoint Presentation

ebook35-21

目錄 目錄 關於手冊 NModbus API 函數 Master API CreateRtu CreateIp CreateAscii WriteSin

Spyder Anaconda Spyder Python Spyder Python Spyder Spyder Spyder 開始 \ 所有程式 \ Anaconda3 (64-bit) \ Spyder Spyder IPython Python IPython Sp

ebook

10-2 SCJP SCJD 10.1 昇陽認證 Java 系統開發工程師 的認證程序 Java IT SCJD

untitled

運算子多載 Operator Overloading

ebook51-14

( )... 5 ( ) ( )

<4D F736F F D20A6D9A6D7B5E4C159B177AACCB971B8A3BFE9A44AB8CBB86D>

2a-4

威 福 髮 藝 店 桃 園 市 蘆 竹 區 中 山 里 福 祿 一 街 48 號 地 下 一 樓 50,000 獨 資 李 依 純 105/04/06 府 經 登 字 第 號 宏 品 餐 飲 桃 園 市 桃 園 區 信 光 里 民

書面

Microsoft Word - 正文.doc

Microsoft PowerPoint - ds-1.ppt [兼容模式]

TwinCAT 1. TwinCAT TwinCAT PLC PLC IEC TwinCAT TwinCAT Masc

MFC 2/e PDF GBK mirror - anyway solution MFC 1/e MFC 2/e 2

_汪_文前新ok[3.1].doc

untitled

3 N D I S N D I S N D I S N D I D D K C p a c k e t. c o p e n c l o s. c r e a d. c w r i t e. c p a c k e t. r c p a c k e t. s y s p a c k e t. i n

Microsoft Word - ACL chapter00a-1ed .doc

多媒體應用 13 新增專案並完成版面配置 <ExMusic01> <activity_main.xml> ImageView ID imgplay ImageView ID imgstop ImageView ID imgfront TextView ID txtsong TextView ID t

運算子多載 Operator Overloading

Scott Effective C++ C++ C++ Roger Orr OR/2 ISO C++ Effective Modern C++ C++ C++ Scoot 42 Bart Vandewoestyne C++ C++ Scott Effective Modern C++ Damien

Microsoft Word - CIN-DLL.doc

使用手冊

untitled

Excel VBA Excel Visual Basic for Application

105A 資管一程式設計實驗 06 函式定義謝明哲老師 2 程式設計實驗 6.3: 自行定義一個可以接受兩個整數並傳回其最大公因數的函式, 接著利用該函式自 行定義一個可以接受兩個整數並傳回其最小公倍數函式 // gcd_fcn.cpp int gcd(int m,

PowerPoint Presentation

INTRODUCTION TO COM.DOC

untitled

Java

深入剖析WTL.doc

《 计 算 机 网 络 》

單步除錯 (1/10) 打開 Android Studio, 點選 Start a new Android Studio project 建立專案 Application name 輸入 BMI 點下 Next 2 P a g e

全国计算机技术与软件专业技术资格(水平)考试

3. 給 定 一 整 數 陣 列 a[0] a[1] a[99] 且 a[k]=3k+1, 以 value=100 呼 叫 以 下 兩 函 式, 假 設 函 式 f1 及 f2 之 while 迴 圈 主 體 分 別 執 行 n1 與 n2 次 (i.e, 計 算 if 敘 述 執 行 次 數, 不

Microsoft Word - ch04三校.doc

課程簡介

bingdian001.com

謙卑的小巨人 文 / 林士涵 印製見證文集是父親在生病後就有的想法 目的是希望更多親朋好友能透 過這些見證認識主耶穌 一起享受屬耶穌那好得無比的生命 我的父親林進聰 民國 42 年 9 月 18 日生於台中縣大肚 鄉 退伍後輾轉來到工業技術研究院化工所上班 認識了他生 命中兩個最愛 信仰耶穌基督以及

Microsoft PowerPoint - Class2.pptx

1 IT IT IT IT Virtual Machine, VM VM VM VM Operating Systems, OS IT

第一篇 : Windows CE

93年各縣國中教師甄試最新考情.doc

雲端 Cloud Computing 技術指南 運算 應用 平台與架構 10/04/15 11:55:46 INFO 10/04/15 11:55:53 INFO 10/04/15 11:55:56 INFO 10/04/15 11:56:05 INFO 10/04/15 11:56:07 INFO

CU0594.pdf

6 C51 ANSI C Turbo C C51 Turbo C C51 C51 C51 C51 C51 C51 C51 C51 C C C51 C51 ANSI C MCS-51 C51 ANSI C C C51 bit Byte bit sbit

华恒家庭网关方案

PowerPoint 演示文稿

用户大会 论文集2.2.doc

ACI pdf

游戏厅捕鱼技巧_天天酷跑游戏技巧 2048游戏技巧,游戏厅打鱼技巧_

ActiveX Control

图 4.2 udpclient 项目解决方案 3. 客户机程序编码如下 : 程序 : udp 客户机程序 udpclient.cpp

Microsoft Word - 把时间当作朋友(2011第3版)3.0.b.06.doc

1 1 Excel VBA 說明 ( ) (_) STEP4 Excel 2 STEP5 A1 1 B2 2 C3 3 STEP6 A1 STEP7 > > 1-11

<4D F736F F D20C8EDC9E82DCFC2CEE7CCE22D3039C9CF>

Transcription:

WSAAsyncSelect 模型開發 WSAAsyncSelect 模型是 Windows Sockets 的一個非同步 I/O 模型 利用該模型應用程式可以在一個 Socket 上, 接收以 Windows 訊息為基礎的網路事件 Windows Sockets 應用程式在建立 Socket 後, 呼叫 WSAAsyncSelect() 函式註冊感興趣的網路事件 當該事件發生時 Windows 視窗收到訊息, 然後應用程式就可以對接收到的網路事件進行處理 本章講述 WSAAsyncSelect 模型和利用該模型開發的區域網路簡易網路聊天程式 7.1 WSAAsyncSelect 模型分析 WSAAsyncSelect 模型是 Select 模型的非同步版本 該模型最早出現在 Windows Sockets 的 1.1 版本, 用於幫助早期的開發人員在 16 位元 Windows 平台, 適應多工訊息環境 該模型應用在一個標準的 Windows 應用程式中, 並且得到 Microsoft Foundation Class(MFC) Sockets 類別的採用 本節講述什麼是 WSAAsyncSelect 模型, 以及與 Select 模型相比存在哪些不同 7.1.1 WSAAsyncSelect 模型 第 6 章學習 Windows Sockes 的 Select 模型 在應用程式中呼叫 select() 函式時, 會發生阻塞現象 開發人員可以透過 select() 函式 timeout 參數, 設置函式呼叫的阻塞時間 在設

定時間內, 執行緒保持等待, 直到其中的一個或者多個 Socket 滿足可讀或者可寫的條件, 該函式才返回 WSAAsyncSelect 模型是非阻塞的 如圖 7.1 所示,Windows Sockets 應用程式在呼叫 recv() 函式接收資料之前, 呼叫 WSAAsyncSelect() 函式註冊網路事件 WSAAsyncSelect() 函式立即返回, 執行緒繼續執行 當系統中資料準備好時, 向應用程式傳送訊息 應用程式接收到這個訊息後, 呼叫 recv() 函式接收資料 應用行程 WSAAsyncSelect 執行緒繼續執行 系統呼叫 返回 內核 等待資料 recv() 消息 系統呼叫 資料準備好複製資料 複製資料期間阻塞 將資料複製到用戶空間 處理資料 回傳成功指示 完成複製 圖 7.1 WSAAsyncSelect 模型 7.1.2 與 Select 模型比較 WSAAsyncSelect 模型與 Select 模型的相同點是, 他們都可以對 Windows Socket 應用程式所使用的多個 Socket 進行有效的管理 但 WSAAsyncSelect 模型與 Select 模型相比存在以下不同 WSAAsyncSelect 模型是非同步的 在應用程式中呼叫 WSAAsyncSelect() 函式, 通知系統感興趣的網路事件, 該函式立即返回, 應用程式繼續執行 發生網路事件時, 應用程式得到通知的方式不同 select() 函式返回時, 說明某個或者某些 Socket 滿足可讀可寫的條件, 應用程式需要使用 FD_ISSET 巨集, 判斷 Socket 7-2

是否存在於可讀可寫集合中 而對於 WSAAsyncSelect 模型來說, 當網路事件發生時, 系統向應用程式傳送訊息 WSAAsyncSelect 模型應用在基於訊息的 Windows 環境下, 使用該模型時必須建立視窗 而 Select 模型廣泛應用在 Unix 系統和 Windows 系統, 使用該模型不需要建立視窗 應用程式中呼叫 WSAAsyncSelect() 函式後, 自動將 Socket 設置為非阻塞模式 而應用程式中呼叫 select() 函式後, 並不能改變該 Socket 的工作方式 7.2 WSAAsyncSelect 模型實作 WSAAsyncSelect 模型核心是 WSAAsyncSelect() 函式, 該函式使得 Windows 應用程式能夠接收網路事件訊息 在應用程式視窗程序中對接收到的網路事件進行處理 由於 WSAAsyncSelect 模型應用在基於訊息的 Windows 應用程式中, 所以本節還將講解視窗程序和如何建立視窗等內容 本節內容分為 WSAAsyncSelect() 函式 視窗程序和建立視窗 3 個部分 7.2.1 WSAAsyncSelect() 函式 WSAAsyncSelect() 函式功能是請求當網路事件發生時為 Socket 傳送訊息 該函式原形如下 : int WSAAsyncSelect( SOCKET s, HWND hwnd, u_int wmsg, long levent ) s: 需要事件通知的 Socket hwnd: 當網路事件發生時接收訊息的視窗控制碼 wmsg: 當網路事件發生時視窗收到的訊息 levent: 應用程式感興趣的網路事件集合 當應用程式中呼叫該函式後, 自動將 Socket 設置為非阻塞模式 通常, 應用程式定義的訊息要比 Windows 的 WM_USER 值大, 以避免該訊息與 Windows 預定義訊息發生混淆 7-3

網路事件種類和涵義見如表 7.1 所示 表 7.1 網路事件類型 種類 FD_READ FD_WRITE FD_ACCEPT FD_CONNECT FD_OOB FD_CLOSE FD_QOS FD_GROUP_QOS FD_ROUTING_INTERFACE_CHANGE FD_ADDRESS_LIST_CHANGE 欲接收可讀的通知 欲接收可寫讀的通知 涵義 欲接收等待接受連線的通知 欲接收一次連線或者多點操作完成的通知 欲接收有緩衝區外 (OOB) 資料到達的通知 欲接收 Socket 關閉的通知 欲接收 Socket 服務質量發生變化的通知 欲接收 Socket 組服務質量發生變化的通知 欲在指定方向上, 與路由介面發生變化的通知欲接收針對 Socket 的協定家族, 本機位址列表發生變化的通知 開發人員註冊哪種網路事件, 取決於實際的需要 如果應用程式同時對多個網路事件感興趣 需要對網路事件類型執行按位元 OR( 或 ) 運算 然後將它們分配給 levent 參數 例如, 應用程式希望在 Socket 上接收有關連線完成 資料可讀和 Socket 關閉的網路事件 那麼在應用程式中, 呼叫 WSAAsyncSelect() 函式如下所示 : WSAAsyncSelect(s, hwnd, WM_SOCKET, FD_CONNECT FD_READ FD_CLOSE); 當該 Socket 連線完成 有資料可讀或者 Socket 關閉的網路事件事件發生時, 就會有 WM_SOCKET 訊息傳送給視窗控制碼為 hwnd 的視窗 7.2.2 視窗程序當呼叫 WSAAsyncSelect() 函式後, 應用程式會在 hwnd 視窗控制碼對應的視窗程序, 以訊息形式接收網路事件通知 視窗程序是回呼函式, 當成功建立視窗後由系統呼叫 視窗程序原形如下 : LRESULT CALLBACK WindowProc( HWND hwnd, // 視窗控制碼 UINT umsg, // 訊息 WPARAM wparam, // 訊息參數 LPARAM lparam // 訊息參數 ); 7-4

hwnd: 視窗控制碼 umsg: 訊息 對 Windows Sockets 應用程式來說感興趣的是在 WSAAsyncSelect() 函式中, 由應用程式定義的訊息 wparam: 訊息參數 在 Windows Sockets 應用程式中, 該參數指明發生網路事件的 Socket lparam: 訊息參數 在 Windows Sockets 應用程式中, 該參數低位元組指明已經發生的網路事件 高位元組包含可能出現的錯誤代碼 在 Windows Sockets 應用程式中, 當 WindowProc() 函式接收到網路事件訊息時, 在該函式內執行下面步驟 1. 讀取 lparam 參數高位元組, 判斷是否發生了一個網路錯誤事件 可以使用 WSAGETSELECTERROR 巨集 2. 如果應用程式發現 Socket 上沒有發生任何錯誤, 則讀取 lparam 低位元組, 檢查到底是發生了什麼網路事件 可以使用 WSAGETSELECTEVENT 巨集 WSAGETSELECTERROR 和 WSAGETSELECTEVENT 巨集如下 : #define WSAGETSELECTEVENT(lParam) #define WSAGETSELECTERROR(lParam) LOWORD(lParam) HIWORD(lParam) 7.2.3 建立視窗利用 WSAAsyncSelect() 函式開發 Windows Sockets 應用程式, 依賴於 Windows 視窗 在視窗程序中接收使用者自定義訊息 視窗程序 hwnd 參數為視窗控制碼 控制碼是用來區分各種記憶體物件的唯一標誌, 是個 32 位元整數 視窗控制碼是整個系統唯一的, 是 Windows 系統對一個視窗的標示 透過對控制碼的操作來完成與控制碼對應的系統資源的操作 例如, 應用程式擁有一個視窗控制碼 hwnd, 希望將視窗調整到左上角為 (0,0), 右下角為 (200,200) 的位置 應用程式就可以使用該視窗控制碼為參數呼叫 MoveWindow() 函式, 將該視窗移動到指定位置程式如下 : MoveWindow(hwnd, 0, 0, 200, 200, TRUE); 7-5

在應用程式中, 透過呼叫 Windows API CreateWindow() 函式建立視窗, 取得視窗控制碼 該函式原形如下 : HWND CreateWindow( LPCTSTR lpclassname, LPCTSTR lpwindowname, DWORD dwstyle, int x, int y, int nwidth, int nheight, HWND hwndparent, HMENU hmenu, HANDLE hinstance, LPVOID lpparam ); // 註冊視窗類名的指標 // 視窗的名字 // 視窗風格 // 水平位置 // 垂直位置 // 視窗寬度 // 視窗高度 // 父視窗或視窗的所有者控制碼 // 功能表控制碼或子視窗控制碼 // 應用行程控制碼 // 建立視窗的資料指標 lpclassname: 註冊視窗類名 在呼叫該函式前必須呼叫 RegisterClassEx() 函式註冊視窗類 lpwindowname: 應用程式為該視窗定義的視窗名稱 dwstyle: 指明視窗風格 如 WS_OVERLAPPEDWINDOW 疊層視窗風格 x: 視窗左上角水平位置 y: 視窗左上角垂直位置 nwidth: 視窗寬度 nheight: 視窗高度 hwndparent: 建立視窗的父視窗或視窗所有者的視窗控制碼 hmenu: 功能表控制碼或子視窗控制碼 hinstance: 應用行程控制碼 lpparam: 建立視窗的資料指標 在呼叫 CreateWindow() 函式建立視窗前, 要呼叫 RegisterClassEx() 函式註冊視窗類 該函式原形如下 : ATOM RegisterClassEx( CONST WNDCLASSEX *lpwcx ); // 指向 WNDCLASSEX 結構的指標 如果註冊視窗類成功, 則回傳一個 ATOM 類型值 該值唯一地標示了一個已註冊視窗 類 如果該函式呼叫失敗則回傳值 0 該函式參數為指向 WNDCLASSEX 結構指標 WNDCLASSEX 原形如下 : 7-6

typedef struct_wndclassex UINT cbsize; // 結構大小 UINT style; // 風格 WNDPROC lpfnwndproc; // 視窗程序 int cbclsextra; // 為視窗類結構額外分配的位元組數 int cbwndextra; // 為視窗實體額外分配的位元組數 HANDLE hinstance; // 應用程式的實體控制碼 HICON hicon; // 圖示 HCURSOR hcursor; // 游標控制碼 HBRUSH hbrbackground; // 畫視窗背景的刷子控制碼 LPCTSTR lpszmenuname; // 功能表資源名稱 LPCTSTR lpszclassname; // 視窗類名稱 HICON hiconsm; // 小圖示控制碼 WNDCLASSEX; cbsize:wndclassex 結構的大小 style: 該視窗類的風格 如 CS_HREDRAW 風格 當使用者移動或者調整視窗客戶 區寬度時, 重畫整個視窗 pfnwndproc: 指向視窗程序的指標 cbclsextra: 為視窗類結構額外分配的位元組數 cbwndextra: 為視窗實體額外分配的位元組數 hinstance: 應用程式的實體控制碼 hicon: 該視窗類所用圖示 hcursor: 該視窗類所用的游標控制碼 hbrbackground: 畫視窗背景的刷子控制碼 lpszmenuname: 功能表資源名稱 可以使用 MAKEINTRESOURCE 巨集指定資源 ID 載入功能表資源 lpszclassname: 視窗類名稱 hiconsm: 視窗類的小圖示控制碼 在該結構中 pfnwndproc 欄位為當網路事件發生時, 接收訊息的視窗程序指標 lpszmenuname 欄位與呼叫 CreateWindow 函式的 lpclassname 參數相同 建立 Windows 應用程式時, 需要執行下面幾個步驟 1. 呼叫 RegisterClassEx 函式註冊視窗類, 在註冊視窗類時指明視窗程序 2. CreateWindow() 函式建立視窗 3. 呼叫 ShowWindow() 函式顯示視窗, 呼叫 UpdateWindow() 函式更新視窗 4. 以 GetMessage() 函式回傳值作為 while 迴圈條件, 不斷接收 Windows 訊息 7-7

在迴圈內呼叫 TranslateMessage() 函式和 DispatchMessage() 函式 GetMessage() 函式接收到的訊息被 TranslateMessage() 函式轉譯 該函式對訊息的處理分為兩種情況 一種情況是轉譯後的訊息被投遞到該執行緒訊息佇列中 另一種情況是呼叫 DispatchMessage() 函式將訊息傳送給視窗程序 當 GetMessage() 函式接收到 WM_QUIT 訊息時,while 迴圈結束, 程式退出 下面程式示範如何建立一個 Windows 應用程式 : // 視窗程序 LRESULT CALLBACK MainWndProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam); // 程式入口 int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hprevinstance, LPSTR lpcmdline, int ncmdshow) WNDCLASSEX wcx; // 填充 WNDCLASSEX 結構 wcx.cbsize = sizeof(wcx); wcx.style = CS_HREDRAW CS_VREDRAW; wcx.lpfnwndproc = MainWndProc; wcx.cbclsextra = 0; wcx.cbwndextra = 0; wcx.hinstance = hinstance; wcx.hicon = LoadIcon(NULL,IDI_APPLICATION); wcx.hcursor = LoadCursor(NULL, IDC_ARROW); wcx.hbrbackground = GetStockObject(WHITE_BRUSH); wcx.lpszmenuname = "MainMenu"; wcx.lpszclassname = "MainWClass"; wcx.hiconsm = LoadImage(hinstance, MAKEINTRESOURCE(5), GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR); // 風格 // 視窗程序 // 應用程式實體控制碼 // 加載圖示資源 // 加載游標資源 // 取得筆刷 // 功能表名稱 // 視窗類名稱 // 小圖示 // 註冊視窗類 RegisterClassEx(&wcx); // 建立視窗 hwnd = CreateWindow( "MainWClass", "Sample", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, (HWND) NULL, (HMENU) NULL, hinstance, (LPVOID) NULL); // 視窗類名稱 // 視窗名稱 // 疊層視窗 // 預設水平位置 // 預設垂直位置 // 預設寬度 // 預設高度 // 沒有父視窗 // 使用視窗類功能表 // 應用程式實體控制碼 // NULL 7-8

if (!hwnd) return FALSE; // 顯示視窗 ShowWindow(hwnd, ncmdshow); UpdateWindow(hwnd); // 訊息迴圈 while (GetMessage(&msg, (HWND) NULL, 0, 0)) TranslateMessage(&msg); DispatchMessage(&msg); return msg.wparam; 視窗程序範例程式如下 : LRESULT CALLBACK MainWndProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam) switch(umsg) case WM_PAINT: // 處理訊息 return 0; case WM_DESTROY: // 處理訊息 return 0; // 處理訊息 return (DefWindowProc(hwnd, umsg, wparam, lparam));// 預設訊息處理 7.3 WSAAsyncSelect 模型範例程式 下面講解一個伺服器程式 該程式是 Win32 Application 在該程式中使用 WSAAsync Select 模型管理接受的客戶端 Socket 該程式是範例程式, 忽略了許多細節 程式設計如圖 7.2 所示, 按照下面步驟編碼 1. 定義自定義訊息 在程式中定義自定義訊息 WM_SOCKET 2. 定義視窗程序 3. 呼叫 MyRegisterClass() 函式註冊視窗類 7-9

4. 呼叫 InitInstance() 函式建立並顯示視窗 因為 WSAAsyncSelect() 函式的第一個參數 是視窗控制碼, 所以要在呼叫該函式之前建立視窗 5. 初始化 Socket DLL, 建立 Socket 6. 呼叫 WSAAsyncSelect() 函式註冊感興趣網路事件 該範例程式中, 伺服器監聽 Socket 感興趣的網路事件有 FD_ACCEPT 和 FD_CLOSE 7. 綁定 Socket, 開始監聽 8. 訊息迴圈 9. 釋放 Socket 和之前配置的其他資源 註冊視窗類 建立視窗 顯示視窗 建立 Socket WSAAsyncSelect 綁定 Socket 監聽 訊息迴圈 訊息 視窗程序 退出 圖 7.2 WSAAsyncSelect 模型範例程式 7.3.1 定義自定義訊息 在應用程式中, 通常要定義一個比 WM_USER 值要大的自定義訊息, 以免與 Windows 定義的訊息衝突 除了定義這些訊息外, 在範例程式中還要定義最大字串長度 伺服器埠 資料緩衝區長度 7-10

程式如下 : #define MAX_LOADSTRING 100 // 最大字串長度 #define WM_SOCKET WM_USER+1 //Socket 訊息 #define PORT 5150 // 伺服器埠 #define MAX_SIZE_BUF 1024 // 資料緩衝區長度 7.3.2 定義視窗程序 視窗程序是由 Windows 系統呼叫的函式, 通常將該函式的定義放在主函式之後, 將宣告 放在主函式之前 在範例程式中為了使主程序結構清晰, 將註冊視窗類 建立和顯示視窗的過程都設計為函式, 並提前定義 定義 HandleSocketMsg() 函式用於對 Windows 網路事件訊息進行處理 程式如下 : ATOM MyRegisterClass(HINSTANCE hinstance); // 註冊視窗 BOOL InitInstance(HINSTANCE, int); // 初始化實體 LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // 視窗程序 void HandleSocketMsg(WPARAM wparam, LPARAM lparam); // 處理 WM_SOCKET 訊 息 7.3.3 註冊視窗類 呼叫 MyRegisterClass() 函式註冊視窗類 // 註冊視窗類 MyRegisterClass(hInstance); 7.3.4 建立和顯示視窗 呼叫 InitInstance () 函式建立 顯示視窗 此時, 視窗程序開始接收 Windows 訊息 // 建立視窗, 顯示視窗 if (!InitInstance (hinstance, ncmdshow)) return FALSE; 7.3.5 建立 Socket 呼叫 WSAStartup() 函式初始化 Socket DLL, 呼叫 socket() 函式建立 Socket 7-11

int Ret; // 回傳值 WSADATA wsadata; //WSADATA 結構變數 // 請求 Winodows Sockets 2.2 版本 if ((Ret = WSAStartup(0x0202, &wsadata))!= 0) return 0; // 建立 Socket if ((slisten = socket (PF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) return 0; 7.3.6 註冊感興趣的網路事件以視窗控制碼 hwnd 和 WM_SOCKET 為第 2 第 3 個參數呼叫 WSAAsyncSelect() 函式 同時註冊 FD_ACCEPT 和 FD_CLOSE 網路事件 請求系統當 FD_ACCEPT 和 FD_CLOSE 網路事件發生時, 給 hwnd 視窗傳送 WM_SOCKET 訊息 WSAAsyncSelect(sListen, hwnd, WM_SOCKET, FD_ACCEPT FD_CLOSE); 7.3.7 綁定 Socket 呼叫 bind() 函式綁定 Socket SOCKADDR_IN InternetAddr; InternetAddr.sin_family = AF_INET; InternetAddr.sin_addr.s_addr = htonl(inaddr_any); InternetAddr.sin_port = htons(port); // 綁定 Socket if (bind(slisten, (PSOCKADDR) &InternetAddr, sizeof(internetaddr)) == SOCKET_ERROR) return 0; 7.3.8 開始監聽 呼叫 listen() 函式 Socket 開始監聽 // 監聽 if (listen(slisten, SOMAXCONN)) return 0; 7-12

7.3.9 訊息迴圈在 while 迴圈語句中,GetMessage() 函式不斷從執行緒訊息佇列中取出訊息 當 FD_ACCEPT 或者 FD_CLOSE 網路事件發生時,WM_SOCKET 訊息被投遞到執行緒訊息佇列中,GetMessage() 函式負責將該訊息從執行緒訊息佇列中取出,DispatchMessage() 函式再將該訊息傳送到視窗程序 // 主訊息迴圈 MSG msg; while (GetMessage(&msg, NULL, 0, 0)) TranslateMessage(&msg); DispatchMessage(&msg); 7.3.10 程式退出 當 GetMessage() 函式接收到 WM_QUIT 訊息時,while 迴圈結束, 釋放資源, 程式退出 closesocket(slisten); WSACleanup(); DeleteAllNode(); // 釋放 Socket 資源 // 釋放節點空間 7.3.11 視窗程序當建立視窗成功後 WndProc() 視窗程序便開始接收 Windows 訊息 在該函式中需要處理許多訊息 例如, 當關閉視窗時傳送 WM_DESTROY 訊息, 在視窗程序中呼叫 PostQuit Message() 函式向執行緒訊息佇列投遞 WM_QUIT 訊息 GetMessage() 函式接收到該訊息後, 程式退出 應用程式不感興趣的訊息交給 DefWindowProc() 函式處理 當 FD_ACCEPT 或者 FD_CLOSE 網路事件發生時, 視窗程序接收到 WM_SOCKET 訊息 在視窗程序中呼叫 HandleSocketMsg() 函式對觸發 WM_SOCKET 訊息的網路事件進行處理 該範例的視窗程序程式如下 : // 視窗程序 LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) switch (message) 7-13

case WM_SOCKET: // 網路事件發生時傳送給該視窗的訊息 HandleSocketMsg(wParam, lparam); // 處理該訊息 break; case WM_PAINT: // 畫客戶區 // break; case WM_DESTROY: // 程式退出 PostQuitMessage(0); break; // 訊息處理 default: return DefWindowProc(hWnd, message, wparam, lparam); return 0; 7.3.12 CClient 類別在程式中定義 CClient 類別管理伺服器接受客戶端的新建 Socket 該類別建構函式的參數為伺服器接受客戶端的新建 Socket 在解構函式中將該 Socket 關閉 在該類別中宣告 RecvData() 函式接收資料,SendData() 函式傳送資料,GetSocket() 函式回傳 Socket CClient 類別定義如下 : #define MAX_SIZE_BUF 1024 // 資料緩衝區長度 class CClient public: CClient(SOCKET s); // 建構函式 virtual ~CClient(); // 解構函式 public: void RecvData(void); // 接收資料 void SendData(void); // 傳送資料 SOCKET GetSocket(void); // 取得 Socket private: SOCKET m_s; //Socket char m_recvbuf[max_size_buf]; // 接收資料緩衝區 char m_sendbuf[max_size_buf]; // 傳送資料緩衝區 ; 7-14

7.3.13 管理客戶端 Socket 的鏈表 定義 _socktnode 結構 該結構 pclient 欄位為 CClient 類別指標 pnext 變數為指向下一個 節點的指標 該結構定義如下 : typedef struct _socktnode CClient *pclient; //CClient 類別指標 _socktnode *pnext; // 指向下一個節點 SOCKETNODE, *PSOCKETNODE; 當伺服器接受一個客戶端連線請求後, 建立一個 CClient 實體, 新建一個 SOCKETNODE 節點 將實體指標賦值給 SOCKETNODE 結構的 pclient 變數 為了對鏈表進行操作, 宣告如下函式 AddNode() 函式 : 新增節點 DeleteNode() 函式 : 刪除節點 GetClient() 函式 : 取得 CClient 類別指標 DeleteAllNode() 函式 : 刪除所有節點 PSOCKETNODE HeaderSocktNode = NULL; // 節點頭 void AddNode(SOCKET s); // 增加結點 void DeleteNode(SOCKET s); // 刪除結點 void DeleteAllNode(void); // 刪除所有結點 CClient*GetClient(SOCKET s); //CClient 類別指標 7.3.14 網路事件訊息處理函式在 HandleSocketMsg() 函式中呼叫 WSAGETSELECTERROR 巨集檢查是否有網路錯誤事件發生 如果有網路錯誤事件發生則呼叫 DeleteNode() 函式將該 Socket 從鏈表中刪除 在前面的講解中, 已經知道 wparam 參數為發生網路事件的 Socket 所以, 以 wparam 為參數呼叫 DeleteNode() 函式 如果沒有網路錯誤事件發生, 則呼叫 WSAGETSELECTEVENT 巨集, 檢查發生了什麼網路事件 如果網路事件為 FD_ACCEPT, 那麼說明此時客戶端等待伺服器接受連線請求 發生這個網路事件的 Socket 一定是伺服器的監聽 Socket 呼叫 accept() 函式接受客戶端的連線請求, 將該 Socket 加入鏈表中, 然後以該新建 Socket 作為參數呼叫 WSAAsyncSelect() 函式, 為該 Socket 請求 FD_READ FD_WRITE 和 FD_CLOSE 網路事件 7-15

當 HandleSocketMsg() 函式接收到 FD_READ 網路事件時, 說明此時在伺服器接受的客戶 端 Socket 中, 某個 Socket 上存在可讀的資料 這個 Socket 就是 wparam 參數值 呼叫 GetClient() 函式得到儲存該 Socket 的 CClient 類別指標 呼叫該類別的 RecvData() 函式接 收客戶端的資料 HandleSocketMsg() 函式接收到 FD_WRITE 網路事件時的處理方法, 同收到 FD_READ 網 路事件時的處理方法相似 當該函式接收到 FD_CLOSE 網路事件時, 說明此時客戶端關閉了 Socket, 可以呼叫 DeleteNode() 函式刪除該客戶端節點 HandleSocketMsg() 函式範例程式如下 : //WM_SOCKET 訊息處理 void HandleSocketMsg(WPARAM wparam, LPARAM lparam) if (WSAGETSELECTERROR(lParam)) // 檢查網路錯誤 DeleteNode(wParam); else switch(wsagetselectevent(lparam)) case FD_ACCEPT: // 刪除節點, 關閉 Socket // 檢查網路事件 // 接受客戶端連線請求 SOCKET saccept; if ((saccept = accept(wparam, NULL, NULL)) == INVALID_SOCKET) break; AddNode(sAccept); // 將 Socket 加入鏈表中 //FD_READ FD_WRITE 和 FD_CLOSE 網路事件發生時, 傳送 WM_SOCKET 訊息 WSAAsyncSelect(sAccept, hwnd, WM_SOCKET, FD_READ FD_WRITE FD_CLOSE); break; case FD_READ: // 接收資料 CClient* pclient = GetClient(wParam); // 根據 Socket, 取得客戶端節點 pclient->recvdata(); // 接收資料 break; case FD_WRITE: // 傳送資料 CClient* pclient = GetClient(wParam); // 根據 Socket, 取得客戶端節點 pclient->senddata(); // 傳送資料 break; case FD_CLOSE: // 對方關閉了 Socket 連線 7-16

return; DeleteNode(wParam); break; // 刪除節點 7.4 呼叫 WSAAsyncSelect() 函式注意問題 使用 WSAAsyncSelect() 函式開發 Windows Sockets 應用程式中, 開發人員需要注意以下問題 7.4.1 接收不到網路事件第一種情況是由於在同一個 Socket 同一個自定義訊息上, 多次呼叫 WSAAsyncSelect() 函式註冊不同的網路事件, 最後一次函式呼叫取消了前面註冊的網路事件 例如, 在應用程式中, 第一次呼叫 WSAAsyncSelect() 函式註冊 FD_READ 網路事件, 然後又呼叫該函式註冊 FD_WRITE 網路事件, 那麼此時應用程式, 就只能接收到 FD_WRITE 網路事件 如果要取消所有請求的網路事件通知, 告知 Windows Sockets 實作不再為該 Socket 傳送任何網路事件相關的訊息, 要以參數 levent 值為 0 呼叫 WSAAsyncSelect() 函式 WSAAsyncSelect(s, hwnd, 0, 0); 需要注意儘管應用程式呼叫上述函式取消了網路事件通知, 但是在應用程式訊息佇列中, 可能還有網路訊息在排隊 所以在呼叫 WSAAsyncSelect() 函式取消網路事件訊息後, 應用程式還應該繼續準備接收網路事件 第二種情況是在同一個 Socket 上, 多次呼叫 WSAAsyncSelect() 函式, 為不同的網路事件定義了不同的訊息, 最後一次該函式呼叫將取消前面註冊的網路事件 下面的程式碼中, 第二次函式呼叫將會取消第一次函式呼叫的作用 只有 FD_WRITE 網路事件透過 wmsg2 訊息通知到視窗 WSAAsyncSelect(s, hwnd, wmsg1, FD_READ); WSAAsyncSelect(s, hwnd, wmsg2, FD_WRITE); 7-17

7.4.2 關於 accept() 函式因為呼叫 accept() 函式接受的 Socket 和監聽 Socket 具有同樣的屬性 所以, 任何為監聽 Socket 設置的網路事件對接受的 Socket 同樣起作用 如果一個監聽 Socket 請求 FD_ACCEPT FD_READ 和 FD_WRITE 網路事件, 則在該監聽 Socket 上接受的任何 Socket 也會請求 FD_ ACCEPT FD_READ 和 FD_WRITE 網路事件, 以及傳送同樣的訊息 若需要不同的訊息和網路事件, 應用程式應該呼叫 WSAAsyncSelect() 函式, 為該 Socket 請求不同的網路事件和訊息 7.4.3 關於 FD_READ 網路事件一個 FD_READ 網路事件不要多次呼叫 recv() 函式 如果應用程式為一個 FD_READ 網路事件, 呼叫了多個 recv() 函式, 會使得該應用程式接收到多個 FD_READ 網路事件 如果在一次接收 FD_READ 網路事件時需要呼叫多次 recv() 函式, 應用程式應該在呼叫 recv() 函式之前關閉 FD_READ 訊息 應用程式不必在收到 FD_READ 訊息時, 讀進所有可讀的資料 每接收到一次 FD_READ 網路事件, 應用程式呼叫一次 recv() 函式是恰當的 7.4.4 如何判斷 Socket 已經關閉 要使用 FD_CLOSE 網路事件來判斷 Socket 是否已經關閉 接收 FD_CLOSE 網路事件時, 錯誤代碼指示出 Socket 是緩衝關閉還是強制關閉 如果錯誤代碼 0, 則為緩衝關閉 ; 若錯誤代碼為 WSAECONNRESET, 則 Socket 是強制關閉 如果 Socket 緩衝關閉, 資料已經都全部接收, 應用程式只會收到 FD_CLOSE 訊息來指出虛擬電路 (Virtual Circuit) 關閉, 它不會收到 FD_READ 訊息來表明這種狀況 呼叫 closesocket() 函式後不會投遞 FD_CLOSE 網路事件 7-18