Channel
字数: 0 字 时长: 0 分钟
Channel 是 Java NIO 中负责数据传输的核心组件。
在传统 IO 中,数据主要通过 Stream 进行传输:
InputStream
OutputStream
Reader
Writer而在 NIO 中,数据主要通过:
Channel
+
Buffer完成读写。
可以简单理解为:
Channel
↓
负责传输数据
Buffer
↓
负责存储数据数据通常不会直接从 Channel 读到程序中,而是需要经过 Buffer。
读取数据:
文件 / 网络
↓
Channel
↓
Buffer
↓
程序写入数据:
程序
↓
Buffer
↓
Channel
↓
文件 / 网络1. Channel 和传统 Stream 的区别
传统 IO 中:
InputStream
↓
只能读
OutputStream
↓
只能写而 Channel 通常可以同时支持读取和写入。
例如:
FileChannel既可以:
read()也可以:
write()简单对比:
| 对比 | Stream | Channel |
|---|---|---|
| 所属 | 传统 IO | NIO |
| 数据传输 | 直接读写 | 配合 Buffer |
| 读写方向 | 通常单向 | 通常可以双向 |
| 非阻塞 | 不支持 | 部分 Channel 支持 |
| Selector | 不支持 | 部分 Channel 支持注册 Selector |
需要注意:
并不是所有 Channel 都支持非阻塞模式。
例如:
FileChannel主要用于文件 IO,本身不属于 Selector 管理的非阻塞网络 Channel。
而:
SocketChannel
ServerSocketChannel
DatagramChannel可以设置为非阻塞模式,并注册到 Selector。
2. Channel 常见实现
Java NIO 中比较常见的 Channel:
Channel
│
├── FileChannel
│ └── 文件读写
│
├── SocketChannel
│ └── TCP 客户端连接
│
├── ServerSocketChannel
│ └── TCP 服务端监听连接
│
└── DatagramChannel
└── UDP 数据传输其中最常用的是:
FileChannel
SocketChannel
ServerSocketChannel3. FileChannel
FileChannel 主要用于文件数据的读取和写入。
它可以通过:
FileInputStream
FileOutputStream
RandomAccessFile获得。
例如:
FileInputStream fis =
new FileInputStream(
"test.txt"
);
FileChannel channel =
fis.getChannel();也可以通过 RandomAccessFile 获取:
RandomAccessFile file =
new RandomAccessFile(
"test.txt",
"rw"
);
FileChannel channel =
file.getChannel();3.1 FileChannel 读取数据
读取文件时,需要准备一个 Buffer:
FileInputStream fis =
new FileInputStream(
"test.txt"
);
FileChannel channel =
fis.getChannel();
ByteBuffer buffer =
ByteBuffer.allocate(1024);
int len =
channel.read(buffer);整个过程:
test.txt
↓
FileChannel
↓
ByteBufferread() 返回本次读取到的字节数。
如果读取到文件末尾,则返回:
-1所以通常会这样写:
while (
channel.read(buffer) != -1
) {
// 切换成读取 Buffer 的状态
buffer.flip();
while (
buffer.hasRemaining()
) {
System.out.print(
(char) buffer.get()
);
}
// 清空状态,准备下一次读取
buffer.clear();
}3.2 FileChannel 写入数据
写入文件时,数据流向相反:
ByteBuffer
↓
FileChannel
↓
文件例如:
FileOutputStream fos =
new FileOutputStream(
"test.txt"
);
FileChannel channel =
fos.getChannel();
ByteBuffer buffer =
ByteBuffer.wrap(
"kerwim".getBytes()
);
channel.write(buffer);这里:
ByteBuffer.wrap()直接把字节数组包装成一个 ByteBuffer。
然后:
channel.write(buffer);将 Buffer 中的数据写入文件。
3.3 FileChannel 文件复制
使用 FileChannel + ByteBuffer 可以完成文件复制:
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();
}
}核心过程:
source.txt
↓
FileChannel
↓
ByteBuffer
↓
FileChannel
↓
target.txt4. FileChannel 的 position
FileChannel 内部维护了一个文件位置,也就是:
position可以获取当前位置:
long position =
channel.position();也可以主动修改:
channel.position(100);表示把文件位置移动到:
第 100 个字节之后的读取或者写入,就会从这个位置开始。
例如:
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 还提供了两个比较重要的方法:
transferTo()
transferFrom()用于在 Channel 之间传输数据。
5.1 transferTo
sourceChannel.transferTo(
0,
sourceChannel.size(),
targetChannel
);表示:
sourceChannel
↓
targetChannel例如:
try (
FileChannel sourceChannel =
new FileInputStream(
"source.txt"
).getChannel();
FileChannel targetChannel =
new FileOutputStream(
"target.txt"
).getChannel()
) {
sourceChannel.transferTo(
0,
sourceChannel.size(),
targetChannel
);
}相比自己手动创建:
ByteBuffer这种方式代码更加简单。
5.2 transferFrom
方向相反:
targetChannel.transferFrom(
sourceChannel,
0,
sourceChannel.size()
);可以理解为:
transferTo()
↓
把自己的数据传出去
transferFrom()
↓
从其他 Channel 接收数据transferTo() 和 transferFrom() 在部分操作系统和场景下,可以利用底层系统提供的数据传输优化,减少不必要的数据复制。
这也是后面学习:
零拷贝时会再次接触到的内容。
详细内容见:[[零拷贝]]
6. SocketChannel
SocketChannel 主要用于 TCP 网络通信。
它类似于传统 IO 中的:
Socket传统方式:
Socket
↓
InputStream
OutputStreamNIO:
SocketChannel
↓
ByteBuffer6.1 创建 SocketChannel
可以通过:
SocketChannel.open();创建:
SocketChannel socketChannel =
SocketChannel.open();然后连接服务器:
socketChannel.connect(
new InetSocketAddress(
"localhost",
8080
)
);完整过程:
SocketChannel socketChannel =
SocketChannel.open();
socketChannel.connect(
new InetSocketAddress(
"localhost",
8080
)
);6.2 SocketChannel 写入数据
向服务器发送数据:
ByteBuffer buffer =
ByteBuffer.wrap(
"Hello kerwim".getBytes()
);
socketChannel.write(buffer);数据流向:
ByteBuffer
↓
SocketChannel
↓
网络6.3 SocketChannel 读取数据
读取服务器返回的数据:
ByteBuffer buffer =
ByteBuffer.allocate(1024);
int len =
socketChannel.read(buffer);数据流向:
网络
↓
SocketChannel
↓
ByteBuffer7. SocketChannel 非阻塞模式
SocketChannel 最大的特点之一,就是可以设置成:
非阻塞模式通过:
socketChannel.configureBlocking(false);设置。
默认情况下:
blocking = true也就是阻塞模式。
设置:
configureBlocking(false)以后就进入非阻塞模式。
SocketChannel
↓
configureBlocking(false)
↓
非阻塞模式非阻塞的 SocketChannel 可以注册到:
Selector由 Selector 统一监控多个 Channel 的 IO 事件。
8. ServerSocketChannel
ServerSocketChannel 主要用于 TCP 服务端监听客户端连接。
它类似于传统 IO 中的:
ServerSocket对应关系:
传统 IO NIO
ServerSocket → ServerSocketChannel
Socket → SocketChannel创建:
ServerSocketChannel serverSocketChannel =
ServerSocketChannel.open();绑定端口:
serverSocketChannel.bind(
new InetSocketAddress(8080)
);接受客户端连接:
SocketChannel socketChannel =
serverSocketChannel.accept();整体过程:
ServerSocketChannel
↓
accept()
↓
SocketChannel
↓
与客户端通信9. ServerSocketChannel 非阻塞模式
同样可以设置成非阻塞:
serverSocketChannel.configureBlocking(
false
);然后注册到 Selector:
serverSocketChannel.register(
selector,
SelectionKey.OP_ACCEPT
);表示:
我关心这个 Channel 上的客户端连接事件。
当客户端发起连接以后:
Selector
↓
发现 OP_ACCEPT
↓
ServerSocketChannel.accept()
↓
得到 SocketChannel然后再把新的:
SocketChannel注册到 Selector:
socketChannel.configureBlocking(false);
socketChannel.register(
selector,
SelectionKey.OP_READ
);这样一个线程就可以通过 Selector 管理多个客户端连接。
详细内容见:[[Selector]]
10. DatagramChannel
DatagramChannel 主要用于:
UDP通信。
对应传统 IO 中的:
DatagramSocket
DatagramPacket基本结构:
传统 UDP
DatagramSocket
↓
DatagramPacket
NIO UDP
DatagramChannel
↓
ByteBuffer创建:
DatagramChannel channel =
DatagramChannel.open();发送数据:
ByteBuffer buffer =
ByteBuffer.wrap(
"Hello".getBytes()
);
channel.send(
buffer,
new InetSocketAddress(
"localhost",
8080
)
);接收数据:
ByteBuffer buffer =
ByteBuffer.allocate(1024);
channel.receive(buffer);11. Channel 和 Selector
并不是所有 Channel 都可以注册到 Selector。
可以注册 Selector 的 Channel,需要属于:
SelectableChannel常见的包括:
SocketChannel
ServerSocketChannel
DatagramChannel例如:
SocketChannel
↓
SelectableChannel
↓
可以注册 Selector而:
FileChannel不能注册到 Selector。
所以:
FileChannel
↓
文件 IO
↓
不能注册 Selector
SocketChannel
ServerSocketChannel
DatagramChannel
↓
网络 IO
↓
可以注册 Selector这也是为什么 NIO 的:
非阻塞
+
IO 多路复用主要体现在网络 IO 中。
12. Channel 和 Buffer 的关系
Channel 和 Buffer 基本上是一起出现的。
读取:
channel.read(buffer);表示:
Channel
↓
Buffer写入:
channel.write(buffer);表示:
Buffer
↓
Channel所以可以记住:
read()
Channel → Buffer
write()
Buffer → Channel这一点非常重要。
13. 小结
Java NIO 中,Channel 负责:
数据的传输。
而 Buffer 负责:
数据的存储。
核心关系:
Channel
↕
Buffer常见 Channel:
Channel
│
├── FileChannel
│ └── 文件 IO
│
├── SocketChannel
│ └── TCP 客户端通信
│
├── ServerSocketChannel
│ └── TCP 服务端监听
│
└── DatagramChannel
└── UDP 通信其中:
FileChannel
↓
主要用于文件 IO
↓
不能注册 Selector而:
SocketChannel
ServerSocketChannel
DatagramChannel
↓
支持非阻塞模式
↓
可以注册 Selector
↓
实现 IO 多路复用整个 NIO 的核心关系也越来越清晰:
Java NIO
│
┌───────────┼───────────┐
│ │ │
Buffer Channel Selector
│ │ │
存储数据 传输数据 管理多个
Channel一句话理解:
Buffer 存数据,Channel 传数据,Selector 管 Channel。