netty数据包
一、TCP粘包和拆包
TCP拆包:一个完整的包可能会被TCP拆分成多个包进行发送
TCP粘包:把多个小的包封装成一个大的数据包发送,Client发送的若干数据包,Server接收粘成一包。
发送方和接收方都可能出现这个问题:
发送方原因:TCP默认使用Nagle算法
接收方原因:TCP接收数据防止缓存中,应用程序中缓存中读取
解决办法:
发送方:可以关闭nagle算法
接收方:TCO是无边界的数据流,并没有处理粘包现象的机制
协议本身无法避免粘包,半包读写的发生需要在应用层处理。应用层处理半包读写的办法:
- 设置定长消息(如10字符),aaaaaaaaa0aaaaaaaaa0
FixedLengthFrameDecoder
- 设置消息的边界(如&&进行切割),sfdsfsdfasdf&&dfasfas&&sfasdf
LineBasedFrameDecoderr和DelemiterBasedFrameDecoder
- 使用带消息头的协议,消息头存储消息开始标识及消息的长度信息:header+body
LengthFieldBasedFrameDecoder
在clienthandler里通过以下方式可以模拟粘包问题,server端如果没有做特定的处理,就会出现一次性读取数据的操作。
//通道准备就绪
@Override public void channelActive(ChannelHandlerContext ctx) throws Exception{
ByteBuf byteBuf = null;
for (int i = 0; i < 10; i++) {
byte[] bytes = ("tcp client" + System.lineSeparator()).getBytes();
byteBuf = Unpooled.buffer(bytes.length);
byteBuf.writeBytes(bytes);
ctx.writeAndFlush(byteBuf);
}
}二、半包问题解决方式
LineBasedFrameDecoder:换行的方式解决,但是每次换行的数据包长度不能大于设定的长度,否则会报错,如client端发送了一个2048字节长度的才换行,server端接收为1024字节,server端就会报异常,无法处理这个消息。
//将对应的channelHandler添加到管道口 //解决半包问题,设置长度 pipeline.addLast(new LineBasedFrameDecoder(1024)); //将消息转成字符串,重写read方法的时候,message对象会是个String对象,不是ByteBuf对象 pipeline.addLast(new StringDecoder()); pipeline.addLast(new TcpServerHandler());DelemiterBasedFrameDecoder:指定分隔符的方式类似上面的换行分割。与上面不同的是DelemiterBasedFrameDecoder构造器有四个参数
maxlength:表示一行的最大长,超过这个长度还没检测到分隔符就会报TooLongFrameException
failFast:如果为true,则超出maxLength长度就抛出异常,不继续进行解码。如果为false,等取到完整的消息并且解码后,在抛出TooLongFrameException
stripDelimiter:解码后的消息是否去掉分隔符。
delimiters:分隔符,参数对象为ByteBuf类型
//将对应的channelHandler添加到管道口 //以字符串内容为分隔符 ByteBuf byteBuf = Unpooled.copiedBuffer("&&".getBytes()); //解决半包问题,设置长度 pipeline.addLast(new DelimiterBasedFrameDecoder(1024,byteBuf)); //将消息转成字符串,重写read方法的时候,message对象会是个String对象,不是ByteBuf对象 pipeline.addLast(new StringDecoder()); pipeline.addLast(new TcpServerHandler());LengthFieldBasedFrameDecoder
maxFrameLength:数据包的最大长度
lengthFieldOffset:长度字段的偏移位,长度字段开始的地方,意思是跳过指定长度的字节以后才是真正的消息体
lengthFieldLength:长度字段占的字节数,帧数据长度的字段本身的长度
lengthAdjustment:一般是header+body,添加到长度字段的补偿值,如果为负数,开发人员认为这个header长度是整个消息包的长度,则netty应该减去对应的数字
initialBytesToStrip:从解码帧当中第一次去除的字节数,获取一个完整的数据包以后,忽略前面指定位数的长度字节,应用解码器拿到的是不带长度域的数据包
failFast:是否快速失败
三、自定义数据处理
数据包类:
import lombok.Data;
/**
* @Description netty自定义消息对象
* @Author lcy
* @Date 2021/10/25 16:01
*/
@Data
public class MessageProtocol {
/**
* 消息长度
*/
private int length;
/**
* 消息体
*/
private byte[] bytes;
}编码器:
/**
* @Description 自定义消息编码器对象
* @Author lcy
* @Date 2021/10/25 16:00
*/
public class MessageEncode extends MessageToByteEncoder<MessageProtocol> {
@Override protected void encode(ChannelHandlerContext ctx,MessageProtocol msg,ByteBuf out){
System.out.println("调用编码器");
//设置数据包长度
out.writeInt(msg.getLength());
//设置数据包
out.writeBytes(msg.getBytes());
}
}解码器:
import java.util.List;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
/**
* @Description 自定义消息解码器对象
* @Author lcy
* @Date 2021/10/25 16:00
*/
public class MessageDecode extends ByteToMessageDecoder {
/**
* 记录获取数据包长度,用于解决拆包问题,默认为0
*/
private int length = 0;
@Override protected void decode(ChannelHandlerContext ctx,ByteBuf in,List<Object> out){
//判断是否为真实的数据,前面4个字节为报文的长度
if (in.readableBytes() < 4) {
return;
}
//判断数据包长度,如果没有发生拆包,赋值
if (length == 0) {
length = in.readInt();
}
//如果缓冲区字节数据小于数据包长度,直接返回。发生拆包,等待下一个包再处理
if (in.readableBytes() < length) {
System.out.println("发生拆包,等待下一个数据包");
return;
}
//读取数据
byte[] content = new byte[length];
if (in.readableBytes() >= length) {
in.readBytes(content);
//封装成自定义对象
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLength(length);
messageProtocol.setBytes(content);
//传输到下一个管道当中
out.add(messageProtocol);
}
//重置长度
length = 0;
}
}netty客户端和服务端在添加pipeline的时候只需要添加对应的编码器和解码器,传输的时候通过数据类来传输即可
