首先说下什么是RPC?RPC全称Remote Procedure Call,即远程过程调用。
RPC的主要目标是让构建分布式计算(应用)更容易、透明,在提供强大的远程调用能力时不损失本地调用的语义简洁性。为实现该目标,RPC框架需提供一种透明调用机制,让使用者不必显式的区分本地调用和远程调用。
RPC框架负责屏蔽底层的传输方式(TCP或者UDP)、序列化方式(XML/JSON/二进制)和通信细节。
我在面试中常常会问:既然已经有http协议了,为什么我们还需要RPC方式来实现分布式系统?
(更多大厂RPC常考内容可以来看我讲的《分布式RPC服务框架精讲》,从零开始快速掌握RPC框架工作原理和实操技巧)
这里就要说到RPC最大的优势:易用性。使用http还需要搭配web服务,RPC可以避免这些问题,无感。
如果没有RPC协议的封装,我们将面临下面的困境:
- 以一种对方能够理解的协议构建请求包
- 将这个请求包设法发送到对方机器的进程上
- 对方机器根据协议理解请求包的内容
- 对方机器处理请求,并以客户机能够理解的协议构造回复包
- 想办法将这个会风暴,发送到请求包来源的远程机器上的具体进程,并设法把请求包和回复包关联起来
典型的RPC框架介于传输层和应用中间,可以帮助处理以下问题:
- 可靠性:“吃掉”传输层遇到的错误
- 平台无关性:比如Windows平台是否可以和Linux平台进行通讯?64位得系统是否可以和32位系统进行通讯?
- 服务发现和路由选择:RPC调用实际上是对某个服务的调用,那么RPC框架需要解决具体调用需要落到哪台机器的哪个进程上。
- 消息分发:一般一个进程上会提供多种RPC调用,RPC框架需要提供区别不同类型RPC消息并转到相应处理函数上。
然后说下RPC框架的核心部分:
- RPC接口
因为RPC的作用:让使用者调用远程请求时,就像调用本地函数
所以不管是本地客户还是远程服务端,需要使用一套统一的接口,然后两边分别实现自己的逻辑。
举例:设计一个获取部门员工列表的RPC请求,接口可能如下设计,传入一个部门ID,返回部门成员的列表:
viod getEmployeeList(const DepartmentID &id, EmployeeList &list);
然后在客户端部分根据接口可以这样实现:
clientVoidGetEmployeeList(const DepartmentID &id, EmployeeList &list){
auto result = Transport.send(GET_EMPLOYEE_LIST, encode(id));
list.decode(result.buffer());
}
接口的作用:告诉客户端你只能这样调用这个RPC,同时告诉服务器客户端那边只会这样调用。
具体RPC实现者只需要关心接口和服务端的实现逻辑。一般RPC框架会提供友好的封装,最简单的形式:只需要实现一个函数。函数的签名就是接口,实现就是服务端的代码,客户端的代码则由框架自动填充
- 对象序列化和反序列化
对象序列化的方式有很多种,比如比较通用的JSON、Protocol Buffers等,也可以按照自己的需求自己定制序列化和反序列化方式。
- 传输协议和传输层
用得比较多的比如TCP、UDP这类的,也有为了追求性能选择RDMA/RoCE/DPDK, 或者想gRPC那样更上层的HTTP2。
传输层的选择需要考虑几个因素:
- 物理限制,有些协议需要在特定的硬件环境中才能运行
- 传输特性,比如TCP协议对可靠性有一定保证,但需要用户自己处理黏包问题
- 性能、安全性、是否好调试等因素也可以进行参考
- RPC消息分发
当服务端收到一个RPC消息后,需要根据RPC类型和相应规则分发请求到相应的处理函数上。
常见的做法:
- 暴力switch/if else,由编译器做这方面的优化
- 实现查找表,以RPC类型(常见为枚举类型)作为下标定位到处理函数
- HashMap,这个方式最为灵活,前两种方案的实现都有一定的局限性
关于RPC框架和分布式计算问题处理,以及RPC框架设计实现的内容,可以来免费试听《大厂常考分布式RPC服务框架精讲》。
转载:https://blog.csdn.net/JiuZhang_ninechapter/article/details/108327491