第三章Netty应用上学习笔记

一、Netty是什么?为什么要用Netty?

Netty是由JBOSS提供的一个java开源框架,现为 Github上的独立项目。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。

也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。

“快速”和“简单”并不用产生维护性或性能上的问题。Netty 是一个吸收了多种协议(包括FTP、SMTP、HTTP等各种二进制文本协议)的实现经验,并经过相当精心设计的项目。最终,Netty 成功的找到了一种方式,在保证易于开发的同时还保证了其应用的性能,稳定性和伸缩性。

1、为什么要用Netty

1)虽然JAVA NIO框架提供了多路复用IO的支持,但是并没有提供上层“信息格式”的良好封装。例如前两者并没有提供针对Protocol、Buffer、JSON这些信息格式的封装,但是Netty框架提供了这些数据格式封装(基于责任链模式的编码和解码功能)。

2)NIO的类库和API相当复杂,使用它来开发,需要非常熟练地掌握Selector、ByteBuffer、ServerSocketChannel、SocketChannel等,需要很多额外的编程技能来辅助使用NIO,例如,因为NIO涉及了Reactor线程模型,所以必须必须对多线程和网络编程非常熟悉才能写出高质量的NIO程序。

3)要编写一个可靠的、易维护的、高性能的NIO服务器应用。除了框架本身要兼容实现各类操作系统的实现外。更重要的是它应该还要处理很多上层特有服务,例如:客户端的权限、还有上面提到的信息格式封装、简单的数据读取、断连重连、半包读写、心跳等等,这些Netty框架都提供了响应的支持。

4)JAVA NIO框架存在一个poll/epoll bug:Selector doesn’t block on Selector.select(timeout),不能block意味着CPU的使用率会变成100%(这是底层JNI的问题,上层要处理这个异常实际上也好办)。当然这个bug只有在Linux内核上才能重现。

这个问题在JDK 1.7版本中还没有被完全解决,但是Netty已经将这个bug进行了处理。这个Bug与操作系统机制有关系的,JDK虽然仅仅是一个兼容各个操作系统平台的软件,但在JDK5和JDK6最初的版本中(严格意义上来将,JDK部分版本都是),这个问题并没有解决,而将这个帽子抛给了操作系统方,这也就是这个bug最终一直到2013年才最终修复的原因(JDK7和JDK8之间)。

2、为什么不用Netty5

  • netty5 中使用了ForkJoinPool,增加了代码的复杂度,但是对性能的改善却不明显。
  • 多个分支的代码同步工作量很大。
  • 作者觉得当下还不到发布一个新版本的时候。
  • 在发布版本之前,还有更多问题需要调查一下,比如是否应该废弃exceptionCaught,是否暴露EventExecutorChooser等等。

3、为什么Netty使用NIO而不是AIO?

Netty不看重Windows上的使用,在Linux系统上,AIO的底层实现仍使用EPOLL,没有很好实现AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易深度优化。

AIO还有个缺点是接收数据需要预先分配缓存, 而不是NIO那种需要接收时才需要分配缓存, 所以对连接数量非常大但流量小的情况, 内存浪费很多。

据说Linux上AIO不够成熟,处理回调结果速度跟不上处理需求,有点像外卖员太少,顾客太多,供不应求,造成处理速度有瓶颈。

作者原话:

Not faster than NIO (epoll) on unix systems (which is true)

There is no daragram suppport

Unnecessary threading model (too much abstraction without usage)

二、第一个Netty程序

1、Channel

Channel 是Java NIO 的一个基本构造。它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O操作的程序组件)的开放连接,如读操作和写操作。

目前,可以把Channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以被打开或者被关闭,连接或者断开连接。

2、回调和Future

一个回调其实就是一个方法,一个指向已经被提供给另外一个方法的方法的引用。这使得后者可以在适当的时候调用前者。回调在广泛的编程场景中都有应用,而且也是在操作完成后通知相关方最常见的方式之一。Netty在内部使用了回调来处理事件,当一个回调被触发时,相关的事件可以被一个interface-ChannelHandler 的实现处理。

Future提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。

JDK预置了interface java.util.concurrent.Future,但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成。这是非常繁琐的,所以Netty提供了它自己的实现ChannelFuture,用于在执行异步操作的时候使用。

ChannelFuture提供了几种额外的方法,这些方法使得我们能够注册一个或者多个ChannelFutureListener实例。监听器的回调方法operationComplete(),将会在对应的操作完成时被调用。然后监听器可以判断该操作是成功地完成了还是出错了。如果是后者,我们可以检索产生的Throwable。简而言之,由ChannelFutureListener提供的通知机制消除了手动检查对应的操作是否完成的必要。每个Netty的出站I/O 操作都将返回一个ChannelFuture。

3、事件和ChannelHandler

Netty使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。

Netty事件是按照它们与入站或出站数据流的相关性进行分类的,可能由入站数据或者相关的状态更改而触发的事件包括:连接已被激活或者连接失活、数据读取、用户事件、错误事件。

出站事件是未来将会触发的某个动作的操作结果,这些动作包括:

  • 打开或者关闭到远程节点的连接;
  • 将数据写到或者冲刷到套接字。

每个事件都可以被分发给ChannelHandler 类中的某个用户实现的方法。

可以认为每个ChannelHandler 的实例都类似于一种为了响应特定事件而被执行的回调。

Netty 提供了大量预定义的可以开箱即用的ChannelHandler 实现,包括用于各种协议(如HTTP 和SSL/TLS)的ChannelHandler。

示例:Hello,Netty!参见模块netty-basic中包com.chj.nettybasic.echo

三、Netty组件再了解

1、Channel、EventLoop(Group)和ChannelFuture

Netty 网络抽象的代表:

Channel—Socket;

EventLoop—控制流、多线程处理、并发;

ChannelFuture—异步通知。

Channel和EventLoop关系如图:

1.1、Channel接口

基本的I/O操作(bind()、connect()、read()和write())依赖于底层网络传输所提供的原语。在基于Java 的网络编程中,其基本的构造是类Socket。Netty的Channel接口所提供的API,被用于所有的I/O操作。大大地降低了直接使用Socket类的复杂性。此外Channel也是拥有许多预定义的、专门化实现的广泛类层次结构的根。

由于Channel是独一无二的,所以为了保证顺序将Channel声明为java.lang.Comparable 的一个子接口。因此,如果两个不同的Channel实例都返回了相同的散列码,那么AbstractChannel 中的compareTo()方法的实现将会抛出一个Error。

1.2、Channel 的生命周期状态

  • ChannelUnregistered :Channel 已经被创建,但还未注册到EventLoop
  • ChannelRegistered :Channel 已经被注册到了EventLoop
  • ChannelActive :Channel处于活动状态(已经连接到它的远程节点),它现在可以接收和发送数据了
  • ChannelInactive :Channel没有连接到远程节点,当这些状态发生改变时,将会生成对应的事件。这些事件将会被转发给ChannelPipeline中的ChannelHandler,其可以随后对它们做出响应。

1)最重要Channel 的方法

  • eventLoop: 返回分配给Channel的EventLoop。
  • pipeline: 返回分配给Channel的ChannelPipeline。
  • isActive: 如果Channel是活动的,则返回true。活动的意义可能依赖于底层的传输。例如,一个Socket 传输一旦连接到了远程节点便是活动的,而一个Datagram传输一旦被打开便是活动的。
  • localAddress: 返回本地的SokcetAddress。
  • remoteAddress: 返回远程的SocketAddress。
  • write: 将数据写到远程节点,这个数据将被传递给ChannelPipeline,并且排队直到它被冲刷。
  • flush: 将之前已写的数据冲刷到底层传输,如一个Socket。
  • writeAndFlush: 一个简便的方法,等同于调用write()并接着调用flush()。

2)EventLoop和EventLoopGroup

回想一下我们在NIO中是如何处理我们关心的事件的?在一个while循环中select出事件,然后依次处理每种事件。我们可以把它称为事件循环,这就是EventLoop,interface io.netty.channel. EventLoop定义了Netty的核心抽象,用于处理网络连接的生命周期中所发生的事件。

io.netty.util.concurrent包构建在JDK的java.util.concurrent包上。而io.netty.channel包中的类,为了与Channel的事件进行交互,扩展了这些接口/类。一个EventLoop将由一个永远都不会改变的Thread驱动,同时任务(Runnable或者Callable)可以直接提交给EventLoop实现,以立即执行或者调度执行。

根据配置和可用核心的不同,可能会创建多个EventLoop实例用以优化资源的使用,并且单个EventLoop 可能会被指派用于服务多个Channel。

Netty的EventLoop在继承了ScheduledExecutorService的同时,只定义了一个方法parent()。在Netty 4 中,所有的I/O操作和事件都由已经被分配给了EventLoop的那个Thread来处理。

1.3、任务调度

偶尔你将需要调度一个任务以便稍后(延迟)执行或者周期性地执行。例如,你可能想要注册一个在客户端已经连接了5分钟之后触发的任务。一个常见的用例是,发送心跳消息到远程节点,以检查连接是否仍然还活着。如果没有响应,你便知道可以关闭该Channel了。

1.4、线程管理

在内部,当提交任务到如果(当前)调用线程正是支撑EventLoop 的线程,那么所提交的代码块将会被(直接)执行。否则,EventLoop 将调度该任务以便稍后执行,并将它放入到内部队列中。当EventLoop下次处理它的事件时,它会执行队列中的那些任务/事件。

1.5、线程的分配

服务于Channel的I/O和事件的EventLoop则包含在EventLoopGroup中。异步传输实现只使用了少量的EventLoop(以及和它们相关联的Thread),而且在当前的线程模型中,它们可能会被多个Channel所共享。这使得可以通过尽可能少量的Thread来支撑大量的Channel,而不是每个Channel分配一个Thread。EventLoopGroup负责为每个新创建的Channel分配一个EventLoop。在当前实现中,使用顺序循环round-robin的方式进行分配以获取一个均衡的分布,并且相同的EventLoop可能会被分配给多个Channel。

一旦一个Channel被分配给一个EventLoop,它将在它的整个生命周期中都使用这个EventLoop(以及相关联的Thread)。请牢记这一点,因为它可以使你从担忧你的ChannelHandler实现中的线程安全和同步问题中解脱出来。

需要注意EventLoop的分配方式对ThreadLocal的使用的影响,因为一个EventLoop通常会被用于支撑多个Channel,所以对于所有相关联的Channel来说,ThreadLocal都将是一样的。这使得它对于实现状态追踪等功能来说是个糟糕的选择。然而在一些无状态的上下文中,它仍然可以被用于在多个Channel之间共享一些重度的或者代价昂贵的对象,甚至是事件。

1.6、ChannelFuture接口

Netty中所有的I/O 操作都是异步的,因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。为此Netty提供了ChannelFuture接口,其addListener()方法注册了一个ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知。

可以将ChannelFuture看作是将来要执行的操作的结果的占位符。它究竟什么时候被执行则可能取决于若干的因素,因此不可能准确地预测,但是可以肯定的是它将会被执行。

2、ChannelHandler、ChannelPipeline和ChannelHandlerContext

2.1、ChannelHandler接口

从应用程序开发人员的角度来看,Netty的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler的方法是由网络事件触发的。事实上ChannelHandler可专门用于几乎任何类型的动作,例如将数据从一种格式转换为另外一种格式,例如各种编解码,或者处理转换过程中所抛出的异常。

举例来说ChannelInboundHandler是一个你将会经常实现的子接口,这种类型的ChannelHandler接收入站事件和数据,这些数据随后将会被你的应用程序的业务逻辑所处理。当你要给连接的客户端发送响应时,也可以从ChannelInboundHandler直接冲刷数据然后输出到对端。应用程序的业务逻辑通常实现在一个或者多个ChannelInboundHandler中。

这种类型的ChannelHandler接收入站事件和数据,这些数据随后将会被应用程序的业务逻辑所处理。

2.2、ChannelHandler的生命周期

接口ChannelHandler定义的生命周期操作,在ChannelHandler被添加到ChannelPipeline中或者被从ChannelPipeline中移除时会调用这些操作。这些方法中的每一个都接受一个ChannelHandlerContext参数。

  • handlerAdded:当把ChannelHandler添加到ChannelPipeline中时被调用。
  • handlerRemoved:当从ChannelPipeline中移除ChannelHandler时被调用。
  • exceptionCaught:当处理过程中在ChannelPipeline中有错误产生时被调用。
  • Netty定义了下面两个重要的ChannelHandler子接口:
  • ChannelInboundHandler:处理入站数据以及各种状态变化;
  • ChannelOutboundHandler:处理出站数据并且允许拦截所有的操作。

2.3、ChannelInboundHandler接口

下面列出了接口ChannelInboundHandler的生命周期方法,这些方法将会在数据被接收时或者与其对应的Channel状态发生改变时被调用,正如我们前面所提到的,这些方法和Channel的生命周期密切相关。

  • channelRegistered:当Channel已经注册到它的EventLoop并且能够处理I/O时被调用。
  • channelUnregistered:当Channel从它的EventLoop注销并且无法处理任何I/O时被调用。
  • channelActive:当Channel处于活动状态时被调用;Channel已经连接/绑定并且已经就绪。
  • channelInactive:当Channel离开活动状态并且不再连接它的远程节点时被调用。
  • channelReadComplete:当Channel上的一个读操作完成时被调用。
  • channelRead:当从Channel读取数据时被调用。
  • ChannelWritabilityChanged:当Channel 的可写状态发生改变时被调用,可以通过调用Channel 的isWritable()方法来检测Channel 的可写性。与可写性相关的阈值可以通过Channel.config().
  • setWriteHighWaterMark()和Channel.config().setWriteLowWaterMark()方法来设置。
  • userEventTriggered:当ChannelnboundHandler.fireUserEventTriggered()方法被调用时被调用。

2.4、ChannelOutboundHandler接口

出站操作和数据将由ChannelOutboundHandler处理,它的方法将被Channel、Channel-Pipeline以及ChannelHandlerContext调用。

所有由ChannelOutboundHandler本身所定义的方法:

  • bind(ChannelHandlerContext,SocketAddress,ChannelPromise):当请求将Channel绑定到本地地址时被调用。
  • connect(ChannelHandlerContext,SocketAddress,SocketAddress,ChannelPromise):当请求将Channel连接到远程节点时被调用。
  • disconnect(ChannelHandlerContext,ChannelPromise):当请求将Channel从远程节点断开时被调用。
  • close(ChannelHandlerContext,ChannelPromise) 当请求关闭Channel时被调用。
  • deregister(ChannelHandlerContext,ChannelPromise):当请求将Channel从它的EventLoop注销时被调用。
  • read(ChannelHandlerContext):当请求从Channel 读取更多的数据时被调用。
  • flush(ChannelHandlerContext):当请求通过Channel 将入队数据冲刷到远程节点时被调用。
  • write(ChannelHandlerContext,Object,ChannelPromise):当请求通过Channel将数据写到远程节点时被调用。

2.5、ChannelHandler的适配器

有一些适配器类可以将编写自定义的ChannelHandler所需要的工作降到最低限度,因为它们提供了定义在对应接口中的所有方法的默认实现。因为你有时会忽略那些不感兴趣的事件,所以Netty提供了抽象基类ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter。

你可以使用ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter类作为自己的ChannelHandler的起始点。这两个适配器分别提供了ChannelInboundHandler和ChannelOutboundHandler的基本实现。通过扩展抽象类ChannelHandlerAdapter,它们获得了它们共同的超接口ChannelHandler的方法。

ChannelHandlerAdapter还提供了实用方法isSharable()。如果其对应的实现被标注为Sharable,那么这个方法将返回true,表示它可以被添加到多个ChannelPipeline。

2.6、ChannelPipeline接口

当Channel被创建时,它将会被自动地分配一个新的ChannelPipeline,这项关联是永久性的;Channel既不能附加另外一个ChannelPipeline,也不能分离其当前的。在Netty 组件的生命周期中,这是一项固定的操作,不需要开发人员的任何干预。

使得事件流经ChannelPipeline是ChannelHandler的工作,它们是在应用程序的初始化或者引导阶段被安装的。这些对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个ChannelHandler。它们的执行顺序是由它们被添加的顺序所决定的。

入站和出站ChannelHandler可以被安装到同一个ChannelPipeline中,如果一个消息或者任何其他的入站事件被读取,那么它会从ChannelPipeline的头部开始流动,最终数据将会到达ChannelPipeline的尾端,届时所有处理就都结束了。

数据的出站运动在概念上也是一样的,在这种情况下,数据将从ChannelOutboundHandler链的尾端开始流动,直到它到达链的头部为止。在这之后出站数据将会到达网络传输层,这里显示为Socket,通常情况下,这将触发一个写操作。

如果将两个类别的ChannelHandler都混合添加到同一个ChannelPipeline中会发生什么。虽然ChannelInboundHandle和ChannelOutboundHandle都扩展自ChannelHandler,但是Netty 能区分ChannelInboundHandler实现和ChannelOutboundHandler实现,并确保数据只会在具有相同定向类型的两个ChannelHandler之间传递。

2.7、ChannelPipeline上的方法

addFirstaddBeforeaddAfteraddLast将一个ChannelHandler添加到ChannelPipeline中

  • Remove:将一个ChannelHandler从ChannelPipeline中移除
  • Replace:将ChannelPipeline中的一个ChannelHandler替换为另一个ChannelHandler
  • get:通过类型或者名称返回ChannelHandler
  • Context:返回和ChannelHandler绑定的ChannelHandlerContext
  • Names:返回ChannelPipeline中所有ChannelHandler的名称
  • ChannelPipeline:的API 公开了用于调用入站和出站操作的附加方法。

2.8、ChannelHandlerContext

通过使用作为参数传递到每个方法的ChannelHandlerContext,事件可以被传递给当前ChannelHandler 链中的下一个ChannelHandler。虽然这个对象可以被用于获取底层的Channel,但是它主要还是被用于写出站数据。

ChannelHandlerContext代表了ChannelHandler和ChannelPipeline之间的关联,每当有ChannelHandler添加到ChannelPipeline中时,都会创建ChannelHandler-Context。ChannelHandlerContext的主要功能是管理它所关联的ChannelHandler和在同一个ChannelPipeline中的其他ChannelHandler之间的交互。

ChannelHandlerContext有很多的方法,其中一些方法也存在于Channel和Channel-Pipeline本身上,但是有一点重要的不同。如果调用Channel或者ChannelPipeline上的这些方法,它们将沿着整个ChannelPipeline 进行传播。而调用位于ChannelHandlerContext上的相同方法,则将从当前所关联的ChannelHandler开始,并且只会传播给位于该ChannelPipeline中的下一个(入站下一个,出站上一个)能够处理该事件的ChannelHandler。

ChannelHandlerContext API

  • Alloc:返回和这个实例相关联的Channel所配置的ByteBufAllocator
  • Bind:绑定到给定的SocketAddress,并返回ChannelFuture
  • Channel:返回绑定到这个实例的Channel
  • Close:关闭Channel,并返回ChannelFuture
  • Connect:连接给定的SocketAddress,并返回ChannelFuture
  • Deregister:从之前分配的EventExecutor注销,并返回ChannelFuture
  • Disconnect:从远程节点断开,并返回ChannelFuture
  • Executor:返回调度事件的EventExecutor
  • fireChannelActive:触发对下一个ChannelInboundHandler上的channelActive()方法(已连接)的调用
  • fireChannelInactive:触发对下一个ChannelInboundHandler上的channelInactive()方法(已关闭)的调用
  • fireChannelRead:触发对下一个ChannelInboundHandler上的channelRead()方法(已接收的消息)的调用
  • fireChannelReadComplete:触发对下一个ChannelInboundHandler上的channelReadComplete()方法的调用。
  • fireChannelRegistered:触发对下一个ChannelInboundHandler上的fireChannelRegistered()方法的调用
  • fireChannelUnregistered:触发对下一个ChannelInboundHandler上的fireChannelUnregistered()方法的调用
  • fireChannelWritabilityChanged:触发对下一个ChannelInboundHandler上的fireChannelWritabilityChanged()方法的调用。
  • fireExceptionCaught:触发对下一个ChannelInboundHandler上的fireExceptionCaught(Throwable)方法的调用
  • fireUserEventTriggered:触发对下一个ChannelInboundHandler上的fireUserEventTriggered(Object evt)方法的调用。
  • Handler:返回绑定到这个实例的ChannelHandler
  • isRemoved:如果所关联的ChannelHandler 已经被从ChannelPipeline中移除则返回true
  • Name:返回这个实例的唯一名称
  • Pipeline:返回这个实例所关联的ChannelPipeline
  • Read:将数据从Channel读取到第一个入站缓冲区,如果读取成功则触发一个channelRead事件,并(在最后一个消息被读取完成后)通知ChannelInboundHandler的channelReadComplete(ChannelHandlerContext)方法。

当使用ChannelHandlerContext的API的时候,有以下两点:

  • ChannelHandlerContext 和ChannelHandler之间的关联(绑定)是永远不会改变的,所以缓存对它的引用是安全的;
  • 如同我们在本节开头所解释的一样,相对于其他类的同名方法,ChannelHandler Context的方法将产生更短的事件流,应该尽可能地利用这个特性来获得最大的性能。

3、选择合适的内置通信传输模式

NIO io.netty.channel.socket.nio 使用java.nio.channels 包作为基础——基于选择器的方式

Epoll io.netty.channel.epoll由 JNI驱动的epoll()和非阻塞 IO。这个传输支持只有在Linux上可用的多种特性,如SO_REUSEPORT,比NIO传输更快,而且是完全非阻塞的。将NioEventLoopGroup替换为EpollEventLoopGroup,并且将NioServerSocketChannel.class替换为EpollServerSocketChannel.class即可。

OIO io.netty.channel.socket.oio 使用java.net包作为基础——使用阻塞流。

Local io.netty.channel.local 可以在VM内部通过管道进行通信的本地传输。

Embedded io.netty.channel.embedded Embedded传输,允许使用ChannelHandler而又不需要一个真正的基于网络的传输。在测试ChannelHandler实现时非常有用。

4、引导Bootstrap

网络编程里“服务器”和“客户端”实际上表示了不同的网络行为;换句话说,是监听传入的连接还是建立到一个或者多个进程的连接。

因此,有两种类型的引导:一种用于客户端(简单地称为Bootstrap),而另一种(ServerBootstrap)用于服务器。无论你的应用程序使用哪种协议或者处理哪种类型的数据,唯一决定它使用哪种引导类的是它是作为一个客户端还是作为一个服务器。

比较Bootstrap

 

Bootstrap

ServerBootstrap

网络编程中的作用

连接到远程主机和端口

绑定到一个本地端口

EventLoopGroup 的数目  

1

2

ServerBootstrap将绑定到一个端口,因为服务器必须要监听连接,而Bootstrap则是由想要连接到远程节点的客户端应用程序所使用的。

第二个区别可能更加明显。引导一个客户端只需要一个EventLoopGroup,但是一个ServerBootstrap 则需要两个(也可以是同一个实例)。

因为服务器需要两组不同的Channel。第一组将只包含一个ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理传入客户端连接(对于每个服务器已经接受的连接都有一个)的Channel。

与ServerChannel相关联的EventLoopGroup将分配一个负责为传入连接请求创建Channel的EventLoop。一旦连接被接受,第二个EventLoopGroup就会给它的Channel分配一个EventLoop。

在引导过程中添加多个ChannelHandler

Netty提供了一个特殊的ChannelInboundHandlerAdapter子类:

public abstract class ChannelInitializer<C extends Channel> ext ends ChannelInboundHandlerAdapter

它定义了下面的方法:protect ed abstract void initChannel(C ch) throws Exception;

这个方法提供了一种将多个ChannelHandler添加到一个ChannelPipeline中的简便方法。你只需要简单地向Bootstrap或ServerBootstrap的实例提供你的ChannelInitializer实现即可,并且一旦Channel被注册到了它的EventLoop之后,就会调用你的initChannel()版本。在该方法返回之后,ChannelInitializer的实例将会从ChannelPipeline中移除它自己。

5、ChannelOption

ChannelOption的各种属性在套接字选项中都有对应。

1)ChannelOption.SO_BACKLOG

ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,

服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小

2)ChannelOption.SO_REUSEADDR

ChanneOption.SO_REUSEADDR对应于套接字选项中的SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口,

比如,某个服务器进程占用了TCP的80端口进行监听,此时再次监听该端口就会返回错误,使用该参数就可以解决问题,该参数允许共用该端口,这个在服务器程序中比较常使用,比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置SO_REUSEADDR    就无法正常使用该端口。

3)ChannelOption.SO_KEEPALIVE

Channeloption.SO_KEEPALIVE参数对应于套接字选项中的SO_KEEPALIVE,该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。

4)ChannelOption.SO_SNDBUF和ChannelOption.SO_RCVBUF

ChannelOption.SO_SNDBUF参数对应于套接字选项中的SO_SNDBUF,ChannelOption.SO_RCVBUF参数对应于套接字选项中的SO_RCVBUF这两个参数用于操作接收缓冲区和发送缓冲区的大小,接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。

5)ChannelOption.SO_LINGER

ChannelOption.SO_LINGER参数对应于套接字选项中的SO_LINGER,Linux内核默认的处理方式是当用户调用close()方法的时候,函数返回,在可能的情况下,尽量发送数据,不一定保证会发生剩余的数据,造成了数据的不确定性,使用SO_LINGER可以阻塞close()的调用时间,直到数据完全发送

6)ChannelOption.TCP_NODELAY

ChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关,Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网络的有效负载,但是却造成了延时,而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输,于TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。

6、ByteBuf

ByteBuf API 的优点:

  • 它可以被用户自定义的缓冲区类型扩展;
  • 通过内置的复合缓冲区类型实现了透明的零拷贝;
  • 容量可以按需增长(类似于JDK的StringBuilder);
  • 在读和写这两种模式之间切换不需要调用ByteBuffer的flip()方法;
  • 读和写使用了不同的索引;
  • 支持方法的链式调用;
  • 支持引用计数;
  • 支持池化。

ByteBuf维护了两个不同的索引,名称以read或者write开头的ByteBuf方法,将会推进其对应的索引,而名称以set或者get开头的操作则不会。如果打算读取字节直到readerIndex达到和writerIndex同样的值时会发生什么?在那时你将会到达“可以读取的”数据的末尾。就如同试图读取超出数组末尾的数据一样,试图读取超出该点的数据将会触发一个IndexOutOf-BoundsException。

可以指定ByteBuf的最大容量,试图移动写索引(即writerIndex)超过这个值将会触发一个异常(默认的限制是Integer.MAX_VALUE)

6.1、分配

堆缓冲区

最常用的ByteBuf模式是将数据存储在JVM的堆空间中。这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放。可以由hasArray()来判断检查ByteBuf是否由数组支撑,如果不是则这是一个直接缓冲区。

直接缓冲区

直接缓冲区是另外一种ByteBuf模式,直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。

ByteBufAllocator

Netty通过interface ByteBufAllocator分配我们所描述过的任意类型的ByteBuf 实例

名称

描述

buffer()

返回一个基于堆或者直接内存存储的ByteBuf

heapBuffer()

返回一个基于堆内存存储的ByteBuf

directBuffer()

返回一个基于直接内存存储的ByteBuf

compositeBuffer()

返回一个可以通过添加最大到指定数目的基于堆的或者直接内存存储的缓冲区来扩展的CompositeByteBuf

ioBuffer()

返回一个用于套接字的I/O操作的ByteBuf,当所运行的环境具有sun.misc.Unsafe支持时,返回基于直接内存存储的ByteBuf,否则返回基于堆内存存储的ByteBuf;当指定使用PreferHeapByteBufAllocator时,则只会返回基于堆内存存储的ByteBuf。

可以通过Channel(每个都可以有一个不同的ByteBufAllocator实例)或者绑定到ChannelHandler的ChannelHandlerContext获取一个到ByteBufAllocator的引用。

Netty提供了两种ByteBufAllocator的实现:PooledByteBufAllocator和Unpooled-ByteBufAllocator,前者池化了ByteBuf的实例以提高性能并最大限度地减少内存碎片,后者的实现不池化ByteBuf实例,并且在每次它被调用时都会返回一个新的实例。

Netty4.1默认使用了PooledByteBufAllocator。

Unpooled缓冲区

Netty提供了一个简单的称为Unpooled的工具类,它提供了静态的辅助方法来创建未池化的ByteBuf

实例。

  • buffer():返回一个未池化的基于堆内存存储的ByteBuf
  • directBuffer():返回一个未池化的基于直接内存存储的ByteBuf
  • wrappedBuffer():返回一个包装了给定数据的ByteBuf
  • copiedBuffer():返回一个复制了给定数据的ByteBuf
  • Unpooled:类还可用于ByteBuf 同样可用于那些并不需要Netty 的其他组件的非网络项目。

6.2、随机访问索引/顺序访问索引/读写操作

如同在普通的Java 字节数组中一样,ByteBuf 的索引是从零开始的:第一个字节的索引是0,最后一个字节的索引总是capacity() - 1。使用那些需要一个索引值参数(随机访问,也即是数组下标)的方法(的其中)之一来访问数据既不会改变readerIndex 也不会改变writerIndex。如果有需要,也可以通过调用readerIndex(index)或者writerIndex(index)来手动移动这两者。顺序访问通过索引访问

有两种类别的读/写操作:

  • get()和set()操作,从给定的索引开始并且保持索引不变;get+数据字长(bool.byte,int,short,long,bytes)
  • read()和write()操作,从给定的索引开始,并且会根据已经访问过的字节数对索引进行调整。

更多的操作

  • isReadable():如果至少有一个字节可供读取,则返回true
  • isWritable():如果至少有一个字节可被写入,则返回true
  • readableBytes():返回可被读取的字节数
  • writableBytes():返回可被写入的字节数
  • capacity():返回ByteBuf可容纳的字节数,在此之后,它会尝试再次扩展直到达到maxCapacity()
  • maxCapacity():返回ByteBuf可以容纳的最大字节数
  • hasArray():如果ByteBuf由一个字节数组支撑,则返回true
  • array():如果ByteBuf由一个字节数组支撑则返回该数组,否则它将抛出一个UnsupportedOperationException异常

6.3、可丢弃字节

为可丢弃字节的分段包含了已经被读过的字节,通过调用discardRead-Bytes()方法,可以丢弃它们并回收空间。这个分段的初始大小为0,存储在readerIndex中,会随着read操作的执行而增加(get*操作不会移动readerIndex)。

缓冲区上调用discardReadBytes()方法后,可丢弃字节分段中的空间已经变为可写的了。频繁地调用discardReadBytes()方法以确保可写分段的最大化,但是请注意,这将极有可能会导致内存复制,因为可读字节必须被移动到缓冲区的开始位置。建议只在有真正需要的时候才这样做,因为内存非常宝贵的。

6.4、可读字节

ByteBuf 的可读字节分段存储了实际数据,新分配的、包装的或者复制的缓冲区的默认的readerIndex 值为0。

6.5、可写字节

可写字节分段是指一个拥有未定义内容的、写入就绪的内存区域。新分配的缓冲区的writerIndex的默认值为0。任何名称以write开头的操作都将从当前的writerIndex处开始写数据,并将它增加已经写入的字节数。

6.6、索引管理

调用markReaderIndex()、markWriterIndex()、resetWriterIndex()和resetReaderIndex()来标记和重置ByteBuf的readerIndex和writerIndex。

也可以通过调用readerIndex(int)或者writerIndex(int)来将索引移动到指定位置。试图将任何一个索引设置到一个无效的位置都将导致一个IndexOutOfBoundsException。可以通过调用clear()方法来将readerIndex和writerIndex都设置为0。注意,这并不会清除内存中的内容。

6.7、查找操作

在ByteBuf中有多种可以用来确定指定值的索引的方法,最简单的是使用indexOf()方法。

较复杂的查找可以通过调用forEach Byte(),代码展示了一个查找回车符(\r)的例子。

ByteBuf byteBuf = ...;

int index = byteBuf.forEachByte(ByteBufProcessor.FIND_CR);

6.8、派生缓冲区

派生缓冲区为ByteBuf 提供了以专门的方式来呈现其内容的视图。这类视图是通过以下方法被创建的:

  • duplicate();
  • slice();
  • slice(int, int);
  • Unpooled.unmodifiableBuffer(…);
  • order(ByteOrder);
  • readSlice(int)。

每个这些方法都将返回一个新的ByteBuf实例,它具有自己的读索引、写索引和标记索引;其内部存储和JDK的ByteBuffer一样也是共享的。

ByteBuf复制,如果需要一个现有缓冲区的真实副本,请使用copy()或者copy(int, int)方法;不同于派生缓冲区,由这个调用所返回的ByteBuf 拥有独立的数据副本。

6.9、引用计数

引用计数是一种通过在某个对象所持有的资源不再被其他对象引用时释放该对象所持有的资源来优化内存使用和性能的技术;Netty在第4版中为ByteBuf引入了引用计数技术,interface ReferenceCounted。

6.10、工具类

ByteBufUtil 提供了用于操作ByteBuf的静态的辅助方法,因为这个API是通用的,并且和池化无关,所以这些方法已然在分配类的外部实现。

这些静态方法中最有价值的可能就是hexdump()方法,它以十六进制的表示形式打印ByteBuf的内容。这在各种情况下都很有用,例如:出于调试的目的记录ByteBuf的内容,十六进制的表示通常会提供一个比字节值的直接表示形式更加有用的日志条目,此外,十六进制的版本还可以很容易地转换回实际的字节表示。另一个有用的方法是boolean equals(ByteBuf, ByteBuf),它被用来判断两个ByteBuf实例的相等性。

6.11、资源释放

当某个ChannelInboundHandler的实现重写channelRead()方法时,它要负责显式地释放与池化的ByteBuf 实例相关的内存,Netty为此提供了一个实用方法ReferenceCountUtil.release()。

Netty 将使用WARN 级别的日志消息记录未释放的资源,使得可以非常简单地在代码中发现违规的实例,但是以这种方式管理资源可能很繁琐;一个更加简单的方式是使用SimpleChannelInboundHandler,SimpleChannelInboundHandler 会自动释放资源。

1)对于入站请求,Netty的EventLoo在处理Channel的读操作时进行分配ByteBuf,对于这类ByteBuf,需要我们自行进行释放,有三种方式,或者使用SimpleChannelInboundHandler、或者在重写channelRead()方法使用ReferenceCountUtil.release()或者使用ctx.fireChannelRead继续向后传递。

2)对于出站请求,不管ByteBuf是否由我们的业务创建的,当调用了write或者writeAndFlush方法后,Netty会自动替我们释放,不需要我们业务代码自行释放。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值