什么是RPC
RPC众所周知是远程过程调用,如何理解呢?首先什么是本地过程调用:就是任何代码触发了内存中(或者磁盘里)其他一段代码的执行,就是所谓了过程调用;而远程过程调用就是设备1上试图运行代码A,但是代码部署在设备2上,尝试在2上运行A的过程就是RPC。(我们最常见的前后端分离场景下,前端发Ajax触发后端crud就是一个RPC的实现)
Web开发中的RPC
笔者是Java初学者,以下web开发视角局限于Java
RPC的首次概念出现在:在微服务架构中,不同微服务间的相互调用需要一种比REST更轻的方式 —— 传统通过发HTTP来完成服务间的调用虽然可以实现,但是HTTP协议的冗余部分较多(或者说为了适配各种场景,设计就需要更普适一点)。
RPC的思路就是从自身微服务的业务角度出发,定义一套能够在方法调用方和方法提供方间明确语义的协议。
以下是一个标准定义的RPC过程
角色与数据流转如下:
- 客户端(client)以本地调用方式(即以接口的方式)调用服务;
- 客户端存根(client stub)接收到调用后,负责将方法、参数等组装成能够进行网络传输的消息体(将消息体对象序列化为二进制 byte[]);
- 客户端通过sockets将消息发送到服务端;
- 服务端存根( server stub)收到消息后进行解码(将消息对象反序列化);
- 服务端存根( server stub)根据解码结果调用本地的服务;
- 本地服务执行并将结果返回给服务端存根( server stub);
- 服务端存根( server stub)将返回结果打包成消息(将结果消息对象序列化);
- 服务端(server)通过sockets将消息发送到客户端;
- 客户端存根(client stub)接收到结果消息,并进行解码(将结果消息反序列化);
- 客户端(client)得到最终结果。
市面上许多RPC框架的作业,都是将2,3,4,7,8封装起来(包括基于HTTP协议的REST);
反向思考:RPC框架的作用是:给网络上传输的二进制流 在冗余信息量最小的情况下,赋予明确的意义
上述数据流转过程使用到如下技术 —— 动态代理、序列号、NIO、注册中心... 详情参考书籍
问题
- 市面有哪些RPC框架?
| 对比项 | Dubbo | gRPC | brpc | Thrift |
|---|---|---|---|---|
| 公司 | Ali | Baidu | ||
| 通讯协议 | tcp/http | http2 | 多种协议 | tcp/http |
| 序列化协议 | 可扩展 | protobuf | protobuf/json/mcpack | 可扩展 |
| 开发语言 | Java | 跨语言 | C++ / Java | 跨语言 |
| 主要特点 | 服务治理、扩展性 | 跨语言、性能 | 高性能、扩展性 | 跨语言 |
| github star | 36.9K | 33.5K | 12.9K | 8.9K |
着重介绍下一位学长参与的trpc
-
架构
主要特点:
-
明晰的分层 & 分模块
-
架构由“框架” & “插件”两部分组成。虚线框内为tRPC,中间红色实线框为框架核心部分,蓝色实线框为插件部分
将很多功能以插件的形式提供有很多好处:其实仔细一想这和Java世界依赖倒置是一样的,都是将上层和下层统一依赖于抽象。
举例而言:在之前高耦合性下的架构下,序列化方案想由JSON变成ProtoBuf,那可能会在代码中每个消息处理的位置都手动修改。
而TRPC的实现方式大致为: 将插件功能串在一条过滤器链filter chain中,通过AOP的方式给每个消息都赋予过滤器链上的功能/任务;详情后面细聊
插件能提供哪些功能?博客上将基本分为两类 —— 个性化需求(比如 校验,请求回访,故障注入)& 服务治理(比如 性能监控,鉴权,日志,调用链跟踪...)
-
-
性能据说很好
(测试机型与环境:测试机型:腾讯云标准型 SA2 CVM虚拟机,CPU处理器型号 AMD EPYC™ Rome,8核,2.60GHz,内存16G。测试场景和数据:吞吐测试:调用方的 P99 延时在 10ms 左右时,测量服务的 QPS。)
-
TODO
-
-
业务数据流转
大佬箴言:学习一个框架,最有效的方法就是先掌握它最基本的业务过程,其次再从各个流程分叉点延伸到各个角落,这样才不会在框架大量的代码中迷失了方向。
流程如下:
-
客户端通过tRPC框架 完成请求内容的内存结构化;示例代码:
-
客户端通过服务发现机制获取目标服务,也就是被调用方的网络地址和端口 示例代码如下:
SayHello中,调用c.client.Invoke()完成RPC调用,接下来看看Invoke
看到倒数第三行c.fixFilters(),是过滤器相关内容,接着进去
代码功能在过滤器链上添加了selectorFilter,猜测应该是负责服务发现的拦截器
回到2中的图 —— 倒数第一行callFunc是核心的调用逻辑
具体方法及功能就是RPC的基本数据流转模式
prepareRequestbuf,方法完成整个请求的序列化和编码opts.Transport.RoundTrip,完成传输层的调用opts.Codec.Decode(msg, rspbuf),完成响应消息的解码processResponseBuf函数,完成的是响应消息的反序列化等
完整功能:
细节:
-
序列化 & 压缩
tRPC-Go 框架早期内置支持 pb、json 和 jce 三种序列化格式,以及 gzip、snappy 压缩格式,但是以现在的版本看的话,已经在框架内部又增加了更多的格式。tRPC 生成的桩代码里,默认会使用 pb 序列化格式,并且不使用任何压缩算法:
要了解框架对于序列化和压缩的实现逻辑,我们需要在前面解读 tRPC-Go 请求过程源码中提到的
prepareRequestBuf函数着眼:tRPC-Go 框架实现将序列化和压缩逻辑放在了一个独立的函数
serializeAndCompress里//TODO
游戏开发中的RPC
游戏开发比传统Web开发似乎更需要高效的RPC通信方式,因为游戏的客户端和服务器交互更加频繁,不同业务的消息结构天差地别。
历史发展:
-
最早期的游戏框架,多数都是client—>server—>db的模式,但随着玩家数量的增长,一台server一个进程就会扛不住;
-
合理的想法是:根据游戏业务进行纵向拆分,比如说把聊天,场景,战斗,支付等部署到不同服务器上(微服务)这种思路直观看来又好 又好;
-
但是问题在于——游戏开发不同于Web开发,分service的开发十分困难,将传统基于进程的开发转变成基于service的开发更是困难。解决方案是:RPC
具体思路是:网关gateway中注册有各个service所提供的方法,客户端只需对gateway提出对某个方法的调用,即由底层完成对具体service的远程过程调用
某游戏项目中 RPC的具体使用
由于游戏业务和web业务有所不同,其架构和RPC调用模式也不同;同时由于游戏公司开源程度不高,各个公司或各个游戏的架构还有区别。以下是在不触及公司不可泄漏信息情况下的正常技术交流。
-
对上游而言,如何准确的调用想要的服务
在NPC场景下,例如Java的Spring项目,Spring的IOC容器中管理所有bean对象,由bean对象完成对内部方法的调用;RPC场景下的需求也是这样:需要一个功能来找到这些接口(或者是能找到接口的具体位置)
和Spring的思路一致,在项目启动时扫描全包,将有@Bean注解标识的类加载成bean,我们这里将声明RPC的方法加载成lua-table的一个元素。
-
玩家登录时,初始化Player的所有Component,过程中会收集Component的所有RPC接口
伪代码:
local ComponentManager = {} function ComponentManager:Init() self.modules = {} local modList = ServerUtil.GetModuleListByDir({'Component',"apps/GameServer/Game"}) -- 类似反射,获取目录下所有Component for i,modName in pairs(modList) do local mod = require(modName) self.modules[mod._name] = mod System.ImportRpcModule(modName) -- 手机具有RPC标记的接口位置 end self:InitComponentList() -- 根据组件列表,加载依赖 end -
在importRpcModule中,将接口的元数据加载如table
function System.ImportRpcModule(requirePath, modName) local module = require(requirePath) -- 注册RPC System.Register(requirePath, modName or module.__name, module) return module end -
将所有RPC接口平铺 / 按模块 / 某种高效查找的数据结构如搜索树 加载在内存 / 发给网关,调用方即可高效查询 + 调用
除此之外,RPC的使用往往会外加装饰器模式;换句话说,就是将普通的NPC代码通过装饰器模式赋予RPC的功能。(装饰器模式感觉天生适合NPC->RPC的升级)
- 对于装饰器模式,面向对象有面向对象的实现模板,面向函数也有面向函数的实现模板;但其基本思想都是用盔甲武装真身,但本质还是真身行动
- 在RPC中,装饰器模式的使用很优雅
RPC() function Test:TestFunc() -- 本地代码 end对于业务代码而言:只需要在本地方法上添加标记即可将方法注册到RPC服务; 之后对RPC接口的管理即可如上方所述@RPC public void test(){ // 本地代码 }
-
-
游戏行业分层有所不同
-
在Web开发中 服务端的服务又是无状态的,区分不同用户的方式是传入的Uid不同。
-
在游戏开发中 首先要明确以下几点:
- skynet框架底层客户端与服务器间通信使用UDP
- 服务端的服务是由具体的Player对象/线程调用的(这也就是在各种方法里不传入用户ID的原因);这和Java开发中 ThreadLocal的用法相近
那么问题来了:使用UDP没有维护连接,失去session存储信息的能力,那是怎么找到对应player线程的呢?
思路有点暴力,就是在UDP中携带用户信息,并在服务端抽象出一层来专门做Player映射。
所以这样一来,请求的调用层次就成了:客户端将Call的消息按照Sprotobuf协议发往Player层,Player层找到对应玩家的对象/线程,并识别出Call的参数,之后由rpc_dispatch模块寻找到具体服务位置,完成函数执行。
-
综上所述,RPC在Web开发和游戏开发中,解决的痛点有所不同;Web中是为了减少微服务间通信的成本,游戏中是为了统一各个服务中 能力的提供。但话又说回来,这只是我片面的理解,希望大佬多多指点