Netty中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如何解决粘包拆包的问题

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

Client.java

public class Client {

    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup workgroup = new NioEventLoopGroup();
        Bootstrap b = new Bootstrap();
        b.group(workgroup)
        .channel(NioSocketChannel.class)
        .handler(new ChannelInitializer<SocketChannel>() {

            @Override
            protected void initChannel(SocketChannel sc) throws Exception {
                //
                ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
                sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,buf));
                sc.pipeline().addLast(new StringDecoder());
                sc.pipeline().addLast(new ClientHandler());
            }
        });

        ChannelFuture cf1 = b.connect("127.0.0.1",8765).sync();
//      ChannelFuture cf2 = b.connect("127.0.0.1",8764).sync();
        //buf
        cf1.channel().writeAndFlush(Unpooled.copiedBuffer("bbbb$_".getBytes()));
        cf1.channel().writeAndFlush(Unpooled.copiedBuffer("cccccccc&_".getBytes()));
        cf1.channel().writeAndFlush(Unpooled.copiedBuffer("aaaaaaaaaaaaa&_".getBytes()));
//      cf1.channel().flush();

        cf1.channel().closeFuture().sync();
        workgroup.shutdownGracefully();
    }
}

ClientHandler.java

public class ClientHandler extends ChannelHandlerAdapter{

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("client channel active...");
    }

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

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

Server.java

public class Server {

    public static void main(String[] args) throws InterruptedException {
        //1 第一个线程组用于接受client端连接的
        NioEventLoopGroup pGroup = new NioEventLoopGroup();
        //2 第二个线程组用于实际的业务处理操作的
        NioEventLoopGroup cGroup = new NioEventLoopGroup();
        //3 创建一个辅助类Bootstrap,就是对我们的Server进行一系列的配置
        ServerBootstrap b = new ServerBootstrap();
        b.group(pGroup,cGroup)//把两个工作线程组加入进来
        .channel(NioServerSocketChannel.class)//使用NIOServerSocketChannel这种类型的通道
        .childHandler(new ChannelInitializer<SocketChannel>() {//使用childHandler绑定具体的事件处理器
            @Override
            protected void initChannel(SocketChannel sc) throws Exception {
                //设置特殊分隔符,当出现下滑线
                ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
                sc.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
                //设置字符串形式的解码
                sc.pipeline().addLast(new StringDecoder());
                sc.pipeline().addLast(new ServerHandler());
            }
        })
        /**
         * 服务器端TCP内核模块维护两个队列,我们称之为A和B
         * 客户端向服务器端connect的时候 会发送带有SYN标志的包(第一次握手)
         * 服务器收到客户端发来的SYN时,向客户端发送SYN ACK确认(第二次握手)
         * 此时TCP内核模块将客户端连接加入A队列中,然后服务器收到客户端发来的ACK时(第三次握手)
         * TCP内核模块将客户端从A队列移动到B队列,连接完成,应用程序的accept会返回。
         * 也就是说accept从B队列中取出完成三次握手的连接
         * A队列和B队列的长度之和是backlog。当A、B队列的长度之和大于backlog时,新连接将会被TCP内核拒绝
         * 所以,如果backlog过小,可能会出现accept速度跟不上,A、B队列满了,导致新的客户无法连接。
         * 要注意的是:backlog对程序的连接数并无影响,backlog影响的知识还没有被accept取出的连接。
         */
        //设置TCP缓冲区
//      .option(ChannelOption.SO_BACKLOG, 128)
        //保持连接
//      .childOption(ChannelOption.SO_KEEPALIVE, true)
        ;

        //绑定指定的端口进行监听
        ChannelFuture f = b.bind(8765).sync();
//      ChannelFuture f2 = b.bind(8764).sync();
        //将服务器阻塞
        f.channel().closeFuture().sync();
//      f2.channel().closeFuture().sync();

        pGroup.shutdownGracefully();
        cGroup.shutdownGracefully();

    }

}

ServerHandler.java

public class ServerHandler extends ChannelHandlerAdapter{

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("server channel active...");
    }

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

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        ctx.close();
    }
}
  1. FixedLengthFrameDecoder(定长)

Client.java

public class Client {

    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup workgroup = new NioEventLoopGroup();
        Bootstrap b = new Bootstrap();
        b.group(workgroup)
        .channel(NioSocketChannel.class)
        .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 cf1 = b.connect("127.0.0.1",8765).sync();
//      ChannelFuture cf2 = b.connect("127.0.0.1",8764).sync();
        //buf
        cf1.channel().writeAndFlush(Unpooled.copiedBuffer("bbbbbaaaaa".getBytes()));
        cf1.channel().writeAndFlush(Unpooled.copiedBuffer("cccccdd   ".getBytes()));
//      cf1.channel().flush();

        cf1.channel().closeFuture().sync();
        workgroup.shutdownGracefully();
    }
}

ClientHandler.java

public class ClientHandler extends ChannelHandlerAdapter{

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("client channel active...");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        try{
            Response resp = (Response)msg;
            System.out.println("Client : " + resp.getId() + ", " + resp.getName() + ", " + resp.getResponseMessage());      
        }finally{
            ReferenceCountUtil.release(msg);
        }
    }

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

Server.java

public class Server {

    public static void main(String[] args) throws InterruptedException {
        //1 第一个线程组用于接受client端连接的
        NioEventLoopGroup pGroup = new NioEventLoopGroup();
        //2 第二个线程组用于实际的业务处理操作的
        NioEventLoopGroup cGroup = new NioEventLoopGroup();
        //3 创建一个辅助类Bootstrap,就是对我们的Server进行一系列的配置
        ServerBootstrap b = new ServerBootstrap();
        b.group(pGroup,cGroup)//把两个工作线程组加入进来
        .channel(NioServerSocketChannel.class)//使用NIOServerSocketChannel这种类型的通道
        .childHandler(new ChannelInitializer<SocketChannel>() {//使用childHandler绑定具体的事件处理器
            @Override
            protected void initChannel(SocketChannel sc) throws Exception {
                //设置字长字符串接收
                sc.pipeline().addLast(new FixedLengthFrameDecoder(5));
                //设置字符串形式的解码
                sc.pipeline().addLast(new StringDecoder());
                sc.pipeline().addLast(new ServerHandler());
            }
        })
        /**
         * 服务器端TCP内核模块维护两个队列,我们称之为A和B
         * 客户端向服务器端connect的时候 会发送带有SYN标志的包(第一次握手)
         * 服务器收到客户端发来的SYN时,向客户端发送SYN ACK确认(第二次握手)
         * 此时TCP内核模块将客户端连接加入A队列中,然后服务器收到客户端发来的ACK时(第三次握手)
         * TCP内核模块将客户端从A队列移动到B队列,连接完成,应用程序的accept会返回。
         * 也就是说accept从B队列中取出完成三次握手的连接
         * A队列和B队列的长度之和是backlog。当A、B队列的长度之和大于backlog时,新连接将会被TCP内核拒绝
         * 所以,如果backlog过小,可能会出现accept速度跟不上,A、B队列满了,导致新的客户无法连接。
         * 要注意的是:backlog对程序的连接数并无影响,backlog影响的知识还没有被accept取出的连接。
         */
        //设置TCP缓冲区
//      .option(ChannelOption.SO_BACKLOG, 128)
        //保持连接
//      .childOption(ChannelOption.SO_KEEPALIVE, true)
        ;

        //绑定指定的端口进行监听
        ChannelFuture f = b.bind(8765).sync();
//      ChannelFuture f2 = b.bind(8764).sync();
        //将服务器阻塞
        f.channel().closeFuture().sync();
//      f2.channel().closeFuture().sync();

        pGroup.shutdownGracefully();
        cGroup.shutdownGracefully();

    }

}

ServerHandler.java

public class ServerHandler extends ChannelHandlerAdapter{

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("server channel active...");
    }

    @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()));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
        ctx.close();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值