Netty补充

Netty和Mina是Java世界非常知名的通讯框架。它们都出自同一个作者,Mina诞生略早,属于Apache基金会,而Netty开始在Jboss名下,后来出来自立门户netty.io。关于Mina已有@FrankHui的Mina系列文章,我正好最近也要做一些网络方面的开发,就研究一下Netty的源码,顺便分享出来了。 Netty目前有两个分支:4.x和3.x。4.0分支重写了很多东西,并对项目进行了分包,规模比较庞大,入手会困难一些,而3.x版本则已经被广泛使用。

Netty团队大概从3.3.0开始,移到netty.io

从4开始,Netty团队做了模块依赖的优化,像Grizzly一样,分离出很多功能独立的Package。比方说,你希望使用Netty的buffer组件,只需简单依赖这个包就好了。不过本着严谨的态度,我们还是来看下netty-all这个一揽子包里究竟都有哪些依赖,

 <dependencies>
        <dependency>
            <groupId>io.nettygroupId>
            <artifactId>netty-handlerartifactId>
            <version>4.0.23.Finalversion>
        dependency>
    dependencies>

线程模型


Netty3只保证 upstream事件在IO线程里执行,但是所有的downstream事件会被调用线程处理,

它可能是IO线程,也可能是用户自定义线程,这就带来了一个问题,用户需要小心地处理同步操作。

除此之外,还会面临线程上下文切换的风险,设想一下,你在write的时候遇到了异常,

转而触发exceptionCaught,但这是一个upstream事件,怎么办?


Netty4的线程模型则不存在此类问题,因为所有的操作都被保证在同一个EventLoop里的同一个Thread完成。

也就是说Netty4不存在并发访问 ChannelHandler,当然这个前提是你没有给该handler打上Sharable注解。

同时它也能保证 happens-before关系,所以你也没必要在 ChannelHandler声明volatile field。

用户可以指定自己的 EventExecutor来执行特定的 handler。

通常情况下,这种EventExecutor是单线程的,

当然,如果你指定了多线程的 EventExecutor或者 EventLoop,线程sticky特性会保证,

除非出现 deregistration,否则其中的一个线程将一直占用。

如果两个handler分别注册了不同的EventExecutor,这时就要注意线程安全问题了。


Netty4的线程模型还是有很多可以优化的地方,比方说目前Eventloop对channel的处理不均等问题,而这些问题都会在Netty 5里面优化掉,


Channel状态模型


Netty补充_第1张图片

第二附图是Netty4优化过的模型。可以看到,channelOpen,channelBound,和channelConnected 已经被channelActive替代。channelDisconnected,channelUnbound和channelClosed 也被 channelInactive替代。

Netty 4

这里就产生了两个问题:

其一,channelRegistered and channelUnregistered 不等价于 channelOpen and channelClosed,它是Netty4新引入的状态为了实现Channel的dynamic registration, deregistration, and re-registration。

第二, 既然是合并,那原先针对channelOpen的方法如何迁移?简单来做,可以直接迁移到替代方法里面。

Handler

1. ChannelPipelineFactory ----> ChannelInitializer

这里需要注意的是,ChannelPipeline的创建方式发生了变化,原先是这么玩的,

ChannelPipeline cp = Channels.pipeline();
现在得这么玩
ChannelPipeline cp = ch.pipeline();

用Netty团队的话来说就是:

“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 a Channel.”

2. SimpleChannelHandler ----> ChannelDuplexHandler

之前是这么玩的

public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e)
      throws Exception
  {
    if (e instanceof ChannelStateEvent) {
      ChannelStateEvent cse = (ChannelStateEvent) e;
      switch (cse.getState()) {
        case OPEN:
          if (Boolean.TRUE.equals(cse.getValue())) {
            // connect
            channelCount.incrementAndGet();
            allChannels.add(e.getChannel());
          }
          else {
            // disconnect
            channelCount.decrementAndGet();
            allChannels.remove(e.getChannel());
          }
          break;
        case BOUND:
          break;
      }
    }


    if (e instanceof UpstreamMessageEvent) {
      UpstreamMessageEvent ume = (UpstreamMessageEvent) e;
      if (ume.getMessage() instanceof ChannelBuffer) {
        ChannelBuffer cb = (ChannelBuffer) ume.getMessage();
        int readableBytes = cb.readableBytes();
        //  compute stats here, bytes read from remote
        bytesRead.getAndAdd(readableBytes);
      }
    }
    ctx.sendUpstream(e);
  }


  public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent e)
      throws Exception
  {
    if (e instanceof DownstreamMessageEvent) {
      DownstreamMessageEvent dme = (DownstreamMessageEvent) e;
      if (dme.getMessage() instanceof ChannelBuffer) {
        ChannelBuffer cb = (ChannelBuffer) dme.getMessage();
        int readableBytes = cb.readableBytes();
        // compute stats here, bytes written to remote
        bytesWritten.getAndAdd(readableBytes);
      }
    }
    ctx.sendDownstream(e);
  }

改成ChannelDuplexHandler之后,我只需要重写read和write方法,来完成同样的功能。

其它

1. 通过下面的代码来完成Channel的限流

ctx.channel().setReadable(false);//Before
       ctx.channel().config().setAutoRead(false);//After
2.  TCP参数优化
// 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
3. 单元测试经常用到的CodecEmbedder类已经变名为EmbeddedChannel
@Test
      public void testMultipleLinesStrippedDelimiters() {
    EmbeddedChannel ch = new EmbeddedChannel(new DelimiterBasedFrameDecoder(8192, true,
    Delimiters.lineDelimiter()));
    ch.writeInbound(Unpooled.copiedBuffer("TestLine\r\ng\r\n", Charset.defaultCharset()));
    assertEquals("TestLine", releaseLater((ByteBuf) ch.readInbound()).toString(Charset.defaultCharset()));
    assertEquals("g", releaseLater((ByteBuf) ch.readInbound()).toString(Charset.defaultCharset()));
    assertNull(ch.readInbound());
    ch.finish();
      }
4. 简化的关闭操作,以前我是这么玩stop的
if (serverChannel != null) {
      log.info("stopping transport {}:{}",getName(), port);
      // first stop accepting
      final CountDownLatch latch = new CountDownLatch(1);
      serverChannel.close().addListener(new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture future) throws Exception {
          // stop and process remaining in-flight invocations
          if (def.getExecutor() instanceof ExecutorService) {
            ExecutorService exe = (ExecutorService) getExecutor();
            ShutdownUtil.shutdownExecutor(exe, "dispatcher");
          }
          latch.countDown();
        }
      });
      latch.await();
      serverChannel = null;
    }

    // If the channelFactory was created by us, we should also clean it up. If the
    // channelFactory was passed in by Bootstrap, then it may be shared so don't clean  it up.
    if (channelFactory != null) {
      ShutdownUtil.shutdownChannelFactory(channelFactory, bossExecutor, ioWorkerExecutor,allChannels);
      }
}
现在我得这么玩
public void stop() throws InterruptedException {
        // Wait until the server socket is closed.
        channelFuture.channel().closeFuture().syncUninterruptibly();
        
        bossGroup.shutdownGracefully().syncUninterruptibly();
        workerGroup.shutdownGracefully().syncUninterruptibly();
    }
5. 编解码命名改变

FrameDecoder ----> ByteToMessageDecoder

OneToOneEncoder  ----> MessageToMessageEncoder 

OneToOneDecoder ----> MessageToMessageDecoder

6. 心跳逻辑优化,之前我是这么玩的

 cp.addLast("idleTimeoutHandler", new IdleStateHandler(getTimer(),
                                                                          getClientIdleTimeout().toMillis(),
                                                                          NO_WRITER_IDLE_TIMEOUT,
                                                                          NO_ALL_IDLE_TIMEOUT,
                                                                          TimeUnit.MILLISECONDS));
  cp.addLast("heartbeatHandler", new HeartbeatHandler());

其中HeartbeatHandler 继承了IdleStateAwareChannelHandler。在Netty4里,IdleStateAwareChannelHandler已经去除,但 IdleStateHandler类还存在,所以我会这么玩

 cp.addLast("idleTimeoutHandler", new IdleStateHandler(
                                NO_WRITER_IDLE_TIMEOUT, NO_WRITER_IDLE_TIMEOUT,
                                NO_ALL_IDLE_TIMEOUT, TimeUnit.MILLISECONDS));

  cp.addLast("heartbeatHandler", new HeartbeatHandler());

其中,HeartbeatHandler 继承了ChannelInboundHandlerAdapter。具体的实现逻辑这里就不贴出来了。再啰嗦几句,很多同学喜欢自己启线程去做心跳逻辑,我是不推荐这种方式的。利用Netty的链路空闲检测机制可以很好的完成这个功能,能更好地配合Netty线程模型和异常捕获机制。自己定制,处理不好,会带来很大的线上隐患。

小结

这篇文章简单记录了升级过程中遇到的一些比较higher的话题,配上代码,希望能更好的重现整个升级思路和过程,也希望能给大家带来帮助。如果你在升级过程中遇到了问题,欢迎留言交流。最后,祝玩的开心~


不像Netty3.x和4.x之间的变化,5.x没有那么大的变化,不过也取得了其简化设计中的一些突破性进展.。我们力求尽可能平滑的从4.x版本过度到5.x版本,如果你在迁移过程中遇到任何问题,请告知我们。

核心变化
  
支持Android
 
提供了:
  •  移动设备变成更加强大
  • 通过Ice Cream Sandwich解决了在ADK中最著名的与NIO和SSLEngine相关的问题,且
  • 用户显然想要重用他们应用中的的编解码和处理器代码。
我们决定官方支持Android(4.0及以上版本)

 
简化处理器层次
 
  ChannelInboundHandler和ChannelOutboundHandler整合为ChannelHandler。ChannelHandler现在包含输入和输出的处理方法。
 
  ChannelInboundHandlerAdapter,ChannelOutboundHandlerAdapter和ChannelDuplexHandlerAdapter已被废弃,由 ChannelHandlerAdapter代替。
 
  由于现在无法区分处理器(handler) 是输入还是输出的处理器,CombinedChannelDuplexHandler现在由 ChannelHandlerAppender代替。


   channelRead0() → messageReceived()
 
  我知道。这是一个愚蠢的错误。如果你使用了SimpleChannelInboundHandler,你需要把channelRead0()重命名为messageReceived()。

 
废弃中移除的
 
  Channel.deregister()已被移除。不再生效和被使用。取而代之的,我们将允许Channel被充注册到不同的事件循环。
 
   ChannelHandlerContext.attr(..) == Channel.attr(..)
 
  Channel和ChannelHandlerContext类都实现了AttributeMap接口,使用户可以在其上关联一个或多个属性。有时会让用户感到困惑的是Channel和ChannelHandlerContext都有其自己的存储用户定义属性的容器。例如,即使你通过Channel.attr(KEY_X).set(valueX)给属性'KEY_X’赋值,你却无法通过ChannelHandlerContext.attr(KEY_X).get()方法获取到值。反之亦是如此。这种行为不仅仅令人不解而且还浪费内存。
 
  为了解决这个问题,我们决定每个Channel内部仅保留一个map。AttributeMap总是用AttributeKey作为它的key。AttributeKey确保键的唯一性,因此每个Channel中如果存在一个以上的属性容易是多余的。只要用户把他自己的AttributeKey定义成ChannelHandler的private static final变量,就不会有出现重复key的风险。
 

更简单更精确的缓冲区泄漏追踪
 
   之前,查找缓冲区泄漏是很困难的,并且泄漏的警告信息也不是很有帮助。现在我们有了增强的泄漏报告机制,该机制会在增长超过上限时触发。
 
  更多的信息可查看:http://netty.io/wiki/reference-counted-objects.html 。由于该特性十分重要,所以也移植入了4..0.14.Final版中。

 
PooledByteBufAllocator成为默认的allocator
 
  在4.x版本中,UnpooledByteBufAllocator是默认的allocator,尽管其存在某些限制。现在PooledByteBufAllocator已经广泛使用一段时间,并且我们有了增强的缓冲区泄漏追踪机制,所以是时候让PooledByteBufAllocator成为默认了。
 

全局唯一的Channel ID
 
 
  每个Channel现在有了全局唯一的ID,其生成的依据是:
 
     ● MAC地址(EUI-48或是EUI-64),最好是全局唯一的,
       当前进程的ID
       System#currentTimeMillis()
       System#nanoTime()
       随机的32位整数,以及
       系列递增的32位整数
 
  可通过Channel.id()方法获取Channel的ID。
 

更灵活的线程模型
  
  增加了新的ChannelHandlerInvoker接口,用于使用户可以选择使用哪个线程调用事件处理方法。替代之前的在向ChannelPipeline添加 ChannelHandler时指定一个EventExecutor的方式,使用该特性需要指定一个用户自定义的 ChannelHandlerInvoker实现。
 
  关于该变化更多的信息,可参考: https://github.com/netty/netty/commit/132af3a485015ff912bd567a47881814d2ce1828
 

EmbeddedChannel的易用性
 
  EmbeddedChannel中的readInbound()和readOutbound()方法返回专门类型的参数,因此你不必在转换他们的返回值。这可以简化你的测试用例代码。
[java]  view plain  copy
  1. EmbeddedChannel ch = ...;  
  2.   
  3. // BEFORE:  
  4. FullHttpRequest req = (FullHttpRequest) ch.readInbound();  
  5.   
  6. // AFTER:  
  7. FullHttpRequest req = ch.readInbound();  
 
使用Executor代替ThreadFactory
  
  有些应用要求用户使用Executor运行他们的任务。4.x版本要求用户在创建事件循环(event loop)时指定ThreadFacotry,现在不再是这样了。
 
  关于该变化的更多信息,可参考:https://github.com/netty/netty/pull/1762
 

Class loader友好化
 
  一些类型,如AttributeKey对于在容器环境下运行的应用是不友好的,现在不是了。
 

编解码和处理器(handlers)
 
       XmlFrameDecoder支持流式的XML文档
      ● 二进制的memcache协议编解码
       支持SPDY/3.1 (也移植到了4.x版本)
       重构了HTTP多部分的编解码



   channelRead0() → messageReceived()
 
  我知道。这是一个愚蠢的错误。如果你使用了SimpleChannelInboundHandler,你需要把channelRead0()重命名为messageReceived()。

 
废弃中移除的
 
  Channel.deregister()已被移除。不再生效和被使用。取而代之的,我们将允许Channel被充注册到不同的事件循环。
 
   ChannelHandlerContext.attr(..) == Channel.attr(..)
 
  Channel和ChannelHandlerContext类都实现了AttributeMap接口,使用户可以在其上关联一个或多个属性。有时会让用户感到困惑的是Channel和ChannelHandlerContext都有其自己的存储用户定义属性的容器。例如,即使你通过Channel.attr(KEY_X).set(valueX)给属性'KEY_X’赋值,你却无法通过ChannelHandlerContext.attr(KEY_X).get()方法获取到值。反之亦是如此。这种行为不仅仅令人不解而且还浪费内存。
 
  为了解决这个问题,我们决定每个Channel内部仅保留一个map。AttributeMap总是用AttributeKey作为它的key。AttributeKey确保键的唯一性,因此每个Channel中如果存在一个以上的属性容易是多余的。只要用户把他自己的AttributeKey定义成ChannelHandler的private static final变量,就不会有出现重复key的风险。
 

更简单更精确的缓冲区泄漏追踪
 
   之前,查找缓冲区泄漏是很困难的,并且泄漏的警告信息也不是很有帮助。现在我们有了增强的泄漏报告机制,该机制会在增长超过上限时触发。
 
  更多的信息可查看:http://netty.io/wiki/reference-counted-objects.html 。由于该特性十分重要,所以也移植入了4..0.14.Final版中。

 
PooledByteBufAllocator成为默认的allocator
 
  在4.x版本中,UnpooledByteBufAllocator是默认的allocator,尽管其存在某些限制。现在PooledByteBufAllocator已经广泛使用一段时间,并且我们有了增强的缓冲区泄漏追踪机制,所以是时候让PooledByteBufAllocator成为默认了。
 

全局唯一的Channel ID
 
 
  每个Channel现在有了全局唯一的ID,其生成的依据是:
 
     ● MAC地址(EUI-48或是EUI-64),最好是全局唯一的,
       当前进程的ID
       System#currentTimeMillis()
       System#nanoTime()
       随机的32位整数,以及
       系列递增的32位整数
 
  可通过Channel.id()方法获取Channel的ID。
 

更灵活的线程模型
  
  增加了新的ChannelHandlerInvoker接口,用于使用户可以选择使用哪个线程调用事件处理方法。替代之前的在向ChannelPipeline添加 ChannelHandler时指定一个EventExecutor的方式,使用该特性需要指定一个用户自定义的 ChannelHandlerInvoker实现。
 
  关于该变化更多的信息,可参考: https://github.com/netty/netty/commit/132af3a485015ff912bd567a47881814d2ce1828
 

EmbeddedChannel的易用性
 
  EmbeddedChannel中的readInbound()和readOutbound()方法返回专门类型的参数,因此你不必在转换他们的返回值。这可以简化你的测试用例代码。
[java]  view plain  copy
  1. EmbeddedChannel ch = ...;  
  2.   
  3. // BEFORE:  
  4. FullHttpRequest req = (FullHttpRequest) ch.readInbound();  
  5.   
  6. // AFTER:  
  7. FullHttpRequest req = ch.readInbound();  
 
使用Executor代替ThreadFactory
  
  有些应用要求用户使用Executor运行他们的任务。4.x版本要求用户在创建事件循环(event loop)时指定ThreadFacotry,现在不再是这样了。
 
  关于该变化的更多信息,可参考:https://github.com/netty/netty/pull/1762
 

Class loader友好化
 
  一些类型,如AttributeKey对于在容器环境下运行的应用是不友好的,现在不是了。
 

编解码和处理器(handlers)
 
       XmlFrameDecoder支持流式的XML文档
      ● 二进制的memcache协议编解码
       支持SPDY/3.1 (也移植到了4.x版本)
       重构了HTTP多部分的编解码

你可能感兴趣的:(Netty)