RPC是什么
RPC的工作原理
RPC调用过程
-
Client以调用本地服务方式调用远程API
-
Client Stub(客户端存根)
-
接受调用的参数和方法,将其封装成能够进行传输的消息体
-
完成网络寻址,定位对应的服务端
-
通过网络将对应的消息体发送到对应的服务端
-
Server Stub负责接收消息,并解码成服务端能够识别的信息,调用对应的服务端方
-
Server本地服务将调用结果发送给Server Stub
-
Server Stub将返回结果包装成消息体返回给Client Stub
-
Client Stub接收消息并进行解码
-
Client获取到最终调用结果,完成一次RPC调用
面对的问题
- callId映射:client如何知道我能执行哪些函数,以及告诉server我要执行的函数是Add的函数,而不是multipy或者其他函数。如果是本地函数调用,函数体是直接通过函数的引用(指针)来调用的,在RPC中,需要有函数和callId的映射机制来约束client和服务端的执行过程。
- 序列化和反序列化:在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种编程语言。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。
- 网络传输:通常远程调用往往发生在不同服务之间,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。HTTP、TCP、UDP都可以用于RPC。
RPC框架
RPC框架
|
dubbon
|
rpcx
|
grpc
|
thrift
|
跨语言
|
Java
|
go
|
多语言
|
多语言
|
服务治理
|
✅
|
✅
|
❌
|
❌
|
多序列化框架支持
|
✅
|
✅
|
❌
|
❌
|
多种注册中心
|
✅
|
✅
|
❌
|
❌
|
管理中心
|
✅
|
✅
|
❌
|
❌
|
跨语言
|
❌
|
❌
|
✅
|
✅
|
出品方
|
阿里
|
Go生态圈
|
Google
|
Facebook & Apache
|
RPC和HTTP
-
客户端调用另外一个应用的方法:
-
可以直接发送 HTTP 请求,通过基于 Restful 风格的接口获取处理结果。
-
可以通过 RPC 请求,调用另一个应用的方法,而处理这个 RPC 请求的过程,可以使用 HTTP 协议来处理通信,也可以使用 TCP、UDP,大部分现有的 RPC 框架是基于TCP实现的,gRPC 是基于http2实现的。
-
一般客户端 Native开发(安卓,iOS)是支持发送 RPC 请求的,而浏览器暴露给开发者的处理通信接口只有 XHRHttpRequest、Fetch、WebSocket、RTCPeerConnection 等,不支持直接发起 RPC 调用。
-
基于 RPC 的 API 更加适用行为(也就是命令和过程),基于 Restful API 更加适用于构建模型(也就是资源和实体),处理 CRUD。
-
HTTP 好比普通话,RPC 好比团伙内部黑话。讲普通话,好处就是谁都听得懂,谁都会讲。讲黑话,好处是可以更精简、更加保密、更加可定制,坏处就是要求大家都明白这个黑话(server 和 client 对接的IDL(interface description language)描述),而且一旦大家都说一种黑话了,换黑话就困难了。
-
RPC 主要用于公司内部的服务调用,性能消耗低,传输效率高,实现复杂。HTTP 主要用于对外的异构环境,浏览器接口调用,App 接口调用,第三方接口调用等。
在Node中使用Thrift
基于官方的Thrift开发
-
安装thrift:
brew install thrift
-
编写一个 IDL 文件,thrift 的 IDL 文件是以 .thrift 结尾的文件,一般包含 struct 和 service 定义,service 定义了服务暴露的可调用接口,struct 定义了接口数据格式,例如下面的account.thrift
// account.thrift struct TAccount { 1: i64 uid, 2: string name, 3: string gender, } service TAccountService { void addAccount(1: TAccount tAccount); TAccount getAccount(1: i64 uid); }
-
使用thrift生成rpc描述文件
thrift --gen js:node list.thrift
,会生成一个 gen-nodejs文件夹,一个是以 service 命名的 TAccountService.js,一个是以 thrift 文件命名的account_types.js 。分别包含了对接口和数据类型的定义。 -
引用 thrift npm 包,利用刚才的文件创建 server 和 client 。
// service.js const thrift = require('thrift'); // 引入生成的struct和service方法 const TAccountService = require('./gen-nodejs/TAccountService'); // 模拟数据库,本地创建一个对象,用作存储account const accounts = {}; // 开启rpc server const server = thrift.createServer(TAccountService, { addAccount(account, result) { console.log('server addAccount:', account.uid); accounts[account.uid] = account; result(null); }, getAccount(uid, result) { console.log('server getAccount:', uid); result(null, accounts[uid]); }, }); server.listen(6789); // client.js const thrift = require('thrift'); // 引入生成的struct和service方法 const TAccountService = require('./gen-nodejs/TAccountService'), TAccountTypes = require('./gen-nodejs/account_types'); // 建立连接和初始化client const connection = thrift.createConnection('localhost', 6789), client = thrift.createClient(TAccountService, connection); const account = new TAccountTypes.TAccount({ uid: 1, name: 'Kobe', gender: 'male' }); // 监听error事件,node的NodeJS.EventEmitter类的on方法 connection.on('error', err => { console.error(err); }); // 调用server端的createAccount方法 client.addAccount(account, err => { if (err) { console.error(err); return; } console.log('client addAccount:', account.uid); // 获取刚刚”新建“的account client.getAccount(account.uid, (err, resp) => { if (err) { console.error(err); return; } console.log(`client getAccount: uid=${resp.uid}, name=${resp.name}, gender=${resp.gender}`); connection.end(); }); });
-
分别运行 node service.js 和 node client.js之后可以得到如下接口
// service侧 server addAccount: { buffer: <Buffer 00 00 00 00 00 00 00 01>, offset: 0 } server getAccount: { buffer: <Buffer 00 00 00 00 00 00 00 01>, offset: 0 } // client侧 client addAccount: 1 client getAccount: uid=1, name=Kobe, gender=male
相比于 HTTP ,RPC 调用能更好的解耦服务,常被用在分布式计算中,由于少了对高层请求头的封装,相对 HTTP 来说,RPC 有更好的速度和为稳定性。由于调用需要有约定的 IDL ,安全性也相对更好。