Netty 组件 Channel 、Future 、Promise

Channel

首先强调一点:NIO的Channel与Netty的Channel不是一个东西!
Netty重新设计了Channel接口,并且给予了很多不同的实现。Channel时Netty的网络抽象类,除了NIO中Channel所包含的网络I/O操作,主动建立/关闭连接,获取双方网络地址的功能外,还包含了Netty框架的功能,例如:获取Channel的EventLoop\Pipeline等。

  • Channel接口是能与一个网络套接字(或组件)进行I/0操作(读取\写入\连接\绑定)的纽带.
  • 通过Channel可以获取连接的状态(是否连接/是否打开),配置通道的参数(设置缓冲区大小等),进行I/O操作

channel的主要方法是:

  • id():返回此通道的全局唯一标识符.
  • isActive():如果通道处于活动状态并连接,则返回true.
  • isOpen():如果通道打开并且可能稍后激活,则返回true.
  • close():关闭channel
  • closeFuture():处理channel的关闭,并处理善后事件
  • isRegistered():如果通道注册了EventLoop,则返回true.
  • localAddress():返回此通道绑定的本地地址.
  • pipeline():返回分派的ChannelPipeline.
  • write():将数据写入.
  • writeAndFlush():写入数据并刷出 ,write+flush方法.

ChannelFuture

由于Netty中的所有I / O操作都是异步的,因此Netty为了解决调用者如何获取异步操作结果的问题而专门设计了ChannelFuture接口。
因此,ChannelChannelFuture可以说形影不离的。

channel和channelfuture的使用:

public class HelloClient {
    public static void main(String[] args) throws InterruptedException, IOException {
        //下面connect()返回值为ChannelFuture。一般带有Future、Promise的类型都是和异步方法配套使用的,用来处理结果
        ChannelFuture channelFuture = new Bootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new StringEncoder());
                    }
                })
        //一般作为事件处理的都是返回一个channelFuture对象,如连接、关闭等
                //此方法是异步非阻塞的,当前main线程发起的调用,真正执行连接的是上面的NioEventLoopGroup,nio线程
                .connect(new InetSocketAddress("localhost", 8080));

        //需要加下面的方法,否则服务器端收不到hello,因为连接还没建立就发送了
        //1)使用sync方法,同步处理
        //sync的主要作用是channelFuture同步处理结果,阻塞当前线程直到nio连接建立完毕
        channelFuture.sync();
        //获取channel
        Channel channel = channelFuture.channel();
        channel.writeAndFlush("hello");

        //2)使用addListener(回调对象) 方法,异步处理结果
        //参数是一个回调对象,现在是主线程不阻塞等结果了,把等待连接建立和建立后做的活给nio线程处理,只需把需要做的事传递过去即可
        channelFuture.addListener(new ChannelFutureListener() {
            @Override
            //在nio线程建立好后,会调用此方法(区别:前一种方法是主线程调用的),该channelFuture就是调用时的channelFuture
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                Channel c = channelFuture.channel();
                c.writeAndFlush("hello");
            }
        });
    }

演示channel的关闭:

设计一个需求:客户端可以不断的发送数据到服务器,当发送quit时就退出,并正常关闭channel。代码如下:

        //…………
        Channel channel =  channelFuture.sync().channel();
        //新开一个输入数据的线程,不影响主线程的运行
        new Thread(()->{
            while (true){
                Scanner scanner = new Scanner(System.in);
                String msg = scanner.nextLine();
                if ("quit".equals(msg)){
                    channel.close();
                    break;
                }
                channel.writeAndFlush(msg);
            }
        },"input").start();
        
        //主线程结束标志
        System.out.println("main结束");

现在多了个需求,关闭之后需要立即处理善后工作

此时我们就需要用到closeFuture()方法了,为了证明此方法的必要性,先举几个反例

1.有小伙伴会想把善后工作的代码写在上面主线程结束标志之前,新线程之后,那么这就不叫善后工作了,主线程创建一个新线程之后,会接着执行,主线程会先于输入线程结束。

2.那把善后工作放在channel.close()之后呢?也不行,因为close()方法是异步操作,关闭这个动作的真正线程是nioEventLoop线程,有可能它还没有把channel关闭,我们就已经把善后工作做完了。小伙伴们可以添加日志自行调试喔

netty结合Slf4j打印日志_清风拂来水波不兴的博客-CSDN博客

正确姿势:

1)同步关闭:先获取closeFuture,再sync,让主线程同步等待,当输入数据的线程close()通道后,sync方法就会停止阻塞,主线程就会继续执行下面的代码

 //1)同步关闭,返回值同样为ChannelFuture ,请勿混淆
ChannelFuture closeFuture = channel.closeFuture();
closeFuture.sync();
//优雅的关闭事件循环组,因为连channel都关闭了,事件循环组里的线程们也需要关闭,结束程序的运行
group.shutdownGracefully();
log.debug("遗言");

2)异步关闭:

ChannelFuture closeFuture = channel.closeFuture();
//2)异步关闭,注意是closeFuture调用,给它添加一个操作完成后执行的方法
closeFuture.addListener(new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture channelFuture) throws Exception {
       log.debug("遗言");
        //关闭事件循环组,优雅的关闭
       group.shutdownGracefully();
    }
});

是不是感觉这两种方法和connect时的处理逻辑一模一样。

为什么netty要用异步的方法去处理事件?

像一些connect、close等方法都是异步调用,实际是事件循环组给你真正的执行,为什么要这样呢?同步方法不好吗?

其实Netty采用的是流水线的工作方式,分工细化,提升效率吞吐率,充分发挥处理器的性能。

  • 单线程没法异步提高效率,必须配合多线程、多核cpu才能发挥异步的优势
  • 异步并没有缩短响应时间,反而有所增加
  • 合理进行任务拆分,也是利用异步的关键

Futrue&Promise

在异步处理时,经常用到这两个接口,类关系如下,上面的channelFuture就是实现了Future

首先要说明netty中的Future 与 jdk juc中的Future同名,但是是两个接口,netty的Future继承自jdk的Future,而Promise又对netty Future进行了扩展。

  • jdk Future 只能同步等待任务结束(或成功、或失败)才能得到结果
  • netty Future可以同步等待任务结束得到结果,也可以异步方式得到结果,但都是要等任务结束
  • netty Promise不仅有netty Future的功能,而且脱离了任务独立存在,只作为两个线程间传递结果的容器
     
方法名jdk Futurenetty Futurenetty Promise
cancel取消任务--
isCanceled任务是否取消--
isDone

任务是否完成

不能区分成功失败

--
get获取任务结果,阻塞等待--
getNow-获取任务结果,非阻塞,还未产生结果为null-
await-等待任务结束,如果任务失败不会抛异常,而是通过isSuccess判断-
sync-等待任务结束,如果任务失败抛异常-
isSuccess-判断任务是否成功-
cause-获取失败信息,非阻塞,如果没有失败返回null-
addListener-添加回调,异步接收结果-
setSuccess--设置成功结果结果
setFailure--设置任务失败结果

netty Future:

演示1:同步等待结果,也就是在当前线程同步等待获取结果

演示2:异步等待,由nio线程获取结果 ,下面是利用回调方法得到结果

netty Promise:

可以看到Future是提交任务时返回的,Future的创建和结果的设置权都不是我们控制的,不够灵活;下面讲解Promise的使用

 //主动的创建Promise对象(作为结果容器),构造方法需要传一个eventLoop对象
        DefaultPromise<Integer> promise = new DefaultPromise<Integer>(eventLoop);
        new Thread(()->{
            //任意一个线程执行计算,计算完毕向promise填充结果
            log.debug("开始计算....");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //放置成功的结果到promise
            promise.setSuccess(5);
            //放置异常对象,放在catch里
            //promise.setFailure(e)
        }).start();

Handler&Pipeline

ChannelHandler用来处理Channel上的各种事件,分为入站(inbound)、出站(outbount)两种。所有ChannelHandler被连成一串,就是Pipeline

  • 入站通常是ChannelInboundHandlerAdapter的子类,主要用于读取客户端数据,写回结果
  • 出站通常是ChannelOutboundHandlerAdapter的子类,主要对写回结果进行加工

演示入站

 此时的pipeline中的handler为:head->h1->h2->tail

演示出站

.childHandler(new ChannelInitializer<NioSocketChannel>() {
    @Override
    protected void initChannel(NioSocketChannel ch) throws Exception {
        //1.通过channel拿到pipeline
        ChannelPipeline pipeline = ch.pipeline();
        //2.添加处理器,netty默认添加了head-> xxx -> tail处理器
        pipeline.addLast("h1",new ChannelInboundHandlerAdapter(){
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                log.debug("h1");
                //切换到下一个read处理器。同上一篇
                super.channelRead(ctx,msg);
            }
        });
        pipeline.addLast("h2",new ChannelInboundHandlerAdapter(){
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                log.debug("h2");
                //可以不写了,因为下一个没有read的处理器了
                super.channelRead(ctx,msg);
                //随便写一个数据,帮助触发下面的出站处理器
                ch.writeAndFlush(ctx.alloc().buffer().writeBytes("server".getBytes()));
            }
        });

        //添加出站handler,只有当向channel里写入了数据才会触发,如果没写数据,此handler就形同虚设
        pipeline.addLast("h3",new ChannelOutboundHandlerAdapter(){
            @Override
            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                log.debug("h3");
                super.write(ctx, msg, promise);
            }
        });
        pipeline.addLast("h4",new ChannelOutboundHandlerAdapter(){
            @Override
            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                log.debug("h4");
                super.write(ctx, msg, promise);
            }
        });

    }

此时的pipeline中的handler为:head->h1->h2->h3->h4->tail

当客户端发送数据时结果为:

可以看到出站和入站有所不同,入站时从前往后依次执行,而出站处理器从后往前依次执行。

 还有一个注意点:writeAndFlush方法上面代码里的NioSocketChannel ch可以调用,ChannelHandlerContext ctx也可以调用,但是ctx调用时是从当前位置开始往前面找,后面的write事件会没有机会执行。

ChannelDuplexHandler结合了出站和入站
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值