Skip to content

Channel 通道

Stream 是单向的,Channel 是双向的。

就这一句话,你能看出它们的核心区别吗?

如果还不行,让我们继续往下看。

Stream vs Channel:不是换了个名字

很多人以为 Channel 只是 Stream 的另一个马甲。但实际上,它们是两种完全不同的设计哲学。

对比BIO 流NIO Channel
方向单向(InputStream 只读,OutputStream 只写)双向(可读可写)
配合对象字节数组Buffer
连接方式节点流,一端连程序,一端连目的地双向通道,两端都是主动的
阻塞模式总是阻塞可设置为非阻塞
支持 Selector✅(FileChannel 例外)

BIO 的流像一条单行道,只能从一个方向走。Channel 像一条双向车道,车可以从两边进出。

Channel 家族图谱

java.nio.channels.Channel(接口)

    ├── WritableByteChannel(接口)
    │       └── GatheringByteChannel(接口)
    ├── ReadableByteChannel(接口)
    │       └── ScatteringByteChannel(接口)
    └── AbstractInterruptibleChannel(抽象类)

            ├── SelectableChannel(抽象类,可注册到 Selector)
            │       ├── SocketChannel          TCP 客户端
            │       ├── ServerSocketChannel    TCP 服务端
            │       ├── DatagramChannel        UDP
            │       └── Pipe.SourceChannel / Pipe.SinkChannel
            └── FileChannel                    文件(不能注册到 Selector)

这张图不需要背,但需要理解层次关系:

  • 最上层是接口,定义能力
  • 中间是抽象类,封装通用逻辑
  • 最下层是实现类,做具体工作

FileChannel:文件操作的专家

FileChannel 是操作文件的主要方式。它有几个独特能力:

基本读写

java
// 打开 FileChannel
RandomAccessFile raf = new RandomAccessFile("data.txt", "rw");
FileChannel channel = raf.getChannel();

// 读取
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);

// 写入
ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put("Hello".getBytes());
writeBuffer.flip();
channel.write(writeBuffer);

// 关闭
channel.close();
raf.close();

位置操作:精准控制读写位置

java
// 获取当前位置
long pos = channel.position();

// 设置位置(跳过前 100 字节)
channel.position(100);

// 获取文件大小
long size = channel.size();

// 截断文件(保留前 1024 字节)
channel.truncate(1024);

// 强制刷新到磁盘(不调用的话数据可能还在系统缓冲区)
channel.force(true); // true = 同时刷新元数据

文件锁:多进程协调

java
// 独占锁(排他锁)
FileLock lock = channel.lock();
try {
    // 临界区操作
    channel.write(buffer);
} finally {
    lock.release();
}

// 共享锁(允许多个并发读)
FileLock sharedLock = channel.lock(0L, Long.MAX_VALUE, true);
try {
    // 只读操作
} finally {
    sharedLock.release();
}

文件锁的好处:即使是多进程环境,也能安全地协调对同一个文件的访问

SocketChannel:TCP 的双向通道

TCP 通信既可以读也可以写,这就是 SocketChannel 的用武之地。

连接服务器

java
// 阻塞模式连接
SocketChannel channel = SocketChannel.open();
channel.connect(new InetSocketAddress("localhost", 8080));

// 非阻塞模式连接
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress("localhost", 8080));
// 非阻塞模式下 connect() 会立即返回,需要循环检查
while (!channel.finishConnect()) {
    // 做别的事,比如处理其他事件
}

读写数据

java
ByteBuffer buffer = ByteBuffer.allocate(1024);

// 读取
int bytesRead = channel.read(buffer);
if (bytesRead == -1) {
    // 客户端关闭了连接
    channel.close();
}

// 写入
buffer.clear();
buffer.put("response".getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
    channel.write(buffer);
}

ServerSocketChannel:服务端的守门人

ServerSocketChannel 不传输数据,它只做一件事:接受新连接

java
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 非阻塞模式

while (true) {
    SocketChannel client = serverChannel.accept();
    if (client != null) {
        // 有新连接,处理它
        handleClient(client);
    }
}

注意 accept() 在非阻塞模式下可能返回 null,所以必须检查。

DatagramChannel:UDP 的通道

UDP 是无连接的,所以 DatagramChannel 的用法和 SocketChannel 不同:

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

ByteBuffer buffer = ByteBuffer.allocate(1024);

// 接收数据
SocketAddress addr = channel.receive(buffer);

// 发送数据到指定地址
buffer.flip();
channel.send(buffer, addr);

Scatter / Gather:批量操作的艺术

有时候你需要处理协议头 + 消息体的场景。比如 HTTP 响应:

┌─────────────┬────────────────────┐
│   Header    │       Body         │
│   (128B)    │      (1024B)       │
└─────────────┴────────────────────┘

Scatter(分散读)

把一个 Channel 的数据分散到多个 Buffer:

java
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
channel.read(new ByteBuffer[]{header, body});

数据会按顺序填满 header,再填 body。这叫 Scatter

Gather(聚合写)

把多个 Buffer 的数据聚合写入一个 Channel:

java
ByteBuffer buf1 = ByteBuffer.allocate(128);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
buf1.put("Header".getBytes()).flip();
buf2.put("Body".getBytes()).flip();
channel.write(new ByteBuffer[]{buf1, buf2});

这叫 Gather

使用场景:处理协议时特别有用——协议头和消息体分开存储,但发送时需要合并。


记住这个对比:

Stream 是管道,数据流过就没了;Channel 是港口,数据可以反复装卸。

下一节,我们来看看 Selector——NIO 的调度中心。

基于 VitePress 构建