记一次解决netty半包问题的经历

最近学习了netty,想写一个简单的rpc,结果发现发送消息时遇到难题了,网上搜了一下,这种情况是半包问题和粘包问题,主要是出现在并发高一些的时候。

talk is cheap 

客户端编码:

    protected void encode(ChannelHandlerContext channelHandlerContext, Object o, ByteBuf byteBuf) throws Exception {
            encode0(channelHandlerContext,o,byteBuf);
    }
    private void encode0(ChannelHandlerContext channelHandlerContext, Object o, ByteBuf byteBuf) throws Exception {
        if(o instanceof UserInfo){
            byte[] data = Serializition.serialize((UserInfo) o,UserInfo.class);
            byteBuf.writeBytes(data);
        }
    }

服务端解码:

protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
myDecode(channelHandlerContext,byteBuf,list);
}
public void myDecode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list){
int len = byteBuf.readableBytes();
byte[] data = new byte[len];
byteBuf.readBytes(data);
UserInfo userInfo = Serializition.deSerialize(data,UserInfo.class);
list.add(userInfo);
}

这是最初版本的,一开始以为只要读出来反序列化成对象就ok了,进行了简单的测试发现没问题,但客户端发送频繁一些服务端就开始报错:

警告: An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.
io.netty.handler.codec.DecoderException: java.lang.RuntimeException: Reading from a byte array threw an IOException (should never happen).

分析一下发现对于来自同一个远程连接来说,服务端只会分配一个bytebuf来接收消息(这里使用的是UnpooledDirectByteBuf),这个bytebuf容量是动态扩增的,如果当前的长度不够用来存储新的消息就会自动扩展。当客户端发送不频繁时,服务端有足够的时间来做准备接收和处理消息,不会出现问题。但客户端频繁发送时就会出现问题了,如上,服务端的可读的字节超过了一个对象,读取后下一个对象反序列化就会出现问题。

解决思路:

  1.每次发送定长的消息,不够就补全,服务端设置对应的长度(但这样有问题:如果这样做客户端会发送很多无用信息,浪费性能,而且不知道设置多大的长度合适)

  2.使用netty自带的编码和解码器,如使用/r/n标志符解码,这就要继承MessageDecoder了,也就是字符解码,即先将消息在字节--字符串--对象将转换(有点浪费效率,而且万一内容中有对应的分隔符就会出问题)

  3.每次发送消息前先获取对象字节数组的长度(我最开始使用的方法,后来在网上也找到别人一样的思路)

  客户端:

    protected void encode(ChannelHandlerContext channelHandlerContext, Object o, ByteBuf byteBuf) throws Exception {
            encode1(channelHandlerContext,o,byteBuf);
    }
    private void encode1(ChannelHandlerContext channelHandlerContext, Object o, ByteBuf byteBuf) throws Exception {
        if(o instanceof UserInfo){
            byte[] data = Serializition.serialize((UserInfo) o,UserInfo.class);
            byteBuf.writeInt(data.length);
            byteBuf.writeBytes(data);
        }
    }

服务端:

    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        myDecode1(channelHandlerContext,byteBuf,list);
    }
    public void myDecode1(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list){
        if(byteBuf.readableBytes()>4){
            int len = byteBuf.readInt();
            byte[] data = new byte[len];
            byteBuf.readBytes(data);
            UserInfo userInfo = Serializition.deSerialize(data,UserInfo.class);
            list.add(userInfo);
        }
    }

这就看起来简单了  数据流是 |int|bytes|int|bytes,但实际情况还是发生了问题,还是出现了一样的问题。异常原因是服务端实例化数组长度后可读字节不够,原因是发送时客户端是分包发送的。

因此我在这个方法的基础上增加了一个条件:如果可读字节数不够就保存已创建好的字节数组,等下一次字节数够时使用

    private volatile int len=0;
    protected void decode5(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        int length =len>0?len:(byteBuf.readableBytes()>=4?byteBuf.readInt():0);
        if(byteBuf.readableBytes()>=length&&length>0) {
            byte[] data = new byte[length];
            byteBuf.readBytes(data);
            UserInfo userInfo = Serializition.deSerialize(data, UserInfo.class);
            list.add(userInfo);
            //bytes.put(length, data);
            len=0;
        }else {
            len = length;
        }
    }

经过测试,问题得到解决。

猜你喜欢

转载自www.cnblogs.com/zjyingchang/p/9970301.html