最全浅析 Netty 源码如何实现心跳机制与断线重连,还有不懂么?,2024阿里Java笔试总结

总结

虽然面试套路众多,但对于技术面试来说,主要还是考察一个人的技术能力和沟通能力。不同类型的面试官根据自身的理解问的问题也不尽相同,没有规律可循。

上面提到的关于这些JAVA基础、三大框架、项目经验、并发编程、JVM及调优、网络、设计模式、spring+mybatis源码解读、Mysql调优、分布式监控、消息队列、分布式存储等等面试题笔记及资料

有些面试官喜欢问自己擅长的问题,比如在实际编程中遇到的或者他自己一直在琢磨的这方面的问题,还有些面试官,尤其是大厂的比如 BAT 的面试官喜欢问面试者认为自己擅长的,然后通过提问的方式深挖细节,刨根到底。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

| Length |Type |   Content     |

|   17   |  1  |“HELLO, WORLD” |

±-------±----±--------------+

  1. 客户端每隔一个随机的时间后, 向服务器发送消息, 服务器收到消息后, 立即将收到的消息原封不动地回复给客户端.

  2. 若客户端在指定的时间间隔内没有读/写操作, 则客户端会自动向服务器发送一个 PING 心跳, 服务器收到 PING 心跳消息时, 需要回复一个 PONG 消息.

通用部分

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

/**

* @author xiongyongshun

* @version 1.0

*/

public abstract class CustomHeartbeatHandler extends SimpleChannelInboundHandler {

public static final byte PING_MSG = 1;

public static final byte PONG_MSG = 2;

public static final byte CUSTOM_MSG = 3;

protected String name;

private int heartbeatCount = 0;

public CustomHeartbeatHandler(String name) {

this.name = name;

}

@Override

protected void channelRead0(ChannelHandlerContext context, ByteBuf byteBuf) throws Exception {

if (byteBuf.getByte(4) == PING_MSG) {

sendPongMsg(context);

} else if (byteBuf.getByte(4) == PONG_MSG){

System.out.println(name + " get pong msg from " + context.channel().remoteAddress());

} else {

handleData(context, byteBuf);

}

}

protected void sendPingMsg(ChannelHandlerContext context) {

ByteBuf buf = context.alloc().buffer(5);

buf.writeInt(5);

buf.writeByte(PING_MSG);

context.writeAndFlush(buf);

heartbeatCount++;

System.out.println(name + " sent ping msg to " + context.channel().remoteAddress() + ", count: " + heartbeatCount);

}

private void sendPongMsg(ChannelHandlerContext context) {

ByteBuf buf = context.alloc().buffer(5);

buf.writeInt(5);

buf.writeByte(PONG_MSG);

context.channel().writeAndFlush(buf);

heartbeatCount++;

System.out.println(name + " sent pong msg to " + context.channel().remoteAddress() + ", count: " + heartbeatCount);

}

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

@Override

public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

// IdleStateHandler 所产生的 IdleStateEvent 的处理逻辑.

if (evt instanceof IdleStateEvent) {

IdleStateEvent e = (IdleStateEvent) evt;

switch (e.state()) {

case READER_IDLE:

handleReaderIdle(ctx);

break;

case WRITER_IDLE:

handleWriterIdle(ctx);

break;

case ALL_IDLE:

handleAllIdle(ctx);

break;

default:

break;

}

}

}

@Override

public void channelActive(ChannelHandlerContext ctx) throws Exception {

System.err.println(“—” + ctx.channel().remoteAddress() + " is active—");

}

@Override

public void channelInactive(ChannelHandlerContext ctx) throws Exception {

System.err.println(“—” + ctx.channel().remoteAddress() + " is inactive—");

}

protected void handleReaderIdle(ChannelHandlerContext ctx) {

System.err.println(“—READER_IDLE—”);

}

protected void handleWriterIdle(ChannelHandlerContext ctx) {

System.err.println(“—WRITER_IDLE—”);

}

protected void handleAllIdle(ChannelHandlerContext ctx) {

System.err.println(“—ALL_IDLE—”);

}

}

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

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

@Override

public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

if (evt instanceof IdleStateEvent) {

IdleStateEvent e = (IdleStateEvent) evt;

switch (e.state()) {

case READER_IDLE:

handleReaderIdle(ctx);

break;

case WRITER_IDLE:

handleWriterIdle(ctx);

break;

case ALL_IDLE:

handleAllIdle(ctx);

break;

default:

break;

}

}

}

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

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

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

@Override

protected void channelRead0(ChannelHandlerContext context, ByteBuf byteBuf) throws Exception {

if (byteBuf.getByte(4) == PING_MSG) {

sendPongMsg(context);

} else if (byteBuf.getByte(4) == PONG_MSG){

System.out.println(name + " get pong msg from " + context.channel().remoteAddress());

} else {

handleData(context, byteBuf);

}

}

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

±-------±----±--------------+

| Length |Type |   Content     |

|   17   |  1  |“HELLO, WORLD” |

±-------±----±--------------+

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

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

客户端部分

客户端初始化

public class Client {

public static void main(String[] args) {

NioEventLoopGroup workGroup = new NioEventLoopGroup(4);

Random random = new Random(System.currentTimeMillis());

try {

Bootstrap bootstrap = new Bootstrap();

bootstrap

.group(workGroup)

.channel(NioSocketChannel.class)

.handler(new ChannelInitializer() {

protected void initChannel(SocketChannel socketChannel) throws Exception {

ChannelPipeline p = socketChannel.pipeline();

p.addLast(new IdleStateHandler(0, 0, 5));

p.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, -4, 0));

p.addLast(new ClientHandler());

}

});

Channel ch = bootstrap.remoteAddress(“127.0.0.1”, 12345).connect().sync().channel();

for (int i = 0; i < 10; i++) {

String content = "client msg " + i;

ByteBuf buf = ch.alloc().buffer();

buf.writeInt(5 + content.getBytes().length);

buf.writeByte(CustomHeartbeatHandler.CUSTOM_MSG);

buf.writeBytes(content.getBytes());

ch.writeAndFlush(buf);

Thread.sleep(random.nextInt(20000));

}

} catch (Exception e) {

throw new RuntimeException(e);

} finally {

workGroup.shutdownGracefully();

}

}

}

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

.handler(new ChannelInitializer() {

protected void initChannel(SocketChannel socketChannel) throws Exception {

ChannelPipeline p = socketChannel.pipeline();

p.addLast(new IdleStateHandler(0, 0, 5));

p.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, -4, 0));

p.addLast(new ClientHandler());

}

});

我们给 pipeline 添加了三个 Handler, IdleStateHandler 这个 handler 是心跳机制的核心, 我们为客户端端设置了读写 idle 超时, 时间间隔是5s, 即如果客户端在间隔 5s 后都没有收到服务器的消息或向服务器发送消息, 则产生 ALL_IDLE 事件.

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

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

客户端 Handler

public class ClientHandler extends CustomHeartbeatHandler {

public ClientHandler() {

super(“client”);

}

@Override

protected void handleData(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) {

byte[] data = new byte[byteBuf.readableBytes() - 5];

byteBuf.skipBytes(5);

byteBuf.readBytes(data);

String content = new String(data);

System.out.println(name + " get content: " + content);

}

@Override

protected void handleAllIdle(ChannelHandlerContext ctx) {

super.handleAllIdle(ctx);

sendPingMsg(ctx);

}

}

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

第二个重写的方法是 handleAllIdle. 我们在前面提到, 客户端负责发送心跳的 PING 消息, 当客户端产生一个 ALL_IDLE 事件后, 会导致父类的 CustomHeartbeatHandler.userEventTriggered 调用, 而 userEventTriggered 中会根据 e.state() 来调用不同的方法, 因此最后调用的是 ClientHandler.handleAllIdle, 在这个方法中, 客户端调用 sendPingMsg 向服务器发送一个 PING 消息.

服务器部分

服务器初始化

public class Server {

public static void main(String[] args) {

NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);

NioEventLoopGroup workGroup = new NioEventLoopGroup(4);

try {

ServerBootstrap bootstrap = new ServerBootstrap();

bootstrap

.group(bossGroup, workGroup)

.channel(NioServerSocketChannel.class)

.childHandler(new ChannelInitializer() {

protected void initChannel(SocketChannel socketChannel) throws Exception {

ChannelPipeline p = socketChannel.pipeline();

p.addLast(new IdleStateHandler(10, 0, 0));

p.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, -4, 0));

p.addLast(new ServerHandler());

}

});

Channel ch = bootstrap.bind(12345).sync().channel();

ch.closeFuture().sync();

} catch (Exception e) {

throw new RuntimeException(e);

} finally {

bossGroup.shutdownGracefully();

workGroup.shutdownGracefully();

}

}

}

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

服务器 Handler

public class ServerHandler extends CustomHeartbeatHandler {

public ServerHandler() {

super(“server”);

}

@Override

protected void handleData(ChannelHandlerContext channelHandlerContext, ByteBuf buf) {

byte[] data = new byte[buf.readableBytes() - 5];

ByteBuf responseBuf = Unpooled.copiedBuffer(buf);

buf.skipBytes(5);

buf.readBytes(data);

String content = new String(data);

System.out.println(name + " get content: " + content);

channelHandlerContext.write(responseBuf);

}

@Override

protected void handleReaderIdle(ChannelHandlerContext ctx) {

super.handleReaderIdle(ctx);

System.err.println(“—client " + ctx.channel().remoteAddress().toString() + " reader timeout, close it—”);

ctx.close();

}

}

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

第二个重写的方法是 handleReaderIdle, 因为服务器仅仅对客户端的读 idle 感兴趣, 因此只重新了这个方法. 若服务器在指定时间后没有收到客户端的消息, 则会触发 READER_IDLE 消息, 进而会调用 handleReaderIdle 这个方法.

我们在前面提到, 客户端负责发送心跳的 PING 消息, 并且服务器的 READER_IDLE 的超时时间是客户端发送 PING 消息的间隔的两倍, 因此当服务器 READER_IDLE 触发时, 就可以确定是客户端已经掉线了, 因此服务器直接关闭客户端连接即可.

最后

笔者已经把面试题和答案整理成了面试专题文档

image

image

image

image

image

image

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

消息, 进而会调用 handleReaderIdle 这个方法.

我们在前面提到, 客户端负责发送心跳的 PING 消息, 并且服务器的 READER_IDLE 的超时时间是客户端发送 PING 消息的间隔的两倍, 因此当服务器 READER_IDLE 触发时, 就可以确定是客户端已经掉线了, 因此服务器直接关闭客户端连接即可.

最后

笔者已经把面试题和答案整理成了面试专题文档

[外链图片转存中…(img-18EUoMjR-1715607783693)]

[外链图片转存中…(img-dhOWdZjm-1715607783693)]

[外链图片转存中…(img-W7w2145j-1715607783694)]

[外链图片转存中…(img-oq3Tvosg-1715607783694)]

[外链图片转存中…(img-yoYb8QGq-1715607783694)]

[外链图片转存中…(img-pJCpzADD-1715607783695)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值