RPC初探

360 阅读6分钟

RPC是什么

远程过程调用(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而无需关注细节。RPC是一种服务器-客户端(Client/Server)模式。简而言之,RPC就是让远程函数能够像本地函数一样被调用。

RPC的工作原理

RPC调用过程

  1. Client以调用本地服务方式调用远程API

  2. Client Stub(客户端存根)

  3. 接受调用的参数和方法,将其封装成能够进行传输的消息体

  4. 完成网络寻址,定位对应的服务端

  5. 通过网络将对应的消息体发送到对应的服务端

  6. Server Stub负责接收消息,并解码成服务端能够识别的信息,调用对应的服务端方

  7. Server本地服务将调用结果发送给Server Stub

  8. Server Stub将返回结果包装成消息体返回给Client Stub

  9. Client Stub接收消息并进行解码

  10. Client获取到最终调用结果,完成一次RPC调用

面对的问题

举个🌰:client期望调用server的Add函数,返回传递过去的两个参数的和。要成功执行这个调用,相比于本地函数调用,RPC至少面对以下三个问题:
  1. callId映射:client如何知道我能执行哪些函数,以及告诉server我要执行的函数是Add的函数,而不是multipy或者其他函数。如果是本地函数调用,函数体是直接通过函数的引用(指针)来调用的,在RPC中,需要有函数和callId的映射机制来约束client和服务端的执行过程。
  2. 序列化和反序列化:在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种编程语言。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。
  3. 网络传输通常远程调用往往发生在不同服务之间,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。HTTP、TCP、UDP都可以用于RPC。
在Http调用中,同样会面临以上问题,http调用通过url来区分调用资源(对应callId映射),通过content-type&accept的header同步消息格式(对应序列化和反序列化),通过http协议来完成网络传输(对应网络传输)。当然除了这些问题之外,RPC调用可能还会面临如何处理网络错误,防止攻击,流量控制等等问题。

RPC框架

一般我们在使用RPC时,不会照着工作原理去实现上述这些功能,大多数开源的RPC框架已经将调用链路封装起来,客户端只需要关注**接口描述和调用**即可,服务端只需要关注**接口定义和实现**即可。
RPC框架
dubbon
rpcx
grpc
thrift
跨语言
Java
go
多语言
多语言
服务治理
多序列化框架支持
多种注册中心
管理中心
跨语言
出品方
阿里
Go生态圈
Google
Facebook & Apache

RPC和HTTP

  1. 客户端调用另外一个应用的方法:

  2. 可以直接发送 HTTP 请求,通过基于 Restful 风格的接口获取处理结果。

  3. 可以通过 RPC 请求,调用另一个应用的方法,而处理这个 RPC 请求的过程,可以使用 HTTP 协议来处理通信,也可以使用 TCP、UDP,大部分现有的 RPC 框架是基于TCP实现的,gRPC 是基于http2实现的。

  4. 一般客户端 Native开发(安卓,iOS)是支持发送 RPC 请求的,而浏览器暴露给开发者的处理通信接口只有 XHRHttpRequest、Fetch、WebSocket、RTCPeerConnection 等,不支持直接发起 RPC 调用。

  5. 基于 RPC 的 API 更加适用行为(也就是命令和过程),基于 Restful API 更加适用于构建模型(也就是资源和实体),处理 CRUD。

  6. HTTP 好比普通话,RPC 好比团伙内部黑话。讲普通话,好处就是谁都听得懂,谁都会讲。讲黑话,好处是可以更精简、更加保密、更加可定制,坏处就是要求大家都明白这个黑话(server 和 client 对接的IDL(interface description language)描述),而且一旦大家都说一种黑话了,换黑话就困难了。

  7. RPC 主要用于公司内部的服务调用,性能消耗低,传输效率高,实现复杂。HTTP 主要用于对外的异构环境,浏览器接口调用,App 接口调用,第三方接口调用等。

在Node中使用Thrift

基于官方的Thrift开发

  1. 安装thrift:brew install thrift​

  2. 编写一个 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);
    }
    
  3. 使用thrift生成rpc描述文件 ​thrift --gen js:node list.thrift​,会生成一个 gen-nodejs文件夹,一个是以 service 命名的 TAccountService.js,一个是以 thrift 文件命名的account_types.js 。分别包含了对接口和数据类型的定义。

  4. 引用 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();
        });
    });
    
  5. 分别运行 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 ,安全性也相对更好。

参考文档

  1. en.wikipedia.org/wiki/Remote…
  2. www.zhihu.com/question/25…
  3. zhuanlan.zhihu.com/p/148044947
  4. developer.51cto.com/art/201906/…
  5. developer.aliyun.com/article/342…
  6. zhuanlan.zhihu.com/p/29857744
  7. www.zhihu.com/question/41…
  8. developer.51cto.com/art/201906/…
  9. www.jianshu.com/p/6e5f6f128…