【Netty学习】channel,channelPipeline,channelhandlerContext源码级分析

channel,channelPipeline,channelhandlerContext是Netty中的核心组件,接下来我们将从源码的角度分析这三大组件是如何协调工作的,本文建立在对三者有一个基本的了解,一些基本知识就不再赘述

前置知识:到底什么是出战和入站

  • 服务器端和客户端都有一个装载ChannelHandler链的ChannelPipeline的容器,所以出战和入站我们分为两个角度
  • 当数据从socket向pipeline流动时,这个动作称之为入站,当数据从pipeline流向socket时,这个动作称为出站。
  • 如果以客户端主动访问服务端为例,则这个过程分别触发:客户端出站 --> 服务端入站 --> 服务端出站 --> 客户端入站;而如果以服务端主动访问客户端为例,则这个过程分别触发:服务端出站 --> 客户端入站 --> 客户端出站 --> 服务端入站

宏观掌控三者的关系

  • 每当ServerSocket创建一个新的连接,就会创建一个Socket,对应着目标客户端
  • 每一个新建的Socket都会分配一个全新的ChannelPipeline
  • 每一个ChannelPipeline内部都含有多个ChannelHandlerContext
  • ChannelHandlerContext是ChannelHander的包装实现,ChannelHander是ChannelHandlerContext的一个成员变量,context底层是一个双向链表
    在这里插入图片描述

深入理解三者关系

首先我们来看看ChannelPipeline的继承关系
在这里插入图片描述可以看到ChannelPipeline可以看到他可以调用数据出战和入站的方法,同时也能遍历内部的链表,下面我们看看一些基本的方法

从接口罗列的方法中也能看出基本上都是对handler的增删改查操作,同时也可以获取到与此pipeline绑定的channel对象,在接口文档中提供了一个入站和出战handler的执行流程,接下来我们对此展开详细的介绍

从源码分析链表中handler的执行流程

我们先来搞清楚handler的执行顺序,为了清晰展示,这里结合一个简单的代码,简单来说就是添加了三个入站处理器以及三个出战处理器,并且在第三个handler处调用write方法触发出战处理器

public class TestContext {
    public static void main(String[] args) {
        new ServerBootstrap()
        .group(new NioEventLoopGroup())
        .channel(NioServerSocketChannel.class)
        .childHandler(new ChannelInitializer<NioSocketChannel>() {
            @Override
            protected void initChannel(NioSocketChannel ch) throws Exception {
                ChannelPipeline pipeline = ch.pipeline();
                pipeline.addLast(new ChannelInboundHandlerAdapter(){
                    @Override
                    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                        System.out.println(1);
                        ctx.fireChannelRead(msg);//调用下一个入站handler的channelRead()方法												@1
                    }
                });
                pipeline.addLast(new ChannelInboundHandlerAdapter(){
                    @Override
                    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                        System.out.println(2);
                        ctx.fireChannelRead(msg);//调用下一个入站handler的channelRead()方法												@2
                    }
                });
                pipeline.addLast(new ChannelInboundHandlerAdapter(){
                    @Override
                    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                        System.out.println(3);
                        ctx.fireChannelRead(msg);//@3
                        ctx.channel().write(msg);//触发出战处理器
                    }
                });
                pipeline.addLast(new ChannelOutboundHandlerAdapter() {
                    @Override
                    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                        System.out.println(4);@4
                        ctx.writeAndFlush(msg,promise);
                    }
                });
                pipeline.addLast(new ChannelOutboundHandlerAdapter() {
                    @Override
                    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                        System.out.println(5);@5
                        ctx.writeAndFlush(msg,promise);
                    }
                });
                pipeline.addLast(new ChannelOutboundHandlerAdapter() {
                    @Override
                    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
                        System.out.println(6);@6
                        ctx.writeAndFlush(msg,promise);
                    }
                });
                
            }
        })
        .bind(8080);
    }
}

可以编写客户端程序或者浏览器访问对应端口触发服务端代码,在此就不展示了,可以看到输出的结果是:

1
2
3
6
5
4

可以看到,ChannelInboundHandlerAdapter是按照addLast的顺序执行,而ChannelOutboundAdapter是按照addLast的逆序执行的,ChannelPipeline的底层链表如图所示
在这里插入图片描述

  • 入站处理器中,ctx.fireChannelRead(msg)是调用下一个入站处理器,稍后会从源码分析
  • 其中注意ctx.channel和ctx都可以调用write方法,都会触发出战处理器,但有所区别,前者会从尾部开始触发后序出战处理器的执行,而后者是从当前节点找上一个出战处理器
    • @3处的代码如果改成ctx.write()则只会打印123,因为第三个handler前没有其他的出战处理器了
    • @6处的代码如果改成ctx.channel.write()则会打印123666…因为ctx.channel.write()会从tail开始找下一个出战处理器,下一个又是6号处理器,无线循环

那么channelPipeline是如何分辨出战入站处理器,又是如何调用到下一个处理器的呢?

这时就要我们的channelHandlerContext出场了

channelHandlerContext使得channelHandler能够和它的channelPipeline以及其他的channelHandler交互,ChannelHandler可以通知其所属的channelPipeline中的下一个ChannelHandler,甚至可以动态修改它所欲的ChannelPipeline
                                     –  《netty实战》

从源码角度来看其实就是channelHandlerContext的一系列以fire开头的方法,以fireChannelread()方法为例,此方法会调用处理器链中下一个入站处理器的ChannelRead()方法,其他的方法以此类推
在这里插入图片描述
下面就以fireChannelread()方法看看底层是如何实现的

 @Override
    ChannelHandlerContext fireChannelRead(Object msg);
// 具体类中的方法实现

@Override
    public ChannelHandlerContext fireChannelRead(final Object msg) {
        invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg);
        return this;
    }
// 继续点findContextInbound方法
private AbstractChannelHandlerContext findContextInbound(int mask) {
        AbstractChannelHandlerContext ctx = this;
        do {
            ctx = ctx.next;
        } while ((ctx.executionMask & mask) == 0);
        return ctx;
    }

我们来看看findContextInbound()方法,mask即传入的参数MASK_CHANNEL_READ,在while循环中我们还可以看到一个变量executionMask,该变量在创建此Handler时(创建handler底层会同步创建一个context对象)就被赋予了初值,具体的值有
在这里插入图片描述这里可以看到可以把标记分为两类,一类是出战处理器的标记,一类是入站处理器的标记,低8位是入站处理器,高8位是出战处理器,所以ctx.executionMask & mask!=0时,即表明遍历到了下一个入站处理器,就退出循环,返回该处理器

所以寻找出战处理器的逻辑相同,注意ctx.write()和ctx.channel.write()的区别 ,前者会从尾部开始触发后序出战处理器的执行,而后者是从当前节点找上一个出战处理器

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值