最近在调研Netty
的使用,在编写编码解码模块的时候遇到了一个中文字符串编码和解码异常的状况,后来发现是笔者犯了个低级错误。这里作一个小小的回顾。html
在设计Netty
的自定义协议的时候,发现了字符串类型的属性,一旦出现中文就会出现解码异常的现象,这个异常并不必定出现了Exception
,而是出现了解码以后字符截断出现了人类不可读的字符。编码和解码器的实现以下:java
// 实体
@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);
}
}
复制代码
简单地编写客户端和服务端代码,而后用客户端服务端发送一条带中文的消息:bootstrap
// 服务端日志
接收到客户端的请求: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-8
中1 char = 3 or 4 byte
,所以ChineseMessageEncoder
在写入字符序列长度的时候虽然字符个数是对的,可是每一个字符老是丢失2个-3个byte
的长度,而ChineseMessageDecoder
在读取字符序列长度的时候老是读到一个比原来短的长度,也就是最终会拿到一个不完整或者错误的字符串序列。网络
UTF-8
编码的中文在大多数状况下占3个字节,在一些有生僻字的状况下可能占4个字节。能够暴力点直接让写入字节缓冲区的字符序列长度扩大三倍,只需修改编码器的代码:ide
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
覆盖以前写入的假值:oop
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 国庆快乐(*^▽^*)
)