一、心跳

什么是心跳

在 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(),读写的时候设置 lastReadTimelastWriteTime 字段。

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:网络通信之心跳与重连机制的更多相关文章

  1. 从零开始实现简单 RPC 框架 5:网络通信之序列化

    我们在接下来会开始讲网络通信相关的内容了.既然是网络通信,那必然会涉及到序列化的相关技术. 下面是 ccx-rpc 序列化器的接口定义. /** * 序列化器 */ public interface ...

  2. 从零开始实现简单 RPC 框架 6:网络通信之 Netty

    网络通信的开发,就涉及到一些开发框架:Java NIO.Netty.Mina 等等. 理论上来说,类似于序列化器,可以为其定义一套统一的接口,让不同类型的框架实现,事实上,Dubbo 就是这么干的. ...

  3. 从零开始实现简单 RPC 框架 7:网络通信之自定义协议(粘包拆包、编解码)

    当 RPC 框架使用 Netty 通信时,实际上是将数据转化成 ByteBuf 的方式进行传输. 那如何转化呢?可不可以把 请求参数 或者 响应结果 直接无脑序列化成 byte 数组发出去? 答:直接 ...

  4. 从零开始实现简单 RPC 框架 2:扩展利器 SPI

    RPC 框架有很多可扩展的地方,如:序列化类型.压缩类型.负载均衡类型.注册中心类型等等. 假设框架提供的注册中心只有zookeeper,但是使用者想用Eureka,修改框架以支持使用者的需求显然不是 ...

  5. 从零开始实现简单 RPC 框架 8:网络通信之 Request-Response 模型

    Netty 在服务端与客户端的网络通信中,使用的是异步双向通信(双工)的方式,即客户端和服务端可以相互主动发请求给对方,发消息后不会同步等响应.这样就会有一下问题: 如何识别消息是请求还是响应? 请求 ...

  6. 从零开始实现简单 RPC 框架 4:注册中心

    RPC 中服务消费端(Consumer) 需要请求服务提供方(Provider)的接口,必须要知道 Provider 的地址才能请求到. 那么,Consumer 要从哪里获取 Provider 的地址 ...

  7. 从零开始实现简单 RPC 框架 3:配置总线 URL

    URL 的定义 URL 对于大部分程序猿来说都是很熟悉的,其全称是 Uniform Resource Locator (统一资源定位器).它是互联网的统一资源定位标志,也就是指网络地址. 一个标准的 ...

  8. Java实现简单RPC框架(转)

    一.RPC简介 RPC,全称Remote Procedure Call, 即远程过程调用,它是一个计算机通信协议.它允许像本地服务一样调用远程服务.它可以有不同的实现方式.如RMI(远程方法调用).H ...

  9. RPC笔记之初探RPC:DIY简单RPC框架

    一.什么是RPC RPC(Remote Procedure Call)即远程过程调用,简单的说就是在A机器上去调用B机器上的某个方法,在分布式系统中极其常用. rpc原理其实很简单,比较容易理解,在r ...

随机推荐

  1. Python - 解包的各种骚操作

    为什么要讲解包 因为我觉得解包是 Python 的一大特性,大大提升了编程的效率,而且适用性很广 啥是解包 个人通俗理解:解开包袱,拿出东西 正确理解:将元素从可迭代对象中一个个取出来 python ...

  2. OpenGL学习笔记(五)变换

    目录 变换 向量 向量的运算 向量与标量运算 向量取反 向量加减 求向量长度 向量的单位化 向量相乘 点乘(Dot Product) 叉乘 矩阵 矩阵的加减 矩阵的数乘 矩阵相乘 矩阵与向量相乘 与单 ...

  3. Flutter开发进阶学习指南Flutter开发进阶学习指南

    Flutter 的起源 Flutter 的诞生其实比较有意思,Flutter 诞生于 Chrome 团队的一场内部实验, 谷歌的前端团队在把前端一些"乱七八糟"的规范去掉后,发现在 ...

  4. 05.表达式目录树Expression

    参考文章 https://www.cnblogs.com/xyh9039/p/12748983.html 1. 基本了解 1.1 Lambda表达式 演变过程 using System; namesp ...

  5. 数据增广imgaug库的使用

    记录一下这两天用imgaug库做数据增广的代码,由于是算用算学的,所以只能把代码写出来,具体每种增广算法的原理和一些参数就不得而知了,不过我觉得也没必要把这么些个算法搜搞懂,毕竟重点是扩种数据.所以, ...

  6. 清理docker常用命令

    0.查看docker占用空间docker system df1.一键删除所有已经停止的容器 docker container prune 2.删除所有容器(包含停止的 正在运行的) docker rm ...

  7. 如何发送一个http请求—apipost

    API界面功能布局 API请求参数 Header 参数 你可以设置或者导入 Header 参数,cookie也在Header进行设置 Query 参数 Query 支持构造URL参数,同时支持 RES ...

  8. 用三个while循环和tkinter实现一个显示屏

    用三个while循环和tkinter实现一个显示屏 import tkinter as tk import time # 输入框是跟程序打交道的一个途径,例如程序要求你输入账号密码,那么它就需要提供两 ...

  9. 绕WAF常见思路整理(一)

    最*被*台的一些事情搞得心态有点崩,很久没写文了 *期想整理一下常见的各种操作中绕过WAF的思路与免杀的思路(这部分之前没整理完以后有机会再说),受限于个人水*因素所以一定是不完全的,而且在WAF日新 ...

  10. 联合迭代器与生成器,enumerate() 内置函数真香!

    花下猫语:Python 中很多内置函数的作用都非常大,比如说 enumerate() 和 zip(),它们使得我们在作迭代操作时极为顺手.这是一篇很多年前的 PEP,提议在 Python 2.3 版本 ...