java-nio网络编程

1.java nio知识体系概述

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

在这里插入图片描述

  • 阻塞IO:

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

在这里插入图片描述

  • 非阻塞(NIO):

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

在这里插入图片描述
从图中可以看出,当有读或写等任何注册的事件发生时,可以从中获取到对应的 SelectionKey ,同时从 SelectionKey 中找到发生的事件和该事件发生的具体SelectableChannel,可获取到客户端发生过来的数据。

非阻塞指的是IO事件本身不阻塞,但是在获取IO事件的select()方法是需要阻塞等待的,区别是在于阻塞的IO会阻塞在IO操作上NIO阻塞在事件获取上,没有事件就没有IO,从高层次上看IO就没有阻塞了,也就是说IO已经发生我们才评估IO是否阻塞,但是select()阻塞的时候IO还没有发生,还有有发生那么久说不上IO阻塞了,NIO的本质是延迟IO的操作,到真正发生IO的时候才发生阻塞,而不是像之前一样,IO流一打开久一直等待IO操作。

在这里插入图片描述

java nio 由着几个核心部分组成:Channel(通道),Buffer(缓冲区),Selector(选择器)

扫描二维码关注公众号,回复: 13538457 查看本文章
  • Channel:

Channel 可以翻译为通道的意思,Channel和IO中的Stream(流)是差不多一个等级的,只不过Stream是单向的,而Channel是双向的。

例如:之前需要用 InputStream(输入流),OutputStream(输出流)才能完成文件的读写,但是现在只需要Channel即可,Channel是双向通道,既可读,也可写。

nio中的Channel主要的实现类有:
在这里插入图片描述

  • Buffer:
  • NIO中关键Buffer实现有:
    在这里插入图片描述
  • Selector:

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

2.Channel

Channel是一个通道,它可以读取和写入数据 ,它就像是水管,网络数据可以通过Channel进行读写,通道流和流的不同之处就是在于,流是单线的,通道流是双向的,流只能在一个方向上移动(一个必须是InputStream或OutputStream的子类),而且Channel可以同时读写,因为Channel是全双工的,它可以比流更好的映射底层操作系统的API。

NIO中通过Channel封装对数据的操作,通过Channel 我们可以操作数据源,但又不必关心数据源的物理结构,这个数据源是多种的,比如说:文件,也可以是网络Socket。在大多数应用中,Channel与文件描述符合或者Socket一 一 对应的,Channel 用在字节缓冲区和位于通道的另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。

Channel接口源码:


package java.nio.channels;

import java.io.IOException;
import java.io.Closeable;


/**
 * A nexus for I/O operations.
 *
 * <p> A channel represents an open connection to an entity such as a hardware
 * device, a file, a network socket, or a program component that is capable of
 * performing one or more distinct I/O operations, for example reading or
 * writing.
 *
 * <p> A channel is either open or closed.  A channel is open upon creation,
 * and once closed it remains closed.  Once a channel is closed, any attempt to
 * invoke an I/O operation upon it will cause a {@link ClosedChannelException}
 * to be thrown.  Whether or not a channel is open may be tested by invoking
 * its {@link #isOpen isOpen} method.
 *
 * <p> Channels are, in general, intended to be safe for multithreaded access
 * as described in the specifications of the interfaces and classes that extend
 * and implement this interface.
 *
 *
 * @author Mark Reinhold
 * @author JSR-51 Expert Group
 * @since 1.4
 */

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.
     *
     * <p> After a channel is closed, any further attempt to invoke I/O
     * operations upon it will cause a {@link ClosedChannelException} to be
     * thrown.
     *
     * <p> If this channel is already closed then invoking this method has no
     * effect.
     *
     * <p> This method may be invoked at any time.  If some other thread has
     * already invoked it, however, then another invocation will block until
     * the first invocation is complete, after which it will return without
     * effect. </p>
     *
     * @throws  IOException  If an I/O error occurs
     */
    public void close() throws IOException;

}

与缓冲区不同,通道API主要由接口指定,不同的操作系统通道实现(Channel Implementation)会有根本上的差异,所以通道API 仅仅描述了可以做什么,因此很自然地,通道实现经常使用操作系统的本地代码,通道接口允许你以一种受控且可移植的方式访问底层的I/O服务。

Channel是一个对象,可以通过它读写数据,拿NIO和IO做个对比,通道像是流,所有的数据都通过Buffer对象来处理,你不能将字节直接写入通道中,也不能直接从通道中读取字节。你应该把数据写入字节缓冲区,然后再写入到通道流中去。

在这里插入图片描述

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

2.1 FileChannel 介绍和示例

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

abstract void force(boolean metaData) 强制将此通道文件的任何更新写入包含该通道的存储设备。
FileLock lock() 获取此通道文件的排他锁。
abstract FileLock lock(long position, long size, boolean shared)获取此通道文件的给定区域的锁定。
abstract MappedByteBuffer map(FileChannel.MapMode mode, long position, long size) 将此频道文件的区域直接映射到内存中。
static FileChannel open(Path path, OpenOption... options) 打开或创建文件,返回文件通道以访问该文件。
static FileChannel open(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) 打开或创建文件,返回文件通道以访问该文件。
abstract long position() 返回此频道的文件位置。
abstract FileChannel position(long newPosition) 设置此通道的文件位置。
abstract int read(ByteBuffer dst) 从该通道读取到给定缓冲区的字节序列。
long read(ByteBuffer[] dsts) 从该通道读取到给定缓冲区的字节序列。
abstract long read(ByteBuffer[] dsts, int offset, int length)` 从该通道读取字节序列到给定缓冲区的子序列中。
abstract int read(ByteBuffer dst, long position) 从给定的文件位置开始,从该通道读取一个字节序列到给定的缓冲区。
abstract long size() 返回此通道文件的当前大小。
abstract long transferFrom(ReadableByteChannel src, long position, long count)从给定的可读字节通道将字节传输到该通道的文件中。
abstract long transferTo(long position, long count, WritableByteChannel target)< 将该通道文件的字节传输到给定的可写字节通道。
abstract FileChannel truncate(long size) 将此频道的文件截断为给定大小。
FileLock tryLock() 尝试获取此频道文件的排他锁。
abstract FileLock tryLock(long position, long size, boolean shared) 尝试获取此通道文件的给定区域的锁定。
abstract int write(ByteBuffer src) 从给定的缓冲区向该通道写入一个字节序列。
long write(ByteBuffer[] srcs)从给定的缓冲区向该通道写入一系列字节。
abstract long write(ByteBuffer[] srcs, int offset, int length)` 从给定缓冲区的子序列将一个字节序列写入该通道。
abstract int write(ByteBuffer src, long position) 从给定的缓冲区向给定的文件位置开始,向该通道写入一个字节序列。

使用FileChannel完成文件的读操作:

Buffer 通常的操作

  1. 将数据写入缓冲区
  2. 调用 buffer.flip() 反转读写模式
  3. 从缓冲区读取数据
  4. 调用 buffer.clear() 或 buffer.compact() 清除缓冲区内容
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-03-15:10
 * @Version:1.0
 * @Description: FileChannel io流通道完成文件的读取
 */
public class FileChannelRead {
    
    
    public static void main(String[] args) throws Exception {
    
    
        //获取到 FileChannel 对象
        RandomAccessFile aFile = new RandomAccessFile("web/nio/FileChannelDemo.txt", "rw");
        FileChannel channel = aFile.getChannel();
        // 得到缓冲数组
        ByteBuffer buf = ByteBuffer.allocate(1024);
        // 开始读取
        int res = channel.read(buf);
        while (res!=-1){
    
    
            System.out.println("读取: " + res);
            buf.flip();
            // 判断是否还有未读取的内容
            while (buf.hasRemaining()){
    
    
                System.out.print((char) buf.get());

            }
            // 清楚缓冲区的内容
            buf.clear();
            res=channel.read(buf);
        }
        aFile.close();
        System.out.println("  read finish");
    }
}

1.打开 FileChannel
在使用 FileChannel 之前,必须先打开它。但是,我们无法直接打开一个FileChannel,需要通过使用一个 InputStream、OutputStream RandomAccessFile 然后通过 getChannel() 方法来获取一个 FileChannel 实例。下面是通过 RandomAccessFile

打开 FileChannel 的示例:

 //获取到 FileChannel 对象
RandomAccessFile aFile = new RandomAccessFile("web/nio/FileChannelDemo.txt", "rw");
 FileChannel channel = aFile.getChannel();

2.从 FileChannel 读取数据

首先,分配一个 Buffer。从 FileChannel 中读取的数据将被读到 Buffer 中。然后,调用FileChannel.read()方法。该方法将数据从 FileChannel 读取到 Buffer 中。read()方法返回的 int 值表示了有多少字节被读到了 Buffer 中。如果返回-1,表示到了文件末尾

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

3.向 FileChannel 写数据

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

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-03-15:43
 * @Version:1.0
 * @Description: FileChannel io流通道完成文件的写入
 * 将从文件开头第5个字节后的内容删除掉: channel.truncate(4);
 *
 */
public class FileChannelWrite {
    
    
    public static void main(String[] args) throws Exception{
    
    
        RandomAccessFile aFile = new RandomAccessFile("web/nio/FileChannelDemo.txt", "rw");
        FileChannel channel = aFile.getChannel();
        // 获取到文件字节数,然后从最大长度开始写入,达到文件追加的方式进行写入
        long size = channel.size();
        channel.position(size);
        byte[] data = size==0?"hello world".getBytes():"\nhello world".getBytes();
        ByteBuffer buf = ByteBuffer.allocate(1024);
        buf.clear();
        buf.put(data);
        buf.flip();

      while (buf.hasRemaining()){
    
    
          channel.write(buf);
      }
      // 将通道中的数据信息强制写入到磁盘中,true表示连同文件权限信息也写入
      channel.force(true);
      channel.close();

    }
}

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

4.关闭 FileChannel

用完 FileChannel 后必须将其关闭。如:

 fileChannel.close();

5.FileChannel 的 position 方法

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

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

如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回-1(文件结束标志)。

如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并
写入数据。这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。

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

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

8.FileChannel 的 force 方

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

9.FileChannel 的 transferTo 和 transferFrom 方法
通道之间的数据传输:如果两个通道中有一个是 FileChannel,那你可以直接将数据从一个 channel 传输到另外一个 channel。

  • transferFrom()方法
    FileChannel 的 transferFrom() 方法可以将数据从源通道传输到 FileChannel中(译者注:这个方法在 JDK 文档中的解释为将字节从给定的可读取字节通道传输到此通道的文件中)。下面是一个 FileChannel 完成文件间的复制的例子:
/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-03-15:43
 * @Version:1.0
 * @Description: 使用transferFrom完成文件复制  将copy1.txt文件中的内容复制到copy2.txt
 *
 *
 */
public class FileChannelWrite2 {
    
    
    public static void main(String[] args) throws Exception{
    
    

        RandomAccessFile aFile1 = new RandomAccessFile("web/nio/copy1.txt", "rw");
        FileChannel channel1 = aFile1.getChannel();

        RandomAccessFile aFile2 = new RandomAccessFile("web/nio/copy2.txt", "rw");
        FileChannel channel2 = aFile2.getChannel();

              long position=0;
              long fileSize = channel1.size();
           
              channel2.transferFrom(channel1,0,fileSize);
              aFile1.close();
              aFile2.close();
              System.out.println("copy over");

    }
}

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

transferTo()方法
transferTo()方法将数据从 FileChannel 传输到其他的 channel 中。
下面是一个 transferTo()方法的例子:

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-03-15:43
 * @Version:1.0
 * @Description: 使用transferFrom完成文件复制  将copy1.txt文件中的内容复制到copy2.txt
 *
 *
 */
public class FileChannelWrite2 {
    
    
    public static void main(String[] args) throws Exception{
    
    

        RandomAccessFile aFile1 = new RandomAccessFile("web/nio/copy1.txt", "rw");
        FileChannel channel1 = aFile1.getChannel();

        RandomAccessFile aFile2 = new RandomAccessFile("web/nio/copy2.txt", "rw");
        FileChannel channel2 = aFile2.getChannel();

              long position=0;
              long fileSize = channel1.size();
             
              channel1.transferTo(0,fileSize,channel2);
     
              aFile1.close();
              aFile2.close();
              System.out.println("copy over");

    }
}

2.2 Socket 通道

Modifier and Type Method and Description
void bind(SocketAddress bindpoint) 将套接字绑定到本地地址。
void close() 关闭此套接字。
void connect(SocketAddress endpoint) 将此套接字连接到服务器。
void connect(SocketAddress endpoint, int timeout) 将此套接字连接到具有指定超时值的服务器。
SocketChannel getChannel() 返回与此套接字相关联的唯一的SocketChannel对象(如果有)。
InetAddress getInetAddress() 返回套接字所连接的地址。
InputStream getInputStream() 返回此套接字的输入流。
boolean getKeepAlive() 测试是否启用了 SO_KEEPALIVE
InetAddress getLocalAddress() 获取套接字所绑定的本地地址。
int getLocalPort() 返回此套接字绑定到的本地端口号。
SocketAddress getLocalSocketAddress() 返回此套接字绑定到的端点的地址。
boolean getOOBInline() 测试是否启用了 SO_OOBINLINE
OutputStream getOutputStream() 返回此套接字的输出流。
int getPort() 返回此套接字连接到的远程端口号。
int getReceiveBufferSize() 获取这个 SocketSO_RCVBUF选项的值,即平台在此 Socket上输入的缓冲区大小。
SocketAddress getRemoteSocketAddress() 返回此套接字连接,或端点的地址 null如果是未连接。
boolean getReuseAddress() 测试是否启用了 SO_REUSEADDR
int getSendBufferSize() 获取此 SocketSO_SNDBUF选项的值,即该平台在此 Socket上输出使用的缓冲区大小。
int getSoLinger() SO_LINGER退货设置。
int getSoTimeout() SO_TIMEOUT退货设置。
boolean getTcpNoDelay() 测试是否启用了 TCP_NODELAY
int getTrafficClass() 在从此Socket发送的数据包的IP头中获取流量类或服务类型
boolean isBound() 返回套接字的绑定状态。
boolean isClosed() 返回套接字的关闭状态。
boolean isConnected() 返回套接字的连接状态。
boolean isInputShutdown() 返回套接字连接的一半是否关闭。
boolean isOutputShutdown() 返回套接字连接的写半是否关闭。
void sendUrgentData(int data) 在套接字上发送一个字节的紧急数据。
void setKeepAlive(boolean on) 启用/禁用 SO_KEEPALIVE
void setOOBInline(boolean on) 启用/禁用 SO_OOBINLINE (接收TCP紧急数据)默认情况下,此选项被禁用,并且在套接字上接收的TCP紧急数据被静默地丢弃。
void setPerformancePreferences(int connectionTime, int latency, int bandwidth) 设置此套接字的性能首选项。
void setReceiveBufferSize(int size) 设置 SO_RCVBUF选项为这个指定的值 Socket
void setReuseAddress(boolean on) 启用/禁用 SO_REUSEADDR套接字选项。
void setSendBufferSize(int size) 设置 SO_SNDBUF选项为这个指定的值 Socket
static void setSocketImplFactory(SocketImplFactory fac) 设置应用程序的客户端套接字实现工厂。
void setSoLinger(boolean on, int linger) 启用/禁用 SO_LINGER ,具有指定的逗留时间(以秒为单位)。
void setSoTimeout(int timeout) 启用/禁用 指定超时的 SO_TIMEOUT(以毫秒为单位)。
void setTcpNoDelay(boolean on) 启用/禁用 TCP_NODELAY (禁用/启用Nagle的算法)。
void setTrafficClass(int tc) 在从此Socket发送的数据包的IP头中设置流量类或服务类型字节。
void shutdownInput() 将此套接字的输入流放置在“流的末尾”。
void shutdownOutput() 禁用此套接字的输出流。
String toString() 将此套接字转换为 String
  • 新的 socket 通道类可以运行非阻塞模式并且是可选择的,可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性。本节中我们会看到,再也没有为每个 socket连接使用一个线程的必要了,也避免了管理大量线程所需的上下文交换开销。借助新的 NIO 类,一个或几个线程就可以管理成百上千的活动 socket 连接了并且只有很少甚至可能没有性能损失。所有的 socket 通道类(DatagramChannel、SocketChannel ServerSocketChannel)都继承了 java.nio.channels.spi 包中的 AbstractSelectableChannel这意味着我们可以用一个 Selector 对象来执行socket 通道的就绪选择(readiness selection)。

  • 请注意 DatagramChannel SocketChannel 实现定义读和写功能的接口而ServerSocketChannel 不实现。ServerSocketChannel 负责监听传入的连接和创建新的 SocketChannel 对象,它本身从不传输数据。

  • 在我们具体讨论每一种 socket 通道前,你应该了解 socket 和 socket 通道之间
    的关系
    。通道是一个连接 I/O 服务导管并提供与该服务交互的方法。就某个 socket 而言,它不会再次实现与之对应的 socket 通道类中的 socket 协议 API,而 java.net 中已经存在的 socket 通道都可以被大多数协议操作重复使用。全部 socket 通道类(DatagramChannel、SocketChannel 和ServerSocketChannel)在被实例化时都会创建一个对等 socket 对象。这些是我们所熟悉的来自 java.net 的类(Socket、ServerSocketDatagramSocket)它们已经被更新以识别通道。对等 socket 可以通过调用 socket( )方法从一个通道上获取。此外,这三个 java.net 类现在都有 getChannel( )方法。

  • 要把一个 socket 通道置于非阻塞模式,我们要依靠所有 socket 通道类的公有超级类:SelectableChannel。就绪选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作,如读或写。非阻塞 I/O 和可选择性是紧密相连的,那也正是管理阻塞模式的 API 代码要在SelectableChannel 超级类中定义的原因。设置或重新设置一个通道的阻塞模式是很简单的,只要调用 configureBlocking( )方法即可,传递参数值为 true 则设为阻塞模式,参数值为 false 值设为非阻塞模式。可以通过调用 isBlocking( )方法来判断某个 socket 通道当前处于哪种模式。

AbstractSelectableChannel.java 中实现的 configureBlocking()方法如下:

 /**
     * Adjusts this channel's blocking mode.
     *
     * <p> If the given blocking mode is different from the current blocking
     * mode then this method invokes the {@link #implConfigureBlocking
     * implConfigureBlocking} method, while holding the appropriate locks, in
     * order to change the mode.  </p>
     */
    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 个通道:

  • ServerSocketChannel:
abstract SocketChannel accept() 接受与此频道套接字的连接。
ServerSocketChannel bind(SocketAddress local) 将通道的套接字绑定到本地地址,并配置套接字以监听连接。
abstract ServerSocketChannel bind(SocketAddress local, int backlog) 将通道的套接字绑定到本地地址,并配置套接字以监听连接。
abstract SocketAddress getLocalAddress() 返回此通道的套接字所绑定的套接字地址。
static ServerSocketChannel open() 打开服务器插槽通道。
abstract <T> ServerSocketChannel setOption(SocketOption<T> name, T value) 设置套接字选项的值。
abstract ServerSocket socket() 检索与此通道关联的服务器套接字。
int validOps() 返回确定此频道支持的操作的操作集。

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 对象以实现新连接到达时自动通知的功能。

以下代码演示了如何使用一个非阻塞的 accept( )方法:

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-03-16:32
 * @Version:1.0
 * @Description: ServerSocketChannel : 服务器套接字通道
 * 在浏览器中输入:http://127.0.0.1:8888 进行访问
 * ServerSocketChannel 是一个基于通道的 socket 监听器。
 */
public class ServerSocketChannelDemo {
    
    
    public static void main(String[] args) throws Exception {
    
    
        int port=8888;
        // 将byte数组包装到 buf 缓冲区中
        ByteBuffer buf = ByteBuffer.wrap("hello nio".getBytes());
        // 打开服务器套接字通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        // 将服务器套接字通道绑定到指定的ip地址与端口号上
        ssc.bind(new InetSocketAddress("localhost",port));
        // 设置服务器套接字通道是否为 阻塞模式 true:是,false:否
        ssc.configureBlocking(true);
        while(true){
    
    
            System.out.println("waiting for connections");
            // 返回一个在非阻塞模式下运行的一个 SocketChannel 对象
            SocketChannel sc = ssc.accept();
            if (sc==null){
    
    
                Thread.sleep(1000);
            }else {
    
    
                System.out.println("connection message form "+sc.getRemoteAddress());
                buf.rewind();
                // 将buf中的内容写入到套接字通道中,返回给客户端
                sc.write(buf);
                // 关闭套接字通道
                sc.close();

            }

        }

    }
}

1.打开 ServerSocketChannel

通过调用 ServerSocketChannel.open() 方法来打开 ServerSocketChannel .

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

2.关闭 ServerSocketChannel
通过调用 ServerSocketChannel.close() 方法来关闭 ServerSocketChannel.

serverSocketChannel.close();

3.监听新的连接
通过 ServerSocketChannel.accept() 方法监听新进的连接。当 accept() 方法返回时候,它返回一个包含新进来的连接的 SocketChannel 。因此, accept()方法会一直阻塞到有新连接到达 。通常不会仅仅只监听一个连接,在 while 循环中调用 accept()方法.
如下面的例子:

阻塞模式会在 SocketChannel sc = ssc.accept();这里阻塞住进程。

     while(true){
    
    
            System.out.println("waiting for connections");
            // 返回一个在非阻塞模式下运行的一个 SocketChannel 对象
            SocketChannel sc = ssc.accept();
         
 System.out.println("connection message form "+sc.getRemoteAddress());
                buf.rewind();
                // 将buf中的内容写入到套接字通道中,返回给客户端
                sc.write(buf);
                // 关闭套接字通道
                sc.close();

     

        }

非阻塞模式
ServerSocketChannel 可以设置成非阻塞模式。在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是 null。 因此,需要检查返回的SocketChannel 是否是 null .如:

        while(true){
    
    
            System.out.println("waiting for connections");
            // 返回一个在非阻塞模式下运行的一个 SocketChannel 对象
            SocketChannel sc = ssc.accept();
            if (sc==null){
    
    
                Thread.sleep(1000);
            }else {
    
    
                System.out.println("connection message form "+sc.getRemoteAddress());
                buf.rewind();
                // 将buf中的内容写入到套接字通道中,返回给客户端
                sc.write(buf);
                // 关闭套接字通道
                sc.close();

            }

        }
  • SocketChannel
Modifier and Type Method and Description
abstract SocketChannel bind(SocketAddress local) 将通道的套接字绑定到本地地址。
abstract boolean connect(SocketAddress remote) 连接此通道的插座。
abstract boolean finishConnect() 完成连接插座通道的过程。
abstract SocketAddress getLocalAddress() 返回此通道的套接字所绑定的套接字地址。
abstract SocketAddress getRemoteAddress() 返回此通道的插座所连接的远程地址。
abstract boolean isConnected() 告知本频道的网络插座是否连接。
abstract boolean isConnectionPending() 告知此频道是否正在进行连接操作。
static SocketChannel open() 打开套接字通道。
static SocketChannel open(SocketAddress remote) 打开套接字通道并将其连接到远程地址。
abstract int read(ByteBuffer dst) 从该通道读取到给定缓冲区的字节序列。
long read(ByteBuffer[] dsts) 从该通道读取到给定缓冲区的字节序列。
abstract long read(ByteBuffer[] dsts, int offset, int length) 从该通道读取字节序列到给定缓冲区的子序列中。
abstract <T> SocketChannel setOption(SocketOption<T> name, T value) 设置套接字选项的值。
abstract SocketChannel shutdownInput() 关闭连接进行阅读,不关闭频道。
abstract SocketChannel shutdownOutput() 关闭连接以进行写入,而不关闭通道。
abstract Socket socket() 检索与此通道相关联的套接字。
int validOps() 返回确定此频道支持的操作的操作集。
abstract int write(ByteBuffer src) 从给定的缓冲区向该通道写入一个字节序列。
long write(ByteBuffer[] srcs) 从给定的缓冲区向该通道写入一系列字节。
abstract long write(ByteBuffer[] srcs, int offset, int length) 从给定缓冲区的子序列将一个字节序列写入该通道。

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

  1. 对于已经存在的 socket 不能创建 SocketChannel
  2. SocketChanne l 中提供的 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 算法

SocketChannel 的使用

1.创建 SocketChannel

方式一:

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 是否为 open 状态
socketChannel.isOpen(); 

//测试 SocketChannel 是否已经被连接
socketChannel.isConnected(); 

//测试 SocketChannel 是否正在进行连接
socketChannel.isConnectionPending();
 
//校验正在进行套接字连接的 SocketChannel是否已经完成连接

socketChannel.finishConnect(); 

3.读写模式
SocketChannel 支持阻塞和非阻塞两种模式,通过以上方法设置 SocketChannel 的读写模式。false 表示非阻塞,true 表示阻塞。

socketChannel.configureBlocking(false);

4.读写

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相同。

5.设置和获取参数

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

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

socketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE);
socketChannel.getOption(StandardSocketOptions.SO_RCVBUF);

可以通过 getOption 获取相关参数的值。如默认的接收缓冲区大小是 8192byte。

SocketChannel 还支持多路复用,但是多路复用在后续内容中会介绍到。

2.3DatagramChannel

Modifier and Type Method and Description
abstract DatagramChannel bind(SocketAddress local) 将通道的套接字绑定到本地地址。
abstract DatagramChannel connect(SocketAddress remote) 连接此通道的插座。
abstract DatagramChannel disconnect() 断开此通道的插座。
abstract SocketAddress getLocalAddress() 返回此通道的套接字所绑定的套接字地址。
abstract SocketAddress getRemoteAddress() 返回此通道的插座所连接的远程地址。
abstract boolean isConnected() 告诉这个通道的插座是否连接。
static DatagramChannel open() 打开数据报通道。
static DatagramChannel open(ProtocolFamily family) 打开数据报通道。
abstract int read(ByteBuffer dst) 从此频道读取数据报。
long read(ByteBuffer[] dsts) 从此频道读取数据报。
abstract long read(ByteBuffer[] dsts, int offset, int length) 从此频道读取数据报。
abstract SocketAddress receive(ByteBuffer dst) 通过该频道接收数据报。
abstract int send(ByteBuffer src, SocketAddress target) 通过此频道发送数据报。
abstract <T> DatagramChannel setOption(SocketOption<T> name, T value) 设置套接字选项的值。
abstract DatagramSocket socket() 检索与此通道相关联的数据报套接字。
int validOps() 返回确定此频道支持的操作的操作集。
abstract int write(ByteBuffer src) 将数据报写入此通道。
long write(ByteBuffer[] srcs) 将数据报写入此通道。
abstract long write(ByteBuffer[] srcs, int offset, int length) 将数据报写入此通道。

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

1.打开DatagramChannel

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

此例子是打开 8888 端口接收 UDP 数据包

2.接收数据

通过 receive() 接收 UDP

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

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

3.发送数据
通过 send() 发送 UDP

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

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 异常。

DatagramChannel数据发送和接收案例:

import org.junit.Test;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.charset.Charset;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-03-18:13
 * @Version:1.0
 * @Description:
 */
public class DatagramChannelDemo {
    
    
    /**
     * 发送的 Datagram
     */
   @Test
    public void sendDatagram() throws Exception {
    
    
        DatagramChannel sendChannel = DatagramChannel.open();
        InetSocketAddress receiveAddress = new InetSocketAddress("localhost", 9999);
       while (true){
    
    
           sendChannel.send(ByteBuffer.wrap("发送-data".getBytes("UTF-8")),receiveAddress);
           System.out.println("send data ......");
           Thread.sleep(1000);
       }
    }

    /**
     *  接收来自9999端口号发送过来的数据
     */
    @Test
    public void receive()throws Exception{
    
    
        DatagramChannel receiveChannel = DatagramChannel.open();
        InetSocketAddress receiveAddress = new InetSocketAddress("localhost", 9999);
        receiveChannel.bind(receiveAddress);
        ByteBuffer buf = ByteBuffer.allocate(1024);

        while (true){
    
    
            buf.clear();
            SocketAddress sendAddress = receiveChannel.receive(buf);
            buf.flip();
            System.out.println(sendAddress.toString());
            // 设置字符集
            System.out.println(Charset.forName("UTF-8").decode(buf));
        }
    }
}

2.4 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 中,这样你可
以方便的处理消息头和消息体。

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 才能正常工作。

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

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

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

3.Buffer

Modifier and Type Method and Description
abstract Object array() 返回支持此缓冲区的数组 (可选操作)
abstract int arrayOffset() 返回该缓冲区的缓冲区的第一个元素的背衬数组中的偏移量 (可选操作)
int capacity() 返回此缓冲区的容量。
Buffer clear() 清除此缓冲区。
Buffer flip() 翻转这个缓冲区。
abstract boolean hasArray() 告诉这个缓冲区是否由可访问的数组支持。
boolean hasRemaining() 告诉当前位置和极限之间是否存在任何元素。
abstract boolean isDirect() 告诉这个缓冲区是否为 direct
abstract boolean isReadOnly() 告知这个缓冲区是否是只读的。
int limit() 返回此缓冲区的限制。
Buffer limit(int newLimit) 设置此缓冲区的限制。
Buffer mark() 将此缓冲区的标记设置在其位置。
int position() 返回此缓冲区的位置。
Buffer position(int newPosition) 设置这个缓冲区的位置。
int remaining() 返回当前位置和限制之间的元素数。
Buffer reset() 将此缓冲区的位置重置为先前标记的位置。
Buffer rewind() 倒带这个缓冲区。

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

在这里插入图片描述

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装 成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。缓冲区实际上是 一个容器对象,更直接的说,其实就是一个数组,在 NIO 库中,所有数据都是用缓冲 区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到 缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流 I/O 系统中,所有数据都是直接写入或者直接将数据读取到 Stream 对象中。 在 NIO 中,所有的缓冲区类型都继承于抽象类 Buffer,最常用的就是 ByteBuffer, 对于 Java 中的基本类型,基本都有一个具体 Buffer 类型与之相对应,它们之间的继 承关系如下图所示:
在这里插入图片描述

3.1 Buffer 的基本用法

1.使用 Buffer 读写数据,一般遵循以下四个步骤:
(1)写入数据到 Buffer
(2)调用 flip() 方法
(3)从 Buffer 中读取数据
(4)调用 clear() 方法或者 compact() 方法

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

使用 Buffer 的例子:

import org.junit.Test;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.channels.FileChannel;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-03-19:31
 * @Version:1.0
 * @Description:
 */
public class ByteBufferDemo {
    
    

    // ByteBuffer 简单示例
    @Test
    public void byteBufferDemo01() throws Exception{
    
    
        // 1.创建一个channel
        RandomAccessFile aFile = new RandomAccessFile("web/nio/FileChannelDemo.txt","rw");
        FileChannel fileChannel = aFile.getChannel();
        // 2.创建Buffer的大小
        ByteBuffer buf = ByteBuffer.allocate(1024);

        // 3.读取,如果读取到-1,表示已经读取到了文件的末尾
        int byteRead = fileChannel.read(buf);
        while (byteRead!=-1){
    
    
        //4. 转换为read模式
            buf.flip();
           while (buf.hasRemaining()){
    
    
               // 从buffer缓冲区中获取数据
               System.out.print((char)buf.get());
           }
            // 读取一次就清空一次缓冲区,方便下一次读取
            buf.clear();
            byteRead=fileChannel.read(buf);
        }

        // 关闭文件流通道
        fileChannel.close();
        System.out.println("\nread finish");
    }

    // IntBuffer 示例
    @Test
    public void IntBufferDemo(){
    
    
        // 创建一个int类型的缓冲区
        IntBuffer buffer = IntBuffer.allocate(10);
        // 往buffer中放入数据
        for (int i = 0; i < buffer.capacity(); i++) {
    
    
            int j=2*(i+1);
            buffer.put(j);
        }
        // 重置缓冲区
        buffer.flip();
        // 获取缓冲区的数据
        while (buffer.hasRemaining()){
    
    
            int res=buffer.get();
            System.out.println("res="+res);
        }
        System.out.println(buffer.capacity());
        System.out.println(buffer.limit());
        // 关闭缓冲区流
        buffer.compact();
    }
}

3.2 Buffer 的 capacity、position 和 limit

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

  • Capacity
  • Position
  • limt

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

这里有一个关于 capacitypositionlimit 在读写模式中的说明

在这里插入图片描述

  • 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)。

Java NIO 有以下 Buffer 类型:

在这里插入图片描述
这些 Buffer 类型代表了不同的数据类型。换句话说,就是可以通过 char,short,int,long,float 或 double 类型来操作缓冲区中的字节。

1 . Buffer 分配
要想获得一个 Buffer 对象首先要进行分配。 每一个 Buffer 类都有一个 allocate 方法。
下面是一个分配 48 字节 capacity 的 ByteBuffer 的例子。

ByteBuffer buf = ByteBuffer.allocate(48);

2.向 Buffer 中写数据

  • 从 Channel 写到 Buffer 。
  • 通过 Buffer 的 put() 方法写到 Buffer 里。

Channel 写到 Buffer 的例子

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

通过 put 方法写 Buffer 的例子

 ByteBuffer buffer = ByteBuffer.allocate(1024);
     byte[] bytes = "admin".getBytes();
     buffer.put(bytes);

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

3.flip()方法

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

从 Buffer 中读取数据有两种方式:

  • 从 Buffer 读取数据到 Channel。
  • 使用 get()方法从 Buffer 中读取数据。

Buffer 读取数据到 Channel 的例子:

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

使用 get()方法从 Buffer 中读取数据的例子

byte aByte = buf.get();

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

3.3 Buffer 几个常用方法

1、rewind()方法 :Buffer.rewind()将 position 设回 0,所以你可以重读 Buffer 中的所有数据。limit 保 持不变,仍然表示能从 Buffer 中读取多少个元素(byte、char 等)。

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 准备好写数据了,但是不会覆盖未读的数据。

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 back to mark.

3.4 缓冲区操作

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

/**
     * 缓冲区分片
     */
    @Test
    public void bufferSlice(){
    
    

        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();
            b*=10;
            slice.put(i,b);
        }
        // 缓冲区还原
        buffer.position(0);
        buffer.limit(buffer.capacity());

        while (buffer.remaining()>0){
    
    
            System.out.println(buffer.get());
        }
    }

2、只读缓冲区

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

 /**
     * 只读缓冲区:只能读,不能写入数据
     */
    @Test
    public void onlyReadBuffer(){
    
    
        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 < readOnly.capacity(); i++) {
    
    
            byte b = buffer.get(i);
            b*=10;
            buffer.put(i,b);
        }

        //改变缓冲区
        readOnly.position(0);
        readOnly.limit(readOnly.capacity());
        // 从只读缓冲区中获取数据 (原缓冲区和只读缓冲区共享数据,任何一方数据修改,数据都会同步给对方)
        while (readOnly.remaining()>0){
    
    
            System.out.println(readOnly.get());
        }

    }

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

3、直接缓冲区

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

/**
     * 直接缓冲区
     */
    @Test
    public void directBufferDemo() throws Exception{
    
    
        // 创建文件输入流
        String fileName1="web/nio/copy1.txt";
        FileInputStream in = new FileInputStream(fileName1);
        FileChannel fileChannelIn = in.getChannel();

        //创建文件输出流
        String fileName2="web/nio/copy2.txt";
        FileOutputStream os = new FileOutputStream(fileName2);
        FileChannel fileChannelOut = os.getChannel();

        // 创建直接缓冲区
        ByteBuffer direct = ByteBuffer.allocateDirect(1024);
        //一边读取一边写
        while (true){
    
    
            direct.clear();
            int read = fileChannelIn.read(direct);
            if(read==-1){
    
    
                break;
            }
            direct.flip();
            fileChannelOut.write(direct);
        }
        //关闭文件流
        fileChannelIn.close();
        fileChannelOut.close();
        in.close();
        os.close();

    }

4、内存映射文件 I/O

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

  /**
     *  内存映射文件
     * @throws Exception
     */
    @Test
    public void memoryMappingBufferDemo() throws Exception{
    
    
        final int START=0;
        final int SIZE=1024;
        RandomAccessFile accessFile = new RandomAccessFile("web/nio/FileChannelDemo.txt","rw");
        FileChannel fileChannel = accessFile.getChannel();

        MappedByteBuffer mapBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, START, SIZE);

        String data="hello world";
        mapBuffer.put(data.getBytes(),0,data.length());
        fileChannel.write(mapBuffer);
        accessFile.close();
        fileChannel.close();
        mapBuffer.clear();

    }

4.Selector

Modifier and Type Method and Description
abstract void close() 关闭此选择器。
abstract boolean isOpen() 告诉这个选择器是否打开。
abstract Set<SelectionKey> keys() 返回此选择器的键集。
static Selector open() 打开选择器。
abstract SelectorProvider provider() 返回创建此通道的提供程序。
abstract int select() 选择一组其相应通道准备好进行I / O操作的键。
abstract int select(long timeout) 选择一组其相应通道准备好进行I / O操作的键。
abstract Set<SelectionKey> selectedKeys() 返回此选择器的选择键集。
abstract int selectNow() 选择一组其相应通道准备好进行I / O操作的键。
abstract Selector wakeup() 导致尚未返回的第一个选择操作立即返回。

4.1 Selector 和 Channel 关系

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

在这里插入图片描述

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

4.2 可选择通道(SelectableChannel)

在这里插入图片描述

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

4.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 ;
  1. 选择器查询的不是通道的操作,而是通道的某个操作的一种就绪状态。什么是操作的就绪状态一旦通道具备完成某个操作的条件,表示该通道的某个操作已经就绪,就可以被 Selector 查询到,程序可以对通道进行对应的操作。比方说,某个SocketChannel 通道可以连接到一个服务器,则处于“连接就绪”(OP_CONNECT)。再比方说,一个 ServerSocketChannel 服务器通道准备好接收新进入的连接,则处于“收就绪”(OP_ACCEPT)状态。还比方说,一个有数据可读的通道,可以说是“读就绪”(OP_READ)。一个等待写数据的通道可以说是“写就绪”(OP_WRITE)。

4.4 选择键(SelectionKey)

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

4. 5 Selector 的使用方法

5.1. Selector 的创建
获取 Selector 选择器,通过调用 Selector.open() 方法创建一个 Selector 对象,如下:

Selector selector = Selector.open();

5.2. 注册 Channel 到 Selecto
要实现 Selector 管理 Channel,需要将 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()方法会将它注册到一个选择器上

首先需要注意的是:

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

5.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.4停止选择的方法

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

  • wakeup()方法 :通过调用 Selector 对象的 wakeup()方法让处在阻塞状态的select()方法立刻返回

  • 该方法使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有进行中的选择操作,那么下一次对 select()方法的一次调用将立即返回

  • close()方法 :通过 close()方法关闭 Selector

  • 该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似 wakeup()),同时使得注册到该 Selector 的所有 Channel 被注销,所有的键将被取消,但是 Channel本身并不会关闭。

5.5代码示例

服务端代码:

 /**
     *  服务端代码
     */
    @Test
    public void selectorServer() throws Exception{
    
    
        // 获取服务通道
        ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
        // 切换为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        // 创建buffer缓冲区
        ByteBuffer serverByteBuffer = ByteBuffer.allocate(1024);
        // 绑定端口号,做到监听的过程
        serverSocketChannel.bind(new InetSocketAddress("localhost",8080));
        // 获取selector选择器
        Selector selector = Selector.open();
        // 通道注册到选择器,进行监听
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 选择器进行轮询,查看那些通道是处于就行状态,进一步操作
        // while (selector.select()>0){
    
    
        while (selector.select()>0){
    
    
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            //遍历
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
           while (iterator.hasNext()){
    
    
               SelectionKey key = iterator.next();
               // 是否就绪接收状态
               if (key.isAcceptable()){
    
    
                   // 获取连接
                   SocketChannel accept = serverSocketChannel.accept();
                   // 切换非阻塞模式
                   accept.configureBlocking(false);
                   // 注册
                   accept.register(selector, SelectionKey.OP_READ);
               }else if (key.isReadable()){
    
     // 是否就绪读取状态
                   SocketChannel socketChannel = (SocketChannel) key.channel();
                   ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
                   // 读取数据
                   int length=0;
                  while ((length=socketChannel.read(serverByteBuffer))>0){
    
    
                      serverByteBuffer.flip();
                      System.out.println(new String(serverByteBuffer.array(),0,length));
                      serverByteBuffer.clear();
                  }

               }
           }
            iterator.remove();
        }
    }

客户端代码:

    public static void main(String[] args) throws Exception {
    
    
        // 获取通道
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost",8080));
        // 设置为非阻塞模式
        socketChannel.configureBlocking(false);
        // 创建buffer缓冲区 写入数据
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        System.out.print("please input message:");
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
    
    
            System.out.print("please input message:");
            String input = scanner.next();
            buffer.put((new Date()+"-->"+input).getBytes());
            // 模式切换
            buffer.flip();
            // 写入通道
            socketChannel.write(buffer);
            // 关闭
            buffer.clear();
        }

    }

NIO 编程步骤总结:
第一步:创建 Selector 选择器
第二步:创建 ServerSocketChannel 通道,并绑定监听端口
第三步:设置 Channel 通道是非阻塞模式
第四步:把 Channel 注册到 Socketor 选择器上,监听连接事件
第五步:调用 Selector 的 select 方法(循环调用),监测通道的就绪状况
第六步:调用 selectKeys 方法获取就绪 channel 集合
第七步:遍历就绪 channel 集合,判断就绪事件类型,实现具体的业务操作
第八步:根据业务,决定是否需要再次注册监听事件,重复执行第三步操作

5. 辅助工具类

5.1 Pipe

Modifier and Type Method and Description
static Pipe open() 打开管道。
abstract Pipe.SinkChannel sink() 返回此管道的接收通道。
abstract Pipe.SourceChannel source() 返回此管道的源通道。

Java NIO 管道是 2 个线程之间的单向数据连接。Pipe 有一个 source 通道和一个
sink 通道。数据会被写到 sink 通道,从 source 通道读取。

在这里插入图片描述
1、创建管道 通过 Pipe.open()方法打开管道。 Pipe pipe = Pipe.open();

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

} 

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

Pipe.SourceChannel sourceChannel = pipe.source();

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

ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = sourceChannel.read(buf);

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

4.完整代码示例:


import java.nio.ByteBuffer;
import java.nio.channels.Pipe;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-04-14:59
 * @Version:1.0
 * @Description: 两个线程间的单向数据传输
 */
public class PipeDemo {
    
    
    public static void main(String[] args)  throws Exception{
    
    
        // 1、获取通道
        Pipe pipe = Pipe.open();
        // 2、获取 sink 管道,用来传送数据
        Pipe.SinkChannel sinkChannel = pipe.sink();
        // 3、申请一定大小的缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.put("New String to write to file ... ".getBytes());
        byteBuffer.flip();
        // 4、sink 发送数据
        sinkChannel.write(byteBuffer);
        // 5、创建接收 pipe 数据的 source 管道
        Pipe.SourceChannel sourceChannel = pipe.source();
        // 6、接收数据,并保存到缓冲区中
        ByteBuffer byteBuffer2 = ByteBuffer.allocate(1024);
        int length = sourceChannel.read(byteBuffer2);
        System.out.println(new String(byteBuffer2.array(), 0, length));
        sourceChannel.close();
        sinkChannel.close();

    }
}

5.2 FileLock

1、FileLock 简介

Modifier and Type Method and Description
Channel acquiredBy() 返回获取此锁的文件的通道。
FileChannel channel() 返回获取此锁的文件通道。
void close() 此方法调用 release()方法。
boolean isShared() 告诉这个锁是否共享。
abstract boolean isValid() 告诉这个锁是否有效。
boolean overlaps(long position, long size) 告诉这个锁是否与给定的锁定范围重叠。
long position() 返回锁定区域的第一个字节的文件中的位置。
abstract void release() 释放这个锁
long size() 以字节为单位返回锁定区域的大小。
String toString() 返回描述此锁的范围,类型和有效性的字符串。
  • 文件锁在 OS 中很常见,如果多个程序同时访问、修改同一个文件,很容易因为文件数据不同步而出现问题 。给文件加一个锁,同一时间,只能有一个程序修改此文件,或者程序都只能读此文件,这就解决了同步问题。

  • 文件锁是进程级别的,不是线程级别的。文件锁可以解决多个进程并发访问、修改同一个文件的问题,但不能解决多线程并发访问、修改同一文件的问题。使用文件锁时,同一进程内的多个线程,可以同时访问、修改此文件

  • 文件锁是当前程序所属的 JVM 实例持有的,一旦获取到文件锁(对文件加锁),要调用 release(),或者关闭对应的 FileChannel 对象,或者当前 JVM 退出,才会释放这个锁

  • 旦某个进程(比如说 JVM 实例)对某个文件加锁,则在释放这个锁之前,此进程不能再对此文件加锁,就是说 JVM 实例在同一文件上的文件锁是不重叠的(进程级别不 能重复在同一文件上获取锁)。

2、文件锁分类:

  • 排它锁:又叫独占锁。对文件加排它锁后,该进程可以对此文件进行读写,该进程独 占此文件,其他进程不能读写此文件,直到该进程释放文件锁。

  • 共享锁:某个进程对文件加共享锁,其他进程也可以访问此文件,但这些进程都只能 读此文件,不能写。线程是安全的。只要还有一个进程持有共享锁,此文件就只能 读,不能写。

3.使用示例:

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

文件锁要通过 FileChannel 对象使用。

4.获取文件锁4 种方法

  • lock() //对整个文件加锁,默认为排它锁。

  • lock(long position, long size, booean shared) //自定义加锁方式。前 2 个参数指定要加锁的部分(可以只对此文件的部分内容加锁),第三个参数值指定是否是共享锁。

  • tryLock() //对整个文件加锁,默认为排它锁。

  • tryLock(long position, long size, booean shared) //自定义加锁方式。如果指定为共享锁,则其它进程可读此文件,所有进程均不能写此文件,如果某进程试图对此文件进行写操作,会抛出异常。

5. lock 与 tryLock 的区别:

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

6. FileLock 两个方法:
boolean isShared() //此文件锁是否是共享锁
boolean isValid() //此文件锁是否还有效
在某些 OS 上,对某个文件加锁后,不能对此文件使用通道映射。

7.完整代码案例:

import org.junit.Test;

import java.io.BufferedReader;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-04-15:11
 * @Version:1.0
 * @Description: 文件锁,基于进程,如果是同一进程下,即使被加锁,也会被其他的线程访问
 */
public class FileLockDemo {
    
    
    public static void main(String[] args) throws Exception {
    
    

        String input = " hello world";
        System.out.println("输入 :" + input);
        ByteBuffer buf = ByteBuffer.wrap(input.getBytes());
        String fp = "web/nio/FileChannelDemo.txt";
        Path pt = Paths.get(fp);
        FileChannel channel = FileChannel.open(pt, StandardOpenOption.WRITE,StandardOpenOption.APPEND);
        long fileSize=channel.size();
        // fileSize-1(当文件中没有任何内容的时候,直接fileSize-1回出现IllegalArgumentException,只能设置为:0)
        channel.position(fileSize>0?fileSize-1:0); // position of a cursor at the end of   file

        // 获得锁方法一:lock(),阻塞方法,当文件锁不可用时,当前进程会被挂起
        //lock = channel.lock();// 无参 lock()为独占锁
        // lock = channel.lock(0L, Long.MAX_VALUE, true);//有参 lock()为共享锁,有写操作会报异常

        // 获得锁方法二:tryLock(),非阻塞的方法,当文件锁不可用时,tryLock()会得到 null 值

        FileLock lock = channel.tryLock(0,Long.MAX_VALUE,false);
        System.out.println("共享锁 shared: " + lock.isShared());
        channel.write(buf);
        channel.close(); // Releases the Lock
        System.out.println("write operation finish");
        //读取数据
        readPrint(fp);

    }

    public  static void readPrint(String path) throws IOException {
    
    
        FileReader filereader = new FileReader(path);
        BufferedReader bufferedreader = new BufferedReader(filereader);
        String readLine = bufferedreader.readLine();
        System.out.println("读取内容: ");
        while (readLine != null) {
    
    
            System.out.println(" " + readLine);
            readLine = bufferedreader.readLine();
        }
        filereader.close();
        bufferedreader.close();
    }
}

5.3 Path

Modifier and Type Method and Description
int compareTo(Path other) 比较两个抽象的路径词典。
boolean endsWith(Path other) 测试此路径是否以给定的路径结束。
boolean endsWith(String other) 测试此路径是否以 Path结束,通过转换给定的路径字符串,完全按照 endsWith(Path)方法指定的方式构建。
boolean equals(Object other) 测试此路径与给定对象的相等性。
Path getFileName() 将此路径表示的文件或目录的名称返回为 Path对象。
FileSystem getFileSystem() 返回创建此对象的文件系统。
Path getName(int index) 返回此路径的名称元素作为 Path对象。
int getNameCount() 返回路径中的名称元素的数量。
Path getParent() 返回 父路径 ,或 null如果此路径没有父。
Path getRoot() 返回此路径的根组分作为 Path对象,或 null如果该路径不具有根组件。
int hashCode() 计算此路径的哈希码。
boolean isAbsolute() 告诉这条路是否是绝对的。
Iterator<Path> iterator() 返回此路径的名称元素的迭代器。
Path normalize() 返回一个路径,该路径是冗余名称元素的消除。
WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events) 使用手表服务注册此路径所在的文件。
WatchKey register(WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers) 使用手表服务注册此路径所在的文件。
Path relativize(Path other) 构造此路径和给定路径之间的相对路径。
Path resolve(Path other) 根据这条路径解决给定的路径。
Path resolve(String other) 一个给定的路径字符串转换为 Path并解析它针对此 Path在完全按规定的方式 resolve方法。
Path resolveSibling(Path other) 根据此路径的 parent路径解决给定的路径。
Path resolveSibling(String other) 将给定的路径字符串转换为 Path ,并按照 resolveSibling方法指定的方式将其解析为该路径的 parent路径。
boolean startsWith(Path other) 测试此路径是否以给定的路径开始。
boolean startsWith(String other) 测试此路径是否以 Path ,通过转换给定的路径字符串,按照 startsWith(Path)方法指定的方式构建。
Path subpath(int beginIndex, int endIndex) 返回一个相对的 Path ,它是该路径的名称元素的子序列。
Path toAbsolutePath() 返回表示此路径的绝对路径的 Path对象。
File toFile() 返回表示此路径的File对象。
Path toRealPath(LinkOption... options) 返回现有文件的 真实路径。
String toString() 返回此路径的字符串表示形式。
URI toUri() 返回一个URI来表示此路径。

1.Path简介:

  • Java Path 接口是 Java NIO 更新的一部分,同 Java NIO 一起已经包括在 Java6 和Java7 中。Java Path 接口是在 Java7 中添加到 Java NIO 的。Path 接口位于java.nio.file 包中,所以 Path 接口的完全限定名称为 java.nio.file.Path。

  • Java Path 实例表示文件系统中的路径。一个路径可以指向一个文件或一个目录。路径可以是绝对路径,也可以是相对路径。绝对路径包含从文件系统的根目录到它指向的文件或目录的完整路径。相对路径包含相对于其他路径的文件或目录的路径。

  • 在许多方面,java.nio.file.Path 接口类似于 java.io.File 类,但是有一些差别。不过,在许多情况下,可以使用 Path 接口来替换 File 类的使用。

2.创建Path实例:
使用 java.nio.file.Path 实例必须创建一个 Path 实例。可以使用 Paths 类
(java.nio.file.Paths)中的静态方法 Paths.get()来创建路径实例。

  Path path = Paths.get("C:/Users/FileChannelDemo.txt");

3.创建相对路径
Java NIO Path 类也可以用于处理相对路径。您可以使用 Paths.get(basePath,
relativePath)
方法创建一个相对路径。

// 路径:web/nio/FileChannelDemo.txt 
Path path = Paths.get("web/nio", "FileChannelDemo.txt");
// 路径:web/nio/code
Path path = Paths.get("web/nio", "code");

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

    Path path = Paths.get("C:/Users/14823/Desktop/../java_project/web/nio/FileChannelDemo.txt");
        //  normalize()方法可以使路径标准化。标准化意味着它将移除所有在路径
        //字符串的中间的.和..代码,并解析路径字符串所引用的路径。
        System.out.println(path);
        System.out.println(path.normalize());

path: C:\Users\14823\Desktop…\java_project\web\nio\FileChannelDemo.txt
path.normalize():C:\Users\14823\java_project\web\nio\FileChannelDemo.txt

5.4 Files

Modifier and Type Method and Description
static long copy(InputStream in, Path target, CopyOption... options) 将输入流中的所有字节复制到文件。
static long copy(Path source, OutputStream out) 将文件中的所有字节复制到输出流。
static Path copy(Path source, Path target, CopyOption... options) 将文件复制到目标文件。
static Path createDirectories(Path dir, FileAttribute<?>... attrs) 首先创建所有不存在的父目录来创建目录。
static Path createDirectory(Path dir, FileAttribute<?>... attrs) 创建一个新的目录。
static Path createFile(Path path, FileAttribute<?>... attrs) 创建一个新的和空的文件,如果该文件已存在失败。
static Path createLink(Path link, Path existing) 为现有文件创建新的链接(目录条目) (可选操作)
static Path createSymbolicLink(Path link, Path target, FileAttribute<?>... attrs) 创建到目标的符号链接 (可选操作)
static Path createTempDirectory(Path dir, String prefix, FileAttribute<?>... attrs) 在指定的目录中创建一个新目录,使用给定的前缀生成其名称。
static Path createTempDirectory(String prefix, FileAttribute<?>... attrs) 在默认临时文件目录中创建一个新目录,使用给定的前缀生成其名称。
static Path createTempFile(Path dir, String prefix, String suffix, FileAttribute<?>... attrs) 在指定的目录中创建一个新的空文件,使用给定的前缀和后缀字符串生成其名称。
static Path createTempFile(String prefix, String suffix, FileAttribute<?>... attrs) 在默认临时文件目录中创建一个空文件,使用给定的前缀和后缀生成其名称。
static void delete(Path path) 删除文件。
static boolean deleteIfExists(Path path) 删除文件(如果存在)。
static boolean exists(Path path, LinkOption... options) 测试文件是否存在。
static Stream<Path> find(Path start, int maxDepth, BiPredicate<Path,BasicFileAttributes> matcher, FileVisitOption... options) 返回一个 Stream ,它通过搜索基于给定起始文件的文件树中的文件来懒惰地填充 Path
static Object getAttribute(Path path, String attribute, LinkOption... options) 读取文件属性的值。
static <V extends FileAttributeView>V getFileAttributeView(Path path, 类<V> type, LinkOption... options) 返回给定类型的文件属性视图。
static FileStore getFileStore(Path path) 返回表示文件所在文件存储区的 FileStore
static FileTime getLastModifiedTime(Path path, LinkOption... options) 返回文件的上次修改时间。
static UserPrincipal getOwner(Path path, LinkOption... options) 返回文件的所有者。
static Set<PosixFilePermission> getPosixFilePermissions(Path path, LinkOption... options) 返回文件的POSIX文件权限。
static boolean isDirectory(Path path, LinkOption... options) 测试文件是否是目录。
static boolean isExecutable(Path path) 测试文件是否可执行。
static boolean isHidden(Path path) 告知文件是否被 隐藏
static boolean isReadable(Path path) 测试文件是否可读。
static boolean isRegularFile(Path path, LinkOption... options) 测试文件是否是具有不透明内容的常规文件。
static boolean isSameFile(Path path, Path path2) 测试两个路径是否找到相同的文件。
static boolean isSymbolicLink(Path path) 测试文件是否是符号链接。
static boolean isWritable(Path path) 测试文件是否可写。
static Stream<String> lines(Path path)Stream读取文件中的所有行。
static Stream<String> lines(Path path, Charset cs) 从文件中读取所有行作为 Stream
static Stream<Path> list(Path dir) 返回一个懒惰的填充 Stream ,其元素是 Stream中的条目。
static Path move(Path source, Path target, CopyOption... options) 将文件移动或重命名为目标文件。
static BufferedReader newBufferedReader(Path path) 打开一个文件进行阅读,返回一个 BufferedReader以高效的方式从文件读取文本。
static BufferedReader newBufferedReader(Path path, Charset cs) 打开一个文件进行阅读,返回一个 BufferedReader ,可以用来以有效的方式从文件读取文本。
static BufferedWriter newBufferedWriter(Path path, Charset cs, OpenOption... options) 打开或创建一个写入文件,返回一个 BufferedWriter ,可以用来以有效的方式将文本写入文件。
static BufferedWriter newBufferedWriter(Path path, OpenOption... options) 打开或创建一个写入文件,返回一个 BufferedWriter以高效的方式写入文件。
static SeekableByteChannel newByteChannel(Path path, OpenOption... options) 打开或创建文件,返回可访问的字节通道以访问该文件。
static SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) 打开或创建文件,返回可访问的字节通道以访问该文件。
static DirectoryStream<Path> newDirectoryStream(Path dir) 打开一个目录,返回一个DirectoryStream以遍历目录中的所有条目。
static DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) 打开一个目录,返回一个DirectoryStream来迭代目录中的条目。
static DirectoryStream<Path> newDirectoryStream(Path dir, String glob) 打开一个目录,返回一个DirectoryStream来迭代目录中的条目。
static InputStream newInputStream(Path path, OpenOption... options) 打开一个文件,返回输入流以从文件中读取。
static OutputStream newOutputStream(Path path, OpenOption... options) 打开或创建文件,返回可用于向文件写入字节的输出流。
static boolean notExists(Path path, LinkOption... options) 测试此路径所在的文件是否不存在。
static String probeContentType(Path path) 探测文件的内容类型。
static byte[] readAllBytes(Path path) 读取文件中的所有字节。
static List<String> readAllLines(Path path) 从文件中读取所有行。
static List<String> readAllLines(Path path, Charset cs) 从文件中读取所有行。
static <A extends BasicFileAttributes>A readAttributes(Path path, 类<A> type, LinkOption... options) 读取文件的属性作为批量操作。
static Map<String,Object> readAttributes(Path path, String attributes, LinkOption... options) 读取一组文件属性作为批量操作。
static Path readSymbolicLink(Path link) 读取符号链接的目标 (可选操作)
static Path setAttribute(Path path, String attribute, Object value, LinkOption... options) 设置文件属性的值。
static Path setLastModifiedTime(Path path, FileTime time) 更新文件上次修改的时间属性。
static Path setOwner(Path path, UserPrincipal owner) 更新文件所有者。
static Path setPosixFilePermissions(Path path, Set<PosixFilePermission> perms) 设置文件的POSIX权限。
static long size(Path path) 返回文件的大小(以字节为单位)。
static Stream<Path> walk(Path start, FileVisitOption... options) 返回一个 Stream ,它通过 Path根据给定的起始文件的文件树懒惰地填充 Path
static Stream<Path> walk(Path start, int maxDepth, FileVisitOption... options) 返回一个 Stream ,它是通过走根据给定的起始文件的文件树懒惰地填充 Path
static Path walkFileTree(Path start, FileVisitor<? super Path> visitor) 走一个文件树。
static Path walkFileTree(Path start, Set<FileVisitOption> options, int maxDepth, FileVisitor<? super Path> visitor) 走一个文件树。
static Path write(Path path, byte[] bytes, OpenOption... options) 将字节写入文件。
static Path write(Path path, Iterable<? extends CharSequence> lines, Charset cs, OpenOption... options) 将文本行写入文件。
static Path write(Path path, Iterable<? extends CharSequence> lines, OpenOption... options) 将文本行写入文件。

Java NIO Files 类(java.nio.file.Files)提供了几种操作文件系统中的文件的方法。以下内容介绍 Java NIO Files 最常用的一些方法。java.nio.file.Files 类与java.nio.file.Path 实例一起工作,因此在学习 Files 类之前,需要先了解 Path 类。

1.Files.createDirectory()
Files.createDirectory()方法,用于根据 Path 实例创建一个新目录

        Path source = Paths.get("web", "nio/age/work");
        try {
    
    
 // 如果age目录存在则创建work
// 如果age目录不存在,就会在web目录下创建一个age,然后再age目录下创建一个work
            Path path = Files.createDirectories(source);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }

2.Files.copy()
Files.copy() 方法从一个路径拷贝一个文件到另外一个目录

Path source = Paths.get("web", "nio/age/logTwo.txt");
Path target = Paths.get("web", "nio/age/log2.txt");
 // 将log1.txt的内容拷贝到log2.txt中
Files.copy(source,target);

首先,该示例创建两个 Path 实例。然后,这个例子调用 Files.copy(),将两个 Path 实例作为参数传递。这可以让源路径引用的文件被复制到目标路径引用的文件中。 如果目标文件已经存在,则抛出一个 java.nio.file.FileAlreadyExistsException 异常。 如果有其他错误,则会抛出一个 IOException。例如,如果将该文件复制到不存在的 目录,则会抛出 IOException。

覆盖已存在的文件:

Path source = Paths.get("web", "nio/age/logTwo.txt");
Path target = Paths.get("web", "nio/age/log2.txt");
 // 将log1.txt的内容拷贝到log2.txt中
Files.copy(source,target,StandardCopyOption.REPLACE_EXISTING);

Files.copy()方法的第三个参数。如果目标文件已经存在,这个参数指示 copy()方法覆
盖现有的文件。

3.Files.move()
Files.move()用于将文件从一个路径移动到另一个路径。移动文件与重命名相同,但是
移动文件既可以移动到不同的目录,也可以在相同的操作中更改它的名称。

Path source = Paths.get("web", "nio/age/logTwo.txt");
Path target = Paths.get("web", "nio/age/log2.txt");
// 用于根据 Path 实例创建一个新目
//Path directories = Files.createDirectories(source);
// 在相同目录下的文件是重命名,不同目录是移动,第三个参数决定是否覆盖
//Files.move(source, target,StandardCopyOption.REPLACE_EXISTING);

4.Files.delete()
Files.delete()方法可以删除一个文件或者目录。

// 如果该目录存在就删除(该文件不存在不会出现异常)
Files.deleteIfExists(Paths.get("web", "nio/user"));
// 直接删除(如果该文件不存在将会抛出异常:NoSuchFileException)
Files.delete(Paths.get("web", "nio/user"));

5.Files.walkFileTree()

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

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

  • FileVisitor 接口的方法中,每个都返回一个 FileVisitResult 枚举实例。 FileVisitResult 枚举包含以下四个选项:

  1. CONTINUE : 继续

  2. TERMINATE: 终止

  3. SKIP_SIBLING : 跳过同级

  4. SKIP_SUBTREE: 跳过子级

示例: 在D:\IDE2019\code这个目录下查找一个pom.xml文件

public class FilesDemo {
    
    
    public static void main(String[] args) throws Exception {
    
    
        // 查找某个目录下是否有该文件,如果找到,则返回该文件的绝对路径
        String fileToFind = "pom.xml";
        Path resourceDir = Paths.get("D:/IDE2019", "code");
        findFile(fileToFind,resourceDir);

    }

    public static void findFile(String fileToFind,Path resourceDir)  {
    
    

        try {
    
    
            Path path = Files.walkFileTree(resourceDir, new SimpleFileVisitor<Path>() {
    
    
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
    
    
                    String fileString = file.toAbsolutePath().toString();
                    System.out.println("pathString = " + 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();
        }

    }

}

使用FileSystems快速读取文本文件:

       // 使用FileSystems快速读取文本
        Path paths = FileSystems.getDefault().getPath("web/nio", "FileChannelDemo.txt");
        BufferedReader reader = Files.newBufferedReader(paths, StandardCharsets.UTF_8);
        String readLine =null;
        while ((readLine=reader.readLine())!=null){
    
    
            System.out.println(readLine);
        }

5.5 AsynchronousFileChannel

Modifier and Type Method and Description
abstract void force(boolean metaData) 强制将此通道文件的任何更新写入包含该通道的存储设备。
Future<FileLock> lock() 获取此通道文件的排他锁。
<A> void lock(A attachment, CompletionHandler<FileLock,? super A> handler) 获取此通道文件的排他锁。
abstract Future<FileLock> lock(long position, long size, boolean shared) 获取此通道文件的给定区域的锁定。
abstract <A> void lock(long position, long size, boolean shared, A attachment, CompletionHandler<FileLock,? super A> handler) 获取此通道文件的给定区域的锁定。
static AsynchronousFileChannel open(Path file, OpenOption... options) 打开或创建用于读取和/或写入的文件,返回异步文件通道以访问该文件。
static AsynchronousFileChannel open(Path file, Set<? extends OpenOption> options, ExecutorService executor, FileAttribute<?>... attrs) 打开或创建用于读取和/或写入的文件,返回异步文件通道以访问该文件。
abstract Future<Integer> read(ByteBuffer dst, long position) 从给定的文件位置开始,从该通道读取一个字节序列到给定的缓冲区。
abstract <A> void read(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer,? super A> handler) 从给定的文件位置开始,从该通道读取一个字节序列到给定的缓冲区。
abstract long size() 返回此通道文件的当前大小。
abstract AsynchronousFileChannel truncate(long size) 将此频道的文件截断为给定大小。
FileLock tryLock() 尝试获取此频道文件的排他锁。
abstract FileLock tryLock(long position, long size, boolean shared) 尝试获取此通道文件的给定区域的锁定。
abstract Future<Integer> write(ByteBuffer src, long position) 从给定的缓冲区向给定的文件位置开始,向该通道写入一个字节序列。
abstract <A> void write(ByteBuffer src, long position, A attachment, CompletionHandler<Integer,? super A> handler) 从给定的缓冲区向给定的文件位置开始,向该通道写入一个字节序列。

1.创建AsynchronousFileChannel
在 Java 7 中,Java NIO 中添加了 AsynchronousFileChannel,也就是是异步地将数
据写入文件。

Path path = Paths.get("web/nio","FileChannelDemo.txt");
AsynchronousFileChannel af = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
  • open()方法的第一个参数指向与 AsynchronousFileChannel 相关联文件的 Path 实
    例。

  • 第二个参数是一个或多个打开选项,它告诉 AsynchronousFileChannel 在文件上执行什么操作。在本例中,我们使用了 StandardOpenOption.READ 选项,表示该文件将被打开阅读。

2.通过 Future 读取数据

可以通过两种方式从 AsynchronousFileChannel 读取数据。第一种方式是调用返回 Future 的 read()方法

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-04-21:56
 * @Version:1.0
 * @Description: 通过 Future 读数据
 */
public class FutureReadDemo1 {
    
    
    public static void main(String[] args) throws Exception{
    
    
        Path path = Paths.get("web/nio", "FileChannelDemo.txt");
        AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);


        ByteBuffer buffer = ByteBuffer.allocate(1024*10);
        long position = 0;

        Future<Integer> future = fileChannel.read(buffer, position);
        while(!future.isDone());

        buffer.flip();
        byte[] data = new byte[buffer.limit()];
        buffer.get(data);
        System.out.println(new String(data));


    }
}

上述代码:

(1)创建了一个 AsynchronousFileChannel,

(2)创建一个 ByteBuffer,它被传递给 read()方法作为参数,以及一个 0 的位置。

(3)在调用 read()之后,循环,直到返回的 isDone()方法返回 true。

(4)读取操作完成后,数据读取到 ByteBuffer 中,然后打印到 System.out 中。

3.通过 CompletionHandler 读取数据

第二种方法是调用 read()方法,该方法将一个 CompletionHandler 作为参数 示例:

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;


/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-04-22:41
 * @Version:1.0
 * @Description: 通过 CompletionHandler 读数据(no)
 */
public class FutureReadDemo2 {
    
    
    public static void main(String[] args) throws Exception {
    
    
        Path path = Paths.get("web/nio", "FileChannelDemo.txt");
        AsynchronousFileChannel af = AsynchronousFileChannel.open(path, StandardOpenOption.READ);

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        long position=0;

        af.read(buffer, position, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    
    
            @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();
                System.out.println("hello");
            }

            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {
    
    

            }
        });
    }
}

(1)读取操作完成,将调用 CompletionHandler 的 completed()方法。

(2)对于 completed()方法的参数传递一个整数,它告诉我们读取了多少字节,以及 传递给 read()方法的“附件”。“附件”是 read()方法的第三个参数。在本代码中, 它是 ByteBuffer,数据也被读取。

(3)如果读取操作失败,则将调用 CompletionHandler 的 failed()方法。

4.通过 Future 写数据

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


import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-04-22:08
 * @Version:1.0
 * @Description: 通过 Future 写数据
 */
public class FutureWriteDemo1 {
    
    
    public static void main(String[] args) throws Exception{
    
    
        Path path = Paths.get("web/nio", "FileChannelDemo.txt");
        AsynchronousFileChannel af = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);

        ByteBuffer buffer = ByteBuffer.wrap("  hello world1".getBytes());
        Future<Integer> future = af.write(buffer, 0);
        buffer.clear();
        while (future.isDone());
        System.out.println("write finish");
        af.close();

    }
}

5.通过 CompletionHandler 写数据

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-04-23:22
 * @Version:1.0
 * @Description: 通过 CompletionHandler 写数据
 */
public class FutureWriteDemo2 {
    
    
    public static void main(String[] args) throws IOException {
    
    
        Path path = Paths.get("web/nio", "FileChannelDemo.txt");
        Files.createFile(path);
        AsynchronousFileChannel af = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);

        AsynchronousFileChannel  fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        long position = 0;
        buffer.put("hello world".getBytes());
        buffer.flip();
        fileChannel.write(buffer, position, buffer, new CompletionHandler<Integer,
                        ByteBuffer>() {
    
    
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
    
    
                System.out.println("bytes written: " + result);
            }
            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {
    
    
                System.out.println("Write failed");
                exc.printStackTrace();
            }
        });

    }
}

当写操作完成时,将会调用 CompletionHandler 的 completed()方法。如果写失 败,则会调用 failed()方法。

5.6 Charset

Modifier and Type Method and Description
Set<String> aliases() 返回一个包含此字符集的别名的集合。
static SortedMap<String,Charset> availableCharsets() 构造从规范字符集名称到字符集对象的排序映射。
boolean canEncode() 告诉这个字符集是否支持编码。
int compareTo(Charset that) 将此字符串与另一个字符集进行比较。
abstract boolean contains(Charset cs) 告知这个字符集是否包含给定的字符集。
CharBuffer decode(ByteBuffer bb) 便利方法,将此字符集中的字节解码为Unicode字符。
static Charset defaultCharset() 返回此Java虚拟机的默认字符集。
String displayName() 返回此字符集的默认语言环境的可读名称。
String displayName(Locale locale) 返回此字符集的给定语言环境的人类可读名称。
ByteBuffer encode(CharBuffer cb) 在这个字符集中将Unicode字符编码为字节的便捷方法。
ByteBuffer encode(String str) 在此字符集中将字符串编码为字节的便捷方法。
boolean equals(Object ob) 告诉这个对象是否等于另一个。
static Charset forName(String charsetName) 返回名为charset的charset对象。
int hashCode() 计算此字符集的哈希码。
boolean isRegistered() 告诉这个字符集是否在IANA Charset Registry中 注册
static boolean isSupported(String charsetName) 告诉是否支持命名的字符集。
String name() 返回此字符集的规范名称。
abstract CharsetDecoder newDecoder() 为此字符集构造一个新的解码器。
abstract CharsetEncoder newEncoder() 为此字符集构造一个新的编码器。
String toString() 返回描述此字符集的字符串。

java 中使用 Charset 来表示字符集编码对象

import org.junit.Test;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.util.Map;
import java.util.Set;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-04-23:32
 * @Version:1.0
 * @Description:
 */
public class CharsetDemo {
    
    
    public static void main(String[] args)  throws Exception{
    
    
        Charset charset=Charset.forName("UTF-8");
        // 获取编码器
        CharsetEncoder newEncoder = charset.newEncoder();
        // 获取解码器
        CharsetDecoder newDecoder = charset.newDecoder();
        // 获取需要解码的数据
        CharBuffer charBuffer = CharBuffer.allocate(1024);
        charBuffer.put("中文字符");
        charBuffer.flip();
        // 对数据进行编码
        ByteBuffer buffer = newEncoder.encode(charBuffer);
        System.out.println("编码后:");
        for (int i=0;i<buffer.limit();i++) {
    
    
            System.out.print(buffer.get()+",");
        }

        // 对数进行解码
        buffer.flip();
        CharBuffer cb=newDecoder.decode(buffer);
        System.out.print("\n解码后:");
        System.out.println(cb.toString());
        // 指定其他的解码方式
        System.out.println("指定其他格式解码:");
        Charset charset1=Charset.forName("gb2312");
        buffer.flip();
        CharBuffer charBuffer2 =charset1.decode(buffer);
        System.out.println(charBuffer2.toString());

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

}

6.Java NIO 综合案例

在这里插入图片描述

6.1 服务器端(ChatServer)

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-05-0:04
 * @Version:1.0
 * @Description: 聊天系统-服务器端
 */

public class ChatServer {
    
    

    //启动主方法
    public static void main(String[] args)  throws Exception{
    
    
        new ChatServer().startServer();
    }

    //服务器端启动的方法
    public void startServer() throws IOException {
    
    
        //1 创建 Selector 选择器
        Selector selector = Selector.open();
        //2 创建 ServerSocketChannel 通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //3 为 channel 通道绑定监听端口
        serverSocketChannel.bind(new InetSocketAddress(8000));
        //设置非阻塞模式
        serverSocketChannel.configureBlocking(false);
        //4 把 channel 通道注册到 selector 选择器上
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务器已经启动成功了");
        //5 循环,等待有新链接接入
        //while(true)
        for(;;) {
    
    
            //获取 channel 数量
            int readChannels = selector.select();
            if(readChannels == 0) {
    
    
                continue;
            }
            //获取可用的 channel
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            //遍历集合
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
    
    
                SelectionKey selectionKey = iterator.next();
                //移除 set 集合当前 selectionKey
                iterator.remove();
                //6 根据就绪状态,调用对应方法实现具体业务操作
                //6.1 如果 accept 状态
                if(selectionKey.isAcceptable()) {
    
    
                    acceptOperator(serverSocketChannel,selector);
                }
                //6.2 如果可读状态
                if(selectionKey.isReadable()) {
    
    
                    readOperator(selector,selectionKey);
                }
            }
        }
    }
    //处理可读状态操作
    private void readOperator(Selector selector, SelectionKey selectionKey) throws IOException {
    
    
        //1 从 SelectionKey 获取到已经就绪的通道
        SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
        //2 创建 buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //3 循环读取客户端消息
        int readLength = socketChannel.read(byteBuffer);
        String message = "";
        if(readLength >0) {
    
    
            //切换读模式
            byteBuffer.flip();
            //读取内容
            message += Charset.forName("UTF-8").decode(byteBuffer);
        }
        //4 将 channel 再次注册到选择器上,监听可读状态
        socketChannel.register(selector,SelectionKey.OP_READ);
        //5 把客户端发送消息,广播到其他客户端
        if(message.length()>0) {
    
    
            //广播给其他客户端
            System.out.println(message);
            castOtherClient(message,selector,socketChannel);
        }
    }
    //广播到其他客户端
    private void castOtherClient(String message, Selector selector, SocketChannel socketChannel) throws IOException {
    
    
        //1 获取所有已经接入 channel
        Set<SelectionKey> selectionKeySet = selector.keys();
        //2 循环向所有 channel 广播消息
        for(SelectionKey selectionKey : selectionKeySet) {
    
    
            //获取每个 channel
            Channel tarChannel = selectionKey.channel();
            //不需要给自己发送
            if(tarChannel instanceof SocketChannel && tarChannel != socketChannel) {
    
    
                ((SocketChannel)tarChannel).write(Charset.forName("UTF-8").encode(message));
            }
        }
    }
    //处理接入状态操作
    private void acceptOperator(ServerSocketChannel serverSocketChannel, Selector selector) throws IOException {
    
    
        //1 接入状态,创建 socketChannel
        SocketChannel socketChannel = serverSocketChannel.accept();
        //2 把 socketChannel 设置非阻塞模式
        socketChannel.configureBlocking(false);
        //3 把 channel 注册到 selector 选择器上,监听可读状态
        socketChannel.register(selector,SelectionKey.OP_READ);
        //4 客户端回复信息
        socketChannel.write(Charset.forName("UTF-8").encode("欢迎进入聊天室,请注意隐私安全"));
    }

}

6.2 客户端(ChatClient)

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Scanner;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-05-0:06
 * @Version:1.0
 * @Description: 聊天系统-客户端
 */
public class ChatClient {
    
    
    //启动客户端方法
    public void startClient(String name) throws IOException {
    
    
        //连接服务端
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8000));
        //接收服务端响应数据
        Selector selector = Selector.open();
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
        //创建线程
        new Thread(new ClientThread(selector)).start();
        //向服务器端发送消息
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNextLine()) {
    
    
            String msg = scanner.nextLine();
            if(msg.length()>0) {
    
    
                socketChannel.write(Charset.forName("UTF-8").encode(name +" : " +msg));
            }
        }
    }
}

客户端线程辅助类:

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-05-0:07
 * @Version:1.0
 * @Description: 客户端线程辅助类
 */
public class ClientThread implements Runnable {
    
    
    private Selector selector;
    public ClientThread(Selector selector) {
    
    
        this.selector = selector;
    }
    @Override
    public void run() {
    
    
        try {
    
    
            for(;;) {
    
    
                //获取 channel 数量
                int readChannels = selector.select();
                if(readChannels == 0) {
    
    
                    continue;
                }
                //获取可用的 channel
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                //遍历集合
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
    
    
                    SelectionKey selectionKey = iterator.next();
                    //移除 set 集合当前 selectionKey
                    iterator.remove();
                    //如果可读状态
                    if(selectionKey.isReadable()) {
    
    
                        readOperator(selector,selectionKey);
                    }
                }
            }
        }catch(Exception e) {
    
    
        }
    }
    //处理可读状态操作
    private void readOperator(Selector selector, SelectionKey selectionKey) throws IOException {
    
    
        //1 从 SelectionKey 获取到已经就绪的通道
        SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
        //2 创建 buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //3 循环读取客户端消息
        int readLength = socketChannel.read(byteBuffer);
        String message = "";
        if(readLength >0) {
    
    
            //切换读模式
            byteBuffer.flip();
            //读取内容
            message += Charset.forName("UTF-8").decode(byteBuffer);
        }
        //4 将 channel 再次注册到选择器上,监听可读状态
        socketChannel.register(selector,SelectionKey.OP_READ);
        //5 把客户端发送消息,广播到其他客户端
        if(message.length()>0) {
    
    
            //广播给其他客户端
            System.out.println(message);
        }
    }
}

6.3 启动类:

客户端启动类:tom

import java.io.IOException;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-05-10:19
 * @Version:1.0
 * @Description: 客户端启动类:tom
 */
public class ChatA {
    
    
    public static void main(String[] args) {
    
    
        try {
    
    
            new ChatClient().startClient("tom");
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }
}

户端启动类:jack

import java.io.IOException;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: compass
 * @Date: 2021-10-05-10:21
 * @Version:1.0
 * @Description: 客户端启动类:jack
 */
public class ChatB {
    
    
    public static void main(String[] args) {
    
    
        try {
    
    
            new ChatClient().startClient("jack");
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }
}

猜你喜欢

转载自blog.csdn.net/m0_46188681/article/details/120671311