65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.68/5 (10投票s)

2004年12月1日

8分钟阅读

viewsIcon

46563

downloadIcon

783

一种用于共享服务器邮件槽的进程间通信方法。

Sample Image - CSharedMailslot1.gif

引言

邮件槽非常有用,如果您想在本地计算机或 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);
}

bufferbufferSize 都由类处理;调用 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() 方法将构建以下名称:

  1. CPDEMO - CSharedMailslot 实例名称;这也是公共邮件槽名称
  2. Global\CSharedMailslotCPDEMO - IPC 内存映射文件
  3. CSharedMailslotCPDEMOMutex - 用于线程同步的互斥锁(见下文)
  4. 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 发布。
© . All rights reserved.