一、参考资料
RPC的有关问题及其解决方法_rpc access denied-优快云博客
Windows RPC调用(一) - zpchcbd - 博客园
二、RPC核心概念与原理
Windows RPC(Remote Procedure Call)是微软提供的分布式计算框架,允许程序调用远程计算机上的函数,隐藏底层网络细节。
1、基本概念
RPC协议基于客户端-服务器模型,通过IDL(接口描述语言)定义接口规范,实现跨进程或跨网络的函数调用。客户端调用远程函数时,参数通过网络传输到服务端,服务端执行后返回结果。
2、核心组件
-
IDL文件:定义接口函数、参数类型及传输属性(如
[in]
、[out]
、[string]
)。 -
ACF文件:配置本地属性(如绑定句柄类型),不影响网络传输。
-
MIDL编译器:生成客户端和服务端的桩代码(Stub)及头文件,处理数据编组(Marshaling)。
3、通信机制
RPC支持多种传输协议(如ncacn_ip_tcp、ncacn_http、ncalrpc、ncacn_np),通过UUID唯一标识接口,确保版本兼容性。
4、工作原理
微软开发并在Windows上使用的RPC实现是DCE/RPC,它是"分布式计算环境/远程过程调用"的缩写。
DCE/RPC只是Windows中使用的众多 IPC(进程间通信)机制之一。例如,它用于允许本地进程甚至网络上的远程客户端与本地或远程机器上的另一个进程或服务交互。
RPC也是一种进程间通信方式,采用的是客户机/服务器模式,它允许本地程序调用另一个地址空间的过程或者函数,而不用程序员去管理调用的细节,发生在同一台主机上就是LPC,发生在不同主机上就是RPC。
而在本地过程调用(LPC)中实现的方式是压栈直接调函数,远程过程调用(RPC)也是调函数,但是在调用另一个进程的函数,而为了区分调用哪个函数设置了一些标识,这些标识则对应两个进程的对应的函数,所以客户端传给服务端不仅仅需要传递函数的参数还需要给那些标识表示调用哪个函数。
如图所示,客户端和服务端调用存根通过运行时库中的函数来发送和接受请求。
微软详细描述:RPC 的工作原理 - Win32 apps
(1)结构部分
客户端(client):服务的调用方。
客户端存根(client stub):存放服务端的地址信息,再将客户端的请求参数打包成网络数据,然后通过网络远程发送给服务方。
服务端存根(server stub):接受客户端发送过来的消息,将消息解包,并调用本地的方法。
服务端(server):真正的服务提供者。
(2)RPC调用协议
RPC技术发送Local请求时使用ncalrpc协议,发送Remote请求时使用ncacn_ip_tcp或者ncacn_np协议,前者微软更推荐。
三、调用示例
微软教程:教程 - Win32 apps
1、步骤
(1)生成 uuid 及 .idl 文件
通过 uuidgen.exe /i /oRPCtest.idl 命令生成 uuid 并输出到 RPCtest.idl 中,会自动生成 RPCtest.idl。
// RPCtest.idl
[
uuid(e082f193-eac1-4964-bd1b-947a1a55def3),
version(1.0)
]
interface RPCtest
{
void HelloProc([in, string] unsigned char* pszString);
void Shutdown(void);
}
1. [in]
参数
-
用途:表示参数值仅从客户端传递到服务端(单向输入)。
-
行为:
-
客户端将参数值序列化后发送给服务端。
-
服务端接收参数值并处理,但不会将修改后的值返回给客户端。
-
服务端对
[in]
参数的修改对客户端不可见。
-
-
适用场景:
-
输入型参数(例如查询条件、配置参数)。
-
不需要服务端返回修改后的值的场景。
-
-
示例:
-
idl
-
IDL(接口定义语言)示例 void QueryData([in] int request_id, [in] string filter);
2. [out]
参数
-
用途:表示参数值仅从服务端传递到客户端(单向输出)。
-
行为:
-
客户端需要预先分配内存(某些框架会自动处理)。
-
服务端填充参数值,结果会返回给客户端。
-
客户端初始传入的值会被忽略。
-
-
适用场景:
-
输出型参数(例如返回计算结果、错误信息)。
-
需要服务端返回新生成的资源(如句柄、指针)。
-
-
示例:
-
idl
-
void GetResult([out] int* result, [out] string* error_message);
3. [in, out]
参数
-
用途:参数值双向传递(客户端→服务端→客户端)。
-
行为:
-
客户端将初始值发送给服务端。
-
服务端可以修改参数值,修改后的值会返回给客户端。
-
-
适用场景:
-
需要服务端修改并返回更新后的参数(例如缓存刷新、增量更新)。
-
-
示例:
-
idl
-
void UpdateCache([in, out] CacheEntry* entry);
(2)创建acf文件
// RPCtest.acf
[
implicit_handle (handle_t hello_IfHandle)
]
interface RPCtest
{
}
注意: 1) RPCtest.idl 文件与 RPCtest.acf 文件中的接口名称(RPCtest)应一致,否则接下来编译的时候会报错。
2)文件名称(RPCtest.idl 和 RPCtest.acf)与接口名称也保持一致,否则编译时.idl 找不到 .acf文件。
ACF 文件配置规则
// RPCtest.acf
[
implicit_handle(handle_t hello_IfHandle), // 匹配IDL中的声明
auto_handle // 可选:自动绑定管理
]
interface RPCtest
{
// ----------------- 传输协议配置 -----------------
// 指定支持的协议序列(优先级顺序)
ncacn_ip_tcp, // TCP/IP
ncacn_np, // Named Pipe
ncalrpc // Local RPC
// ----------------- 安全配置 ---------------------
security_descriptor("D:(A;;GRGW;;;WD)"), // DACL权限
security(impersonation) // 安全模拟级别
// ----------------- 性能优化 ---------------------
strict_context_handle, // 严格上下文检查
max_calls(1000), // 最大并发调用数,控制单个 RPC 接口允许的最大并发调用数。
max_rpc_size(65536) // 最大数据包大小(字节)
}
(3)编译IDL文件,生成存根文件
通过命令 midl RPCtest.idl 生成 .h 和 .c 文件。
midl RPCtest.idl
其中,RPCtest.h文件是客户端和服务器端程序共同要用到的,RPCtest_c.c是客户端程序需要的,RPCtest_s.c是服务器程序所需要的。
RPCtest.h
/* this ALWAYS GENERATED file contains the definitions for the interfaces */
/* File created by MIDL compiler version 8.01.0628 */
/* at Tue Jan 19 11:14:07 2038
*/
/* Compiler settings for RPCtest.idl:
Oicf, W1, Zp8, env=Win32 (32b run), target_arch=X86 8.01.0628
protocol : dce , ms_ext, c_ext, robust
error checks: allocation ref bounds_check enum stub_data
VC __declspec() decoration level:
__declspec(uuid()), __declspec(selectany), __declspec(novtable)
DECLSPEC_UUID(), MIDL_INTERFACE()
*/
/* @@MIDL_FILE_HEADING( ) */
#pragma warning( disable: 4049 ) /* more than 64k source lines */
/* verify that the <rpcndr.h> version is high enough to compile this file*/
#ifndef __REQUIRED_RPCNDR_H_VERSION__
#define __REQUIRED_RPCNDR_H_VERSION__ 475
#endif
#include "rpc.h"
#include "rpcndr.h"
#ifndef __RPCNDR_H_VERSION__
#error this stub requires an updated version of <rpcndr.h>
#endif /* __RPCNDR_H_VERSION__ */
#ifndef __RPCtest_h__
#define __RPCtest_h__
#if defined(_MSC_VER) && (_MSC_VER >= 1020)
#pragma once
#endif
#ifndef DECLSPEC_XFGVIRT
#if defined(_CONTROL_FLOW_GUARD_XFG)
#define DECLSPEC_XFGVIRT(base, func) __declspec(xfg_virtual(base, func))
#else
#define DECLSPEC_XFGVIRT(base, func)
#endif
#endif
/* Forward Declarations */
#ifdef __cplusplus
extern "C"{
#endif
#ifndef __RPCtest_INTERFACE_DEFINED__
#define __RPCtest_INTERFACE_DEFINED__
/* interface RPCtest */
/* [implicit_handle][version][uuid] */
#define BUFSIZE ( 1024 )
void HelloProc(
/* [string][in] */ unsigned char *pszString);
void HelloBack(
/* [out] */ unsigned char achBackBuf[ 1024 ]);
void Shutdown( void);
extern handle_t hello_IfHandle;
extern RPC_IF_HANDLE RPCtest_v1_0_c_ifspec;
extern RPC_IF_HANDLE RPCtest_v1_0_s_ifspec;
#endif /* __RPCtest_INTERFACE_DEFINED__ */
/* Additional Prototypes for ALL interfaces */
/* end of Additional Prototypes */
#ifdef __cplusplus
}
#endif
#endif
Hello.h 包含 HelloProc 和 Shutdown 的函数原型,以及两个内存管理功能的正向声明, midl_user_allocate 和 midl_user_free。 将在服务器应用程序中提供这两个函数。 如果通过 [out] 参数将数据从服务器传输到客户端 () 则还需要在客户端应用程序中提供这两个内存管理功能。
请注意全局句柄变量的定义,hello_IfHandle,客户端和服务器接口句柄名称、hello_v1_0_c_ifspec和hello_v1_0_s_ifspec。 客户端和服务器应用程序将在运行时调用中使用接口句柄名称。
2、测试程序
服务器程序 RPCserver.cpp
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include "RPCtest.h"
#include <windows.h>
#pragma comment(lib,"RpcRT4.lib")
int main() {
RPC_STATUS status;
// LPC本地调用
//RPC_WSTR pszProtocolSequence = (RPC_WSTR)L"ncacn_np";
//RPC_WSTR pszEndpoint = (RPC_WSTR)L"\\pipe\\helloTest-{DB37F2AA-CD1B-4F7D-87B6-2082854D21B7}";
// RPC远程调用
RPC_WSTR pszProtocolSequence = (RPC_WSTR)L"ncacn_ip_tcp";
RPC_WSTR pszEndpoint = (RPC_WSTR)L"5000";
unsigned char* pszSecurity = NULL;
unsigned int cMinCalls = 1;
unsigned int fDontWait = FALSE;
// 指定在RPC运行时使用的协议序列
status = RpcServerUseProtseqEp(
pszProtocolSequence,
RPC_C_PROTSEQ_MAX_REQS_DEFAULT, // 允许最大并发连接,控制单个协议端点(如 TCP 端口、命名管道)的最大并发连接数。
pszEndpoint,
pszSecurity
);
if (status) {
printf("Invoke RpcServerUseProtseqEp Error, The Error Is %d\n", status);
exit(status);
}
printf("Invoke RpcServerUseProtseqEp...\n");
// 注册接口,使其指定接口能够被用于RPC远程调用
status = RpcServerRegisterIfEx(
RPCtest_v1_0_s_ifspec, // MIDL生成的接口规范
NULL, // 不指定UUID
NULL, // 不提供入口点
RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, // 安全选项
0, // 使用默认线程池大小
NULL // 安全回调
);
if (status) {
printf("Invoke RpcServerRegisterIf Error, The Error Is %d\n", status);
exit(status);
}
printf("Invoke RpcServerRegisterIf...\n");
// 开启监听服务在服务器上面
printf("Invoke RpcServerListen...\n");
status = RpcServerListen(
1, // 最小线程数
RPC_C_LISTEN_MAX_CALLS_DEFAULT, // 最大并发调用(系统默认),控制整个 RPC 服务器的最大并发调用数
FALSE // 阻塞模式,直到服务停止
);
if (status) {
printf("Invoke RpcServerListen Error, The Error Is %d\n", GetLastError());
exit(status);
}
return 0;
}
/******************************************************/
/* MIDL allocate and free */
/******************************************************/
void __RPC_FAR* __RPC_USER midl_user_allocate(size_t len)
{
return(malloc(len));
}
void __RPC_USER midl_user_free(void __RPC_FAR* ptr)
{
free(ptr);
}
/************************************************************************/
/* Interfaces */
/************************************************************************/
void HelloProc(unsigned char* szhello)
{
printf("This is client data: %s\n", szhello);
}
void HelloBack(unsigned char achBackBuf[1024])
{
memcpy(achBackBuf, "Hello, I am server!", 20);
printf("This is server data: %s\n", achBackBuf);
}
void Shutdown(void)
{
RPC_STATUS status = 0;
status = RpcMgmtStopServerListening(NULL);
if (status != 0) {
printf("RpcMgmtStopServerListening returns: %ld\n", status);
}
status = RpcServerUnregisterIf(NULL, NULL, FALSE);
if (status != 0) {
printf("RpcServerUnregisterIf returns: %ld\n", status);
}
}
注意:注册调用方式为 ncacn_ip_tcp 时,如果使用 RpcServerRegisterIf 方式注册客户端连接时会返回 RPC_S_ACCESS_DENIED 异常 ,改为使用 RpcServerRegisterIfEx 方式可以解决问题。这是因为从Windows XP SP2 开始,增强了安全性的要求,所以使用 RpcServerRegisterIfEx 。
客户端程序 RPCclient.cpp
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include "RPCtest.h"
#include <windows.h>
#pragma comment(lib,"RpcRT4.lib")
int main() {
RPC_STATUS status;
RPC_WSTR pszUuid = NULL;
// LPC本地调用
//RPC_WSTR pszProtocolSequence = (RPC_WSTR)L"ncacn_np";
//RPC_WSTR pszNetworkAddress = NULL;
//RPC_WSTR pszEndpoint = (RPC_WSTR)L"\\pipe\\helloTest-{DB37F2AA-CD1B-4F7D-87B6-2082854D21B7}";
// RPC远程调用
RPC_WSTR pszProtocolSequence = (RPC_WSTR)L"ncacn_ip_tcp";
RPC_WSTR pszNetworkAddress = (RPC_WSTR)L"localhost";
RPC_WSTR pszEndpoint = (RPC_WSTR)L"5000";
RPC_WSTR pszOptions = NULL;
RPC_WSTR pszStringBinding = NULL;
unsigned char* pszString = (unsigned char*)"hello, world";
unsigned long ulCode;
unsigned char achBackBuf[1024] = {0};
// 创建StringBinding对象用于描述连接服务端的信息
status = RpcStringBindingCompose(pszUuid,
pszProtocolSequence,
pszNetworkAddress,
pszEndpoint,
pszOptions,
&pszStringBinding);
if (status) {
printf("Invoke RpcStringBindingCompose, The Error Is %d\n", status);
exit(status);
}
// StringBinding对象转换为BindingHandle对象
status = RpcBindingFromStringBinding(pszStringBinding, &hello_IfHandle);
if (status) {
printf("Invoke RpcBindingFromStringBinding, The Error Is %d\n", status);
exit(status);
}
// 调用远程服务
RpcTryExcept
{
HelloProc(pszString);
printf("Call server : %s\n", pszString);
HelloBack(achBackBuf);
printf("This is server data: %s\n", achBackBuf);
Shutdown();
}
RpcExcept(1)
{
ulCode = RpcExceptionCode();
printf("Runtime reported exception 0x%lx = %ld\n", ulCode, ulCode);
}
RpcEndExcept;
// 释放StringBinding指针
status = RpcStringFree(&pszStringBinding);
if (status) {
printf("Invoke RpcStringFree, The Error Is %d\n", status);
exit(status);
}
// 释放hello_IfHandle句柄
status = RpcBindingFree(&hello_IfHandle);
if (status) {
printf("Invoke RpcBindingFree, The Error Is %d\n", status);
exit(status);
}
return 0;
}
/******************************************************/
/* MIDL allocate and free */
/******************************************************/
void __RPC_FAR* __RPC_USER midl_user_allocate(size_t len)
{
return(malloc(len));
}
void __RPC_USER midl_user_free(void __RPC_FAR* ptr)
{
free(ptr);
}
3、多协议示例
// 注册TCP和Named Pipe
RpcServerUseProtseqEp(
(RPC_WSTR)L"ncacn_ip_tcp",
RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
(RPC_WSTR)L"5000",
NULL
);
RpcServerUseProtseqEp(
(RPC_WSTR)L"ncacn_np",
RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
(RPC_WSTR)L"\\pipe\\RPCtest",
NULL
);
客户端调用示例
// 绑定到服务器
RpcBindingFromStringBinding(
(RPC_WSTR)L"ncacn_ip_tcp:192.168.1.100[5000]",
&hello_IfHandle
);
4、运行结果
服务器监听请求:
客户端发送数据:
服务器接收数据并返回数据:
客户端获取服务器数据: