CSharedMailslot:一个共享服务器邮件槽






4.68/5 (10投票s)
2004年12月1日
8分钟阅读

46563

783
一种用于共享服务器邮件槽的进程间通信方法。
引言
邮件槽非常有用,如果您想在本地计算机或 Windows LAN(包括 NT 和 9x 内核)中的进程之间轻松交换消息。但是,同一时间只能有一个进程拥有同一台计算机上的服务器邮件槽——即接收方。在本文中,我将描述一种跨不同本地进程共享服务器邮件槽的方法。
重要提示:由于 CodeProject 上有其他与邮件槽相关的文章(例如这篇),我将不再重复解释它们的工作原理,即使通过禁用共享功能,CSharedMailslot
也可以作为一个简单的邮件槽通信包装器。
为什么要共享服务器邮件槽?
LAN 上的任何进程都可以拥有一个定义的服务器邮件槽;当客户端发送广播消息时,所有进程都会收到它,无论它们位于哪个计算机上。但是,每台计算机上只能有一个进程创建并拥有一个邮件槽,任何其他后续尝试都会失败。例如,如果您开发了一个依赖于邮件槽的应用程序,那么您不能在您的计算机上运行多个启用了邮件槽的实例。想象一下 Windows 终端服务场景:服务器邮件槽将被创建,然后由第一个用户会话的应用程序实例拥有,而其他实例将收到错误。
您在这里唯一的解决方案是将服务器邮件槽封装在 NT 服务中,然后所有应用程序实例都将与其通信。这是一个好的架构设计,但可能无法实现。那么,CSharedMailslot
可能就是解决方案:只需使用它来包装邮件槽消息传递,然后忘记所有这些。
背后的想法
只有一个进程可以拥有一个给定的服务器邮件槽,但该进程可以创建任意数量的邮件槽。这里的想法是创建一个次要的从属邮件槽,它是拥有进程独有的,但其名称可以被其他进程猜测到(参见图 2)。每当主邮件槽收到消息时,拥有进程就会通过发送到其从属邮件槽将其转发给所有其他进程。
图 2 - 所采用的架构
假设我们有一个使用名为 CPDEMO
的邮件槽的 CSharedMailslot
应用程序;我们有三个应用程序实例在内存中。根据设计,这三个实例中只有一个可以拥有 CPDEMO
的所有权,我们称之为 instanceOwner
。每当远程进程向我们的计算机发送消息时,它都会被 instanceOwner
接收;然后,通过读取包含每个运行实例的线程 ID 的进程间共享结构 ipcInstances
,消息会通过构建其从属邮件槽名称(通过主邮件槽名称和其唯一的线程 ID 派生)并从中发送来发送给其他两个实例。
CSharedMailslot
实例会检查来自主邮件槽(如果它是 instanceOwner
)和从属邮件槽的新消息。每当 instanceOwner
退出或崩溃时,其中一个从属实例会选举自己成为新的 instanceOwner
。
关于演示
CSharedMailslot
演示是一个简单的 MFC 应用程序,演示了共享邮件槽类的用法。一旦确定了邮件槽名称,请按“打开”按钮进行创建。如果还有其他演示实例正在运行,您将收到错误,除非两者都勾选了“共享”选项。打开邮件槽后,您可以修改任何选项,然后单击“重新打开”以调用 Close()
和 Open()
类方法。
“消息格式”选项是一个额外功能,与 CSharedMailslot
类没有严格关系:如果勾选,消息将使用 Windows Messenger 格式构建,从而实现两者之间的消息交换。Messenger 服务定义了一个名为 messngr
的邮件槽,所以您也必须这样调用它;更重要的是,请注意它不使用 CSharedMailslot
:),所以您不能在同一台计算机上定义自己的 messngr
邮件槽:如果您运行 Messenger 服务,请将其停止。
注意:邮件槽只是 Messenger 服务使用的协议之一;替换它不属于本文档的范围。
使用其他控件发送和接收消息;“收件人:”字段在收到消息后会变成“发件人:”;但是,这仅在启用了 Messenger 格式时有效,因为邮件槽的设计不提供发送者名称。实际应用程序应在给定间隔轮询 Receive()
方法。
使用代码
CSharedMailslot
是一个 C++ 类(没有 MFC 的引用)。使用它非常简单;一个好的方法是在类级别定义它
class CSharedMailslotDemoDlg : public CDialog { protected: CSharedMailslot mailslotServer; };
然后在需要时打开它
if (mailslotServer.Open(true, "CPDEMO", NULL, true)) { // OK } else { AfxMessageBox("could not open the mailslot", MB_OK+MB_ICONEXCLAMATION); }
第一个参数 isMailslotServer
指定它是否应作为服务器(接收方,true
)或客户端(发送方,false
)运行;mailslotName
解释得很清楚,而如果它是客户端,则需要 destinationName
,否则将其传递为 NULL
。
最后一个参数 shared
是关键:将其设置为 true
以启用进程间共享功能。请注意,由于实现的共享方法仅在 NT 平台下工作,如果在任何 9x 系统上运行该类,则该选项将被忽略。
使用以下调用来读取消息
char *buffer=NULL; DWORD bufferSize=0, messagesWaiting=0; if (mailslotServer.Read(buffer, bufferSize, messagesWaiting)) { if (bufferSize>0) { // a new received message is in the buffer } } else { AfxMessageBox("could not read the mailslot", MB_OK+MB_ICONEXCLAMATION); }
buffer
和 bufferSize
都由类处理;调用 Read()
方法后,如果 bufferSize
大于 0,则 buffer
包含一条新接收的消息。messagesWaiting
包含队列中剩余的消息数量。
发送消息更简单;只需直接调用
sendMailslot.Write(buffer, strlen(buffer), "CPDEMO", "MATRODESKTOP");
该方法将为您打开,然后关闭邮件槽。还可以 Open()
一个客户端类型的邮件槽,然后为要发送的每条消息重复调用 Write()
方法。
使用完毕后,调用 Close()
方法以释放所有资源并通知其他进程。
探索代码
进程间(IPC)共享结构 ipcInstances
通过内存映射文件实现(有关更多信息,请参见 Code Project 上这篇优秀的文章)。为了在不同进程(包括 NT 服务进程)之间共享此类映射文件,我们必须通过 SetNamedSecurityInfo()
处理安全问题,该函数包含在 ADVAPI32.DLL 库中。该 DLL 是动态加载的,因为它仅在 NT 内核中受支持,而我希望我的类能在任何 Win32 平台上运行,即使它只是一个邮件槽包装器。
libADVAPI32 = LoadLibrary( _T("advapi32.dll") ); if (!libADVAPI32) return false; SetNamedSecurityInfo = (LPSETNAMEDSECURITYINFO) GetProcAddress( libADVAPI32, "SetNamedSecurityInfoA" ); if (!SetNamedSecurityInfo) return false;
类的大部分工作都在 Open()
方法中完成,因为它初始化上述库、邮件槽和 IPC 结构名称、内存映射文件以及邮件槽本身。与其他所有函数一样,适当的标志检查将使代码能够处理共享或单例场景。
Open()
方法中的 mailslotName
参数将决定类使用的所有名称;通过提供 CPDEMO
名称,BuildSharedNames()
方法将构建以下名称:
- CPDEMO -
CSharedMailslot
实例名称;这也是公共邮件槽名称 - Global\CSharedMailslotCPDEMO - IPC 内存映射文件
- CSharedMailslotCPDEMOMutex - 用于线程同步的互斥锁(见下文)
- Global\C\SharedMa\ilslotCP\DEMO71b - 内部从属邮件槽(71b 是线程 ID 示例)
(2)的 Global 前缀用于在启用 Windows 终端服务时实现跨会话 IPC 共享(这由 isSharedAllowed()
检查,最终返回 CSHAREDMAILSLOT_SYSREQ_OKNOWTS
)。请注意,WTS 也用于 Windows XP 的快速用户切换功能。
(4)的名称是从(2)加上当前线程 ID 派生的;由于在 9x 平台上邮件槽名称不能超过八个字符,BuildSharedMailslotName()
方法会定期添加一个 '\' 字符,将纯名称转换为支持的文件夹路径。此解决方法对类使用者是隐藏的,因为从属邮件槽仅在内部使用。
构建好所有名称并定义内存映射文件后,Open()
方法会扫描共享的 ipcInstancesView
结构中一个空的线程 ID 占位符——即第一个包含 0 的 instances[]
数组元素——并添加自己。如果没有定义的 instanceOwner
,则它将声明自己。值得注意的是,每当类需要读取或写入 IPC 共享结构时,它都会首先通过调用 WaitForSingleObject()
API 来锁定 ipcMutex
互斥锁。这是必需的,以防止由于未同步的调用而导致内存损坏。互斥锁用作信号量(有关线程同步的更多信息,请在此处阅读 Code Project 上的好文章)。
// the CSHAREDMAILSLOT_IPC_WAIT is an arbitrary timeout value if (WaitForSingleObject(ipcMutex, CSHAREDMAILSLOT_IPC_WAIT)==WAIT_TIMEOUT) { // close anything declared, then exit } while (counter<CSHAREDMAILSLOT_SLOTS_MAX) { if (ipcInstancesView->instances[counter]==0) { ipcInstancesView->instances[counter]=GetCurrentThreadId(); instanceIndex=counter; if (ipcInstancesView->instanceOwner==0) ipcInstancesView->instanceOwner=counter; break; } counter++; } ReleaseMutex(ipcMutex);
其余代码处理共享方法的逻辑;进一步的细节已作为注释包含在内。
注释
需要牢记的是,消息会被所有具有相同定义名称的 CSharedMailslot
实例接收。这与 Windows Messenger 服务的工作方式略有不同,因为它只向一个用户会话显示消息。由于选定的会话没有特殊含义(通常是第一个登录的),因为消息是发送到整个工作站的,所以我选择让所有实例都接收它。但是,如果您想与 Windows Messenger 的行为保持一致,只需在 Read()
方法中注释掉 Forward()
调用。
CSharedMailslot
的实现满足了我的所有需求,并且目前在我免费应用程序RealPopup 的最新测试版中使用。即使它将在实际世界中发布,它也可以进一步扩展:例如,它目前不支持回调,因此您必须轮询它来检查新消息。
历史
- 2004 年 9 月 7 日 - 0.001 - RealPopup build 149 的第一个构建:IPC 尚未实现。
- 2004 年 11 月 9 日 - 1.004 - 第一个公开版本(包含在 RealPopup build 155 中)。
- 2004 年 12 月 2 日 - 1.006 - 为 Code Project 发布。