网站公告

本次更新:没啥更新的

“钱塘江上潮信起,今日方知我是我”

Skip to content

Channel ​

字数: 0 字 时长: 0 分钟

Channel 是 Java NIO 中负责数据传输的核心组件。

在传统 IO 中,数据主要通过 Stream 进行传输:

text
InputStream
OutputStream
Reader
Writer

而在 NIO 中,数据主要通过:

text
Channel
    +
Buffer

完成读写。

可以简单理解为:

text
Channel
    ↓
负责传输数据

Buffer
    ↓
负责存储数据

数据通常不会直接从 Channel 读到程序中,而是需要经过 Buffer。

读取数据:

text
文件 / 网络
    ↓
Channel
    ↓
Buffer
    ↓
程序

写入数据:

text
程序
    ↓
Buffer
    ↓
Channel
    ↓
文件 / 网络

1. Channel 和传统 Stream 的区别 ​

传统 IO 中:

text
InputStream
    ↓
只能读

OutputStream
    ↓
只能写

而 Channel 通常可以同时支持读取和写入。

例如:

java
FileChannel

既可以:

java
read()

也可以:

java
write()

简单对比:

对比StreamChannel
所属传统 IONIO
数据传输直接读写配合 Buffer
读写方向通常单向通常可以双向
非阻塞不支持部分 Channel 支持
Selector不支持部分 Channel 支持注册 Selector

需要注意:

并不是所有 Channel 都支持非阻塞模式。

例如:

text
FileChannel

主要用于文件 IO,本身不属于 Selector 管理的非阻塞网络 Channel。

而:

text
SocketChannel
ServerSocketChannel
DatagramChannel

可以设置为非阻塞模式,并注册到 Selector。


2. Channel 常见实现 ​

Java NIO 中比较常见的 Channel:

text
Channel
│
├── FileChannel
│     └── 文件读写
│
├── SocketChannel
│     └── TCP 客户端连接
│
├── ServerSocketChannel
│     └── TCP 服务端监听连接
│
└── DatagramChannel
      └── UDP 数据传输

其中最常用的是:

text
FileChannel
SocketChannel
ServerSocketChannel

3. FileChannel ​

FileChannel 主要用于文件数据的读取和写入。

它可以通过:

text
FileInputStream
FileOutputStream
RandomAccessFile

获得。

例如:

java
FileInputStream fis =
        new FileInputStream(
                "test.txt"
        );

FileChannel channel =
        fis.getChannel();

也可以通过 RandomAccessFile 获取:

java
RandomAccessFile file =
        new RandomAccessFile(
                "test.txt",
                "rw"
        );

FileChannel channel =
        file.getChannel();

3.1 FileChannel 读取数据 ​

读取文件时,需要准备一个 Buffer:

java
FileInputStream fis =
        new FileInputStream(
                "test.txt"
        );

FileChannel channel =
        fis.getChannel();

ByteBuffer buffer =
        ByteBuffer.allocate(1024);

int len =
        channel.read(buffer);

整个过程:

text
test.txt
    ↓
FileChannel
    ↓
ByteBuffer

read() 返回本次读取到的字节数。

如果读取到文件末尾,则返回:

text
-1

所以通常会这样写:

java
while (
        channel.read(buffer) != -1
) {

    // 切换成读取 Buffer 的状态
    buffer.flip();

    while (
            buffer.hasRemaining()
    ) {

        System.out.print(
                (char) buffer.get()
        );
    }

    // 清空状态,准备下一次读取
    buffer.clear();
}

3.2 FileChannel 写入数据 ​

写入文件时,数据流向相反:

text
ByteBuffer
    ↓
FileChannel
    ↓
文件

例如:

java
FileOutputStream fos =
        new FileOutputStream(
                "test.txt"
        );

FileChannel channel =
        fos.getChannel();

ByteBuffer buffer =
        ByteBuffer.wrap(
                "kerwim".getBytes()
        );

channel.write(buffer);

这里:

java
ByteBuffer.wrap()

直接把字节数组包装成一个 ByteBuffer。

然后:

java
channel.write(buffer);

将 Buffer 中的数据写入文件。


3.3 FileChannel 文件复制 ​

使用 FileChannel + ByteBuffer 可以完成文件复制:

java
try (
        FileChannel inputChannel =
                new FileInputStream(
                        "source.txt"
                ).getChannel();

        FileChannel outputChannel =
                new FileOutputStream(
                        "target.txt"
                ).getChannel()
) {

    ByteBuffer buffer =
            ByteBuffer.allocate(1024);

    while (
            inputChannel.read(buffer) != -1
    ) {

        // 写模式 → 读模式
        buffer.flip();

        while (
                buffer.hasRemaining()
        ) {

            outputChannel.write(buffer);
        }

        // 重新切换为写模式
        buffer.clear();
    }
}

核心过程:

text
source.txt
    ↓
FileChannel
    ↓
ByteBuffer
    ↓
FileChannel
    ↓
target.txt

4. FileChannel 的 position ​

FileChannel 内部维护了一个文件位置,也就是:

text
position

可以获取当前位置:

java
long position =
        channel.position();

也可以主动修改:

java
channel.position(100);

表示把文件位置移动到:

text
第 100 个字节

之后的读取或者写入,就会从这个位置开始。

例如:

java
RandomAccessFile file =
        new RandomAccessFile(
                "test.txt",
                "rw"
        );

FileChannel channel =
        file.getChannel();

channel.position(5);

ByteBuffer buffer =
        ByteBuffer.wrap(
                "kerwim".getBytes()
        );

channel.write(buffer);

因此 FileChannel 可以进行类似随机访问的文件操作。


5. FileChannel 的 transferTo 和 transferFrom ​

FileChannel 还提供了两个比较重要的方法:

java
transferTo()

transferFrom()

用于在 Channel 之间传输数据。

5.1 transferTo ​

java
sourceChannel.transferTo(
        0,
        sourceChannel.size(),
        targetChannel
);

表示:

text
sourceChannel
    ↓
targetChannel

例如:

java
try (
        FileChannel sourceChannel =
                new FileInputStream(
                        "source.txt"
                ).getChannel();

        FileChannel targetChannel =
                new FileOutputStream(
                        "target.txt"
                ).getChannel()
) {

    sourceChannel.transferTo(
            0,
            sourceChannel.size(),
            targetChannel
    );
}

相比自己手动创建:

java
ByteBuffer

这种方式代码更加简单。

5.2 transferFrom ​

方向相反:

java
targetChannel.transferFrom(
        sourceChannel,
        0,
        sourceChannel.size()
);

可以理解为:

text
transferTo()
    ↓
把自己的数据传出去


transferFrom()
    ↓
从其他 Channel 接收数据

transferTo() 和 transferFrom() 在部分操作系统和场景下,可以利用底层系统提供的数据传输优化,减少不必要的数据复制。

这也是后面学习:

text
零拷贝

时会再次接触到的内容。

详细内容见:[[零拷贝]]


6. SocketChannel ​

SocketChannel 主要用于 TCP 网络通信。

它类似于传统 IO 中的:

java
Socket

传统方式:

text
Socket
    ↓
InputStream
OutputStream

NIO:

text
SocketChannel
    ↓
ByteBuffer

6.1 创建 SocketChannel ​

可以通过:

java
SocketChannel.open();

创建:

java
SocketChannel socketChannel =
        SocketChannel.open();

然后连接服务器:

java
socketChannel.connect(
        new InetSocketAddress(
                "localhost",
                8080
        )
);

完整过程:

java
SocketChannel socketChannel =
        SocketChannel.open();

socketChannel.connect(
        new InetSocketAddress(
                "localhost",
                8080
        )
);

6.2 SocketChannel 写入数据 ​

向服务器发送数据:

java
ByteBuffer buffer =
        ByteBuffer.wrap(
                "Hello kerwim".getBytes()
        );

socketChannel.write(buffer);

数据流向:

text
ByteBuffer
    ↓
SocketChannel
    ↓
网络

6.3 SocketChannel 读取数据 ​

读取服务器返回的数据:

java
ByteBuffer buffer =
        ByteBuffer.allocate(1024);

int len =
        socketChannel.read(buffer);

数据流向:

text
网络
    ↓
SocketChannel
    ↓
ByteBuffer

7. SocketChannel 非阻塞模式 ​

SocketChannel 最大的特点之一,就是可以设置成:

text
非阻塞模式

通过:

java
socketChannel.configureBlocking(false);

设置。

默认情况下:

text
blocking = true

也就是阻塞模式。

设置:

java
configureBlocking(false)

以后就进入非阻塞模式。

text
SocketChannel
    ↓
configureBlocking(false)
    ↓
非阻塞模式

非阻塞的 SocketChannel 可以注册到:

text
Selector

由 Selector 统一监控多个 Channel 的 IO 事件。


8. ServerSocketChannel ​

ServerSocketChannel 主要用于 TCP 服务端监听客户端连接。

它类似于传统 IO 中的:

java
ServerSocket

对应关系:

text
传统 IO              NIO

ServerSocket   →   ServerSocketChannel

Socket         →   SocketChannel

创建:

java
ServerSocketChannel serverSocketChannel =
        ServerSocketChannel.open();

绑定端口:

java
serverSocketChannel.bind(
        new InetSocketAddress(8080)
);

接受客户端连接:

java
SocketChannel socketChannel =
        serverSocketChannel.accept();

整体过程:

text
ServerSocketChannel
        ↓
      accept()
        ↓
   SocketChannel
        ↓
   与客户端通信

9. ServerSocketChannel 非阻塞模式 ​

同样可以设置成非阻塞:

java
serverSocketChannel.configureBlocking(
        false
);

然后注册到 Selector:

java
serverSocketChannel.register(
        selector,
        SelectionKey.OP_ACCEPT
);

表示:

我关心这个 Channel 上的客户端连接事件。

当客户端发起连接以后:

text
Selector
    ↓
发现 OP_ACCEPT
    ↓
ServerSocketChannel.accept()
    ↓
得到 SocketChannel

然后再把新的:

text
SocketChannel

注册到 Selector:

java
socketChannel.configureBlocking(false);

socketChannel.register(
        selector,
        SelectionKey.OP_READ
);

这样一个线程就可以通过 Selector 管理多个客户端连接。

详细内容见:[[Selector]]


10. DatagramChannel ​

DatagramChannel 主要用于:

text
UDP

通信。

对应传统 IO 中的:

text
DatagramSocket
DatagramPacket

基本结构:

text
传统 UDP

DatagramSocket
    ↓
DatagramPacket


NIO UDP

DatagramChannel
    ↓
ByteBuffer

创建:

java
DatagramChannel channel =
        DatagramChannel.open();

发送数据:

java
ByteBuffer buffer =
        ByteBuffer.wrap(
                "Hello".getBytes()
        );

channel.send(
        buffer,
        new InetSocketAddress(
                "localhost",
                8080
        )
);

接收数据:

java
ByteBuffer buffer =
        ByteBuffer.allocate(1024);

channel.receive(buffer);

11. Channel 和 Selector ​

并不是所有 Channel 都可以注册到 Selector。

可以注册 Selector 的 Channel,需要属于:

text
SelectableChannel

常见的包括:

text
SocketChannel
ServerSocketChannel
DatagramChannel

例如:

java
SocketChannel
    ↓
SelectableChannel
    ↓
可以注册 Selector

而:

text
FileChannel

不能注册到 Selector。

所以:

text
FileChannel
    ↓
文件 IO
    ↓
不能注册 Selector


SocketChannel
ServerSocketChannel
DatagramChannel
    ↓
网络 IO
    ↓
可以注册 Selector

这也是为什么 NIO 的:

text
非阻塞
+
IO 多路复用

主要体现在网络 IO 中。


12. Channel 和 Buffer 的关系 ​

Channel 和 Buffer 基本上是一起出现的。

读取:

java
channel.read(buffer);

表示:

text
Channel
    ↓
Buffer

写入:

java
channel.write(buffer);

表示:

text
Buffer
    ↓
Channel

所以可以记住:

text
read()

Channel → Buffer


write()

Buffer → Channel

这一点非常重要。


13. 小结 ​

Java NIO 中,Channel 负责:

数据的传输。

而 Buffer 负责:

数据的存储。

核心关系:

text
Channel
    ↕
Buffer

常见 Channel:

text
Channel
│
├── FileChannel
│     └── 文件 IO
│
├── SocketChannel
│     └── TCP 客户端通信
│
├── ServerSocketChannel
│     └── TCP 服务端监听
│
└── DatagramChannel
      └── UDP 通信

其中:

text
FileChannel
    ↓
主要用于文件 IO
    ↓
不能注册 Selector

而:

text
SocketChannel
ServerSocketChannel
DatagramChannel
    ↓
支持非阻塞模式
    ↓
可以注册 Selector
    ↓
实现 IO 多路复用

整个 NIO 的核心关系也越来越清晰:

text
                 Java NIO
                    │
        ┌───────────┼───────────┐
        │           │           │
      Buffer      Channel    Selector
        │           │           │
     存储数据      传输数据     管理多个
                               Channel

一句话理解:

Buffer 存数据,Channel 传数据,Selector 管 Channel。

Noctiflorous Garden · MIT License