从零开始实现简单 RPC 框架 9:网络通信之心跳与重连机制
一、心跳
什么是心跳
在 TPC 中,客户端和服务端建立连接之后,需要定期发送数据包,来通知对方自己还在线,以确保 TPC 连接的有效性。如果一个连接长时间没有心跳,需要及时断开,否则服务端会维护很多无用连接,浪费服务端的资源。
IdleStateHandler
Netty 已经为我们提供了心跳的 Handler:IdleStateHandler
。当连接的空闲时间(读或者写)太长时,IdleStateHandler
将会触发一个 IdleStateEvent
事件,传递的下一个 Handler。我们可以通过在 Pipeline Handler 中重写 userEventTrigged
方法来处理该事件,注意我们自己的 Handler 需要在 IdleStateHandler
后面。
下面我们来看看 IdleStateHandler 的源码。
1. 构造函数
最完整的构造函数如下:
public IdleStateHandler(boolean observeOutput,
long readerIdleTime, long writerIdleTime, long allIdleTime,
TimeUnit unit) {
}
参数解析:
observeOutput
:是否考虑出站时较慢的情况。如果 true:当出站时间太长,超过空闲时间,那么将不触发此次事件。如果 false,超过空闲时间就会触发事件。默认 false。readerIdleTime
:读空闲的时间,0 表示禁用读空闲事件。writerIdleTime
:写空闲的时间,0 表示禁用写空闲事件。allIdleTime
:读或写空闲的时间,0 表示禁用事件。unit
:前面三个时间的单位。
2. 事件处理
IdleStateHandler
继承 ChannelDuplexHandler
,重写了出站和入站的事件,我们来看看代码。
当 channel 添加、注册、活跃的时候,会初始化 initialize(ctx)
,删除、不活跃的时候销毁 destroy()
,读写的时候设置 lastReadTime
和 lastWriteTime
字段。
public class IdleStateHandler extends ChannelDuplexHandler {
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
if (ctx.channel().isActive() && ctx.channel().isRegistered()) {
initialize(ctx);
}
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
destroy();
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
if (ctx.channel().isActive()) {
initialize(ctx);
}
super.channelRegistered(ctx);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
initialize(ctx);
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
destroy();
super.channelInactive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 判断是否开启 读空闲 或者 读写空闲 监控
if (readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) {
// 设置 reading 标志位
reading = true;
firstReaderIdleEvent = firstAllIdleEvent = true;
}
ctx.fireChannelRead(msg);
}
// 读完成之后
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 判断是否开启 读空闲 或者 读写空闲 监控,检查 reading 标志位
if ((readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) && reading) {
// 设置 lastReadTime,后面判断读超时有用
lastReadTime = ticksInNanos();
reading = false;
}
ctx.fireChannelReadComplete();
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
// 判断是否开启 写空闲 或者 读写空闲 监控
if (writerIdleTimeNanos > 0 || allIdleTimeNanos > 0) {
// writeListener 的方法在下面,主要是设置 lastWriteTime
ctx.write(msg, promise.unvoid()).addListener(writeListener);
} else {
ctx.write(msg, promise);
}
}
private final ChannelFutureListener writeListener = new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
lastWriteTime = ticksInNanos();
firstWriterIdleEvent = firstAllIdleEvent = true;
}
};
}
3. 初始化
当 channel 添加、注册、活跃的时候,会初始化 initialize(ctx)
,下面我们就来看看初始化的代码:
private void initialize(ChannelHandlerContext ctx) {
// Avoid the case where destroy() is called before scheduling timeouts.
// See: https://github.com/netty/netty/issues/143
switch (state) {
case 1:
case 2:
return;
}
state = 1;
initOutputChanged(ctx);
lastReadTime = lastWriteTime = ticksInNanos();
if (readerIdleTimeNanos > 0) {
readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
readerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
if (writerIdleTimeNanos > 0) {
writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
writerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
if (allIdleTimeNanos > 0) {
allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
allIdleTimeNanos, TimeUnit.NANOSECONDS);
}
}
其实初始化很简单,就是根据构造函数给的 读写空闲时间 去决定初始化哪些定时任务,分别是:ReaderIdleTimeoutTask
(读空闲超时任务)、WriterIdleTimeoutTask
(写空闲超时任务)、AllIdleTimeoutTask
(读写空闲超时任务)。
4. 定时任务
我们来看看 ReaderIdleTimeoutTask
,剩下两个的原理跟 ReaderIdleTimeoutTask
差不多,感兴趣的同学自行阅读源码吧。
private final class ReaderIdleTimeoutTask extends AbstractIdleTask {
ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {
super(ctx);
}
@Override
protected void run(ChannelHandlerContext ctx) {
// 查看是否超时
long nextDelay = readerIdleTimeNanos;
if (!reading) {
nextDelay -= ticksInNanos() - lastReadTime;
}
if (nextDelay <= 0) {
// 超时了,重新启动一个新的定时器,然后触发事件
// Reader is idle - set a new timeout and notify the callback.
readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);
boolean first = firstReaderIdleEvent;
firstReaderIdleEvent = false;
try {
// 构造事件
IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
// 触发事件
channelIdle(ctx, event);
} catch (Throwable t) {
ctx.fireExceptionCaught(t);
}
} else {
// 没有超时,设置新的定时器,不过这次的时间是更短的时间
// Read occurred before the timeout - set a new timeout with shorter delay.
readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
}
}
}
从上面的代码可以看出:
① 如果读空闲超时了,则重新起一个定时器,然后触发事件
② 如果读空闲未超时,则新起一个时间更短(readerIdleTimeNanos - ticksInNanos() - lastReadTime
)的定时器
5. 触发事件
上面的触发事件方法是:channelIdle
,经过重重代码拨开,其实最终就是调用到了下面的代码:
private void invokeUserEventTriggered(Object event) {
if (invokeHandler()) {
try {
// 触发事件,说白了,就是直接调用 userEventTriggered 方法而已
((ChannelInboundHandler) handler()).userEventTriggered(this, event);
} catch (Throwable t) {
notifyHandlerException(t);
}
} else {
fireUserEventTriggered(event);
}
}
其实触发事件,就是把事件传给下一个 Handler (next
),就是调用 userEventTriggered
方法而已。所以我们处理心跳的 Handler 一定要写到 IdleStateHandler
。
ccx-rpc 心跳实现
1. 客户端
IdleStateHandler
放到启动类的 PipleLine
注册上,业务处理器 NettyClientHandler
在其后面。
public class NettyClient {
// ... 忽略其他代码
private NettyClient() {
bootstrap = new Bootstrap()
// ... 省略其他代码
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
// 设定 IdleStateHandler 心跳检测每 5 秒进行一次写检测
// write()方法超过 5 秒没调用,就调用 userEventTrigger
p.addLast(new IdleStateHandler(0, 5, 0, TimeUnit.SECONDS));
// 编码器
p.addLast(new RpcMessageEncoder());
// 解码器
p.addLast(new RpcMessageDecoder());
// 业务处理器
p.addLast(new NettyClientHandler());
}
});
}
}
接下来我们来看看 NettyClientHandler
是如何处理心跳事件的:
public class NettyClientHandler extends SimpleChannelInboundHandler<RpcMessage> {
// ... 忽略其他代码
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
// 根据上面的配置,超过 5 秒没有写请求,会触发 WRITER_IDLE 事件
IdleState state = ((IdleStateEvent) evt).state();
if (state == IdleState.WRITER_IDLE) {
log.info("write idle happen [{}]", ctx.channel().remoteAddress());
Channel channel = ctx.channel();
// 触发写空闲事件后,就应该发心跳了。
// 组装消息
RpcMessage rpcMessage = new RpcMessage();
rpcMessage.setSerializeType(SerializeType.PROTOSTUFF.getValue());
rpcMessage.setCompressTye(CompressType.DUMMY.getValue());
rpcMessage.setMessageType(MessageType.HEARTBEAT.getValue());
// 发心跳消息
channel.writeAndFlush(rpcMessage).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
}
} else {
super.userEventTriggered(ctx, evt);
}
}
}
2. 服务端
同样,服务端的 IdleStateHandler
放到启动类的 PipleLine
注册上,业务处理器 NettyServerHandler
在其后面。
public class NettyServerBootstrap {
public void start() {
ServerBootstrap bootstrap = new ServerBootstrap()
// ... 忽略其他代码
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
// 30 秒之内没有收到客户端请求的话就关闭连接
p.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS));
// 编解码器
p.addLast(new RpcMessageEncoder());
p.addLast(new RpcMessageDecoder());
// RPC 消息处理器
p.addLast(serviceHandlerGroup, new NettyServerHandler());
}
});
// ... 忽略其他代码
}
}
服务端收到超过 30 秒没有读请求的事件后,调用 ctx.close
将连接关闭。
同时,如果收到了客户端发来的心跳消息,直接忽略即可。如果每个心跳都要去响应,会加重服务器的负担的。
NettyServerHandler
的代码如下
public class NettyServerHandler extends SimpleChannelInboundHandler<RpcMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcMessage requestMsg) {
// 不处理心跳消息
if (requestMsg.getMessageType() != MessageType.REQUEST.getValue()) {
return;
}
}
@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 check happen, so close the connection");
ctx.close();
}
} else {
super.userEventTriggered(ctx, evt);
}
}
}
二、重连机制
很多时候服务端和客户端连接断开,仅仅是因为网络问题或者处理程序慢,并不是程序挂了。那么客户端想再发起请求,就发不出去了。此时需要一个功能:当发现连接断了之后,如果想往连接写数据,就自动重新连接上,这个就是重连机制。
客户端想请求服务端的接口,先从注册中心中,获得服务端的地址,然后跟服务端连接,然后写数据。
简单代码如下:
protected RpcResult doInvoke(RpcRequest request, URL selected) throws RpcException {
// ... 忽略其他代码
// 服务端地址
InetSocketAddress socketAddress = new InetSocketAddress(selected.getHost(), selected.getPort());
// 获取连接(Channel)
Channel channel = nettyClient.getChannel(socketAddress);
// 构建消息
RpcMessage rpcMessage = buildRpcMessage(request);
// 写消息(发请求)
channel.writeAndFlush(rpcMessage);
}
这个 nettyClient.getChannel(socketAddress)
是重连机制的秘密:
/**
* 获取和指定地址连接的 channel,如果获取不到,则连接
*
* @param address 指定要连接的地址
* @return channel
*/
public Channel getChannel(SocketAddress address) {
// 根据地址从缓存中获取 Channel
Channel channel = CHANNEL_MAP.get(address);
// 如果获取不到,或者 channel 已经断开,则重连,然后放到 CHANNEL_MAP 缓存起来
if (channel == null || !channel.isActive()) {
// 连接
channel = connect(address);
CHANNEL_MAP.put(address, channel);
}
return channel;
}
代码一目了然,就是使用了 CHANNEL_MAP
作为缓存,发现找不到或者已断开,就重新连接,然后放到 CHANNEL_MAP
中,以便下次获取。
总结
心跳是用于服务端和客户端保持有效连接的一种手段,客户端每隔一小段时间发一个心跳包,服务端收到之后不用响应,但是会记下客户端最后一次读的时间。服务器起定时器,定时检测客户端上次读请求的时间超过配置的值,超过就会触发事件,断开连接。
重连机制是连接断开之后,要使用的时候自动重连的机制。
心跳和重连机制,结合起来让服务端和客户端的连接使用更加合理,该断开的断开节省服务端资源,该重连的重连提高可用性。
ccx-rpc 代码已经开源
Github:https://github.com/chenchuxin/ccx-rpc
Gitee:https://gitee.com/imccx/ccx-rpc
从零开始实现简单 RPC 框架 9:网络通信之心跳与重连机制的更多相关文章
- 从零开始实现简单 RPC 框架 5:网络通信之序列化
我们在接下来会开始讲网络通信相关的内容了.既然是网络通信,那必然会涉及到序列化的相关技术. 下面是 ccx-rpc 序列化器的接口定义. /** * 序列化器 */ public interface ...
- 从零开始实现简单 RPC 框架 6:网络通信之 Netty
网络通信的开发,就涉及到一些开发框架:Java NIO.Netty.Mina 等等. 理论上来说,类似于序列化器,可以为其定义一套统一的接口,让不同类型的框架实现,事实上,Dubbo 就是这么干的. ...
- 从零开始实现简单 RPC 框架 7:网络通信之自定义协议(粘包拆包、编解码)
当 RPC 框架使用 Netty 通信时,实际上是将数据转化成 ByteBuf 的方式进行传输. 那如何转化呢?可不可以把 请求参数 或者 响应结果 直接无脑序列化成 byte 数组发出去? 答:直接 ...
- 从零开始实现简单 RPC 框架 2:扩展利器 SPI
RPC 框架有很多可扩展的地方,如:序列化类型.压缩类型.负载均衡类型.注册中心类型等等. 假设框架提供的注册中心只有zookeeper,但是使用者想用Eureka,修改框架以支持使用者的需求显然不是 ...
- 从零开始实现简单 RPC 框架 8:网络通信之 Request-Response 模型
Netty 在服务端与客户端的网络通信中,使用的是异步双向通信(双工)的方式,即客户端和服务端可以相互主动发请求给对方,发消息后不会同步等响应.这样就会有一下问题: 如何识别消息是请求还是响应? 请求 ...
- 从零开始实现简单 RPC 框架 4:注册中心
RPC 中服务消费端(Consumer) 需要请求服务提供方(Provider)的接口,必须要知道 Provider 的地址才能请求到. 那么,Consumer 要从哪里获取 Provider 的地址 ...
- 从零开始实现简单 RPC 框架 3:配置总线 URL
URL 的定义 URL 对于大部分程序猿来说都是很熟悉的,其全称是 Uniform Resource Locator (统一资源定位器).它是互联网的统一资源定位标志,也就是指网络地址. 一个标准的 ...
- Java实现简单RPC框架(转)
一.RPC简介 RPC,全称Remote Procedure Call, 即远程过程调用,它是一个计算机通信协议.它允许像本地服务一样调用远程服务.它可以有不同的实现方式.如RMI(远程方法调用).H ...
- RPC笔记之初探RPC:DIY简单RPC框架
一.什么是RPC RPC(Remote Procedure Call)即远程过程调用,简单的说就是在A机器上去调用B机器上的某个方法,在分布式系统中极其常用. rpc原理其实很简单,比较容易理解,在r ...
随机推荐
- 关于 IE8 console不未定义的问题
在开发的过程中由于调试的原因,在代码中加入console.info("xxxx"),而未进行删除 在IE8下测试该代码所在的页面报错,如下: 需要注意的是,使用console对象查 ...
- Pdb— Python的调试器
参考:Pdb- Python的调试器 pdb 模块定义了一个交互式源代码调试器,用于 Python 程序.它支持在源码行间设置(有条件的)断点和单步执行,检视堆栈帧,列出源码列表,以及在任何堆栈帧的上 ...
- 还在用Postman?来,花2分钟体验下ApiPost的魅力
2分钟玩转APIPOST 本文通过简单介绍如何利用ApiPost调试接口和快速的生成接口文档,让您初步体验ApiPost的魅力! 1. API写完想要测试?试试模拟发送一次请求 新建接口,我想模拟发送 ...
- Mybatis学习笔记-第一个Mybatis程序
思路 搭建环境 搭建数据库(略) CREATE DDATABASE CREATE TABLE INSERT VALUES 新建项目 普通Maven项目 删除src文件夹 --> 建立父工程 导入 ...
- 缩减Azure上Linux虚拟机系统盘容量
[话在前头] 这么些年微软 Azure 创建虚拟机一直不能修改系统盘大小,但很多时候实际又用不了这么大的操作系统磁盘.微软自己甚至还针对 Windows 服务器镜像推出一个 smalldisk 的镜像 ...
- vue中rem的转换
1 function rems(doc: any, win: any): void { 2 let docEl = doc.documentElement, 3 resizeEvt = 'orient ...
- HCIA-数据链路层
数据链路层 1.数据的差错检测 |FCS| 2.组帧|解帧 |数据帧帧头 帧尾| 3.标识身份 |MAC地址| 以太网络标准数据链路层的标准 数据链路层不仅仅只有以太网 地域来进行分类 局域网:小型地 ...
- @Value(value="${***.***}")配置文件赋值给static静态变量
public static String topicName; @Value("${activemq.topicName}") public void setTopicName(S ...
- Linux命令(九)之安装mysql
.personSunflowerP { background: rgba(51, 153, 0, 0.66); border-bottom: 1px solid rgba(0, 102, 0, 1); ...
- java.lang.instrument.Instrumentation
java.lang.instrument.Instrumentation 看完文档之后,我们发现这么两个接口:redefineClasses和retransformClasses.一个是重新定义cla ...