【Netty】Netty 4重大变动及特性(官方文档翻译)
官方文档地址
New and noteworthy in 4.0
本文带你了解Netty4中值得关注的变化及新特性,它会帮助你在应用中使用这个新版本。
项目结构变更
Netty的软件包名称已从org.jboss.netty
更改为io.netty
因为我们不再属于JBoss.org。
二进制JAR已拆分为多个子模块,因此用户可以从类路径中排除不必要的功能。当前结构如下:
Artifact ID | 描述 |
---|---|
netty-parent |
Maven父POM |
netty-common |
工具类和日志框架 |
netty-buffer |
替代 java.nio.ByteBuffer 的ByteBuf API |
netty-transport |
Channel API和核心传输core transports |
netty-transport-rxtx |
Rxtx传输 |
netty-transport-sctp |
SCTP传输 |
netty-transport-udt |
UDT传输 |
netty-handler |
有用的ChannelHandler 实现 |
netty-codec |
有助于编写编码器和解码器的编解码器框架 |
netty-codec-http |
与HTTP,Web套接字,SPDY和RTSP相关的编解码器 |
netty-codec-socks |
与SOCKS协议相关的编解码器 |
netty-all |
结合了以上所有模块的多合一JAR |
netty-tarball |
Tarball发行版本 |
netty-example |
例子 |
netty-testsuite-* |
集成测试的集合 |
netty-microbench |
微基准 |
现在,所有Artifacts(除了netty-all.jar
)都是OSGi捆绑包,可以在您喜欢的OSGi容器中使用。
常规API更改
-
Netty中的大多数操作现在都支持链式调用。
-
非配置的get方法不再具有get-前缀。(例如Channel.getRemoteAddress()→Channel.remoteAddress())
-
布尔属性仍带有前缀
is-
以避免混淆(例如,“ empty”既是形容词又是动词,因此empty()
可以有两种含义。) -
有关4.0 CR4和4.0 CR5之间的API更改,请参阅随新API发布的Netty 4.0.0.CR5
缓冲区API更改
ChannelBuffer
→ ByteBuf
由于上述包结构上的更改,缓冲区API可以用作单独的程序包。即使您不希望将Netty用作网络应用程序框架,也可以使用我们的缓冲区API。因此,类型名称ChannelBuffer
不再有意义,并且已重命名为ByteBuf
。
实用类ChannelBuffers
,它创建了一个新的缓冲区,已分裂成两个实用工具类,Unpooled
以及ByteBufUtil
。从其名称Unpooled
可以猜到,4.0引入了池化的ByteBuf
,可以通过ByteBufAllocator
实现进行分配。
ByteBuf
不是接口而是抽象类
根据我们的内部性能测试,将ByteBuf
从接口转换为抽象类可将整体吞吐量提高约5%。
大多数缓冲区都是动态的,具有最大容量
在3.x中,缓冲区是固定的或动态的。固定缓冲区的容量一旦创建就不会更改,而动态缓冲区的容量只要其write*(...)
方法需要更多空间就会更改。
从4.0开始,所有缓冲区都是动态的。但是,它们比旧的动态缓冲区要好。您可以更轻松,更安全地减少或增加缓冲区的容量。这是容易做到的,是因为有一个新方法ByteBuf.capacity(int newCapacity)
。这是安全的,因为您可以设置缓冲区的最大容量,以使其不会无限增长。
//不再需要dynamicBuffer()-使用buffer()。
ByteBuf buf = Unpooled.buffer();
// 增加缓冲区大小
buf.capacity(1024);
...
//减少缓冲区的大小(删除最后512个字节。)
buf.capacity(512);
唯一的例外是由wrappedBuffer()
创建的单个缓冲区或单个字节数组。您不能增加其容量,因为它会使现有缓冲区的整个无效-保存内存副本。如果要在包装缓冲区后更改容量,则应仅创建一个具有足够容量的新缓冲区,然后复制要包装的缓冲区。
新缓冲区类型: CompositeByteBuf
一个名为CompositeByteBuf
的新缓冲区实现为复合缓冲区实现定义了各种高级操作。用户可以使用复合缓冲区来节省大容量存储器复制操作,但代价是相对昂贵的随机访问。要创建新的复合缓冲区,请像之前一样使用Unpooled.wrappedBuffer(...), Unpooled.compositeBuffer(...)
,或ByteBufAllocator.compositeBuffer()
。
可预测的NIO缓冲区转换
在3.x中,ChannelBuffer.toByteBuffer()
与其变量的的约定不够确定。用户不可能知道他们是否返回带有共享数据的视图缓冲区或带单独数据的复制缓冲区。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的变体,它结合了buddy allocation和slab allocation。它具有以下优点:
- 降低频繁分配和重新分配缓冲区导致的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
采用引用计数
为了以一种更可预测的方式控制ByteBuf
的生命周期,Netty不再依赖垃圾回收器,而是使用了一个显式的引用计数器。下面是基本规则:
-
分配缓冲区后,其初始引用计数为1。
-
如果缓冲区的引用计数减少到0,则将其重新分配或将其放回创建它的池中。
-
以下操作会触发IllegalReferenceCountException:
- 访问一个引用计数为0的缓冲区,
- 将引用计数减少为负值,或者
- 将参考计数增加到大于Integer.MAX_VALUE`。
-
派生缓冲区(例如,切片和重复项)和交换缓冲区(即小端序缓冲区)与派生它的缓冲区共享引用计数。请注意,创建派生缓冲区时,引用计数不会更改。
当ByteBuf
在ChannelPipeline
中使用时,您需要牢记额外的规则:
- 管道pipeline 中的每个入站inbound (又称upstream)handler 都必须主动释放接收到的消息。Netty不会自动为您释放它们。
- 请注意,编解码器框架确实会自动释放消息,并且如果用户想按原样将消息传递给下一个处理程序,则用户必须增加引用计数。
- 当出站outbound (又名downstream)消息到达管道pipeline的开头时,Netty将其写出后将其释放。
缓冲区泄漏自动检测
尽管引用计数功能非常强大,但也容易出错。为了帮助用户找到忘记释放缓冲区的位置,泄漏检测器记录了泄漏缓冲区自动分配位置的堆栈跟踪。
因为泄漏检测器依赖PhantomReference
并且获得堆栈跟踪是非常昂贵的操作,所以它仅对分配的1%进行采样。因此,最好在相当长的时间内运行应用程序以查找所有可能的泄漏。
找到并修复所有泄漏后,您可以通过指定-Dio.netty.noResourceLeakDetection
JVM选项来关闭此功能以完全消除其运行时开销。
io.netty.util.concurrent
4.0与新的独立缓冲区API一起,提供了各种类,这些类位于io.netty.util.concurrent
的新包,通常可用于编写异步应用程序。其中一些类是:
Future
和Promise
-与ChannelFuture
相似,但不依赖于Channel
EventExecutor
和EventExecutorGroup
-通用事件循环API
它们用作channel API的基础,本文档后面将对此进行说明。例如,ChannelFuture
extends io.netty.util.concurrent.Future
和EventLoopGroup
extends EventExecutorGroup
。
Channel API变化
在4.0中,io.netty.channel
包下的许多类都进行了大修,因此简单的文本搜索和替换将无法使您的3.x应用程序在4.0上正常工作。本节将试图显示出如此巨大的变化背后的思考过程,而不是成为所有变化的详尽资源。
改进的ChannelHandler接口
上游Upstream →入站Inbound,下游Upstream →出站Outbound
对于初学者来说,“上游Upstream ”和“下游Upstream ”这两个术语非常令人困惑。4.0尽可能使用“入站Inbound”和“出站Outbound”。
新的ChannelHandler
层次结构
在 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;
}
下图描述了新的类型层次结构:
事件对象从ChannelHandler中消失了
在3.x中,每个I / O操作都会创建一个ChannelEvent
对象。对于每次读/写,它还额外创建了一个ChannelBuffer
。它大大简化了Netty的内部结构,因为它将资源管理和缓冲区池委托给了JVM。但是,这通常是GC压力和不确定性的根本原因,有时会在基于Netty的应用程序中在高负载下观察到这种情况。
4.0通过用强类型方法调用替换事件对象,几乎完全删除了事件对象的创建。3.x具有全部捕获的事件处理程序方法,例如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中ChannelInboundHandler
,有一个称为userEventTriggered()
的处理方法专用于此特定用户情况。
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt)
throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state().equals(IdleState.READER_IDLE)) {
System.out.println("READER_IDLE");
// 超时关闭channel
ctx.close();
} else if (event.state().equals(IdleState.WRITER_IDLE)) {
System.out.println("WRITER_IDLE");
} else if (event.state().equals(IdleState.ALL_IDLE)) {
System.out.println("ALL_IDLE");
// 发送心跳
ctx.channel().write("ping\n");
}
}
super.userEventTriggered(ctx, evt);
}
简化的通道状态模型
当一个新的连接Channel
在3.X创建,至少有三个ChannelStateEvent
s的触发: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
的动态注册,注销和重新注册,如下所示:
Channel的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)
禁用默认的自动读取auto-read标志,则在您明确调用该read()
操作之前,Netty不会读取任何内容。一旦read()
发出的操作完成并且通道再次停止读取后,将触发一个名为channelReadSuspended()
的入站事件,以便您可以重新发出另一个read()
操作。您还可以拦截read()
操作以执行更高级的流量控制。
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//连接建立后,设置为不自动读入站流量
ctx.channel().config().setAutoRead(false);
super.channelActive(ctx);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//这里可根据条件,决定是否再读一次
ctx.channel().read();
super.channelReadComplete(ctx);
}
暂停接受新连接
用户没有办法告诉Netty 3.x停止接受传入的连接,除非阻塞I / O线程或关闭服务器套接字。像普通通道一样,4.0会在read()
未设置自动读取标志时遵守操作。
半封闭式sockets
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
时自动关闭连接。
灵活的I / O线程分配
在3.x中,Channel
由ChannelFactory
创建,并且新创建的Channel
将自动注册到隐藏的I / O线程。4.0替换ChannelFactory
为一个名为EventLoopGroup
包含一个或多个EventLoop
s的新接口。同样,新Channel
的不会自动注册到EventLoopGroup
,而是用户必须明确地调用EventLoopGroup.register()
。
由于此更改(即,ChannelFactory
线程和I / O线程分离),用户可以将不同的Channel
实现注册到相同的EventLoopGroup
,或者将相同的Channel
实现注册到不同EventLoopGroupS
。例如,您可以在同一I / O线程中运行一个NIO服务器套接字,NIO客户端套接字,NIO UDP套接字和in-VM本地channels。当编写需要最小延迟的代理服务器时,这将非常有用。
能够从现有的JDK套接字创建一个Channel
3.x无法通过现有的JDK套接字(例如java.nio.channels.SocketChannel
)创建新的Channel 。您可以在4.0中做到。
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);
I / O线程上注销和重新注册Channel
一旦在3.x中创建了新的Channel
,它将完全绑定到单个I / O线程,直到其底层套接字关闭。在4.0中,用户可以将Channel
从其I / O线程中注销 ,以获得对其底层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
管理的EventLoopS
中的一个。EventLoop
实现了java.util.concurrent.ScheduledExecutorService
。这意味着用户可以执行或调度用户Channel所属的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() { ... });
}
}
简化关闭
在4.x里不再有releaseExternalResources()
。您可以立即关闭所有打开的通道,并通过调用EventLoopGroup.shutdownGracefully()
使所有I / O线程停止运行。
类型安全的 ChannelOption
有两种方法可以配置Netty中的Channel
套接字参数。一种是显式调用ChannelConfig
的setter ,例如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
为了响应用户需求,您可以将任何对象附加到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
引导bootstrap 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
正如您在上面的示例中注意到的那样,ChannelPipelineFactory
不再存在。已将其替换为ChannelInitializer
,从而可以更好地控制Channel
和ChannelPipeline
配置。
请注意,您不是自己创建新的ChannelPipeline
。在查看许多用例之后,Netty项目团队得出结论,用户创建自己的管道pipeline 实现或扩展默认实现对用户没有好处。因此,ChannelPipeline
不再由用户创建。ChannelPipeline
是由自动创建的Channel
。
ChannelFuture
→ChannelFuture
和ChannelPromise
ChannelFuture
已被拆分为ChannelFuture
和ChannelPromise
。这不仅使异步操作的使用者和生产者的约定明确,而且使使用链中的ChannelFuture
返回值(如过滤)更加安全,因为ChannelFuture
不能更改状态。
由于此更改,某些方法现在接受ChannelPromise
而不是ChannelFuture
来修改其状态。
定义明确的线程模型
尽管尝试修复3.5中的不一致问题,但3.x中没有明确定义的线程模型。4.0定义了一个严格的线程模型,可以帮助用户编写ChannelHandler而不用过多担心线程安全。
-
Netty绝不会 同时调用
ChannelHandler
的方法,除非使用注解@Sharable
标识ChannelHandler
。这与处理程序方法的类型无关-入站,出站或生命周期事件处理程序方法。- 用户不再需要同步入站或出站事件处理程序方法。
- 4.0不允许
ChannelHandler
多次添加,除非带有注解@Sharable
。
-
Netty进行的每个
ChannelHandle
方法调用之间总是存在happens-before
关系- 用户无需定义
volatile
字段即可保留处理程序的状态。
- 用户无需定义
-
用户可以在将处理程序添加到
ChannelPipeline
中时指定一个EventExecutor
。- 如果指定,则的处理程序方法
ChannelHandler
始终由指定的EventExecutor
调用。 - 如果未指定,则处理程序方法始终由与其相关联
Channel
注册到的EventLoop
调用。
- 如果指定,则的处理程序方法
-
分配给处理程序或通道的
EventExecutor
和EventLoop
始终是单线程的。- 处理程序方法将始终由同一线程调用。
- 如果指定了多线程
EventExecutor
或EventLoop
,则将首先选择一个线程,然后将使用所选择的线程,直到注销为止。 - 如果在同一管道中的两个处理程序分配有不同的
EventExecutor
,则将同时调用它们。如果多个处理程序访问共享数据,即使共享数据仅由同一管道中的多个处理程序访问,用户也必须注意线程安全。
-
被添加到
ChannelFuture
的ChannelFutureListeners
总是由分配给与Future
关联的Channel
的EventLoop
线程调用。 -
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
告诉管道pipeline 总是调用添加的ChannelHandler
的处理方法时,您可以指定EventExecutor
。
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()
的方法签名进行了些许改变以支持泛型和删除冗余参数。
编解码器嵌入→ EmbeddedChannel
编解码器嵌入器已被替换为io.netty.channel.embedded.EmbeddedChannel
,允许用户测试包括编解码器的任何类型的管道。
HTTP编解码器
现在,HTTP解码器总是为单个HTTP消息生成多个消息对象:
1 * HttpRequest / HttpResponse
0 - n * HttpContent
1 * LastHttpContent
更多详细信息,请参阅更新的HttpSnoopServer
示例。如果您不希望为一个HTTP消息处理多个消息,则可以在管道中添加一个HttpObjectAggregator
。HttpObjectAggregator
会将多个消息转换为一个FullHttpRequest
或FullHttpResponse
。
Transport实现的变化
新添加了以下传输:
- OIO SCTP传输
- UDT传输
案例研究:移植阶乘示例
本节显示了将阶乘示例从3.x移植到4.0的粗略步骤。io.netty.example.factorial
软件包中的阶乘示例已移植到4.0 。请浏览示例的源代码以查找每个更改的地方。
移植服务器
- 重写
FactorialServer.run()
方法以使用新的引导API。 - 移除
ChannelFactory
。 自己实例化NioEventLoopGroup
(一个实例接受传入的连接,另一个实例处理接受的连接)。 - 重命名
FactorialServerPipelineFactory
为FactorialServerInitializer
。 - 使它继承
ChannelInitializer<Channel>
。 - 不现主动创建一个新的
ChannelPipeline
,而是通过Channel.pipeline()
获取。 - 使
FactorialServerHandler
继承ChannelInboundHandlerAdapter
。 - 替换
channelDisconnected()
为channelInactive()
。 handleUpstream()
不再使用。- 重命名
messageReceived()
成channelRead()
并相应地调整方法签名。 - 替换
ctx.write()
为ctx.writeAndFlush()
。 - 使
BigIntegerDecoder
继承ByteToMessageDecoder<BigInteger>
。 - 使
NumberEncoder
继承MessageToByteEncoder<Number>
。 encode()
不再返回缓冲区。将编码的数据填充到ByteToMessageDecoder
提供的缓冲区中。
移植客户端
与移植服务器大致相同,但是在编写潜在的大数据流时需要注意。
- 重写
FactorialClient.run()
方法以使用新的引导API。 - 重命名
FactorialClientPipelineFactory
为FactorialClientInitializer
。 - 使
FactorialClientHandler
继承ChannelInboundHandlerAdapter