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

简单高级进程间通信库 (SHLIPC)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (7投票s)

2011 年 9 月 2 日

CPOL

10分钟阅读

viewsIcon

35105

downloadIcon

1103

一个简单的高级进程间通信库,支持使用原生的 C++ 接口。

引言

Windows 操作系统提供了相当多的进程间通信 (IPC) API。最广为人知和使用的包括:

  • Windows 套接字
  • WM_COPYDATA 消息
  • DDE(大部分已弃用)
  • 命名管道
  • 邮槽
  • 远程过程调用 (RPC)
  • 分布式 COM (DCOM)

此外,还开发了许多“自定义”IPC协议,它们要么基于上述协议之一,要么使用其他方法(如共享内存或文件)。Boost C++ 库也有自己的 IPC 库,非常灵活,但使用起来有点困难,并且要求您对多个 Boost 库有二进制依赖(也就是说,它不是头文件唯一的)。

大多数标准的 Windows IPC API 都存在一个问题,那就是它们非常底层。它们通常专注于将简单的字节流从一个端点传递到另一个端点。当您尝试在自己的代码中使用它们时,您经常会编写某种高级包装器。那些旨在成为高级的 API 通常有其自身的缺点,例如需要系统范围的注册(DCOM 和 RPC)。

我创建这个库的目标是开发一个简单的高级进程间通信组件,它将

  • 无需额外的工具(如 MIDL 编译器),
  • 提供原生的 C++ 接口,并为客户端自动进行原生的 C++ 类型封送,
  • “底层”利用其中一个内置的健壮的操作系统 IPC 机制,
  • 无需对任何库有二进制依赖(也就是说,它是一个头文件唯一的库)。

该库是在 Visual C++ 2010 SP1 上开发和测试的。它使用了新的 C++11 语言标准的一个子集。该库专注于 Windows 开发,因此是 Windows 平台相关的。它还使用了 Boost 的一些头文件库(经测试为 1.47)。它依赖于 ATL,但除此之外,不需要您在代码中使用 ATL,也不需要静态或动态链接到 ATL。该库支持 Windows 2000 及更高版本的操作系统(包括 32 位和 64 位版本)。

何时使用此库

当您需要在程序中快速实现进程间通信时,应考虑使用此库。

SHLIPC 库 vs. Sockets(命名管道、邮件槽等)

与简单的流式或消息式 IPC 机制相比,SHLIPC 为您提供了高级 C++ 接口,带有输入参数和结果值以及自动消息分发。与套接字相比,它还允许您指定基于 ACL 的安全性,因为它“底层”使用命名管道来实际传输数据。

SHLIPC 库 vs. RPC 或 DCOM

当您需要复杂的封送或需要在多台计算机之间进行 IPC 时,应选择 DCOM 或 RPC。另一方面,SHLIPC 在开发或构建过程中不需要单独的工具(如 MIDL 编译器),也不需要您进行系统范围或每个用户的注册。

库应用的一个好例子(实际上也是此库开发的初衷)是在一个在受限用户帐户下运行的程序与同一程序(或其一部分)在提升的用户帐户下运行之间建立高级通信。

当一个程序逐个执行多个操作,而只有一小部分(可能可选)操作需要提升的管理员权限时,开发人员可以选择以下方式之一:

  1. 将整个程序标记为需要管理员权限。这种方法存在安全隐患,因为它极大地增加了病毒和恶意软件的攻击面。此外,如果需要管理员权限的操作是可选的,则可能不合适。
  2. 安装一个系统服务并通过 IPC 与其通信。显而易见的缺点是需要安装一个系统服务。SHLIPC 库可用于执行 IPC。
  3. 注册一个特殊的 COM 组件并通过 elevation moniker 使用它。同样,显而易见的缺点是需要在目标系统上执行 COM 组件注册。
  4. 运行自身的一个副本(或单独的可执行文件)并获得提升的权限,然后与其进行 IPC。SHLIPC 库的开发旨在简化采用此方法。

示例应用程序

本文的源代码包含了一个示例应用程序,演示了如何使用该库。我们将在此列出部分源代码来说明库的用法。

这个简单的应用程序会转储给定卷列表的 USN 日志 ID。问题在于,为了获取 USN 日志 ID,我们必须打开一个卷以获取其句柄。此操作需要提升权限。我们的示例代码将自动启动自身的一个提升权限的副本以打开句柄。然后,提升权限的助手会将打开的句柄返回给调用方应用程序(作为标准用户运行),以便它可以继续处理卷。

您可以通过以下方式运行应用程序:

pipetest c: d:

使用库

首先,您需要 `#include` 该库的单个头文件

#include "pipeex.h"

库中的所有类和函数都定义在 `pipe_transport` 命名空间中。

封送和适配

SHLIPC 库会自动封送 C++ 基本类型(布尔型、整型和浮点型)、STL 字符串(`std::basic_string`)和集合(随机访问容器)(例如,它可以自动封送 `std::vector<std::wstring>`)。此外,还支持包含支持类型的简单 `struct`。

在 `struct` 可以使用之前,必须对其进行适配。这是使用 boost.Fusion 的其中一个适配宏完成的。有关更多信息,请参阅 boost.Fusion 文档。例如,以下代码将定义和适配一个结构

BOOST_FUSION_DEFINE_STRUCT(
    (test),CallerInformation,
    (DWORD,ProcessId)
    (wstring,ProcessName))

创建了以下普通 C++ `struct`:

namespace test
{
    struct CallerInformation
    {
        DWORD ProcessId;
        wstring ProcessName;
    };
}

请注意,已经适配的类型、boost.Fusion 序列也支持(这包括 `std::pair`、`std::tuple`、`boost::tuple` 和所有 boost.Fusion 原生序列)。

接口声明

接下来您需要定义一个接口。您定义的接口最终将被转换为一个普通的抽象 C++ 类。

要定义一个接口,您需要在全局范围内定义一个 `MACRO`,其语法如下:

#define IMyInterface_decl (IMyInterface) \
    (method_decl) \
    (method_decl) \
    ...
    (method_decl)) \
// end of macro

其中 `method_decl` 是:

(result_type)(method_name)(arg1_type)(arg2_type)...(argN_type)

注意:所有括号都是必需的!

`result_type` 必须是上面描述的支持类型之一。允许返回集合和适配的结构。 `void` 不能用作返回类型。如果指定了参数类型,它们也必须都符合描述的方案。仅支持“单向”或“输入”参数。如果您需要从方法返回一些信息,请使用返回类型(例如,定义一个结构)。

我们的示例应用程序定义了以下接口:

#define ITest_decl (ITest) \
    ((test::CallerInformation)(GetCallerInfo)) \
    ((std::vector<wstring>)(GetVolumes)) \
    ((bool)(SetResult)(int)(const std::vector<DWORD_PTR> &)) \
// end of macro

第一个方法 `GetCallerInfo` 不接受任何参数,并返回我们适配的结构 `CallerInformation`。示例应用程序实际上只使用 `ProcessId` 成员,但为了说明目的,该方法返回一个结构。

第二个方法 `GetVolumes` 也不接受任何参数,并返回一个提升的应用程序需要打开句柄的卷列表。

最后一个方法接受一个整数和一个整数向量(实际上是 `HANDLE` 类型的值)。请注意,我们不得不使用 `DWORD_PTR` 而不是 `HANDLE`,因为后者被定义为指针。该库故意不允许指针类型,因为它只处理值。

创建服务器

下一步是创建服务器和客户端。需要注意的重要一点是,库建立的协议更像是点对点的,而不是客户端-服务器的,所以这些术语可能会有点令人困惑。您选择您的程序是服务器还是客户端的最简单方法是:服务器是实际**实现**已定义接口的代码,而客户端是只**调用**接口方法的代码。

您还应考虑同一可执行文件扮演服务器和客户端角色,还是它们有单独的可执行文件。该库允许您选择任一场景。

我们的示例应用程序同时扮演服务器(非提升运行时)和客户端(提升运行时)的角色。

要创建服务器,您需要执行以下操作。首先,生成服务器(将此行放在全局作用域中):

PIPE_GEN_SERVER(ITest_decl);

接下来,定义一个实际实现您的接口的类:

class MyServer : public ITest
{
public:
    // implement all methods of ITest here:
    virtual test::CallerInformation GetCallerInfo()
    {
        // ...
    }

    virtual std::vector<std::wstring> GetVolumes()
    {
        // ...
    }

    virtual bool SetResult(int errors, 
            const std::vector<DWORD_PTR> &handles)
    {
        // ...
    }
};

创建 `pipe_transport::PipeServer` 模板类的一个实例:

pipe_transport::PipeServer<ITestServer> server(ITest *pImpl, const std::wstring &pipe_name, 
    DWORD additional_flags = 0, LPSECURITY_ATTRIBUTES security_attributes = nullptr);

其中,

  • ITestServer
  • 您的接口名称后附加“Server”。

  • pImpl
  • 指向 `MyServer` 类型对象的指针,该对象实际实现您的接口。

  • pipe_name
  • 一个有效的命名管道名称。请参阅 MSDN 中命名管道的命名规则。会自动附加“\\.\" 前缀。服务器和客户端必须指定相同的管道名称。它可以硬编码在您的源代码中(确保使用唯一的名称,例如 GUID),也可以在运行时生成(并传递给客户端,例如通过命令行)。

  • additional_flags
  • 要传递给 `CreateNamedPipe` API 函数的 `dwPipeMode` 参数的附加标志。例如,如果您的代码运行在 Windows Vista 或更高版本上,您可以传递 `PIPE_REJECT_REMOTE_CLIENTS` 来禁止从远程计算机连接。此参数是可选的。

  • security_attributes
  • 用于创建命名管道的安全属性的可选指针。如果指定 `nullptr`,则使用默认安全。

您也可以让您的服务器类同时继承接口和 `pipe_transport::PipeServer`,如示例代码所示。

运行服务器

创建服务器实例后,您可以运行它。启动后,服务器会等待客户端。一旦客户端连接,服务器会等待传入的消息。收到消息后,会解包其参数并调用相应的服务器方法。

此方法不会返回,直到客户端断开与服务器的连接。

server.Operate(DWORD timeout = INFINITE);

`timeout` 指定服务器等待客户端连接的时间(以毫秒为单位)。如果在指定的时间内客户端未连接,则返回 `false`。否则,在客户端断开连接后返回 `true`。

创建客户端

创建客户端比创建服务器简单得多。首先,您需要生成一个客户端(将此行放在全局作用域):

PIPE_GEN_CLIENT(ITest_decl);

然后定义以下变量:

pipe_transport::PipeClient<ITestClient> client;

同样,使用您的接口名称后附加“Client”。

创建后,客户端必须连接到正在运行的服务器。

if (client.Connect(const std::wstring &pipe_name, DWORD Timeout = 10000, 
                   const std::wstring &server_name = L"."))
{
    // work with client
}   // client automatically disconnects when it goes out of scope

其中

  • pipe_name
  • 管道的名称。必须与创建服务器时使用的名称相同。

  • Timeout
  • 等待服务器出现的超时时间。如果在指定的时间间隔内找不到服务器,则返回 `false`。

  • server_name
  • 可选的服务器名称。请记住,此库的主要目的是为在同一台计算机上运行的进程提供 IPC。尽管如此,它也应该适用于在单独的计算机上运行的服务器和客户端,但库将无法从网络错误中恢复。

客户端对象直接“继承”您的接口。您可以直接使用 `client` 变量来调用您接口的方法,就像调用其他 C++ 方法一样。参数将被自动打包并发送到服务器。返回值将被解包并从方法中返回。客户端的执行会阻塞,直到收到服务器的响应。

test::CallerInformation info = client.GetCallerInfo();
std::vector<std::wstring> volumes = client.GetVolumes();
client.SetResult(nErrors, handles);

自定义封送

可以添加对自定义类型封送的支持。自定义封送代码必须将自定义对象“拆卸”为库原生支持的类型的值,并将其“组装”回来。有两种方法。

自定义类的“侵入式”方法

如果您想为自己的类添加封送支持,并且愿意修改类定义,您可以这样做:

首先,向您的类添加 `marshal` 和 `unmarshal` 方法:

class MyClass
{
    // ...
public:    
    template<typename Writer>
    void marshal(Writer &writer) const
    {
        // use writer's operator << to marshal your class
    }

    template<typename Reader>
    void unmarshal(Reader &reader)
    {
        // use reader's operator >> to unmarshal your class
    }
};

然后将以下代码添加到 `pipe_transport::marshal` 命名空间中:

namespace pipe_transport { namespace marshal {
template<>
struct tag<MyClass>
{
    typedef stream_tag type;
};
}
}

之后,您就可以开始使用 `MyClass` 类的对象作为接口方法的参数或返回值。当库需要封送或解封对象的值时,它会调用其 `marshal` 和 `unmarshal` 成员函数。

非侵入式方法

如果您无法修改要封送的类,或者将要封送非类类型,您可以使用此方法。为您的类型在 `pipe_transport::marshal` 命名空间中添加部分模板特化:

namespace pipe_transport { namespace marshal {
template<>
struct Marshaller<MyType>
{
    template<typename Writer>
    static void write(Writer &writer, const MyType &v)
    {
        // marshal v into writer    
    }

    template<typename Reader>
    static void read(Reader &reader, MyType &v)
    {
        // unmarshal v from reader
    }
};

`Writer` 对象提供了 `operator <<` 的重载,它可以封送支持类型的任何值以及任何具有自定义封送器的值。此外,它还提供了三个 `operator ()` 重载:

// Marshal fundamental type
template<class T>
void operator ()(const T &v);

// Marshal continuous random-access range of fundamental types
template<class ConstInterator>
void operator ()(ConstIterator begin, ConstIterator end, std::true_type);

// Marshal range of non-fundamental types
template<class ConstInterator>
void operator ()(ConstIterator begin, ConstIterator end, std::false_type);

`Reader` 对象提供了 `operator >>` 的重载,它可以解封支持类型的值以及具有自定义封送器的值。此外,它还提供了三个 `operator ()` 重载:

// Unmarshal fundamental type
template<class T>
void operator ()(T &v);

// Unmarshal continuous random-access range of fundamental types
template<class Interator>
void operator ()(Iterator begin, Iterator end, std::true_type);

// Unmarshal range of non-fundamental types
template<class Interator>
void operator ()(Iterator begin, Iterator end, std::false_type);

更改历史

  • 2011/09/02 - 版本 1
    • 原始版本。
© . All rights reserved.