原文链接 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 ID | Description |
---|---|
netty-parent | Maven parent POM |
netty-common | Utility classes and logging facade |
netty-buffer | ByteBuf API that replaces java.nio.ByteBuffer |
netty-transport | Channel API and core transports |
netty-transport-rxtx | Rxtx transport |
netty-transport-sctp | SCTP transport |
netty-transport-udt | UDT transport |
netty-handler | Useful ChannelHandler implementations |
netty-codec | Codec framework that helps write an encoder and a decoder |
netty-codec-http | Codecs related with HTTP, Web Sockets, SPDY, and RTSP |
netty-codec-socks | Codecs related with SOCKS protocol |
netty-all | All-in-one JAR that combines all artifacts above |
netty-tarball | Tarball distribution |
netty-example | Examples |
netty-testsuite-* | A collection of integration tests |
netty-microbench | Microbenchmarks |
基本的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.)
- Boolean properties are still prefixed with
- 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更改
ChannelBuffer
→ ByteBuf
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 pooledByteBuf
s which can be allocated via ByteBufAllocator
implementations.
上面介绍的模块化的更改,我们可以单独的使用buffer API。即使你不打算用Netty来开发网络程序,你仍然可以使用buffer API。因此我们不在使用ChannelBuffer来命名而是采用ByteBuf。
ChannelBuffer 中的类被分解为
通过Unpooled就能猜到,4.0引入一个新的pooledUnpooled
和 ByteBufUtil
两个工具类,ByteBuf
,通过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.
- 当分配一个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
andPromise
- similar toChannelFuture
, but has no dependency toChannel
EventExecutor
andEventExecutorGroup
- 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
.
io.netty.util.concurrent
包来开发异步程序。其中的一些构造方式如下:
Future
,Promise和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:
ChannelHandler
只是一个接口,
ChannelUpstreamHandler
,
ChannelDownstreamHandler
和
LifeCycleAwareChannelHandler
才定义了实际的方法。在4.0中,
ChannelHandler
将
LifeCycleAwareChannelHandler
中很多关于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.
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 ChannelStateEvent
s are triggered: channelOpen
, channelBound
, and channelConnected
. When a Channel
is closed, at least 3 more:channelDisconnected
,channelUnbound
, and channelClosed
.
在3.x中,当一个新的连接建立时,至少会有3个ChannelStateEvent
s被触发:channelOpen
,channelBound
, andchannelConnected。当一个channel关闭还会触发3个事件:
channelDisconnected
,channelUnbound
, andchannelClosed
。
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
已经被合并为一个方法channelActive
。channelDisconnected
,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.
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.
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 moreEventLoop
s. 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 EventLoopGroup
s. 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.
ChannelFactory
创建的Channel将会被自动注册到I/O线程中。4.0采用新接口
EventLoopGroup
来替换
ChannelFactory
已提供更多的
EventLoop
s。当然用户也可以调用
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 EventLoop
s 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.
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.
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();
}
}
ChannelPipelineFactory
→ ChannelInitializer
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
而是交给系统去完成这些工作。
ChannelFuture
→ ChannelFuture
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
被分割成
ChannelFuture
和
ChannelPromise
两部分。这样做不仅使生产者消费者这种模式更加明确,也让操作返回的
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 theChannelHandler
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 does not need to define a
- A user can specify an
EventExecutor
when he or she adds a handler to aChannelPipeline
.- If specified, the handler methods of the
ChannelHandler
are always invoked by the specifiedEventExecutor
. - If unspecified, the handler methods are always invoked by the
EventLoop
that its associatedChannel
is registered to.
- If specified, the handler methods of the
EventExecutor
andEventLoop
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
orEventLoop
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
EventExecutor
s, 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 toChannelFuture
are always invoked by theEventLoop
thread assigned to the future's associatedChannel
.
- 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的所有方法总是运行在同一个线程上。如果指定了EventExecutor
或EventLoop
,那么首次被指定到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 toByteToMessageDecoder
.OneToOneEncoder
andOneToOneDecoder
were replaced withMessageToMessageEncoder
andMessageToMessageDecoder
.- The method signatures of
decode()
,decodeLast()
,encode()
were changed slightly to support generics and to remove redundant parameters.
- 核心codec 类移到
io.netty.handler.codec
包中。 FrameDecoder
被重命名为ByteToMessageDecoder
.OneToOneEncoder
,OneToOneDecoder被
所替代。MessageToMessageEncoder
和MessageToMessageDecoder
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.
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中添加一个
HttpObjectAggregator
。
HttpObjectAggregator
会将多个消息放在一个单独的
FullHttpRequest
或
FullHttpResponse
中进行传输。
(感觉这里前后矛盾)
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
- Rewrite
FactorialServer.run()
method to use the new bootstrap API.- No
ChannelFactory
anymore. InstantiateNioEventLoopGroup
(one for accepting incoming connections and the other for handling the accepted connections) by yourself.
- No
- Rename
FactorialServerPipelineFactory
toFactorialServerInitializer
.- Make it extends
ChannelInitializer<Channel>
. - Instead of creating a new
ChannelPipeline
, get it viaChannel.pipeline()
.
- Make it extends
- Make
FactorialServerHandler
extendsChannelInboundHandlerAdapter
.- Replace
channelDisconnected()
withchannelInactive()
. - handleUpstream() is not used anymore.
- Rename
messageReceived()
intochannelRead()
and adjust the method signature accordingly. - Replace
ctx.write()
withctx.writeAndFlush()
.
- Replace
- Make
BigIntegerDecoder
extendByteToMessageDecoder<BigInteger>
. - Make
NumberEncoder
extendMessageToByteEncoder<Number>
.encode()
does not return a buffer anymore. Fill the encoded data to the buffer provided byByteToMessageDecoder
.
- 用新的bootstrap API重写了
FactorialServer.run()
方法。
- 不再使用
ChannelFactory
,可以通过自己创建
NioEventLoopGroup
类实例来接受连接并且管理相关的连接。
- 不再使用
将
FactorialServerPipelineFactory
重命名为ChannelInboundHandlerAdapter
。
通过继承ChannelInitializer<Channel>
而无需再新建一个ChannelPipeline
。- 确保
FactorialServerHandler
继承自ChannelInboundHandlerAdapter
。用channelInactive()
替换channelDisconnected()
。handleUpstream()不再使用。messageReceived()
被重命名为channelRead()
。采用ctx.writeAndFlush()
替换ctx.write()
。 BigIntegerDecoder
继承自ByteToMessageDecoder<BigInteger>
。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.
- Rewrite
FactorialClient.run()
method to use the new bootstrap API. - Rename
FactorialClientPipelineFactory
toFactorialClientInitializer
. - Make
FactorialClientHandler
extendsChannelInboundHandlerAdapter
大多数服务器的开发移植流程基本上都相同,但当你要开发一个大数据流的服务器时需要注意一下几点
- 需要用新的bootstrap API来重写
FactorialClient.run()
方法。 - 将
FactorialClientPipelineFactory
重命名为FactorialClientInitializer
。 - 让
FactorialClientHandler
继承自ChannelInboundHandlerAdapter
。