1.tcp粘包/拆包原因
2.粘包解决策略
3.具体实现思路
4.netty提供的粘包解决方法
我们都知道Netty是基于NIO的,nio进行客户端与服务端socket编程,在发送消息时,底层是基于TCP传输协议的,首先,TCP协议是基于字节流的,把发送或接受的数据看成一段无结构的字节流,没有边界。其次,在TCP的首部也没有表示数据长度的字段。因此当使用tcp传输数据时,会有粘包和拆包的现象发生。
常见的发生粘包或拆包的原因有以下几种:
1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
由于底层即传输层的TCP无法理解应用层的业务数据,所以在传输层无法保证数据包不被拆分和重组的,因为只能通过应用层协议栈的设计上来解决,解决方案可以归纳为:
1、消息定长。例如将每个报文的大小固定为200字节,如果不够,则用空位补空格
2、在包尾增加回车换行符进行分割或者其他分割符进行消息分割,例如FTP协议
3、将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,消息体为实际要发送的数据
4、更复杂的应用层协议
其实就是具体的实现就是不停的从TCP中读取数据,每次读完数据都要分析是否是一个完整的数据包:
1.如果当前读取的数据不足以拼接成一个完整的业务数据包,那就保留该数据,继续从tcp缓冲区中读取,直到得到一个完整的数据包
2.如果当前读到的数据加上已经读取的数据足够拼接成一个数据包,那就将已经读取的数据拼接上本次读取的数据,够成一个完整的业务数据包传递到业务逻辑,多余的数据仍然保留,以便和下次读到的数据尝试拼接。
1)、一个粘包拆包的案例
服务端:
public class TimerServer {
public static void main(String[] args) throws InterruptedException {
new TimerServer().start(8090);
}
private void start(int port) throws InterruptedException {
NioEventLoopGroup bossGroup = new NioEventLoopGroup();
NioEventLoopGroup workGroup = new NioEventLoopGroup();
try{
ServerBootstrap sb = new ServerBootstrap();
sb.group(bossGroup,workGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,1024)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new TimeServerHandler());
}
});
ChannelFuture cf = sb.bind(port).sync();
cf.channel().closeFuture().sync();
}finally {
// 优雅关闭线程资源
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
private class TimeServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] bytes = new byte[buf.readableBytes()];
buf.readBytes(bytes);
System.out.println("收到客户端发送过来的数据是:" + new String(bytes));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println(cause.getMessage());
ctx.close();
}
}
}
客户端代码:
public class TimeClient {
public static void main(String[] args) throws InterruptedException {
new TimeClient().connect("localhost", 8090);
}
private void connect(String localhost, int port) throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
try{
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.handler(new ChannelInitializer() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new TimeClientHandler());
}
});
ChannelFuture cf = b.connect(localhost, port).sync();
cf.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
private class TimeClientHandler extends ChannelInboundHandlerAdapter{
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for(int i=1; i<=100; i++){
byte[] bytes = ("deal tcp test 粘包/拆包现象...............test" +System.getProperty("line.separator")).getBytes();
ByteBuf buffer = Unpooled.buffer(bytes.length);
buffer.writeBytes(bytes);
ctx.writeAndFlush(buffer);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println(cause.getMessage());
ctx.close();
}
}
}
运行发现:服务端收到的消息分为两次,并发生了粘包现象。
2)Netty关于粘包/拆包的解决方法
一:LineBasedFrameDecoder
这个解码器是,通过判断是否有"\n" 或者 “\r\n”, 如果有,则以此位置为结束位置,如果超过定义的最大长度都没找到结束符,则会抛出异常
二:DelimiterBasedFrameDecoder 解码器
此解码器,是通过以自定义的特殊分隔符,进行分割,并定义数据的长度,超过长度未找到分隔符则抛出异常。
服务端改版:
客户端,在给服务端发送数据时,则以 服务端定义的分隔符为数据的结尾即可
客户端改版如下:
三:FixedLengthFrameDecoder固定长度解码器
此解码器作用是,按照指定的长度对消息进行自动解码,开发者无需考虑TCP的粘包/拆包问题,所以非常实用。
案例:
客户端给服务端发送的数据长度是:53,因此固定长度,
改造服务端为:
通过不同环境下,实用LineBasedFrameDecoder ,DelimiterBasedFrameDecoder,FixedLengthFrameDecoder已经可以解决我们日常开发所遇到的大部分问题,当然我们也可以自定义我们自己的解码器进行字节码处理,下一个主要讲解netty关于解码器的实现方面。
博客中案例代码:https://download.csdn.net/download/qq_22871607/11072379