学习Netty的Channel很重要,因为Channel是Netty网络通信的核心组件。每一个新的连接都会创建一个新的Channel,Channel负责与操作系统进行交互,进行实际的IO操作。
本文将会对 Netty 的 Channel 使用和原理进行概述
Channel
Channel 的字面意思是"通道",它是网络通信的载体。为用户提供:
- 通道当前的状态(例如它是打开?还是已连接?)
- channel的配置参数(例如接收缓冲区的大小)
- channel支持的IO操作(例如读、写、连接和绑定),以及处理与channel相关联的所有IO事件和请求的ChannelPipeline。
AbstractChannel 是整个家族的基类,派生出 AbstractNioChannel、AbstractOioChannel、AbstractEpollChannel 等子类,每一种都代表了不同的 I/O 模型和协议类型。常用的 Channel 实现类有:
- NioServerSocketChannel 异步 TCP 服务端。
- NioSocketChannel 异步 TCP 客户端。
- OioServerSocketChannel 同步 TCP 服务端。
- OioSocketChannel 同步 TCP 客户端。
- NioDatagramChannel 异步 UDP 连接。
- OioDatagramChannel 同步 UDP 连接。
当然 Channel 会有多种状态,如连接建立、连接注册、数据读写、连接销毁等。
常见方法

attr
Channel.attr是一个接口,它可以让我们在Channel上绑定一些自定义的属性,这些属性可以在Channel的整个生命周期内使用。Channel.attr()方法可以给服务端的channel指定一些自定义属性,然后通过channel.attr()取出这个属性,其实就是给channel维护一个map,一般也用不上。
使用 AttributeKey 非常简单,首先我们需要创建一个 AttributeKey 对象,例如:
swift
private static final AttributeKey<Integer> LOGIN_COUNT = AttributeKey.valueOf("loginCount");
上面的代码创建了一个 AttributeKey 对象,它的键名是 "loginCount",值的类型是 Integer。我们可以在 Channel 上使用 AttributeKey 存储或获取 Integer 类型的属性,例如:
ini
Channel channel = ...; // 获取一个 Channel 对象
channel.attr(LOGIN_COUNT).set(1); // 存储一个 Integer 类型的属性
Integer loginCount = channel.attr(LOGIN_COUNT).get(); // 获取 Integer 类型的属性
获取channel的状态
arduino
boolean isOpen(); //如果通道打开,则返回true
boolean isRegistered();//如果通道注册到EventLoop,则返回true
boolean isActive();//如果通道处于活动状态并且已连接,则返回true
boolean isWritable();//当且仅当I/O线程将立即执行请求的写入操作时,返回true。
writeAndFlush 处理流程剖析
如何触发事件传播的?
writeAndFlush 是特有的出站操作,那么我们猜测它是从 Pipeline 的 Tail 节点 开始传播的,然后一直向前传播到 Head 节点。我们跟进去 ctx.channel().writeAndFlush()的源码,如下所示,发现 DefaultChannelPipeline 类中果然是调用的 Tail 节点 writeAndFlush 方法。
typescript
@Override
public final ChannelFuture writeAndFlush(Object msg) {
return tail.writeAndFlush(msg);
}
继续跟进 tail.writeAndFlush 的源码,最终会定位到 AbstractChannelHandlerContext 中的 write 方法。该方法是 writeAndFlush 的核心逻辑
arduino
private void write(Object msg, boolean flush, ChannelPromise promise) {
// ...... 省略部分非核心代码 ......
// 找到 Pipeline 链表中下一个 Outbound 类型的 ChannelHandler 节点
final AbstractChannelHandlerContext next = findContextOutbound(flush ?
(MASK_WRITE | MASK_FLUSH) : MASK_WRITE);
final Object m = pipeline.touch(msg, next);
EventExecutor executor = next.executor();
// 判断当前线程是否是 NioEventLoop 中的线程
if (executor.inEventLoop()) {
if (flush) {
// 因为 flush == true,所以流程走到这里
next.invokeWriteAndFlush(m, promise);
} else {
next.invokeWrite(m, promise);
}
} else {
final AbstractWriteTask task;
if (flush) {
task = WriteAndFlushTask.newInstance(next, m, promise);
} else {
task = WriteTask.newInstance(next, m, promise);
}
if (!safeExecute(executor, task, promise, m)) {
task.cancel();
}
}
}
通过上述我们得知调用 writeAndFlush 时数据是在 Outbound 类型的 ChannelHandler 节点之间进行传播。数据将会在 Pipeline 中一直寻找 Outbound 节点并向前传播,直到 Head 节点结束,由 Head 节点完成最后的数据发送。所以 Pipeline 中的 Head 节点在完成 writeAndFlush 过程中扮演着重要的角色。
writeAndFlush 主要分为两个步骤,write 和 flush。通过上面的分析可以看出只调用 write 方法,数据并不会被真正发送出去,而是存储在 ChannelOutboundBuffer的缓存内。
flush什么时候被调用
根据源码发现最终会它会执行到 AbstractChannelHandlerContext 中 invokeWriteAndFlush 方法
scss
private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
if (invokeHandler()) {
invokeWrite0(msg, promise);
// 调用flush
invokeFlush0();
} else {
writeAndFlush(msg, promise);
}
}
Channel 和 ChannelHandlerContext 都有 writeAndFlush 方法,它们之间有什么区别呢?
ctx.writeAndFlush()是从 pipeline 链中的当前节点开始往前找到第一个 outBound 类型的 handler 把对象往前进行传播,如果这个对象确认不需要经过其他 outBound 类型的 handler 处理,就使用这个方法。
ctx.channel().writeAndFlush()是从 pipeline 链中的最后一个 outBound 类型的 handler 开始,把对象往前进行传播,如果你确认当前创建的对象需要经过后面的 outBound 类型的 handler,那么就调用此方法。
相比下,前者可以少执行一些 outbound的操作
总结以下三点:
- writeAndFlush 属于出站操作,它是从 Pipeline 的 Tail 节点开始进行事件传播,一直向前传播到 Head 节点。不管在 write 还是 flush 过程,Head 节点都中扮演着重要的角色。
- write 方法并没有将数据写入 Socket 缓冲区,只是将数据写入到 ChannelOutboundBuffer 缓存中,ChannelOutboundBuffer 缓存内部是由单向链表实现的。
- flush 方法才最终将数据写入到 Socket 缓冲区。
ChannelFuture
netty中所有的IO都是异步IO,也就是说所有的IO都是立即返回的,返回的时候,IO可能还没有结束,所以需要返回一个ChannelFuture,当IO有结果之后,会去通知ChannelFuture,这样就可以取出结果了。
ChannelFuture是java.util.concurrent.Future的子类,它除了可以拿到线程的执行结果之外,还对其进行了扩展,加入了当前任务状态判断、等待任务执行和添加Listener的功能。
typescript
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ChannelFuture future = ctx.channel().close();
future.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
// 调用其他逻辑
}
});
}
ChannelGroup
ChannelGroup 是 Netty 提供的一个特殊类型的 Channel。它可以将多个 Channel 组合在一起,形成一个逻辑上的集合,方便对这些 Channel 进行批量处理。
ChannelGroup 是一个线程安全的容器,可以保证多线程并发访问时的数据安全。它提供了一系列方法来管理其中的 Channel,例如添加、移除、遍历、广播等操作。具体来说,ChannelGroup 可以实现以下功能:
- 添加或移除 Channel:当有新的 Channel 加入或者某个 Channel 断开连接时,可以通过 ChannelGroup 的 add()、remove()、discard() 等方法来实现添加或移除操作。
- 遍历所有 Channel:可以使用 ChannelGroup 的 iterator() 或 stream() 方法来获取所有 Channel 并进行遍历操作。
- 向所有 Channel 发送消息:可以使用 ChannelGroup 的 writeAndFlush() 方法向所有 Channel 发送消息。此时,每个 Channel 中的消息发送顺序可能会因为网络原因被打乱,因此需要根据具体情况进行处理。
ChannelGroup 在 Netty 中应用非常广泛,特别是在服务器开发中,通常会将所有连接到服务器的 Channel 保存在一个全局的 ChannelGroup 中,方便后续对这些 Channel 进行统一管理和处理。同时,由于 ChannelGroup 可以快速地获取并遍历所有 Channel,因此也可以用于实现一些群发、广播等功能。
ChannelGroup是如何自动移除已经关闭的Channel的?
通过添加ChannelFutureListener,在Channel 被close后,从ChannelGroup中移除。
DefaultChannelGroup源码截图如下:

