【Netty4核心原理⑥】【揭开Bootstrap的神秘面纱 - 客户端Bootstrap ❶】

一、前言

本系列虽说本意是作为 《Netty4 核心原理》一书的读书笔记,但在实际阅读记录过程中加入了大量个人阅读的理解和内容,因此对书中内容存在大量删改。

本篇涉及内容 :第七章 揭开Bootstrap的神秘面纱


本系列内容基于 Netty 4.1.73.Final 版本,如下:

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

系列文章目录:
【Netty4核心原理】【全系列文章目录】


二、Channel 简介

在 Netty 中, Channel 相当于一个 Socket 的抽象,他为用户提供了关于 Socket状态、读、写等操作,每当 Netty 建立了一个连接,都创建了一个与其对应的 Channel 实例。

除了传统 TCP,Netty 还支持多种协议,并且每种协议头提供了 NIO 和 BIO 模式。不同协议不同阻塞类型的连接都有不同的 Channel 类型与之对应,如下表:

类名解释
NioSocketChannel客户端 Tcp Socket 连接
NioServerSocketChannel服务端 Tcp Socket 连接
NioDatagramChannelUDP 连接
NioSctpChannel客户端 SCTP 连接
NioSctpServerChannel服务端 SCTP 连接

我们以下面的例子为例:

   public void connect() throws InterruptedException {
   	   // 1. 创建工作线程
       EventLoopGroup group = new NioEventLoopGroup();
       try {
           // 通过 netty 连接 服务提供者
           // Bootstrap 是 Netty 提供的一个便利的工厂类,可以通过他来完成客户端或服务端的 Netty 初始化。
           Bootstrap bootstrap = new Bootstrap();
           // 指定工作线程 group
           bootstrap.group(group)
           			// 2. NioSocketChannel 的创建 : 指定 Channel 类型,因为是客户端,所以使用NioSocketChannel, 如果是服务端则使用 NioServerSocketChannel
                   .channel(NioSocketChannel.class)
                   // 设置 TCP 参数
                   .option(ChannelOption.SO_KEEPALIVE, true)
                   // 3.设置处理数据的 Handler
                   .handler(new ChannelInitializer<Channel>() {
                       @Override
                       protected void initChannel(Channel channel) throws Exception {
                           ...
                       }
                   });
           // 4. 连接提供者服务
           ChannelFuture future = bootstrap.connect(new InetSocketAddress("127.0.0.1", 8090)).sync();
           // 阻塞主线程,防止直接执行 finally 中语句导致服务关闭,当有关闭事件到来时才会放行
           future.channel().closeFuture().sync();
       } finally {
           group.shutdownGracefully();
       }
   }

根据上面的代码注释顺序,我们将其分为四个部分,如下:

  1. 创建工作线程 :这里直接创建了一个 NioEventLoopGroup 对象。顾名思义 NioEventLoopGroup 是一组 NioEventLoop,每个 NioEventLoop 通常绑定一个本地线程,所以 NioEventLoopGroup 可以简单认为是一个Netty封装的线程池。而 NioEventLoopGroup 中的这些线程会不断地从 Selector(Java NIO 中的选择器)中获取网络事件,如读事件、写事件、连接事件等。

    与普通线程不同的是,NioEventLoop 内部有一个循环机制(通过 NioEventLoop#run 方法触发),它会不断地执行以下操作:

    • 首先,调用 Selector.select()(或者相关变体方法)来阻塞等待事件发生。一旦有事件就绪,例如有新的客户端连接请求或者已有连接上有数据可读 / 写,Selector 就会返回对应的 SelectionKey。
    • 然后,根据 SelectionKey 所表示的事件类型(如 OP_ACCEPT、OP_READ、OP_WRITE)来处理相应的事件。例如,对于 OP_ACCEPT 事件,会接受新的客户端连接并为其创建新的 NioSocketChannel;对于 OP_READ 事件,会从通道中读取数据并进行后续处理。
    • 在条件允许的情况下还会从 任务队列(每个 NioEventLoop 内部都存在一个任务队列 taskQueue,Netty 会将一些需要当前 NioEventLoop 执行的方法封装成一个个任务放入 NioEventLoop 的任务队列中)取出任务执行。

    NioEventLoopGroup 通过合理地分配这些 NioEventLoop 来处理多个通道的事件,从而实现了高效的并发处理。例如,在一个服务器应用中,可以同时处理多个客户端连接的读写请求,每个连接的事件都由 NioEventLoopGroup 中的一个合适的 NioEventLoop 来处理。

  2. 创建 NioSocketChannel :实际上这里只是传入了一个 Channel 类型,真正的逻辑创建是在 Bootstrap#connect 中通过反射创建。客户端的Channel 类型是 NioSocketChannel, 如果是服务端则使用 NioServerSocketChannel。

  3. Handler 添加过程 :这里是 Netty 的核心,可以供用户扩展实现自定义的 Handler来处理消息。

  4. 客户端发起连接请求 :这里会发起与服务器的连接。

下面来详细看看看具体每一步的内容。

二、创建工作线程

在上面的代码中,我们需要实例化一个 NioEventLoopGroup 对象,这个对象的作用上面已经做过介绍,这里不再赘述。

NioEventLoopGroup 的继承层次结构如下:
在这里插入图片描述
NioEventLoopGroup 有几个重载的构造函数,不过殊途同归,最终都会调用 MultithreadEventExecutorGroup#MultithreadEventExecutorGroup(int, Executor, java.lang.Object…) 方法。如下:

	// 这里注入入参中的几个参数
	// 1. nThreads  :线程池线程数量,可以通过 NioEventLoopGroup 的重载构造函数传入。需要注意 如果线程数量传入 0 会被初始化为 Max(1,CPU 核心数 * 2),在 MultithreadEventLoopGroup 的构造函数中有该逻辑
	// 2. executor :线程池类型,可以通过 NioEventLoopGroup 的重载构造函数传入。默认传入空,会在 MultithreadEventExecutorGroup 的重载构造函数中被初始化为 ThreadPerTaskExecutor
	// 3. args :构造 NioEventLoopGroup  需要的参数,在不断重载的构造函数中会默认传入,在 NioEventLoopGroup#newChild 中会获取各个参数并使用
 	protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                            EventExecutorChooserFactory chooserFactory, Object... args) {
        // 校验 nThreads 参数是否为正数,如果不是则抛出异常
        checkPositive(nThreads, "nThreads");
		// 1. 初始化 ThreadPerTaskExecutor : 如果传入的 executor 为 null,则创建一个 ThreadPerTaskExecutor,它会为每个任务创建一个新线程,线程工厂使用 newDefaultThreadFactory() 创建
        if (executor == null) {
            executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
        }
		// 初始化线程数组
        children = new EventExecutor[nThreads];

        for (int i = 0; i < nThreads; i ++) {
            boolean success = false;
            try {
            	// 2. 创建线程,这里调用的是 NioEventLoopGroup#newChild
                children[i] = newChild(executor, args);
                success = true;
            } catch (Exception e) {
                // TODO: Think about if this is a good exception type
                throw new IllegalStateException("failed to create a child event loop", e);
            } finally {
            	// 如果创建失败,则优雅停机
                if (!success) {
                    for (int j = 0; j < i; j ++) {
                        children[j].shutdownGracefully();
                    }

                    for (int j = 0; j < i; j ++) {
                        EventExecutor e = children[j];
                        try {
                            while (!e.isTerminated()) {
                                e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
                            }
                        } catch (InterruptedException interrupted) {
                            // Let the caller handle the interruption.
                            Thread.currentThread().interrupt();
                            break;
                        }
                    }
                }
            }
        }
		// 3. 生成 EventExecutorChooser 
        chooser = chooserFactory.newChooser(children);
			
		// 创建一个监听器,当 children 数组中的所有线程都完成任务时会调用  terminationFuture.setSuccess(null);
		// 这里的 terminationFuture 实际类型是 DefaultPromise 
        final FutureListener<Object> terminationListener = new FutureListener<Object>() {
            @Override
            public void operationComplete(Future<Object> future) throws Exception {
            	// 当一个子线程完成任务时会回调该方法,而当 terminatedChildren.incrementAndGet() == children.length 时即代表所有的子线程任务都完成
                if (terminatedChildren.incrementAndGet() == children.length) {
                	// 这里调用 DefaultPromise#setSuccess,该方法会判断如果入参时null,则会复制为 SUCCESS。
                	
                	/******摘抄的 DefaultPromise#setSuccess0代码如下:*****/
                	//	private boolean setSuccess0(V result) {
					//	       return setValue0(result == null ? SUCCESS : result);
    				//	}
    				/***************************************************/
    				
                    terminationFuture.setSuccess(null);
                }
            }
        };

		// 跟每个子线程添加该监听器
        for (EventExecutor e: children) {
            e.terminationFuture().addListener(terminationListener);
        }
		
		// 创建一个只读的 readonlyChildren 
        Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
        Collections.addAll(childrenSet, children);
        readonlyChildren = Collections.unmodifiableSet(childrenSet);
    }

具体逻辑在注释上已经写明,综上,这里的 MultithreadEventExecutorGroup 中的处理逻辑可以简单总结如下:

  1. 创建一个大小为 nThreads 的 SingleThreadEventExecutor 数组
  2. 调用 MultithreadEventExecutorGroup#newChild 方法初始化 children 数据。
  3. 根据 nThreads 的大小,通过 EventExecutorChooserFactory#newChooser 方法创建不同的 Chooser:如果 nThread 是 2 的平方,则使用 PowerOfTwoEventExecutorChooser ,否则使用 GenericEventExecutorChooser 。他们的功能都是从 children 数组中选出一个合适的 EventExecutor 实例。

我们按照注释顺序来看下面三部分:

1. 初始化 ThreadPerTaskExecutor

ThreadPerTaskExecutor 是对 ThreadFactory 的封装, ThreadFactory 是用来创建线程的工厂类。

		// 1. 初始化 ThreadPerTaskExecutor : 如果传入的 executor 为 null,则创建一个 ThreadPerTaskExecutor,它会为每个任务创建一个新线程,线程工厂使用 newDefaultThreadFactory() 创建,返回的是 DefaultThreadFactory 类型。
      if (executor == null) {
          executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
      }

这里涉及到 DefaultThreadFactory 和 ThreadPerTaskExecutor 两个类,下面我们来看:

1.1 DefaultThreadFactory

newDefaultThreadFactory() 调用的是 MultithreadEventExecutorGroup#newDefaultThreadFactory,逻辑比较简单,直接创建了一个 DefaultThreadFactory 对象返回,具体实现如下:

    protected ThreadFactory newDefaultThreadFactory() {
        return new DefaultThreadFactory(getClass());
    }

DefaultThreadFactory 类就是一个线程工厂类, NioEventLoopGroup 通过调用 DefaultThreadFactory#newThread 方法来创建线程并保存到线程池中(在 ThreadPerTaskExecutor 中实现,下面会讲)。DefaultThreadFactory#newThread 实现如下:

    @Override
    public Thread newThread(Runnable r) {
    	// 通过 FastThreadLocalRunnable 包装 runnable 
        Thread t = newThread(FastThreadLocalRunnable.wrap(r), prefix + nextId.incrementAndGet());
        try {
        	// 设置是否守护线程
            if (t.isDaemon() != daemon) {
                t.setDaemon(daemon);
            }
			// 设置优先级
            if (t.getPriority() != priority) {
                t.setPriority(priority);
            }
        } catch (Exception ignored) {
            // Doesn't matter even if failed to set.
        }
       
        return t;
    }
   	// 将 Thread 包装成 FastThreadLocalThread
    protected Thread newThread(Runnable r, String name) {
        return new FastThreadLocalThread(threadGroup, r, name);
    }

这里可以看到,DefaultThreadFactory#newThread 会将普通线程包装成 FastThreadLocalThread 类型,Runnable 包装成 FastThreadLocalRunnable 这一切都是为了更好的使用 FastThreadLocal 特性。

FastThreadLocal 是 Netty 提供的一种比 Java 标准 ThreadLocal 更快的线程局部变量实现,这里的包装操作是为了在新线程执行任务时能更好地利用 FastThreadLocal 的特性。

1.2 ThreadPerTaskExecutor

ThreadPerTaskExecutor 类的实现如下,其核心功能是为每个提交的任务创建一个新的线程来执行。可以看到交由 ThreadPerTaskExecutor 执行的事件都会被 ThreadFactory 的线程来处理。

public final class ThreadPerTaskExecutor implements Executor {
    private final ThreadFactory threadFactory;

    public ThreadPerTaskExecutor(ThreadFactory threadFactory) {
        this.threadFactory = ObjectUtil.checkNotNull(threadFactory, "threadFactory");
    }

    @Override
    public void execute(Runnable command) {
        threadFactory.newThread(command).start();
    }
}

综上这里 ThreadPerTaskExecutor#execute 会产生一个新的 FastThreadLocalThread 线程来执行 被包装成 FastThreadLocalRunnable 类型的任务。

上面我们介绍了这里 ThreadFactory 类型是 DefaultThreadFactory,所以这里的 threadFactory.newThread(command) 产生的线程是 FastThreadLocalThread类型,并且 Runnable 也会被封装成 FastThreadLocalRunnable 。


2. NioEventLoopGroup#newChild

上面提到,通过如下代码,会完成 children 数组的初始化。

 	// 2. 创建线程,这里调用的是 NioEventLoopGroup#newChild
     children[i] = newChild(executor, args);

newChild(executor, args) 实际调用的是 NioEventLoopGroup#newChild,该方法的目的就是根据入参实例化 EventLoop 对象,具体实现如下:

	// newChild 的作用就是根据入参实例化 EventLoop  对象。
   @Override
   protected EventLoop newChild(Executor executor, Object... args) throws Exception {
       SelectorProvider selectorProvider = (SelectorProvider) args[0];
       SelectStrategyFactory selectStrategyFactory = (SelectStrategyFactory) args[1];
       RejectedExecutionHandler rejectedExecutionHandler = (RejectedExecutionHandler) args[2];
       EventLoopTaskQueueFactory taskQueueFactory = null;
       EventLoopTaskQueueFactory tailTaskQueueFactory = null;

       int argsLength = args.length;
       if (argsLength > 3) {
           taskQueueFactory = (EventLoopTaskQueueFactory) args[3];
       }
       if (argsLength > 4) {
           tailTaskQueueFactory = (EventLoopTaskQueueFactory) args[4];
       }
       // 根据参数创建一个 NioEventLoop
       return new NioEventLoop(this, executor, selectorProvider,
               selectStrategyFactory.newSelectStrategy(),
               rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory);
   }

NioEventLoop 是 Netty 中用于处理 I/O 操作和任务执行的关键组件。它基于 Java 的 NIO(Non - Blocking I/O)实现高效的事件驱动编程模型。在 Netty 的架构中,它属于 Reactor 线程模型的一部分,主要负责处理通道(Channel)上的事件。

从线程的角度看,NioEventLoop实际上是一个单线程的执行器(Executor),它会在一个循环中不断地获取和处理事件。这个循环被称为事件循环(Event Loop),这也是NioEventLoop名字的由来。

3. EventExecutorChooserFactory#newChooser

EventExecutorChooserFactory#newChooser 的其逻辑比较简单:如果 nThread 是 2 的平方,则使用 PowerOfTwoEventExecutorChooser ,否则使用 GenericEventExecutorChooser ,这两个 Chooser 都重写了next 方法,而 next 方法的主要功能是将数组索引循环位移(即当数组索引指向数组最后一个元素时时,再调用 next 方法则索引会重新指向数组第一个元素)。

EventExecutorChooser 是 Netty 中的一个重要组件,主要用于在多线程事件执行器组(如 MultithreadEventExecutorGroup)中选择合适的 EventExecutor 来执行任务
在 Netty 的多线程模型中,通常会有多个 EventExecutor 组成一个事件执行器组,当有新的任务到来时,需要从这些 EventExecutor 中选择一个来执行该任务。EventExecutorChooser 就是负责完成这个选择过程的组件,它能够根据一定的策略将任务均匀地分配到各个 EventExecutor 上,从而实现任务的负载均衡,提高系统的并发处理能力和性能。

其代码具体实现如下:

    @Override
    public EventExecutorChooser newChooser(EventExecutor[] executors) {
    	// 判断是否是executors 的长度是否是 2 的平方, 根据是否生成不同的 Chooser
        if (isPowerOfTwo(executors.length)) {
            return new PowerOfTwoEventExecutorChooser(executors);
        } else {
            return new GenericEventExecutorChooser(executors);
        }
    }

    private static boolean isPowerOfTwo(int val) {
        return (val & -val) == val;
    }

    private static final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser {
        private final AtomicInteger idx = new AtomicInteger();
        private final EventExecutor[] executors;

        PowerOfTwoEventExecutorChooser(EventExecutor[] executors) {
            this.executors = executors;
        }

        @Override
        public EventExecutor next() {
            return executors[idx.getAndIncrement() & executors.length - 1];
        }
    }

    private static final class GenericEventExecutorChooser implements EventExecutorChooser {
        private final AtomicLong idx = new AtomicLong();
        private final EventExecutor[] executors;

        GenericEventExecutorChooser(EventExecutor[] executors) {
            this.executors = executors;
        }

        @Override
        public EventExecutor next() {
        	// & 比 % 运算效率要高。
            return executors[(int) Math.abs(idx.getAndIncrement() % executors.length)];
        }
    }

4. 总结

最后总结一下整个 NioEventLoopGroup 的初始化过程:

  1. NioEventLoopGroup 内部维护了一个 EventExecutor 类型的 children 数组,大小是 nThreads,这样就构成了一个线程池。
  2. 在实例化 NioEventLoopGroup 时,如果没指定线程池大小,则线程池大小默认为 CPU 核心数 * 2
  3. 在 NioEventLoopGroup 初始过程中会调用父类 MultithreadEventExecutorGroup 的构造函数,其内部会调用 MultithreadEventExecutorGroup#newChild 方法来初始化 children 数组。
  4. MultithreadEventExecutorGroup#newChild 是一个抽象方法,在 NioEventLoopGroup 中有具体实现,他会返回一个 NioEventLoop 实例。因此这里会为 children 数组 的每个元素创建一个 NioEventLoop 对象。
  5. 在NioEventLoopGroup#newChild 方法中创建 NioEventLoop 对象时,NioEventLoop 在构造函数中会初始化一些属性,具体赋值如下:
    • NioEventLoop#provider :在 NioEventLoop 构造函数可以传入的参数,如果没传,默认由SelectorProvider.provider() 生成
    • NioEventLoop#selector :在 NioEventLoop 构造函数中调用 NioEventLoop#openSelector 生成。
  6. 之后通过 EventExecutorChooserFactory#newChooser 创建一个 EventExecutorChooserFactory.EventExecutorChooser 对象,这个对象会在新任务到达时根据指定的策略从 children 数组 中挑选出一个 NioEventLoop 对象来执行任务。

综上 :这里就是为 NioEventLoopGroup 在创建时会按照指定的线程数量(没指定默认是 CPU 核心数 * 2)创建对应的 NIoEventLoop 对象保存在 NioEventLoopGroup#children 数组中。


三、创建 NioSocketChannel

在上面的例子中我们提到 AbstractBootstrap#channel 方法实际上这里只是传入了一个 Channel 类型,在这一步就是对NioSocketChannel进行封装,而真正的逻辑创建实际是在 Bootstrap#connect 中。
在这里插入图片描述
具体如下:

1. NioSocketChannel 的创建

当我们通过 AbstractBootstrap#channel 方法传入 NioSocketChannel.class 会执行如下逻辑:

    public B channel(Class<? extends C> channelClass) {
    	// 1. 创建一个 ReflectiveChannelFactory 对象:将 channelClass 作为构造函数如下
    	// 2. 将 ReflectiveChannelFactory 对象作为入参调用 channelFactory 方法
        return channelFactory(new ReflectiveChannelFactory<C>(
                ObjectUtil.checkNotNull(channelClass, "channelClass")
        ));
    }

可以看到这里实际上是生成了一个 ReflectiveChannelFactory 对象,也就是 Channel 工厂类,并保存到 Bootstrap#channelFactory 属性中。

ReflectiveChannelFactory 实际上就是 Channel 工厂类,用于创建指定的 Channel 类型。

ReflectiveChannelFactory 的构造函数如下,可以看到这里通过反射保存了 clazz(也就是传入的 NioSocketChannel.class)的默认构造函数(在后面会通过反射构造函数创建出 NioSocketChannel 实例 ) :

	// 这里的clazz 类型是 NioSocketChannel
    public ReflectiveChannelFactory(Class<? extends T> clazz) {
    	... 
    	// 获取 clazz 的默认构造函数
        this.constructor = clazz.getConstructor();
    }

而在 Bootstrap#connect方法中会根据下面的调用链路执行,最终调用到 ReflectiveChannelFactory#newChannel 方法。
在这里插入图片描述

ReflectiveChannelFactory#newChannel 的实现如下:

    @Override
    public T newChannel() {
    	...
        return constructor.newInstance();
    }

这里 Bootstrap 中的 ChannelFactory 实现类是 ReflectiveChannelFactory,而 ReflectiveChannelFactory#newChannel 的作用就是通过反射生成 Channel 实例,上面我们提到这里的 constructor 就是 AbstractBootstrap#channel 方法传入的类型的构造函数。也就是说这里生成就是 NioSocketChannel 类型实例。

因此这里我们可以得知这里的逻辑就是会通过反射创建一个 NioSocketChannel 实例(服务端则是创建 NioServerSocketChannel )。

2. NioSocketChannel 的初始化

上面我们介绍了通过调用 ReflectiveChannelFactory#newChannel 方法 可以反射创建NioSocketChannel,而 NioSocketChannel 在构造函数中还存在一些逻辑,下面我们具体来看。

由于 NioSocketChannel 是通过反射创建,所以会调用其默认构造函数,其构造函数具有多个重载,具体如下:


    private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider();
    
	...
	
    // 反射调用该方式生成 NioSocketChannel 实例
    public NioSocketChannel() {
    	// 构造入参默认传入一个 SelectorProvider 
        this(DEFAULT_SELECTOR_PROVIDER);
    }

	// 构造时会调用 newSocket来生成一个 SocketChannel实例
    public NioSocketChannel(SelectorProvider provider) {
    	// 1. 调用 NioSocketChannel#newSocket 方法来创建了一个SocketChannel 实例
        this(newSocket(provider));
    }

    public NioSocketChannel(SocketChannel socket) {
    	// 2. 调用父类AbstractNioByteChannel#AbstractNioByteChannel 的构造函数
        this(null, socket);
    }
	...

上面代码主要有两个部分:

  1. 调用 NioSocketChannel#newSocket 方法来创建了一个SocketChannel 实例
  2. 调用父类 AbstractNioByteChannel#AbstractNioByteChannel 的构造函数

下面我们具体来看这两部分内容

2.1 NioSocketChannel#newSocket

在上述构造函数中可以看到其调用了 NioSocketChannel#newSocket 方法来创建了一个SocketChannel 实例,如下:

	// 构造传入的 SocketChannel 实例
    private static SocketChannel newSocket(SelectorProvider provider) {
        try {
        	// 默认情况下 provider 是 NioSocketChannel#DEFAULT_SELECTOR_PROVIDER,
            return provider.openSocketChannel();
        } catch (IOException e) {
            throw new ChannelException("Failed to open a socket.", e);
        }
    }

这里看到是 直接委托给 provider.openSocketChannel() 来创建 SocketChannel,从上面的代码我们可以看到 默认情况下 provider 是 NioSocketChannel#DEFAULT_SELECTOR_PROVIDER,因此这里调用的是 SelectorProviderImpl#openSocketChannel。

SelectorProvider是一个抽象类,它是用于创建Selector、ServerSocketChannel、SocketChannel和DatagramChannel这些关键的 NIO 组件的工厂类。

简单来说,它是一个用于获取 NIO 通信基础设施对象的提供器。它定义了一组方法来创建这些对象,这些方法是抽象的,具体的实现由不同的平台提供。

SelectorProviderImpl#openSocketChannel 实现如下:可以看到这里是直接创建了一个 SocketChannelImpl实例返回。

    @Override
    public SocketChannel openSocketChannel() throws IOException {
        return new SocketChannelImpl(this);
    }

综上: NioSocketChannel#newSocket 会返回一个创建的 SocketChannelImpl 实例。

2.2 AbstractNioByteChannel#AbstractNioByteChannel

上面代码可以看到 NioSocketChannel 调用了父类的 AbstractNioByteChannel#AbstractNioByteChannel 的构造函数,如下:

	// 这里两个入参
	// 1. parent 传入时为 null, 
	// 2. ch 则为 通过 newSocket 创建出的 SocketChannel, 类型为 SocketChannelImpl
    protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) {
    	// 默认传入 SelectionKey.OP_READ,这里调用父类 AbstractNioChannel#AbstractNioChannel 的构造函数
        super(parent, ch, SelectionKey.OP_READ);
    }

这里注意父类构造函数多了个入参,客户端是默认传入的是 SelectionKey.OP_READ,而服务端传入的则是 SelectionKey.OP_ACCEPT

  • SelectionKey.OP_READ :表示通道已经准备好进行读取操作,即通道中有数据可供读取。当一个 Channel(如 SocketChannel)注册到 Selector 上并监听 OP_READ 事件时,Selector 会检测通道的可读状态。如果 SelectionKey 的 isReadable() 方法返回 true,则可以从通道中读取数据。一般用于从通道中读取数据,例如从客户端接收数据或者从服务器读取响应数据。
  • SelectionKey.OP_ACCEPT :表示服务器套接字通道(ServerSocketChannel)准备好接受新的连接。当一个 ServerSocketChannel 注册到 Selector 上并监听 OP_ACCEPT 事件时,Selector 会检测到新的客户端连接请求。如果 SelectionKey 的 isAcceptable() 方法返回 true,则说明有新的连接可以被接受。一般用于 在服务器端编程中,用于处理客户端的连接请求。

对于客户端,他需要的是监听与服务器的连接上是否有服务端发送的消息,所以监听 SelectionKey.OP_READ 事件,而对于服务端,他首先需要的是监听哪些客户端要与自身建立连接,因此监听 SelectionKey.OP_ACCEPT 事件


AbstractNioChannel#AbstractNioChannel 的构造函数 如下:

// 这里三个入参
// 1. parent 传入时为 null, 
// 2. ch 则为 通过newSocket 创建出的 SocketChannel, 类型为 SocketChannelImpl
// 3. readInterestOp 在客户端时传入 `SelectionKey.OP_READ`,而服务端传入的则是 `SelectionKey.OP_ACCEPT`
 protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
 	   // 调用 AbstractChannel#AbstractChannel(io.netty.channel.Channel) 函数
       super(parent);
       // 保存 ch 和 readInterestOp
       this.ch = ch;
       this.readInterestOp = readInterestOp;
       try {
       	// 设置非阻塞模式
           ch.configureBlocking(false);
       } catch (IOException e) {
           try {
               ch.close();
           } catch (IOException e2) {
           		... 
           }

           throw new ChannelException("Failed to enter non-blocking mode.", e);
       }
   }

上面可以看到了调用了重载构造函数 AbstractChannel#AbstractChannel(io.netty.channel.Channel) ,其实现如下:

    protected AbstractChannel(Channel parent) {
        this.parent = parent;
        id = newId();
        // 1. Unsafe 属性的初始化
        unsafe = newUnsafe();
        // 2. ChannelPipeline 的初始化
        pipeline = newChannelPipeline();
    }

上面的方法中主要有两个方面:

  1. unsafe = newUnsafe(); :Unsafe 属性的初始化
  2. pipeline = newChannelPipeline(); :ChannelPipeline 的初始化

下面我们具体来看这两点

2.2.1 Unsafe 属性的初始化

在实例化 NioSocketChannel 的过程中 Unsafe 非常关键,他是对 Java 底层 Socket 操作的封装,因此可以说它是沟通Netty 上层和 Java 底层的重要桥梁。

在上面的中 AbstractChannel#AbstractChannel(io.netty.channel.Channel) 的构造函数中会调用 AbstractChannel#newUnsafe 来构造一个 unsafe 对象,而 AbstractChannel#newUnsafe 方法在 NioSocketChannel 中被重写了,其代码如下:

    @Override
    protected AbstractNioUnsafe newUnsafe() {
        return new NioSocketChannelUnsafe();
    }

NioSocketChannel#newUnsafe 方法会返回一个 NioSocketChannelUnsafe 实例,因此我们可以确定在实例化 NioSocketChannel 中的 Unsafe 属性其实是一个 NioSocketChannelUnsafe 实例。

2.2.2 ChannelPipeline 的初始化

这里我们看到,在实例化一个 Channel 时,必然要实例化一个 ChannelPipeline。而这里的 AbstractChannel#newChannelPipeline 实际上构建的是一个 DefaultChannelPipeline 实例。

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

DefaultChannelPipeline 的构造函数如下

	// 这里传入的 Channel 就是上述初始化的 Channel
    protected DefaultChannelPipeline(Channel channel) {
    	// 将 channel 保存到自身属性的 this.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;
    }

这里需要注意:

  1. 这里传入的 Channel 就是上述初始化的 Channel,客户端是 NioSocketChannel,服务端是 NioServerSocketChannel。
  2. 在 DefaultChannelPipeline 中维护了一个以 AbstractChannelHandlerContext 为节点元素的双向链表,而 head 和 tail 分别指向双向链表的头尾节点,这个双向链表是实现 Netty 的 Pipeline 机制的关键,简单的说即:当有事件发生时会遍历这个双向链表上的所有 ChannelHandler,以寻找能够过处理该事件的 ChannelHandler 并执行逻辑。(由于篇幅所限,这部分内容我们在 【Netty4核心原理⑪】【Netty 大动脉 Pipeline】 中会详细介绍。)

2.3 总结

至此 NioSocketChannel 就完成了初始化,总结如下:

  1. 在 NioSocketChannel 构造函数中。
  2. 调用 NioSocketChannel#newSocket 打开一个新的 Java NioSocketChannel
  3. 初始化 AbstractChannel 对象并给属性赋值,如下:
    • id : 每个 Channel 都会被分配一个唯一id
    • parent :属性值默认为空
    • unsafe : 通过调用 newUnsafe 方法实例化一个 Unsafe 对象,他的类型是 AbstractNioByteChannel.NioByteUnsafe。
    • pipeline :通过 new DefaultChannelPipeline(this) 新创建的实例。
  4. AbstractNioChannel 中被赋值的属性如下:
    • ch :被赋值为 Java 原生 SocketChannel,即 NioSocketChannel 的 newSocket() 方法返回的 Java NIO SocketChannel。
    • readInterestOp :被赋值为 SelectionKey.OP_READ。
    • ch :被配置为非阻塞。
  5. NioSocketChannel 中被赋值的属性: config = new NioSocketChannelConfig(this, socket.socket());

3. NioSocketChannel 的注册

上面我们介绍了 NioSocketChannel 的创建和初始化过程。实际上,NioSocketChannel 初始化完成后还会注册到 NioEventLoop 的 Selector 中,这样才能监听到指定的事件,注册的过程发生在 AbstractBootstrap#initAndRegister 方法中,这个我们在下文【客户端发起连接请求】部分再进行分析。

三、Handler 添加过程

Netty 有一个强大和灵活之处就是基于 Pipeline 的自定义 Handler 机制。基于此,开发者可以像添加插件一样自由组合各种各样的 Handler 来完成业务逻辑。例如如果需要处理 Http 数据,只需要在 Pipeline 前添加一个针对 Http 编解码的 Handler ,然后添加我们自己的业务逻辑 Handler ,这样数据流就像通过一个管道一样,从不同的 Handler 中流过并进行编解码,最终到达我们自定义的 Handler 中。


以下面代码为例,这段代码就实现了 Handler 的添加功能,Bootstrap#handler 方法接收一个 ChannelHandler ,而我们传入的参数是一个派生于抽象类 ChannelInitializer 的匿名类。

   bootstrap.group(group)
            .channel(NioSocketChannel.class)
            .option(ChannelOption.SO_KEEPALIVE, true)
            // 调用 AbstractBootstrap#handler(io.netty.channel.ChannelHandler)方法
            .handler(new ChannelInitializer<Channel>() {
                @Override
                protected void initChannel(Channel channel) throws Exception {
                    ChannelPipeline pipeline = channel.pipeline();
                    // 添加解码器
                    pipeline.addLast(new StringDecoder());
                    pipeline.addLast(new StringEncoder());
                    pipeline.addLast(new ChatClientHandler());
                }
            });

AbstractBootstrap#handler(io.netty.channel.ChannelHandler) 的实现如下,可以看到这里是将 handler (ChannelInitializer)保存到 AbstractBootstrap 的 handler 属性中 :

    public B handler(ChannelHandler handler) {
        this.handler = ObjectUtil.checkNotNull(handler, "handler");
        return self();
    }

1. ChannelInitializer

下面我们来看下 ChannelInitializer 的详细内容。

ChannelInitializer 是 Netty 中一个特殊的 ChannelHandler,它主要用于在Channel(通道)被注册到 EventLoop(事件循环)之后,对Channel进行初始化操作。


ChannelInitializer 实现如下:

public abstract class ChannelInitializer<C extends Channel> extends ChannelInboundHandlerAdapter {

    private final Set<ChannelHandlerContext> initMap = Collections.newSetFromMap(
            new ConcurrentHashMap<ChannelHandlerContext, Boolean>());
            
	// 供子类进行实现的抽象方法
    protected abstract void initChannel(C ch) throws Exception;

	...
	
	/**
	 * 当 Channel 成功注册到 EventLoop 时,此方法会被调用。
	 * 正常情况下,由于 handlerAdded(...) 方法通常会调用 initChannel(...) 并移除该处理器,所以此方法不会被调用。
	 *
	 * @param ctx ChannelHandler 与 ChannelPipeline 之间的上下文关联对象,用于操作 Channel 并传播事件
	 * @throws Exception 处理过程中可能抛出的异常
	 */
	public final void channelRegistered(ChannelHandlerContext ctx) throws Exception {
	    // 注释说明:通常情况下,handlerAdded 方法会调用 initChannel 方法对 Channel 进行初始化,
	    // 并移除当前的 ChannelInitializer 处理器,所以此方法一般不会被调用。
	    // 不过,若 handlerAdded 方法未调用 initChannel 方法,或者出现异常情况,此方法会被执行。
	
	    // 调用 initChannel 方法对 Channel 进行初始化操作
	    // initChannel 方法会尝试初始化 Channel,并返回一个布尔值表示初始化是否成功完成
	    if (initChannel(ctx)) {
	        // 注释说明:若 initChannel 方法返回 true,意味着在当前方法中刚刚完成了 Channel 的初始化操作。
	        // 由于 initChannel 方法可能在 channelRegistered 事件触发之前执行,
	        // 为了确保不会遗漏 channelRegistered 事件,需要手动触发该事件。
	        // ctx.pipeline() 获取当前 Channel 的 ChannelPipeline,fireChannelRegistered() 触发 channelRegistered 事件
	        ctx.pipeline().fireChannelRegistered();
	
	        // 注释说明:当 Channel 初始化完成后,需要移除与该 Channel 相关的所有状态信息,
	        // 以避免内存泄漏。removeState 方法负责清除这些状态信息。
	        removeState(ctx);
	    } else {
	        // 注释说明:若 initChannel 方法返回 false,表明 initChannel 方法已经在之前被调用过,
	        // 这是预期的行为。此时,只需要将 channelRegistered 事件继续传递给 ChannelPipeline 中的下一个处理器即可。
	        // ctx.fireChannelRegistered() 会将 channelRegistered 事件传播到下一个处理器
	        ctx.fireChannelRegistered();
	    }
	}
    
	...
	
	// 当ChannelInitializer被添加到ChannelPipeline时,handlerAdded方法会被调用。
	// 在 Netty 的引导流程中,无论是服务器端(ServerBootstrap)还是客户端(Bootstrap),在构建ChannelPipeline时都会添加ChannelInitializer。
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
    	// 1. 检查 Channel 是否已经注册,如果 Channel 已经注册则开始初始化 ChannelHandler
        if (ctx.channel().isRegistered()) {
            // 2. 初始化 Channel (添加 ChannelHandler)
            if (initChannel(ctx)) {
                // We are done with init the Channel, removing the initializer now.
                // 3. 移除与 Channel 初始化相关的状态信息
                removeState(ctx);
            }
        }
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
    	// 移除处理器
        initMap.remove(ctx);
    }
    
	...

这里我们看到 ChannelInitializer 存在一个抽象方法 initChannel,也就是我们自己实现的方法。而 initChannel 方法在 handlerAddedchannelRegistered 方法中都有调用。不过在上面的注释也写了【通常情况下,handlerAdded 方法会调用 initChannel 方法对 Channel 进行初始化】所以 channelRegistered 方法中是【若 handlerAdded 方法未调用 initChannel 方法,或者出现异常情况,此方法会被执行】

在 AbstractChannel.AbstractUnsafe#register0 方法中存在下面的逻辑:当 Channel 创建成功后会注册到 NIoEventLoopGroup 中的一个 NIoEventLoop, 在注册时会调用该方法,并且执行下面逻辑。(这段逻辑我们在 【Netty4核心原理⑦】【揭开Bootstrap的神秘面纱 - 客户端Bootstrap ❷】 的【Channel 的注册】部分进行了详细说明)

 private void register0(ChannelPromise promise) {
	...
   pipeline.invokeHandlerAddedIfNeeded();
	...	
    pipeline.fireChannelRegistered();
   ...
}

其中 pipeline.invokeHandlerAddedIfNeeded 会触发 Pipeline 中所有 ChannelHandler 的 handlerAdded 方法, pipeline.fireChannelRegistered 会触发所有 Pipeline 中所有 ChannelHandler 的 channelRegistered 方法。这里就可以看到 channelRegistered 方法的调用时机在 handlerAdded 方法 之后,符合注释上的解释。


2. ChannelInitializer#handlerAdded

下面我们按照上面代码的注释顺序,主要看看 ChannelInitializer#handlerAdded方法。

2.1 ctx.channel().isRegistered()

ctx 是 ChannelHandlerContext 类型的对象,它提供了与 ChannelHandler 相关的上下文信息,ctx.channel() 则是获取到了 Channel 的引用。而ctx.channel().isRegistered() 用于检查当前 Channel 是否已经注册到某个 EventLoop 上。只有当 Channel 已经注册时,才继续进行后续的初始化操作。这是因为只有注册后的 Channel 才处于可操作状态,可以进行初始化配置。

综上 ctx.channel().isRegistered() 会返回一个标志位,用于标识当前 Channel 是否已经注册,实现如下:

在 AbstractChannel.AbstractUnsafe#register0 中Channel 注册结束后会将 registered 字段置为 true。这个在下文 【客户端发起连接请求】部分有介绍。

 @Override
   public boolean isRegistered() {
       return registered;
   }

2.2 initChannel(ctx)

initChannel(ctx) 的实现是 ChannelInitializer#initChannel(ChannelHandlerContext) 方法,主要负责对 Channel 的初始化操作。

    private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
    	// 将 ctx 添加到 initMap 中, 保证并发安全
        if (initMap.add(ctx)) { // Guard against re-entrance.
            try {
            	// 调用 ChannelInitializer#initChannel 抽象方法 来添加编解码器,这个方法由我们自己来实现。
                initChannel((C) ctx.channel());
            } catch (Throwable cause) {
                // Explicitly call exceptionCaught(...) as we removed the handler before calling initChannel(...).
                // We do so to prevent multiple calls to initChannel(...).
                // 异常处理
                exceptionCaught(ctx, cause);
            } finally {
            	// 将自己从 ChannelPipeline 中移除
                if (!ctx.isRemoved()) {
                    ctx.pipeline().remove(this);
                }
            }
            return true;
        }
        return false;
    }

这里我们注意下面几个点:

  1. initMap.add(ctx) : 将 ctx 添加到 initMap 中, 保证并发安全

  2. initChannel((C) ctx.channel()); : 调用 ChannelInitializer#initChannel 抽象方法 来添加编解码器,这个方法就是我们在创建 ChannelInitializer 匿名类的时候实现的抽象方法

  3. ctx.pipeline().remove(this); : 从 Pipeline 中移除自己,保证handlerAdded方法只会被调用一次。

    对于一个特定的ChannelInitializer实例,handlerAdded方法只会被调用一次。因为一旦ChannelInitializer完成了对ChannelPipeline的初始化设置,通常会将自己从ChannelPipeline中移除。在initChannel方法执行完成后,ChannelInitializer就完成了它的主要任务,为了避免后续不必要的处理,会自动移除。例如,在ChannelInitializer的内部实现中,initChannel方法最后会调用pipeline.remove(this)来移除自身。这也保证了handlerAdded方法不会被重复调用。

2.3 removeState(ctx);

removeState(ctx); 方法通过检查 ChannelHandlerContext 的移除状态,采取同步或异步的方式从 initMap 中移除对应的状态信息,确保了在 ChannelHandlerContext 移除后,相关的状态信息也能正确清理,避免了内存泄漏和潜在的逻辑错误。这种设计考虑到了移除操作可能的异步特性,增强了代码的健壮性和稳定性。

    private void removeState(final ChannelHandlerContext ctx) {
        // The removal may happen in an async fashion if the EventExecutor we use does something funky.
        // 将 ctx 从 initMap 中移除,某些场景下需要异步移除。
        if (ctx.isRemoved()) {
            initMap.remove(ctx);
        } else {
            // The context is not removed yet which is most likely the case because a custom EventExecutor is used.
            // Let's schedule it on the EventExecutor to give it some more time to be completed in case it is offloaded.
            ctx.executor().execute(new Runnable() {
                @Override
                public void run() {
                    initMap.remove(ctx);
                }
            });
        }
    }

3. 总结

综上所属 Handler 的添加过程简单来说如下:

  1. 在服务端创建 NioServerSocketChannel 或 客户端创建NioSocketChannel 时都会调用 其父类的构造函数 AbstractChannel#AbstractChannel(io.netty.channel.Channel) ,该方法中中初始化 DefaultChannelPipeline 时,默认存在 Head 和 Tail 两个节点,并构成双向链表。如下:
    在这里插入图片描述
    所以此时在 ChannelPipeline 中存在两个节点,如下图:
    在这里插入图片描述

  2. 随后我们通过调用 AbstractBootstrap#handler(io.netty.channel.ChannelHandler) 方法时会添加一个 ChannelInitializer 匿名内部类到 ChannelPipeline 中。
    在这里插入图片描述

    此时 ChannelPipeline 中只有三个 Handler, 分别是 Head、Tail 和我们添加的 ChannelInitializer 。如下图:

  3. 无论是服务端还是客户端 Channel ,Netty 都会为之分配一个 NioEventLoop 作为其 “指定线程” 来处理这个 Channel 的实践,在将 Channel 注册到 NioEventLoop 上时,会调用 在 AbstractChannel.AbstractUnsafe#register0 ,在这个方法中会触发 ChannelInitializer#initChannel 方法(通过 pipeline.invokeHandlerAddedIfNeededpipeline.fireChannelRegistered 方法),而 ChannelInitializer#initChannel 方法会往 ChannelPipeline 添加自定义的 ChatClinetHandler ,如下图:
    在这里插入图片描述

    此时 ChannelPipeline 则会存在四个 Handler ,如下图:
    在这里插入图片描述

  4. 在 ChannelInitializer#initChannel(ChannelHandlerContext) 中调用结束上述的调用逻辑后最后会将 ChannelInitializer 从 ChannelPipeline 中移除,如下图:
    在这里插入图片描述

    当 ChannelPipeline 将 ChannelInitializer 移除后,此时ChannelPipeline 最后剩下三个 Handler,如下图:
    在这里插入图片描述

四、客户端发起连接请求

由于篇幅所限,这一部分另开新篇,详参 【Netty4核心原理⑦】【揭开Bootstrap的神秘面纱 - 客户端Bootstrap ❷】

五、总结

结合上面的内容,我们这里总结一下 Netty 客户端的创建启动流程:

  1. 客户端启动引导(Bootstrap)创建与配置

    1. 创建Bootstrap对象:

      • 首先会创建一个Bootstrap实例,它是 Netty 客户端的启动引导类。例如:Bootstrap bootstrap = new Bootstrap();。
    2. 设置线程模型(EventLoopGroup):

      • 为Bootstrap配置EventLoopGroup,用于处理客户端的网络事件。通常会创建一个NioEventLoopGroup,它包含一组NioEventLoop,每个NioEventLoop绑定一个线程来处理 I/O 操作。如:bootstrap.group(new NioEventLoopGroup());
        这个EventLoopGroup中的线程会负责从Selector获取网络事件,如连接建立、数据读写等事件,并调用相应的处理器来处理这些事件。

        NioEventLoopGroup 内部会根据指定的线程数量通过 NioEventLoopGroup#newChild 方法来循环创建 NioEventLoop。每个 NioEventLoop 内部都与一个 JVM 本地线程绑定,因此 NioEventLoop实际上是一个单线程的执行器(Executor),它会在一个循环中不断地获取和处理事件。这个循环被称为事件循环(Event Loop),这也是NioEventLoop名字的由来。

    3. 指定通道类型(Channel):

      • 通过bootstrap.channel(NioSocketChannel.class)指定客户端使用的通道类型为NioSocketChannel(基于 NIO 的套接字通道)。不同的通道类型适用于不同的网络协议和传输方式。

        bootstrap.channel(NioSocketChannel.class) 是记录当前使用的 Channel 类型,在 Bootstrap#connect 方法中会通过反射创建 NioSocketChannel 对象。

    4. 添加处理器(ChannelHandler)到ChannelPipeline:

      • 使用handler方法添加一个ChannelInitializer,它是一个特殊的ChannelHandler,用于在Channel初始化时向ChannelPipeline添加其他ChannelHandler。例如:

        bootstrap.handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                // 在这里添加各种ChannelHandler,如编解码器、业务逻辑处理器等
                ch.pipeline().addLast(new SomeDecoder());
                ch.pipeline().addLast(new MyBusinessHandler());
                ch.pipeline().addLast(new SomeEncoder());
            }
        });
        

        ChannelInitializer#initChannel 方法只会执行一遍,当方法执行结束后会将其从 ChannelPipeline 中 移除,通过 ChannelPipeline#addLast 方法将用户添加的自定义的 ChannelHandler 会在 ChannelPipeline内部形成一个双向链表。

      • ChannelPipeline是一个由多个ChannelHandler组成的管道,用于处理Channel上的事件。每个ChannelHandler负责处理特定类型的事件或者对数据进行特定的操作,数据会按照添加的顺序在这些ChannelHandler之间流动。

        ChannelPipeline#addLast 方法会将添加的 ChannelHandler 包装成 AbstractChannelHandlerContext,并会根据其实现的方法判断当前 Handler 支持处理哪些事件,当对应事件来临时会找到可以处理该事件的 Handler 并调用。

  2. 连接远程服务器

    1. 发起连接:
      • 使用bootstrap.connect(new InetSocketAddress("服务器IP", 服务器端口))发起连接操作。

      • 这里的 connect 方法返回一个 ChannelFuture 对象,它代表了一个异步操作的结果。sync方法会阻塞当前线程,直到连接操作完成。

    2. 连接过程的内部实现(AbstractChannel和AbstractNioChannel):
      • 在 AbstractChannel 及其子类 AbstractNioChannel 中实现了连接的核心逻辑。
      • 首先会进行一些状态检查,如在AbstractNioChannel的connect方法中,会检查 ChannelPromise 是否可取消以及通道是否打开。
      • 然后调用 doConnect 方法(AbstractNioChannel中的抽象方法,由具体子类实现实际的连接操作)尝试连接。如果指定了本地地址,会先进行绑定操作。
      • 在 doConnect 方法中,通过 SocketUtils.connect 尝试连接远程地址,如果连接不成功,会设置SelectionKey的感兴趣操作集为OP_CONNECT,以便在连接准备好时 Selector 能够通知 EventLoop。
  3. 连接后的操作和事件处理

    1. 连接成功后的操作:
      • 当连接成功后,ChannelPipeline中的ChannelHandler会开始处理后续的事件。例如,在ChannelInitializer的initChannel方法中添加的MyBusinessHandler可以处理从服务器接收的数据或者向服务器发送数据。
    2. 事件处理循环(EventLoop):
      • NioEventLoop会不断地从Selector获取事件,对于连接事件,它会调用ChannelPipeline中的ChannelHandler来处理。对于读事件和写事件也是类似,会根据SelectionKey的操作集触发相应的ChannelHandler。
        例如,当有数据可读时,EventLoop会调用ChannelPipeline中的解码器ChannelHandler将字节流解码为消息对象,然后调用业务逻辑处理器ChannelHandler处理消息,最后如果需要发送响应,会调用编码器ChannelHandler将消息编码为字节流发送出去。
  4. 客户端关闭

    • 当客户端完成任务或者出现异常等情况需要关闭时,通常会调用channel.closeFuture().sync();等待通道关闭,然后调用eventLoopGroup.shutdownGracefully();优雅地关闭EventLoopGroup,释放线程等资源。这样确保了所有的网络资源被正确地释放,避免资源泄漏。

六、参考内容

  1. 《Netty4核心原理》
  2. 豆包
内容概要:本文档详细介绍了Android开发中内容提供者(ContentProvider)的使用方法及其在应用间数据共享的作用。首先解释了ContentProvider作为四大组件之一,能够为应用程序提供统一的数据访问接口,支持不同应用间的跨进程数据共享。接着阐述了ContentProvider的核心方法如onCreate、insert、delete、update、query和getType的具体功能与应用场景。文档还深入讲解了Uri的结构和作用,它是ContentProvider中用于定位资源的重要标识。此外,文档说明了如何通过ContentResolver在客户端应用中访问其他应用的数据,并介绍了Android 6.0及以上版本的运行时权限管理机制,包括权限检查、申请及处理用户的选择结果。最后,文档提供了具体的实例,如通过ContentProvider读写联系人信息、监听短信变化、使用FileProvider发送彩信和安装应用等。 适合人群:对Android开发有一定了解,尤其是希望深入理解应用间数据交互机制的开发者。 使用场景及目标:①掌握ContentProvider的基本概念和主要方法的应用;②学会使用Uri进行资源定位;③理解并实现ContentResolver访问其他应用的数据;④熟悉Android 6.0以后版本的权限管理流程;⑤掌握FileProvider在发送彩信和安装应用中的应用。 阅读建议:建议读者在学习过程中结合实际项目练习,特别是在理解和实现ContentProvider、ContentResolver以及权限管理相关代码时,多进行代码调试和测试,确保对每个知识点都有深刻的理解。
开发语言:Java 框架:SSM(Spring、Spring MVC、MyBatis) JDK版本:JDK 1.8 或以上 开发工具:Eclipse 或 IntelliJ IDEA Maven版本:Maven 3.3 或以上 数据库:MySQL 5.7 或以上 此压缩包包含了本毕业设计项目的完整内容,具体包括源代码、毕业论文以及演示PPT模板。 项目配置完成后即可运行,若需添加额外功能,可根据需求自行扩展。 运行条件 确保已安装 JDK 1.8 或更高版本,并正确配置 Java 环境变量。 使用 Eclipse 或 IntelliJ IDEA 打开项目,导入 Maven 依赖,确保依赖包下载完成。 配置数据库环境,确保 MySQL 服务正常运行,并导入项目中提供的数据库脚本。 在 IDE 中启动项目,确认所有服务正常运行。 主要功能简述: 用户管理:系统管理员负责管理所有用户信息,包括学生、任课老师、班主任、院系领导和学校领导的账号创建、权限分配等。 数据维护:管理员可以动态更新和维护系统所需的数据,如学生信息、课程安排、学年安排等,确保系统的正常运行。 系统配置:管理员可以对系统进行配置,如设置数据库连接参数、调整系统参数等,以满足不同的使用需求。 身份验证:系统采用用户名和密码进行身份验证,确保只有授权用户才能访问系统。不同用户类型(学生、任课老师、班主任、院系领导、学校领导、系统管理员)具有不同的操作权限。 权限控制:系统根据用户类型分配不同的操作权限,确保用户只能访问和操作其权限范围内的功能和数据。 数据安全:系统采取多种措施保障数据安全,如数据库加密、访问控制等,防止数据泄露和非法访问。 请假审批流程:系统支持请假申请的逐级审批,包括班主任审批和院系领导审批(针对超过三天的请假)。学生可以随时查看请假申请的审批进展情况。 请假记录管理:系统记录学生的所有请假记录,包括请假时间、原因、审批状态及审批意见等,供学生和审批人员查询。 学生在线请假:学生可以通过系统在线填写请假申请,包括请假的起止日期和请假原因,并提交给班主任审批。超过三天的请假需经班主任审批后,再由院系领导审批。 出勤信息记录:任课老师可以在线记录学生的上课出勤情况,包括迟到、早退、旷课和请假等状态。 出勤信息查询:学生、任课老师、班主任、院系领导和学校领导均可根据权限查看不同范围的学生上课出勤信息。学生可以查看自己所有学年的出勤信息,任课老师可以查看所教班级的出勤信息,班主任和院系领导可以查看本班或本院系的出勤信息,学校领导可以查看全校的出勤信息。 出勤统计与分析:系统提供出勤统计功能,可以按班级、学期等条件统计学生的出勤情况,帮助管理人员了解学生的出勤状况
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值