本来是打算用netty的handler来做业务处理,把业务分散到多个handler来处理,这样子系统看上去就比较模块化了,方便扩展和拆除,但netty5的 handler的请求传递是有“坑”的,不能说是错误,只能说开发者不注意这个错可能不太容易被发现。下面我把这个问题描述一下。
我用netty封装的HTTP编解码处理的HTTP请求,当然我把处理POST,GET 以及GET中的图片等等请求方法,还有按照content-type 分成多个handler来处理,因为post/get请求中的表单数据或者json数据我是要存redis的,其余的数据我只做转接,所以我感觉按照请求头的信息这样处理业务会比较容易。于是我封装了四个handler: GetRequestHandler, PostRequestHandler, ImageHandler TextHandler(分别处理数据类型为JSON的GET请求,POST请求,图片请求,静态文本资源)。于是所有不符合当前handler的请求,我都用 ctx.fireChannelRead方法将请求传递下去。
这里我不想重复 inbound outbound handler的概念,别的博客有写,下面这哥们儿写的就不错:
http://my.oschina.net/jamaly/blog/272385
PS:这里还要注意,netty5把inbound outbound handler的概念模糊了,做成了一个ChannelHandlerAdapter,4里面是这样的两个类:ChannelInboundHandlerAdapter ChannelOutboundHandlerAdapter. 用新版本的netty时候要注意这两个类被废弃了直接用那个ChannelHandlerAdapter即可。
然后下面我的代码,这里只给一个GetRequestHandler的接收消息方法,其他的handler和他差不离儿。
这个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来处理。
这个代码最终出了一个异常: io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1 详细异常信息请看我昨晚大半夜发的问题(这种偏的问题很少有人关注苦逼得不要不要的):http://www.oschina.net/question/2320871_2182481
后来第二天我问了学长,我们一起看了看源码,知道哪里的问题了并得到了解决方案。
首先这个异常的意思是 对象引用计数器 当前值是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地址:
https://github.com/rpgmakervx/jproxy
版权声明:本文为CSDN博主「weixin_34176694」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_34176694/article/details/92495482