李佑民
---- 在Internet Explorer 中,微软带有两个很好的局域网通信工具:Chat 和NetMeeting,它们能使局域网中的用户通过互发消息文本、电子白板,甚至语音和视频图像进行交流,但是它们都需要指定一个服务器才能正常工作。在通常由若干台Windows 95/98 组成的对等网中,真正适用的消息传送工具仍然是微软通过网络组件安装的WinPopup.EXE,但微软好像忘记了这个小程序,使它从最初发行到现在依然是老样子,程序界面跟不上时代不说,每次只能发送38 个字节的消息文本,消息不能保存等不足使人感到十分遗憾。既然认为它不好,那我们就自己写一个。就像VC ++中某个类的增强版都带有Ex 后缀一样,我们也决定将增强后的WinPopup.EXE 命名为WinPopup Ex.EXE,图1 是完成后的WinPopupEx 的外观。
----要在局域网中实现计算机之间的通信,可以采用的办法很多,最容易想到的是针对某一个网络协议进行编程,如TCP/IP、IPX/SPX 和NetBEUI,但是控制稍显复杂,不易实现网络广播及只能针对某一个协议,显得不够灵活。微软为我们提供了内部进程的通信(IPC)接口,如果按照ISO 的OSI 模型划分,它工作在会话层,与它的下一层(传输层)采用何种协议无关。在IPC 接口中,MailLosts(邮槽)和NamedPipes(命名管道)都可以在服务器进程和客户机进程之间进行通信,而且不论服务器进程和客户机进程是驻留在同一台机器,还是通过网络联系在一起,IPC 接口都能正确地将信息从一个进程传送到另一个进程。而我们要做的就是在网络中的每台计算机上以它的“计算机名”建立一个邮槽或命名管道,其他计算机如果要发送信息给某台计算机,它只需要像打开一个文件一样(后面您将看到,的确是采用文件操作函数)打开以那台计算机命名的邮槽或命名管道,然后像写文件一样将数据写入,最后关闭它就完成了一次通信操作。
----邮槽和命名管道各有优缺点,命名管道是可靠的,在发送方不能确认接收方已接收到数据时,它会返回一个错误,但是它对网络广播操作就显得力不从心;而邮槽则刚好相反,它可以将消息一次传送给一组计算机,比如一个“工作组”或整个局域网,但它不能保证发送出去的数据一定就被接收方所接收。考虑到WinPopup 使用的是邮槽,为保证连续性,我们也决定采用MailLosts(邮槽)机制,至于通信的不可靠性,您在后面将看到,我们用一点手工代码就可以弥补它。
----在这个增强版本中,我们要实现以下一些WinPopup 没有的功能:
消息可以自动保存, 根据您的选择最多可以保存30 天;
消息大小不再限制在38 字节, 每条消息最多可以达到400 字节;
对单个计算机发出的消息, 可以要求接收方确认“已收到";
可以广播消息到局域网中的多个工作组;
可将它缩小为系统状态条图标, 当有消息到达时, 它可以发出声音或闪动图标加以提醒;
可定制的消息文本显示字体和颜色;
可选择让它开机自动运行;
自动收集网络信息, 您可以在“网络邻居”列表中选择接收人, 而不是手工输入它。
----本文不打算在这里将开发过程中的每一步细节都写出来,而是只就一些重点问题进行说明,开发环境是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 正好符合这个条件,它将一直在后台运行,当有消息到达时,我们不停地闪动图标并通过系统音频发出电话振铃的声音,以这种方式提醒用户,直到程序被用户手工切换到前台,见图2。既然在系统托盘区放置了图标,那么系统任务条按钮就不需要了,它被函数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 中处理,只有关机造成的退出才往系统注册表中写前面那个键值,这样就达到了我们的目的。
处理中,通过判断那个参数就可以确定本次程序的退出是否是因为系统关机,这个信息被保留到WM_CLOSE 中处理,只有关机造成的退出才往系统注册表中写前面那个键值,这样就达到了我们的目的。