Netty(1) 引子篇 | 网络IO模型一网打尽

前言

Netty 的学习是进阶中高级开发必备的技能。虽然它的大名早就如雷贯耳了,但却一直没有正式揭开它的真面目,本系列又是一个小菜鸡开始踏上网络编程的学习之路,将 Netty 相关的学习提上议程。

本篇是启动篇,只是作为 Netty 的引子,会介绍 IO 的读写基础知识、以及 BIO、NIO、AIO 等基础知识,然后引出 Netty。

当然后续还有 Netty 入门篇、进阶篇、应用篇、源码篇等内容输出,再随着自己对 Netty 的深入掌握,编写几个 Netty 的应用达到对 Netty 的熟练掌握目的。

一、网络编程

网络编程指的是在计算机网络环境中进行程序开发和通信的过程。它涉及到使用网络协议、套接字(Socket)和相关的编程接口来实现不同计算机之间的数据传输和通信。

网络编程广泛应用于各种场景,如网站开发、远程访问、分布式系统、实时通信等。通过网络编程,不同计算机之间可以通过网络进行数据交换和通信,实现了信息共享和资源利用的目标。

在网络通信协议下,不同计算机运行的程序,可以进行数据传输, 例如下图,思考,如何将消息发送给指定的电脑呢?需要哪些条件。

网络编程三要素:

  • IP 地址:设备在网络的地址,唯一标识
  • 端口:应用程序在设备中唯一的标识
  • 协议:数据在网络中传输的规则,常见的协议有 UDP 协议和 TCP 协议。

在网络编程中,通常涉及以下几个方面:

  1. 网络协议:网络编程需要了解和理解不同的网络协议,如TCP/IP协议簇。TCP/IP协议簇是互联网上最常用的一种网络协议,包括IP地址分配、路由选择、数据包封装、传输控制等功能。
  2. 套接字(Socket):套接字是网络编程中用于实现网络通信的接口。通过套接字,程序可以创建、连接、发送和接收数据。套接字提供了一种抽象层,使得开发人员可以通过简单而统一的接口与不同网络设备进行通信。
  3. 编程接口:网络编程需要使用编程语言提供的相关接口和库函数来实现网络通信。不同的编程语言提供了不同的网络编程接口,如C/C++的socket API、Python的socket模块、Java的java.net包等。
  4. 客户端-服务器模型:网络编程中常采用客户端-服务器(Client-Server)模型。服务器端提供服务,客户端通过网络连接到服务器并请求服务。服务器接收客户端的请求,进行处理并返回结果。

而我们要学习的主角 Netty 是一款用于开发高性能、可扩展的网络应用程序的Java框架,因此可以说 Netty 是一种网络编程工具。

二、IO 读写的底层原理

操作系统为了保证安全性,即避免用户直接操作内核,将内存分为两部分:

  • 内核空间(Kernel-Space):内核模型运行在内核态
  • 用户空间(User-Space):用户程序运行在用户空间,称用户态。

应用程序,比如 Java 程序,是不允许直接在内核空间区域进行读写,应用进程对应的操作空间就在用户态,处于用户态。

内核态进程可以执行任意命令,调用系统的一切资源,虽然用户进程不能访问内核空间的数据,也不能调用内核函数,但是内核态提供了系统接口,提供用户态调用,在系统调用的时候,要将用户进程切换到内核态才能进行。

应用程序的IO操作,实际上不是物理设备级别的读写,而是缓存的复制。 sys_read & sys_write 两大系统调用,都不负责数据在内核缓冲区和物理设备(如磁盘、网卡等)之间的交换。这项底层的读写交换操作,是由操作系统内核(Kernel)来完成的。所以,应用程序中的IO操作,无论是对Socket的IO操作,还是对文件的IO操作,都属于上层应用的开发,它们的在输入(Input)和输出(Output)维度上的执行流程,都是类似的,都是在内核缓冲区进程缓冲区之间的进行数据交换。

为什么要设置那么多的缓冲区?

缓冲区的目的,是为了减少 频繁与设备之间的物理交换。外部设备的直接读取涉及操作系统的中断,发生系统中断时,需要保存之前的进程数据和状态,而结束中断时,还要恢复之前的状态和数据。造成时间损耗,因此,增加了缓冲区。

有了内核缓冲区,操作系统会对内核缓冲区进行监控,等待缓冲区达到一定数量的时候, 再进行IO设备的中断处理,集中执行物理设备的实际IO操作,通过这种机制来提升系统的 性能。至于具体在什么时候执行系统中断(包括读中断、写中断),则由操作系统的内核来决定,应用程序不需要关心。

所以,用户进程使用 sys_read 和 sys_write 调用时,都是针对缓冲区而言的。

sys_read 与 sys_write 执行流程

用户程序所使用的read和write函数,可以理解为C语言中的库函数,这个库函数专供用户程序使用。注意:这些库函数并不是内核程序,而内核空间的数据读写需要内核程序完成,所以,这些库函数里,还需要对系统调用进行更进一步的封装和调用。那么,这里涉及到哪 里系统调用呢?

由于不同的操作系统,或者同一个操作系统的不同版本,在具体实现上都有差异,所以,大家可以大致的理解为,C程序中使用的read库函数会调用到的系统调用为 sys_read,由sys_read完成内核空间的数据读取;用户C程序中使用的write库函数会调用到的系统调用为sys_write,由sys_write完成内核空间的数据写入。 系统调用sys_read&sys_write,并不是使数据在内核缓冲区和物理设备之间的交换。

  • sys_read调用把数据从内核缓冲区复制到应用的用户缓冲区,
  • sys_write调用把数据从应用的用户缓冲区复制到内核缓冲区,

两个系统调用的大致的流程,如图

三、五种 IO 模型

在网络编程中,涉及到网络通信的过程,其中就包含了 IO 操作,通过网络编程,我们可以使用各种 IO 模型来进行数据的传输和处理。

IO 模型严格上分为 5 种 IO

  • 同步阻塞IO(Blocking IO):在阻塞IO模型中,当应用程序执行IO操作时,如果数据没有准备好或者无法立即读取或写入,IO操作会被阻塞,直到数据准备好或者操作完成。
  • 同步非阻塞IO(Non-Blocking IO):非阻塞IO模型中,应用程序可以通过设置非阻塞状态,在执行IO操作时立即返回结果;
  • IO 多路复用(Multiplexing IO):多路复用IO模型利用操作系统提供的事件通知机制,如select、poll、epoll等,允许应用程序同时监听多个IO事件。当任意一个IO事件就绪时,应用程序可以立即处理该事件,而不需要遍历所有的IO操作。
  • 信号驱动 IO 模型:信号驱动IO 可以看成是一种异步 IO,可以简单理解为系统进行用户函数的回调。只是,信号驱动 IO 的异步特性做的不彻底,因为信号驱动 IO 仅仅在 IO 时间的通知阶段是异步的,而在第二阶段,也就是在将数据从内核缓冲区复制到用户缓冲区的这个过程,用户进程是阻塞的、同步的。
  • 异步IO (AIO):异步 IO 包含不完全的信号驱动IO,和完全的异步IO模型。类似于 Java 中典型的回调模式,用户进程向内核空间注册了各种IO事件的回调函数,由内核去主动调用,内核在IO完成后通知用户线程直接使用即可。

3.1 同步阻塞IO

默认情况下,在 Java 应用中,所有的 Socket 连接进行的 IO 操作都是同步阻塞 IO

在阻塞式 IO 模型中,Java 应用程序从发起 IO 系统调用开始,一直到系统调用返回,在这段时间内,发起 IO 请求的 Java 进程是阻塞的,直到返回成功后,应用进程才能开始处理用户空间的缓冲区数据。

阻塞 IO 的缺点是:一般情况下,会为每个连接配备一个独立的线程,一个线程维护一个连接的 IO 操作,在并发量大的情况下,大量线程开销是非常大的,基本不可用。

3.2 同步非阻塞 NIO

在 Linux 系统下,socket 连接模式是阻塞模式,可以通过设置将 Socket 改为非阻塞的模式。

NIO 模型中,应用程序一旦开始 IO 系统调用,会出现以下两种情况:

  • 1)在内核缓冲区中没有数据的情况下,系统调用会立即返回,返回一个调用失败的信息
  • 2)在内核缓冲区中有数据的情况下,在数据的复制过程中系统调用是阻塞的,直到完成数据从内核缓冲复制到用户缓冲。复制完成后,系统调用返回成功,用户进程可以处理用户空间的缓存数据。

特点:应用程序的线程需要不断的进行 IO 系统调用,轮询数据是否已经准备好,如果没有准备好,就继续轮询,直到完成 IO 系统调用为止。

优点:每次发起的 IO 系统调用,在内核等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好

缺点:轮询内核,占用大量CPU

非阻塞 IO,指的是用户空间的程序,不需要等待内核 IO 操作彻底完成,可以立即返回用户空间去执行后续的指令,即发起 IO 请求的用户进程(用户线程)处于非阻塞状态,同时,内核会立即返回给用户一个 IO 的状态值。

3.3 IO 多路复用模型

如何避免 NIO 的轮询等待问题?

答案是 IO 多路复用模型

在该模型下,引入了一种新的系统调用 ==> 查询 IO 的就绪状态。

在IO多路复用模型中通过select/epoll系统调用,单个应用程序的线程,可以不断地轮询成百上千的socket连接的就绪状态,当某个或者某些socket网络连接有IO就绪状态,就返回这些就绪的状态(或者说就绪事件)。

IO多路复用模型与同步非阻塞IO模型是有密切关系的,具体来说,注册在选择器上的每一个可以查询的socket连接,一般都设置成为同步非阻塞模型。只是这一点对于用户程序而言,是无感知的。

IO多路复用模型的优点:一个选择器查询线程,可以同时处理成千上万的网络连接, 所以,用户程序不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。这是一个线程维护一个连接的阻塞IO模式相比,使用多路IO复用模型的最大优势。

通过 JDK 的源码可以看出,Java 语言的 NIO(New IO)组件,在 Linux 系统上,是使用的是 select 系统调用实现的。所以,Java 语言的 NIO(New IO)组件所使用的,就是IO多路复用模型。

IO多路复用模型的缺点:本质上,select/epoll系统调用是阻塞式的,属于同步阻塞IO。 都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是说这个事件的查询过程是阻塞的。 如果彻底地解除线程的阻塞,就必须使用异步 IO 模型。

3.4 信号驱动 IO

在信号驱动 IO 模型中,用户线程通过向核心注册 IO 事件的回调函数,来避免 IO 时间查询的阻塞。

具体的做法是,用户进程预先在内核中设置一个回调函数,当某个事件发生时,内核使用信号(SIGIO)通知进程运行回调函数。

然后用户线程会继续执行,在信号回调函数中调用 IO 读写操作来进行实际的 IO 请求操作。

信号驱动IO的基本流程是:用户进程通过系统调用,向内核注册SIGIO信号的owner进程和以及进程内的回调函数。内核IO事件发生后(比如内核缓冲区数据就位)后,通知用户程序,用户进程通过sys_read系统调用,将数据复制到用户空间,然后执行业务逻辑。

3.5 异步 IO

异步IO ,AIO

AIO 的基本流程是:用户线程通过系统调用,向内核注册某个 IO 操在,内核在整个 IO 操作完成后,通知用户程序,用户执行后续的业务操作。

在异步 IO 模型中,在整个内核的数据处理过程中,包括内核将数据从网络物理设备读取到内核缓冲区、将内核缓冲区的数据复制到用户缓冲区,用户程序都不需要阻塞。

异步IO模型的特点:在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。

用户线程需要接收内核的IO操作完成的事件,或者用户线程需要注册一个IO操作完成的回调函数。

正因为如此,异步IO有的时候也被称为信号驱动IO。 异步IO异步模型的缺点:应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,也就是说,需要底层内核提供支持。

四、Reactor 模型

目前为止,高性能网络编程都绕不开反应器模式。

很多著名的服务器软件或者中间件都是基于反应器模式实现的。

比如说,"全宇宙最有名的、最高性能"的Web服务器Nginx,就是基于反应器模式的;

比如雷贯耳的Redis,作为最高性能的缓存服务器之一,也是基于反应器模式的;

目前火得"一 塌糊涂"、在开源项目中应用极为广泛的高性能通信中间件Netty,更是基于反应器模式的。

从开发的角度来说,如果要完成和胜任高性能的服务器开发,反应器模式是必须学会和掌握的。从学习的角度来说,反应器模式相当于高性能、高并发的一项非常重要的基础知识,只有掌握了它,才能真正理解和掌握Nginx、Redis、Netty等这些大名鼎鼎的中间件技术。

总之,反应器模式是高性能网络编程的必知、必会的模式。

Reactor 的线程模型不是 Java 专属的,也不是 Netty 专属的,它是一种思想,该模型定义了三种角色:

  • Reactor :负责监听和分配事件,将 IO 事件分派给对应的 Handler,新的事件包含连接建立就绪、读就绪、写就绪等。
  • Acceptor:处理客户端连接,并分派请求到处理器链中;
  • Handler:将自身与事件绑定,执行非阻塞读\写 任务,完成 channel 的读入,完成处理业务逻辑后,负责将结果写出 channel 。

根据 Reactor 的数量和处理资源池线程的数量不同,分三种实现方式:

  • 单 Reactor 单线程
  • 单 Reactor 多线程
  • 主从 Reactor 多线程

注:Netty 主要基于主从 Reactor 多线程模型做了一定改进,其中主从 Reactor 多线程模型有多个 Reactor。

4.1 单 Reactor 单线程

所有的接收连接,处理数据的相关操作都在一个线程中来完成,性能上有瓶颈。

单 Reactor 单线程,角色分为三种。

  • 架构:使用一个Reactor(反应器)负责监听事件和处理事件回调,并且只有一个线程来处理所有的IO操作。
  • 特点:
    • 实现简单,代码清晰易懂。
    • 由于只有一个线程,避免了线程切换带来的开销,适用于连接数较少且每个连接的处理时间短暂的场景。
    • 可以通过非阻塞IO(Non-blocking IO)和事件驱动的方式实现高并发。
  • 缺点:
    • 整体性能受限,因为只有一个线程在处理所有的IO操作,无法利用多核CPU的优势。
    • 如果某个连接上的处理时间过长,会影响其他连接的响应,造成性能瓶颈。

4.2 单Reactor 多线程

把比较耗时的数据的业务逻辑处理操作放入线程池中执行,提升了性能,但是还不是最好的方式。

当多个客户端进入服务器后,Reactor线程会监听多种事件(如连接事件,读事件,写事件),如果监听到连接事件,则把该事件分配给acceptor处理,如果监听到读事件,那么则会发起系统调用,将数据写入内存,之后再把数据交给工作线程池进行业务处理。这个时候我们会发现,业务处理的逻辑已经变成多线程处理了。不过一个Reactor既要负责连接事件,又要负责读写事件,同时还要负责数据准备的过程。因为拷贝数据是阻塞的,假如说Reactor阻塞到拷贝数据的时候,服务器进来了很多连接,这个时候,这些连接是很有可能会被服务器拒绝掉的。

所以,单个Reactor看来是不够的,我们需要使用多个Reactor来处理。

  • 特点:
    • 可以利用多核CPU的优势,提高整体的处理能力。
    • 适用于连接数较多且每个连接的处理时间相对较短的场景。
  • 缺点:
    • 线程之间的同步和数据共享需要谨慎处理,可能引入锁竞争和线程安全性问题。
    • 处理复杂度增加,需要考虑线程池管理、任务调度等。

4.3 主从Reactor多线程

主从多线程,对于服务器来说,接收客户端的连接是比较重要的,因此,将这部分操作单独用线程去操作。

架构:使用一个主Reactor负责监听事件和分发给多个子Reactor来处理IO操作,每个子Reactor在一个独立的线程中运行

特点:

  • 主Reactor负责接受客户端连接,并将连接分配给子Reactor处理,实现了事件的分离和处理的并行化。
  • 子Reactor独立运行,并发处理IO操作。
  • 可以充分利用多核CPU的优势,提高整体的处理能力。

缺点:

  • 实现较为复杂,涉及到线程间的协调和通信。
  • 进一步提高并发能力时,需考虑负载均衡、资源管理等问题。

优势:

  • 主从 Reactor 与从 Reactor 分工职责明确,Main 线程只需要接收新连接,Sub 线程完成后续的业务处理
  • 多个从 Reactor 线程能够应对更高的并发请求。

主从工作流程:

  • 1、Reactor 主线程 MainReactor 对象通过 select 监听客户端连接事件,收到事件后,通过 Acceptor 处理客户端连接事件。
  • 2、当 Acceptor 处理完客户端连接事件之后(与客户端建立好 Socket 连接),MainReactor 将连接分派给 SubReactor。就是主处理器只处理客户端的连接请求,而多个从处理器去监听后面的 IO 事件。
  • 3、SubReactor 将连接加入到自己的连接队列进行监听,并创建 Handler 对各种事件进行处理
  • 4、当连接上有新事件发生的时候,SubReactor 就会调用对应的 Handler 处理
  • 5、Handler 通过 read 从连接上读取请求数据,将请求数据分发给 Worker 线程池进行业务处理
  • 6、Worker 线程池会分配独立线程来完成真正的业务处理,并将处理结果返回给 Handler,Handler 通过 send 向客户端发送响应数据。
  • 7、一个 MainReactor 可以对应多个 SubReactor,即一个 MainReactor 线程可以对应多个 SubReactor 线程

总结:这种模式的缺点是编程复杂度较高,但是由于其优点明显,在许多项目中被广泛使用,包括 Netty、Nginx、Memcached 等。

这种模式也被叫服务器的 1+M+N 线程模式,这是业界成熟的服务器程序设计模式。

Netty 的线程模型与主从 Reactor 多线程模型非常的相似,区别是做了些改进,我们下文分解,本文主要介绍了学习 Netty 网络编程的前置知识,作为引子篇。关于 Netty 的更多学习,详看后续文章。感谢阅读

相关推荐
suweijie7682 小时前
SpringCloudAlibaba | Sentinel从基础到进阶
java·大数据·sentinel
公贵买其鹿3 小时前
List深拷贝后,数据还是被串改
java
向前看-6 小时前
验证码机制
前端·后端
xlsw_6 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹7 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭8 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫8 小时前
泛型(2)
java
超爱吃士力架8 小时前
邀请逻辑
java·linux·后端
南宫生8 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石8 小时前
12/21java基础
java