前言

最近在调研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();
}
}
}

链接

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

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

  1. 架构师养成记--21.netty编码解码

    背景 作为网络传输框架,免不了哟啊传输对象,对象在传输之前就要序列化,这个序列化的过程就是编码过程.接收到编码后的数据就需要解码,还原传输的数据. 代码 工厂类 import io.netty.han ...

  2. 关于Integer的比较,今天又犯了一个低级错误,记录下

    今天查看以前所写的代码,看到有一部分被人改了,代码如下: if (orgId != organizationUpdateReq.getOrgId()) { //orgId的类型为Integer,org ...

  3. [bug] 验证selenium的显式和隐式等待而发现的一个低级错误

    隐式等待:如果在规定时间内网页加载完成,则执行下一步,否则一直等到时间截止,然后执行下一步.按照这说法举了个例子为啥不会按照预期执行了,难不成是这个定义有问题(~~~~~直接否定不是定义的问题,相信它 ...

  4. rtmp连接服务器失败(一个低级错误)

    由于rtmp底层使用的也是socket ,所以如果想正常使用RTMP_Connect(); 则需要在使用该连接之前先初始化套接字: WORD wVersionRequested; WSADATA ws ...

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

    对于初学的小菜鸡,经常会犯一些低级错误. 现在记录一下我在使用vue-resource发送post请求时的一个低级错误: window.BaseURL = '127.0.0.1:8888'; 8888 ...

  6. C语言错误之--初始值(低级错误)

    今天犯了一个低级错误,虽然低级,但是也不能忽视,一个低级错误以后可能小则浪费时间和精力,大则酿成整个app的项目bug.    

  7. git clone的低级错误

    犯了一个低级错误: server ip: 192.168.40.41 有一个git账户 所有的git仓库都在/home/git仓库下 比如/home/git/u-boot-2018.07-fmxx.g ...

  8. 引发了未经处理的异常:读取访问权限冲突。 _First 是 nullptr。

    1.问题:程序崩溃出现错误 引发了未经处理的异常:读取访问权限冲突. _First 是 nullptr. string strreponse=0: 定义这条语句,字符串初始化错误. 自己开发了一个股票 ...

  9. java中文乱码解决之道(六)-----javaWeb中的编码解码

    在上篇博客中LZ介绍了前面两种场景(IO.内存)中的java编码解码操作,其实在这两种场景中我们只需要在编码解码过程中设置正确的编码解码方式一般而言是不会出现乱码的.对于我们从事java开发的人而言, ...

随机推荐

  1. Codeforces 1204C

    题意略. 思路:我的想法是逐步地找出这个序列中的重要点,我要判断当前这个点能不能删去,就要看上一个重要点和当前这个点 i 在序列中的下一个点 i + 1之间的距离 是否是最短距离,如果是,那么我们就可 ...

  2. React生命周期函数理解

    一.组件挂载阶段 1. componentWillMount() 该方法在首次渲染之前调用,在一个组件挂载到卸载的过程中,仅仅执行这一次.该函数内可以state初始化的工作,与constructor的 ...

  3. 【凭据不工作】Win远程桌面提示您的凭据不工作

    1.浏览器直接进入云服务器 2.打开运行 --输入gpedit.msc--计算机配置--管理模板--windows组件--远程桌面服务--远程桌面会话主机--安全--远程(RDP)链接要求使用制定的安 ...

  4. ASP.NET Core on K8S深入学习(9)Secret & Configmap

    本篇已加入<.NET Core on K8S学习实践系列文章索引>,可以点击查看更多容器化技术相关系列文章. 一.Secret 1.1 关于Secret 在应用启动过程中需要一些敏感信息, ...

  5. POJ2084 Game of Connections 卡特兰数 关于卡特兰数经典的几个问题

    Game of Connections Time Limit: 1000MS   Memory Limit: 30000K Total Submissions: 9128   Accepted: 44 ...

  6. 通过对微信pc hook实现微信助手

    本软件主要通过对pc端微信hook来实现的,微信版本2.6.8.52. 软件下载地址: http://blog.yshizi.cn/104.html 软件实现功能: 支持爆粉 支持文本消息群发 支持自 ...

  7. Windows CVE-2019-0708 远程桌面代码执行漏洞复现

    Windows CVE-2019-0708 远程桌面代码执行漏洞复现 一.漏洞说明 2019年5月15日微软发布安全补丁修复了CVE编号为CVE-2019-0708的Windows远程桌面服务(RDP ...

  8. RodRego — a register machine

    RodRego - a register machine 寄存器机(register machine)是一种类似于图灵机一样的抽象机器,是计算机模型的一种,他和其他的抽象机一样都是图灵等价的. Rod ...

  9. XSS漏洞之加载远程js文件

    这次在对一个系统渗透测试过程中,发现一个XSS漏洞,可弹窗,并且没有httponly 但是在尝试加载远程js文件的时候发现,script标签被过滤掉了,准确的说应该是服务器后端在识别到输入内容有< ...

  10. 机器学习Label Encoder和One Hot Encoder

    标签编码(Label Encoder) 在本例中第一列是Country, 如果我们要运行任何模型, 数据中不能包含文本 所以要对文本进行处理 接下来,我们从sklearn库中导入LabelEncode ...