java基础巩固-宇宙第一AiYWM:为了维持生计,四大基础之OS_Part_2整起~IO们那些事【包括五种IO模型:(BIO、NIO、IO多路复用、信号驱动、AIO);零拷贝、事件处理及并发等模型】

PART0.前情提要:

  • 通常用户进程的一个完整的IO分为两个阶段(IO有内存IO、网络IO和磁盘IO三种,通常我们说的IO指的是后两者!):【操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能使用指针传递数据,因为Linux使用的虚拟内存机制,必须通过系统调用请求内核来完成IO动作。】
    • 内存IO:
    • 磁盘IO:
      在这里插入图片描述
      • 和磁盘打交道就是费事,但也没办法,咱们缺容量呀。咱们在 Java 中 IO 流分为输入流和输出流,根据数据的处理方式又分为字节流和字符流。根据擒贼先擒王的手法,咱们把Java IO 流的 40 多个类的如下 4 个抽象类基类先抓住,【Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的】。
        • InputStream/Reader: 所有的输入流的基类,InputStream是字节输入流【用于从源头(通常是文件)读取数据(字节信息)到内存中,java.io.InputStream抽象类是所有字节输入流的父类。】,Reader是字符输入流【不管是文件读写还是网络发送接收,信息的最小存储单元都是字节。 那为什么 I/O 流操作要分为字节流操作和字符流操作呢?原因主要是有时候如果我们不知道编码类型就很容易出现乱码问题,I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。字符流默认采用的是 Unicode 编码,我们可以通过构造方法自定义编码。utf8 :英文占 1 字节,中文占 3 字节,unicode:任何字符都占 2 个字节,gbk:英文占 1 字节,中文占 2 字节
          • InputStream
            • InputStream 常用方法 :
              在这里插入图片描述
              • 通过 readAllBytes() 读取输入流所有字节并将其直接赋值给一个 String 对象
                // 新建一个 BufferedInputStream 对象
                BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt"));
                // 读取文件的内容并复制到 String 对象中
                String result = new String(bufferedInputStream.readAllBytes());
                System.out.println(result);
                
            • 一般我们是不会直接单独使用 FileInputStream ,通常会配合 BufferedInputStream字节缓冲输入流
            • DataInputStream 用于读取指定类型数据,不能单独使用,必须结合 FileInputStream
              在这里插入图片描述
            • ObjectInputStream 用于从输入流中读取 Java 对象(反序列化,用于序列化和反序列化的类必须实现 Serializable 接口,对象中如果有属性不想被序列化,使用 transient 修饰),ObjectOutputStream 用于将对象写入到输出流(序列化)
          • Reader:Reader 用于读取文本, InputStream 用于读取原始字节。
            • Reader常用方法:
              在这里插入图片描述
            • InputStreamReader 是字节流转换为字符流的桥梁,其子类 FileReader 是基于该基础上的封装,可以直接操作字符文件
        • OutputStream/Writer: 所有输出流的基类,OutputStream是字节输出流【OutputStream用于将数据(字节信息)写入到目的地(通常是文件),java.io.OutputStream抽象类是所有字节输出流的父类。】,Writer是字符输出流
          • FileOutputStream
            • FileOutputStream 是最常用的字节输出流对象,可直接指定文件路径,可以直接输出单字节数据,也可以输出指定的字节数组。FileOutputStream 通常也会配合 BufferedOutputStream字节缓冲输出流
              在这里插入图片描述
            • DataOutputStream 用于写入指定类型数据,不能单独使用,必须结合 FileOutputStream
          • Writer:
            • OutputStreamWriter 是字符流转换为字节流的桥梁,其子类 FileWriter 是基于该基础上的封装,可以直接将字符写入到文件
        • 字节缓冲流:IO 操作是很消耗性能的,所以缓冲流用来将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的 IO 操作,提高流的传输效率
          在这里插入图片描述
          • 字节缓冲流采用了装饰器模式 来增强 InputStream 和OutputStream子类对象的功能。【字节流和字节缓冲流的性能差别主要体现在我们使用两者的时候都是调用 write(int b) 和 read() 这两个一次只读取一个字节的方法的时候。由于字节缓冲流内部有缓冲区(字节数组),因此,字节缓冲流会先将读取到的字节存放在缓存区,大幅减少 IO 次数,提高读取效率。】
          • 如果是调用 read(byte b[]) 和 write(byte b[], int off, int len) 这两个写入一个字节数组的方法的话,只要字节数组的大小合适,两者的性能差距其实不大,基本可以忽略。
          • BufferedInputStream(字节缓冲输入流):
            • BufferedInputStream 内部维护了一个缓冲区,这个缓冲区实际就是一个字节数组
              在这里插入图片描述
          • BufferedOutputStream(字节缓冲输入流):
            • BufferedOutputStream 内部也维护了一个缓冲区,并且,这个缓存区的大小也是 8192 字节
        • 字符缓冲流:
          • BufferedReader (字符缓冲输入流)和 BufferedWriter(字符缓冲输出流)类似于 BufferedInputStream(字节缓冲输入流)和BufferedOutputStream(字节缓冲输入流),内部都维护了一个字节数组作为缓冲区。不过,前者主要是用来操作字符信息
        • 打印流:
          • System.out.println(“Hello!”);System.out 实际是用于获取一个 PrintStream 对象,print方法实际调用的是 PrintStream 对象的 write 方法。PrintStream 属于字节打印流,与之对应的是 PrintWriter (字符打印流)。PrintStream 是 OutputStream 的子类,PrintWriter 是 Writer 的子类。
        • 随机访问流:随机访问流指的是 支持随意跳转到文件的任意位置进行读写的 RandomAccessFile
          在这里插入图片描述
          • 文件内容指的是文件中实际保存的数据,元数据则是用来描述文件属性比如文件的大小信息、创建和修改时间。
          • RandomAccessFile 中 有一个文件指针 用来表示 下一个将要被写入或者读取的字节所处的位置。我们可以通过 RandomAccessFile 的 seek(long pos) 方法来设置文件指针的偏移量(距文件开头 pos 个字节处)。如果想要获取文件指针当前的位置的话,可以使用 getFilePointer() 方法。
          • RandomAccessFile 比较常见的一个应用就是实现大文件的 断点续传 。何谓断点续传?简单来说就是上传文件中途暂停或失败(比如遇到网络问题)之后,不需要重新上传,只需要上传那些未成功上传的文件分片即可。分片(先将文件切分成多个文件分片)上传是断点续传的基础
    • 网络IO:
      在这里插入图片描述
      • 客户端和服务器能在网络中通信,那必须得使用 Socket 编程来支持跨主机间通信。Socket这个货叫插口,其实我觉得就是个,比如咱们自己家里有两台路由器要通信,你不能拿一条线直接粘到路由器屁股上吧,肯定是两台路由器屁股上都有口,然后一条线这边插好那边插好,然后就可以跨主机通信了呗,路由器屁股上开的口我觉得就可以看作是Socket。
        • 创建 Socket 的时候,可以指定网络层使用的是 IPv4 还是 IPv6,传输层使用的是 TCP 还是 UDP。 不管是那种,反正肯定是 服务器的程序要先跑起来,然后等待客户端的连接和数据
          • 我们先来看看服务端的 Socket 编程过程是怎样的。或者叫**TCP Socket**。它基本只能一对一通信,因为使用的是同步阻塞的方式,当服务端在还没处理完一个客户端的网络 I/O 时,或者 读写操作发生阻塞时,其他客户端是无法与服务端连接的。可如果我们服务器只能服务一个客户,那这样就太浪费资源了,于是我们要改进这个网络 I/O 模型,以支持更多的客户端。
            在这里插入图片描述
          • 或者说,先看看 客户端和服务端的基于 TCP 的通信流程
            在这里插入图片描述
            • 服务端的伪代码:
              在这里插入图片描述
            • 在 LInux 中一切皆文件,socket 也不例外,每个打开的文件都有读写缓冲区,对文件执行read()、write()时的具体流程如下:
              在这里插入图片描述
          • 可以看到 传统的 socket 通信会阻塞在 connect,accept,read/write 这几个操作上,这样的话如果 server 是单进程/线程的话,只要 server 阻塞,就不能再接收其他 client 的处理了,由此可知传统的 socket 无法支持 C10K
            • 针对传统 IO 模型缺陷的改进,主要有两种:
              • 多进程/线程模型
              • IO 多路程复用:下面有
            • 高并发即我们所说的 C10K(一个server 服务 1w 个 client),C10M。高并发架构其实有一些很通用的架构设计,如无锁化,缓存等
            • 经典的 C10K 问题:如果服务器的内存只有 2 GB,网卡是千兆的,能支持并发 1 万请求吗【单机同时处理 1 万个请求的问题。】从硬件资源角度看,对于 2GB 内存千兆网卡的服务器,如果每个请求处理占用不到 200KB 的内存和 100Kbit 的网络带宽就可以满足并发 1 万个请求。不过,要想真正实现 C10K 的服务器,要考虑的地方在于服务器的网络 I/O 模型,效率低的模型,会加重系统开销基于进程或者线程模型的,其实还是有问题的。新到来一个 TCP 连接,就需要分配一个进程或者线程,那么如果要达到 C10K,意味着要一台机器维护 1 万个连接,相当于要维护 1 万个进程/线程,操作系统就算死扛也是扛不住的
        • 服务器单机理论最大能连接多少个客户端?
          • TCP 连接是由四元组唯一确认的,这个四元组就是:本机IP, 本机端口, 对端IP, 对端端口。服务器作为服务方,通常会在本地固定监听一个端口,等待客户端的连接。 因此服务器的本地 IP 和端口是固定的,于是对于服务端 TCP 连接的四元组只有对端 IP 和端口是会变化的,所以最大 TCP 连接数 = 客户端 IP 数×客户端端口数。对于 IPv4,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是服务端单机最大 TCP 连接数约为 2 的 48 次方。但是服务器肯定承载不了那么大的连接数,主要会受两个方面的限制:
            在这里插入图片描述
            • fd(文件描述符):Socket 实际上是一个文件,也就会对应一个文件描述符。在 Linux 下,单个进程打开的文件描述符数是有限制的,没有经过修改的值一般都是 1024,不过我们可以通过 ulimit 增大文件描述符的数目;
              • 在 Linux 中无论是文件,socket,还是管道,设备等,一切皆文件,Linux 抽象出了一个 VFS(virtual file system) 层,屏蔽了所有的具体的文件,VFS 提供了统一的接口给上层调用,这样应用层只与 VFS 打交道,极大地方便了用户的开发,仔细对比你会发现,这和 Java 中的面向接口编程很类似【fd 的值从 0 开始,其中 0,1,2 是固定的,分别指向标准输入(指向键盘),标准输出/标准错误(指向显示器),之后每打开一个文件,fd 都会从 3 开始递增,但需要注意的是 fd 并不一定都是递增的,如果关闭了文件,之前的 fd 是可以被回收利用的】
                在这里插入图片描述
            • 系统内存,每个 TCP 连接在内核中都有对应的数据结构,意味着每个连接都是会占用一定内存的;
      • 基于 Linux 一切皆文件的理念,在内核中 Socket 也是以文件的形式存在的,也是有对应的文件描述符,每个打开的文件也都有读写缓冲区。每个文件都有一个 inode,Socket 文件的 inode 指向了内核中的 Socket 结构,在这个结构体里有两个队列,分别是发送队列和接收队列,这个两个队列里面保存的是一个个 struct sk_buff,用链表的组织形式串起来。sk_buff 可以表示各个层的数据包,在应用层数据包叫 data,在 TCP 层我们称为 segment,在 IP 层我们叫 packet,在数据链路层称为 frame,这不就和计算机网络穿起来了嘛。协议栈采用的是分层结构,上层向下层传递数据时需要增加包头,下层向上层数据时又需要去掉包头,如果每一层都用一个结构体,那在层之间传递数据的时候,就要发生多次拷贝,这将大大降低 CPU 效率。于是,为了在层级之间传递数据时,不发生拷贝,只用 sk_buff 一个结构体来描述所有的网络包,那它是如何做到的呢?是通过调整 sk_buff 中 data 的指针。【当接收报文时,从网卡驱动开始,通过协议栈层层往上传送数据报,通过增加 skb->data 的值,来逐步剥离协议首部+++++当要发送报文时,创建 sk_buff 结构体,数据缓存区的头部预留足够的空间,用来填充各层首部,在经过各下层协议时,通过减少 skb->data 的值来增加协议首部。】
        在这里插入图片描述

PART1:Unix 常见的五种IO模型:网络编程中的五个 I/O 模型:同步阻塞 I/O(BIO)、同步非阻塞 I/O(NIO)、 I/O 多路复用、信号驱动、异步非阻塞 I/O(AIO))【只有 AIO 为异步 IO,其他都是同步 IO】,最常用的就是同步阻塞BIO 和 IO 多路复用

  • Unix 常见的IO模型:对于一次IO访问(以read举例),数据会先被 拷贝到操作系统内核的缓冲区中,然后 才会从操作系统内核的缓冲区拷贝到应用程序的地址空间

    • 无论是阻塞 I/O、非阻塞 I/O,还是基于非阻塞 I/O 的多路复用以及信号驱动都是同步调用。因为 它们在 read 调用时,内核将数据从内核空间拷贝到应用程序空间,过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷贝效率不高,read 调用就会在这个同步过程中等待比较长的时间
      在这里插入图片描述
      • 5中I/O中,I/O 阻塞、I/O非阻塞、I/O复用、SIGIO 都会在不同程度上阻塞应用程序,而只有异步I/O模型在整个操作期间都不会阻塞应用程序。】
        在这里插入图片描述
        • 真正的异步 I/O 是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待
        • POSIX 规范中定义了同步I/O 和异步I/O的术语:
          • 同步I/O : 需要进程去真正的去操作I/O
          • 异步I/O:内核在I/O操作完成后再通知应用进程操作结果
      • 但是如果使用同步的方式来通信的话,所有的操作都在一个线程内顺序执行完成,这么做缺点是很明显的:因为同步的通信操作会阻塞同一个线程的其他任何操作,只有这个操作完成了之后,后续的操作才可以完成,所以出现了同步阻塞+多线程(每个Socket都创建一个线程对应),但是系统内线程数量是有限制的,同时线程切换很浪费时间,适合Socket少的情况,因该需要出现IO模型
    • 阻塞 IO 和 IO 多路复用最为常用,原因如下:
      • 在系统内核的支持上,现在大多数系统内核都会支持阻塞 IO、非阻塞 IO 和 IO 多路复用,但像信号驱动 IO、异步 IO,只有高版本的 Linux 系统内核才会支持。
      • 在编程语言上,无论 C++ 还是 Java,在高性能的网络编程框架的编写上,大多数都是基于 Reactor 模式,其中最为典型的便是 Java 的 Netty 框架,而 Reactor 模式是基于 IO 多路复用的。当然,在非高并发场景下,同步阻塞 IO 是最为常见的
    • 当一个read操作发生时,会经历两个阶段:或者说I/O 是分为两个过程的【或者说当应用程序发起 I/O 调用后,会经历两个步骤:】
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      • 过程一:内核等待数据准备就绪 (Waiting for the data to be ready)【内核程序要从磁盘、网卡等读取数据到内核空间缓存区;】
        • 传统的IO流程,包括read和write的过程:
          • read:把数据从磁盘读取到内核缓冲区,再从内核缓冲区拷贝到用户缓冲区
          • write:先把数据写入到socket缓冲区,最后写入网卡设备。
      • 过程二:内核或者说用户程序将数据从内核空间缓存拷贝到用户空间的进程中 (Copying the data from the kernel to the process)【大多数文件系统的默认 IO 操作都是缓存 IO。缓存 IO 的缺点:数据在传输过程中需要在应用程序地址空间和内核空间进行多次数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存开销非常大。】
    • 正式因为这两个阶段,linux系统产生了下面五种网络模式的方案:【阻塞 I/O 会阻塞在过程 1 和过程 2,非阻塞 I/O 和基于非阻塞 I/O 的多路复用只会阻塞在过程 2,所以这三个都可以认为是同步 I/O异步 I/O 则不同,过程 1 和过程 2 都不会阻塞。】【主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的。当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话必然就阻塞,但CPU是被释放出来的,开启多线程,就可以让CPU去处理更多的事情。利用多核,当I/O阻塞时,但CPU空闲的时候,可以利用多线程使用CPU资源。
      • Unix 常见的五种IO模型之一:同步阻塞式IO模型BIO(blocking IO model):在linux中,默认情况下所有的IO操作都是blocking
        在这里插入图片描述
        在这里插入图片描述
        • 计算机有内核,内核可以接收客户端来的连接【客户端的所有连接是先到达内核】,建立连接就会产生文件描述符,原来内核中有read函数可以读文件描述符,这个read操作是在一个线程或者进程中读,而来一个客户端请求后服务端就会new一个新线程,一个线程对应一个连接,利用CPU的时间片轮转去处理当前的用户读写操作,但是线程资源毕竟是有限的。socket在这个时期是block的,数据包不能返回一直在阻塞,这就是对应的早期的BIO。这样就浪费了计算机硬件。NIO此时出来了,内核此时发生变化了,内核升级
          • yum install man man-pages:这条命令可以看linux中命令的实现原理。利用man 2 socket可以看到
            在这里插入图片描述
          • 到了NIO之后,此时说明文件描述符可以是nonblock。然后由于是非阻塞的,我就可以单线程或者单进程,在这个单个里面写一个while循环,read fd1,有没有数据,fd1说额没有。好,那我就继续read fd2,fd2说有数据,拿着有的东西进行处理,处理完之后继续去用户空间轮询read fdx【轮询操作发生在用户空间哦】,但是拿出来处理都由我自己来弄,就叫做同步+非阻塞,
          • 那现在如果有10000个fd,在用户空间内的用户进程需要轮询调用10000次kernel,这么多次系统调用,太浪费了,所以内核进行升级,增加一个系统调用,select
          • 然后,继续可以man 2 select看一下这个系统调用
            在这里插入图片描述
          • 虽然select是,比如1000个客户端连接请求,你告诉我里面50个准备好数据了,我去挨个读这50个,节省了用户态到内核态的切换。但是select归根到底有缺点,往下看咯
        • 通常把阻塞的文件描述符(file descriptor,fd)称之为阻塞I/O。默认条件下,创建的socket fd是阻塞的,针对阻塞I/O调用系统接口,可能因为等待的事件没有到达而被系统挂起,直到等待的事件触发调用接口才返回,例如,tcp socket的connect调用会阻塞至第三次握手成功(不考虑socket 出错或系统中断)
          在这里插入图片描述
        • 在JDK1.4推出JavaNIO之前,基于Java的所有Socket通信都采用了同步阻塞模式(BIO),这种一请求一应答的通信模型简化了上层的应用开发,但是BIO在性能和可靠性方面却存在着巨大的瓶颈。因此,在很长一段时间里,大型的应用服务器都采用C或者C++语言开发,因为它们可以直接使用操作系统提供的异步I/O或者AIO能力。当并发访问量增大、响应时间延迟增大之后,采用JavaBIO开发的服务端软件只有通过硬件的不断扩容来满足高并发和低时延,它极大地增加了企业的成本,并且随着集群规模的不断膨胀,系统的可维护性也面临巨大的挑战,只能通过采购性能更高的硬件服务器来解决问题,这会导致恶性循环。正是由于Java传统BIO的拙劣表现,才使得Java支持非阻塞I/O的呼声日渐高涨,最终,JDK1.4版本提供了新的NIO类库,Java 终于也可以支持非阻塞I/O 了。
        • 一个典型的读操作流程大概是这样:在这里插入图片描述
          • 当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来),而数据被拷贝到操作系统内核的缓冲区中是需要一个过程的,这个过程需要等待。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户空间的缓冲区以后,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
            • 同步阻塞 BIO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间
              在这里插入图片描述
        • blocking IO的特点就是在IO执行的下两个阶段的时候都被block了。BIO 模型有两处阻塞的地方
          • 等待数据准备就绪 (Waiting for the data to be ready) 阻塞 (服务端阻塞等待客户端发起连接。也就是通过 serverSocket.accept()方法服务端等待用户发连接请求过来。)
          • 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 阻塞(连接成功后,工作线程阻塞读取客户端 Socket 发送数据。也就是服务端通过 in.readLine() 从网络中读客户端发送过来的数据,这个地方也会阻塞。如果客户端已经和服务端建立了一个连接,但客户端迟迟不发送数据,那么服务端的 readLine() 操作会一直阻塞,造成资源浪费。)
        • BIO模型的特点:或者说BIO模型的缺点:
          • Socket 连接数量受限,不适用于高并发场景)缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1 : 1的正比关系,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。显而易见,如果我们要构建高性能、低时延、支持大并发的应用系统,使用同步阻塞I/O模型是无法满足性能线性增长和可靠性的。当面对十万甚至百万级连接的时候,传统的BIO模型是无能为力的。随着移动端应用的兴起和各种网络游戏的盛行,百万级长连接日趋普遍,此时,必然需要一种更高效的I/O处理模型NIO
          • 有两处阻塞,分别是等待用户发起连接,和等待用户发送数据。这不是个好事情,你想呀,你去买东西,别人给你取盒烟让你等了一个小时,然后你扫码时网不太好,又让你等了半小时,你不得不付现金,他给你找零钱又让你等了一小时…。所以肯定得解决这个两个地方阻塞的问题。用NIO网络模型(NIO网络模型操作上是用一个线程处理多个连接,使得每一个工作线程都可以处理多个客户端的 Socket 请求,这样工作线程的利用率就能得到提升,所需的工作线程数量也随之减少。此时 NIO 的线程模型就变为 1 个工作线程对应多个客户端 Socket 的请求,这就是所谓的 I/O多路复用。)来解决
          • 另外补充一点,网络编程中,通常把可能永远阻塞的系统API调用 称为慢系统调用,典型的如 accept、recv、select等。慢系统调用在阻塞期间可能被信号中断而返回错误,相应的errno 被设置为EINTR,我们需要处理这种错误,解决办法有:
            • 重启系统调用:
              在这里插入图片描述
            • 信号处理
              在这里插入图片描述
        • 先理一理哈,在计算机网络这篇中提到了 应用程序建立连接后通过OS实现的TCP协议的socket接口给服务器发了数据(假设咱们服务器上用的服务器软件是Tomcat),服务器会利用自己体内的endPoint的实现去socket(OS实现的TCP协议的socket接口)中拿到数据,然后再解析成为一个又一个请求,再交给tomcat去处理,处理完响应给客户端。在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
        • BI/O 模型典型的Java实现: 基于 BIO 的文件复制程序:字节流方式、字符流方式、字符缓冲,按行读取、随机读写(RandomAccessFile)
          在这里插入图片描述
          public class BIOSever {
          //在服务端创建一个 ServerSocket 对象
              ServerSocket ss = new ServerSocket();
              // 绑定端口 9090,然后启动运行服务端,然后阻塞等待客户端发起连接请求,直到有客户端的连接发送过来之后。当有客户端的连接请求后,服务端会启动一个新线程 ServerTaskThread,用新创建的线程去处理当前用户的读写操作。
              ss.bind(new InetSocketAddress("localhost", 9090));
              System.out.println("server started listening " + PORT);
              try {
                  Socket s = null;
                  while (true) {
                      // 阻塞等待客户端发送连接请求,直到有客户端的连接发送过来之后,accept() 方法返回Socket 
                      s = ss.accept();
                      new Thread(new ServerTaskThread(s)).start();
                  }
              } catch (Exception e) {
                  ...
              } finally {
                  if (ss != null) {
                      ss.close();
                      ss = null;
              }
          }
          /*
          *当有客户端的连接请求后,服务端会启动一个新线程 ServerTaskThread,用新创建的线程去处理当前用户的读写操作。
          */
          public class ServerTaskThread implements Runnable {
              ...
              while (true) {
                  // 阻塞等待客户端发请求过来
                  String readLine = in.readLine();
                  if (readLine == null) {
                      break;
                  }
                  ...
              }
              ...
          }
          
        • 使用 Java NIO 包组成一个简单的客户端-服务端网络通讯所需要的 ServerSocketChannel、SocketChannel 和 Buffer,一个完整的可运行的例子:(例子来自javadoop老师)
          在这里插入图片描述
          • SocketHandler:【来一个新的连接,我们就新开一个线程来处理这个连接,之后的操作全部由那个线程来完成。】
            在这里插入图片描述
          • 客户端 SocketChannel 的使用:
            在这里插入图片描述
        • 上面这个例子的性能瓶颈或者说问题:非阻塞 IO应运而生
          在这里插入图片描述
      • Unix 常见的五种IO模型之二:同步非阻塞式IO模型(noblocking IO model)NIO一般很少直接使用这种模型,而是在其他 I/O 模型中使用非阻塞 I/O 这一特性。这种方式对单个 I/O 请求意义不大,但给 I/O 多路复用铺平了道路):linux下,可以通过设置socket使其变为non-blockingNIO 的日常操作,这个文章有例子:文件复制、文件复制—映射方式、文件复制—零拷贝方式、
        在这里插入图片描述
        • 把非阻塞的文件描述符称为非阻塞I/O。可以通过设置SOCK_NONBLOCK标记创建非阻塞的socket fd,或者使用fcntl将fd设置为非阻塞。
          • 对非阻塞fd调用系统接口时,不需要等待事件发生而立即返回,事件没有发生,接口返回-1,此时需要通过errno的值来区分是否出错。不同的接口,立即返回时的errno值不尽相同,如,recv、send、accept errno通常被设置为EAGIN 或者EWOULDBLOCK,connect 则为EINPRO- GRESS 。
            在这里插入图片描述
        • NIO 也称新 IO 或者非阻塞 IO(Non-Blocking IO)【NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。】。Java 中的 NIO 于 Java 1.4 中引入,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象传统 IO 是面向输入/输出流编程的,而 NIO 是面向通道编程的,或者说它是支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO 。
        • NIO 的 3 个核心概念或者说Java NIO 中三大组件 Buffer、Channel、Selector:Channel、Buffer、Selector:
          • Channel(通道):
            在这里插入图片描述
            • 所有的 NIO 操作始于通道,通道是数据来源或数据写入的目的地,主要地,我们将关心 java.nio 包中实现的以下几个 Channel:
              在这里插入图片描述
              • FileChannel:
                • FileChannel 是不支持非阻塞的。
                  在这里插入图片描述
              • SocketChannel:
                • 可以将 SocketChannel 理解成一个 TCP 客户端。虽然这么理解有点狭隘,因为我们在介绍 ServerSocketChannel 的时候会看到另一种使用方式。【SocketChannel 了,它不仅仅是 TCP 客户端,它代表的是一个网络通道,可读可写。】
                  //打开一个 TCP 连接:
                  SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("http://aiminhuqaqq.fun/", 80));
                  /**上面的这行代码等价于下面的两行:
                  *打开一个通道:SocketChannel socketChannel = SocketChannel.open();
                  *发起连接:socketChannel.connect(new InetSocketAddress("http://aiminhuqaqq.fun/", 80));
                  */
                  
                • SocketChannel 的读写和 FileChannel 没什么区别,就是操作缓冲区。
                  在这里插入图片描述
              • ServerSocketChannel:
                • SocketChannel 是 TCP 客户端,这里说的 ServerSocketChannel 就是对应的服务端
                • ServerSocketChannel 用于监听机器端口,管理从这个端口进来的 TCP 连接
                  在这里插入图片描述
                • ServerSocketChannel 不和 Buffer 打交道了,因为它并不实际处理数据,它一旦接收到请求后,实例化 SocketChannel,之后在这个连接通道上的数据传递它就不管了,因为它需要继续监听端口,等待下一个连接
              • DatagramChannel:
                • UDP 和 TCP 不一样,DatagramChannel 一个类处理了服务端和客户端。
                • 监听端口:
                  在这里插入图片描述
                • 发送数据:
                  在这里插入图片描述
            • Channel 与Buffer 打交道,读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中
              • channel 实例的两个方法:
                • channel.read(buffer);
                  在这里插入图片描述
                • channel.write(buffer);
                  在这里插入图片描述
            • Channel 是对 IO 输入/输出系统的抽象,是 IO 源与目标之间的连接通道,NIO 的通道类似于传统 IO 中的各种“流”,用于读取和写入。与 InputStream 和 OutputStream 不同的是,Channel 是双向的,既可以读,也可以写,且支持异步操作。这契合了操作系统的特性,比如 linux 底层通道就是双向的。此外 Channel 还提供了 map() 方法,通过该方法可以将“一块”数据直接映射到内存中。因此也有人说,NIO 是面向块处理的,而传统 I/O 是面向流处理的
              在这里插入图片描述
          • Buffer(缓冲):
            在这里插入图片描述
            • Buffer 本质上就是一个容器,其底层持有了一个具体类型的数组来存放具体数据。或者说一个 Buffer 本质上是内存中的一块,我们可以将数据写入这块内存,之后从这块内存获取数据。。从 Channel 中取数据或者向 Channel 中写数据都需要通过 Buffer。在 Java 中 Buffer 是一个抽象类,除 boolean 之外的基本数据类型都提供了对应的 Buffer 实现类。比较常用的是 ByteBuffer 和 CharBuffer
              在这里插入图片描述
            • java.nio定义的几个Buffer的实现:
              在这里插入图片描述
            • Buffer 中的几个重要属性和几个重要方法:就像数组有数组容量,每次访问元素要指定下标,Buffer 中也有几个重要属性:position、limit、capacity。
              在这里插入图片描述
              • position:position 的初始值是 0,每往 Buffer 中写入一个值,position 就自动加 1,代表下一次的写入位置,所以 position 最后会指向最后一次写入的位置的后面一个,如果 Buffer 写满了,那么 position 等于 capacity(position 从 0 开始)。读操作的时候也是类似的,每读一个值,position 就自动加 1
                • 写操作模式到读操作模式切换的时候(flip()),position 都会归零,这样就可以从头开始读写了
                  在这里插入图片描述
                • rewind():会重置 position 为 0,通常用于重新从头读写 Buffer。和rewind相近的有:
                  在这里插入图片描述
                  • clear():有点重置 Buffer 的意思,相当于重新实例化了一样。clear() 方法会重置几个属性,但是我们要看到,clear() 方法并不会将 Buffer 中的数据清空,只不过后续的写入会覆盖掉原来的数据,也就相当于清空了数据了。
                    在这里插入图片描述
                  • compact():和 clear() 一样的是,它们都是在准备往 Buffer 填充新的数据之前调用。compact() 方法有点不一样,调用这个方法以后,会先处理还没有读取的数据,也就是 position 到 limit 之间的数据(还没有读过的数据),先将这些数据移到左边,然后在这个基础上再开始写入。很明显,此时 limit 还是等于 capacity,position 指向原来数据的右边
              • limit:写操作模式下,limit 代表的是最大能写入的数据,这个时候 limit 等于 capacity。写结束后,切换到读模式,此时的 limit 等于 Buffer 中实际的数据大小,因为 Buffer 不一定被写满了
              • capacity:
              • mark:除了 position、limit、capacity 这三个基本的属性外,还有一个常用的属性就是 mark。
                • mark 用于临时保存 position 的值,每次调用 mark() 方法都会将 mark 设值为当前的 position,便于后续需要的时候使用。
                  在这里插入图片描述
                  在这里插入图片描述
            • 初始化 Buffer:
              • 每个 Buffer 实现类都提供了一个静态方法 allocate(int capacity) 帮助我们快速实例化一个 Buffer
                在这里插入图片描述
              • 另外,我们经常使用 wrap 方法来初始化一个 Buffer。
                在这里插入图片描述
            • 填充 Buffer:
              • 各个 Buffer 类都提供了一些 put 方法用于将数据填充到 Buffer 中,如 ByteBuffer 中的几个 put 方法:
                在这里插入图片描述
              • 对于 Buffer 来说,另一个常见的操作中就是,我们要将来自 Channel 的数据填充到 Buffer 中,在系统层面上,这个操作我们称为读操作,因为数据是从外部(文件或网络等)读到内存中
                在这里插入图片描述
              • 提取 Buffer 中的值:
                • 每往 Buffer 中写入一个值,position 就自动加 1,代表下一次的写入位置,所以 position 最后会指向最后一次写入的位置的后面一个,如果 Buffer 写满了,那么 position 等于 capacity(position 从 0 开始)
                  在这里插入图片描述
                • 如果要读 Buffer 中的值,需要切换模式,从写入模式切换到读出模式。注意,通常在说 NIO 的读操作的时候,我们说的是从 Channel 中读数据到 Buffer 中,对应的是对 Buffer 的写入操作
                  在这里插入图片描述
                  在这里插入图片描述
          • Selector:JDK1.4开始引入了NIO类库,主要是使用Selector多路复用器来实现。Selector在Linux等主流操作系统上是通过IO复用Epoll实现的。通过Selector多路复用器,只需要一个线程便可以管理多个客户端连接【非阻塞 IO 的核心在于使用一个 Selector 来管理多个通道,可以是 SocketChannel,也可以是 ServerSocketChannel,将各个通道注册到 Selector 上,指定监听的事件。之后可以只用一个线程来轮询这个 Selector,看看上面是否有通道是准备好的,当通道准备好可读或可写,然后才去开始真正的读写,这样速度就很快了。我们就完全没有必要给每个通道都起一个线程。】。主要点见下述内容。
            在这里插入图片描述
            • Selector 建立在非阻塞的基础之上,大家经常听到的 多路复用 在 Java 世界中指的就是Selector,用于实现一个线程管理多个 Channel
            • NIO 中 Selector 是对底层操作系统实现的一个抽象,管理通道状态其实都是底层系统实现的
              • select:上世纪 80 年代就实现了,它支持注册 FD_SETSIZE(1024) 个 socket,在那个年代肯定是够用的,不过现在嘛,肯定是不行了
              • poll:1997 年,出现了 poll 作为 select 的替代者,最大的区别就是,poll 不再限制 socket 数量
                • select 和 poll 都有一个共同的问题,那就是它们都只会告诉你有几个通道准备好了,但是不会告诉你具体是哪几个通道。所以,一旦知道有通道准备好以后,自己还是需要进行一次扫描,显然这个不太好,通道少的时候还行,一旦通道的数量是几十万个以上的时候,扫描一次的时间都很可观了,时间复杂度 O(n)。所以,后来才催生了以下epoll实现。
              • epoll:2002 年随 Linux 内核 2.5.44 发布,epoll 能直接返回具体的准备好的通道,时间复杂度 O(1)
                • 除了 Linux 中的 epoll,2000 年 FreeBSD 出现了 Kqueue,还有就是,Solaris 中有 /dev/poll。Windows 平台的非阻塞 IO 使用 select,我们也不必觉得 Windows 很落后,在 Windows 中 IOCP 提供的异步 IO 是比较强大的
            • Selector一些基本的接口操作:
              • 首先,我们 开启一个 Selector选择器或者叫多路复用器Selector selector = Selector.open();
              • 将 Channel 注册到 Selector 上。Selector 建立在非阻塞模式之上,所以注册到 Selector 的 Channel 必须要支持非阻塞模式,FileChannel 不支持非阻塞,我们这里讨论最常见的 SocketChannel 和 ServerSocketChannel
                在这里插入图片描述
            • Selector常用的几个方法:
              在这里插入图片描述
        • NIO 网络模型,非阻塞IO,操作上是用一个线程处理多个连接,使得每一个工作线程都可以处理多个客户端的 Socket 请求,这样工作线程的利用率就能得到提升,所需的工作线程数量也随之减少。此时 NIO 的线程模型就变为 1 个工作线程对应多个客户端 Socket 的请求,这就是所谓的 I/O多路复用。使用 I/O 多路复用时,最好搭配非阻塞 I/O 一起使用【多路复用 API 返回的事件并不一定可读写的,如果使用阻塞 I/O, 那么在调用 read/write 时则会发生程序阻塞,因此最好搭配非阻塞 I/O,以便应对极少数的特殊情况】
          在这里插入图片描述
        • JDK1.4开始引入了NIO类库,主要是使用Selector多路复用器来实现。Selector在Linux等主流操作系统上是通过IO复用Epoll实现的
          • 通过Selector多路复用器,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。
            在这里插入图片描述
          • NIO 比 BIO 提高了服务端工作线程的利用率,并增加了一个调度者,来实现 Socket 连接与 Socket 数据读写之间的分离。
          • JDK 1.4提供了对非阻塞I/O (NIO)的支持,JDK1.5_ update10版本使用epoll替代了传统的select/poll,极大地提升了NIO通信的性能。与Socket类和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现,这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式则正好相反。开发人员一般可以根据自己的需要来选择合适的模式,一般来说,低负载、低并发的应用程序可以选择同步阻塞I/O以降低编程复杂度,但是对于高负载、高并发的网络应用,需要使用NIO的非阻塞模式进行开发
            • NIO采用多路复用技术,一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll(0代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端,这确实是个非常巨大的进步。
              在这里插入图片描述
          • NIO的实现流程,类似于Select:【新事件到来的时候,会在Selector上注册标记位,标示可读、可写或者有连接到来。NIO由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程。并且由于线程的节约,连接数大的时候因为线程切换带来的问题也随之解决,进而为处理海量连接提供了可能。】
            • 创建ServerSocketChannel监听客户端连接并绑定监听端口,设置为非阻塞模式
            • 创建Reactor线程,创建多路复用器(Selector)并启动线程
            • 将ServerSocketChannel注册到Reactor线程的Selector上,监听Accept事件
            • Selector在线程run方法中无线循环轮询准备就绪的Key
            • Selector监听到新的客户端接入,处理新的请求,完成TCP三次握手,建立物理连接
            • 将新的客户端连接注册到Selector上,监听读操作,读取客户端发送的网络消息
            • 客户端发送的数据就绪则读取客户端请求,进行处理
          • 既然服务端的工作线程可以服务于多个客户端的连接请求,那么具体由哪个工作线程服务于哪个客户端请求呢
            • 这时就需要一个调度者去监控所有的客户端连接,比如当图中的客户端 A 的输入已经准备好后,就由这个调度者 ,也就是Selector 选择器去通知服务端的工作线程,告诉它们由工作线程 1 去服务于客户端 A 的请求。这种思路就是 NIO 编程模型的基本原理,调度者就是 Selector 选择器【selector的作用就是配合一个线程来管理多个channel,获取这些channel.上发生的事件,这些channel工作在非阻塞模式下,不会让线程吊死在一个channel上。适合连接数特别多,但流量低的场景(low traffic)】
              在这里插入图片描述
              • 升级为线程池版,解决上面问题,阻塞式I/O
                在这里插入图片描述
              • 线程升级为线程池版,线程池再升级为selector版:selector的作用就是配合一个线程来管理多个channel,获取这些channel.上发生的事件,这些channel工作在非阻塞模式下,不会让线程吊死在一个channel上。所以用了selector后防止了线程吊死在同一颗树上。适合连接数特别多,但流量低的场景(low traffic)】
                在这里插入图片描述
        • socket设置为 NONBLOCK(非阻塞)就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误码(EWOULDBLOCK) ,这样请求就不会阻塞
          在这里插入图片描述
          • 当用户进程调用了recvfrom这个系统调用,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个EWOULDBLOCK error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个EWOULDBLOCK error时,它就知道数据还没有准备好,于是它 可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户空间缓冲区,然后返回。可以看到,I/O 操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续轮询,直到数据准备好为止。整个 I/O 请求的过程中,虽然用户线程每次发起 I/O 请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的 CPU 的资源
        • non blocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有:
          • 等待数据准备就绪 (Waiting for the data to be ready) 「这一步是非阻塞的
            在这里插入图片描述
          • 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 「这一步是阻塞的
        • 将BIO那个例子改装为NIO的。
          在这里插入图片描述
          • 客户端 SocketChannel 的使用:
            在这里插入图片描述
      • Unix 常见的五种IO模型之三:IO复用式IO模型(IO multiplexing model):也叫,I/O 多路复用( IO multiplexing):NIO不停问问问,给人烦坏了,把CPU资源也消耗的差不多了,然后神兽继续究极进化,从BIO—>NIO—>多路复用
        在这里插入图片描述
        在这里插入图片描述
        在这里插入图片描述
        • 在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求
          • 与传统的多线程/多进程模型相比,I/O 多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。
        • IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用,相当于IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。【相当于 IO复用模型核心思路:系统给我们提供一类函数(如我们耳濡目染的select、poll、epoll函数),它们可以同时监控多个fd的操作,任何一个返回内核数据就绪,应用进程再发起recvfrom系统调用
          在这里插入图片描述
        • IO多路复用是指 通过一种机制监视多个文件描述符fd【文件描述符在形式上是一个非负整数,实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符】,一旦某个文件描述符fd就绪(一般是读就绪或者写就绪),或者说内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程,进行相应的读写操作
          • IO多路复用模型指的是:使用 单个进程同时处理多个网络连接IO,他的原理就是select、poll、epoll 不断轮询所负责的所有 socket,当某个socket有数据到达了,就通知用户进程该模型的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
            • 多路指的是多个 Socket 连接,就是指多个通道,也就是多个网络连接的 IO
            • 复用指的是复用一个线程,多个通道或者多个网络连接的IO可以注册到或这说复用在一个复用器上
        • I/O多路复用有两种事件触发模式,分别是 边缘触发(edge-triggered,ET)水平触发(level-triggered,LT)。边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数,系统调用也是有一定的开销的的,毕竟也存在上下文的切换。【可以看看腾讯云社区的范蠡老师的epoll LT 模式和 ET 模式详解
          • 使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此 我们程序要保证一次性将内核缓冲区的数据读取完【驿站只发一条短信不会发第二条第三条让你去取快递的模式就叫边缘触发】。
            • 如果使用边缘触发模式,I/O 事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read 和 write)返回错误,错误类型为 EAGAIN 或 EWOULDBLOCK。
          • 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取。如果使用水平触发模式,当内核通知文件描述符可读写时,接下来还可以继续去检测它的状态,看它是否依然可读或可写。所以在收到通知后,没必要一次执行尽可能多的读写操作【如果快递箱发现你的快递没有被取出,它就会不停地发短信通知你,直到你取出了快递,它才消停,这个就是水平触发的方式】
        • Linux 系统中的 select、poll、epoll等系统调用都是 I/O 多路复用的机制。(IO multiplexing就是我们常说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。)
          • select/poll/epol获取网络事件的过程:在获取事件时,先把我们要关心的连接传给内核,再由内核检测
            • 如果没有事件发生,线程只需阻塞在这个系统调用,而无需像线程池那样轮训调用 read 操作来判断是否有数据
            • 如果有事件发生,内核会返回产生了事件的连接,线程就会从阻塞状态返回,然后在用户态中再处理这些连接对应的业务即可
          • select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这些个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程
            • 目前支持 IO 多路复用的系统调用有 select,epoll 等等。select 系统调用,目前几乎在所有的操作系统上都有支持。
              • select 调用 :内核提供的系统调用,select 它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持
                • 应用进程通过调用select函数,可以同时监控多个fd,在select函数监控的fd中,只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时应用进程再发起recvfrom请求去读取数据
              • epoll 调用 :linux 2.6 内核,epoll 属于 select 调用的增强版本,优化了 IO 的执行效率
          • select、poll 和 epoll 之间的区别:(select,poll,epoll 都是 IO 多路复用的机制, select,poll,epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间。)
            • select:【多个网络连接的 IO 可以注册到一个复用器(select)上,当用户进程调用了 select复用器,那么整个进程会被阻塞。同时,内核会“监视”所有 select复用器 负责的 socket,当任何一个 socket 中的数据准备好了,select 复用器就会返回再从内核中拿数据。这个时候用户进程再调用 read 操作,将数据从内核中拷贝到用户进程。用户可以注册多个 socket,然后不断地调用 select 读取被激活的 socket,即可达到在同一个线程内同时处理多个 IO 请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。好比我们去餐厅吃饭,这次我们是几个人一起去的,我们专门留了一个人在餐厅排号等位,其他人就去逛街了,等排号的朋友通知我们可以吃饭了,我们就直接去享用了。】
              在这里插入图片描述
              • 时间复杂度 O(n)。
              • select/poll 只有水平触发模式
              • select 仅仅知道有 I/O 事件发生,但 并不知道是哪几个流,所以 只能无差别轮询所有流,找出能读出数据或者写入数据的流,并对其进行操作。所以 select 具有 O(n) 的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。除了这个,select还有几个缺点:
                在这里插入图片描述
            • poll:因为存在连接数限制,所以后来又提出了poll。与select相比,poll解决了连接数限制问题。但是呢,select和poll一样,还是需要通过遍历文件描述符来获取已经就绪的socket。如果同时连接的大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长,效率也会线性下降
              • 时间复杂度 O(n)
              • select/poll 只有水平触发模式
              • poll 本质上和 select 没有区别,它将用户传入的数组拷贝到内核空间,然后**查询每个 fd 对应的设备状态, 但是它没有最大连接数的限制**,原因是它是基于链表来存储的
            • epoll:为了解决select/poll存在的问题,多路复用模型epoll诞生,epoll采用事件驱动来实现【epoll先通过epoll_ctl()来注册一个fd(文件描述符),一旦基于某个fd就绪时,内核会采用回调机制,迅速激活这个fd,当进程调用epoll_wait()时便得到通知。这里去掉了遍历文件描述符的坑爹操作,而是采用监听事件回调的机制。这就是epoll的亮点。】
              int s = socket(AF_INET, SOCK_STREAM, 0);
              bind(s, ...);
              listen(s, ...)
              
              int epfd = epoll_create(...);//先用e poll_create 创建一个 epoll对象 epfd
              epoll_ctl(epfd, ...); //再通过 epoll_ctl 将所有需要监听的socket添加到epfd中。epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里
              
              while(1) {
                  int n = epoll_wait(...);//最后调用 epoll_wait 等待数据。epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。
                  for(接收到数据的socket){
                      //处理
                  }
              }
              
              • 时间复杂度 O(1)
              • epoll 可以理解为 event poll,不同于忙轮询和无差别轮询epoll 会把哪个流发生了怎样的 I/O 事件通知我们。所以说 epoll 实际上是事件驱动(每个事件关联上 fd)的
                在这里插入图片描述
              • epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。
              • 经典的 C10K 问题:如果服务器的内存只有 2 GB,网卡是千兆的,能支持并发 1 万请求吗【单机同时处理 1 万个请求的问题。】从硬件资源角度看,对于 2GB 内存千兆网卡的服务器,如果每个请求处理占用不到 200KB 的内存和 100Kbit 的网络带宽就可以满足并发 1 万个请求。不过,要想真正实现 C10K 的服务器,要考虑的地方在于服务器的网络 I/O 模型,效率低的模型,会加重系统开销。基于进程或者线程模型的,其实还是有问题的。新到来一个 TCP 连接,就需要分配一个进程或者线程,那么如果要达到 C10K,意味着要一台机器维护 1 万个连接,相当于要维护 1 万个进程/线程,操作系统就算死扛也是扛不住的
        • IO多路复用适用如下场合:
          • 多路复用 IO 是在高并发场景中使用最为广泛的一种 IO 模型,如 Java 的 NIO、Redis、Nginx 的底层实现就是此类 IO 模型的应用,经典的 Reactor 模式也是基于此类 IO 模型
          • 最常用的I/O事件通知机制就是I/O复用(I/O multiplexing)。Linux 环境中使用select/poll/epoll 实现I/O复用,I/O复用接口本身是阻塞的在应用程序中通过I/O复用接口向内核注册fd所关注的事件,当关注事件触发时,通过I/O复用接口的返回值通知到应用程序
            • 以recv为例。I/O复用接口可以同时监听多个I/O事件以提高事件处理效率。
              在这里插入图片描述
          • 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用
          • 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现
          • 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用
          • 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用
          • 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用
          • 与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销
            在这里插入图片描述
            • 当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。这个图和blocking IO的图其实并没有太大的不同,事实上因为IO多路复用多了添加监视 socket,以及调用 select 函数的额外操作,效率更差。还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,使用 select 以后最大的优势是用户可以在一个线程内同时处理多个 socket 的 I/O 请求。用户可以注册多个 socket,然后不断地调用 select 读取被激活的 socket,即可达到在同一个线程内同时处理多个 I/O 请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
            • 因此对于IO多路复用模型来说:
              • 等待数据准备就绪 (Waiting for the data to be ready) 「阻塞」
              • 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 「阻塞」
      • Unix 常见的五种IO模型之四:异步非阻塞 I/O(asynchronous IO)AIO:linux下的asynchronous IO的流程
        在这里插入图片描述
        在这里插入图片描述
        在这里插入图片描述
        在这里插入图片描述
        • POSIX规范定义了一组异步操作I/O的接口,不用关心fd 是阻塞还是非阻塞,异步I/O是由内核接管应用层对fd的I/O操作。异步I/O向应用层通知I/O操作完成的事件,这与前面介绍的I/O 复用模型、SIGIO模型通知事件就绪的方式明显不同。以aio_read 实现异步读取IO数据为例,在等待I/O操作完成期间,不会阻塞应用程序
          在这里插入图片描述
          • 异步其实之前咱们就接触过:通常,我们会有一个线程池用于执行异步任务,提交任务的线程将任务提交到线程池就可以立马返回,不必等到任务真正完成。如果想要知道任务的执行结果,通常是通过传递一个回调函数的方式,任务结束后去调用这个函数。同样的原理,Java 中的异步 IO 也是一样的,都是由一个线程池来负责执行任务,然后使用回调或自己去查询结果异步 IO 主要是为了控制线程数量,减少过多的线程带来的内存消耗和 CPU 在线程调度上的开销
            • 异步 IO 一定存在一个线程池,这个线程池负责接收任务、处理 IO 事件、回调等。这个线程池就在 group 内部【AsynchronousChannelGroup 这个类】,group 一旦关闭,那么相应的线程池就会关闭。AsynchronousServerSocketChannels 和 AsynchronousSocketChannels 是属于 group 的,当我们调用 AsynchronousServerSocketChannel 或 AsynchronousSocketChannel 的 open() 方法的时候,相应的 channel 就属于默认的 group,这个 group 由 JVM 自动构造并管理
              • 配置这个默认的 group,可以在 JVM 启动参数中指定以下系统变量:
                在这里插入图片描述
              • 使用自己定义的 group,这样可以对其中的线程进行更多的控制,使用以下几个方法:
                在这里插入图片描述
              • group 的使用:
                在这里插入图片描述
            • AsynchronousFileChannels 不属于 group。但是它们也是关联到一个线程池的,如果不指定,会使用系统默认的线程池,如果想要使用指定的线程池,可以在实例化的时候使用以下方法:
              在这里插入图片描述
        • AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型。【JDK 7 对原有的 NIO 进行了改进。第一个改进是提供了全面的文件 I/O 相关 API第二个改进是增加了异步的基于 Channel 的 IO 机制
          在这里插入图片描述
          • 异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
            在这里插入图片描述
        • 用户进程发起aio_read调用之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它发现一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了
          • 异步 I/O 模型使用了 Proactor 设计模式实现了这一机制。因此对异步IO模型来说:
            • 等待数据准备就绪 (Waiting for the data to be ready) 「非阻塞」
            • 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process) 「非阻塞」
        • AIO:JDK1.7引入NIO2.0,提供了异步文件通道和异步套接字通道的实现。其底层在Windows上是通过IOCP实现,在Linux上是通过IO复用Epoll来模拟实现的。在JAVA NIO框架中,Selector它负责代替应用查询中所有已注册的通道到操作系统中进行IO事件轮询、管理当前注册的通道集合,定位发生事件的通道等操作。但是在JAVA AIO框架中,由于应用程序不是轮询方式,而是订阅-通知方式,所以不再需要Selector(选择器)了,改由Channel通道直接到操作系统注册监听 。【More New IO,或称 NIO.2,随 JDK 1.7 发布,包括了引入异步 IO 接口和 Paths 等文件访问接口。】
          在这里插入图片描述
          • JAVA AIO框架中,只实现了两种网络IO通道:
            • AsynchronousServerSocketChannel(服务器监听通道)
              • 这个类对应的是非阻塞 IO 的 ServerSocketChannel。
              • 用回调函数的方式写一个简单的服务端:
                在这里插入图片描述
                • ChannelHandler 类
                  在这里插入图片描述
                • 自定义的 Attachment 类:
                  在这里插入图片描述
                • 接下来可以接收客户端请求了
            • AsynchronousSocketChannel(Socket套接字通道)
              • 使用 AsynchronousSocketChannel 的方式和非阻塞 IO 基本类似。
            • AsynchronousFileChannel:异步的文件 IO
              • 文件 IO 在所有的操作系统中都不支持非阻塞模式,但是我们可以对文件 IO 采用异步的方式来提高性能
              • AsynchronousFileChannel 里面的一些重要的接口:
                在这里插入图片描述
                在这里插入图片描述
                在这里插入图片描述
          • Java 异步 IO 提供了两种使用方式,分别是 返回 java.util.concurrent.Future 实例和使用CompletionHandler 回调函数
            • 返回 java.util.concurrent.Future 实例:JDK 线程池就是这么使用的
              • Future 接口的几个方法语义:
                在这里插入图片描述
            • 提供 CompletionHandler 回调函数:
              • java.nio.channels.CompletionHandler 接口定义:
                在这里插入图片描述
        • 异步 I/O 并没有涉及到 PageCache,所以使用异步 I/O 就意味着要绕开 PageCache。绕开 PageCache 的 I/O 叫 直接 I/O使用 PageCache 的 I/O 则叫缓存 I/O。通常,对于磁盘,异步 I/O 只支持直接 I/O在高并发的场景下,针对大文件的传输的方式,应该使用「异步 I/O + 直接 I/O」来替代零拷贝技术就可以无阻塞地读取文件了
          在这里插入图片描述
          • 传输文件的时候,我们要根据文件的大小来使用不同的方式:
            • 传输 大文件 的时候,使用「异步 I/O + 直接 I/O
            • 传输小文件的时候,则使用「零拷贝技术
      • Unix 常见的五种IO模型之五:信号驱动式IO模型(signal-driven IO model)
        在这里插入图片描述
        • 除了I/O复用方式通知I/O事件,还可以通过SIGIO信号来通知I/O事件。两者不同的是,在等待数据达到期间,I/O复用是会阻塞应用程序,而SIGIO方式是不会阻塞应用程序的
          在这里插入图片描述
        • 首先我们允许 socket 进行信号驱动 I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用 I/O 操作函数处理数据。
          在这里插入图片描述
  • 零拷贝技术:上面咱们看到了 应用进程的一次完整的读写操作,都需要在用户空间与内核空间中来回拷贝,并且每一次拷贝,都需要 CPU 进行一次上下文切换(由用户进程切换到系统内核,或由系统内核切换到用户进程),这样是不是很浪费 CPU 和性能呢?那有没有什么方式,可以减少进程间的数据拷贝,提高数据传输的效率呢?========零拷贝技术零拷贝是指计算机执行IO操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,从而可以减少上下文切换以及CPU的拷贝时间。它是一种I/O操作优化技术。取消用户空间与内核空间之间的数据拷贝操作,应用进程每一次的读写操作,都可以通过一种方式,让应用进程直接向用户空间写入或者读取数据,(效果就如同直接向内核空间写入或者读取数据一样,然后再通过 DMA 将内核中的数据拷贝到网卡,或将网卡中的数据 copy 到内核)】
    在这里插入图片描述

    • 传统IO的读写流程,包括了4次上下文切换(4次用户态和内核态的切换),4次数据拷贝(两次CPU拷贝以及两次的DMA拷贝)。零拷贝只是减少了用户态/内核态的切换次数以及CPU拷贝的次数
      • 升级到epoll后,为了沟通有没有数据还是得用户态内核态把fd相关数据拷来拷去,所以为了减少拷贝的次数,并且用mmap这个系统调用实现了一个用户态和内核态共享的空间,
    • 是不是用户空间与内核空间都将数据写到一个地方,就不需要拷贝了?此时有没有想到虚拟内存?零拷贝有两种解决方式,分别是 mmap+write 方式和 sendfile 方式,mmap+write 方式的核心原理就是通过虚拟内存来解决的
    • 实现零拷贝的两种方式:
      • mmap + write:
        • mmap就是用了虚拟内存这个特点,mmap将内核中的读缓冲区与用户空间的缓冲区进行映射,以减少数据拷贝次数!
          在这里插入图片描述
          在这里插入图片描述
          • mmap+write实现的零拷贝,I/O发生了4次用户空间与内核空间的上下文切换,以及3次数据拷贝(包括了2次DMA拷贝和1次CPU拷贝)
            在这里插入图片描述
        • read() 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,于是为了减少这一步开销,我们可以用 mmap() 替换 read() 系统调用函数。mmap() 系统调用函数会直接把内核缓冲区里的数据映射到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作
          在这里插入图片描述
      • sendfile
        • 在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile()ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。首先,sendfile可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。其次,该sendfile系统调用和mmap一样,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次上下文切换,和 3 次数据拷贝
          在这里插入图片描述
          • sendfile实现的零拷贝,I/O发生了2次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中和mmap+write一样,包括了2次DMA拷贝和1次CPU拷贝。
            在这里插入图片描述
          • 带有DMA收集拷贝功能的sendfile:但是这还不是真正的零拷贝技术,如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术(和普通的 DMA 有所不同),我们可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程。【linux 2.4版本之后,对sendfile做了优化升级,引入SG-DMA技术,SG-DMA技术其实就是对DMA拷贝加入了scatter/gather操作,它可以直接从内核空间缓冲区中将数据读取到网卡。使用这个特点搞零拷贝,即还可以多省去一次CPU拷贝。】
            • sendfile+DMA scatter/gather实现的零拷贝,I/O发生了2次用户空间与内核空间的上下文切换,以及2次数据拷贝。其中2次数据拷贝都是包DMA拷贝。这就是真正的 零拷贝(Zero-copy) 技术,全程都没有通过CPU来搬运数据,所有的数据都是通过DMA来进行传输的
              在这里插入图片描述
            • 从 Linux 内核 2.4 版本开始起,对于支持网卡支持 SG-DMA 技术的情况下, sendfile() 系统调用的过程发生了点变化:采用了零拷贝
              在这里插入图片描述
        • Kafka 这个开源项目,就利用了零拷贝技术,从而大幅提升了 I/O 的吞吐率,这也是 Kafka 在处理海量数据为什么这么快的原因之一,它调用了 Java NIO 库里的 transferTo 方法。如果 Linux 系统支持 sendfile() 系统调用,那么 transferTo() 实际上最后就会使用到 sendfile() 系统调用函数。
        • Nginx 也支持零拷贝技术,一般默认是开启零拷贝技术,这样有利于提高文件传输的效率
    • 零拷贝更细致的理论点这里,然后ctrl+F,搜零拷贝
      • 这个零拷贝是操作系统层面上的零拷贝,主要目标是避免用户空间与内核空间之间的数据拷贝操作,可以提升 CPU 的利用率。
      • Netty 相关的零拷贝技术
    • RPC 框架在网络通信框架的选型上,我们最优的选择是基于 Reactor 模式实现的框架,如 Java 语言,首选的便是 Netty 框架
      • Netty 的零拷贝则不大一样,他完全站在了用户空间上,也就是 JVM 上,Netty 的零拷贝主要是偏向于数据操作的优化上
        • 在传输过程中,RPC 并不会把请求参数的所有二进制数据整体一下子发送到对端机器上,中间可能会拆分成好几个数据包,也可能会合并其他请求的数据包,所以消息都需要有边界。那么一端的机器收到消息之后,就需要对数据包进行处理,根据边界对数据包进行分割和合并,最终获得一条完整的消息。收到消息后,对数据包的分割和合并是在用户空间【因为对数据包的处理工作都是由应用程序来处理的】。这里也是会存在拷贝操作的,但是 不是在用户空间与内核空间之间的拷贝,是用户空间内部内存中的拷贝处理操作。Netty 的零拷贝就是为了解决这个问题,在用户空间对数据操作进行优化
    • Netty 是怎么对数据操作进行优化的呢?
      • Netty 提供了 CompositeByteBuf 类,它可以 将多个 ByteBuf 合并为一个逻辑上的 ByteBuf,避免了各个 ByteBuf 之间的拷贝【ByteBuf 支持 slice 操作,因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf,避免了内存的拷贝】
      • 通过 wrap 操作,我们可以将 byte[] 数组、ByteBuf、ByteBuffer 等包装成一个 Netty ByteBuf 对象, 进而避免拷贝操作。
    • Netty 框架中很多内部的 ChannelHandler 实现类,都是通过 CompositeByteBuf、slice、wrap 操作来处理 TCP 传输中的拆包与粘包问题的。etty 的 ByteBuffer 可以采用 Direct Buffers,使用堆外直接内存进行 Socket 的读写操作,最终的效果与虚拟内存所实现的效果是一样的。Netty 还提供 FileRegion 中包装 NIO 的 FileChannel.transferTo() 方法实现了零拷贝,这与 Linux 中的 sendfile 方式在原理上也是一样的
  • 除了上面基本的I/O模型之外,还有如下集中模型:

    • 事件处理模型Reactor 和Proactor两种事件处理模型
      • 网络设计模式中,如何处理各种I/O事件是其非常重要的一部分,Reactor 和Proactor两种事件处理模型应运而生。上面提到将I/O分为同步I/O 和 异步I/O,可以使用同步I/O实现Reactor模型,使用异步I/O实现Proactor模型
      • Reactor事件处理模型:Reactor模型是同步I/O事件处理的一种常见模型
        • 一个典型的Reactor模型类图结构
          在这里插入图片描述
        • Reactor的核心思想:将关注的I/O事件注册到多路复用器上,一旦有I/O事件触发,将事件分发到事件处理器中、执行就绪I/O事件对应的处理函数中。模型中有三个重要的组件:
          • 多路复用器:由操作系统提供接口,Linux提供的I/O复用接口有select、poll、epoll;
          • 事件分离器:将多路复用器返回的就绪事件分发到事件处理器中;
          • 事件处理器:处理就绪事件处理函数
        • Reactor模型工作的简化流程:
          在这里插入图片描述
      • Proactor事件处理模型:
        • 与Reactor不同的是,Proactor使用异步I/O系统接口将I/O操作托管给操作系统Proactor模型中分发处理异步I/O完成事件,并调用相应的事件处理接口来处理业务逻辑
        • Proactor类结构:
          在这里插入图片描述
        • Proactor模型的简化的工作流程:
          在这里插入图片描述
        • 同步I/O模拟Proactor:
          在这里插入图片描述
    • 并发模式:多线程、多进程的编程的模式【多进程/线程模型】
      • 在I/O密集型的程序,采用并发方式可以提高CPU的使用率可采用多进程和多线程两种方式实现并发。当前有高效的两种并发模式,半同步/半异步模式、Follower/Leader模式
        • 多进程/多线程模型:
          在这里插入图片描述
          • 通过这种方式确实解决了单进程 server 阻塞无法处理其他 client 请求的问题,但众所周知 fork 创建子进程是非常耗时的,包括页表的复制,进程切换时页表的切换等都非常耗时,每来一个请求就创建一个进程显然是无法接受的。为了节省进程创建的开销,于是有人提出把多进程改成多线程,创建线程(使用 pthread_create)的开销确实小了很多,但同样的,线程与进程一样,都需要占用堆栈等资源,而且碰到阻塞,唤醒等都涉及到用户态,内核态的切换,这些都极大地消耗了性能
      • 半同步/半异步模式:
        • 并发模式中的“同步”、“异步”与 I/O模型中的“同步”、“异步”是两个不同的概念:
          • 并发模式中,“同步”指程序按照代码顺序执行,“异步”指程序依赖事件驱动,如图12 所示并发模式的“同步”执行和“异步”执行的读操作;
            • 同步读操作示意图
              在这里插入图片描述
            • 异步读操作示意图
              在这里插入图片描述
          • I/O模型中,“同步”、“异步”用来区分I/O操作的方式是主动通过I/O操作拿到结果,还是由内核异步的返回操作结果
        • 半同步/半异步工作流程
          在这里插入图片描述
        • 半同步/半反应堆模式
          • 考虑将两种事件处理模型,即Reactor和Proactor,与几种I/O模型结合在一起,那么半同步/半异步模式就演变为半同步/半反应堆模式
            在这里插入图片描述
          • 使用Reactor的方式:
            • 工作流程:
              在这里插入图片描述
          • 将Reactor替换为Proactor
            • 工作流程:
              在这里插入图片描述
          • 半同步/半反应堆模式有明显的缺点:
            在这里插入图片描述
          • 半同步/半反应堆模式的演变模式:
            在这里插入图片描述
      • Follower/Leader模式
        • Follower/Leader是多个工作线程轮流进行事件监听、事件分发、处理事件的模式。在Follower/Leader模式工作的任何一个时间点,只有一个工作线程处理成为Leader ,负责I/O事件监听,而其他线程都是Follower,并等待成为Leader
          在这里插入图片描述
          在这里插入图片描述
        • Leader/Follow模式的工作线程的三种状态的转移关系
          在这里插入图片描述
    • Swoole异步网络模型分析

巨人的肩膀:
Linux网络编程
B站OS课程各位老师
操作系统概论
https://xiaolincoding.com/
很好的一篇文章,既讲了Java 中 IO 相关的理论知识,并通过多个代码案例加深了理解。很赞
https://learn.lianglianglee.com/%E6%96%87%E7%AB%A0/Java%20NIO%E6%B5%85%E6%9E%90.md
Javadoop
javaGuide
CS-Note
清华大学OS课
在 Windows 操作系统中,提供了一个叫做 I/O Completion Ports 的方案,通常简称为 IOCP,操作系统负责管理线程池,其性能非常优异,所以在 Windows 中 JDK 直接采用了 IOCP 的支持,使用系统支持,把更多的操作信息暴露给操作系统,也使得操作系统能够对我们的 IO 进行一定程度的优化。

程序员田螺老师的零拷贝详解

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值