使用Netty在客戶端和服務端建立通訊通道,一般來說,一個連接可能很久沒有訪問,由於各種各樣的網絡問題導致連接已經失效,客戶端再次發送請求時會產生連接異常。
基於這個原因,需要在客戶端和服務端之間建立ping-pong的心跳機制,我的本意是想通過IdleStateHandler這個netty提供的工具來自動發現連接空閑狀態,卻出現了以下問題:
1.客戶端和服務端使用 IdleStateHandler 時,原本的請求響應機制失效
2.IdleStateHandler的空閑通知功能正常,但是卻不准確
最開始排查這個問題的原因是一頭霧水,通過斷點和分析程序已經走到哪個位置,最終確定IdleStateHandler在輸出服務端的響應到channel時,出現了問題
/** * 發送服務端的響應 * @param messageId * @param channel * @param messageResult */ protected void sendResponse(long messageId, Channel channel, Object messageResult) { MsgHeader msgHeader = new MsgHeader(Constants.RESPONSE_MSG); if(messageResult != null) { msgHeader.setClz(messageResult.getClass().getName()); }else { msgHeader.setClz(Constants.NULL_RESULT_CLASS); } ResponseMsg responseMsg = new ResponseMsg(); responseMsg.setReceiveTime(System.currentTimeMillis()); responseMsg.setResponse(messageResult); responseMsg.setMsgHeader(msgHeader); responseMsg.getMsgHeader().setMsgId(messageId); //如果使用voidPromise,則無法和IdleStateHandler同時使用,因為它會觸發voidPromise的addListener(...)操作,從而導致write失敗 channel.writeAndFlush(responseMsg, channel.voidPromise()); }
在處理完請求進行響應輸出時,最后一行在寫響應時,使用了空的promise,最終會導致IdleStateHandler的write方法處獲得這個空的Promise,進而導致異常
@Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { promise.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { lastWriteTime = System.nanoTime(); firstWriterIdleEvent = firstAllIdleEvent = true; } }); ctx.write(msg, promise); }
空的Promise的addListener實現方式是簡單粗暴的拋出異常,這就導致末行的ctx.write(msg, promise)永遠也不能執行到。
final class VoidChannelPromise extends AbstractFuture<Void> implements ChannelPromise {
... @Override public VoidChannelPromise addListener(GenericFutureListener<? extends Future<? super Void>> listener) { fail(); return this; } private static void fail() { throw new IllegalStateException("void future"); }
... }
找到這個原因后,立即修改了Server端Handler的發送方法
/** * 發送服務端的響應 * @param messageId * @param channel * @param messageResult */ protected void sendResponse(long messageId, Channel channel, Object messageResult) { MsgHeader msgHeader = new MsgHeader(Constants.RESPONSE_MSG); if(messageResult != null) { msgHeader.setClz(messageResult.getClass().getName()); }else { msgHeader.setClz(Constants.NULL_RESULT_CLASS); } ResponseMsg responseMsg = new ResponseMsg(); responseMsg.setReceiveTime(System.currentTimeMillis()); responseMsg.setResponse(messageResult); responseMsg.setMsgHeader(msgHeader); responseMsg.getMsgHeader().setMsgId(messageId); //如果使用voidPromise,則無法和IdleStateHandler同時使用,因為它會觸發voidPromise的addListener(...)操作,從而導致write失敗 channel.writeAndFlush(responseMsg, channel.newPromise()); }
末行new一個默認的Promise,執行addListener不會產生異常
這樣server端的問題就解決了
然后client端卻無法解決這個問題,原因時client端接收響應后,netty內部會new一個VoidPromise,這就導致了IdleStateHandler放置於客戶端時無論如何都不能正常使用,因為一旦觸發write方法就直接異常了,目前還沒有找到這兩者同時存在的方案。這是Netty4.0.24.Final版本的硬傷,好在的是,Netty4的最新版本已經fix了這個問題
<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.12.Final</version> </dependency>
IdleStateHandler的write方法已經對此做出了修正
@Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { // Allow writing with void promise if handler is only configured for read timeout events. if (writerIdleTimeNanos > 0 || allIdleTimeNanos > 0) { ChannelPromise unvoid = promise.unvoid(); //此處保證返回的一定是非void的 unvoid.addListener(writeListener); ctx.write(msg, unvoid); } else { ctx.write(msg, promise); } }
總結,一個復雜的框架無論如何會有一些毛病,就像業務系統中的一個bug,兩個東西不是同一個人開發的,就會出現使用時的異常情況,找到這個原因進行分析,解決的思路也就躍然於紙上。
最終,我把netty4的版本升級到了最新的4.1.12.Final,防止更多這樣的問題產生。