关注公众号:离心计划,一起离开地球表面呀
【RPC系列合集】
作者:离心计划
链接:juejin.cn/post/715165…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
| 前言
上一节【专栏】RPC系列(番外)-“土气”的IO实现 中我们用了Java中最朴素的方式实现了在番外篇【专栏】RPC系列(番外)-IO模型与线程模型 中的各种IO模型与线程模型,大家应该有了一个基本的印象,计算机世界的产物本质上都是封装,如果有一个框架帮我们做掉了复杂的连接管理、IO模型以及线程模型等等,甚至提供了一系列友好的API,那就万事大吉了,Netty就是这样的一个网络框架,它强大到各种你熟悉的分布式框架都会使用它作为网络层工具,比如Dubbo、RocketMq等等,那么在我们Sparrow-Rpc中也会使用Netty帮我们处理请求/响应这一基本通信方式,首先我们介绍一下Netty中的各种组件。
| Netty组件
Channel
Channel 是NIO中的概念,在前一节代码的代码中也有体现,Channel是对底层操作系统Socket的封装抽象,提供了java层操作Socket的Api,Channel 的生命周期包括:
-
ChannelUnregisteredChannel 已经被创建,但还未注册到EventLoop
-
ChannelRegisteredChannel 已经被注册到了EventLoop
-
ChannelActiveChannel 处于活跃状态,允许进行收发数据
-
ChannelInactiveChannel 还没连接成功
ChannelFuture
Netty中对于Channel的IO操作都是异步的,都会返回ChannelFuture对象并可以通过addListener方法给这个IO操作注册一个ChannelFutureListener用于IO事件回调
ChannelPipline
一个channel对应一个pipline,pipline顾名思义就是管道,按顺序将channel收发的数据流向一个个ChannelHandler,所以ChannelHandler是组成ChannelPipline的基本单位。
ChannelPipline还有一个重要的功能就是区分ChannelHandler,因为input的数据和output的数据可能只需要按顺序经过pipline上的某几个handler就行,ChannelPipline会确保数据只会在具有相同定向类型的两个ChannelHandler 之间传递
ChannelHandler
用于处理对应Channel上发生的任何事件,事件类型主要有两种:Channel状态变化和Channel读写数据变化。
这也是开发者重点关注的组件,消息的前后置处理是留给开发者处理的。
ChannelHandlerContext
可以看到ChannelHandler的方法参数都有ChannelHandlerContext,这是作为ChannelPipline整个传递过程的上下文,这种策略在业务代码中也经常出现,将整个请求周期内的有效数据放到一个Context中流转,这种做法面对复杂的业务场景需要繁多的上下文信息很有效,当然也会出现BigObject的问题
EventLoop和Event Loop Group
我们在【专栏】RPC系列(番外)-“土气”的IO实现 中发现处理IO的线程都会处在一个循环中,循环中的内容取决于我们对这个线程的定义,如果是监听连接的线程那么我们只要accept后有连接就分配给另一个线程去监听后续的读写事件,监听读写事件也是处在一个循环中等待读写事件发生,这个循环就是Loop,读写事件就是Event,合起来就是EventLoop。而在Netty中,我们不用自己定义线程,只要创建EventLoopGroup就可以,里面的线程数量也就是EventLoop的数量由开发者自己决定。
| Server端实现
首先引入Netty的依赖:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.34.Final</version>
</dependency>
基于面向接口的初衷,我们定义一个TransportServer接口类表示实现网络传输的Server
public interface TransportServer extends Closeable {
void start() throws Exception;
@Override
void close();
}
然后创建一个NettyServer实现类,根据上面Netty的组件我们需要定义一个Channel以及两个EventGroup,为了使用在我们序列化小节提到的SPI机制创建一个配置文件(com.sparrow.rpc.core.TransportServer)指向NettyServer
com.sparrow.rpc.core.netty.server.NettyServer
public class NettyServer implements TransportServer {
private final int port = 8888;
private EventLoopGroup acceptEventGroup;
private EventLoopGroup workerEventGroup;
private Channel channel;
}
然后我们主要在start方法中启动我们的NettyServer,整个start方法如下,我们一个个讲
@Override
public void start() throws Exception {
//专门处理连接的group,相当于多路复用的监听线程
acceptEventGroup = buildEventGroup();
//处理io事件的worker
workerEventGroup = buildEventGroup();
ChannelHandler channelHandler = buildChannelHandler();
ServerBootstrap serverBootstrap = buildBoostrap(acceptEventGroup, workerEventGroup, channelHandler);
//绑定host和端口
bindChannel(serverBootstrap);
}
首先是创建EventLoopGroup,方法如下,由于Epoll并不是在所有操作系统上都支持,所以我们动态决定使用select还是epoll作为selector
private EventLoopGroup buildEventGroup() {
if (Epoll.isAvailable()) {
return new EpollEventLoopGroup();
} else {
return new NioEventLoopGroup();
}
}
然后是构建ChannelHandler处理channel上的每一个IO事件,对应的是ChannelPipline将重要的几个组件串在一起,我们这里包括了四个部分
-
IdleStateHandler 用于心跳检测,前三个参数分别表示读、写、读写三个超时时间
-
RpcCommandDecoder 由于Server端是接收请求,所以需要一个对RpcCommand的反序列化步骤
-
RpcResponseEncoder 对返回的RpcResponse进行序列化
-
RpcRequestHandler 对每个接收到的请求进行处理的步骤
private ChannelHandler buildChannelHandler() { return new ChannelInitializer() { @Override protected void initChannel(Channel channel) throws Exception { channel.pipeline() .addLast(new IdleStateHandler(10, 0, 0, TimeUnit.SECONDS)) .addLast(new RpcCommandDecoder()) .addLast(new RpcResponseEncoder()) .addLast(new RpcRequestHandler()); } }; }
先贴一下到目前为止我们的项目代码结构是这样的,大家可以对照文章开头的项目地址查看此节的代码,上面涉及到的四个部分我们稍后会讲,不急
\
接着NettyServer的构建,下一步是BootStrap的构建,BootStrap可以理解为Netty的控制中心,它把在此之前创建的EventLoopGroup、ChannelHandler都绑定到一起。这里涉及到Netty可对连接的配置,包括对TCP的配置,通过childOption进行配置,这里就不展开了
private ServerBootstrap buildBoostrap(EventLoopGroup acceptLoopGroup, EventLoopGroup workerEventGroup, ChannelHandler channelHandler) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(Epoll.isAvailable() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
.group(acceptLoopGroup, workerEventGroup)
.childHandler(channelHandler)
//是否开启Nagle算法,该算法会缓存网络数据包,相当于批处理(适合高流量,不适合实时性高场景)
.childOption(ChannelOption.TCP_NODELAY, true)
//是否连接保活,可以复用连接,减少建立连接开销
.childOption(ChannelOption.SO_KEEPALIVE, true);
return serverBootstrap;
}
bootstrap创建后相当于IO模型、线程模型都已经搭建完成,最后只需要把我们监听的端口和bootstrap绑定到一起生成对外的这个channel就可以进行对外连接了
private void bindChannel(ServerBootstrap serverBootstrap) throws InterruptedException {
this.channel = serverBootstrap.bind("localhost", port)
.sync()
.channel();
}
ChannelHandler的实现
刚才在实现ChannelHandler的时候提到了Encoder、Decoder还有两个handler,这里我们先讲序列化与反序列化。
在章节【专栏】RPC系列(实战)-优雅的序列化 中我们了解过了序列化,也实现了几种不同的序列化方式,其实这里的实现原理和之前对RpcRequest的序列化是一样的,把一个个的字段通过put、putInt等方法放到一个ByteBuffer中就行了,只不过我们因为和Netty相关这里用的不是ByteBuffer而是Netty自定义的ByteBuf,篇幅原因我们贴一下RpcResponseEncoder的方法,这里我们直接用了序列化小结准备的Hessian方式
public class RpcResponseEncoder extends MessageToByteEncoder<RpcResponse> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, RpcResponse rpcResponse, ByteBuf byteBuf) throws Exception {
byte[] bytes = SerializeSupport.serialize(rpcResponse, SerializerType.HESSIAN.getType());
byteBuf.writeInt(bytes.length);
byteBuf.writeBytes(bytes);
}
}
而关于handler我们先介绍IdleStateHandler。心跳检测是为了保活,在这里也就是保证连接的正常。IdleStateHandler的机制是通过传入的三个参数,分别对读事件、写事件或者读写事件进行检测,如果配置了readerIdleTime是3秒,那么当前channel如果读事件没出现超过三秒,就会调用 userEventTrigger 方法,所以IdleHandler配置完了后就要在我们自定义的Handler中重写userEventTrigger方法
public IdleStateHandler(
long readerIdleTime, long writerIdleTime, long allIdleTime,
TimeUnit unit)
RpcRequestHandler是核心组件,主要处理接收到请求后要做的事情,在写代码之前想清楚,我们拿到请求后要做的事情主要分为三步
-
根据服务签名找到对应的服务
-
反序列化请求参数,调用服务对应方法
-
拿到结果组装成RpcResponse返回给客户端
这三步逻辑都在invokeService方法中,这里还区别了一下心跳的请求。代码中涉及到一个ServiceHub 这个在之后介绍NameServer与服务注册的时候会讲,可以直接拷贝这个类到目录下就行
private RpcResponse invokeService(RpcCommand command) {
RpcResponse response = new RpcResponse();
response.setHeader(command.getHeader());
//心跳检测请求返回Pong
if (CommandTypes.RPC_HEARTBEAT_REQUEST.getType().equals(command.getHeader().getType())) {
response.getHeader().setType(CommandTypes.RPC_HEARTBEAT_RESPONSE.getType());
response.setData("Pong");
} else {
response.getHeader().setType(CommandTypes.RPC_RESPONSE.getType());
RpcRequest rpcRequest = SerializeSupport.parse(command.getData());
//找到ServiceHub中注册的服务
Object service = ServiceHub.getInstance().getService(rpcRequest.buildServiceSign());
if (Objects.isNull(service)) {
response.setCode(RspCode.UNKNOWN_SERVICE.getCode());
response.setErrorMsg(RspCode.UNKNOWN_SERVICE.getMessage());
log.warn("Service named {} not found!", rpcRequest.buildServiceSign());
return response;
}
//反序列化参数
Object[] args = SerializeSupport.parse(rpcRequest.getParameters());
try {
Class[] argClass = new Class[args.length];
for (int i = 0; i < args.length; i++) {
argClass[i] = args[i].getClass();
}
//调用服务对应方法
Method method = service.getClass().getMethod(rpcRequest.getMethodName(), argClass);
Object ans = method.invoke(service, args);
//组装response返回
response.setData(ans);
response.setCode(RspCode.SUCCESS.getCode());
} catch (Exception e) {
log.warn("invoke error", e);
response.setCode(RspCode.UNKNOWN_SERVICE.getCode());
response.setErrorMsg(RspCode.UNKNOWN_SERVICE.getMessage());
}
}
return response;
}
对于心跳检测中对应空闲事件发生后出发的方法,我们简单粗暴的处理,发生后就直接断开连接
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleState state = ((IdleStateEvent) evt).state();
//如果是读空闲
if (state == IdleState.READER_IDLE) {
log.info("Idle state error,prepare to close");
ctx.close();
}
} else {
super.userEventTriggered(ctx, evt);
}
}
至此我们的NettyServer完成的差不多了,文章中只讲解了重要代码片段,具体代码参考文章开头链接此节的代码。
| Client实现
Client的Netty部分实现方式和Server是差不多的,涉及到的也是相关的一些组件,主要讲解一下项目设计上,为了对客户端调用方便,我们设计了两个接口TransportClient 和 RpcTransport ,RpcTransport 主要负责提供发送请求的功能,TransportClient 负责创建RpcTransport 目的也是为了利用SPI可拔插,客户端可以通过TransportClient 创建想要的 RpcTransport ,所以我们的NettyClient是实现了TransportClient ,记得也要对这两个接口配置SPI的配置文件。
Client的ChannelPipline上同样和Server一样要串上四个部分:IdleStateHandler、一对Decoder和Encoder以及自定义Handler。IdleStateHandler在客户端是对写事件的超时控制,长时间没有发送请求就会触发 userEventTrigger 方法,逻辑也简单就是判断是写空闲后往channel中发送心跳上报的请求就行了,通过头部中的type区别正常请求和心跳请求
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.WRITER_IDLE) {
log.info("Idle send to [{}]", ctx.channel().remoteAddress());
RpcCommand rpcCommand = new RpcCommand();
RpcHeader header = new RpcHeader();
header.setType(CommandTypes.RPC_HEARTBEAT_REQUEST.getType());
header.setVersion("v1.0");
header.setTraceId(UUID.randomUUID().toString());
rpcCommand.setHeader(header);
rpcCommand.setData("Ping".getBytes(StandardCharsets.UTF_8));
ctx.channel().writeAndFlush(rpcCommand).addListener(channelListenFuture -> {
if (!channelListenFuture.isSuccess()) {
log.warn("Send request error");
}
});
}
} else {
super.userEventTriggered(ctx, evt);
}
}
客户端与服务端的Pipline的传递方式就像是这样:
在 createChannel 方法中用到一个 ChannelRegister ,它用来避免重复创建连接,可以连接复用。
我们重点讲解一下NettyTransport请求,它实现了唯一的方法 send ,我们在实现了 TransportClient 的 NettyClient 中创建它,传入了一个channel告诉它需要往哪里send数据
public CompletableFuture<RpcResponse> send(RpcCommand request) {
if (!channel.isActive()) {
throw new IllegalStateException("Unhealthy channel state");
}
CompletableFuture<RpcResponse> future = new CompletableFuture<>();
try {
pendingRequests.put(request.getHeader().getTraceId(), future);
channel.writeAndFlush(request).addListener(channelListenFuture -> {
if (!channelListenFuture.isSuccess()) {
log.warn("Send request error");
future.completeExceptionally(channelListenFuture.cause());
channel.close();
}
});
} catch (Exception e) {
log.warn("Send message error");
future.completeExceptionally(e);
pendingRequests.remove(request.getHeader().getTraceId());
}
return future;
}
用到了PendingRequest 用来装载在途请求,里面存在的请求是还没拿到响应的请求,有了这个在途请求我们才能在RpcResponseHandler中当拿到响应后,根据头部的traceId找到这个响应对应的请求并通过future.complete方法通知被这个future.get 阻塞的请求线程 (com.sparrow.mason.core.netty.handler.RpcResponseHandler)
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, RpcResponse rpcResponse) throws Exception {
String type = rpcResponse.getHeader().getType();
if (CommandTypes.RPC_HEARTBEAT_RESPONSE.getType().equals(type)) {
log.info("HeartBeat [{}]", rpcResponse.getData());
} else if (CommandTypes.RPC_RESPONSE.getType().equals(type)) {
CompletableFuture<RpcResponse> rspFuture = PendingRequests.getInstance().remove(rpcResponse.getHeader().getTraceId());
//通过complete将reponse返回给对应future的get阻塞线程
rspFuture.complete(rpcResponse);
}
}
最后我们梳理一下一个正常的RpcCommand通过刚才写的这么多代码发送的一个路径
-
客户端调用NettyClient的createTransport方法创建NettyTransport
-
组装好RpcCommand后调用NettyTransport的send方法,send方法将请求写入建立好连接的channel中返回一个future,并将future暂存到PendingRequest中
-
客户端拿到future后调用future.get方法阻塞等待数据到来
-
RpcCommand来到NettyServer的ChannelPipline流转,经过反序列化以及RpcRequestHandler的处理后,拿到远端服务对应方法的结果,组装成RpcResponse,写到channel中返回给客户端
-
客户端拿到Response后经过反序列化后从PendingRequest拿到对应的future,调用complete方法完成这个future
-
被future阻塞的线程拿到结果,做自定义处理
| 小结
这一节代码量有点大,非常建议先把代码clone下来对照着看会清楚很多,最后的流程梳理应该对大家也有帮助。至此我们通过两篇番外篇+一篇实战篇熟悉了Netty所涉及的知识,也完成了Sparrow-Rpc的Netty部分开发,下一节我们会讲解服务注册的流程,NameServer我们会以很简单的方式实现,有一说一不规范,但是你懂了它是做什么的,那么怎么做只是一种选择,下周见~