Java - IO流学习笔记

1、文件和文件夹

内存中存放的数据信息在计算机关机后就会消失,如果想要长久的保存数据,就需要使用到光盘、硬盘等设备,为了便于数据的管理以及检索,引入了“文件”的概念。一篇文章,一个图片,一个视频、一个可执行的程序等等,都可以被保存成一个文件。

从文件的功能上,可以吧文件分成 文本文件,视频文件,音频文件,可执行文件,图像文件等等类别,但是从数据存储的角度看,所有的文件本质上都是一样的,都是由一个个字节组成,归根到底都是由0/1组成的,只不过呈现出了不同的状态。

大量的文件如果不进行分类,那么使用起来会十分不方便,因此,可以用文件夹对文件进行管理。

2、Java对 File 对象的一些操作

package IO;

import java.io.File;
import java.io.IOException;

public class TestFile {
    public static void main(String[] args) throws IOException {
        // win下是\ ,Linux 下为/
        File file = new File("e:\\test.txt");
        // File.separator 可以获取当前操作系统路径的拼接符号
        File file1 = new File("e:" + File.separator + "test.txt");
        file.canRead(); // 文件是否可读
        file.canWrite(); // 文件是否可写
        file.getName(); // 获取文件名称
        file.getParent(); // 获取上级目录
        file.isDirectory(); // 是否是目录
        file.isFile();// 是否是文件
        file.isHidden();// 文件是否隐藏
        file.length();// 文件的大小
        file.exists(); // 文件是否存在
        System.out.println(file == file1); // false
        System.out.println(file.equals(file1)); // true , 比较的是两个对象的文件路径
        System.out.println(file.exists());
        // if (file.exists()) {
        //     file.delete(); //文件存在就删除
        // } else {
        //     file.createNewFile(); // 文件不存在就创建
        // }

        file.toString(); // 和相对路径一样
        System.out.println(file.getPath());// 相对路径:相对位置
        System.out.println(file.getAbsolutePath());// 绝对路径:真实的准确的完整的路径

        // 文件夹相关的,上述有关文件的方法都可以对文件夹进行操作
        File file2 = new File("e:\\kaoshi\\kexin");
        //file2.mkdir(); // 帮你在创建文件夹,单层目录
        file2.mkdirs(); // 创建多层目录
        file2.delete(); // 删除只能删除最内层的目录,并且前提是这个内层目录是空的,如果不为空则不会删除

        String[] list = file2.list(); // 当前目录下,目录或者文件名称的数组
        File[] files = file2.listFiles(); // 当前目录下,files 对象的数组
    }
}

上面File 类的学习,可以发现File对象只能获取到一些比较表层的数据,比如路径,文件名,但是并没有获取到文件内的信息。如何获得文件的内容信息呢,IO流可以帮我们实现这一需求。

3、I/O 流的概念

简单来说I/O 流 可以当做是一根管子,是程序和数据源之间沟通的桥梁,所有的数据被串行化(串行化就是数据按照顺序进行输入输出)的被输入和输出。站在程序的角度,往里面读就是输入input,往外面写就是输出output。

四个抽象类型的基类

 处理数据的单位

处理数据的方向

字节流字符流
输入流InputStreamReader
输出流OutputStreamWriter

4、FileReader 和 FileWriter 操作文本文件。

FileReader的使用

public class TestFileReader {
    public static void main(String[] args) throws IOException {
        //程序从文件中读取内容,一个字符一个字符的
        // 有一个文件-----》创建一个File对象
        File file = new File("e:\\test\\test.txt");
        // 利用 FileReader 这个流,将“管子” 怼到源文件上去
        FileReader fileReader = new FileReader(file);
        // 利用管子,进行读取的动作
        int n = fileReader.read(); //每次只能读一个字符,如果到了文件的末尾,返回的值是-1;
        while (n != -1) {
            System.out.print((char) n); // 读取的是Unicode码,可以转换为字符输出
            n = fileReader.read();
        }
        System.out.println();
        //管子不用了,就把管子关闭; ----->手动关闭流
        fileReader.close();

        //程序从文件中读取内容,多个字符多字符的
        // 有一个文件-----》创建一个File对象
        File file1 = new File("e:\\test\\test.txt");
        // 利用 FileReader 这个流,将“管子” 怼到源文件上去
        FileReader fileReader1 = new FileReader(file);
        // 利用管子,进行读取的动作
        char[] chars = new char[5]; // 一次性读取五个字符
        int read = fileReader1.read(chars); // 返回值是char数组的长度
        while (read >= chars.length) {
            System.out.print(chars);
            read = fileReader1.read(chars);
        }
        //管子不用了,就把管子关闭; ----->手动关闭流
        fileReader.close();
    }
}

FileWriter的使用

public class TestFileWriter {
    public static void main(String[] args) throws IOException {
        // 创建一个文件对象
        File file = new File("e:\\test\\test.txt");
        // 创建FileWriter对象,将管子怼到文件上去
        // 没有文件会自动创建一个文件,如果有文件则会覆盖文件,append如果设置为true,则不会覆盖文件,而是在之前的文件后面进行追加。
        FileWriter fileWriter = new FileWriter(file, true);
        // 开始往外写文件
        String str = "创建FileWriter对象,开始往外写文件";
        // 一个字符一个字符的写
        for (int i = 0; i < str.length(); i++) {
            fileWriter.write(str.charAt(i));
        }
        // // 直接往外写
        // char[] chars = str.toCharArray();
        // fileWriter.write(chars);
        // 关闭流
        fileWriter.close();
    }
}

使用FileReader 和 FileWriter完成文件的复制。

public class TestFileReaderWriter {
    public static void main(String[] args) throws IOException {
        // 源文件
        File infile = new File("e:\\test\\test.txt");
        // 目标文件
        File outFile = new File("e:\\test\\test1.txt");
        // 输入的管子
        FileReader fileReader = new FileReader(infile);
        // 输出的管子
        FileWriter fileWriter = new FileWriter(outFile);
        // 一遍读入源文件 一遍将读到的文件写出去
        // 方式1
        // int n = fileReader.read();
        // while (n != -1) {
        //     fileWriter.write(n);
        //     n = fileReader.read();
        // }

        // 方式2
        char[] chars = new char[5];
        int length = fileReader.read(chars);
        while (length != -1) {
            fileWriter.write(chars);
            length = fileReader.read(chars);
        }

        // 关闭流 后用的先关闭
        fileWriter.close();
        fileReader.close();
    }
}

5、不能用字符流去操作非文本文件,否则得到的文件会不可用。

文本文件:.txt  .java .c .cpp 等,建议使用字符流去操作

非文件文件:.jpg .mp3等,建议使用字节流去操作。

6、FileInputStream 和 FileOutputStream 的使用

  • 假设文件是UTF-8存储的,那么英文占一个字节,中文占三个字节 --->因此文本文件就不建议用字节流读取
  • 如果一个字节一个字节的读,那么 read方法底层做了处理,让返回的数都是“正数”,为了避免返回-1的话,不知道是读入的字节,还是文件的结尾。
  • 如果用缓冲数组byte[]去读,那么read的时候,字节就可以为负值,-128~127,因为判断文件末尾用的是 “read的返回值是byte数组的有效长度=-1”,和字节本身不会冲突
  • 如果用缓冲数组取写,wirte的时候要注意长度,否则复制得到的文件会比源文件大(最后一个数组里面有些是无效内容) fileOutputStream.write(byte,0,length); 写入数组的第0位到底length位
public class TestFileInputOutputStream {
    public static void main(String[] args) throws IOException {
        //程序从文件中读取内容,一个字符一个字符的
        // 有一个文件-----》创建File对象
        File file = new File("e:\\test\\图片.gif");
        File file2 = new File("e:\\test\\图片2.gif");
        // 利用 FileInputStream FileOutputStream 这个流,将“管子” 把程序和源文件连接
        FileInputStream fileInputStream = new FileInputStream(file);
        FileOutputStream fileOutputStream = new FileOutputStream(file2);
        // 利用管子,进行读取和写出的动作
        // 假设文件是UTF-8存储的,那么英文占一个字节,中文占三个字节 --->因此文本文件就不建议用字节流读取
        int read = fileInputStream.read(); //每次只能读一个字符,如果到了文件的末尾,返回的值是-1;
        while (read != -1) { // read方法底层做了处理,让返回的数都是“正数”,为了避免返回-1的话,不知道是读入的字节,还是文件的结尾。
            fileOutputStream.write(read); // 边读边写
            read = fileInputStream.read();
        }
        System.out.println();
        //管子不用了,就把管子关闭; ----->手动关闭流
        fileOutputStream.close();
        fileInputStream.close();
    }
}

7、缓冲字节流(处理流) BufferedInputStream/BufferedOutputStream 的使用

  • 利用缓冲字节数组可以减少访问硬盘的次数,提升效率,但是访问次数还是较多,能不能进一步减少访问硬盘次数呢?我们可以引入一个新的流 “缓冲字节流(处理流)”
  • 其实BufferedInputStream/BufferedOutputStream底层也是通过新建缓冲数组进行数据操作的,size值为8192
  • 如果数据流存在着嵌套,那么其实只要关闭高级流,那么里面的字节流也会随之关闭。

例如BufferedOutputStream 的源码:(BufferedInputStream类似)

    public BufferedOutputStream(OutputStream out) {
        this(out, 8192);
    }

    public BufferedOutputStream(OutputStream out, int size) {
        super(out);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }
public class TestBuffer {
    public static void main(String[] args) throws IOException {
        //程序从文件中读取内容,一个字符一个字符的
        // 有一个文件-----》创建File对象
        File file = new File("e:\\test\\图片.gif");
        File file2 = new File("e:\\test\\图片2.gif");
        // 利用 FileInputStream FileOutputStream 这个流,将“管子” 把程序和源文件连接
        FileInputStream fileInputStream = new FileInputStream(file);
        FileOutputStream fileOutputStream = new FileOutputStream(file2);
        // 功能加强,引入新的流
        BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);

        // 开始动作 其实BufferedInputStream/BufferedOutputStream底层也是通过新建缓冲区数据操作的,size值为8192
        byte[] bytes = new byte[1026 * 8];
        int len = bufferedInputStream.read(bytes);
        while (len != -1) {
            bufferedOutputStream.write(bytes, 0, len);
            // bufferedOutputStream.flush();底层已经帮忙做了刷新缓冲区的操作,不需要手动完成刷新
            len = bufferedInputStream.read(bytes);
        }

        // 关闭流 ,倒着关闭
        // 如果数据流存在着嵌套,那么其实只要关闭高级流,那么里面的字节流也会随之关闭。
        bufferedOutputStream.close();
        bufferedInputStream.close();
        // fileOutputStream.close();  可以不写
        // fileInputStream.close(); 可以不写
    }
}

8、缓冲字符流(处理流) BufferedReader/BufferedWriter 的使用

public class TestBuffer02 {
    public static void main(String[] args) throws IOException {
        //程序从文件中读取内容,一个字符一个字符的
        // 有一个文件-----》创建File对象
        File file = new File("e:\\test\\test.txt");
        File file2 = new File("e:\\test\\test1.txt");
        // 利用 FileInputStream FileOutputStream 这个流,将“管子” 把程序和源文件连接
        FileReader fileReader = new FileReader(file);
        FileWriter fileWriter = new FileWriter(file2);
        // 功能加强,引入新的流
        BufferedReader bufferedReader = new BufferedReader(fileReader);
        BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);

        // 开始动作 其实BufferedInputStream/BufferedOutputStream底层也是通过新建缓冲区数据操作的,size值为8192
        // char[] chars = new char[1024];
        // int len = bufferedReader.read(chars);
        // while (len != -1) {
        //     bufferedWriter.write(chars, 0, len);
        //     len = bufferedReader.read(chars);
        // }

        // 新的方式
        String str = bufferedReader.readLine(); // 每次读取文本文件中的一行
        while (str != null) {
            bufferedWriter.write(str);
            bufferedWriter.newLine(); // 注意写入时,每行之间要新加一行,否则复制的文件中没有换行。
            str = bufferedReader.readLine();
        }

        // 关闭流 ,倒着关闭
        // 如果数据流存在着嵌套,那么其实只要关闭高级流,那么里面的字节流也会随之关闭。
        bufferedWriter.close();
        bufferedReader.close();
        // FileWriter.close();  可以不写
        // FileReader.close(); 可以不写
    }
}

9、转换流(将字节流和字符流进行转换)InputStreamReader / OutputStreamWriter的使用。

看后缀,这两个转换流属于字符流。

  • InputStreamReader:字节输入流转换为字符输入流,完成文件到程序
  • OutputStreamWriter:字符输出流转换为字节输出流,完成程序到文件
public class Test_InputStreamReader_OutputStreamWriter {
    public static void main(String[] args) throws IOException {
        // 源文件 和目标文件
        File file = new File("e:\\test\\test.txt");
        File file1 = new File("e:\\test\\test1.txt");
        // 输入方向,创建一个字节流接触文件,创建转换流,负责将字节流转换为字符流(相当于一个处理流)
        // 创建转换流,负责将字节流转换为字符流(相当于一个处理流)
        // 编码格式要保持统一,否则会出现乱码; 如果不手动写编码格式,会自动获取编译器的编码格式
        FileInputStream fileInputStream = new FileInputStream(file);
        InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "utf-8");
        // 输出方向
        FileOutputStream fileOutputStream = new FileOutputStream(file1);
        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, "utf-8");
        // 开始动作
        char[] chars = new char[20];
        int len = inputStreamReader.read(chars);
        while (len != -1) {
            outputStreamWriter.write(chars);
            len = inputStreamReader.read(chars);
        }
        // 关闭流
        outputStreamWriter.close();
        inputStreamReader.close();
    }
}

10、System类对IO属性的支持

  • System.in:从键盘输入
  • System.out:输出到控制台

11、数据流DataInputStream / DataOutputStream

将文件中存储的基本数据类型和字符串写入到内存的变量中,但是读的过程中,要注意变量的类型要和写入变量的类型要一一对应,否则会读出错。

12、对象流 ObjectInputStream 和 ObjectOutputStream

用于存储和读取基本数据类型或者对象的处理流,他的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。

ObjectOutputStream : 将一个对象转化为字节写到一个文件中去,以此来实现对象的持久化保存,这个过程被称为序列化

ObjectInputStream : 从文件中读取被序列化的对象, 称作反序列化

序列化的可以参考以下链接

Java 反射reflect 序列化_材料小菜鸟的博客-CSDN博客

13、什么是同步,什么是异步?

同步:如果有多个任务或者事件要发生,这些任务或者事件必须逐个进行,一个事件任务的执行会导致整个流程的暂时等待,其他事件没有办法并发的执行下去;

异步:如果有多个任务或者事件要发生,那么这些事件可以并发的执行

例子:一个任务包括两个子任务A和B,同步时,A执行过程中B只能等待,直到A执行完毕后,B才能继续执行;异步时,AB可以并发的执行,B不用等待A执行结束。

14、什么是阻塞,什么是非阻塞?

阻塞:某个事件或者任务执行过程中,发出一个请求,但是由于该请求操作需要的条件不满足而无法继续执行时,他就一直在那等待,直到条件满足后继续执行;

非阻塞:某个事件或者任务执行过程中,发出一个请求,如果该请求操作需要的条件不满足,会立即返回一个标志信息告知条件不满足,不会一直等待。

举个简单的例子:

假如我要读取一个文件中的内容,如果此时文件中没有内容可读,对于同步来说就是会一直在那等待,直至文件中有内容可读;而对于非阻塞来说,就会直接返回一个标志信息告知文件中暂时无内容可读。

阻塞和非阻塞着重点在于发出一个请求操作时,如果进行操作的条件不满足是否会返会一个标志信息告知条件不满足。理解阻塞和非阻塞可以同线程阻塞类比地理解,当一个线程进行一个请求操作时,如果条件不满足,则会被阻塞,即在那等待条件满足。

有关BIO/NIO/AIO的知识,后续学习完网络相关的在来补充。

15、BIO(Bolcking IO)

同步阻塞IO,jdk1.4之前只有这一种IO模型,指的是调用发起后,会阻塞线程,直到获得结果,服务端一个线程只能同时处理一个客户端的请求。 相关类和接口在java.io包下 。

问题:服务端在等待接收客户端请求时阻塞,等待客户端的连接,有多个客户端连接时,由于client1先和服务端进行了连接,此时服务端只能处理client1 的请求,什么时候client1处理完,什么时候才可以处理其他的请求,此时client2发送请求,服务端是没办法处理的

TCP/IP底层使用的是BIO,同步阻塞IO,发起请求后,必须等待接收到响应后,代码才能继续向下执行,服务端一个线程只能处理一个客户端的请求

解决方案:服务端为每个客户端创建一个线程,

BIO的问题:但是如果客户端特别多,服务器就需要创建对应数量的子线程,由于每个子线程都有自己的独立线程区,子线程创建多了就会产生内存不足等问题,而且子线程也需要调度,切换和销毁也都是非常消耗性能的。使用线程池可以一定程度上提升线程创建和销毁的性能,但是调度和切换带来的性能下降还是无法解决。

15.1、BIO的工作机制

BIO的代码实现 其实就是TCP/IP中讲到的C/S客户端的实现。

16、NIO 

new IO 或者 Non-Blocking IO,NIO的特性就是同步非阻塞IO;

调用发起后,等待获得结果,但是不会阻塞线程,NIO中服务端的一个线程可以处理多个客户端。

NIO解决了使用BIO服务端创建过多的子线程产生内存消耗、子线程调度切换销毁等带来的性能消耗问题。

NIO支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

17、Buffer缓冲区

Buffer缓冲区,缓冲区中缓存的是流数据,BIO直接操作流数据,NIO模型变为操作缓存区数据通过channel管道进行传输。

Buffer缓冲区是一个对象,他包含一些要写入或者刚读出的数据,缓冲区实质上是一个数组 ,NIO所有的数据都是用缓冲区处理,写入数据时,首先写到缓冲区中,读数据也是从缓冲区中去读。

Buffer的主要实现类

ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer

在NIO中,Buffer类是抽象类,是所有缓存的父类,提供了除boolean外基本数据类型的缓存类,但是我们一般只使用ByteBuffer,因为TCP协议是基于字节的,所以缓存区也使用的是Byte类型的缓存区

18、channel 通道

通道是对原IO包中流的一个模拟,源与目标的连接是借助一个Channel对象(通道)实现的,channel 不能直接访问数据,需要和缓冲区进行交互 。

举个栗子:

A和B两个地区之间可以进行铁路运输,他们分别在火车站旁边有个仓库AB;这里的铁路就是管道channel,而AB两个仓库就是缓冲区Buffer,仓库用于临时存储运来的货物,即缓冲区用于临时存储传输来的数据。

Channel的主要实现类

1 、FileChannel:用于读取、写入、映射和操作文件的通道。

2、 DatagramChannel:通过UDP读写网络中的数据通道。

3 、SocketChannel:通过tcp读写网络中的数据。

4 、ServerSocketChannel:可以监听新进来的tcp连接,对每一个连接都创建一个SocketChannel。
其中FileChannel没有继承SelectableChannel, 因此不支持非阻塞操作;其他的类都支持非阻塞操作。

获取通道的方式

1、通过getChannel()方法获取

前提是该类支持该方法。支持该类的方法有:

FileInputStream/FileOutputStream,RandomAccessFile,Socket,ServerSocket ,DatagramSocket

2、通过静态方法open()

3、通过jdk1.7中Files的newByteChannel()方法

NIO的底层工作原理

首先Buffer的工作机制

1、buffer数组的几个属性设置

public abstract class Buffer {
    private int mark = -1; // 用于记录当前 position 的前一个位置或者默认是 -1
    private int position = 0; // 下一个要操作的数据元素的位置
    private int limit; // 缓冲区数组中不可操作的下一个元素的位置 limit <=capacity
    private int capacity;  // 缓冲区数组的长度
}

2、初始化时,默认状态为:

3、开始往数组中写入字节的时候,变为下图,position会移动到数据结束的下一个位置

 4、调用buffer.flip() 方法,byte数组中的内容不变,position赋值给limit,position重置为0; 适合读之前的操作

    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

 5、调用buffer.clear() 方法,byte数组中的内容不变,position重置为0,capacity赋值给limit,适合写之前的操作

    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

6、mark()方法回将position的值赋给mark,reset() 方法是讲mark的值附回给position,这里是jdk1.8中的源码和网上的博客大多不一样。

    public final Buffer mark() {
        mark = position;
        return this;
    }

    public final Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();
        position = m;
        return this;
    }

代码样例:

public class TestByteBuffer {
    public static void main(String[] args) {
        // 方式一:创建byteBuffer对象,ByteBuffer是抽象类,使用allocate(capacity)方法给buffer分配缓存空间
        // 底层使用byte[]数组存储缓存数据,数组的长度为传入的参数capacity
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        // 方式二:根据传递的内容自动创建缓存区大小,大小为内容的长度
        // ByteBuffer byteBuffer1 = ByteBuffer.wrap("hello,world".getBytes());

        // 向缓存区中放内容
        byteBuffer.put("hello".getBytes());

        // position 代表下一个存放位置的开始,初始为0
        System.out.println(byteBuffer.position()); // 5

        // 最大限制,就是capacity
        System.out.println(byteBuffer.limit()); //1024

        // 获取到缓存区中存值的数组 byteBuffer.array();
        byte[] array = byteBuffer.array();
        System.out.println(new String(array)); //hello000000000000... 0代表空格
        // 这种方法要保证不能对position进行重置操作
        System.out.println(new String(array, 0, byteBuffer.position())); // hello

        // 重置buffer
        // byte数组中的内容不变,position重置为0,capacity赋值给limit,适合写之前的操作
        byteBuffer.clear();
        // byte数组中的内容不变,position赋值给limit,position重置为0 适合读之前的操作
        byteBuffer.flip();
    }
}

Selector简介

selector一般被称为选择器,也称为多路复用器。是Java NIO核心组件中的一个,用于检查一个躲着多个NIO channel 通道的状态是否处于可读或者可写。如此可以实现单线程管理多个channels,也就是单个线程管理多个网络连接。

可选择通道SelectableChannel 

  • 判断一个channel是否可以被selector复用,是判断是否继承了抽象类SelectableChannel,如果继承了就可以被复用,否则就不能。 
  • SelectableChannel 类提供了实现通道的可选择性所需要的公共方法,FileChannel类没有继承SelectableChannel类,因此不是可选通道。
  • 一个通道可以被注册到多个选择器上,但是对每个选择器而言,只能注册一次。通道和选择器之间的关系,是使用注册的方式完成。SelectableChannel可以被注册到Selector对象上,注册的时候,需要制定通道的那些操作是Selector感兴趣的,如可读,可写等。

Channel注册到Selector

  1. 使用Channel.register(Selector sel, int ops)方法,将一个通道注册到一个选择器。第一个参数:指定通道要注册的选择器,第二个参数指定选择器需要查询的通道操作。
  2. 可以供选择器查询的通道操作,类型上分为以下四种。
    1. SelectionKey.OP_READ  可读
    2. SelectionKey.OP_WRITE 可写
    3. SelectionKey.OP_CONNECT 连接
    4. SelectionKey.OP_ACCEPT 接收
    5. 通过“位或”操作实现对多通道感兴趣  int key=SelectionKey.OP_READ | SelectionKey.OP_WRITE
  3. 选择器查询的不是通道的操作,而是通道某个操作的一种就绪状态。就是说,一旦通道具备完成某个操作的条件,就标识该通道的这个操作依据就绪,就可以被selector查询到,程序就可以对通道进行对应的操作。如:某个SocketChannel可以连接到一个服务器,则处于连接就绪

选择键 SelectionKey

  1. channel注册到selector后,并且一旦处于某种就绪状态,就可以被选择器查询到,这个功能使用Selector的select()方法完成。
  2. selector可以不断查询channel中发生的操作的就绪状态,并且挑选感兴趣的操作就绪状态,一旦通道有操作的就绪状态达成,并且是selector感兴趣的操作,就会被selector选择,放入选择键集合中。
  3. 一个选择键,首先包含了注册在selector的通道操作类型,比如可读状态,也包含了特定的通道与特定选择器之间的注册关系。
  4. 选择键的概念,和事件的概念比较相似,差别是事件是触发模式,选择键是主动查询模式。

Selector的使用

  1. 创建选择器 Selector selector = Selector.open();
  2. 注册channel 到选择器
    1. 获取通道 ServerSocketChannel ssc = ServerSocketChannel.open();
    2. 设置为非阻塞模式  ssc.configureBlocking(false);
    3. 绑定连接  ssc.bind(new InetSocketAddress("127.0.0.1", 9999));
    4. 将通道注册到选择器上,并制定监听事件 ssc.register(selector, SelectionKey.OP_ACCEPT);
  3. 轮询查询就绪操作
    1.  selector.select(); 可以查询已经就绪的通道操作,这些就绪的状态集合,存在一个元素是SelectionKey的Set集合中。 循环调用这个方法,监测通道的就绪状态
    2. 几个重载的select()方法:
      1. select():阻塞方法,有至少一个通道在注册的事件上就绪了;返回值为int类型,表示有多少个通道就绪了。(更准确的说是,前一次select()方法到这一次select()方法之间的时间段内,有多少通道变成就绪状态。)一旦调用select()方法返回值不为0时,在Selector中有一个selectedKeys()方法,用来访问选择键集合
      2. select(long timeout):长阻塞时间为timeouthaomia
      3. selectNow():非阻塞,只要有通道就绪就立刻返回。
    3. 遍历selectedKeys()方法返回的set集合,获取就绪的channel集合
    4. 遍历channel集合,判断就绪事件类型,实现具体业务操作
    5. 根据业务,是否需要再次注册监听事件,重复执行上述操作。
  4. 停止选择的方法
    1. 选择器执行选择的过程,系统底层会一次询问每个 通道是否就绪,这个过程可能会造成调用线程进入阻塞状态,有以下几种方式可以唤醒在select()方法中阻塞的线程。
    2. wakeup():调用Selector对象的wakeup()方法让在阻塞状态下的select()方法立刻返回
    3. close()方法:通过该方法关闭Selector,这个方法使任何一个在选择操作中阻塞的线程都被唤醒,同时使得注册到这个Selector上的所有channel被注销,所有的键被取消,但是channel本身不会被关闭。

注意:与selector 一起使用时,channel必须处于非阻塞模式,否则将抛出异常IllegalBlockingModeExcption;因此FileChannel不能和selector一起使用,因为FileChannel不能切换到非阻塞模式,而套接字相关的通道都可以

一个通道并不是要支持所有四种操作,如服务器通道ServerSocketChannel支持ACCEPT操作,而SocketChannel不支持。可以通过通道上的validOps()方法,获取特定通道下支持的操作集合。

NIO实现服务端和客户端的连接

服务端

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
 */

package IO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class TestSeverSocketChannel {
    public static void main(String[] args) throws IOException, InterruptedException {
        // 创建TCP服务端的管道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        // 默认管道是阻塞的,因此要手动设置为非阻塞模式
        ssc.configureBlocking(false);
        // 绑定对应的ip 端口号
        ssc.bind(new InetSocketAddress("127.0.0.1", 9999));
        // 创建选择器(多路复用器)
        Selector selector = Selector.open();
        //将管道注册到多路复用器上 --->参数1,多路复用器,参数2.多路复用器的监听服务端接收客户端的状态(此时没有状态)
        // 如果接收到了客户端,此时管道会变为该状态
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            // 选择管道的状态   没有状态会阻塞,有状态会继续执行
            selector.select();
            System.out.println("------------------");
            // 获取所有被监听到的状态
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 获取迭代器,方便后续操作,当然也可以用foreach遍历
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey next = iterator.next();
                // 服务端接收到了客户端,Accept状态被监听到
                if (next.isAcceptable()) {
                    System.out.println(next.isAcceptable());
                    SocketChannel accept = ssc.accept();// 这个管道是客户端管道,可以理解是在服务端的这头
                    accept.configureBlocking(false);// 设置客户端的管道在服务端这头为非阻塞
                    //System.out.println(accept);
                    // 将客户端管道注册到多路复用器,监听管道中内容的状态
                    accept.register(selector, SelectionKey.OP_READ);
                } else if (next.isReadable()) { // 管道中有数据
                    // 可读状态时,获取被选中的管道
                    SelectableChannel sc = next.channel();
                    // 通过客户端管道接收数据,放入缓存中
                    ByteBuffer allocate = ByteBuffer.allocate(1024);
                    ((SocketChannel) sc).read(allocate);
                    byte[] array = allocate.array();
                    String s = new String(array, 0, allocate.position());
                    System.out.println(s);

                    sc.register(selector, SelectionKey.OP_WRITE);// 读完之后监听写的状态

                } else if (next.isWritable()) {
                    Thread.sleep(1000);
                    SelectableChannel sc = next.channel();
                    // 服务端向客户端发送数据
                    ((SocketChannel) sc).write(ByteBuffer.wrap("你好".getBytes()));
                    sc.register(selector, SelectionKey.OP_READ); // 写完之后监听读
                }
                // 去除已经被监听到的状态
                iterator.remove();
            }
        }
    }
}

客户端

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
 */

package IO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class TestSocketChannel {
    public static void main(String[] args) throws IOException, InterruptedException {
        // 创建客户端管道
        SocketChannel sc = SocketChannel.open();
        // 设置为非阻塞
        sc.configureBlocking(false);
        // 创建多路复用器
        Selector selector = Selector.open();
        // 将客户端管道注册到多路复用器中
        sc.register(selector, SelectionKey.OP_CONNECT);
        // 连接服务端
        sc.connect(new InetSocketAddress("127.0.0.1", 9999));
        while (true) {
            // 选择客户端管道的状态
            selector.select();
            // 获取所有被监听到的状态
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 通过迭代器获取所有被监听到的状态,方便后续操作
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey next = iterator.next();
                //判断客户端是否连接了服务端,
                if (next.isConnectable()) {
                    // 此时是挂起状态,并没有真正连接
                    System.out.println(
                        sc); // java.nio.channels.SocketChannel[connection-pending remote=/127.0.0.1:9999]
                    // 让客户端管道挂起的状态变成真正的链接状态
                    if (sc.isConnectionPending()) {
                        sc.finishConnect();
                        System.out.println(sc);
                        // java.nio.channels.SocketChannel[connected local=/127.0.0.1:54278 remote=/127.0.0.1:9999]
                    }
                    // 向服务端输出一句话
                    ByteBuffer wrap = ByteBuffer.wrap("success".getBytes());
                    sc.write(wrap);
                    // 监听客户端管道中有数据的状态
                    sc.register(selector, SelectionKey.OP_READ);

                } else if (next.isReadable()) {
                    // 通过客户端管道接收数据,放入缓存中
                    ByteBuffer allocate = ByteBuffer.allocate(1024);
                    ((SocketChannel) sc).read(allocate);
                    byte[] array = allocate.array();
                    String s = new String(array, 0, allocate.position());
                    System.out.println(s);
                    sc.register(selector, SelectionKey.OP_WRITE); // 读完监听写
                    // 想服务端写数据

                } else if (next.isWritable()) {
                    Thread.sleep(1000);
                    sc.write(ByteBuffer.wrap("nihao".getBytes()));
                    sc.register(selector, SelectionKey.OP_READ);// 写完监听读
                }
            }
        }
    }
}

Pipe管道

Java NIO管道是两个线程之间的单向数据连接,Pipe中有一个source通道和一个sink通道,数据会被写到sink通道中,然后从source通道中读取。

创建管道 Pipe pipe = Pipe.open();

FileLock 文件锁

  1. 多个程序同时访问、修改一个文件,很容易出现文件数据不同步的问题。给文件加一个锁,同一时间,只能有同一个程序修改某个文件,或者程序都只能只读此文件,就解决了同步问题。
  2. 文件锁是进程级别的,不是线程级别的。无法解决多线程并发访问、修改一个文件的问题。
  3. 文件锁是当前程序所属的JVM实例持有的,一旦获取到文件锁(对文件加锁),要调用release()方法,或者关闭对应的FileChannel对象,或者当JVM退出,才会释放这个锁。
  4. 一旦某个进程对某个文件加锁,在释放这个锁之前,此进程不能在对此文件加锁,就是说JVM实例在同一个文件上的文件锁是不重叠的。

文件锁分类

  • 排他锁:又叫独占锁,对文件加排他锁后,该进程可以对此文件进行读写操作,其他进程不能读写此文件,直到该进程释放文件锁。
  • 共享锁:某个进程对文件加共享锁,其他进程也可以访问该文件,但是都是只读操作。

获取文件锁的方法

  • lock() 对整个文件加锁,默认排他锁
  • lock(long position, long size , boolean shared)  自定义加锁方式,前两个参数指定加锁的部分(可以只对此文件部分内容加锁),第三个参数指定是否为共享锁
  • tryLock() 对整个文件加锁,默认为排他锁
  • tryLock(long position, long size , boolean shared)  自定义加锁方式

lock和 tryLock 的区别:

  • lock是阻塞式的,如果未获取到文件锁,会一直阻塞当前线程,直到获取文件锁
  • tryLock是非阻塞式的,尝试获取文件锁,获取成功就返回锁对象,失败返回null,不会阻塞当前线程。

AIO 

Asynchronous IO:异步非阻塞IO,在执行时,当前线程不会阻塞,调用发起后,不需要等待返回结果(结果是以事件驱动的形式返回),而且也不需要多线程就可以实现多客户端访问。

AIO实现服务端和客户端的连接

服务端

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
 */

package IO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.CountDownLatch;

public class TestAsynchronousSeverSocketChannel {
    public static void main(String[] args) throws IOException, InterruptedException {
        // 计数器
        // CountDownLatch   await() : CountDownLatch数组不等于0 的时候,阻塞
        // countDown(),调用一次,计数器的值-1
        CountDownLatch latch = new CountDownLatch(1);

        // 1 创建服务端AsynchronousServerSocketChannel
        AsynchronousServerSocketChannel assc = AsynchronousServerSocketChannel.open();
        // 2 绑定ip 和 端口号
        assc.bind(new InetSocketAddress("127.0.0.1", 9999));
        // 3 接收客户端
        assc.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
            // 接收成功执行该回调方法
            // 参数1: 客户端的管道
            @Override
            public void completed(AsynchronousSocketChannel result, Object attachment) {
                ByteBuffer allocate = ByteBuffer.allocate(1024);
                result.read(allocate, null, new CompletionHandler<Integer, Object>() {
                    @Override // 成功接收到客户端数据执行
                    public void completed(Integer result, Object attachment) {
                        System.out.println("连接成功");
                        // 先获取ByteBuffer中的数组
                        byte[] array = allocate.array();
                        // 数组转为字符串
                        String s = new String(array, 0, allocate.position());
                        System.out.println(s);
                        // 拿到数据,计数器减一
                        latch.countDown();
                    }

                    @Override // 接收客户端数据失败执行
                    public void failed(Throwable exc, Object attachment) {
                        System.out.println("接收数据失败");
                    }
                });
            }

            // 接收失败,执行该方法
            @Override
            public void failed(Throwable exc, Object attachment) {
                System.out.println("接收失败了");
            }
        });

        latch.await(); // 主线程等待
    }
}

客户端

/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
 */

package IO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.CountDownLatch;

public class TestAsynchronousSocketChannel {
    public static void main(String[] args) throws IOException, InterruptedException {
        CountDownLatch latch = new CountDownLatch(1);
        // 1 创建客户端
        AsynchronousSocketChannel asc = AsynchronousSocketChannel.open();
        // 2 连接服务端
        asc.connect(new InetSocketAddress("127.0.0.1", 9999), null, new CompletionHandler<Void, Object>() {
            // 连接服务端成功,执行该方法
            @Override
            public void completed(Void result, Object attachment) {
                System.out.println("连接成功");
                asc.write(ByteBuffer.wrap("你好".getBytes()));
                latch.countDown();
            }

            // 连接服务端失败,执行该方法
            @Override
            public void failed(Throwable exc, Object attachment) {
                System.out.println("连接失败");
            }
        });
        // 3 阻塞主线程,直到向服务端发送了消息
        latch.await();
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值