java NIO学习笔记上(尚硅谷)

Posted 今夜月色很美

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java NIO学习笔记上(尚硅谷)相关的知识,希望对你有一定的参考价值。

1、java NIO概述

Java NIO(New IO 或 Non Blocking IO)是从 Java 1.4 版本开始引入的一个新的IO API,可以替代标准的 Java IO API。
NIO 支持面向缓冲区的、基于通道的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。

1.1阻塞IO

通常在进行同步 I/O 操作时,如果读取数据,代码会阻塞直至有可供读取的数据。同样,写入调用将会阻塞直至数据能够写入。
传统的 Server/Client 模式会基于 TPR(Thread per Request),服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户请求。
这种模式带来的一个问题就是线程数量的剧增,大量的线程会增大服务器的开销。大多数的实现为了避免这个问题,都采用了线程池模型,并设置线程池线程的最大数量,这由带来了新的问题,如果线程池中有 100 个线程,而有100 个用户都在进行大文件下载,会导致第 101 个用户的请求无法及时处理,即便第101 个用户只想请求一个几 KB 大小的页面。

传统的 Server/Client 模式如下图所示:

1.2非阻塞 IO(NIO)

NIO 中非阻塞 I/O 采用了基于 Reactor 模式的工作方式,I/O 调用不会被阻塞,相反是注册感兴趣的特定 I/O 事件,如可读数据到达,新的套接字连接等等,在发生特定事件时,系统再通知我们。NIO 中实现非阻塞 I/O 的核心对象就是 Selector,Selector 就是注册各种 I/O 事件地方,而且当我们感兴趣的事件发生时,就是这个对象告诉我们所发生的事件,如下图所示:

从图中可以看出,当有读或写等任何注册的事件发生时,可以从 Selector 中获得相应的 SelectionKey,同时从 SelectionKey 中可以找到发生的事件和该事件所发生的具体的 SelectableChannel,以获得客户端发送过来的数据。
非阻塞指的是 IO 事件本身不阻塞,但是获取 IO 事件的 select()方法是需要阻塞等待的.区别是阻塞的 IO 会阻塞在 IO 操作上, NIO 阻塞在事件获取上,没有事件就没有 IO, 从高层次看 IO 就不阻塞了.也就是说只有 IO 已经发生那么我们才评估 IO 是否阻塞,但是select()阻塞的时候 IO 还没有发生,何谈 IO 的阻塞呢?NIO 的本质是延迟 IO 操作到真正发生 IO 的时候,而不是以前的只要 IO 流打开了就一直等待 IO 操作。

1.3 NIO 概述

Java NIO 由以下几个核心部分组成:

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

1.3.1 Channel

首先说一下 Channel,可以翻译成“通道”。Channel 和 IO 中的 Stream(流)是差不多一个等级的。只不过 Stream 是单向的,譬如:InputStream,OutputStream.而Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。NIO 中的 Channel 的主要实现有:FileChannel、DatagramChannel、SocketChannel 和 ServerSocketChannel,这里看名字就可以猜出个所以然来:分别可以对应文件 IO、UDP 和 TCP(Server 和 Client)。

1.3.2 Buffer

NIO 中的关键 Buffer 实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。

1.3.3 Selector

Selector 运行单线程处理多个 Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用 Selector 就会很方便。例如在一个聊天服务器中。要使用Selector, 得向 Selector 注册 Channel,然后调用它的 select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等。

2、Java NIO(Channel)

2.1 Channel 概述

Channel 是一个通道,可以通过它读取和写入数据,它就像水管一样,网络数据通过Channel 读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而且通道可以用于读、写或者同时用于读写。因为 Channel 是全双工的,所以它可以比流更好地映射底层操作系统的 API。NIO 中通过 channel 封装了对数据源的操作,通过 channel 我们可以操作数据源,但又不必关心数据源的具体物理结构。这个数据源可能是多种的。比如,可以是文件,也可以是网络 socket。在大多数应用中,channel 与文件描述符或者 socket 是一一对应的。Channel 用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。
Channel 源码

public interface Channel extends Closeable {

    /**
     * Tells whether or not this channel is open.
     *
     * @return <tt>true</tt> if, and only if, this channel is open
     */
    public boolean isOpen();

    /**
     * Closes this channel.
     * @throws  IOException  If an I/O error occurs
     */
    public void close() throws IOException;

}

Channel 是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,通道就像是流。所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
Java NIO 的通道类似流,但又有些不同:

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

2.2Channel实现

下面是 Java NIO 中最重要的 Channel 的实现:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel
    (1) FileChannel 从文件中读写数据。
    (2) DatagramChannel 能通过 UDP 读写网络中的数据。
    (3) SocketChannel 能通过 TCP 读写网络中的数据。
    (4) ServerSocketChannel 可以监听新进来的 TCP 连接,像 Web 服务器那样。对每一个新进来的连接都会创建一个 SocketChannel。
    正如你所看到的,这些通道涵盖了 UDP 和 TCP 网络 IO,以及文件 IO

2.4FileChannel详解

2.4.1从FileChannel读数据样例代码

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

/**
 * @author lyz
 * @Title: FileChannelDemo
 * @Description:
 * @date 2021/10/14 9:51
 */
public class FileChannelDemo {
    public static void main(String[] args) throws Exception {
        //打开FileChannel
        RandomAccessFile accessFile = new RandomAccessFile("D:\\\\testfile\\\\1.txt", "rw");
        FileChannel channel = accessFile.getChannel();
        //创建一个Buffer对象
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //从channel中读数据到buffer
        int read = channel.read(buffer);
        while (read != -1){
            //反转读写模式
            buffer.flip();
            System.out.println(new String(buffer.array()));
            buffer.clear();
            read = channel.read(buffer);
        }
        System.out.println("读完了");
        accessFile.close();
    }
}

2.4.2向FileChannel写数据

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

/**
 * @author lyz
 * @Title: FileChannelDemo2
 * @Description:
 * @date 2021/10/14 10:30
 */
public class FileChannelDemo2 {
    public static void main(String[] args) throws Exception {
        //打开FileChannel
        RandomAccessFile accessFile = new RandomAccessFile("D:\\\\testfile\\\\1.txt", "rw");
        FileChannel channel = accessFile.getChannel();
        //创建buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //向buffer中放入数据
        buffer.put("let's show them who we are".getBytes());
        //反转读写模式
        buffer.flip();
        //将buffer中的数据写入文件
        channel.write(buffer);
        accessFile.close();
        System.out.println("写完了");
    }
}

注意
buffer读写时要反转读写模式
buffer.flip();
用完 FileChannel 后必须将其关闭。如:
inChannel.close();

2.4.3 FileChannel 的 position 方法

有时可能需要在 FileChannel 的某个特定位置进行数据的读/写操作。可以通过调用position()方法获取 FileChannel 的当前位置。也可以通过调用 position(long pos)方法设置 FileChannel 的当前位置。这里有两个例子:
long pos = channel.position(); channel.position(pos +123);
如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回1 (文件结束标志)。
如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。

2.4.4 FileChannel 的 size 方法

FileChannel 实例的 size()方法将返回该实例所关联文件的大小。如:
long fileSize = channel.size();

2.4.5 FileChannel 的 truncate 方法

可以使用 FileChannel.truncate()方法截取一个文件。截取文件时,文件将中指定长度后面的部分将被删除。如:
channel.truncate(1024);
这个例子截取文件的前 1024 个字节。

2.4.6 FileChannel 的 force 方法

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

2.4.7 FileChannel 的 transferTo 和 transferFrom 方法

通道之间的数据传输:
如果两个通道中有一个是 FileChannel,那你可以直接将数据从一个 channel 传输到另外一个 channel。
transferFrom测试代码

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

/**
 * @author lyz
 * @Title: TransferFromDemo
 * @Description:
 * @date 2021/10/14 11:02
 */
public class TransferFromDemo {
    public static void main(String[] args) throws Exception {
        //创建源FileChannel和目的FileChannel
        RandomAccessFile fromFile = new RandomAccessFile("D:\\\\testfile\\\\一周统计.txt", "rw");
        FileChannel fromChannel = fromFile.getChannel();
        RandomAccessFile toFile = new RandomAccessFile("D:\\\\testfile\\\\1.txt", "rw");
        FileChannel toChannel = toFile.getChannel();

        //fromChannel传输到toChannel
        toChannel.transferFrom(fromChannel, 0, fromChannel.size());

        fromChannel.close();
        toChannel.close();
        System.out.println("over");
    }
}

transferTo测试代码

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

/**
 * @author lyz
 * @Title: TransferToDemo
 * @Description:
 * @date 2021/10/14 11:02
 */
public class TransferToDemo {
    public static void main(String[] args) throws Exception {
        //创建源FileChannel和目的FileChannel
        RandomAccessFile fromFile = new RandomAccessFile("D:\\\\testfile\\\\一周统计.txt", "rw");
        FileChannel fromChannel = fromFile.getChannel();
        RandomAccessFile toFile = new RandomAccessFile("D:\\\\testfile\\\\2.txt", "rw");
        FileChannel toChannel = toFile.getChannel();

        //fromChannel传输到toChannel
        fromChannel.transferTo(0, fromChannel.size(), toChannel);

        fromChannel.close();
        toChannel.close();
        System.out.println("over");
    }
}

2.5Socket 通道

(1)	新的 socket 通道类可以运行非阻塞模式并且是可选择的,可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性。本节中我们会看到,再也没有为每个 socket 连接使用一个线程的必要了,也避免了管理大量线程所需的上下文交换开销。借助新的 NIO 类,一个或几个线程就可以管理成百上千的活动 socket 连接了并且只有很少甚至可能没有性能损失。所有的 socket 通道类(DatagramChannel、
SocketChannel 和 ServerSocketChannel)都继承了位于 java.nio.channels.spi 包中的 AbstractSelectableChannel。这意味着我们可以用一个 Selector 对象来执行 socket 通道的就绪选择(readiness selection)。 
(2)	请注意 DatagramChannel 和 SocketChannel 实现定义读和写功能的接口而 ServerSocketChannel 不实现。ServerSocketChannel 负责监听传入的连接和创建新的 SocketChannel 对象,它本身从不传输数据。 
(3)	在我们具体讨论每一种 socket 通道前,您应该了解 socket 和 socket 通道之间的关系。通道是一个连接 I/O 服务导管并提供与该服务交互的方法。就某个 socket 而言,它不会再次实现与之对应的 socket 通道类中的 socket 协议 API,而 java.net 中已经存在的 socket 通道都可以被大多数协议操作重复使用。 
全部 socket 通道类(DatagramChannel、SocketChannel 和
ServerSocketChannel)在被实例化时都会创建一个对等 socket 对象。这些是我们所
熟悉的来自 java.net 的类(Socket、ServerSocket 和 DatagramSocket),它们已经被更新以识别通道。对等 socket 可以通过调用 socket( )方法从一个通道上获取。
此外,这三个 java.net 类现在都有 getChannel( )方法。 
(4)	要把一个 socket 通道置于非阻塞模式,我们要依靠所有 socket 通道类的公有超级类:SelectableChannel。就绪选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作,如读或写。非阻
塞 I/O 和可选择性是紧密相连的,那也正是管理阻塞模式的 API 代码要在
SelectableChannel 超级类中定义的原因。 

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

/**
     * Adjusts this channel's blocking mode.
     */
    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;
    }

非阻塞 socket 通常被认为是服务端使用的,因为它们使同时管理很多 socket 通道变得更容易。但是,在客户端使用一个或几个非阻塞模式的 socket 通道也是有益处的,例如,借助非阻塞 socket 通道,GUI 程序可以专注于用户请求并且同时维护与一个或多个服务器的会话。在很多程序上,非阻塞模式都是有用的。
偶尔地,我们也会需要防止 socket 通道的阻塞模式被更改。API 中有一个blockingLock( )方法,该方法会返回一个非透明的对象引用。返回的对象是通道实现修改阻塞模式时内部使用的。只有拥有此对象的锁的线程才能更改通道的阻塞模式。
下面分别介绍这 3 个通道

2.5.1ServerSocketChannel

ServerSocketChannel 是一个基于通道的 socket 监听器。它同我们所熟悉的
java.net.ServerSocket 执行相同的任务,不过它增加了通道语义,因此能够在非阻塞模式下运行。 
由于 ServerSocketChannel 没有 bind()方法,因此有必要取出对等的 socket 并使用它来绑定到一个端口以开始监听连接。我们也是使用对等 ServerSocket 的 API 来根据需要设置其他的 socket 选项。 
同 java.net.ServerSocket 一样,ServerSocketChannel 也有 accept( )方法。一旦创建了一个 ServerSocketChannel 并用对等 socket 绑定了它,然后您就可以在其中一个上调用 accept()。如果您选择在 ServerSocket 上调用 accept( )方法,那么它会同任何其他的 ServerSocket 表现一样的行为:总是阻塞并返回一个 java.net.Socket 对
象。如果您选择在 ServerSocketChannel 上调用 accept( )方法则会返回
SocketChannel 类型的对象,返回的对象能够在非阻塞模式下运行。 
换句话说: 
ServerSocketChannel 的 accept()方法会返回 SocketChannel 类型对象,
SocketChannel 可以在非阻塞模式下运行。 
其它 Socket 的 accept()方法会阻塞返回一个 Socket 对象。如果
ServerSocketChannel 以非阻塞模式被调用,当没有传入连接在等待时,
ServerSocketChannel.accept( )会立即返回 null。正是这种检查连接而不阻塞的能力实现了可伸缩性并降低了复杂性。可选择性也因此得到实现。我们可以使用一个选择器实例来注册 ServerSocketChannel 对象以实现新连接到达时自动通知的功能。

代码演示

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.concurrent.TimeUnit;

/**
 * @author lyz
 * @Title: ServerSocketChannelDemo
 * @Description:
 * @date 2021/10/14 12:47
 */
public class ServerSocketChannelDemo {

    public static void main(String[] args) throws IOException, InterruptedException {
        //将字节数组放入ByteBuffer
        ByteBuffer buffer = ByteBuffer.wrap("this is server socket channel demo.".getBytes());
        //ServerSocketChannel监听8888端口
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        socketChannel.socket().bind(new InetSocketAddress(8888));
        //设置模式为非阻塞
        socketChannel.configureBlocking(false);
        while (true){
            System.out.println("wait for connections");
            SocketChannel sc = socketChannel.accept();
            if (sc == null){//没有请求进入
                System.out.println("null");
                TimeUnit.SECONDS.sleep(2);
            } else {
                System.out.println("incoming request from address:" + sc.socket().getRemoteSocketAddress());
                buffer.rewind();//指针0
                sc.write(buffer);
                sc.close();
            }
        }
    }
}

在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是 null。

2.5.2SocketChannel

Java NIO 中的 SocketChannel 是一个连接到 TCP 网络套接字的通道。
A selectable channel for stream-oriented connecting sockets.
以上是 Java docs 中对于 SocketChannel 的描述:SocketChannel 是一种面向流连接 sockets 套接字的可选择通道。从这里可以看出:
• SocketChannel 是用来连接 Socket 套接字
• SocketChannel 主要用途用来处理网络 I/O 的通道
• SocketChannel 是基于 TCP 连接传输
• SocketChannel 实现了可选择通道,可以被多路复用的
SocketChannel 特征:

(1)	对于已经存在的 socket 不能创建 SocketChannel 
(2)	SocketChannel 中提供的 open 接口创建的 Channel 并没有进行网络级联,需要使用 connect 接口连接到指定地址 
(3)	未进行连接的 SocketChannle 执行 I/O 操作时,会抛出NotYetConnectedException 
(4)	SocketChannel 支持两种 I/O 模式:阻塞式和非阻塞式 
(5)	SocketChannel 支持异步关闭。如果 SocketChannel 在一个线程上 read 阻塞,另一个线程对该 SocketChannel 调用 shutdownInput,则读阻塞的线程将返回-1 表示没有读取任何数据;如果 SocketChannel 在一个线程上 write 阻塞,另一个线程对该
SocketChannel 调用 shutdownWrite,则写阻塞的线程将抛出AsynchronousCloseException 
(6)	SocketChannel 支持设定参数 
SO_SNDBUF 套接字发送缓冲区大小 
SO_RCVBUF 套接字接收缓冲区大小 
SO_KEEPALIVE 	保活连接 
O_REUSEADDR 	复用地址 
SO_LINGER 有数据传输时延缓关闭 Channel (只有在非阻塞模式下有用) 
TCP_NODELAY 	禁用 Nagle 算法 

代码演示

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * @author lyz
 * @Title: SocketChannelDemo
 * @Description:
 * @date 2021/10/14 13:59
 */
public class SocketChannelDemo {

    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("www.baidu.com", 80));
        socketChannel.configureBlocking(false);

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        socketChannel.read(buffer);
        socketChannel.close();
        System.out.println("read over");
    }
}

.5.3 DatagramChannel

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

import org.junit.Test;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * @author lyz
 * @Title: DatagramChannelDemo
 * @Description:
 * @date 2021/10/14 14:39
 */
public class DatagramChannelDemo {

    @Test
    public void sendMessage() throws Exception {
        DatagramChannel datagramChannel = DatagramChannel.open();
        datagramChannel.socket().bind(new InetSocketAddress("127.0.0.1", 9999));

        while (true){
            String message = "发送消息" + new Date();
            ByteBuffer buffer = ByteBuffer.wrap(message.getBytes("utf-8"));
            datagramChannel.send(buffer, new InetSocketAddress("127.0.0.1", 9998));
            System.out.println("消息已发送");
            TimeUnit.SECONDS.sleep(1);
        }
    }

    @Test
    public void receiveMessage() throws Exception {
        DatagramChannel receiveChannel = DatagramChannel.open();
        receiveChannel.socket().bind(new InetSocketAddress("127.0.0.1", 9998));

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (true){
            buffer.clear();
            receiveChannel.receive(buffer);
            buffer.flip();
            System.out.println(Charset.forName("utf-8").decode(buffer));
        }
    }
    
	@Test
    public void connect() throws Exception {
        DatagramChannel datagramChannel = DatagramChannel.open();
        datagramChannel.bind(new InetSocketAddress("127.0.0.1", 9998));
        datagramChannel.connect(new InetSocketAddress("127.0.0.1", 9998));

        String message = "发送消息" + new Date();
        datagramChannel.write(ByteBuffer.wrap(message.getBytes("utf-8")));
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (true) {
            buffer.clear();
            datagramChannel.read(buffer);
            buffer.flip();
            System.out.println(Charset.forName("utf-8").decode(buffer));
        }
    }
}

以上是关于java NIO学习笔记上(尚硅谷)的主要内容,如果未能解决你的问题,请参考以下文章

juc并发编程学习笔记上(尚硅谷)

尚硅谷_Java零基础教程(多线程)-- 学习笔记

尚硅谷_Java零基础教程(泛型generics)-- 学习笔记

Elasticsearch - 尚硅谷(4. Elasticsearch 基本操作_下)学习笔记

尚硅谷_Java零基础教程(常用类——ComparableComparator)-- 学习笔记

尚硅谷Spring学习笔记-- Spring5新功能