3、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类的常用方法

过滤文件

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 流体系

分类 字节输入流 字节输出流 字符输入流 字符输出流
抽象基类 InputStream OutputStream Reader Writer
访问文件 FileInputStream FileOutputStream FileReader FileWriter
访问数组 ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter
访问管道 PipedInputStream PipedOutputStream PipedReader PipedWriter
访问字符串 StringReader StringWriter
缓冲流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter
转换流 InputStreamReader OutputSreamWriter
对象流 ObjectInputStream ObjectOutputStream
FilterInputStream FilterOutputStream FilterReader FilterWriter
打印流 PrintStream PrintWriter
推回输入流 PushbackInputStream PushbackReader
特殊流 DataInputStream DataOutputStream

IO类的继承关系

这里写图片描述

节点流

如:InputStream、Reader、OutputStream、Writer

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

输入流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(字符流)**是所有输出流的基类。

因为字符流直接以字符作为操作单位,所以 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();
常见的编码表

客户端/浏览器端——后台(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 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本质上就是一块可以重复进行读写的内存空间(底层就是个数组),重要的五个概念如下

创建Buffer

// 创建非直接缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 创建直接缓冲区(只有ByteBuffer有这个方法)
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
直接缓冲区和非直接缓冲区

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

常见方法

  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

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

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的核心。

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

常用方法

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();
        }
    }
}