Netty In Action中文版 - 第六章:ChannelHandler

本文深入探讨Netty框架中的核心组件,包括ChannelPipeline、ChannelHandlerContext及ChannelHandler等,解析它们的工作原理、交互方式以及如何动态调整ChannelPipeline。


本章介绍

  • ChannelPipeline
  • ChannelHandlerContext
  • ChannelHandler
  • Inbound vs outbound(入站和出站)
        接受连接或创建他们只是你的应用程序的一部分,虽然这些任何很重要,但是一个网络应用程序旺旺是更复杂的,需要更多的代码编写,如处理传入和传出的数据。Netty提供了一个强大的处理这些事情的功能,允许用户自定义ChannelHandler的实现来处理数据。使得ChannelHandler更强大的是可以连接每个ChannelHandler来实现任务,这有助于代码的整洁和重用。但是处理数据只是ChannelHandler所做的事情之一,也可以压制I/O操作,例如写请求。所有这些都可以动态实现。

6.1 ChannelPipeline

        ChannelPipeline是ChannelHandler实例的列表,用于处理或截获通道的接收和发送数据。ChannelPipeline提供了一种高级的截取过滤器模式,让用户可以在ChannelPipeline中完全控制一个事件及如何处理ChannelHandler与ChannelPipeline的交互。
        对于每个新的通道,会创建一个新的ChannelPipeline并附加至通道。一旦连接,Channel和ChannelPipeline之间的耦合是永久性的。Channel不能附加其他的ChannelPipeline或从ChannelPipeline分离。
        下图描述了ChannelHandler在ChannelPipeline中的I/O处理,一个I/O操作可以由一个ChannelInboundHandler或ChannelOutboundHandler进行处理,并通过调用ChannelInboundHandler处理入站IO或通过ChannelOutboundHandler处理出站IO。

如上图所示,ChannelPipeline是ChannelHandler的一个列表;如果一个入站I/O事件被触发,这个事件会从第一个开始依次通过ChannelPipeline中的ChannelHandler;若是一个入站I/O事件,则会从最后一个开始依次通过ChannelPipeline中的ChannelHandler。ChannelHandler可以处理事件并检查类型,如果某个ChannelHandler不能处理则会跳过,并将事件传递到下一个ChannelHandler。ChannelPipeline可以动态添加、删除、替换其中的ChannelHandler,这样的机制可以提高灵活性。
        修改ChannelPipeline的方法:
  • addFirst(...),添加ChannelHandler在ChannelPipeline的第一个位置
  • addBefore(...),在ChannelPipeline中指定的ChannelHandler名称之前添加ChannelHandler
  • addAfter(...),在ChannelPipeline中指定的ChannelHandler名称之后添加ChannelHandler
  • addLast(ChannelHandler...),在ChannelPipeline的末尾添加ChannelHandler
  • remove(...),删除ChannelPipeline中指定的ChannelHandler
  • replace(...),替换ChannelPipeline中指定的ChannelHandler
ChannelPipeline pipeline = ch.pipeline();
FirstHandler firstHandler = new FirstHandler();
pipeline.addLast("handler1", firstHandler);
pipeline.addFirst("handler2", new SecondHandler());
pipeline.addLast("handler3", new ThirdHandler());
pipeline.remove("“handler3“");
pipeline.remove(firstHandler);
pipeline.replace("handler2", "handler4", new FourthHandler());
        被添加到ChannelPipeline的ChannelHandler将通过IO-Thread处理事件,这意味了必须不能有其他的IO-Thread阻塞来影响IO的整体处理;有时候可能需要阻塞,例如JDBC。因此,Netty允许通过一个EventExecutorGroup到每一个ChannelPipeline.add*方法,自定义的事件会被包含在EventExecutorGroup中的EventExecutor来处理,默认的实现是DefaultEventExecutorGroup。
        ChannelPipeline除了一些修改的方法,还有很多其他的方法,具体是方法及使用可以看API文档或源码。

6.2 ChannelHandlerContext

        每个ChannelHandler被添加到ChannelPipeline后,都会创建一个ChannelHandlerContext并与之创建的ChannelHandler关联绑定。ChannelHandlerContext允许ChannelHandler与其他的ChannelHandler实现进行交互,这是相同ChannelPipeline的一部分。ChannelHandlerContext不会改变添加到其中的ChannelHandler,因此它是安全的。
6.2.1 通知下一个ChannelHandler
        在相同的ChannelPipeline中通过调用ChannelInboundHandler和ChannelOutboundHandler中各个方法中的一个方法来通知最近的handler,通知开始的地方取决你如何设置。下图显示了ChannelHandlerContext、ChannelHandler、ChannelPipeline的关系:

        如果你想有一些事件流全部通过ChannelPipeline,有两个不同的方法可以做到:
  • 调用Channel的方法
  • 调用ChannelPipeline的方法
        这两个方法都可以让事件流全部通过ChannelPipeline。无论从头部还是尾部开始,因为它主要依赖于事件的性质。如果是一个“入站”事件,它开始于头部;若是一个“出站”事件,则开始于尾部。
        下面的代码显示了一个写事件如何通过ChannelPipeline从尾部开始:
@Override
protected void initChannel(SocketChannel ch) throws Exception {
	ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
		@Override
		public void channelActive(ChannelHandlerContext ctx) throws Exception {
			//Event via Channel
			Channel channel = ctx.channel();
			channel.write(Unpooled.copiedBuffer("netty in action", CharsetUtil.UTF_8));
			//Event via ChannelPipeline
			ChannelPipeline pipeline = ctx.pipeline();
			pipeline.write(Unpooled.copiedBuffer("netty in action", CharsetUtil.UTF_8));
		}
	});
}
        下图表示通过Channel或ChannelPipeline的通知:

        可能你想从ChannelPipeline的指定位置开始,不想流经整个ChannelPipeline,如下情况:
  • 为了节省开销,不感兴趣的ChannelHandler不让通过
  • 排除一些ChannelHandler
        在这种情况下,你可以使用ChannelHandlerContext的ChannelHandler通知起点。它使用ChannelHandlerContext执行下一个ChannelHandler。下面代码显示了直接使用ChannelHandlerContext操作:
// Get reference of ChannelHandlerContext
ChannelHandlerContext ctx = ..;
// Write buffer via ChannelHandlerContext
ctx.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));
该消息流经ChannelPipeline到下一个ChannelHandler,在这种情况下使用ChannelHandlerContext开始下一个ChannelHandler。下图显示了事件流:

如上图显示的,从指定的ChannelHandlerContext开始,跳过前面所有的ChannelHandler,使用ChannelHandlerContext操作是常见的模式,最常用的是从ChannelHanlder调用操作,也可以在外部使用ChannelHandlerContext,因为这是线程安全的。
6.2.2 修改ChannelPipeline
        调用ChannelHandlerContext的pipeline()方法能访问ChannelPipeline,能在运行时动态的增加、删除、替换ChannelPipeline中的ChannelHandler。可以保持ChannelHandlerContext供以后使用,如外部Handler方法触发一个事件,甚至从一个不同的线程。
        下面代码显示了保存ChannelHandlerContext供之后使用或其他线程使用:
public class WriteHandler extends ChannelHandlerAdapter {
	private ChannelHandlerContext ctx;

	@Override
	public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
		this.ctx = ctx;
	}
	
	public void send(String msg){
		ctx.write(msg);
	}
}
        请注意,ChannelHandler实例如果带有@Sharable注解则可以被添加到多个ChannelPipeline。也就是说单个ChannelHandler实例可以有多个ChannelHandlerContext,因此可以调用不同ChannelHandlerContext获取同一个ChannelHandler。如果添加不带@Sharable注解的ChannelHandler实例到多个ChannelPipeline则会抛出异常;使用@Sharable注解后的ChannelHandler必须在不同的线程和不同的通道上安全使用。怎么是不安全的使用?看下面代码:
@Sharable
public class NotSharableHandler extends ChannelInboundHandlerAdapter {

	private int count;

	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		count++;
		System.out.println("channelRead(...) called the " + count + " time“");
		ctx.fireChannelRead(msg);
	}
	
}
上面是一个带@Sharable注解的Handler,它被多个线程使用时,里面count是不安全的,会导致count值错误。
        为什么要共享ChannelHandler?使用@Sharable注解共享一个ChannelHandler在一些需求中还是有很好的作用的,如使用一个ChannelHandler来统计连接数或来处理一些全局数据等等。

6.3 状态模型

        Netty有一个简单但强大的状态模型,并完美映射到ChannelInboundHandler的各个方法。下面是Channel生命周期四个不同的状态:
  • channelUnregistered
  • channelRegistered
  • channelActive
  • channelInactive
Channel的状态在其生命周期中变化,因为状态变化需要触发,下图显示了Channel状态变化:

        还可以看到额外的状态变化,因为用户允许从EventLoop中注销Channel暂停事件执行,然后再重新注册。在这种情况下,你会看到多个channelRegistered和channelUnregistered状态的变化,而永远只有一个channelActive和channelInactive的状态,因为一个通道在其生命周期内只能连接一次,之后就会被回收;重新连接,则是创建一个新的通道。
        下图显示了从EventLoop中注销Channel后再重新注册的状态变化:

6.4 ChannelHandler和其子类

        Netty中有3个实现了ChannelHandler接口的类,其中2个是接口,一个是抽象类。如下图:

6.4.1 ChannelHandler中的方法
        Netty定义了良好的类型层次结构来表示不同的处理程序类型,所有的类型的父类是ChannelHandler。ChannelHandler提供了在其生命周期内添加或从ChannelPipeline中删除的方法。
  • handlerAdded,ChannelHandler添加到实际上下文中准备处理事件
  • handlerRemoved,将ChannelHandler从实际上下文中删除,不再处理事件
  • exceptionCaught,处理抛出的异常
上面三个方法都需要传递ChannelHandlerContext参数,每个ChannelHandler被添加到ChannelPipeline时会自动创建ChannelHandlerContext。ChannelHandlerContext允许在本地通道安全的存储和检索值。Netty还提供了一个实现了ChannelHandler的抽象类:ChannelHandlerAdapter。ChannelHandlerAdapter实现了父类的所有方法,基本上就是传递事件到ChannelPipeline中的下一个ChannelHandler直到结束。
6.4.2 ChannelInboundHandler
        ChannelInboundHandler提供了一些方法再接收数据或Channel状态改变时被调用。下面是ChannelInboundHandler的一些方法:
  • channelRegistered,ChannelHandlerContext的Channel被注册到EventLoop;
  • channelUnregistered,ChannelHandlerContext的Channel从EventLoop中注销
  • channelActive,ChannelHandlerContext的Channel已激活
  • channelInactive,ChannelHanderContxt的Channel结束生命周期
  • channelRead,从当前Channel的对端读取消息
  • channelReadComplete,消息读取完成后执行
  • userEventTriggered,一个用户事件被处罚
  • channelWritabilityChanged,改变通道的可写状态,可以使用Channel.isWritable()检查
  • exceptionCaught,重写父类ChannelHandler的方法,处理异常
        Netty提供了一个实现了ChannelInboundHandler接口并继承ChannelHandlerAdapter的类:ChannelInboundHandlerAdapter。ChannelInboundHandlerAdapter实现了ChannelInboundHandler的所有方法,作用就是处理消息并将消息转发到ChannelPipeline中的下一个ChannelHandler。ChannelInboundHandlerAdapter的channelRead方法处理完消息后不会自动释放消息,若想自动释放收到的消息,可以使用SimpleChannelInboundHandler<I>。
        看下面代码:
/**
 * 实现ChannelInboundHandlerAdapter的Handler,不会自动释放接收的消息对象
 * @author c.k
 *
 */
public class DiscardHandler extends ChannelInboundHandlerAdapter {
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
		//手动释放消息
		ReferenceCountUtil.release(msg);
	}
}
/**
 * 继承SimpleChannelInboundHandler,会自动释放消息对象
 * @author c.k
 *
 */
public class SimpleDiscardHandler extends SimpleChannelInboundHandler<Object> {
	@Override
	protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
		//不需要手动释放
	}
}
        如果需要其他状态改变的通知,可以重写Handler的其他方法。通常自定义消息类型来解码字节,可以实现ChannelInboundHandler或ChannelInboundHandlerAdapter。有一个更好的解决方法,使用编解码器的框架可以很容的实现。使用ChannelInboundHandler、ChannelInboundHandlerAdapter、SimpleChannelInboundhandler这三个中的一个来处理接收消息,使用哪一个取决于需求;大多数时候使用SimpleChannelInboundHandler处理消息,使用ChannelInboundHandlerAdapter处理其他的“入站”事件或状态改变。
        ChannelInitializer用来初始化ChannelHandler,将自定义的各种ChannelHandler添加到ChannelPipeline中。
6.4.3 ChannelOutboundHandler
        ChannelOutboundHandler用来处理“出站”的数据消息。ChannelOutboundHandler提供了下面一些方法:
  • bind,Channel绑定本地地址
  • connect,Channel连接操作
  • disconnect,Channel断开连接
  • close,关闭Channel
  • deregister,注销Channel
  • read,读取消息,实际是截获ChannelHandlerContext.read()
  • write,写操作,实际是通过ChannelPipeline写消息,Channel.flush()属性到实际通道
  • flush,刷新消息到通道
        ChannelOutboundHandler是ChannelHandler的子类,实现了ChannelHandler的所有方法。所有最重要的方法采取ChannelPromise,因此一旦请求停止从ChannelPipeline转发参数则必须得到通知。Netty提供了ChannelOutboundHandler的实现:ChannelOutboundHandlerAdapter。ChannelOutboundHandlerAdapter实现了父类的所有方法,并且可以根据需要重写感兴趣的方法。所有这些方法的实现,在默认情况下,都是通过调用ChannelHandlerContext的方法将事件转发到ChannelPipeline中下一个ChannelHandler。
        看下面的代码:
public class DiscardOutboundHandler extends ChannelOutboundHandlerAdapter {
	@Override
	public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
		ReferenceCountUtil.release(msg);
		promise.setSuccess();
	}
}
        重要的是要记得释放致远并直通ChannelPromise,若ChannelPromise没有被通知可能会导致其中一个ChannelFutureListener不被通知去处理一个消息。
        如果消息被消费并且没有被传递到ChannelPipeline中的下一个ChannelOutboundHandler,那么就需要调用ReferenceCountUtil.release(message)来释放消息资源。一旦消息被传递到实际的通道,它会自动写入消息或在通道关闭是释放。

<think>首先,用户的问题是关于解决seata依赖引入时出现的一个错误:报io.netty:netty-transport-native-kqueue:jar:osx-aarch_64:4.1.92.Final问题。错误信息表明在构建过程中找不到这个特定的artifact。 从站内引用来看: - 引用[1]讨论了Netty的tcnative.classifier问题,提到需要将classifier改为windows-x86_64,并确保环境是64位。这暗示了classifier的设置在解决依赖问题中的重要性。 - 引用[2]是关于Java集合的内容,似乎与当前问题无关,可以忽略。 用户上一次的问题(在系统级指令中)是关于快速排序的,但当前问题是一个全新的依赖问题,所以我需要直接关注用户当前的查询。 关键错误信息是:"io.netty:netty-transport-native-kqueue:jar:osx-aarch_64:4.1.92.Final"。这看起来是针对macOS的特定架构(aarch_64,即ARM64)的依赖问题。用户可能是使用macOS with Apple Silicon(如M1/M2芯片),在引入seata依赖时遇到了这个依赖缺失的问题。 解决这类依赖问题的常见方法: 1. **检查依赖版本兼容性**:确保seata版本与Netty版本兼容。seata可能需要特定版本的Netty。 2. **添加classifier或排除依赖**:类似于引用[1]中的建议,可能需要手动指定classifier或排除不必要的native依赖。 3. **配置Maven仓库**:确保Maven可以访问正确的仓库,如Maven Central或公司内部仓库。 4. **本地编译或下载**:如果artifact缺失,可能需要手动下载或编译。 5. **使用正确的classifier**:错误中提到了osx-aarch_64,这可能是针对Apple Silicon的。如果用户的环境不是这个架构,可能需要指定不同的classifier。 具体到这个错误: - `netty-transport-native-kqueue`是Netty的一个模块,用于支持kqueue事件通知机制,主要在BSD-like系统(如macOS)上使用。 - `osx-aarch_64`表示macOS on ARM64 architecture。 - 错误是找不到这个artifact,可能是因为在仓库中不存在,或者用户的环境需要配置。 可能的解决方案: - 在pom.xml中,为Netty依赖添加classifier,或者排除这个特定的native dependency。 - 由于seata可能依赖于Netty,我需要查看seata的官方文档或常见问题来解决这个问题。 - 参考引用[1],它提到在Netty tcnative的问题中,需要设置classifier为windows-x86_64,这里类似,可能需要指定classifier为适合用户环境的版本。 用户的环境:用户提到引用[1]中有windows-x86_64的讨论,但目前错误是针对macOS的(osx-aarch_64),所以用户可能在macOS上开发。我需要假设用户使用的是macOS Apple Silicon。 标准步骤: 1. **添加依赖时指定classifier**:在pom.xml中,对于Netty依赖,添加classifier属性。 2. **排除不必要的依赖**:在seata的依赖中,排除掉native transport依赖。 3. **检查依赖树**:使用Maven命令(如`mvn dependency:tree`)查看依赖关系。 4. **更新版本**:确保使用的seata和Netty版本兼容。 具体到seata:seata是一个分布式事务解决方案,它依赖于Netty。我回想或假设常见问题中,可能需要添加特定的classifier。 在回答中,我需要: - 使用中文,因为系统指令要求。 - 结构清晰,逐步解决问题。 - 引用添加标识,如[^1]。 - 生成相关问题。 - 遵守格式:行内数学表达式用$...$,独立公式用$$...$$(但这里可能不涉及数学公式)。 步骤建议: 1. 确认问题:描述错误。 2. 分析原因:为什么会出现这个错误。 3. 提供解决方案: - Option 1: 在pom.xml中指定正确的classifier。 - Option 2: 排除掉这个依赖。 - Option 3: 确保Maven仓库配置正确。 4. 示例代码:给出pom.xml的修改片段。 5. 验证步骤:如何测试是否解决。 可能的解决方案代码: ```xml <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>your_version</version> <exclusions> <exclusion> <groupId>io.netty</groupId> <artifactId>netty-transport-native-kqueue</artifactId> <classifier>osx-aarch_64</classifier> </exclusion> </exclusions> </dependency> ``` 或者添加依赖时指定classifier。 但更常见的是排除它,因为如果不需要这个native绑定,可以排除以避免错误。 另一个思路:如果用户确实需要这个依赖,但它在仓库中缺失,可能需要添加额外的仓库或手动安装。 然而,在引用[1]中,它提到编译Netty源码来生成类文件,这可能适用于这里。 但是,最直接的是排除这个依赖。 生成相关问题: 基于这个错误,相关问题可以是: 1. 如何排查Maven依赖冲突? 2. 如何在Maven中排除特定依赖? 3. seata的常见依赖问题有哪些? 4. 如何配置Maven仓库来解决依赖缺失? 确保回答真实可靠:我需要基于一般知识。我不是seata专家,但可以从Maven和Netty角度给出通用建议。 最终回答结构: - 开头确认问题。 - 分析原因。 - 列出解决方案步骤。 - 提供示例代码。 - 结尾添加相关问题。 </think>### 解决seata依赖引入报`io.netty:netty-transport-native-kqueue:jar:osx-aarch_64:4.1.92.Final`错误的方法 此错误通常在macOS(尤其是Apple Silicon架构)环境下引入seata依赖时发生,原因是对特定操作系统架构的Netty native依赖缺失或版本不兼容。以下是详细解决方案: --- ### **问题分析** - **错误根源**: - `netty-transport-native-kqueue`是Netty的Native Transport模块,用于优化macOS系统的网络性能。 - `osx-aarch_64`表示针对Apple Silicon芯片(如M1/M2)的架构。 - 当Maven仓库中缺少该架构的artifact或JDK/IDE环境不匹配时,会报此错误[^1]。 - **常见场景**: - 使用macOS Apple Silicon设备开发。 - seata依赖的Netty版本与本地环境冲突。 - Maven未正确配置仓库或依赖。 --- ### **解决方案步骤** #### **1. 检查并确保环境兼容性** - **JDK版本**:使用64位JDK,确保与macOS架构匹配。验证命令: ```bash java -version # 输出应包含"64-Bit Server VM" ``` - **IDE**:如IntelliJ或Eclipse,必须为64位版本(Apple Silicon原生版本更佳)。 - **操作系统**:确认macOS为ARM64架构(Apple Silicon),可通过终端运行`uname -m`,输出应为`arm64`。 #### **2. 在pom.xml中排除冲突依赖** - 在seata的依赖声明中添加`<exclusions>`,排除特定native依赖: ```xml <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <!-- 或您使用的seata artifact --> <version>1.7.1</version> <!-- 替换为您的seata版本 --> <exclusions> <exclusion> <groupId>io.netty</groupId> <artifactId>netty-transport-native-kqueue</artifactId> <classifier>osx-aarch_64</classifier> <!-- 指定classifier避免匹配错误 --> </exclusion> </exclusions> </dependency> ``` - **为何有效**:seata间接依赖Netty,排除后强制Maven使用通用版本,避免架构校验失败[^1]。 #### **3. 手动添加Netty依赖并指定classifier** - 显式添加兼容的Netty依赖,匹配本地架构: ```xml <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport-native-kqueue</artifactId> <version>4.1.92.Final</version> <!-- 与错误中的版本一致 --> <classifier>osx-aarch_64</classifier> <!-- 关键:指定classifier --> </dependency> ``` - **注意**:若Maven Central无此artifact: - 添加Netty仓库到pom.xml: ```xml <repositories> <repository> <id>netty-releases</id> <url>https://repo1.maven.org/maven2/</url> </repository> </repositories> ``` #### **4. 降级或升级Netty版本** - **降级**:若seata版本允许,使用低版本Netty(如4.1.86.Final),避免高版本兼容性问题。 ```xml <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.86.Final</version> </dependency> ``` - **升级**:更新seata至最新版(如1.8.0),可能已修复依赖冲突。 #### **5. 本地编译Netty(可选)** - 若上述无效,参考引用[1]方法编译Netty源码: 1. 克隆Netty仓库: ```bash git clone https://github.com/netty/netty.git ``` 2. 进入`netty-transport-native-kqueue`模块目录: ```bash cd netty/transport-native-unix-common ``` 3. 执行Maven编译: ```bash mvn clean compile -DskipTests ``` 4. 安装到本地仓库: ```bash mvn install -DskipTests ``` - **效果**:生成的artifact将包含`osx-aarch_64` classifier,供Maven本地解析[^1]。 --- ### **验证步骤** 1. 清理Maven缓存: ```bash mvn clean install -U ``` 2. 重新构建项目,确认错误是否消失。 3. 运行简单测试,验证seata功能正常。 --- ### **常见问题预防** - **依赖树分析**:使用`mvn dependency:tree`检查冲突,优先解决版本不一致。 - **环境对齐**:确保JDK、IDE、操作系统均为64位ARM架构。 - **仓库镜像**:在企业内网中,配置Nexus镜像加速依赖下载。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值