一个入门rpc框架的学习
参考
简介
- RPC,即 Remote Procedure Call(远程过程调用),说得通俗一点就是:调用远程计算机上的服务,就像调用本地服务一样。
- RPC 可基于 HTTP 或 TCP 协议,web Service 就是基于 HTTP 协议的 RPC,
- 它具有良好的跨平台性,但其性能却不如基于 TCP 协议的 RPC。会两方面会直接影响 RPC 的性能,一是传输方式,二是序列化。
- 众所周知,TCP 是传输层协议,HTTP 是应用层协议,而传输层较应用层更加底层,
- 在数据传输方面,越底层越快,因此,在一般情况下,TCP 一定比 HTTP 快。
- 就序列化而言,Java 提供了默认的序列化方式,但在高并发的情况下,
- 这种方式将会带来一些性能上的瓶颈,于是市面上出现了一系列优秀的序列化框架,比如:Protobuf、Kryo、Hessian、Jackson 等,
- 它们可以取代 Java 默认的序列化,
- 从而提供更高效的性能。
- 为了支持高并发,传统的阻塞式 IO 显然不太合适,因此我们需要异步的 IO,即 NIO。
- Java 提供了 NIO 的解决方案,Java 7 也提供了更优秀的 NIO.2 支持,用 Java 实现 NIO 并不是遥不可及的事情,只是需要我们熟悉 NIO 的技术细节。
- 我们需要将服务部署在分布式环境下的不同节点上,通过服务注册的方式,
- 让客户端来自动发现当前可用的服务,并调用这些服务。
- 这需要一种服务注册表(Service Registry)的组件,让它来注册分布式环境下所有的服务地址(包括:主机名与端口号)。
- 应用、服务、服务注册表之间的关系见下图:
- 每台 Server 上可发布多个 Service,这些 Service 共用一个 host 与 port,
- 在分布式环境下会提供 Server 共同对外提供 Service。此外,为防止 Service Registry 出现单点故障,因此需要将其搭建为集群环境。
- 本文将为您揭晓开发轻量级分布式 RPC 框架的具体过程,
- 该框架基于 TCP 协议,提供了 NIO 特性,提供高效的序列化方式,同时也具备服务注册与发现的能力。
- 根据以上技术需求,我们可使用如下技术选型:
- Spring:它是最强大的依赖注入框架,也是业界的权威标准。
- Netty:它使 NIO 编程更加容易,屏蔽了 Java 底层的 NIO 细节。
- Protostuff:它基于 Protobuf 序列化框架,面向 POJO,无需编写 .proto 文件。
- ZooKeeper:提供服务注册与发现功能,开发分布式系统的必备选择,同时它也具备天生的集群能力。
源代码目录结构
- rpc-client
- 实现了 rpc 的服务动态代理 (RpcProxy) 以及基于 Netty 封装的一个客户端网络层(RpcClient)
- rpc-common
- 封装了 RpcRequest 和 RpcResponse,即 rpc 请求和响应的数据结构
- 基于 Netty 提供了编解码器
- 提供了序列化反序列化等工具
- rpc-registry
- rpc-registry-zookeeper
- rpc-server
- rpc 服务器 (RpcServer) 的实现,用来监听 rpc 请求以及向 Zookeeper 注册服务地址
- rpc 服务本地调用
- rpc-sample-api
- rpc-sample-client
- rpc-sample-server
启动顺序
- 配置 Zookeeper
- 解压 zookeeper-3.4.9
- 进入 conf 目录,重命名 zoo_sample.cfg 为 zoo.cfg(或者复制一份重命名)并修改一些配置选项如 dataDir. 另外可以看到默认的 clientPort 是 2181
- 将 bin 目录加入环境变量 PATH,这样则可直接使用 zkServer 命令直接启动
- 启动 rpc-sample-server 工程的下 RpcBootstrap
- 启动 rpc-sample-client 工程下的测试程序 HelloClient 等
关键实现和核心模块分析
- RpcBootstrap
- 加载 spring.xml 实例化 RpcServer
- 两个参数分别是 rpc 服务地址 (127.0.0.1:8000) 和基于 ZooKeeper 的服务注册接口实现(使用 ZkClient 连接 Zookeeper 的 2181 端口)
- 加载过程中,会首先调用 setApplicationContext 方法
- , 本例是 HelloServiceImpl 和 HelloServiceImpl2, 即有两个 rpc 服务类,其中 HelloServiceImpl2 加了一个版本号用来区分第一个服务类,扫描后放入 handlerMap,即服务名和服务对象之间的映射 map
- 加载后,调用 afterPropertiesSet 方法
- 启动 Netty 服务端,监听 8000 端口; channelpipeline 增加编解码器 (RpcDecoder、RpcEncoder) 和逻辑处理类(RpcServerHandler)
- RpcEncoder, 编码器,消息格式为 4 个字节的消息长度和消息体; 直接使用 Protostuff 进行序列化, 编码对象为 RpcResponse
- RpcDecoder, 解码器; 已解决粘包问题; 使用 Objenesis 和 Protostuff 继续反序列化
- RpcServerHandler, 收到 RpcRequest 后直接从 handlerMap 找到对应的服务类反射进行方法调用 (使用了 CGLib); 最后向客户端写入 rpc 响应,完毕则主动关闭连接(所以从这里看是短连接)
- ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE)
- 这行代码相当于在 rpc 响应发送的这个操作完成之后关闭连接
- 注意 Netty 强烈建议直接通过添加监听器的方式获取 I/O 操作结果; 当 I/O 操作完成之后,I/O 线程会回调 ChannelFuture 中 GenericFutureListener#operationComplete 方法
- 绑定端口成功后,向 Zookeeper 注册上面的两个 rpc 服务
- ChannelFutureListener CLOSE = new ChannelFutureListener() {
- @Override
- public void operationComplete(ChannelFuture future) {
- future.channel().close();
- }
- };
- RpcProxy
- 初始化亦通过加载 spring.xml,指定了基于 zookeeper 的服务发现类 ZooKeeperServiceDiscovery
- create 方法,主要使用了 jdk 的动态代理; 当代理方法执行的时候,首先根据请求的服务名利用 Zookeeper 的服务发现接口获取服务的 address; 然后封装 rpc 请求调用 Netty 客户端连接服务地址并发送
- 关于 RpcClient,同 Netty 服务端,需要设置 channelpipeline 的编解码器和逻辑处理 handler
- Channel channel = future.channel();
channel.writeAndFlush(request).sync();
channel.closeFuture().sync();
return response;
- 注意上部分代码,发送 rpc 请求后等待发送完毕;发送完毕后等待连接关闭,此时线程阻塞直到服务端发送完回复消息并主动关闭连接,线程继续;所以这个例子并没有会有 request 对不上 reponse 的问题,因为每次 rpc 调用都是短连接且当前执行线程挂起;另外服务端收到 request 的时候,也会用 requestId 作为 response 的 requestId
可改进地方
- 本人觉得 spring 相对较厚重,所以将 spring 去掉,对象实例化和依赖注入用比较简单的方式去处理;要手动处理
- 目前该示例提供的两个服务均是在同一个端口 8000 下的服务;如何测试不同的两个服务在不同的端口?按照该例子的设计,一个 RpcServer 即一个 rpc 发布服务器,该监听的端口下可以注册不同很多服务(当然一个 Netty server 本身可以 bind 多个端口, 这个暂时不考虑实现);如果需要增加不同的服务,则需要单独启动 RpcServer 并向 Zookeeper 注册
其他待调研 rpc 框架
来源: http://www.bubuko.com/infodetail-1977683.html