学习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源码截图如下: