一、编解码
通过 Netty 发送或者接收一个消息的时候,就将会发生一次数据转换。转换的原因:网络数据总是一系列的字节。
入站:消息会被解码。也就是说,从字节转换为另一种格式,通常是一个 Java 对象
出站:相反方向转换,当前格式转换成字节

ChannelHandler:充当了处理入站和出站数据的应用程序逻辑容器
ChannelInboundHandler——处理入站数据以及各种状态变化;
ChannelOutboundHandler——处理出站数据并且允许拦截所有的操作。
ChannelPipeline:提供了ChannelHandler链的容器。
如果事件是出站,调用实现ChannelOutboundHandler的handler,从tail到head方向
如果事件是入站,调用实现ChannelInboundHandler的handler,从head到tail方向
Google Protobuf编解码:
Protobuf的优点:
(1)在谷歌内部长期使用,产品成熟度高;
(2)跨语言、支持多种语言;
(3)编码后的消息更小,更加有利于存储和传输;
(4)编解码的性能非常高;
Protobuf开发环境搭建:
2、现在的文件本地解压

3、设置环境变量

输入protoc --version,出现以下信息则安装成功
C:\Users\kbp1234>protoc --version
libprotoc 22.0

4.编写proto文件,生成对应java文件
SubScribeReq.proto:
package netty;
option java_package="com.netty.codec.protobuf";
option java_outer_classname="SubScribeReqDTO";
message SubScribeReq{
required int32 subReqID = 1;
required string userName = 2;
required string productName = 3;
required string address = 4;
}
窗口中输入以下命令:
PS D:\soft\taaaa> protoc --java_out . SubScribeReq.proto

5、netty protobuf 编解码实例:
引入maven依赖:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.21.7</version>
</dependency>
NettyServer:
package com.netty.codec.protobuf;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
public class NettyServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("encode", new ProtobufEncoder());
pipeline.addLast("decoder",new ProtobufDecoder(SubScribeReqDTO.SubScribeReq.getDefaultInstance()));
pipeline.addLast(new NettyServerHandler());
}
});
System.out.println("netty server start。。");
ChannelFuture channelFuture = serverBootstrap.bind(9000).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
NettyServerHandler:
package com.netty.codec.protobuf;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
SubScribeReqDTO.SubScribeReq.Builder builder = new SubScribeReqDTO.SubScribeReq().newBuilderForType();
builder.setSubReqID(1)
.setAddress("上海")
.setUserName("嘿嘿")
.setProductName("手机");
ctx.writeAndFlush(builder);;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
SubScribeReqDTO.SubScribeReq subScribeReq = (SubScribeReqDTO.SubScribeReq)msg;
System.out.println("addresss:" + subScribeReq.getAddress() +
" productName:"+ subScribeReq.getProductName() +
" userName:"+ subScribeReq.getUserName());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
NettyClient:
package com.netty.codec.protobuf;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
public class NettyClient {
public static void main(String[] args) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("encode", new ProtobufEncoder());
pipeline.addLast("decoder", new ProtobufDecoder(SubScribeReqDTO.SubScribeReq.getDefaultInstance()));
pipeline.addLast(new NettyClientHandler());
}
});
System.out.println("netty client start。。");
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9000).sync();
channelFuture.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
NettyClientHandler:
package com.netty.codec.protobuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
SubScribeReqDTO.SubScribeReq req = (SubScribeReqDTO.SubScribeReq)msg;
System.out.println("收到服务器消息:" + req.getAddress());
}
//通道就绪触发该方法
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
SubScribeReqDTO.SubScribeReq.Builder builder = new SubScribeReqDTO.SubScribeReq().newBuilderForType();
builder.setSubReqID(1)
.setAddress("地址")
.setUserName("张三")
.setProductName("产品名字");
ctx.writeAndFlush(builder);;
}
}
二、TCP粘包拆包

假设客户端发送2个数据包D1和D2给服务端,由于服务端一次读取到不确定,存在一下5种情况

粘包的解决策略:
消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格
在包尾增加回车换行符进行分隔符
将消息分为消息头和消息体,消息头中包含表示消息总长度
三、Netty心跳检测机制
心跳:TCP 长连接中, 客户端和服务器之间定期发送的一种特殊的数据包, 通知对方自己还在线, 以确保 TCP 连接的有效性。
在 Netty 中, 实现心跳机制的关键是 IdleStateHandler:
public IdleStateHandler(
long readerIdleTime, long writerIdleTime, long allIdleTime,
TimeUnit unit) {
this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);
}
readerIdleTimeSeconds: 指定的时间间隔内读超时 会触发一个 READER_IDLE 的IdleStateEvent 事件.
writerIdleTimeSeconds: 指定的时间间隔内写超时.,会触发一个 WRITER_IDLE 的IdleStateEvent 事件.
allIdleTimeSeconds: 读/写超时 即当在指定的时间间隔内没有读或写操作时, 会触发一个 ALL_IDLE 的 IdleStateEvent 事件.
添加IdleStateHandler:
package com.netty.codec.protobuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.timeout.IdleStateHandler;
import java.util.concurrent.TimeUnit;
public class IdleStateHandlerInitializer extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//添加空闲状态处理器,当超过 5s 没有收到远端连接的信息时,该处理器会传播一个 IdleStateHandler
pipeline.addLast(new IdleStateHandler(0, 0, 5, TimeUnit.SECONDS));
//添加 IdleStateHandler 传播的 IdleStateEvent 的处理器
pipeline.addLast(new HeartbeatHandler());
//添加普通入站处理器
}
}
添加HeartbeatHandler处理IdleStateEvent事件:
//IdleEvent 事件处理器
public final class HeartbeatHandler extends ChannelInboundHandlerAdapter {
private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled.unreleasableBuffer(
Unpooled.copiedBuffer("HEARTBEAT", Charset.forName("UTF-8")));
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
//当捕获到 IdleStateEvent,发送心跳到远端,并添加一个监听器,如果发送失败就关闭服务端连接
ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate())
.addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
} else {
//不是IdleStateEvent,就将该事件传递到下一个处理器
super.userEventTriggered(ctx, evt);
}
}
}
四、Netty断线自动重连实现
4.1 如何监听到断线重连
1、void channelInactive(ChannelHandlerContext ctx);在客户端关闭时被调用,表示客户端断开连接
2、void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception 入栈时发生异常调用
3、心跳检测断开链接
4.2 客户端向服务端发起重连,仅需加上connect()调用即可:
