基于消息的事件驅(qū)動(dòng)機(jī)制(Message Based, Event Driven)
本文轉(zhuǎn)載自微信公眾號(hào)「一個(gè)程序員的修煉之路」,作者地下潛行者。轉(zhuǎn)載本文請(qǐng)聯(lián)系一個(gè)程序員的修煉之路公眾號(hào)。
1. 基本模型概述
基于消息的事件驅(qū)動(dòng)機(jī)制是一個(gè)通用模型,廣泛應(yīng)用于桌面軟件開(kāi)發(fā)、網(wǎng)絡(luò)應(yīng)用程序開(kāi)發(fā)、前端開(kāi)發(fā)等技術(shù)方向中。本文主要描述基本模型、基本框架,用于說(shuō)明不同技術(shù)的共性知識(shí)??梢岳斫鉃橥獠坎僮魇录?,被轉(zhuǎn)化為消息存放于隊(duì)列中;而每種類(lèi)型的消息都有對(duì)應(yīng)的處理;通過(guò)消息循環(huán),完成讀消息、調(diào)用消息處理這個(gè)過(guò)程。這個(gè)過(guò)程,只要應(yīng)用不退出,會(huì)一直進(jìn)行下去。下圖的模型從Windows應(yīng)用程序而來(lái),但是具有一定的通用性。
2. 模型在MFC程序中的應(yīng)用
MFC(Microsoft Foundation Classes)是微軟的基礎(chǔ)類(lèi)庫(kù),對(duì)大部分的Windows API進(jìn)行封裝,同時(shí)也是桌面軟件的UI開(kāi)發(fā)框架,下圖是一個(gè)用VS2019自動(dòng)生成的MFC多文檔應(yīng)用。不用做任何開(kāi)發(fā)工作,就可以得到一個(gè)自帶菜單欄、工具欄、狀態(tài)欄、屬性展示框等豐富的界面框架。不過(guò)現(xiàn)在MFC已經(jīng)沒(méi)落,除了歷史項(xiàng)目,已經(jīng)很少有新項(xiàng)目,采用MFC。下文會(huì)基于鼠標(biāo)點(diǎn)擊后完整的系統(tǒng)響應(yīng)過(guò)程,說(shuō)明該模型在MFC中的體現(xiàn)。
2.1 從鼠標(biāo)點(diǎn)擊到響應(yīng)處理的完整過(guò)程
1.用戶點(diǎn)擊鼠標(biāo);
2.鼠標(biāo)驅(qū)動(dòng)產(chǎn)生鼠標(biāo)點(diǎn)擊消息(通過(guò)中斷實(shí)現(xiàn)),進(jìn)行系統(tǒng)消息隊(duì)列;
3.系統(tǒng)消息轉(zhuǎn)換為應(yīng)用程序消息,放入應(yīng)用程序隊(duì)列;
4.消息泵從應(yīng)用程序消息隊(duì)列中讀取消息;
5.消息派發(fā)及處理,借助USER模塊,將消息派發(fā)至對(duì)應(yīng)窗口的對(duì)應(yīng)消息處理函數(shù);
問(wèn)題:為什么消息處理函數(shù)中不能做長(zhǎng)耗時(shí)的任務(wù)?
消息泵處理消息時(shí)是依次處理,處理完一條消息后,再處理下一條消息。如果當(dāng)前消息的處理事件過(guò)長(zhǎng),會(huì)導(dǎo)致后續(xù)的消息無(wú)法得到及時(shí)響應(yīng),會(huì)導(dǎo)致界面卡頓等非常不佳的用戶體驗(yàn)。
2.2 事件類(lèi)型
1) 鼠標(biāo)點(diǎn)擊(單擊、雙擊、右擊)
2) 鍵盤(pán)按鍵
3) 用戶在觸摸屏上的點(diǎn)擊事件
4) …
用戶在電腦上的各種操作,對(duì)應(yīng)到各種事件類(lèi)型、不同的事件類(lèi)型,會(huì)被轉(zhuǎn)換為不同的消息。
2.3 消息定義
用戶操作事件,會(huì)被轉(zhuǎn)化為消息。消息定義如下:
- /*
- * Message structure
- */
- typedef struct tagMSG {
- HWND hwnd; //接受消息的窗口句柄
- UINT message; //消息常量標(biāo)識(shí)符(消息號(hào))
- WPARAM wParam; //32位消息特定附加信息
- LPARAM lParam; //32位消息特定附加信息
- DWORD time; //消息創(chuàng)建時(shí)的時(shí)間
- POINT pt; //消息創(chuàng)建時(shí)的光標(biāo)位置
- #ifdef _MAC
- DWORD lPrivate;
- #endif
- } MSG
微軟有提供一系列的消息定義,用戶也可以自定義消息,進(jìn)行應(yīng)用程序的開(kāi)發(fā)。
windows 消息類(lèi)型可以分為以下兩大類(lèi):
(1)系統(tǒng)消息:范圍在[0x0000,0x03ff]之間,細(xì)分為三小類(lèi):
- 窗口消息:與窗口運(yùn)作有關(guān),窗口創(chuàng)建,窗口繪制,窗口移動(dòng),窗口銷(xiāo)毀;
- 命令消息:一般指WM_COMMAND消息,與處理用戶請(qǐng)求有關(guān),通常由控件或者菜單產(chǎn)生。
- 通知消息:特指WM_NOTIFY消息。通常指一個(gè)窗口內(nèi)的子控件發(fā)生了一些事情,需要通知父窗口。
微軟官方鏈接,給出了系統(tǒng)消息的范圍:
The system reserves message-identifier values in the range 0x0000 through 0x03FF (the value of WM_USER – 1) for system-defined messages. Applications cannot use these values for private messages.
(2)應(yīng)用定義的消息
- WM_USER : 【0X0400-0X7FFF】, 用戶自定義的消息范圍。
- WM_APP : 【0X8000-0XBFFF】,用于程序之間的消息通信。
- RegisterWindowMessage :【0XC000-0XFFFF】
微軟官方內(nèi)容,給出了應(yīng)用消息的取值范圍:
Values in the range 0x0400 (the value of WM_USER) through 0x7FFF are available for message identifiers for private window classes.
If your application is marked version 4.0, you can use message-identifier values in the range 0x8000 (WM_APP) through 0xBFFF for private messages.
The system returns a message identifier in the range 0xC000 through 0xFFFF when an application calls the RegisterWindowMessage function to register a message. The message identifier returned by this function is guaranteed to be unique throughout the system. Use of this function prevents conflicts that can arise if other applications use the same message identifier for different purposes.
2.4 消息處理映射表(事件處理綁定)
消息處理映射表指每個(gè)消息對(duì)應(yīng)的處理函數(shù)。只有先做好映射表,當(dāng)消息到達(dá)時(shí),消息泵才知道怎么處理該消息。
2.4.1 Win32應(yīng)用程序中的消息處理映射表
WndProc為消息處理函數(shù),代碼內(nèi)部通過(guò)switch case,給不同的消息指定不同的處理函數(shù)。
- LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
- {
- switch (message)
- {
- case WM_COMMAND:
- {
- int wmId = LOWORD(wParam);
- // 分析菜單選擇:
- switch (wmId)
- {
- case IDM_ABOUT:
- DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
- break;
- case IDM_EXIT:
- DestroyWindow(hWnd);
- break;
- default:
- return DefWindowProc(hWnd, message, wParam, lParam);
- }
- }
- break;
- case WM_PAINT:
- {
- PAINTSTRUCT ps;
- HDC hdc = BeginPaint(hWnd, &ps);
- // TODO: 在此處添加使用 hdc 的任何繪圖代碼...
- EndPaint(hWnd, &ps);
- }
- break;
- case WM_DESTROY:
- PostQuitMessage(0);
- break;
- default:
- return DefWindowProc(hWnd, message, wParam, lParam);
- }
- return 0;
- }
2.4.2 MFC中的消息處理映射表
在如下代碼中可以看到,WINDOWS消息WM_CREATE,對(duì)應(yīng)的消息處理函數(shù)為OnCreate.當(dāng)消息到達(dá)時(shí),消息泵知道去調(diào)用OnCreate函數(shù)。
宏BEGIN_MESSAGE_MAP,END_MESSAGE_MAP就是用于定義消息映射表的。
- BEGIN_MESSAGE_MAP(CFileView, CDockablePane)
- ON_WM_CREATE()
- ...
- END_MESSAGE_MAP()
- #define ON_WM_CREATE() \
- { WM_CREATE, 0, 0, 0, AfxSig_is, \
- (AFX_PMSG) (AFX_PMSGW) \
- (static_cast< int (AFX_MSG_CALL CWnd::*)(LPCREATESTRUCT) > ( &ThisClass :: OnCreate)) },
2.5 消息泵(Windows應(yīng)用程序)
消息泵負(fù)責(zé)從應(yīng)用程序的消息隊(duì)列中讀取消息、轉(zhuǎn)換消息、派發(fā)消息。
- MSG msg;
- // 主消息循環(huán):
- while (GetMessage(&msg, nullptr, 0, 0))
- {
- if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
- {
- TranslateMessage(&msg);
- DispatchMessage(&msg);
- }
- }
以上出現(xiàn)的函數(shù)都是Windows API 函數(shù)
- GetMessage 從消息隊(duì)列中讀取消息
- TranslateMessage 消息翻譯、轉(zhuǎn)換。
- DispatchMessage 派發(fā)消息、找到消息對(duì)應(yīng)的窗口、調(diào)用響應(yīng)函數(shù)
2.6 消息隊(duì)列
(1)系統(tǒng)消息隊(duì)列:這是系統(tǒng)唯一隊(duì)列,設(shè)備驅(qū)動(dòng)把用戶的操作輸入轉(zhuǎn)化成消息存放于系統(tǒng)隊(duì)列中,然后系統(tǒng)會(huì)把此消息放到目標(biāo)窗口所在的線程消息隊(duì)列中等待處理。
(2)線程消息隊(duì)列:每一個(gè)GUI線程都會(huì)維護(hù)一個(gè)線程消息隊(duì)列,然后線程消息隊(duì)列中的消息會(huì)被送到相應(yīng)的窗口過(guò)程處理。
消息隊(duì)列并不可以直接訪問(wèn),但是我們可以通過(guò)指定接口去訪問(wèn)消息隊(duì)列。
- PostMessage函數(shù),用于向消息隊(duì)列中追加消息,并立即返回;
- GetMessage函數(shù),用于從消息隊(duì)列中讀取消息;
2.7 Windows消息攔截機(jī)制
上文介紹Windows消息的產(chǎn)生、讀取、派發(fā)處理等,其實(shí)用戶可以通過(guò)Windows的消息攔截機(jī)制,對(duì)消息到達(dá)目標(biāo)窗體之前進(jìn)行提前處理。這主要通過(guò)Windows的Hook機(jī)制實(shí)現(xiàn)。常用的調(diào)試工具SPY++,就是利用HOOK機(jī)制截獲窗口消息。
此處只做介紹,不做詳細(xì)深入。
2.8 模態(tài)對(duì)話框和非模態(tài)對(duì)話框的區(qū)別
模態(tài)對(duì)話框:在子界面活動(dòng)期間,父窗口是無(wú)法進(jìn)行消息響應(yīng)。獨(dú)占用戶輸入
- 非模態(tài)對(duì)話框:各窗口之間不影響。
- 模態(tài)對(duì)話框通過(guò)在消息循環(huán)內(nèi)再造消息循環(huán)。如果當(dāng)前窗口內(nèi)的消息循環(huán)不退出,父窗口的消息循環(huán)將無(wú)法運(yùn)轉(zhuǎn),也即無(wú)法響應(yīng)。從而產(chǎn)生模態(tài)對(duì)話框獨(dú)占響應(yīng)的效果。
3. 模型在瀏覽器中的應(yīng)用
在網(wǎng)頁(yè)應(yīng)用程序開(kāi)發(fā)中(前端開(kāi)發(fā)),用戶的點(diǎn)擊操作產(chǎn)生事件,同時(shí)在網(wǎng)頁(yè)應(yīng)用程序中進(jìn)行處理響應(yīng)。瀏覽器應(yīng)用,同樣適用于該模型。
3.1 事件類(lèi)型
1)用戶在某個(gè)元素上點(diǎn)擊鼠標(biāo)或懸停光標(biāo)。
2)用戶在鍵盤(pán)中按下某個(gè)按鍵。
3)用戶調(diào)整瀏覽器的大小或者關(guān)閉瀏覽器窗口。
4)提交表單。
5)…
完整的瀏覽器事件清單,可以參考如下鏈接:
https://developer.mozilla.org/en-US/docs/Web/Events
3.2 事件綁定
在如下示例中,對(duì)HTML的DOM元素中進(jìn)行事件綁定,增加了click事件響應(yīng)。當(dāng)用戶點(diǎn)擊該div的時(shí)候,響應(yīng)函數(shù)就會(huì)執(zhí)行。瀏覽器中有多種事件綁定方式,此處只用addEventListener,作為示例說(shuō)明。
3.3 事件傳播
用戶在點(diǎn)擊div后,事件會(huì)按照 捕獲階段、目標(biāo)階段、冒泡階段的過(guò)程進(jìn)行處理。用戶可以通過(guò)addEventListener中useCapture字段,決定事件的捕獲階段。
- true - 捕獲階段執(zhí)行事件響應(yīng)函數(shù)
- false- 冒泡階段執(zhí)行事件響應(yīng)函數(shù)
3.4 事件循環(huán)
事件循環(huán)之所以稱(chēng)之為事件循環(huán),是因?yàn)樗?jīng)常按照類(lèi)似如下的方式來(lái)被實(shí)現(xiàn):
- while (queue.waitForMessage()) {
- queue.processNextMessage();
- }
queue.waitForMessage() 會(huì)同步地等待消息到達(dá)(如果當(dāng)前沒(méi)有任何消息等待被處理)。
該段內(nèi)容來(lái)自于鏈接:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop
3.5 任務(wù)隊(duì)列
Javascript腳本的執(zhí)行環(huán)境是單線程的,所以必定存在一個(gè)任務(wù)隊(duì)列用于依次存放待響應(yīng)任務(wù)。即3.4章節(jié)中的queue.
4. 模型在網(wǎng)絡(luò)應(yīng)用程序中的應(yīng)用
4.1 點(diǎn)對(duì)點(diǎn)的網(wǎng)絡(luò)應(yīng)用程序工作過(guò)程
一個(gè)服務(wù)端角色,一個(gè)客戶端角色的兩個(gè)進(jìn)程之間建立通信的完成過(guò)程,如下文所述。
4.1.1 服務(wù)端
1)創(chuàng)建SOCKET;
2)綁定IP:Port;
3)SOCKET進(jìn)入監(jiān)聽(tīng)模式;
4)等待外部連接請(qǐng)求進(jìn)入,如果有,建立連接;
5)數(shù)據(jù)讀寫(xiě)處理;
6)處理結(jié)束,關(guān)閉連接。
4.1.2 客戶端
1)創(chuàng)建SOCKET;
2)向指定的IP:Port發(fā)起連接請(qǐng)求,并建立連接;
3)發(fā)送數(shù)據(jù)/接收數(shù)據(jù);
4)處理結(jié)束,關(guān)閉連接。
問(wèn)題:當(dāng)一臺(tái)機(jī)器有10W,乃至更多的并發(fā)網(wǎng)絡(luò)連接,如何處理?
一個(gè)線程處理一個(gè)SOCKET連接?(大量的線程,會(huì)導(dǎo)致CPU資源花在線程切換上,而不是真正的有效工作)
通過(guò)SELECT周期性輪詢所有SOCKET,檢查是否可讀、可寫(xiě)?(主動(dòng)遍歷所有SOCKET集合,當(dāng)SOCKET基數(shù)特別大、活躍量少的時(shí)候,低效。SELECT本身也有數(shù)量限制)
通過(guò)事件通知,只處理活躍的局部少量SOCKET (參考CPU中斷處理、高效)
4.2事件清單
網(wǎng)絡(luò)應(yīng)用程序中存在一些基本的事件以及圍繞這些事件開(kāi)展的處理。在陳碩的書(shū)籍《Linux多線程服務(wù)器端編程》有介紹三個(gè)半事件。
1)連接建立,包含服務(wù)端接收新連接、客戶端發(fā)起連接;
2)連接斷開(kāi),包括主動(dòng)斷開(kāi)、被動(dòng)斷開(kāi);阿
3)消息到達(dá),表示有數(shù)據(jù)到緩沖區(qū),可以讀,拷貝到用戶自己控制的緩沖區(qū)中;
4)消息發(fā)送完畢,算半個(gè)事件。
開(kāi)發(fā)人員應(yīng)針對(duì)指定事件,開(kāi)發(fā)對(duì)應(yīng)的處理函數(shù),并通過(guò)引擎完成事件處理。
4.3 事件處理引擎
目前操作系統(tǒng)層面提供了高效的網(wǎng)絡(luò)通信處理機(jī)制,不同的語(yǔ)言也提供了各種類(lèi)庫(kù)。
4.3.1 操作系統(tǒng)層支持
1)Windows IOCP
2)CentOS Epoll
3)xxxBSD kqueue
4.3.2 語(yǔ)言層面的框架支持
1)C/C++ libevent/Muduo/Asio/…
2)Java Netty
3)DotNet DotNetty
4.3.3 Epoll機(jī)制說(shuō)明
1)創(chuàng)建Epoll實(shí)例句柄:可以理解為管理其他socket的領(lǐng)頭羊;
2)事件注冊(cè):為每個(gè)SOCKET要關(guān)注的事件進(jìn)行注冊(cè),服務(wù)端監(jiān)聽(tīng)SOCKET
主要關(guān)注有沒(méi)有新的連接進(jìn)來(lái);
一般性SOCKET關(guān)注是否有數(shù)據(jù)進(jìn)來(lái),需要讀取;
超時(shí),事件處理;
…
3)進(jìn)入等待狀態(tài),有事件進(jìn)來(lái)時(shí),操作系統(tǒng)會(huì)進(jìn)行通知;
4)事件處理,根據(jù)操作系統(tǒng)的通知,應(yīng)用程序進(jìn)行反饋,調(diào)用對(duì)應(yīng)事件的處理函數(shù)進(jìn)行響應(yīng)。
由于操作系統(tǒng)層面的支持,系統(tǒng)反饋時(shí),只對(duì)活躍的SOCKET進(jìn)行處理,數(shù)據(jù)量少,檢查量少,處理量也少。因此可以處理大量socket并發(fā)。
能夠這么做,是因?yàn)榫W(wǎng)絡(luò)應(yīng)用程序進(jìn)行數(shù)據(jù)收發(fā),必然存在網(wǎng)絡(luò)延遲,所以才可以這么處理。如果每個(gè)SOCKET都是滿負(fù)荷運(yùn)作,那么這種機(jī)制也不
能用于大量的連接處理。
4.3.4 Muduo網(wǎng)絡(luò)庫(kù)說(shuō)明
Muduo是由陳碩編寫(xiě)的,基于Epoll,采用Reactor模式開(kāi)發(fā)的開(kāi)源網(wǎng)絡(luò)通信庫(kù)。
Reactor模式稱(chēng)為反應(yīng)堆模型,是指有一個(gè)循環(huán)的過(guò)程,不斷監(jiān)聽(tīng)對(duì)應(yīng)事件是否觸發(fā),事件觸發(fā)時(shí)調(diào)用對(duì)應(yīng)的 callback 進(jìn)行處理。
如下圖所示:
所有的客戶端連接請(qǐng)求事件都由acceptor處理,并建立新的連接;
所有已建立的連接,按照讀數(shù)據(jù)、解碼、處理、編碼、數(shù)據(jù)發(fā)送返回的過(guò)程進(jìn)行處理。其中數(shù)據(jù)讀寫(xiě),由反應(yīng)堆根據(jù)事件進(jìn)行處理。
Muduo的詳細(xì)說(shuō)明,可以參考如下文檔:
https://www.cyhone.com/articles/analysis-of-muduo/
4.3.5 基于Muduo的網(wǎng)絡(luò)應(yīng)用程序開(kāi)發(fā)模式
1)建立一個(gè)事件循環(huán)器EventLoop(也可以理解為消息泵)
2)建立對(duì)應(yīng)的服務(wù)器TcpServer
3)設(shè)置TcpServer的Callback(可以理解為建立事件處理映射表)
4)啟動(dòng)server
5)開(kāi)啟事件循環(huán),進(jìn)行事件處理。
此處的消息隊(duì)列,可以理解為由操作系統(tǒng)返回的待處理SOCKET及其對(duì)應(yīng)事件的清單。
5. 總結(jié)
通過(guò)上文可以看出,在不同的技術(shù)方向上,其實(shí)是可以挖掘出通性技術(shù),并進(jìn)行學(xué)習(xí)的。因此我做了如下歸納:
1)不同技術(shù),采用類(lèi)似設(shè)計(jì)思路
2)研究共性,便于知識(shí)觸類(lèi)旁通
3)細(xì)節(jié)差異,通過(guò)工程實(shí)踐掌握
6. 參考資料
1. 微軟官方關(guān)于消息及其隊(duì)列的介紹: https://docs.microsoft.com/en-us/windows/win32/winmsg/about-messages-and-message-queues#application-defined-messages
2. Muduo細(xì)節(jié): https://github.com/chenshuo/muduo


































