netty粘包,拆包问题及解决办法

 什么是粘包、拆包?
对于什么是粘包、拆包问题?  
客户端和服务器建立一个连接,客户端发送一条消息,客户端关闭与服务端的连接。  
客户端和服务器简历一个连接,客户端连续发送两条消息,客户端关闭与服务端的连接。  
对于第一种情况,服务端的处理流程可以是这样的:当客户端与服务端的连接建立成功之后,服务端不断读取客户端发送过来的数据,当客户端与服务端连接断开之后,服务端知道已经读完了一条消息,然后进行解码和后续处理...。  
对于第二种情况,如果按照上面相同的处理逻辑来处理,那就有问题了  
我们来看看第二种情况下客户端发送的两条消息递交到服务端有可能出现的情况:

第一种情况:  
服务端一共读到两个数据包,第一个包包含客户端发出的第一条消息的完整信息,第二个包包含客户端发出的第二条消息,那这种情况比较好处理,服务器只需要简单的从网络缓冲区去读就好了,第一次读到第一条消息的完整信息,消费完再从网络缓冲区将第二条完整消息读出来消费。


第二种情况:  
服务端一共就读到一个数据包,这个数据包包含客户端发出的两条消息的完整信息,这个时候基于之前逻辑实现的服务端就蒙了,因为服务端不知道第一条消息从哪儿结束和第二条消息从哪儿开始,这种情况其实是发生了TCP粘包。  


第三种情况:  
服务端一共收到了两个数据包,第一个数据包只包含了第一条消息的一部分,第一条消息的后半部分和第二条消息都在第二个数据包中,或者是第一个数据包包含了第一条消息的完整信息和第二条消息的一部分信息,第二个数据包包含了第二条消息的剩下部分,这种情况其实是发送了TCP拆包,因为发生了一条消息被拆分在两个包里面发送了,同样上面的服务器逻辑对于这种情况是不好处理的。  

解决tcp传输数据,粘包问题  
比较主流的解决方法由如下几种:  
1、消息定长,报文大小固定长度,例如每个报文的长度固定为200字节,如果不够空位补空格;  
2、包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分;  
3、将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段;  
4、自定义更复杂的应用层协议。  

Netty粘包和拆包解决方案
Netty提供了多个解码器,可以进行分包的操作,分别是:  
LineBasedFrameDecoder  
DelimiterBasedFrameDecoder(添加特殊分隔符报文来分包)  
FixedLengthFrameDecoder(使用定长的报文来分包)  
LengthFieldBasedFrameDecoder  

LengthFieldBasedFrameDecoder
本实例使用LengthFieldBasedFrameDecoder屏蔽TCP底层的拆包和粘包问题  
使用对象进行传输  
LengthFieldBasedFrameDecoder的构造函数:  

public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder {
    //...
  public LengthFieldBasedFrameDecoder(ByteOrder byteOrder, 
                                    int maxFrameLength, 
                                    int lengthFieldOffset, 
                                    int lengthFieldLength, 
                                    int lengthAdjustment, 
                                    int initialBytesToStrip, 
                                    boolean failFast) {
   }
   //...
}
```
byteOrder:表示字节流表示的数据是大端还是小端,因为Netty要读取Length字段的值,所以大端小端要设置好,默认Netty是大端序ByteOrder.BIG_ENDIAN。  
maxFrameLength:表示的是包的最大长度,超出包的最大长度netty将会报错;  
lengthFieldOffset:指的是长度域(Length)的偏移量,表示跳过指定长度个字节之后的才是长度域,也就是length前面的字节,也就是头部信息;  
lengthFieldLength:记录该帧数据长度的字段本身的长度;  
lengthAdjustment:该字段加长度字段等于数据帧的长度,包体长度调整的大小,长度域的数值表示的长度加上这个修正值表示的就是带header的包;  
initialBytesToStrip:从数据帧中跳过的字节数,表示获取完一个完整的数据包之后,忽略前面的指定的位数个字节,应用解码器拿到的就是不带长度域的数据包;  
failFast:如果为true,则表示读取到长度域,TA的值的超过maxFrameLength,就抛出一个 TooLongFrameException,而为false表示只有当真正读取完长度域的值表示的字节之后,才会抛出 TooLongFrameException,默认情况下设置为true,建议不要修改,否则可能会造成内存溢出。  

一,服务端代码

public class Server {

    private final int port;

    public Server(int port) {
        this.port = port;
    }

    public static void main(String[] args) throws Exception {
        int port = 8081;
        new Server(port).start();
    }

    public void start() throws Exception {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) {
                            ch.pipeline()
                                    .addLast(new LengthFieldBasedFrameDecoder(1024 * 1024, 0, 4, 0, 0))
                                    // 添加编解码. 发送自定义的类型, 而Handler的方法接收的msg参数的实际类型也是相应的自定义类了
                                    .addLast(new TinyDecoder(Request.class))
                                    .addLast(new TinyEncoder(Response.class))
                                    .addLast(new ServerHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);
            ChannelFuture f = b.bind(port).sync();
            System.out.println(Server.class.getName() + " 服务器端启动并监听端口号: " + f.channel().localAddress());
            f.channel().closeFuture().sync();
        } finally {
            //释放 channel 和 块,直到它被关闭
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

}

public class ServerHandler extends SimpleChannelInboundHandler<Request> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Request request) throws Exception {
        System.out.println("服务端接收到的消息 : " + request);
        Response response = new Response();
        response.setRequestId(2L);
        User user = new User();
        user.setUsername("测试");
        user.setPassword("1234");
        user.setAge(21);
        response.setResult(user);
        //addListener是非阻塞的,异步执行。它会把特定的ChannelFutureListener添加到ChannelFuture中,然后I/O线程会在I/O操作相关的future完成的时候通知监听器。
        ctx.writeAndFlush(response).addListener((ChannelFutureListener) channelFuture ->
                System.out.println("接口响应:" + request.getRequestId())
        );
    }
}

三,客户端代码

public class Client {
    private final String host;
    private final int port;

    public Client(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public static void main(String[] args) throws Exception {
        final String host = "127.0.0.1";
        final int port = 8081;

        new Client(host, port).start();
    }

    public void start() throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .remoteAddress(new InetSocketAddress(host, port))
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch)
                                throws Exception {
                            ch.pipeline()
                                    .addLast(new TinyEncoder(Request.class))
                                    .addLast(new LengthFieldBasedFrameDecoder(1024 * 1024, 0, 4, 0, 0))
                                    // 添加编解码. 发送自定义的类型, 而Handler的方法接收的msg参数的实际类型也是相应的自定义类了
                                    .addLast(new TinyDecoder(Response.class))
                                    .addLast(new ClientHandler());
                        }
                    });

            ChannelFuture f = b.connect().sync();
            f.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully().sync();
        }
    }
}

public class ClientHandler extends SimpleChannelInboundHandler<Response> {

    /**
     * 通道注册
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        super.channelRegistered(ctx);
    }

    /**
     * 服务器的连接被建立后调用
     * 建立连接后该 channelActive() 方法被调用一次
     *
     * @param ctx
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        Request request = new Request();
        request.setRequestId(3L);
        User user = new User();
        user.setUsername("测试客户端");
        user.setPassword("4567");
        user.setAge(21);
        request.setParameter(user);
        //当被通知该 channel 是活动的时候就发送信息
        ctx.writeAndFlush(request);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Response response) throws Exception {
        System.out.println("服务器发来消息 : " + response);
    }

    /**
     * 捕获异常时调用
     *
     * @param ctx
     * @param cause
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,
                                Throwable cause) {
        //记录错误日志并关闭 channel
        cause.printStackTrace();
        ctx.close();
    }

}

四,工具类代码

public class FstSerializer {
    private static FSTConfiguration conf = FSTConfiguration.createDefaultConfiguration();

    /**
     * 反序列化
     *
     * @param data
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T deserialize(byte[] data, Class<T> clazz) {
        return (T) conf.asObject(data);
    }

    /**
     * 序列化
     *
     * @param obj
     * @param <T>
     * @return
     */
    public static <T> byte[] serialize(T obj) {
        return conf.asByteArray(obj);
    }
}

public class TinyDecoder extends ByteToMessageDecoder {
    /**
     * 头部长度字节数
     * 由于在TinyEncoder的encode方法中使用的是writeInt,int为4个字节
     */
    private Integer HEAD_LENGTH = 4;
    private Class<?> genericClass;

    public TinyDecoder(Class<?> genericClass) {
        this.genericClass = genericClass;
    }

    /**
     * 解码
     *
     * @param ctx
     * @param in
     * @param out
     */
    @Override
    public final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        //头部信息是int类型,长度是4,所以信息长度不可能小于4的
        if (in.readableBytes() < HEAD_LENGTH) {
            return;
        }
        //标记当前的readIndex的位置
        in.markReaderIndex();
        //读取传送过来的消息的长度。ByteBuf 的readInt()方法会让他的readIndex增加4。指针会向前移动4
        int dataLength = in.readInt();
        if (in.readableBytes() < dataLength) {
            //到的消息体长度如果小于我们传送过来的消息长度,则resetReaderIndex.
            // 这个配合markReaderIndex使用的。把readIndex重置到mark的地方
            in.resetReaderIndex();
            return;
        }
        byte[] data = new byte[dataLength];
        in.readBytes(data);
        //反序列化
        Object obj = FstSerializer.deserialize(data, genericClass);
        out.add(obj);
    }

}

public class User implements Serializable {
    private static final long serialVersionUID = -5135011481747489263L;
    private String username;
    private String password;
    private Integer age;
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    public static long getSerialversionuid() {
        return serialVersionUID;
    }
    @Override
    public String toString() {
        return "User [username=" + username + ", password=" + password
                + ", age=" + age + "]";
    }
    
    
    
}

public class Request implements Serializable {
    private static final long serialVersionUID = -2747321595912488569L;
    private Long requestId;
    private String className;
    private String methodName;
    private Class<?> parameterType;
    private Object parameter;
    public Long getRequestId() {
        return requestId;
    }
    public void setRequestId(Long requestId) {
        this.requestId = requestId;
    }
    public String getClassName() {
        return className;
    }
    public void setClassName(String className) {
        this.className = className;
    }
    public String getMethodName() {
        return methodName;
    }
    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }
    public Class<?> getParameterType() {
        return parameterType;
    }
    public void setParameterType(Class<?> parameterType) {
        this.parameterType = parameterType;
    }
    public Object getParameter() {
        return parameter;
    }
    public void setParameter(Object parameter) {
        this.parameter = parameter;
    }
    public static long getSerialversionuid() {
        return serialVersionUID;
    }
    
    
}

public class Response implements Serializable {
    private static final long serialVersionUID = -3136380221020337915L;
    private Long requestId;
    private String error;
    private Object result;
    public Long getRequestId() {
        return requestId;
    }
    public void setRequestId(Long requestId) {
        this.requestId = requestId;
    }
    public String getError() {
        return error;
    }
    public void setError(String error) {
        this.error = error;
    }
    public Object getResult() {
        return result;
    }
    public void setResult(Object result) {
        this.result = result;
    }
    public static long getSerialversionuid() {
        return serialVersionUID;
    }
    
    
}
 

发布了23 篇原创文章 · 获赞 0 · 访问量 193

猜你喜欢

转载自blog.csdn.net/liuerchong/article/details/105184717