一、前言
本系列虽说本意是作为 《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 连接 |
NioDatagramChannel | UDP 连接 |
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();
}
}
根据上面的代码注释顺序,我们将其分为四个部分,如下:
-
创建工作线程 :这里直接创建了一个 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 来处理。
-
创建 NioSocketChannel :实际上这里只是传入了一个 Channel 类型,真正的逻辑创建是在 Bootstrap#connect 中通过反射创建。客户端的Channel 类型是 NioSocketChannel, 如果是服务端则使用 NioServerSocketChannel。
-
Handler 添加过程 :这里是 Netty 的核心,可以供用户扩展实现自定义的 Handler来处理消息。
-
客户端发起连接请求 :这里会发起与服务器的连接。
下面来详细看看看具体每一步的内容。
二、创建工作线程
在上面的代码中,我们需要实例化一个 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 中的处理逻辑可以简单总结如下:
- 创建一个大小为 nThreads 的 SingleThreadEventExecutor 数组
- 调用 MultithreadEventExecutorGroup#newChild 方法初始化 children 数据。
- 根据 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 的初始化过程:
- NioEventLoopGroup 内部维护了一个 EventExecutor 类型的 children 数组,大小是 nThreads,这样就构成了一个线程池。
- 在实例化 NioEventLoopGroup 时,如果没指定线程池大小,则线程池大小默认为 CPU 核心数 * 2
- 在 NioEventLoopGroup 初始过程中会调用父类 MultithreadEventExecutorGroup 的构造函数,其内部会调用 MultithreadEventExecutorGroup#newChild 方法来初始化 children 数组。
- MultithreadEventExecutorGroup#newChild 是一个抽象方法,在 NioEventLoopGroup 中有具体实现,他会返回一个 NioEventLoop 实例。因此这里会为 children 数组 的每个元素创建一个 NioEventLoop 对象。
- 在NioEventLoopGroup#newChild 方法中创建 NioEventLoop 对象时,NioEventLoop 在构造函数中会初始化一些属性,具体赋值如下:
- NioEventLoop#provider :在 NioEventLoop 构造函数可以传入的参数,如果没传,默认由SelectorProvider.provider() 生成
- NioEventLoop#selector :在 NioEventLoop 构造函数中调用 NioEventLoop#openSelector 生成。
- 之后通过 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);
}
...
上面代码主要有两个部分:
- 调用 NioSocketChannel#newSocket 方法来创建了一个SocketChannel 实例
- 调用父类 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();
}
上面的方法中主要有两个方面:
unsafe = newUnsafe();
:Unsafe 属性的初始化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;
}
这里需要注意:
- 这里传入的 Channel 就是上述初始化的 Channel,客户端是 NioSocketChannel,服务端是 NioServerSocketChannel。
- 在 DefaultChannelPipeline 中维护了一个以 AbstractChannelHandlerContext 为节点元素的双向链表,而 head 和 tail 分别指向双向链表的头尾节点,这个双向链表是实现 Netty 的 Pipeline 机制的关键,简单的说即:当有事件发生时会遍历这个双向链表上的所有 ChannelHandler,以寻找能够过处理该事件的 ChannelHandler 并执行逻辑。(由于篇幅所限,这部分内容我们在 【Netty4核心原理⑪】【Netty 大动脉 Pipeline】 中会详细介绍。)
2.3 总结
至此 NioSocketChannel 就完成了初始化,总结如下:
- 在 NioSocketChannel 构造函数中。
- 调用 NioSocketChannel#newSocket 打开一个新的 Java NioSocketChannel
- 初始化 AbstractChannel 对象并给属性赋值,如下:
- id : 每个 Channel 都会被分配一个唯一id
- parent :属性值默认为空
- unsafe : 通过调用 newUnsafe 方法实例化一个 Unsafe 对象,他的类型是 AbstractNioByteChannel.NioByteUnsafe。
- pipeline :通过
new DefaultChannelPipeline(this)
新创建的实例。
- AbstractNioChannel 中被赋值的属性如下:
- ch :被赋值为 Java 原生 SocketChannel,即 NioSocketChannel 的 newSocket() 方法返回的 Java NIO SocketChannel。
- readInterestOp :被赋值为 SelectionKey.OP_READ。
- ch :被配置为非阻塞。
- 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
方法在 handlerAdded
和 channelRegistered
方法中都有调用。不过在上面的注释也写了【通常情况下,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;
}
这里我们注意下面几个点:
-
initMap.add(ctx)
: 将 ctx 添加到 initMap 中, 保证并发安全 -
initChannel((C) ctx.channel());
: 调用 ChannelInitializer#initChannel 抽象方法 来添加编解码器,这个方法就是我们在创建 ChannelInitializer 匿名类的时候实现的抽象方法 -
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 的添加过程简单来说如下:
-
在服务端创建 NioServerSocketChannel 或 客户端创建NioSocketChannel 时都会调用 其父类的构造函数 AbstractChannel#AbstractChannel(io.netty.channel.Channel) ,该方法中中初始化 DefaultChannelPipeline 时,默认存在 Head 和 Tail 两个节点,并构成双向链表。如下:
所以此时在 ChannelPipeline 中存在两个节点,如下图:
-
随后我们通过调用 AbstractBootstrap#handler(io.netty.channel.ChannelHandler) 方法时会添加一个 ChannelInitializer 匿名内部类到 ChannelPipeline 中。
此时 ChannelPipeline 中只有三个 Handler, 分别是 Head、Tail 和我们添加的 ChannelInitializer 。如下图:
-
无论是服务端还是客户端 Channel ,Netty 都会为之分配一个 NioEventLoop 作为其 “指定线程” 来处理这个 Channel 的实践,在将 Channel 注册到 NioEventLoop 上时,会调用 在 AbstractChannel.AbstractUnsafe#register0 ,在这个方法中会触发 ChannelInitializer#initChannel 方法(通过
pipeline.invokeHandlerAddedIfNeeded
或pipeline.fireChannelRegistered
方法),而 ChannelInitializer#initChannel 方法会往 ChannelPipeline 添加自定义的 ChatClinetHandler ,如下图:
此时 ChannelPipeline 则会存在四个 Handler ,如下图:
-
在 ChannelInitializer#initChannel(ChannelHandlerContext) 中调用结束上述的调用逻辑后最后会将 ChannelInitializer 从 ChannelPipeline 中移除,如下图:
当 ChannelPipeline 将 ChannelInitializer 移除后,此时ChannelPipeline 最后剩下三个 Handler,如下图:
四、客户端发起连接请求
由于篇幅所限,这一部分另开新篇,详参 【Netty4核心原理⑦】【揭开Bootstrap的神秘面纱 - 客户端Bootstrap ❷】 。
五、总结
结合上面的内容,我们这里总结一下 Netty 客户端的创建启动流程:
-
客户端启动引导(Bootstrap)创建与配置
-
创建Bootstrap对象:
- 首先会创建一个Bootstrap实例,它是 Netty 客户端的启动引导类。例如:
Bootstrap bootstrap = new Bootstrap();。
- 首先会创建一个Bootstrap实例,它是 Netty 客户端的启动引导类。例如:
-
设置线程模型(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名字的由来。
-
-
指定通道类型(Channel):
-
通过
bootstrap.channel(NioSocketChannel.class)
指定客户端使用的通道类型为NioSocketChannel(基于 NIO 的套接字通道)。不同的通道类型适用于不同的网络协议和传输方式。bootstrap.channel(NioSocketChannel.class) 是记录当前使用的 Channel 类型,在 Bootstrap#connect 方法中会通过反射创建 NioSocketChannel 对象。
-
-
添加处理器(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 并调用。
-
-
-
连接远程服务器
- 发起连接:
-
使用
bootstrap.connect(new InetSocketAddress("服务器IP", 服务器端口))
发起连接操作。 -
这里的 connect 方法返回一个 ChannelFuture 对象,它代表了一个异步操作的结果。sync方法会阻塞当前线程,直到连接操作完成。
-
- 连接过程的内部实现(AbstractChannel和AbstractNioChannel):
- 在 AbstractChannel 及其子类 AbstractNioChannel 中实现了连接的核心逻辑。
- 首先会进行一些状态检查,如在AbstractNioChannel的connect方法中,会检查 ChannelPromise 是否可取消以及通道是否打开。
- 然后调用 doConnect 方法(AbstractNioChannel中的抽象方法,由具体子类实现实际的连接操作)尝试连接。如果指定了本地地址,会先进行绑定操作。
- 在 doConnect 方法中,通过 SocketUtils.connect 尝试连接远程地址,如果连接不成功,会设置SelectionKey的感兴趣操作集为
OP_CONNECT
,以便在连接准备好时 Selector 能够通知 EventLoop。
- 发起连接:
-
连接后的操作和事件处理
- 连接成功后的操作:
- 当连接成功后,ChannelPipeline中的ChannelHandler会开始处理后续的事件。例如,在ChannelInitializer的initChannel方法中添加的MyBusinessHandler可以处理从服务器接收的数据或者向服务器发送数据。
- 事件处理循环(EventLoop):
- NioEventLoop会不断地从Selector获取事件,对于连接事件,它会调用ChannelPipeline中的ChannelHandler来处理。对于读事件和写事件也是类似,会根据SelectionKey的操作集触发相应的ChannelHandler。
例如,当有数据可读时,EventLoop会调用ChannelPipeline中的解码器ChannelHandler将字节流解码为消息对象,然后调用业务逻辑处理器ChannelHandler处理消息,最后如果需要发送响应,会调用编码器ChannelHandler将消息编码为字节流发送出去。
- NioEventLoop会不断地从Selector获取事件,对于连接事件,它会调用ChannelPipeline中的ChannelHandler来处理。对于读事件和写事件也是类似,会根据SelectionKey的操作集触发相应的ChannelHandler。
- 连接成功后的操作:
-
客户端关闭
- 当客户端完成任务或者出现异常等情况需要关闭时,通常会调用
channel.closeFuture().sync();
等待通道关闭,然后调用eventLoopGroup.shutdownGracefully();
优雅地关闭EventLoopGroup,释放线程等资源。这样确保了所有的网络资源被正确地释放,避免资源泄漏。
- 当客户端完成任务或者出现异常等情况需要关闭时,通常会调用
六、参考内容
- 《Netty4核心原理》
- 豆包