二、Java NIO Channel


一、Channel 概述

  • Channel 是一个通道,可以通过它 读取 和 写入 数据。
    就像水管一样,网络数据通过 Channel 读取 和 写入。
  1. 通道 与 流 的不同之处在于 通道是双向的
    流只是在一个方向上移动(一个流必须是 InputStreamOutputStream 的子类)。
  2. 通道 可以用于 读、写 或者 同时用于 读写。
    因为 Channel 是全双工的,所以它可以比 流 更好地映射底层操作系统的 API。

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

二、Channel 接口源码

public interface Channel extends Closeable {

    /**
     * 该通道是否打开。
     */
    public boolean isOpen();

    /**
     * 关闭此通道。
     */
    public void close() throws IOException;

}
  • 与缓冲区不同,通道 API 主要由接口指定。
  1. 不同的操作系统上,通道实现(Channel Implementation)会有根本性的差异,所以通道 API 仅仅描述了可以做什么。
  2. 因此通道的实现,使用操作系统的本地代码。
  3. 通道接口,允许您以一种受控且可移植的方式,来访问底层的 IO服务。

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

  • Java NIO 的通道类似流,但又有些不同。
  1. 可以从 通道 中读取数据,又可以写数据到 通道。
    但流的 读写 通常是单向的。
  2. 通道可以异步地读写。
  3. 通道中的数据总是要先读到一个 Buffer,或者总是要从一个 Buffer 中写入。

  • 从通道 读取 数据到 缓冲区,从 缓冲区 写入数据到通道,如下图所示。
    在这里插入图片描述

三、Channel 接口实现


1. FileChannel 文件 IO

  • 从文件中读写数据。

2. DatagramChannel UDP

  • 能通过 UDP 读写网络中的数据。

3. SocketChannel TCP

  • 能通过 TCP 读写网络中的数据。

4. ServerSocketChannel 网络 IO

  • 可以监听新进来的 TCP 连接,像 Web 服务器那样。
  • 对每一个新进来的连接都会创建一个 SocketChannel。

四、FileChannel


1. FileChannel 介绍

  • FileChannel 可以实现常用的 Read、Write 以及 Scatter/Gather 操作。
  • 同时也提供了很多专用于文件的新方法,这些方法都是我们所熟悉的文件操作。
    在这里插入图片描述

2. 从 FileChannel 读取数据

/**
 * @author: wy
 * describe: 从 FileChannel 读取数据
 * 1. 读取数据到缓冲区(Buffer)
 * 2. 反转读写模式
 * 3. 从缓冲区中读取数据
 * 4. 调用 buffer.clear() 或 buffer.compact() 清除缓冲区内容
 */
public class FileChannel1 {

    public static void main(String[] args) throws IOException {
        /*
        一、打开 FileChannel (通过 RandomAccessFile)
            name: 文件路径
            mode:`rw`读写模式
         */
        RandomAccessFile accessFile = new RandomAccessFile("E:\\TEMP\\nio\\FileChannel1.txt", "rw");
        FileChannel channel = accessFile.getChannel();

        // 二、从 FileChannel 读取数据到缓冲区(Buffer)
        ByteBuffer buffer = ByteBuffer.allocate(6);
        int bytesRead = channel.read(buffer);

        // 三、获取缓冲区中数据
        while (bytesRead != -1) {
            System.out.printf("当前读取到缓冲区的字节数: %s,分别是: ", bytesRead).println();

            // 1. 反转读写模式
            buffer.flip();

            while (buffer.hasRemaining()) {
                // 2. 从缓冲区中获取数据
                byte b = buffer.get();
                System.out.printf("%s=%s", b, (char) b).println();
            }

            // 3. 调用 buffer.clear() 或 buffer.compact() 清除缓冲区中数据
            buffer.clear();

            // 4. 如果返回 -1,表示读到了文件末尾
            bytesRead = channel.read(buffer);
        }

        // 四、 关闭 Channel
        channel.close();
        accessFile.close();
        System.out.println("FileChannel1 结束");
    }
}
2.1 打开 FileChannel
  • 在使用 FileChannel 之前,必须先打开它。
  1. 无法直接打开一个 FileChannel。
  2. 需要使用一个 InputStreamOutputStreamRandomAccessFile 来获取一个 FileChannel 实例。
  • 下面通过 RandomAccessFile 打开 FileChannel 的示例。
/*
一、打开 FileChannel (通过 RandomAccessFile)
    name: 文件路径
    mode:rw读写模式
 */
RandomAccessFile accessFile = new RandomAccessFile("E:\\TEMP\\nio\\FileChannel1.txt", "rw");
FileChannel channel = accessFile.getChannel();
2.2 写入数据到缓冲区
  • 先分配一个 Buffer。
  1. 从 FileChannel 中读取的数据将被写到 Buffer 中。
  2. 调用 FileChannel.read() 方法,将数据从 FileChannel 读取到 Buffer 中。
  3. read() 方法返回的 int 值,表示有多少字节被读到了 Buffer 中。
  4. 如果返回 -1,表示读到了文件末尾。
// 二、从 FileChannel 读取数据到缓冲区(Buffer)
ByteBuffer buffer = ByteBuffer.allocate(6);
int bytesRead = channel.read(buffer);

3. 向 FileChannel 写入数据

/**
 * @author: wy
 * describe: 向 FileChannel 写入数据
 */
public class FileChannel2 {

    public static void main(String[] args) throws IOException {
        /*
        一、打开 FileChannel (通过 RandomAccessFile)
            name: 文件路径
            mode:`rw`读写模式
         */
        RandomAccessFile accessFile = new RandomAccessFile("E:\\TEMP\\nio\\FileChannel1.txt", "rw");
        FileChannel channel = accessFile.getChannel();

        // 二、添加数据到缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.clear();
        // 1. 加入缓冲区
        buffer.put("qs".getBytes());
        // 2. 反转读写模式
        buffer.flip();

        // 三、从缓冲区写入文件
        while (buffer.hasRemaining()) {
            // 1. 将 Buffer 中的数据写入到 Channel
            channel.write(buffer);
        }
        // 2. 强制刷新到磁盘
        channel.force(true);

        // 四、关闭 Channel
        channel.close();
        accessFile.close();
        System.out.println("FileChannel2 结束");
    }
}
  • 注意 FileChannel.write() 是在 while 循环中调用的。
  1. 因为无法保证 write() 方法,一次能向 FileChannel 写入多少字节。
  2. 因此需要重复调用 write() 方法,直到 Buffer 中已经没有尚未写入通道的字节。
3.1 关闭 FileChannel
// 四、关闭 Channel
channel.close();

4. position() 方法

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

5. size() 方法

返回所关联文件的大小。

// 文件大小
long size = channel.size();
System.out.printf("size: %s", size).println();
// size: 12

6. truncate() 方法

  • 截取一个文件。
  1. 截取文件时,文件将指定长度后面的部分删除。
// 截取文件的前 2 个字节。
FileChannel truncate = channel.truncate(2);
System.out.printf("truncate.size: %s", truncate.size()).println();
// truncate.size: 2

7. force() 方法

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

8. transferTo()transferFrom() 方法

  • 通道之间的数据传输。
  1. 如果两个通道中有一个是 FileChannel,可以直接将数据从一个 Channel 传输到另外一个 Channel。
8.1 transferTo() 方法

将数据从 FileChannel 传输到其他的 Channel 中。

/**
 * 1. transferFrom() 将数据从 FileChannel 传输到其他的 Channel 中。
 */
@Test
public void testTransferTo() throws IOException {
    // 一、打开两个 FileChannel
    RandomAccessFile fromAccessFile = new RandomAccessFile("E:\\TEMP\\nio\\FileChannel31.txt", "rw");
    RandomAccessFile toAccessFile = new RandomAccessFile("E:\\TEMP\\nio\\FileChannel32.txt", "rw");

    FileChannel fromChannel = fromAccessFile.getChannel();
    FileChannel toChannel = toAccessFile.getChannel();

    // 二、从 fromChannel 传输到 toChannel
    // 开始位置 0
    long position = 0;
    // fromChannel 文件大小
    long size = fromChannel.size();
    System.out.printf("size: %s", size).println();
    fromChannel.transferTo(position, size, toChannel);

    // 三、关闭
    fromAccessFile.close();
    toAccessFile.close();
    System.out.println("testTransferTo over");
}
8.2 transferFrom() 方法
  • 将数据从源通道传输到 FileChannel 中。
  1. 在 JDK 文档中的解释为:将字节从给定的可读取字节通道,传输到此通道的文件中。
/**
 * 2. transferFrom() 将数据从源通道传输到 FileChannel 中。
 */
@Test
public void testTransferFrom() throws IOException {
    // 打开两个 FileChannel
    RandomAccessFile fromAccessFile = new RandomAccessFile("E:\\TEMP\\nio\\FileChannel31.txt", "rw");
    RandomAccessFile toAccessFile = new RandomAccessFile("E:\\TEMP\\nio\\FileChannel32.txt", "rw");

    FileChannel fromChannel = fromAccessFile.getChannel();
    FileChannel toChannel = toAccessFile.getChannel();

    // 二、从 fromChannel 传输到 toChannel
    // fromChannel 传输到 toChannel
    // 开始位置 0
    long position = 0;
    // fromChannel 文件大小
    long size = fromChannel.size();
    System.out.printf("size: %s", size).println();
    toChannel.transferFrom(fromChannel, position, size);

    // 三、关闭
    fromAccessFile.close();
    toAccessFile.close();
    System.out.println("testTransferFrom over");
}
  1. position 参数:表示从 position 处开始向目标文件写入数据。
  2. count 参数:表示最多传输的字节数。
  1. 如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数。
  2. 在 SoketChannel 的实现中,SocketChannel 只会传输此刻准备好的数据(可能不足 count 字节)。因此,SocketChannel 可能不会将请求的所有数据(count 个字节)全部传输到 FileChannel 中。

五、Socket 通道

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

1. AbstractSelectableChannel.configureBlocking() 方法

/**
 * 设置一个通道的阻塞模式
 */
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,通常被认为是服务端使用的。
  1. 使同时管理很多 Socket 通道变得更容易。
  • 在客户端使用一个或几个非阻塞模式的 Socket 通道也是有益处的。
  1. 如:借助非阻塞 Socket 通道,GUI 程序可以专注于用户请求,并且同时维护与一个或多个服务器的会话。
  2. 在很多程序上,非阻塞模式都是有用的。
  • blockingLock() 方法,防止 Socket 通道的阻塞模式被更改。
  1. 该方法会返回一个非透明的对象引用。
  2. 返回的对象是通道实现,修改阻塞模式时内部使用的。
  3. 只有拥有此对象的锁的线程,才能更改通道的阻塞模式。

2. ServerSocketChannel 介绍

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

3. ServerSocketChannel 示例

/**
 * @author: wy
 * describe: ServerSocketChannel 示例
 */
public class ServerSocketChannel1 {

    /**
     * http://127.0.0.1:8888/
     */
    public static void main(String[] args) throws IOException, InterruptedException {
        // 默认端口号
        int port = 8888;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        }
        // 数据加入缓冲区
        ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes());

        // 一、打开 ServerSocketChannel
        ServerSocketChannel channel = ServerSocketChannel.open();
        // 1. 绑定端口
        channel.socket().bind(new InetSocketAddress(port));

        // 二、设置阻塞模式(false 非阻塞)
        channel.configureBlocking(false);

        // 三、监听有新连接
        int count = 0;
        while (true) {
            System.out.println("accept()监听新的连接...");
            // 1. 非阻塞时,accept()方法不会阻塞
            SocketChannel socketChannel = channel.accept();
            if (socketChannel == null) {
                // 2. 没有新的连接,返回 null
                System.out.println("没有新的连接");
                TimeUnit.SECONDS.sleep(2);
            } else {
                // 3. 新的连接来自: /127.0.0.1:53917
                System.out.printf("新的连接来自: %s", socketChannel.socket().getRemoteSocketAddress()).println();
                // 指针0
                buffer.rewind();
                socketChannel.write(buffer);
                socketChannel.close();
            }
            count++;
            if (count == 20) {
                break;
            }
        }

        // 四、关闭 ServerSocketChannel
        channel.close();
    }
}
3.1 打开 ServerSocketChannel
// 一、打开 ServerSocketChannel
ServerSocketChannel channel = ServerSocketChannel.open();
// 1. 绑定端口
channel.socket().bind(new InetSocketAddress(port));
3.2 关闭 ServerSocketChannel
// 四、关闭 ServerSocketChannel
channel.close();
3.3 监听新的连接
  • ServerSocketChannel.accept() 方法监听新进的连接。
  1. 当 accept() 方法返回的时候,它返回一个包含新进来的连接的 SocketChannel。
  2. 因此 accept() 方法会一直阻塞到有新连接到达。
  • 通常不会只监听一个连接,在 while 循环中调用 accept() 方法。
while (true) {
    System.out.println("accept()监听新的连接...");
    // 1. 非阻塞时,accept()方法不会阻塞
    SocketChannel socketChannel = channel.accept();
}
3.4 阻塞模式

会在 serverSocketChannel.accept(); 阻塞进程。

// 二、设置阻塞模式(false 非阻塞)
channel.configureBlocking(true);
3.5 非阻塞模式
  • 在非阻塞模式下,accept() 方法会立刻返回。
  1. 如果还没有新进来的连接,返回的将是 null
  2. 因此,需要检查返回的 SocketChannel 是否是 null。
// 二、设置阻塞模式(false 非阻塞)
channel.configureBlocking(false);

4. SocketChannel 介绍

  • Java NIO 中的 SocketChannel 是一个连接到 TCP 网络套接字的通道。
  1. SocketChannel 是用来连接 Socket 套接字。
  2. SocketChannel 主要用途用来处理网络 I/O 的通道。
  3. SocketChannel 是基于 TCP 连接传输。
  4. SocketChannel 实现了可选择通道,可以被多路复用的。

5. SocketChannel 特征

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

6. SocketChannel 示例

/**
 * @author: wy
 * describe: SocketChannel 示例
 */
public class SocketChannel1 {

    public static void main(String[] args) throws IOException {
        // 一、创建 SocketChannel
        SocketChannel socketChannel = SocketChannel
                .open(new InetSocketAddress("www.baidu.com", 80));

        // 二、设置阻塞模式(false非阻塞)
        socketChannel.configureBlocking(true);

        // 三、读操作
        ByteBuffer buffer = ByteBuffer.allocate(16);
        socketChannel.read(buffer);
        socketChannel.close();
        /*
        1. true: 为阻塞模式,当执行到 read() 线程将阻塞,控制台将无法打印 read over。
        2. false: 为非阻塞模式,控制台将打印 read over。
        3. 读写都是面向缓冲区,这个读写方式与 FileChannel 相同。
         */
        System.out.println("read over");
    }
}
6.1 创建 SocketChannel
// 一、创建 SocketChannel
SocketChannel socketChannel = SocketChannel
        .open(new InetSocketAddress("www.baidu.com", 80));

// 1. 方式二、无参 open() 只是创建了一个 SocketChannel 对象,并没有进行实质的 tcp 连接
SocketChannel socketChannel2 = SocketChannel.open();
socketChannel2.connect(new InetSocketAddress("www.baidu.com", 80));
6.2 连接校验
// 2. 测试 SocketChannel 是否为 open 状态
boolean open = socketChannel.isOpen();
System.out.printf("open: %s", open).println();
// open: true
// 3. 测试 SocketChannel 是否已经被连接
boolean connected = socketChannel.isConnected();
System.out.printf("connected: %s", connected).println();
// connected: true
// 4. 测试 SocketChannel 是否正在进行连接
boolean connectionPending = socketChannel.isConnectionPending();
System.out.printf("connectionPending: %s", connectionPending).println();
// connectionPending: false
// 5. 校验正在进行套接字连接的 SocketChannel 是否已经完成连接
boolean finishConnect = socketChannel.finishConnect();
System.out.printf("finishConnect: %s", finishConnect).println();
// finishConnect: true
6.3 阻塞模式

支持 阻塞 和 非阻塞 两种模式。

// 二、设置阻塞模式(false非阻塞)
socketChannel.configureBlocking(true);
6.4 读操作
// 三、读操作
ByteBuffer buffer = ByteBuffer.allocate(16);
socketChannel.read(buffer);
socketChannel.close();
/*
1. true: 为阻塞模式,当执行到 read() 线程将阻塞,控制台将无法打印 read over。
2. false: 为非阻塞模式,控制台将打印 read over。
3. 读写都是面向缓冲区,这个读写方式与 FileChannel 相同。
 */
System.out.println("read over");
System.out.println(buffer);
6.5 设置和获取参数
  • setOption() 方法,设置 Socket 套接字的相关参数。
  • getOption() 方法,获取相关参数的值。
  1. 如默认的接收缓冲区大小是 8192 byte,SocketChannel 还支持多路复用。
// 1. setOptions 设置参数
socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, Boolean.TRUE)
        .setOption(StandardSocketOptions.TCP_NODELAY, Boolean.TRUE);

// 2. getOption 获取参数
Boolean option = socketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE);
System.out.println(option);// true
Integer option2 = socketChannel.getOption(StandardSocketOptions.SO_RCVBUF);
System.out.println(option2);// 65536

7. DatagramChannel 介绍

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

8. DatagramChannel 示例

8.1 打开 DatagramChannel
  • 打开 9999 端口接收 UDP 数据包。
// 一、打开 DatagramChannel
DatagramChannel channel = DatagramChannel.open();
// 1. 绑定端口
channel.bind(new InetSocketAddress(9999));
// 2. 连接
channel.connect(new InetSocketAddress("127.0.0.1", 9999));
8.2 接收数据
  • receive() 接收 UDP 包。
/**
 * 接收包
 */
@Test
public void receiveDatagram() throws IOException {
    // 一、打开 DatagramChannel
    DatagramChannel channel = DatagramChannel.open();
    // 1. 绑定端口
    channel.bind(new InetSocketAddress(9999));

    // 2. 创建缓冲区
    ByteBuffer buffer = ByteBuffer.allocate(1024);

    System.out.println("等待接收: ");
    while (true) {
        buffer.clear();
        // 3. 接收数据
        SocketAddress socketAddress = channel.receive(buffer);
        buffer.flip();

        // socketAddress 可以获得发包的Ip、端口等信息
        System.out.printf("socketAddress: %s, ", socketAddress.toString());
        System.out.printf("buffer: %s", StandardCharsets.UTF_8.decode(buffer)).println();
        // socketAddress: /127.0.0.1:57443, buffer: qs: 1637043210121
    }
}
8.3 发送数据
  • send() 发送 UDP 包。
/**
 * 发送包
 */
@Test
public void sendDatagram() throws IOException, InterruptedException {
    // 一、打开 DatagramChannel
    DatagramChannel channel = DatagramChannel.open();
    InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 9999);

    while (true) {
        String msg = String.format("qs: %d", new Date().getTime());
        ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes(StandardCharsets.UTF_8));
        // 1. 发送数据
        channel.send(buffer, socketAddress);

        System.out.printf("发送: %s", msg).println();
        // 发送: qs: 1641566408995
        TimeUnit.SECONDS.sleep(1);
    }
}
8.4 连接
  • UDP 不存在真正意义上的连接。
  1. 这里的连接是向特定服务地址,用 read() 接收 和 write() 发送数据包。
/**
 * 连接 read() 接收包 和 write() 发送包
 * 1. read() 和 write() 只有在 connect() 后才能使用,不然会抛 NotYetConnectedException 异常。
 * 2. 用 read() 接收时,如果没有接收到包,会抛 PortUnreachableException 异常。
 */
@Test
public void testConnect() throws IOException {
    // 一、打开 DatagramChannel
    DatagramChannel channel = DatagramChannel.open();
    // 1. 绑定端口
    channel.bind(new InetSocketAddress(9999));
    // 2. 连接
    channel.connect(new InetSocketAddress("127.0.0.1", 9999));

    // write
    channel.write(ByteBuffer.wrap("qs".getBytes(StandardCharsets.UTF_8)));
    System.out.printf("发送: %s", "qs").println();

    // read
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    while (true) {
        buffer.clear();
        int read = channel.read(buffer);
        buffer.flip();
        System.out.printf("接收: %s, %d", StandardCharsets.UTF_8.decode(buffer), read).println();
    }
}

六、Scatter/Gather

  • Java NIO 开始支持 Scatter/Gather。
  1. Scatter/Gather 用于描述从 Channel 中 读取 或 写入 到 Channel 的操作。
  • 分散(Scatter)。
  1. 从 Channel 中读取是指,在读操作时将读取的数据写入多个 Buffer 中。
  2. 因此,Channel 将从 Channel 中读取的数据 分散(Scatter)到多个 Buffer 中。
  • 聚集(Gather)
  1. 写入 Channel 是指,在写操作时将多个 Buffer 的数据写入同一个 Channel。
  2. 因此,Channel 将多个 Buffer 中的数据 聚集(Gather)后发送到 Channel。
  • Scatter/Gather 经常用于,需要将传输的数据分开处理的场合。
  1. 如:传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的 Buffer 中,这样你可以方便的处理消息头和消息体。

1. Scattering Reads

  • Scattering Reads 是指数据从一个 Channel 读取到多个 Buffer 中。如下图描述:
    在这里插入图片描述
/**
 * 1. 数据从一个 Channel 读取到多个 Buffer 中。
 */
@Test
public void testScatter() throws IOException {
    RandomAccessFile accessFile = new RandomAccessFile("E:\\TEMP\\nio\\Scatter.txt", "rw");
    FileChannel channel = accessFile.getChannel();

    ByteBuffer header = ByteBuffer.allocate(2);
    ByteBuffer body = ByteBuffer.allocate(4);
    ByteBuffer[] bufferArr = {header, body};

    long bytesRead = channel.read(bufferArr);
    System.out.printf("读取到缓冲区的大小: %s", bytesRead).println();// 读取到缓冲区的大小: 6
    // 1. 反转读写模式
    header.flip();
    body.flip();
    System.out.println((char) header.get());// a
    System.out.println((char) body.get());// c

    channel.close();
}
  • 注意 Buffer 首先被插入到数组,然后再将数组作为 channel.read() 的输入参数。
  1. read() 方法按照 Buffer 在数组中的顺序,将从 Channel 中读取的数据写入到 Buffer。
  2. 当一个 Buffer 被写满后,Channel 紧接着向另一个 Buffer 中写。
  • Scattering Reads 在移动下一个 Buffer 前,必须填满当前的 Buffer。
  1. 这也意味着它不适用于动态消息(注:消息大小不固定)。
  2. 换句话说,如果存在消息头和消息体,消息头必须完成填充(如 128byte)Scattering Reads 才能正常工作。

2. Gathering Writes

  • Gathering Writes 是指数据从多个 Buffer 写入到同一个 Channel。如下图描述:
    在这里插入图片描述
/**
 * 2. 数据从多个 Buffer 写入到同一个 Channel。
 */
@Test
public void testGather() throws IOException {
    RandomAccessFile accessFile = new RandomAccessFile("E:\\TEMP\\nio\\Gather.txt", "rw");
    FileChannel channel = accessFile.getChannel();

    ByteBuffer header = ByteBuffer.allocate(128);
    ByteBuffer body = ByteBuffer.allocate(1024);
    // 1. 将数据写入缓冲区
    header.put("abcd".getBytes());
    body.put("ABCD".getBytes());
    // 2. 反转读写模式
    header.flip();
    body.flip();
    ByteBuffer[] bufferArr = {header, body};

    // 3. 将数据写入 Channel
    channel.write(bufferArr);
    channel.force(true);
    channel.close();
}
  • Buffers 数组是 write() 方法的入参。
  1. write() 方法会按照 Buffer 在数组中的顺序,将数据写入到 Channel。
  2. 注意只有 position 和 limit 之间的数据才会被写入。
  3. 因此,如果一个 Buffer 的容量为 128 byte,但是仅仅包含 58 byte 的数据,那么这 58 byte 的数据将被写入到 Channel 中。
  4. 因此与 Scattering Reads 相反,Gathering Writes 能较好的处理动态消息。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

骑士梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值