What?
RPC( Remote Procedure Call),远程过程调用,相比于IPC来说RPC就是基于远程的工作机制,说白了RPC也是一种进程间通信方式,它只不过可以允许本地程序调用另一个地址空间的过程或者函数,而不用程序员去管理调用的细节。对于IPC来说,程序只能调用本地空间的函数,而RPC机制提供了一种程序员不必显示的区分本地调用和远程调用。
为什么RPC可以实现远程功能?
原因在于它是一种通过网络通信从远程计算机程序上请求服务,而不需要去了解底层网络技术的协议。RPC机制是假定某些传输协议的存在,比如TCP或者UDP,为通信程序之间携带有效信息,RPC将原来的本地调用过程变为调用远端服务器的方法,如果在OSI网络通信模型中来讲,RPC是跨越了传输层和应用层的。
RPC采用的是客户机/服务器模式。请求程序就是一个客户机,而服务程序就是一个服务器。
首先 ,客户机调用进程发送一个带有进程参数的调用信息到服务进程,然后等待应答信息。
然后 ,服务端处于等待状态直到调用信息到达。当调用信息到达后,服务器获得参数,计算结果,发送回复信息,然后继续等待下一个信息到来。
最后 ,客户机接受到答复信息,获取回复结果,继续执行。
Why?
一种新技术的出现的目的一定是为了解决某一需求的,如果开发是一个逻辑简单、用户不大、流量不大的程序,那么我们无需解决资源问题,但是往往一个好的程序需要去面对不同的资源分配问题。
以下的两种情况简单介绍一下RPC出现过程:
- 当系统的访问量突然增长,一台单机的服务器已经无法承受如此多的业务,此时我们可以去增加服务器,将不同的业务部署在不同的服务器上,划清逻辑界限以减少压力。此时也不需要RPC机制,因为各个服务器上运行的是不同的业务。
- 如果我们业务越来越多,新业务的出现可以需要集合很多旧业务的功能,但是旧的业务是部署在不同服务器上的,此时就出现了一个问题,发现部署在各个服务器上的功能,已经不能简单的划分开了。为了开发这个新的业务,如果开发人员去开发一套新的功能,那么就意味你需要更多的时间和人力以及金钱去搞,这样的代价往往是不能接受的。所以我们只能选择去用旧的业务集成开发出新业务,为了处理新旧业务复用的问题,此时就出现了一个解决方法,可以将公共业务,也就是新业务中都有用到的旧业务,将其逻辑抽离出来,组成一个独立的服务,而原有的、新增的服务都可以去和公共服务去交互,以来完成完整的业务功能。
这时,我们就需要一种高效的不同服务之间可以通信的手段来完成这种需求,因此就出现了RPC。
读过这两段话,其实你也能发现,这就是在描述服务化、微服务和分布式系统架构的基础场景,而RPC就是实现以上结构的一种手段。
RPC
RPC框架
- 客户端(client):服务的调用方。
- 客户端存根(client stub):存放服务端的地址信息,再将客户端的请求参数打包成网络数据,然后通过网络远程发送给服务方。
- 服务端存根(server stub):接受客户端发送过来的消息,将消息解包,并调用本地的方法。
- 服务端(server):正真的服务提供者。
RPC调用过程
- client 以本地调用方式(接口)调用服务。
- client stub 接受到调用后,负责将方法,参数等组装成能够进行网络传输的消息体(将消息对象序列化为二进制)
- 客户端通过sockets将网络消息发送到服务端
- server stub 收到消息后节进行解码(将消息对象反序列化)
- server stub 根据解码结果调用本地的服务
- server 执行本地过程并将执行结果返回给server stub
- server stub 将返回结果打包成网络消息(将结果消息进行发序列)
- 服务端通过sockets将网络消息发送到客户端
- client stub 接受到结果消息,并进行解码(将结果消息反序列化)
- client 接受到返回结果
RPC就是将2、3、4、7、8、9过程进行分装,使用者只需要对其他部分进行设计即可。如下图所示:
这个上面的RPC原理图一致,只不过是把sockets部分分装成为了RPC Runtime 。
至此我们大概了解到了RPC的概念、出现的背景和调用的过程。虽然文章中没有对RPC机制进行细致分析,但是通过这个篇文章还是可以大体了解一下RPC机制的。本文也不会对RPC框架核心技术点进行介绍,这毕竟不是本文的技术范围,作者的目的就是为了说清什么是RPC,Windows下的RPC编程如何操作而已。
Windows RPC Demo
RPC的接口标准使用了IDL(Interface Description Language接口描述语言)语言标准描述,熟悉COM接口的用户应该一眼就能看出,因为它们的接口风格非常相似。相应微软的编译器是MIDL,通过IDL文件来定义RPC客户端与服务器之间的通信接口,只有通过这些接口客户端才能访问服务器。
下面我们通过一个Demo来具体解释一下RPC编程的具体过程。
开发环境: Windows10
Visual Studio 2019
在vs2019中建立一个工程。
定义IDL文件
在工程中添加一个IDL文件,IDL文件又一个或多个接口定义组成,每一个接口定义都有一个接口头和接口体。
- 接口头:包含此接口的信息,如UUID和版本号。这次信息分装一堆中括号内。版本号做兼容使用。客户端、服务器只有版本兼容,才能进行链接。
- 接口体:interface关键字和接口名。接口体中包含的是接口(函数)的原型,接口的编写符合C语言风格。
可以直接在vs工程中添加idl文件。
[
implicit_handle (handle_t useless_IfHandle)
]
interface matthewRpc
{
}
useless.idl
import "oaidl.idl";
import "ocidl.idl";
[
uuid(aaf3c26e-2970-42db-9189-f2bc0e073e7c),
version(1.0)
]
interface matthewRpc
{
void UselessProc([in, string] unsigned char* pszInput);
void Shutdown(void);
}
定义IDL文件后,可以直接编译,但是编译后不会自动生成相对应的服务端和客户端的C文件。如果要自动生成,还需要配置一下ACF文件。
定义ACF文件
ACF文件可以使用户定义自己的客户端或服务端的RPC接口。例如,如果你的客户端程序包含了一个复杂的数据结构,此数据结构只在本地机上有意义,那么你就可以在ACF文件中指定如何描述独立于机器的数据结构,使用数据结构用于远程过程调用。下面我们在ACF文件中定义一个handle类型,用来代表客户端与服务端的连接。[implicit_handle]属性允许客户端程序为它的远程过程调用选择一个服务端。ACF定义了此句柄为handle_t类型(MIDL基本数据类型)。
useless.acf
[
implicit_handle (handle_t useless_IfHandle)
]
interface matthewRpc
{
}
vs中并没有定义acf文件格式,可以将其他格式的文件该为.afc文件。
定义好ACF文件后,直接可以编译该工程,vs会自动在工程目录下包含生成服务端和客户端所用的.c和.h文件,如下图所示。
useless_h.h被客户端和服务端共用。 useless_c.c被客户端所用。useless_s.c被服务端所用。
服务端
服务端主要是实现远程过程。代码如下所示。
服务端必须包含两个内存分配函数。这两个函数被服务端程序调用:midl_user_allocate和midl _user_free。这两个函数在服务端分配和释放内存,一般情况下midl _user_allocate和midl_user_free都会简单地封装C库函数malloc和free。
- RpcServerUseProtseqEp 通过调用这个例程,让 RPC 运行时使用指定的协议序列
- RpcServerRegisterIf 注册接口
- RpcServerListen 让服务器监听到达的远程过程调用
- 实现IDL文件中定义的函数过程
server.cpp
#include <tchar.h>
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include "useless_h.h"
#include <windows.h>
extern "C" {
void UselessProc(unsigned char* pszString)
{
printf("%s\n", pszString);
MessageBox(0, 0, 0, 0);
}
}
void Shutdown(void) {
MessageBox(0, _T("123"), 0, 0);
}
void Rpc_port();
void Rpc_socket();
int main()
{
Rpc_port();
//Rpc_socket();
}
/******************************************************/
/* 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);
}
void Rpc_port()
{
printf("%s", "This is RPC port");
RPC_STATUS status;
unsigned char* pszProtocolSequence_port = (unsigned char*)"ncacn_np";
unsigned char* pszEndpoint_port = (unsigned char*)"\\pipe\\useless";
unsigned char* pszSecurity = NULL;
unsigned int cMinCalls = 1;
unsigned int fDontWait = FALSE;
//通过调用这个例程,让 RPC 运行时使用指定的协议序列
status = RpcServerUseProtseqEp(pszProtocolSequence_port,
RPC_C_LISTEN_MAX_CALLS_DEFAULT,
pszEndpoint_port,
pszSecurity);
if (status) exit(status);
//注册接口 useless_v1_0_s_ifspec在.h文件中会提供
status = RpcServerRegisterIf(matthewRpc_v1_0_s_ifspec,
NULL,
NULL);
if (status) exit(status);
//这个例程用于让服务器监听到达的远程过程调用
status = RpcServerListen(cMinCalls,
RPC_C_LISTEN_MAX_CALLS_DEFAULT,
fDontWait);
if (status) exit(status);
}
void Rpc_socket()
{
printf("%s", "This is RPC socket");
RPC_STATUS status;
unsigned char* pszProtocolSequence_tcp = (unsigned char*)"ncacn_ip_tcp";
unsigned char* pszEndpoint_tcp = (unsigned char*)"13521";
unsigned char* pszSecurity = NULL;
unsigned int cMinCalls = 1;
unsigned int fDontWait = FALSE;
//通过调用这个例程,让 RPC 运行时使用指定的协议序列
status = RpcServerUseProtseqEp(pszProtocolSequence_tcp,
RPC_C_PROTSEQ_MAX_REQS_DEFAULT,
pszEndpoint_tcp,
pszSecurity);
if (status) exit(status);
//注册接口 useless_v1_0_s_ifspec在.h文件中会提供
status = RpcServerRegisterIfEx(matthewRpc_v1_0_s_ifspec, NULL, NULL, RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, 0, NULL);
if (status) exit(status);
//这个例程用于让服务器监听到达的远程过程调用
status = RpcServerListen(cMinCalls,
RPC_C_LISTEN_MAX_CALLS_DEFAULT,
fDontWait);
if (status) exit(status);
}
客户端
客户端作为接口的使用方,只需要在绑定服务端的句柄后,调用接口接口,等待远程过程返回后,释放掉这个句柄就可以了。
- RpcStringBindingCompose
- RpcBindingFromStringBinding
- 调用远程函数过程。
client.cpp
#include <stdlib.h>
#include <stdio.h>
#include <ctype.h>
#include "..\RpcServer\useless_h.h"
#include <windows.h>
void Rpc_port();
void Rpc_socket();
void main()
{
Rpc_port();
//Rpc_socket();
}
/******************************************************/
/* 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);
}
void Rpc_port()
{
printf("%s", "This is RPC port");
RPC_STATUS status;
unsigned char* pszUuid = NULL;
unsigned char* pszProtocolSequence_port = (unsigned char*)"ncacn_np";
unsigned char* pszEndpoint_port = (unsigned char*)"\\pipe\\useless";
unsigned char* pszNetworkAddress_port = NULL;
unsigned char* pszOptions = NULL;
unsigned char* pszStringBinding = NULL;
unsigned char* pszString = (unsigned char*)"hello, world";
unsigned long ulCode;
status = RpcStringBindingCompose(pszUuid,
pszProtocolSequence_port,
pszNetworkAddress_port,
pszEndpoint_port,
pszOptions,
&pszStringBinding);
if (status) exit(status);
status = RpcBindingFromStringBinding(pszStringBinding, &useless_IfHandle);
if (status) exit(status);
RpcTryExcept
{
UselessProc(pszString);
Shutdown();
}
RpcExcept(1)
{
ulCode = RpcExceptionCode();
printf("Runtime reported exception 0x%lx = %ld\n", ulCode, ulCode);
}
RpcEndExcept
status = RpcStringFree(&pszStringBinding);
if (status) exit(status);
status = RpcBindingFree(&useless_IfHandle);
if (status) exit(status);
exit(0);
}
void Rpc_socket()
{
printf("%s","This is RPC socket");
RPC_STATUS status;
unsigned char* pszUuid = NULL;
unsigned char* pszProtocolSequence_tcp = (unsigned char*)"ncacn_ip_tcp";
unsigned char* pszNetworkAddress_tcp = (unsigned char*)"192.168.138.128"; //服务器地址
unsigned char* pszEndpoint_tcp = (unsigned char*)"13521";
unsigned char* pszOptions = NULL;
unsigned char* pszStringBinding = NULL;
unsigned char* pszString = (unsigned char*)"hello, world";
unsigned long ulCode;
status = RpcStringBindingCompose(pszUuid,
pszProtocolSequence_tcp,
pszNetworkAddress_tcp,
pszEndpoint_tcp,
pszOptions,
&pszStringBinding);
if (status) exit(status);
status = RpcBindingFromStringBinding(pszStringBinding, &useless_IfHandle);
if (status) exit(status);
RpcTryExcept
{
UselessProc(pszString);
Shutdown();
}
RpcExcept(1)
{
ulCode = RpcExceptionCode();
printf("Runtime reported exception 0x%lx = %ld\n", ulCode, ulCode);
}
RpcEndExcept
status = RpcStringFree(&pszStringBinding);
if (status) exit(status);
status = RpcBindingFree(&useless_IfHandle);
if (status) exit(status);
exit(0);
}
测试结果
如果代码编译出现链接库不通过的错误,要在依赖项中添加如下lib文件。
本地调用 --利用
客户端传入hello world,服务端输出。
两个进程位于同一空间中(同一计算机)。
远程调用
用虚拟机模拟远程服务器,本机模拟客户端。
两个进程位于两个空间(不同计算机)
逆向分析RPC远程过程接口寻找过程
status = RpcServerRegisterIfEx(matthewRpc_v1_0_s_ifspec, NULL, NULL, RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, 0, NULL);
static const RPC_SERVER_INTERFACE matthewRpc___RpcServerInterface =
{
sizeof(RPC_SERVER_INTERFACE),
{{0xaaf3c26e,0x2970,0x42db,{0x91,0x89,0xf2,0xbc,0x0e,0x07,0x3e,0x7c}},{1,0}},
{{0x8A885D04,0x1CEB,0x11C9,{0x9F,0xE8,0x08,0x00,0x2B,0x10,0x48,0x60}},{2,0}},
(RPC_DISPATCH_TABLE*)&matthewRpc_v1_0_DispatchTable,
0,
0,
0,
&matthewRpc_ServerInfo,
0x04000000
};
RPC_IF_HANDLE matthewRpc_v1_0_s_ifspec = (RPC_IF_HANDLE)& matthewRpc___RpcServerInterface;
static const MIDL_SERVER_INFO matthewRpc_ServerInfo =
{
&matthewRpc_StubDesc,
matthewRpc_ServerRoutineTable,
useless__MIDL_ProcFormatString.Format,
matthewRpc_FormatStringOffsetTable,
0,
0,
0,
0};
函数实现
static const SERVER_ROUTINE matthewRpc_ServerRoutineTable[] =
{
(SERVER_ROUTINE)UselessProc,
(SERVER_ROUTINE)Shutdown
};