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 的调度中心。
