什么是Netty?
Netty 是一个广泛使用的 Java 网络编程框架,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。
Netty 是一款提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序
也就是说,Netty 是一个基于 NIO 的客户端、服务器端编程框架,使用 Netty 可以确保你快速和简单地开发出一个网络应用,它极大的简化了TCP和UDP套接字服务器等网络编程,而且支持各种协议,如FTP、SMTP、HTTP以及各种二进制和基于文本的传统协议。
为什么使用netty?
统一的API,支持各种传输类型。
简单而强大的线程模型,支持高并发。
自带编解码器解决TCP粘包/拆包问题。
定制能力强大,可以通过ChannleHandler对通讯架构进行灵活的扩展。
比直接使用java的核心API有更高的吞吐量、更低的延迟、更低的资源消耗和更少的内存负复制。
社区活跃,出现问题比较好找到解决办法。也修复了很多NIO的bug。
成熟稳定,很多开源项目都使用到了Netty。如果阿里的Dubbo、RocketMQ。
Netty的应用场景:
Netty主要用来做网络通信:
1.作为 RPC 框架的网络通信工具。
2.实现一个自己的 HTTP 服务器。
3.实现一个即时通讯系统。
4.实现消息推送系统。
Netty和Tomcat有什么区别?
Netty和Tomcat最大的区别就在于通信协议,Tomcat是基于Http协议的,他的实质是一个基于http协议的web容器,但是Netty不一样,他能通过编程自定义各种协议,因为netty能够通过codec自己来编码/解码字节流,完成类似redis访问的功能,这就是netty和tomcat最大的不同。
为什么netty受欢迎?
1.并发高
2.传输快
3.封装好
Netty核心组件有哪些?分别有什么作用。
1.Channel
Channel 接口是 Netty 对网络操作抽象类,它除了包括基本的 I/O 操作,如 bind()、connect()、read()、write() 等。
比较常用的Channel接口实现类是NioServerSocketChannel(服务端)和NioSocketChannel(客户端),这两个 Channel 可以和 BIO 编程模型中的ServerSocket以及Socket两个概念对应上。Netty 的 Channel 接口所提供的 API,大大地降低了直接使用 Socket 类的复杂性。
2.EventLoop
Netty 基于事件驱动模型,EventLoop(事件循环)接口可以说是 Netty 中最核心的概念了!EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。
下图是Channel、EventLoop、Thread、EventLoopGroup之间的关系(摘自《Netty In Action》):
一个 EventLoopGroup 包含一个或多个 EventLoop。
一个 EventLoop 在它的生命周期内只能与一个Thread绑定。
所有有 EnventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理。
一个 Channel 在它的生命周期内只能注册与一个 EventLoop。
一个 EventLoop 可被分配至一个或多个 Channel 。
当一个连接到达时,Netty 就会注册一个 Channel,然后从 EventLoopGroup 中分配一个 EventLoop 绑定到这个Channel上,在该Channel的整个生命周期中都是由这个绑定的 EventLoop 来服务的。
上面已经对EventloopGroup和EventLoop和关系进行了简单说明,下面对EventloopGroup和EventLoop的关系进行更加详细的说明:
EventLoopGroup 包含多个 EventLoop(每一个 EventLoop 通常内部包含一个线程),上面我们已经说了 EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。
并且 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理,即 Thread 和 EventLoop 属于 1 : 1 的关系,从而保证线程安全。
上图是一个服务端对 EventLoopGroup 使用的大致模块图,其中 BossEventloopGroup 用于接收连接,WorkerEventloopGroup 用于具体的处理(消息的读写以及其他逻辑处理)。
从上图可以看出:当客户端通过 connect 方法连接服务端时,bossGroup 处理客户端连接请求。当客户端处理完成后,会将这个连接提交给 workerGroup 来处理,然后 workerGroup 负责处理其 IO 相关操作。
3.ChannelFuture
Netty 是异步非阻塞的,所有的 I/O 操作都为异步的。因此,我们不能立刻得到操作是否执行成功,但是,你可以通过 ChannelFuture 接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。
并且,你还可以通过ChannelFuture 的 channel() 方法获取关联的Channel
另外,我们还可以通过 ChannelFuture 接口的 sync()方法让异步的操作变成同步的。
4.Selector
Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件。
当向一个 Selector 中注册 Channel 后,Selector 内部的机制就可以自动不断地查询(Select) 这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel。
5.ChannelHandler
ChannelHandler 为 Netty 中最核心的组件,它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。
ChannelHandler 两个核心子类 ChannelInboundHandler 和 ChannelOutboundHandler,其中 ChannelInboundHandler 用于接收、处理入站数据和事件,而 ChannelOutboundHandler 则相反。
6.ChannelPipeline
ChannelPipeline 为 ChannelHandler 链提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API 。当 Channel 被创建时,它会被自动地分配到它专属的 ChannelPipeline。我们可以在 ChannelPipeline 上通过 addLast() 方法添加一个或者多个ChannelHandler ,因为一个数据或者事件可能会被多个 Handler 处理。当一个 ChannelHandler 处理完之后就将数据交给下一个 ChannelHandler 。
当一个数据流进入 ChannlePipeline 时,它会从 ChannelPipeline 头部开始传给第一个 ChannelInboundHandler ,当第一个处理完后再传给下一个,一直传递到管道的尾部。与之相对应的是,当数据被写出时,它会从管道的尾部开始,先经过管道尾部的 “最后” 一个ChannelOutboundHandler,当它处理完成后会传递给前一个 ChannelOutboundHandler 。
那么Channel、 ChannlePipeline 、ChannelHandler 和ChannelHandlerContext 到底是一个什么样的关系???
首先,一个Channel仅仅只有一个ChannlePipeline 。这个ChannlePipeline 在Channel被创建时创建。ChannlePipeline 相当于是ChannelHandler 的容器,它里面是一个ChannelHandler 的链表,且所有的ChannelHandler 都会注册到这个ChannlePipeline 中。
那么,ChannelHandlerContext 有是个什么东东呢?
首先,一个ChannelHandler 是无状态的(只负责处理直接的业务逻辑,可以给不同的Channel使用),虽然ChannelHandler 都会注册到这个ChannlePipeline 中,但是并不是它直接与ChannlePipeline 进行关联的。因此,ChannelHandler 与ChannlePipeline 之间就需要一个中间角色,就是ChannelHandlerContext 。
其实,ChannlePipeline 中维护的是ChannelHandlerContext 组成的双向链表。而ChannelHandler 只是作为ChannelHandlerContext 中的一个成员。在对应关系上,每个ChannelHandlerContext 仅仅关联着一个ChannelHandler 。
Bootstrap和ServerBootstrap
Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件。Bootstrap 是客户端的启动引导类/辅助类.具体使用方法如下:
ServerBootstrap 客户端的启动引导类/辅助类,具体使用方法如下:
从上面的示例中,我们可以看出:
Bootstrap 通常使用 connet() 方法连接到远程的主机和端口,作为一个 Netty TCP 协议通信中的客户端。另外,Bootstrap 也可以通过 bind() 方法绑定本地的一个端口,作为 UDP 协议通信中的一端。
ServerBootstrap通常使用 bind() 方法绑定本地的端口上,然后等待客户端的连接。
Bootstrap 只需要配置一个线程组— EventLoopGroup ,而 ServerBootstrap需要配置两个线程组— EventLoopGroup ,一个用于接收连接,一个用于具体的处理。
NioEventLoopGroup默认的构造函数会起多少线程?
NioEventLoopGroup 默认的构造函数实际会起的线程数为 CPU核心数*2。
Netty线程模型:
Netty线程模型就是Reactor模式的一个实现。关于Reactor模式的说明可以看另一篇博客。
https://blog.csdn.net/qq_19801061/article/details/118364144?spm=1001.2014.3001.5501
Netty中主要通过NioEventLoopGroup线程池来实现具体的线程模型的。
1)Netty抽象出两组线程池BossGroup专门负责接受客户端的连接,WorkerGroup专门负责网络的读写。
2)BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
3)NioEventLoopGroup 相当于一个事件循环组, 这个组中含有多个事件循环 ,每一个事件循环是NioEventLoop(即图中的NioEventGroup,图中表述不够准确)。
4)NioEventLoop表示一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上的socket的网络通讯
5)NioEventLoopGroup 可以有多个线程, 即可以含有多个NioEventLoop
6)每个BossGroup下 NioEventLoop 循环执行的步骤有3步 :
1. 轮询accept 事件 。
2. 处理accept事件,与client建立连接,生成NioScocketChannel,并将其注册到某个workerGroup的NioEventLoop(即图中的NioEventGroup,图中表述不够准确)上的selector。
3. 处理任务队列的任务,即runAllTasks。
7) 每个 WorkerGroup中 NioEventLoop 循环执行的步骤 :
1. 轮询read,write事件
2. 处理i/o事件, 即read,write事件,在对应NioScocketChannel 处理。
3. 处理任务队列的任务,即runAllTasks
8) 每个WorkerGroup NioEventLoop处理业务时,会使用pipeline(管道), pipeline 中包含了 channel , 即通过pipeline 可以获取到对应通道, 管道中维护了很多的处理器 。
Netty模型概述:
1.Netty 抽象出两组线程池,BossGroup (NioEventLoopGroup)专门负责接收客户端连接,WorkerGroup (NioEventLoopGroup)专门负责网络读写操作。
2.NioEventLoop 表示一个不断循环执行处理任务的线程,每个 NioEventLoop 都有一个 selector,用于监听绑定在其上的 socket 网络通道。
3.NioEventLoop 内部采用串行化设计,从消息的读取->解码->处理->编码->发送,始终由 IO 线程 NioEventLoop 负责
NioEventLoopGroup 下包含多个 NioEventLoop
每个 NioEventLoop 中包含有一个 Selector,一个 taskQueue
每个 NioEventLoop 的 Selector 上可以注册监听多个 NioChannel
每个 NioChannel 只会绑定在唯一的 NioEventLoop 上
每个 NioChannel 都绑定有一个自己的 ChannelPipeline
TCP粘包、拆包:
A和B两个包都刚好满足TCP缓冲区的大小,或者说其等待时间已经达到TCP等待时长,从而还是使用两个独立的包进行发送。
A和B两次请求间隔时间内较短,并且数据包较小,因而合并为同一个包发送给服务端。
B包比较大,因而将其拆分为两个包B_1和B_2进行发送,而这里由于拆分后的B_2比较小,其又与A包合并在一起发送。
拆包:一个完整的包可能会被 TCP 拆分成多个包进行发送。
粘包:也可能把小的封装成一个大的数据包发送。
产生粘包和拆包问题的主要原因:操作系统在发送TCP数据的时候,底层会有一个缓冲区,例如1024个字节大小,如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题;如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包,也就是将一个大的包拆分为多个小包进行发送。
解决办法:
使用netty自带的解码器。
FixedLengthFrameDecoder
,基于固定长度消息进行粘包拆包处理的。
2. LineBasedFrameDecoder
,基于换行符(\n或者\r\n)来进行消息粘包拆包处理的。(发送端发送数据包的时候,每个数据包之间以换行符作为分隔。工作原理是它依次遍历 ByteBuf 中的可读字节,判断是否有换行符,然后进行相应的截取。)
3. DelimiterBasedFrameDecoder,基于指定消息边界方式(自定义分隔符)进行粘包拆包处理的。
4. LengthFieldBasedFrameDecoder,基于消息头指定消息长度进行粘包拆包处理的。
maxFrameLength:指定了每个包所能传递的最大数据包大小;
lengthFieldOffset:指定了长度字段在字节码中的偏移量;
lengthFieldLength:指定了长度字段所占用的字节长度;
lengthAdjustment: 这个参数很多时候设为负数,这是最让小伙伴们迷惑的。当Netty利用lengthFieldOffset(偏移位)和lengthFieldLength(Length字段长度)成功读出Length字段的值后,Netty认为这个值是指从Length字段之后,到包结束一共还有多少字节,如果这个值是13,那么Netty就会再等待13个Byte的数据到达后,拼接成一个完整的包。但是更多时候,Length字段的长度,是指整个包的长度,如果是这种情况,当Netty读出Length字段的时候,它已经读取了包的4个Byte的数据,所以,后续未到达的数据只有9个Byte,即13 - 4 = 9,这个时候,就要用lengthAdjustment来告诉Netty,后续的数据并没有13个Byte,要减掉4个Byte,所以lengthAdjustment要设为 -4!!!
initialBytesToStrip:接收到的发送数据包,去除前initialBytesToStrip位。对于长度字段在消息头中间的情况,可以通过initialBytesToStrip忽略掉消息头以及长度字段占用的字节;
failFast:读取到长度域超过maxFrameLength,就抛出一个 TooLongFrameException。false: 只有真正读取完长度域的值表示的字节之后,才会抛出 TooLongFrameException,默认情况下设置为true,建议不要修改,否则可能会造成内存溢出;
ByteOrder:数据存储采用大端模式或小端模式。Netty中默认为大端模式(高位在前,低位在后)。
Netty长连接、心跳机制:
Netty实现心跳机制主要是通过IdleStateHandle类来产生对应的idle事件。它可以对一个 Channel 的 读/写设置定时器, 当 Channel 在一定事件间隔内没有数据交互时(即处于 idle 状态), 就会触发指定的事件。
readerIdleTimeSeconds,读超时。即当在指定的时间间隔内没有从 Channel 读取到数据时, 会触发一个 READER_IDLE 的 IdleStateEvent 事件.
writerIdleTimeSeconds,写超时。即当在指定的时间间隔内没有数据写入到 Channel时, 会触发一个WRITER_IDLE的IdleStateEvent事件.
allIdleTimeSeconds,读/写超时。即当在指定的时间间隔内没有读或写操作时, 会触发一个 ALL_IDLE 的 IdleStateEvent 事件.
IdleStateHandler是实现心跳的关键, 它会根据不同的 IO idle类型来产生不同的IdleStateEvent 事件,而这个事件的捕获,其实就是在userEventTriggered方法中实现的。
客户端断线重连:
断线重连的关键一点是检测连接是否已经断开. 因此需要重写了channelInactive方法。当TCP连接断开时,会回调channelInactive方法,因此我们在这个方法中来进行重连。
断线重连也不一定需要一直重连,可以设置重连的次数以及重连间隔时间。
Netty零拷贝(https://www.cnblogs.com/xys1228/p/6088805.html):
在 OS 层面上的 Zero-copy 通常指避免在 用户态(User-space) 与 内核态(Kernel-space) 之间来回拷贝数据。而在 Netty 层面 ,零拷贝主要体现在对于数据操作的优化。
Netty 中的零拷贝体现在以下几个方面:
1. Netty的文件传输调用FileRegion包装的transferTo方法,可以直接将文件缓冲区的数据发送到目标Channel,避免通过循环write方式导致的内存拷贝问题。
2. Netty提供CompositeByteBuf类, 可以将多个ByteBuf合并为一个逻辑上的ByteBuf, 避免了各个ByteBuf之间的拷贝。
比如有两个ByteBuf,我们想合并成到一个ByteBuf中,通常的做法:
可以看到, 我们将 header 和 body 都拷贝到了新的 allBuf 中了, 这无形中增加了两次额外的数据拷贝操作了。
使用CompositeByteBuf类:
虽然看起来 CompositeByteBuf 是由两个 ByteBuf 组合而成的, 不过在 CompositeByteBuf 内部, 这两个ByteBuf 都是单独存在的, CompositeByteBuf 只是逻辑上是一个整体。
3. 通过wrap操作, 我们可以将byte[]数组、ByteBuf、ByteBuffer等包装成一个Netty ByteBuf对象, 进而避免拷贝操作。
4. ByteBuf支持slice操作,可以将ByteBuf分解为多个共享同一个存储区域的ByteBuf, 避免内存的拷贝。
Netty服务端和客户端的启动过程
服务端:
1.首先创建了两个 NioEventLoopGroup 对象实例:bossGroup(处理客户端的连接请求) 和 workerGroup(负责每一条连接的具体读写数据的处理逻辑,真正负责 I/O 读写操作,交由对应的 Handler 处理)。
2.创建了一个服务端启动引导/辅助类:ServerBootstrap,这个类将引导我们进行服务端的启动工作。
3. 通过ServerBootstrap的group() 方法给引导类 ServerBootstrap 配置两大线程组,确定了线程模型。
4. 通过ServerBootstrap的channel()方法给引导类 ServerBootstrap指定了 IO 模型为NIO
5. 通过ServerBootstrap的childHandler()给引导类创建一个ChannelInitializer 。
6. 调用ServerBootstrap的bind()进行端口服务端端口绑定,并允许。
客户端:
1.创建一个 NioEventLoopGroup 对象实例
2.创建客户端启动的引导类是 Bootstrap
3.通过 .group() 方法给引导类 Bootstrap 配置一个线程组
4.通过channel()方法给引导类 Bootstrap指定了 IO 模型为NIO
5.通过 .childHandler()给引导类创建一个ChannelInitializer ,然后制定了客户端消息的业务处理逻辑对象
6.调用 Bootstrap 类的 connect()方法进行连接,这个方法需要指定两个参数:inetHost,inetPort。connect 方法返回的是一个 Future 类型的对象,也就是说这个方是异步的,我们通过 addListener 方法可以监听到连接是否成功。