跳过导航

4.0 中的新增功能和值得注意的更改

您是否知道此页面是根据 Github Wiki 页面 自动生成的?您可以在 此处 自行改进!

本文档将引导您了解 Netty 主要版本中值得注意的更改和新增功能列表,以便您了解如何将应用程序移植到新版本。

项目结构更改

我们不再是 JBoss.org 的一部分 以来,Netty 的包名已从 org.jboss.netty 更改为 io.netty

二进制 JAR 已被拆分为多个子模块,以便用户可以从类路径中排除不必要的特性。当前结构如下所示

工件 ID 说明
netty-parent Maven 父 POM
netty-common 实用类和日志门面
netty-buffer 替换 java.nio.ByteBufferByteBuf API
netty-transport Channel API 和核心传输
netty-transport-rxtx Rxtx 传输
netty-transport-sctp SCTP 传输
netty-transport-udt UDT 传输
netty-handler 有用的 ChannelHandler 实现
netty-codec 编解码框架,用于编写编码器和解码器
netty-codec-http 与 HTTP、Web Sockets、SPDY 和 RTSP 相关的编解码器
netty-codec-socks 与 SOCKS 协议相关的编解码器
netty-all 将上述所有工件组合在一起的一体化 JAR
netty-tarball Tarball 分发
netty-example 示例
netty-testsuite-* 集成测试集合
netty-microbench 微基准

所有工件(netty-all.jar 除外)现在都是 OSGi 捆绑包,可以在你最喜欢的 OSGi 容器中使用。

通用 API 更改

  • Netty 中的大多数操作现在都支持方法链接,以提高简洁性。
  • 非配置 getter 不再带有 get- 前缀。(例如,Channel.getRemoteAddress()Channel.remoteAddress()
    • 布尔属性仍带有 is- 前缀,以避免混淆(例如,“empty”既是形容词又是动词,因此 empty() 可以有两个含义)。
  • 有关 4.0 CR4 和 4.0 CR5 之间的 API 更改,请参阅 Netty 4.0.0.CR5 发布,带有全新的 API

缓冲区 API 更改

ChannelBufferByteBuf

由于上述结构更改,缓冲区 API 可用作单独的包。即使你对采用 Netty 作为网络应用程序框架不感兴趣,你仍然可以使用我们的缓冲区 API。因此,类型名称 ChannelBuffer 不再有意义,并已重命名为 ByteBuf

用于创建新缓冲区的实用程序类 ChannelBuffers 已拆分为两个实用程序类,即 UnpooledByteBufUtil。正如其名称 Unpooled 所示,4.0 引入了可以通过 ByteBufAllocator 实现进行分配的池化 ByteBuf

ByteBuf 不是接口,而是抽象类

根据我们的内部性能测试,将 ByteBuf 从接口转换为抽象类将整体吞吐量提高了约 5%。

大多数缓冲区都是动态的,具有最大容量

在 3.x 中,缓冲区是固定的或动态的。固定缓冲区的容量在创建后不会改变,而动态缓冲区的容量在其 write*(...) 方法需要更多空间时会改变。

自 4.0 起,所有缓冲区都是动态的。但是,它们比旧的动态缓冲区更好。您可以更轻松、更安全地减少或增加缓冲区的容量。这很容易,因为有一个新方法 ByteBuf.capacity(int newCapacity)。这是安全的,因为您可以设置缓冲区的最大容量,以便它不会无限增长。

// No more dynamicBuffer() - use buffer().
ByteBuf buf = Unpooled.buffer();

// Increase the capacity of the buffer.
buf.capacity(1024);
...

// Decrease the capacity of the buffer (the last 512 bytes are deleted.)
buf.capacity(512);

唯一的例外是包装单个缓冲区或单个字节数组的缓冲区,由 wrappedBuffer() 创建。您不能增加其容量,因为它会使包装现有缓冲区的全部目的失效——节省内存副本。如果您想在包装缓冲区后更改容量,您应该创建一个具有足够容量的新缓冲区,并复制您想要包装的缓冲区。

新的缓冲区类型:CompositeByteBuf

名为 CompositeByteBuf 的新缓冲区实现为复合缓冲区实现定义了各种高级操作。用户可以使用复合缓冲区节省大量内存复制操作,但代价是随机访问相对昂贵。要创建新的复合缓冲区,请像以前一样使用 Unpooled.wrappedBuffer(...)Unpooled.compositeBuffer(...)ByteBufAllocator.compositeBuffer()

可预测的 NIO 缓冲区转换

ChannelBuffer.toByteBuffer() 及其变体的协定在 3.x 中不够确定。用户不可能知道它们会返回带有共享数据的视图缓冲区还是带有单独数据的复制缓冲区。4.0 用 ByteBuf.nioBufferCount()nioBuffer()nioBuffers() 替换了 toByteBuffer()。如果 nioBufferCount() 返回 0,用户始终可以通过调用 copy().nioBuffer() 获取复制缓冲区。

小端序支持更改

小端序支持已发生重大改变。以前,用户应该指定 LittleEndianHeapChannelBufferFactory 或包装具有所需字节顺序的现有缓冲区以获取小端序缓冲区。4.0 添加了一个新方法:ByteBuf.order(ByteOrder)。它返回具有所需字节顺序的被调用者的视图

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.nio.ByteOrder;
 
ByteBuf buf = Unpooled.buffer(4);
buf.setInt(0, 1);
// Prints '00000001'
System.out.format("%08x%n", buf.getInt(0)); 
 
ByteBuf leBuf = buf.order(ByteOrder.LITTLE_ENDIAN);
// Prints '01000000'
System.out.format("%08x%n", leBuf.getInt(0));
 
assert buf != leBuf;
assert buf == buf.order(ByteOrder.BIG_ENDIAN);

池化缓冲区

Netty 4 引入了高性能缓冲区池,它是 jemalloc 的一个变体,它结合了 伙伴分配slab 分配。它提供了以下好处

  • 减少了缓冲区频繁分配和释放造成的 GC 压力
  • 减少了在创建新缓冲区时消耗的内存带宽,而新缓冲区不可避免地必须用零填充
  • 及时释放直接缓冲区

要利用此功能,除非用户希望获取一个未池化的缓冲区,否则他或她应该从 ByteBufAllocator 获取一个缓冲区

Channel channel = ...;
ByteBufAllocator alloc = channel.alloc();
ByteBuf buf = alloc.buffer(512);
....
channel.write(buf);
 
ChannelHandlerContext ctx = ...
ByteBuf buf2 = ctx.alloc().buffer(512);
....
channel.write(buf2)

一旦 ByteBuf 被写入到远程对等方,它将自动返回到它最初所在的池。

默认的 ByteBufAllocatorPooledByteBufAllocator。如果您不希望使用缓冲区池化或使用您自己的分配器,请使用 Channel.config().setAllocator(...) 以及一个备用分配器,例如 UnpooledByteBufAllocator

注意:目前,默认分配器是 UnpooledByteBufAllocator。一旦我们确保 PooledByteBufAllocator 中没有内存泄漏,我们将再次默认使用它。

ByteBuf 始终是引用计数的

为了以更可预测的方式控制 ByteBuf 的生命周期,Netty 不再依赖垃圾回收器,而是采用显式的引用计数器。以下是基本规则

  • 当分配一个缓冲区时,其初始引用计数为 1。
  • 如果缓冲区的引用计数减少到 0,则释放它或将其返回到它最初所在的池。
  • 以下尝试将触发 IllegalReferenceCountException
    • 访问引用计数为 0 的缓冲区,
    • 将引用计数减少到负值,或
    • 将引用计数增加到 Integer.MAX_VALUE 之外。
  • 派生缓冲区(例如切片和副本)和交换缓冲区(即小端缓冲区)与它派生的缓冲区共享引用计数。请注意,在创建派生缓冲区时,引用计数不会改变。

ByteBufChannelPipeline 中使用时,您需要记住其他规则

  • 管道中的每个入站(又名上游)处理程序都必须释放收到的消息。Netty 不会自动为您释放它们。
    • 请注意,编解码框架会自动释放消息,如果用户希望按原样将消息传递给下一个处理程序,则必须增加引用计数。
  • 当一个出站(又名下游)消息到达管道开头时,Netty 将在写出它之后释放它。

自动缓冲区泄漏检测

尽管引用计数非常强大,但它也容易出错。为了帮助用户找到他或她忘记释放缓冲区的位置,泄漏检测器会自动记录泄漏缓冲区分配位置的堆栈跟踪。

由于泄漏检测器依赖于 PhantomReference 且获取堆栈跟踪是一项非常昂贵的操作,因此它仅对大约 1% 的分配进行采样。因此,最好运行应用程序相当长的时间以查找所有可能的泄漏。

一旦找到并修复所有泄漏。您可以通过指定 -Dio.netty.noResourceLeakDetection JVM 选项来关闭此功能,以完全消除其运行时开销。

io.netty.util.concurrent

除了新的独立缓冲区 API,4.0 还提供了各种构造,这些构造对于在名为 io.netty.util.concurrent 的新包中编写异步应用程序非常有用。其中一些构造包括

  • FuturePromise - 类似于 ChannelFuture,但与 Channel 无关
  • EventExecutorEventExecutorGroup - 通用事件循环 API

它们用作通道 API 的基础,将在本文档后面进行解释。例如,ChannelFuture 扩展了 io.netty.util.concurrent.FutureEventLoopGroup 扩展了 EventExecutorGroup

Event loop type hierarchy diagram

通道 API 更改

在 4.0 中,io.netty.channel 包下的许多类都经过了重大修改,因此简单的文本搜索和替换不会使您的 3.x 应用程序与 4.0 一起工作。本节将尝试展示如此重大更改背后的思考过程,而不是成为所有更改的详尽资源。

改进的 ChannelHandler 接口

上行 → 入站,下行 → 出站

对于初学者来说,“上行”和“下行”这两个术语非常令人困惑。4.0 尽可能使用“入站”和“出站”。

新的 ChannelHandler 类型层次结构

在 3.x 中,ChannelHandler 只是一个标记接口,而 ChannelUpstreamHandlerChannelDownstreamHandlerLifeCycleAwareChannelHandler 定义了实际的处理程序方法。在 Netty 4 中,ChannelHandler 合并了 LifeCycleAwareChannelHandler 以及对入站和出站处理程序都有用的其他几个方法

public interface ChannelHandler {
    void handlerAdded(ChannelHandlerContext ctx) throws Exception;
    void handlerRemoved(ChannelHandlerContext ctx) throws Exception;
    void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}

下图描述了新的类型层次结构

ChannelHandler type hierarchy diagram

没有事件对象的 ChannelHandler

在 3.x 中,每个 I/O 操作都会创建一个 ChannelEvent 对象。对于每次读/写,它还会创建一个新的 ChannelBuffer。它极大地简化了 Netty 的内部结构,因为它将资源管理和缓冲池委托给了 JVM。但是,它通常是 GC 压力和不确定性的根源,有时在高负载下的基于 Netty 的应用程序中会观察到这些问题。

4.0 通过用强类型方法调用替换事件对象几乎完全删除了事件对象创建。3.x 具有 catch-all 事件处理程序方法,例如 handleUpstream()handleDownstream(),但这不再是这种情况。现在每种事件类型都有自己的处理程序方法

// Before:
void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e);
void handleDownstream(ChannelHandlerContext ctx, ChannelEvent e);
 
// After:
void channelRegistered(ChannelHandlerContext ctx);
void channelUnregistered(ChannelHandlerContext ctx);
void channelActive(ChannelHandlerContext ctx);
void channelInactive(ChannelHandlerContext ctx);
void channelRead(ChannelHandlerContext ctx, Object message);
 
void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise);
void connect(
        ChannelHandlerContext ctx, SocketAddress remoteAddress,
        SocketAddress localAddress, ChannelPromise promise);
void disconnect(ChannelHandlerContext ctx, ChannelPromise promise);
void close(ChannelHandlerContext ctx, ChannelPromise promise);
void deregister(ChannelHandlerContext ctx, ChannelPromise promise);
void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise);
void flush(ChannelHandlerContext ctx);
void read(ChannelHandlerContext ctx);

ChannelHandlerContext 也已更改以反映上述更改

// Before:
ctx.sendUpstream(evt);
 
// After:
ctx.fireChannelRead(receivedMessage);

所有这些更改意味着用户无法再扩展不存在的 ChannelEvent 接口。那么用户如何定义他或她自己的事件类型,例如 IdleStateEvent?4.0 中的 ChannelHandlerContext 具有一个 fireUserEventTriggered 方法,用于触发自定义事件,而 ChannelInboundHandler 现在有一个名为 userEventTriggered() 的处理程序方法,专门用于处理自定义事件的特定用户案例。

简化的通道状态模型

在 3.x 中创建新的已连接 Channel 时,至少会触发三个 ChannelStateEventchannelOpenchannelBoundchannelConnected。当 Channel 关闭时,至少会触发 3 个以上:channelDisconnectedchannelUnboundchannelClosed

Netty 3 Channel state diagram

但是,触发那么多事件的价值是可疑的。当 Channel 进入可以执行读写操作的状态时,用户收到通知会更有用。

Netty 4 Channel state diagram

channelOpenchannelBoundchannelConnected 已合并到 channelActivechannelDisconnectedchannelUnboundchannelClosed 已合并到 channelInactive。同样,Channel.isBound()isConnected() 已合并到 isActive()

请注意,channelRegisteredchannelUnregistered 不等同于 channelOpenchannelClosed。它们是引入的新状态,用于支持 Channel 的动态注册、注销和重新注册,如下所示

Netty 4 Channel state diagram for re-registration

write() 不会自动刷新

4.0 引入了一个名为 flush() 的新操作,该操作显式刷新 Channel 的出站缓冲区,而 write() 操作不会自动刷新。你可以将其视为 java.io.BufferedOutputStream,只不过它在消息级别上工作。

由于此更改,你必须非常小心,不要忘记在写入内容后调用 ctx.flush()。或者,你可以使用快捷方法 writeAndFlush()

明智且不易出错的入站流量暂停

3.x 有一个由 Channel.setReadable(boolean) 提供的非直观的入站流量暂停机制。它引入了 ChannelHandlers 之间的复杂交互,如果实现不正确,则处理程序很容易相互干扰。

在 4.0 中,添加了一个名为 read() 的新出站操作。如果你使用 Channel.config().setAutoRead(false) 关闭默认自动读取标志,则 Netty 将不会读取任何内容,直到你显式调用 read() 操作。一旦你发出的 read() 操作完成并且通道再次停止读取,将触发一个名为 channelReadSuspended() 的入站事件,以便你可以重新发出另一个 read() 操作。你还可以拦截 read() 操作以执行更高级的流量控制。

暂停接受传入连接

用户无法告知 Netty 3.x 停止接受传入连接,除非阻塞 I/O 线程或关闭服务器套接字。4.0 尊重未设置自动读取标志时的 read() 操作,就像普通通道一样。

半关闭套接字

TCP 和 SCTP 允许用户关闭套接字的出站流量,而无需完全关闭它。这样的套接字称为“半关闭套接字”,用户可以通过调用 SocketChannel.shutdownOutput() 方法来创建一个半关闭套接字。如果远程对等方关闭出站流量,SocketChannel.read(..) 将返回 -1,这似乎与关闭连接无法区分。

3.x 没有 shutdownOutput() 操作。此外,当 SocketChannel.read(..) 返回 -1 时,它总是关闭连接。

为了支持半关闭套接字,4.0 添加了 SocketChannel.shutdownOutput() 方法,用户可以设置“ALLOW_HALF_CLOSUREChannelOption,以防止 Netty 在 SocketChannel.read(..) 返回 -1 时自动关闭连接。

灵活的 I/O 线程分配

在 3.x 中,ChannelChannelFactory 创建,新创建的 Channel 会自动注册到隐藏的 I/O 线程。4.0 用一个名为 EventLoopGroup 的新接口替换了 ChannelFactory,该接口由一个或多个 EventLoop 组成。此外,不会自动将新的 Channel 注册到 EventLoopGroup,但用户必须显式调用 EventLoopGroup.register()

由于此更改(即 ChannelFactory 和 I/O 线程分离),用户可以将不同的 Channel 实现注册到同一个 EventLoopGroup,或将同一个 Channel 实现注册到不同的 EventLoopGroup。例如,您可以在同一个 I/O 线程中运行 NIO 服务器套接字、NIO 客户端套接字、NIO UDP 套接字和虚拟机内部本地通道。这在编写需要最小延迟的代理服务器时非常有用。

从现有 JDK 套接字创建 Channel 的能力

3.x 没有提供从现有 JDK 套接字(如 java.nio.channels.SocketChannel)创建新 Channel 的方法。您可以使用 4.0。

从 I/O 线程取消注册并重新注册一个 Channel

一旦在 3.x 中创建了一个新的 Channel,它就会完全绑定到一个 I/O 线程,直到其底层套接字关闭。在 4.0 中,用户可以从其 I/O 线程取消注册一个 Channel,以完全控制其底层 JDK 套接字。例如,您可以利用 Netty 提供的高级非阻塞 I/O 来处理复杂协议,然后取消注册 Channel 并切换到阻塞模式,以尽可能高的吞吐量传输文件。当然,可以重新注册已取消注册的 Channel

java.nio.channels.FileChannel myFile = ...;
java.nio.channels.SocketChannel mySocket = java.nio.channels.SocketChannel.open();
 
// Perform some blocking operation here.
...
 
// Netty takes over.
SocketChannel ch = new NioSocketChannel(mySocket);
EventLoopGroup group = ...;
group.register(ch);
...
 
// Deregister from Netty.
ch.deregister().sync();
 
// Perform some blocking operation here.
mySocket.configureBlocking(true);
myFile.transferFrom(mySocket, ...);
 
// Register back again to another event loop group.
EventLoopGroup anotherGroup = ...;
anotherGroup.register(ch);

安排由 I/O 线程运行的任意任务

当一个 Channel 注册到一个 EventLoopGroup 时,Channel 实际上注册到由 EventLoopGroup 管理的一个 EventLoopEventLoop 实现 java.util.concurrent.ScheduledExecutorService。这意味着用户可以在用户的通道所属的 I/O 线程中执行或安排一个任意 RunnableCallable。随着新的明确定义的线程模型(稍后将解释),编写线程安全处理程序变得极其容易。

public class MyHandler extends ChannelOutboundHandlerAdapter {
    ...
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise p) {
        ...
        ctx.write(msg, p);
        
        // Schedule a write timeout.
        ctx.executor().schedule(new MyWriteTimeoutTask(p), 30, TimeUnit.SECONDS);
        ...
    }
}
 
public class Main {
    public static void main(String[] args) throws Exception {
        // Run an arbitrary task from an I/O thread.
        Channel ch = ...;
        ch.executor().execute(new Runnable() { ... });
    }
}

简化关闭

不再有 releaseExternalResources()。你可以立即关闭所有打开的通道,并通过调用 EventLoopGroup.shutdownGracefully() 使所有 I/O 线程停止自身。

类型安全的 ChannelOption

在 Netty 中有两种方法可以配置 Channel 的套接字参数。一种方法是显式调用 ChannelConfig 的设置器,例如 SocketChannelConfig.setTcpNoDelay(true)。这是最类型安全的方法。另一种方法是调用 ChannelConfig.setOption() 方法。有时你必须在运行时确定要配置哪些套接字选项,这种方法适用于此类情况。然而,在 3.x 中它容易出错,因为用户必须将选项指定为字符串和对象的配对。如果用户使用错误的选项名称或值,他或她将遇到 ClassCastException,或者指定的选项甚至可能被静默忽略。

4.0 引入了名为 ChannelOption 的新类型,它提供对套接字选项的类型安全访问。

ChannelConfig cfg = ...;
 
// Before:
cfg.setOption("tcpNoDelay", true);
cfg.setOption("tcpNoDelay", 0);  // Runtime ClassCastException
cfg.setOption("tcpNoDelays", true); // Typo in the option name - ignored silently
 
// After:
cfg.setOption(ChannelOption.TCP_NODELAY, true);
cfg.setOption(ChannelOption.TCP_NODELAY, 0); // Compile error

AttributeMap

为了响应用户需求,你可以将任何对象附加到 ChannelChannelHandlerContext。已添加了一个名为 AttributeMap 的新接口,ChannelChannelHandlerContext 扩展了该接口。相反,ChannelLocalChannel.attachment 已被删除。当关联的 Channel 被垃圾回收时,属性也会被垃圾回收。

public class MyHandler extends ChannelInboundHandlerAdapter {
 
    private static final AttributeKey<MyState> STATE =
            AttributeKey.valueOf("MyHandler.state");
 
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) {
        ctx.attr(STATE).set(new MyState());
        ctx.fireChannelRegistered();
    }
 
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        MyState state = ctx.attr(STATE).get();
    }
    ...
}

新的引导 API

引导 API 已从头开始重写,尽管其目的保持不变;它执行使服务器或客户端启动并运行所需的典型步骤,这些步骤通常在样板代码中找到。

新的引导还采用了流畅的界面。

public static void main(String[] args) throws Exception {
    // Configure the server.
    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workerGroup = new NioEventLoopGroup();
    try {
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup)
         .channel(NioServerSocketChannel.class)
         .option(ChannelOption.SO_BACKLOG, 100)
         .localAddress(8080)
         .childOption(ChannelOption.TCP_NODELAY, true)
         .childHandler(new ChannelInitializer<SocketChannel>() {
             @Override
             public void initChannel(SocketChannel ch) throws Exception {
                 ch.pipeline().addLast(handler1, handler2, ...);
             }
         });
 
        // Start the server.
        ChannelFuture f = b.bind().sync();
 
        // Wait until the server socket is closed.
        f.channel().closeFuture().sync();
    } finally {
        // Shut down all event loops to terminate all threads.
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
        
        // Wait until all threads are terminated.
        bossGroup.terminationFuture().sync();
        workerGroup.terminationFuture().sync();
    }
}

ChannelPipelineFactoryChannelInitializer

正如您在上面的示例中注意到的,ChannelPipelineFactory 不再存在。它已被 ChannelInitializer 取代,后者提供了对 ChannelChannelPipeline 配置的更多控制。

请注意,您不会自己创建新的 ChannelPipeline。在观察到迄今为止报告的许多用例后,Netty 项目团队得出结论,用户创建自己的管道实现或扩展默认实现没有任何好处。因此,ChannelPipeline 不再由用户创建。ChannelPipelineChannel 自动创建。

ChannelFutureChannelFutureChannelPromise

ChannelFuture 已拆分为 ChannelFutureChannelPromise。这不仅使异步操作的消费者和生产者的契约显式化,而且还使在链中(如过滤)使用返回的 ChannelFuture 更加安全,因为 ChannelFuture 的状态无法更改。

由于此更改,现在某些方法接受 ChannelPromise 而不是 ChannelFuture 来修改其状态。

定义明确的线程模型

3.x 中没有定义明确的线程模型,尽管曾尝试在 3.5 中修复其不一致性。4.0 定义了一个严格的线程模型,可帮助用户编写 ChannelHandler,而无需过多担心线程安全性。

  • 除非 ChannelHandler 使用 @Sharable 注释,否则 Netty 绝不会并发调用 ChannelHandler 的方法。这与处理程序方法的类型无关 - 入站、出站或生命周期事件处理程序方法。
    • 用户不再需要同步入站或出站事件处理程序方法。
    • 4.0 禁止多次添加 ChannelHandler,除非使用 @Sharable 对其进行注释。
  • Netty 调用的每个 ChannelHandler 方法调用之间始终存在 先于 关系。
    • 用户不需要定义 volatile 字段来保持处理程序的状态。
  • 用户可以在将处理程序添加到 ChannelPipeline 时指定 EventExecutor
    • 如果指定,则始终由指定的 EventExecutor 调用 ChannelHandler 的处理程序方法。
    • 如果未指定,则始终由其关联的 Channel 注册到的 EventLoop 调用处理程序方法。
  • 分配给处理程序或频道的 EventExecutorEventLoop 始终是单线程的。
    • 处理程序方法将始终由同一线程调用。
    • 如果指定了多线程的 EventExecutorEventLoop,则首先将选择其中一个线程,然后在取消注册之前一直使用所选的线程。
    • 如果同一管道中的两个处理程序被分配了不同的 EventExecutor,则它们将同时被调用。如果多个处理程序访问共享数据,则用户必须注意线程安全性,即使共享数据仅由同一管道中的处理程序访问。
  • 添加到 ChannelFutureChannelFutureListeners 始终由分配给 future 关联的 ChannelEventLoop 线程调用。
  • ChannelHandlerInvoker 可用于控制 Channel 事件的顺序。DefaultChannelHandlerInvoker 立即从 EventLoop 线程执行事件,并将来自其他线程的事件作为 Runnable 对象在 EventExecutor 上执行。请参阅以下示例,了解在从 EventLoop 线程和其他线程与 Channel 交互时可能产生的影响。(此功能已删除。请参阅 相关提交
写入顺序 - 混合 EventLoop 线程和其他线程
Channel ch = ...;
ByteBuf a, b, c = ...;

// From Thread 1 - Not the EventLoop thread
ch.write(a);
ch.write(b);

// .. some other stuff happens

// From EventLoop Thread
ch.write(c);

// The order a, b, and c will be written to the underlying transport is not well
// defined. If order is important, and this threading interaction occurs, it is
// the user's responsibility to enforce ordering.

不再有 ExecutionHandler - 它在核心之中。

当将 ChannelHandler 添加到 ChannelPipeline 时,可以指定一个 EventExecutor,以告诉管道始终通过指定的 EventExecutor 调用所添加 ChannelHandler 的处理程序方法。

Channel ch = ...;
ChannelPipeline p = ch.pipeline();
EventExecutor e1 = new DefaultEventExecutor(16);
EventExecutor e2 = new DefaultEventExecutor(8);
 
p.addLast(new MyProtocolCodec());
p.addLast(e1, new MyDatabaseAccessingHandler());
p.addLast(e2, new MyHardDiskAccessingHandler());

编解码框架更改

编解码框架中发生了实质性的内部更改,因为 4.0 要求处理程序创建并管理其缓冲区(请参阅本文档中的“按处理程序缓冲区”部分)。但是,从用户的角度来看,这些更改并不大。

  • 核心编解码类已移至 io.netty.handler.codec 包。
  • FrameDecoder 已重命名为 ByteToMessageDecoder
  • OneToOneEncoderOneToOneDecoder 已替换为 MessageToMessageEncoderMessageToMessageDecoder
  • decode()decodeLast()encode() 的方法签名略有更改,以支持泛型并删除冗余参数。

编解码器嵌入器 → EmbeddedChannel

编解码器嵌入器已被 io.netty.channel.embedded.EmbeddedChannel 替换,以便用户可以测试包括编解码器在内的任何类型的管道。

HTTP 编解码器

HTTP 解码器现在始终为每个 HTTP 消息生成多个消息对象

1       * HttpRequest / HttpResponse
0 - n   * HttpContent
1       * LastHttpContent

有关更多详细信息,请参阅更新后的 HttpSnoopServer 示例。如果您不希望处理单个 HTTP 消息的多个消息,则可以在管道中放置一个 HttpObjectAggregatorHttpObjectAggregator 将把多个消息转换成一个 FullHttpRequestFullHttpResponse

传输实现中的更改

新增了以下传输

  • OIO SCTP 传输
  • UDT 传输

案例研究:移植阶乘示例

本节展示了将阶乘示例从 3.x 移植到 4.0 的大致步骤。阶乘示例已移植到 io.netty.example.factorial 包中的 4.0。请浏览示例的源代码以查找所有更改。

移植服务器

  1. 重写 FactorialServer.run() 方法以使用新的引导 API。
  2. 不再有 ChannelFactory。自行实例化 NioEventLoopGroup(一个用于接受传入连接,另一个用于处理已接受连接)。
  3. FactorialServerPipelineFactory 重命名为 FactorialServerInitializer
  4. 使其扩展 ChannelInitializer<Channel>
  5. 获取 Channel.pipeline(),而不是创建新的 ChannelPipeline
  6. 使 FactorialServerHandler 扩展 ChannelInboundHandlerAdapter
  7. channelInactive() 替换 channelDisconnected()
  8. 不再使用 handleUpstream()。
  9. messageReceived() 重命名为 channelRead(),并相应地调整方法签名。
  10. ctx.writeAndFlush() 替换 ctx.write()
  11. 使 BigIntegerDecoder 扩展 ByteToMessageDecoder<BigInteger>
  12. 使 NumberEncoder 扩展 MessageToByteEncoder<Number>
  13. encode() 不再返回缓冲区。将编码数据填充到 ByteToMessageDecoder 提供的缓冲区中。

移植客户端

与移植服务器基本相同,但当您编写潜在的大流时,您需要小心。

  1. 重写 FactorialClient.run() 方法以使用新的引导 API。
  2. FactorialClientPipelineFactory 重命名为 FactorialClientInitializer
  3. 使 FactorialClientHandler 扩展 ChannelInboundHandlerAdapter
最后于 2024 年 7 月 19 日检索