Netty的拆包粘包问题

文章介绍了Netty在使用TCP/IP协议时遇到的拆包粘包问题,以及Netty提供的三种解决方案:定长消息、特殊结束符和自定义协议,包括FixedLengthFrameDecoder、DelimiterBasedFrameDecoder和LineBasedFrameDecoder的使用。

Netty使用的是TCP/IP协议,必然会遇到拆包粘包的问题,Netty也给出了相关的解决方案,记录下Netty如何解决拆包粘包问题。

TCP/IP协议是"流"协议,就是类似水流一样的数据传输方式,当我们多次请求的时候,就会存在多发送和少发送的问题,也就是拆包粘包问题,简单来讲,粘包就是本来想独立发送M、N两个数据,但是最后把M、N一起发送了,把MN粘在一起了,就是粘包;发送M数据的时候,M被拆了几份,但是不是自己主动拆的,而是TCP自动拆的,就是拆包。

Netty的解决方案:

  • 1、消息定长
  • 2、特殊结束符
  • 3、协议

基础Netty的程序

服务端

import java.nio.charset.Charset;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;

public class Server {
    //监听线程组,监听客户端请求
    private EventLoopGroup acceptorGroup = null;
    //处理客户端相关操作线程组,负责处理与客户端的数据通讯
    private EventLoopGroup clientGroup = null;
    //服务端启动相关配置信息
    private ServerBootstrap bootstrap = null;

    public Server(){
        init();
    }

    private void init() {
        //初始化线程组
        acceptorGroup = new NioEventLoopGroup();
        clientGroup = new NioEventLoopGroup();
        //初始化服务端配置
        bootstrap = new ServerBootstrap();
        //绑定线程组
        bootstrap.group(acceptorGroup,clientGroup)
        //设定通讯模式为NIO,同步非阻塞
        .channel(NioServerSocketChannel.class)
        //设定缓冲区的大小 缓存区的单位是字节
        .option(ChannelOption.SO_BACKLOG, 1024)
        //SO_SNDBUF 发送缓冲区 SO_RCVBUF 接受缓冲区  SO_KEEPALIVE 开启心跳监测(保证连接有效)
        .option(ChannelOption.SO_SNDBUF, 16*1024)
                .option(ChannelOption.SO_RCVBUF, 16*1024)
                .option(ChannelOption.SO_KEEPALIVE, true);

    }

    /**
     * 监听处理逻辑
     * @param port 端口
     * @return
     * @throws InterruptedException
     */
    public ChannelFuture getChannelFuture(int port ) throws InterruptedException{
        bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {        
                //字符串解码器handler,会自动处理channelRead方法的msg参数,将ByteBuf类型的数据转换为字符串
                ch.pipeline().addLast(new StringDecoder(Charset.forName("UTF-8")));
                ch.pipeline().addLast(new ServerHandler());
            }
        });
        ChannelFuture future = bootstrap.bind(port).sync();
        return future;
    }

    public void release(){
        this.acceptorGroup.shutdownGracefully();
        this.clientGroup.shutdownGracefully();
    }

    public static void main(String[] args) {
        ChannelFuture future = null;
        Server server = null;

        try{
            server = new Server();
            future = server.getChannelFuture(9999);
            System.out.println("server started ... ");

            future.channel().closeFuture().sync();

        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            if(null != future){
                try {
                    future.channel().closeFuture().sync();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if(null != server){
                server.release();
            }
        }    
    }
}

服务端消息处理器

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;

public class ServerHandler extends ChannelHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        String message = msg.toString();

        System.out.println("from client : "+ message);

        String line = "ok";
        ctx.writeAndFlush(Unpooled.copiedBuffer(line.getBytes("UTF-8")));
    }

    /**
     * 异常处理逻辑,当客户端异常退出时,也会运行
     *     ChannelHandlerContext关闭,也代表当前客户端连接的资源关闭
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("server exceptionCaught method run...");
        ctx.close();
    }
}

客户端

import java.nio.charset.Charset;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.FixedLengthFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;

public class Client {

    //处理请求和处理服务端响应的线程组
    private EventLoopGroup group = null;

    //客户启动相关配置信息
    private Bootstrap bootstrap = null;

    public Client(){
        init();
    }

    private void init() {
        group = new NioEventLoopGroup();
        bootstrap= new Bootstrap();
        //绑定线程组
        bootstrap.group(group);
        //设定通讯模式为NIO,同步非阻塞
        bootstrap.channel(NioSocketChannel.class);
    }

    public ChannelFuture getChannelFuture(String host,int port) throws InterruptedException{
        this.bootstrap.handler(new ChannelInitializer<SocketChannel>() {
            protected void initChannel(SocketChannel ch) throws Exception {    

                //字符串解码器handler,会自动处理channelRead方法的msg参数,将ByteBuf类型的数据转换为字符串
                ch.pipeline().addLast(new StringDecoder(Charset.forName("UTF-8")));
                ch.pipeline().addLast(new ClientHandler());
            }
        });

        ChannelFuture future = this.bootstrap.connect(host,port).sync();
        return future;
    }

    public void release(){
        this.group.shutdownGracefully();
    }

    public static void main(String[] args) {
        Client client = null;
        ChannelFuture future = null;
        Scanner scanner =null;
        try {
            client = new Client();
            future = client.getChannelFuture("localhost", 9999);

            while(true){
                scanner = new Scanner(System.in);
                System.out.println("enter message send to server > ");
                String line = scanner.nextLine();
                future.channel().writeAndFlush(Unpooled.copiedBuffer(line.getBytes("UTF-8")));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            if(null != scanner){
                scanner.close();
            }
            if(null != future){
                try {
                    future.channel().closeFuture().sync();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if(null != client){
                client.release();
            }
        }
    }
}

客户端消息处理器

import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ReferenceCountUtil;

public class ClientHandler extends ChannelHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        try{

            String message = msg.toString();
            System.out.println("from server : "+message);
        }finally {
            //用于释放缓存。避免内存溢出
            ReferenceCountUtil.release(msg);
        }

    }

    /**
     * 异常处理逻辑,当客户端异常退出时,也会运行
     *     ChannelHandlerContext关闭,也代表当前客户端连接的资源关闭
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("client exceptionCaught method run...");
        ctx.close();
    }
}

Netty解决方案

消息定长

数据用一定的长度发送,如果数据长度不够使用空白字符代替。

在以下方法中添加定长消息的Handler

public ChannelFuture getChannelFuture(int port ) throws InterruptedException{
    bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {    
            //通过构造参数设置消息长度(单位是字节)
            ch.pipeline().addLast(new FixedLengthFrameDecoder(3));    
            //字符串解码器handler,会自动处理channelRead方法的msg参数,将ByteBuf类型的数据转换为字符串
            ch.pipeline().addLast(new StringDecoder(Charset.forName("UTF-8")));
            ch.pipeline().addLast(new ServerHandler());
        }
    });
    ChannelFuture future = bootstrap.bind(port).sync();
    return future;
}

上述是其中一个Handler,其中还有一个LengthFieldBasedFrameDecoder它是从头部字段确定为帧长,然后从数据流中提取指定的字节数。

特殊结束符

客户端和服务端协商特殊的分隔符,以此为结束符。

在以下方法中添加处理分隔符的Handler

public ChannelFuture getChannelFuture(int port ) throws InterruptedException{
    bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {    
            //数据分割符,定义的数据分隔符一定是一个ByteBuf类型的数据对象
            ByteBuf delimiter = Unpooled.copiedBuffer("$E$".getBytes());
            //使用特殊符号分割
            ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delimiter));    
            //字符串解码器handler,会自动处理channelRead方法的msg参数,将ByteBuf类型的数据转换为字符串
            ch.pipeline().addLast(new StringDecoder(Charset.forName("UTF-8")));
            ch.pipeline().addLast(new ServerHandler());
        }
    });
    ChannelFuture future = bootstrap.bind(port).sync();
    return future;
}

除了上述的一个Handler,还有一个LineBasedFrameDecoder它是提取由行尾符(\r或\r\n)分割的Handler。它比DelimiterBasedFrameDecoder效率高。

协议

服务端提供一个固定的协议标准。然后客户端和服务端以协议来传输数据,类似一个HTTPS协议。协议需要自己定义灵活度比较高,这里就不用代码测试了,当然协议这种方式也是一种相对比较好的方法。

Netty处理拆包问题的思路是客户端在发送数据之前先对数据按一定的规则进行编码,服务端在接收到数据后按照相同的规则进行解码。Netty已经提供了很多不同的解码器,可直接使用这些现成的解码器来解决问题。以下是Netty处理拆包的方法和解决方案: - **固定长度解码器(FixedLengthFrameDecoder)**:该解码器可将接收到的字节流按固定长度进行拆分。当发送方和接收方约定好每个消息的长度是固定的时候适用。示例代码如下: ```java public class FixedLengthFrameDecoder extends ByteToMessageDecoder { private final int length; public FixedLengthFrameDecoder(int length) { this.length = length; } @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { while (in.readableBytes() >= length) { out.add(in.readBytes(length)); } } } ``` 使用时,只需在ChannelPipeline中添加该解码器实例: ```java pipeline.addLast(new FixedLengthFrameDecoder(10)); // 假设每个消息长度固定为10字节 ``` - **行拆包器(LineBasedFrameDecoder)**:以换行符(`\n` 或 `\r\n`)作为消息的分隔符,将接收到的字节流按行进行拆分。适用于消息以换行符作为结束标志的场景。使用时,在ChannelPipeline中添加该解码器: ```java pipeline.addLast(new LineBasedFrameDecoder(1024)); // 最大长度为1024字节 ``` 此外,还有其他解码器可用于处理不同场景下的拆包问题Netty一共提供了5种解决方案,不过文档中未详细提及另外几种解码器的具体信息。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值