1.IO
1.IO定义
- 1.
Java
中I/O
操作主要是指使用Java
进行输入
,输出
操作- 2.
Java
所有的I/O
操作都是基于数据流
进行输入输出,数据流表示字符或者字节数据的流动序列- 3.
Java
的I/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.典型实现(
FileInputSteam
、FileOutStream
)
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.典型实现(
FileReader
、FileWriter
)- 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.
OutputStreamWriter
和InputStreamReader
是字符流
和字节流
的桥梁- 2.字符转换流原理:
字节流+编码表
- 3.
FileReader
继承自InputStreamReader
- 4.
FileWriter
继承自OutputStreamWriter
- 5.
FileWriter
和FileReader
:作为子类仅作为操作字符文件
的便捷类存在,当操作的字符文件,使用的是默认编码表
时可以不用父类,而直接用子类完成操作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.字节缓冲流:
BufferedInputStream
、BufferedOutputStream
- 3.字符缓冲流:
BufferedReader
、BufferedWriter
- 4.
IO
操作时通常会定义一个字节或字符数组,将读取/写入
的数据先存放到数组中,当内部定义的数组满了,就会进行下一步操作- 5.缓冲流的
JDK
底层源码可以看到,程序中定义了大小为8192
的缓存数组- 6.设置缓冲区的大小不要随便设置,要么就设置成
8192
的整数倍,要么就用默认值- 7.因为
Windows
和Linux
都使用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.
字节数组流
:ByteArrayOutputStream
、ByteArrayInputStream
- 2.
字符数组流
:CharArrayReader
、CharArrayWriter
- 3.
字符串流
:StringReader
,StringWriter
(将数据临时存储到字符串中)
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读写
,基本上都会用到底层的read
和write
两大系统调用- 2.不同的操作系统中,
IO读写
调用的名称可能不完全一样,但基本功能是一样的
- 1.
read
系统调用,并不是直接从物理设备
把数据
读取到内存
中- 2.
write
系统调用,也不是直接把数据
从内存
写入到物理设备
中- 3.
上层应用
无论是调用操作系统的read
还是write
,都会涉及缓冲区
- 1.调用操作系统的
read
是把数据
从内核缓冲区
复制到进程缓冲区
- 2.调用操作系统的
write
是把数据
从进程缓冲区
复制到内核缓冲区
- 4.上层应用的
IO操作
,实际上不是物理设备
级别的读写,而是缓存的复制
- 5.
read
和write
两大系统调用,都不负责数据
在内核缓冲区
和物理设备
(如磁盘)之间的交换,而是由操作系统内核
(Kernel
)来完成- 6.用户程序中,无论是
Socket
的IO
,还是文件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
中发起一个socket
的read
操作的系统调用流程
如下
- 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
中非阻塞IO
的socket
被设置为NONBLOCK
模式- 5.
注意
- 1.
同步非阻塞IO
也可以简称为NIO
,但它不是Java
中的NIO
,Java
中的NIO
(New IO
)类库组件
归属的不是基础IO模型中的NIO模型
,而是IO多路复用模型
- 6.
总结
- 1.
同步非阻塞IO
:指用户进程主动发起,不需要等待内核IO操作
彻底完成就能立即返回用户空间的IO操作
- 2.
同步非阻塞IO
操作过程中,发起IO请求
的用户程序(进程或线程)处于非阻塞状态
1.流程图
- 1.
Linux
系统下socket
连接默认是阻塞模式
,可以将socket
连接设置成非阻塞模式
- 2.
NIO
模型中,应用程序一旦开始IO系统调用
就会出现以下两种情况
- 1.
内核缓冲区
中没有数据的情况下,系统调用会立即返回一个调用失败
的信息- 2.
内核缓冲区
中有数据的情况下,数据的复制过程
中系统调用是阻塞
的,直到完成数据从内核缓冲区
复制到用户缓冲区
,复制完成后系统返回调用成功
的信息,用户程序(进程或线程)可以开始处理用户空间的缓冲区数据- 3.非阻塞
socket
的read
操作的系统调用流程如下
- 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
模式实现,也称为异步阻塞IO
,Java
中的Selector
属于这种模型
1.流程图
1.为了避免
同步非阻塞IO模型
中轮询等待
的问题采用了IO多路复用模型
2.目前支持
IO多路复用
的系统调用有select
、epoll
等
- 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
连接)提前注册到Linux
的select/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
,即一个进程最多可以接受1024
个socket
连接- 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 IO
为OIO(Old IO)
- 4.
NIO
弥补了原来面向流
的OIO同步阻塞
的不足,为标准Java
代码提供了高速、面向缓冲区
的IO
- 5.
NIO
的非阻塞是因为使用了通道
和通道的多路复用技术
2.NIO和IO的对比
- 1.
Java
中NIO
和IO
的区别主要体现在一下三个方面
- 1.
IO
是面向流
(Stream Oriented
),NIO
是面向缓冲区
(Buffer Oriented
)
- 1.面向
字节流
或字符流
的IO操作
总是以流式的方式顺序地
从一个流中读取一个或多个字节/字符,因此不能随意改变读取指针的位置- 2.
NIO
中引入了Channel
和Buffer
,面向缓冲区
的读取和写入只需要从通道
读取数据到缓冲区
中或将数据从缓冲区
写入通道
中,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.
总结
:这四种通道涵盖了文件IO
、TCP网络
、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.因此将缓冲区数据写入通道时,为了保证数据都能写入磁盘,可以在写入后调用一下
FileChannel
的force()
方法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.调用缓冲区
ByteBuffer
的flip
方法,将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.
NIO
的Buffer
内部是一个内存块(数组),与普通的内存块不同的是: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
,如果在创建实例时其capacity
是100
,那么最多可以写入100
个double
类型的数据
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.从
写模式
到读模式
的翻转过程中,position
和limit
属性值会进行调整,具体的规则是
- 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
在翻转时的属性值调整主要涉及position
、limit
两个属性
- 1.首先创建缓冲区,新创建的缓冲区处于写
模式
,其position
值为0
,limit
值为最大容量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.
通道
和选择器
的监控关系注册成功后就可以选择就绪事件
,具体的选择工作可调用Selector
的select()
方法完成- 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.
Netty
的IO
模型也是基于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.黏包和半包问题