NIO&&Buffer&&Channel&&Selector

文章目录

1.BIO,NIO,AIO的区别

1.1 BIO(Blocking I/O)

同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内,等待其完成。调用是可靠的线性顺序。

在这里插入图片描述

1.2 NIO (New I/O)

NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。

Java中的NIO实际上是多路复用:IO实际指的就是网络的IO、多路也就是多个不同的tcp连接;复用也就是指使用同一个线程合并处理多个不同的IO操作,这样的话可以减少CPU资源。(单个线程可以同时处理多个不同的io操作,应用场景非常广泛:redis原理。Mysql连接原理)

在这里插入图片描述

1.3 AIO(Asynchronous I/O)

AIO 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

在这里插入图片描述

2. Java NIO概述

2.1 NIO解释

NIO 采用非阻塞模式,基于 Reactor 模式的工作方式,I/O 调用不会被阻塞,它的实现过程是:会先对每个客户端注册感兴趣的事件,然后有一个线程专门去轮询每个客户端是否有事件发生,当有事件发生时,便顺序处理每个事件,当所有事件处理完之后,便再转去继续轮询。如下图所示:

在这里插入图片描述

NIO 中实现非阻塞 I/O 的核心对象就是 Selector,Selector 就是注册各种I/O事件地方,而且当我们感兴趣的事件发生时,就是这个对象告诉我们所发生的事件,如下图所示:

在这里插入图片描述

NIO 的最重要的地方是当一个连接创建后,不需要对应一个线程,这个连接会被注册到多路复用器上面,一个选择器线程可以同时处理成千上万个连接,系统不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。

2.2 NIO核心点

Java NIO由以下几个核心组成

  • Channels(管道)
  • Buffers(缓冲区)
  • Selectors(选择器)

虽然 Java NIO 中除此之外还有很多类和组件,但 Channel,Buffer 和Selector 构成了核心的 API。其它组件,如 Pipe 和 FileLock,只不过是与三个核心组件共同使用的工具类。

3. Buffer

首先从缓冲区看起,Java NIO就是面向缓冲区的,无论取数据还是拿数据,都是在Buffer中得到。Buffer相当于一个水池,两端的水都放在水池中,需要的时候就来池子里面取。

3.1 Buffer简介

buffer本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到 Stream 对象中。

在NIO中,所有的缓冲区类型都继承于抽象类 Buffer,最常用的就是ByteBuffer,对于 Java 中的基本类型,基本都有一个具体 Buffer 类型与之相对应,它们之间的继承关系如下图所示:

在这里插入图片描述

3.2 Buffer的基本用法

使用Buffer读写数据,一般就是四步:

  1. 写入数据到Buffer
  2. 调用flip()方法,(此方法将buffer的写状态变为读状态)
  3. 从Buffer中读取数据
  4. 调用clear()(完全清楚数据)或者compact()(保留未读取的)

当向 buffer 写入数据时,buffer 会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将 Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到buffer 的所有数据。一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用 clear()或 compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

例子:

public void test() throws Exception {
    RandomAccessFile file = new RandomAccessFile("D:\\test.txt");
    FileChannel inChannel = aFile.getChannel();
    //create buffer with capacity of 48 bytes
    ByteBuffer buf = ByteBuffer.allocate(48);
    int bytesRead = inChannel.read(buf); //read into buffer. 
    while (bytesRead != -1) {
        buf.flip(); //make buffer ready for read
        while(buf.hasRemaining()){
            System.out.print((char) buf.get()); // read 1 byte at a time
        }
        buf.clear(); //make buffer ready for writing
        bytesRead = inChannel.read(buf);
    }
    aFile.close();
}

3.3 Buffer关键参数

  • Capacity:容量
  • position:当前读写指针的头
  • limit:最多可以读的数量

position 和 limit 的含义取决于 Buffer 处在读模式还是写模式。不管Buffer 处在什么模式,capacity 的含义总是一样的。

这里有一个关于 capacity,position 和 limit 在读写模式中的说明

在这里插入图片描述

3.3.1 capacity

作为一个内存块,Buffer 有一个固定的大小值,叫“capacity”.你只能往里写capacity 个 byte、long,char 等类型。一旦 Buffer 满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。

3.3.2 position

  1. 写数据到 Buffer 中时,position 表示写入数据的当前位置,position 的初始值为0。当一个 byte、long 等数据写到 Buffer 后, position 会向下移动到下一个可插入数据的 Buffer 单元。position 最大可为 capacity – 1(因为 position 的初始值为0).
  2. 读数据到 Buffer 中时,position 表示读入数据的当前位置,如position=2 时表示已开始读入了 3 个 byte,或从第 3 个 byte 开始读取。通过ByteBuffer.flip()切换到读模式时 position 会被重置为 0,当 Buffer 从 position 读入数据后,position会下移到下一个可读入的数据 Buffer 单元。

3.3.3 limit

  1. 写数据时,limit 表示可对 Buffer 最多写入多少个数据。写模式下,limit 等于Buffer 的 capacity。
  2. 读数据时,limit 表示 Buffer 里有多少可读数据(not null 的数据),因此能读到之前写入的所有数据(limit 被设置成已写数据的数量,这个值在写模式下就是position)。

3.4 Buffer常用方法分析

3.4.1 翻转buffer模式:flip()

public final Buffer flip() {
    limit = position; //limit指向最后的可读数据
    position = 0; //position指到0,表示从头开始读
    mark = -1;
    return this;
}

3.4.2 buffer长度的初始化:allocate()

public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}

//实际调用的方法
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;
    }
}

3.4.3 清空Buffer内容:clear()

position 将被设回 0,limit 被设置成capacity 的值。换句话说,Buffer 被清空了。Buffer 中的数据并未清除,只是这些标记告诉我们可以从哪里开始往 Buffer 里写数据。

public final Buffer clear() {
    position = 0; //将位置直接指向头部
    limit = capacity;
    mark = -1;
    return this;
}

3.4.4 保留Buffer中未读取的内容:compact()

如果 Buffer 中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用 compact()方法。

compact()方法将所有未读的数据拷贝到 Buffer 起始处。然后将position 设到最后一个未读元素正后面。limit 属性依然像 clear()方法一样,设置成capacity。现在Buffer 准备好写数据了,但是不会覆盖未读的数据。

public ByteBuffer compact() {
    System.arraycopy(hb, ix(position()), hb, ix(0), remaining()); //将未读取的移到前面
    position(remaining());//position设到最后一个未读元素正后面
    limit(capacity());
    discardMark();
    return this;
}

3.4.5 将一个字节数组包裹至Buffer中:wrap()

public static ByteBuffer wrap(byte[] array) {
    return wrap(array, 0, array.length);
}

3.4.6 重读Buffer中的数据:rewind()

public final Buffer rewind() {
    position = 0; //position 设回 0,可以重读 Buffer 中的所有数据
    mark = -1;
    return this;
}

3.4.7 标记特定位置和恢复:mark()、reset()

通过调用 Buffer.mark()方法,可以标记 Buffer 中的一个特定position。之后可以通过调用 Buffer.reset()方法恢复到这个 position。

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

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

3.5 缓冲区操作

3.5.1 缓冲区分片

在 NIO 中,除了可以分配或者包装一个缓冲区对象外,还可以根据现有的缓冲区对象来创建一个子缓冲区,即在现有缓冲区上切出一片来作为一个新的缓冲区,但现有的缓冲区与创建的子缓冲区在底层数组层面上是数据共享的,也就是说,子缓冲区相当于是现有缓冲区的一个视图窗口。调用 slice()方法可以创建一个子缓冲区。

public void testConect3() throws IOException {
    ByteBuffer buffer = ByteBuffer.allocate(10);
    // 缓冲区中的数据 0-9
    for (int i = 0; i < buffer.capacity(); ++i) {
        buffer.put((byte) i);
    }
    // 创建子缓冲区
    buffer.position(3);
    buffer.limit(7);
    ByteBuffer slice = buffer.slice();
    // 改变子缓冲区的内容
    for (int i = 0; i < slice.capacity(); ++i) {
        byte b = slice.get(i);
        b *= 10;
        slice.put(i, b);
    }
    buffer.position(0);
    buffer.limit(buffer.capacity());
    while (buffer.remaining() > 0) {
        System.out.println(buffer.get());
    }
}

slice()源码:通过设置位置指针和最大长度,进行切片

public ByteBuffer slice() {
    int pos = this.position();
    int lim = this.limit();
    int rem = (pos <= lim ? lim - pos : 0);
    return new HeapByteBuffer(hb,
                              -1,
                              0,
                              rem,
                              rem,
                              pos + offset);
}

3.5.2 只读缓冲区

只读缓冲区非常简单,可以读取它们,但是不能向它们写入数据。可以通过调用缓冲区的 asReadOnlyBuffer()方法,将任何常规缓冲区转 换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化:

public void testConect4() throws IOException {
    ByteBuffer buffer = ByteBuffer.allocate(10);
    // 缓冲区中的数据 0-9
    for (int i = 0; i < buffer.capacity(); ++i) {
        buffer.put((byte) i);
    }
    // 创建只读缓冲区
    ByteBuffer readonly = buffer.asReadOnlyBuffer();
    // 改变原缓冲区的内容
    for (int i = 0; i < buffer.capacity(); ++i) {
        byte b = buffer.get(i);
        b *= 10;
        buffer.put(i, b);
    }
    readonly.position(0);
    readonly.limit(buffer.capacity());
    // 只读缓冲区的内容也随之改变
    while (readonly.remaining() > 0) {
        System.out.println(readonly.get());
    }
}

如果尝试修改只读缓冲区的内容,则会报 ReadOnlyBufferException 异常。只读缓冲区对于保护数据很有用。在将缓冲区传递给某个 对象的方法时,无法知道这个方法是否会修改缓冲区中的数据。创建一个只读的缓冲区可以保证该缓冲区不会被修改。只可以把常规缓冲区转换为只读缓冲区,而不能将只读的缓冲区转换为可写的缓冲区。

3.5.3 直接缓冲区

直接缓冲区是为加快 I/O 速度,使用一种特殊方式为其分配内存的缓冲区,JDK文档中的描述为:给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中 或者从一个中间缓冲区中拷贝数据。要分配直接缓冲区,需要调用 allocateDirect()方法,而不是 allocate()方法,使用方式与普通缓冲区并无区别。

public void testConect5() throws IOException {
    String infile = "d:\\01.txt";
    FileInputStream fin = new FileInputStream(infile);
    FileChannel fcin = fin.getChannel();
    String outfile = String.format("d:\\02.txt");
    FileOutputStream fout = new FileOutputStream(outfile);
    FileChannel fcout = fout.getChannel();
    // 使用 allocateDirect,而不是 allocate
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    while (true) {
        buffer.clear();
        int r = fcin.read(buffer);
        if (r == -1) {
            break;
        }
        buffer.flip();
        fcout.write(buffer);
    }
}

3.5.4 内存映射文件I/O

内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快的多。内存映射文件 I/O 是通过使文件中的数据出现为内存数组的内容来完成的,这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说,只有文件中实际读取或者写入的部分才会映射到内存中。

static private final int start = 0;
static private final int size = 1024;
static public void main(String args[]) throws Exception {
    RandomAccessFile raf = new RandomAccessFile("d:\\01.txt", "rw");
    FileChannel fc = raf.getChannel();
    MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, start, size);
    mbb.put(0, (byte) 97);
    mbb.put(1023, (byte) 122);
    raf.close();
}

4.Channel

4.1 概述

Java NIO 的通道类似流,但又有些不同:

  • 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
  • 通道可以异步地读写。
  • 通道中的数据总是要先读到一个 Buffer,或者总是要从一个Buffer 中写入。

4.2 Channel的主要实现

  • FileChannel:文件读写
  • DatagramChannel:UDP读写网络数据
  • SocketChannel:通过TCP读写网络数据
  • ServerSockectChannel:可以监听新进来的 TCP 连接,像Web 服务器那样。对每一个新进来的连接都会创建一个 SocketChannel。

4.3 FileChannel

4.3.1 FileChannel 简介

FileChannel 类可以实现常用的 read,write 以及 scatter/gather 操作,同时它也提供了很多专用于文件的新方法。这些方法中的许多都是我们所熟悉的文件操作。

在这里插入图片描述

下面给出一个使用FileChannel的例子

public static void main(String[] args) throws IOException {
    RandomAccessFile aFile = new
        RandomAccessFile("d:\\01.txt", "rw"); //打开文件,支持随机读写
    FileChannel inChannel = aFile.getChannel(); //从文件读取器中获得管道
    ByteBuffer buf = ByteBuffer.allocate(48); //申请48字节的buffer
    int bytesRead = inChannel.read(buf);//将文件的内容读取到缓冲区buffer
    while (bytesRead != -1) {
        System.out.println("读取: " + bytesRead);
        buf.flip();//从写模式换到读模式
        while (buf.hasRemaining()) {
            System.out.print((char) buf.get());
        }
        buf.clear();//清空缓冲区
        bytesRead = inChannel.read(buf);//继续读
    }
    aFile.close();
    System.out.println("操作结束");
}

4.3.2 支持特定位置读写:position()

有时可能需要在 FileChannel 的某个特定位置进行数据的读/写操作。可以通过调用position()方法获取 FileChannel 的当前位置。也可以通过调用position(longpos)方法设置 FileChannel 的当前位置。

如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回- 1 (文件结束标志)。 如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。

4.3.3 截取文件:truncate()

可以使用 FileChannel.truncate()方法截取一个文件。截取文件时,文件将中指定长度后面的部分将被删除。

如: channel.truncate(1024);

这个例子截取文件的前 1024 个字节。

4.3.4 强制写入磁盘:force()

FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到FileChannel 里的数据一定会即时写到磁盘上。要保证这一点,需要调用 force()方法。force()方法有一个 boolean 类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。

4.3.5 通道之间的数据传输:transferTo() 、transferFrom()

通道之间的数据传输:

如果两个通道中有一个是 FileChannel,那你可以直接将数据从一个channel 传输到另外一个 channel。

  1. transferFrom()

    FileChannel 的 transferFrom()方法可以将数据从源通道传输到FileChannel 中(译者注:这个方法在 JDK 文档中的解释为将字节从给定的可读取字节通道传输到此通道的文件中)。

    public static void main(String args[]) throws Exception {
        RandomAccessFile aFile = new
            RandomAccessFile("d:\\01.txt", "rw");
        FileChannel fromChannel = aFile.getChannel();
        RandomAccessFile bFile = new
            RandomAccessFile("d:\\02.txt", "rw");
        FileChannel toChannel = bFile.getChannel();
        
        long position = 0;
        long count = fromChannel.size();
        toChannel.transferFrom(fromChannel, position, count);
        aFile.close();
        bFile.close();
        System.out.println("over!");
    }
    

    方法的输入参数 position 表示从 position 处开始向目标文件写入数据,count 表示最多传输的字节数。如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数。此外要注意,在 SoketChannel 的实现中,SocketChannel 只会传输此刻准备好的数据(可能不足 count 字节)。因此,SocketChannel 可能不会将请求的所有数据(count 个字节)全部传输到 FileChannel 中。

  2. transferTo()

    transferTo()方法将数据从 FileChannel 传输到其他的 channel 中

    public static void main(String args[]) throws Exception {
        RandomAccessFile aFile = new
            RandomAccessFile("d:\\02.txt", "rw");
        FileChannel fromChannel = aFile.getChannel();
        RandomAccessFile bFile = new
            RandomAccessFile("d:\\03.txt", "rw");
        FileChannel toChannel = bFile.getChannel();
        long position = 0;
        long count = fromChannel.size();
        fromChannel.transferTo(position, count, toChannel);
        aFile.close();
        bFile.close();
        System.out.println("over!");
    }
    

4.3.6 Scatter/Gather

Java NIO 开始支持 scatter/gather,scatter/gather 用于描述从Channel 中读取或者写入到 Channel 的操作。

分散:scatter 从 Channel 中读取是指在读操作时将读取的数据写入多个buffer 中。因此,Channel 将从 Channel 中读取的数据“分散(scatter)”到多个Buffer 中。

聚集:gather 写入 Channel 是指在写操作时将多个 buffer 的数据写入同一个Channel,因此,Channel 将多个 Buffer 中的数据“聚集(gather)”后发送到Channel。

scatter / gather 经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer 中,这样你可以方便的处理消息头和消息体。

4.3.6.1 Scattering Reads

Scattering Reads 是指数据从一个 channel 读取到多个 buffer 中。如下图描述:

在这里插入图片描述

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);

注意 buffer 首先被插入到数组,然后再将数组作为 channel.read() 的输入参数。read()方法按照 buffer 在数组中的顺序将从 channel 中读取的数据写入到buffer,当一个 buffer 被写满后,channel 紧接着向另一个 buffer 中写。

Scattering Reads 在移动下一个 buffer 前,必须填满当前的 buffer,这也意味着它不适用于动态消息(译者注:消息大小不固定)。换句话说,如果存在消息头和消息体,消息头必须完成填充(例如 128byte),Scattering Reads 才能正常工作。

4.3.6.2 Gathering Writes

Gathering Writes 是指数据从多个 buffer 写入到同一个 channel。如下图描述:

在这里插入图片描述

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
//write data into buffers
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);

buffers 数组是 write()方法的入参,write()方法会按照 buffer 在数组中的顺序,将数据写入到 channel,注意只有 position 和 limit 之间的数据才会被写入。因此,如果一个 buffer 的容量为 128byte,但是仅仅包含 58byte 的数据,那么这58byte 的数据将被写入到 channel 中。因此与 Scattering Reads 相反,Gathering Writes 能较好的处理动态消息。

4.4 SocketChannel总述

  1. SocketChannel 就是 NIO 对于非阻塞 socket 操作的支持的组件,其在socket 上封装了一层,主要是支持了非阻塞的读写。同时改进了传统的单向流API,,Channel 同时支持读写。

  2. socket 通道类主要分为 DatagramChannel、SocketChannel 和ServerSocketChannel,它们在被实例化时都会创建一个对等socket 对象。要把一个socket 通道置于非阻塞模式,我们要依靠所有 socket 通道类的公有超级类:SelectableChannel。就绪选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作,如读或写。非阻塞I/O和可选择性是紧密相连的,那也正是管理阻塞模式的 API 代码要在SelectableChannel 超级类中定义的原因。

  3. 设置或重新设置一个通道的阻塞模式是很简单的,只要调用configureBlocking( )方法即可,传递参数值为 true 则设为阻塞模式,参数值为false值设为非阻塞模式。可以通过调用 isBlocking( )方法来判断某个socket 通道当前处于哪种模式。

public final SelectableChannel configureBlocking(boolean block)
    throws IOException
{
    synchronized (regLock) {
        if (!isOpen())
            throw new ClosedChannelException();
        if (blocking == block)
            return this;
        if (block && haveValidKeys())
            throw new IllegalBlockingModeException();
        implConfigureBlocking(block);
        blocking = block;
    }
    return this;
}

4.5 ServerSocketChannel

ServerSocketChannel类似于我们在传统I/O下的ServerSocket,只不过它增加了通道语义,可以在非阻塞模式下运行。

由于 ServerSocketChannel 没有 bind()方法,因此有必要取出对等的socket 并使用它来绑定到一个端口以开始监听连接。我们也是使用对等 ServerSocket 的API 来根据需要设置其他的 socket 选项。

同 java.net.ServerSocket 一样,ServerSocketChannel 也有accept( )方法。ServerSocketChannel 的 accept()方法会返回 SocketChannel 类型对象,SocketChannel 可以在非阻塞模式下运行。

示例:

public class FileChannelAccept {
    public static final String GREETING = "Hello java nio.\r\n";
    
    public static void main(String[] argv) throws Exception {
        int port = 1234; // default
        if (argv.length > 0) {
            port = Integer.parseInt(argv[0]);
        }
        ByteBuffer buffer = ByteBuffer.wrap(GREETING.getBytes());
        ServerSocketChannel ssc = ServerSocketChannel.open(); //创建ServerSocketChannel
        ssc.socket().bind(new InetSocketAddress(port));//绑定端口
        ssc.configureBlocking(false);//设置非阻塞模式
        while (true) {
            System.out.println("Waiting for connections");
            SocketChannel sc = ssc.accept();//接受请求
            if (sc == null) {
                System.out.println("null");
                Thread.sleep(2000);
            } else {
                System.out.println("Incoming connection from: " +
                                   sc.socket().getRemoteSocketAddress());
                buffer.rewind();
                sc.write(buffer);
                sc.close();
            }
        }
    }
}

常用方法:

  1. open() :打开ServerSocketChannel
  2. close(): 关闭ServerSocketChannel
  3. accept(): 监听新的连接,阻塞模式就阻塞在这块,非阻塞模式直接返回
  4. configureBlocking(): 设置阻塞模式

4.6 SocketChannel

4.6.1 SocketChannel介绍

Java NIO 中的 SocketChannel 是一个连接到 TCP 网络套接字的通道。

  • socketChannel 是用来连接 Socket 套接字
  • SocketChannel 主要用途用来处理网络 I/O 的通道
  • SocketChannel 是基于 TCP 连接传输
  • SocketChannel 实现了可选择通道,可以被多路复用的

4.6.2 SocketChannel特征

  • 对于已经存在的 socket 不能创建 SocketChannel

  • SocketChannel 中提供的 open 接口创建的 Channel 并没有进行网络级联,需要使 用 connect 接口连接到指定地址

  • 未进行连接的 SocketChannle 执行 I/O 操作时,会抛出 NotYetConnectedException

  • SocketChannel 支持两种 I/O 模式:阻塞式和非阻塞式

  • SocketChannel 支持异步关闭。如果 SocketChannel 在一个线程上 read 阻塞,另 一个线程对该 SocketChannel 调用 shutdownInput,则读阻塞的线程将返回-1 表示没有 读取任何数据;如果 SocketChannel 在一个线程上 write 阻塞,另一个线程对该 SocketChannel 调用 shutdownWrite,则写阻塞的线程将抛出 AsynchronousCloseException

  • SocketChannel 支持设定参数

    • SO_SNDBUF 套接字发送缓冲区大小

    • SO_RCVBUF 套接字接收缓冲区大小 -

    • SO_KEEPALIVE 保活连接

    • O_REUSEADDR 复用地址

    • SO_LINGER 有数据传输时延缓关闭 Channel (只有在非阻塞模式下有用)

    • TCP_NODELAY 禁用 Nagle 算法

4.6.1 SocketChannel的使用

  1. 创建SocketChannel

    //方式1
    SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80));
    
    //方式2
    SocketChannel socketChanne2 = SocketChannel.open();
    socketChanne2.connect(new InetSocketAddress("www.baidu.com", 80));
    

    直接使用有参 open api 或者使用无参 open api,但是在无参 open 只是创建了一个 SocketChannel 对象,并没有进行实质的 tcp 连接。

  2. 连接校验

    socketChannel.isOpen(); // 测试 SocketChannel 是否为 open 状态
    socketChannel.isConnected(); //测试 SocketChannel 是否已经被连接
    socketChannel.isConnectionPending(); //测试 SocketChannel 是否正在进行连接
    socketChannel.finishConnect(); //校验正在进行套接字连接的 SocketChannel是否已经完成连接
    
  3. 读写

    byte[] bytes = new String("GET / HTTP/1.0\\r\\n\\r\\n").getBytes();
    ByteBuffer buffer = ByteBuffer.wrap(bytes);
    socketChannel.write(buffer); //写
    
    ByteBuffer readBuff = MappedByteBuffer.allocate(1500);
    while (-1 != socketChannel.read(readBuff))  {
        System.out.println("----"); //读
        System.out.println(new String(readBuff.array()).trim());
        readBuff.clear();
    }
    
  4. 设置和获取参数

    //通过 setOptions 方法可以设置 socket 套接字的相关参数
    socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, Boolean.TRUE) .setOption(StandardSocketOptions.TCP_NODELAY, Boolean.TRUE);
    
    //可以通过 getOption 获取相关参数的值。如默认的接收缓冲区大小是 8192byte。
    socketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE);
    socketChannel.getOption(StandardSocketOptions.SO_RCVBUF);
    

4.7 DatagramChannel

正如 SocketChannel 对应 Socket,ServerSocketChannel 对应 ServerSocket,每 一个 DatagramChannel 对象也有一个关联的 DatagramSocket 对象。正如 SocketChannel 模拟连接导向的流协议(如 TCP/IP),DatagramChannel 则模拟包 导向的无连接协议(如 UDP/IP)。DatagramChannel 是无连接的,每个数据报 (datagram)都是一个自包含的实体,拥有它自己的目的地址及不依赖其他数据报的 数据负载。与面向流的的 socket 不同,DatagramChannel 可以发送单独的数据报给 不同的目的地址。同样,DatagramChannel 对象也可以接收来自任意地址的数据包。 每个到达的数据报都含有关于它来自何处的信息(源地址)

4.7.1.打开DatagramChannel

DatagramChannel server = DatagramChannel.open();
server.socket().bind(new InetSocketAddress(10086)); //打开10086端口接受UDP的包

4.7.2 接受数据

通过receive()接收UDP包

ByteBuffer receiveBuffer = ByteBuffer.allocate(64);
receiveBuffer.clear();
SocketAddress receiveAddr = server.receive(receiveBuffer);

SocketAddress 可以获得发包的 ip、端口等信息,用 toString 查看,格式如下 /127.0.0.1:57126

4.7.3 发送数据

DatagramChannel server = DatagramChannel.open();
ByteBuffer sendBuffer = ByteBuffer.wrap("client send".getBytes());
server.send(sendBuffer, new InetSocketAddress("127.0.0.1",10086)); //发送UDP包

4.7.4 连接

UDP 不存在真正意义上的连接,这里的连接是向特定服务地址用 read 和 write 接收 发送数据包。

client.connect(new InetSocketAddress("127.0.0.1",10086));
int readSize= client.read(sendBuffer);
server.write(sendBuffer);

5. Selector

5.1 简述

5.1.1 Selector 和Channel 关系

Selector 一般称 为选择器 ,也可以翻译为 多路复用器 。它是 Java NIO 核心组件中 的一个,用于检查一个或多个 NIO Channel(通道)的状态是否处于可读、可写。如 此可以实现单线程管理多个 channels,也就是可以管理多个网络链接。

在这里插入图片描述

使用 Selector 的好处在于: 使用更少的线程来就可以来处理通道了, 相比使用多个 线程,避免了线程上下文切换带来的开销。

5.1.2 可选择通道(SelectableChannel)

  1. 不是所有的 Channel 都可以被 Selector 复用的。比方说,FileChannel 就不能 被选择器复用。判断一个 Channel 能被 Selector 复用,有一个前提:判断他是否继承了一个抽象类 SelectableChannel。如果继承了 SelectableChannel,则可以被复用,否则不能。

  2. SelectableChannel 类提供了实现通道的可选择性所需要的公共方法。它是所有 支持就绪检查的通道类的父类。所有 socket 通道,都继承了 SelectableChannel 类 都是可选择的,包括从管道(Pipe)对象的中获得的通道。而 FileChannel 类,没有继 承 SelectableChannel,因此是不是可选通道。

  3. 一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。通道和选择器之间的关系,使用注册的方式完成。SelectableChannel 可以被注册到 Selector 对象上,在注册的时候,需要指定通道的哪些操作,是 Selector 感兴趣的。

    在这里插入图片描述

5.1.3 Channel注册到Selector

  1. 使用 Channel.register(Selector sel,int ops)方法,将一个通道注册到一个 选择器时。第一个参数,指定通道要注册的选择器。第二个参数指定选择器需要查询 的通道操作。
  2. 可以供选择器查询的通道操作,从类型来分,包括以下四种:
    • 可读 : SelectionKey.OP_READ
    • 可写 : SelectionKey.OP_WRITE
    • 连接 : SelectionKey.OP_CONNECT
    • 接收 : SelectionKey.OP_ACCEPT
    • 如果 Selector 对通道的多操作类型感兴趣,可以用“位或”操作符来实现: 比如:int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;
  3. 选择器查询的不是通道的操作,而是通道的某个操作的一种就绪状态。什么是操 作的就绪状态?一旦通道具备完成某个操作的条件,表示该通道的某个操作已经就绪, 就可以被 Selector 查询到,程序可以对通道进行对应的操作。比方说,某个 SocketChannel 通道可以连接到一个服务器,则处于“连接就绪”(OP_CONNECT)。 再比方说,一个 ServerSocketChannel 服务器通道准备好接收新进入的连接,则处于 “接收就绪”(OP_ACCEPT)状态。还比方说,一个有数据可读的通道,可以说是 “读就绪”(OP_READ)。一个等待写数据的通道可以说是“写就绪”(OP_WRITE)。

5.1.4 选择键(SelectionKey)

  1. Channel 注册到后,并且一旦通道处于某种就绪的状态,就可以被选择器查询到。 这个工作,使用选择器 Selector 的 select()方法完成。select 方法的作用,对感兴 趣的通道操作,进行就绪状态的查询。

  2. Selector 可以不断的查询 Channel 中发生的操作的就绪状态。并且挑选感兴趣 的操作就绪状态。一旦通道有操作的就绪状态达成,并且是 Selector 感兴趣的操作, 就会被 Selector 选中,放入选择键集合中。

  3. 一个选择键,首先是包含了注册在 Selector 的通道操作的类型,比方说 SelectionKey.OP_READ。也包含了特定的通道与特定的选择器之间的注册关系。

    开发应用程序是,选择键是编程的关键。NIO 的编程,就是根据对应的选择键,进行 不同的业务逻辑处理

  4. 选择键的概念,和事件的概念比较相似。一个选择键类似监听器模式里边的一个 事件。由于 Selector 不是事件触发的模式,而是主动去查询的模式,所以不叫事件 Event,而是叫 SelectionKey 选择键

5.2 Selector的使用方法

5.2.1 Selector的创建

Selector selector = Selector.open()

5.2.2 注册Channel到Selector

// 1、获取 Selector 选择器
Selector selector = Selector.open();
// 2、获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 3.设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 4、绑定连接
serverSocketChannel.bind(new InetSocketAddress(9999));
// 5、将通道注册到选择器上,并制定监听事件为:“接收”事件
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);

上面通过调用通道的 register()方法会将它注册到一个选择器上。 首先需要注意的是:

  1. 与 Selector 一起使用时,Channel 必须处于非阻塞模式下,否则将抛出异常 IllegalBlockingModeException。这意味着,FileChannel 不能与 Selector 一起使用,因 为 FileChannel 不能切换到非阻塞模式,而套接字相关的所有的通道都可以。
  2. 一个通道,并没有一定要支持所有的四种操作。比如服务器通道 ServerSocketChannel 支持 Accept 接受操作,而 SocketChannel 客户端通道则不支持。 可以通过通道上的 validOps()方法,来获取特定通道下所有支持的操作集合

5.2.3 轮询查询就绪操作

  1. 通过 Selector 的 select()方法,可以查询出已经就绪的通道操作,这些就绪的 状态集合,包存在一个元素是 SelectionKey 对象的 Set 集合中。

  2. 下面是 Selector 几个重载的查询 select()方法:

    • select():阻塞到至少有一个通道在你注册的事件上就绪了。
    • select(long timeout):和 select()一样,但最长阻塞事件为 timeout 毫秒。
    • selectNow():非阻塞,只要有通道就绪就立刻返回。

    select()方法返回的 int 值,表示有多少通道已经就绪,更准确的说,是自前一次 select 方法以来到这一次 select 方法之间的时间段上,有多少通道变成就绪状态。

    例如:首次调用 select()方法,如果有一个通道变成就绪状态,返回了 1,若再次调用 select()方法,如果另一个通道就绪了,它会再次返回 1。如果对第一个就绪的 channel 没有做任何操作,现在就有两个就绪的通道,但在每次 select()方法调用之间, 只有一个通道就绪了。 一旦调用 select()方法,并且返回值不为 0 时,在 Selector 中有一个 selectedKeys()方 法,用来访问已选择键集合,迭代集合的每一个选择键元素,根据就绪操作的类型, 完成对应的操作:

Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel. 
    } else if (key.isConnectable()) {
        // a connection was established with a remote server. 
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
    keyIterator.remove();
}

5.3.4 停止选择的方法

选择器执行选择的过程,系统底层会依次询问每个通道是否已经就绪,这个过程可能会造成调用线程进入阻塞状态,那么我们有以下三种方式可以唤醒在 select()方法中阻塞的线程。

  • wakeup()方法 :通过调用 Selector 对象的 wakeup()方法让处在阻塞状态的 select()方法立刻返回 该方法使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有进行中 的选择操作,那么下一次对 select()方法的一次调用将立即返回。 –
  • close()方法 :通过 close()方法关闭 Selector, 该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似 wakeup()),同时 使得注册到该 Selector 的所有 Channel 被注销,所有的键将被取消,但是 Channel 本身并不会关闭。

5.3 实例代码

@Test
public void ServerDemo() {
    try {
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000));
        ssc.configureBlocking(false);
        Selector selector = Selector.open();
        // 注册 channel,并且指定感兴趣的事件是 Accept
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        ByteBuffer readBuff = ByteBuffer.allocate(1024);
        ByteBuffer writeBuff = ByteBuffer.allocate(128);
        writeBuff.put("received".getBytes());
        writeBuff.flip();
        while (true) {
            int nReady = selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> it = keys.iterator();
            while (it.hasNext()) {
                SelectionKey key = it.next();
                it.remove();
                if (key.isAcceptable()) {
                    // 创建新的连接,并且把连接注册到 selector 上,而且,
                    // 声明这个 channel 只对读操作感兴趣。
                    SocketChannel socketChannel = ssc.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }
                else if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    readBuff.clear();
                    socketChannel.read(readBuff);
                    readBuff.flip();
                    System.out.println("received : " + new String(readBuff.array()));
                    key.interestOps(SelectionKey.OP_WRITE);
                }
                else if (key.isWritable()) {
                    writeBuff.rewind();
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    socketChannel.write(writeBuff);
                    key.interestOps(SelectionKey.OP_READ);
                }
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

客户端代码

@Test
public void ClientDemo() {
    try {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));
        ByteBuffer writeBuffer = ByteBuffer.allocate(32);
        ByteBuffer readBuffer = ByteBuffer.allocate(32);
        writeBuffer.put("hello".getBytes());
        writeBuffer.flip();
        while (true) {
            writeBuffer.rewind();
            socketChannel.write(writeBuffer);
            readBuffer.clear();
            socketChannel.read(readBuffer);
        }
    } catch (IOException e) {
    }
}

6. NIO编程步骤

1.创建 Selector 选择器
2.创建 ServerSocketChannel 通道,并绑定监听端口
3.设置 Channel 通道是非阻塞模式
4.把 Channel 注册到 Socketor 选择器上,监听连接事件
5.调用 Selector 的 select 方法(循环调用),监测通道的就绪状况
6.调用 selectKeys 方法获取就绪 channel 集合
7.遍历就绪 channel 集合,判断就绪事件类型,实现具体的业务操作
8.根据业务,决定是否需要再次注册监听事件,重复执行第三步操作
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值