4.0 中的新增功能和值得注意的更改
本文档将引导您了解 Netty 主要版本中值得注意的更改和新增功能列表,以便您了解如何将应用程序移植到新版本。
自 我们不再是 JBoss.org 的一部分 以来,Netty 的包名已从 org.jboss.netty
更改为 io.netty
。
二进制 JAR 已被拆分为多个子模块,以便用户可以从类路径中排除不必要的特性。当前结构如下所示
工件 ID | 说明 |
---|---|
netty-parent |
Maven 父 POM |
netty-common |
实用类和日志门面 |
netty-buffer |
替换 java.nio.ByteBuffer 的 ByteBuf 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 容器中使用。
- Netty 中的大多数操作现在都支持方法链接,以提高简洁性。
- 非配置 getter 不再带有
get-
前缀。(例如,Channel.getRemoteAddress()
→Channel.remoteAddress()
)- 布尔属性仍带有
is-
前缀,以避免混淆(例如,“empty”既是形容词又是动词,因此empty()
可以有两个含义)。
- 布尔属性仍带有
- 有关 4.0 CR4 和 4.0 CR5 之间的 API 更改,请参阅 Netty 4.0.0.CR5 发布,带有全新的 API
由于上述结构更改,缓冲区 API 可用作单独的包。即使你对采用 Netty 作为网络应用程序框架不感兴趣,你仍然可以使用我们的缓冲区 API。因此,类型名称 ChannelBuffer
不再有意义,并已重命名为 ByteBuf
。
用于创建新缓冲区的实用程序类 ChannelBuffers
已拆分为两个实用程序类,即 Unpooled
和 ByteBufUtil
。正如其名称 Unpooled
所示,4.0 引入了可以通过 ByteBufAllocator
实现进行分配的池化 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
的新缓冲区实现为复合缓冲区实现定义了各种高级操作。用户可以使用复合缓冲区节省大量内存复制操作,但代价是随机访问相对昂贵。要创建新的复合缓冲区,请像以前一样使用 Unpooled.wrappedBuffer(...)
、Unpooled.compositeBuffer(...)
或 ByteBufAllocator.compositeBuffer()
。
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
被写入到远程对等方,它将自动返回到它最初所在的池。
默认的 ByteBufAllocator
是 PooledByteBufAllocator
。如果您不希望使用缓冲区池化或使用您自己的分配器,请使用 Channel.config().setAllocator(...)
以及一个备用分配器,例如 UnpooledByteBufAllocator
。
注意:目前,默认分配器是 UnpooledByteBufAllocator
。一旦我们确保 PooledByteBufAllocator
中没有内存泄漏,我们将再次默认使用它。
为了以更可预测的方式控制 ByteBuf
的生命周期,Netty 不再依赖垃圾回收器,而是采用显式的引用计数器。以下是基本规则
- 当分配一个缓冲区时,其初始引用计数为 1。
- 如果缓冲区的引用计数减少到 0,则释放它或将其返回到它最初所在的池。
- 以下尝试将触发
IllegalReferenceCountException
- 访问引用计数为 0 的缓冲区,
- 将引用计数减少到负值,或
- 将引用计数增加到
Integer.MAX_VALUE
之外。
- 派生缓冲区(例如切片和副本)和交换缓冲区(即小端缓冲区)与它派生的缓冲区共享引用计数。请注意,在创建派生缓冲区时,引用计数不会改变。
当 ByteBuf
在 ChannelPipeline
中使用时,您需要记住其他规则
- 管道中的每个入站(又名上游)处理程序都必须释放收到的消息。Netty 不会自动为您释放它们。
- 请注意,编解码框架会自动释放消息,如果用户希望按原样将消息传递给下一个处理程序,则必须增加引用计数。
- 当一个出站(又名下游)消息到达管道开头时,Netty 将在写出它之后释放它。
尽管引用计数非常强大,但它也容易出错。为了帮助用户找到他或她忘记释放缓冲区的位置,泄漏检测器会自动记录泄漏缓冲区分配位置的堆栈跟踪。
由于泄漏检测器依赖于 PhantomReference
且获取堆栈跟踪是一项非常昂贵的操作,因此它仅对大约 1% 的分配进行采样。因此,最好运行应用程序相当长的时间以查找所有可能的泄漏。
一旦找到并修复所有泄漏。您可以通过指定 -Dio.netty.noResourceLeakDetection
JVM 选项来关闭此功能,以完全消除其运行时开销。
除了新的独立缓冲区 API,4.0 还提供了各种构造,这些构造对于在名为 io.netty.util.concurrent
的新包中编写异步应用程序非常有用。其中一些构造包括
-
Future
和Promise
- 类似于ChannelFuture
,但与Channel
无关 -
EventExecutor
和EventExecutorGroup
- 通用事件循环 API
它们用作通道 API 的基础,将在本文档后面进行解释。例如,ChannelFuture
扩展了 io.netty.util.concurrent.Future
,EventLoopGroup
扩展了 EventExecutorGroup
。
在 4.0 中,io.netty.channel
包下的许多类都经过了重大修改,因此简单的文本搜索和替换不会使您的 3.x 应用程序与 4.0 一起工作。本节将尝试展示如此重大更改背后的思考过程,而不是成为所有更改的详尽资源。
对于初学者来说,“上行”和“下行”这两个术语非常令人困惑。4.0 尽可能使用“入站”和“出站”。
在 3.x 中,ChannelHandler
只是一个标记接口,而 ChannelUpstreamHandler
、ChannelDownstreamHandler
和 LifeCycleAwareChannelHandler
定义了实际的处理程序方法。在 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;
}
下图描述了新的类型层次结构
在 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
时,至少会触发三个 ChannelStateEvent
:channelOpen
、channelBound
和 channelConnected
。当 Channel
关闭时,至少会触发 3 个以上:channelDisconnected
、channelUnbound
和 channelClosed
。
但是,触发那么多事件的价值是可疑的。当 Channel
进入可以执行读写操作的状态时,用户收到通知会更有用。
channelOpen
、channelBound
和 channelConnected
已合并到 channelActive
。channelDisconnected
、channelUnbound
和 channelClosed
已合并到 channelInactive
。同样,Channel.isBound()
和 isConnected()
已合并到 isActive()
。
请注意,channelRegistered
和 channelUnregistered
不等同于 channelOpen
和 channelClosed
。它们是引入的新状态,用于支持 Channel
的动态注册、注销和重新注册,如下所示
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_CLOSURE
”ChannelOption
,以防止 Netty 在 SocketChannel.read(..)
返回 -1
时自动关闭连接。
在 3.x 中,Channel
由 ChannelFactory
创建,新创建的 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 套接字和虚拟机内部本地通道。这在编写需要最小延迟的代理服务器时非常有用。
3.x 没有提供从现有 JDK 套接字(如 java.nio.channels.SocketChannel
)创建新 Channel 的方法。您可以使用 4.0。
一旦在 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);
当一个 Channel
注册到一个 EventLoopGroup
时,Channel
实际上注册到由 EventLoopGroup
管理的一个 EventLoop
。EventLoop
实现 java.util.concurrent.ScheduledExecutorService
。这意味着用户可以在用户的通道所属的 I/O 线程中执行或安排一个任意 Runnable
或 Callable
。随着新的明确定义的线程模型(稍后将解释),编写线程安全处理程序变得极其容易。
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 线程停止自身。
在 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
为了响应用户需求,你可以将任何对象附加到 Channel
和 ChannelHandlerContext
。已添加了一个名为 AttributeMap
的新接口,Channel
和 ChannelHandlerContext
扩展了该接口。相反,ChannelLocal
和 Channel.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 已从头开始重写,尽管其目的保持不变;它执行使服务器或客户端启动并运行所需的典型步骤,这些步骤通常在样板代码中找到。
新的引导还采用了流畅的界面。
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();
}
}
正如您在上面的示例中注意到的,ChannelPipelineFactory
不再存在。它已被 ChannelInitializer
取代,后者提供了对 Channel
和 ChannelPipeline
配置的更多控制。
请注意,您不会自己创建新的 ChannelPipeline
。在观察到迄今为止报告的许多用例后,Netty 项目团队得出结论,用户创建自己的管道实现或扩展默认实现没有任何好处。因此,ChannelPipeline
不再由用户创建。ChannelPipeline
由 Channel
自动创建。
ChannelFuture
已拆分为 ChannelFuture
和 ChannelPromise
。这不仅使异步操作的消费者和生产者的契约显式化,而且还使在链中(如过滤)使用返回的 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
调用处理程序方法。
- 如果指定,则始终由指定的
-
分配给处理程序或频道的
EventExecutor
和EventLoop
始终是单线程的。- 处理程序方法将始终由同一线程调用。
- 如果指定了多线程的
EventExecutor
或EventLoop
,则首先将选择其中一个线程,然后在取消注册之前一直使用所选的线程。 - 如果同一管道中的两个处理程序被分配了不同的
EventExecutor
,则它们将同时被调用。如果多个处理程序访问共享数据,则用户必须注意线程安全性,即使共享数据仅由同一管道中的处理程序访问。
- 添加到
ChannelFuture
的ChannelFutureListeners
始终由分配给 future 关联的Channel
的EventLoop
线程调用。 -
ChannelHandlerInvoker
可用于控制Channel
事件的顺序。DefaultChannelHandlerInvoker
立即从EventLoop
线程执行事件,并将来自其他线程的事件作为Runnable
对象在EventExecutor
上执行。请参阅以下示例,了解在从EventLoop
线程和其他线程与 Channel 交互时可能产生的影响。(此功能已删除。请参阅 相关提交)
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.
当将 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
。 -
OneToOneEncoder
和OneToOneDecoder
已替换为MessageToMessageEncoder
和MessageToMessageDecoder
。 decode()
、decodeLast()
、encode()
的方法签名略有更改,以支持泛型并删除冗余参数。
编解码器嵌入器已被 io.netty.channel.embedded.EmbeddedChannel
替换,以便用户可以测试包括编解码器在内的任何类型的管道。
HTTP 解码器现在始终为每个 HTTP 消息生成多个消息对象
1 * HttpRequest / HttpResponse
0 - n * HttpContent
1 * LastHttpContent
有关更多详细信息,请参阅更新后的 HttpSnoopServer
示例。如果您不希望处理单个 HTTP 消息的多个消息,则可以在管道中放置一个 HttpObjectAggregator
。HttpObjectAggregator
将把多个消息转换成一个 FullHttpRequest
或 FullHttpResponse
。
新增了以下传输
- OIO SCTP 传输
- UDT 传输
本节展示了将阶乘示例从 3.x 移植到 4.0 的大致步骤。阶乘示例已移植到 io.netty.example.factorial
包中的 4.0。请浏览示例的源代码以查找所有更改。
- 重写
FactorialServer.run()
方法以使用新的引导 API。 - 不再有
ChannelFactory
。自行实例化NioEventLoopGroup
(一个用于接受传入连接,另一个用于处理已接受连接)。 - 将
FactorialServerPipelineFactory
重命名为FactorialServerInitializer
。 - 使其扩展
ChannelInitializer<Channel>
。 - 获取
Channel.pipeline()
,而不是创建新的ChannelPipeline
。 - 使
FactorialServerHandler
扩展ChannelInboundHandlerAdapter
。 - 用
channelInactive()
替换channelDisconnected()
。 - 不再使用 handleUpstream()。
- 将
messageReceived()
重命名为channelRead()
,并相应地调整方法签名。 - 用
ctx.writeAndFlush()
替换ctx.write()
。 - 使
BigIntegerDecoder
扩展ByteToMessageDecoder<BigInteger>
。 - 使
NumberEncoder
扩展MessageToByteEncoder<Number>
。 -
encode()
不再返回缓冲区。将编码数据填充到ByteToMessageDecoder
提供的缓冲区中。
与移植服务器基本相同,但当您编写潜在的大流时,您需要小心。
- 重写
FactorialClient.run()
方法以使用新的引导 API。 - 将
FactorialClientPipelineFactory
重命名为FactorialClientInitializer
。 - 使
FactorialClientHandler
扩展ChannelInboundHandlerAdapter