Java-NIO笔记

在这里插入图片描述

NIO概述

  • java NIO是jdk1.4引入的新的IO API、它可以代替标准的java IO API。
  • NIO支持面相缓冲区、鲫鱼通道IO操作、以更高效的方式进行文件的读写操作。

1、阻塞IO

通常在进行同步I/O操作时、如果读取数据、代码会阻塞直至有可供读取\写入的线程、 传统的 server/Client 模式会基于TPR、服务器会为每个客户端请求建立一个线程池、由于该线程池单独负责一个客户请求、所以就出现了一个问题:线程数量的剧增、大量的线程会增大服务器的开销。而大多是情况为了避免这个问题、都采用线程池、并设置线程池的最大数量、但是这就又会带来一个问题:如果线程池中最大线程数是100、而这100个用户都在进行大文件下载、那么第101个用户就算是要请求一个几KB的页面也会阻塞、无法及时处理。
在这里插入图片描述

2、非阻塞IO(NIO)

非阻塞NIO采用Reactor模式的工作方式、I/O调用不会被阻塞、相反是注册感兴趣的特定I/O事件、如可读数据到达、新的套接字连接等等、再发生特定事件时、系统再通知我们。NIO中实现非阻塞I/O的核心对象就是selector

  • select是注册IO事件的地方、而当我们感兴趣的事件发生时、就是这个对象告诉我们所发生的事件
    在这里插入图片描述
    selector的本质是延迟IO操作到真正发生IO的时候、而不是以前的IO流只要打开就一直等待IO操作
IO NIO
面向流 面向缓冲区
阻塞IO 非阻塞IO
(无) 选择器

3、NIO的主要组成

核心部分:

  • Channels
  • Buffers
  • Selectors
    NIO 中还有很多类和组件、但是Channel、Buffer、Selector构成了核心的API、其他组件例如Pipe、FileLock、只不过是与这三个核心组件共同使用的工具类

4、Channel

Channel 和 IO 中的 Stream 是差不多一个等级的、只不过 Stream 是单向的、也就是只能进行读\写中的一个操作
Channel是双向的、也就是即可以用来读也可以用来写、同时读写。因为Channel是全双工的、所以它可以比流更好的映射底层操作系统的API
而NIO中通过Channel封装了对数据源的操作、tongguoChannel我们可以操作数据源、但又不关心数据源的具体物理结构、这个数据源可能是多种的、比如是文件、也可以是网络Socket。再大多数应用中、Channel与文件描述符或者Socket是一一对应的。Channel用于在字节缓冲区和位于通道另一侧的实体之间有效的传输数据。

4.1、Channel接口源码

public interface Channel extends Closeable {
    
    
 // 通道是否打开?
 public boolean isOpen();
 // 关闭通道 
 public void close() throws IOException;
}

Channel主要实现有:FileChannel、DataGramChannel、SocketChannel、ServerSocketChannel。分别对应IO、UDP、TCP(Server\Client)

4.2、Channel 实现

  • FileChannel 从文件中读写数据
  • DataGramChannel 能通过 UDP 读写网络中的数据
  • SocketChannel 能通过 TCP 读写网络中的数据
  • ServerSocketChannel 可以进啊听新进来的 TCP 连接、像 Web 服务那样、对每一个新进来的连接都会创建一个SocketChannel

4.2.1、FileChannel 介绍

FieChannel类可以实现常用的 read、write、scatter、gather操作、同时它也提供了很多专用于文件的新方法、这些方法中的许多都是我们所属洗的文件操作
在这里插入图片描述

public class FileChannelDemo1 {
    
    
    // 通过 FIleChannel 读取数据到 Buffer 中
    public static void main(String[] args) throws Exception {
    
    
//        1、创建 FIleChannel
        RandomAccessFile aFile = new RandomAccessFile("D:\\Cloud\\note\\Channel.txt","rw");
        FileChannel channel = aFile.getChannel();
//        2、创建 Buffer
        ByteBuffer buf = ByteBuffer.allocate(1024);
//        3、读取数据到 Buffer 中
        int bytesRead = channel.read(buf);
        while (bytesRead != -1) {
    
    
            System.out.println("读取了:" + bytesRead);
            buf.flip();
            while (buf.hasRemaining()) {
    
    
                System.out.println((char) buf.get());
            }
            buf.clear();
            bytesRead = channel.read(buf);
        }
        aFile.close();
        System.out.println("读取结束~");
    }
}
public class FileChannelDemo2 {
    
    
    // FileChannel 写操作
    public static void main(String[] args) throws Exception {
    
    
        // 1、打开FileCHannel
        RandomAccessFile accessFile = new RandomAccessFile("D:\\Cloud\\note\\Channel.txt","rw");
        FileChannel channel = accessFile.getChannel();
        // 2、创建 Buffer对象
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        String newData = "data at Cloud";
        // 3、向缓冲区写入内容
        buffer.put(newData.getBytes());
        // 4、切换模式
        buffer.flip();
        // 5、FileChannel 从缓冲区写入到文件中
        while (buffer.hasRemaining()){
    
    
            channel.write(buffer);
        }
        // 6、关闭Channel
        channel.close();
    }
}

FIleChannel.write 是在 while 循环中调用,因为无法保证 write 方法一次能向FileChannel写入多少直接、因此需要重复调用 write 方法、直到 Buffer 中已经没有尚未写入通道的字节

4.2.2、FileChannel 方法介绍

1、position 获取FileChannel的当前位置、也可以通过传入 Long pos 参数 设置FileChannel 的当前位置

long pos = channel.position();
channel.position(pos + 123);

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

2.、size 返回该实例所关联的文件大小
3、truncate 截取一个文件、截取文件时、文件将指定长度的后面部分删除。

// 截取文件袋前1024个字节数据
channel.truncate(1024);

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

5、transferTo、transferFrom 两个方法的传输方向是相反的
通道之间的数据传输:

/**
 6. 通道之间数据传输
 */
public class FileChannelDemo3 {
    
    
    public static void main(String[] args) throws Exception {
    
    
        // TODO transferFrom
        // 1、创建两个FileChannel
        RandomAccessFile aFile = new RandomAccessFile("D:\\Cloud\\note\\Channel.txt","rw");
        FileChannel fromChannel = aFile.getChannel();// 1、打开FileCHannel
        RandomAccessFile bFile = new RandomAccessFile("D:\\Cloud\\note\\0822.txt","rw");
        FileChannel toChannel = bFile.getChannel();
        // fromChannel 传输到 toChannel
        long position = 0;
        long size = fromChannel.size();
        toChannel.transferTo(fromChannel,position,size);
        // 关闭资源
        aFile.close();
        bFile.close();
        System.out.println("over!");
    }
}
/**
 7. 通道之间数据传输
 */
public class FileChannelDemo3 {
    
    
    public static void main(String[] args) throws Exception {
    
    
        // TODO transferTo
        // 1、创建两个FileChannel
        RandomAccessFile aFile = new RandomAccessFile("D:\\Cloud\\note\\Channel.txt","rw");
        // 2、打开FileCHannel
        FileChannel fromChannel = aFile.getChannel();
        RandomAccessFile bFile = new RandomAccessFile("D:\\Cloud\\note\\0822.txt","rw");
        FileChannel toChannel = bFile.getChannel();
        // 3、toChannel 传输到 fromChannel
        long position = 0;
        long size = fromChannel.size();
        toChannel.transferTo(position,size,fromChannel);
        // 4、关闭资源
        aFile.close();
        bFile.close();
        System.out.println("over!");
    }
}

4.3、Socket 通道 介绍

1、 新的Socket通道类可以运行非阻塞模式并且是可选择的,可以激活大程序(网络服务器和中间件)巨大的可伸缩性和灵活性、避免了每个socket连接使用一个线程的必要、也避免了管理大量线程所需的上下文交换开销。借助新的NIO类、一个或几个线程就可以管理成百上千的活动socket连接、并且只有很少甚至可能没有性能损失、所有socket 通道(DatagramChannel、SocketChannel、ServerSocketChannel)都继承位于java.nio.channels.spi包中的AbstractSelectableChannel、这意味着我们可以用一个selector对象来执行socket通道的就绪选择(readiness selection)
2、请注意DatagramChannel、SocketChannel 实现定义读写功能的接口、而ServerScoketChannel不实现。ServerSocketChannel负责监听传入的连接和创建新的SocketChannel对象、它本身从不传输数据。
3、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 通道当前处于哪个模式。

4.3.1、ServerSocketChannel

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

public class ServerSocketChannelDemo {
    
    
    public static void main(String[] args) throws Exception {
    
    
        // 端口号
        int port = 8888;
        // buffer
        ByteBuffer buffer = ByteBuffer.wrap("hello at Cloud".getBytes());
        // ServerSocketChannel
        ServerSocketChannel ssc = ServerSocketChannel.open();
        // 绑定
        ssc.socket().bind(new InetSocketAddress(port));
        // 设置非阻塞模式
        ssc.configureBlocking(false);
        // 持续监听是否有新的链接连入
        while (true) {
    
    
            System.out.println("Waiting fot 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(); // 指针指向0
                sc.write(buffer); // 开始写操作
                sc.close(); // 关闭资源
            }
        }
    }
}

注意事项:
1、打开 ServerSocketChannel

  • 通过调用 ServerSocketChannel 方法来打开 ServerSocketChannel
  • ServerSocketChannel ssc = ServerSocketChannel.open();

2、关闭 ServerSocketChannel

  • 通过调用 ServerSocketChannel.closes 方法关闭 ServerSocketChannel
  • serverSocketChannel.close();

3、监听新的链接

  • 通过 ServerSocketChannel 方法监听新进来的链接。当 accept 方法返回时、它返回一个包含新进来的连接的 SockeChannel。因此、accept 方法会一直阻塞到有新连接到达。通常不会仅仅监听一个连接、在 while 循环中调用 accept方法。

4、阻塞模式

  • 会在 SocketChannel sc = ssc.accept();这里阻塞住进程
    5、非阻塞模式
  • 在非阻塞模式下、accept会立刻返回结果、如果没有新进来的链接、返回的将是 null

4.4、SocketChannel

Java NIO 的 SocketChannel 是一个连接到 TCP 网络套接字的通道。
A selectable channel for stream-oriented connecting sockets.
SocketChannel是一种面向流连接sockets套接字的可选择通道。

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

4.4.1、SocketChannel 特征

1、对于已经存在的 socket 不能创建 SocketChannel
2、SocketChannel 中提供的 open 接口创建的 Channel 并没有进行网络级联、需要使用 connect 接口连接到指定地址
3、未进行连接的SocketChannel 执行 I/O 操作时、会抛出NotyetConnectedException
4、SocketChannel支持两种 I/O模式:阻塞和非阻塞
5、SOCketChannel支持异步关闭。如果 SocketChannel在一个线程上 read阻塞、另一个线程对该SocketChannel 调用 shutdownlnput、则读阻塞的线程将返回 -1 表示没有读取任何数据。如果 SocketChannel 在一个线程上write 阻塞、另一个线程对该 SocketChannel 调用 shutdownWrite、则写阻塞的线程将抛出 AsynchronousCloseException
6、SocketChannel 支持设定参数

  • SO_SNDBUF 套接字发送缓冲区大小
  • SO_RCVBUF 套接字接收缓冲区大小
  • SO_KEEPALIVE 保活连接
  • SO_REUSEADDR 复用地址
  • SO_LINGER 有数据传输时延缓关闭 Channel (只有在非阻塞模式下有用)
  • TCP_NODELAY 禁用Nagle算法

4.4.2、创建 SocketChannel

方法一

// 1、创建SocketChannel
// 1.1、hostname 主机IP    port 端口号
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com",80));

方法二

SocketChannel socketChannel1 = SocketChannel.open();
        socketChannel1.connect(new InetSocketAddress("www.baidu.com",80));

4.4.3、连接校验

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

4.4.4、设置 阻塞/非阻塞

// true 阻塞 false 非阻塞
socketChannel.configureBlocking(false);

4.4.5、读写模式

SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com",80));
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
socketChannel.read(byteBuffer);
socketChannel.close();
System.out.println("read over!");

以上为阻塞模式读、当执行到 read 处、线程将阻塞、控制台无法打印 read over!

SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com",80));
socketChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
socketChannel.read(byteBuffer);
socketChannel.close();
System.out.println("read over!");

以上为非阻塞模式、控制台将打印 read over
读写都是面向缓冲区、这个读写方式与 FileChannel 相同

4.4.6、设置和获取参数

通过 setOptions 方法可以设置 socket 套接字的相关参数

socketChannel.setOption(
	StandardSocketOptions.SO_KEEPALIVE, Boolean.TRUE)
 	.setOption(StandardSocketOptions.TCP_NODELAY,Boolean.TRUE);

可以通过 getOption 获取相关参数的值。如默认的接收缓冲区大小是 8192byte。SocketChannel 还支持多路复用。

// 获取参数 getOption
socketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE);
socketChannel.getOption(StandardSocketOptions.SO_RCVBUF);

4.5、DatagramChannel

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

4.5.1、打开 DatagramChannel

打开10086 端口接受UDP数据包

DatagramChannel server = DatagramChannel.open();
server.socket().bind(new InetSocketAddress(10086));

4.5.2、接收数据

通过 receive() 接收 UDP 包

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

SocketAddress 可以获得发包的ip、端口等信息、用 toString 查看

4.5.3、发送数据

通过 send() 发送 UDP 包

DatagramChannel server = DatagrChannel.open();
ByteBuffer senBuffer = ByteBuffer.wrap("client send".getBytes());
server.send(sendBuffer,new InetSocketAddress("127.0.0.1",10086));

4.5.4、连接

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

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

read() 和 write() 只有在 connect() 后才能使用、不然会抛 NotYetConnectedException 异常。用 read() 接收时、如果没有接收到包、会抛 PortUnreachableException 异常

/**
* 连接 read 和 write
* @throws Exception
*/
@Test
public void testConnect() throws Exception {
    
    
   // 打开 DatagaramChannel
   DatagramChannel connChannel = DatagramChannel.open();
   // 绑定
   connChannel.bind(new InetSocketAddress(9999));
   // 连接
   connChannel.connect(new InetSocketAddress("127.0.01",9999));
   // write
   connChannel.write(ByteBuffer.wrap("发送 at cloud".getBytes("UTF-8")));
   // buffer
   ByteBuffer readBuffer = ByteBuffer.allocate(1024);
   while (true) {
    
    
       readBuffer.clear();
       connChannel.read(readBuffer);
       readBuffer.flip();
       System.out.println(Charset.forName("UTF-8").decode(readBuffer));
   }
}

4.5.5、DatagramChannel 示例

/**
 * 发送包
 */
@Test
public void sendDatagram () throws Exception {
    
    
    // 打开DatagramChannel
    DatagramChannel sendChannel = DatagramChannel.open();
    ByteBuffer buffer = ByteBuffer.wrap("发送 at Cloud".getBytes("UTF-8"));
    InetSocketAddress sendAddress = new InetSocketAddress("127.0.0.1",9999);
    // 发送
    while (true) {
    
    
        sendChannel.send(buffer,sendAddress);
        System.out.println("发送完成!");
        Thread.sleep(1000);
    }
}

/**
 * 接收包
 */
@Test
public void receiveDatagram () throws IOException {
    
    
    // 打开 DatagaramChannel
    DatagramChannel receiveChannel = DatagramChannel.open();
    InetSocketAddress receiveAddress = new InetSocketAddress(9999);
    // 绑定
    receiveChannel.bind(receiveAddress);
    // 创建Buffer
    ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
    while (true) {
    
    
        receiveBuffer.clear();
        SocketAddress socketAddress = receiveChannel.receive(receiveBuffer);
        receiveBuffer.flip();
        System.out.println("接收数据完成!" + socketAddress.toString() + "\n" + Charset.forName("UTF-8").decode(receiveBuffer));
    }
}

4.6、Scatter/Gather

scatter/gather 用于描述从Channel中读取或者写入到Channel的操作

  • **分散(scatter):**从Channel 中读取是指在杜操作时将读取的数据写入到多个 Buffer 中。因此、Channel 将从 Channel 中读取的数据 “分散” 到多个 Buffer 中
  • **聚集(garher):**写入 Channel 是指在写操作时姜多个 Buffer 中的数据 “聚集” 后发送到 Channel

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

4.6.1、Scattering Reads

在这里插入图片描述

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.6.2 Gathering Write

在这里插入图片描述

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

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

5、Buffers

Buffer 用于和 NIO 通道进行交互。数据是从通道写入到缓冲区,从缓冲区写入到通道中

Buffer 通常的操作
1、将数据写入缓冲区
2、调用 buffer.flip() 反转读写模式
3、从缓冲区读取数据
4、调用 buffer.clear() 或 buffer.compact() 清除缓冲区内容

主要是作为缓冲区的作用
Biffers实现有:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer
在这里插入图片描述
在 NIO 中、所有的缓冲区类型都继承于抽象类 Buffer、最常用的就是 ByteBuffer、对于 Java 中的基本类型、基本都有一个具体 Buffer 类型与之相对应、他们之间的继承关系如下图所示
在这里插入图片描述

5.1、Buffer 基本用法

1、使用 Buffer 读写数据、一半遵循以下四个步骤:

  • 写入数据到 Buffer
  • 调用 flip 方法
  • 从 Buffer 中读数据
  • 调用 clear 方法或者 compact 方法

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

5.2、Buffer 示例

ByteBuffer

// 打开 FileChannel
RandomAccessFile aFile = new RandomAccessFile("D:\\Cloud\\note\\Channel.txt","rw");
FileChannel channel = aFile.getChannel();
// 创建 buffer、指定大小
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取 buffer
int bytesRead = channel.read(buffer);
while (bytesRead != -1){
    
    
    // 转换 read 模式
    buffer.flip();
    while (buffer.hasRemaining()){
    
    
        System.out.println("读取到了" + (char)buffer.get());
    }
    buffer.clear();
    bytesRead = channel.read(buffer);
}
aFile.close();

IntBuffer

// 创建 Buffer
        IntBuffer intBuffer = IntBuffer.allocate(1024);
        // 向 buffer 中写入数据
        for (int i = 0; i < intBuffer.capacity(); i++) {
    
    
            int j = 8 * 22 + i;
            intBuffer.put(j);
            System.out.println("写入了的数据是:" + j);
        }
        // 重置缓冲区
        intBuffer.flip();
        // 获取
        while (intBuffer.hasRemaining()) {
    
    
            int value = intBuffer.get();
            System.out.println("读取到了:" + value);
        }

5.3、 Buffer 的 capacity、position、limit

为了理解 Buffer 的工作原理、需要熟悉它的三个属性:

  • Capacity
  • Position
  • limit
    position 和 limit 的含义取决于 Buffer 处在读模式还是写模式。不管Buffer处于什么模式、capacity 的含义都是一样的。·

capacity:

  • 作为内存块、Buffer 有一个固定的大小值、也叫 “capacity”、你只能往里写 capacity 个 byte、long、char 等类型。一旦 Buffer 满了,需要将其清空 (通过读数据或者清除数据) 才能继续往里写数据。
    position:
  • 写数据到 buffer 中时、position 表示写入数据的当前位置、position 的初始值为0。当一个 byte、long 等数据写到 buffer 后、position 会向下移动到下一个可插入数据的 buffer单元。position 最大可为 capacity -1 因为 position 的初始值为 0。
  • 读数据到 buffer 中时、position 表示读入数据的当前位置、如 position = 2 时表示已开始读入了第3个 byte、或者从第3个 byte 开始读取。通过 byteBuffer.flip 切换到读模式时 position 会被充值为0、当 buffer 从 position 读入数据后、position 会下移到下一个可读入的数据 buffer 单元。
    limit:
  • 写数据时、limit 表示可对 Buffer 最多写入多少个数据。写模式下、limit 等于 Buffer 的 capacity
  • 读数据时、limit 表示 buffer 里有多少可读数据(Not null 的数据),因此能读到之前写入的所有数据(limit 被设置成以写数据的数量、这个值在写模式下就是 position)
    在这里插入图片描述

5.4、Buffer 类型

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

5.5、Buffer 分配和写数据

5.5.1、Buffer 分配

要想获得一个 Buffer 对象首先要进行分配。每一个 Buffer 类都有一个 allocate 方法

// 给 buf 分配48个字节大小
ByteBuffer buf = ByteBuffer.allocate(48);

5.5.2、向Buffer中写数据

写数据到 Buffer 有两种方式:
1、从 Channel 写到 Buffer
2、通过 Buffer 的 put 方法写到 Buffer 中

5.5.2.1、从 Channel 写到 Buffer

// read into buffer
int bytesRead = inChannel.read(buf);

5.5.2.2、通过 Buffer 的 put 方法写到 Buffer 中

put 方法有很多版本、允许你以不同的方式把数据写到 Buffer 中。例如、写到一个指定的位置、或者把一个字节数据写到 Buffer

buf.put(127);

5.5.3、flip() 方法

flip() 方法将 Buffer 从写模式切换到读模式。调用 flip() 方法会将 position 设回 0、并将 limit 设置成之前 position 的值。position 现在用于标记读的位置、limit 表示之前写进了多少个 byte、char 等(现在能读取多少个 byte、char)

5.6、从 Buffer 中读取数据

从 Buffer 中读取数据有两种方式
1、从 Buffer 读取数据到 Channel
2、使用 get 方法从 Buffer 中读取数据

从 Buffer 读取数据到 Channel

// read from buffer into channel;
int bytesWritten = inChannel.write(buf);

使用 get 方法从 Buffer 中读取数据

byte aByte = buf.get();

get 方法有很多版本、允许你以不同的方式从 buffer 中读取数据。例如、从指定 position 读取、或者从 buffer 中读取数据到字节数组。

5.7、Buffer 方法

5.7.1、rewind 方法

Buffer.rewind() 将 position 设回 0、所以你可以重新读 Buffer 中干的所有数据。limit 保持不变、仍然表示从 Buffer 中读取多少个元素。

5.7.2、clear 与 compact

一旦读完 Buffer 中的数据、需要让 Buffer 准备好再次被写入。可以通过 clear 或 compact 方法来完成
如果调用 clear 方法、position 将被设回0、limit被设置成 capacity 的值。换句话说、Buffer 被清空了。Buffer 重点数据并未清除、只是这些标记告诉我们可以从哪里开始往 Buffer 里写数据。
如果 Buffer 中又一些未读的数据、调用 clear 方法、数据将“被遗忘”、意味着不再有任何标记会告诉你哪些数据被读过、那些还没有。
如果 Buffer 中仍有未读的数据、且后续还需要这些数据、但是此时想要先写一些数据、那么使用 compact 方法。
compact 方法将所有未读的数据拷贝到 Buffer 起始处。然后将 position 设到最后一个未读完元素正后面。limit 属性依然像 clear 方法一样、设置成 capacity。现在 Buffer 准备写好数据了、但是不会覆盖未读的数据。

5.7.3、mark 与 reset

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

buffer.mark();
// call buffer.get() a couple of times,e.g.during parsing
buffer.reset(); //set position bak to mark

5.8、缓冲区操作

5.8.1、缓冲区分片

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

public static void main(String[] args) {
    
    
   // 缓冲区分片
   // 创建 Buffer
   ByteBuffer buffer = ByteBuffer.allocate(10);
   // 写入数据
   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());
   }
}

5.8.2、只读缓冲区

只读缓冲区非常简单、可以读取它们、但是不能向它们写入数据。可以通过调用缓冲区的 asReadOnlyBuffer() 方法、将任何常规缓冲区共享数据、只不过它是只读的。如果原缓冲区的内容发生了变化、只读缓冲区的内容也随之发生变化

public static void main(String[] args) {
    
    
    // 只读缓冲区
    // 创建 buffer
    ByteBuffer buffer = ByteBuffer.allocate(10);
    // 写入数据
    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 (buffer.remaining() > 0){
    
    
        System.out.println(readonly.get());
    }
}

5.8.3、直接缓冲区

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

public static void main(String[] args) throws Exception {
    
    
   // 直接缓冲区
   // 定义文件路径
   String infile = "D:\\Cloud\\note\\Channel.txt";
   FileInputStream fin = new FileInputStream(infile);
   FileChannel finChannel = fin.getChannel();

   String outFile = "D:\\Cloud\\note\\0822.txt";
   FileOutputStream fout = new FileOutputStream(infile);
   FileChannel foutChannel = fin.getChannel();
   
   // 创建直接缓冲区
   ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
   while (true){
    
    
       buffer.clear();
       int read = finChannel.read(buffer);
       if (read != -1) {
    
    
           buffer.flip();
           foutChannel.write(buffer);
       }
   }
}

5.8.4、内存映射文件 I/O

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

/**
 * 内存映射文件IO
 */
public class BufferDemo5 {
    
    
    
	// position起始位置
	static private final int start = 0;
	// 缓冲区大小
	static private final int size = 1024;
	
	public static void main(String[] args) throws IOException {
    
    
	    // 打开 FileChannel
	    RandomAccessFile raf = new RandomAccessFile("D:\\Cloud\\note\\Channel.txt","rw");
	    FileChannel fc = raf.getChannel();
	    // MappedByteBuffer 实现内存映射关系
	    MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE,start,size);
	    // 存放数据
	    mbb.put(0,(byte) 97);
	    mbb.put(1024,(byte) 122);
	    raf.close();
	}
}

6、Selectors

选择器、事件监听
Selector 运行单线程处理多个Channel、也就是说可以管理多个网络连接。还可以检查一个或者多个 Channel 的装填是否处于可读、可写。如果你的应用打开了多个通道、但每个连接的流量都很低、使用selector就会很方便、例如一个聊天服务器中、要使用selector、得向selector注册Channel、然后调用它的select()方法、这个方法会一直阻塞到某个注册的通道有事件就绪、一旦方法返回、线程就可以处理这些事件、事件的例子有:新的连接进来、数据接收。
在这里插入图片描述

6.1、可选择通道(selectableChannel)

1、不是所有的Channel 都可以被 Selector复用的。比如、FileChannel 就不能被选择器复用。判断一个 Channel 能被 Selector 复用、有一个前提:判断他是否继承了一个抽象类 SelectavleChannel。如果继承了 SelectableChannel、则可以被复用、反之不能。
2、SelectableChannel 类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。所有 socket 通道、都继承了 SelectableChannel 类都是可选择的,包括从管道 (pipe) 对象当中获得的通道。而 FileChanel 类、没有继承 SelectableChannel、因此是不可选通道。
3、一个通道可以被注册到多个选择器上、但对每个选择器而言只能被注册一个。通道和选择器之间的关系、使用注册的方式完成。SelectableChannel 可以被注册到 Selector 对象上、在注册的时候、需要指定通道的哪些操作、是 Slector 感兴趣的。
在这里插入图片描述

6.2、Channel 注册到 Selector

1、使用 Channel.refister(Selecrot 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)

6.3、选择建 (SelectionKey)

1、Channel 注册到后、并且一旦通道处于某种就绪的状态、就可以被选择器查询到。这个工作、使用选择器 Selector 的 select() 方法完成。select 方法的作用、对感兴趣的通道操作、进行就绪状态的查询。
2、Selector 可以不断的查询 Channel 中发生的操作的就绪状态。并且挑选感兴趣的操作就绪状态。一旦通道有操作的就绪状态完成、并且是 Selector 感兴趣的操作、就会被 Selector 选中、放入选择键集合中。
3、一个选择键、首先是包含了注册在 Selecor 的通道操作的类型、比方说、SelectionKey.OP_READ。也包含了特定的通道与特定的选择器之间的注册关系。开发应用程序时、选择键是编程的关键。NIO 的编程、就是根据对应的选择键、进行不同的业务逻辑处理。
4、 选择键的概念、和事件的概念比较相似。一个选择键类似于监听器模式里边的一个事件。由于 Selector 不是事件触发的模式、而是主动去查询的模式、所以不叫事件 Event、而是叫 SelectionKey 选择键

6.4、Selector 的使用方法

6.4.1、Selector 的创建

通过调用 Selector.open() 方法创建一个 Selector 对象

// 获取 Selector 选择器
Selector selector = Selector.open();

6.4.2、注册 Channel 到 Selector

要实现 Selector 管理 Channel、需要将 channel 注册到相应的 Selector 上

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

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

6.4.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 方法、用来访问以选择键集合、迭代集合的每一个选择键元素、根据就绪操作的类型、完成对应的操作:

 public static void main(String[] args) throws IOException {
    
    
	    // 创建 selector
	     Selector selector = Selector.open();
	     // 通道
	     ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
	     // 设置非阻塞通道
	     serverSocketChannel.configureBlocking(false);
	     // 绑定连接
	     serverSocketChannel.bind(new InetSocketAddress(9999));
	     // 将通道注册到选择器上、并制定监听事件为:“接收” 事件
	     serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
	     // 查询已经就绪通道操作
	     Set<SelectionKey> selectionKeys = selector.selectedKeys();
	     // 遍历集合
	     Iterator<SelectionKey> iterator = selectionKeys.iterator();
	     while (iterator.hasNext()){
    
    
	         SelectionKey key = iterator.next();
	         // 判断 key 就绪状态操作
	         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
	         }
	         iterator.remove();
	     }
	 }
}

6.4.4、停止选择的方法

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

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

/**
 * 客户端
 */
public class SelectorClient {
    
    
    public static void main(String[] args) throws Exception {
    
    
        // 1、获取通道、绑定主机与端口号
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8080));
        // 2、切换到非阻塞模式
        socketChannel.configureBlocking(false);
        // 3、创建 buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
    
    
            String next = scanner.next();
            // 4、写入 buffer 数据
            buffer.put((new Date().toString() + "--->" + next).getBytes());
            // 5、模式切换
            buffer.flip();
            // 6、写入通道
            socketChannel.write(buffer);
            // 7、关闭资源
            buffer.clear();
        }
    }
}
/**
 * 服务端
 */
public class SelectorServer {
    
    
    public static void main(String[] args) throws IOException {
    
    
        // 1、获取服务端通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 2、切换到非阻塞模式
        serverSocketChannel.configureBlocking(false);
        // 3、创建 buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 4、绑定端口号
        serverSocketChannel.bind(new InetSocketAddress("127.0.0.1",8080));
        // 5、获取 Select;ector 选择器
        Selector selector = Selector.open();
        // 6、通道注册到选择器、进行监听
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 7、选择器进行轮询选择、进行后续操作
        while (selector.select() > 0) {
    
    
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 遍历
            Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
            while (selectionKeyIterator.hasNext()) {
    
    
                // 获取就绪操作
                SelectionKey next = selectionKeyIterator.next();
                // 判断什么操作
                // 判断 key 就绪状态操作
                if (next.isAcceptable()){
    
    
                    // 获取连接
                    SocketChannel accept = serverSocketChannel.accept();
                    // 切换非阻塞模式
                    accept.configureBlocking(false);
                    // 注册
                    accept.register(selector,SelectionKey.OP_READ);
                }else if (next.isReadable()){
    
    
                    SocketChannel channel = (SocketChannel) next.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    // 读取数据
                    int length = 0;
                    while ((length = channel.read(byteBuffer)) > 0) {
    
    
                        byteBuffer.flip();
                        System.out.println(new String(byteBuffer.array(),0,length));
                        byteBuffer.clear();
                    }
                }
            }
            selectionKeyIterator.remove();
        }
    }
}

小结

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

7、Pipe 和 FileLock

NIO 通道是2哥线程之间的单向数据连接、Pipe 有一个 source 通道和一个 sink 通道。数据会被写到 sink 通道、从 source 通道读取
在这里插入图片描述

7.1、Pipe

7.1.1、创建管道

通过 Pipe.open() 方法打开管道

Pipe pipe = Pipe.open();

7.1.2、写入管道

要向管道写数据、需要访问 sink 管道。

Pipe.SinkChannel sinkChannel = pipe.sink();

通过调用 SinkChannel 的 write 方法、将数据写入 SinkChannel:

String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()){
    
    
	sinkChannel.write(buf);
}

7.1.3、从管道读取数据

读取管道的数据、需要访问 source 通道、像这样:

Pipe.SourceChannel sourceChannel = pipe.source();

调用 source 通道的 read() 方法来读取数据

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = sourceChannel.read(buf);

read 方法返回的 int 值会告诉我们有多少字节被读进缓冲区

7.1.4、示例

// 1、获取管道
Pipe pipe = Pipe.open();
// 2、获取 sink 通道
Pipe.SinkChannel sinkChannel = pipe.sink();
// 3、创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("atCloud".getBytes());
byteBuffer.flip();
// 4、写入数据
sinkChannel.write(byteBuffer);
// 5、获取 Source 通道
Pipe.SourceChannel sourceChannel = pipe.source();
// 6、读取数据
byteBuffer.flip();
int length = sourceChannel.read(byteBuffer);
System.out.println(new String(byteBuffer.array(),0, length));
// 7、关闭通道
sourceChannel.close();
sinkChannel.close();

7.2、FIleLock

文件锁在 OS 中很常见、如果多个程序同事访问、修改同一个文件、很容易因为文件数据不同步而出现问题。给文件加一个锁、同一时间、只能有一个程序修改此文件、或者程序都只能读此文件、这就解决了同步问题。
文件锁是进程级别的、不是线程级别的。文件锁可以解决多个进程并发访问、修改同一文件问题、但不能解决多线程并发访问、修改同一文件问题。
使用文件锁时、统一进程内的多个线程、可以同时访问、修改此文件。
文件锁时当前程序所属的 JVM 实例持有的、一旦获取到文件锁(对文件加锁)、但要调用 release()、或者关闭对应的 FileChannel 对象、或者当前 JVM 退出、才会释放这个锁。
一旦某个进程(比如说 JVM 实例)对某个文件加锁、则在释放这个锁之前、此进程不能再对此文件加锁、就是说 JVM 实例在同一文件上的文件锁是不重叠的(进程级别不能重复在同一文件上获取锁)。

7.2.1、文件锁分类

**排它锁:**又叫独占锁。对文件加排它锁后、该进程可以对此文件进行读写、该进程独占此文件、其他进程不能读写此文件、直到该进程释放文件锁。
**共享锁:**某个进程对文件加共享锁、其他进程也可以访问此文件、但这些进程都只能读此文件、不能写。线程是安全的。只要还有一个进程持有共享锁、此文件就只能读、不能写。

7.2.2、使用示例

// 创建 FileChannel 对象、文件锁只能通过 FileChannel 对象来使用
FileChannel fileChannel = new FileOutputStream("./1.txt").getChannel();
// 对文件加锁
FileLock lock = fileChannel.lock();
// 对此文件进行读写操作
.......
//释放文件锁
lock.release();

文件锁要通过 FileChannel 对象调用

7.2.3、获取文件锁方法

有 4 种 获取文件锁的方法

// 对整个文件加锁、默认为排它锁
lock();
// 自定义加锁方式
// 前两个参数指定要加锁的部分(可以只对此文件的部分内容加锁)、第三个参数值指定是否是共享锁
lock(long position,long size,boolean shared);
// 对整个文件加锁、默认为排它锁
tryLock();
// 自定义加锁方式
tryLock(long position,long size,boolean shared);

如果指定为共享锁、则其他进程可读此文件、所有进程均不能写此文件、如果某进程视图对此文件进行写操作、会抛出异常。

7.2.4、lock 与 tryLock 的区别

lock 是阻塞的、如果为获取到文件锁、会一直阻塞当前线程、直到获取文件锁
tryLock 和 lock 的作用相同、只不过 tryLock 是非阻塞的、tryLock 是尝试获取文件锁、获取成功就返回锁对象、否则返回 null、不会阻塞当前线程。

7.2.5、FileLock 两个方法

// 此文件锁是否是共享锁
boolean isShared();
// 此文件锁是否还有效
boolean isValid();

在某些 OS 上、对某个文件加锁后、不能对此文件使用通道映射

7.2.6、完整例子

排它锁

public class FileLockDemo1 {
    
    
    public static void main(String[] args) throws Exception {
    
    
        String input = "atCloud";
        System.out.println("input: " + input);
        ByteBuffer buffer = ByteBuffer.wrap(input.getBytes());
        String filePath = "D:\\Cloud\\note\\0822.txt";
        Path path = Paths.get(filePath);
        FileChannel channel = FileChannel.open(path,
                StandardOpenOption.WRITE,
                StandardOpenOption.APPEND);
        channel.position(channel.size()-1);
        // 加锁
        FileLock lock = channel.lock();
        System.out.println("是否共享锁: " + lock.isShared());
        channel.write(buffer);
        channel.close();
        // 读文件
        readFile(filePath);
    }

    private static void readFile(String filePath) throws Exception {
    
    
        FileReader fileReader = new FileReader(filePath);
        BufferedReader bufferedReader = new BufferedReader(fileReader);
        String str = bufferedReader.readLine();
        System.out.println("读取出内容:");
        while (str != null) {
    
    
            System.out.println(str);
            str = bufferedReader.readLine();
        }
        fileReader.close();
        bufferedReader.close();
    }
}

共享锁

public class FileLockDemo1 {
    
    
    public static void main(String[] args) throws Exception {
    
    
        String input = "atCloud";
        System.out.println("input: " + input);
        ByteBuffer buffer = ByteBuffer.wrap(input.getBytes());
        String filePath = "D:\\Cloud\\note\\0822.txt";
        Path path = Paths.get(filePath);
        FileChannel channel = FileChannel.open(path,
                StandardOpenOption.WRITE,
                StandardOpenOption.APPEND);
        channel.position(channel.size()-1);
        // 加锁 共享锁
        FileLock lock = channel.lock(0L,Long.MAX_VALUE,true);
        System.out.println("是否共享锁: " + lock.isShared());
        channel.write(buffer);
        channel.close();
        // 读文件
        readFile(filePath);
    }

    private static void readFile(String filePath) throws Exception {
    
    
        FileReader fileReader = new FileReader(filePath);
        BufferedReader bufferedReader = new BufferedReader(fileReader);
        String str = bufferedReader.readLine();
        System.out.println("读取出内容:");
        while (str != null) {
    
    
            System.out.println(str);
            str = bufferedReader.readLine();
        }
        fileReader.close();
        bufferedReader.close();
    }
}

8、其他

8.1、Path

Java Path 接口是 Java NIO 更新的一部分、同 NIO 一起已经包括在Java6 和 Java7 中。Java Path 接口是在 Java7 中添加到 NIO 的、Path 接口位于 Java.nio.file包中、所以 Path 接口的完全限定名称为 java.nio.file.Path
Path 实例表示文件系统中的路径。一个路径可以指向一个文件或者一个目录。路径可以是绝对路径、也可以是相对路径。绝对路径包含从文件系统的根目录到它指向的文件或者目录的完整路径。相对路径包含相对于其他路径的文件或者目录的路径。
在许多方面、java.nio.file.Path 接口类似于 java.io.File 类、但是有一些区别、不过许多情况下、可以使用 Path 接口替换 File 类的使用。

8.1.1、创建 Path 实例

想要创建 Path 实例可以使用 Paths 当中的静态方法 get 来创建 Path 实例。

Path path = paths.get("D:\\Cloud\\note\\Channel.txt");

get 可以理解为 Path 的实例工厂方法。

8.1.2、 创建绝对路径

创建绝对路径、使用 get 方法给定绝对路径文件作为参数就可以。注意单斜杠要变成双斜杠才可以编译为路径分级符
如果在 linux、Macos 等操作字体上、绝对路径可能就是单个的 / 为路径分级符

8.1.3、创建相对路径

Path 也可以处理相对路径。使用 get 方法创建一个相对路径即可。

// 创建 相对路径
// TODO 方式1
Path projects = Paths.get("D:\\Cloud","note");
// TODO 方式2
Path file = Paths.get("D:\\Cloud","projects\\Channel.txt");

方式1:创建一个 Path 实例、指向路径:D:\Cloud\note
方式2:创建一个 Path 实例、指向路径:D:\Cloud\note\Channel.txt

8.1.4、Path.normalize()

Path 接口的 normalize 方法可以使路径标准化。标准化意味着它将溢出所有在路径字符串的中间的 . 和 … 代码、并解析路径字符串所引用的路径。

// normalize()
 String originalPath = "D:\\Cloud\\note\\..\\Channel.txt";
 Path path1 = Paths.get(originalPath);
 System.out.println("Path1: " + path1);
 Path path2 = Paths.get(originalPath).normalize();
 System.out.println("Path2: " + path2);

不过此方法知道有就行、经过本人测试发现这个方法几乎没有什么卵用

8.2、Files

Files 类提供了几种常用的操作文件系统中的文件的方法。通常与 Path 实例一起工作。

8.2.1、Files.createDirectory()

创建新的目录

Path path = Paths.get("D:\\Cloud\\atCloud");
// createDirectory
Path directories = Files.createDirectories(path);

第一行表示要创建的 Path 实例、在 try-catch 块中、用路径作为参数调用 FIles.createDirectory() 方法、如果创建目录成功、将返回一个 Path 实例、改实例指向新创建的路径。
如果该目录已存在、则会跑出目录已存在异常、如果出现其他错误会抛出 IO 异常。

8.2.2、Files.copy()

// TODO copy
Path file1 = Paths.get("D:\\Cloud\\note\\Channel.txt");
Path file2 = Paths.get("D:\\Cloud\\note\\0822.txt");
try {
    
    
    Files.copy(file1,file2);
} catch (IOException e) {
    
    
    e.printStackTrace();
}

如果目标路径文件已存在、但是你想把内容复制进去或者替换掉,那么可以加上 StandardCopyOption.REPLACE_EXISTING 参数

Files.copy(file1,file2,StandardCopyOption.REPLACE_EXISTING);

8.2.3、Files.move()

移动文件路径、如果遇到重名文件、移动文件可以移动到不同的目录、也可以在相同的操作中更改它的名称

// 移动路径
Files.move(file1,file2);

8.2.3、Files.delete()

删除文件

Files.delete(file2);

8.2.4、Files.walkFileTree()

1、Files.walkFileTree 方法包含递归遍历目录树功能、将 Path 实例和 FileVisitor 作为参数。Path 实例指向要遍历的目录,FIleVisitor 在遍历期间被调用。

2、FIleVisitor 是一个接口、必须自己实现 FIleVisitor 接口、并将实现的实例传递给walkFIleTree 方法。在目录遍历过程中、您的 FIleVisitor 实现的每个方法都将被调用。如果不需要实现所有这些方法、那么可以扩展 SimpleFileVisitor 类、它包含 FIleVIsitor 接口中所有方法的默认实现。

3、FIleVIsitor 接口的方法中、每个都返回一个 FileVisitResult 枚举实例。FileVIsitResult 枚举包含以下四个选项:
CONTINUE 继续
TERMINATE 终止
SKIP_SIBLING 跳过同级
SKIP_SUBTREE 跳过子级

// TODO  walkFileTree()
Path rootPath = Paths.get("D:\\Cloud\\note");
String fileToFind = File.separator + "Channel.txt";
System.out.println(fileToFind);
try {
    
    
    Files.walkFileTree(rootPath,new SimpleFileVisitor<Path>(){
    
    
        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
    
    
            String fileString = file.toAbsolutePath().toString();
//                    System.out.println("fileString:" + fileString);
            if (fileString.endsWith(fileToFind)){
    
    
                System.out.println("file found at path:" + file.toAbsolutePath());
                return FileVisitResult.TERMINATE;
            }
            return FileVisitResult.CONTINUE;
        }
    });
} catch (IOException e) {
    
    
    e.printStackTrace();
}

8.3、AsynchronousFileChannel

Java7 中、NIO 添加了 AsynchronousFileChannel、也就是异步的将数据写入文件。

8.3.1、创建 AsynchronousFileChannel

通过静态方法 open() 创建
这里可以传入两个参数:
参数一:与 AsynchronousFileChannel 相关联文件的 path 实例
参数二:通过 StandardOpenOption 中的属性表示 AsynchronousFileChannel 可以进行的操作

AsynchronousFileChannel file = AsynchronousFileChannel.open(path,StandardOpenOption.READ);

8.3.2、通过 Future 读取数据

调用返回 Future 的 read() 方法

    /**
     * One:创建 AsynchronousFileChannel
     * Two:创建 ByteBuffer、它被传递给 read() 方法作为参数、以及一个 0 的位置
     * Three:在调用 read() 之后、循环、直到返回的 isDone() 方法返回true
     * Four:读取操作完成后、数据读取到 ByteBuffer 中、然后打印到 System.out 中
     * @param args
     */
    public static void main(String[] args) throws IOException {
    
    
        // 1、创建 AsynchronousFileChannel 对象
        Path path = Paths.get("D:\\Cloud\\note\\Channel.txt");
        AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
        // 2、创建 Buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 3、调用 channel 的 read 方法得到 Future
        Future<Integer> future = fileChannel.read(buffer,0);
        // 4、判断是否完成 isDone、返回 true 完成
        while (!future.isDone()) {
    
    

        }
        // 5、读取数据到 Buffer
        buffer.flip();
//        while (buffer.remaining() > 0) {
    
    
//            System.out.println(buffer.get());
//        }
        byte[] data = new byte[buffer.limit()];
        buffer.get(data);
        System.out.println(new String(data));
        buffer.clear();
    }

8.3.3、通过 CompletionHandler 读取数据

调用 read() 方法、将一个 CompletionHandler 作为参数

 // 1、创建 AsynchronousFileChannel 对象
Path path = Paths.get("D:\\Cloud\\note\\Channel.txt");
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
// 2、创建 Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 3、调用 channel 的 read 方法得到 Future
fileChannel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    
    
   /**
    * 读取完成后
    * @param result 文件内容字节长度
    * @param attachment 读取到的内容
    */
   @Override
   public void completed(Integer result, ByteBuffer attachment) {
    
    
       System.out.println("result: " + result);
       attachment.flip();
       byte[] data = new byte[attachment.limit()];
       attachment.get(data);
       System.out.println(new String(data));
       attachment.clear();
   }

   /**
    * 读取失败后
    * @param exc
    * @param attachment
    */
   @Override
   public void failed(Throwable exc, ByteBuffer attachment) {
    
    
       System.out.println("失败");
   }
});

8.3.4、通过 Future 写数据

和读取一样、可以通过两种方式将数据写入一个 AsynchronousFileChannel

 // 1、创建 AsynchronousFileChannel 对象
Path path = Paths.get("D:\\Cloud\\note\\Channel.txt");
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
// 2、创建 Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 3、向 buffer 写入数据
buffer.put("atCloud".getBytes());
buffer.flip();
// 4、调用 write
Future<Integer> future = fileChannel.write(buffer, 0);

while (!future.isDone());
buffer.clear();
System.out.println("write over~");

首先、AsnchronousFileChannel 以写模式打开。然后创建一个 ByteBuffer、并将一些数据写入期中。然后、ByteBuffer 中的数据被写入到文件中。最后示例检查返回的 FUture、一查看写操作完成时的情况。
如果文件不存在则会抛出异常

8.3.5、通过 CompletionHandler 写数据

// 1、创建 AsynchronousFileChannel 对象
Path path = Paths.get("D:\\Cloud\\note\\Channel.txt");
AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
// 2、创建 Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 3、write
buffer.put("atCloud".getBytes());
buffer.flip();
fileChannel.write(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    
    
    /**
     * 写入成功
     * @param result
     * @param attachment
     */
    @Override
    public void completed(Integer result, ByteBuffer attachment) {
    
    
        System.out.println("bytes written: " + result);
    }

    /**
     * 写入失败
     * @param exc
     * @param attachment
     */
    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
    
    

    }
});

8.4、字符集

  • Charset
// 通过编码类型获取 Charset 对象
public static Charset forName(String charsetName);
// 获得系统支持的所有编码方式
public static SortedMap<String,Charset> axailableCharsets();
// 获得虚拟机默认的编码方式
public static Charset defaultCharset();
// 判断是否支持该编码类型
public static boolean isSupported(String charsetName);
  • Charset 普通方法
// 获得 Charset 对象的编码类型(String)
public final String name();
// 获得编码器对象
public abstract CharsetEncoder newEncoder();
// 获得解码器对象
public abstract CharsetDecoder newDecoder();
  • 完整代码示例
// 1、获取 charset 对象
Charset charset = Charset.forName("UTF-8");

// 2、获得编码器对象
CharsetEncoder charsetEncoder = charset.newEncoder();

// 3、创建缓冲区
CharBuffer charBuffer = CharBuffer.allocate(1024);
charBuffer.put("atCloud 程序猿");
charBuffer.flip();

// 4、编码
ByteBuffer byteBuffer = charsetEncoder.encode(charBuffer);
System.out.println("编码后");
for (int i = 0; i < byteBuffer.limit(); i++) {
    
    
    System.out.println(byteBuffer.get());
}

// 5、获取解码器对象
byteBuffer.flip();
CharsetDecoder charsetDecoder = charset.newDecoder();

// 6、解码
CharBuffer charBuffer1 = charsetDecoder.decode(byteBuffer);
System.out.println("解码后");
System.out.println(charBuffer1.toString());

// 7、使用 GBK 解码
Charset charset1 = Charset.forName("GBK");
byteBuffer.flip();
CharBuffer charBuffer2 = charset1.decode(byteBuffer);
System.out.println("使用其他编码方式解码");
System.out.println(charBuffer2.toString());

// TODO 获取 Charset所支持的所有字符编码
Map<String,Charset> map = Charset.availableCharsets();
Set<Map.Entry<String,Charset>> set = map.entrySet();
for (Map.Entry<String,Charset> entry: set) {
    
    
    System.out.println(entry.getKey() + "=" + entry.getValue().toString());
}

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_54177999/article/details/120461232