IO相关

概述

  1. Java程序中, IO流用来处理设备之间的数据传输。对于数据的输入/输出操作以”流(stream)” 的形式从源节点到目标节点进行数据的流动。 源节点和目标节点可以是文件、网络、内存、键盘、显示器等等。<br />java.io包下提供了各种“流”的类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。
  • 输入input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中
  • 输出output:将程序(内存)数据输出到磁盘、光盘等存储设备中

按操作数据单位不同分为:字节流(8 bit),字符流(16 bit) 。而按数据流的流向不同分为:输入流,输出流。 Java的IO流共涉及40多个类,实际上非常规则,都是从如下4个抽象基类派生的。
image.png
无论是文本文件还是二进制文件,当需要读取文件数据时,需要完成以下步骤:

  • 使用文件输入流打开指定文件:
    • 对于文本文件,应使用字符输入流FileReader流
    • 对于二进制文件,应使用字节输入流FileInputStream流
  • 读取文件数据
  • 关闭输入流

无论是文本文件还是二进制文件,当需要将数据写入文件时,需要完成以下步骤:

  • 使用文件输出流打开指定文件:
    • 对于文本文件,应使用字符输出流FileWriter流
    • 对于二进制文件,应使用字节输出流FileOutputStream流
  • 将数据写入文件
  • 关闭输出流

Java中如何实现序列化,有什么意义?

答:序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决对象流读写操作时可能引发的问题(如果不进行序列化可能会存在数据乱序的问题)。
要实现序列化,需要让一个类实现Serializable接口,该接口是一个标识性接口,标注该类对象是可被序列化的,然后使用一个输出流来构造一个对象输出流并通过writeObject(Object)方法就可以将实现对象写出(即保存其状态);如果需要反序列化则可以用一个输入流建立对象输入流,然后通过readObject方法从流中读取对象。

序列化除了能够实现对象的持久化之外,还能够用于对象的深度克隆。

Java中有几种类型的流?

答:字节流和字符流。字节流继承于InputStream、OutputStream,字符流继承于Reader、Writer。在java.io 包中还有许多其他的流,主要是为了提高性能和使用方便。

关于Java的I/O需要注意的有两点:一是两种对称性(输入和输出的对称性,字节和字符的对称性);二是两种设计模式(适配器模式和装潢模式)。另外Java中的流不同于C#的是它只有一个维度一个方向。

谈谈Java IO里面的常见类,字节流,字符流、接口、实现类、方法阻塞

答:输入流就是从外部文件输入到内存,输出流主要是从内存输出到文件。
IO里面常见的类,第一印象就只知道IO流中有很多类,IO流主要分为字符流和字节流。

  • 字符流中有抽象类InputStream和OutputStream,它们的子类FileInputStream,FileOutputStream,BufferedOutputStream等。
  • 字符流BufferedReader和Writer等。都实现了Closeable, Flushable, Appendable这些接口。程序中的输入输出都是以流的形式保存的,流中保存的实际上全都是字节文件。

java中的阻塞式方法是指在程序调用改方法时,必须等待输入数据可用或者检测到输入结束或者抛出异常,否则程序会一直停留在该语句上,不会执行下面的语句。比如read()和readLine()方法。

字符流和字节流有什么区别?

要把一片二进制数据逐一输出到某个设备中,或者从某个设备中逐一读取一片二进制数据,不管输入输出设备是什么,我们要用统一的方式来完成这些操作,用一种抽象的方式进行描述,这个抽象描述方式起名为IO流,对应的抽象类为OutputStream和InputStream ,不同的实现类就代表不同的输入和输出设备,它们都是针对字节进行操作的。
在应用中,经常要完全是字符的一段文本输出去或读进来,用字节流可以吗?
计算机中的一切最终都是二进制的字节形式存在。对于“中国”这些字符,首先要得到其对应的字节,然后将字节写入到输出流。读取时,首先读到的是字节,可是我们要把它显示为字符,我们需要将字节转换成字符。由于这样的需求很广泛,人家专门提供了字符流的包装类。
底层设备永远只接受字节数据,有时候要写字符串到底层设备,需要将字符串转成字节再进行写入。字符流是字节流的包装,字符流则是直接接受字符串,它内部将串转成字节,再写入底层设备,这为我们向IO设别写入或读取字符串提供了一点点方便。

NIO

引言

  1. 早期的程序员受CPU的影响较大,而随着CPU能力的提升,现在的程序性能更多的是受到IO操作的影响。其实各大系统对IO操作做了很多性能的改进,但是Java虚拟机为了保证Java程序员在各种平台上的运行效果一致,把操作系统对IO性能的提升给屏蔽了,使得JavaIO领域一直处于劣势。虽然Java有一套完整的IO类,但是需要处理大量数据的时候却可能对执行效率造成致命的伤害。传统的IO也不具备当今操作系统的许多功能,如文件锁定、非阻塞IO、内存映射等,故JavaJDK1.4就引进了NIO

概述

基本流程

  1. NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。虽然Java NIO中除此之外还有很多类和组件,但ChannelBufferSelector构成了核心的API。其它组件,如PipeFileLock,只不过是与三个核心组件共同使用的工具类。<br /> 传统的IO主要基于字节流和字符流进行操作,而NIO基于ChannelBuffer(缓冲区)进行操作。即数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。<br /> 比如说我们的程序想要去读取来自磁盘的数据,但是我们的程序并不会去直接读取到磁盘上的数据,因为我们任何的程序都是运行在操作系统之上的。而操作系统是通过磁盘控制器来读取磁盘中的数据的。<br /> 磁盘控制器将数据从磁盘中读取到操作系统中的缓冲区,从磁盘控制器到缓冲区有一个专有名词叫直接存储访问DMA。然后我们的程序就是从操作系统中的缓存区中读取数据的。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/28814483/1658471875634-dd3feeb6-fd6c-4462-824e-a5e33692bb6e.png#clientId=ua7810f66-376c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=249&id=u60cda051&name=image.png&originHeight=343&originWidth=1075&originalType=binary&ratio=1&rotation=0&showTitle=false&size=62800&status=done&style=none&taskId=udca47502-2b99-4616-adf9-99d3b0eef78&title=&width=781.8181818181819)
  2. 我们传统的IO可以将缓冲区中的数据一个一个来读或者一个数组一个数组来读。以上就是我们的Java程序读取磁盘数据的大致模型。
  3. 目前来说电脑的CPU处理速度很快,已经不是程序执行速度快慢的决定性因素了, 更多的时候是受到IO操作的影响,需要等待用户的操作。也就是程序的执行效率更多时候是由IO效率来决定的,毕竟需要等待数据的传送。
  4. 虽然磁盘控制器读取磁盘数据的速度已经很快了,但是由于Java虚拟机为了使得Java程序能够在各个平台运行一致,将操作系统的一些强大功能给屏蔽了。这就导致了IO操作阻碍了程序的运行效率降低。也就是说在操作系统中,可以从硬件上**直接读取大块的数据**,而JVMI/O更喜欢小块数据的读取。是Java虚拟机自身的不足引起的效率低下。故JavaJDK1.4就引进了NIO可以最大限度的满足Java程序I/o的需求。

NIO和传统IO 的区别

image.png
1、NIO和传统IO(简称IO)之间第一个最大的区别是,IO是面向的,NIO是面向缓冲区的。

  • Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
  • NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有自己需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

简单来说就是NIO是以块的方式处理数据,但是IO是以最基础的字节流的形式去写入和读出的。所以在效率上的话,肯定是NIO效率比IO效率会高出很多。
2、
IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程就会被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
NIO是非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。
一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
3、
在NIO中,所有的数据都需要通过Channel传输,通道可以直接将一块块数据映射到内存中。Channel是双向的(传统IO是单向的,只能向前不能向后),不仅可以读取数据,还能保存数据。而我们的程序不能直接读写Channel通道,Channel它只与Buffer缓冲区交互。
image.png

BIO与NIO对比( block IO与Non-block IO )

1、区别

image.png

2、 面向流与面向缓冲
  1. Java NIOIO之间第一个最大的区别是,IO是面向流的而NIO是面向缓冲区的。Java IO面向流意味着毎次从流中读一个到多个字节,直至读取所有字节,它们没有被缓存到任何地方。此外,它不能前后移动流中的数据(单向的)。如果需要前后移动从流中读取的教据,需要先将它缓存到一个缓冲区。<br /> **就是说NIO的通道是可以双向的,但是IO中的流只能是单向的。**<br /> Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数裾。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

3、 阻塞与非阻塞
  1. Java IO的各种流是阻塞的。这意味着,当一个线程调用read() write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。<br /> Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞;所以直至数据变的可以读取之前,**该线程可以继续做其他的事情。** 非阻塞写也是如此:一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,**所以一个单独的线程现在可以管理多个输入和输出通道(channel)。**

4、选择器
  1. Java NIO的选择器允**许一个单独的线程来监视多个输入通道**,你可以注册**多个通道使用一个选择器**,然后使用一个单**独的线程来“选择"通道**:这些通里已经有可以处理的褕入,或者选择已准备写入的通道。这种选怿机制,使得一个单独的线程很容易来管理多个通道。

5、NIO和BIO读取文件

BIO从一个阻塞的流中一行一行的读取数据
image.png

NIO读取文件时, 通道是数据的载体,buffer是存储数据的地方,线程每次从buffer检查数据通知给通道 。
image.png

6、 处理数据的线程数

NIO:一个线程管理多个连接
BIO:一个线程管理一个连接

阻塞IO

  1. 通常在使用传统 I/O 操作进行同步时,如果读取数据,代码会阻塞直至有可供读取的数据。同样,写入调用将会阻塞直至数据能够写入。<br /> 传统的 Server/Client 模式会基于 TPRThread per Request),**服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户请求**。<br /> 这种模式带来的一个问题就是线程数量的剧增,大量的线程会增大服务器的开销。<br />大多数的实现为了避免这个问题,都采用了线程池模型,并设置线程池线程的最大数量,但是这又带来了新的问题,如果线程池中有 100 个线程,而有100 个用户都在进行**大文件下载**,会导致第 101 个用户的简单请求无法及时处理,即便第101 个用户只想请求**一个几 KB 大小的页面**。<br />传统的 Server/Client 模式如下图所示:<br />如果线程数量不够别的请求使用,那其他的**请求就会被阻塞**,等待拿到线程去处理;<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/28814483/1658471992359-a13bf67e-0e56-4f1e-a1af-b41062bda658.png#clientId=ua7810f66-376c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=347&id=uad050eeb&name=image.png&originHeight=477&originWidth=1174&originalType=binary&ratio=1&rotation=0&showTitle=false&size=209208&status=done&style=none&taskId=u76dada25-a954-4aa6-a429-735e2ef1a12&title=&width=853.8181818181819)

非阻塞IO

  1. NIO 中非阻塞 I/O 采用了基于 Reactor 模式的工作方式,使得I/O 调用不会被阻塞,相反是注册感兴趣的特定 I/O 事件,如可读数据到达,新的套接字连接等等。在发生特定事件时,系统再通知我们。<br /> **NIO 中实现非阻塞 I/O 的核心对象就是 Selector**,Selector 就是注册各种 I/O **事件**地方,而且当我们感兴趣的事件发生时,就是这个对象告诉我们所发生的事件,如下图所示:<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/28814483/1658472009859-909dd1e9-6a28-40a6-83e2-3580b0731bd4.png#clientId=ua7810f66-376c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=623&id=uff0734bf&name=image.png&originHeight=857&originWidth=1128&originalType=binary&ratio=1&rotation=0&showTitle=false&size=201088&status=done&style=none&taskId=u4848433f-7032-4dfc-b2fe-e999c4a25d3&title=&width=820.3636363636364)
  2. 从图中可以看出,当有读或写等任何注册的事件发生时,可以从 Selector 中获得相应的 SelectionKey,同时从 SelectionKey 中可以找到发生的事件和该事件所发生的具体的 SelectableChannel,以获得客户端发送过来的数据。
  3. 非阻塞指的是 IO 事件本身不阻塞,但是获取 IO 事件的 select()方法是需要阻塞等待的。
  4. 区别是阻塞 IO 会阻塞在 IO 操作上,NIO 阻塞在事件获取上,没有事件就没有 IO,,从高层次看 IO 就不阻塞了。
  5. 也就是说只有 IO 已经发生那么我们才评估 IO 是否阻塞,但是select()方法阻塞的时候 IO 还没有发生,何谈 IO 的阻塞呢? ?????啥玩意儿
  6. 简单来说NIO 的本质是延迟 IO 操作到真正发生 IO 的时候(什么叫真正发生IO的时候),**而不是以前的只要 IO 流打开了就一直等待 IO 操作**

阻塞与同步

1)阻塞(Block)和非租塞(NonBlock):

阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式,当数据没有准备的时候阻塞:往往需要等待缓冲区中的数据准备好过后才处理其他的事情,否則一直等待在那里。

非阻塞:当我们的进程访问我们的数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。如果数据已经准备好,也直接返回。

2)同步(Synchronization)和异步(Async)的方式:

同步和异步都是基于应用程序操作系统处理IO事件所采用的方式,比如同步:是应用程序要直接参与IO读写的操作。异步:所有的IO读写交给操作系统去处理,应用程序只需要等待通知。

同步方式在处理IO事件的时候,必须阻塞在某个方法上等待我们的IO事件完成(阻塞IO事件或者通过轮询IO事件的方式)。

对于异步来说,所有的IO读写都交给了操作系统。这个时候,我们可以去做其他的事情,并不需要去完成真正的IO操作,当操作完成IO后会给我们的应用程序一个通知。

同步:阻塞到IO事件,阻塞到read成则write。这个时候我们就完全不能做自己的事情,让读写方法加入到线程里面,然后阻塞线程来实现,对线程的性能开销比较大。

Buffer

概述

  1. 缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,把数组的内容与信息包装成一个Buffer对象,它提供了一组访问这些信息的方法。

在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中。

NIO中的关键Buffer实现有:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer,、LongBuffer,、ShortBuffer,分别对应基本数据类型: byte, char, double,float, int, long, short。也就是说 各种类型的缓冲区中,底层都是一个对应类型的数组

比如 ByteBuffer

  1. final byte[] hb; // Non-null only for heap buffersCopy

image.png

Buffer的重要属性

1、capacity容量:是指缓冲区可以存储多少个数据。容量在创建Buffer缓冲区时指定大小,创建并指定容量后不能再修改。如果缓冲区满了,需要清空后才能继续写数据。

2、position表示读写的当前位置索引,即缓冲区写入/读取的位置。 该位置会自动由相应的 get( )和 put( )方法更新。 我们刚创建Buffer对象后,position就初始化为0。写入一个数据,position就向后移动一个单元,它的最大值是capacity - 1。

当Buffer从写模式切换到读模式, position会被重置0。从Buffer的开始位置读取数据,每读一个数据postion,就向后移动一个单元。

3、limit 上界 ,是指缓冲区中第一个不能被读出或写入的位置( 或者说,缓冲区中现存元素的计数 )。limit 上限后面的单元既不能读也不能写。在 Buffer缓冲区的写模式下,,limit表示能够写入多少个数据;在读取模式下,limit表示最多可以读取多少个数据。

4、mark标记,设置一个标记位置( 下一个要被读或写的元素的索引),可以调用mark()方法,把标记设置在position位置,当调用reset()方法时,就把postion设置为mark标记的位置。

这四个属性之间总是遵循以下关系:0 <= mark <= position <= limit <= capacity

Buffer常用API

put()方法

  • put()方法可以将一个数据放入到缓冲区中。
  • 进行该操作后,postition的值会+1,指向下一个可以放入的位置。capacity = limit ,为缓冲区容量的值。

image.png

get()方法

  • get()方法会读取缓冲区中的一个值
  • 进行该操作后,position会+1,如果超过了limit则会抛出异常

allocate方法()

通过allocate方法可以获取一个对应缓冲区的对象,它是缓冲区类的一个静态方法 。

flip()

  • flip()方法会切换对缓冲区的操作模式,由写->读 / 读->写。即将读写模式互换。
  • 进行该操作后
    • 如果是写模式->读模式,position = 0,即position置零; limit 指向最后一个元素的下一个位置,capacity不变。
    • 如果是读->写,则恢复为put()方法中的值。????

rewind()

将 position 重置为 0 ,一般用于重复读。

  • 该方法只能在读模式下使用
  • rewind()方法后,会恢复position、limit和capacity的值,变为进行get()前的值

clear()

  • clean()方法会将缓冲区中的各个属性恢复为最初的状态,position = 0, capacity = limit
  • 此时缓冲区的数据依然存在,处于“被遗忘”状态,下次进行写操作时会覆盖这些数据

image.png

compact()

  • 将未读取的数据拷贝到 buffer 的头部位。

mark()和reset()方法

  • mark()方法会将postion的值保存到mark属性中
  • reset()方法会将position的值改为mark中保存的值

非直接缓冲区和直接缓冲区

非直接缓冲区

通过 allocate()方法获取的缓冲区都是非直接缓冲区。这些缓冲区是建立在JVM堆内存 之中的。

通过非直接缓冲区,想要将数据写入到物理磁盘中,或者是从物理磁盘读取数据。都需要经过JVM和操作系统,数据在两个地址空间中传输时,会copy一份保存在对方的空间中。所以非直接缓冲区的读取效率较低。
image.png

直接缓冲区

只有ByteBuffer可以获得直接缓冲区,即通过allocateDirect()获取的缓冲区为直接缓冲区,这些缓冲区是建立在物理内存之中的。

直接缓冲区通过在操作系统和JVM之间创建物理内存映射文件加快缓冲区数据读/写入物理磁盘的速度。放到物理内存映射文件中的数据就不归应用程序控制了,操作系统会自动将物理内存映射文件中的数据写入到物理内存中。

image.png

缓冲区存取数据流程

?????????????

存数据时position会++,当停止数据读取的时候,调用flip(),此时limit=position,position=0
读取数据时position++,一直读取到limit
clear() 清空 buffer ,准备再次被写入 (position 变成 0 , limit 变成 capacity) 。

NIO简单工作原理

1、 当我们刚开始初始化这个buffer数组的时候,开始默认是这样的
image.png

2、但是当你往buffer数组中开始写入的时候几个字节的时候就会变成下面的图,position会移动你数据的结束的下一个位置,这个时候你需要把buffer中的数据写到channel管道中,所以此时我们就需要用这个buffer.flip();方法,
image.png

3、当你调用完buffer.flip();方法时,这个时候就会变成下面的图了,这样的话其实就可以知道你刚刚写到buffer中的数据是在position——>limit之间,然后下一步调用clear();
image.png

4、这时底层操作系统就可以从缓冲区中正确读取这 5 个字节数据发送出去了。在下一次写数据之前我们在调一下 clear() 方法。缓冲区的索引状态又回到初始位置。

(其实这一步有点像IO中的把转运字节数组 char[] buf = new char[1024]; 不足1024字节的部分给强制刷新出去的意思)

Channel

概述

  1. Channel 是一个通道,可以通过它读取和写入数据,表示**IO 源与目标打开的连接**。它就像水管一样,网络数据就像水一样通过Channel 读取和写入。 Channel 类似于传统的“流”。只不过**Channel 本身不能直接访问数据,Channel 只能与Buffer 进行交互**
  2. 通道与流的不同之处在于通道是**双向**的,流只是在一个方向上移动(一个流必须是 InputStream 或者OutputStream 的子类)。而通道可以用于读、写**或者同时用于读写。**因为 Channel 是全双工的,所以它可以比流更好地映射底层操作系统的 API
  3. NIO 中通过 channel 封装了对数据源的操作,通过 channel 我们可以操作数据源,但又不必关心数据源的具体物理结构。这个数据源可能是多种的。比如,可以是文件,也可以是网络 socket。在大多数应用中,channel 与文件描述符或者 socket 是一一对应的。
  1. **Channel 用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。**

接口源码截图
image.png

  1. 与缓冲区不同的是,通道 API 主要由接口指定。而不同的操作系统上通道实现(Channel Implementation)会有根本性的差异,所以通道 API 仅仅描述了可以做什么。**因此很自然地,通道实现经常使用操作系统的本地代码。**通道接口允许您以一种受控且可移植的方式来访问底层的 I/O 服务。
  2. Channel 是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,**通道就像是流**。NIO所有数据都通过 Buffer 对象来处理。**您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。**同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。

Java NIO 的通道类似流,但又有些不同:

  • 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
  • 通道可以异步地读写。
  • 通道中的数据总是要先读到一个 Buffer缓冲区,或者总是要从一个 Buffer 中写入。
  • 正如上面所说,从通道读取数据到缓冲区,从缓冲区写入数据到通道。
    image.png

实现类

NIO中的Channel的主要实现类有以下四个:

(1)FileChannel 从文件中读写数据。

(2)DatagramChannel 能通过 UDP 读写网络中的数据。

(3)SocketChannel 能通过 TCP 读写网络中的数据。

(4)ServerSocketChannel 可以监听新进来的 TCP 连接,像 Web 服务器那样。对每一个新进来的连接都会创建一个 SocketChannel。

FileChannel概述

FileChannel 类可以实现常用的 read,write 以及 scatter/gather 操作,同时它也提供了很多专用于文件的新方法。这些方法中的许多都是我们所熟悉的文件操作。
image.png

涉及Buffer的通常操作:将数据写入缓冲区、调用 buffer.flip() 反转读写模式、从缓冲区读取数据、调用 buffer.clear() 或 buffer.compact() 清除缓冲区内容。

实例:将数据读取到Buffer中

  1. public class FileChannelTest {
  2. public static void main(String[] args) throws IOException {
  3. //创建FileChannel
  4. RandomAccessFile raf = new RandomAccessFile("C:\\Users\\PePe\\Desktop\\achang.txt","rw");
  5. FileChannel channel = raf.getChannel();
  6. //创建Buffer
  7. ByteBuffer buffer = ByteBuffer.allocate(1024);
  8. //读取数据到Buffer中
  9. int readNum = channel.read(buffer);
  10. //有内容
  11. while (readNum != -1){
  12. System.out.println("读取数据。。。");
  13. buffer.flip();//读写模式的转换
  14. while (buffer.hasRemaining()){//判断是否有剩余的内容
  15. System.out.println((char)buffer.get());
  16. }
  17. buffer.clear();//清空
  18. readNum = channel.read(buffer);
  19. }
  20. raf.close();
  21. System. out .println("操作结束");
  22. }
  23. }

(出现乱码?????可能是读取的文件的编码方式跟Java中的编码方式不一样)

FileChannel操作详解

1、打开 FileChannel

在使用 FileChannel 通道之前,必须先打开它。但是,我们没有办法直接创建一个FileChannel,需要通过使用文件去创建,而文件要先去获取到,一开始我们是通过流的方式去获取文件的,如 InputStream、OutputStream 或RandomAccessFile 来获取一个 FileChannel 实例。下面是通RandomAccessFile来打开 FileChannel 的示例:

  1. RandomAccessFile raf = new RandomAccessFile("C:\\Users\\PePe\\Desktop\\achang.txt","rw");
  2. FileChannel channel = raf.getChannel();

2、从 FileChannel 读取数据

调用多次调用 read()方法来从 FileChannel 中读取数据。如:

  1. //创建Buffer
  2. ByteBuffer buffer = ByteBuffer.allocate(1024);
  3. //读取数据到Buffer中
  4. int readNum = channel.read(buffer);

首先,分配一个 Buffer。从 FileChannel 中读取的数据将被读到 Buffer 中。然后,调用 FileChannel.read()方法。该方法将数据从 FileChannel 读取到 Buffer 中。read()方法返回的 int 值表示了有多少字节被读到了 Buffer 中。如果返回-1,表示到了文件末尾。

3、向 FileChannel 写数据

使用 FileChannel.write()方法向 FileChannel 写数据,该方法的参数是一个 Buffer。如:

  1. public class FileChannelDemo {
  2. public static void main(String[] args) throws IOException {
  3. //读入指定文件内容
  4. RandomAccessFile aFile = new RandomAccessFile("C:\\Users\\PePe\\Desktop\\achang.txt", "rw");
  5. //获取channel
  6. FileChannel inChannel = aFile.getChannel();
  7. //封装数据
  8. String newData = "New String to write to file..." + System.currentTimeMillis();
  9. //创建Buffer
  10. ByteBuffer buf1 = ByteBuffer.allocate (1024);
  11. buf1.clear();
  12. //写入数据,以字节形式
  13. buf1.put(newData.getBytes());
  14. buf1.flip();//读写转换,变为写
  15. while(buf1.hasRemaining()) {
  16. inChannel.write(buf1);
  17. }
  18. inChannel.close();
  19. }
  20. }

注意 FileChannel.write()是在 while 循环中调用的。因为无法保证 write()方法一次能向 FileChannel 写入多少字节,因此需要重复调用 write()方法,直到 Buffer 中已经没有尚未写入通道的字节

4、关闭 FileChannel

用完 FileChannel 后必须将其关闭。如:

  1. inChannel.close();

5、FileChannel 的 position 方法
  1. 有时可能需要在 FileChannel 的某个特定位置进行数据的读/写操作。可以通过调用position()方法获取 FileChannel 的当前位置。也可以通过调用 position(long pos)方法设置 FileChannel 的当前位置。
  1. long pos = channel.position();//获取当前位置
  2. channel.position(pos +123);//设置当前位置

如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回-1 (文件结束标志)。

如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致“**文件空洞**磁盘上物理文件中写入的数据间有空隙

6、FileChannel 的 size 方法

FileChannel 实例的 size()方法将返回该实例所关联文件的大小。如:

  1. long fileSize = channel.size();

7、FileChannel 的 truncate 方法(了解)

可以使用 FileChannel.truncate()方法截取一个文件。截取文件时,文件将指定长度后面的部分将被删除。如:

  1. channel.truncate(1024);
  2. //这个例子截取文件的前 1024 个字节。

8、FileChannel 的 force 方法

FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到 FileChannel 里的数据一定会即时写到磁盘上。要保证这一点,需要调用 force()方法。

force()方法有一个 boolean 类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。

9、FileChannel 的 transferTo 和 transferFrom 方法(重要)

通道之间的数据传输:

如果两个通道中有一个是 FileChannel(不是这个不行?),那你可以直接将数据从一个 channel 传输到另外一个 channel。

(1)transferFrom()方法

FileChannel 的 transferFrom()方法可以将数据从源通道传输到 FileChannel 中(译者注:这个方法在 JDK 文档中的解释为将字节从给定的可读取字节通道传输到此通道的文件中)。

下面是一个 FileChannel 完成文件间的复制的例子:

  1. public class FileChannelWrite {
  2. public static void main(String args[]) throws Exception {
  3. //创建两个FileChannel通道
  4. RandomAccessFile aFile = new RandomAccessFile("d:\\achang\\01.txt", "rw");
  5. FileChannel fromChannel = aFile.getChannel();
  6. RandomAccessFile bFile = new RandomAccessFile("d:\\achang\\02.txt", "rw");
  7. FileChannel toChannel = bFile.getChannel();
  8. //设置起始/结束位置
  9. long position = 0;
  10. long count = fromChannel.size();
  11. //将fromChannel中全部的数据传到toChannel中
  12. toChannel.transferFrom(fromChannel, position, count);
  13. //关闭channel通道
  14. aFile.close();
  15. bFile.close();
  16. System. out .println("over!");
  17. }
  18. }

方法的输入参数 position 表示从 position 处开始向目标文件写入数据,count 表示最多传输的字节数。

如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数。

此外要注意,在 SoketChannel 的实现中,SocketChannel 只会传输此刻准备好的数据(可能不足 count 字节)。因此,SocketChannel 可能不会将请求的所有数据(count 个字节)全部传输到 FileChannel 中。

2)transferTo()方法

transferTo()方法将数据从 FileChannel 传输到其他的 channel 中。

下面是一个 transferTo()方法的例子

  1. public class FileChannelDemo {
  2. public static void main(String args[]) throws Exception {
  3. RandomAccessFile aFile = new RandomAccessFile("d:\\achang\\02.txt", "rw");
  4. FileChannel fromChannel = aFile.getChannel();
  5. RandomAccessFile bFile = new RandomAccessFile("d:\\achang\\03.txt", "rw");
  6. FileChannel toChannel = bFile.getChannel();
  7. long position = 0;
  8. long count = fromChannel.size();
  9. //将fromChannel传到toChannel中
  10. fromChannel.transferTo(position, count, toChannel);
  11. aFile.close();
  12. bFile.close();
  13. System. out .println("over!");
  14. }
  15. }

Socket通道概述

(1)新的 socket 通道类可以运行非阻塞模式并且是可选择的,可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性。

本节中我们会看到,再也不需要为每个 socket 连接使用一个线程的必要了,也避免了管理大量线程所需的上下文交换开销。借助新的 NIO 类,一个或几个线程就可以管理成百上千的活动 socket 连接了并且只有很少甚至可能没有性能损失。

所有的 socket 通道类(DatagramChannel、SocketChannel 和 ServerSocketChannel)都继承了位于java.nio.channels.spi 包中的AbstractSelectableChannel。

这意味着我们可以用一个 Selector 对象来执行socket 通道的就绪选择(readiness selection)。

(2)注意 DatagramChannel 和 SocketChannel 实现定义读和写功能的接口而ServerSocketChannel 不实现。ServerSocketChannel 负责监听传入的连接和创建新的 SocketChannel 对象,它本身从不传输数据

(3)在我们具体讨论每一种 socket 通道前,应该先了解 socket 和 socket 通道之间的关系。通道是一个连接 I/O 服-务导管并提供与该服务交互的方法。就某个 socket 而言,它不会再次实现与之对应的 socket 通道类中的 socket 协议 API,而 java.net 中已经存在的 socket 通道都可以被大多数协议操作重复使用。全部 socket 通道类(DatagramChannel、SocketChannel 和ServerSocketChannel)在被实例化时都会创建一个对等 socket 对象。socket 不能被重复使用,channel可以被重复使用。

这些是我们所熟悉的来自 java.net 的类(Socket、ServerSocket 和 DatagramSocket),它们已经被更新以识别通道。对等 socket 可以通过调用 socket( )方法从一个通道上获取。此外,这三个 java.net 类现在都有getChannel( )方法。

(4)要把一个 socket 通道置于非阻塞模式,我们要依靠所有 socket 通道类的公有超级类:SelectableChannel。

就绪选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作,如读或写。非阻塞 I/O 和可选择性是紧密相连的,那也正是管理阻塞模式的 API 代码要在SelectableChannel 超级类中定义的原因。

设置或重新设置一个通道的阻塞模式是很简单的,只要调用 configureBlocking( )方法即可,传递参数值为 true 则设为阻塞模式,参数值为 false 值设为非阻塞模式。可以通过调用 isBlocking( )方法来判断某个 socket 通道当前处于哪种模式。

非阻塞 socket 通常被认为是服务端使用的,因为它们使同时管理很多 socket 通道变得更容易。但是,在客户端使用一个或几个非阻塞模式的 socket 通道也是有益处的。例如,借助非阻塞 socket 通道,GUI 程序可以专注于用户请求并且同时维护与一个或多个服务器的会话。在很多程序上,非阻塞模式都是有用的。

偶尔地,我们也会需要防止 socket 通道的阻塞模式被更改。API 中有一个blockingLock( )方法,该方法会返回一个非透明???的对象引用。返回的对象是通道实现修改阻塞模式时内部使用的。只有拥有此对象的锁的线程才能更改通道的阻塞模式。

ServerSocketChannel

ServerSocketChannel 是一个基于通道的 socket 监听器(本身不传数据,而是一个监听器)。它同我们所熟悉的java.net.ServerSocket 执行相同的任务,不过它增加了通道语义,因此能够在非阻塞模式下运行

由于 ServerSocketChannel 没有 bind()方法,因此有必要取出对等的 socket 并使用它来绑定到一个端口以开始监听连接。我们也是使用对等 ServerSocket 的 API 来根据需要设置其他的 socket 选项。

同 java.net.ServerSocket 一样,ServerSocketChannel 也有 accept( )方法。一旦创建了一个 ServerSocketChannel 并用对等 socket 绑定了它,然后就可以在其中一个上调用 accept()。如果您选择在 ServerSocket 上调用 accept( )方法,那么它会同任何其他的 ServerSocket 表现一样的行为:总是阻塞并返回一个 java.net.Socket 对
象。

如果您选择在 ServerSocketChannel 上调用 accept( )方法则会返回SocketChannel 类型的对象,返回的对象能够在非阻塞模式下运行。

换句话说:

ServerSocketChannel 的 accept()方法会返回 SocketChannel 类型对象,SocketChannel 可以在非阻塞模式下运行。其它 Socket 的 accept()方法会阻塞返回一个 Socket 对象。如果ServerSocketChannel 以非阻塞模式被调用,当没有传入连接在等待时,ServerSocketChannel.accept( )会立即返回 null。正是这种检查连接而不阻塞的能力实现了可伸缩性并降低了复杂性。可选择性也因此得到实现。

我们可以使用一个选择器实例来注册 ServerSocketChannel 对象以实现新连接到达时自动通知的功能。

  1. public static final String GREETING = "Hello java nio.\r\n";
  2. public static void main(String[] argv) throws Exception {
  3. //端口号
  4. int port = 8888; // default
  5. /* if (argv.length > 0) {
  6. port = Integer.parseInt(argv[0]);
  7. }*/
  8. //Buffer
  9. ByteBuffer buffer = ByteBuffer.wrap(GREETING.getBytes());
  10. //ServerSocketChannel
  11. ServerSocketChannel ssc = ServerSocketChannel.open();
  12. //绑定,监听端口
  13. ssc.socket().bind(new InetSocketAddress(port));
  14. //设置非阻塞模式
  15. ssc.configureBlocking(false);
  16. //监听循环,是否有新连接传入
  17. while (true) {
  18. System.out.println("Waiting for connections");
  19. SocketChannel sc = ssc.accept();
  20. //没有连接传入的情况
  21. if (sc == null) {
  22. System.out.println("null");
  23. Thread.sleep (2000);
  24. } else {
  25. //有连接传入的情况
  26. System.out.println("Incoming connection from: "+sc.socket().getRemoteSocketAddress());//获取连接ip
  27. buffer.rewind();//指针指向0,就是位置制到0 ????
  28. //向Buffer中写入数据
  29. sc.write(buffer);
  30. sc.close();
  31. }
  32. }
  33. }

(1)打开 ServerSocketChannel

通过调用 ServerSocketChannel.open() 方法来打开 ServerSocketChannel.

(2)关闭 ServerSocketChannel

通过调用 ServerSocketChannel.close() 方法来关闭 ServerSocketChannel.

3)监听新的连接

通过 ServerSocketChannel.accept() 方法监听新进的连接。当 accept()方法返回时候,它返回一个包含新进来的连接的 SocketChannel。因此, accept()方法会一直阻塞到有新连接到达。 通常不会仅仅只监听一个连接,在 while 循环中调用 accept()方法. 如下面的例子:
image.png

阻塞模式下 会在 SocketChannel sc = ssc.accept();这里阻塞住进程。

非阻塞模式 下 accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是 null。 因此,需要检查返回的SocketChannel 是否是 null.如:
image.png

SocketChannel

Java NIO 中的 SocketChannel 是一个连接到 TCP 网络套接字的通道操作面向Buffer缓冲区 。 SocketChannel 是一种面向流连接sockets 套接字的可选择通道。

  • SocketChannel 是用来连接 Socket 套接字
  • SocketChannel 主要用途用来处理网络 I/O 的通道
  • SocketChannel 是基于 TCP 连接传输
  • SocketChannel 实现了可选择通道,可以被多路复用
  • 用于TCP的网络I/O套接字的通道

SocketChannel 特点

(1)对于已经存在的 socket 不能创建 SocketChannel

(2)SocketChannel 中提供的 open 接口创建的 Channel 并没有进行网络级联,需要使用 connect 接口连接到指定地址。

(3)未进行连接的 SocketChannle 执行 I/O 操作时,会抛出NotYetConnectedException

(4)SocketChannel 支持两种 I/O 模式:阻塞式和非阻塞式

(5)SocketChannel 支持异步关闭。如果 SocketChannel 在一个线程上 read 阻塞,另一个线程对该 SocketChannel 调用 shutdownInput,则读阻塞的线程将返回-1 表示没有读取任何数据;如果 SocketChannel 在一个线程上 write 阻塞,另一个线程对该SocketChannel 调用 shutdownWrite,则写阻塞的线程将抛出AsynchronousCloseException。

(6)SocketChannel 支持设定参数

  • SO_SNDBUF 套接字发送缓冲区大小
  • SO_RCVBUF 套接字接收缓冲区大小
  • SO_KEEPALIVE 保活连接
  • O_REUSEADDR 复用地址
  • SO_LINGER 有数据传输时延缓关闭 Channel (只有在非阻塞模式下有用)TCP_NODELAY 禁用 Nagle 算法

SocketChannel 的使用

(1)创建 SocketChannel

方式一

  1. SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80));

方式二:

  1. SocketChannel socketChanne2 = SocketChannel.open();
  2. socketChanne2.connect(new InetSocketAddress("www.baidu.com",80));

直接使用有参 open api 或者使用无参 open api,但是在无参 open 只是创建了一个SocketChannel 对象,并没有进行实质的 tcp 连接。

(2)连接校验

  1. socketChannel.isOpen(); // 测试 SocketChannel 是否为 open 状态
  2. socketChannel.isConnected(); //测试 SocketChannel 是否已经被连接
  3. socketChannel.isConnectionPending(); //测试 SocketChannel 是否正在进行连接
  4. socketChannel.finishConnect(); //校验正在进行套接字连接的 SocketChannel是否已经完成连接

(3)读写模式

前面提到 SocketChannel支持阻塞和非阻塞两种模式:

  1. socketChannel.configureBlocking(false);//非阻塞

通过以上方法设置 SocketChannel 的读写模式。false 表示非阻塞,true 表示阻塞。

(4)读写

  1. SocketChannel socketChannel = SocketChannel.open (new InetSocketAddress("www.baidu.com", 80));
  2. ByteBuffer byteBuffer = ByteBuffer.allocate (16);
  3. socketChannel.read(byteBuffer);
  4. socketChannel.close();
  5. System.out.println("read over");

以上为阻塞式读,当执行到 read 出,线程将阻塞,控制台将无法打印 read over 。

  1. SocketChannel socketChannel = SocketChannel. open (new InetSocketAddress("www.baidu.com", 80));
  2. socketChannel.configureBlocking(false);
  3. ByteBuffer byteBuffer = ByteBuffer.allocate (16);
  4. socketChannel.read(byteBuffer);
  5. socketChannel.close();
  6. System.out.println("read over");

以上为非阻塞读,控制台将打印 read over

读写都是面向缓冲区,这个读写方式与前文中的 FileChannel 相同。

(5)设置和获取参数

  1. socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE,Boolean.TRUE).setOption(StandardSocketOptions.TCP_NODELAY,Boolean.TRUE);

通过 setOptions 方法可以设置 socket 套接字的相关参数

  1. socketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE);
  2. socketChannel.getOption(StandardSocketOptions.SO_RCVBUF);

可以通过 getOption 获取相关参数的值。如默认的接收缓冲区大小是 8192byte

SocketChannel 还支持多路复用,但是多路复用在后续内容中会介绍到。

https://www.bilibili.com/video/BV1E64y1h7Z4?p=11&t=3.3