New and noteworthy - Netty

原文链接 http://netty.io/wiki/new-and-noteworthy.html

This document walks you through the list of notable changes and new features in the major Netty release to give you an idea to port your application to the new version.

本文主要介绍Netty的主线版本的新功能,以及指导你如何将你的应用程序迁移到新版本的Netty上。

项目结构更改

The package name of Netty has been changed from org.jboss.netty toio.netty since we are not part of JBoss.org anymore.

The binary JAR has been split into multiple submodules so that a user can exclude unnecessary features from the class path. The current structure looks like this following:

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

JAR包也被分解为很多个子模块,降低了各模块之间的耦合性。具体结构如下:

Artifact IDDescription
netty-parentMaven parent POM
netty-commonUtility classes and logging facade
netty-bufferByteBuf API that replaces java.nio.ByteBuffer
netty-transportChannel API and core transports
netty-transport-rxtxRxtx transport
netty-transport-sctpSCTP transport
netty-transport-udtUDT transport
netty-handlerUseful ChannelHandler implementations
netty-codecCodec framework that helps write an encoder and a decoder
netty-codec-httpCodecs related with HTTP, Web Sockets, SPDY, and RTSP
netty-codec-socksCodecs related with SOCKS protocol
netty-allAll-in-one JAR that combines all artifacts above
netty-tarballTarball distribution
netty-exampleExamples
netty-testsuite-*A collection of integration tests
netty-microbenchMicrobenchmarks

基本的API更改

  • Most operations in Netty now support method chaining for brevity.
  • Non-configuration getters have no get- prefix anymore. (e.g. Channel.getRemoteAddress()Channel.remoteAddress())
    • Boolean properties are still prefixed with is- to avoid confusion (e.g. 'empty' is both an article and a verb, soempty() can have two meanings.)
  • For API changes between 4.0 CR4 and 4.0 CR5 see Netty 4.0.0.CR5 released with new-new API

  • 大多数的操作都采用简短的链接
  • getters方法不在以get开头(例如 Channel.getRemoteAddress()Channel.remoteAddress())。Boolean相关的属性仍然以is开头以防止造成混淆
  • 对于4.0的RC4和RC5之间的差别请参考http://netty.io/news/2013/06/18/4-0-0-CR5.html

Buffer API更改

ChannelBufferByteBuf

Thanks to the structural changes mentioned above, the buffer API can be used as a separate package. Even if you are not interested in adopting Netty as a network application framework, you can still use our buffer API. Therefore, the type name ChannelBuffer does not make sense anymore, and has been renamed to ByteBuf.

The utility class ChannelBuffers, which creates a new buffer, has been split into two utility classes,Unpooled and ByteBufUtil. As can be guessed from its name Unpooled, 4.0 introduced pooledByteBufs which can be allocated via ByteBufAllocator implementations.

上面介绍的模块化的更改,我们可以单独的使用buffer API。即使你不打算用Netty来开发网络程序,你仍然可以使用buffer API。因此我们不在使用ChannelBuffer来命名而是采用ByteBuf。

ChannelBuffer 中的类被分解为UnpooledByteBufUtil两个工具类,通过Unpooled就能猜到,4.0引入一个新的pooledByteBuf,通过ByteBufAllocator来实现ByteBuf的实现。

ByteBuf 不是一个接口而是一个抽象类

According to our internal performance test, converting ByteBuf from an interface to an abstract class improved the overall throughput around 5%.

根据我们专业的测试,采用类的的方式转换ByteBuf要比接口的方式效率高5%左右。

大多数buffer都是有最大容量的动态buffer

In 3.x, buffers were fixed or dynamic. The capacity of a fixed buffer does not change once it is created while the capacity of a dynamic buffer changes whenever its write*(...) method requires more space.

Since 4.0, all buffers are dynamic. However, they are better than the old dynamic buffers. You can decrease or increase the capacity of a buffer more easily and more safely. It's easy because there is a new method ByteBuf.capacity(int newCapacity). It's safe because you can set the maximum capacity of a buffer so that it does not grow boundlessly.

在3.x中,一个buffer可能是固定的也可能是动态的。指定容量的固定buffer一旦创建就不能再改变大小。而动态的分配的buffer可以通过write*(...)来增加容量。

从4.0开始,所有的buffer都是动态的。但他们仍然比老版本的动态buffer好。你可以很容易并且安全的增加或减少一个buffer的容量;之所以简单因为我们有一个新的方法ByteBuf.capacity(int newCapacity)。之所以安全是因为你可以设置一个buffer的最大容量防止其无限制的增加。

// 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);
The only exception is the buffer which wraps a single buffer or a single byte array, created by wrappedBuffer(). You cannot increase its capacity because it invalidates the whole point of wrapping an existing buffer - saving memory copies. If you want to change the capacity after you wrap a buffer, you should just create a new buffer with enough capacity and copy the buffer you wanted to wrap.

唯一可能出现异常的地方就是通过wrappedBuffer()创建的buffer,你不能增加它的容量,因为它把一个已存在的buffer进行了封装。当你相对一个封装的buffer增加容量,你应该创建一个新的有足够容量的buffer并将其内容拷贝到你的buffer中来。

新的buffer类型:CompositeByteBuf

A new buffer implementation named CompositeByteBuf defines various advanced operations for composite buffer implementations. A user can save bulk memory copy operations using a composite buffer at the cost of relatively expensive random access. To create a new composite buffer, use either Unpooled.wrappedBuffer(...) like before,Unpooled.compositeBuffer(...), or ByteBufAllocator.compositeBuffer().

CompositeByteBuf定义了很多高级和复杂的buffer操作的实现。用户可以用composite buffer操作整块内存,这样比零碎的内存访问要搞笑很多。要创建一个新的composite buffer 可以使用Unpooled.wrappedBuffer(...) ,Unpooled.compositeBuffer(...) 或者ByteBufAllocator.compositeBuffer()

Predictable NIO buffer conversion

The contract of ChannelBuffer.toByteBuffer() and its variants were not deterministic enough in 3.x. It was impossible for a user to know if they would return a view buffer with shared data or a copied buffer with separate data. 4.0 replaces to ByteBuffer() with ByteBuf.nioBufferCount(), nioBuffer(), and nioBuffers(). If nioBufferCount() returns0, a user can always get a copied buffer by calling copy().nioBuffer().

在3.x中,ChannelBuffer.toByteBuffer()系列的方法在很多规则上不是很明确,用户无法明确的知道这些方法返回的是一段数据的直接引用还是其内存的拷贝。4.0采用

新的toByteBuffer()方法不再使用ByteBuf.nioBufferCount(), nioBuffer()nioBuffers()方法。如果nioBufferCount()返回0,则用户应该调用copy().nioBuffer()来获取copied buffer。

Little endian support changes

Little endian support has been changed significantly. Previously, a user was supposed to specify a LittleEndianHeapChannelBufferFactory or wrap an existing buffer with the desired byte order to get a little endian buffer. 4.0 adds a new method:ByteBuf.order(ByteOrder). It returns a view of the callee with the desired byte order:

小字节序的支持是一个很重要的更改。以前的版本用户如果需要小字节序的支持需要指定一个LittleEndianHeapChannelBufferFactory或者需要将buffer封装到一个小字节序的buffer中。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);

Pooled buffers

Netty 4 introduces a high-performance buffer pool which is a variant of jemalloc that combines buddy allocation and slab allocation. It gives the following benefits:

  • Reduced GC pressure incurred by frequent allocation and deallocation of the buffers
  • Reduced memory bandwidth consumption incurred when creating a new buffer which inevitably has to be filled with zeroes
  • Timely deallocation of direct buffers

To take advantage of this feature, unless a user wants to get an unpooled buffer, he or she should get a buffer from a ByteBufAllocator:

Netty4 引入一个高度优化的缓冲池,和jemalloc 不同,jemalloc 依赖于buddy allocation slab allocation。使用缓存池有以下几点好处:

  • 不用频繁的申请和释放内存以减轻GC的压力。
  • 不需要再考虑新申请buffer时将其置零以减轻内存带宽的压力。
  • 可以及时而直接的进行缓存分配

上面介绍了这么多好处,除非用户还是想用unpooled buffer。否则他们应该使用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)

Once a ByteBuf is written to the remote peer it will be returned automatically to the pool it originated from.

The default ByteBufAllocator is PooledByteBufAllocator. If you do not wish to use buffer pooling or use your own allocator, use Channel.config().setAllocator(...) with an alternative allocator such as UnpooledByteBufAllocator.

NOTE: At the moment, the default allocator is UnpooledByteBufAllocator. Once we ensure there's no memory leak inPooledByteBufAllocator, we will default back again to it.

一旦ByteBuf写入完成,它将会被申请它的缓存池回收。

ByteBufAllocator默认是PooledByteBufAllocator,如果你不想使用缓存池或者想使用自己的allocator,可以使用Channel.config().setAllocator(...)来进行设置,例如可以使用UnpooledByteBufAllocator

注意: 这种情况下allocator是UnpooledByteBufAllocator。一旦我们想确保没有内存泄露,我们应该再次回来使用PooledByteBufAllocator

ByteBuf is always reference counted

To control the life cycle of a ByteBuf in a more predictable way, Netty does not rely on the garbage collector anymore but employs an explicit reference counter. Here's the basic rule:

  • When a buffer is allocated, its initial reference count is 1.
  • If the reference count of the buffer is decreased to 0, it is deallocated or returned to the pool it originated from.
  • The following attempts trigger an IllegalReferenceCountException:
    • Accessing a buffer whose reference count is 0,
    • Decreasing the reference count to a negative value, or
    • Increasing the reference count beyond Integer.MAX_VALUE.
  • Derived buffers (e.g. slices and duplicates) and swapped buffers (i.e. little endian buffers) share the reference count with the buffer it was derived from. Note that the reference count does not change when a derived buffer is created.
要控制一个ByteBuff的生命周期有很多方式。Netty并不是依靠GC来回收buffer而是采用计数器的方式来管理ByteBuff的生命期。以下是一些基本规则:
  • 当分配一个buffer,其初始引用计数为1
  • 当一个buffer的引用计数为0,它将会被缓存池回收。
  • 以下几种情况将会引发IllegalReferenceCountException异常:1. 访问一个引用计数为0的buffer。2. 引用计数减为负数。3.引用计数的增加超过Integer.MAX_VALUE
  • Derived buffers(切片,副本)和swapped buffers(小字节序)将和原始的buffer共享引用计数。当创建一个derived buffer时并不会改变引用计数器。

When a ByteBuf is used in a ChannelPipeline, there are additional rules you need to keep in mind:

  • Each inbound (a.k.a. upstream) handler in a pipeline has to release the received messages. Netty does not release them automatically for you.
    • Note that codec framework does release the messages automatically and a user has to increase the reference count if he or she wants to pass a message as-is to the next handler.
  • When an outbound (a.k.a. downstream) message reaches at the beginning of the pipeline, Netty will release it after writing it out.
当在 ChannelPipeline中使用ByteBuf时,还需要注意以下两个规则:
  • 对于inbound (a.k.a. upstream) handler, 需要手动释放收到的消息,Netty不会自动帮你释放。注意:codec framework 会自动释放消息,如果用户要将消息传给其它handler需要增加其引用计数。
  • 对于outbound (a.k.a. downstream)消息,Netty在将其发送出去后释放它;无需用户手动释放。

Automatic buffer leak detection

Although reference counting is very powerful, it is also error-prone. To help a user find where he or she forgot to release the buffers, the leak detector logs the stack trace of the location where the leaked buffer was allocated automatically.

Because the leak detector relies on PhantomReference and obtaining a stack trace is a very expensive operation, it samples approximately 1% of allocations only. Therefore, it's a good idea to run the application for a reasonably long time to find all possible leaks.

Once all leaks are found and fixed. You can turn this feature off to remove its runtime overhead completely by specifying the -Dio.netty.noResourceLeakDetection JVM option.

虽然引用计数的方式非常强大,但使用不当依然会出错。为了帮助用户查找哪些buffer没有释放,内存泄露检测器会将相关的buffer内存泄露信息记录下来。

因为内存泄露检查器依赖于PhantomReference并且生成堆栈信息对性能的影响很大。但是,还是建议你尽量长时间的运行你的程序以方便找出内存泄露的地方。

一旦所有内存泄露的问题解决,你就可以关闭这个功能,通过在运行前向JVM添加-Dio.netty.noResourceLeakDetection参数来关闭内存泄露检查功能。

io.netty.util.concurrent

Along with the new standalone buffer API, 4.0 provides various constructs which are useful for writing asynchronous applications in general at the new package called io.netty.util.concurrent. Some of those constructs are:

  • Future and Promise - similar to ChannelFuture, but has no dependency  to Channel
  • EventExecutor and EventExecutorGroup - generic event loop API

They are used as the base of the channel API which will be explained later in this document. For example,ChannelFuture  extends io.netty.util.concurrent.Future and EventLoopGroup extends EventExecutorGroup.

由于采用新的独立的buffer API, 4.0 提供了 io.netty.util.concurrent包来开发异步程序。其中的一些构造方式如下:
  • FuturePromise和ChannelFuture相似,但是不在依赖于Channel
  • EventExecutor , EventExecutorGroup -常用的消息循环处理API




Channel API changes

In 4.0, many classes under the io.netty.channel package have gone through a major overhaul, and thus simple text search-and-replace will not make your 3.x application work with 4.0. This section will try to show the thought process behind such a big change, rather than being an exhaustive resource for all the changes.

4.0中,io.netty.channel包中的很多类都经过了重大修改。简单的文本替换不能从3.x切换到4.0 。本节将讨论需要注意的重大更改。

Revamped ChannelHandler interface

Upstream → Inbound, Downstream → Outbound

The terms 'upstream' and 'downstream' were pretty confusing to beginners. 4.0 uses 'inbound' and 'outbound' wherever possible.

术语'upstream'和'downstream'容易造成混淆,4.0后采用'inbound'和‘outbound’。

New ChannelHandler type hierarchy

In 3.x, ChannelHandler was just a tag interface, and ChannelUpstreamHandler,ChannelDownstreamHandler, and LifeCycleAwareChannelHandler defined the actual handler methods. In Netty 4,ChannelHandler merges LifeCycleAwareChannelHandler along with a couple more methods which are useful to both an inbound and an outbound handler:

在3.x中 ChannelHandler只是一个接口, ChannelUpstreamHandler, ChannelDownstreamHandler 和  LifeCycleAwareChannelHandler才定义了实际的方法。在4.0中, ChannelHandlerLifeCycleAwareChannelHandler中很多关于inbound,outbound有用的方法合并在了一起:

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

The following diagram depicts the new type hierarchy:

下面有张相关类的层级图:


ChannelHandler with no event object

In 3.x, every I/O operation created a ChannelEvent object. For each read / write, it additionally created a new ChannelBuffer. It simplified the internals of Netty quite a lot because it delegates resource management and buffer pooling to the JVM. However, it often was the root cause of GC pressure and uncertainty which are sometimes observed in a Netty-based application under high load.

在3.x中,每次I/O操作都会创建一个 ChannelEvent对象。每次读写操作都需要创建一个新的 ChannelBuffer.这样做让Netty内部更简化,因为资源的管理和缓存池都交给JVM来托管了。但是这种方式会经常增加GC的压力造成系统过载。

4.0 removes event object creation almost completely by replacing the event objects with strongly typed method invocations. 3.x had catch-all event handler methods such as handleUpstream() and handleDownstream(), but this is not the case anymore. Every event type has its own handler method now:

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 has also been changed to reflect the changes mentioned above:

ChannelHandlerContext的改动和上面提及的方式相同:

// Before:
ctx.sendUpstream(evt);
 
// After:
ctx.fireChannelRead(receivedMessage);
All these changes mean a user cannot extend the non-existing ChannelEvent interface anymore. How then does a user define his or her own event type such as IdleStateEvent? ChannelInboundHandler in 4.0 has a handler method called userEventTriggered() which is dedicated to this specific user case.

上面的改动意味着用户无需再去实现ChannelEvent接口了。用户应该如何定义自己的事件类型了?例如IdleStateEvent。在4.0中,ChannelInboundHandler有一个方法userEventTriggered()来处理用户的特殊需求。

Simplified channel state model

When a new connected Channel is created in 3.x, at least three ChannelStateEvents are triggered: channelOpen, channelBound, and channelConnected. When a Channel is closed, at least 3 more:channelDisconnected,channelUnbound, and channelClosed.

在3.x中,当一个新的连接建立时,至少会有3个ChannelStateEvents被触发:channelOpen,channelBound, andchannelConnected。当一个channel关闭还会触发3个事件:channelDisconnected,channelUnbound, andchannelClosed


However, it's of dubious value to trigger that many events. It is more useful for a user to get notified when a Channel enters the state where it can perform reads and writes.

但是,并不是每次都会触发这么多事件。其实用户更关心的是Channel是否已准备好读写操作。


channelOpen, channelBound, and channelConnected have been merged to channelActive.channelDisconnected,channelUnbound, and channelClosed have been merged to channelInactive. Likewise,Channel.isBound() and isConnected() have been merged to isActive().

Note that channelRegistered and channelUnregistered are not equivalent to channelOpen and channelClosed. They are new states introduced to support dynamic registration, deregistration, and re-registration of a Channel, as illustrated below:

channelOpen, channelBound, and channelConnected已经被合并为一个方法channelActivechannelDisconnected,channelUnbound, andchannelClosed也被合并到一个方法中channelInactive。同样,Channel.isBound() andisConnected()被合并为isActive()

注意,channelRegistered and channelUnregistered并不等同于channelOpen andchannelClosed。这是两个新引入的状态用来动态注册和反注册channel,也可以再次注册一个Channel.


write() does not flush automatically

4.0 introduced a new operation called flush() which explicitly flushes the outbound buffer of aChannel, and write() operation does not flush automatically. You can think of this as a java.io.BufferedOutputStream, except that it works at message level.

Because of this change, you must be very careful not to forget to call ctx.flush() after writing something. Alternatively, you could use a shortcut method writeAndFlush().

4.0引入了一个新的操作flush()用来将channel的数据送出,write()操作并不会自动送出数据。你可以把它理解为java.io.BufferedOutputStream,只是它工作在消息级别。

你必须谨记write操作后调用ctx.flush()。当然你也可以调用writeAndFlush()同时完成上面2个步骤。

Sensible and less error-prone inbound traffic suspension

3.x had an unintuitive inbound traffic suspension mechanism provided by Channel.setReadable(boolean). It introduced complicated interactions between ChannelHandlers and the handlers were easy to interfere with each other if implemented incorrectly.

In 4.0, a new outbound operation called read() has been added. If you turn off the default auto-read flag withChannel.config().setAutoRead(false), Netty will not read anything until you explicitly invoke the read() operation. Once the read() operation you issue is complete and the channel again stops reading, an inbound event called channelReadSuspended() will be triggered so that you can re-issue another read() operation. You can also intercept a read() operation to perform more advanced traffic control.

3.x提供了一个Channel.setReadable(boolean)方法用来控制是否需要接收数据。要说清楚各handler之间的交互不难,但真正使用的时候却不是那么容易。

4.0中加入了一个新的read()方法。如果你通过Channel.config().setAutoRead(false)关闭自动读取数据,Netty将不会读取任何数据直到你明确的调用read()操作。一旦read()操作完成channel将会再次停止读操作,channelReadSuspended()将会被触发这样你就可以再次调用read()来读取新的数据。你也可以通过拦截read()操作来实现高级的传输控制。

Suspension of accepting incoming connections

There was no way for a user to tell Netty 3.x to stop accepting incoming connections except for blocking the I/O thread or closing the server socket. 4.0 respects the read() operation when the auto-read flag is not set, just like an ordinary channel.

在3.x中如果你不阻塞I/O线程或关闭服务器socket,将不能停止进入的链接。在4.0中你可以不设置auto-read标记来达到这个目的。

Half-closed sockets

TCP and SCTP allow a user to shut down the outbound traffic of a socket without closing it completely. Such a socket is called 'a half-closed socket', and a user can make a half-closed socket by calling SocketChannel.shutdownOutput() method. If a remote peer shuts down the outbound traffic,SocketChannel.read(..) will return-1, which was seemingly indistinguishable from a closed connection.

TCP和SCTP允许用户在不关闭socket的情况停止向外发送数据。这类socket被称为'a half-closed socket。用户可以调用 SocketChannel.shutdownOutput()方法实现half-closed socket。如果对端关闭了outbound, SocketChannel.read()将返回-1,看上去像对端关闭了链接。

3.x did not have shutdownOutput() operation. Also, it always closed the connection when SocketChannel.read(..) returns -1.

To support a half-closed socket, 4.0 adds SocketChannel.shutdownOutput() method, and a user can set the 'ALLOW_HALF_CLOSURE' ChannelOption to prevent Netty from closing the connection automatically even if SocketChannel.read(..) returns -1.

3.x没有shutdownOutput()方法,当链接关闭时SocketChannel.read(..)方法将返回-1.

为了支持half-closed.4.0加入了SocketChannel.shutdownOutput()方法,用户可以设置'ALLOW_HALF_CLOSURE'ChannelOption来阻止SocketChannel.read(..)返回-1时关闭链接。

Flexible I/O thread allocation

In 3.x, a Channel is created by a ChannelFactory and the newly created Channel is automatically registered to a hidden I/O thread. 4.0 replaces ChannelFactory with a new interface called EventLoopGroup which consists of one or moreEventLoops. Also, a newChannel is not registered to the EventLoopGroup automatically but a user has to call EventLoopGroup.register() explicitly.

Thanks to this change (i.e. separation of ChannelFactory and I/O threads), a user can register different Channel implementations to the same EventLoopGroup, or same Channel implementations to different EventLoopGroups. For example, you can run a NIO server socket, NIO client sockets, NIO UDP sockets, and in-VM local channels in the same I/O thread. It should be very useful when writing a proxy server which requires minimal latency.

在3.x中,通过 ChannelFactory创建的Channel将会被自动注册到I/O线程中。4.0采用新接口 EventLoopGroup来替换 ChannelFactory已提供更多的 EventLoops。当然用户也可以调用 EventLoopGroup.register()来手动注册。

由于上面说的更改,用户可以将不同的Channel注册到一个EventLoopGroup上,或者将一个Channel注册到不同的EventLoopGroup上。例如,你可以在一个I/O线程上同时运行NIO server socket, NIO client sockets, NIO UDP sockets, and in-VM local channels。这种方式对于开发低延迟的代理服务器很有帮助。

Ability to create a Channel from an existing JDK socket

3.x provided no way to create a new Channel from an existing JDK socket such as java.nio.channels.SocketChannel. You can with 4.0.

3.x中不能从已知的JDK socket上创建Channel,但是4.0可以。

Deregistration and re-registration of a Channel from·to an I/O thread

Once a new Channel is created in 3.x, it is completely tied to a single I/O thread until its underlying socket is closed. In 4.0, a user can deregister a Channel from its I/O thread to gain the full control of its underlying JDK socket. For example, you can take advantage of high-level non-blocking I/O Netty provides to deal with complex protocols, and then later deregister the Channel and switch to blocking mode to transfer a file at possible maximum throughput. Of course, it is possible to register the deregistered Channel back again.

在3.x中一旦新的Channel被创建,它将一直被绑定在一个I/O线程上直到它关闭为止。在4.0中,用户可以从I/O线程上注销一个Channel来获得对JDK socket的完全控制。例如,你可以采用一个更高优先级的I/O线程来处理复杂的协议交互,处理完成后可以注销相关的Channel并切换到阻塞模式来处理大文件的传输。当然,你也可以再次注册以前注销过的Channel到你的I/O线程上。

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(false);
myFile.transferFrom(mySocket, ...);
 
// Register back again to another event loop group.
EventLoopGroup anotherGroup = ...;
anotherGroup.register(ch);

Scheduling an arbitrary task to be run by an I/O thread

When a Channel is registered to an EventLoopGroup, the Channel is actually registered to one of the EventLoops which is managed by the EventLoopGroup.EventLoop implements java.util.concurrent.ScheduledExecutorService. It means a user can execute or schedule an arbitrary Runnable or Callable in an I/O thread where the user's channel belongs to. Along with the new well-defined thread model, which will be explained later, it became extremely easier to write a thread-safe handler.

当一个Channel注册到 EventLoopGroup时,这个Channel会自动和其中的一个 EventLoop关联起来。 EventLoop实现了 java.util.concurrent.ScheduledExecutorService接口。这意味着用户可以指定一个专属的Runnable或Callable和Channel相关联。稍后将介绍如何定义良好的线程模型,以及很容易的编写线程安全的handler.

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() { ... });
    }
}

Simplified shutdown

There's no more release ExternalResources(). You can close all open channels immediately and make all I/O threads stop themselves by calling EventLoopGroup.shutdownGracefully().

不需要再调用releaseExternalResources()。你可以调用EventLoopGroup.shutdownGracefully()关闭所有打开的Channel并停止所有I/O线程。

Type-safe ChannelOption

There are two ways to configure the socket parameters of a Channel in Netty. One is to call the setters of a ChannelConfig explicitly, such as SocketChannelConfig.setTcpNoDelay(true). It is the most type-safe way. The other is to call ChannelConfig.setOption() method. Sometimes you have to determine what socket options to configure in runtime, and this method is ideal for such cases. However, it is error-prone in 3.x because a user has to specify the option as a pair of a string and an object. If a user calls with the wrong option name or value, he or she will encounter a ClassCastException or the specified option might even be ignored silently.

4.0 introduces a new type called ChannelOption, which provides type-safe access to socket options.

在Netty中有两种方式来配置socket的参数。一种方式是直接调用 ChannelConfig的setter方法,例如 SocketChannelConfig.setTcpNoDelay(true)方法,这是最类型安全的方式。另一种方式调用 ChannelConfig.setOption()方法。有时候,你需要在程序运行期间配置socket option,就得采用第二种方式来处理。在3.x中,用户采用字符串的方式来设置相关的option,所以很容易出错。如果不小心将相关的option的名字或值填错,就会引发 ClassCastException异常或设置不能生效。

4.0引入一个新类ChannelOption来提供类型安全的socket option。

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

In response to user demand, you can attach any object to Channel and ChannelHandlerContext. A new interface called AttributeMap, which Channel and ChannelHandlerContext extend, has been added. Instead,ChannelLocal and Channel.attachment are removed. The attributes are garbage-collected when their associated Channel is garbage-collected.

为了能够正确的处理响应,你可以向 Channel and ChannelHandlerContext附加任何对象。 Channel and ChannelHandlerContext实现了一个新的接口 AttributeMap,移除了 ChannelLocal and Channel.attachment。当Channel被回收时,附加到它上面的属性也会被回收。

public class MyHandler extends ChannelInboundHandlerAdapter {
 
    private static final AttributeKey<MyState> STATE =
            new AttributeKey<MyState>("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();
    }
    ...
}

New bootstrap API

The bootstrap API has been rewritten from scratch although its purpose stays same; it performs the typical steps required to make a server or a client up and running, often found in boilerplate code.

The new bootstrap also employs a fluent interface.

bootstrap API已经完全重写但是功能不变;它主要用来启动服务器或客户端,在示例代码中会经常看见它的身影。

新bootstrap使用起来也很方便。

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

As you noticed in the example above, there is no ChannelPipelineFactory anymore. It has been replaced with ChannelInitializer, which gives more control over Channel and ChannelPipeline configuration.

Please note that you don't create a new ChannelPipeline by yourself. After observing many use cases reported so far, the Netty project team concluded that it has no benefit for a user to create his or her own pipeline implementation or to extend the default implementation. Therefore, ChannelPipeline is not created by a user anymore.ChannelPipeline is automatically created by aChannel.

上面的示例中没有找到 ChannelPipelineFactory,我们已经需要这个类而采用新的类 ChannelInitializer,可以让我们对Channel和 ChannelPipeline有更多的控制权。

注意,你不能创建ChannelPipeline。通过多次调研后,Netty项目组发现实现默认的pipeline或者自己开发pipeline并没有给用户带来方便。因此,用户无需再创建自己的ChannelPipeline而是交给系统去完成这些工作。

ChannelFutureChannelFuture and ChannelPromise

ChannelFuture has been split into ChannelFuture and ChannelPromise. This not only makes the contract of consumer and producer of an asynchronous operation explicit, but also makes it more safe to use the returned ChannelFuture in a chain (like filtering), because the state of the ChannelFuture cannot be changed.

Due to this change, some methods now accept ChannelPromise rather than ChannelFuture to modify its state.

ChannelFuture被分割成 ChannelFutureChannelPromise两部分。这样做不仅使生产者消费者这种模式更加明确,也让操作返回的 ChannelFuture更加安全,因为 ChannelFuture的状态不能更改。

由于这种更改,有些方法可以采用ChannelPromise来更改相关的状态。

Well-defined thread model

There is no well-defined thread model in 3.x although there was an attempt to fix its inconsistency in 3.5. 4.0 defines a strict thread model that helps a user write a ChannelHandler without worrying too much about thread safety.

  • Netty will never call a ChannelHandler's methods concurrently, unless the ChannelHandler is annotated with @Shareable. This is regardless of the type of handler methods - inbound, outbound, or life cycle event handler methods.
    • A user does not need to synchronize either inbound or outbound event handler methods anymore.
    • 4.0 disallows adding a ChannelHandler more than once unless it's annotated with @Sharable.
  • There is always happens-before relationship between each ChannelHandler method invocations made by Netty.
    • A user does not need to define a volatile field to keep the state of a handler.
  • A user can specify an EventExecutor when he or she adds a handler to a ChannelPipeline.
    • If specified, the handler methods of the ChannelHandler are always invoked by the specified EventExecutor.
    • If unspecified, the handler methods are always invoked by the EventLoop that its associated Channel is registered to.
  • EventExecutor and EventLoop assigned to a handler or a channel are always single-threaded.
    • The handler methods will always be invoked by the same thread.
    • If multithreaded EventExecutor or EventLoop is specified, one of the threads will be chosen first and then the chosen thread will be used until deregistration.
    • If two handlers in the same pipeline are assigned with different EventExecutors, they are invoked simultaneously. A user has to pay attention to thread safety if more than one handler access shared data even if the shared data is accessed only by the handlers in the same pipeline.
  • The ChannelFutureListeners added to ChannelFuture are always invoked by the EventLoop thread assigned to the future's associated Channel.
3.x中没有什么好的线程模式,3.5以后才试图解决这个问题。4.0定义了严格的线程模型来帮助用户开发线程安全的ChannelHandler。
  • Netty绝不会并发的调用ChannelHandler的方法,除非该ChannelHandler被标记为@shareable。这条规则适用于handler的所有方法。用户无需考虑inbound, outbound相关方法的同步问题。4.0不允许多次添加同一个ChannelHandler,当然被标记为@Sharable的除外。
  • ChannelHandler的每个方法调用都遵循happens-before关系。用户无需使用volatile字段来保持handler的状态。
  • 当用户要将handler附加到ChannelPipeline上时可以指定一个EventExecutor。如果指定了EventExecutor,那么ChannelHandler的所有方法都会运行在指定的EventExecutor上。如果没有指定,ChannelHandler的方法将会运行在注册到Channel上时被分配的EventLoop上面。
  • EventExecutor 和 EventLoop会指定一个handler上,或者channel总是运行在一个单独的线程上。 handler的所有方法总是运行在同一个线程上。如果指定了EventExecutorEventLoop,那么首次被指定到Channel的线程将会一直伴随该Channel直到它从线程池中注销。如果一个pipeline中的两个handler被指定到2各不同的EventExecutor,那么他们将会同时运行。用户需要自己处理多个handler访问共享数据的线程安全问题。
  • 添加到ChannelFuture上的ChannelFutureListener总是在Channel分配的线程中执行。

No more ExecutionHandler - it's in the core.

You can specify an EventExecutor when you add a ChannelHandler to a ChannelPipeline to tell the pipeline to always invoke the handler methods of the added ChannelHandler via the specified EventExecutor.

你可以在向 ChannelPipeline添加ChannelHandler时指定一个 EventExecutor。这样所有handler相关的方法都会在这个指定的 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());

Codec framework changes

There were substantial internal changes in the codec framework because 4.0 requires a handler to create and manage its buffer (see Per-handler buffer section in this document.) However, the changes from a user's perspective are not very big.

  • Core codec classes are moved to the io.netty.handler.codec package.
  • FrameDecoder has been renamed to ByteToMessageDecoder.
  • OneToOneEncoder and OneToOneDecoder were replaced with MessageToMessageEncoder and MessageToMessageDecoder.
  • The method signatures of decode(), decodeLast(), encode() were changed slightly to support generics and to remove redundant parameters.
由于4.0要求handler自行创建和管理其buffer,所以codec框架做了根本性的改变。但是用户并不会感觉到有太大的变化。
  • 核心codec 类移到io.netty.handler.codec包中。
  • FrameDecoder被重命名为ByteToMessageDecoder.
  • OneToOneEncoderOneToOneDecoder被MessageToMessageEncoderMessageToMessageDecoder所替代。
  • decode(), decodeLast(), encode()这几个方法删除了多余的参数并且添加了泛型支持。

Codec embedder → EmbeddedChannel

Codec embedder has been replaced by io.netty.channel.embedded.EmbeddedChannel to allow a user to test any kind of pipeline including a codec.

Codec embedder被 io.netty.channel.embedded.EmbeddedChannel所替代,以运行用户测试某个pipeline是否包含指定的codec。

HTTP codec

HTTP decoders now always generates multiple message objects per a single HTTP message:

HTTP decoders现在可以将多个消息对象放在一条HTTP消息中:

1       * HttpRequest / HttpResponse
0 - n   * HttpContent
1       * LastHttpContent
For more detail, please refer to the updated HttpSnoopServer example. If you wish not to deal with multiple messages for a single HTTP message, you can put an HttpObjectAggregator in the pipeline. HttpObjectAggregator will transform multiple messages into a single Full HttpRequest or Full HttpResponse.
如果要进一步了解,请参考 HttpSnoopServer例子。如果你不想将多个消息对象放在一条HTTP消息中,你可以在pipeline中添加一个 HttpObjectAggregatorHttpObjectAggregator会将多个消息放在一个单独的 FullHttpRequestFullHttpResponse中进行传输。 (感觉这里前后矛盾)

Changes in transport implementations

The following transports were newly added:

加入了几个新的传输方式:

  • OIO SCTP transport
  • UDT transport

Case study: porting the Factorial example

This section shows rough steps to port the Factorial example from 3.x to 4.0. The Factorial example has been ported to 4.0 already in theio.netty.example.factorial package. Please browse the source code of the example to find every bits changed.

本节大概的介绍下3.x到4.0相关示例修改的地方。4.0的例子已经移动到io.netty.example.factorial包中。请浏览具体的代码来查看更改的地方

Porting the server

  1. Rewrite FactorialServer.run() method to use the new bootstrap API.
    1. No ChannelFactory anymore. Instantiate NioEventLoopGroup (one for accepting incoming connections and the other for handling the accepted connections) by yourself.
  2. Rename FactorialServerPipelineFactory to FactorialServerInitializer.
    1. Make it extends ChannelInitializer<Channel>.
    2. Instead of creating a new ChannelPipeline, get it via Channel.pipeline().
  3. Make FactorialServerHandler extends ChannelInboundHandlerAdapter.
    1. Replace channelDisconnected() with channelInactive().
    2. handleUpstream() is not used anymore.
    3. Rename messageReceived() into channelRead() and adjust the method signature accordingly.
    4. Replace ctx.write() with ctx.writeAndFlush().
  4. Make BigIntegerDecoder extend ByteToMessageDecoder<BigInteger>.
  5. Make NumberEncoder extend MessageToByteEncoder<Number>.
    1. encode() does not return a buffer anymore. Fill the encoded data to the buffer provided by ByteToMessageDecoder.
  1. 用新的bootstrap API重写了FactorialServer.run()方法。
    1.  不再使用ChannelFactory, 可以通过自己创建NioEventLoopGroup类实例来接受连接并且管理相关的连接。
  2. FactorialServerPipelineFactory重命名为ChannelInboundHandlerAdapter
    通过继承ChannelInitializer<Channel>而无需再新建一个ChannelPipeline
  3. 确保FactorialServerHandler继承自ChannelInboundHandlerAdapter。用channelInactive()替换channelDisconnected()。handleUpstream()不再使用。messageReceived()被重命名为channelRead()。采用ctx.writeAndFlush()替换ctx.write()
  4. BigIntegerDecoder继承自ByteToMessageDecoder<BigInteger>
  5. NumberEncoder继承自MessageToByteEncoder<Number>encode()不再返回一个buffer。

Porting the client

Mostly same with porting the server, but you need to pay attention when you write a potentially large stream.

  1. Rewrite FactorialClient.run() method to use the new bootstrap API.
  2. Rename FactorialClientPipelineFactory to FactorialClientInitializer.
  3. Make FactorialClientHandler extends ChannelInboundHandlerAdapter

大多数服务器的开发移植流程基本上都相同,但当你要开发一个大数据流的服务器时需要注意一下几点

  1. 需要用新的bootstrap API来重写FactorialClient.run()方法。
  2. FactorialClientPipelineFactory重命名为FactorialClientInitializer
  3. FactorialClientHandler继承自ChannelInboundHandlerAdapter


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值