[TOC]
首先思考:如何实现一个网络应用?
了解一个东西之前,首先要考虑为什么需要这样一个东西。那么为什么我们需要 Netty 呢?
现在互联网上的绝大多数应用都是网络应用程序,大多数都是标准的 CS 架构,需要进行频繁的网络通信,在真正介绍 Netty 之前,先让我们想一个极其基础的问题:如何实现一个网络应用?
网络基础
先回忆一下计算机网络的基础。
网络会分为很多层,一般有两种模型,一种分七层,一种分四层。但是,不管分几层,理解内在最重要。
为什么要分层?需要实现一个完整的互联网,整个的内容一定是很庞大的,通过分层可以进行隔离,彼此不影响。每一层都有自己独一无二的功能,并且上层依赖于下层。
这里把互联网分层五层:
![](https://img.haomeiwen.com/i11345047/9a38c375345fae3f.png)
越下面的层越接近硬件,越上面的层越接近人,上层依赖于下层,五层的功能简单来讲就是实体层是一堆硬件,链路层确定 MAC 地址,网络层确定 IP,传输层确定端口号,应用层是直接面向用户的应用。
理解了计算机网络的多层结构之后,现在我们要写的网络应用在哪一层呢,对了,就在应用层。其他几层的内容完全不需要我们去操心,一般来说操作系统已经替我们做好了,现在我们从 Java 的角度看看如何在应用层上去写一个网络应用。
JDK 如何实现网络应用?
网络分层上层一定是基于下层的,Java 的网络编程是基于其下层:传输层和网络层。
为什么这么讲呢?网络层的功能,是建立「主机」到「主机」的通信。而一台主机上必然有很多应用,传输层的功能就是实现「端口」到「端口」的通信。只要确定了主机和端口号,我们就能够实现程序之间的交流。Linux 系统把主机+端口,叫做「套接字」(socket),socket 就是通信的基石,提供了进程通信的端点,进程之间通信之前,必须各自创建一个端点,否则是没有办法建立联系并相互通信的。一个完整的 socket 有一个本地唯一的 socket 号,这是由操作系统分配的。
在 Linux 世界中,「一切皆文件」,socket 通信和读写文件都被当作是 IO 操作,IO 操作有多种处理方式,如同步阻塞式 IO、同步非阻塞式 IO、异步非阻塞式 AIO 等等。基于此,Java 提供了三种类型的 IO 包:
- 传统的 java.io 包,基于流模型实现的 BIO
- 升级的 java.nio 包,同步非阻塞的 NIO
- 改造的 NIO2,引入了异步非阻塞的 AIO
Java 最基本的网络编程模型是 BIO,即阻塞式 I/O,BIO 中所有的读写操作都会阻塞当前线程。
![](https://img.haomeiwen.com/i11345047/7a79ae8192eb1d20.png)
如果客户端和服务端建立了一个连接,但是客户端一直没有请求过来,那么服务端的 read() 就会一直处于阻塞状态。如果服务端处理这个请求时在等待其他资源时阻塞了,那么此时客户端就会一直处于阻塞状态,无法继续发送请求。所以使用 BIO 模型时一般都会为每个 socket 分配一个独立的线程。
如果并发比较大,我们就需要创建多个线程来处理,为了避免频繁创建、消耗线程,我们可以采用线程池创建线程,但是 socket 和线程的关系的对应关系是不变的。
![](https://img.haomeiwen.com/i11345047/bf9dd2016430c96c.png)
BIO 这样的线程模型适用于 socket 连接不是很多的场景,但是现在的互联网场景,往往需要服务器支撑成百上千万的连接,而创建上百万个线程显然不现实,BIO 无法满足,我们需要的线程模型应该是这样的,NIO 就粉墨登场了:
![](https://img.haomeiwen.com/i11345047/f8880a948fc6a624.png)
NIO 基于多路复用已经极大提升了性能,为什么不用呢,还是因为存在一些问题的:
- NIO 的类库和 API 繁杂,使用麻烦。你需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。 需要具备其他的额外技能做铺垫。例如熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的 NIO 程序。
- 可靠性能力缺失,开发工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等。 NIO 编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大。
- JDK NIO 的 Bug。例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。官方声称在 JDK 1.6 版本的 update 18 修复了该问题,但是直到 JDK 1.7 版本该问题仍旧存在,只不过该 Bug 发生概率降低了一些而已,它并没有被根本解决。
Netty 是如何设计的?
Reactor 线程模型
Netty 是基于 Reactor 线程模型设计的,Reactor 是反应堆的意思,服务器会处理多路的请求,并将这些请求同步分派给对应的单独的处理线程去处理。Reactor 模式也叫 Dispatcher 模式,即通过 IO 多路复用统一监听事件,收到事件后分发(dispatch),是编写高性能网络服务器的必备技术之一。
Reactor 模型中有两个关键组成:
- Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的线程处理到来的 IO 事件。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人。
- Handlers:处理程序执行 IO 事件要完成的实际操作,Reactor 通过调度适当的处理程序来响应 IO 事件,处理程序执行非阻塞操作。
[图片上传失败...(image-852940-1607739389459)]
取决于 Reactor 的数量和 Handler 线程数的不同,Reactor 模型有 3 个变种:
- 单 Reactor 单线程
- 单 Reactor 多线程
- 主从 Reactor 多线程
可以这样理解,Reactor 就是一个执行 while (true) { selector.select(); …} 循环的线程,会源源不断地产生新的事件,称作反应堆很贴切。
Netty 线程模型
Netty 主要基于主从 Reactor 多线程模型(如下图)做了一定的修改,其中主从 Reactor 多线程模型有多个 Reactor:
- MainReactor 负责客户端的连接请求,并将请求转交给 SubReactor
- SubReactor 负责相应通道的 IO 读写请求
- 非 IO 请求(具体逻辑处理)的任务则会直接写入队列,等待 worker threads 进行处理
这里引用 Doug Lee 大神的 Reactor 介绍:Scalable IO in Java 里面关于主从 Reactor 多线程模型的图:
![](https://img.haomeiwen.com/i11345047/8f05f23a091e33fb.png)
网友评论