原本是打算用netty的handler來作業務處理,把業務分散到多個handler來處理,這樣子系統看上去就比較模塊化了,方便擴展和拆除,但netty5的 handler的請求傳遞是有「坑」的,不能說是錯誤,只能說開發者不注意這個錯可能不太容易被發現。下面我把這個問題描述一下。java
我用netty封裝的HTTP編解碼處理的HTTP請求,固然我把處理POST,GET 以及GET中的圖片等等請求方法,還有按照content-type 分紅多個handler來處理,由於post/get請求中的表單數據或者json數據我是要存redis的,其他的數據我只作轉接,因此我感受按照請求頭的信息這樣處理業務會比較容易。因而我封裝了四個handler: GetRequestHandler, PostRequestHandler, ImageHandler TextHandler(分別處理數據類型爲JSON的GET請求,POST請求,圖片請求,靜態文本資源)。因而全部不符合當前handler的請求,我都用 ctx.fireChannelRead方法將請求傳遞下去。git
這裏我不想重複 inbound outbound handler的概念,別的博客有寫,下面這哥們兒寫的就不錯:github
http://my.oschina.net/jamaly/blog/272385redis
PS:這裏還要注意,netty5把inbound outbound handler的概念模糊了,作成了一個ChannelHandlerAdapter,4裏面是這樣的兩個類:ChannelInboundHandlerAdapter ChannelOutboundHandlerAdapter. 用新版本的netty時候要注意這兩個類被廢棄了直接用那個ChannelHandlerAdapter便可。編程
而後下面個人代碼,這裏只給一個GetRequestHandler的接收消息方法,其餘的handler和他差不離兒。json
這個handler繼承自SimpleInboundChannelHandler<HttpRequest>(這也是「坑」之源)緩存
@Override protected void messageReceived(ChannelHandlerContext ctx, Object msg) throws Exception { String context = ""; byte[] bytes = null; CloseableHttpResponse response = null; HttpRequest request = (HttpRequest) msg; boolean isGet = request.method().equals(HttpMethod.GET); boolean isJSON = "application/json".equals(request.headers().get("Content-Type")); if (isGet){ fetchInetAddress(); ProxyClient client = new ProxyClient(address,WebUtil.ROOT.equals(request.uri())?"":request.uri()); if (isJSON){ System.out.println("GET 業務請求"); response = client.fetchText(request.headers()); context = client.getResponse(response); //redis緩存 bytes = context.getBytes(); response(ctx, bytes, response.getAllHeaders()); }else{ System.out.println("GET 頁面請求"); response = client.fetchText(request.headers()); context = client.getResponse(response); //CDN緩存 bytes = context.getBytes(); response(ctx, bytes, response.getAllHeaders()); } }else{ System.out.println("非GET請求或JSON類型 "+request.uri()); ctx.fireChannelRead(request); } }
看上去彷佛並無問題,isGet若是是false說明當前請求根本不是GET的,那就交給別的handler來處理。app
這個代碼最終出了一個異常: io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1 詳細異常信息請看我昨晚大半夜發的問題(這種偏的問題不多有人關注苦逼得不要不要的):http://www.oschina.net/question/2320871_2182481框架
後來次日我問了學長,咱們一塊兒看了看源碼,知道哪裏的問題了並獲得瞭解決方案。ide
首先這個異常的意思是 對象引用計數器 當前值是0,無法再減小了,netty本身維護了一個引用計數器,用來對ByteBuf作內存管理,而不是讓JVM來作。
在接收消息的時候,那個msg是Object類型的,但其是它是在編解碼的時候對ByteBuf對象作了處理才能夠作成各類對象的,所以msg自己也是個ByteBuf.
可是我在非GET請求下並無對msg作任何處理啊,怎麼就會有引用計數器的異常呢?只能說明計數器減小了 這個事兒 不是我乾的,因而只能去自定義handler的父類SimpleInboundChannelHandler 來看看究竟。
這個類其實和別的ChannelHandler乾的事兒同樣,只不過它提供一個messageRecive方法讓你來實現,它在正統的channelRead裏面調用了一下而已,問題就在這個類的channelRead是怎麼處理的了。我複製一段源碼:
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { boolean release = true; try { if(this.acceptInboundMessage(msg)) { this.messageReceived(ctx, msg); } else { release = false; ctx.fireChannelRead(msg); } } finally { if(this.autoRelease && release) { ReferenceCountUtil.release(msg); } } }
this.acceptInboundMessage 首先這個方法就是個雞肋,它調用一個match方法,其父類有兩種match的實現,其中一個直接return true(寫死的有意義麼?),還有一個判斷你的msg.isInstance msg確定是一個實例因此這確定是返回true咯!因此這個方法try塊兒裏面不管如何都會走this.messageRecived,因此不太多是由於調用了兩次fireChannelRead.
問題就在finally釋放計數器了,若是this.autoRelease是true, 就說明了不管如何計數器都會-1,剛纔在網上發現了這麼一句話: 當某個消息被徹底發送成功以後,會經過ReferenceCountUtil.release(message)方法釋放已經發送成功的ByteBuf 這個ByteBuf就是msg。因此在它-1完了,咱們本身還fireChannelRead了一下,到最後他認爲發送成功了,可是引用計數器已經給咱們-1了,因此也就拋那個異常了。
解決方案能夠這樣:
1. 咱們把那個autoRelease字段設置爲false,這樣他就不會給咱們在messageRecived後還要強行-1了
2.這種方法感受很差,就是在咱們本身調用fireChannelRead往下漏請求以前,調用ReferenceCountUtil.retain(),讓計數器+1(但你說這樣作有道理麼,我我的感受對象生命週期管理這個事兒應該讓它框架本身處理好纔是,全都本身管,那和你本身寫malloc和free差很少了)
3.乾脆咱就別繼承SimpleChannelInboundHandler,繼承ChannelHandlerAdapter多好,msg強轉類型而已。或者用4的話繼承那個ChannelInboundHandlerAdapter也行。
我的感受netty的handler作成這種業務可插拔的編程方式真心不錯的,但就是碰上這個坑噁心到我了,話說netty5彷佛廢棄了,估計又有填不完的坑吧。
順便一說,這個問題是我在實現一個Java的代理中間件的時候發現的。開始是用netty5,如今想一想仍是用4好了。github地址: