—— 兼谈 Windows 的 MailLosts(邮槽)机制
摘 要:MailLosts(邮槽)和 Pipes(管道)、FileMapping(文件映射)、Clipboard(剪切板)、DDE(动态数据交换)被用于内部进程的通信(IPC),但是MailLosts(邮槽)特别适用于网络计算机之间的通信,Windows 95/98 自带的 WinPopup.EXE 就是利用 MailLosts(邮槽)机制实现无服务器、对等网络之间的通信服务,只不过它的功能有限。本文介绍用 VC++ 开发一个 WinPopup 的增强版的过程,并演示 IPC 和多线程技术。程序源代码可在 http://www.csdn.net/ 上找到。
关键字:邮槽 IPC VC++ 编程
在 Internet Explorer 中,微软带有两个很好的局域网通信工具:Chat 和 NetMeeting,它们能使局域网中的用户通过互发消息文本、电子白板,甚至语音和视频图象进行交流,但是它们都需要指定一个服务器才能正常工作。在通常由若干台 Windows 95/98 组成的对等网中,真正适用的消息传送工具仍然是微软通过网络组件安装的 WinPopup.EXE,但微软好象忘记了这个小程序,使它从最初发行到现在依然使老样子,程序界面跟不上时代不说,每次只能发送 38 个字节的消息文本,消息不能保存等不足使人感到十分遗憾。既然认为它不好,那我们就自己写一个,这应该是程序员们的原则。就象 VC++ 中某个类的增强版都带有 Ex 后缀一样,我们也决定将增强后的 WinPopup.EXE 命名为 WinPopupEx.EXE,图一是完成后的 WinPopupEx 的外观。
(图一)
要在局域网中实现计算机之间的通信,可以采用的办法很多,最容易想到的是针对某一个网络协议进行编程,如 TCP/IP、IPX/SPX 和 NetBEUI,但是控制稍显复杂,不易实现网络广播及只能针对某一个协议,显得不够灵活。微软为我们提供了内部进程的通信(IPC)接口,如果按照 ISO 的 OSI 模型划分,它工作在会话层,与它的下一层(传输层)采用何种协议无关。在 IPC 接口中,MailLosts(邮槽)和 NamedPipes(命名管道)都可以在服务器进程和客户机进程之间进行通信,而且不论服务器进程和客户机进程是驻留在同一台机器,还是通过网络联系在一起,IPC 接口都能正确地将信息从一个进程传送到另一个进程。而我们要做地就是在网络中的每台计算机上以它的“计算机名”建立一个邮槽或命名管道,其他计算机如果要发送信息给某台计算机,它只需要象打开一个文件一样(后面您将看到,的确是采用文件操作函数)打开以那台计算机命名的邮槽或命名管道,然后象写文件一样将数据写入,最后关闭它就完成了一次通信操作。
邮槽和命名管道各有优缺点,命名管道是可靠的,在发送方不能确认接收方已接收到数据时,它会返回一个错误,但是它对网络广播操作就显得力不从心;而邮槽则刚好相反,它可以将消息一次传送给一组计算机,比如一个“工作组”或整个局域网,但它不能保证发送出去的数据一定就被接收方所接收。考虑到 WinPopup 使用的是邮槽,为保证连续性,我们也决定采用 MailLosts(邮槽)机制,至于通信的不可靠性,您在后面将看到,我们用一点手工代码就可以弥补它。
在这个增强版本中,我们要实现以下一些 WinPopup 没有的功能:
. 消息可以自动保存, 根据您的选择最多可以保存 30 天;
. 消息大小不再限制在 38 字节, 每条消息最多可以达到 400 字节;
. 对单个计算机发出的消息, 可以要求接收方确认"已收到";
. 可以广播消息到局域网中的多个工作组;
. 可将它缩小为系统状态条图标, 当有消息到达时, 它可以发出声音或闪动图标加以提醒;
. 可定制的消息文本显示字体和颜色;
. 可选择让它开机自动运行;
. 自动收集网络信息, 您可以在“网络邻居”列表中选择接收人, 而不是手工输入它。
我们并不打算在这里将开发过程中的每一步细节都写出来,而是只就一些重点问题进行说明,如果您感兴趣的地方没有讲到,请查看源程序或发信给 lzlym@263.net 直接问我。开发环境是 Celeron 333、64M、Windows 98 和 Visual C++ 6.0。
接收和发送消息
WinPopupEx 的核心是消息的接收和发送,也就是对邮槽的处理。在程序开始运行时,它会调用函数:
HANDLE CreateMailslot(
LPCTSTR lpName, // 格式:“\\.\\MailSlot\\邮槽名”-本地邮槽
DWORD nMaxMessageSize, // 最大的消息文本长度,帮助文档上说将该值设//为0则消息长度无限
// 实际上每次收发的消息长度不能超过 424 字节
DWORD lReadTimeout, // 读超时时间(毫秒)
LPSECURITY_ATTRIBUTES lpSecurityAttributes // Windows 95/98 的安//全属性应设置为 NULL
);
建立两个本地邮槽 WinPopup 和 WPAnswer,邮槽 \\.\\MailSlot\\WinPopup 用于接收消息正文,而邮槽 \\.\\MailSlot\\WPAnswer 则是为了弥补邮槽机制传送消息的不可靠。当邮槽建立成功后,程序就在主线程之外新启动一个工作线程,这个线程不停的检查邮槽 \\.\\MailSlot\\WinPopup,当邮槽不为空(有消息到达)时,它首先查看消息数据包中的发送方名字,如发送方名为 B,则它向邮槽 \\B\\MailSlot\\WPAnswer 发送一个极短的标志文本,以通知发送方自己已经受到它发来的消息,然后向主线程发送一条自定义消息,通知主线程有消息到达,主线程在该自定义消息处理函数中从邮槽 \\.\\MailSlot\\WinPopup 里读出消息正文并将它显示给用户。如果计算机 A 要向计算机 B 发送消息,它只需将消息正文按一定格式的数据包写入邮槽 \\B\\MailSlot\\WinPopup 中,然后在预定义的延迟时间后,检查本地邮槽 \\.\\MailSlot\\WPAnswer 是否有计算机 B 返回的应答标志文本,就可知道接收方是否已受到消息。
检查邮槽中是否有消息到达使用函数:
BOOL GetMailslotInfo(
HANDLE hMailslot, // 邮槽句柄
LPDWORD lpMaxMessageSize, // 指向存放最大消息长度的变量的指针
LPDWORD lpNextSize, // 指向存放下一条消息长度的变量的指针
LPDWORD lpMessageCount, // 指向存放消息条数的变量的指针
LPDWORD lpReadTimeout // 读超时时间(毫秒)
);
如果 (*lpNextSize) != MAILSLOT_NO_MESSAGE,则说明有消息到达。
从邮槽中读取消息同从文件中读取数据没有区别:
BOOL ReadFile(
HANDLE hFile, // 句柄(这里是邮槽)
LPVOID lpBuffer, // 接收数据的缓冲区指针
DWORD nNumberOfBytesToRead, // 要读取的字节数
LPDWORD lpNumberOfBytesRead, // 指向存放已读取字节数的变量的指针
LPOVERLAPPED lpOverlapped // 指向 OVERLAPPD(重叠I/O)结构的指针
);
写入消息到邮槽遵循一般文件的建立、写入和关闭三个步骤:
建立:HANDLE CreateFile(
LPCTSTR lpFileName, // 文件名,通常是对方计算机的邮槽名,如: // "\\B\\MailSlot\\WinPopup"
DWORD dwDesiredAccess, // 存取模式,一般是:GENERIC_WRITE
DWORD dwShareMode, // 共享模式,一般是:FILE_SHARE_READ
LPSECURITY_ATTRIBUTES lpSecurityAttributes,// Windows 95/98 的安//全属性应设置为 NULL
DWORD dwCreationDisposition, // 如何建立,一般是:OPEN_EXISTING
DWORD dwFlagsAndAttributes, // 文件属性,一般是:FILE_ATTRIBUTE_NORMAL
HANDLE hTemplateFile // 设置为 NULL 即可
);
写入:BOOL WriteFile(
HANDLE hFile, // 文件句柄
LPCVOID lpBuffer, // 要写的数据缓冲区指针
DWORD nNumberOfBytesToWrite, // 要写入的字节数
LPDWORD lpNumberOfBytesWritten, // 指向存放已写入字节数的变量的//指针
LPOVERLAPPED lpOverlapped // 指向 OVERLAPPD(重叠I/O)结构的指针
);
关闭:BOOL CloseHandle(
HANDLE hObject // 文件句柄
);
消息数据包格式
消息正文的数据包格式为:
{
UINT m_uMID; // 唯一表示本消息的 ID
char m_cNeedAnswer; // 是否需要应答
char m_cEntirNet; // 是否广播到"整个网络"
LPCTSTR m_lpcsTo; // 接收人显示姓名(转换"整个网络"为"*")
LPCTSTR m_lpcsMessage; // 消息正文
}
应答消息包的格式为:
{
UINT m_uMID; // 表示要应答的消息的 ID (UINT)
LPCTSTR m_lpcsTo; // 应答接收人(LPCTSTR)
}
请注意上面的两个数据包格式中都包含一个 ID 值,原因比较有趣:就象我们前面说过的那样,邮槽是工作在会话层,与下一层(传输层)采用何种协议无关。但是,下层的每种协议都是单独与邮槽机制绑定在一起的,其结果就是当您通过邮槽发送数据时,对方计算机不只收到一条消息,而是若干条一样的消息,数量是两台计算机安装的通信协议数量的最小值,比如说计算机 A 安装有 TCP/IP、IPX/SPX 和 NetBEUI 三种协议,计算机 B 安装有 TCP/IP 和 NetBEUI 两种协议,那么计算机 A 向计算机 B 通过邮槽发送消息,则计算机 B 将会收到两条一样的消息。为了过滤掉多余的消息,我们给每条消息生成一个唯一的随机数 ID,接收消息时只其中保留一条,其余的简单抛弃即可。
界 面
我们一直认为系统托盘区是桌面上比较敏感的区域,只有那些对某个事件进行监视的应用才应该在系统托盘区放置图标,否则只能使人反感。而 WinPopupEx 正好符合这个条件,它将一直在后台运行,当有消息到达时,我们不停的闪动图标并通过系统音频发出电话振铃的声音,以这种方式提醒用户,直到程序被用户手工将它切换到前台。见图二。既然在系统托盘区放置了图标,那么系统任务条按钮就不需要了,它被函数 ShowWindow( SW_HIDE ) 隐藏了起来。
(图二)
程序的主窗口被分为上下两部分,上面是一个 ListCtrl,它的内容包括消息发送人、接收人、接收时间和消息正文的摘要;下面是一个 RichEditCtrl,通过选择上面列表中的项目,这里将会显示该消息正文的详细内容。这两个子窗口的字体和颜色都是可以定制的。
消息的保存
原来的 WinPopup 最不足的地方就是历史消息不能保存下来,每次重新打开它都是一片空白。而我们通过网络的交流一般都希望保存下来以后再看看。这个功能实现起来并不复杂,每次程序被关闭时,它都将所有的消息写入处于同一目录下的 WinPopupEx.History 文件中,每次运行时也从这个文件中读入,并将它填入程序对应的消息结构中即可。
开机自动运行
要让一个程序开机自动运行并不是一个新技术,您只需往系统注册表中新建一个键值就可以实现,即在 "Software\\Microsoft\\windows\\CurrentVersion\\Run" 下新建一个键,键名为 "WinPopupEx",值为您的 WinPopupEx.EXE 所在的磁盘路径。让我们考虑另外一种情况:“如果关机时 WinPopupEx 仍在运行,请在下次开机时自动运行它”。这就需要一点技巧,我们要注意两条 Windows 消息,一个是 WM_QUERYENDSESSION,每当 Windows 准备关闭时,它都会向所有运行的程序发送这条消息,通知系统准备关机,这时我们用一个 BOOL 变量将这个信息保存起来,如:theApp.m_bShutDown = TRUE,并返回 TRUE 同意关闭系统;另一个是 WM_ENDSESSION,当 Windows 从所有程序的 WM_QUERYENDSESSION 处理结果那里都得到 TRUE,它就将以 TRUE 为参数再次广播 WM_ENDSESSION 消息,如果某个程序的 WM_QUERYENDSESSION 处理返回 FALSE,那么将以 FALSE 为参数。在我们的 WM_ENDSESSION 消息处理中,通过判断那个参数就可以确定本次程序的退出是否是因为系统关机,这个信息一致被保留到 WM_CLOSE 中处理,只有关机造成的退出才往系统注册表中写前面那个键值,这样就达到了我们的目的。
其 它
耐心地阅读别人的源代码将会使您受益非浅。我们在源代码中尽量加入详尽的注释,希望您读起来不会太吃力。如果还有问题,请给我们来信:lzlym@263.net
|