本文转载自微信公众号《一个程序员的修行之路》,作者:地下潜行者。转载本文请联系程序员修炼之路公众号。一、基本模型概述基于消息的事件驱动机制是一种通用模型,广泛应用于桌面软件开发、网络应用开发、前端开发等技术方向。本文主要介绍基本模型和基本框架,用于说明不同技术的常识。可以理解为外部操作事件,转化为消息存入队列;并且每种类型的消息都有相应的处理;通过消息循环,完成读取消息和调用消息处理的过程。只要应用程序不退出,此过程就会继续。下图中的模型来自一个Windows应用程序,但具有一定的通用性。2、模型在MFC程序中的应用MFC(MicrosoftFoundationClasses)是微软的一个基础类库,封装了大部分的WindowsAPI,也是桌面软件的UI开发框架。下图是VS2019多文档应用程序自动生成的一个MFC。无需任何开发工作,就可以得到一个丰富的界面框架,自带菜单栏、工具栏、状态栏、属性显示框等。但是现在MFC没落了。除了历史项目,很少有新项目使用MFC。下面将基于鼠标点击后完整的系统响应过程来说明该模型在MFC中的体现。2.1从鼠标点击到响应处理的完整过程1.用户点击鼠标;2、鼠标驱动产生鼠标点击消息(通过中断实现),执行系统消息队列;3、将系统消息转换为应用程序消息,放入应用程序队列;4.消息泵从应用消息队列中读取消息;5.消息分发与处理,借助USER模块,将消息分发给对应窗口对应的消息处理函数;问题:为什么消息处理函数不能做一个耗时长的任务?消息泵按顺序处理消息。处理完一条消息后,将处理下一条消息。如果当前消息的处理事件过长,后续消息将无法及时响应,会导致界面卡顿等极差的用户体验。2.2事件类型1)鼠标点击(单击、双击、右键)2)键盘按键3)触摸屏用户点击事件4)...用户在电脑上的各种操作,对应各种事件类型,不同的事件类型,将被翻译成不同的消息。2.3Messages定义用户操作事件,将转化为消息。消息定义如下:/**Messagestructure*/typedefstructtagMSG{HWNDhwnd;//接受消息的窗口句柄UINTmessage;//消息常量标识符(消息号)WPARAMwParam;//32位消息特定附加信息LPARAMlParam;//32位消息具体附加信息DWORDtime;//消息创建时间POINTpt;//消息创建时光标位置#ifdef_MACDWORDlPrivate;#endif}MSG微软提供了一系列的消息定义,用户也可以为应用程序开发自定义消息。Windows消息类型可分为以下两类:(1)系统消息:范围在[0x0000,0x03ff]之间,又细分为三个子类:窗口消息:与窗口操作、窗口创建、窗口绘制、窗口移动有关,窗户被毁;命令消息:一般指WM_COMMAND消息,与处理用户请求有关,通常由控件或菜单??产生。通知消息:特指WM_NOTIFY消息。通常表示某个窗口的子控件发生了一些事情,需要通知父窗口。微软官方链接给出了系统消息的范围:系统为系统定义的消息保留0x0000到0x03FF范围内的message-identifier值(WM_USER的值-1)。应用程序不能将这些值用于私人消息。(2)应用程序定义的消息WM_USER:[0X0400-0X7FFF],用户定义的消息范围。WM_APP:[0X8000-0XBFFF],用于程序间的消息通信。RegisterWindowMessage:【0XC000-0XFFFF】微软官方内容,给出了应用消息的取值范围:0x0400(WM_USER的值)到0x7FFF范围内的值可用于私有窗口类的消息标识符。如果你的应用是标记版本4.0,您可以使用0x8000(WM_APP)到0xBFFF范围内的消息标识符值来表示私人消息。当应用程序调用RegisterWindowMessageresigner函数注册消息时,系统返回一个0xC000到0xFFFF范围内的消息标识符,该函数保证在整个系统中是唯一的。如果其他应用程序出于不同目的使用相同的消息标识符,则使用此功能可以防止可能出现的冲突。2.4消息处理映射表(事件处理绑定)消息处理映射表是指每条消息对应的处理函数。只有先准备好映射表,当消息到达时,消息泵才会知道如何处理消息。2.4.1Win32应用程序WndProc中的消息处理映射表是一个消息处理函数,代码通过switchcase对不同的消息指定不同的处理函数。LRESULTCALLBACKWndProc(HWNDhWnd,UINTmessage,WPARAMwParam,LPARAMlParam){switch(message){caseWM_COMMAND:{intwmId=LOWORD(wParam);//分析菜单选择:switch(wmId){caseIDM_ABOUT:DialogBox(hInst,MAKEINTRESOURCE(IDD_ABOUTBOX),hWnd,关于);break;caseIDM_EXIT:DestroyWindow(hWnd);break;default:returnDefWindowProc(hWnd,message,wParam,lParam);}}break;caseWM_PAINT:{PAINTSTRUCTps;HDChdc=BeginPaint(hWnd,&ps);//TODO:在在此处添加任何使用hdc的绘图代码...EndPaint(hWnd,&ps);}break;caseWM_DESTROY:PostQuitMessage(0);break;default:returnDefWindowProc(hWnd,message,wParam,lParam);}return0;}2.4。2MFC中的消息处理映射表如下代码可见,WINDOWS消息WM_CREATE,对应的消息处理函数为OnCreate。当消息到达时,消息泵知道调用OnCreate函数。宏BEGIN_MESSAGE_MAP、END_MESSAGE_MAP用于定义消息映射表。BEGIN_MESSAGE_MAP(CFileView,CDockablePane)ON_WM_CREATE()...END_MESSAGE_MAP()#defineON_WM_CREATE()\{WM_CREATE,0,0,0,AfxSig_is,\(AFX_PMSG)(AFX_PMSGW)\(static_cast(&ThisClass::OnCreate))},2.5消息泵(Windows应用程序)消息泵负责从应用程序的消息队列中读取消息、转换消息和分发消息。MSGmsg;//主消息循环:while(GetMessage(&msg,nullptr,0,0)){if(!TranslateAccelerator(msg.hwnd,hAccelTable,&msg)){TranslateMessage(&msg);DispatchMessage(&msg);}}同上出现的函数是WindowsAPI函数GetMessage从消息队列TranslateMessage中读取消息进行消息翻译和转换。DispatchMessage派发消息,找到消息对应的窗口,调用响应函数。2.6消息队列(1)系统消息队列:这是系统中唯一的队列。设备驱动程序将用户的操作输入转换成消息存入系统队列,然后系统会将这条消息放入目标窗口所在线程的消息队列中等待处理。(2)线程消息队列:每个GUI线程都会维护一个线程消息队列,然后将线程消息队列中的消息发送给相应的窗口过程进行处理。消息队列不能直接访问,但是我们可以通过指定的接口访问消息队列。PostMessage函数用于向消息队列中添加消息并立即返回;GetMessage函数用于从消息队列中读取消息;2.7Windows消息拦截机制。用户可以利用Windows的消息拦截机制,在消息到达目标窗口之前对其进行提前处理。这主要是通过Windows的Hook机制来实现的。常用的调试工具SPY++就是利用HOOK机制拦截窗口消息。这只是一个介绍,不是很详细。2.8模态对话框与非模态对话框的区别模态对话框:子界面活动期间,父窗口不能响应消息。独占用户输入无模式对话框:窗口之间没有影响。模态对话框通过在消息循环内重新创建消息循环来工作。如果当前窗口的消息循环没有退出,那么父窗口的消息循环就不会工作,也就是不会响应。这样就产生了模态对话框独占响应的效果。3、模型在浏览器中的应用在web应用开发(前端开发)中,用户的点击操作产生一个事件,同时在web应用中处理响应。浏览器应用程序,也适用于此模型。3.1事件类型1)用户单击鼠标或将光标悬停在元素上。2)用户按下键盘上的一个键。3)用户调整浏览器大小或关闭浏览器窗口。4)提交表格。5)...完整的浏览器事件列表可以参考以下链接:https://developer.mozilla.org/en-US/docs/Web/Events3.2事件绑定在下面的例子中,在HTML的DOM元素中为事件绑定添加了点击事件响应。当用户点击div时,将执行响应函数。浏览器绑定事件的方式有很多种,这里仅以addEventListener为例。3.3事件传播用户点击div后,事件会按照捕获阶段、目标阶段、冒泡阶段的流程进行处理。用户可以通过addEventListener中的useCapture字段来确定事件捕获阶段。true-事件响应函数在捕获阶段执行false-事件响应函数在冒泡阶段执行){queue.processNextMessage();}queue.waitForMessage()会同步等待消息到达(如果没有当前等待处理的消息)。本段内容来自链接:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop3.5任务队列Javascript脚本的执行环境是单线程的,所以必须有是顺序存储挂起任务的任务队列。即3.4章节中的queue.4。模型在网络应用中的应用4.1点对点网络应用工作过程具有服务器角色和客户端角色的两个进程之间建立通信的过程如下。4.1.1服务器1)创建SOCKET;2)绑定IP:端口;3)SOCKET进入监听模式;4)等待外部连接请求进入,有则建立连接;5)数据读写处理;6)处理结束,关闭连接。4.1.2Client1)创建SOCKET;2)向指定IP:Port发起连接请求,建立连接;3)发送数据/接收数据;4)处理后关闭连接。问题:当一台机器有10W以上并发网络连接时,如何处理?一个线程处理一个SOCKET连接?(大量线程会导致CPU资源花费在线程切换上,而不是真正有效的工作上)SELECT周期性轮询所有SOCKET,检查是否可读或可写?(主动遍历所有SOCKET集合,当SOCKET基数特别大,activity小的时候效率低,SELECT本身也有数量限制)通过事件通知,只处理Active本地少量的SOCKET(参考CPU中断处理,效率高)4.2事件列表网络应用中存在一些基本的事件以及围绕这些事件的处理。在陈硕的书《Linux多线程服务器端编程》中,介绍了三个半的事件。1)连接建立,包括服务器接收新连接和客户端发起连接;2)连接断开,包括主动断开和被动断开;3)消息到达,说明缓冲区中有数据,可以读取并复制到用户自己控制的缓冲区中;4)消息发送后,算半个事件。开发者针对指定事件开发相应的处理函数,通过引擎完成事件处理。4.3事件处理引擎目前操作系统层面提供了高效的网络通信处理机制,不同的语言也提供了各种类库。4.3.1操作系统层支持1)WindowsIOCP2)CentOSEpoll3)xxxBSDkqueue4.3.2语??言层框架支持1)C/C++libevent/Muduo/Asio/…2)JavaNetty3)DotNetDotNetty4.3.3Epoll机制说明1)创建一个epoll实例句柄:可以理解为领导管理其他套接字;2)事件注册:注册每个SOCKET需要关注的事件,服务器监听SOCKET关注是否有新的连接进来;一般SOCKET关注是否有数据进来需要读取;超时,事件处理;...3)进入等待状态,当有事件进来时,操作系统会通知;4)事件处理,根据操作系统的通知,应用程序会进行反馈,并调用相应的事件处理函数进行响应。由于操作系统层面的支持,系统反馈时,只处理活跃的SOCKET,数据少,检查少,处理少。因此,它可以处理大量的套接字并发。可以这样做是因为网络应用程序收发数据,必然存在网络延迟,所以可以这样处理。如果每个SOCKET都在满负荷运行,那么这种机制就不能用于大量的连接处理。4.3.4Muduo网络库说明Muduo是陈硕编写的开源网络通信库,基于Epoll,采用Reactor模式开发。Reactor模式全称为reactor模型,指的是一个循环的过程,不断监听对应的事件是否被触发,当事件触发时调用对应的回调进行处理。如下图所示:所有的客户端连接请求事件都经过acceptor处理,并建立了新的连接;所有建立的连接都按照读取数据、解码、处理、编码、发回数据的过程进行处理。其中,数据的读写是由reactor根据事件进行处理的。关于Muduo的详细说明可以参考以下文档:https://www.cyhone.com/articles/analysis-of-muduo/4.3.5基于Muduo的网络应用开发模型1)创建事件循环器EventLoop(也可以理解为消息泵)2)建立对应的服务器TcpServer3)设置TcpServer的Callback(可以理解为建立一个事件处理映射表)4)启动服务器5)启动事件循环进行处理事件。这里的消息队列可以理解为操作系统返回的待处理SOCKET及其对应事件的列表。5.总结从上面可以看出,在不同的技术方向上,其实是可以挖掘出共性技术并借鉴的。因此,我得出以下结论:1)不同的技术,采用相似的设计思路2)研究共性,类比容易理解3)细节上的差异,通过工程实践掌握6.参考资料1.微软官方对消息及其介绍队列:https://docs.microsoft.com/en-us/windows/win32/winmsg/about-messages-and-message-queues#application-defined-messages2。木多详情:https://github.com/chenshuo/muduo