最后更新:2022-03-05 19:24:30 手机定位技术交流文章
本文主要论述的是“RPC 实现原理”,那么首先明确一个问题什么是 RPC 呢?RPC 是 Remote Procedure Call 的缩写,即,远程过程调用。RPC 是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而开发人员无需额外地为这个交互编程。
值得注意是,两个或多个应用程序都分布在不同的服务器上,它们之间的调用都像是本地方法调用一样。接下来我们便来分析一下一次 RPC 调用发生了些什么?
一次基本的 RPC 调用会涉及到什么?
现在业界内比较流行的一些 RPC 框架,例如 Dubbo 提供的是基于接口的远程方法调用,即客户端只需要知道接口的定义即可调用远程服务。在 Java 中接口并不能直接调用实例方法,必须通过其实现类对象来完成此操作,这意味着客户端必须为这些接口生成代理对象,对此 Java 提供了 Proxy、InvocationHandler 生成动态代理的支持;
生成了代理对象,那么每个具体的发方法是怎么调用的呢?jdk 动态代理生成的代理对象调用指定方法时实际会执行 InvocationHandler 中定义的 #invoke 方法,在该方法中完成远程方法调用并获取结果。
抛开客户端,回过头来看 RPC 是两台计算机间的调用,实质上是两台主机间的网络通信,涉及到网络通信又必然会有序列化、反序列化,编解码等一些必须要考虑的问题;同时实际上现在大多系统都是集群部署的,多台主机/容器对外提供相同的服务,如果集群的节点数量很大的话,那么管理服务地址也将是一件十分繁琐的事情,常见的做法是各个服务节点将自己的地址和提供的服务列表注册到一个 注册中心,由 注册中心 来统一管理服务列表;
这样的做法解决了一些问题同时为客户端增加了一项新的工作——那就是服务发现,通俗来说就是从注册中心中找到远程方法对应的服务列表并通过某种策略从中选取一个服务地址来完成网络通信。
聊了客户端和 注册中心,另外一个重要的角色自然是服务端,服务端最重要的任务便是提供服务接口的真正实现并在某个端口上监听网络请求,监听到请求后从网络请求中获取到对应的参数(比如服务接口、方法、请求参数等),再根据这些参数通过反射的方式调用接口的真正实现获取结果并将其写入对应的响应流中。
综上所述,一次基本的 RPC 调用流程大致如下:

基本实现
| 服务端(生产者)
服务接口
在 RPC 中,生产者和消费者有一个共同的服务接口 API。如下,定义一个 HelloService 接口。
服务实现
生产者要提供服务接口的实现,创建 HelloServiceImpl 实现类。
服务注册
本例使用 Spring 来管理 bean,采用自定义 xml 和解析器的方式来将服务实现类载入容器(当然也可以采用自定义注解的方式,此处不过多论述)并将服务接口信息注册到注册中心。
首先自定义xsd,
分别指定 schema 和 xmd,schema 和对应 handler 的映射:
schema
handler
将编写好的文件放入 classpath 下的 META-INF 目录下:

在 Spring 配置文件中配置服务类:
编写对应的 Handler 和 Parser:
StormServiceNamespaceHandler
ProviderFactoryBeanDefinitionParser
ProviderFactoryBean
至此服务实现类已被载入 Spring 容器中,且服务接口信息也注册到了注册中心。
网络通信
作为生产者对外提供 RPC 服务,必须有一个网络程序来来监听请求和做出响应。在 Java 领域 Netty 是一款高性能的 NIO 通信框架,很多的框架的通信都是采用 Netty 来实现的,本例中也采用它当做通信服务器。
构建并启动 Netty 服务监听指定端口:
上面的代码中向 Netty 服务的 pipeline 中添加了编解码和业务处理器,当接收到请求时,经过编解码后,真正处理业务的是业务处理器,即NettyServerInvokeHandler, 该处理器继承自SimpleChannelInboundHandler, 当数据读取完成将触发一个事件,并调用NettyServerInvokeHandler#channelRead0方法来处理请求。
此处还有部分细节如自定义的编解码器等,篇幅所限不在此详述,继承 MessageToByteEncoder 和 ByteToMessageDecoder 覆写对应的 encode 和 decode 方法即可自定义编解码器,使用到的序列化工具如 Hessian/Proto 等可参考对应的官方文档。
请求和响应包装
为便于封装请求和响应,定义两个 bean 来表示请求和响应。
请求:
响应:
| 客户端(消费者)
客户端(消费者)在 RPC 调用中主要是生成服务接口的代理对象,并从注册中心获取对应的服务列表发起网络请求。
客户端和服务端一样采用 Spring 来管理 bean 解析 xml 配置等不再赘述,重点看下以下几点:
通过 jdk 动态代理来生成引入服务接口的代理对象
从注册中心获取服务列表并依据某种策略选取其中一个服务节点
通过 Netty 建立连接,发起网络请求
Netty 的响应是异步的,为了在方法调用返回前获取到响应结果,需要将异步的结果同步化。
Netty 异步返回的结果存入阻塞队列
请求发出后同步获取结果
测 试
Server
Client
结 果
| 生产者

| 消费者

| 注册中心

总 结
本文简单介绍了 RPC 的整个流程,并实现了一个简单的 RPC 调用。希望阅读完本文之后,能加深你对 RPC 的一些认识。
- 生产者端流程:
加载服务接口,并缓存
服务注册,将服务接口以及服务主机信息写入注册中心(本例使用的是 zookeeper)
启动网络服务器并监听
反射,本地调用
- 消费者端流程:
代理服务接口生成代理对象
服务发现(连接 zookeeper,拿到服务地址列表,通过客户端负载策略获取合适的服务地址)
远程方法调用(本例通过 Netty,发送消息,并获取响应结果
限于篇幅,本文代码并不完整,如有需要,访问:
https://github.com/fankongqiumu/storm.git
获取完整代码。
如有错误之处,还望大家指正。
文章来源:https://xiaomi-info.github.io/2020/03/02/rpc-achieve/
本文由 在线网速测试 整理编辑,转载请注明出处。