第一:Netty 是一个 基于 NIO 模型的高性能网络通信框架,其实可以认为它是 对 NIO 网络模型的封装,提供了简单易用的 API,我们可以利用这些封装好的 API 快速开发自己的网络程序。
第二:Netty 在 NIO 的基础上做了很多优化,比如零拷贝机制、高性能无锁队列、 内存池等,因此性能会比 NIO 更高。
第三:Netty 可以支持多种通信协议,如 Http、WebSocket 等,并且针对数据通信的拆包黏包问题,Netty 内置了拆包策略。
Nety 相比于直接使用 JDK 自带的 NIO 相关的 API 来说更加易用。同时,它还具有以下特点:
1. 统一的 API,支持多种传输类型,如阻塞、非阻塞,以及 epoll、poll 等模型。
2. 我们可以使用非常少的代码来实现,多线程 Reactor 模型以及主从多线程 Reactor 模型
3. 自带编解码器解决 TCP 粘包/拆包问题。
4. 自带各种协议栈。
5. 比直接使用 Java 库中的 NIO API 有更高的吞吐量、更低的延迟、更低的资源消耗和更少 的内存复制。
6. 安全性不错,有完整的 SSL/TLS 以及 StartTLS 支持。
7. 社区活跃成熟稳定,经历了大型项目的使用和考验,而且很多开源项目都使用到了 Netty, 比如我们经常接触的Dubbo、RocketMQ 等等
我们之所以要用 Netty,核心点还是在于解决服务器如何承载更多的用户同时访问的问 题。 传统的 BIO 模型,由于阻塞的特性,使得在高并发场景中,很难获得更高的吞吐量。 而后来基于 NIO 的多路复用模型虽然在阻塞方面进行了优化,但是它的 API 使用比较 复杂,对于初学者来说使用不是很友好。
而 Netty 是基于 NIO 的封装,提供了成熟且 简单易用的 API,降低了使用成本和学习成本。 本质上来说,Netty 和 NIO 所扮演的角色是相同的,都是为了提升服务端的吞吐量, 让用户获得更好的产品体验。 另外,Netty 这个中间件经过很多年的验证,在目前主流的中间件如 Zookeeper、 Dubbo、RocketMQ 中都有应用。
Netty 由三层结构构成:网络通信层、事件调度器与服务编排层
在网络通信层有三个核心组件:Bootstrap、ServerBootStrap、Channel
1、Bootstrap 负责客户端启动并用来链接远程 netty server
2、ServerBootStrap 负责服务端监听,用来监听指定端口,
3、Channel 是负责网络通信的载体
事件调度器有两个核心组件:EventLoopGroup 与 EventLoop
1、 EventLoopGroup 本质上是一个线程池,主要负责接收 I/O 请求,并分配线程执行 处理请求。
2、 EventLoop。相当于线程池中的线程
在服务编排层有三个核心组件:ChannelPipeline、ChannelHandler、ChannelHandlerContext 1、ChannelPipeline 负责将多个 Channelhandler链接在一起
2、ChannelHandler 针对 IO 数据的处理器,数据接收后,通过指定的 Handler 进行处理。
3、ChannelHandlerContext 用来保存 ChannelHandler
Netty 提供了三种 Reactor 模型的支持:
1、单线程单 Reactor 模型
2、多线程单 Reactor 模型
3、多线程多 Reactor 模型
Reactor 模型有三个重要的组件:
1. Reactor :将 I/O 事件发派给对应的 Handler
2. Acceptor :处理客户端连接请求
3. Handlers :执行非阻塞读/写
这是最基本的单 Reactor 单线程模
其中 Reactor 线程,负责多路分离套接字,有新连接到来触发 connect 事件之后,交由 Acceptor 进行处理,有 IO读写事件之后交给hanlder处理。
Acceptor 主要任务就是构建 handler ,在获取到和 client 相关的 SocketChannel 之 后 ,绑定到相应的 hanlder 上,对应的 SocketChannel 有读写事件之后,基于 racotor 分发,hanlder 就可以处理了(所有的 IO 事件都绑定到 selector 上,有 Reactor 分发
多线程单 Reactor模型
单线程 Reactor 这种实现方式有存在着缺点,从实例代码中可以看出,handler 的执行是串行的,如果其中一个 handler 处理线程阻塞将导致其他的业务处理阻塞。由于 handler 和 reactor 在同一个线程中的执行,这也将导致无法接收新的请求。 为了解决这种问题,有人提出使用多线程的方式来处理业务,也就是在业务处理的地方加入线程池异步处理,将 reactor 和 handler 在不同的线程来执行,这就是多线程单 Reactor模型
多线程多 Reactor
在多线程单 Reactor 模型中,所有的 I/O 操作是由一个 Reactor 来完成,而 Reactor 运行在单个线程中,它需要处理包括 Accept()/read()/write/connect 操作,对于小 量的场景,影响不大。但是对于高负载、大并发或大数据量的应用场景时,容易成为瓶 颈,主要原因如下:
1、一个 NIO 线程同时处理成百上千的链路,性能上无法支撑,即便 NIO 线程的 CPU 负荷达到 100%,也无法满足海量消息的读取和发送;
2、当 NIO 线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了 NIO 线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;
所以,我们还可以更进一步优化,引入多 Reactor 多线程模式,
1、Main Reactor 负责接收客户端的连接请求,然后把接收到的请求传递给 SubReactor(其中 subReactor 可以有多个),具体的业务 IO 处理由 SubReactor 完成。
2、Acceptor,请求接收者,在实践时其职责类似服务器,并不真正负责连接请求的 建立,而只将其请求委托 Main Reactor 线程池来实现,起到一个转发的作用。
3、Main Reactor,主 Reactor 线程组,主要负责连接事件,并将 IO 读写请求转发到 SubReactor 线程池。
4、Sub Reactor,Main Reactor 通常监听客户端连接后会将通道的读写转发到 Sub Reactor 线程池中一个线程(负载均衡),负责数据的读写。在 NIO 中 通常注册通 道的读(OP_READ)、写事件(OP_WRITE)。
TCP 是一个面向「流」的协议,所谓流就是没有界限的一长串二进制数据。在实际的传输过程中,TCP 会根据网络情况将数据包进行拆分或者拼装,如果业务没有定义一个明确的界限规则,在应用层的业务上就会出现粘包拆包的现象。
平时大家在网络编程过程中可能会遇到这样一种现象:客户端发送了一长串消息,服务端接受的消息揉在一起或者被拆分了,这样就会造成消息难以被正确理解。比如说有一天你特别想喝奶茶,看了一下外卖,某某奶茶看着不错,于是你在群里发了一条消息,想找几个人拼奶茶:
奶茶有人喝吗?结果群里同事回了一句:现在不是已经三点了吗?你觉得莫名其妙,看了一眼同事的手机,他收到的消息是这样的两行:
一点
点奶茶有人喝吗?
用专业的术语来说这种现象就是「拆包」了。
TCP 粘包拆包的现象
粘包拆包问题一般是处于应用层下的问题,在数据链路层、网络层以及传输层都有可能发生。我们日常的网络应用开发大多都在传输层进行,因此本文着重讲解传输层粘包拆包问题。
传输层 传输层有两个协议我们都很熟悉:UDP 和 TCP,UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。
下面用一个简单的例子来讲解什么是粘包和拆包。
假设客户端向服务端连续发送了两个数据包,用 packet1 和 packet2 来表示,那么服务端收到的数据可能有四种:
(1) 第一种情况,服务端按顺序正常收到两个包,即未出现粘包和拆包的现象。
(2) 第二种情况,服务端只收到一个数据包,由于 TCP 保证送达的特性,所以这一个数据包包含了客户端发送的两个数据包的信息,这种现象就是粘包。除非客户端发送的数据包有明确的规则,否则服务端不知道两个包的界限,难以处理数据。
(3) 第三种情况,服务端收到了三个数据包,Package1数据包被拆分为两个数据包:Package1.1和Package1.2,这种现象就是拆包,至于拆包的原因下面会讲,服务端收到拆开的数据包也很难处理。
(4) 第四种情况,一些大的数据包被拆分为小的数据包,小的数据包与其他数据包粘在一起,这种现象是将上面的粘包和拆包综合在一块。
TCP 粘包拆包的原因
TCP 是一个面向「流」的协议,所谓流就是没有界限的一长串二进制数据。TCP 作为传输层协议并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就会出现粘包拆包的问题。
例如,TCP缓冲区是1024个字节大小,如果应用一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,站在业务上来看这就是「粘包」;
如果应用一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是「拆包」,也就是将一个大的包拆分为多个小包进行发送。
TCP 粘包拆包的解决方法
TCP 是面向流的,会发生粘包和拆包,那作为应用程序,如何从这源源不断涌来的数据流中拆分出或者合并出有意义的信息呢? 通常会有以下一些常用的方法:
(1) 发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
如下图,在每个包前面加上包的实际长度。
(2) 发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
下图每个包的固定长度为 4,接收端很容易进行区分。
(3) 可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
如下图,在每个包的后面加上特殊字符:/
Netty框架解决粘包拆包问题
Netty 作为一款高性能的 Java 网络编程框架,不仅是基于 Java NIO 进行了深度封装,还在客户端与服务端之间的数据传输上做了有效处理。
前面说过 TCP 传输会出现粘包和拆包的现象,Netty 针对这一点内置了多款数据流编解码器,客户端服务端按照约定好的规则进行数据传输即可解决这个问题。
Netty 提供了多款开箱即用的编解码器:
TCP 是一个面向「流」的协议,所谓流就是没有界限的一长串二进制数据。在实际的传输过程中,TCP 会根据网络情况将数据包进行拆分或者拼装,如果业务没有定义一个明确的界限规则,在应用层的业务上就会出现粘包拆包的现象。
针对 TCP 粘包拆包的现象,常见的解决思路如下:
(1) 发送端给每个数据包添加包首部。
(2) 发送端将每个数据包封装为固定长度。
(3) 可以在数据包之间设置边界。
为了解决粘包拆包,Netty 框架也提供了很多开箱即用的编解码器,极大简化网络编程解决此类问题的难度。