当前位置:网站首页 > Java基础 > 正文

java 网络编程基础



一. NIO 基础

哔哩哔哩黑马程序员 netty实战视频

0.什么是nio?

NIO(New I/O)是Java中提供的一种基于通道和缓冲区的I/O(Input/Output)模型。它是相对于传统的IO(InputStream和OutputStream)模型而言的新型I/O模型。NIO的主要特点包括:

1.通道与缓冲区:

2.NIO引入了通道(Channel)和缓冲区(Buffer)的概念。通道是对传统IO中的流的抽象,它可以支持双向数据传输。而缓冲区则是存储数据的地方,数据在通道和缓冲区之间传递。

3.非阻塞IO:

4.NIO提供了非阻塞I/O操作的能力。在传统IO模型中,当一个线程在读取或写入数据时会被阻塞,而在NIO中,线程可以继续处理其他任务,而不必等待数据的读取或写入完成。

5.选择器(Selector):

6.NIO引入了选择器的概念,使得一个线程可以同时监控多个通道的IO事件。这样,一个线程可以有效地管理多个通道,从而提高系统的性能。

7.面向缓冲区的操作:

8.NIO中的数据读取和写入都是面向缓冲区的。数据首先被读取到缓冲区,然后再从缓冲区写入到通道,或者从通道读取到缓冲区。这种方式相对于直接流式IO更加灵活,可以更好地处理不同大小的数据块。

9.异步IO:

10.Java NIO提供了异步I/O操作的支持。通过使用Future、Callback等机制,可以实现异步的IO操作,使得程序可以在数据读取或写入的同时执行其他任务。

11.多路复用:

12.多路复用是NIO的一个重要特性,通过选择器可以实现同时管理多个通道的IO操作。这在高并发的网络应用中特别有用。

1. 三大组件

1.1 Channel & Buffer

channel 有一点类似于 stream,它就是读写数据的双向通道,可以从 channel 将数据读入 buffer,也可以将 buffer 的数据写入 channel,而之前的 stream 要么是输入,要么是输出,channel 比 stream 更为底层

channel
buffer

常见的 Channel 有

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

buffer 则用来缓冲读写数据,常见的 buffer 有

  • ByteBuffer
    • MappedByteBuffer
    • DirectByteBuffer
    • HeapByteBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  • CharBuffer

1.2 Selector

selector 单从字面意思不好理解,需要结合服务器的设计演化来理解它的用途

多线程版设计
多线程版
socket1
thread
socket2
thread
socket3
thread
⚠️ 多线程版缺点
  • 内存占用高
  • 线程上下文切换成本高 #cpu的线程是固定的,如果无限制增加线程处理请求会导致线程堵塞
  • 只适合连接数少的场景
线程池版设计
线程池版
socket1
thread
socket2
thread
socket3
socket4
⚠️ 线程池版缺点
  • 阻塞模式下,线程仅能处理一个 socket 连接 //线程池中线程固定
  • 仅适合短连接场景 //http
selector 版设计

selector 的作用就是配合一个线程来管理java 网络编程基础多个 channel,获取这些 channel 上发生的事件,这些 channel 工作在非阻塞模式下(不会因为单个线程的堵塞而失去效果),不会让线程吊死在一个 channel 上。适合连接数特别多,但流量低的场景(low traffic)

selector 版
selector
thread
channel
channel
channel

调用 selector 的 select() 会阻塞直到 channel 发生了读写就绪事件,这些事件发生,select 方法就会返回这些事件交给 thread 来处理

2. ByteBuffer

有一普通文本文件 data.txt,内容为

 

使用 FileChannel 来读取文件内容

 

输出

 

2.1 ByteBuffer 正确使用姿势

  1. 向 buffer 写入数据,例如调用 channel.read(buffer)
  2. 调用 flip() 切换至读模式
  3. 从 buffer 读取数据,例如调用 buffer.get()
  4. 调用 clear() 或 compact() 切换至写模式
  5. 重复 1~4 步骤

    是 类中的一个方法,它用于将缓冲区的当前读取位置设置为缓冲区的开始位置,并将缓冲区的大小调整为等于缓冲区剩余的字节数。

    在将缓冲区中的数据写入网络或文件之前,通常需要调用 方法,以便将缓冲区中的数据移动到读取位置。这样,下一次从缓冲区中读取数据时,就可以从缓冲区的开始位置开始读取

和 分别是 和 两个类。

类是 NIO 中的核心接口,它表示一个通信通道,如套接字、文件描述符等。它提供了一种将字节数据从一个地方读取到另一个地方的方法,可以用于网络编程、文件读写等场景。

类是 NIO 中的缓冲区类,它提供了一种高效的字节读写操作。它允许将字节数据从一个地方读取到另一个地方,同时提供了一些方法来改变当前读取和写入的位置,以及调整缓冲区的大小等。

语句表示从指定的 中读取数据到 中。 会尝试读取数据到 中,直到 中的字节数达到或超过 或者读取到 EOF(表示读取到了流的末尾)。如果读取到了 EOF,则返回 -1,否则返回读取到的字节数。

这个语句的作用是将 中的数据读取到 中,从而可以对读取到的数据进行处理。如果读取到了 EOF,说明已经读取到了流的末尾,可以关闭 和 ,或者重新设置 的位置,以便读取下一部分数据。

2.2 ByteBuffer 结构

ByteBuffer 有以下重要属性

  • capacity
  • position
  • limit

一开始

在这里插入图片描述

写模式下,position 是写入位置,limit 等于容量,下图表示写入了 4 个字节后的状态
在这里插入图片描述

flip 动作发生后,position 切换为读取位置,limit 切换为读取限制

在这里插入图片描述

读取 4 个字节后,状态
在这里插入图片描述

clear 动作发生后,状态
在这里插入图片描述

compact 方法,是把未读完的部分向前压缩,然后切换至写模式

在这里插入图片描述

💡 调试工具类
 

演示工具类 (需要导入netty依赖)

 

申明buffer10byte的缓冲空间 分别存入 a b c d 后的缓存区变化 上面是索引 下面是值

 

在这里插入图片描述

读取缓存的字符 读取之前需要使用flip api将position 当前位置指针移到队列头(保存完数据后 读取的开始指针在当前数据末尾加一 无法读取数据)

 

从头开始读取转换到10进制,position读取后指针向后加一

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用compact将为读完的数据想前压缩,指针也跟着向前

 

读取了俩次就是是说position后移到第2个位置63时候,使用compact api把数据和指针移动到首位,此时再插入数据,便拼接在63之后

在这里插入图片描述

2.3 ByteBuffer 常见方法

分配空间

可以使用 allocate 方法为 ByteBuffer 分配空间,其它 buffer 类也有该方法

 

分配空间的api 有以下俩种

 
向 buffer 写入数据

有两种办法

  • 调用 channel 的 read 方法
  • 调用 buffer 自己的 put 方法
 

 
从 buffer 读取数据

同样有两种办法

  • 调用 channel 的 write 方法
  • 调用 buffer 自己的 get 方法
 

 

get 方法会让 position 读指针向后走,如果想重复读取数据

  • 可以调用 rewind 方法将 position 重新置为 0
  • 或者调用 get(int i) 方法获取索引 i 的内容,它不会移动读指针

此时又从(0)第一个数据开始读取

 

在这里插入图片描述

源码
在这里插入图片描述

mark 和 reset

mark 是在读取时,做一个标记,即使 position 改变,只要调用 reset 就能回到 mark 的位置(中间位置需要返复读取)

注意

rewind 和 flip 都会清除 mark 位置

 

输出重复读取俩次cd

在这里插入图片描述

get(i) 根据在buffer中的索引读取 并且不会改变position

 
字符串与 ByteBuffer 互转
 

utif-8的编码

在这里插入图片描述

 

输出

 

值得注意的是使用byte转字符串时候,如果不是使用字节编码类encode给到的buffer,不会自动转position,需要手动调用flip(),不然解答码失败

在这里插入图片描述

⚠️ Buffer 的线程安全

Buffer 是非线程安全的

2.4 Scattering Reads

分散读取,有一个文本文件 3parts.txt

 

使用如下方式读取,可以将数据填充至多个 buffer

 

结果

 

2.5 Gathering Writes

使用如下方式写入,可以将多个 buffer 的数据填充至 channel

 

输出

 

文件内容

 

2.6 练习

  • Hello,world
  • I’m zhangsan
  • How are you?

变成了下面的两个 byteBuffer (黏包,半包)

  • Hello,world I’m zhangsan Ho
  • w are you?

现在要求你编写程序,将错乱的数据恢复成原始的按 分隔的数据

 

3. 文件编程

3.1 FileChannel

⚠️ FileChannel 工作模式

FileChannel 只能工作在阻塞模式下

获取

不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法

  • 通过 FileInputStream 获取的 channel 只能读
  • 通过 FileOutputStream 获取的 channel 只能写
  • 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定
读取

会从 channel 读取数据填充 ByteBuffer,返回值表示读到了多少字节,-1 表示到达了文件的末尾

 
写入

写入的正确姿势如下, SocketChannel

 

在 while 中调用 channel.write 是因为 write 方法并不能保证一次将 buffer 中的内容全部写入 channel

关闭

channel 必须关闭,不过调用了 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 close 方法会间接地调用 channel 的 close 方法

位置

获取当前位置

 

设置当前位置

 

设置当前位置时,如果设置为文件的末尾

  • 这时读取会返回 -1
  • 这时写入,会追加内容,但要注意如果 position 超过了文件末尾,再写入时在新内容和原末尾之间会有空洞(00)
大小

使用 size 方法获取文件的大小

强制写入

操作系统出于性能的考虑,会将数据缓存,不是立刻写入磁盘。可以调用 force(true) 方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘

3.2 两个 Channel 传输数据

 

输出

 
 

超过 2g 大小的文件传输 文件大小限制到了2g

 

实际传输一个超大文件

 

3.3 Path

jdk7 引入了 Path 和 Paths 类

  • Path 用来表示文件路径
  • Paths 是工具类,用来获取 Path 实例
 
  • 代表了当前路径
  • 代表了上一级路径

例如目录结构如下

 

代码

 

会输出

 

3.4 Files

检查文件是否存在

 

创建一级目录

 
  • 如果目录已存在,会抛异常 FileAlreadyExistsException
  • 不能一次创建多级目录,否则会抛异常 NoSuchFileException

创建多级目录用

 

拷贝文件

 
  • 如果文件已存在,会抛异常 FileAlreadyExistsException

如果希望用 source 覆盖掉 target,需要用 StandardCopyOption 来控制

 

移动文件

 
  • StandardCopyOption.ATOMIC_MOVE 保证文件移动的原子性

删除文件

 
  • 如果文件不存在,会抛异常 NoSuchFileException

删除目录

 
  • 如果目录还有内容,会抛异常 DirectoryNotEmptyException

遍历目录文件

 

统计 jar 的数目

 

删除多级目录

 
⚠️ 删除很危险

删除是危险操作,确保要递归删除的文件夹没有重要内容

拷贝多级目录

 

4. 网络编程

服务器和客户端之间建立通道 nio通道俩边都可以读取,所以服务器的接收客户端的请求后 俩边都可以对通道进行操作 io

4.1 非阻塞 vs 阻塞

阻塞
  • 阻塞模式下,相关方法都会导致线程暂停
    • ServerSocketChannel.accept 会在没有连接建立时让线程暂停
    • SocketChannel.read 会在没有数据可读时让线程暂停
    • 阻塞的表现其实就是线程暂停了,暂停期间不会占用 cpu,但线程相当于闲置
  • 单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持
  • 但多线程下,有新的问题,体现在以下方面
    • 32 位 jvm 一个线程 320k,64 位 jvm 一个线程 1024k,如果连接数过多,必然导致 OOM,并且线程太多,反而会因为频繁上下文切换导致性能降低
    • 可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多连接建立,但长时间 inactive,会阻塞线程池中所有线程,因此不适合长连接,只适合短连接

服务器端

 

客户端

 
非阻塞
  • 非阻塞模式下,相关方法都会不会让线程暂停
    • 在 ServerSocketChannel.accept 在没有连接建立时,会返回 null,继续运行
    • SocketChannel.read 在没有数据可读时,会返回 0,但线程不必阻塞,可以去执行其它 SocketChannel 的 read 或是去执行 ServerSocketChannel.accept
    • 写数据时,线程只是等待数据写入 Channel 即可,无需等 Channel 通过网络把数据发送出去
  • 但非阻塞模式下,即使没有连接建立,和可读数据,线程仍然在不断运行,白白浪费了 cpu
  • 数据复制过程中,线程实际还是阻塞的(AIO 改进的地方)

服务器端,客户端代码不变

 
多路复用

单线程可以配合 Selector 完成对多个 Channel 可读写事件的监控,这称之为多路复用

  • 多路复用仅针对网络 IO、普通文件 IO 没法利用多路复用
  • 如果不用 Selector 的非阻塞模式,线程大部分时间都在做无用功,而 Selector 能够保证
    • 有可连接事件时才去连接
    • 有可读事件才去读取
    • 有可写事件才去写入
      • 限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件

4.2 Selector

selector 版
selector
thread
channel
channel
channel

好处

  • 一个线程配合 selector 就可以监控多个 channel 的事件,事件发生线程才去处理。避免非阻塞模式下所做无用功
  • 让这个线程能够被充分利用
  • 节约了线程的数量
  • 减少了线程上下文切换
创建
 
绑定 Channel 事件

也称之为注册事件,绑定的事件 selector 才会关心

 
  • channel 必须工作在非阻塞模式
  • FileChannel 没有非阻塞模式,因此不能配合 selector 一起使用
  • 绑定的事件类型可以有
    • connect - 客户端连接成功时触发
    • accept - 服务器端成功接受连接时触发
    • read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况
    • write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况
监听 Channel 事件

可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少 channel 发生了事件

方法1,阻塞直到绑定事件发生

 

方法2,阻塞直到绑定事件发生,或是超时(时间单位为 ms)

 

方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件

 
 

通道可以处理接收请求,i并且读取触发的是同一个channel
在这里插入图片描述

💡 select 何时不阻塞
  • 事件发生时
    • 客户端发起连接请求,会触发 accept 事件
    • 客户端发送数据过来,客户端正常、异常关闭时,都会触发 read 事件,另外如果发送的数据大于 buffer 缓冲区,会触发多次读取事件
    • channel 可写,会触发 write 事件
    • 在 linux 下 nio bug 发生时
  • 调用 selector.wakeup()
  • 调用 selector.close()
  • selector 所在线程 interrupt

4.3 处理 accept 事件

客户端代码为

 

服务器端代码为

 
💡 事件发生后能否不处理

事件发生后,要么处理,要么取消(cancel),不能什么都不做,否则下次该事件仍会触发,这是因为 nio 底层使用的是水平触发

ServerSocketChannel 是 Java NIO 中用于服务器端的通道,它能够监听传入的 TCP 连接请求,并创建对应的 SocketChannel 与客户端进行通信。在使用 ServerSocketChannel 时,accept() 方法是一个关键的方法。
当调用 ServerSocketChannel 的 accept() 方法时,它会阻塞当前线程,直到有客户端连接进来。一旦有连接请求到达,accept() 方法将返回一个新的 SocketChannel 实例,代表与客户端的连接。
在上面提到的代码示例中,存在以下这段代码:
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
try {
channel.accept();
} catch (IOException e) {
throw new RuntimeException(e);
}

4.4 处理 read 事件

 

开启两个客户端,修改一下发送文字,输出

 
💡 为何要 iter.remove()

因为 select 在事件发生后,就会将相关的 key 放入 selectedKeys 集合,但不会在处理完后从 selectedKeys 集合中移除,需要我们自己编码删除。例如

  • 第一次触发了 ssckey 上的 accept 事件,没有移除 ssckey
  • 第二次触发了 sckey 上的 read 事件,但这时 selectedKeys 中还有上次的 ssckey ,在处理时因为没有真正的 serverSocket 连上了,就会导致空指针异常
💡 cancel 的作用

cancel 会取消注册在 selector 上的 channel,并从 keys 集合中删除 key 后续不会再监听事件

⚠️ 不处理边界的问题

以前有同学写过这样的代码,思考注释中两个问题,以 bio 为例,其实 nio 道理是一样的

 

客户端

 

输出

 

为什么?

字符编码问题,中文超过4个字节

处理消息的边界

在这里插入图片描述

  • 一种思路是固定消息长度,数据包大小一样,服务器按预定长度读取,缺点是浪费带宽
  • 另一种思路是按分隔符拆分,缺点是效率低
  • TLV 格式,即 Type 类型、Length 长度、Value 数据,类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer,缺点是 buffer 需要提前分配,如果内容过大,则影响 server 吞吐量
    • Http 1.1 是 TLV 格式
    • Http 2.0 是 LTV 格式
客户端1 服务器 ByteBuffer1 ByteBuffer2 发送 0abcdef3333 第一次 read 存入 0abcdef 扩容 拷贝 0abcdef 第二次 read 存入 3333 0abcdef3333 客户端1 服务器 ByteBuffer1 ByteBuffer2

服务器端

 

客户端

 
ByteBuffer 大小分配
  • 每个 channel 都需要记录可能被切分的消息,因为 ByteBuffer 不能被多个 channel 共同使用,因此需要为每个 channel 维护一个独立的 ByteBuffer
  • ByteBuffer 不能太大,比如一个 ByteBuffer 1Mb 的话,要支持百万连接就要 1Tb 内存,因此需要设计大小可变的 ByteBuffer
    • 一种思路是首先分配一个较小的 buffer,例如 4k,如果发现数据不够,再分配 8k 的 buffer,将 4k buffer 内容拷贝至 8k buffer,优点是消息连续容易处理,缺点是数据拷贝耗费性能,参考实现 http://tutorials.jenkov.com/java-performance/resizable-array.html
    • 另一种思路是用多个数组组成 buffer,一个数组不够,把多出来的内容写入新的数组,与前面的区别是消息存储不连续解析复杂,优点是避免了拷贝引起的性能损耗

4.5 处理 write 事件

一次无法写完例子
  • 非阻塞模式下,无法保证把 buffer 中所有数据都写入 channel,因此需要追踪 write 方法的返回值(代表实际写入字节数)
  • 用 selector 监听所有 channel 的可写事件,每个 channel 都需要一个 key 来跟踪 buffer,但这样又会导致占用内存过多,就有两阶段策略
    • 当消息处理器第一次写入消息时,才将 channel 注册到 selector 上
    • selector 检查 channel 上的可写事件,如果所有的数据写完了,就取消 channel 的注册
    • 如果不取消,会每次可写均会触发 write 事件

模拟服务器给接收请求的客户端发送大量数据

 

当发送的数据量过大,网络通道缓冲区可能就无法一次性携带所有数据 但是一致轮询执行写入操作 造成多次无效写入

在这里插入图片描述

这样并不符合nio思想 已经会有线程堵塞 进行优化 当缓冲区满的时候进行其他操作

 

此时可以一快速通道 没有缓存区满 无效写入的情况

在这里插入图片描述

客户端

 
💡 write 为何要取消

只要向 channel 发送数据时,socket 缓冲可写,这个事件会频繁触发,因此应当只在 socket 缓冲区写不下时再关注可写事件,数据写完之后再取消关注

4.6 更进一步

//也是netty的核心逻辑

💡 利用多线程优化

现在都是多核 cpu,设计时要充分考虑别让 cpu 的力量被白白浪费

前面的代码只有一个选择器,没有充分利用多核 cpu,如何改进呢?

分两组选择器

  • 单线程配一个选择器,专门处理 accept 事件
  • 创建 cpu 核心数的线程,每个线程配一个选择器,轮流处理 read 事件
 
💡 如何拿到 cpu 个数
  • Runtime.getRuntime().availableProcessors() 如果工作在 docker 容器下,因为容器不是物理隔离的,会拿到物理 cpu 个数,而不是容器申请时的个数
  • 这个问题直到 jdk 10 才修复,使用 jvm 参数 UseContainerSupport 配置, 默认开启

4.7 UDP

  • UDP 是无连接的,client 发送数据不会管 server 是否开启
  • server 这边的 receive 方法会将接收到的数据存入 byte buffer,但如果数据报文超过 buffer 大小,多出来的数据会被默默抛弃

首先启动服务器端

 

输出

 

运行客户端

 

接下来服务器端输出

 

5. NIO vs BIO

5.1 stream vs channel

  • stream 不会自动缓冲数据,channel 会利用系统提供的发送缓冲区、接收缓冲区(更为底层)
  • stream 仅支持阻塞 API,channel 同时支持阻塞、非阻塞 API,网络 channel 可配合 selector 实现多路复用
  • 二者均为全双工,即读写可以同时进行

5.2 IO 模型

同步阻塞、同步非阻塞、同步多路复用、异步阻塞(没有此情况)、异步非阻塞

  • 同步:线程自己去获取结果(一个线程)
  • 异步:线程自己不去获取结果,而是由其它线程送结果(至少两个线程)

在计算机网络编程中,I/O(输入/输出)模型是处理网络请求的关键部分。理解不同类型的I/O模型有助于优化程序性能。这里我将解释五种常见的I/O模型:同步阻塞、同步非阻塞、同步多路复用、异步阻塞(虽然这种模型实际上并不存在)和异步非阻塞。

1.同步阻塞(Blocking Synchronous):

4.同步非阻塞(Non-Blocking Synchronous):

7.同步多路复用(Synchronous Multiplexing):

10.异步阻塞(Blocking Asynchronous):

11.解释:实际上,这种模型是不存在的,因为“异步”与“阻塞”是相互矛盾的概念。异步意味着你不需要等待操作完成,而阻塞则意味着你在等待。

12.异步非阻塞(Non-Blocking Asynchronous):

同步类似单线程,异步类似并发同时执行

同步阻塞、同步非阻塞、同步多路复用都属于同步,非阻塞io本质上是通过单线任务执行,有在这种模式下,当一个I/O请求发起时,如果数据不可用,系统调用会立即返回一个状态,表明数据目前不可读或不可写,阻塞模式就会一直等待,非阻塞模式不管结果继续执行,然后不断轮询等待结果,多路复用则是添加选择器来进行状态的事件触发,其都是单一执行

只有异步非阻塞是异步的,如果当前任务没有执行到状态结果,另一个同时执行的线程可以通过回调方法返回给当前线程,而异步阻塞不存在,异步操作了不可能会阻塞线程

俩者区别

  1. 非阻塞I/O:在这种模式下,当一个I/O请求发起时,如果数据不可用,系统调用会立即返回一个状态,表明数据目前不可读或不可写。这种情况下,应用程序可以继续执行其他任务。然而,为了完成I/O操作,应用程序需要不断地轮询或检查数据是否变得可用。这就意味着虽然应用程序不会因为一个I/O请求而停止运行,但它仍需要定期地检查或等待I/O操作的完成。
  2. 异步I/O:当应用程序发起一个异步I/O操作时,它可以立即继续执行下一段代码,无需等待I/O操作的完成。在I/O操作完成时,系统会通过某种机制(如事件、通知或回调函数)来通知应用程序。这意味着应用程序不需要主动检查I/O操作的状态,从而可以更有效地利用其资源来执行其他任务。

当调用一次 channel.read 或 stream.read 后,会切换至操作系统内核态来完成真正数据读取,而读取又分为两个阶段,分别为:

  • 等待数据阶段
  • 复制数据阶段

在这里插入图片描述

  • 阻塞 IO

    在这里插入图片描述

  • 非阻塞 IO

    在这里插入图片描述

  • 多路复用

    在这里插入图片描述

  • 信号驱动
  • 异步 IO

在这里插入图片描述

  • 阻塞 IO vs 多路复用

    在这里插入图片描述

🔖 参考

UNIX 网络编程 - 卷 I

5.3 零拷贝

传统 IO 问题

传统的 IO 将一个文件通过 socket 写出

 

内部工作流程是这样的:

在这里插入图片描述

  1. java 本身并不具备 IO 读写能力,因此 read 方法调用后,要从 java 程序的用户态切换至内核态,去调用操作系统(Kernel)的读能力,将数据读入内核缓冲区。这期间用户线程阻塞,操作系统使用 DMA(Direct Memory Access)来实现文件读,其间也不会使用 cpu

    DMA 也可以理解为硬件单元,用来解放 cpu 完成文件 IO

  2. 内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即 byte[] buf),这期间 cpu 会参与拷贝,无法利用 DMA
  3. 调用 write 方法,这时将数据从用户缓冲区(byte[] buf)写入 socket 缓冲区,cpu 会参与拷贝
  4. 接下来要向网卡写数据,这项能力 java 又不具备,因此又得从用户态切换至内核态,调用操作系统的写能力,使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu

可以看到中间环节较多,java 的 IO 实际不是物理设备级别的读写,而是缓存的复制,底层的真正读写是操作系统来完成的

  • 用户态与内核态的切换发生了 3 次,这个操作比较重量级
  • 数据拷贝了共 4 次
NIO 优化

通过 DirectByteBuf

  • ByteBuffer.allocate(10) HeapByteBuffer 使用的还是 java 内存 使用的byte数组封装
  • ByteBuffer.allocateDirect(10) DirectByteBuffer 使用的是操作系统内存 操作系统和java代码哦都可以对这部分内存共享

在这里插入图片描述

大部分步骤与优化前相同,不再赘述。唯有一点:java 可以使用 DirectByteBuf 将堆外内存映射到 jvm 内存中来直接访问使用

  • 这块内存不受 jvm 垃圾回收的影响,因此内存地址固定,有助于 IO 读写
  • java 中的 DirectByteBuf 对象仅维护了此内存的虚引用,内存回收分成两步
    • DirectByteBuf 对象被垃圾回收,将虚引用加入引用队列
    • 通过专门线程访问引用队列,根据虚引用释放堆外内存
  • 减少了一次数据拷贝,用户态与内核态的切换次数没有减少

进一步优化(底层采用了 linux 2.1 后提供的 sendFile 方法),java 中对应着两个 channel 调用 transferTo/transferFrom 方法拷贝数据

在这里插入图片描述

  1. java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu
  2. 数据从内核缓冲区传输到 socket 缓冲区,cpu 会参与拷贝
  3. 最后使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu

可以看到

  • 只发生了一次用户态与内核态的切换
  • 数据拷贝了 3 次

进一步优化(linux 2.4)

在这里插入图片描述

  1. java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu
  2. 只会将一些 offset 和 length 信息拷入 socket 缓冲区,几乎无消耗
  3. 使用 DMA 将 内核缓冲区的数据写入网卡,不会使用 cpu

整个过程仅只发生了一次用户态与内核态的切换,数据拷贝了 2 次。所谓的【零拷贝】,并不是真正无拷贝,而是在不会拷贝重复数据到 jvm 内存中,直接磁盘读取到缓冲区或者网络,零拷贝的优点有

  • 更少的用户态与内核态的切换
  • 不利用 cpu 计算,减少 cpu 缓存伪共享
  • 零拷贝适合小文件传输

5.3 AIO(async input out)

AIO 用来解决数据复制阶段的阻塞问题

  • 同步意味着,在进行读写操作时,线程需要等待结果,还是相当于闲置
  • 异步意味着,在进行读写操作时,线程不必等待结果,而是将来由操作系统来通过回调方式由另外的线程来获得结果

异步模型需要底层操作系统(Kernel)提供支持

  • Windows 系统通过 IOCP 实现了真正的异步 IO
  • Linux 系统异步 IO 在 2.6 版本引入,但其底层实现还是用多路复用模拟了异步 IO,性能没有优势
文件 AIO

先来看看 AsynchronousFileChannel

 

输出

 

可以看到

  • 响应文件读取成功的是另一个线程 Thread-5
  • 主线程并没有 IO 操作阻塞
💡 守护线程

默认文件 AIO 使用的线程都是守护线程,所以最后要执行 以避免守护线程意外结束

网络 AIO
 

二 操作文件基础

nio学习各种io通信,其中顺便会议javaio操作文件当在 Java 中操作文件时,你可以使用 Java 的标准类库中的 java.io.File 类以及其他类来进行文件的读取、写入、删除等操作。以下是一些常见的文件操作示例:

  1. 创建文件对象:
 
  1. 检查文件或目录是否存在:
 
  1. 创建新文件或目录:
 
  1. 读取文件内容:
 
  1. 写入文件内容:
 
  1. 删除文件或目录:
 

在实际应用中,需要处理异常、考虑文件编码、处理大文件等情况。记得在处理文件时要遵循**实践,包括正确关闭文件流以及适当处理异常。

当进行文件操作时,遵循一些**实践可以提高代码的健壮性和可靠性。以下是一些常见的**实践:

  • 异常处理: 使用 try-catch-finally 块来处理文件操作可能出现的异常,确保及时释放资源并进行适当的错误处理。
 
  • 关闭资源: 在使用文件读写等操作后,要确保及时关闭相关的流对象,以释放系统资源。
 
  • 使用 try-with-resources: 在 Java 7 及以上版本,可以使用 try-with-resources 来自动关闭实现了 AutoCloseable 或 Closeable 接口的资源,简化代码并提高可读性。
 
  • 检查文件或目录状态: 在进行文件操作之前,可以先检查文件或目录的状态,避免出现意外的异常或错误。
 
  • 使用合适的字符编码: 在进行文件读写时,要注意使用合适的字符编码,以免出现乱码或数据损坏的情况。
 
  • . 处理大文件: 当处理大文件时,可以采用逐行读取或分块读取的方式,避免一次性加载整个文件导致内存溢出。

                            

  • 上一篇: java编程基础问题
  • 下一篇: java基础50
  • 版权声明


    相关文章:

  • java编程基础问题2025-04-07 19:58:02
  • java代码基础知识点2025-04-07 19:58:02
  • 太原java基础培训2025-04-07 19:58:02
  • java基础5412025-04-07 19:58:02
  • 零基础能学会java吗2025-04-07 19:58:02
  • java基础502025-04-07 19:58:02
  • 怎么学java基础入门2025-04-07 19:58:02
  • java基础串讲2025-04-07 19:58:02
  • java基础视频壁虎2025-04-07 19:58:02
  • java需要的基础2025-04-07 19:58:02