IO与NIO

File类

  1. File类的一个对象,代表一个文件或一个文件目录(Directory)

  2. File类声明在java.io包下

  3. File类中涉及到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法**,并未涉及到写入或读取文件内容的操作。**如果需要读取或写入文件内容,必须使用IO流(Stream)来完成。

  4. 后续File类的对象常会作为参数传递到流的构造器中,指明读取或写入的终点

File的实例化

// public File(String pathname) 
File disc = new File("D:\\");
// public File(String parent, String child) 
File c1 = new File("D:\\","child");
// public File(File parent, String child) 
File c2 = new File(disc,"child");
// public File(URI uri) 
File file = new File(URI.create("file:///d:/"));

路径分隔符

由于java跨平台运行的特性,我们有的时候必须兼顾所有平台的路径分隔符

windows和DOS系统:\

UNIX和URL:/

File类提供了静态变量separator来根据操作系统动态提供路径分隔符

File.separator

File类的常用方法

  • 获取信息
    • 获取绝对路径:public String getAbsolutePath()
    • 获取路径:public String getPath()
    • 获取名称:public String getName()
    • 获取上层目录路径(无则返回null):public String getParent()
    • 获取文件大小(字节数):public long length()
    • 获取最后一次修改时间(毫秒时间戳):public long lastModified()
    • 获取目录下所有子文件或子目录名称:public String[] list()
    • 获取目录下所有子文件或子目录File数组:public File[] listFiles()
  • 重命名(移动文件):public boolean renameTo(File dest)
  • 判断
    • 判断是否是目录:public boolean isDirectory()
    • 判断是否是文件:public boolean isFile()
    • 判断是否存在:public boolean exists()
    • 判断是否可读:public boolean canRead()
    • 判断是否可写:public boolean canWrite()
    • 判断是否隐藏:public boolean isHidden()
  • 创建
    • 创建文件:public boolean createNewFile()
    • 创建目录:public boolean mkdir()
    • 创建目录(可递归创建多级):public boolean mkdirs()
  • 删除:public boolean delete()

过滤文件

File file = new File("D:\\");
File[] files = file.listFiles(new FileFilter() {
    @Override
    public boolean accept(File pathname) {
        // 过滤出所有的文件
        return pathname.isFile();
    }
});
System.out.println(Arrays.toString(files));

IO流(stream)

I:input ; O : output

I/O即输入输出,是计算机与外界世界的一个接口。IO操作的实际主体是操作系统。在java编程中,一般使用流的方式来处理IO,所有的IO都被视作是单个字节的移动,通过stream对象一次移动一个字节。流IO负责把对象转换为字节,然后再转换为对象。

流的分类

  1. 操作数据单位:字节流(8bit)、字符流 (16bit)

  2. 数据的流向:输入流(写入内存)、输出流(从内存写出)

  3. 流的角色:节点流(从一个特定的数据源读写数据)、处理流(连接在已存在的流之上,为程序提供更为强大的读写功能)

IO 流体系

分类字节输入流字节输出流字符输入流字符输出流
抽象基类InputStreamOutputStreamReaderWriter
访问文件FileInputStreamFileOutputStreamFileReaderFileWriter
访问数组ByteArrayInputStreamByteArrayOutputStreamCharArrayReaderCharArrayWriter
访问管道PipedInputStreamPipedOutputStreamPipedReaderPipedWriter
访问字符串StringReaderStringWriter
缓冲流BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter
转换流InputStreamReaderOutputSreamWriter
对象流ObjectInputStreamObjectOutputStream
FilterInputStreamFilterOutputStreamFilterReaderFilterWriter
打印流PrintStreamPrintWriter
推回输入流PushbackInputStreamPushbackReader
特殊流DataInputStreamDataOutputStream

IO类的继承关系

这里写图片描述

节点流

如:InputStream、Reader、OutputStream、Writer

节点流与具体节点相连接,直接读写节点数据

  • 字节流操作字节,也就是二进制文件,比如:.mp3,.avi,.rmvb,mp4,.jpg,.doc,.ppt
  • 字符流操作字符,只能操作普通文本文件。最常见的文本文件:.txt,.java,.c,.cpp 等语言的源代码。尤其注意.doc,.excel,.ppt这些不是文本文件。

输入流InputStream & Reader

InputStream(字节流)Reader **(字符流)**是所有输入流的基类。

程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该==显式关闭文件 IO 资源(使用close方法)。==

FileInputStream 从文件系统中的某个文件中获得输入字节。FileInputStream 用于读取非文本数据之类的原始字节流。要读取字符流,需要使用 FileReader

常用方法
InputStream
// 从输入流读取下一个字节,返回0到255的int字节值,如果到达流末尾则返回-1
int read()
// 从输入流中读取最多b.length个字节到byte[] b中,并返回读取字节的长度,如果到达流末尾则返回-1
int read(byte[] b)
// 从输入流中读取最多len个字节到byte[] b中,读到的第一个字节会被写到b[off]中并依次往后,并返回读取字节的长度,如果到达流末尾则返回-1
int read(byte[] b,int off,int len)
// 返回从该输入流中可读取的剩余字节数之和
int available()
// 关闭输入流并释放与该流有关的所有系统资源
public void close() throws IOException
Reader
// 从输入流读取单个字符,返回0到65535的int类型Unicode码值,如果到达流末尾则返回-1
int read()
// 从输入流读取最多cbuf.length个字符到char[] cbuf中,并返回读取字符的长度,如果到达流末尾则返回-1
int read(char[] cbuf)
// 从输入流中读取最多len个字符到char[] cbuf中,读到的第一个字符会被写到cbuf[off]中并依次往后,并返回读取字符的长度,如果到达流末尾则返回-1
int read(char[] cbuf,int off,int len)
// 关闭输入流并释放与该流有关的所有系统资源
public void close() throws IOException

输出流OutputStream & Writer

**OutputStream(字节流)Writer(字符流)**是所有输出流的基类。

  • 在写入一个文件时,如果使用构造器FileOutputStream(file),则目录下有同名文件将被覆盖。

  • 如果使用构造器FileOutputStream(file,true),则目录下的同名文件不会被覆盖, 在文件内容末尾追加内容。

因为字符流直接以字符作为操作单位,所以 Writer 可以用字符串来替换字符数组, 即以 String 对象作为参数

显式关闭IO资源,需要使用flush(防止数据丢失)和close方法

FileOutputStream 用于写出非文本数据之类的原始字节流。要写出字符流,需要使用 FileWriter

常用方法
OutputStream
// 向输出流写入一个0-255范围的int类型的字节值
void write(int b)
// 将byte[] b中的所有字节写入输出流
void write(byte[] b)
// 将byte[] b数组从b[off]开始的len个字节写入输出流
void write(byte[] b,int off,int len)
// 刷新输出流,并强制将缓冲中的所有字节写出到输出流
public void flush()throws IOException
// 关闭输出流并释放与该流关联的系统资源
public void close()throws IOException
Writer
// 向输出流写入一个0-65535范围的int类型的Unicode码值
void write(int c)
// 将char[] cbuf中的所有字符写入输出流
void write(char[] cbuf)
// 将char[] cbuf从cbuf[off]开始的len个字符写入输出流
void write(char[] cbuf,int off,int len)
// 将字符串写入输出流
void write(String str)
// 将字符串从第off个字符开始写入len个字符到输出流
void write(String str,int off,int len)
// 刷新输出流,并强制将缓冲中的所有字节写出到输出流
public void flush()throws IOException
// 关闭输出流并释放与该流关联的系统资源
public void close()throws IOException

说明: untitle.png

文件的复制

public static void copy(String copyBy,String copyTo) throws IOException {
        //输入流
        InputStream inputStream = new FileInputStream(new File(copyBy));
        //输出流
        OutputStream outputStream = new FileOutputStream(new File(copyTo));
        //创建缓冲数组
        byte[] bytes = new byte[1024];
        int i;
        //循环写入缓冲,然后从缓冲数组中输出到目标文件
        while ((i = inputStream.read(bytes)) != -1){
            outputStream.write(bytes,0,i);
        }
        //释放资源
        outputStream.flush();
        inputStream.close();
        outputStream.flush();
 }

处理流

缓冲流

BufferedInputStream 、BufferedOutputStream、BufferedReader、BufferedWriter

优点:提供流的读取、写入的速度

提高读写速度的原因:内部提供了一个缓冲区。默认情况下是8kb

File f = new File("D:\\test.txt");
FileInputStream inputStream = new FileInputStream(f);
// 创建缓冲流,并设置缓冲区大小
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream,1024);
byte[] bytes = new byte[bufferedInputStream.available()];
bufferedInputStream.read(bytes);
bufferedInputStream.close();
String s = new String(bytes);
System.out.println(s);

转换流

InputStreamReader:将一个字节的输入流转换为字符的输入流

File f = new File("D:\\test.txt");
InputStream inputStream = new FileInputStream(f);
// 将字节流转换为字符流
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, Charset.forName("utf8"));
StringBuilder stringBuilder = new StringBuilder();
while (true){
    int read = inputStreamReader.read();
    if (read == -1){
        break;
    }
    stringBuilder.append((char) read);
}
System.out.println(stringBuilder.toString());
inputStreamReader.close();

OutputStreamWriter:将一个字节的输出流转换为字符的输出流

File f = new File("D:\\test.txt");
OutputStream outputStream = new FileOutputStream(f);
// 将字节流转换为字符流
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, Charset.forName("utf8"));
String target = "Hello Java IO";
outputStreamWriter.write(target);
outputStreamWriter.flush();
outputStreamWriter.close();
常见的编码表
  • ASCII:美国标准信息交换码。
  • ISO8859-1:拉丁码表。欧洲码表
  • GB2312:中国的中文编码表。最多两个字节编码所有字符。
  • GBK:中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码。
  • Unicode:国际标准码,融合了目前人类使用的所字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示。
  • UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。

客户端/浏览器端——后台(Java,GO,Python)——数据库

要求前后使用的字符集都要统一:UTF-8

对象流

ObjectInputStream、OjbectOutputSteam

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

序列化需注意
  1. 需要实现接口:java.io.Serializable
  2. 当前类提供一个全局常量:serialVersionUID序列化类新增属性时,请不要修改serialVersionUID字段,避免反序列失败),作用是验证版本一致性。
  3. 除了当前类需要实现Serializable接口之外,还必须保证其内部所属性也必须是可序列化的。(默认情况下,基本数据类型可序列化;如果是引用数据类型,那么需要改类型是可序列化类)
  4. 如果子类实现Serializable接口而父类未实现时,父类不会被序列化,但此时父类必须有个无参构造方法,否则会抛InvalidClassException异常
  5. ObjectOutputStreamObjectInputStream不能序列化statictransient修饰的成员变量
serialVersionUID

private static final long serialVersionUID = 771652260758459933L;

可序列化类中的版本标识,JVM用这个字段来确定是否能够反序列化出对象。换句话说,只有对象序列化后的二进制数据中的serialVersionUID与当前对象的serialVersionUID相同,反序列化才能成功,否则就会失败。

即使自己不显式的声明serialVersionUID,在序列化时,JVM也会生成一个。但是,最好还是自己手动声明一个,避免后续程序执行出现问题。

Idea开启自动生成

image-20230411142930951

然后在实现java.io.Serializable接口的类名上,ctrl + enter选择add 'serialVersionUID' field即可

transient

Java关键字

对于transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略。 因此,transient变量不会贯穿对象的序列化和反序列化,生命周期仅存于调用者的内存中而不会写到磁盘里进行持久化。

在持久化对象时,对于一些特殊的数据成员(如用户的密码,银行卡号等),我们不想用序列化机制来保存它。为了在一个特定对象的一个成员变量上关闭序列化,可以在这个成员变量前加上关键字transient。

import java.io.Serializable;

public class Student implements Serializable {

    private static final long serialVersionUID = 771652260758459933L;

    private String id;

    private String name;

    private Integer age;
    // transient 修饰的属性不会被序列化
    private transient String tempNum;

    public Student(String id, String name, Integer age, String tempNum) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.tempNum = tempNum;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", tempNum='" + tempNum + '\'' +
                '}';
    }
}

序列化

Student student = new Student("s1", "tom", 18,"t1");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./s1"));
objectOutputStream.writeObject(student);
objectOutputStream.flush();
objectOutputStream.close();

反序列化

ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("./s1"));
Student student = (Student)objectInputStream.readObject();
System.out.println(student);
objectInputStream.close();
自定义序列化规则

简单的话,可以在实现Serializable接口的类中,编写private void writeObject(ObjectOutputStream out) (序列化时会调用此方法)和private void readObject(ObjectInputStream in)(反序列化时会调用此方法)两个方法来实现,但是性能欠佳

可以实现java.io.Externalizable接口,并重写writeExternalreadExternal方法来实现,性能不错

import java.io.*;

public class Student implements Externalizable {

    private static final long serialVersionUID = 771652260758459933L;

    private String id;

    private String name;

    private Integer age;
    // transient 修饰的属性不会被序列化
    private transient String tempNum;

    // 实现Externalizable反序列化必须要有无参构造
    public Student() {
    }

    public Student(String id, String name, Integer age, String tempNum) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.tempNum = tempNum;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", tempNum='" + tempNum + '\'' +
                '}';
    }

    // 定义序列化方法
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(this.id);
        out.writeUTF(this.name);
        out.writeInt(this.age);
    }
    // 定义反序列化逻辑
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        // 读写顺序一定要一致
        this.id = in.readUTF();
        this.name = in.readUTF();
        this.age = in.readInt();
    }
}

序列化

Student student = new Student("s1", "tom", 18,"t1");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./s1"));
objectOutputStream.writeObject(student);
objectOutputStream.flush();
objectOutputStream.close();

反序列化

ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("./s1"));
Student student = (Student)objectInputStream.readObject();
System.out.println(student);
objectInputStream.close();

NIO

很多技术框架都使用NIO技术,学习和掌握Java NIO技术对于高性能、高并发网络的应用是非常关键的。

NIO简介

NIO (New lO)也有人称之为Java non-blocking lO是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java lO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。

  • NIO相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写。
  • NIO有三大核心部分:Channel(通道)Buffer(缓冲区)Selector(选择器)
  • Java NlO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
  • 通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有1000个请求过来,根据实际情况,可以分配20或者80个线程来处理。不像之前的阻塞IO那样,非得分配1000个。

NIO VS BIO

BIO

BIO全称是Blocking IO,同步阻塞式IO,是JDK1.4之前的传统IO模型,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如下图所示

image-20230411164203045

虽然此时服务器具备了高并发能力,即能够同时处理多个客户端请求了,但是却带来了一个问题,随着开启的线程数目增多,将会消耗过多的内存资源,导致服务器变慢甚至崩溃,NIO可以一定程度解决这个问题。

NIO

同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。

image-20230411171848803

一个线程中就可以调用多路复用接口(java中是select)阻塞同时监听来自多个客户端的IO请求,一旦有收到IO请求就调用对应函数处理,NIO擅长1个线程管理多条连接,节约系统资源。

关系图的说明:

  1. 每个Channel对应一个 Buffer。
  2. Selector 对应一个线程,一个线程对应多个Channel。
  3. 该图反应了有三个Channel注册到该Selector。
  4. 程序切换到那个Channel是由事件决定的(Event)。
  5. Selector会根据不同的事件,在各个通道上切换。
  6. Buffer就是一个内存块,底层是一个数组。
  7. 数据的读取和写入是通过Buffer,但是需要flip()切换读写模式,而BIO是单向的,要么输入流要么输出流。

Buffer

Buffer本质上就是一块可以重复进行读写的内存空间(底层就是个==数组==),重要的五个概念如下

  • capacity:容量,缓冲区的总长度(数组的长度),如果缓冲区已满还需要写入数据,就需要先清空再写入,并且创建后不能更改
  • position:位置,类似于指针,表示下一个要读取或写入的数据的索引。起始位置为0,随着数据的写入不断的后移,最大为capacity - 1。当从buffer中读取数据时,position重置回0,记录下一个要读取数据的位置。
  • limit:缓冲区中不可操作的下一个元素的位置(界限),即操作停止的最终位置 + 1,用于限制程序可以写入或者读取的数据量,通常为limit <= capacity
  • **标记 (mark)与重置 (reset):**标记是一个索引, 通过 Buffer 中的mark()方法 指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个 position
  • 四个变量的关系:0 <= mark <= position <= limit <= capacity

创建Buffer

  • 常见的Buffer子类
    • ByteBuffer
    • CharBuffer
    • ShortBuffer
    • IntBuffer
    • LongBuffer
    • FloatBuffer
    • DoubleBuffer
// 创建非直接缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 创建直接缓冲区(只有ByteBuffer有这个方法)
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
直接缓冲区和非直接缓冲区

只有ByteBuffer可以区分创建直接缓冲区和非直接缓冲区

  • 直接缓冲区
    • ByteBuffer.allocateDirect()
    • 创建在非应用内存(非堆内存)上
    • 数据流:本地IO-->直接内存-->本地IO
    • 使用场景:存储大数据量、长生命周期数据、频繁IO操作
  • 非直接缓冲区
    • ByteBuffer.allocate()
    • 创建在应用内存(堆内存)
    • 数据流:本地IO-->直接内存-->非直接内存-->直接内存-->本地IO
    • 使用场景:存储小数据量、短生命周期数据、不频繁IO操作

常见方法

  1. Buffer clear():清空缓冲区并返回对缓冲区的引用,并且设置position=0limit=capcity
  2. Buffer flip():为将缓冲区的界限设置为当前位置, 并将当前位置重置为0
  3. int capacity():返回 Buffer 的 capacity 大小
  4. boolean hasRemaining(): 判断缓冲区中是否还有元素
  5. int limit():返回 Buffer 的界限(limit) 的位置
  6. Buffer limit(int n):将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象
  7. Buffer mark():对缓冲区设置标记
  8. int position():返回缓冲区的当前位置 position
  9. Buffer position(int n):将设置缓冲区的当前位置为 n, 并返回修改后的 Buffer 对象
  10. int remaining():返回 position 和 limit 之间的元素个数
  11. Buffer reset():将位置 position 转到以前设置的mark 所在的位置
  12. Buffer rewind():将位置设为为 0, 取消设置的 mark
  13. get():读取单个字节
  14. get(byte[] dst):批量读取多个字节到 dst 中
  15. get(int index):读取指定索引位置的字节(不会移动 position)放到入数据到Buffer中
  16. put(byte b):将给定单个字节写入缓冲区的当前位置
  17. put(byte[] src):将 src 中的字节写入缓冲区的当前位置
  18. put(int index, byte b):将指定字节写入缓冲区的索引 位置(不会移动 position)
Buffer读写数据的四个步骤
  1. 写入数据到Buffer
  2. 调用flip()方法,转换为读取模式
  3. 从Buffer中读取数据
  4. 调用buffer.clear()方法或者buffer.compact()方法清除缓冲区

理解

1、创建一个容量capacity5的buffer,此时缓冲区默认为写模式

ByteBuffer buffer = ByteBuffer.allocate(10);

image-20230411174015113

2、写入数据到Buffer中,写入三个字节

channel.read(buffer)

image-20230411174028977

3、切换为读模式,将position指向0位置,并将limit指向上一步position的位置3(最后有效数据的后一位)

buffer.flip();

image-20230413111358769

4、从Buffer中逐个字节读取,直到position = limit位置

StringBuilder stringBuilder = new StringBuilder();
while (buffer.position() < buffer.limit()){
    stringBuilder.append((char) buffer.get());
}

5、清空Buffer,还原为写模式,准备下一次读取,循环至第一步

buffer.clear();

Channel

Channel 是 NIO 的核心概念,它表示 IO 源与目标打开的连接,这个连接可以连接到 I/O 设备(例如:磁盘文件,Socket)或者一个支持 I/O 访问的应用程序。 Channel 类似于传统的“流”,只不过 Channel本身不能直接访问数据,Channel 只能与 Buffer 进行交互。

Channel和Stream的区别

  • 通道可以同时进行读写,而流只能读或者只能写
  • 通道可以实现异步读写数据
  • 通道可以从缓冲读数据,也可以写数据到缓冲

创建Channel

  • 常见Channel实现类
    • FileChannel:用于读取、写入、映射和操作文件的通道。
    • DatagramChannel:通过UDP读写网络中的数据通道。
    • SocketChannel:通过TCP读写网络中的数据。
    • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个SocketChannel。
  • 获取Channel的方式
    • 对于支持Channel的对象调用getChannel()方法
      • FileInputStream
      • FileOutputStream
      • RandomAccessFile
      • DatagramSocket
      • Socket
      • ServerSocket
    • 使用 Files 类的静态方法newByteChannel()获取字节通道
    • 通过通道的静态方法open()打开并返回指定通道

FileChannel

Java NIO FileChannel是连接文件的通道。使用FileChannel,可以从文件中读取数据和将数据写入文件。

FileChannel 的优点包括:

  • 在文件的特定位置读取和写入
  • 将文件的一部分直接加载到内存中,这样效率最高;
  • 可以以更快的速度将文件数据从一个通道传输到另一个通道
  • 可以锁定文件的一部分以限制其他线程访问;
  • 为了避免数据丢失,我们可以强制将更新的文件立即写入存储
open

FileChannel可以通过静态方法public static FileChannel open(Path path, OpenOption... options)打开

OpenOption
package java.nio.file;

public enum StandardOpenOption implements OpenOption {
    READ, // 读
    WRITE, // 写
    APPEND, // 追加写
    TRUNCATE_EXISTING, // 如果文件存在并且以WRITE的方式连接时就会把文件内容清空,如果文件只以READ连接时,该选项会被忽略
    CREATE, // 如果文件不存在则创建
    CREATE_NEW, // 创建文件如果存在则抛异常
    DELETE_ON_CLOSE, // Channel关闭时删除文件
    SPARSE, // 创建稀疏文件,与CREATE_NEW选项配合使用
    SYNC, // 要求每次写入要把内容和元数据刷到存储设备上
    DSYNC; // 要求每次写入要把内容刷到存储设备上
}
常见方法
  1. int read(ByteBuffer dst):从Channel 到 中读取数据到 ByteBuffer
  2. long read(ByteBuffer[] dsts): 将Channel中的数据“分散”到 ByteBuffer[]
  3. int write(ByteBuffer src):将 ByteBuffer中的数据写入到 Channel
  4. long write(ByteBuffer[] srcs):将 ByteBuffer[] 到 中的数据“聚集”到 Channel
  5. long position():返回此通道的文件位置
  6. FileChannel position(long p):设置此通道的文件位置
  7. long size():返回此通道的文件的当前大小
  8. FileChannel truncate(long s):将此通道的文件截取为给定大小
  9. void force(boolean metaData):强制将所有对此通道的文件更新写入到存储设备中
零拷贝

零拷贝是指计算机操作的过程中,CPU不需要为数据在内存之间的拷贝消耗资源。

long transferTo(long position, long count, WritableByteChannel target)

long transferFrom(ReadableByteChannel src, long position, long count)

这两个方法底层调用了Linux和UNIX系统的sendfile()系统调用实现了零拷贝

// 源数据通道
FileChannel sChan = FileChannel.open(Paths.get("D:\\test.jpg"), StandardOpenOption.READ);
// 目标数据通道
FileChannel tChan = FileChannel.open(Paths.get("E:\\test_copy.jpg"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
// 进行拷贝
sChan.transferTo(0,sChan.size(),tChan);
sChan.close();
tChan.close();
Demo
读数据
// 1、创建通道
FileChannel channel = FileChannel.open(Paths.get("D:\\test.txt"), StandardOpenOption.READ);
// 2、创建字节缓冲区,大小10
ByteBuffer buffer = ByteBuffer.allocate(5);
// 3、读数据
StringBuilder stringBuilder = new StringBuilder();
// 4、每次读取字节数量 > 0,并且 <= buffer.capacity()
while (channel.read(buffer) > 0){
    // 5、切换为读模式
    buffer.flip();
    // 6、将Buffer中的字节挨个读取
    while (buffer.position() < buffer.limit()){
        stringBuilder.append((char) buffer.get());
    }
    // 7、清空Buffer切换为写模式
    buffer.clear();
}
// 8、关闭通道
channel.close();
System.out.println(stringBuilder.toString());
写数据
// 1、创建通道
FileChannel channel = FileChannel.open(Paths.get("D:\\test.txt"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
// 2、创建字节缓冲区,大小1024
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 3、读数据
String target = "Hello Java Nio!";
byte[] bytes = target.getBytes(StandardCharsets.UTF_8);
// 4、依次写入字节数据
for (int i = 0; i < bytes.length ; i++){
    buffer.put(bytes[i]);
    if (buffer.position() == buffer.limit() || i == bytes.length - 1){
        // 5、切换写模式
        buffer.flip();
        // 6、缓冲中的数据写入到通道
        channel.write(buffer);
        // 7、清空缓冲
        buffer.clear();
    }
}
// 8、将通道中的数据强制刷出到磁盘
channel.force(false);
// 9、关闭通道
channel.close();

ServerSocketChannel和SocketChannel

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

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

  • DatagramChannel、SocketChannel、ServerSocketChannel)在被实例化时都会创建一个对等 socket 对象: java.net 包中的(DatagramSocketSocketServerSocket),可以通过Channel的socket()方法获取,此外,这三个 java.net 类现在都有getChannel()方法。

  • configureBlocking()(参数为false:非阻塞,true:阻塞)的作用:对于ServerSocketChannel来说,非阻塞意味着accept()方法为非阻塞,即时没有客户端连接,也会立即返回null;对于SocketChannel来说,read()方法为非阻塞,即时没有读到数据,也会继续执行下面的逻辑。

Demo
服务端
// 1、开启ServerSocketChannel
ServerSocketChannel channel = ServerSocketChannel.open();
// 2、设置监听的端口
channel.bind(new InetSocketAddress(8080));
// 3、设置为非阻塞
channel.configureBlocking(false);
SocketChannel socketChannel;
while (true){
    // 4、接受客户端连接
    socketChannel = channel.accept();
    // 判断是否获取到连接
    if (socketChannel != null){
        // 5、设置客户端连接通道为非阻塞
        socketChannel.configureBlocking(false);
        // 6、获取客户端信息
        Socket socket = socketChannel.socket();
        System.out.println("客户端连接:" + socket.getRemoteSocketAddress());
        // 7、创建Buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 8、轮询读取客户端发来的消息
        while (true){
            // 判断是否有消息
            if (socketChannel.read(buffer) > 0){
                buffer.flip();
                byte[] b = new byte[buffer.limit()];
                buffer.get(b);
                System.out.println("客户端说:" + new String(b, StandardCharsets.UTF_8));
                buffer.clear();
            }
        }
    }
}
客户端
// 1、建立和服务端的连接通道
SocketChannel channel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8080));
// 2、设置通道为非阻塞
channel.configureBlocking(false);
// 3、创建Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4、向服务器发送消息
while (true){
    String s = new Scanner(System.in).nextLine();
    if ("exit".equals(s)){
        break;
    }
    buffer.put(s.getBytes(StandardCharsets.UTF_8));
    buffer.flip();
    channel.write(buffer);
    buffer.clear();
}

Selector

选择器(Selector)是SelectableChannle对象多路复用器,Selector可以同时监控多个SelectableChannel的IO状况,也就是说,利用Selector可使一个单独的线程管理多个Channel。Selector是非阻塞IO的核心。

  • Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)
  • Selector 能够检测多个注册的通道上是否有事件发生。注意:多个 Channel 以事件的方式可以注册到同一个Selector,如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管
  • 理多个通道,也就是管理多个连接和请求。
  • 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
  • 避免了多线程之间的上下文切换导致的开销

注意:注册到Selector的Channel必须是非阻塞的以及可选择的(继承实现SelectableChannel抽象类),FileChannel不能注册到Selector,因为FileChannel不能切换为非阻塞模式,SelectableChannel抽象类有一个configureBlocking()方法,SocketChannel, ServerSocketChannel, DatagramChannel 都是直接继承了 AbstractSelectableChannel(SelectableChannel的子类)抽象类。

常用方法

  • Selector.open():创建选择器

  • SelectableChannel.register(Selector sel, int ops):将一个可选择的通道注册到选择器sel上,第二个参数是选择选择器监听Channel关注什么事件

    • SelectionKey.OP_CONNECT:连接就绪,一个客户端成功连接到另一个服务器时触发

    • SelectionKey.OP_ACCEPT:接收就绪,当服务端收到客户端的一个连接请求时触发

    • SelectionKey.OP_READ:读就绪,有一个数据可读的通道时触发

    • SelectionKey.OP_WRITE:写就绪,有一个等待写数据的通道时触发

  • int select():阻塞到至少有一个通道在注册的事件上就绪了

  • select(long timeout):和select()方法一样,可以设置最长阻塞时间(毫秒)

  • selectNow():不会阻塞,即时没有通道事件也会返回0

  • selectedKeys():一旦调用了select()方法,并且返回值表明有一个或更多个通道就绪了,然后可以调用该方法获取所有就绪通道。

  • keys():返回当前所有注册在selector中channel的selectionKey,和selectedKeys()的区别就是,这个方法返回的是所有已注册的通道,而不是当前已就绪的通道。

Demo

服务端

// 创建选择器
Selector selector = Selector.open();
// 创建可选择通道ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
// 设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
// 注册到选择器,并且关注通道的连接事件
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
// select()为阻塞方法,当获取到新事件后,会返回事件的数量,并将就绪事件放入SelectionKey集合中
while (selector.select() > 0){
    // 获取事件集合
    Set<SelectionKey> selectionKeys = selector.selectedKeys();
    Iterator<SelectionKey> iterator = selectionKeys.iterator();
    // 处理每一个事件
    while (iterator.hasNext()){
        SelectionKey selectionKey = iterator.next();
        // 如果是连接事件(有新的客户端连接)
        if (selectionKey.isAcceptable()){
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false);
            System.out.println("有新的客户端连接:" + socketChannel.socket().getRemoteSocketAddress());
            // 将客户端通道注册到选择器,并且关注通道的可读事件
            socketChannel.register(selector,SelectionKey.OP_READ);
        // 如果是可读取事件
        }else if (selectionKey.isReadable()){
            // 创建buffer接数据
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 获取事件对应的通道
            SocketChannel channel = (SocketChannel)selectionKey.channel();
            channel.read(buffer);
            buffer.flip();
            System.out.println("收到客户端信息:" + new String(buffer.array()));
        }
        // 处理完事件,切记要移除事件
        iterator.remove();
    }
}

客户端

SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8080));
socketChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true){
    System.out.print("input:");
    String a = new Scanner(System.in).nextLine();
    if ("quit".equals(a)){
        break;
    }
    byte[] bytes = a.getBytes(StandardCharsets.UTF_8);
    for (int i = 0 ;i < bytes.length;i ++){
        buffer.put(bytes[i]);
        if (buffer.position() == buffer.limit() || i == bytes.length - 1){
            buffer.flip();
            socketChannel.write(buffer);
            buffer.clear();
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GradyYoung

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值