Netty 【2】 TCP粘包拆包

                                   Netty TCP粘包拆包

 

  • 熟悉tcp编程的可能都知道,无论是服务器端还是客户端,当我们读取或者发送数据的时候,都需要考虑TCP底层的粘包/拆包机制。
  • TCP是一个“流”协议,所谓流就是没有界限的遗传数据。大家可以想象下如果河里的水就好比数据,他们是连成一片的,没有分界线,TCP底层并不了解上层的业务数据具体的含义,它会根据TCP缓冲区的实际情况进行包的划分,也就是说,在业务上,我们一个完整的包可能会被TCP分成多个包进行发送,也可能把多个小包封装成一个大的数据包发送出去,这就是所谓的TCP粘包、拆包问题。
  • 分析TCP粘包、拆包问题的产生原因:
    • 1 应用程序write写入的字节大小大于套接口发送缓冲区的大小
    • 2 进行MSS大小的TCP分段
    • 3 以太网帧的payload大于MTU进行IP分片

 

TCP拆包、粘包解决方案

根据业界主流协议,有三种方案

  1. 消息定长,例如每个报文的大小固定为200个字节,如果不够,空位补空格;如果大于这个长度,就发多个。
  2. 在包尾部增加特殊字符进行分割,例如加回车等
  3. 消息分为消息头和消息体,在消息头中包含表示消息总长度的字段,然后进行业务处理

一般采用第三种消息头和消息体模式。  netty 为我们提供了前两种

 

NETTY解决TCP拆包粘包问题

netty 为我们引入两个类,两种方法解决TCP拆包粘包问题

  1. 分隔符类 DelimiterBasedFrameDecoder(自定义分隔符)
  2. FixedLengthFrameDecoder(定长)

 

一 . 自定义分隔符

目录结构:Server 使用 ServerHandler , Client 使用 ClientHandler

在 initChannel 中 添加分隔符定义,  这里自定义分隔符 $_

    .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        // 设置自定义分隔符。特殊字符应该用 ByteBuf 转换
                        ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
                        //设置分隔符,发送的最大长度是32*1024 这个应该和SO_SNDBUF设置的一样
                        ch.pipeline().addLast(new DelimiterBasedFrameDecoder(32*1024,buf));
                        //设置字符串形式的解码。<传过来的数据直接转码成字符串>
                        ch.pipeline().addLast(new StringDecoder());
                        ch.pipeline().addLast(new ServerHandler());	//1
                    }
                });

完整代码:

Server :

 public static void main(String[] args) throws InterruptedException {


        //1. 创建两个线程组: 一个用于进行网络连接接受的 另一个用于我们的实际处理(网络通信的读写)
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();

        //2 通过辅助类去构造server/client
        ServerBootstrap bootstrap = new ServerBootstrap();

        bootstrap.group(bossGroup, workGroup)
                .channel(NioServerSocketChannel.class)    // NIO 通道
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)  // 连接超时时间
                .option(ChannelOption.SO_BACKLOG, 1024)  // sync 和 accept队列加起来=backlog 即1024
                .option(ChannelOption.SO_KEEPALIVE,false)   // 是否是长连接
                .childOption(ChannelOption.TCP_NODELAY, true)  //禁用Nagel算法,禁止小的请求包的合并
                .childOption(ChannelOption.SO_RCVBUF, 1024 * 32)   //接受区的缓存大小  1024byte * 32
                .childOption(ChannelOption.SO_SNDBUF, 1024 * 32)   //发送区的缓存大小  1024byte * 32
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        /**
                         * 底层其实是一个责任链模式
                         */

                        // 设置特殊分隔符 ,特殊字符应该用 ByteBuf 转换
                        ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
                        //设置分隔符,发送的最大长度是32*1024 这个应该和SO_SNDBUF设置的一样
                        ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024*32,buf));
                        //设置字符串形式的解码。<传过来的数据直接转码成字符串>
                        ch.pipeline().addLast(new StringDecoder());
                        ch.pipeline().addLast(new ServerHandler());	//1
                        /**
                         *
                         可以加好多个,一个一个执行下去。对数据一层一层的解析过滤
                        ch.pipeline().addLast(new ServerHandler());	//2
                        ch.pipeline().addLast(new ServerHandler());	//3
                        ch.pipeline().addLast(new ServerHandler());	//4
                         */
                    }
                });
        //服务器端绑定端口并启动服务
        ChannelFuture cf = bootstrap.bind(8765).sync();
        //使用channel级别的监听close端口 阻塞的方式
        cf.channel().closeFuture().sync();

        bossGroup.shutdownGracefully();
        workGroup.shutdownGracefully();
    }

ServerHandler :

注意 : server 返回消息给 客户端时也要加上特殊分隔符 &_

public class ServerHandler extends ChannelInboundHandlerAdapter {
    //ServerHandler implements ChannelHandler  一般不实现  ChannelHandler 这个太顶层了



    // 通道激活
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.err.println("server channel active..");
    }

    /**
     * 真正的数据最终会走到这个方法进行处理
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        // 读取客户端的数据
        // 因为服务端设置了字符串解码: ch.pipeline().addLast(new StringDecoder());。<传过来的数据直接转码成字符串>
        // 所以可以直接把数据强转成String类型
        String request =  (String) msg;

        System.err.println("Server收到消息: " + request);

        //返回响应数据,   【记得加上 $_ 分隔符】
        String responseBody = "Server返回响应数据: " + request + "$_";

        ctx.writeAndFlush(Unpooled.copiedBuffer(responseBody.getBytes()));

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.err.println("  exceptionCaught ");
        ctx.fireExceptionCaught(cause);
    }

    //数据读取完毕
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        System.err.println("  channelReadComplete :  数据读取完毕");
        ctx.fireChannelReadComplete();
    }

 

client :

如果这个  32*1024 设置的过小,就会接收不到那一条数据。  假设传过来的数据长度是10字节, 这是设置了8,那么这条数据就接受不到。
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(32*1024,buf));
 public static void main(String[] args) throws InterruptedException {

        //1. 创建两个线程组: 只需要一个线程组用于我们的实际处理(网络通信的读写)
        EventLoopGroup workGroup = new NioEventLoopGroup();

        //2 通过辅助类去构造server/client
        Bootstrap b = new Bootstrap();
        b.group(workGroup)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
                .option(ChannelOption.SO_RCVBUF, 1024 * 32)
                .option(ChannelOption.SO_SNDBUF, 1024 * 32)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
                        //设置分隔符,一次性接受的最大长度是32*1024.这里是Client, 这个应该和SO_RCVBUF设置的一样。
                        ch.pipeline().addLast(new DelimiterBasedFrameDecoder(32*1024,buf));
                        //设置字符串形式的解码。<传过来的数据直接转码成字符串>
                        ch.pipeline().addLast(new StringDecoder());
                        ch.pipeline().addLast(new ClientHandler());	//1
                    }
                });
        //服务器端绑定端口并启动服务
        //使用channel级别的监听close端口 阻塞的方式
        ChannelFuture cf = b.connect("127.0.0.1", 8765).syncUninterruptibly();


        // 开始写数据
        // writeAndFlush  写入数据到缓冲并冲刷到channel
        //  Unpooled.copiedBuffer 把字节数组变成buff对象

        for (int i = 1; i <= 10; i++) { 
             /**
             * "hello" 占5个字节;“i” 0~9 占1个字节,10就占两个字节;"$_" 占两个字节; 总计8个字节~9个字节
             * 所以,如果设置最长为8,那么最后一条9个字节长度的数据就接受不到
             * ch.pipeline().addLast(new DelimiterBasedFrameDecoder(8,buf)); 
                中文占两个字节
             */  
            //cf.channel().writeAndFlush(Unpooled.copiedBuffer(("hello netty 呀 ! "+i+" # $_ ").getBytes()));
            cf.channel().writeAndFlush(Unpooled.copiedBuffer(("hello "+i+"$_ ").getBytes()));
        }


        cf.channel().closeFuture().sync();
        workGroup.shutdownGracefully();

    }

 

clientHandler :

public class ClientHandler extends ChannelInboundHandlerAdapter {


    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
    	System.err.println("client channel active..");
    }
    
    /**
     * 真正的数据最终会走到这个方法进行处理
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    	try {
            String request  = (String)msg;

            System.out.println(" Client 接受到消息 : "+request );
//			Response resp = (Response)msg;
//			System.err.println("Client: " + resp.getId() + "," + resp.getName() + "," + resp.getResponseMessage());
		} finally {
			ReferenceCountUtil.release(msg);
			//为什么服务端不需要做这一步释放的动作,因为服务端 writeAndFlush 的时候就释放了
		}
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.fireExceptionCaught(cause);
    }
}

 

【注意】

 //设置分隔符,发送的最大长度是32*1024 这个应该和SO_SNDBUF设置的一样
 ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024*32,buf));

这个长度我们要预估下,大概有多长。

server :

假设我们这边1000条数据,990条数据量都是在 32 * 1024 左右大小。 少数极个别的 数据量特别大,可能有 34*1024 。那么这个值 就设置 32 * 1024 就可以了。那几条大数据,可以让客户端那边做好 两次 flush 处理 ,把数据压过来。

 

 

 二 . 定长

//  设置定长字符串接收  5个字节长度
sc.pipeline().addLast(new FixedLengthFrameDecoder(5));

 

Client

.childHandler(new ChannelInitializer<SocketChannel>() {
			@Override
			protected void initChannel(SocketChannel sc) throws Exception {
				//	设置定长字符串接收
				sc.pipeline().addLast(new FixedLengthFrameDecoder(5));
				//	设置字符串形式的解码
				sc.pipeline().addLast(new StringDecoder());
				sc.pipeline().addLast(new ServerHandler());
			}
		});

ClientHandler

@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		String response = (String)msg;
		System.out.println("Client: " + response);
	}

Server :

 .childHandler(new ChannelInitializer<SocketChannel>() {
			@Override
			protected void initChannel(SocketChannel sc) throws Exception {
				//	设置定长字符串接收
				sc.pipeline().addLast(new FixedLengthFrameDecoder(5));
				//	设置字符串形式的解码
				sc.pipeline().addLast(new StringDecoder());
				sc.pipeline().addLast(new ServerHandler());
			}
		});

ServerHandler  :


	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		String request = (String)msg;
		System.out.println("Server :" + msg);
		String response =  request ;
		ctx.writeAndFlush(Unpooled.copiedBuffer(response.getBytes()));
	}

测试下:

首先看下 Client 的发送数据的代码

1 . 设置的定长是 5个字节(服务端设置的接受数据定长也是5个字节)

2. 发送的数据如下 aaaaabbbbb    ccccccc      c    cc

 .handler(new ChannelInitializer<SocketChannel>() {
			@Override
			protected void initChannel(SocketChannel sc) throws Exception {
				sc.pipeline().addLast(new FixedLengthFrameDecoder(5));
				sc.pipeline().addLast(new StringDecoder());
				sc.pipeline().addLast(new ClientHandler());
			}
		});
		
		ChannelFuture cf = b.connect("127.0.0.1", 8765).sync();
		
		cf.channel().writeAndFlush(Unpooled.wrappedBuffer("aaaaabbbbb".getBytes()));
		cf.channel().writeAndFlush(Unpooled.copiedBuffer("ccccccc12".getBytes()));
		cf.channel().writeAndFlush(Unpooled.copiedBuffer("c4 5cc".getBytes()));
		//	等待客户端端口关闭
		cf.channel().closeFuture().sync();
		group.shutdownGracefully();
		

最终服务端收到数据如下:  会自动把数据按照5个字节长度 分隔

Server :aaaaa
Server :bbbbb
Server :ccccc
Server :cc12c
Server :4 5cc

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值