一个低级错误引发Netty编码解码中文异常

前言

最近在调研Netty的使用,在编写编码解码模块的时候遇到了一个中文字符串编码和解码异常的情况,后来发现是笔者犯了个低级错误。这里做一个小小的回顾。

错误重现

在设计Netty的自定义协议的时候,发现了字符串类型的属性,一旦出现中文就会出现解码异常的现象,这个异常并不一定出现了Exception,而是出现了解码之后字符截断出现了人类不可读的字符。编码和解码器的实现如下:

// 实体
@Data
public class ChineseMessage implements Serializable {

    private long id;
    private String message;
}

// 编码器  - <错误示范,不要拷贝>
public class ChineseMessageEncoder extends MessageToByteEncoder<ChineseMessage> {

    @Override
    protected void encode(ChannelHandlerContext ctx, ChineseMessage target, ByteBuf out) throws Exception {
        // 写入ID
        out.writeLong(target.getId());
        String message = target.getMessage();
        int length = message.length();
        // 写入Message长度
        out.writeInt(length);
        // 写入Message字符序列
        out.writeCharSequence(message, StandardCharsets.UTF_8);
    }
}

// 解码器
public class ChineseMessageDecoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        // 读取ID
        long id = in.readLong();
        // 读取Message长度
        int length = in.readInt();
        // 读取Message字符序列
        CharSequence charSequence = in.readCharSequence(length, StandardCharsets.UTF_8);
        ChineseMessage message = new ChineseMessage();
        message.setId(id);
        message.setMessage(charSequence.toString());
        out.add(message);
    }
}

简单地编写客户端和服务端代码,然后用客户端服务端发送一条带中文的消息:

// 服务端日志
接收到客户端的请求:ChineseMessage(id=1, message=张)
io.netty.handler.codec.DecoderException: java.lang.IndexOutOfBoundsException: readerIndex(15) + length(8) exceeds writerIndex(21) ......
// 客户端日志
接收到服务端的响应:ChineseMessage(id=2, message=张)
io.netty.handler.codec.DecoderException: java.lang.IndexOutOfBoundsException: readerIndex(15) + length(8) exceeds writerIndex(21) ......

其实,问题就隐藏在编码解码模块中。由于笔者前两个月一直996,在疯狂编写CRUD代码,业余在看Netty的时候,有一些基础知识一时短路没有回忆起来。笔者带着这个问题在各大搜索引擎中搜索,有可能是姿势不对或者关键字不准,没有得到答案,加之,很多博客文章都是照搬其他人的Demo,而这些Demo里面恰好都是用英文编写消息体例子,所以这个问题一时陷入了困局(2019年国庆假期之前卡住了大概几天,业务忙也没有花时间去想)。

灵光一现

2019年国庆假期前夕,由于团队一直在赶进度做一个前后端不分离的CRUD后台管理系统,当时有几个同事在做一个页面的时候讨论一个乱码的问题。在他们讨论的过程中,无意蹦出了两个让笔者突然清醒的词语:乱码UTF-8。笔者第一时间想到的是刚用Cnblogs的时候写过的一篇文章:《小伙子又乱码了吧-Java字符编码原理总结》(现在看起来标题起得挺二的)。当时有对字符编码的原理做过一些探究,想想有点惭愧,1年多前看过的东西差不多忘记得一干二净。

直接说原因:UTF-8编码的中文,大部分情况下一个中文字符长度占据3个字节(3 byte,也就是32 x 3或者32 x 4个位),而Java中字符串长度的获取方法String#length()是返回String实例中的Char数组的长度。但是我们多数情况下会使用Netty的字节缓冲区ByteBuf,而ByteBuf读取字符序列的方法需要预先指定读取的长度ByteBuf#readCharSequence(int length, Charset charset);,因此,在编码的时候需要预先写入字符串序列的长度。但是有一个隐藏的问题是:ByteBuf#readCharSequence(int length, Charset charset)方法底层会创建一个length长度的byte数组作为缓冲区读取数据,由于UTF-81 char = 3 or 4 byte,因此ChineseMessageEncoder在写入字符序列长度的时候虽然字符个数是对的,但是每个字符总是丢失2个或者4个byte的长度,而ChineseMessageDecoder在读取字符序列长度的时候总是读到一个比原来短的长度,也就是最终会拿到一个不完整或者错误的字符串序列。

解决方案

UTF-8编码的中文在大多数情况下占3个字节,在一些有生僻字的情况下可能占4个字节。可以暴力点直接让写入字节缓冲区的字符序列长度扩大三倍,只需修改编码器的代码:

public class ChineseMessageEncoder extends MessageToByteEncoder<ChineseMessage> {

    @Override
    protected void encode(ChannelHandlerContext ctx, ChineseMessage target, ByteBuf out) throws Exception {
        // 写入ID
        out.writeLong(target.getId());
        String message = target.getMessage();
        int length = message.length() * 3;      // <1> 直接扩大字节序列的预读长度
        // 写入Message长度
        out.writeInt(length);
        // 写入Message字符序列
        out.writeCharSequence(message, StandardCharsets.UTF_8);
    }
}

当然,这样做太暴力,硬编码的做法既不规范也不友好。其实Netty已经提供了内置的工具类io.netty.buffer.ByteBufUtil

// 获取UTF-8字符的最大字节序列长度
public static int utf8MaxBytes(CharSequence seq){}

// 写入UTF-8字符序列,返回写入的字节长度 - 建议使用此方法
public static int writeUtf8(ByteBuf buf, CharSequence seq){}

我们可以先记录一下writerIndex,先写一个假的值(例如0),再使用ByteBufUtil#writeUtf8()写字符序列,然后根据返回的写入的字节长度,通过writerIndex覆盖之前写入的假值:

public class ChineseMessageEncoder extends MessageToByteEncoder<ChineseMessage> {

    @Override
    protected void encode(ChannelHandlerContext ctx, ChineseMessage target, ByteBuf out) throws Exception {
        out.writeLong(target.getId());
        String message = target.getMessage();
        // 记录写入游标
        int writerIndex = out.writerIndex();
        // 预写入一个假的length
        out.writeInt(0);
        // 写入UTF-8字符序列
        int length = ByteBufUtil.writeUtf8(out, message);
        // 覆盖length
        out.setInt(writerIndex, length);
    }
}

至此,问题解决。如果遇到其他Netty编码解码问题,解决的思路是一致的。

小结

Netty学习过程中,编码解码占一半,网络协议知识和调优占另一半。

Netty的源码很优秀,很有美感,阅读起来很舒适。

Netty真好玩。

附录

引入依赖:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.41.Final</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.10</version>
    <scope>provided</scope>
</dependency>

代码:

// 实体
@Data
public class ChineseMessage implements Serializable {

    private long id;
    private String message;
}

// 编码器
public class ChineseMessageEncoder extends MessageToByteEncoder<ChineseMessage> {

    @Override
    protected void encode(ChannelHandlerContext ctx, ChineseMessage target, ByteBuf out) throws Exception {
        out.writeLong(target.getId());
        String message = target.getMessage();
        int writerIndex = out.writerIndex();
        out.writeInt(0);
        int length = ByteBufUtil.writeUtf8(out, message);
        out.setInt(writerIndex, length);
    }
}

// 解码器
public class ChineseMessageDecoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        long id = in.readLong();
        int length = in.readInt();
        CharSequence charSequence = in.readCharSequence(length, StandardCharsets.UTF_8);
        ChineseMessage message = new ChineseMessage();
        message.setId(id);
        message.setMessage(charSequence.toString());
        out.add(message);
    }
}

// 客户端
@Slf4j
public class ChineseNettyClient {

    public static void main(String[] args) throws Exception {
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        try {
            bootstrap.group(workerGroup);
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
            bootstrap.option(ChannelOption.TCP_NODELAY, Boolean.TRUE);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {

                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));
                    ch.pipeline().addLast(new LengthFieldPrepender(4));
                    ch.pipeline().addLast(new ChineseMessageEncoder());
                    ch.pipeline().addLast(new ChineseMessageDecoder());
                    ch.pipeline().addLast(new SimpleChannelInboundHandler<ChineseMessage>() {

                        @Override
                        protected void channelRead0(ChannelHandlerContext ctx, ChineseMessage message) throws Exception {
                            log.info("接收到服务端的响应:{}", message);
                        }
                    });
                }
            });
            ChannelFuture future = bootstrap.connect("localhost", 9092).sync();
            System.out.println("客户端启动成功...");
            Channel channel = future.channel();
            ChineseMessage message = new ChineseMessage();
            message.setId(1L);
            message.setMessage("张大狗");
            channel.writeAndFlush(message);
            future.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

// 服务端
@Slf4j
public class ChineseNettyServer {

    public static void main(String[] args) throws Exception {
        int port = 9092;
        ServerBootstrap bootstrap = new ServerBootstrap();
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 4, 0, 4));
                            ch.pipeline().addLast(new LengthFieldPrepender(4));
                            ch.pipeline().addLast(new ChineseMessageEncoder());
                            ch.pipeline().addLast(new ChineseMessageDecoder());
                            ch.pipeline().addLast(new SimpleChannelInboundHandler<ChineseMessage>() {

                                @Override
                                protected void channelRead0(ChannelHandlerContext ctx, ChineseMessage message) throws Exception {
                                    log.info("接收到客户端的请求:{}", message);
                                    ChineseMessage chineseMessage = new ChineseMessage();
                                    chineseMessage.setId(message.getId() + 1L);
                                    chineseMessage.setMessage("张小狗");
                                    ctx.writeAndFlush(chineseMessage);
                                }
                            });
                        }
                    });
            ChannelFuture future = bootstrap.bind(port).sync();
            log.info("启动Server成功...");
            future.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

链接

  • Github Page:http://www.throwable.club/2019/10/03/netty-codec-chinese-exception
  • Coding Page:http://throwable.coding.me/2019/10/03/netty-codec-chinese-exception

(本文完 c-2-d e-a-20191003 国庆快乐(*^▽^*)

原文地址:https://www.cnblogs.com/throwable/p/11619080.html

时间: 2024-11-05 01:06:25

一个低级错误引发Netty编码解码中文异常的相关文章

关于实现数据查询条件输入功能的一个低级错误

我们常常要实现检索数据的功能.复杂的查询条件输入,最好有辅助输入功能,能帮助使用者更轻松的完成查询条件输入.最近我们见到一个查询条件输入功能实现的时候犯的一个低级错误,觉得在新手中可能会典型,故拿出来说一下. 有个查询基站监控历史数据的功能,要查基站的历史数据,先得选择几个基站.第一个版本查询条件很简单,只需按站名或地区搜索基站,在结果集中选中一个或多个站,再输入其他查询条件.第二个版本,客户要求增加基站的基础信息作为查询条件,比如郊区还是市区,墙体材料等等,这些条件影响基站的冷却所需的能耗.这

[python]一个低级错误/xxx instance has no attribute &#39;xxx&#39;/&#39;module&#39; object is not callable

今天在写代码的时候出现了以下两个错误: TypeError: 'module' object is not callable AttributeError: excelChange instance has no attribute 'xlBook' 上网一查,发现第一个错误是由于python中有两种不同的引用方式 import xxx 和 from xxx import *,前者在代码中引用时需要加上模块名和具体的方法或属性,具体方法如下: import catchForm self.xls

一个小错误引发思考最终得出数组转字符串的新方法

今天在项目中犯了一个算得上是低级错误的错误吧 我在构造函数里声明了一个angle变量, this.angle; 然后在后面某函数中对此变量进行了+=操作, this.angle+=90 最后在一系列逻辑后我发现this.angle报错 NaN,马上定位到构造函数,就发现了原来该变量声明了但并未初始化赋值 求不笑,偶尔也是会傻乎乎犯些这种低级错误的,而这一次暴露出来完全是因为我在后面使用了+=操作,如果后面是单纯的 = 赋值操作,这样构造函数里是可以不初始化的 这时,我突然想试试如果+=后面不是数

今天犯了一个低级错误

一直信心满满,觉得对php中curl各种post模拟提交,远程获取等代码非常熟悉. 可今天利用原来自己封装的类,进行简单的模拟表单提交文件这个功能,就调了很久.还以为自己的类写错了,结果发现自己在文件接收端使用$_POST打印数组,所以怎么都获取不到提交的文件(大家都知道是用$_FILES). 这个错误很低级,但也证明自己已经很少写底层代码了.如果是工作几年的老人,这样当然是可以的,但是对于我来说,这样是不行的.还是要从底层开发做起呀.

7.30犯了一个低级错误,查不出来问题一定要做log

file_put_contents('./log.html', M()->getlastsql().'<br>',FILE_APPEND); file_put_contents('./log.html', '================<br>',FILE_APPEND); file_put_contents('./log.html', json_encode($str).'<br>',FILE_APPEND); 原文地址:https://www.cnblog

记一个vue-resource请求的低级错误

对于初学的小菜鸡,经常会犯一些低级错误. 现在记录一下我在使用vue-resource发送post请求时的一个低级错误: window.BaseURL = '127.0.0.1:8888'; 8888是访问在本机的后台程序的端口 请求代码如下, 1 this.$http.post(BaseURL+'/login', {telphone: this.phone,password: this.password}).then((response) => { 2 // success callback

Netty专题(四)-----Bootstrap、ByteBuf、编码解码

引导Bootstrap 我们把前面的用例称作引导一个服务器,后面的用例称作引导一个客户端.虽然这个术语简单方便,但是它略微掩盖了一个重要的事实,即“服务器”和“客户端”实际上表示了不同的网络行为:换句话说,是监听传入的连接还是建立到一个或者多个进程的连接.因此,有两种类型的引导:一种用于 客户端(简单地称为Bootstrap),而另一种(ServerBootstrap)用于服务器.无论你的应用程序使用哪种协议或者处理哪种类型的数据,唯一决定它使用哪种引导类的是它是作为一个客户端还是作为一个服务器

理解netty对protocol buffers的编码解码

一,netty+protocol buffers简要说明 Netty是业界最流行的NIO框架之一优点:1)API使用简单,开发门槛低:2)功能强大,预置了多种编解码功能,支持多种主流协议:3)定制能力强,可以通过ChannelHandler对通信框架进行灵活的扩展:4)性能高,通过与其它业界主流的NIO框架对比,Netty的综合性能最优:5)成熟.稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼:6)社区活跃,版本迭代周期短,发现的BUG可以

java中文乱码解决之道(六)—–javaWeb中的编码解码

在上篇博客中LZ介绍了前面两种场景(IO.内存)中的java编码解码操作,其实在这两种场景中我们只需要在编码解码过程中设置正确的编码解码方式一般而言是不会出现乱码的.对于我们从事java开发的人而言,其实最容易也是产生乱码最多的地方就是web部分.首先我们来看在javaWeb中有哪些地方存在编码转换操作. 编码&解码 通过下图我们可以了解在javaWeb中有哪些地方有转码: 用户想服务器发送一个HTTP请求,需要编码的地方有url.cookie.parameter,经过编码后服务器接受HTTP请