Netty的Channel详解(建议收藏)

学习Netty的Channel很重要,因为Channel是Netty网络通信的核心组件。每一个新的连接都会创建一个新的Channel,Channel负责与操作系统进行交互,进行实际的IO操作。

本文将会对 Netty 的 Channel 使用和原理进行概述

Channel

Channel 的字面意思是"通道",它是网络通信的载体。为用户提供:

  1. 通道当前的状态(例如它是打开?还是已连接?)
  2. channel的配置参数(例如接收缓冲区的大小)
  3. 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 可以实现以下功能:

  1. 添加或移除 Channel:当有新的 Channel 加入或者某个 Channel 断开连接时,可以通过 ChannelGroup 的 add()、remove()、discard() 等方法来实现添加或移除操作。
  2. 遍历所有 Channel:可以使用 ChannelGroup 的 iterator() 或 stream() 方法来获取所有 Channel 并进行遍历操作。
  3. 向所有 Channel 发送消息:可以使用 ChannelGroup 的 writeAndFlush() 方法向所有 Channel 发送消息。此时,每个 Channel 中的消息发送顺序可能会因为网络原因被打乱,因此需要根据具体情况进行处理。

ChannelGroup 在 Netty 中应用非常广泛,特别是在服务器开发中,通常会将所有连接到服务器的 Channel 保存在一个全局的 ChannelGroup 中,方便后续对这些 Channel 进行统一管理和处理。同时,由于 ChannelGroup 可以快速地获取并遍历所有 Channel,因此也可以用于实现一些群发、广播等功能。

ChannelGroup是如何自动移除已经关闭的Channel的?

通过添加ChannelFutureListener,在Channel 被close后,从ChannelGroup中移除。

DefaultChannelGroup源码截图如下:

相关推荐
小灰灰__18 分钟前
IDEA加载通义灵码插件及使用指南
java·ide·intellij-idea
夜雨翦春韭21 分钟前
Java中的动态代理
java·开发语言·aop·动态代理
程序媛小果42 分钟前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
追风林1 小时前
mac m1 docker本地部署canal 监听mysql的binglog日志
java·docker·mac
芒果披萨1 小时前
El表达式和JSTL
java·el
许野平1 小时前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
duration~2 小时前
Maven随笔
java·maven
zmgst2 小时前
canal1.1.7使用canal-adapter进行mysql同步数据
java·数据库·mysql
跃ZHD2 小时前
前后端分离,Jackson,Long精度丢失
java
blammmp2 小时前
Java:数据结构-枚举
java·开发语言·数据结构