本文参考Oracle的RPC文档并对内容进行删改,版权所有为Oracle。
阅读本文需要对TCP/IP协议簇和linux系统有一定了解。
一、RPC介绍
1.1 RPC是什么
RPC (Remote Procedure Call):一个计算机通信协议,基于扩展常规或本地过程调用的概念。因此被调用过程不必与调用过程存在于同一地址空间中。这两个进程可能位于同一系统上,也可能位于不同的系统上,并通过网络连接它们。程序员就像调用本地程序一样,无需额外地为这个交互编程(无需关注细节)。
上图表示两个联网系统之间 RPC 调用流程。客户端进行过程调用,向服务器发送请求并阻塞等待,直到收到答复或请求超时。当请求到达时,服务器执行对应的请求过程,并将执行结果发送给客户端。客户端收到应答消息后被唤醒继续运行。
1.2 TI-RPC 相关说明
TI-RPC : Transport-independent RPC.
1.2.1 参数传递
TI-RPC允许一个参数从客户端传递到服务器。如果需要多个参数,则可以将这些参数组合成一个结构体,作为一个元素。从服务器传递给客户端的信息作为函数的返回值。
1.2.2 传输协议
传输协议指定了客户端和服务器之间如何传输call message 和 reply message。TS-RPC 使用 TCP 和 UDP 作为传输协议,但当前版本的 TI-RPC 是独立于传输的,因此它可以与任何传输协议一起使用。
您可以编写程序在特定传输类型上运行,或者在由系统选择或用户选择的传输上运行。两种网络选择机制是 /etc/netconfig
数据库文件和环境变量 NETPATH。这些机制可以精细控制使用哪种网络传输类型 - 用户可以指定首选传输,应用程序将在可以的情况下使用该传输。如果指定的传输不合适,应用程序会自动尝试具有正确特征的其他传输。
/etc/netconfig
列出了主机可用的传输方式并按类型标识它们。如果未设置 NETPATH,则系统默认使用/etc/netconfig
中指定的所有可见传输方式,按照它们在该文件中出现的顺序。
1.2.3 调用语义
调用语义定义了客户端可以对远程过程的执行做出何种假设;特别是,该过程执行了多少次。这些语义在处理错误情况时非常重要。三种选择分是恰好一次、 最多一次和至少一次。ONC+ 提供 至少一次语义。远程调用的过程是 幂等的。它们每次被调用时都应该返回相同的结果,即使经过多次迭代也是如此。
1.2.4 数据表示
数据表示描述了在进程之间传递参数和结果时所使用的格式。为了在各种系统架构上运行,RPC 需要标准的数据表示。TI-RPC 使用外部数据表示 (XDR)。XDR 是一种独立于系统的数据描述和编码协议。使用 XDR,RPC 可以处理任意数据结构,不必在意不同主机的字节顺序或结构布局约定如何。
1.3 远程调用标识
一个远程调用由以下三元组唯一标识:
- Program number :标识一个远程程序
- Procedure number:标识一个调用过程,即调用哪个函数
- Version number:一个调用过程可以有多个版本
说明:程序名到Program numbe的映射关系可参考 /etc/rpc
文件
1.4 动态端口
RPC服务监听的端口并不固定,客户端如果想要访问这些服务要先向rpcbind
守护进程查询对应服务监听的端口。rpcbind
监听TCP/UDP 111端口。rpcbind
是唯一具有已知端口的RPC 服务。
RPC服务(程序)启动后会向rpcbind
注册其自身信息,包括Program number、Version、Protocol type、Port。当客户端来查询时将这些信息回复给客户端。
const PMAP_PORT = 111; /* portmapper port number */
/*
* A mapping of (program, version, protocol) to port number
*/
struct pmap {
rpcprog_t prog;
rpcvers_t vers;
rpcprot_t prot;
rpcport_t port;
};
/*
* Supported values for the "prot" field
*/
const IPPROTO_TCP = 6; /* protocol number for TCP/IP */
const IPPROTO_UDP = 17; /* protocol number for UDP/IP */
/*
* A list of mappings
*/
struct pmaplist {
pmap map;
pmaplist *next;
};
/*
* Arguments to callit
*/
struct call_args {
rpcprog_t prog;
rpcvers_t vers;
rpcproc_t proc;
opaque args<>;
};
/*
* Results of callit
*/
struct call_result {
rpcport_t port;
opaque res<>;
};
/*
* Port mapper procedures
*/
program PMAP_PROG {
version PMAP_VERS {
void
PMAPPROC_NULL(void) = 0;
bool
PMAPPROC_SET(pmap) = 1;
bool
PMAPPROC_UNSET(pmap) = 2;
unsigned int
PMAPPROC_GETPORT(pmap) = 3;
pmaplist
PMAPPROC_DUMP(void) = 4;
call_result
PMAPPROC_CALLIT(call_args) = 5;
} = 2;
} = 100000;
二、RPC协议
2.1 协议概述
RPC 协议提供以下内容:
- 被调用过程的唯一规范。
- 将响应消息与请求消息进行匹配的规定。
- 提供对服务的调用者进行身份验证以及反向验证的规定,此外,RPC还提供以下功能的检测
- RPC协议不匹配
- 远程程序协议版本不匹配
- 协议错误,例如对过程参数指定不正确
- 远程认证失败的原因
考虑由两个程序组成的网络文件服务。一个程序可能处理高级应用程序,例如文件系统访问控制和锁定。另一个程序可能处理低级文件 I/O,并具有读取和写入等过程。网络文件服务的客户端系统将代表客户端系统上的某个用户调用与该服务的两个程序相关联的过程。在客户端-服务器模型中,使用远程过程调用来调用服务。
2.1.1 RPC 模型
RPC 模型类似于本地过程调用模型。在本地情况下,调用者将过程的参数放置在某个明确指定的位置。然后,调用者将控制权移交给过程,并最终重新获得控制权。此时,从明确指定的位置提取过程的结果,然后调用者继续运行。
RPC 模型与之类似,一个控制线程在逻辑上会经过两个进程。一个是调用者的进程,另一个是服务器的进程。从概念上讲,调用者进程向服务器进程发送调用消息并等待回复消息。调用消息包含过程的参数以及其他信息。回复消息包含过程的结果以及其他信息。收到回复消息后,将提取过程的结果并恢复调用者的执行。
在服务器端,进程处于休眠状态,等待调用消息的到来。当调用消息到达时,服务器进程提取过程的参数、计算结果、发送回复消息,然后等待下一个调用消息。
请注意,在此描述中,两个进程中只有一个在任何给定时间处于活动状态。但是,RPC 协议对实现并发模型没有任何限制。例如,实现可能选择让 RPC 调用异步,以便客户端可以在等待服务器回复时执行有用的工作。另一种可能性是让服务器创建一个任务来处理传入的请求,以便服务器可以自由接收其他请求。
2.1.2 传输和语义
RPC 协议独立于传输协议。也就是说,RPC 不考虑消息如何通过网络从一个进程传递到另一个进程。该协议仅对消息处理进行规范和解释。
RPC 不会尝试确保传输可靠性。因此,您必须向应用程序提供有关 RPC 下使用的传输协议类型的信息。如果您告诉 RPC 服务它在可靠的传输(如 TCP)上运行,则该服务的大部分工作已经完成。另一方面,如果 RPC 在不可靠的传输(如 UDP)上运行,则该服务必须设计自己的重新传输和超时策略。RPC 不提供此服务。
由于传输独立性,RPC 协议不会将特定语义附加到远程过程或其执行。语义可以从底层传输协议推断出来,但应由底层传输协议明确指定。例如,假设 RPC 在不可靠的传输上运行。如果应用程序在短时间超时后未收到回复而重新传输 RPC 消息,则它只能推断该过程已执行零次或多次。如果应用程序收到了回复,它可以推断该过程至少执行了一次。
服务器可以选择记住以前授予的来自客户端的请求,并且不再重新授予它们,以确保某种程度的“最多执行一次”语义。服务器可以使用与每个 RPC 请求一起打包的事务 ID 来实现这一点。此事务 ID 的主要用途是供 RPC 客户端匹配对请求的回复。但是,客户端应用程序可以选择在重新传输请求时重用其以前的事务 ID。服务器应用程序检查这一事实后,可以选择在授予请求后记住此 ID,并且不再重新授予具有相同 ID 的请求。服务器不允许以任何其他方式检查此 ID,除非将其作为相等性测试。
另一方面,如果使用可靠的传输(如 TCP),应用程序可以从回复消息中推断该过程只执行了一次。如果应用程序没有收到回复消息,它就不能假设远程过程没有执行。请注意,即使使用面向连接的协议(如 TCP),应用程序仍然需要超时和重新连接来处理服务器崩溃。
2.2 RPC消息字段说明
2.2.1 Program and Procedure Numbers
RPC call message具有三个无符号字段,用于唯一标识要调用的过程:
- Remote program number
- Remote program version number
- Remote procedure number
Program numbers以0x20000000
为一组分布,如下表所示。
Program Numbers | Description |
---|---|
00000000 - 1fffffff | Defined by host |
20000000 - 3fffffff | Defined by user |
40000000 - 5fffffff | Transient (reserved for customer-written applications) |
60000000 - 7fffffff | Reserved |
80000000 - 9fffffff | Reserved |
a0000000 - bfffffff | Reserved |
c0000000 - dfffffff | Reserved |
e0000000 - ffffffff | Reserved |
说明:
-
Oracle 负责管理第一组编号,这些编号对于所有客户来说都应该是相同的。如果客户开发的应用程序可能引起普遍的兴趣,那么应该为该应用程序分配第一个范围内的编号。
-
第二组数字是为特定客户应用程序保留的。此范围主要用于调试新程序。
-
第三组保留给动态生成程序编号的应用程序。
-
最后的组保留供将来使用,不应使用。
-
RPC Program Number由互联网编号分配机构 (IANA) 分配。获取程序编号分配的政策和程序在RFC 5531的第 13 节中进行了描述。已分配的 RPC 程序编号列表可在 IANA 网站 上找到。
**Version number **: 程序的首次实现很可能具有版本号 1。大多数新协议都会发展成为更好、更稳定、更成熟的协议。因此,call message的版本字段会标识调用者正在使用的协议的版本。版本号使得同一服务器进程使用新旧协议成为可能。
Procedure number: 标识要调用的过程。这些编号记录在各个程序的协议规范中。
2.2.2 RPC version number
正如Procedure有多个版本一样,RPC 协议本身也有多个版本。因此,call message中也包含 RPC 版本号,对于此处描述的 RPC 版本,该版本号始终等于 2。
对请求消息的回复消息具有足够的信息来区分以下错误情况:
- RPC 的远程实现不使用协议版本 2。返回支持的最低和最高 RPC 版本号。
- remote program在远程系统上不可用。
- remote program不支持请求的version number。返回支持的最低和最高remote program version number。
- 请求的procedure number不存在。此结果通常是调用方协议或编程错误。
- 服务器将remote procedure的参数解释为垃圾。同样,这种结果通常是由于客户端和服务之间的协议不一致造成的。
2.2.3 Authentication
作为 RPC 协议的一部分,提供对服务的调用者进行身份验证以及反向验证。call message 有两个身份验证字段,即credentials和verifier。reply message有一个身份验证字段,即verifier。RPC 协议规范将这三个字段定义为以下不透明类型。
enum auth_flavor {
AUTH_NONE = 0,
AUTH_SYS = 1,
AUTH_SHORT = 2,
AUTH_DES = 3,
/* and more to be defined */
};
struct opaque_auth {
enum auth_flavor; /* style of credentials */
u_int oa_length; /* not to exceed MAX_AUTH_BYTES */
caddr_t oa_base; /* address of more auth stuff */
};
说明:
-
opaque_auth结构是一个auth_flavor枚举,后面是RPC协议实现不透明的字节。
-
认证字段中包含的数据的解释和语义由独立的认证协议规范指定。有关各种身份验证协议的定义,后文有介绍。
-
如果认证参数被拒绝,则响应消息中应说明被拒绝的原因。
2.3 RPC消息格式
下面以 XDR 数据描述语言定义 RPC消息格式。该消息以自上而下的方式定义,如以下代码示例所示。
enum msg_type {
CALL = 0,
REPLY = 1
};
/*
* 对call message的回复可以采用两种形式:该消息被accepted or rejected
*/
enum reply_stat {
MSG_ACCEPTED = 0,
MSG_DENIED = 1
};
/*
* 假如call message已被accepted,则以下是
* 尝试调用remote procedure的状态。
*/
enum accept_stat {
SUCCESS = 0, /* RPC executed successfully */
PROG_UNAVAIL = 1, /* remote service hasn't exported program */
PROG_MISMATCH = 2, /* remote service can't support version # */
PROC_UNAVAIL = 3, /* program can't support procedure */
GARBAGE_ARGS = 4 /* procedure can't decode params */
};
/*
* call message被rejected的原因:
*/
enum reject_stat {
RPC_MISMATCH = 0, /* RPC version number != 2 */
AUTH_ERROR = 1 /* remote can't authenticate caller */
};
/*
* authentication失败的原因:
*/
enum auth_stat {
AUTH_BADCRED = 1, /* bad credentials */
AUTH_REJECTEDCRED = 2, /* clnt must do new session */
AUTH_BADVERF = 3, /* bad verifier */
AUTH_REJECTEDVERF = 4, /* verify expired or replayed */
AUTH_TOOWEAK = 5 /* rejected for security */
};
/*
* RPC消息:
* 1. 所有消息都以事务标识符 xid 开头,后面跟着一个联合体,联合体的判断标准是msg_type,
* 根据它的值切换到两种消息类型之一。
*
* 2. reply message的 xid 总是与call message的xid匹配。注意:xid 字段仅用于客户端将
* reply message与call message进行匹配或者用于服务器检测重传;服务端不能将此ID视为任何类型的序列号。
*/
struct rpc_msg {
unsigned int xid;
union switch (msg_type mtype) {
case CALL:
call_body cbody;
case REPLY:
reply_body rbody;
} body;
};
/*
* Body of an RPC request call:
* 1. 在RPC协议V2版中,rpcvers必须等于 2。字段 prog、vers 和 proc 指定远程程序,其版本号和程序内部的过程。
*
* 2. 紧接着是两个认证参数:cred(认证凭证 和 verf(身份验证验证器)。两个身份验证参数
*
* 3. 最后面参数由具体的程序协议指定。
*/
struct call_body {
unsigned int rpcvers; /* must be equal to two (2) */
unsigned int prog;
unsigned int vers;
unsigned int proc;
opaque_auth cred;
opaque_auth verf;
/* procedure specific parameters start here */
};
/*
* Body of a reply to an RPC request:
* call messag已被accepted or rejected。
*/
union reply_body switch (reply_stat stat) {
case MSG_ACCEPTED:
accepted_reply areply;
case MSG_DENIED:
rejected_reply rreply;
} reply;
所有消息都以事务标识符 xid 开头,后面跟着一个联合体,联合体的判断标准是msg_type,
/*
* 回复被服务器accepted的RPC请求,说明:即使请求被接受,也可能是错误。
* 1. 第一个字段是服务器生成的身份验证验证器,用于向调用者验证自身
*
* 2. 后面跟着一个联合体,其判断标准是accept_stat。
*/
struct accepted_reply {
opaque_auth verf;
union switch (accept_stat stat) {
case SUCCESS:
opaque results[0];
/* procedure-specific results start here */
case PROG_MISMATCH:
struct {
unsigned int low;
unsigned int high;
} mismatch_info;
default:
/*
* Void. Cases include PROG_UNAVAIL, PROC_UNAVAIL, and
* GARBAGE_ARGS.
*/
void;
} reply_data;
};
/*
* 回复被服务器rejected的RPC请求:
* 请求可能因两个原因被拒绝:
* 1.服务器没有运行兼容版本的RPC协议(RPC_MISMATCH),返回服务器支持的最低和最高RPC版本号
*
* 2.服务器身份验证被拒绝(AUTH_ERROR),返回拒绝状态码
*/
union rejected_reply switch (reject_stat stat) {
case RPC_MISMATCH:
struct {
unsigned int low;
unsigned int high;
} mismatch_info;
case AUTH_ERROR:
auth_stat stat;
};
2.4 粘包问题
当 RPC 消息基于字节流传输(如 TCP)传递时,要将一个RPC消息与另一个RPC消息分隔开。RPC使用record marking (RM)解决这个问题。一个record即一个RPC消息,由一个或多个record fragments组成。record fragments有一个4字节大小的fragment header,后面跟着0
到`(2*31) - 1 字节的fragment数据。这些字节编码一个无符号二进制数,类似于XDR整数,字节顺序是网络字节顺序。
fragment header:
- 布尔值,指定该fragment是否为record的最后一个fragment。位值 1 表示该fragment是最后一个fragment。
- 31 位无符号二进制值,表示fragment数据的长度(以字节为单位)。布尔值是header的最高位bit。长度是 31 个低位bit。此record规范不是 XDR 标准格式。
2.5 Authentication Protocols
2.5.1 AUTH_NONE Authentication
调用者不进行身份验证,服务器也不理会调用者是谁。在这种情况下,RPC消息的credentials、verifier和response verifier的flavor值是AUTH_NONE。即opaque_auth中auth_flavor值。在使用AUTH_NONE身份验证类型时,opaque_auth中oa_length为0。
2.5.2 AUTH_SYS Authentication
AUTH_SYS与以前称为 AUTH_UNIX 的身份验证风格相同。远程过程的调用者可能希望使用传统的UNIX进程权限身份验证来标识自己。这种RPC call message的opaque_auth中auth_flavor值是AUTH_SYS。oa_base的字节编码为以下结构:
/*
* UNIX style credentials.
*/
struct auth_sysparms {
unsigned int stamp; // credentials creation time
string machine_name<255>; // caller system的名字,最多不超过255字节
uid_t uid; // caller的有效用户ID
gid_t gid; // caller的有效组ID
gids_t gids; // caller所属于所有组
};
string machine_name{
unsigned int name_len; // 主机名长度
char* name; // 主机名
char* padding; // machine_name大小基于4字节对齐
}
struct gids_t{
unsigned int gid_num; // 所属组个数,最多不能超过10个
gid_t gid<10>;
}
2.5.3 AUTH_SHORT Verifier
当使用 AUTH_SYS 身份验证时, 来自服务器的reply message中response verifier类型可能是AUTH_NONE 或 AUTH_SHORT。如果是 AUTH_SHORT,response verifier字符串字节编码为short_hand_verf结构。这个不透明的结构现在可以传递给服务器,而不是原始的AUTH_SYS credentials。
说明:
- 服务器会保留一个缓存,将简短不透明结构映射到调用者的original credentials。这些结构通过 AUTH_SHORT类型的response verifier返回。调用者可以使用new credentials节省网络带宽和服务器 CPU 周期。
- 服务器可以随时刷新简短不透明结构。如果发生刷新,则远程过程call message 会因身份验证错误而被拒绝。失败的原因是
AUTH_REJECTEDCRED
。此时,调用者可能会尝试原始的 AUTH_SYS 类型的credentials。
2.5.4 AUTH_DES Authentication
使用 AUTH_SYS 身份验证的情况:1. 如果同一网络上有不同操作系统的系统,则无法保证呼叫者识别的唯一性。2. 由于不存在verifier,因此credentials很容易被伪造。AUTH_DES身份验证就是为了解决这两个问题,AUTH_DES比较复杂,有兴趣可以自行了解。
扩展
RPC请求消息与响应消息数据包
下面捕捉的是客户端向远程服务器的rpcbind
查询mount程序的端口所产生的RPC数据包
RPC 请求消息
RPC 响应消息
一个简单portmapper程序源码
可参考libnfs项目下examples/portmap-server.c
文件