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






4.75/5 (7投票s)
一个简单的高级进程间通信库,支持使用原生的 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 编译器),也不需要您进行系统范围或每个用户的注册。
库应用的一个好例子(实际上也是此库开发的初衷)是在一个在受限用户帐户下运行的程序与同一程序(或其一部分)在提升的用户帐户下运行之间建立高级通信。
当一个程序逐个执行多个操作,而只有一小部分(可能可选)操作需要提升的管理员权限时,开发人员可以选择以下方式之一:
- 将整个程序标记为需要管理员权限。这种方法存在安全隐患,因为它极大地增加了病毒和恶意软件的攻击面。此外,如果需要管理员权限的操作是可选的,则可能不合适。
- 安装一个系统服务并通过 IPC 与其通信。显而易见的缺点是需要安装一个系统服务。SHLIPC 库可用于执行 IPC。
- 注册一个特殊的 COM 组件并通过 elevation moniker 使用它。同样,显而易见的缺点是需要在目标系统上执行 COM 组件注册。
- 运行自身的一个副本(或单独的可执行文件)并获得提升的权限,然后与其进行 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
pImpl
pipe_name
additional_flags
security_attributes
您的接口名称后附加“Server”。
指向 `MyServer` 类型对象的指针,该对象实际实现您的接口。
一个有效的命名管道名称。请参阅 MSDN 中命名管道的命名规则。会自动附加“\\.\" 前缀。服务器和客户端必须指定相同的管道名称。它可以硬编码在您的源代码中(确保使用唯一的名称,例如 GUID),也可以在运行时生成(并传递给客户端,例如通过命令行)。
要传递给 `CreateNamedPipe` API 函数的 `dwPipeMode` 参数的附加标志。例如,如果您的代码运行在 Windows Vista 或更高版本上,您可以传递 `PIPE_REJECT_REMOTE_CLIENTS` 来禁止从远程计算机连接。此参数是可选的。
用于创建命名管道的安全属性的可选指针。如果指定 `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
server_name
管道的名称。必须与创建服务器时使用的名称相同。
等待服务器出现的超时时间。如果在指定的时间间隔内找不到服务器,则返回 `false`。
可选的服务器名称。请记住,此库的主要目的是为在同一台计算机上运行的进程提供 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
- 原始版本。