使用netty4.x客戶端接收較大數據量報文時發生的讀取不完整bug修復記錄


1、先說問題

背景:服務是運行在Linux上的安全網關提供的,TCP協議發送 通過二進制編碼的xml字符串 報文,報文頭的第一個字段是int類型的表示字節序標記,第二個字段是int類型的表示整個報文長度。

現象:數據量較小時完全可以正常收發報文,當服務端發送的報文數據量較大時(本例是將近600k)概率性出現接收數據直接調用readComplete()方法而沒有走channelRead()

跟蹤:跟蹤代碼發現出問題時context 的 read() 方法執行中讀取到一百多k(有時兩百多也可能三百多,總之是還沒讀取到全部數據)時某次讀到的數據本應該是1024字節(填滿默認分配的ByteBuf)卻只讀到了576字節;

    netty框架代碼中判斷如果當前讀到的字節數小於ByteBuf的size則認為是讀取完成,因此調用了readComplete()方法,出錯。。。

解決方案:在ClientHandler類添加一個標記flag,用於是否正常讀取數據判斷。channelRead()方法正常調用則將其置為true;readComplete方法中添加一個判斷只有當flag為true時關閉context否則繼續調用ctx.read()。

2、再上核心代碼

Client:

 1 ClientHandler clientHandler = new ClientHandler(this);
 2             bootstrap.group(eventLoop)
 3                     .channel(NioSocketChannel.class)
 4                     .option(ChannelOption.TCP_NODELAY, true)
 5                     .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
 6                     .option(ChannelOption.MAX_MESSAGES_PER_READ, Integer.MAX_VALUE)
 7                     .handler(new ClientChannelInitializer(clientHandler));
 8             
 9             ChannelFuture f = bootstrap.connect(host, port).sync();
10 
11             f.channel().closeFuture().sync();
 1 private class ClientChannelInitializer extends ChannelInitializer<SocketChannel> {
 2         private ClientHandler clientHandler;
 3 
 4         public ClientChannelInitializer(ClientHandler clientHandler) {
 5             this.clientHandler = clientHandler;
 6         }
 7 
 8         @Override
 9         protected void initChannel(SocketChannel socketChannel) throws Exception {
10 
11             socketChannel.pipeline().addLast(new SplDecoder());
12             socketChannel.pipeline().addLast(clientHandler);
13             channel = socketChannel;
14         }
15     }
// 解決問題前 initChannel的實現是這樣的,使用了netty內部的長度字段解碼器
@Override
protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,4,4,-8,0)); ch.pipeline().addLast(clientHandler); }

 

ClientHandler:

public class ClientHandler extends ChannelInboundHandlerAdapter {
 1 @Override
 2     public void channelActive(ChannelHandlerContext context) throws Exception {
 3         logger.info("Ready to send request...");
 4         ByteBuffer result = getByteBuffer();
 5         ByteBuf buf = Unpooled.buffer(result.remaining());
 6         buf.writeBytes(result);
 7 
 8         context.writeAndFlush(buf);
 9     }
10 
11     @Override
12     public void channelRead(ChannelHandlerContext context, Object msg) throws Exception {
13         logger.info("Get server response...");
14 
15         String[] result = (String[]) msg;
16 
17         logger.debug("response xml is : " + result[1]);
18         client.setResponse(result);
19 
20         ok = true;
21     }
22 
23     @Override
24     public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
25         ctx.flush();
26         if (ok) {
27             ctx.close();
28         } else {
29             ctx.read();
30         }
31     }

3、最后說解決過程

起初我懷疑是使用netty的定長字段解碼器LengthFieldBasedFrameDecoder參數不當引起的,因為自認為對它理解不深;於是自己寫了一個繼承byteToMessageDecoder的解碼器可以實現解決拆包問題和解碼功能,但是問題依然概率性出現...

后來抱着試試看的態度在ClientHandler里面添加了一個實例屬性ok(默認false),在正常執行channelRead()方法后將其置為true,readComplete()方法中做判斷如果ok==false調用ctx.read(),運行發現完美解決問題

因為調用read()方法是繼續讀取數據而不是重新讀取(因為此時ctx和channel、pipline等數據狀態都沒變)!

在SplDecoder類中添加當前讀取數據打印信息:“logger.debug("讀取數據:本次" + readableBytes + ";累計" + currentLength + ";總共" + total);”;

在ctx.read()前面添加打印錯誤信息“****** 讀取數據不完整,再次讀取......”

運行正常和出錯時的控制台打印信息如下(由於實際打印行數太多,我用"......"代替了部分重復行):

 1 Connected to the target VM, address: '127.0.0.1:62194', transport: 'socket'
 2 log4j:WARN No appenders could be found for logger (io.netty.util.internal.logging.InternalLoggerFactory).
 3 log4j:WARN Please initialize the log4j system properly.
 4 讀取數據:本次1024;累計1024;總共574842
 5 讀取數據:本次1024;累計2048;總共574842
 6 讀取數據:本次1024;累計3072;總共574842
 7 ......
 8 讀取數據:本次1024;累計572416;總共574842
 9 讀取數據:本次1024;累計573440;總共574842
10 讀取數據:本次1024;累計574464;總共574842
11 讀取數據:本次378;累計574842;總共574842
12 0 ~~ null
13 Disconnected from the target VM, address: '127.0.0.1:62194', transport: 'socket'
14 
15 Process finished with exit code 0
正常時結果
 1 Connected to the target VM, address: '127.0.0.1:62068', transport: 'socket'
 2 log4j:WARN No appenders could be found for logger (io.netty.util.internal.logging.InternalLoggerFactory).
 3 log4j:WARN Please initialize the log4j system properly.
 4 讀取數據:本次1024;累計1024;總共574842
 5 讀取數據:本次1024;累計2048;總共574842
 6 讀取數據:本次1024;累計3072;總共574842
 7 讀取數據:本次1024;累計4096;總共574842
 8 讀取數據:本次1024;累計5120;總共574842
 9 讀取數據:本次1024;累計6144;總共574842
10 讀取數據:本次1024;累計7168;總共574842
11 讀取數據:本次1024;累計8192;總共574842
12 讀取數據:本次1024;累計9216;總共574842
13 讀取數據:本次1024;累計10240;總共574842
14 讀取數據:本次1024;累計11264;總共574842
15 讀取數據:本次1024;累計12288;總共574842
16 讀取數據:本次1024;累計13312;總共574842
17 讀取數據:本次576;累計13888;總共574842
18 ****** 讀取數據不完整,再次讀取......
19 讀取數據:本次16384;累計30272;總共574842
20 讀取數據:本次16384;累計46656;總共574842
21 讀取數據:本次16384;累計63040;總共574842
22 ......
23 讀取數據:本次16384;累計554560;總共574842
24 讀取數據:本次16384;累計570944;總共574842
25 讀取數據:本次3898;累計574842;總共574842
26 0 ~~ null
27 Disconnected from the target VM, address: '127.0.0.1:62068', transport: 'socket'
28 
29 Process finished with exit code 0
出錯時結果

 結果最后打印0~~null表示正常結束(返回code為0錯誤信息為null)。

附:

問題處理前控制台打印結果

1 "C:\Program Files...
2 log4j:WARN No appenders could be found for logger (io.netty.util.internal.logging.InternalLoggerFactory).
3 log4j:WARN Please initialize the log4j system properly.
4 -1 ~~ 服務異常;Detail:java.lang.NullPointerException
5 
6 Process finished with exit code 0
問題處理前控制台打印信息

由於沒有執行channelRead()方法,所以我獲取到的數據沒能執行賦值操作,報了空指針異常。

結果最后打印 -1 ~~ 服務異常;Detail:java.lang.NullPointerException 表示發生了異常(返回code為-1;錯誤信息為"服務異常;Detail:java.lang.NullPointerException")


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM