日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

發布時間:2025/10/17 编程问答 21 豆豆
生活随笔 收集整理的這篇文章主要介紹了 一个低级错误引发Netty编码解码中文异常 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言

最近在調研Netty的使用,在編寫編碼解碼模塊的時候遇到了一個中文字符串編碼和解碼異常的情況,后來發現是筆者犯了個低級錯誤。這里做一個小小的回顧。

錯誤重現

在設計Netty的自定義協議的時候,發現了字符串類型的屬性,一旦出現中文就會出現解碼異常的現象,這個異常并不一定出現了Exception,而是出現了解碼之后字符截斷出現了人類不可讀的字符。編碼和解碼器的實現如下:

// 實體 @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 {// 寫入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);} }

簡單地編寫客戶端和服務端代碼,然后用客戶端服務端發送一條帶中文的消息:

// 服務端日志 接收到客戶端的請求: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個或者4個byte的長度,而ChineseMessageDecoder在讀取字符序列長度的時候總是讀到一個比原來短的長度,也就是最終會拿到一個不完整或者錯誤的字符串序列。

解決方案

UTF-8編碼的中文在大多數情況下占3個字節,在一些有生僻字的情況下可能占4個字節。可以暴力點直接讓寫入字節緩沖區的字符序列長度擴大三倍,只需修改編碼器的代碼:

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> 直接擴大字節序列的預讀長度// 寫入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> {@Overrideprotected void encode(ChannelHandlerContext ctx, ChineseMessage target, ByteBuf out) throws Exception {out.writeLong(target.getId());String message = target.getMessage();// 記錄寫入游標int writerIndex = out.writerIndex();// 預寫入一個假的lengthout.writeInt(0);// 寫入UTF-8字符序列int length = ByteBufUtil.writeUtf8(out, message);// 覆蓋lengthout.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> {@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("接收到服務端的響應:{}", 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>() {@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("啟動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

總結

以上是生活随笔為你收集整理的一个低级错误引发Netty编码解码中文异常的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。