此文档涵盖了此次Netty大版本中值得关注的变更点及新特性,以帮助你将自己的应用顺利移植到新版本。
基于netty已经不再是JBoss.org的一部分,我们将包名从 org.jboss.netty 变更为 io.netty。
二进制jar包也被分割成了多个子模块,以便用户可以排除非必要的特性。当前结构如下:
Artifact ID描述netty-parentMaven parent POMnetty-common工具类及日志接口netty-buffer ByteBuf API,用来替换java.nio.ByteBuffer netty-transportChannel API 及核心 transportsnetty-transport-rxtx Rxtx transportnetty-transport-sctp SCTP transportnetty-transport-udt UDT transportnetty-handler ChannelHandler 的相关实现netty-codec编解码框架,用于编写encoder及decodernetty-codec-httpHTTP, Web Sockets, SPDY, and RTSP相关的编解码器netty-codec-socksSOCKS协议相关的编解码器netty-all包含以上所有artifacts的All-in-one的JARnetty-tarballTarball distributionnetty-example样例netty-testsuite-*整合的测试集netty-microbench微基准测试(Microbenchmarks)所有的artifacts(除了netty-all.jar)都已经是OSGi bundles了,可以直接在你的OSGi容器中使用。
在对netty包结构进行如上调整之后,buffer API也可以作为独立包使用了。所以,即使你不把Netty用来作为网络框架,你仍然可以使用buffer API。因此,ChannelBuffer 这个名字也变得不合时宜,我们便将之重命名为 ByteBuf。
用于创建buffer的工具类 ChannelBuffers 现在被拆分为 Unpooled 及 ByteBufUtil 两个工具类。一如其名,4.0引入了池化的 ByteBuf,可以使用 ByteBufAllocator 的实现类进行分配。
根据我们的内部性能测试,将接口 ByteBuf 变更为抽象类,能带来约5%的吞吐量提升。
3.x版本,buffer有定长(fixed)和动态(dynamic)两种。定长的buffer一旦创建,它的容量就不会再变化。而动态的buffer在每次操作 write*(…) 时都会根据需要动态调整容量。
4.0开始,所有的buffer都是动态的。并且比旧的动态buffer更加优秀。你可以更加容易和安全的增减buffer的容量。容易是因为提供了新方法 ByteBuf.capacity(int newCapacity)。而安全则是因为你可以设定buffer的最大容量从而防止其无限扩增。
// 不再使用 dynamicBuffer() - 换为 buffer(). ByteBuf buf = Unpooled.buffer(); // 增加buffer容量 buf.capacity(1024); ... // 减少buffer容量(最后512字节会被删除) buf.capacity(512);通过 wrappedBuffer() 创建的包装了单个buffer或者单个byte数组的buffer是唯一的例外。如果增大其容量就会破坏它包装已存在buffer的意义——节省内存(saving memory copies)。如果你包装了一个buffer以后还想改变它的容量,那么你需要新建一个拥有足够容量的buffer,并且copy你需要包装的部分过去。
新的buffer实现类 CompositeByteBuf 为复合buffer定义了很多高级操作。使用复合buffer可以在相对昂贵的随机访问操作中节省大量的内存复制操作。可跟以前一样使用 Unpooled.wrappedBuffer(…) 新建一个复合buffer,或者使用 Unpooled.compositeBuffer(…) 及 ByteBufAllocator.compositeBuffer() 进行创建。
3.x版本,ChannelBuffer.toByteBuffer() 及其变体的约定(contract)都不是很清晰。用户并不知道它返回的buffer含有共享数据的视图还是独立的数据副本。4.0版本将 toByteBuffer() 替换为 ByteBuf.nioBufferCount(), nioBuffer() 及nioBuffers()。如果 nioBufferCount() 返回了 0,意味着用户总是可以通过调用 copy().nioBuffer() 拿到一份buffer副本。
小端字节序的支持进行了较大的更改。以前版本,用户可使用 LittleEndianHeapChannelBufferFactory 或者按目标字节序包装一个已存在的buffer来获取一个小端字节序的buffer。4.0新增了一个方法:ByteBuf.order(ByteOrder)。其返回原buffer的目标字节序的视图。
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("x%n", buf.getInt(0)); ByteBuf leBuf = buf.order(ByteOrder.LITTLE_ENDIAN); // Prints '01000000' System.out.format("x%n", leBuf.getInt(0)); assert buf != leBuf; assert buf == buf.order(ByteOrder.BIG_ENDIAN);Netty4引入了一种高效缓存池(buffer pool),它是结合了 buddy allocation 以及 slab allocation 的 jemalloc 的变体。
减小buffer的频繁分配及回收导致的GC压力减少新建buffer时0值填充产生的内存带宽消耗及时回收直接内存(direct buffers)为了可以利用这些特性,用户都应该使用 ByteBufAllocator 获取buffer,除非你希望使用非池化buffer:
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 写入远程节点(remote peer),就立即自动归还原缓存池。
默认的 ByteBufAllocator 为 PooledByteBufAllocator。如果你不想用缓存池或者希望使用自己的allocator,那么使用 Channel.config().setAllocator(…) 设置你自己的allocator,比如 UnpooledByteBufAllocator。
注意:目前(译者注:这里指此文档发布时)默认的allocator是 UnpooledByteBufAllocator。一旦我们确认 PooledByteBufAllocator 没有内存泄露问题,我们会重新将其设为默认值。
为了使 ByteBuf 的生命周期更可控,Netty引入了显式的引用计数,而不再依赖GC了。
buffer被分配时,它的初始引用计数为1当buffer的引用计数降为0时,会被回收或者归还到池里以下行为会试图触发 IllegalReferenceCountException: 访问引用计数为0的buffer引用计数降为负,或者引用计数超过 Integer.MAX_VALUE 衍生buffer(如:slices及duplicates)及交换buffer(如:little endian buffers)共享其来源buffer的引用计数。需注意,衍生buffer创建时,引用计数不会变化。在 ChannelPipeline 中使用 ByteBuf 时,还需要注意一些额外规则:
pipeline中的入站(又名:上行)(译者注:原文为inbound及upstream)handler需要手动释放接受到的消息。Netty不会自动进行释放。 注意,编解码器框架会自动释放消息,如果用户想要把消息原样传递到下一个handler中,那就必须手动增加引用计数。当出站(又名:下行)(译者注:原文为outbound及downstream)消息到达pipline的起始点,Netty会在写出之后进行释放尽管引用计数已经很强大了,可同时也很容易出错。泄露探测器(leak detector)会记录buffer自动分配时的栈轨迹信息,协助用户排查忘记释放buffer的问题。
由于泄露探测器使用了 PhantomReference,并且获取栈轨迹信息成本很高,它仅进行了1%分配的采样。因此,应该让应用运行足够长的时间来查找所有可能的泄露问题。
一旦所有的泄露问题都找到并解决了,就可以通过指定JVM参数 -Dio.netty.noResourceLeakDetection 来关掉此特性,以此消除运行时的额外开销。
随着buffer API的独立化,4.0也提供了很多通用的用于异步应用的组件,并且新包命名为 io.netty.util.concurrent。部分组件如下:
Future 及 Promise – 类似 ChannelFuture,但是不依赖 Channel EventExecutor 及 EventExecutorGroup – 通用的事件循环API他们是文档后面提到的channel API的基础。例如, ChannelFuture 继承了 io.netty.util.concurrent.Future,EventLoopGroup 继承了EventExecutorGroup。
经过4.0的这次大调整,许多 io.netty.channel 包下的类都已经不见了,所以3.x的应用没法通过简单的搜索-替换来升级到4.0。本小节会展示如此大的变化背后的思路历程,而不再一一描述所有的细节变化了。
初学者常对’upstream’ 及 ‘downstream’ 感到困惑,所以4.0中尽量使用了’inbound’ 及’outbound’。
3.x中,ChannelHandler 仅是一个标记接口,ChannelUpstreamHandler, ChannelDownstreamHandler, 及LifeCycleAwareChannelHandler 定义了实际的handler方法。Netty4中,ChannelHandler 合并了很多 LifeCycleAwareChannelHandler 中对于inbound及outbound handler都适用的方法。
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。得益于将资源管理及buffer池交给JVM处理,这大大简化了Netty的内部实现。但是,这往往也是GC压力和偶尔观察到基于Netty的应用会处于高负载状态的源头。
4.0通过替换event对象为强类型方法调用的方式,几乎完全去掉了event对象的创建。3.x中类似 handleUpstream() 及 handleDownstream() 这种catch-all的event handler不再有了。每种event现在都会有单独的handler方法:
// 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 这样的event类型呢?4.0中 ChannelInboundHandler 有一个handler方法 userEventTriggered() 就是专门做这个的。
3.x中,每有一个新的 Channel 连接创建,就至少会触发3个 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 的出站buffer进行flush,而write()操作本身不会自动进行flush。你可以认为这跟 java.io.BufferedOutputStream 类似,差别只是这里是运用于消息级别。
基于此变更,在进行写操作之后千万别忘了调用 ctx.flush()。当然,你也可以使用更便捷的 writeAndFlush()。
3.x使用了 Channel.setReadable(boolean) 这种很不直观的方式实现入站传输暂停机制。这导致ChannelHandlers之间的交互更为复杂,如果实现有误也很容易导致互相干扰。
4.0新增了 read() 这个出站操作。如果你通过 Channel.config().setAutoRead(false) 关闭了 auto-read 标记,那除非你显式的调用 read(),否则Netty不会自动进行任何读取。一旦你的 read() 操作完成,channel又会停止读取,并且会触发 channelReadSuspended() 这个入站事件,然后,你又可以重新执行 read() 操作了。你也可以拦截 read() 操作来做一些更高级的传输控制。
你无法让Netty 3.x停止接受入站连接,而只能阻塞I/O线程,或者关闭服务端socket。4.0中,当 auto-read 未设置时,就会切断(译者注:原文为respects,译者怀疑是笔误) read() 操作,就像一个普通的channel一样。
转载自 并发编程网 - ifeve.com
相关资源:netty4中文用户手册