写在前面:
本文2万多字,阅读需要花费一点时间, 主要介绍网络编程部分知识,包括Netty 和 IO 网络等知识点;
仅供复习使用!
1. 网络四元组
四元组,简单理解就是在 TCP 协议中,去确定一个客户端连接的组成要素,它包括源IP 地址、目标 IP 地址、源端口号、目标端口号。
正常情况下,我们对于网络通信的认识可能是这样(如图)。
服务端通过 ServerSocket 建立一个对指定端口号的监听,比如 8080。 客户端通过目标 ip 和端口就可以和服务端建立一个连接,然后进行数据传输。
但是我们知道的是,一个 Server 端可以接收多个客户端的连接,比如像这种情况(如图)。
那,当多个客户端连接到服务端的时候,服务端需要去识别每一个连接
并且(如图),TCP 是全双工协议,也就是说数据允许在连接的两个方向上同时传输,因此这里的客户端,如果是反向通信,它又变成了服务端
所以基于这两个原因,就引入了四元组的设计,也就是说,当一个客户端和服务端建立一个 TCP 连接的时候,通过源 IP 地址、目标 IP 地址、源端口号、目标端口号来确定一个唯一的 TCP 连接。因为服务器的 IP 和端口是不变的,只要客户端的 IP 和端口彼此不同就 OK 了。
比如像这种情况(如图),同一个客户端主机上有三个连接连到 Server 端,那么这个时候源 IP 相同,源端口号不同。此时建立的四元组就是(10.23.15.3,59461 ,192.168.8.135,8080)
其中,源端口号是每次建立连接的时候系统自动分配的。
:::info
“终局思维”来看待自己的职业规划
:::
2. 对 Netty 的认识
好的,我用三点来简单的介绍下 Netty
- 第一:Netty 是一个 基于 NIO 模型的高性能网络通信框架,其实可以认为它是对 NIO 网络模。型的封装,提供了简单易用的 API,我们可以利用这些封装好的API 快速开发自己的网络程序。
- 第二:Netty 在 NIO 的基础上做了很多优化,比如零拷贝机制、高性能无锁队列、内存池等,因此性能会比 NIO 更高。
- 第三:Netty 可以支持多种通信协议,如 Http、WebSocket 等,并且针对数据通信的拆包黏包问题,Netty 内置了拆包策略。
为什么要用 Netty
Nety 相比于直接使用 JDK 自带的 NIO 相关的 API 来说更加易用
- 统一的 API,支持多种传输类型,如阻塞、非阻塞,以及 epoll、poll 等模型。
- 我们可以使用非常少的代码来实现,多线程 Reactor 模型以及主从多线程 Reactor模型
- 自带编解码器解决 TCP 粘包/拆包问题。
- 自带各种协议栈。
- 比直接使用 Java 库中的 NIO API 有更高的吞吐量、更低的延迟、更低的资源消耗和更少的内存复制。
- 安全性不错,有完整的 SSL/TLS 以及 StartTLS 支持。
- 社区活跃成熟稳定,经历了大型项目的使用和考验,而且很多开源项目都使用到了Netty, 比如我们经常接触的 Dubbo、RocketMQ ZK 等等。
Netty 可以做什么事情
核心点还是在于解决服务器如何承载更多的用户同时访问
传统的 BIO 模型,由于阻塞的特性,使得在高并发场景中,很难获得更高的吞吐量。
而后来基于 NIO 的多路复用模型虽然在阻塞方面进行了优化,但是它的 API 使用比较复杂,对于初学者来说使用不是很友好。而 Netty 是基于 NIO 的封装,提供了成熟且简单易用的 API,降低了使用成本和学习成本。
本质上来说,Netty 和 NIO 所扮演的角色是相同的,都是为提升服务端的吞吐量,让用户获得更好的产品体验。
另外,Netty 这个中间件经过很多年验证,在目前主流中间件如 Zookeeper、Dubbo、RocketMQ 中都有应用。
3. Netty 核心组件和作用
高手:Netty 由三层结构构成:网络通信层、事件调度器与服务编排层
在网络通信层有三个核心组件:Bootstrap、ServerBootStrap、Channel
Bootstrap 负责客户端启动并用来链接远程 netty server
ServerBootStrap 负责服务端监听,用来监听指定端口,
Channel 是负责网络通信的载体
事件调度器有两个核心组件:EventLoopGroup 与 EventLoop
EventLoopGroup 本质上是一个线程池,主要负责接收 I/O 请求,并分配线程执行处理请求。
EventLoop。相当于线程池中的线程
在服务编排层有三个核心组件 ChannelPipeline、ChannelHandler、ChannelHandlerContext
ChannelPipeline 负责将多个 Channelhandler 链接在一起
ChannelHandler 针对 IO 数据的处理器,数据接收后,通过指定的 Handler 进行处理。
ChannelHandlerContext 用来保存 ChannelHandler 的上下文信息
Netty 有几种线程模型吧?
Netty 提供了三种 Reactor 模型的支持
单线程单 Reactor 模型
多线程单 Reactor 模型
多线程多 Reactor 模型
对于这三种线程 Reactor 模型的理解
Reactor 模型有三个重要的组件:
- Reactor :将 I/O 事件发派给对应的 Handler
- Acceptor :处理客户端连接请求
- 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 操作,对于小容
量的场景,影响不大。但是对于高负载、大并发或大数据量的应用场景时,容易成为瓶
颈,主要原因如下:
一个 NIO 线程同时处理成百上千的链路,性能上无法支撑,即便 NIO 线程的 CPU
负荷达到 100%,也无法满足海量消息的读取和发送;
当 NIO 线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超
时之后往往会进行重发,这更加重了 NIO 线程的负载,最终会导致大量消息积压
和处理超时,成为系统的性能瓶颈;
所以,我们还可以更进一步优化,引入多 Reactor 多线程模式
Main Reactor 负责接收客户端的连接请求,然后把接收到的请求传递给
SubReactor(其中 subReactor 可以有多个),具体的业务 IO 处理由 SubReactor
完成。