What

 顾名思义, 所谓 心跳, 即在 TCP 长连接中, 客户端和服务器之间定期发送的一种特殊的数据包, 通知对方自己还在线, 以确保 TCP 连接的有效性.

Why

因为网络的不可靠性, 有可能在 TCP 保持长连接的过程中, 由于某些突发情况, 例如网线被拔出, 突然掉电等, 会造成服务器和客户端的连接中断. 在这些突发情况下, 如果恰好服务器和客户端之间没有交互的话, 那么它们是不能在短时间内发现对方已经掉线的. 为了解决这个问题, 我们就需要引入 心跳 机制. 心跳机制的工作原理是: 在服务器和客户端之间一定时间内没有数据交互时, 即处于 idle 状态时, 客户端或服务器会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互. 自然地, 当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性

How

我们可以通过两种方式实现心跳机制:


  • 使用 TCP 协议层面的 keepalive 机制.
  • 在应用层上实现自定义的心跳机制.

虽然在 TCP 协议层面上, 提供了 keepalive 保活机制, 但是使用它有几个缺点:


  1. 它不是 TCP 的标准协议, 并且是默认关闭的.
  2. TCP keepalive 机制依赖于操作系统的实现, 默认的 keepalive 心跳时间是 两个小时, 并且对 keepalive 的修改需要系统调用(或者修改系统配置), 灵活性不够.
  3. TCP keepalive 与 TCP 协议绑定, 因此如果需要更换为 UDP 协议时, keepalive 机制就失效了.

虽然使用 TCP 层面的 keepalive 机制比自定义的应用层心跳机制节省流量, 但是基于上面的几点缺点, 一般的实践中, 人们大多数都是选择在应用层上实现自定义的心跳.

既然如此, 那么我们就来大致看看在在 Netty 中是怎么实现心跳的吧. 在 Netty 中, 实现心跳机制的关键是 IdleStateHandler, 它可以对一个 Channel 的 读/写设置定时器, 当 Channel 在一定事件间隔内没有数据交互时(即处于 idle 状态), 就会触发指定的事件.


使用netty实现心跳

上面我们提到了, 在 Netty 中, 实现心跳机制的关键是 IdleStateHandler, 那么这个 Handler 如何使用呢? 我们来看看它的构造器:


  1. ​public IdleStateHandler(int readerIdleTimeSeconds, int writerIdleTimeSeconds, int allIdleTimeSeconds) {​
  2. ​this((long)readerIdleTimeSeconds, (long)writerIdleTimeSeconds, (long)allIdleTimeSeconds, TimeUnit.SECONDS);​
  3. ​}​


实例化一个 IdleStateHandler 需要提供三个参数:


  • readerIdleTimeSeconds, 读超时. 即当在指定的时间间隔内没有从 Channel 读取到数据时, 会触发一个 READER_IDLE 的 IdleStateEvent 事件.
  • writerIdleTimeSeconds, 写超时. 即当在指定的时间间隔内没有数据写入到 Channel 时, 会触发一个 WRITER_IDLE 的 IdleStateEvent 事件.
  • allIdleTimeSeconds, 读/写超时. 即当在指定的时间间隔内没有读或写操作时, 会触发一个 ALL_IDLE 的 IdleStateEvent 事件.

为了展示具体的 IdleStateHandler 实现的心跳机制, 下面我们来构造一个具体的EchoServer 的例子, 这个例子的行为如下:


  1. 在这个例子中, 客户端和服务器通过 TCP 长连接进行通信.
  2. TCP 通信的报文格式是:


  1. ​+--------+-----+---------------+ ​
  2. ​| Length |Type | Content |​
  3. ​| 17 | 1 |"HELLO, WORLD" |​
  4. ​+--------+-----+---------------+​


  5. 客户端每隔一个随机的时间后, 向服务器发送消息, 服务器收到消息后, 立即将收到的消息原封不动地回复给客户端.
  6. 若客户端在指定的时间间隔内没有读/写操作, 则客户端会自动向服务器发送一个 PING 心跳, 服务器收到 PING 心跳消息时, 需要回复一个 PONG 消息.


通用部分


根据上面定义的行为, 我们接下来实现心跳的通用部分 CustomHeartbeatHandler:


  1. ​public abstract class CustomHeartbeatHandler extends SimpleChannelInboundHandler<ByteBuf> {​
  2. ​public static final byte PING_MSG = 1;​
  3. ​public static final byte PONG_MSG = 2;​
  4. ​public static final byte CUSTOM_MSG = 3;​
  5. ​protected String name;​
  6. ​private int heartbeatCount = 0;​

  7. ​public CustomHeartbeatHandler(String name) {​
  8. ​this.name = name;​
  9. ​}​

  10. ​@Override​
  11. ​protected void channelRead0(ChannelHandlerContext context, ByteBuf byteBuf) throws Exception {​
  12. ​if (byteBuf.getByte(4) == PING_MSG) {​
  13. ​sendPongMsg(context);​
  14. ​} else if (byteBuf.getByte(4) == PONG_MSG){​
  15. ​System.out.println(name + " get pong msg from " + context.channel().remoteAddress());​
  16. ​} else {​
  17. ​handleData(context, byteBuf);​
  18. ​}​
  19. ​}​

  20. ​protected void sendPingMsg(ChannelHandlerContext context) {​
  21. ​ByteBuf buf = context.alloc().buffer(5);​
  22. ​buf.writeInt(5);​
  23. ​buf.writeByte(PING_MSG);​
  24. ​context.writeAndFlush(buf);​
  25. ​heartbeatCount++;​
  26. ​System.out.println(name + " sent ping msg to " + context.channel().remoteAddress() + ", count: " + heartbeatCount);​
  27. ​}​

  28. ​private void sendPongMsg(ChannelHandlerContext context) {​
  29. ​ByteBuf buf = context.alloc().buffer(5);​
  30. ​buf.writeInt(5);​
  31. ​buf.writeByte(PONG_MSG);​
  32. ​context.channel().writeAndFlush(buf);​
  33. ​heartbeatCount++;​
  34. ​System.out.println(name + " sent pong msg to " + context.channel().remoteAddress() + ", count: " + heartbeatCount);​
  35. ​}​

  36. ​protected abstract void handleData(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf);​

  37. ​@Override​
  38. ​public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {​
  39. ​// IdleStateHandler 所产生的 IdleStateEvent 的处理逻辑.​
  40. ​if (evt instanceof IdleStateEvent) {​
  41. ​IdleStateEvent e = (IdleStateEvent) evt;​
  42. ​switch (e.state()) {​
  43. ​case READER_IDLE:​
  44. ​handleReaderIdle(ctx);​
  45. ​break;​
  46. ​case WRITER_IDLE:​
  47. ​handleWriterIdle(ctx);​
  48. ​break;​
  49. ​case ALL_IDLE:​
  50. ​handleAllIdle(ctx);​
  51. ​break;​
  52. ​default:​
  53. ​break;​
  54. ​}​
  55. ​}​
  56. ​}​

  57. ​@Override​
  58. ​public void channelActive(ChannelHandlerContext ctx) throws Exception {​
  59. ​System.err.println("---" + ctx.channel().remoteAddress() + " is active---");​
  60. ​}​

  61. ​@Override​
  62. ​public void channelInactive(ChannelHandlerContext ctx) throws Exception {​
  63. ​System.err.println("---" + ctx.channel().remoteAddress() + " is inactive---");​
  64. ​}​

  65. ​protected void handleReaderIdle(ChannelHandlerContext ctx) {​
  66. ​System.err.println("---READER_IDLE---");​
  67. ​}​

  68. ​protected void handleWriterIdle(ChannelHandlerContext ctx) {​
  69. ​System.err.println("---WRITER_IDLE---");​
  70. ​}​

  71. ​protected void handleAllIdle(ChannelHandlerContext ctx) {​
  72. ​System.err.println("---ALL_IDLE---");​
  73. ​}​
  74. ​}​

类 CustomHeartbeatHandler 负责心跳的发送和接收, 我们接下来详细地分析一下它的作用. 我们在前面提到, IdleStateHandler 是实现心跳的关键, 它会根据不同的 IO idle 类型来产生不同的 IdleStateEvent 事件, 而这个事件的捕获, 其实就是在 userEventTriggered 方法中实现的.

我们来看看 CustomHeartbeatHandler.userEventTriggered 的具体实现:



  1. ​@Override​
  2. ​public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {​
  3. ​if (evt instanceof IdleStateEvent) {​
  4. ​IdleStateEvent e = (IdleStateEvent) evt;​
  5. ​switch (e.state()) {​
  6. ​case READER_IDLE:​
  7. ​handleReaderIdle(ctx);​
  8. ​break;​
  9. ​case WRITER_IDLE:​
  10. ​handleWriterIdle(ctx);​
  11. ​break;​
  12. ​case ALL_IDLE:​
  13. ​handleAllIdle(ctx);​
  14. ​break;​
  15. ​default:​
  16. ​break;​
  17. ​}​
  18. ​}​
  19. ​}​


在 userEventTriggered 中, 根据 IdleStateEvent 的 state() 的不同, 而进行不同的处理. 例如如果是读取数据 idle, 则 e.state() == READER_IDLE, 因此

就调用 handleReaderIdle 来处理它. CustomHeartbeatHandler 提供了三个 idle 处理方法: handleReaderIdle, handleWriterIdle, handleAllIdle,

这三个方法目前只有默认的实现, 它需要在子类中进行重写, 现在我们暂时略过它们, 在具体的客户端和服务器的实现部分时再来看它们.

知道了这一点后, 我们接下来看看数据处理部分:


  1. ​@Override​
  2. ​protected void channelRead0(ChannelHandlerContext context, ByteBuf byteBuf) throws Exception {​
  3. ​if (byteBuf.getByte(4) == PING_MSG) {​
  4. ​sendPongMsg(context);​
  5. ​} else if (byteBuf.getByte(4) == PONG_MSG){​
  6. ​System.out.println(name + " get pong msg from " + context.channel().remoteAddress());​
  7. ​} else {​
  8. ​handleData(context, byteBuf);​
  9. ​}​
  10. ​}​

在 CustomHeartbeatHandler.channelRead0 中, 我们首先根据报文协议:


  1. ​+--------+-----+---------------+ ​
  2. ​| Length |Type | Content |​
  3. ​| 17 | 1 |"HELLO, WORLD" |​
  4. ​+--------+-----+---------------+​

来判断当前的报文类型, 如果是 PING_MSG 则表示是服务器收到客户端的 PING 消息, 此时服务器需要回复一个 PONG 消息, 其消息类型是 PONG_MSG.

扔报文类型是 PONG_MSG, 则表示是客户端收到服务器发送的 PONG 消息, 此时打印一个 log 即可.

客户端部分

客户端初始化:



  1. ​public class Client {​
  2. ​public static void main(String[] args) {​
  3. ​NioEventLoopGroup workGroup = new NioEventLoopGroup(4);​
  4. ​Random random = new Random(System.currentTimeMillis());​
  5. ​try {​
  6. ​Bootstrap bootstrap = new Bootstrap();​
  7. ​bootstrap​
  8. ​.group(workGroup)​
  9. ​.channel(NioSocketChannel.class)​
  10. ​.handler(new ChannelInitializer<SocketChannel>() {​
  11. ​protected void initChannel(SocketChannel socketChannel) throws Exception {​
  12. ​ChannelPipeline p = socketChannel.pipeline();​
  13. ​p.addLast(new IdleStateHandler(0, 0, 5));​
  14. ​p.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, -4, 0));​
  15. ​p.addLast(new ClientHandler());​
  16. ​}​
  17. ​});​

  18. ​Channel ch = bootstrap.remoteAddress("127.0.0.1", 12345).connect().sync().channel();​
  19. ​for (int i = 0; i < 10; i++) {​
  20. ​String content = "client msg " + i;​
  21. ​ByteBuf buf = ch.alloc().buffer();​
  22. ​buf.writeInt(5 + content.getBytes().length);​
  23. ​buf.writeByte(CustomHeartbeatHandler.CUSTOM_MSG);​
  24. ​buf.writeBytes(content.getBytes());​
  25. ​ch.writeAndFlush(buf);​

  26. ​Thread.sleep(random.nextInt(20000));​
  27. ​}​
  28. ​} catch (Exception e) {​
  29. ​throw new RuntimeException(e);​
  30. ​} finally {​
  31. ​workGroup.shutdownGracefully();​
  32. ​}​
  33. ​}​
  34. ​}​

上面的代码是 Netty 的客户端端的初始化代码, 使用过 Netty 的朋友对这个代码应该不会陌生. 别的部分我们就不再赘述, 我们来看看

ChannelInitializer.initChannel 部分即可:



  1. ​.handler(new ChannelInitializer<SocketChannel>() {​
  2. ​protected void initChannel(SocketChannel socketChannel) throws Exception {​
  3. ​ChannelPipeline p = socketChannel.pipeline();​
  4. ​p.addLast(new IdleStateHandler(0, 0, 5));​
  5. ​p.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, -4, 0));​
  6. ​p.addLast(new ClientHandler());​
  7. ​}​
  8. ​});​

我们给 pipeline 添加了三个 Handler, IdleStateHandler 这个 handler 是心跳机制的核心, 我们为客户端端设置了读写 idle 超时, 时间间隔是5s, 即如果客


户端在间隔 5s 后都没有收到服务器的消息或向服务器发送消息, 则产生 ALL_IDLE 事件.

接下来我们添加了 LengthFieldBasedFrameDecoder, 它是负责解析我们的 TCP 报文, 因为和本文的目的无关, 因此这里不详细展开.

最后一个 Handler 是 ClientHandler, 它继承于 CustomHeartbeatHandler, 是我们处理业务逻辑部分.


客户端


  1. ​public class ClientHandler extends CustomHeartbeatHandler {​
  2. ​public ClientHandler() {​
  3. ​super("client");​
  4. ​}​

  5. ​@Override​
  6. ​protected void handleData(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) {​
  7. ​byte[] data = new byte[byteBuf.readableBytes() - 5];​
  8. ​byteBuf.skipBytes(5);​
  9. ​byteBuf.readBytes(data);​
  10. ​String content = new String(data);​
  11. ​System.out.println(name + " get content: " + content);​
  12. ​}​

  13. ​@Override​
  14. ​protected void handleAllIdle(ChannelHandlerContext ctx) {​
  15. ​super.handleAllIdle(ctx);​
  16. ​sendPingMsg(ctx);​
  17. ​}​
  18. ​}​

ClientHandler 继承于 CustomHeartbeatHandler, 它重写了两个方法, 一个是 handleData, 在这里面实现 仅仅打印收到的消息.

第二个重写的方法是 handleAllIdle. 我们在前面提到, 客户端负责发送心跳的 PING 消息, 当客户端产生一个 ALL_IDLE 事件后, 会导致父类的

CustomHeartbeatHandler.userEventTriggered 调用, 而 userEventTriggered 中会根据 e.state() 来调用不同的方法, 因此最后调用的是 

ClientHandler.handleAllIdle, 在这个方法中, 客户端调用 sendPingMsg 向服务器发送一个 PING 消息.


服务器部分

初始化服务器



  1. ​public class Server {​
  2. ​public static void main(String[] args) {​
  3. ​NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);​
  4. ​NioEventLoopGroup workGroup = new NioEventLoopGroup(4);​
  5. ​try {​
  6. ​ServerBootstrap bootstrap = new ServerBootstrap();​
  7. ​bootstrap​
  8. ​.group(bossGroup, workGroup)​
  9. ​.channel(NioServerSocketChannel.class)​
  10. ​.childHandler(new ChannelInitializer<SocketChannel>() {​
  11. ​protected void initChannel(SocketChannel socketChannel) throws Exception {​
  12. ​ChannelPipeline p = socketChannel.pipeline();​
  13. ​p.addLast(new IdleStateHandler(10, 0, 0));​
  14. ​p.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, -4, 0));​
  15. ​p.addLast(new ServerHandler());​
  16. ​}​
  17. ​});​

  18. ​Channel ch = bootstrap.bind(12345).sync().channel();​
  19. ​ch.closeFuture().sync();​
  20. ​} catch (Exception e) {​
  21. ​throw new RuntimeException(e);​
  22. ​} finally {​
  23. ​bossGroup.shutdownGracefully();​
  24. ​workGroup.shutdownGracefully();​
  25. ​}​
  26. ​}​
  27. ​}​

服务器的初始化部分也没有什么好说的, 它也和客户端的初始化一样, 为 pipeline 添加了三个 Handler


服务器handler



  1. ​public class ServerHandler extends CustomHeartbeatHandler {​
  2. ​public ServerHandler() {​
  3. ​super("server");​
  4. ​}​

  5. ​@Override​
  6. ​protected void handleData(ChannelHandlerContext channelHandlerContext, ByteBuf buf) {​
  7. ​byte[] data = new byte[buf.readableBytes() - 5];​
  8. ​ByteBuf responseBuf = Unpooled.copiedBuffer(buf);​
  9. ​buf.skipBytes(5);​
  10. ​buf.readBytes(data);​
  11. ​String content = new String(data);​
  12. ​System.out.println(name + " get content: " + content);​
  13. ​channelHandlerContext.write(responseBuf);​
  14. ​}​

  15. ​@Override​
  16. ​protected void handleReaderIdle(ChannelHandlerContext ctx) {​
  17. ​super.handleReaderIdle(ctx);​
  18. ​System.err.println("---client " + ctx.channel().remoteAddress().toString() + " reader timeout, close it---");​
  19. ​ctx.close();​
  20. ​}​
  21. ​}​



ServerHandler 继承于 CustomHeartbeatHandler, 它重写了两个方法, 一个是 handleData, 在这里面实现 EchoServer 的功能: 即收到客户端的消息后, 立即原封不动地将消息回复给客户端.

第二个重写的方法是 handleReaderIdle, 因为服务器仅仅对客户端的读 idle 感兴趣, 因此只重新了这个方法. 若服务器在指定时间后没有收到客户端的消息, 则会触发 READER_IDLE 消息, 进而会调用 handleReaderIdle 这个方法. 我们在前面提到, 客户端负责发送心跳的 PING 消息, 并且服务器的 READER_IDLE 的超时时间是客户端发送 PING 消息的间隔的两倍, 因此当服务器 READER_IDLE 触发时, 就可以确定是客户端已经掉线了, 因此服务器直接关闭客户端连接即可.

总结


  1. 使用 Netty 实现心跳机制的关键就是利用 IdleStateHandler 来产生对应的 idle 事件.
  2. 一般是客户端负责发送心跳的 PING 消息, 因此客户端注意关注 ALL_IDLE 事件, 在这个事件触发后, 客户端需要向服务器发送 PING 消息, 告诉服务器"我还存活着".
  3. 服务器是接收客户端的 PING 消息的, 因此服务器关注的是 READER_IDLE 事件, 并且服务器的 READER_IDLE 间隔需要比客户端的 ALL_IDLE 事件间隔大(例如客户端ALL_IDLE 是5s 没有读写时触发, 因此服务器的 READER_IDLE 可以设置为10s)
  4. 当服务器收到客户端的 PING 消息时, 会发送一个 PONG 消息作为回复. 一个 PING-PONG 消息对就是一个心跳交互.