0
0
0

Java NIO:Channel 通道的工作原理与数据读写

2026-11-01
文章摘要
|

一、Channel 是什么?

Channel(通道)是 Java NIO 的核心组件之一,主要负责数据的读取和写入。

在传统 BIO 中,数据主要通过 InputStream 和 OutputStream 进行传输;在 NIO 中,数据通常通过 Channel 和 Buffer 配合完成传输。

三个核心组件:

组件

作用

Channel

负责数据传输

Buffer

负责存储数据

Selector

负责监听多个 Channel 的 IO 就绪事件

Channel 和 Buffer 的关系:

读取数据:

文件 / 网络 → Channel → Buffer → 业务程序

写入数据:

业务程序 → Buffer → Channel → 文件 / 网络

常见 Channel 类型:

类型

主要作用

FileChannel

文件数据读写

SocketChannel

TCP 连接的数据读写

ServerSocketChannel

TCP 服务端监听和接收连接

DatagramChannel

UDP 数据收发

注意:ServerSocketChannel 主要负责接收客户端连接,不负责客户端业务数据的读写。建立连接后,由 SocketChannel 负责数据通信。

二、Channel 的 read() 和 write()

1. channel.read(buffer):读取数据

int length = channel.read(buffer);

作用:从 Channel 读取数据,写入 Buffer。

假设 Channel 中有 5 个字节:

10、20、30、40、50

创建 Buffer:

ByteBuffer buffer = ByteBuffer.allocate(10);

初始状态:

capacity = 10
limit = 10
position = 0

执行:

int length = channel.read(buffer);

假设本次成功读取 5 个字节:

索引:     0   1   2   3   4   5
Buffer:  10  20  30  40  50   -
                              ↑
                         position=5

此时:

capacity = 10
limit = 10
position = 5

需要注意:

  1. channel.read() 是从 Channel 读取数据,但对于 Buffer 来说,相当于写入数据。

  2. position 会根据实际读取的字节数向后移动。

  3. 一次 read() 不保证读取到全部数据。

  4. 如果要从 Buffer 中读取刚接收的数据,需要先调用 flip()。

例如:

int length = channel.read(buffer);

if (length > 0) {
    buffer.flip();

    while (buffer.hasRemaining()) {
        byte data = buffer.get();
        System.out.println(data);
    }
}

2. channel.write(buffer):写入数据

int length = channel.write(buffer);

作用:从 Buffer 读取数据,写入 Channel。

例如:

ByteBuffer buffer = ByteBuffer.allocate(10);

// 向Buffer写入数据
buffer.put((byte) 10);
buffer.put((byte) 20);
buffer.put((byte) 30);

// 切换为读取模式
buffer.flip();

// 将数据写入Channel
channel.write(buffer);

执行流程:

buffer.put()
    ↓
Buffer存储数据
    ↓
buffer.flip()
    ↓
position = 0
limit = 3
    ↓
channel.write(buffer)
    ↓
Channel从Buffer读取数据
    ↓
发送到文件或网络

假设本次成功写入 3 个字节:

position = 3
limit = 3
capacity = 10

注意:一次 write() 不保证写入全部数据。

推荐写法:

while (buffer.hasRemaining()) {
    channel.write(buffer);
}

对于阻塞模式的 FileChannel 或 SocketChannel,通常可以这样处理。

对于非阻塞 SocketChannel,如果 write() 返回 0,不应该一直空转重试,而应该通过 Selector 的 OP_WRITE 事件等待通道再次可写。

3. read() 和 write() 的区别

方法

数据流向

对 Buffer 的操作

channel.read(buffer)

Channel → Buffer

写入 Buffer

channel.write(buffer)

Buffer → Channel

读取 Buffer

buffer.put()

数据 → Buffer

写入 Buffer

buffer.get()

Buffer → 程序

读取 Buffer

容易混淆:read() 和 write() 是站在 Channel 的角度命名的,而不是 Buffer 的角度。

三、FileChannel、SocketChannel、ServerSocketChannel

1. FileChannel:文件读写

FileChannel 主要用于文件数据的读取、写入和位置管理。

完整示例:

import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class FileChannelDemo {

    public static void main(String[] args) throws Exception {

        try (FileChannel channel = FileChannel.open(
                Paths.get("test.txt"),
                StandardOpenOption.CREATE,
                StandardOpenOption.READ,
                StandardOpenOption.WRITE,
                StandardOpenOption.TRUNCATE_EXISTING)) {

            // 1. 准备写入数据
            byte[] data = "Hello NIO"
                    .getBytes(StandardCharsets.UTF_8);

            ByteBuffer buffer = ByteBuffer.wrap(data);

            // 2. 将Buffer中的数据写入文件
            while (buffer.hasRemaining()) {
                channel.write(buffer);
            }

            // 3. 将文件通道的位置移动到开头
            channel.position(0);

            // 4. 重置Buffer,准备读取
            buffer.clear();

            // 5. 从文件读取数据
            int length = channel.read(buffer);

            if (length > 0) {

                // 6. 切换读取模式
                buffer.flip();

                byte[] result = new byte[buffer.remaining()];
                buffer.get(result);

                System.out.println(
                        new String(result, StandardCharsets.UTF_8)
                );
            }
        }
    }
}

输出:

Hello NIO

问题一:为什么需要 channel.position(0)?

因为文件写入后,FileChannel 的文件位置已经移动到了文件末尾。

如果不重新设置位置,下一次读取可能直接到达文件结束,返回 -1。

注意:Channel 的 position 和 Buffer 的 position 是两个独立的概念。

属性

作用

channel.position()

文件通道当前读写的文件位置

buffer.position()

Buffer 下一次读写的位置

问题二:为什么 ByteBuffer.wrap(data) 不需要 flip()?

因为 wrap() 会将已有 byte[] 包装成 ByteBuffer。

初始状态:

position = 0
limit = data.length
capacity = data.length

因此已经可以从 Buffer 开头读取数据,直接传给 channel.write() 即可。

如果先通过 put() 写入,则通常需要先调用 flip()。

注意:示例中的 TRUNCATE_EXISTING 会清空原文件内容,生产环境不能随意使用。

2. SocketChannel:TCP 数据通信

SocketChannel 主要用于 TCP 连接的数据读写。

创建连接:

SocketChannel channel = SocketChannel.open();

channel.connect(
    new InetSocketAddress("127.0.0.1", 8080)
);

发送数据:

ByteBuffer buffer = ByteBuffer.wrap(
    "Hello Server".getBytes(StandardCharsets.UTF_8)
);

while (buffer.hasRemaining()) {
    channel.write(buffer);
}

读取数据:

ByteBuffer buffer = ByteBuffer.allocate(1024);

int length = channel.read(buffer);

if (length > 0) {

    buffer.flip();

    // 处理读取的数据

} else if (length == 0) {

    // 本次没有读取到数据

} else {

    // 返回-1,表示对端输出流结束
    channel.close();
}

read() 返回值:

返回值

含义

大于 0

成功读取的字节数

0

本次未读取到数据

-1

对端输出流已经结束

注意:返回 -1 不一定代表整个 TCP 连接完全关闭,因为 TCP 支持半关闭。实际业务中通常会进行连接清理。

3. ServerSocketChannel:接收客户端连接

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

例如:

ServerSocketChannel server = ServerSocketChannel.open();

// 绑定服务端端口
server.bind(new InetSocketAddress(8080));

// 接收客户端连接
SocketChannel client = server.accept();

执行流程:

ServerSocketChannel
        ↓
监听8080端口
        ↓
客户端建立TCP连接
        ↓
server.accept()
        ↓
返回SocketChannel
        ↓
SocketChannel负责read/write

二者区别:

类型

作用

ServerSocketChannel

监听和接收 TCP 连接

SocketChannel

TCP 数据通信

一个 ServerSocketChannel 可以接收多个客户端连接,每次成功 accept() 都会返回对应的 SocketChannel。

四、Channel 的阻塞与非阻塞模式

1. 阻塞模式

server.configureBlocking(true);

执行:

SocketChannel client = server.accept();

如果没有客户端连接,当前线程会阻塞等待。

SocketChannel 的阻塞模式也类似:

int length = channel.read(buffer);

如果当前没有数据且连接未结束,read() 通常会阻塞等待。

2. 非阻塞模式

server.configureBlocking(false);

执行:

SocketChannel client = server.accept();

如果没有客户端连接,直接返回 null,不会一直阻塞等待。

对于非阻塞 SocketChannel:

channel.configureBlocking(false);

int length = channel.read(buffer);

如果没有数据可以读取,可能直接返回 0。

注意:ServerSocketChannel 设置为非阻塞,并不意味着 accept() 返回的 SocketChannel 也自动变成非阻塞。接收到的 SocketChannel 默认仍为阻塞模式,需要单独配置:

SocketChannel client = server.accept();

if (client != null) {
    client.configureBlocking(false);
}

3. 非阻塞模式为什么不能一直循环 read()?

错误示例:

while (true) {
    int length = channel.read(buffer);
}

如果没有数据,非阻塞 read() 可能不断返回 0,导致 CPU 空转。

推荐使用 Selector:

多个SocketChannel
       ↓
注册到Selector
       ↓
Selector监听IO事件
       ↓
某个Channel可读
       ↓
调用channel.read(buffer)
       ↓
处理数据

这样可以避免线程不断轮询没有数据的 Channel。

需要注意:FileChannel 不支持注册到 Selector,也不能通过 configureBlocking(false) 实现 SocketChannel 这样的非阻塞模式。

五、Channel 常见问题与踩坑

1. channel.read() 后没有 flip()

错误:

channel.read(buffer);

byte data = buffer.get();

问题:read() 已经将 position 移动到写入数据末尾,此时直接 get() 会从当前 position 读取,不会自动回到开头。

正确:

channel.read(buffer);

buffer.flip();

byte data = buffer.get();

实际代码还需要先判断 read() 的返回值是否大于 0。

2. channel.write() 一次写入不完整

错误:

channel.write(buffer);

// 直接认为全部发送成功

问题:write() 不保证一次写完全部数据。

正确:

while (buffer.hasRemaining()) {
    channel.write(buffer);
}

注意:上述循环适合阻塞写入场景。非阻塞网络写入应正确处理返回 0 的情况,并使用 OP_WRITE 等机制完成后续发送。

3. read() 返回 0 就关闭连接

错误:

int length = channel.read(buffer);

if (length == 0) {
    channel.close();
}

问题:非阻塞模式下,返回 0 只代表本次没有读取到数据。

正确做法:

if (length > 0) {
    // 处理数据
} else if (length == 0) {
    // 不关闭连接,等待后续数据
} else {
    // 对端输出流结束,根据业务清理连接
}

4. 误以为 TCP 一次 read() 就是一条完整消息

例如客户端发送:

消息A:100字节
消息B:200字节

服务端可能读取成:

第一次:50字节
第二次:250字节

因为 TCP 是字节流协议,不保证消息边界。

需要根据协议进行拆包、粘包处理。

常见解决方式:

  1. 固定长度协议。

  2. 分隔符协议。

  3. 消息头携带长度字段。

例如 Netty 中的 LengthFieldBasedFrameDecoder,就是根据消息中的长度字段进行拆包的常见解码器。

5. read() 和 write() 的数据方向容易记反

需要特别注意:

channel.read(buffer);

是 Channel 向 Buffer 写数据。

channel.write(buffer);

是 Channel 从 Buffer 取数据。

不要将 Channel 和 Buffer 的读写视角混为一谈。

六、重点总结

  1. Channel 是 NIO 中负责数据传输的通道。

  2. Buffer 负责数据存储,Channel 负责数据传输。

  3. channel.read(buffer):从 Channel 读取数据,写入 Buffer。

  4. channel.write(buffer):从 Buffer 读取数据,写入 Channel。

  5. FileChannel:负责文件读写。

  6. SocketChannel:负责 TCP 连接的数据读写。

  7. ServerSocketChannel:负责 TCP 服务端监听和接收连接。

  8. read() 返回 0 不代表 TCP 连接断开。

  9. write() 不保证一次写完所有数据。

  10. Channel 与 Buffer 的 position 是两个不同的概念。

  11. TCP 不保证消息边界,业务层需要处理拆包和粘包。

  12. 非阻塞 Channel 通常配合 Selector 使用,避免 CPU 空转。

七、常见问题与答案

1. Channel 和 Buffer 的区别是什么?

Channel 负责传输数据,Buffer 负责存储数据。两者通常配合完成 IO 操作。

2. channel.read(buffer) 为什么会增加 Buffer 的 position?

因为从 Channel 读取的数据会写入 Buffer,所以 position 会根据实际写入字节数增加。

3. 为什么 channel.write(buffer) 之前通常需要 flip()?

如果数据刚通过 put() 写入 Buffer,position 位于数据末尾。flip() 将 position 设置为 0,并将 limit 设置为有效数据末尾,使 Channel 能够读取这些数据。

4. SocketChannel 和 ServerSocketChannel 有什么区别?

ServerSocketChannel 负责接收客户端连接,SocketChannel 负责连接建立后的数据通信。

5. 非阻塞 read() 返回 0 代表什么?

代表本次没有读取到数据,不代表连接已经断开。

6. 为什么非阻塞模式下通常需要 Selector?

因为不断调用 read() 轮询数据可能导致 CPU 空转。Selector 能够监听多个 Channel 的 IO 就绪事件,减少无效轮询。

7. FileChannel 能不能注册到 Selector?

不能。FileChannel 不支持 Selector 注册,Selector 主要用于支持非阻塞模式的网络 Channel。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论