Netty4.x源码分析:Netty常见组件(Channe、Unsafe、Pipeline)

当我们看Netty源码分析的时候,经常会看到几个常见组件比如Channel、Pipeline、Unsafe、EventLoopGroup、EventLoop。每个组件都是必不可少,而且这种设计对网络通信和逻辑处理的解耦起到非常重要的作用,比如我们在应用层不用关系底层网络通信的实现逻辑,只需编写ChannelHandler针对不同的请求事件处理即可。当我们想把BIO切换到NIO只需要做小小的变动,即可完成切换。所以我们今天主要来分析下三个比较关键的组件Channel、Unsafe、Pipeline。它们的作用如下:

Channel:用于执行网络I/O操作【BIO、NIO等等】,它是一个顶层的接口,针对不同的I/O类型它有不同的实现类,下面做一个简单的展示
Netty4.x源码分析:Netty常见组件(Channe、Unsafe、Pipeline)_第1张图片
列举一些常见的Channel类型:

  • NioSocketChannel:代表异步的客户端 TCP Socket 连接
  • NioServerSocketChannel:异步的服务器端 TCP Socket 连接
  • NioDatagramChannel:异步的 UDP 连接
  • NioSctpChannel:异步的客户端 Sctp 连接
  • NioSctpServerChannel:异步的 Sctp 服务器端连接
  • OioSocketChannel:同步的客户端 TCP Socket 连接(也就是BIO)
  • OioServerSocketChannel:同步的服务器端 TCP Socket 连接
  • OioDatagramChannel:同步的 UDP 连接
  • OioSctpChannel:同步的 Sctp 服务器端连接
  • OioSctpServerChannel:同步的客户端 TCP Socket 连接

Unsafe:这个不是我们常说的Java自带的sun.misc.Unsafe;我觉得因为它是不能被用户代码调用,所以取名叫做Unsafe;它的作用主要是用于数据传输操作,比如发送消息、把Channel注册到EventLoop等等数据操作。Unsafe主要跟自己专属的Channel一起使用

下面是它相关的API,我们从中也可以简单的了解它的作用

interface Unsafe {
        RecvByteBufAllocator.Handle recvBufAllocHandle();
        SocketAddress localAddress();
        SocketAddress remoteAddress(); 
        void register(EventLoop eventLoop, ChannelPromise promise);
        void bind(SocketAddress localAddress, ChannelPromise promise);
        void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise);
        void disconnect(ChannelPromise promise);
        void close(ChannelPromise promise);
        void closeForcibly();
        void deregister(ChannelPromise promise);
        void beginRead();
        void write(Object msg, ChannelPromise promise);     
        void flush();
        ChannelPromise voidPromise();  
        ChannelOutboundBuffer outboundBuffer();
    }

下面列举一下它的常用的实现类:
Netty4.x源码分析:Netty常见组件(Channe、Unsafe、Pipeline)_第2张图片

从上面我们很容易了解到,针对不同的I/O类型它都有一种不同的实现,下面对几个常见的Unsafe做一个介绍

  • NioMessageUnsafe:主要处理NIO服务端的一些I/O操作
  • NioSocketChannelUnsafe:主要处理NIO客户端的一些I/O操作
  • DefaultOioUnsafe:主要处理传统BIO的一些I/O操作

Pipeline:Pipeline中由ChannelHandler组成的一条双向链表,创建一个Pipeline默认有一个头(HeadContext)、尾(TailContext)节点。ChannelHandler它是Channel中的逻辑处理,我们使用比较多的就是ChannelHandler这一块。

源码分析

我们从上面分析可知Unsafe、Pipeline都是跟Channel有关联的,那么我们就从Channel为出发点,而且从Channel的类图我们可以看到,AbstractChannel是最顶层的父类,那么我们就以AbstractChannel的源码开始分析。

AbstractChannel的API如下:
Netty4.x源码分析:Netty常见组件(Channe、Unsafe、Pipeline)_第3张图片
我们主要从构造器->bind()->connect()简单浏览下源码

Channel构造器

首先我们来看AbstractChannel的构造器,它有2个构造器,不过都大同小异,一个需要子类传入ChannelId,一个是调用newId()自动生成


    protected AbstractChannel(Channel parent) {
        this.parent = parent;
        id = newId();
        unsafe = newUnsafe();
        pipeline = newChannelPipeline();
    }


    protected AbstractChannel(Channel parent, ChannelId id) {
        this.parent = parent;
        this.id = id;
        unsafe = newUnsafe();
        pipeline = newChannelPipeline();
    }

这里主要由三个关键方法

  • newId():创建一个ChannelId
  • newUnsafe():构建Unsafe对象,这个需要调用子类的newUnsafe()方法生成实例,因为它不知道子类使用的是那种I/O类型
  • newChannelPipeline():创建一个Pipeline。双向链表组成
    protected ChannelId newId() {
        return DefaultChannelId.newInstance();
    }
    public static DefaultChannelId newInstance() {
        return new DefaultChannelId();
    }
    private DefaultChannelId() {
        data = new byte[MACHINE_ID.length + PROCESS_ID_LEN + SEQUENCE_LEN + TIMESTAMP_LEN + RANDOM_LEN];
        int i = 0;

        // machineId
        System.arraycopy(MACHINE_ID, 0, data, i, MACHINE_ID.length);
        i += MACHINE_ID.length;

        // processId
        i = writeInt(i, PROCESS_ID);

        // sequence
        i = writeInt(i, nextSequence.getAndIncrement());

        // timestamp (kind of)
        i = writeLong(i, Long.reverse(System.nanoTime()) ^ System.currentTimeMillis());

        // random
        int random = PlatformDependent.threadLocalRandom().nextInt();
        i = writeInt(i, random);
        assert i == data.length;

        hashCode = Arrays.hashCode(data);
    }

ChannelId主要由机器Id、进程Id、数字、时间戳、随机数组成

newUnsafe()它是一个模板方法,那么就以NIO的Server端的NioServceSocketChannel为例,那么调用的则是AbstractNioMessageChannel的newUnsafe()

    protected AbstractNioUnsafe newUnsafe() {
        return new NioMessageUnsafe();
    }

这里很简单,就创建了一个NioMessageUnsafe实例

newChannelPipeline()方法也很简单,就是创建了一个DefaultChannelPipeline实例,并且把Channel实例传给了DefaultChannelPipeline对象。

   protected DefaultChannelPipeline newChannelPipeline() {
        return new DefaultChannelPipeline(this);
    }

    protected DefaultChannelPipeline(Channel channel) {
        this.channel = ObjectUtil.checkNotNull(channel, "channel");
        succeededFuture = new SucceededChannelFuture(channel, null);
        voidPromise =  new VoidChannelPromise(channel, true);
		/**
		 * 创建了一个尾节点、头节点
		 */
        tail = new TailContext(this);
        head = new HeadContext(this);

		/**
		 * 尾节点和头节点互相关联,形成一个双向链表
		 */
        head.next = tail;
        tail.prev = head;
    }

我们来看一下 经常使用的DefaultChannelPipeline的addLast()方法

    public final ChannelPipeline addLast(ChannelHandler handler) {
        return addLast(null, handler);
    }
    @Override
    public final ChannelPipeline addLast(String name, ChannelHandler handler) {
        return addLast(null, name, handler);
    }
        @Override
    public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
        final AbstractChannelHandlerContext newCtx;
        synchronized (this) {
            checkMultiplicity(handler);
			
			/**
			 * 把ChannelHandler包装成一个AbstractChannelHandlerContext
			 */
            newCtx = newContext(group, filterName(name, handler), handler);
			
			/**
			 * 把上面生成的Context加入链表中,即尾节点的前面
			 */
            addLast0(newCtx);
			
			//下面省略一些其他代码,有兴趣的,可以去看下我前面讲的Netty4.x源码分析:服务端绑定端口有讲
        return this;
    }
    
    private void addLast0(AbstractChannelHandlerContext newCtx) {
        AbstractChannelHandlerContext prev = tail.prev;
        newCtx.prev = prev;
        newCtx.next = tail;
        prev.next = newCtx;
        tail.prev = newCtx;
    }

AbstractChannel的构造方法,主要做了3件事情

  • 通过newId()生成一个ChannelId
  • 通过调用newUnsafe()模板方法,根据子类的不同,生成不一样的Unsafe实例。
  • 调用newChannelPipeline()生成一个双向链表的Pipeline,默认有一个头结点(HeadContext)、尾节点(TailContext);当我们调用addLast(),把ChannelHandler加到Pipeline,会把ChannelHandler包装成一个AbstractChannelHandlerContext节点,存到尾节点的前面。

这里我们也可以发现每个Channel都有对应的Unsafe实例、Pipeline实例。

Channel的bind()

我们还是以AbstractChannel的bind方法为入口

    public ChannelFuture bind(SocketAddress localAddress) {
        return pipeline.bind(localAddress);
    }

这里调用了DefaultChannelPipeline的bind()

DefaultChannelPipeline
    public final ChannelFuture bind(SocketAddress localAddress) {
        return tail.bind(localAddress);
    }
    
TailContext

    public ChannelFuture bind(SocketAddress localAddress) {
        return bind(localAddress, newPromise());
    }
    
    public ChannelFuture bind(final SocketAddress localAddress, final ChannelPromise promise) {
        if (localAddress == null) {
            throw new NullPointerException("localAddress");
        }
        if (isNotValidPromise(promise, false)) {
            // cancelled
            return promise;
        }
		/**
		 * 从当前节点开始,找到Pipeline中前一个outbound节点
		 */
        final AbstractChannelHandlerContext next = findContextOutbound();
        EventExecutor executor = next.executor();
        /**
         * 如果next节点是在当前线程执行的,那么直接同步执行next节点的bind方法
         */
        if (executor.inEventLoop()) {
            next.invokeBind(localAddress, promise);
        } else {
        /**
         * 如果next节点指定了EventExecutor,那么就在指定的EventExecutor里异步调用next节点的bind方法
         */
            safeExecute(executor, new Runnable() {
                @Override
                public void run() {
                    next.invokeBind(localAddress, promise);
                }
            }, promise, null);
        }
        return promise;
    }

从上面我们知道,从尾节点TailContext开始往前找属于outbound的节点,然后调用该节点的bind方法。这时,我们先设想Pipeline中只有头结点HeadContext和尾节点TailContext,那么这时候就调用了HeadContext的bind方法。【这里解释一下HeadContext即是inbound又是outbound,可以去看下HeadContext的类结构就清楚了】

HeadContext
public void bind(
          ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)
          throws Exception {
      /**
       * 这里就是调用了Unsafe的bind方法,上面我们说到,在创建Channel的时候,针对不同的I/O类型Channel都会创建一个特定的Unsafe实例。
       * 那么我们还是以NIO为例,上面我们介绍过,创建NioServerSocketChannel的时候会创建NioMessageUnsafe实例。那么这里就是调用
       * NioMessageUnsafe的bind方法,但是NioMessageUnsafe没有实现这个bind方法,所以就调用父类的AbstractUnsafe的bind方法
       */
      unsafe.bind(localAddress, promise);
  }
AbstractUnsafe
        public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
				// 省略前面的
				/**
				 * 我们只关注这个doBind方法,实现方法在Channel中。doBind它是一个模板方法,实现类在它的子类。
				 * 因为我们是NioServerSocketChannel,所以实现方法在NioServerSocketChannel类中,为什么要这样实现呢?其实很简单,
				 * 不同的I/O类型的绑定都是不一样的,而Channel是对应I/O操作相关的。所以自然而然真正的绑定端口肯定在对应的子类Channel中实
				 * 现
				 */ 
                doBind(localAddress);
                // 省略后面的
        }
        
NioServerSocketChannel
    protected void doBind(SocketAddress localAddress) throws Exception {
        if (PlatformDependent.javaVersion() >= 7) {
            javaChannel().bind(localAddress, config.getBacklog());
        } else {
        	/**
        	 * 这里就很简单了,调用了我们熟悉的JDK的API
        	 */
            javaChannel().socket().bind(localAddress, config.getBacklog());
        }
    }

Channel的connect就不讲了,跟bind逻辑差不多,可以自己试着去分析下。

结论

1、Netty底层把I/O抽象成了Channel,对应不同I/O模型有对应的Channel实例与之对应。比如NIO的服务端就对应NioServerSocketChannel等等。这就当我们想改变I/O模型,只需要改变对应的Channel即可,非常之方便,并不需要关心底层的API。
2、创建Channel的时候会生成2个非常重要的组件Unsafe、Pipeline。Unsafe针对不同I/O模型也有对应的实现。而Pipeline不管使用哪种I/O模型都是一样的,都会创建一条DefaultChannelPipeline,它是一个双向链表,有一个头结点HeadContext、尾节点TailContext组成。
3、在Pipeline中执行业务逻辑的时候,是在单线程中执行的,所以是线程安全的。当我们在分析bind方法的时候,当取出下一个outbound的next节点时,首先判断它是否有指定的EventExecutor,如果没有,那么就同步执行,如果有,那么就异步在指定的EventExecutor内执行。这种设计减少了线程的上下文切换,并且是线程安全的,所以在Pipeline中不用考虑一些数据安全的问题,效率非常高。

你可能感兴趣的:(Netty4.x源码分析)