netty客户端发送数据时异常被隐藏

问题描述

    最近需要用netty实现一个中间件通信,开始为了先快速把客户端和服务端通信的demo完成,只是采用了字符串的编解码方式(StringEncoder,StringDecoder)。客户端和服务端可以正常互发数据,一切运行正常。
    但是字符串的编解码并不适合业务实体类的传输,为了快速实现实体类传输,所以决定采用jboss-marshalling-serial序列化方式先完成demo,但是在客户端发送数据时,服务端却无法收到数据,客户端控制台也没有任何异常信息。
    先看整个demo实现代码,再查找问题原因。(先提前说明,示例代码是完全正确无逻辑bug的)

pom依赖

<dependencies>
    <!--只是用到了里面的日志框架-->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter</artifactId>
	</dependency>

	<dependency>
		<groupId>io.netty</groupId>
		<artifactId>netty-all</artifactId>
		<version>4.1.56.Final</version>
	</dependency>

	<dependency>
		<groupId>org.jboss.marshalling</groupId>
		<artifactId>jboss-marshalling-serial</artifactId>
		<version>2.0.10.Final</version>
	</dependency>
</dependencies>

jboss-marshalling-serial序列化工具类

  • netty提供的Marshalling编解码器采用消息头和消息体的方式
  • JBoss Marshalling是一个Java对象序列化包,对jdk默认的序列化框架进行优化,但又保持跟Serializable接口的兼容,同时增加了一些可调用的参数和附加的特性
  • 经过测试发现序列化后的流较protostuff,MessagePack还是比较大的,
  • 序列化和反序列化的类必须是同一个类,否则抛出异常: io.netty.handler.codec.DecoderException: java.lang.ClassNotFoundException: com.bruce.netty.rpc.entity.UserInfo
public final class MarshallingCodeFactory {
    private static final InternalLogger log = InternalLoggerFactory.getInstance(MarshallingCodeFactory.class);

    /** 创建Jboss marshalling 解码器 */
    public static MyMarshallingDecoder buildMarshallingDecoder() {
        //参数serial表示创建的是Java序列化工厂对象,由jboss-marshalling-serial提供
        MarshallerFactory factory = Marshalling.getProvidedMarshallerFactory("serial");
        MarshallingConfiguration configuration = new MarshallingConfiguration();
        configuration.setVersion(5);
        DefaultUnmarshallerProvider provider = new DefaultUnmarshallerProvider(factory, configuration);

        return new MyMarshallingDecoder(provider, 1024);
    }

    /** 创建Jboss marshalling 编码器 */
    public static MarshallingEncoder buildMarshallingEncoder() {
        MarshallerFactory factory = Marshalling.getProvidedMarshallerFactory("serial");
        MarshallingConfiguration configuration = new MarshallingConfiguration();
        configuration.setVersion(5);
        DefaultMarshallerProvider provider = new DefaultMarshallerProvider(factory, configuration);
        return new MarshallingEncoder(provider);
    }

    public static class MyMarshallingDecoder extends MarshallingDecoder {
        public MyMarshallingDecoder(UnmarshallerProvider provider, int maxObjectSize) {
            super(provider, maxObjectSize);
        }
        @Override
        protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
            log.info("读取数据长度:{}", in.readableBytes());
            return super.decode(ctx, in);
        }
    }
}

服务端代码实现

服务端业务处理器:(真实场景中不要在io线程执行耗时业务逻辑处理)

@ChannelHandler.Sharable
public class SimpleServerHandler extends ChannelInboundHandlerAdapter {
    private static final InternalLogger log = InternalLoggerFactory.getInstance(SimpleServerHandler.class);

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        log.info("handlerAdded" + this.hashCode());
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        log.info("server channelRead:{}", msg);
        ctx.channel().writeAndFlush("hello netty");
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        if (cause instanceof java.io.IOException) {
            log.warn("client close");
        } else {
            cause.printStackTrace();
        }
    }
}

服务端启动类

public class NettyServer {
    private static final InternalLogger log = InternalLoggerFactory.getInstance(NettyServer.class);

    public static void main(String[] args) throws Exception {
        EventLoopGroup acceptGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup =  new NioEventLoopGroup();
        Class<? extends ServerSocketChannel> serverSocketChannelClass = NioServerSocketChannel.class;
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(acceptGroup, workerGroup)
                .channel(serverSocketChannelClass)
                .option(ChannelOption.SO_BACKLOG, 128)
                .option(ChannelOption.SO_REUSEADDR, true)
                .childOption(ChannelOption.SO_KEEPALIVE, false) //默认为false
                .handler(new LoggingHandler())
                .childHandler(new CustomCodecChannelInitializer());

        try {
            //sync() 将异步变为同步,绑定到8088端口
            ChannelFuture channelFuture = bootstrap.bind(8088).sync();
            log.info("server 启动成功");
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        Thread serverShutdown = new Thread(() -> {
            log.info("执行jvm ShutdownHook, server shutdown");
            acceptGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        });
        //注册jvm ShutdownHook,jvm退出之前关闭服务资源
        Runtime.getRuntime().addShutdownHook(serverShutdown);
    }

    static class CustomCodecChannelInitializer extends ChannelInitializer<SocketChannel> {
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
            pipeline.addLast(MarshallingCodeFactory.buildMarshallingDecoder());
            pipeline.addLast(MarshallingCodeFactory.buildMarshallingEncoder());
            pipeline.addLast(new SimpleServerHandler());
        }
    }
}

客户端代码实现

客户端业务处理器

public class SimpleClientHandler extends ChannelInboundHandlerAdapter {
    private static final InternalLogger log = InternalLoggerFactory.getInstance(SimpleClientHandler.class);

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        log.info("client receive:{}", msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
    }
}

客户端启动类

public class NettyClient {
    private static final InternalLogger log = InternalLoggerFactory.getInstance(NettyClient.class);

    public static void main(String[] args) {
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        Class<? extends SocketChannel> socketChannelClass = NioSocketChannel.class;

        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(workerGroup)
                .channel(socketChannelClass)
                .option(ChannelOption.TCP_NODELAY, true)
                .option(ChannelOption.SO_KEEPALIVE, false)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30000)
                .handler(new CustomCodecChannelInitializer());

        Channel clientChannel;
        try {
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8088);
            //同步等待连接建立成功, 这里示例代码, 可以认为是一定会连接成功
            boolean b = channelFuture.awaitUninterruptibly(10, TimeUnit.SECONDS);
            clientChannel = channelFuture.channel();

            for (int i = 0; i < 10; i++) {
                Thread.sleep(1000);
                UserInfo userInfo = new UserInfo("bruce", 18);
                log.info("send user info");
                //连接成功后发送数据
                send(clientChannel, userInfo);
            }
            //实际上这个地方会永远阻塞等待
            clientChannel.closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }

    static void send(Channel channel, UserInfo data) {
        //连接成功后发送数据
        ChannelFuture channelFuture1 = channel.writeAndFlush(data);
    }

    static class CustomCodecChannelInitializer extends ChannelInitializer<SocketChannel> {
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
            pipeline.addLast(MarshallingCodeFactory.buildMarshallingDecoder());
            pipeline.addLast(MarshallingCodeFactory.buildMarshallingEncoder());
            pipeline.addLast(new SimpleClientHandler());
        }
    }
}

实体类UserInfo,

public class UserInfo {
    private String username;
    private int age;

    public UserInfo() {
    }

    public UserInfo(String username, int age) {
        this.username = username;
        this.age = age;
    }
    //getter / setter 省略
}

先启动服务端,再启动客户端可以在idea控制台发现:

服务端和客户端建立了连接,客户端在发送数据, 但 是 服 务 端 却 没 有 收 到 , 并 且 控 制 台 没 有 任 何 异 常 信 息 \color{#FF3030}{但是服务端却没有收到,并且控制台没有任何异常信息}
在这里插入图片描述
在这里插入图片描述
既然没有异常,只能先在客户端断点,确认客户端是否正常,根据经验直接查看MarshallingEncoder的编码方法MarshallingEncoder#encode,debug执行先确认UserInfo对象有没有被正确序列化。
在这里插入图片描述
在执行到marshaller.writeObject(msg)时出现了异常。
在这里插入图片描述
继续跟进断点会进入catch中,显示java.io.NotSerializableException,(脑中出现一句话:我大意了,没有…)已经可以知道UserInfo类没有继承序列化接口java.io.Serializable而抛出异常。UserInfo只需要继承java.io.Serializable就可以正常向客户端发送数据。

但是为什么控制台没有抛出异常呢 !?

继续跟进断点NotSerializableException被包装在io.netty.handler.codec.EncoderException中抛出,序列化的buf也在finally中被释放。而EncoderException会被AbstractChannelHandlerContext#invokeWrite0方法的catch语句中被处理。

private void invokeWrite0(Object msg, ChannelPromise promise) {
    try {
        ((ChannelOutboundHandler) handler()).write(this, msg, promise);
    } catch (Throwable t) {
        notifyOutboundHandlerException(t, promise);
    }
}
 private static void notifyOutboundHandlerException(Throwable cause, ChannelPromise promise) {
    // Only log if the given promise is not of type VoidChannelPromise as tryFailure(...) is expected to return
    // false.
    PromiseNotificationUtil.tryFailure(promise, cause, promise instanceof VoidChannelPromise ? null : logger);
}

最终会执行到io.netty.util.concurrent.DefaultPromise#setValue0,主要目的就是为了记录这个异常信息,然后检查是否有GenericFutureListener监听这次发送请求的结果。如果有Listener则在nio线程中回调监听器方法。

private boolean setValue0(Object objResult) {
    if (RESULT_UPDATER.compareAndSet(this, null, objResult) ||
        RESULT_UPDATER.compareAndSet(this, UNCANCELLABLE, objResult)) {
        if (checkNotifyWaiters()) {
            notifyListeners();
        }
        return true;
    }
    return false;
}

private synchronized boolean checkNotifyWaiters() {
    if (waiters > 0) {
        notifyAll();
    }
    return listeners != null;
}

然而笔者的示例中并没有设置GenericFutureListener,checkNotifyWaiters方法返回的是false,不会执行notifyListeners();方法,所以整个异常被吞没。而Promise#tryFailure方法最终返回true。
再看方法io.netty.util.internal.PromiseNotificationUtil#tryFailure,虽然也是会处理Throwable,但是只在Promise#tryFailure返回false并且logger不为null时执行。所以这里也不会打印出日志

public static void tryFailure(Promise<?> p, Throwable cause, InternalLogger logger) {
	if (!p.tryFailure(cause) && logger != null) {
		Throwable err = p.cause();
		if (err == null) {
			logger.warn("Failed to mark a promise as failure because it has succeeded already: {}", p, cause);
		} else if (logger.isWarnEnabled()) {
			logger.warn(
					"Failed to mark a promise as failure because it has failed already: {}, unnotified cause: {}",
					p, ThrowableUtil.stackTraceToString(err), cause);
		}
	}
}

如何打印出这些异常信息呢?

方案1 (异步处理)
在数据发送过后,给ChannelFuture添加监听器,用于监听此次发送的结果,当出现异常时,对异常进行处理。

static void send(Channel channel, UserInfo data) {
    //连接成功后发送数据
    ChannelFuture channelFuture1 = channel.writeAndFlush(data);
    channelFuture1.addListener(new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) throws Exception {
            Throwable cause = future.cause();
            if (cause != null) {
                cause.printStackTrace();
            }
        }
    });
}

方案2 (不推荐,根据业务决定)
在数据发送过后,同步等待发送结果,判断是否存在异常。

static void send(Channel channel, UserInfo data) {
    //连接成功后发送数据
    ChannelFuture channelFuture1 = channel.writeAndFlush(data);
    while (!channelFuture1.isDone()) {
        try {
            //超时时间示例值,根据业务决定
            boolean notTimeout = channelFuture1.await(50);
        } catch (Exception e) {
            log.warn(e.getMessage());
        }
    }
    Throwable cause = channelFuture1.cause();
    if (cause != null) {
        cause.printStackTrace();
    }
}

没有监听ChannelFuture,异常就被隐藏是否合理呢?

这个问题见仁见智,对笔者有点代码洁癖来说,这里至少是可有优化一下的,不至于让开发者耗费时间去查找丢失的异常信息。优化逻辑也简单,在io.netty.util.concurrent.DefaultPromise#setFailure0中,如果既没有listeners也没有await等待时,则打印异常信息。
修改DefaultPromise代码如下:

private boolean setFailure0(Throwable cause) {
    if (listeners == null && waiters == 0) {
        logger.error("cause:", cause);
    }
    return setValue0(new CauseHolder(checkNotNull(cause, "cause")));
}

请看pr: https://github.com/netty/netty/pull/10917

总结

这里只是通过编码时没有注意到的细节(实体类没有实现序列化接口),来分析为什么异常被吞及处理方案,可以通过异常栈快速定位问题,但如果想要没有异常,则只能根据异常做相应的修改了。同时可以让我们更加了解netty的实现细节。
最后还是建议通过channel发送数据后,对返回的ChannelFuture做是否存在异常判断以及处理,防止出现类似的情况。

  • 9
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值