NIO基础详解

NIO

JAVA NIO 概述

Java NIO(新IO)是Java的替代IO API(来自Java 1.4),意味着可以替代标准 Java IOJava Networking API。与标准IO API相比,Java NIO提供了一种不同的IO处理方式。

在Java NIO中的核心主要是由三部分组成:Buffer(缓冲区)、Channel(通道)、Selector(选择器)

Java NIO 和IO的主要区别

下表总结了Java IO和NIO之间的主要区别:

IO NIO
面向流 面向缓冲
阻塞IO 非阻塞IO
选择器

1、面向流与面向缓冲

Java IO和NIO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

2、阻塞与非阻塞IO

Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

io的各种流是阻塞的,就是当一个线程调用读写方法时,该线程会被阻塞,直到读写完,在这期间该线程不能干其他事,CPU转而去处理其他线程,假如一个线程监听一个端口,一天只会有几次请求进来,但是CPU却不得不为该线程不断的做上下文切换,并且大部分切换以阻塞告终。

NIO通讯是将整个任务切换成许多小任务,由一个线程负责处理所有io事件,并负责分发。它是利用事件驱动机制,而不是监听机制,事件到的时候再触发。NIO线程之间通过wait,notify等方式通讯。保证了每次上下文切换都有意义,减少无谓的进程切换。

3、选择器(Selectors)

Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

使用场景:

​ NIO可让您只使用一个(或几个)单线程管理多个通道(网络连接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。

如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,实现NIO的服务器可能是一个优势。同样,如果你需要维持许多打开的连接到其他计算机上,如P2P网络中,使用一个单独的线程来管理你所有出站连接,可能是一个优势。一个线程多个连接的设计方案如下图所示:

img

Java NIO: 单线程管理多个连接

如果你有少量的连接使用非常高的带宽,一次发送大量的数据,也许典型的IO服务器实现可能非常契合。下图说明了一个典型的IO服务器设计:

img

参考:

JAVA NIO Buffer(缓冲区)

与NIO通道进行交互时,将使用Java NIO缓冲区。如您所知,数据从通道读取到缓冲区,然后从缓冲区写入通道。

缓冲区本质上是一个内存块,您可以在其中写入数据,然后可以在以后再次读取。该内存块包装在NIO Buffer对象中,该对象提供了一组方法,可以更轻松地使用该内存块。

Buffer属性

​ 在Buffer(缓冲区)中主要三个属性:capacity(容量)、Position(位置)、Limit(最大允许值)

​ capacity(容量):作为存储块,Buffer具有一定的固定大小,也称为容量。只能将byte,long,char等类型写入到缓冲区中,一旦缓冲区已满,您需要先清空它(读取数据或清除它),然后才能向其中写入更多数据。

​ Position(位置):表示数据的位置,初始化位置为0,在写入模式时,按照postion的位置写入,当当前位置插入数据时,数据将移向下一个位置插入。最大的位置=capacity-1。

从a读取数据时,Buffer您也从指定位置读取数据。当您 Buffer从写入模式切换到读取模式时,该位置将重置为0。从中读取数据时,Buffer您将从中读取数据position,并position 前进到下一个读取位置。

​ Limit(最大允许值):在写入模式下,Buffer的限制是可以写入缓冲区的数据量的限制。在写模式下,限制等于的容量Buffer。当转换Buffer到读模式,限制意味着多少数据可以从数据中读取的限制。因此,当将a切换Buffer到读取模式时,将限制设置为写入模式的写入位置。换句话说,您可以读取与写入的字节一样多的字节(限制设置为写入的字节数,该字节数由位置标记)。

Java NIO:写入和读取模式下的缓冲区容量,位置和限制。
写入和读取模式下的缓冲区容量,位置和限制。
缓冲区类型

Buffer具有以下缓冲区类型

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer
Buffer使用

使用Buffer来读取和写入数据通常遵循以下四个步骤:

  1. 将数据写入缓冲区
  2. buffer.flip()
  3. 从缓冲区读取数据
  4. buffer.clear()或buffer.compact()

当您将数据写入缓冲区时,缓冲区会跟踪您已写入多少数据。一旦需要读取数据,就需要使用flip()方法调用将缓冲区从写入模式切换到读取模式。在读取模式下,缓冲区使您可以读取写入缓冲区的所有数据。

读取所有数据后,需要清除缓冲区,以使其可以再次写入。您可以通过两种方式执行此操作:通过调用clear()或通过 compact()。该clear()方法清除整个缓冲区。该compact() 方法仅清除您已读取的数据。任何未读的数据都将移至缓冲区的开头,并且现在将在未读的数据之后将数据写入缓冲区。

package com.jack.nio;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
* @Description:
* @Author:         JackQ
* @CreateDate:     2020/2/6 14:30
*/
public class FileChannelTest {

    public static void main(String[] args) throws IOException {
        RandomAccessFile randomAccessFile = new RandomAccessFile("test.txt", "rw");
        FileChannel fileChannel = randomAccessFile.getChannel();
        //创建缓冲区容量为100个字节
        ByteBuffer buffer = ByteBuffer.allocate(100);
        //将数据读入缓冲区总
        int read = fileChannel.read(buffer);
        while (read !=-1){
            System.out.println("Read...:"+read);
            //将缓冲区由写入模式改为读取模式
            buffer.flip();
            //判断缓冲区容量是否满了
            while (buffer.hasRemaining()){
                System.out.print((char) buffer.get());
            }
            //将位置复位到0,使缓冲区做好写入准备
            buffer.clear();
            read = fileChannel.read(buffer);
        }
        fileChannel.close();
    }
}

JAVA NIO Channel(通道)

Channel简介

​ Channel优点类似于IO中的流,但是又有一些区别

  • 数据可以读取和写入Channel中,流是单向的(读或者写)
  • Channel可以异步读写
  • Channel始终读取或者写入缓冲区中
Java NIO:通道和缓冲区
Java NIO:通道将数据读取到缓冲区中,而缓冲区将数据写入通道中

在Java中重要的实现Channel:

  1. FileChannel(从文件中读取或者写入数据)
  2. DatagramChannel(读取并在通过UDP网络写入数据)
  3. SocketChannel(读取和通过TCP网络写入数据)
  4. ServerSocketChannel(监听进入的TCP连接,例如Web服务器一样。为每个传入连接SocketChannel创建一个)

JAVA NIO Selector(选择器)

1、Selector简介

​ Java NIO Selector是一个组件,可以检查一个或多个Java NIO Channel 实例,并确定准备好进行读取或写入的通道。这样,单个线程可以管理多个通道,从而可以管理多个网络连接.

Selector优点:

​ 如上图所示,一个Selector可以管理多个Channel,那么可以通过单线程来管理。相比较多线程来讲,单线程占用系统资源要小很多,节约资源。

2、Selector使用

​ 1、创建一个Selector

​ 通过Selector对象调用静态工厂方法open()来创建,如下代码

 Selector selector = Selector.open();

​ 2、将Channel注册到Selector中

 //设置为非阻塞状态
serverSocketChannel.configureBlocking(false);
//将channel注册到selector中
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

注意:

​ 通过调用通道的register()方法会将它注册到一个选择器上。与Selector一起使用时,Channel必须处于非阻塞模式下,否则将抛出IllegalBlockingModeException异常,这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式,而套接字通道都可以。另外通道一旦被注册,将不能再回到阻塞状态,此时若调用通道的configureBlocking(true)将抛出BlockingModeException异常。

register()方法的第二个参数是“interest集合”,表示选择器所关心的通道操作,它实际上是一个表示选择器在检查通道就绪状态时需要关心的操作的比特掩码。比如一个选择器对通道的read和write操作感兴趣,那么选择器在检查该通道时,只会检查通道的read和write操作是否已经处在就绪状态。
它有以下四种操作类型:

  • Connect 连接
  • Accept 接受
  • Read 读
  • Write 写

需要注意并非所有的操作在所有的可选择通道上都能被支持,比如ServerSocketChannel支持Accept,而SocketChannel中不支持。我们可以通过通道上的validOps()方法来获取特定通道下所有支持的操作集合。

Java中定义了四个常量来表示这四种操作类型:

SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE

如果Selector对通道的多操作类型感兴趣,可以用“位或”操作符来实现:int interestSet=SelectionKey.OP_READ|SelectionKey.OP_WRITE;
当通道触发了某个操作之后,表示该通道的某个操作已经就绪,可以被操作。因此,某个SocketChannel成功连接到另一个服务器称为“连接就绪”(OP_CONNECT)。一个ServerSocketChannel准备好接收新进入的连接称为“接收就绪”(OP_ACCEPT)。一个有数据可读的通道可以说是“读就绪”(OP_READ)。等待写数据的通道可以说是“写就绪”(OP_WRITE)。

我们注意到register()方法会返回一个SelectionKey对象,我们称之为键对象。该对象包含了以下四种属性:

  • interest集合
  • read集合
  • Channel
  • Selector

interest集合是Selector感兴趣的集合,用于指示选择器对通道关心的操作,可通过SelectionKey对象的interestOps()获取。最初,该兴趣集合是通道被注册到Selector时传进来的值。该集合不会被选择器改变,但是可通过interestOps()改变。我们可以通过以下方法来判断Selector是否对Channel的某种事件感兴趣:

   int interestSet=selectionKey.interestOps();
   boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;12

read集合是通道已经就绪的操作的集合,表示一个通道准备好要执行的操作了,可通过SelctionKey对象的readyOps()来获取相关通道已经就绪的操作。它是interest集合的子集,并且表示了interest集合中从上次调用select()以后已经就绪的那些操作。(比如选择器对通道的read,write操作感兴趣,而某时刻通道的read操作已经准备就绪可以被选择器获知了,前一种就是interest集合,后一种则是read集合。)。JAVA中定义以下几个方法用来检查这些操作是否就绪:

    //int readSet=selectionKey.readOps();
    selectionKey.isAcceptable();//等价于selectionKey.readyOps()&SelectionKey.OP_ACCEPT
    selectionKey.isConnectable();
    selectionKey.isReadable();
    selectionKey.isWritable();12345

需要注意的是,通过相关的选择键的readyOps()方法返回的就绪状态指示只是一个提示,底层的通道在任何时候都会不断改变,而其他线程也可能在通道上执行操作并影响到它的就绪状态。另外,我们不能直接修改read集合。

取出SelectionKey所关联的Selector和Channel

通过SelectionKey访问对应的Selector和Channel:

Channel channel =selectionKey.channel();
Selector selector=selectionKey.selector();12
关于取消SelectionKey对象的那点事

我们可以通过SelectionKey对象的cancel()方法来取消特定的注册关系。该方法调用之后,该SelectionKey对象将会被”拷贝”至已取消键的集合中,该键此时已经失效,但是该注册关系并不会立刻终结。在下一次select()时,已取消键的集合中的元素会被清除,相应的注册关系也真正终结。

为SelectionKey绑定附加对象

可以将一个或者多个附加对象绑定到SelectionKey上,以便容易的识别给定的通道。通常有两种方式:
1 在注册的时候直接绑定:

SelectionKey key=channel.register(selector,SelectionKey.OP_READ,theObject);

2 在绑定完成之后附加:

selectionKey.attach(theObject);//绑定

绑定之后,可通过对应的SelectionKey取出该对象:

selectionKey.attachment();。

如果要取消该对象,则可以通过该种方式:

selectionKey.attach(null).

需要注意的是如果附加的对象不再使用,一定要人为清除,因为垃圾回收器不会回收该对象,若不清除的话会成内存泄漏。

一个单独的通道可被注册到多个选择器中,有些时候我们需要通过isRegistered()方法来检查一个通道是否已经被注册到任何一个选择器上。 通常来说,我们并不会这么做。

通过Selector选择通道

我们知道选择器维护注册过的通道的集合,并且这种注册关系都被封装在SelectionKey当中。接下来我们简单的了解一下Selector维护的三种类型SelectionKey集合:

已注册的键的集合(Registered key set)

所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过keys()方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引发java.lang.UnsupportedOperationException。

已选择的键的集合(Selected key set)

已注册的键的集合的子集。这个集合的每个成员都是相关的通道被选择器(在前一个选择操作中)判断为已经准备好的,并且包含于键的interest集合中的操作。这个集合通过selectedKeys()方法返回(并有可能是空的)。
不要将已选择的键的集合与ready集合弄混了。这是一个键的集合,每个键都关联一个已经准备好至少一种操作的通道。每个键都有一个内嵌的ready集合,指示了所关联的通道已经准备好的操作。键可以直接从这个集合中移除,但不能添加。试图向已选择的键的集合中添加元素将抛出java.lang.UnsupportedOperationException。

已取消的键的集合(Cancelled key set)

已注册的键的集合的子集,这个集合包含了cancel()方法被调用过的键(这个键已经被无效化),但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。

在刚初始化的Selector对象中,这三个集合都是空的。通过Selector的select()方法可以选择已经准备就绪的通道(这些通道包含你感兴趣的的事件)。比如你对读就绪的通道感兴趣,那么select()方法就会返回读事件已经就绪的那些通道。下面是Selector几个重载的select()方法:

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

select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。之前在select()调用时进入就绪的通道不会在本次调用中被记入,而在前一次select()调用进入就绪但现在已经不在处于就绪的通道也不会被记入。例如:首次调用select()方法,如果有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。

一旦调用select()方法,并且返回值不为0时,则可以通过调用Selector的selectedKeys()方法来访问已选择键集合。如下:
Set selectedKeys=selector.selectedKeys();
进而可以放到和某SelectionKey关联的Selector和Channel。如下所示:

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();
}123456789101112131415
关于Selector执行选择的过程

我们知道调用select()方法进行通道,现在我们再来深入一下选择的过程,也就是select()执行过程。当select()被调用时将执行以下几步:

  1. 首先检查已取消键集合,也就是通过cancle()取消的键。如果该集合不为空,则清空该集合里的键,同时该集合中每个取消的键也将从已注册键集合和已选择键集合中移除。(一个键被取消时,并不会立刻从集合中移除,而是将该键“拷贝”至已取消键集合中,这种取消策略就是我们常提到的“延迟取消”。)
  2. 再次检查已注册键集合(准确说是该集合中每个键的interest集合)。系统底层会依次询问每个已经注册的通道是否准备好选择器所感兴趣的某种操作,一旦发现某个通道已经就绪了,则会首先判断该通道是否已经存在在已选择键集合当中,如果已经存在,则更新该通道在已注册键集合中对应的键的ready集合,如果不存在,则首先清空该通道的对应的键的ready集合,然后重设ready集合,最后将该键存至已注册键集合中。这里需要明白,当更新ready集合时,在上次select()中已经就绪的操作不会被删除,也就是ready集合中的元素是累积的,比如在第一次的selector对某个通道的read和write操作感兴趣,在第一次执行select()时,该通道的read操作就绪,此时该通道对应的键中的ready集合存有read元素,在第二次执行select()时,该通道的write操作也就绪了,此时该通道对应的ready集合中将同时有read和write元素。
深入已注册键集合的管理

到现在我们已经知道一个通道的的键是如何被添加到已选择键集合中的,下面我们来继续了解对已选择键集合的管理 。首先要记住:选择器不会主动删除被添加到已选择键集合中的键,而且被添加到已选择键集合中的键的ready集合只能被设置,而不能被清理。如果我们希望清空已选择键集合中某个键的ready集合该怎么办?我们知道一个键在新加入已选择键集合之前会首先置空该键的ready集合,这样的话我们可以人为的将某个键从已注册键集合中移除最终实现置空某个键的ready集合。被移除的键如果在下一次的select()中再次就绪,它将会重新被添加到已选择的键的集合中。这就是为什么要在每次迭代的末尾调用keyIterator.remove()。

停止选择

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

  1. 通过调用Selector对象的wakeup()方法让处在阻塞状态的select()方法立刻返回
    该方法使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有进行中的选择操作,那么下一次对select()方法的一次调用将立即返回。
  2. 通过close()方法关闭Selector**
    该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似wakeup()),同时使得注册到该Selector的所有Channel被注销,所有的键将被取消,但是Channel本身并不会关闭。
  3. 调用interrupt()
    调用该方法会使睡眠的线程抛出InterruptException异常,捕获该异常并在调用wakeup()
参考文档
基于NIO实现的简单聊天室

客户端:

package com.jack.nio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.time.LocalDateTime;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* @Description:
* @Author:         JackQ
* @CreateDate:     2020/2/7 12:12
*/
public class NIOClient {

    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        Selector selector = Selector.open();
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        socketChannel.connect(new InetSocketAddress("127.0.0.1",8686));

        while (true){
            int select = selector.select();
            if (select==0){
                continue;
            }
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            selectionKeys.forEach(selectionKey -> {
                if (selectionKey.isConnectable()){
                    SocketChannel socketChannel1 = (SocketChannel)selectionKey.channel();
                    try {
                        //判断是否处于连接状态中
                        if (socketChannel1.isConnectionPending()){
                            socketChannel1.finishConnect();
                            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                            byteBuffer.put((LocalDateTime.now()+socketChannel1.getRemoteAddress().toString()
                                    +"连接成功!!").getBytes());
                            byteBuffer.flip();
                            socketChannel1.write(byteBuffer);
                            //接受键盘输入
                            ExecutorService executorService = Executors.newSingleThreadExecutor();
                            executorService.submit(() ->{
                                while (true){
                                    try {
                                        InputStreamReader inputStreamReader = new InputStreamReader(System.in);
                                        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
                                        String msg = bufferedReader.readLine();
                                        byteBuffer.clear();
                                        byteBuffer.put(msg.getBytes());
                                        byteBuffer.flip();
                                        socketChannel1.write(byteBuffer);
                                    } catch (Exception e) {
                                        e.printStackTrace();
                                    }
                                }
                            });
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    try {
                        socketChannel1.register(selector,SelectionKey.OP_READ);
                    } catch (ClosedChannelException e) {
                        e.printStackTrace();
                    }
                }else if(selectionKey.isReadable()){
                    SocketChannel channel = (SocketChannel)selectionKey.channel();
                    ByteBuffer serverMsg = ByteBuffer.allocate(1024);
                    try {
                        int read = channel.read(serverMsg);
                        if (read!=-1){
                            serverMsg.flip();
                            Charset charset = Charset.forName("utf-8");
                            String receiveMessage = String.valueOf(charset.decode(serverMsg).array());
                            System.out.println(socketChannel+"客户端收到服务端消息:"+receiveMessage);
                        }
                    }catch (Exception e){
                        e.printStackTrace();
                        System.out.println("客户端连接断开!!!");
                        try {
                            channel.close();
                        } catch (IOException e1) {
                            e1.printStackTrace();
                        }
                    }

                }
            });
            selectionKeys.clear();
        }

    }
}

服务端:

package com.jack.nio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* @Description:    Secetor+channel 服务端代码实现
* @Author:         JackQ
* @CreateDate:     2020/2/7 11:26
*/
public class NIOServer {

    public static void main(String[] args) throws IOException {

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //设置为非阻塞状态
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress("127.0.0.1",8686));
        //打开选择器
        Selector selector = Selector.open();
        //将通道注册到选择器中
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true){
            int select = selector.select();
            if (select==0){
                continue;
            }
            //获取到当前状态集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            selectionKeys.forEach(selectionKey -> {
                final SocketChannel socketChannel;
                if (selectionKey.isAcceptable()){
                    try {
                        socketChannel = serverSocketChannel.accept();
                        System.out.println("客户端:"+socketChannel.getRemoteAddress()+"已经通过端口"+socketChannel
                                .getLocalAddress()+"连接上来了");
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector,SelectionKey.OP_READ);
                        selectionKeys.remove(selectionKey);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }else if (selectionKey.isReadable()){
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    socketChannel = (SocketChannel) selectionKey.channel();
                    try {
                        int read = socketChannel.read(buffer);
                        if(read!=-1){
                            buffer.flip();
                            Charset charset = Charset.forName("utf-8");
                            String receiveMessage = String.valueOf(charset.decode(buffer).array());
                            System.out.println(socketChannel+"服务端收到客户端消息:"+receiveMessage);
                            buffer.clear();
                            //接收到消息之后将消息写回
                            //接受键盘输入
                            ExecutorService executorService = Executors.newSingleThreadExecutor();
                            executorService.submit(() ->{
                                while (true){
                                    try {
                                        ByteBuffer buffer1 = ByteBuffer.allocate(1024);

                                        InputStreamReader inputStreamReader = new InputStreamReader(System.in);
                                        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
                                        String msg = bufferedReader.readLine();
                                        buffer1.put(msg.getBytes());
                                        buffer1.flip();
                                        socketChannel.write(buffer1);
                                    } catch (Exception e) {
                                        e.printStackTrace();
                                    }
                                }
                            });
                        }
                    } catch (IOException e) {
                        System.out.println("客户端断开连接");
                        e.printStackTrace();
                    }
                    selectionKeys.remove(selectionKey);
                }
            });
        }
    }
}

FileChannelAPI详解

1、两种获取通道的方法
FileChannel.open()的方式

FileChannel channell = FileChannel.open(Paths.get("a.txt","c.txt"), StandardOpenOption.CREATE,StandardOpenOption.WRITE);FileChannel channel2 = FileChannel.open(new File("a.txt").toPath(), StandardOpenOption.CREATE_NEW,StandardOpenOption.WRITE,StandardOpenOption.READ);

path获取

Paths.get()new File(“a.txt”).toPath()

OpenOption接口的实现类通常由StandardOpenOption枚举进行代替。

public enum StandardOpenOption implements OpenOption {READ,WRITE,APPEND,//累加TRUNCATE_EXISTING,//如果该文件已存在并且为写入访问而打开,则其长度将被截断为0。如果只为读取访问打开文件,则忽略此选项。CREATE,//不能单独使用,要与WRITE配套使用,单独使用会报错java.nio.file.NoSuchFileException,如果文件已存在,重复创建不会报错CREATE_NEW,//不能单独使用,要与WRITE配套使用,如果文件已存在,则出现异常java.nio.file.FileAlreadyExistsExceptionDELETE_ON_CLOSE,SPARSE,//稀疏文件,空闲位置不占内存(不要使用CREATE来创建稀疏文件)SYNC,//要求对文件内容或元数据的每次更新都同步写入底层存储设备。如果这样做,程序运行的效率就降低了。DSYNC;//要求对文件内容的每次更新都同步写入底层存储设备。//枚举常量SYNC与DSYNC的区别:SYNC更新内容与元数据,而DSYNC只更新内容,与force(boolean)方法作用一样。

从io流中获得通道getChannel()

FileChannel inchannel = new FileInputStream("a.txt").getChannel();
FileChannel outchannel = new FileOutputStream("b.txt").getChannel();
FileChannel inchannel1 = new RandomAccessFile("a.txt","r").getChannel();
FileChannel outchannel1 = new RandomAccessFile("a.txt","rw").getChannel();

2、read

ByteBuffer buffer = ByteBuffer.allocate(10);ByteBuffer buffer1 = ByteBuffer.allocate(10);ByteBuffer[] buffers = {buffer,buffer1};channel2.read(buffer);//将字节序列从此通道的当前位置读入给定的缓冲区的当前位置,方法同步,返回值正数为读取的字节数,0为未读取到数据,可能是缓冲区中没有剩余空间了,-1是到了流的末端channel2.read(buffer,2);//position代表通道的位置channel2.read(buffers);//将通道当前位置的字节序列读入多个ByteBuffer缓冲区的remaining剩余空间中,方法同步channel2.read(buffers,0,8);//offset代表数组的下表,length为向后的缓冲区个数

3、write

ByteBuffer buffer2 = ByteBuffer.wrap(new byte[]{1,2,3,4});ByteBuffer buffer3 = ByteBuffer.wrap(new byte[]{11,21,31,41});ByteBuffer[] buffers1 = {buffer2,buffer3};channell.write(buffer2);//将一个缓冲区中remaining字节序列写入通道的当前位置,write方法同步channell.write(buffer2,2);//将一个缓冲区中remaining字节序列写入通道的指定位置,此方法不改变痛的位置,write方法同步channell.write(buffers);//将多个缓冲区中的remaining剩余字节序列写入通道的当前位置,方法同步channell.write(buffers1,0,8);//指定缓冲区数组的offset下表开始,向后length个字节缓冲区,将每个缓冲区的remaining剩余字节序列写入此通道的当前位置

4、获取和设置通道的位置、大小

channell.position();channell.position(2); channell.size();//此通道关联文件的当前大小

5、long transferTo(position,count,WritableByteChannel dest)
1)position:文件中的位置,从此位置开始传输,必须为非负数。
2)count:要传输的最大字节数;必须为非负数。
3)dest:目标通道。

long transferTo(position,count,WritableByteChannel dest)方法的作用是将字节从此通道的文件传输到给定的可写入字节通道。
1、试图读取从此通道的文件中给定position处开始的count个字节,并将其写入目标通道的当前位置。
2、此方法的调用不一定传输所有请求的字节,是否传输取决于通道的性质和状态。
如果此通道的文件从给定的position处开始所包含的字节数小于count个字节,或者如果目标通道是非阻塞的并且其输出缓冲区中的自由空间少于count个字节,则所传输的字节数要小于请求的字节数。
3、此方法不修改此通道的位置。如果给定的位置大于该文件的当前大小,则不传输任何字节,否则从目标通道的position位置起始开始写入各字节,然后将该位置增加写入的字节数。
4、与从此通道读取并将内容写入目标通道的简单循环语句相比,此方法可能高效得多。很多操作系统可将字节直接从文件系统缓存传输到目标通道,而无须实际复制各字节。

6、long transferFrom(ReadableByteChannel src,position,count)
1)src:源通道。
2)position:文件中的位置,从此位置开始传输;必须为非负数。
3)count:要传输的最大字节数;必须为非负数。
注意,参数position是指当前通道的位置,而不是指src源通道的位置。参数position针对于调用transferTo()或transferFrom()方法的对象。

long transferFrom(ReadableByteChannel src,position,count)方法的作用是将字节从给定的可读取字节通道传输到此通道的文件中。
1、试着从源通道中最多读取count个字节,并将其写入到此通道的文件中从给定position处开始的位置。
2、此方法的调用不一定传输所有请求的字节;是否传输取决于通道的性质和状态。
如果源通道的剩余空间小于count个字节,或者如果源通道是非阻塞的并且其输入缓冲区中直接可用的空间小于count个字节,则所传输的字节数要小于请求的字节数。
3、此方法不修改此通道的位置。如果给定的位置大于该文件的当前大小,则不传输任何字节。从源通道中的当前位置开始读取各字节写入到当前通道,然后将src通道的位置增加读取的字节数。
4、与从源通道读取并将内容写入此通道的简单循环语句相比,此方法可能高效得多。很多操作系统可将字节直接从源通道传输到文件系统缓存,而无须实际复制各字节。

7、截断缓冲区(在源文件上截取,并不是得到新文件)

channel2.truncate(100);

truncate(long size)方法的作用是将此通道的文件截取为给定大小。
如果给定大小小于该文件的当前大小,则截取该文件,丢弃文件新末尾后面的所有字节。
如果给定大小大于或等于该文件的当前大小,则不修改文件。
无论是哪种情况,如果此通道的文件位置大于给定大小,则将位置设置为该大小。
8、将通道文件区域直接映射到内存 map()
MappedByteBuffer map(FileChannel.MapMode mode,long position,long size)方法的作用是将此通道的文件区域直接映射到内存中。
1)mode:根据只读、读取/写入或专用(写入时复制)来映射文件,分别为FileChannel.MapMode类中所定义的READ_ONLY、READ_WRITE和PRIVATE;
2)position:文件中的位置,映射区域从此位置开始;必须为非负数。
3)size:要映射的区域大小;必须为非负数且不大于Integer.MAX_VALUE。

可以通过下列3种模式将文件区域映射到内存中。
1)只读:试图修改得到的缓冲区将导致抛出ReadOnlyBufferException异常。(MapMode.READ_ONLY)
2)读取/写入:对得到的缓冲区的更改最终将传播到文件;该更改对映射到同一文件的其他程序不一定是可见的。(MapMode.READ_WRITE)
3)专用:对得到的缓冲区的更改不会传播到文件,并且该更改对映射到同一文件的其他程序也不是可见的;相反,会创建缓冲区已修改部分的专用副本。(MapMode.PRIVATE)
总结:
1、对于只读映射关系,此通道必须可以进行读取操作;对于读取/写入或专用映射关系,此通道必须可以进行读取和写入操作。
2、此方法返回的已映射字节缓冲区位置为零,限制和容量为size;其标记是不确定的。在缓冲区本身被作为垃圾回收之前,该缓冲区及其表示的映射关系都是有效的。
3、映射关系一经创建,就不再依赖于创建它时所用的文件通道。特别是关闭该通道对映射关系的有效性没有任何影响。
4、对于大多数操作系统而言,与通过普通的read()和write()方法读取或写入数千字节的数据相比,将文件映射到内存中开销更大。从性能的观点来看,通常将相对较大的文件映射到内存中才是值得的。

MappedByteBuffer的简单介绍:
它是直接字节缓冲区,其内容是文件的内存映射区域。映射的字节缓冲区是通过FileChannel.map()方法创建的。此类用特定于内存映射文件区域的操作扩展ByteBuffer类。

public abstract class MappedByteBuffer extends ByteBuffer

作为ByteBuffer的子类,除了具有父类的方法外,还新增了

force()将此缓冲区所做的内容更改强制写入包含映射文件的存储设备中。
load()将此缓冲区内容加载到物理内存中。
isLoaded()判断次缓冲区的内容是否位于物理内存中。
FileChannel类或MappedByteBuffer类对文件进行操作时,在大部分情况下,它们的效率并不比使用InputStream或OutputStream高很多,这是因为NIO的出现是为了解决操作I/O线程阻塞的问题,使用NIO就把线程变成了非阻塞,这样就提高了运行效率。

NIO真正的优势:非阻塞。

来自博课 https://www.cnblogs.com/felixzh/p/12013122.html

posted @ 2020-06-02 22:22  张and强  阅读(750)  评论(0编辑  收藏  举报