1 Channel接口体系结构
1.1 Channel到底是什么
Channel(通道)是Java NIO的核心概念,你可以把它理解为数据传输的管道。和传统的Stream不同,Channel有几个很实用的特点:
Channel既能读数据,也能写数据,就像一条双向车道。传统的InputStream只能读,OutputStream只能写,需要分别处理。
更重要的是,Channel可以设置成非阻塞模式。什么意思?就是读写数据时不会卡住程序,没数据就继续干别的事,这对高并发应用特别有用。
Channel还能配合Selector使用,一个线程就能管理成百上千个连接。想象一下,以前需要1000个服务员的餐厅,现在只要几个就够了。
从技术实现上看,Channel把操作系统底层的I/O操作包装了一下,让我们用起来更方便。不用关心底层怎么传输数据,调用几个方法就行。
1.2 Channel的家族关系
Java NIO的Channel接口设计得很有层次感:
Channel (interface)
├── ReadableByteChannel (interface)
│ └── ScatteringByteChannel (interface)
├── WritableByteChannel (interface)
│ └── GatheringByteChannel (interface)
├── ByteChannel (interface)
│ └── SeekableByteChannel (interface)
└── InterruptibleChannel (interface)
└── SelectableChannel (abstract class)
├── AbstractSelectableChannel (abstract class)
│ ├── SocketChannel
│ ├── ServerSocketChannel
│ ├── DatagramChannel
│ └── Pipe.SinkChannel/SourceChannel
└── ...
这样设计的好处是,不同类型的Channel可以共享一些基础功能,但又能保持自己的特色。
1.3 常用的Channel类型
Java NIO提供了好几种Channel,每种都有自己的用途:
Channel类型 | 替代了什么 | 主要用来干什么 |
---|---|---|
FileChannel | FileInputStream/FileOutputStream | 读写文件 |
SocketChannel | Socket | TCP客户端连接 |
ServerSocketChannel | ServerSocket | TCP服务器监听 |
DatagramChannel | DatagramSocket | UDP通信 |
Pipe.SinkChannel/SourceChannel | PipedOutputStream/PipedInputStream | 线程间传数据 |
做网络编程的话,SocketChannel和ServerSocketChannel用得最多。一个负责连接服务器,一个负责接受连接,配合起来就能搭建高性能的网络应用。
2 SocketChannel和ServerSocketChannel详解
2.1 ServerSocketChannel:服务器端的门卫
ServerSocketChannel就是服务器端用来接收客户端连接的,可以理解为传统ServerSocket的升级版。最大的区别是,它支持非阻塞操作,不会让程序傻等着。
2.1.1 怎么创建和配置
java
// 创建ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 绑定端口,就像给门牌号
serverChannel.bind(new InetSocketAddress(8080));
// 设置为非阻塞模式,这是关键
serverChannel.configureBlocking(false);
// 如果需要,还能拿到传统的ServerSocket
ServerSocket serverSocket = serverChannel.socket();
2.1.2 常用方法
- accept():接受客户端连接,返回一个SocketChannel
- bind(SocketAddress):绑定到指定地址和端口
- configureBlocking(boolean):设置是否阻塞,false表示非阻塞
- isOpen():检查Channel是否还开着
- close():关闭Channel,释放资源
2.2 SocketChannel:客户端的连接器
SocketChannel是客户端用来连接服务器的,相当于传统Socket的NIO版本。它有两个身份:既可以主动连接服务器,也可以作为服务器接收到的客户端连接。
2.2.1 客户端怎么连接
java
// 创建SocketChannel
SocketChannel socketChannel = SocketChannel.open();
// 设置为非阻塞模式
socketChannel.configureBlocking(false);
// 尝试连接服务器
boolean connected = socketChannel.connect(new InetSocketAddress("localhost", 8080));
// 非阻塞模式下,connect可能还没连上就返回了
if (!connected) {
// 需要等连接真正建立
while (!socketChannel.finishConnect()) {
// 这期间可以干点别的事
Thread.sleep(100);
}
}
System.out.println("连接成功!");
2.2.2 常用方法
SocketChannel的主要方法:
- open():创建一个新的SocketChannel
- connect(SocketAddress):连接到指定地址,非阻塞模式下可能立即返回
- finishConnect():完成连接,配合connect()使用
- isConnected():检查是否已经连上了
- isConnectionPending():检查连接是否还在进行中
- read(ByteBuffer):从通道读数据到缓冲区
- write(ByteBuffer):把缓冲区的数据写到通道
- configureBlocking(boolean):设置阻塞模式
- close():关闭连接
2.3 物联网平台中的应用
在物联网平台的网络通信模块中,ServerSocketChannel和SocketChannel通常用于以下场景:
- 设备网关服务:使用ServerSocketChannel接收来自设备的连接请求
- 实时数据采集:使用SocketChannel建立与传感器设备的高效连接
- 命令下发系统:通过SocketChannel向设备发送控制指令
- 多设备并发管理:结合Selector实现单线程管理多设备连接
3 阻塞模式vs非阻塞模式
3.1 两种模式有什么区别
Channel有两种工作方式:阻塞模式和非阻塞模式。这是NIO比传统I/O强的地方。
简单来说:
- 阻塞模式:就像排队买奶茶,必须等前面的人买完才轮到你,期间什么都干不了
- 非阻塞模式:像网上点外卖,下单后可以继续干别的,偶尔看看外卖到了没
特性 | 阻塞模式 | 非阻塞模式 |
---|---|---|
等待方式 | 傻等着,直到操作完成 | 立即返回,不等结果 |
返回结果 | 返回真实结果 | 可能返回"还没好"的标志 |
线程利用 | 一个线程只能干一件事 | 一个线程能同时处理多件事 |
编程难度 | 简单,符合直觉 | 稍微复杂,需要轮询检查 |
适合场景 | 连接少,要求简单 | 连接多,要求高性能 |
3.2 怎么切换模式
Channel默认是阻塞模式,可以这样切换:
java
// 设置为非阻塞模式
channel.configureBlocking(false);
// 设置为阻塞模式
channel.configureBlocking(true);
// 检查当前是什么模式
boolean isBlocking = channel.isBlocking();
3.3 两种模式具体有什么不同
3.3.1 ServerSocketChannel接收连接时
- 阻塞模式:程序会卡在accept()这里,直到真的有客户端连过来
- 非阻塞模式:accept()立即返回,有连接就返回SocketChannel,没连接就返回null
java
// 阻塞模式的例子
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8888));
// 程序会卡在这里等连接
SocketChannel socketChannel = serverChannel.accept();
// 非阻塞模式的例子
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8888));
serverChannel.configureBlocking(false);
// 立即返回,可能是null
SocketChannel socketChannel = serverChannel.accept();
if (socketChannel != null) {
// 有新连接,处理一下
}
3.3.2 SocketChannel的各种操作
不同模式下,SocketChannel的行为也不一样:
连接操作 connect()
- 阻塞模式:程序等着,直到连接成功或失败
- 非阻塞模式:立即返回,后面用finishConnect()检查是否连上
读数据 read()
- 阻塞模式:等着,直到读到数据或连接断了
- 非阻塞模式:立即返回,返回值是读到的字节数,可能是0
写数据 write()
- 阻塞模式:等着,直到数据全部写完
- 非阻塞模式:立即返回,返回值是实际写入的字节数,可能比预期少
3.4 该选哪种模式
选择哪种模式主要看你的应用场景:
选择阻塞模式的情况:
- 连接数不多,几十个就够了
- 逻辑简单,不想搞太复杂
- 对性能要求不高,够用就行
选择非阻塞模式的情况:
- 连接数很多,成百上千个
- 要求响应快,不能让用户等太久
- 服务器资源有限,一个线程要干多个活
对于物联网平台来说,设备连接数量通常比较多,而且服务器资源宝贵,所以非阻塞模式用得更多。
4 Channel的连接、读写和关闭操作
4.1 连接操作
4.1.1 服务器端怎么接收连接
java
// 创建服务器通道,相当于开了个门店
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8888)); // 选个门牌号
serverChannel.configureBlocking(false); // 设置成非阻塞,不傻等
// 找个管家(Selector)来帮忙看门
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT); // 告诉管家关注新客户
// 开始营业,无限循环处理客户
while (true) {
if (selector.select() > 0) { // 管家检查有没有事情要处理
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove(); // 处理完就移除,避免重复处理
if (key.isAcceptable()) { // 有新客户要进门
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = server.accept(); // 接待新客户
clientChannel.configureBlocking(false); // 客户也设置成非阻塞
clientChannel.register(selector, SelectionKey.OP_READ); // 关注客户的消息
System.out.println("新客户来了: " + clientChannel.getRemoteAddress());
}
// 处理其他事件...
}
}
}
4.1.2 客户端怎么连接服务器
java
// 创建客户端通道,准备去连接服务器
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false); // 设置非阻塞,连接时不等待
// 也找个管家来帮忙
Selector selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_CONNECT); // 关注连接事件
// 开始尝试连接
socketChannel.connect(new InetSocketAddress("localhost", 8888));
// 等待连接结果
while (true) {
if (selector.select() > 0) { // 检查有没有事件发生
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove();
if (key.isConnectable()) { // 连接有结果了
SocketChannel channel = (SocketChannel) key.channel();
if (channel.isConnectionPending()) { // 连接还在进行中
channel.finishConnect(); // 完成连接
System.out.println("连上服务器了!");
// 现在可以关注读事件了
channel.register(selector, SelectionKey.OP_READ);
// 先打个招呼
ByteBuffer buffer = ByteBuffer.wrap("Hello Server".getBytes());
channel.write(buffer);
}
}
// 处理其他事件...
}
}
}
4.2 读写操作
Channel读写数据都要通过ByteBuffer,这是NIO的特色。就像传菜要用盘子一样,数据传输要用Buffer。
4.2.1 怎么读数据
java
// 准备一个盘子(缓冲区)来装数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 从通道读数据到盘子里
int bytesRead = socketChannel.read(buffer);
if (bytesRead > 0) {
// 翻转盘子,准备取数据(从写模式切换到读模式)
buffer.flip();
// 把盘子里的数据倒出来
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String message = new String(data);
System.out.println("收到消息: " + message);
// 洗干净盘子,准备下次用
buffer.clear();
} else if (bytesRead == -1) {
// 对方挂断了连接
socketChannel.close();
}
4.2.2 怎么写数据
java
// 准备要发送的消息
String message = "Hello Client";
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
// 把数据写到通道里,可能需要多次写入
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
// 更安全的写法,防止写不完
int totalWritten = 0;
int bytesWritten;
while (totalWritten < message.length()) {
bytesWritten = socketChannel.write(buffer);
if (bytesWritten <= 0) {
// 对方接收缓冲区满了,暂时写不进去
break;
}
totalWritten += bytesWritten;
}
4.2.3 分散读取和聚集写入
Channel还有个高级功能:可以同时操作多个Buffer,就像用多个盘子一起传菜:
java
// 分散读取:一次读取分别放到不同的Buffer里
ByteBuffer header = ByteBuffer.allocate(128); // 消息头的盘子
ByteBuffer body = ByteBuffer.allocate(1024); // 消息体的盘子
ByteBuffer[] buffers = {header, body};
long bytesRead = socketChannel.read(buffers); // 一次性读到两个盘子里
// 聚集写入:把多个Buffer的数据一次性写出去
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
// 往两个盘子里装数据...
ByteBuffer[] buffers = {header, body};
long bytesWritten = socketChannel.write(buffers); // 一次性把两个盘子的数据都发出去
4.3 关闭操作
用完Channel记得关闭,不然会浪费系统资源:
java
try {
// 使用Channel做各种操作
// ...
} finally {
if (socketChannel != null && socketChannel.isOpen()) {
socketChannel.close(); // 手动关门
}
}
// 更简单的写法,自动关闭
try (SocketChannel socketChannel = SocketChannel.open()) {
// 使用Channel做各种操作
// ...
} // Java会自动帮你关门
关闭Channel之后:
- 系统资源被释放
- 网络连接断开
- 如果注册了Selector,也会自动取消注册
4.4 异常处理
使用Channel时可能遇到各种问题,要做好异常处理:
java
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 8080));
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
} catch (ConnectException e) {
System.err.println("连不上服务器: " + e.getMessage());
} catch (SocketTimeoutException e) {
System.err.println("连接等太久了: " + e.getMessage());
} catch (IOException e) {
System.err.println("网络出问题了: " + e.getMessage());
} catch (Exception e) {
System.err.println("出了其他问题: " + e.getMessage());
} finally {
// 不管怎样都要清理资源
if (socketChannel != null && socketChannel.isOpen()) {
try {
socketChannel.close();
} catch (IOException e) {
System.err.println("关闭连接时又出错了: " + e.getMessage());
}
}
}
5 总结
Channel就是NIO的核心,它让网络编程变得更灵活。和传统Socket比起来,Channel有这些好处:
- 省资源:一个线程可以管理很多连接,不用每个连接都开一个线程
- 速度快:数据直接在缓冲区里操作,减少了复制
- 能扩展:配合Selector可以处理成千上万的连接
- 控制精细:想怎么操作就怎么操作
如果你要做高并发的系统,比如聊天服务器、游戏服务器,Channel绝对是个好选择。当然,刚开始可能觉得有点复杂,但用熟了就会发现它的强大。
下一篇我们聊聊Buffer,看看它是怎么和Channel配合工作的。