一、心跳

什么是心跳

在 TPC 中,客户端和服务端建立连接之后,需要定期发送数据包,来通知对方自己还在线,以确保 TPC 连接的有效性。如果一个连接长时间没有心跳,需要及时断开,否则服务端会维护很多无用连接,浪费服务端的资源。

IdleStateHandler

Netty 已经为我们提供了心跳的 Handler:IdleStateHandler。当连接的空闲时间(读或者写)太长时,IdleStateHandler 将会触发一个 IdleStateEvent 事件,传递的下一个 Handler。我们可以通过在 Pipeline Handler 中重写 userEventTrigged 方法来处理该事件,注意我们自己的 Handler 需要在 IdleStateHandler 后面。

下面我们来看看 IdleStateHandler 的源码。

1. 构造函数

最完整的构造函数如下:

  1. public IdleStateHandler(boolean observeOutput,
  2. long readerIdleTime, long writerIdleTime, long allIdleTime,
  3. TimeUnit unit) {
  4. }

参数解析:

  • observeOutput:是否考虑出站时较慢的情况。如果 true:当出站时间太长,超过空闲时间,那么将不触发此次事件。如果 false,超过空闲时间就会触发事件。默认 false。
  • readerIdleTime:读空闲的时间,0 表示禁用读空闲事件。
  • writerIdleTime:写空闲的时间,0 表示禁用写空闲事件。
  • allIdleTime:读或写空闲的时间,0 表示禁用事件。
  • unit:前面三个时间的单位。

2. 事件处理

IdleStateHandler 继承 ChannelDuplexHandler,重写了出站和入站的事件,我们来看看代码。

当 channel 添加、注册、活跃的时候,会初始化 initialize(ctx),删除、不活跃的时候销毁 destroy(),读写的时候设置 lastReadTimelastWriteTime 字段。

  1. public class IdleStateHandler extends ChannelDuplexHandler {
  2. @Override
  3. public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
  4. if (ctx.channel().isActive() && ctx.channel().isRegistered()) {
  5. initialize(ctx);
  6. }
  7. }
  8. @Override
  9. public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
  10. destroy();
  11. }
  12. @Override
  13. public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
  14. if (ctx.channel().isActive()) {
  15. initialize(ctx);
  16. }
  17. super.channelRegistered(ctx);
  18. }
  19. @Override
  20. public void channelActive(ChannelHandlerContext ctx) throws Exception {
  21. initialize(ctx);
  22. super.channelActive(ctx);
  23. }
  24. @Override
  25. public void channelInactive(ChannelHandlerContext ctx) throws Exception {
  26. destroy();
  27. super.channelInactive(ctx);
  28. }
  29. @Override
  30. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  31. // 判断是否开启 读空闲 或者 读写空闲 监控
  32. if (readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) {
  33. // 设置 reading 标志位
  34. reading = true;
  35. firstReaderIdleEvent = firstAllIdleEvent = true;
  36. }
  37. ctx.fireChannelRead(msg);
  38. }
  39. // 读完成之后
  40. @Override
  41. public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
  42. // 判断是否开启 读空闲 或者 读写空闲 监控,检查 reading 标志位
  43. if ((readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) && reading) {
  44. // 设置 lastReadTime,后面判断读超时有用
  45. lastReadTime = ticksInNanos();
  46. reading = false;
  47. }
  48. ctx.fireChannelReadComplete();
  49. }
  50. @Override
  51. public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
  52. // 判断是否开启 写空闲 或者 读写空闲 监控
  53. if (writerIdleTimeNanos > 0 || allIdleTimeNanos > 0) {
  54. // writeListener 的方法在下面,主要是设置 lastWriteTime
  55. ctx.write(msg, promise.unvoid()).addListener(writeListener);
  56. } else {
  57. ctx.write(msg, promise);
  58. }
  59. }
  60. private final ChannelFutureListener writeListener = new ChannelFutureListener() {
  61. @Override
  62. public void operationComplete(ChannelFuture future) throws Exception {
  63. lastWriteTime = ticksInNanos();
  64. firstWriterIdleEvent = firstAllIdleEvent = true;
  65. }
  66. };
  67. }

3. 初始化

当 channel 添加、注册、活跃的时候,会初始化 initialize(ctx),下面我们就来看看初始化的代码:

  1. private void initialize(ChannelHandlerContext ctx) {
  2. // Avoid the case where destroy() is called before scheduling timeouts.
  3. // See: https://github.com/netty/netty/issues/143
  4. switch (state) {
  5. case 1:
  6. case 2:
  7. return;
  8. }
  9. state = 1;
  10. initOutputChanged(ctx);
  11. lastReadTime = lastWriteTime = ticksInNanos();
  12. if (readerIdleTimeNanos > 0) {
  13. readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
  14. readerIdleTimeNanos, TimeUnit.NANOSECONDS);
  15. }
  16. if (writerIdleTimeNanos > 0) {
  17. writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
  18. writerIdleTimeNanos, TimeUnit.NANOSECONDS);
  19. }
  20. if (allIdleTimeNanos > 0) {
  21. allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
  22. allIdleTimeNanos, TimeUnit.NANOSECONDS);
  23. }
  24. }

其实初始化很简单,就是根据构造函数给的 读写空闲时间 去决定初始化哪些定时任务,分别是:ReaderIdleTimeoutTask(读空闲超时任务)、WriterIdleTimeoutTask(写空闲超时任务)、AllIdleTimeoutTask(读写空闲超时任务)。

4. 定时任务

我们来看看 ReaderIdleTimeoutTask,剩下两个的原理跟 ReaderIdleTimeoutTask 差不多,感兴趣的同学自行阅读源码吧。

  1. private final class ReaderIdleTimeoutTask extends AbstractIdleTask {
  2. ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {
  3. super(ctx);
  4. }
  5. @Override
  6. protected void run(ChannelHandlerContext ctx) {
  7. // 查看是否超时
  8. long nextDelay = readerIdleTimeNanos;
  9. if (!reading) {
  10. nextDelay -= ticksInNanos() - lastReadTime;
  11. }
  12. if (nextDelay <= 0) {
  13. // 超时了,重新启动一个新的定时器,然后触发事件
  14. // Reader is idle - set a new timeout and notify the callback.
  15. readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);
  16. boolean first = firstReaderIdleEvent;
  17. firstReaderIdleEvent = false;
  18. try {
  19. // 构造事件
  20. IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
  21. // 触发事件
  22. channelIdle(ctx, event);
  23. } catch (Throwable t) {
  24. ctx.fireExceptionCaught(t);
  25. }
  26. } else {
  27. // 没有超时,设置新的定时器,不过这次的时间是更短的时间
  28. // Read occurred before the timeout - set a new timeout with shorter delay.
  29. readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
  30. }
  31. }
  32. }

从上面的代码可以看出:

① 如果读空闲超时了,则重新起一个定时器,然后触发事件

② 如果读空闲未超时,则新起一个时间更短(readerIdleTimeNanos - ticksInNanos() - lastReadTime)的定时器

5. 触发事件

上面的触发事件方法是:channelIdle,经过重重代码拨开,其实最终就是调用到了下面的代码:

  1. private void invokeUserEventTriggered(Object event) {
  2. if (invokeHandler()) {
  3. try {
  4. // 触发事件,说白了,就是直接调用 userEventTriggered 方法而已
  5. ((ChannelInboundHandler) handler()).userEventTriggered(this, event);
  6. } catch (Throwable t) {
  7. notifyHandlerException(t);
  8. }
  9. } else {
  10. fireUserEventTriggered(event);
  11. }
  12. }

其实触发事件,就是把事件传给下一个 Handler (next),就是调用 userEventTriggered 方法而已。所以我们处理心跳的 Handler 一定要写到 IdleStateHandler

ccx-rpc 心跳实现

1. 客户端

IdleStateHandler 放到启动类的 PipleLine 注册上,业务处理器 NettyClientHandler 在其后面。

  1. public class NettyClient {
  2. // ... 忽略其他代码
  3. private NettyClient() {
  4. bootstrap = new Bootstrap()
  5. // ... 省略其他代码
  6. .handler(new ChannelInitializer<SocketChannel>() {
  7. @Override
  8. protected void initChannel(SocketChannel ch) {
  9. ChannelPipeline p = ch.pipeline();
  10. // 设定 IdleStateHandler 心跳检测每 5 秒进行一次写检测
  11. // write()方法超过 5 秒没调用,就调用 userEventTrigger
  12. p.addLast(new IdleStateHandler(0, 5, 0, TimeUnit.SECONDS));
  13. // 编码器
  14. p.addLast(new RpcMessageEncoder());
  15. // 解码器
  16. p.addLast(new RpcMessageDecoder());
  17. // 业务处理器
  18. p.addLast(new NettyClientHandler());
  19. }
  20. });
  21. }
  22. }

接下来我们来看看 NettyClientHandler 是如何处理心跳事件的:

  1. public class NettyClientHandler extends SimpleChannelInboundHandler<RpcMessage> {
  2. // ... 忽略其他代码
  3. @Override
  4. public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
  5. if (evt instanceof IdleStateEvent) {
  6. // 根据上面的配置,超过 5 秒没有写请求,会触发 WRITER_IDLE 事件
  7. IdleState state = ((IdleStateEvent) evt).state();
  8. if (state == IdleState.WRITER_IDLE) {
  9. log.info("write idle happen [{}]", ctx.channel().remoteAddress());
  10. Channel channel = ctx.channel();
  11. // 触发写空闲事件后,就应该发心跳了。
  12. // 组装消息
  13. RpcMessage rpcMessage = new RpcMessage();
  14. rpcMessage.setSerializeType(SerializeType.PROTOSTUFF.getValue());
  15. rpcMessage.setCompressTye(CompressType.DUMMY.getValue());
  16. rpcMessage.setMessageType(MessageType.HEARTBEAT.getValue());
  17. // 发心跳消息
  18. channel.writeAndFlush(rpcMessage).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
  19. }
  20. } else {
  21. super.userEventTriggered(ctx, evt);
  22. }
  23. }
  24. }

2. 服务端

同样,服务端的 IdleStateHandler 放到启动类的 PipleLine 注册上,业务处理器 NettyServerHandler 在其后面。

  1. public class NettyServerBootstrap {
  2. public void start() {
  3. ServerBootstrap bootstrap = new ServerBootstrap()
  4. // ... 忽略其他代码
  5. .childHandler(new ChannelInitializer<SocketChannel>() {
  6. @Override
  7. protected void initChannel(SocketChannel ch) {
  8. ChannelPipeline p = ch.pipeline();
  9. // 30 秒之内没有收到客户端请求的话就关闭连接
  10. p.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS));
  11. // 编解码器
  12. p.addLast(new RpcMessageEncoder());
  13. p.addLast(new RpcMessageDecoder());
  14. // RPC 消息处理器
  15. p.addLast(serviceHandlerGroup, new NettyServerHandler());
  16. }
  17. });
  18. // ... 忽略其他代码
  19. }
  20. }

服务端收到超过 30 秒没有读请求的事件后,调用 ctx.close 将连接关闭。

同时,如果收到了客户端发来的心跳消息,直接忽略即可。如果每个心跳都要去响应,会加重服务器的负担的。

NettyServerHandler 的代码如下

  1. public class NettyServerHandler extends SimpleChannelInboundHandler<RpcMessage> {
  2. @Override
  3. protected void channelRead0(ChannelHandlerContext ctx, RpcMessage requestMsg) {
  4. // 不处理心跳消息
  5. if (requestMsg.getMessageType() != MessageType.REQUEST.getValue()) {
  6. return;
  7. }
  8. }
  9. @Override
  10. public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
  11. // 处理空闲状态的
  12. if (evt instanceof IdleStateEvent) {
  13. IdleState state = ((IdleStateEvent) evt).state();
  14. if (state == IdleState.READER_IDLE) {
  15. log.info("idle check happen, so close the connection");
  16. ctx.close();
  17. }
  18. } else {
  19. super.userEventTriggered(ctx, evt);
  20. }
  21. }
  22. }

二、重连机制

很多时候服务端和客户端连接断开,仅仅是因为网络问题或者处理程序慢,并不是程序挂了。那么客户端想再发起请求,就发不出去了。此时需要一个功能:当发现连接断了之后,如果想往连接写数据,就自动重新连接上,这个就是重连机制。

客户端想请求服务端的接口,先从注册中心中,获得服务端的地址,然后跟服务端连接,然后写数据。

简单代码如下:

  1. protected RpcResult doInvoke(RpcRequest request, URL selected) throws RpcException {
  2. // ... 忽略其他代码
  3. // 服务端地址
  4. InetSocketAddress socketAddress = new InetSocketAddress(selected.getHost(), selected.getPort());
  5. // 获取连接(Channel)
  6. Channel channel = nettyClient.getChannel(socketAddress);
  7. // 构建消息
  8. RpcMessage rpcMessage = buildRpcMessage(request);
  9. // 写消息(发请求)
  10. channel.writeAndFlush(rpcMessage);
  11. }

这个 nettyClient.getChannel(socketAddress) 是重连机制的秘密:

  1. /**
  2. * 获取和指定地址连接的 channel,如果获取不到,则连接
  3. *
  4. * @param address 指定要连接的地址
  5. * @return channel
  6. */
  7. public Channel getChannel(SocketAddress address) {
  8. // 根据地址从缓存中获取 Channel
  9. Channel channel = CHANNEL_MAP.get(address);
  10. // 如果获取不到,或者 channel 已经断开,则重连,然后放到 CHANNEL_MAP 缓存起来
  11. if (channel == null || !channel.isActive()) {
  12. // 连接
  13. channel = connect(address);
  14. CHANNEL_MAP.put(address, channel);
  15. }
  16. return channel;
  17. }

代码一目了然,就是使用了 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. mongodb使用场景及与mysql区别

    MySQL是关系型数据库. 优势: 在不同的引擎上有不同 的存储方式. 查询语句是使用传统的sql语句,拥有较为成熟的体系,成熟度很高. 开源数据库的份额在不断增加,mysql的份额页在持续增长. 缺 ...

  2. 使用C#winform编写渗透测试工具--Web指纹识别

    使用C#winform编写渗透测试工具--web指纹识别 本篇文章主要介绍使用C#winform编写渗透测试工具--Web指纹识别.在渗透测试中,web指纹识别是信息收集关键的一步,通常是使用各种工具 ...

  3. 痞子衡嵌入式:嵌入式Cortex-M中断向量表原理及其重定向方法

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是Cortex-M中断向量表原理及其重定向方法. 接着前文 <嵌入式Cortex-M裸机环境下临界区保护的三种实现> 继续聊, ...

  4. Dubbo 实现一个Route Factory(用于灰度发布)

    Dubbo 可以实现的扩展很多, 官方文档在这: https://dubbo.apache.org/zh/docs/v2.7/dev/impls/ (太简单了....) 下面我们实现一个Route F ...

  5. Linux下-LNMP环境搭建博客网站(全过程)

    通常我们所说的LNMP是指一个网站基本的组织框架,即Linux系统支持,Nginx静态服务,Mysql数据库支持以及PHP动态编程语言支持.目前Mysql数据库被Oracle数据库分析公司收购,其创始 ...

  6. TypeScript学习笔记(一)环境搭建和数据类型

    目录 一.学习TypeScript的缘由 二.学习环境的搭建 1. TypeScript的编译环境 2. vscode自动编译的配置 三.TypeScript中的数据类型 1. 简单变量的定义和初始化 ...

  7. 为了彻底搞懂 hashCode,我钻了一下 JDK 的源码

    今天我们来谈谈 Java 中的 hashCode() 方法--通过源码的角度.众所周知,Java 是一门面向对象的编程语言,所有的类都会默认继承自 Object 类,而 Object 的中文意思就是& ...

  8. Solidity

    起因是Xenc师傅给我截了张图,我日 居然看不懂 ,一搜才知道,之前学的版本有些老了.. 这次学下新一点的记录下 HelloWorld pragma solidity ^0.6.0; // versi ...

  9. 9419页最新一线互联网Android面试题解析大全

    网上高级工程师面试相关文章鱼龙混杂,要么一堆内容,要么内容质量太浅, 鉴于此我整理了如下安卓开发高级工程师面试题以及答案帮助大家顺利进阶,下面进入正题: 一.Android相关 1.Activity ...

  10. netty系列之:文本聊天室

    目录 简介 聊天室的工作流程 文本处理器 初始化ChannelHandler 真正的消息处理逻辑 总结 简介 经过之前的系列文章,我们已经知道了netty的运行原理,还介绍了基本的netty服务搭建流 ...