粘 / 黏
TCP是一个字节流协议,所谓流,就像流水一样,是连成一片的,没有分割线,你没法知道什么时候开始,什么时候结束,也就是我们通过TCP传输的数据是一连串没有界限的数据,TCP底层并不了解上层要传输的业务数据的具体含义,TCP只会根据缓冲区的大小及实际情况进行数据包的分割,那么我们一个完整的业务数据,可能会被TCP拆分成多个包进行发送,也有可能业务上的多条完整数据被合并成一个包发送,这就是TCP的粘包和拆包问题;
一个TCP协议传输的过程:
发送端的字节流都会先传入缓冲区,再通过网络传入到接收端的缓冲区中,最终由接收端获取;
1、第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象;
2、第二种情况,接收端只收到一个数据包,由于TCP是不会出现丢包的,所以一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包,这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理;
3、第三种情况,这种情况有两种表现形式,接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包,这两种情况如果不加特殊处理,对于接收端同样是不好处理的;
粘包和拆包发生原因
1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包;
2、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包;
3、待发送数据大于最大报文长度,TCP在传输前将进行拆包;
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;
粘包和拆包解决策略
由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,该问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,归纳如下:
消息定长:发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从缓冲区中读取固定长度的数据,这就自然而然的把每个数据包拆分开来;
设置消息边界:服务端从网络流中按消息边界分离出消息内容,比如在数据包末尾增加回车换行符进行分割;
将消息分为消息头和消息体:消息头中包含表示消息总长度(或者消息体长度)的字段,消息体是要读取的内容;
更复杂的应用层协议:比如Netty中实现的一些协议对粘包、拆包进行处理;
Netty粘包和拆包解决方案
Netty框架对于客户端和服务端之间的数据传输做了很好的处理,客户端在发送数据之前先对数据按一定的规则进行编码,服务端在接收到数据后按照相同的规则进行解码,这就是Netty解决粘包拆包问题的思路;
对于粘包拆包问题,Netty 已经为我们提供了很多不同的解码器,在无必要时不必重复发明轮子,我们可以直接使用Netty现成的解码器即可;
Netty中提供如下四种解码器用来解决粘包和拆包问题:
1、固定长度解码器FixedLengthFrameDecoder;
每个应用层数据包都拆分成都是固定长度的大小,比如1024字节;
对于使用固定长度的粘包和拆包场景,可以使用FixedLengthFrameDecoder,该解码器会每次读取固定长度的消息,如果当前读取到的消息不足指定长度,那么就会等待下一个消息到达后进行补足,其使用也比较简单,只需要在构造函数中指定每个消息的长度即可;
由于解码有可能需要等待下一个包进行补全,代码相对复杂,所以Netty框架帮我们提供了解码器,但是对于编码器,需要用户自行编写,因为编码时只需要将不足指定长度的部分进行补全即可,代码比较简单,所以Netty就没有帮我们实现编码器;
数据在编码发送的时候,以固定长度作为一条完整的消息,代码实现:
channelPipeline.addLast(new FixedLengthFrameDecoder(22));
这里面的22表示占用的字节个数,utf-8编码下,一个汉字占3个字节,一个英文字母占1个字节;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
/**
* 服务handler
*
* @author liulei
*/
@Slf4j
@ChannelHandler.Sharable
public class EChoServerHandler extends ChannelInboundHandlerAdapter {
public EChoServerHandler() {
super();
log.info("EChoServerHandler方法执行");
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
super.channelRegistered(ctx);
log.info("channelRegistered方法执行");
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
super.channelUnregistered(ctx);
log.info("channelUnregistered方法执行");
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
super.channelActive(ctx);
log.info("channelActive方法执行");
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
log.info("channelInactive方法执行");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf)msg;
System.out.println("服务端接受到消息:"+byteBuf.toString(CharsetUtil.UTF_8));
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
super.channelReadComplete(ctx);
log.info("channelReadComplete方法执行");
// 数据读完后回调用此方法
// 读取完后关闭
//ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
super.userEventTriggered(ctx, evt);
log.info("userEventTriggered方法执行");
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
super.channelWritabilityChanged(ctx);
log.info("channelWritabilityChanged方法执行");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
log.info("exceptionCaught方法执行");
}
@Override
public boolean isSharable() {
log.info("isSharable方法执行");
return super.isSharable();
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
super.handlerAdded(ctx);
log.info("handlerAdded方法执行");
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
super.handlerRemoved(ctx);
log.info("handlerRemoved方法执行");
}
}
@Slf4j
public class EchoServerRun {
public static final int port = 8888;
public static void main(String[] args) {
EchoServerRun echoServerRun = new EchoServerRun();
echoServerRun.run();
}
public void run(){
final EChoServerHandler eChoServerHandler = new EChoServerHandler();
// 创建线程池
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
// 创建服务启动类
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 给启动引导类进行配置
serverBootstrap.group(eventLoopGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(eChoServerHandler);
}
});
try {
// 端口绑定
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
}
finally {
// 优雅关闭
eventLoopGroup.shutdownGracefully();
}
}
}
/**
* @author liulei
**/
@Slf4j
public class EChoClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
super.channelRegistered(ctx);
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
super.channelUnregistered(ctx);
}
/**
* 连接通道建立
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String message = "hello,netty.";
for (int i = 0; i < 100; i++) {
ctx.writeAndFlush(Unpooled.copiedBuffer(message.getBytes(CharsetUtil.UTF_8)));
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
}
/**
* 数据连接被建立
*
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
super.channelReadComplete(ctx);
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
super.userEventTriggered(ctx, evt);
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
super.channelWritabilityChanged(ctx);
}
/**
* 捕获一个异常时调用
*
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
}
/**
* @author liulei
**/
public class EChoClientRun {
public static void main(String[] args) {
EChoClientRun eChoClientRun = new EChoClientRun();
eChoClientRun.run();
}
public void run() {
final EChoClientHandler eChoServerHandler = new EChoClientHandler();
// 创建线程池
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
// 启动引导类
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(eChoServerHandler);
}
});
//绑定一个端口,返回未来的通道 .bind() --> udp
try {
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8888).sync();
Channel channel = channelFuture.channel();
channel.writeAndFlush(Unpooled.copiedBuffer("123", CharsetUtil.UTF_8));
// 当channel被关闭的时候会通知此处关闭chanel(closeFuture方法)
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
eventLoopGroup.shutdownGracefully();
}
}
}
客户端发送了100个hello,netty.服务端接受结果为:
发生了粘包现象我们修改用固定长度解码器FixedLengthFrameDecoder
客户端和服务端均要加上字节长度。
2、行解码器LineBasedFrameDecoder
每个应用层数据包,都以换行符作为分隔符(\r\n或者\n),进行分割拆分;
数据在编码发送的时候,会以换行符作为一条完整的消息;
也没有提供编码器;
代码实现:
pipeline.addLast(new LineBasedFrameDecoder(Integer.MAX_VALUE));
String message = "hello,netty.";
for (int i = 0; i < 100; i++) {
ctx.writeAndFlush(Unpooled.copiedBuffer(message.concat("\n").getBytes(CharsetUtil.UTF_8)));
}
3、分隔符解码器DelimiterBasedFrameDecoder
每个应用层数据包,通过自定义的分隔符进行分割拆分,该解码器与LineBasedFrameDecoder本质上是一样的,都是使用分隔符对数据包进行拆分,只是可以指定自己的分割符;
数据在编码发送的时候,会以一个自定义的分隔符作为一条完整的消息;
也没有提供编码器;
代码实现:
channelPipeline.addLast(
new DelimiterBasedFrameDecoder(Integer.MAX_VALUE,
Unpooled.copiedBuffer("$", CharsetUtil.UTF_8))
);
4、基于数据包长度的解码器 LengthFieldBasedFrameDecoder(重要)
将应用层数据包的长度,作为接收端应用层数据包的拆分依据,按照应用层数据包的大小解码,这个解码器要求应用层协议中包含数据包的长度,这里的长度是动态长度; abcd sduihfd32 09werferjgvlk
一般是LengthFieldBasedFrameDecoder与LengthFieldPrepender配合起来使用,前者是解码,后者是编码,它们处理粘拆包的主要思想是在生成的数据包中添加一个长度字段,用于记录当前数据包的长度,
LengthFieldBasedFrameDecoder会按照参数指定的包长度对接收到的数据进行解码,从而得到目标消息体数据,而LengthFieldPrepender则会在响应的数据前面添加指定的包长度,这个包长度保存了当前消息体的整体字节数据长度;
数据在编码发送的时候,会指定当前这条消息的长度;
使用前,对其构造函数参数进行说明:
maxFrameLength:指定了每个包所能传递的最大数据包大小;
lengthFieldOffset:指定了长度字段在字节码中的偏移量;
lengthFieldLength:指定了长度字段所占用的字节长度;
lengthFieldOffset和lengthFieldLength可以去掉长度字段
lengthAdjustment:对一些不仅包含有消息头和消息体的数据进行消息头的长度的调整,这样就可以只得到消息体的数据,这里的lengthAdjustment指定的就是消息头的长度;
initialBytesToStrip:对于长度字段在消息头中间的情况,可以通过
initialBytesToStrip忽略掉消息头以及长度字段占用的字节,从而得到消息体的内容和lengthFieldLength长度字节保存一致;
LengthFieldBasedFrameDecoder内部实现原理:
首先对长度字段进行设置,如果需要包含消息长度自身,则在原来长度的基础之上再加上lengthFieldLength的长度;
如果调整后的消息长度小于0,则抛出参数非法异常,对消息长度自身所占的字节数进行判断,以便采用正确的方法将长度字段写入到ByteBuf中,共有以下6种可能:
channelPipeline.addLast(new LengthFieldBasedFrameDecoder(
1024, 0, 4, 0, 4));
channelPipeline.addLast(new LengthFieldPrepender(4));