4、Netty使用和常用组件
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId
<version>4.1.42.Final </version>
<scope>compile</scope>
</dependency>

1、API使用简单,开发门槛低;
2、功能强大,预置了多种编解码功能,支持多种主流协议;
3、定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;
4、性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优;
5、成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;
6、社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会加入;
7、经历了大规模的商业应用考验,质量得到验证。
Netty5 已经停止开发了。
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)
Bootstrap是Netty框架的启动类和主入口类,分为客户端类Bootstrap和服务器类ServerBootstrap两种。
Channel 是Java NIO 的一个基本构造。
它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O操作的程序组件)的开放连接,如读操作和写操作
目前,可以把Channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以被打开或者被关闭,连接或者断开连接。
EventLoop暂时可以看成一个线程、EventLoopGroup自然就可以看成线程组。
Netty 使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。
Netty事件是按照它们与入站或出站数据流的相关性进行分类的。
可能由入站数据或者相关的状态更改而触发的事件包括:连接已被激活或者连接失活;数据读取;用户事件;错误事件。
出站事件是未来将会触发的某个动作的操作结果,这些动作包括:打开或者关闭到远程节点的连接;将数据写到或者冲刷到套接字。
每个事件都可以被分发给ChannelHandler 类中的某个用户实现的方法,既然事件分为入站和出站,用来处理事件的ChannelHandler 也被分为可以处理入站事件的Handler和出站事件的Handler,当然有些Handler既可以处理入站也可以处理出站。
Netty 提供了大量预定义的可以开箱即用的ChannelHandler 实现,包括用于各种协议(如HTTP 和SSL/TLS)的ChannelHandler。
基于Netty的网络应用程序中根据业务需求会使用Netty已经提供的ChannelHandler或者自行开发ChannelHandler,这些ChannelHandler都放在ChannelPipeline中统一管理,事件就会在ChannelPipeline中流动,并被其中一个或者多个ChannelHandler处理。

Netty 中所有的I/O 操作都是异步的,我们知道“异步的意思就是不需要主动等待结果的返回,而是通过其他手段比如,状态通知,回调函数等”,那就是说至少我们需要一种获得异步执行结果的手段。
JDK 预置了interface java.util.concurrent.Future,Future 提供了一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成。这是非常繁琐的,所以Netty提供了它自己的实现ChannelFuture,用于在执行异步操作的时候使用。
参见模块netty-basic中包cn.tuling.nettybasic.echo
2、
io.netty.util.concurrent 包构建在JDK 的java.util.concurrent 包上。而io.netty.channel 包中的类,为了与Channel 的事件进行交互,扩展了这些接口/类。一个EventLoop 将由一个永远都不会改变的Thread 驱动,同时任务(Runnable 或者Callable)可以直接提交给EventLoop 实现,以立即执行或者调度执行。
服务于Channel 的I/O 和事件的EventLoop 包含在EventLoopGroup 中。
异步传输实现只使用了少量的EventLoop(以及和它们相关联的Thread),而且在当前的线程模型中,它们可能会被多个Channel 所共享。这使得可以通过尽可能少量的Thread 来支撑大量的Channel,。在当前实现中,使用顺序循环(round-robin)的方式进行分配以获取一个均衡的分布,并且相同的EventLoop可能会被分配给多个Channel。
****。

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

这样就不会有并发安全的问题了
比如:
EventLoop的具体实现:有线程 有阻塞队列,这是个类似线程池啊!

Netty 网络抽象的代表:
Channel—Socket;
EventLoop—控制流、多线程处理、并发;
ChannelFuture—异步通知。
Channel和EventLoop关系如图:

从图上我们可以看出Channel需要被注册到某个EventLoop上,在Channel整个生命周期内都由这个EventLoop处理IO事件,也就是说一个Channel和一个EventLoop进行了绑定,但是一个EventLoop可以同时被多个Channel绑定。这一点在“EventLoop和EventLoopGroup”节里也提及过。
基本的I/O 操作(bind()、connect()、read()和write())依赖于底层网络传输所提供的原语。在基于Java 的网络编程中,其基本的构造是类Socket。Netty 的Channel 接口所提供的API,被用于所有的I/O 操作。大大地降低了直接使用Socket 类的复杂性。此外,Channel 也是拥有许多预定义的、专门化实现的广泛类层次结构的根。
由于Channel 是独一无二的,所以为了保证顺序将Channel 声明为java.lang.Comparable 的一个子接口。因此,如果两个不同的Channel 实例都返回了相同的散列码,那么AbstractChannel 中的compareTo()方法的实现将会抛出一个Error。
ChannelUnregistered :Channel 已经被创建,但还未注册到EventLoop
ChannelRegistered :Channel 已经被注册到了EventLoop
ChannelActive :Channel 处于活动状态(已经连接到它的远程节点)。它现在可以接收和发送数据了
ChannelInactive :Channel 没有连接到远程节点
当这些状态发生改变时,将会生成对应的事件。这些事件将会被转发给ChannelPipeline 中的ChannelHandler,其可以随后对它们做出响应。在我们的编程中,关注ChannelActive 和ChannelInactive 会更多一些。
eventLoop: 返回分配给Channel 的EventLoop
pipeline: 返回Channel 的ChannelPipeline,也就是说每个Channel 都有自己的ChannelPipeline。
isActive: 如果Channel 是活动的,则返回true。活动的意义可能依赖于底层的传输。例如,一个Socket 传输一旦连接到了远程节点便是活动的,而一个Datagram 传输一旦被打开便是活动的。
localAddress: 返回本地的SokcetAddress
remoteAddress: 返回远程的SocketAddress
write: 将数据写到远程节点,注意,这个写只是写往Netty内部的缓存,还没有真正写往socket。
flush: 将之前已写的数据冲刷到底层socket进行传输。
writeAndFlush: 一个简便的方法,等同于调用write()并接着调用flush()

当Channel 被创建时,它将会被自动地分配一个新的ChannelPipeline,每个Channel 都有自己的ChannelPipeline。这项关联是永久性的。在Netty 组件的生命周期中,这是一项固定的操作,不需要开发人员的任何干预。
ChannelPipeline 提供了ChannelHandler 链的容器,并定义了用于在该链上传播**入站(也就是从网络到业务处理)**和 出站(也就是从业务处理到网络),各种事件流的API,我们代码中的ChannelHandler 都是放在ChannelPipeline 中的。
使得事件流经ChannelPipeline 是ChannelHandler 的工作,它们是在应用程序的初始化或者引导阶段被安装的。这些ChannelHandler 对象接收事件、执行它们所实现的处理逻辑,并将数据传递给链中的下一个ChannelHandler,而且ChannelHandler 对象也完全可以拦截事件不让事件继续传递。它们的执行顺序是由它们被添加的顺序所决定的。
在ChannelHandler被添加到ChannelPipeline 中或者被从ChannelPipeline 中移除时会调用下面这些方法。这些方法中的每一个都接受一个ChannelHandlerContext 参数。
handlerAdded 当把ChannelHandler 添加到ChannelPipeline 中时被调用
handlerRemoved 当从ChannelPipeline 中移除ChannelHandler 时被调用
exceptionCaught 当处理过程中在ChannelPipeline 中有错误产生时被调用
入站和出站ChannelHandler 被安装到同一个ChannelPipeline中,****形式进行维护管理。比如下图,我们在网络上传递的数据,要求加密,但是加密后密文比较大,需要压缩后再传输,而且按照业务要求,需要检查报文中携带的用户信息是否合法,于是我们实现了5个Handler:解压(入)Handler、压缩(出)handler、解密(入) Handler、加密(出) Handler、授权(入) Handler。

如果一个消息或者任何其他的入站事件被读取,那么它会从ChannelPipeline 的头部开始流动,但是只被处理入站事件的Handler处理,也就是解压(入)Handler、解密(入) Handler、授权(入) Handler,最终,数据将会到达ChannelPipeline 的尾端,届时,所有处理就都结束了。

数据的出站运动(即正在被写的数据)在概念上也是一样的。在这种情况下,数据将从链的尾端开始流动,但是只被处理出站事件的Handler处理,也就是加密(出) Handler、压缩(出)handler,直到它到达链的头部为止。在这之后,出站数据将会到达网络传输层,也就是我们的Socket。

Netty 能区分入站事件的Handler和出站事件的Handler,并确保数据只会在具有相同定向类型的两个ChannelHandler 之间传递。


所以在我们编写Netty应用程序时要注意,分属出站和入站不同的Handler ,在业务没特殊要求的情况下是无所谓顺序的,正如我们下面的图所示,比如‘压缩(出)handler‘可以放在‘解压(入)handler‘和‘解密(入) Handler‘中间,也可以放在‘解密(入) Handler‘和‘授权(入) Handler‘之间。
而同属一个方向的Handler则是有顺序的,因为上一个Handler处理的结果往往是下一个Handler的要求的输入。比如入站处理,对于收到的数据,只有先解压才能得到密文,才能解密,只有解密后才能拿到明文中的用户信息进行授权检查,所以解压->解密->授权这个三个入站Handler的顺序就不能乱。


Netty 能区分入站事件的Handler和出站事件的Handler,并确保数据只会在具有相同定向类型的两个ChannelHandler 之间传递。


所以在我们编写Netty应用程序时要注意,分属出站和入站不同的Handler ,在业务没特殊要求的情况下是无所谓顺序的,正如我们下面的图所示,比如‘压缩(出)handler‘可以放在‘解压(入)handler‘和‘解密(入) Handler‘中间,也可以放在‘解密(入) Handler‘和‘授权(入) Handler‘之间。
****只有解密后才能拿到明文中的用户信息进行授权检查,所以解压->解密->授权这个三个入站Handler的顺序就不能乱。


既然ChannelPipeline以双向链表的形式进行维护管理Handler,自然也提供了对应的方法在ChannelPipeline中增加或者删除、替换Handler。
addFirst、addBefore、addAfter、addLast
将一个ChannelHandler 添加到ChannelPipeline 中
**get **通过类型或者名称返回ChannelHandler
**context **返回和ChannelHandler 绑定的ChannelHandlerContext
**names **返回ChannelPipeline 中所有ChannelHandler 的名称
ChannelPipeline 的API 公开了用于调用入站和出站操作的附加方法。
ChannelHandlerContext 代表了ChannelHandler 和ChannelPipeline 之间的关联,每当有ChannelHandler 添加到ChannelPipeline 中时,都会创建ChannelHandlerContext,为什么需要这个ChannelHandlerContext ?前面我们已经说过,ChannelPipeline以双向链表的形式进行维护管理Handler,毫无疑问,Handler在放入ChannelPipeline的时候必须要有两个指针pre和next来说明它的前一个元素和后一个元素,但是Handler本身来维护这两个指针合适吗?想想我们在使用JDK的LinkedList的时候,我们放入LinkedList的数据是不会带这两个指针的,LinkedList内部会用类Node对我们的数据进行包装,而类Node则带有两个指针pre和next。
所以,ChannelHandlerContext 的主要作用就和LinkedList内部的类Node类似。
不过ChannelHandlerContext 不仅仅只是个包装类,它还提供了很多的方法,比如让事件从当前ChannelHandler传递给链中的下一个ChannelHandler,还可以被用于获取底层的Channel,还可以用于写出站数据。

💯
ChannelHandlerContext 有很多的方法,其中一些方法也存在于Channel 和Channel-Pipeline 本身上,****

我们用一个实际例子来说明,比如服务器收到对端发过来的报文,解压后需要进行解密,结果解密失败,要给对端一个应答。
如果发现解密失败原因是服务器和对端的加密算法不一致,应答报文只能以明文的压缩格式发送,就可以在解密handler中直接使用ctx.write给对端应答,这样应答报文就只经过压缩Handler就发往了对端;
其他情况下,应答报文要以加密和压缩格式发送,就可以在解密handler中使用channel.write()或者channelpipeline.write()给对端应答,这样应答报文就会流经整个出站处理过程。

**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(ctx)方法
**write **通过这个实例写入消息并经过ChannelPipeline
**writeAndFlush **通过这个实例写入并冲刷消息并经过ChannelPipeline
当使用ChannelHandlerContext 的API 的时候,有以下两点:
Netty 的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler 的方法是由网络事件触发的。事实上,ChannelHandler 可专门用于几乎任何类型的动作,例如将数据从一种格式转换为另外一种格式,例如各种编解码,或者处理转换过程中所抛出的异常。
举例来说,ChannelInboundHandler 是一个你将会经常实现的子接口。这种类型的ChannelHandler 接收入站事件和数据,这些数据随后将会被你的应用程序的业务逻辑所处理。当你要给连接的客户端发送响应时,也可以从ChannelInboundHandler 直接冲刷数据然后输出到对端。应用程序的业务逻辑通常实现在一个或者多个ChannelInboundHandler 中。
这种类型的ChannelHandler 接收入站事件和数据,这些数据随后将会被应用程序的业务逻辑所处理。
Netty 定义了下面两个重要的ChannelHandler 子接口:
下面列出了接口 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()方法被调用时被调用。
__
出站操作和数据将由ChannelOutboundHandler 处理。它的方法将被Channel、ChannelPipeline 以及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 注销时被调用
事件在ChannelPipeline里面流动,可能是数据、动作;要求读 的出站事件:netty触发了一个要求读更多数据的动作,这个动作被打包为一个事件,触发的read方法,读到了数据就去走inhandle的方法
**flush(ChannelHandlerContext) **当请求通过Channel 将入队数据冲刷到远程节点时被调用
有一些适配器类可以将编写自定义的ChannelHandler 所需要的工作降到最低限度,因为它们提供了定义在对应接口中的所有方法的默认实现。因为你有时会忽略那些不感兴趣的事件,所以Netty提供了抽象基类ChannelInboundHandlerAdapter(处理入站) 和ChannelOutboundHandlerAdapter(处理出站)。
我们可以使用ChannelInboundHandlerAdapter 和ChannelOutboundHandlerAdapter类作为自己的ChannelHandler 的起始点。这两个适配器分别提供了ChannelInboundHandler和ChannelOutboundHandler 的基本实现。通过扩展抽象类ChannelHandlerAdapter,它们获得了它们共同的超接口ChannelHandler 的方法。

不过ChannelOutboundHandler有个非常让人迷惑的read方法,ChannelOutboundHandler不是处理出站事件的吗?怎么会有read方法呢?其实这个read方法不是表示读数据,而是表示业务发出了读(read)数据的要求,这个要求也会封装为一个事件进行传播,这个事件因为是业务发出到网络的,自然就是个出站事件,而且这个事件触发的就是ChannelOutboundHandler中read方法。
可以参考cn.tuling.nettybasic.outread下的代码,可以看到

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

这就牵涉到了我们实现的Handler的共享性和线程安全性。回顾我们的Netty代码,在往pipeline安装Handler的时候,我们基本上是new出Handler的实例
因为每个socketChannel有自己的pipeline而且每个socketChannel又是和线程绑定的,所以这些Handler的实例之间完全独立的,只要Handler的实例之间不是共享了全局变量,Handler的实例是线程安全的。
但是如果业务需要我们在多个socketChannel之间共享一个Handler的实例怎么办呢?比如统计服务器接受到和发出的业务报文总数,我们就需要用一个Handler的实例来横跨所有的socketChannel来统计所有socketChannel业务报文数。
为了实现这一点,我们可以实现一个MessageCountHandler,并且在MessageCountHandler上使用Netty的@Sharable注解,然后在安装MessageCountHandler实例到pipeline时,共用一个即可。当然,因为MessageCountHandler实例是共享的,所以在实现MessageCountHandler的统计功能时,请务必注意线程安全,我们在具体实现时就使用了Java并发编程里的Atomic类来保证这一点。
具体代码请参考包cn.tuling.nettybasic.sharehndler。
回想一下我们在NIO中是如何接收和发送网络数据的?都是首先创建了一个Buffer,应用程序中的业务部分和Channel之间通过Buffer进行数据的交换:

Netty在处理网络数据时,同样也需要Buffer,在Read网络数据时由Netty创建Buffer,Write网络数据时Buffer往往是由业务方创建的。不管是读和写,Buffer用完后都必须进行释放,否则可能会造成内存泄露。
在Write网络数据时,可以确保数据被写往网络了,Netty会自动进行Buffer的释放,因为Netty会在pipeline中安装两个Handle:



Handler的顺序是:
网络 -> Head -> 我们的Handler -> Tail
在Read网络数据时,如果我们可以确保每个InboundHandler都把数据往后传递了,也就是调用了相关的fireChannelRead方法,Netty也会帮我们释放,这个是由Tail负责的 ,同样的,如果我们有InboundHandler处理了数据,又不继续往后传递,又不调用负责释放的ReferenceCountUtil.release方法,就可能会造成内存泄露。
但是由于消费入站数据是一项常规任务,所以Netty 提供了一个特殊的被称为SimpleChannelInboundHandler 的ChannelInboundHandler 实现。这个实现会在数据被channelRead0()方法消费之后自动释放数据。

同时系统为我们提供的各种预定义Handler实现,都实现了数据的正确处理,所以我们自行在编写业务Handler时,也需要注意这一点:
网络编程里,“服务器”和“客户端”实际上表示了不同的网络行为;换句话说,是监听传入的连接还是建立到一个或者多个进程的连接。
因此,有两种类型的引导:一种用于客户端(简单地称为Bootstrap),而另一种(ServerBootstrap)用于服务器。无论你的应用程序使用哪种协议或者处理哪种类型的数据,唯一决定它使用哪种引导类的是它是作为一个客户端还是作为一个服务器。
比较**Bootstrap **类
ServerBootstrap 将绑定到一个端口,因为服务器必须要监听连接,而Bootstrap 则是由想要连接到远程节点的客户端应用程序所使用的。
第二个区别可能更加明显。引导一个客户端只需要一个EventLoopGroup,但是一个ServerBootstrap 则需要两个(也可以是同一个实例)。
因为服务器需要两组不同的Channel。第一组将只包含一个ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字。而第二组将包含所有已创建的用来处理传入客户端连接(对于每个服务器已经接受的连接都有一个)的Channel。


Netty 提供了一个特殊的ChannelInboundHandlerAdapter 子类:
public abstract class ChannelInitializer ext ends ChannelInboundHandlerAdapter
它定义了下面的方法:
protect ed abstract void initChannel(C ch) throws Exception;
这个方法提供了一种将多个ChannelHandler 添加到一个ChannelPipeline 中的简便方法。你只需要简单地向Bootstrap 或ServerBootstrap 的实例提供你的ChannelInitializer 实现即可,
所以,在我们自己的应用程序中,****比如授权handler,某客户端第一次连接登录以后,进行授权检查,检查通过后就可以把这个授权handler移除了。如果客户端关闭连接下线,下次再连接的时候,就是一个新的连接,授权handler依然会被安装到ChannelPipeline ,依然会进行授权检查。
ChannelOption的各种属性在套接字选项中都有对应。
💯设置tcp的相关参数,以下常用并且可能被面试问到
💯
ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理。所以操作系统里一般有两个队列,一个是ACCEPT队列,保存着已经完成了TCP的三次握手的连接,一个SYN队列,服务器正在等待TCP的三次握手完成的队列。
BSD派生系统里backlog指的就是SYN队列的大小,在Linux的实现里backlog相对来说,就含糊不清了,有些内核版本指的是ACCEPT队列+SYN队列合起来的大小,有的是指SYN队列的大小。
但是从 Linux 2.2 开始,backlog 的参数行为在 Linux 2.2 中发生了变化,现在它指定等待接受的完全建立的套接字的队列长度,而不是不完整的连接请求的数量。 不完整套接字队列的最大长度可以使用 /proc/sys/net/ipv4/tcp_max_syn_backlog 设置,默认值为 128。
如果 backlog 参数大于 /proc/sys/net/core/somaxconn 中的值,那么它会被静默截断为值128。在 2.4.25 之前的内核中,此限制是硬编码值,后续内核版本也可以通过vim /etc/sysctl.conf来修改,包括我们前面所说的tcp_max_syn_backlog也可以在此处修改,然后通过命令sysctl -p生效。

ChanneOption.SO_REUSEADDR对应于套接字选项中的SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口,
比如,让多网卡(IP)绑定相同端口;再比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置SO_REUSEADDR就无法正常使用该端口。
但是注意,这个参数无法做到让应用绑定完全相同 IP + Port 来重复启动。
Channeloption.SO_KEEPALIVE参数对应于套接字选项中的SO_KEEPALIVE,该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。
ChannelOption.SO_SNDBUF参数对应于套接字选项中的SO_SNDBUF,ChannelOption.SO_RCVBUF参数对应于套接字选项中的SO_RCVBUF这两个参数用于操作接收缓冲区和发送缓冲区的大小,接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。
yib一般都会自行限定
ChannelOption.SO_LINGER参数对应于套接字选项中的SO_LINGER,Linux内核默认的处理方式是当用户调用close()方法的时候,函数返回,在可能的情况下,尽量发送数据,不一定保证会发生剩余的数据,造成了数据的不确定性,使用SO_LINGER可以阻塞close()的调用时间,直到数据完全发送
ChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关,Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网络的有效负载,但是却造成了延时,而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输,于TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。
参见cn.tuling.nettybasic.splicing.demo下的代码

假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。
(1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
(2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
(3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;
(4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。
如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。
由于TCP协议本身的机制(面向连接的可靠地协议-三次握手机制)客户端与服务器会维持一个连接(Channel),数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包;服务器在接收到数据库后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象
UDP:本身作为无连接的不可靠的传输协议(适合频繁发送较小的数据包),他不会对数据包进行合并发送(也就没有Nagle算法之说了),他直接是一端发送什么数据,直接就发出去了,既然他不会对数据合并,每一个数据包都是完整的(数据+UDP头+IP头等等发一次数据封装一次)也就没有粘包一说了。
分包产生的原因就简单的多:就是一个数据包被分成了多次接收。
更具体的原因至少包括:
1. 应用程序写入数据的字节大小大于套接字发送缓冲区的大小
2. 进行MSS大小的TCP分段。MSS是最大报文段长度的缩写。MSS是TCP报文段中的数据字段的最大长度。数据字段加上TCP首部才等于整个的TCP报文段。所以MSS并不是TCP报文段的最大长度,而是:MSS=TCP报文段长度-TCP首部长度。
由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,可以归纳如下。
参见cn.tuling.nettybasic.splicing.linebase(回车换行符进行分割)和cn.tuling.nettybasic.splicing.delimiter(自定义分割符)下的代码
(2)消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格;
参见cn.tuling.nettybasic.splicing.fixed下的代码
(3)将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度,使用LengthFieldBasedFrameDecoder,后面会有详细说明和使用。
两者的区别:
Netty是在读到完整的业务请求报文后才调用一次业务ChannelHandler的 channelRead方法,无论这条报文底层经过了几次SocketChannel的 read调用。
但是 channelReadComplete方法并不是在业务语义上的读取消息完成后被触发的,而是在每次从SocketChannel 成功读到消息后,由系统触发,也就是说如果一个业务消息被TCP协议栈发送了N次,则服务端的channelReadComplete方法就会被调用N次。
我们用代码来看看cn.tuling.nettybasic.checkread:

我们在客户端发送5次较小报文
服务端输出:

很明显,channelRead是一个报文执行一次,执行的执行次数和客户端发送报文数一样,channelReadComplete虽然也执行了多次,但是和客户端发送报文数没什么关系,而且也没什么规律可寻。
每个网络应用程序都必须定义如何解析在两个节点之间来回传输的原始字节,以及如何将其和目标应用程序的数据格式做相互转换。这种转换逻辑由编解码器处理,编解码器由编码器和解码器组成,它们每种都可以将字节流从一种格式转换为另一种格式。那么它们的区别是什么呢?
如果将消息看作是对于特定的应用程序具有具体含义的结构化的字节序列—它的数据。那么编码器是将消息转换为适合于传输的格式(最有可能的就是字节流);而对应的解码器则是将网络字节流转换回应用程序的消息格式。因此,编码器操作出站数据,而解码器处理入站数据。我们前面所学的解决粘包半包的其实也是编解码器框架的一部分。
将字节解码为消息——ByteToMessageDecoder
将一种消息类型解码为另一种——MessageToMessageDecoder。
因为解码器是负责将入站数据从一种格式转换到另一种格式的,所以Netty 的解码器实现了ChannelInboundHandler。
什么时候会用到解码器呢?很简单:每当需要为ChannelPipeline 中的下一个ChannelInboundHandler 转换入站数据时会用到。此外,得益于ChannelPipeline 的设计,可以将多个解码器链接在一起,以实现任意复杂的转换逻辑。
比如一个实际的业务场景,两端通信,通过JSON交换信息,而且JSON文本需要加密,接收端就可以:
网络加密报文 -> 经过ByteToMessageDecoder -> String类型的JSON明文;
String类型的JSON文本-> 经过MessageToMessageDecoder -> Java里的对象
所以我们可以把ByteToMessageDecoder 看成一次解码器,MessageToMessageDecoder 看成二次或者多次解码器
抽象类ByteToMessageDecoder
将字节解码为消息(或者另一个字节序列)是一项如此常见的任务,Netty 为它提供了一个抽象的基类:ByteToMessageDecoder。由于你不可能知道远程节点是否会一次性地发送一个完整的消息,所以这个类会对入站数据进行缓冲,直到它准备好处理。
它最重要方法
decode(ChannelHandlerContext ctx,ByteBuf in,List out)
是必须实现的唯一抽象方法。decode()方法被调用时将会传入一个包含了传入数据的ByteBuf,以及一个用来添加解码消息的List。对这个方法的调用将会重复进行,直到确定没有新的元素被添加到该List,或者该ByteBuf 中没有更多可读取的字节时为止。然后,如果该List 不为空,那么它的内容将会被传递给ChannelPipeline 中的下一个ChannelInboundHandler。
在两个消息格式之间进行转换(例如,从String->Integer)
decode(ChannelHandlerContext ctx,I msg,List out)
对于每个需要被解码为另一种格式的入站消息来说,该方法都将会被调用。解码消息随后会被传递给ChannelPipeline中的下一个ChannelInboundHandler
MessageToMessageDecoder,T代表源数据的类型
由于Netty 是一个异步框架,所以需要在字节可以解码之前在内存中缓冲它们。因此,不能让解码器缓冲大量的数据以至于耗尽可用的内存。为了解除这个常见的顾虑,Netty 提供了TooLongFrameException 类,其将由解码器在帧超出指定的大小限制时抛出。
为了避免这种情况,你可以设置一个最大字节数的阈值,如果超出该阈值,则会导致抛出一个TooLongFrameException(随后会被ChannelHandler.exceptionCaught()方法捕获)。然后,如何处理该异常则完全取决于该解码器的用户。某些协议(如HTTP)可能允许你返回一个特殊的响应。而在其他的情况下,唯一的选择可能就是关闭对应的连接。

解码器的功能正好相反。Netty 提供了一组类,用于帮助你编写具有以下功能的编码器:
将消息编码为字节;MessageToByteEncoder
将消息编码为消息:MessageToMessageEncoder,T代表源数据的类型
还是用我们上面的业务场景,两端通信,通过JSON交换信息,而且JSON文本需要加密,发送端就可以:
Java里的对象-> 经过MessageToMessageEncoder -> String类型的JSON文本
String类型的JSON明文 -> 经过MessageToByteEncoder-> 网络加密报文;
所以我们可以把MessageToByteEncoder看成网络报文编码器,MessageToMessageEncoder看成业务编码器。
encode(ChannelHandlerContext ctx,I msg,ByteBuf out)
encode()方法是你需要实现的唯一抽象方法。它被调用时将会传入要被该类编码为ByteBuf 的出站消息(类型为I 的)。该ByteBuf 随后将会被转发给ChannelPipeline中的下一个ChannelOutboundHandler
encode(ChannelHandlerContext ctx,I msg,List out)
这是需要实现的唯一方法。每个通过write()方法写入的消息都将会被传递给encode()方法,以编码为一个或者多个出站消息。随后,这些出站消息将会被转发给ChannelPipeline中的下一个ChannelOutboundHandler
我们一直将解码器和编码器作为单独的实体讨论,但是有时在同一个类中管理入站和出站数据和消息的转换是很有用的。Netty 的抽象编解码器类正好用于这个目的,因为它们每个都将捆绑一个解码器/编码器对。这些类同时实现了ChannelInboundHandler 和ChannelOutboundHandler 接口。
为什么我们并没有一直优先于单独的解码器和编码器使用这些复合类呢?因为通过尽可能地将这两种功能分开,最大化了代码的可重用性和可扩展性,这是Netty 设计的一个基本原则。
相关的类:
抽象类ByteToMessageCodec
抽象类MessageToMessageCodec
Netty 为许多通用协议提供了编解码器和处理器,几乎可以开箱即用,这减少了我们花费的时间与精力。
HTTP 是基于请求/响应模式的:客户端向服务器发送一个HTTP 请求,然后服务器将会返回一个HTTP 响应。Netty 提供了多种编码器和解码器以简化对这个协议的使用。
一个HTTP 请求/响应可能由多个数据部分组成,FullHttpRequest 和FullHttpResponse 消息是特殊的子类型,分别代表了完整的请求和响应。所有类型的HTTP 消息(FullHttpRequest、LastHttpContent等等)都实现了HttpObject 接口。
HttpRequestEncoder 将HttpRequest、HttpContent 和LastHttpContent 消息编码为字节
HttpResponseEncoder 将HttpResponse、HttpContent 和LastHttpContent 消息编码为字节
HttpRequestDecoder 将字节解码为HttpRequest、HttpContent 和LastHttpContent 消息
HttpResponseDecoder 将字节解码为HttpResponse、HttpContent 和LastHttpContent 消息
HttpClientCodec和HttpServerCodec则将请求和响应做了一个组合。


所以可以看到,Http客户端使用了HttpClientCodec,而服务器端使用了HttpResponseEncoder和HttpRequestDecoder


由于HTTP 的请求和响应可能由许多部分组成,因此你需要聚合它们以形成完整的消息。为了消除这项繁琐的任务,Netty 提供了一个聚合器HttpObjectAggregator,它可以将多个消息部分合并为FullHttpRequest 或者FullHttpResponse 消息。通过这样的方式,你将总是看到完整的消息内容。
当使用HTTP 时,建议开启压缩功能以尽可能多地减小传输数据的大小。虽然压缩会带来一些CPU 时钟周期上的开销,但是通常来说它都是一个好主意,特别是对于文本数据来说。Netty 为压缩和解压缩提供了ChannelHandler 实现,它们同时支持gzip 和deflate 编码。
SSL和TLS这样的安全协议,它们层叠在其他协议之上,用以实现数据安全。我们在访问安全网站时遇到过这些协议,但是它们也可用于其他不是基于HTTP的应用程序,如安全SMTP(SMTPS)邮件服务器甚至是关系型数据库系统。
为了支持SSL/TLS,Java 提供了javax.net.ssl 包,它的SSLContext 和SSLEngine类使得实现解密和加密相当简单直接。Netty 通过一个名为SslHandler 的ChannelHandler实现利用了这个API,其中SslHandler 在内部使用SSLEngine 来完成实际的工作。
在大多数情况下,SslHandler 将是ChannelPipeline 中的第一个ChannelHandler。
启用HTTPS 只需要将SslHandler 添加到ChannelPipeline 的ChannelHandler 组合中。
SSL和HTTP的代码参见模块netty-http
视频中实现步骤:
1、首先实现Http服务器并Http客户端访问;
2、增加SSL控制;
3、浏览器访问。
其实Netty里还提供了一个OptionalSslHandler,可以帮我们实现根据客户端的访问来决定是否启用SSL,这个OptionalSslHandler的关键在于可以让我们学会如何根据业务需求在运行时替换和删除Handler,大家在学习Dubbo源码的时候,可以留意下,里面就有Handler在运行时的移除。
