一个低级错误引发Netty编码解码中文异常
前言
最近在調(diào)研Netty的使用,在編寫編碼解碼模塊的時(shí)候遇到了一個(gè)中文字符串編碼和解碼異常的情況,后來發(fā)現(xiàn)是筆者犯了個(gè)低級錯(cuò)誤。這里做一個(gè)小小的回顧。
錯(cuò)誤重現(xiàn)
在設(shè)計(jì)Netty的自定義協(xié)議的時(shí)候,發(fā)現(xiàn)了字符串類型的屬性,一旦出現(xiàn)中文就會(huì)出現(xiàn)解碼異常的現(xiàn)象,這個(gè)異常并不一定出現(xiàn)了Exception,而是出現(xiàn)了解碼之后字符截?cái)喑霈F(xiàn)了人類不可讀的字符。編碼和解碼器的實(shí)現(xiàn)如下:
// 實(shí)體 @Data public class ChineseMessage implements Serializable {private long id;private String message; }// 編碼器 - <錯(cuò)誤示范,不要拷貝> public class ChineseMessageEncoder extends MessageToByteEncoder<ChineseMessage> {@Overrideprotected void encode(ChannelHandlerContext ctx, ChineseMessage target, ByteBuf out) throws Exception {// 寫入IDout.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 {@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {// 讀取IDlong 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);} }簡單地編寫客戶端和服務(wù)端代碼,然后用客戶端服務(wù)端發(fā)送一條帶中文的消息:
// 服務(wù)端日志 接收到客戶端的請求:ChineseMessage(id=1, message=張) io.netty.handler.codec.DecoderException: java.lang.IndexOutOfBoundsException: readerIndex(15) + length(8) exceeds writerIndex(21) ...... // 客戶端日志 接收到服務(wù)端的響應(yīng):ChineseMessage(id=2, message=張) io.netty.handler.codec.DecoderException: java.lang.IndexOutOfBoundsException: readerIndex(15) + length(8) exceeds writerIndex(21) ......其實(shí),問題就隱藏在編碼解碼模塊中。由于筆者前兩個(gè)月一直996,在瘋狂編寫CRUD代碼,業(yè)余在看Netty的時(shí)候,有一些基礎(chǔ)知識一時(shí)短路沒有回憶起來。筆者帶著這個(gè)問題在各大搜索引擎中搜索,有可能是姿勢不對或者關(guān)鍵字不準(zhǔn),沒有得到答案,加之,很多博客文章都是照搬其他人的Demo,而這些Demo里面恰好都是用英文編寫消息體例子,所以這個(gè)問題一時(shí)陷入了困局(2019年國慶假期之前卡住了大概幾天,業(yè)務(wù)忙也沒有花時(shí)間去想)。
靈光一現(xiàn)
2019年國慶假期前夕,由于團(tuán)隊(duì)一直在趕進(jìn)度做一個(gè)前后端不分離的CRUD后臺管理系統(tǒng),當(dāng)時(shí)有幾個(gè)同事在做一個(gè)頁面的時(shí)候討論一個(gè)亂碼的問題。在他們討論的過程中,無意蹦出了兩個(gè)讓筆者突然清醒的詞語:亂碼和UTF-8。筆者第一時(shí)間想到的是剛用Cnblogs的時(shí)候?qū)戇^的一篇文章:《小伙子又亂碼了吧-Java字符編碼原理總結(jié)》(現(xiàn)在看起來標(biāo)題起得挺二的)。當(dāng)時(shí)有對字符編碼的原理做過一些探究,想想有點(diǎn)慚愧,1年多前看過的東西差不多忘記得一干二凈。
直接說原因:UTF-8編碼的中文,大部分情況下一個(gè)中文字符長度占據(jù)3個(gè)字節(jié)(3 byte,也就是32 x 3或者32 x 4個(gè)位),而Java中字符串長度的獲取方法String#length()是返回String實(shí)例中的Char數(shù)組的長度。但是我們多數(shù)情況下會(huì)使用Netty的字節(jié)緩沖區(qū)ByteBuf,而ByteBuf讀取字符序列的方法需要預(yù)先指定讀取的長度ByteBuf#readCharSequence(int length, Charset charset);,因此,在編碼的時(shí)候需要預(yù)先寫入字符串序列的長度。但是有一個(gè)隱藏的問題是:ByteBuf#readCharSequence(int length, Charset charset)方法底層會(huì)創(chuàng)建一個(gè)length長度的byte數(shù)組作為緩沖區(qū)讀取數(shù)據(jù),由于UTF-8中1 char = 3 or 4 byte,因此ChineseMessageEncoder在寫入字符序列長度的時(shí)候雖然字符個(gè)數(shù)是對的,但是每個(gè)字符總是丟失2個(gè)或者4個(gè)byte的長度,而ChineseMessageDecoder在讀取字符序列長度的時(shí)候總是讀到一個(gè)比原來短的長度,也就是最終會(huì)拿到一個(gè)不完整或者錯(cuò)誤的字符串序列。
解決方案
UTF-8編碼的中文在大多數(shù)情況下占3個(gè)字節(jié),在一些有生僻字的情況下可能占4個(gè)字節(jié)。可以暴力點(diǎn)直接讓寫入字節(jié)緩沖區(qū)的字符序列長度擴(kuò)大三倍,只需修改編碼器的代碼:
public class ChineseMessageEncoder extends MessageToByteEncoder<ChineseMessage> {@Overrideprotected void encode(ChannelHandlerContext ctx, ChineseMessage target, ByteBuf out) throws Exception {// 寫入IDout.writeLong(target.getId());String message = target.getMessage();int length = message.length() * 3; // <1> 直接擴(kuò)大字節(jié)序列的預(yù)讀長度// 寫入Message長度out.writeInt(length);// 寫入Message字符序列out.writeCharSequence(message, StandardCharsets.UTF_8);} }當(dāng)然,這樣做太暴力,硬編碼的做法既不規(guī)范也不友好。其實(shí)Netty已經(jīng)提供了內(nèi)置的工具類io.netty.buffer.ByteBufUtil:
// 獲取UTF-8字符的最大字節(jié)序列長度 public static int utf8MaxBytes(CharSequence seq){}// 寫入U(xiǎn)TF-8字符序列,返回寫入的字節(jié)長度 - 建議使用此方法 public static int writeUtf8(ByteBuf buf, CharSequence seq){}我們可以先記錄一下writerIndex,先寫一個(gè)假的值(例如0),再使用ByteBufUtil#writeUtf8()寫字符序列,然后根據(jù)返回的寫入的字節(jié)長度,通過writerIndex覆蓋之前寫入的假值:
public class ChineseMessageEncoder extends MessageToByteEncoder<ChineseMessage> {@Overrideprotected void encode(ChannelHandlerContext ctx, ChineseMessage target, ByteBuf out) throws Exception {out.writeLong(target.getId());String message = target.getMessage();// 記錄寫入游標(biāo)int writerIndex = out.writerIndex();// 預(yù)寫入一個(gè)假的lengthout.writeInt(0);// 寫入U(xiǎn)TF-8字符序列int length = ByteBufUtil.writeUtf8(out, message);// 覆蓋lengthout.setInt(writerIndex, length);} }至此,問題解決。如果遇到其他Netty編碼解碼問題,解決的思路是一致的。
小結(jié)
Netty學(xué)習(xí)過程中,編碼解碼占一半,網(wǎng)絡(luò)協(xié)議知識和調(diào)優(yōu)占另一半。
Netty的源碼很優(yōu)秀,很有美感,閱讀起來很舒適。
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>代碼:
// 實(shí)體 @Data public class ChineseMessage implements Serializable {private long id;private String message; }// 編碼器 public class ChineseMessageEncoder extends MessageToByteEncoder<ChineseMessage> {@Overrideprotected 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 {@Overrideprotected 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>() {@Overrideprotected 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>() {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, ChineseMessage message) throws Exception {log.info("接收到服務(wù)端的響應(yīng):{}", message);}});}});ChannelFuture future = bootstrap.connect("localhost", 9092).sync();System.out.println("客戶端啟動(dòng)成功...");Channel channel = future.channel();ChineseMessage message = new ChineseMessage();message.setId(1L);message.setMessage("張大狗");channel.writeAndFlush(message);future.channel().closeFuture().sync();} finally {workerGroup.shutdownGracefully();}} }// 服務(wù)端 @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>() {@Overrideprotected 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>() {@Overrideprotected 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("啟動(dòng)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 國慶快樂(*^▽^*))
轉(zhuǎn)載于:https://www.cnblogs.com/throwable/p/11619080.html
總結(jié)
以上是生活随笔為你收集整理的一个低级错误引发Netty编码解码中文异常的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis高级客户端Lettuce详解
- 下一篇: 关于python 类的使用