IO/NIO/Netty

1.IO

1.IO定义

  • 1.JavaI/O操作主要是指使用Java进行输入输出操作
  • 2.Java所有的I/O操作都是基于数据流进行输入输出,数据流表示字符或者字节数据的流动序列
  • 3.JavaI/O流提供了读写数据的标准方法,Java中表示数据源的对象都会提供数据流读写的方法

2.流定义

  • 1.java中将输入输出抽象称为流,将不同的系统连接起来
  • 2.输入流:将数据从外存中读取到内存
  • 3.输出流:将数据从内存写入到外存

3.流的分类

在这里插入图片描述

  • 1.上述是Java IO流中的四大基本数据流,这四大基流都是抽象类,其他流都是继承于四大基流
  • 2.所有的都实现了java.io.Closeable接口,即都是可关闭的,都有close()方法
  • 3.所有的输出流都实现了java.io.Flushable接口,即都是可刷新的,都有flush()方法
  • 4.流是一个管道,这个是内存和硬盘之间的通道,用完之后一定要关闭,不然会耗费很多资源
  • 5.输出流在最终输出之后,一定要flush()刷新一下,这个刷新表示将管道当中剩余未输出的数据强行输出完,如果没有flush()可能会导致丢失数据
    • 1.因为进行流的操作时,数据先被读取到内存中,然后再把数据写入到文件中
    • 2.如果当数据读取完时调用close()方法关闭读写流,这时就可能造成数据丢失
    • 3.因为读取数据完成时不代表写入数据也一定完成,一部分数据可能会留在缓存区中,flush()则用于强制将缓冲区的数据流输出完

1.输入流和输出流

  • 1.根据流向分为:输入流输出流
  • 2.输入流和输出流是相对于程序而言
    • 1.输出:将程序(内存)中的内容输出到磁盘、光盘等存储设备中
    • 2.输入:读取外部数据(磁盘、光盘等存储设备中的数据)到程序(内存)内

2.字节流和字符流

  • 1.根据传输数据单位分为:字节流字符流
  • 2.字节流:数据流中最小的数据单元是字节
    • 1.按照字节单位读取数据,一次读取1byte,等同于一次读取8bit
    • 2.字节流什么类型的文件都可以读取(文本文件、图片、声音文件、视频文件等)
  • 3.字符流:数据流中最小的数据单元是字符
    • 1.Java中的字符Unicode编码,一个字符占用两个字节(无论中文还是英文都是两个字节)
    • 2.按照字符单位读取数据,一次读取一个字符
    • 3.字符流只能读取纯文本类型的文件,不能读取(图片、声音、视频、word等文件)
    • 4.纯文本文件:能用记事本打开的文件都是普通文本文件(.txt.java.ini.py等)
1.字节流
  • 1.典型实现(FileInputSteamFileOutStream
1.字节输入流:InputStream
  • 1.InputStream抽象类表示输入字节流所有类的父类
  • 2.数据传输单位为字节(8bit)
    在这里插入图片描述
    // 1.创建目标对象,输入流表示文件的数据读取到程序中
    	// 不写盘符,默认该文件是在该项目的根目录下
    	// a.txt 保存的文件内容为:AAaBCDEF
    File target = new File("io"+File.separator+"a.txt");
    // 2.创建输入流对象
    InputStream in = new FileInputStream(target);
    // 3.具体的 IO 操作(读取 a.txt 文件中的数据到程序中)
    /**
     * 注意:读取文件中的数据,读到最后没有数据时,返回-1
     *  int read():读取一个字节,返回读取的字节
     *  int read(byte[] b):读取多个字节,并保存到数组 b 中,从数组 b 的索引为 0 的位置开始存储,返回读取了几个字节
     *  int read(byte[] b,int off,int len):读取多个字节,并存储到数组 b 中,从数组b 的索引为 0 的位置开始,长度为len个字节
     */
    int data1 = in.read();	// 获取 a.txt 文件中的数据的第一个字节
    System.out.println((char)data1);	// A
    
    byte[] buffer  = new byte[10];
    in.read(buffer);	// 获取 a.txt 文件中的前10 个字节,并存储到 buffer 数组中
    System.out.println(Arrays.toString(buffer));	// [65, 97, 66, 67, 68, 69, 70, 0, 0, 0]
    System.out.println(new String(buffer));	// AaBCDEF[][][]
           
    in.read(buffer, 0, 3);
    System.out.println(Arrays.toString(buffer));	// [65, 97, 66, 0, 0, 0, 0, 0, 0, 0]
    System.out.println(new String(buffer));	// AaB[][][][][][][]
    //4、关闭流资源
    in.close();
    
2.字节输出流:OutputStream
  • 1.OutputStream抽象类表示字节输出流所有类的父类
  • 2.数据传输单位为字节(8bit)
    在这里插入图片描述
    //1.创建目标对象,输出流表示把数据保存到哪个文件
    	//不写盘符,默认该文件是在该项目的根目录下
    File target = new File("io"+File.separator+"a.txt");
    //2.创建文件的字节输出流对象,第二个参数是 Boolean 类型,true 表示后面写入的文件追加到数据后面,false 表示覆盖
    OutputStream out = new FileOutputStream(target,true);
    //3、具体的 IO 操作(将数据写入到文件 a.txt 中)
    /**
     * void write(int b):把一个字节写入到文件中
     * void write(byte[] b):把数组b 中的所有字节写入到文件中
     * void write(byte[] b,int off,int len):把数组b 中的从 off 索引开始的 len 个字节写入到文件中
     */
    out.write(65); // 将 A 写入到文件中
    out.write("Aa".getBytes()); // 将 Aa 写入到文件中
    out.write("ABCDEFG".getBytes(), 1, 5); // 将 BCDEF 写入到文件中
    //经过上面的操作,a.txt 文件中数据为 AAaBCDEF
    //4、关闭流资源
    out.close();
    System.out.println(target.getAbsolutePath());
    
3.字节流完成文件的复制
  • 1.注意:缓冲区buffer的大小设置可能会造成读取一半从而导致乱码
    /**
     * 将 a.txt 文件 复制到 b.txt 中
     */
    //1.创建源和目标
    File srcFile = new File("io"+File.separator+"a.txt");
    File descFile = new File("io"+File.separator+"b.txt");
    //2.创建输入输出流对象
    InputStream in = new FileInputStream(srcFile);
    OutputStream out = new FileOutputStream(descFile);
    //3.读取和写入操作
    byte[] buffer = new byte[10];//创建一个容量为 10 的字节数组,存储已经读取的数据
    int len = -1;//表示已经读取了多少个字节,如果是 -1,表示已经读取到文件的末尾
    while((len=in.read(buffer))!=-1){
    //打印读取的数据
    System.out.println(new String(buffer,0,len));
    //将 buffer 数组中从 0 开始,长度为 len 的数据读取到 b.txt 文件中
    out.write(buffer, 0, len);
    }
    //4、关闭流资源
    out.close();
    in.close();
    
2.字符流
  • 1.典型实现(FileReaderFileWriter
  • 2.使用字节流操作汉字或特殊符号语言的时候容易乱码,因为汉字不止一个字节,为了解决这个问题建议使用字符流
  • 3.一般可以用记事本打开的文件,内容不乱码的就是文本文件,可以使用字符流,而操作二进制文件(比如图片、音频、视频)必须使用字节流
1.字符输入流:Reader
  • 1.Reader抽象类表示输入字符流所有类的父类
  • 2.数据传输单位为字符
    在这里插入图片描述
    //1.创建源
    File srcFile = new File("io"+File.separator+"a.txt");
    //2.创建字符输出流对象
    Reader in = new FileReader(srcFile);
    //3.具体的 IO 操作
     /**
      * int read():每次读取一个字符,读到最后返回 -1
      * int read(char[] buffer):将字符读进字符数组,返回结果为读取的字符数
      * int read(char[] buffer,int off,int len):将读取的字符存储进字符数组 buffer,返回结果为读取的字符数,从索引 off 开始,长度为 len
      */
    int len = -1;//定义当前读取字符的数量
    while((len = in.read())!=-1){
        //打印 a.txt 文件中所有内容
        System.out.print((char)len);
    }
    
    char[] buffer = new char[10]; //每次读取 10 个字符
    while((len=in.read(buffer))!=-1){
        System.out.println(new String(buffer,0,len));
    }
    
    while((len=in.read(buffer,0,10))!=-1){
        System.out.println(new String(buffer,0,len));
    }
    //4、关闭流资源
    in.close();
    
2.字符输出流:Writer
  • 1.Writer抽象类表示输出字符流所有类的父类
  • 2.数据传输单位为字符
    在这里插入图片描述
    //1.创建源
    File srcFile = new File("io"+File.separator+"a.txt");
    //2.创建字符输出流对象
    Writer out = new FileWriter(srcFile);
    //3.具体的 IO 操作
     /***
      * void write(int c):向外写出一个字符
      * void write(char[] buffer):向外写出多个字符 buffer
      * void write(char[] buffer,int off,int len):把 buffer 数组中从索引 off 开始到 len个长度的数据写出去
      * void write(String str):向外写出一个字符串
      */
    
    out.write(65);//将 A 写入 a.txt 文件中
    
    out.write("Aa帅锅".toCharArray());//将 Aa帅锅 写入 a.txt 文件中
    
    out.write("Aa帅锅".toCharArray(),0,2);//将 Aa 写入a.txt文件中
    
    out.write("Aa帅锅");//将 Aa帅锅 写入 a.txt 文件中
     
    //4、关闭流资源
    /***
     * 注意如果这里有一个缓冲的概念,如果写入文件的数据没有达到缓冲的数组长度,那么数据是不会写入到文件中的
     * 解决办法:手动刷新缓冲区 flush()
     * 或者直接调用close()方法,这个方法会默认刷新缓冲区
     */
    out.flush();
    out.close();
    
    //FileWriter写数据之换行和追加写
    //1.数据的换行
    //  \n可以实现换行,但是windows系统自带的记事本打开并没有换行,因为windows识别的换行不是\n,而是\r\n
    //	例:fw.write("\r\n");
    //	windows:\r\n
    //	Linux:\n
    //	Mac:\r
    //2.数据的追加写入
    //	FileWriter(String fileName,boolean append)
    //	FileWriter fw = new FileWriter("a.txt",true);   //true表示追加写入,默认是false覆盖
    
3.用字符流完成文件的复制
/**
* 将 a.txt 文件复制到 b.txt 中
*/
//1.创建源和目标
File srcFile = new File("io"+File.separator+"a.txt");
File descFile = new File("io"+File.separator+"b.txt");
//2.创建字符输入输出流对象
Reader in = new FileReader(srcFile);
Writer out = new FileWriter(descFile);
//3.读取和写入操作
char[] buffer = new char[10];	//创建一个容量为 10 的字符数组,存储已经读取的数据
int len = -1;	//表示已经读取了多少个字节,如果是 -1,表示已经读取到文件的末尾
while((len=in.read(buffer))!=-1){
   out.write(buffer, 0, len);
}
//4.关闭流资源
out.close();
in.close();

3.节点流和包装流

  • 1.根据功能分为:节点流包装流
  • 2.节点流:可以从或向一个特定的地方(节点)读写数据,直接连接数据源
  • 3.包装流:并不直接连接数据源,而是对一个已存在的流的连接和封装
    • 1.一种典型的装饰器设计模式包装流隐藏了底层节点流的差异,主要是为了更方便的执行输入输出
    • 2.一个流对象经过其他流的多次包装,称为流的链接
    • 3.一个IO流可以即是输入流又是字节流又或是以其他方式分类的流类型,不同分类的流是同级关系
    • 4.关闭包装流的时候,只需要关闭包装流即可

4.特别类型的流

  • 1.转换流
    • 1.将字节流转换为字符流
    • 2.转换流只有字节流转换为字符流,因为字符流处理文本更方便
  • 2.缓冲流
    • 1.关键字Buffered,同时也是一种包装流,其包装的流增加了缓冲功能,提高了输入输出的效率
    • 2.增加缓冲功能后需要使用flush()才能将缓冲区中内容写入到实际的物理节点
    • 3.现在版本的Java只需调用close()方法,就会自动执行输出流的flush()方法,可以保证将缓冲区中内容全部输出
  • 3.对象流
    • 1.关键字Object
    • 2.主要用于将目标对象保存到磁盘中或允许在网络中直接传输对象时使用(对象序列化
1.转换流
  • 1.将字节流转换为字符流
  • 2.InputStreamReader:将字节输入流转换为字符输入流
  • 3.OutputStreamWriter:将字节输出流转换为字符输出流
1.用转换流进行文件的复制
/**
 * 将 a.txt 文件复制到 b.txt 中
 */
//1.创建源和目标
File srcFile = new File("io"+File.separator+"a.txt");
File descFile = new File("io"+File.separator+"b.txt");
//2.创建字节输入输出流对象
InputStream in = new FileInputStream(srcFile);
OutputStream out = new FileOutputStream(descFile);
//3、创建转换输入输出对象
Reader rd = new InputStreamReader(in);
Writer wt = new OutputStreamWriter(out);
//3、读取和写入操作
char[] buffer = new char[10];//创建一个容量为 10 的字符数组,存储已经读取的数据
int len = -1;//表示已经读取了多少个字符,如果是 -1,表示已经读取到文件的末尾
while((len=rd.read(buffer))!=-1){
	wt.write(buffer, 0, len);
}
//4、关闭流资源
rd.close();
wt.close();
2.转换流和子类区别
  • 1.OutputStreamWriterInputStreamReader字符流字节流的桥梁
  • 2.字符转换流原理:字节流+编码表
  • 3.FileReader继承自InputStreamReader
  • 4.FileWriter继承自OutputStreamWriter
  • 5.FileWriterFileReader:作为子类仅作为操作字符文件的便捷类存在,当操作的字符文件,使用的是默认编码表时可以不用父类,而直接用子类完成操作
    InputStreamReader isr = new InputStreamReader(new FileInputStream("a.txt"));//默认字符集
    InputStreamReader isr = new InputStreamReader(new FileInputStream("a.txt"),"GBK");//指定GBK字符集
    FileReader fr = new FileReader("a.txt");
    
  • 6.上述这三种代码的功能是一样的,其中第三句最为便捷
  • 7.注意:一旦要指定其他编码时,必须使用字符转换流,不能使用子类
  • 8.使用子类的时机:
    • 1.操作的是字符文件
    • 2.使用默认编码
2.缓冲流
  • 1.目的是使用缓冲区加快读取和写入数据的速度
  • 2.字节缓冲流:BufferedInputStreamBufferedOutputStream
  • 3.字符缓冲流:BufferedReaderBufferedWriter
  • 4.IO操作时通常会定义一个字节或字符数组,将读取/写入的数据先存放到数组中,当内部定义的数组满了,就会进行下一步操作
  • 5.缓冲流的JDK底层源码可以看到,程序中定义了大小为 8192的缓存数组
  • 6.设置缓冲区的大小不要随便设置,要么就设置成8192的整数倍,要么就用默认值
  • 7.因为WindowsLinux都使用4KB的内存页面大小,因此BufferedReader上的默认缓冲区将恰好占用2页
    在这里插入图片描述
    在这里插入图片描述
    //字节缓冲输入流
    BufferedInputStream bis = new BufferedInputStream(
         new FileInputStream("io"+File.separator+"a.txt"));
    //定义一个字节数组,用来存储数据
    byte[] buffer = new byte[1024];
    int len = -1;//定义一个整数,表示读取的字节数
    while((len=bis.read(buffer))!=-1){
        System.out.println(new String(buffer,0,len));
    }
    //关闭流资源
    bis.close();
    
    //字节缓冲输出流
    BufferedOutputStream bos = new BufferedOutputStream(
         new FileOutputStream("io"+File.separator+"a.txt"));
    bos.write("ABCD".getBytes());
    bos.close();
    
    //字符缓冲输入流
    BufferedReader br = new BufferedReader(
            new FileReader("io"+File.separator+"a.txt"));
    char[] buffer = new char[10];
    int len = -1;
    while((len=br.read(buffer))!=-1){
        System.out.println(new String(buffer,0,len));
    }
    br.close();
    
    //字符缓冲输出流
    BufferedWriter bw = new BufferedWriter(
            new FileWriter("io"+File.separator+"a.txt"));
    bw.write("ABCD");
    bw.close();
    
3.数组流
  • 1.将数据先临时存在数组中也就是内存
  • 2.所以关闭数组流是无效的,关闭后还是可以调用这个类的方法,底层源码的close()是一个空方法
  • 2.字节数组流ByteArrayOutputStreamByteArrayInputStream
  • 2.字符数组流CharArrayReaderCharArrayWriter
  • 3.字符串流StringReaderStringWriter(将数据临时存储到字符串中)
    在这里插入图片描述
1.字节数组流
//字节数组输出流:程序->内存
ByteArrayOutputStream bos = new ByteArrayOutputStream();
//将数据写入到内存中
bos.write("ABCD".getBytes());
//创建一个新分配的字节数组。 其大小是此输出流的当前大小,缓冲区的有效内容已被复制到其中
byte[] temp = bos.toByteArray();
System.out.println(new String(temp,0,temp.length));

byte[] buffer = new byte[10];
///字节数组输入流:内存---》程序
ByteArrayInputStream bis = new ByteArrayInputStream(temp);
int len = -1;
while((len=bis.read(buffer))!=-1){
   System.out.println(new String(buffer,0,len));
}

//这里不写也没事,因为源码中的 close()是一个空的方法体
bos.close();
bis.close();
2.字符数组流
//字符数组输出流
CharArrayWriter caw = new CharArrayWriter();
caw.write("ABCD");
//返回内存数据的副本
char[] temp = caw.toCharArray();
System.out.println(new String(temp));

//字符数组输入流
CharArrayReader car = new CharArrayReader(temp);
char[] buffer = new char[10];
int len = -1;
while((len=car.read(buffer))!=-1){
   System.out.println(new String(buffer,0,len));
}
3.字符串流
//字符串输出流,底层采用 StringBuffer 进行拼接
StringWriter sw = new StringWriter();
sw.write("ABCD");
sw.write("帅锅");
System.out.println(sw.toString());//ABCD帅锅

//字符串输入流
StringReader sr = new StringReader(sw.toString());
char[] buffer = new char[10];
int len = -1;
while((len=sr.read(buffer))!=-1){
   System.out.println(new String(buffer,0,len));//ABCD帅锅
}
4.合并流
  • 1.将多个输入流合并为一个流
  • 2.也称为顺序流,因为读取的时候是依次读取
    在这里插入图片描述
    //定义字节输入合并流
    SequenceInputStream seinput = new SequenceInputStream(
            new FileInputStream("io/a.txt"), new FileInputStream("io/b.txt"));
    byte[] buffer = new byte[10];
    int len = -1;
    while((len=seinput.read(buffer))!=-1){
        System.out.println(new String(buffer,0,len));
    } 
    seinput.close();
    
5.对象流
  • 1.ObjectOutputStream:通过writeObject()方法做序列化操作
  • 2.ObjectInputStream:通过readObject()方法做反序列化操作
1.对象流实现序列化和反序列化
  • 1.打开a.txt文件,里面是Person对象的二进制文件
  • 2.反序列化的对象必须要提供该对象的字节码文件,且需要实现Serializable
    在这里插入图片描述
    // 第一步:创建一个 JavaBean 对象
    public class Person implements Serializable{
        private String name;
        private int age;
         
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
        @Override
        public String toString() {
            return "Person [name=" + name + ", age=" + age + "]";
        }
        public Person(String name, int age) {
            super();
            this.name = name;
            this.age = age;
        }
    }  
    
    //第二步:使用 ObjectOutputStream 对象实现序列化
    OutputStream op = new FileOutputStream("io"+File.separator+"a.txt");
    ObjectOutputStream ops = new ObjectOutputStream(op);
    ops.writeObject(new Person("vae",1));
    ops.close();
    
    //第三步:使用ObjectInputStream 对象实现反序列化
    InputStream in = new FileInputStream("io"+File.separator+"a.txt");
    ObjectInputStream os = new ObjectInputStream(in);
    byte[] buffer = new byte[10];
    int len = -1;
    Person p = (Person) os.readObject();
    System.out.println(p);  //Person [name=vae, age=1]
    os.close();
    
6.数据流
  • 1.可以将数据连同数据的类型一并写入文件
  • 2.java.io.DataInputStream数据字节输入流
  • 3.java.io.DataOutputStream数据字节输出流(可。)
  • 4.DataOutputStream写的文件,只能使用DataInputStream去读,并且读取是需要按写入的顺序
    // 创建数据专属的字节输出流
    DataOutputStream dos = new DataOutputStream(new >	FileOutputStream("data"));
    // 写数据
    byte b = 100;
    short s = 200;
    int i = 300;
    long l = 400L;
    float f = 3.0F;
    double d = 3.14;
    boolean sex = false;
    char c = 'a';
    // 写
    dos.writeByte(b); // 把数据以及数据的类型一并写入到文件当中。
    dos.writeShort(s);
    dos.writeInt(i);
    dos.writeLong(l);
    dos.writeFloat(f);
    dos.writeDouble(d);
    dos.writeBoolean(sex);
    dos.writeChar(c);
    
    // 刷新
    dos.flush();
    // 关闭最外层
    dos.close();
    
    //读
    DataInputStream dis = new DataInputStream(new FileInputStream("data"));
    // 开始读
    byte b = dis.readByte();
    short s = dis.readShort();
    int i = dis.readInt();
    long l = dis.readLong();
    float f = dis.readFloat();
    double d = dis.readDouble();
    boolean sex = dis.readBoolean();
    char c = dis.readChar();
    
    dis.close();
    

4.流的整体架构图

在这里插入图片描述

5.操作IO流步骤

  • 1.创建源或目标对象
    • 1.输入:将文件中的数据流向到程序中,此时文件是源,程序是目标
    • 2.输出:将程序中的数据流向到文件中,此时文件是目标,程序是源
  • 2.创建IO流对象
    • 1.输入:创建输入流对象
    • 2.输出:创建输出流对象
  • 3.具体的IO操作
  • 4.关闭资源
    • 1.输入:输入流的close()方法
    • 2.输出:输出流的close()方法
  • 5.注意:
    • 1.程序中打开的IO资源文件不属于内存里的资源,垃圾回收机制无法回收该资源
    • 2.如果不关闭该资源,那么磁盘的文件将一直被程序引用着,不能删除也不能更改,所以应该手动调用close()方法关闭流资源

6.标准输出流

  • 1.java.io.PrintWriter:标准输出字符流,默认输出到控制台
  • 2.java.io.PrintStream:标准输出字节流,默认输出到控制台
  • 3.标准输出流不需要手动close()关闭,可以自动关闭
  • 4.改变标准输出流的输出方向 System.setOut(PrintStream对象)
    // 可以改变标准输出流的输出方向
    // 标准输出流不再指向控制台,指向log文件
    PrintStream printStream = new PrintStream(new FileOutputStream("log"));
    // 修改输出方向,将输出方向修改到log文件
    System.setOut(printStream);// 修改输出方向
    // 再输出
    System.out.println("hello world");
    System.out.println("hello kitty");
    System.out.println("hello zhangsan");
    
    public final class System {
    	...
    	public final static InputStream in = null;	//标准字节输入流(控制台输入)  
    	public final static PrintStream out = null;	//标准字节输出流(控制台输出)
    	public final static PrintStream err = null;	//标准字节错误流(控制台输出)  
    	...
    }  
    
  • 5.注意
    • 1.System类不能创建对象,只能通过类名调用三个静态标准数据流(构造函数为private

7.File类

  • 1.File类提供了文件和目录的操作方法
  • 2.File类只能操作文件的属性,不能操作文件的内容,通过IO流可以更改

1.File的分隔符

在这里插入图片描述

  • 1.各个平台之间的路径分隔符是不一样的
  • 2.为了屏蔽各个平台之间的分隔符差异,构造File类的时使用Java提供的分隔符字段
    • 1.File.pathSeparator:指分隔连续多个路径字符串的分隔符,如java -cp test.jar; abc.jar HelloWorld中指;
    • 2.File.separator:指分隔同一个路径字符串中目录的分隔符,如C:\Program Files\Common中指\
      // windows平台
      System.out.println(File.pathSeparator);//输出 ;
      System.out.println(File.pathSeparatorChar);//输出 ;
      System.out.println(File.separator);//输出 \ 
      System.out.println(File.separatorChar);//输出 \ 
      

2.File的构造方法

在这里插入图片描述

  • 1.windows系统下指定文件路径分隔符时,可以用/或者\\,两者效果相同
  • 2.创建一个文件时,如果目录下有同名文件将被覆盖
    //1.不使用 Java 提供的分隔符字段,注意:这样写只能在 Windows 平台有效
    File f1 = new File("D:\\IO\\a.txt");//或者是D:/IO/a.txt
    
    //2.使用 Java 提供的分隔符
    File f2 = new File("D:"+File.separator+"IO"+File.separator+"a.txt");
    System.out.println(f1);//输出 D:\IO\a.txt  
    System.out.println(f2);//输出 D:\IO\a.txt
    
    //3.File(File parent, String child)
    //从父抽象路径名和子路径名字符串创建新的 File实例。
    File f3 = new File("D:");
    File f4 = new File(f3,"IO");
    System.out.println(f4); //D:\IO
    
    //4.File(String pathname)
    //通过将给定的路径名字符串转换为抽象路径名来创建新的 File实例。
    File f5 = new File("D:"+File.separator+"IO"+File.separator+"a.txt");
    System.out.println(f5); //D:\IO\a.txt
    
    //5.File(String parent, String child)
    //从父路径名字符串和子路径名字符串创建新的 File实例。
    File f6 = new File("D:","IO\\a.txt");
    System.out.println(f6); //D:\IO\a.txt
    

3.File的常用方法

  • 1.创建
    • 1.boolean createNewFile():创建成功返回true,失败返回false
    • 2.boolean mkdir():创建目录,如果上一级目录不存在,则会创建失败
    • 3.boolean mkdirs():创建多级目录,如果上一级目录不存在也会自动创建
  • 2.删除
    • 1.boolean delete():删除文件或目录,如果表示目录,则目录下必须为空才能删除
    • 2.boolean deleteOnExit():文件使用完成后删除
  • 3.判断
    • 1.boolean canExecute():判断文件是否可执行
    • 2.boolean canRead():判断文件是否可读
    • 3.boolean canWrite():判断文件是否可写
    • 4.boolean exists():判断文件或目录是否存在
    • 5.boolean isDirectory() :判断此路径是否为一个目录
    • 6.boolean isFile():判断是否为一个文件
    • 7.boolean isHidden():判断是否为隐藏文件
    • 8.boolean isAbsolute():判断是否是绝对路径,文件不存在也能判断
  • 4.获取
    • 1.String getName():获取此路径表示的文件或目录名称
    • 2.String getPath():将此路径名转换为路径名字符串
    • 3.String getAbsolutePath():返回此抽象路径名的绝对形式
    • 4.String getParent():如果没有父目录返回null
    • 5.long lastModified():获取最后一次修改的时间
    • 6.long length():返回由此抽象路径名表示的文件的长度
    • 7.boolean renameTo(File f):重命名由此抽象路径名表示的文件
    //File(File parent, String child)
            //从父抽象路径名和子路径名字符串创建新的 File实例。
            File dir = new File("D:"+File.separator+"IO");
            File file = new File(dir,"a.txt");
             
            //判断dir 是否存在且表示一个目录
            if(!(dir.exists()||dir.isDirectory())){
                //如果 dir 不存在,则创建这个目录
                dir.mkdirs();
                //根据目录和文件名,创建 a.txt文件
                file.createNewFile();
     
            }
            //返回由此抽象路径名表示的文件或目录的名称。 这只是路径名称序列中的最后一个名字。 如果路径名的名称序列为空,则返回空字符串。
            System.out.println(file.getName()); //a.txt
            //返回此抽象路径名的父null的路径名字符串,如果此路径名未命名为父目录,则返回null。
            System.out.println(file.getParent());//D:\IO
            //将此抽象路径名转换为路径名字符串。 结果字符串使用default name-separator character以名称顺序分隔名称。
            System.out.println(file.getPath()); //D:\IO\a.txt
    
    //打印给定目录下的所有文件夹和文件夹里面的内容
    public static void getFileList(File file){
            //第一级子目录
            File[] files = file.listFiles();
            for(File f:files){
             	//打印目录和文件
    	        System.out.println(f);
                if(f.isDirectory()){
                    getFileList(f);
                }
            }
     }
    public static void main(String[] args) throws Exception {
         File f = new File("D:"+File.separator+"WebStormFile");
         getFileList(f);
    }
    

2.IO底层原理

1.读写原理

  • 1.程序进行IO读写操作,依赖于底层的IO读写,基本上都会用到底层的readwrite两大系统调用
  • 2.不同的操作系统中,IO读写调用的名称可能不完全一样,但基本功能是一样的
    • 1.read系统调用,并不是直接从物理设备数据读取到内存
    • 2.write系统调用,也不是直接把数据内存写入到物理设备
  • 3.上层应用无论是调用操作系统的read还是write,都会涉及缓冲区
    • 1.调用操作系统的read是把数据内核缓冲区复制到进程缓冲区
    • 2.调用操作系统的write是把数据从进程缓冲区复制到内核缓冲区
  • 4.上层应用的IO操作,实际上不是物理设备级别的读写,而是缓存的复制
  • 5.readwrite两大系统调用,都不负责数据内核缓冲区物理设备(如磁盘)之间的交换,而是由操作系统内核Kernel)来完成
  • 6.用户程序中,无论是SocketIO,还是文件IO都属于上层应用的开发,其输入(Input)和输出(Output)的处理,编程的流程上都是一致的

1.内核缓冲区与进程缓冲区

  • 1.缓冲区的目的是减少频繁地与设备之间的物理交换
    • 1.外部设备直接读写涉及操作系统的中断,发生系统中断时需要保存之前的进程数据和状态等信息,结束中断之后还需要恢复之前的进程数据和状态等信息
    • 2.为了减少这种底层系统的时间,性能的损耗,于是出现了内存缓冲区
    • 3.上层应用使用read系统调用时,仅仅把数据从内核缓冲区复制到上层应用的缓冲区(进程缓冲区
    • 4.上层应用使用write系统调用时,仅仅把数据从进程缓冲区复制到内核缓冲区
    • 5.底层操作会对内核缓冲区进行监控,等待缓冲区达到一定数量的时候,再进行IO设备的中断处理,集中执行物理设备的实际IO操作
    • 6.这种机制提升了系统的性能,至于什么时候中断(读中断、写中断),由操作系统的内核来决定
  • 2.从数量上来说
    • 1.Linux系统中,操作系统内核只有一个内核缓冲区
    • 2.每个用户程序(进程)都有独立的缓冲区(进程缓冲区)
    • 3.所以用户程序的IO读写操作,大多数情况下并没有进行实际的IO操作,而是在进程缓冲区内核缓冲区之间直接进行数据的交换

2.典型的系统调用流程

在这里插入图片描述

  • 1.read系统调用完整输入流程的两个阶段
    • 1.等待数据准备好
    • 2.从内核进程复制数据
  • 2.如果read一个socket(套接字)则以上两阶段的具体处理流程如下
    • 1.第一个阶段:等待数据从网络中到达网卡,当所等待的分组到达时被复制到内核中的某个缓冲区,这个工作由操作系统自动完成,用户程序无感知
    • 2.第二个阶段:将数据从内核缓冲区复制到应用进程缓冲区
  • 3.如果Java服务器端,完成一次socket请求和响应完整的流程如下
    • 1.客户端请求Linux通过网卡读取客户端的请求数据,将数据读取到内核缓冲区
    • 2.获取请求数据Java服务器通过read系统调用,从Linux内核缓冲区读取数据,再送入Java进程缓冲区
    • 3.服务器端业务处理Java服务器在自己的用户空间中处理客户端的请求
    • 4.服务器端返回数据Java服务器完成处理后构建好响应数据,将这些数据从用户缓冲区写入内核缓冲区,这里使用write系统调用
    • 5.发送给客户端Linux内核通过网络IO,将内核缓冲区中的数据写入网卡,网卡通过底层的通信协议会将数据发送给目标客户端

2.四种主要的IO模型

1.同步阻塞IO(Blocking IO,BIO)

  • 1.阻塞IO:指需要内核IO操作彻底完成后才返回到用户空间执行用户程序的操作指令
  • 2.阻塞:指用户程序(发起IO请求的进程或线程)的执行状态
  • 3.传统的IO模型都是阻塞IO模型,并且Java中默认创建的socket都属于阻塞IO模型
  • 4.同步与异步是发起IO请求的两种方式
    • 1.同步IO:指用户空间(进程或线程)是主动发起IO请求的一方,系统内核是被动接收方
    • 2.异步IO:指系统内核是主动发起IO请求的一方,用户空间(进程或线程)是被动接收方
  • 5.总结同步阻塞IO指的是用户空间(进程或者线程)主动发起,需要等待内核IO操作彻底完成后才返回用户空间的IO操作,此IO操作过程中发起IO请求的用户空间(进程或者线程)处于阻塞状态
1.流程图

在这里插入图片描述

  • 1.默认情况Java应用程序进程中所有对socket连接进行的IO操作都是同步阻塞IO
  • 2.阻塞式IO模型中,Java应用程序发起IO系统调用开始一直到系统调用返回,这段时间内发起IO请求Java程序进程或线程)是阻塞状态,直到返回成功后应用程序才能开始处理用户空间的缓冲区数据
  • 3.Java中发起一个socketread操作的系统调用流程如下
    • 1.从Java进行IO读后发起read系统调用开始,用户程序(进程或线程)就进入阻塞状态
    • 2.当系统内核收到read系统调用后就开始准备数据,一开始数据可能还没有到达内核缓冲区(还没有收到一个完整的socket数据包),这时内核需要等待
    • 3.内核一直等到完整的数据到达,就会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存),然后内核返回结果(返回复制到用户缓冲区中的字节数)
    • 4.直到内核返回后用户线程才会解除阻塞状态重新运行起来
  • 4.阻塞IO的特点:在内核执行IO操作的两个阶段,发起IO请求的用户程序(进程或线程)被阻塞
2.优缺点
  • 1.阻塞IO的优点:应用程序开发简单;阻塞等待数据期间,用户线程挂起基本不会占用CPU资源
  • 2.阻塞IO的缺点
    • 1.一般情况下会为每个连接配备一个独立的线程,一个线程维护一个连接的IO操作
    • 2.并发量小的情况下没有什么问题,但高并发的应用场景下,阻塞IO模型需要大量的线程来维护大量的网络连接,内存,线程切换开销非常大,性能很低,基本不可用

2.同步非阻塞IO(Non-Blocking IO,NIO)

  • 1.非阻塞IO:指用户空间的程序不需要等待内核IO操作彻底完成,调用后可以立即返回用户空间执行后续的指令
  • 2.发起IO请求的用户程序(进程或线程)处于非阻塞状态,与此同时内核会立即返回给用户一个IO状态值
  • 3.阻塞非阻塞的区别
    • 1.阻塞:指用户程序(进程或线程)一直在等待,而不能做别的事情
    • 2.非阻塞:指用户程序(进程或线程)获得内核返回的状态值就返回用户空间,可以执行后续代码
  • 4.Java非阻塞IOsocket被设置为NONBLOCK模式
  • 5.注意
    • 1.同步非阻塞IO也可以简称为NIO,但它不是Java中的NIOJava中的NIONew IO类库组件归属的不是基础IO模型中的NIO模型,而是IO多路复用模型
  • 6.总结
    • 1.同步非阻塞IO:指用户进程主动发起,不需要等待内核IO操作彻底完成就能立即返回用户空间的IO操作
    • 2.同步非阻塞IO操作过程中,发起IO请求的用户程序(进程或线程)处于非阻塞状态
1.流程图

在这里插入图片描述

  • 1.Linux系统下socket连接默认是阻塞模式,可以将socket连接设置成非阻塞模式
  • 2.NIO模型中,应用程序一旦开始IO系统调用就会出现以下两种情况
    • 1.内核缓冲区中没有数据的情况下,系统调用会立即返回一个调用失败的信息
    • 2.内核缓冲区中有数据的情况下,数据的复制过程中系统调用是阻塞的,直到完成数据从内核缓冲区复制到用户缓冲区,复制完成后系统返回调用成功的信息,用户程序(进程或线程)可以开始处理用户空间的缓冲区数据
  • 3.非阻塞socketread操作的系统调用流程如下
    • 1.内核数据没有准备好的阶段,用户线程发起IO请求后立即返回
    • 2.所以为了读取最终的数据,用户程序(进程或线程)需要不断地发起IO系统调用
    • 3.内核数据到达后,用户程序(进程或线程)发起系统调用,用户程序(进程或线程),内核开始复制数据,将数据从内核缓冲区复制到用户缓冲区,然后内核返回结果(返回复制到的用户缓冲区的字节数)
    • 4.用户程序(进程或线程)读到数据后,才会解除阻塞状态,重新运行起来,用户空间需要经过多次尝试才能保证最终真正读到数据然后继续执行
  • 4.同步非阻塞IO的特点:应用程序的线程需要不断地进行IO系统调用,轮询数据是否已经准备好,如果没有准备好就继续轮询,直到完成IO系统调用为止
2.优缺点
  • 1.同步非阻塞IO的优点:每次发起的IO系统调用在内核等待数据过程中可以立即返回,用户线程不会阻塞,实时性较好
  • 2.同步非阻塞IO的缺点
    • 1.高并发应用场景中,同步非阻塞IO性能很低,基本不可用
    • 2.一般Web服务器都不使用这种IO模型Java中不涉及这种IO模型

3.IO多路复用(IO Multiplexing)

  • 1.为了提高性能,操作系统引入了一种新的系统调用,专门用于查询IO文件描述符(含socket连接)的就绪状态
  • 2.Linux系统中,新的系统调用为select/epoll系统调用
    • 1.通过该系统调用,一个用户程序(进程或线程)可以监视多个文件描述符
    • 2.一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核就能够将文件描述符的就绪状态返回给户程序(进程或线程)
    • 3.用户空间可以根据文件描述符的就绪状态进行相应的IO系统调用
  • 3.IO多路复用属于一种经典的Reactor模式实现,也称为异步阻塞IOJava中的Selector属于这种模型
1.流程图

在这里插入图片描述

  • 1.为了避免同步非阻塞IO模型轮询等待的问题采用了IO多路复用模型

  • 2.目前支持IO多路复用的系统调用有selectepoll

    • 1.几乎所有的操作系统都支持select系统调用,它具有良好的跨平台特性
    • 2.epoll是在Linux 2.6内核中提出的,是select系统调用的Linux增强版本
  • 3.IO多路复用模型中通过select/epoll系统调用

    • 1.单个应用程序的线程可以不断地轮询成百上千的socket连接的就绪状态
    • 2.当某个或某些socket网络连接有IO就绪状态时就返回这些就绪的状态
  • 4.IO多路复用模式的read操作的系统调用流程如下

    • 1.选择器注册
      • 1.首先将需要read操作的目标文件描述符(socket连接)提前注册到Linuxselect/epoll选择器中
      • 2.Java中所对应的选择器类是Selector类,然后开启整个IO多路复用模型轮询流程
    • 2.就绪状态的轮询
      • 1.通过选择器的查询方法,查询所有提前注册过的目标文件描述符(socket连接)的IO就绪状态
      • 2.通过select的系统调用,内核会返回一个就绪的socket列表
      • 3.当任何一个注册过的socket中的数据准备好或就绪,说明内核缓冲区已有数据,内核将该socket加入就绪的列表中,并且返回就绪状态
    • 3.复制数据
      • 1.用户线程获得就绪状态的列表后,根据其中的socket连接发起read系统调用,用户线程阻塞
      • 2.内核开始复制数据,将数据从内核缓冲区复制到用户缓冲区
    • 4.执行后续代码
      • 1.复制完成后,内核返回结果,用户线程才会解除阻塞的状态,用户线程读取数据并继续执行后续代码
  • 5.注意

    • 1.用户进程进行IO就绪事件的轮询时,需要调用选择器的select查询方法,发起查询的用户进程或者线程是阻塞
    • 2.如果使用了查询方法的非阻塞的重载版本,发起查询的用户进程或者线程也不会阻塞,重载版本会立即返回
  • 6.IO多路复用模型的特点

    • 1.IO多路复用模型IO涉及两种系统调用:一种是IO操作的系统调用,另一种是select/epoll就绪查询系统调用
    • 2.IO多路复用模型建立在操作系统的基础设施之上,即操作系统的内核必须能够提供多路分离的select/epoll系统调用
    • 3.IO多路复用模型同步非阻塞IO模型相似,IO多路复用也需要轮询,负责select/epoll状态查询调用的线程,需要不断地进行select/epoll轮询,以找出达到IO操作就绪socket连接
    • 4.IO多路复用模型中注册在选择器上的每一个可以查询的socket连接一般都设置成同步非阻塞模型,只是这一点对于用户程序而言是无感知的
2.优缺点
  • 1.IO多路复用模型的优点
    • 1.一个选择器查询线程可以同时处理成千上万的网络连接,所以用户程序不必创建大量的线程,也不必维护这些线程,从而大大减少了系统的开销
    • 2.通过JDK的源码可以看出,Java语言的NIO组件在Linux系统上是使用epoll系统调用实现的,所以Java语言的NIO组件使用的是IO多路复用模型
  • 2.IO多路复用模型的缺点
    • 1.本质上select/epoll系统调用是阻塞式的,属于同步IO,需要在读写事件就绪后由系统调用本身负责读写,也就是说这个读写过程是阻塞
    • 2.如果要彻底地解除线程的阻塞,就必须使用异步IO模型
3.select/epoll
  • 1.select
    • 1.Linux系统提供select函数来实现多路复用输入/输出模型
    • 2.select系统调用用来让程序轮询监视多个文件描述符状态变化
    • 3.程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变
  • 2.poll
    • 1.Linux系统提供poll函数来实现多路复用输入/输出模型
    • 2.poll系统调用将内核中的每一个事件进行轮询遍历监控,当有描述符就绪时,则将就绪的事件信息记录到相应的结构数组节点的revents
    • 3.用户遍历数组,通过每一个节点revents,判断描述符是否就绪
  • 3.epoll
    • 1.epoll系统调用修改主动轮询监视被动通知,当有事件发生时被动接收通知
    • 2.所以epoll注册socket后,主程序可做其他事情,当事件发生时接收到通知后再去处理

4.异步IO(Asynchronous IO,AIO)

  • 1.异步IO模型:指用户空间的线程变成被动接收者,而内核空间成为主动调用者
  • 2.异步IO模型中,当用户线程接收到通知时,数据已经被内核读取完毕并放在了用户缓冲区内,内核在IO完成后通知用户线程直接使用即可
  • 3.异步IO模型类似Java中典型的回调模式,用户程序(进程或线程)向内核空间注册了各种IO事件的回调函数,由内核去主动调用
1.流程图

在这里插入图片描述

  • 1.异步IO模型的基本流程

    • 1.用户线程通过系统调用向内核注册某个IO操作
    • 2.内核在整个IO操作(包括数据准备、数据复制)完成后通知用户程序
    • 3.用户执行后续的业务操作
  • 2.异步IO模型中整个内核的数据处理过程,包括内核将数据从网络物理设备(网卡)读取到内核缓冲区、将内核缓冲区的数据复制到用户缓冲区中,用户程序都不需要阻塞

  • 3.异步IO模式的read操作的系统调用流程如下

    • 1.用户线程发起了read系统调用后,立刻就可以去做其他的事,用户线程不阻塞
    • 2.内核开始IO的第一个阶段:准备数据
    • 3.内核准备好数据后会将数据从内核缓冲区复制到用户缓冲区
    • 4.内核会给用户线程发送一个信号(Signal)或回调用户线程注册的回调方法,告诉用户线程read系统调用已经完成,数据已经读入用户缓冲区
    • 5.用户线程读取用户缓冲区的数据,完成后续的业务操作
  • 4.异步IO模型的特点

    • 1.内核等待数据复制数据的两个阶段,用户线程都不是阻塞的
    • 2.用户线程需要接收内核的IO操作完成事件或用户线程需要注册一个IO操作完成的回调函数,因此异步IO也称为信号驱动IO
2.优缺点
  • 1.异步IO模型的优点内核等待数据复制数据的两个阶段,用户线程都不是阻塞的,吞吐量高于IO多路复用模型的吞吐量
  • 2.异步IO模型的缺点
    • 1.应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,因此需要底层内核提供支持
    • 2.Linux系统下的异步IO模型2.6版本才引入

3.通过合理配置来支持百万级并发连接

1.文件描述符

  • 1.文件句柄
    • 1.也叫文件描述符Linux系统中的文件可分为普通文件、目录文件、链接文件和设备文件
    • 2.文件描述符(File Descriptor)是内核为了高效管理已被打开的文件所创建的索引,是一个非负整数(通常是小整数),用于指代被打开的文件
    • 3.所有的IO系统调用(包括socket的读写调用)都是通过文件描述符完成
  • 2.Linux操作系统中文件句柄数的限制
    • 1.生产环境的Linux系统基本上都需要解除文件句柄数的限制
    • 2.因为Linux系统的默认值为1024,即一个进程最多可以接受1024socket连接
  • 3.Linux系统中通过调用ulimit命令可以查看一个进程能够打开的最大文件句柄数量
    ulimit -n
    
    在这里插入图片描述
    • 1.ulimit命令:用来显示和修改当前用户进程的基础限制命令
    • 2.-n选项:用来引用或设置当前的文件句柄数量的限制值,Linux系统的默认值为1024
    • 3.理论上1024文件描述符对绝大多数应用已经足够,但对于一些用户基数很大的高并发应用则是远远不够
    • 4.高并发的应用面临的并发连接数往往是十万级、百万级、千万级,甚至像腾讯QQ一样的上亿级
    • 5.当单个进程打开的文件句柄数量超过了系统配置的上限值时会发出Socket/File:Can't open so many files的错误提示
    • 6.所以对于高并发、高负载的应用,必须调整这个系统参数以适应并发处理大量连接的应用场景
  • 4.Linux系统中通过调用ulimit命令也可以设置一个进程能够打开的最大文件句柄数量
     ulimit -n 1000000
    
    在这里插入图片描述
    • 1.ulimit命令中n的值设置越大,可以打开的文件句柄数量越大,建议以root用户来执行此命令
    • 2.使用ulimit命令的缺陷
      • 1.该命令只能修改当前用户环境的一些基础限制,仅在当前用户环境有效
      • 2.当前的终端工具连接当前shell期间修改是有效的,一旦断开用户会话或用户重启/退出Linux,句柄数值又变成系统默认的1024
  • 5.如果想永久地把最大文件描述符数量值保存下来,则编辑/etc/rc.local开机启动文件,文件中添加如下内容
     ulimit -SHn 1000000
    
    • 1.-S选项表示软性极限值
    • 2.-H选项表示硬性极限值
    • 3.硬性极限值是实际的限制,即最大可以是100万,不能再多
    • 4.软性极限值是系统发出警告(Warning)的极限值,即超过这个极限值,内核会发出警告
    • 5.普通用户通过ulimit命令可将软性极限值更改到硬性极限值的最大设置值,如果要更改硬性极限值必须拥有root用户权限
  • 6.如果想彻底解除Linux系统的最大文件打开数量的限制,可以通过编辑Linux的极限配置文件/etc/security/limits.conf,文件中加入如下内容
    soft nofile 1000000
    hard nofile 1000000
    
    • 1.soft nofile:表示软性极限
    • 2.hard nofile:表示硬性极限
    • 3.实际中使用和安装分布式搜索引擎ElasticSearch时,必须修改这个文件以增加最大的文件描述符的极限值
    • 4.生产环境运行Netty时也需要修改/etc/security/limits.conf文件来增加文件描述符数量的极限值

3.NIO

  • 1.同步与异步:指消息处理的方式
  • 2.阻塞与非阻塞:指等待消息响应时的状态

1.简介

  • 1.Java1.4版本之前,Java IO类库采用阻塞IO
  • 2.Java1.4版本开始,引进了新的异步IO库,被称为Java New IO类库,简称为Java NIO
  • 3.New IO类库目标是要让Java支持非阻塞IO,基于此也称Java NIO非阻塞IO(Non-Blocking IO),也称旧的阻塞式Java IOOIO(Old IO)
  • 4.NIO弥补了原来面向流OIO同步阻塞的不足,为标准Java代码提供了高速、面向缓冲区IO
  • 5.NIO的非阻塞是因为使用了通道和通道的多路复用技术

2.NIO和IO的对比

  • 1.JavaNIOIO的区别主要体现在一下三个方面
    • 1.IO是面向Stream Oriented),NIO是面向缓冲区Buffer Oriented
      • 1.面向字节流字符流IO操作总是以流式的方式顺序地从一个流中读取一个或多个字节/字符,因此不能随意改变读取指针的位置
      • 2.NIO中引入了ChannelBuffer,面向缓冲区的读取和写入只需要从通道读取数据到缓冲区中或将数据从缓冲区写入通道中,NIO不是顺序操作,可以读取Buffer中任意位置的数据
    • 2.IO的操作是阻塞的,NIO的操作是非阻塞
      • 1.IO操作是阻塞的,调用read方法读取一个文件的内容,此时调用read的线程就会被阻塞,直到read操作完成
      • 2.NIO操作是非阻塞的,调用read方法读取一个文件的内容,如果此时有数据,则read读取数据并返回;如果此时没有数据,则read也会直接返回,而不会阻塞当前线程
    • 3.IO没有选择器(Selector),NIO有选择器
      • 1.IO不需要用到选择器
      • 2.NIO的实现基于底层选择器的系统调用,所以NIO需要底层操作系统提供支持

3.NIO的三个核心组件

1.Channel(通道)

  • 1.IO操作中同一个网络连接会关联到两个流
    • 1.一个是输入流(Input Stream
    • 2.一个是输出流(Output Stream
    • 3.Java应用程序通过这两个流进行输入和输出的操作
  • 2.NIO操作中一个网络连接使用一个通道表示
    • 1.所有NIO的操作都是通过连接通道完成的
    • 2.一个通道类似于IO中两个流的结合体,既可以从通道读取数据,也可以向通道写入数据,它是读写数据的双向通道
  • 3.Java NIO中一个socket连接使用一个Channel来表示,不同的网络传输协议类型,在Java中都有不同的NIO Channel实现
1.四种通道
  • 1.FileChannel:文件通道,用于文件的数据读写
  • 2.SocketChannel:套接字通道,用于套接字TCP连接的数据读写
  • 3.ServerSocketChannel:服务器套接字通道(或服务器监听通道),允许监听TCP连接请求,为每个监听到的请求创建一个SocketChannel通道
  • 4.DatagramChannel:数据报通道,用于UDP的数据读写
  • 5.注意:FileChannel只能工作在阻塞模式下,其他通道可以工作在非阻塞模式下,因为其他模式可以和Selecter配合工作实现多路IO复用,而FileChannel不可以
  • 6.总结:这四种通道涵盖了文件IOTCP网络UDP IO三类基础IO读写操作,下面从通道的获取读取写入关闭这四个重要的操作入手
2.FileChannel
  • 1.FileChannel:文件通道,用于文件的数据读写
  • 2.通过FileChannel既可以从一个文件中读取数据,也可以将数据写入文件
  • 3.注意FileChannel为阻塞模式,不能设置为非阻塞模式
1.FileChannel基本操作
  • 1.获取FileChannel
    在这里插入图片描述
    • 1.可以通过文件的输入流、输出流获取FileChannel
    • 2.也可以通过RandomAccessFile(文件随机访问)类来获取FileChannel实例
  • 2.读取FileChannel
    在这里插入图片描述
    • 1.大部分应用场景中从通道读取数据都会调用通道的int read(ByteBuffer buf)方法,把从通道读取的数据写入ByteBuffer缓冲区,并返回读取的数据量,-1表示到达文件的末尾
    • 2.channel.read(buf)读取通道的数据时,对于通道来说是读模式对于ByteBuffer缓冲区来说是写模式
  • 3.写入FileChannel
    在这里插入图片描述
    • 1.大部分应用场景中将数据写入通道都会调用通道的int write(ByteBuffer)方法,从ByteBuffer缓冲区中读取数据,然后写入通道,返回值是写入成功的字节数
    • 2.int write(ByteBuffer)调用时对于入参ByteBuffer缓存区,需要从其中读取数据写入通道中,所以入参ByteBuffer必须处于读模式,不能处于写模式
  • 4.关闭通道
    • 1.当通道使用完成后,必须将其关闭
    • 2.关闭调用close()方法即可
  • 5.强制刷新到磁盘
    在这里插入图片描述
    • 1.将缓冲区数据写入通道时,出于性能的原因,操作系统不可能每次都实时地将写入数据刷新到磁盘,完成最终的数据保存
    • 2.因此将缓冲区数据写入通道时,为了保证数据都能写入磁盘,可以在写入后调用一下FileChannelforce()方法
    package BufferDemo;
    
    import java.io.*;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    
    public class FileChannelDemo1 {
        private final  static int CAPACITY = 20;
    
        public static void main(String[] args) throws IOException {
            //获取
            //创建输入源
            File srcFile = new File("E:" + File.separator + "a.txt");
            //创建文件输入流
            FileInputStream fis = new FileInputStream(srcFile);
            //获取文件流的通道
            FileChannel fisChannel = fis.getChannel();
    
            //创建输出源
            File destFile = new File("E:" + File.separator + "b.txt");
            //创建文件输出流
            FileOutputStream fos = new FileOutputStream(destFile);
            //获取文件流的通道
            FileChannel fosChannel = fos.getChannel();
    
            //或者通过创建RandomAccessFile(文件随机访问)类来获取FileChannel实例
            //创建RandomAccessFile随机访问对象
            String fileName = "E:" + File.separator + "c.txt";
            RandomAccessFile rw = new RandomAccessFile(fileName, "rw");
            FileChannel channel = rw.getChannel();
    
            System.out.println("-------------------------");
    
            //读取
            //从通道读取数据都会调用通道的int read(ByteBuffer buf)方法,它把从通道读取的数据写入ByteBuffer缓冲区,并且返回读取的数据量
            RandomAccessFile accessFile = new RandomAccessFile(fileName, "rw");
            //获取通道(可读可写)
            FileChannel fileChannel = accessFile.getChannel();
            //获取一个字节缓冲区
            ByteBuffer buf = ByteBuffer.allocate(CAPACITY);
            int length = -1;
            //调用通道的read()方法,读取数据并写入字节类型的缓冲区
            while ((length = fileChannel.read(buf)) != -1) {
    
            }
    
            buf.flip();
            int outLength = 0;
            while ((outLength = fileChannel.write(buf)) != -1) {
                System.out.println("写入的字节数" + outLength);
            }
    
            fileChannel.close();
            fileChannel.force(true);
        }
    }
    
2.FileChannel文件复制
  • 1.除了FileChannel通道操作外,还需要注意代码执行过程中隐藏的ByteBuffer的模式切换
  • 2.新建的ByteBuffer写模式时才可作为inChannel.read(ByteBuffer)方法的参数
  • 3.inChannel.read(ByteBuffer)方法将从通道inChannel读到的数据写入ByteBuffer
  • 4.调用缓冲区ByteBufferflip方法,将ByteBuffer从写模式切换成读模式才能作为outchannel.write(ByteBuffer)方法的参数,以便从ByteBuffer读取数据,最终写入outchannel(输出通道)
  • 5.完成一次复制之后,进入下一次复制前还要进行一次缓冲区的模式切换,
  • 6.此时需要通过clear方法将ByteBuffer切换成写模式才能进入下一次的复制
  • 7.每一轮外层的while循环都需要两次ByteBuffer模式切换
    • 1.第一次模式切换时翻转buf,变成读模式
    • 2.第二次模式切换时清除buf,变成写模式
    package BufferDemo;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    
    public class FileNIOCopyDemo {
        public static void main(String[] args) throws IOException {
            nioCopyResourceFile();
        }
    
        public static void nioCopyResourceFile() throws IOException {
            //源
            String srcPath = "E:" + File.separator + "a.txt";
            //目标
            String destPath = "E:" + File.separator + "h.txt";
    
            nioCopyFile(srcPath,destPath);
        }
    
        public static void nioCopyFile(String srcPath, String destPath) throws IOException {
            File srcFile = new File(srcPath);
            File destFile = new File(destPath);
            FileInputStream fis = null;
            FileOutputStream fos = null;
            FileChannel inChannel = null;
            FileChannel outChannel = null;
            long startTime = System.currentTimeMillis();
            try {
                if(!destFile.exists()){
                    destFile.createNewFile();
                }
                fis = new FileInputStream(srcFile);
                fos = new FileOutputStream(destFile,true);
                inChannel = fis.getChannel();
                outChannel = fos.getChannel();
                int len = -1;
                //新建buf,处于写模式
                ByteBuffer buf = ByteBuffer.allocate(1024);
                //从输入通道读取到buf
                while ((len = inChannel.read(buf)) != -1) {
                    //buf第一次模式切换:翻转buf,从写模式变成读模式
                    buf.flip();
                    int outLength = 0;
                    //将buf写入输出的通道
                    while ((outLength = outChannel.write(buf)) != 0) {
                        System.out.println("写入的字节数:" + outLength);
                    }
                    //buf第二次模式切换,清除buf,变成写模式
                    buf.clear();
                }
                outChannel.force(true);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                outChannel.close();
                fos.close();
                inChannel.close();
                fis.close();
            }
            long endTime = System.currentTimeMillis();
            System.out.println("复制消耗时间:" + (endTime - startTime));
        }
    }
    
    在这里插入图片描述
    在这里插入图片描述
3.Files和Path
  • 1.Path
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 2.Files
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

2.Buffer(缓冲区)

  • 1.应用程序通道的交互主要是进行数据的读取和写入
    • 1.通道的读取:是将数据从通道读取到缓冲区
    • 2.通道的写入:是将数据从缓冲区写入通道
  • 2.缓冲区的使用是面向流进行读写操作没有的,也是NIO非阻塞的重要前提和基础之一
1.Buffer类
  • 1.NIO中代表缓冲区的Buffer类是一个抽象类,且非线程安全类,位于java.nio包中
  • 2.NIOBuffer内部是一个内存块(数组),与普通的内存块不同的是:NIO Buffer对象提供了一组较有效的方法,用来进行写入和读取的交替访问
  • 3.NIO中有7种缓冲区类
    • 1.ByteBuffer
      • 1.MappedByteBuffer
      • 2.DirectByteBuffer
      • 3.HeapByteBuffer
    • 2.CharBuffer
    • 3.ShortBuffer
    • 4.IntBuffer
    • 5.LongBuffer
    • 6.FloatBuffer
    • 7.DoubleBuffer
  • 4.7种Buffer类型覆盖IO传输Java基本数据类型,MappedByteBuffer是专门用于内存映射的ByteBuffer类型
  • 5.不同的Buffer子类可以操作的数据类型能够通过名称进行判断,如IntBuffer只能操作Integer类型的对象
  • 6.ByteBuffer和字符串的相互转换以及分散读取和集中写
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
2.Buffer类的属性
  • 1.Buffer子类会拥有一块内存,作为数据的读写缓冲区,但是读写缓冲区并没有定义在Buffer类中,而是定义在具体的子类
  • 2.ByteBuffer子类就拥有一个byte[]类型的数组成员final byte[] hb,可以作为自己的读写缓冲区,数组的元素类型与Buffer子类的操作类型相对应
  • 3.作为读写缓冲区的数组,并没有定义在Buffer类中,而是定义在各具体子类中
  • 4.为了记录读写的状态和位置,Buffer类额外提供了一些重要的属性,其中有三个重要的成员属性
    • 1.capacity(容量)
    • 2.position(读写位置)
    • 3.limit(读写的限制)
      在这里插入图片描述
1.capacity
  • 1.Buffer类的capacity属性表示内部容量的大小
  • 2.一旦写入的对象数量超过了capacity,缓冲区就满了,不能再写入
  • 3.Buffer类的子类对象在初始化时会按照capacity分配内部数组的内存,在数组内存分配好之后,大小就不能改变了,因为子类都是常量数组
  • 4.Buffer类是一个抽象类,Java不能直接用来新建对象,具体使用的时,必须使用Buffer的某个子类
  • 5.capacity并不是指内部的内存块byte[]数组的字节数量,而是指能写入的数据对象的最大限制数量
  • 6.如DoubleBuffer子类,该子类能写入的数据类型是double,如果在创建实例时其capacity100,那么最多可以写入100double类型的数据
2.position
  • 1.Buffer类的position属性表示当前的位置
  • 2.position属性的值与缓冲区的读写模式有关,不同的模式下,position属性值的含义是不同的
  • 3.缓冲区进行读写的模式改变时,position值会进行相应的调整
  • 4.写模式下,position值的变化规则如下
    • 1.刚进入写模式时,position值为0(默认值),表示当前的写入位置为从头开始
    • 2.每当一个数据写到缓冲区之后,position会向后移动到下一个可写的位置
    • 3.初始的position值为0,最大可写值为limit-1,当position值达到limit时,缓冲区就已经无空间可写
  • 5.读模式下,position值的变化规则如下:
    • 1.当缓冲区刚开始进入读模式时,position会被重置为0
    • 2.当从缓冲区读取时,也是从position位置开始读,读取数据后,position向前移动到下一个可读的位置
    • 3.读模式下,limit表示可读数据的上限,position的最大值为最大可读上限limit,当position达到limit时表明缓冲区已经无数据可读
  • 6.Buffer的读写模式的切换
    • 1.当新建了一个缓冲区实例时,缓冲区处于写模式,这时可以写数据
    • 2.数据写入完成后,如果要从缓冲区读取数据,就要进行模式的切换,可以调用flip()方法将缓冲区变成读模式
  • 7.从写模式读模式的翻转过程中,positionlimit属性值会进行调整,具体的规则是
    • 1.limit属性被设置成写模式时的position值,表示可以读取的最大数据位置
    • 2.position由原来的写入位置变成新的可读位置,也就是0,表示可以从头开始读
3.limit
  • 1.Buffer类的limit属性表示可以写入或者读取的数据最大上限,其属性值的具体含义也与缓冲区的读写模式有关
  • 2.不同的模式下,limit值的含义是不同的,具体分为以下两种情况
    • 1.写模式下,limit属性值的含义为可以写入的数据最大上限
    • 2.刚进入写模式时,limit的值会被设置成缓冲区的capacity值,表示可以一直将缓冲区的容量写满
    • 2.读模式下,limit值的含义为最多能从缓冲区读取多少数据
  • 3.一般进行缓冲区操作时是先写入再读取,当缓冲区写入完成后,就可以开始从Buffer读取数据,调用flip()方法,这时limit的值也会进行调整
  • 4.将写模式下的position值设置成读模式下的limit值,即将之前写入的最大数量作为可以读取数据的上限值
  • 5.Buffer在翻转时的属性值调整主要涉及positionlimit两个属性
    • 1.首先创建缓冲区,新创建的缓冲区处于写模式,其position值为0limit值为最大容量capacity
    • 2.然后向缓冲区写数据,每写入一个数据,position向后面移动一个位置,也就是position的值加1,假定写入了5个数,当写入完成后,position的值为5
    • 3.之后使用flip方法将缓冲区切换到读模式limit的值会先被设置成写模式时的position值,所以新的limit值是5,表示可以读取数据的最大上限是5
    • 4.最后调整position值,新的position会被重置为0,表示可以从0开始读
    • 5.缓冲区切换到读模式后就可以从缓冲区读取数据了,一直到缓冲区的数据读取完毕
4.Mark
  • 1.Buffer还有一个比较重要的标记属性:mark(标记)属性
  • 2.作用:缓冲区操作过程当中,可以将当前的position值临时存入mark属性中,需要的时候再从mark中取出暂存的标记值,恢复到position属性中,重新从position位置开始处理
5.总结

在这里插入图片描述

3.Buffer类的方法
  • 1.使用Buffer实例之前首先需要获取Buffer子类的实例对象,并且分配内存空间
1.allocate方法

在这里插入图片描述

  • 1.需要获取一个Buffer实例对象时,并不是使用子类的构造器来创建,而是调用子类的allocate(int capacity)方法

  • 2.运行结果可以看出一个缓冲区在新建后处于写模式,即position属性的值为0

  • 3.缓冲区的capacity值是初始化时allocate方法的参数值20,而limit最大可写上限值也为allocate方法的初始化参数值20

    package BufferDemo;
    
    import java.nio.IntBuffer;
    
    public class BufferDemo1 {
        //一个整型的Buffer变量
        static IntBuffer intBuffer = null;
    
        public static void main(String[] args) {
            intBuffer = IntBuffer.allocate(20);
            System.out.println("position = " + intBuffer.position());
            System.out.println("limit = " + intBuffer.limit());
            System.out.println("capacity = " + intBuffer.capacity());
        }
    }
    
    //分配一个缓冲区
    //新缓冲区的位置将为零,其限制将是其容量,其标记将未定义,并且其每个元素将被初始化为零
    //它将有一个 {@link array backing array},它的 {@link arrayOffset array offset} 将为零
    public static IntBuffer allocate(int capacity) {
            if (capacity < 0)
                throw new IllegalArgumentException();
            return new HeapIntBuffer(capacity, capacity);
        }
    HeapIntBuffer(int cap, int lim) {            // package-private
         super(-1, 0, lim, cap, new int[cap], 0);
         /*
         hb = new int[cap];
         offset = 0;
         */
     }
    // Creates a new buffer with the given mark, position, limit, capacity,
    // backing array, and array offset
    //使用给定的标记、位置、限制、容量、后备数组和数组偏移量创建一个新缓冲区
    IntBuffer(int mark, int pos, int lim, int cap,   // package-private
                 int[] hb, int offset)
    {
        super(mark, pos, lim, cap);
        this.hb = hb;
        this.offset = offset;
    }
    // Creates a new buffer with the given mark, position, limit, and capacity,
    // after checking invariants.
    //在检查不变量后,使用给定的标记、位置、限制和容量创建一个新缓冲区
    Buffer(int mark, int pos, int lim, int cap) {       // package-private
        if (cap < 0)
            throw new IllegalArgumentException("Negative capacity: " + cap);
        this.capacity = cap;
        limit(lim);
        position(pos);
        if (mark >= 0) {
            if (mark > pos)
                throw new IllegalArgumentException("mark > position: ("
                                                   + mark + " > " + pos + ")");
            this.mark = mark;
        }
    }
    //设置此缓冲区的限制。如果容量大于新限制,则将其设置为新限制。如果标记已定义且大于新限制,则将其丢弃
    public final Buffer limit(int newLimit) {
            if ((newLimit > capacity) || (newLimit < 0))
                throw new IllegalArgumentException();
            limit = newLimit;
            if (position > limit) position = limit;
            if (mark > limit) mark = -1;
            return this;
        }
    //设置此缓冲区的位置。如果标记已定义且大于新位置,则将其丢弃
    public final Buffer position(int newPosition) {
            if ((newPosition > limit) || (newPosition < 0))
                throw new IllegalArgumentException();
            position = newPosition;
            if (mark > position) mark = -1;
            return this;
        }
    
2.put方法
  • 1.调用allocate()方法分配内存并返回了实例对象后,缓冲区实例对象处于写模式,可以写入对象
  • 2.如果将将对象写入缓冲区,就需要调用put()方法
    public static void main(String[] args) {
            intBuffer = IntBuffer.allocate(20);
            for (int i = 1; i <= 5; i++) {
                intBuffer.put(i);
            }
            System.out.println("position = " + intBuffer.position());
            System.out.println("limit = " + intBuffer.limit());
            System.out.println("capacity = " + intBuffer.capacity());
        }
    
  • 3.结果显示
    • 1.写入5个元素之后,缓冲区的position属性值变成了5,所以指向了第6个(从0开始)可以进行写入的元素位置
    • 2.limit最大可写上限,capacity最大容量两个属性的值都没有发生变化
3.flip方法

在这里插入图片描述

  • 1.向缓冲区写入数据之后,不能直接从缓冲区读取数据,因为这时缓冲区还处于写模式
  • 2.如果需要读取数据,需要将缓冲区转换成读模式
  • 3.flip()方法是Buffer类提供的一个模式转变的方法,其作用是将写模式翻转成读模式
    package BufferDemo;
    
    import java.nio.IntBuffer;
    
    public class BufferDemo1 {
        //一个整型的Buffer变量
        static IntBuffer intBuffer = null;
    
        public static void main(String[] args) {
            intBuffer = IntBuffer.allocate(20);
            for (int i = 1; i <= 5; i++) {
                intBuffer.put(i);
            }
            System.out.println("position = " + intBuffer.position());
            System.out.println("limit = " + intBuffer.limit());
            System.out.println("capacity = " + intBuffer.capacity());
    
            intBuffer.flip();
            System.out.println("position = " + intBuffer.position());
            System.out.println("limit = " + intBuffer.limit());
            System.out.println("capacity = " + intBuffer.capacity());
            for (int i = 0; i < intBuffer.limit(); i++) {
                System.out.println(intBuffer.get(i));
            }
    
        }
    }
    
  • 4.调用flip()方法后
    • 1.新模式下可读上限limit的值变成了之前写模式下的position属性值5
    • 2.新的读模式下的position值变成了0,表示从头开始读取
  • 5.flip()方法从写入到读取转换的规则
    • 1.首先设置可读上限limit的属性值,将写模式下的缓冲区中内容的最后写入位置position值作为读模式下的limit上限值
    • 2.其次把读的起始位置position的值设为0,表示从头开始读
    • 3.最后清除之前的mark标记,因为mark保存的是写模式下的临时位置,发生模式翻转后,如果继续使用旧的mark标记会造成位置混乱
  • 6.读取完成后如果想再一次将缓冲区切换成写模式可以调用Buffer.clear()清空或者Buffer.compact()压缩方法,可以将缓冲区转换为写模式
    在这里插入图片描述
4.get()方法
  • 1.调用flip()方法将缓冲区切换成读模式后,就可以开始从缓冲区读取数据
  • 2.读取数据的方法很简单,可以调用get()方法每次从position的位置读取一个数据,并且进行相应的缓冲区属性的调整
  • 3.结果显示读取操作会改变可读位置position的属性值,而可读上限limit值并不会改变
  • 4.当position值和limit值相等时,表示所有数据读取完成,此时position指向了一个没有数据的元素位置,表示已经不能再读,再读就会抛出BufferUnderflowException异常
  • 5.读完之后不可以立即对缓冲区进行数据写入,因为现在还处于读模式,必须调用Buffer.clear()Buffer.compact()方法清空或压缩缓冲区,将缓冲区切换成写模式,让其重新可写
  • 6.缓冲区可以重复读,通过倒带方法rewind()完成重复读,也可以通过mark()reset()两个方法组合实现
  • 7.注意get()get(int index)的区别:其中get()position的值会发生变化,而get(int index)position的值不会改变
    package BufferDemo;
    
    import java.nio.IntBuffer;
    
    public class BufferDemo1 {
        //一个整型的Buffer变量
        static IntBuffer intBuffer = null;
    
        public static void main(String[] args) {
            intBuffer = IntBuffer.allocate(20);
            for (int i = 1; i <= 5; i++) {
                intBuffer.put(i);
            }
            System.out.println("position = " + intBuffer.position());
            System.out.println("limit = " + intBuffer.limit());
            System.out.println("capacity = " + intBuffer.capacity());
    
            intBuffer.flip();
            System.out.println("position = " + intBuffer.position());
            System.out.println("limit = " + intBuffer.limit());
            System.out.println("capacity = " + intBuffer.capacity());
            for (int i = 0; i < intBuffer.limit(); i++) {
                System.out.println(intBuffer.get());
            }
            System.out.println("position = " + intBuffer.position());
            System.out.println("limit = " + intBuffer.limit());
            System.out.println("capacity = " + intBuffer.capacity());
            intBuffer.rewind();
            System.out.println(intBuffer.get());
        }
    }
    
5.rewind()
  • 1.已经读完的数据,如果需要再读一遍可以调用rewind()方法
  • 2.rewind ()方法主要调整了缓冲区的position属性与mark属性,具体的调整规则如下
    • 1.position重置为0,所以可以重读缓冲区中的所有数据
    • 2.limit保持不变,数据量还是一样的,仍然表示能从缓冲区中读取的元素数量
    • 3.mark被清理,表示之前的临时位置不能再用
  • 3.源码可以看出rewind()方法与flip()方法类似,区别在于
    • 1.倒带方法rewind()不会影响limit属性值
    • 2.翻转方法flip()会重设limit属性值
6.mark()和reset()
  • 1.mark()reset()两个方法是配套使用
    • 1.Buffer.mark()方法将当前position的值保存起来放在mark属性中,让mark属性记录这个临时位置
    • 2.然后可以调用Buffer.reset()方法将mark的值恢复到position
  • 2.Buffer.mark()Buffer.reset()两个方法都涉及mark属性的使用
  • 3.读到第n个元素时,可以调用mark()方法,把当前位置position的值保存到mark属性中,这时mark属性的值为n-1,接下来可以调用reset()方法将mark属性的值恢复到position中,这样就可以从位置n开始重复读取
7.clear()

在这里插入图片描述
在这里插入图片描述

  • 1.读模式下调用clear()方法将缓冲区切换为写模式,具体的调整规则如下
    • 1.将position清零
    • 2.limit设置为capacity最大容量值,表示可以写入,直到缓冲区写满
  • 2.缓冲区处于读模式时调用clear(),缓冲区会被切换成写模式
4.Buffer类的使用步骤
  • 1.使用创建子类实例对象的allocate()方法创建一个Buffer类的实例对象
  • 2.调用put()方法将数据写入缓冲区中
  • 3.写入完成后,在开始读取数据前调用Buffer.flip()方法,将缓冲区转换为读模式
  • 4.调用get()方法从缓冲区中读取数据
  • 5.读取完成后,调用Buffer.clear()Buffer.compact()方法,将缓冲区转换为写模式,可以继续写入
    在这里插入图片描述
    在这里插入图片描述

3.Selector(选择器)

  • 1.IO多路复用:指的是一个进程/线程可以同时监视多个文件描述符(含socket连接),一旦其中的一个或多个文件描述符可读或可写,该监听进程/线程就能够进行IO就绪事件的查询
  • 2.Java应用层面实现对多个文件描述符的监视需要用到Java NIO组件选择器Selector
  • 3.Selector可以理解为一个IO事件的监听与查询器,通过Selector一个线程可以查询多个通道的IO事件的就绪状态
  • 4.从编程实现IO多路复用编程
    • 1.第一步是把通道注册到选择器
    • 2.第二步是通过选择器所提供的事件查询(select)方法来查询这些注册的通道是否有已经就绪的IO事件(可读、可写、网络连接完成等)
  • 5.由于一个Selector只需要一个线程进行监控,因此可以很简单地使用一个线程,通过选择器去管理多个连接通道
  • 6.与IO相比NIO使用Selector的最大优势是系统开销小,系统不必为每一个网络连接(文件描述符)创建进程/线程,从而大大减少了系统的开销
  • 7.一个线程负责多个连接通道的IO处理是非常高效的,这种高效来自Java的选择器组件Selector及其底层的操作系统IO多路复用技术的支持
1.选择器与注册
  • 1.选择器的作用是完成IO的多路复用,其主要工作是通道的注册、监听、事件查询
  • 2.一个通道代表一条连接通路,通过选择器可以同时监控多个通道的IO状况,选择器通道的关系是监控和被监控
  • 3.选择器提供了独特的API,能够选出(select)所监控的通道已经发生了哪些IO事件,包括读写就绪IO操作事件
  • 4.NIO编程中
    • 1.一般是一个单线程处理一个选择器一个选择器可以监控很多通道
    • 2.所以通过选择器一个单线程可以处理数百、数千、数万甚至更多的通道,这样会大量地减少线程之间上下文切换的开销
  • 4.通道选择器之间的关联通过注册(register)的方式完成,调用通道的Channel.register(Selector sel,int ops)方法,可以将通道实例注册到一个选择器
  • 5.register(Selector sel,int ops)方法有两个参数
    • 1.第一个参数:指定通道注册到的选择器实例
    • 2.第二个参数:指定选择器要监控的IO事件类型
    • 3.可供选择器监控的通道IO事件类型包括以下四种
      • 1.可读:SelectionKey.OP_READ
      • 2.可写:SelectionKey.OP_WRITE
      • 3.连接:SelectionKey.OP_CONNECT
      • 4.接收:SelectionKey.OP_ACCEPT
  • 6.以上事件类型常量定义在SelectionKey类中,如果选择器要监控通道的多种事件,可以用按位或运算符来实现
    //监控通道的多种事件,用按位或运算符来实现
    int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE
    
  • 7.IO事件
    • 1.这里的IO事件不是对通道的IO操作,而是通道处于某个IO操作的就绪状态,表示通道具备执行某个IO操作的条件,示例如下
    • 1.某个SocketChannel传输通道如果完成了和对端的三次握手过程,就会发生连接就绪OP_CONNECT)事件
    • 2.某个ServerSocketChannel服务器连接监听通道,监听到一个新连接到来时,则会发生接收就绪OP_ACCEPT)事件
    • 3.一个SocketChannel通道有数据可读,就会发生读就绪OP_READ)事件
    • 4.一个SocketChannel通道等待数据写入,就会发生写就绪OP_WRITE)事件
  • 8.Socket连接事件的核心原理和TCP连接的建立过程有关,关于TCP的核心原理和连接建立时的三次握手、四次挥手等可参考
2.SelectableChannel
  • 1.并不是所有的通道都是可以被选择器监控或选择的,FileChannel就不能被选择器复用
  • 2.判断一个通道能否被选择器监控或选择有一个前提,即判断其是否继承了抽象类SelectableChannel(可选择通道),如果是则可以被选择,否则不能被选择
  • 3.SelectableChannel类提供了实现通道可选择性所需要的公共方法,Java NIO中所有网络连接Socket通道都继承了SelectableChannel类,都是可选择的
  • 4.FileChannel并没有继承SelectableChannel,因此不是可选择通道
3.SelectionKey
  • 1.通道选择器的监控关系注册成功后就可以选择就绪事件,具体的选择工作可调用Selectorselect()方法完成
  • 2.通过select()方法选择器可以不断地选择通道中所发生操作的就绪状态,返回注册过的IO事件
  • 3.一旦在通道中发生了某些IO事件(就绪状态达成),并且是在选择器中注册过的IO事件,就会被选择器选中,并放入SelectionKey(选择键)的集合中
  • 4.SelectionKey就是那些被选择器选中的IO事件
    • 1.一个IO事件发生(就绪状态达成)后,如果之前在选择器中注册过就会被选择器选中并放入SelectionKey
    • 2.如果之前没有注册过,那么即使发生了IO事件,也不会被选择器选中
  • 5.实际编程时,通过SelectionKey,不仅可以获得通道的IO事件类型还可以获得发生IO事件所在的通道以及获得选择器实例
4.选择器使用流程

4.Netty

1.定义

  • 1.Netty是一个异步的,基于事件驱动的网络应用程序框架
  • 2.用于快速开发可维护,高性能的网络服务器和客户端
  • 3.NettyIO模型也是基于IO多路复用,使用的是NIO,而不是真正的异步(读写分别一个线程)

2.应用

在这里插入图片描述

3.特点

在这里插入图片描述

4.入门程序

  • 1.开发一个简单的服务端和客户端
  • 2.客户端向服务端发送信息
  • 3.服务端仅接口,不返回

1.导入依赖

<dependency>
     <groupId>io.netty</groupId>
     <artifactId>netty-all</artifactId>
     <version>4.1.39.Final</version>
</dependency>

2.客户端和服务端

package com.redis.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;

public class TestServer {
   public static void main(String[] args) {
       // 1. 启动器 负责组装netty,启动服务器
       new ServerBootstrap()
               // 2. BossEventLoop, WorkerEventLoop(Selector,thread), group 组
               .group(new NioEventLoopGroup())
               // 3. 选择 服务器的 ServerSocketChannel 实现
               .channel(NioServerSocketChannel.class)
               // 4. boss 负责处理连接 worker(child) 负责处理读写 决定了worker(child)能执行哪些操作(handler)
               .childHandler(
                   // 5. channel 代表和客户端能进行数据读写的通道 Initializer 初始化 负责添加别的handler
                   new ChannelInitializer<NioSocketChannel>() {
                   @Override
                   protected void initChannel(NioSocketChannel ch) throws Exception {
                       // 6.添加handler
                       ch.pipeline().addLast(new StringDecoder()); // 将 ByteBuf 转换为字符串
                       ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { // 自定义 handler
                           @Override
                           // 读事件
                           public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                               // 打印上一步转换好的字符串
                               System.out.println(msg);
                           }
                       });
                   }
               })
               // 7. 绑定监听端口
               .bind(8080);
   }
}
package com.redis.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;

import java.net.InetSocketAddress;

public class TestClient {
   public static void main(String[] args) throws InterruptedException {
       // 1. 启动类
       new Bootstrap()
               // 2. 添加 EventLoop
               .group(new NioEventLoopGroup())
               // 3. 选择客户端 channel 实现
               .channel(NioSocketChannel.class)
               // 4. 添加处理器
               .handler(new ChannelInitializer<NioSocketChannel>() {
                   @Override
                   // 在连接建立后被调用
                   protected void initChannel(NioSocketChannel ch) throws Exception {
                       ch.pipeline().addLast(new StringEncoder());
                   }
               })
               // 5. 连接到服务器
               .connect(new InetSocketAddress("localhost",8080))
               .sync()
               .channel()
               // 6. 向服务器发送数据
               .writeAndFlush("hello,world");
   }
}

3.流程

在这里插入图片描述

5.黏包和半包问题

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值