1. 编码和解码器
在netty中编码器和解码器,其本质也是ChannelHandler的一种实现。
1.1 半包粘包问题
产生原因:TCP/IP协议是面向流的协议。
当客户端向服务器端发送数据时,会把数据划分为一个一个的包进行发送,服务器端收到数据时,也是一个一个的包,那么服务器端怎么知道一个请求包含几个包呢?即服务器端怎么把一个一个包组装成一个完整请求呢?
1.2 粘包/拆包解决思路
不断从TCP缓冲区中读取数据,每次读取完都需要判断是否是一个完整的请求数据。
- 若当前读取的数据不足以拼接成一个完整的请求数据,那就保留该数据,继续从tcp缓冲区中读取,直到得到一个完整的请求数据。
- 若当前读到的数据加上已经读取的数据足够拼接成一个请求数据,那就将已经读取的数据拼接上本次读取的数据,构成一个完整的请求传递到业务逻辑,多余的数据仍然保留,以便和下次读到的数据尝试拼接。
如何判断一个完整的业务数据包呢?
- 定长:客户端和服务器协商一个请求为N个字节。
- 分隔符:客户端和服务器端协商以一个分隔符作为标记,表示一个完整的业务数据包
- 基于长度的变长包:客户端和服务端协商,在请求的最前面用N个字节表示这个请求涉及到的数据长度,后面再传具体数据
1.3 netty常见的编解码器
常用的解码器如下:
- LineBasedFrameDecoder:回车换行解码器,即遇到一个回车换行符,表示一个请求数据的结束。
- DelimiterBasedFrameDecoder:分隔符解码器
- FixedLengthFrameDecoder:固定长度解码器
- LengthFieldBasedFrameDecoder:变长解码器(常用)
对应的常见编码器如下:
- LineEncoder:回车换行编码器,99%的时候没用,理由和分隔符编码器一样
- 分隔符编码器netty未提供,因为你只需要在发送消息时,将分隔符写入到消息末尾即可
- 固定长度编码器netty未提供,因为你在写入消息时,只能写入N个字符,超过N个将会变成多个业务消息,少于N个则需要补齐。
- LengthFieldPrepender:变长编码器(常用)
接下来我们就详细介绍变长编解码器。
1.4 netty常见编解码类继承关系
从上图可以看出,编解码器都是channelHandler,解码器是入站处理器,编码器为出站处理器。
编码器分为MessageToByteEncoder和MessageToMessageEncoder两大类,解码器分为ByteToMessageDecoder和MessageToMessageDecoder两大类。
2. 变长编解码器
2.1 变长编解码器的使用
注意:pipeline上的handler是有顺序要求的哟。尤其是编码器与编码器之间,解码器与解码器之间。即入站handler与入站handler之间,出站handler与出站handler之间。入站handler和出站handler之间则没有顺序要求。
2.2 变长解码器
LengthFieldBasedFrameDecoder详解:
构造函数:
public LengthFieldBasedFrameDecoder(int maxFrameLength,int lengthFieldOffset, int lengthFieldLength,int lengthAdjustment, int initialBytesToStrip)
- maxFrameLength:发送的数据包最大长度;
- lengthFieldOffset:长度域偏移量,指的是长度域位于整个数据包字节数组中的下标;
- lengthFieldLength:长度域的自己的字节数长度;
- lengthAdjustment:长度域的偏移量矫正。 如果长度域的值,除了包含有效数据域的长度外,还包含了其他域(如长度域自身)长度,那么,就需要进行矫正。矫正的值为:包长 - 长度域的值 – 长度域偏移 – 长度域长。
- initialBytesToStrip:丢弃的起始字节数。丢弃处于有效数据前面的字节数量。比如前面有4个节点的长度域,则它的值为4。
一般用法为:new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4),表示消息最大长度为Integer的最大值,长度域在消息的最前面,长度域占4个字节,实际消息在整个字节数组的第四个位置开始。
2.3 变长编码器
LengthFieldPrepender编码器详解:
构造函数:
LengthFieldPrepender(int lengthFieldLength)
- lengthFieldLength:长度域所占字节数,和解码器中的lengthFieldLength对应。
一般用法:new LengthFieldPrepender(4),表示将消息字节数组的长度先写在消息前面,并占4个字节。
2.4 流程解析
根据2.1小结的源码进行。
3. 自定义协议
3.1 编写协议类
public class MyProtocol {
private Integer header;
private Integer length;
private byte[] content;
public Integer getHeader() {
return header;
}
public void setHeader(Integer header) {
this.header = header;
}
public Integer getLength() {
return length;
}
public void setLength(Integer length) {
this.length = length;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
}
3.2 编码器
public class MyEncoder extends MessageToByteEncoder<MyProtocol> {
@Override
protected void encode(ChannelHandlerContext ctx, MyProtocol msg, ByteBuf out) throws Exception {
out.writeInt(msg.getHeader());
out.writeInt(msg.getLength());
out.writeBytes(msg.getContent());
}
}
3.3 解码器
public class MyDecoder extends ReplayingDecoder<MyProtocol> {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int header=in.readInt();
int length=in.readInt();
byte[] content=new byte[length];
in.readBytes(content);
MyProtocol myProtocol =new MyProtocol();
myProtocol.setHeader(header);
myProtocol.setContent(content);
myProtocol.setLength(length);
out.add(myProtocol);
}
}
为什么继承ReplayingDecoder而不是ByteToMessageDecoder?
ReplayingDecoder是一个特殊的ByteToMessageDecoder ,可以在阻塞的i/o模式下实现非阻塞的解码。
ReplayingDecoder 和ByteToMessageDecoder 最大的不同就是继承ReplayingDecoder 并实现decode()和decodeLast()方法时,你不需要手动判断接收到的字节数,你可以认为所有字节都已经接收到;如果直接继承ByteToMessageDecoder,则需要你手动判断已经接收到了多少字节。
3.4 服务器端
public class MyServer {
public static void main(String[] args) {
//负责接收客户端的连接请求
EventLoopGroup boosGroup = new NioEventLoopGroup(1);
//负责接收客户端读写请求
EventLoopGroup workerGroup = new NioEventLoopGroup(1);
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boosGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler())
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyDecoder());
pipeline.addLast(new MyEncoder());
pipeline.addLast(new MyServerHandler1());
}
});
ChannelFuture channelFuture = bootstrap.bind(9999).sync();
System.out.println("系统启动成功!!!");
channelFuture.channel().closeFuture().sync();
System.out.println("系统执行完成!!!");
} catch (Exception e) {
e.printStackTrace();
} finally {
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class MyServerHandler1 extends SimpleChannelInboundHandler<MyProtocol> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, MyProtocol msg) throws Exception {
System.out.println("服务端接收到的数据:");
System.out.println("版本:"+ msg.getHeader());
System.out.println("长度:"+msg.getLength());
System.out.println("内容:"+new String(msg.getContent(),"UTF-8"));
MyProtocol myProtocol =new MyProtocol();
myProtocol.setContent("你好,客户端".getBytes("UTF-8"));
myProtocol.setLength(myProtocol.getContent().length);
myProtocol.setHeader(1);
ctx.writeAndFlush(myProtocol);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
3.5 客户端
public class MyClient {
public static void main(String[] args) {
EventLoopGroup group=new NioEventLoopGroup();
try {
Bootstrap boot = new Bootstrap();
boot.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline=ch.pipeline();
pipeline.addLast(new MyDecoder());
pipeline.addLast(new MyEncoder());
pipeline.addLast(new MyClientHandler());
}
});
ChannelFuture channelFuture = boot.connect("localhost", 9999).sync();
channelFuture.channel().closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
group.shutdownGracefully();
}
}
}
public class MyClientHandler extends SimpleChannelInboundHandler<MyProtocol> {
/**
*
* @param ctx 上下文请求对象
* @param msg 表示服务端发来的消息
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, MyProtocol msg) throws Exception {
System.out.println("客户端收到的消息:");
System.out.println("版本:"+ msg.getHeader());
System.out.println("长度:"+msg.getLength());
System.out.println("内容:"+new String(msg.getContent(),"UTF-8"));
}
/**
* 如果没有这个方法,Client并不会主动发消息给Server
* 那么Server的channelRead0无法触发,导致Client的channelRead0也无法触发
* 这个channelActive可以让Client连接后,发送一条消息
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
MyProtocol myProtocol =new MyProtocol();
myProtocol.setHeader(1);
myProtocol.setContent("你好,服务器".getBytes("UTF-8"));
myProtocol.setLength(myProtocol.getContent().length);
ctx.writeAndFlush(myProtocol);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
参考博客:https://blog.csdn.net/qq_37909508/article/category/8983741/5