
Channel(通道)是Java NIO的核心概念,你可以把它理解为数据传输的管道。和传统的Stream不同,Channel有几个很实用的特点:
Channel既能读数据,也能写数据,就像一条双向车道。传统的InputStream只能读,OutputStream只能写,需要分别处理。
更重要的是,Channel可以设置成非阻塞模式。什么意思?就是读写数据时不会卡住程序,没数据就继续干别的事,这对高并发应用特别有用。
Channel还能配合Selector使用,一个线程就能管理成百上千个连接。想象一下,以前需要1000个服务员的餐厅,现在只要几个就够了。
从技术实现上看,Channel把操作系统底层的I/O操作包装了一下,让我们用起来更方便。不用关心底层怎么传输数据,调用几个方法就行。
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可以共享一些基础功能,但又能保持自己的特色。
Java NIO提供了好几种Channel,每种都有自己的用途:
Channel类型 | 替代了什么 | 主要用来干什么 |
|---|---|---|
FileChannel | FileInputStream/FileOutputStream | 读写文件 |
SocketChannel | Socket | TCP客户端连接 |
ServerSocketChannel | ServerSocket | TCP服务器监听 |
DatagramChannel | DatagramSocket | UDP通信 |
Pipe.SinkChannel/SourceChannel | PipedOutputStream/PipedInputStream | 线程间传数据 |
做网络编程的话,SocketChannel和ServerSocketChannel用得最多。一个负责连接服务器,一个负责接受连接,配合起来就能搭建高性能的网络应用。
ServerSocketChannel就是服务器端用来接收客户端连接的,可以理解为传统ServerSocket的升级版。最大的区别是,它支持非阻塞操作,不会让程序傻等着。
// 创建ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 绑定端口,就像给门牌号
serverChannel.bind(new InetSocketAddress(8080));
// 设置为非阻塞模式,这是关键
serverChannel.configureBlocking(false);
// 如果需要,还能拿到传统的ServerSocket
ServerSocket serverSocket = serverChannel.socket();SocketChannel是客户端用来连接服务器的,相当于传统Socket的NIO版本。它有两个身份:既可以主动连接服务器,也可以作为服务器接收到的客户端连接。
// 创建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("连接成功!");SocketChannel的主要方法:
在物联网平台的网络通信模块中,ServerSocketChannel和SocketChannel通常用于以下场景:
Channel有两种工作方式:阻塞模式和非阻塞模式。这是NIO比传统I/O强的地方。
简单来说:
特性 | 阻塞模式 | 非阻塞模式 |
|---|---|---|
等待方式 | 傻等着,直到操作完成 | 立即返回,不等结果 |
返回结果 | 返回真实结果 | 可能返回"还没好"的标志 |
线程利用 | 一个线程只能干一件事 | 一个线程能同时处理多件事 |
编程难度 | 简单,符合直觉 | 稍微复杂,需要轮询检查 |
适合场景 | 连接少,要求简单 | 连接多,要求高性能 |
Channel默认是阻塞模式,可以这样切换:
// 设置为非阻塞模式
channel.configureBlocking(false);
// 设置为阻塞模式
channel.configureBlocking(true);
// 检查当前是什么模式
boolean isBlocking = channel.isBlocking();// 阻塞模式的例子
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) {
// 有新连接,处理一下
}不同模式下,SocketChannel的行为也不一样:
连接操作 connect()
读数据 read()
写数据 write()
选择哪种模式主要看你的应用场景:
选择阻塞模式的情况:
选择非阻塞模式的情况:
对于物联网平台来说,设备连接数量通常比较多,而且服务器资源宝贵,所以非阻塞模式用得更多。
// 创建服务器通道,相当于开了个门店
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());
}
// 处理其他事件...
}
}
}// 创建客户端通道,准备去连接服务器
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);
}
}
// 处理其他事件...
}
}
}Channel读写数据都要通过ByteBuffer,这是NIO的特色。就像传菜要用盘子一样,数据传输要用Buffer。
// 准备一个盘子(缓冲区)来装数据
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();
}// 准备要发送的消息
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;
}Channel还有个高级功能:可以同时操作多个Buffer,就像用多个盘子一起传菜:
// 分散读取:一次读取分别放到不同的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); // 一次性把两个盘子的数据都发出去用完Channel记得关闭,不然会浪费系统资源:
try {
// 使用Channel做各种操作
// ...
} finally {
if (socketChannel != null && socketChannel.isOpen()) {
socketChannel.close(); // 手动关门
}
}
// 更简单的写法,自动关闭
try (SocketChannel socketChannel = SocketChannel.open()) {
// 使用Channel做各种操作
// ...
} // Java会自动帮你关门关闭Channel之后:
使用Channel时可能遇到各种问题,要做好异常处理:
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());
}
}
}Channel就是NIO的核心,它让网络编程变得更灵活。和传统Socket比起来,Channel有这些好处:
如果你要做高并发的系统,比如聊天服务器、游戏服务器,Channel绝对是个好选择。当然,刚开始可能觉得有点复杂,但用熟了就会发现它的强大。
下一篇我们聊聊Buffer,看看它是怎么和Channel配合工作的。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。