Netty 5.0/4.0新变化和注意点

Netty 5.0/4.0新变化和注意点

🗨

本文带你了解Netty 5.0 的值得注意的改变和新特性,帮助你将应用程序迁移到最新的版本.

不像 3.x 和 4.0 之间的改变那么大, 5.0 并没有很大的改变,尽管它在设计的简化性上做了一些突破. 我们尽可能平滑地使4.x转换到5.0,但是如果在迁移的过程中有问题请让我们知道.

目录 [−]

  1. 核心改变
    1. 简化 handler 类型继承关系
    2. channelRead0() → messageReceived()
    3. 更灵活的线程模型
    4. 更好的Channel.deregister(...)

 

核心改变

简化 handler 类型继承关系

ChannelInboundHandlerChannelOutboundHandler 被合并到 ChannelHandler. 现在ChannelHandler拥有inbound 和 outbound handler 方法.

ChannelInboundHandlerAdapter, ChannelOutboundHandlerAdapter, 和 ChannelDuplexHandlerAdapter 弃用了, 被 ChannelHandlerAdapter 取代.

因为现在你无法区分一个 handler是 inbound handler 或者 outbound handler, 所以CombinedChannelDuplexHandlerChannelHandlerAppender取代.

想了解这个改变的更多信息,请看 pull request #1999.

channelRead0() → messageReceived()

我知道,这是一个傻傻的错误. 如果你正在使用SimpleChannelInboundHandler, 你不得不将channelRead0()重命名为messageReceived().

更灵活的线程模型

在Netty 4.x中, 每个EventLoop和一个固定的线程紧密耦合, 这个线程会执行它注册的channel的所有的I/O事件,以及指派给它的任务.
在5.0中, 一个EventLoop不再直接使用线程,而是使用一个 Executor. 也就是, 它使用一个 Executor 对象作为构造函数的参数,以前是在一个无尽的循环中拉取I/O事件, 吸纳在是每次迭代的结果是一个task,将此task提交给Executor执行.
如果没有特别指定,Executor默认使用 ForkJoinPool. ForkJoinPool使用thread-local 队列. 也就是说, 从线程A中提交到ForkJoinPool到非常可能再由线程A执行. 这提供了EventLoop高层次的thread affinity.

而且,程序员也可以提供他们自己的Executor (也叫做 thread pool) 调度 EventLoop. 一个场景可以证明它有用:当 Netty用作大规模的软件系统中. 假定此系统已经使用一个线程池并发地执行它的任务. Netty 4.x简单的产生大量的线程,完全不顾它是一个大规模系统的一部分. 自Netty 5.0起, 开发者可以运行 Netty 和系统的其它部分在同一个线程池中, 通过应用更好的调度策略和较少的调度开支可以潜在地提高性能. 细节讨论可以参照 GitHub issue 2250.

应该提到的是,这个改变不会影响ChannelHandlers的方式. 在开发者看来,唯一改变的是不再保证同一个ChannelHandler 会被同一个线程执行. 然而可以保证的是, 它不会同时被两个或者两个以上的线程执行.此外, Netty 会负责内存可见性的问题,所以不必担心线程安全性和 ChannelHandler的volatile变量.

这个改变的另一个影响就是 NioEventLoop, NioEventLoopGroup, EpollEventLoopEpollEventLoopGroup 不再使用ThreadFactory作为构造函数的参数. 这些构造函数现在取而代之使用ExecutorExecutorFactory对象.

更好的Channel.deregister(...)

Netty 4.0引入了Channel.deregister(...), 5.0中它的行为被更新了以便符合Netty的线程模型.
现在可以保证在ChannelHandler中提交到EventLoop中的所有task在Channel取消注册(deregister)前都会被EventLoop执行
然而Channel.deregister(...)保留了非阻塞的操作,所以你不得不等待 返回的ChannelFuture成功后才能将它安全的注册到另一个EventLoop.

当调用Channel.deregister(...)后任何尝试在这个ChannelHandler中提交新的task (Runnable 或 Callable) 到会触发 RejectedExecutionException. 一旦这个 Channel注册到另外一个EventLoop, 一切归于正常.

ChannelHandler通过 EventLoop.schedule*(...)方法提交的task当Channel取消注册后会停止执行, 当Channel再次注册时这些task会自动移到新的EventLoop中继续执行. 这个限制只会影响在Channel取消注册时被调度的task. 那么delay的或者定期执行的task不受影响.

你也可以突破这个限制,尽管不被推荐. Netty 5.0 引入了一个新的方法 EventLoop.unwrap(), 它返回原始的EventLoop并不会执行一个健全的检查. 更准确的讲, 当提交task或者调度task到 "unwrapped" EventLoop时, 不会保证这些task会被并发执行,调度的task也不保证自动移到新的EventLoop.

 


 

 

Netty 4.1中的新变化和注意点

目录 [−]

  1. 题外话
  2. 核心改变
    1. Android支持
    2. ChannelHandlerContext.attr(..) == Channel.attr(..)
    3. Channel.hasAttr(...)
    4. 更容易更精确的追踪buffer leak
    5. PooledByteBufAllocator作为默认的buffer allocator
    6. 全局唯一的 channel ID
    7. 更灵活的线程模型
    8. EmbeddedChannel 可用性
    9. 能够使用Executor替换ThreadFactory
    10. 更友好的类加载器 Class loader friendliness
    11. ByteBufAllocator.calculateNewCapacity()
  3. 新的编解码和handler
  4. 其它编解码的改变
    1. AsciiString
    2. TextHeaders
    3. MessageAggregator
    4. HttpObjectAggregator更好的处理超出尺寸的消息
    5. ChunkedInput 和 ChunkedWriteHandler
    6. SnappyFramedEncoder 和 SnappyFramedDecoder

本文翻译自官方文档New and noteworthy in 4.1

本文带你了解Netty 4.0到Netty 4.1的值得注意的改变和新特性.

题外话

尽管我们尽量保持向下兼容,4.1 还是有一些和4.0不完全兼容的地方. 请确保使用新的Netty版本重新编译你的应用.
当你重新编译你的应用时,你可以能看到一些弃用警告. 请依照修改建议来更正它们, 这样当你升级到新的版本时会遇到较少的麻烦.

核心改变

Android支持

考虑到:

  • 移动设备日益强大
  • 自 Ice Cream Sandwich版本后ADK中大部分NIO 和 SSLEngine的问题都被修复
  • 用户很想在移动应用中重用它们的编解码器和handler

我们决定官方的支持 Android (4.0 及以上版本) .
然而,我们并没有一个为Android提供的测试套件. 如果你发现在Android使用的问题, 请提交一个 issue. 也请考虑贡献Android的测试代码作为整个构建过程的一部分.

ChannelHandlerContext.attr(..) == Channel.attr(..)

ChannelChannelHandlerContext实现了 AttributeMap接口, 允许用户附加一个或者多个属性在它们上. 有时候用户迷惑的是[ChannelChannelHandlerContext用户自己的属性存储. 例如,即使你通过Channel.attr(KEY_X).set(valueX)设置一个属性'KEY_X' , 你也不会通过ChannelHandlerContext.attr(KEY_X).get()得到这个属性, 相反亦然. 这种行为不仅让人迷惑,而且也浪费内存.

为了解决这个问题, 我们决定为每个Channel在内部只保留一个map. AttributeMap总是使用AttributeKey 作为它的主键. AttributeKey确保主键的唯一性, 这样每个Channel不会有多余一个的attribute map. 由于用户会将他们自己的主键定义为 ChannelHandler的private static final 字段 , 不会有重复的键值的担忧.

Channel.hasAttr(...)

现在我们可以有效地检查一个属性是否存在.

更容易更精确的追踪buffer leak

先前, 找到buffer泄漏并不容易,泄漏警告不太有帮助. 现在我们有了一个先进的泄漏报告机制,当开销增加时会被启用。(We now have an advanced leak reporting mechanism which can be enabled at the cost of increased overhead.)

查看Reference counted objects得到更多信息 . 这个特性太重要了,所以也被增加回 4.0.14.

PooledByteBufAllocator作为默认的buffer allocator

在 4.x 中, 尽管有一些功能局限性, UnpooledByteBufAllocator 曾是默认的allocator. 现在PooledByteBufAllocator 已经使用了很长一段时间了,而且我们有了先进的buffer泄漏追踪机制,是时候把它作为默认的buffer allocator了.

现在PooledByteBufAllocator 是默认的buffer allocator.

全局唯一的 channel ID

每个Channel都有一个唯一的ID,依据一下信息产生:

  • MAC 地址 (EUI-48 or EUI-64),
  • 进程 ID,
  • System#currentTimeMillis()
  • System#nanoTime()
  • 一个随机的 32-bit integer
  • 一个顺序增加的32-bit integer.

Channe ID可以通过 Channel.id()方法得到.

更灵活的线程模型

一个新的ChannelHandlerInvoker 加入, 允许用户对哪个线程处理handler方法有更多的控制
作为往ChannelPipeline中增加ChannelHandler时指定EventExecutor的替代,指定一个定制的ChannelHandlerInvoker实现可以实现更多的控制.

想了解更多的信息,可以参考 commit 132af3a.

EmbeddedChannel 可用性

EmbeddedChannelreadInbound()readOutbound() 返回一个 特定类型的参数, 你不必将返回值在转型, 减少了单元测试代码的啰嗦.

1
2
3
4
5
6
7
EmbeddedChannel ch = ...;
 
// BEFORE:
FullHttpRequest req = (FullHttpRequest) ch.readInbound();
 
// AFTER:
FullHttpRequest req = ch.readInbound();

能够使用Executor替换ThreadFactory

一些应用要求用户运行他们的任务在一个指定的Executor. 而4.x在创建event loop时需要用户指定的是ThreadFactory ,现在已经用Executor替换了.

关于这个改变的更多信息,你可以查看 pull request #1762.

更友好的类加载器 Class loader friendliness

在容器环境中一些类型如AttributeKey对应用程序来说不是太友好,现在没有这个问题了.

ByteBufAllocator.calculateNewCapacity()

计算可扩展的ByteBuf容量的代码从AbstractByteBuf移到ByteBufAllocator, 因为ByteBufAllocator更方便的知道它管理的buffer的容量计算信息.

新的编解码和handler

  • Binary memcache protocol codec
  • Compression codecs
    * BZip2
    * FastLZ
    * LZ4
    * LZF
    
  • DNS protocol codec
  • HAProxy protocol codec
  • MQTT protocol codec
  • SPDY/3.1 support
  • STOMP codec
  • SOCKSx codec, 支持版本 4, 4a, 和 5; 查看 socksx包.
  • XmlFrameDecoder 允许处理XML文档流.
  • JsonObjectDecoder 允许处理JSON对象流.
  • IP filtering handlers

其它编解码的改变

AsciiString

AsciiString是一个新的CharSequence实现, 包含的字符只占1个字节. 当你处理US-ASCII 或者 ISO-8859-1 字符串时可以节省空间.

例如, HTTP 编解码器和STOMP编解码器使用AsciiString处理header name. 因为将AsciiString编码到ByteBuf中不会有类型转换的代价,比String类型有更好的性能.

TextHeaders

TextHeaders 提供了一个通用的数据结构,类似Http Header类型的字符串 mutimap. HttpHeaders也用TextHeaders重写.

MessageAggregator

MessageAggregator 为聚合多个小消息成一个大消息提供了通用的功能,就像HttpObjectAggregator实现的那样.HttpObjectAggregator使用MessageAggregator进行了重写.

HttpObjectAggregator更好的处理超出尺寸的消息

在4.0中在客户端发送消息前没有办法拒绝一个超过指定大小的HTTP 消息,即使 100-continue header已经设置.
4.1中增加了一个可以override方法,叫做handleOversizedMessage, 因此用户可以执行他想要的任务. 默认条件下, 它会返回一个 '413 Request Entity Too Large' response, 然后关闭连接.

ChunkedInputChunkedWriteHandler

ChunkedInput 有两个新的方法; progress()length() 返回数据传输的进度以及流的程度.ChunkedWriteHandler使用这个信息通知 ChannelProgressiveFutureListener.

SnappyFramedEncoderSnappyFramedDecoder

这两个类被改名为SnappyFrameEncoder and SnappyFrameDecoder. T老的类被标记为弃用, 实际上它们是新的类的子类.

 


 

Netty 4.0中的新变化和注意点

目录 [−]

  1. 项目结构的改变
  2. 通用 API 的改变
  3. Buffer API 的改变
    1. ChannelBuffer → ByteBuf
    2. ByteBuf 不是一个接口,而是一个抽象类class
    3. 大部分的buffer变成了动态的,具有可配置的最大容量
    4. 新的buffer类型: CompositeByteBuf
    5. 可预知的NIO buffer转型
    6. 对小端序(Little endian)支持的改变
    7. 池化buffer Pooled buffers
      1. ByteBuf 总是使用引用计数(reference counted)
      2. 自动 buffer泄漏检查
  4. io.netty.util.concurrent
  5. Channel API 的改变
    1. 改组 ChannelHandler 接口
      1. Upstream → Inbound, Downstream → Outbound
      2. 新的 ChannelHandler 类型继承关系
      3. ChannelHandler 不需要 event object
      4. 简化channel 状态模型
      5. write() 不会自动 flush
    2. 可知的和不易出错的入站流量挂起(暂停读取)
      1. 暂停接受新的的连接
    3. 半关闭的socket
    4. 灵活的I/O线程分配
    5. 能够从一个已存在的jdk套接字上创建一个Channel
    6. 取消注册和重新注册一个Channel从/到一个I/O线程
    7. 调度任意的任务到I/O线程里运行
    8. 简化shutdown
    9. 类型安全的ChannelOption
    10. AttributeMap
    11. 新的 bootstrap API
      1. ChannelPipelineFactory → ChannelInitializer
    12. ChannelFuture → ChannelFuture 和 ChannelPromise
  6. 良好定义的线程模型
    1. 没有 ExecutionHandler 了 - 它被放入核心代码中.
  7. 编解码框架的改变
    1. Codec embedder → EmbeddedChannel
    2. HTTP codec
    3. transport 实现的改变
  8. 用例学习:迁移阶乘(Factorial)的例子
    1. 迁移服务端
    2. 迁移客户端

本文翻译自官方文档New and noteworthy in 4.0

本文带你了解Netty 4.0的值得注意的改变和新特性,帮助你将应用程序从老的Netty迁移到最新的版本上。

项目结构的改变

Netty的包名从 org.jboss.netty 改成 io.netty, 因为 我们不再是JBoss.org的一份子了.
二进制的 JAR 文件被分成多个模块, 这样用户可以从类路径中排除不需要的特性. 当前的结构如下:

Artifact IDDescription
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

现在所有的 artifacts (除了 netty-all.jar) 都实现了 OSGi bundle, 可以用在你的 OSGi 容器中.

通用 API 的改变

  • Netty中大部分的操作都支持链式方法,简化了操作
  • 不能配置的getter 不再有get- 前缀. (例如 Channel.getRemoteAddress()Channel.remoteAddress())
  • 布尔属性仍然保留 is- 前缀, 避免造成迷惑 (例如 'empty' 既是形容词也是动词, 所以 empty() 有两个意思)
  • 4.0 CR4 和 4.0 CR5之间的 改变请参照 Netty 4.0.0.CR5 released with new-new API

Buffer API 的改变

ChannelBufferByteBuf

由于上面提到的项目结构的改变, buffer API可以作为一个独立的包使用. 即使你对使用Netty作为网络应用框架不感兴趣,你也可以使用buffer API.
因此, 类型名ChannelBuffer 不再合适了, 所以改名为 ByteBuf.

用来创建新buffer的工具类 ChannelBuffers被分成了两个工具类: UnpooledByteBufUtil. 从名字Unpooled上也能猜出, 4.0引入了池化的 ByteBuf, 可以通过 ByteBufAllocator
的具体实现来分配(allocated).

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

根据我们内部的性能测试,将ByteBuf 从接口改为抽象类可以给总吞吐量带来5%的提升.

大部分的buffer变成了动态的,具有可配置的最大容量

在3.x, buffer可以是固定大小或者动态的,固定大小的buffer一旦创建容量就不能改变。 而动态buffer的容量在write*(...)方法需要更多空间时可以改变。
自4.0开始, 所有的buffer都是动态的。 然而,它们要比以前的动态buffer更好。你可以更容易更安全地增加或者减少buffer的容量。因为提供了新的方法 ByteBuf.capacity(int newCapacity)所以
改变容量更容易。之所以说它安全, 是因为你可以设置一个最大容量,这样buffer就不会不限制的增长.

1
2
3
4
5
6
7
8
9
// No more dynamicBuffer() - use buffer().
ByteBuf buf = Unpooled.buffer();
 
// Increase the capacity of the buffer.
buf.capacity(1024);
...
 
// Decrease the capacity of the buffer (the last 512 bytes are deleted.)
buf.capacity(512);

唯一的例外是那些使用wrappedBuffer方法包装的(wrapped)一个单一的buffer或者一个单一字节数组。你不能增加它的容量, 因为这样会使包装一个已有buffer的目的(节省内存复制)失去意义。
如果你想改变这样的buffer的容量, 你应该使用你需要的容量创建一个新的buffer, 然后将原来包装的buffer中的数据复制到这个新的buffer中.

新的buffer类型: CompositeByteBuf

一个名叫CompositeByteBuf 的buffer实现为composite buffer实现定义了多个高级操作。用户可以使用composite buffer节省大量的内存复制,只比随机访问buffer的代价大一点。
为了创建一个新的composite buffer, 可以像以前一样使用Unpooled.wrappedBuffer(...) , 也可以使用Unpooled.compositeBuffer(...), 或者 ByteBufAllocator.compositeBuffer().

可预知的NIO buffer转型

在3.x中, ChannelBuffer.toByteBuffer() 以及它的变体所提供的约定并不那么明确。用户无法确定这些方法会返回一个拥有共享数据的视图buffer还是一个拥有独立数据的通过复制得到的buffer(a view buffer with shared data or a copied buffer with separate data)。
4.0 中使用 ByteBuf.nioBufferCount(), nioBuffer(), 和 nioBuffers()代替toByteBuffer(). 如果nioBufferCount() 返回 0, 用户总是可以通过copy().nioBuffer()得到一个复制buffer.

对小端序(Little endian)支持的改变

小端序(Little endian)的支持做了很大的改变。先前用户为了得到小端序buffer,可以通过LittleEndianHeapChannelBufferFactory 或者按指定的字节序包装一个已有的buffer:
4.0 中增加了一个新的方法: ByteBuf.order(ByteOrder), 它返回当前buffer对象的一个具有指定字节序的视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);

池化buffer Pooled buffers

Netty 4引入了一个高性能的buffer池, 它是 jemalloc 的变种, 组合了 buddy allocationslab allocation的功能. 有以下好处:
*减少了 GC 压力,因为使用unpooled buffer会带来频繁的内存分配和回收

  • 减少了内存的带宽消耗(memory bandwidth consumption) 因为新的内存不可避免地要用zero填充(初始化)
  • 定时回收 direct buffers

为了使用这个特性, 除非用户非要得到unpooled buffer, 他应该通过 ByteBufAllocator分配buffer:

1
2
3
4
5
6
7
8
9
10
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 被写入到远程端, 它立即自动地放回原来的buffer池中.

默认的 ByteBufAllocatorPooledByteBufAllocator. 如果你不想使用buffer池, 抑或你想使用自己的allocator, 使用 Channel.config().setAllocator(...) 设置可选的allocator, 比如 UnpooledByteBufAllocator.

注意: 当我们使用的默认allocator 是 UnpooledByteBufAllocator时, 一旦我们确保PooledByteBufAllocator没有内存泄漏的问题, 我们应该将默认的allocator换回PooledByteBufAllocator.

ByteBuf 总是使用引用计数(reference counted)

为了精确的控制ByteBuf生命周期,Netty不再依赖垃圾回收器,而是使用一个显示的引用计数器. 基本规则如下:

  • 当 buffer 首次分配时, 它的初始引用计数为 1.
  • 当buffer的引用计数降为0时,它被回收或者放回原始的buffer池中.
  • 下面的动作会触发IllegalReferenceCountException异常:
    • 访问引用计数为0的buffer,
    • 将引用计数降少为负数,
    • 或者将引用计数的值超过 Integer.MAX_VALUE.
  • 衍生的buffers (比如 slices 和 duplicates) 和 交换的buffers (比如 little endian buffers) 和原buffer共享同一个引用计数器. 注意,当一个衍生的buffer创建时它的引用计数器不会改变.

ByteBufChannelPipeline中使用时,你需要注意一些额外的规则:

  • 管道中的每个 inbound (a.k.a. upstream) handler 必须release接收到的消息. Netty不会自动release 它们.
    • 注意 codec 框架的确自动 release消息, 用户如果想传递一个as-is message给下一个handler, 就不得不增加引用计数器 .
  • 当outbound (a.k.a. downstream) message 到达了管道的开始位置时,Netty会在写完它后 release it.

自动 buffer泄漏检查

尽管引用计数很强大,但却容易出错。为了帮助用户找到他们忘记release buffer的地方, 泄漏检查器会自动地将那些泄漏buffer分配时的堆栈信息输出到日志中.

因为泄漏检查器(leak detector)依靠PhantomReference 并且获取堆栈信息是一个花费很大的操作, 它只会采样大约1%的分配. 因此,最好运行应用程序相当长的时间来找到所有可能的泄漏是一个好主意.

只要所有的泄漏否被发现和修改, 你就可以将这个特性关闭, 通过-Dio.netty.noResourceLeakDetection JVM 参数. (译者: jinfo可以运行时修改jvm参数,但是不是所有的参数都能被它修改)

io.netty.util.concurrent

除了 buffer API, 4.0还提供了写异步应用程序的一些通用的类,它们被放入包 io.netty.util.concurrent. 一些类如:

  • FuturePromise - 类似ChannelFuture, 但是不依赖 Channel
  • EventExecutorEventExecutorGroup - 通用的 event loop API

它们是channel API基础,文章后面会有解释. 例如, ChannelFuture 扩展 io.netty.util.concurrent.FutureEventLoopGroup 扩展 EventExecutorGroup.

Event loop type hierarchy diagramEvent loop type hierarchy diagram

Channel API 的改变

在4.0中, 很多 io.netty.channel 包下的类都经历了大的整理 所以简单的文本搜索-替换不会将老的Netty 3.x 应用轻松迁移到 4.0.
这一节将尽量介绍这么大的变化背后的考量,而不是罗列所有的变化.

改组 ChannelHandler 接口

Upstream → Inbound, Downstream → Outbound

术语 'upstream' 和 'downstream' 对新人来讲很让人困惑。 4.0 使用 'inbound' 和 'outbound' .

新的 ChannelHandler 类型继承关系

在3.x 中, ChannelHandler 只是一个 tag interface, 而 ChannelUpstreamHandler, ChannelDownstreamHandler, 和LifeCycleAwareChannelHandler 定义了实际的 handler 方法.
在Netty 4中, ChannelHandler 合并了 LifeCycleAwareChannelHandler 连同许多新方法, 它们对inbound 和 outbound handler 很有用:

1
2
3
4
5
public interface ChannelHandler {
void handlerAdded(ChannelHandlerContext ctx) throws Exception;
void handlerRemoved(ChannelHandlerContext ctx) throws Exception;
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}

下图描绘了新的类型继承关系:

ChannelHandler type hierarchy diagramChannelHandler type hierarchy diagram

ChannelHandler 不需要 event object

在3.x中, 每个 I/O 操作都会创建安一个 ChannelEvent 对象. 对于每一次 read / write, 它都额外的创建一个新的ChannelBuffer.
它极大地简化了Netty的内部处理,因为它代理了JVM的资源管理和buffer池.
然而,这经常也是Netty应用程序在高负载的情况下GC压力大的原因.

4.0完全移除了event object, 取而代之的是强类型的方法调用. 3.x 包含处理所有事件的handler method如handleUpstream()handleDownstream(), 但Netty 4.0中 每个 event 类型都有它自己的handler method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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 也根据上面改变而改变了:

1
2
3
4
5
// Before:
ctx.sendUpstream(evt);
 
// After:
ctx.fireChannelRead(receivedMessage);

所有这些改变意味着用户无法扩展一个不存在的 ChannelEvent 接口了. 然而用户怎么定义他自己的事件类型呢,比如 IdleStateEvent?
4.0的ChannelInboundHandler 有一个 userEventTriggered() handler method,可以用来处理这样的需求.

简化channel 状态模型

在3.x中,当一个新的Channel被创建并连接成功,至少三个ChannelStateEvent会被触发:channelOpen、channelBound以及channelConnected.当一个Channel关闭时,也至少有三个事件会被触发:channelDisconnected、channelUnbound以及channelClosed.
Netty 3 Channel state diagramNetty 3 Channel state diagram

但是,触发这么多事件的意义并不那么大。更有用的是当一个Channel进入可读或可写的状态时通知用户。
Netty 4 Channel state diagramNetty 4 Channel state diagram

channelOpen, channelBound, 和 channelConnected 被合并到 channelActive. channelDisconnected,channelUnbound, 和 channelClosed 被合并到 channelInactive.
同样 Channel.isBound()isConnected() 也被合并为isActive().

需要注意的是,channelRegistered and channelUnregistered 这两个事件与channelOpen and channelClosed并不等价。它们是在支持Channel的动态注册、注销以及再注册时被引入的新的状态,就像下图所示:
Netty 4 Channel state diagram for re-registrationNetty 4 Channel state diagram for re-registration

write() 不会自动 flush

4.0 引入了新的操作 flush() 它可以显示地将Channel输出缓存输出. write()操作并不会自动 flush. 你可以把它想象成java.io.BufferedOutputStream, 除了 它工作于消息级这一点.
由于这个改变, 你必须万分小心,写入数据后不要忘了调用 ctx.flush() . 当然你也可以使用一个更直接的方法writeAndFlush().

可知的和不易出错的入站流量挂起(暂停读取)

(Sensible and less error-prone inbound traffic suspension)
3.x有一个由Channel.setReadable(boolean)提供的不是很明显的入站流量挂起机制。它在ChannelHandler之间带来了复杂的交互操作,同时handler由于不正确实现很容易互相干扰。

4.0里,增加了一个新的名为read() 的outbound操作。如果你使用Channel.config().setAutoRead(false)来关闭默认的auto-read标志,Netty不会读入任何东西,直到你显式地调用read()操作.
一旦你启动的read() 操作完成,同时channel再次停止读,一个名为channelReadSuspended()的inbound事件会被触发, 这样你就能够重新启动另一次的read() 操作。你同样也可以拦截read() 操作来执行更多高级的流量控制。

暂停接受新的的连接

Netty 3.0 中用户没有办法停止接受新的连接, 除非使用阻塞的I/O线程或者关闭server socket. 4.0中 当auto-read没有设置时 read() 操作就像一个原始的channel.

半关闭的socket

TCP和SCTP允许用户关闭一个socket的outbound流量而不用完全关闭它。这样的socket被称为"半关闭的socket",用户能够通过调用SocketChannel.shutdownOutput()方法来获取一个半关闭socket。如果一个远端关闭了outbound流量,SocketChannel.read(..)会返回-1,这看上去并没有和一个关闭了的连接有什么区别。

3.x没有 shutdownOutput() 操作。它总是在 SocketChannel.read(..) 返回-1的时候关闭连接。为了支持半关闭socket,4.0增加了SocketChannel.shutdownOutput() 方法,同时用户能设置ALLOW_HALF_CLOSUREChannelOption来阻止Netty在SocketChannel.read(..) 返回-1的时候自动关闭连接.

灵活的I/O线程分配

在3.x里,一个 Channel 是由ChannelFactory创建的,同时新建的 Channel 会自动注册到一个隐藏的I/O线程。4.0使用新的名为 EventLoopGroup 的接口来替换ChannelFactoryEventLoopGroup包含一个或多个EventLoop。一个新的Channel 也不会自动注册到EventLoopGroup,但用户可以显式调用EventLoopGroup.register() 来注册。

由于这个改变(例如,分离了ChannelFactory和I/O线程),用户可以注册不同的 Channel实现到同一个EventLoopGroup,或者同一个 Channel实现到不同的EventLoopGroup
例如,你可以运行一个NIO server socket,NIO client socket,NIO UDP socket,以及虚拟机内的本地通道在同一个I/O线程里。在编写一个需要最小延迟的代理服务器时这确实很有用。

能够从一个已存在的jdk套接字上创建一个Channel

3.x没有方法从已存在的jdk套接字(如java.nio.channels.SocketChannel. )创建一个新的channel。4.0可以。

取消注册和重新注册一个Channel从/到一个I/O线程

3.x中一旦一个新的Channel被创建,它就完全绑定到一个单一的I/O线程上,直到它底层的socket关闭。在4.0中,用户能够从I/O线程里取消注册一个Channel来完全控制它底层jdk套接字。例如,你能够利用Netty提供的高层次非阻塞I/O的优势来解决复杂的协议,然后取消注册Channel 并且切换到阻塞模式来在可能的最大吞吐量下传输一个文件。当然,它能够再次注册已经取消了注册的Channel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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管理 EventLoop中。EventLoop实现了 java.util.concurrent.ScheduledExecutorService接口。这意味着用户可以在一个用户channel归属的I/O线程里运行或调度一个任意的Runnable或Callable。使用良好设计的线程模型(稍后会介绍),编写一个线程安全的handler变得极其容易地。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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() { ... });
}
}

简化shutdown

releaseExternalResources()不再使用。你可以通过调用EventLoopGroup.shutdownGracefully()关闭所有打开的channel同时使所有I/O线程停止.

类型安全的ChannelOption

有两种方法来配置Netty的Channel的socket参数。一种是明确地调用ChannelConfig的setter,例如SocketChannelConfig.setTcpNoDelay(true).。这是最为类型安全的方法。另外一种是调用ChannelConfig.setOption() 方法。有时候你不得不在运行时的时候socket决定要配置什么选项,这个方法就特别适合这样的情况。然而,在3.x里它是容易出错的,因为一个用户必需用一个字符串/对象对来指定选项。如果用户调用了错误的选项名或者值,他或她将会遇到一个ClassCastException 异常或指定的选项甚至可能会默默地忽视了。

4.0引入了名为ChannelOption的新的类型,它提供了类型安全地访问socket选项。

1
2
3
4
5
6
7
8
9
10
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

在处理用户指令里,你可以附加任意的对象到ChannelChannelHandlerContext。一个名为AttributeMap的新接口被加入了,它被ChannelChannelHandlerContext继承。相反,ChannelLocalChannel.attachment被移除。这些属性会在他们关联的Channel 垃圾回收的同时回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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();
}
...
}

新的 bootstrap API

bootstrap API已经被重写,尽管它的目的还是一样;它执行需要配置和运行服务器或客户端程序的典型步骤,通常能在样板代码里找到。
新的bootstrap同样采取了流式接口(fluent interface)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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

就像你在在上面的例子注意到的一样,ChannelPipelineFactory 不再存在了。而是由 ChannelInitializer来替换,它给予了ChannelChannelPipeline 配置的更多控制。

请注意,你不能自己创建一个新的ChannelPipeline。通过观察目前为止的用例报告,Netty团队得到一个结论,让用户去创建自己的管道实现或者是继承默认的实现是没有好处的。因此,ChannelPipeline 不再让用户创建。ChannelPipelineChannel自动创建。

ChannelFutureChannelFutureChannelPromise

ChannelFuture已经被拆分为ChannelFutureChannelPromise。这不仅仅是让异步操作里的生产者和消费者间的约定更明显,同样也是得在使用从链中(如filtering)返回的ChannelFuture更加安全,因为ChannelFuture的状态是不能改变的。

由于这个改变,一些方法现在都采用ChannelPromise 而不是ChannelFuture 来改变它的状态。

良好定义的线程模型

3.x中没有很好定义线程模型, 尽管3.5尝试修改它的不一致性。
4.0定义了一个严格的线程模型,可以帮助用户编写ChannelHandler, 而用户不必太担心线程安全.

  • Netty 绝不会并发地调用 ChannelHandler'的方法, 除非 ChannelHandler 被标记为 @Sharable. 这与handler方法的类型无关 - inbound, outbound, life cycle event handler method.
    • 用户不必同步inbound or outbound event handler方法.
    • 4.0 不允许增加 ChannelHandler 多次, 除非它被标记为 @Sharable.
  • Netty 调用的ChannelHandler之间总是遵循 happens-before .
    • 用户不必定义volatile字段来保存 handler 的状态.
  • 用户往ChannelPipeline中增加handler时可以指定 EventExecutor.
    • 如果指定了一个EventExecutor, ChannelHandler的handler方法总是被这个指定的 EventExecutor执行.
    • 如果没有指定, handler 方法总是被它的Channel注册的 EventLoop 执行.
  • 指派给handler或者channle的EventExecutorEventLoop 总是单线程的.
    • handler 方法总是被同一个线程调用.
    • 如果使用多线程的EventExecutorEventLoop, 首先会选择其中一个线程, 然后一直使用这个线程,直到取消注册(deregistration).
    • 如果在同意管道pipeline中的两个handler 被指派给不同的 EventExecutor, 它们可以被并发地调用. 如果它们访问共享数据, 用户必须小心线程安全 .
  • 增加到ChannelFutureChannelFutureListeners 总是此Channel相关的EventLoop 执行.

没有 ExecutionHandler 了 - 它被放入核心代码中.

在你往ChannelPipeline增加ChannelHandler 时你可以指定一个EventExecutor, 这样管道总是使用这个EventExecutor来调用这个新增加的 ChannelHandler的handler方法.

1
2
3
4
5
6
7
8
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需要一个handler来创建和管理它的buffer(看这篇文章的 Per-handler buffer章节)然而,从用户角度来看这些变化并不大。

  • 核心编解码类移入到 io.netty.handler.codec 包下.
  • FrameDecoder 被重新命名为 ByteToMessageDecoder.
  • OneToOneEncoderOneToOneDecoderMessageToMessageEncoderMessageToMessageDecoder 取代.
  • decode(), decodeLast(), encode() 的方法签名有些许改变以便支持泛型, 也移除了一些冗余的参数.

Codec embedder → EmbeddedChannel

Codec embedder 被 io.netty.channel.embedded.EmbeddedChannel 取代, 允许用户对任何包含编码解码器的管道进行测试.

HTTP codec

HTTP 解码器现在对每个HTTP消息中总是生成多个消息对象:

1
2
3
1 * HttpRequest / HttpResponse
0 - n * HttpContent
1 * LastHttpContent

如果想了解更多的细节, 请参考最新的 HttpSnoopServer 例子. 如果你不想为每个HTTP消息生成多个消息, 你可以将一个 HttpObjectAggregator 加入到管道中. HttpObjectAggregator 总是将多个消息转换成一个单一的FullHttpRequest 或者 FullHttpResponse.

transport 实现的改变

新加了下面的 transports :

  • OIO SCTP transport
  • UDT transport

用例学习:迁移阶乘(Factorial)的例子

这部分粗略地展示把阶乘例子从3.0迁移到4.0的步骤。阶乘例子已经移植到4.0了,它被放在io.netty.example.factorial包里。请浏览示例的源代码来看下每一处的变化。

迁移服务端

  1. 使用新的bootstrap API重写 FactorialServer.run() 方法.
    1. 不再使用 ChannelFactory . 初始化一个 NioEventLoopGroup (一个用来接受连接,其它的用来处理接受后的连接.
  2. 重命名 FactorialServerPipelineFactoryFactorialServerInitializer.
    1. 让它扩展 ChannelInitializer<Channel>.
    2. 不创建一个 ChannelPipeline, 而是通过 Channel.pipeline()得到它.
  3. FactorialServerHandler 扩展 ChannelInboundHandlerAdapter.
    1. channelInactive()替换 channelDisconnected() .
    2. 不再使用handleUpstream().
    3. messageReceived() 命名为 channelRead(), 并相应的调整方法签名.
    4. ctx.writeAndFlush()替换 ctx.write() .
  4. BigIntegerDecoder 扩展 ByteToMessageDecoder<BigInteger>.
  5. NumberEncoder 扩展 MessageToByteEncoder<Number>.
    1. encode() 不再返回一个buffer. 由ByteToMessageDecoder负责将编码的数据填入到buffer中.

迁移客户端

大部分和移植服务端差不多,但在编写一个潜在的大数据流时要注意。

  1. 使用新的 bootstrap API 重写 FactorialClient.run() 方法.
  2. FactorialClientPipelineFactory 重命名为 FactorialClientInitializer.
  3. FactorialClientHandler 扩展 ChannelInboundHandlerAdapter
 

频道:Java