BIO、NIO 和 AIO 的区别?
在 Java 后端开发中,I/O 操作(如网络通信、文件读写)是核心场景——小到接口调用,大到分布式中间件(如 Netty、Kafka),都依赖 I/O 模型的支撑。而 BIO、NIO、AIO 作为 Java 中三种主流 I/O 模型,直接决定了系统的并发能力、资源开销和响应速度。很多人初学时常混淆“同步/异步”“阻塞/非阻塞”,甚至觉得“NIO 就是异步”——其实三者的差异本质是“线程与 I/O
面试口语化表达
好的,面试官。BIO、NIO 和 AIO 的区别主要在于它们的 I/O 处理方式、线程模型和适用场景三个方面。BIO 是同步阻塞的,每个连接需要一个独立线程,适合连接数少的场景,但资源消耗大;
NIO 是同步非阻塞的,用少量线程管理多个连接,适合高并发短连接;
AIO 是异步非阻塞的,由系统通知完成结果,适合长耗时操作。
实际开发中,NIO 应用最广泛,比如 Netty 框架就是基于 NIO 的。
题目解析
这道题如果一般也不会单独问,往往是先问了NIO之后,这道题基本上就是高频必问题了,如果你NIO都不知道的话,那这题也没有问的必要了。BIO、NIO、AIO 是 Java 中处理 I/O 操作的三种模型,理解它们的区别是后端网络编程开发的基础。面试官通过此题考察候选人对 I/O 模型的理解深度、实际应用场景的判断能力以及对并发编程和网络编程的掌握程度。
面试得分点
候选者能清楚的区分同步/异步、阻塞/非阻塞的本质差异。对线程模型有一定的理解,能明确不同 I/O 模型下的线程工作机制。能在实际场景应用中能结合具体的情况说明选择的原因。
题目详细答案
BIO(Blocking I/O)同步阻塞模型
BIO是同步阻塞模型,当线程发起 I/O 操作后,必须等待数据就绪并完成传输才能继续执行。
由于是阻塞模型,因此每个客户端的连接都需要独立的线程来处理,一旦客户端连接数过多,线程资源耗尽后,系统也就崩了。连接数和线程数是1:1的关系。因此缺点也很明显,线程开销大,无法支撑高并发,适用于连接数少且固定的传统应用(如早期 Tomcat 的 BIO 模式)。
// 服务端伪代码
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept(); // 阻塞等待连接
new Thread(() -> {
InputStream in = socket.getInputStream();
in.read(); // 阻塞等待数据读取
// 处理数据...
}).start();
}
NIO(Non-blocking I/O)同步非阻塞模型
特点
同步非阻塞
线程发起 I/O 操作后立即返回,通过轮询(如 Selector)检查数据就绪状态。
多路复用
单线程通过 Selector 监听多个 Channel 的事件(如读、写、连接)。
核心组件
Channel
双向数据传输通道(如 SocketChannel)。
Buffer
数据缓冲区。
Selector
事件监听器,管理多个 Channel。
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT); // 注册连接事件
while (true) {
selector.select(); // 阻塞直到有事件就绪
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) {
// 处理新连接
} else if (key.isReadable()) {
// 读取数据
}
}
}
优势
用少量线程处理大量连接,资源利用率高。
适用场景
高并发短连接(如即时通讯、API 网关),典型框架 Netty。
AIO(Asynchronous I/O)异步非阻塞模型
特点
异步非阻塞
线程发起 I/O 操作后立即返回,操作系统完成数据读写后主动通知应用。
回调机制
通过 CompletionHandler 或 Future 处理结果,无需轮询。
代码示例
AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
@Override
public void completed(AsynchronousSocketChannel client, Void attachment) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
// 处理读取的数据
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) { /* 处理异常 */ }
});
}
@Override
public void failed(Throwable exc, Void attachment) { /* 处理异常 */ }
});
优势
避免轮询开销,适合长耗时 I/O(如文件操作)。
缺点
实现复杂,Linux 支持有限(底层依赖 epoll 模拟实现)。
适用场景
大规模文件读写、长连接业务(实际应用较少,多采用 NIO+多线程替代)。
BIO、NIO、AIO详解
为了清晰对比 Java 中的 BIO、NIO、AIO 三种 I/O 模型,这篇博客会从“生活场景类比”切入,结合核心原理、代码示例和实际应用场景,帮你彻底搞懂三者的差异与适用场景,尤其突出 NIO 成为高并发主流选择的原因。
一、前言:为什么要懂 I/O 模型?
在 Java 后端开发中,I/O 操作(如网络通信、文件读写)是核心场景——小到接口调用,大到分布式中间件(如 Netty、Kafka),都依赖 I/O 模型的支撑。而 BIO、NIO、AIO 作为 Java 中三种主流 I/O 模型,直接决定了系统的并发能力、资源开销和响应速度。
很多人初学时常混淆“同步/异步”“阻塞/非阻塞”,甚至觉得“NIO 就是异步”——其实三者的差异本质是“线程与 I/O 操作的协作方式”不同。这篇文章会用最通俗的方式,带你理清三者的核心逻辑。
二、先搞懂两个关键概念:同步 vs 异步,阻塞 vs 非阻塞
在讲三种模型前,必须先明确两个基础概念——它们是区分 I/O 模型的核心标尺:
维度 |
核心含义 |
同步(Sync) |
线程需要主动等待 I/O 操作完成(比如主动轮询“数据是否就绪”) |
异步(Async) |
线程发起 I/O 后无需等待,由操作系统完成后主动通知线程(回调机制) |
阻塞(Block) |
线程发起 I/O 后,在结果返回前会“卡住”,无法执行其他任务 |
非阻塞(Non-Block) |
线程发起 I/O 后,无论结果是否返回,都会立即返回,可继续执行其他任务 |
简单记:
- 同步/异步:决定“谁来等结果”(线程主动等 vs 操作系统通知);
- 阻塞/非阻塞:决定“等结果时线程能不能干别的”(卡住 vs 继续干活)。
三、BIO:同步阻塞模型——“一个窗口接待一个客户”
BIO(Blocking I/O)是 Java 最早期的 I/O 模型,逻辑最简单,但也最“笨重”。
1. 生活场景类比:银行窗口
想象一家只有“人工窗口”的银行:
- 每个窗口只能接待一个客户,客户办理业务时(如存钱、转账),窗口工作人员必须全程等待客户填完单、确认信息,期间不能接待其他客户;
- 若有10个客户,就需要10个窗口(或客户排队)——对应到程序中,就是“一个客户端连接对应一个线程”,线程必须等待 I/O 操作完成才能处理其他任务。
2. 核心原理:1 连接 → 1 线程,同步阻塞
BIO 的工作流程是“线性的”:
- 线程发起 I/O 操作(如
accept()
等待连接、read()
读取数据); - 在 I/O 操作完成前,线程会阻塞(无法执行其他任务);
- 只有 I/O 操作完成(如连接建立成功、数据读取完毕),线程才能继续执行后续逻辑。
3. 代码示例:BIO 服务端
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class BioServer {
public static void main(String[] args) throws IOException {
// 1. 启动服务端,监听 8080 端口
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("BIO 服务端启动,等待客户端连接...");
while (true) {
// 2. accept() 阻塞:没有新连接时,线程会卡在这
Socket clientSocket = serverSocket.accept();
System.out.println("新客户端连接:" + clientSocket.getRemoteAddress());
// 3. 启动新线程处理该客户端(1 连接 → 1 线程)
new Thread(() -> {
try (
InputStream in = clientSocket.getInputStream();
OutputStream out = clientSocket.getOutputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
PrintWriter writer = new PrintWriter(out, true)
) {
String msg;
// 4. readLine() 阻塞:客户端没发消息时,线程会卡住
while ((msg = reader.readLine()) != null) {
System.out.println("BIO 接收:" + msg);
// 回复客户端
writer.println("BIO 已收到:" + msg);
}
} catch (IOException e) {
System.out.println("客户端断开连接");
}
}).start();
}
}
}
4. 优缺点与适用场景
维度 |
具体说明 |
优点 |
逻辑简单,开发成本低,无需处理复杂的事件监听 |
缺点 |
1. 线程开销大:1000 个连接需要 1000 个线程,内存占用高(每个线程默认 1MB 栈内存); |
适用场景 |
连接数少且固定的场景,如早期的简单 TCP 服务、本地文件读写(非高并发) |
四、NIO:同步非阻塞模型——“一个经理盯多个窗口”
NIO(Non-blocking I/O)是 Java 1.4 引入的模型,核心解决了 BIO“线程爆炸”的问题,靠“多路复用”实现“少量线程处理大量连接”,是目前高并发场景的主流选择。
1. 生活场景类比:银行大堂经理
想象一家“智能化银行”:
- 只需要1个大堂经理(1个线程),不用管具体业务,只负责“盯”所有窗口的状态;
- 客户填单时(数据未就绪),经理可以去看其他窗口;客户填完单举手(数据就绪),经理再安排处理;
- 1个经理就能管理10个窗口——对应到程序中,就是“1个线程通过 Selector 监听多个连接”,线程无需阻塞等待 I/O 操作。
2. 核心原理:多路复用 + 同步非阻塞
NIO 的核心是“事件驱动”,靠三大组件(Channel、Buffer、Selector)协作实现:
- Channel(通道):双向数据传输载体(替代 BIO 的单向流),支持非阻塞模式;
- Buffer(缓冲区):所有数据读写必须经过 Buffer(数据的“临时仓库”);
- Selector(选择器):线程通过 Selector 监听多个 Channel 的“事件”(如“新连接”OP_ACCEPT、“数据可读”OP_READ),仅处理就绪的事件。
工作流程:
- 线程将 Channel 注册到 Selector,并指定监听的事件;
- 调用
selector.select()
阻塞等待事件(无事件时线程阻塞,不占用 CPU); - 有事件就绪时,线程遍历处理这些事件(如接收新连接、读取数据);
- 处理完事件后,线程继续等待下一批事件(无需阻塞在单个连接上)。
3. 代码示例:NIO 服务端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
public class NioServer {
public static void main(String[] args) throws IOException {
// 1. 初始化 ServerSocketChannel(服务端通道)
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8081));
serverChannel.configureBlocking(false); // 关键:开启非阻塞模式
// 2. 初始化 Selector(选择器)
Selector selector = Selector.open();
// 注册“接收新连接”事件到 Selector
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO 服务端启动,等待客户端连接...");
while (true) {
// 3. 阻塞等待事件就绪(无事件时线程阻塞)
int readyCount = selector.select();
if (readyCount == 0) continue;
// 4. 遍历处理就绪事件
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove(); // 必须移除,避免重复处理
if (key.isAcceptable()) {
// 处理新连接
handleAccept(serverChannel, selector);
} else if (key.isReadable()) {
// 处理数据读取
handleRead(key);
}
}
}
}
// 处理新连接:将客户端通道注册到 Selector
private static void handleAccept(ServerSocketChannel serverChannel, Selector selector) throws IOException {
SocketChannel clientChannel = serverChannel.accept(); // 非阻塞:无连接时返回 null
if (clientChannel == null) return;
clientChannel.configureBlocking(false);
// 注册“读事件”,并绑定 Buffer(用于该连接的读写)
clientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("NIO 新客户端连接:" + clientChannel.getRemoteAddress());
}
// 处理数据读取:从客户端通道读取数据并回复
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
int readBytes = clientChannel.read(buffer); // 非阻塞:无数据时返回 0
if (readBytes > 0) {
buffer.flip(); // 切换为读模式
String msg = StandardCharsets.UTF_8.decode(buffer).toString().trim();
System.out.println("NIO 接收:" + msg);
// 回复客户端
buffer.clear();
buffer.put(StandardCharsets.UTF_8.encode("NIO 已收到:" + msg));
buffer.flip();
clientChannel.write(buffer);
} else if (readBytes < 0) {
// 客户端断开连接
key.cancel();
clientChannel.close();
System.out.println("NIO 客户端断开连接");
}
}
}
4. 优缺点与适用场景
维度 |
具体说明 |
优点 |
1. 线程利用率高:少量线程(如 1-4 个)即可处理 thousands 级连接; |
缺点 |
开发复杂度高:需手动处理 Buffer 翻转、事件注册、异常关闭等细节; |
适用场景 |
高并发短连接场景,如即时通讯(IM)、API 网关、分布式中间件(Netty、Kafka、Elasticsearch) |
五、AIO:异步非阻塞模型——“客户线上提交,办好通知”
AIO(Asynchronous I/O)是 Java 7 引入的模型,核心是“异步回调”——线程发起 I/O 后无需轮询,由操作系统完成后主动通知线程,是理论上“最高效”的 I/O 模型。
1. 生活场景类比:线上银行App
想象一家“纯线上银行”:
- 客户在 App 上提交贷款申请(线程发起 I/O 操作),无需等待审核结果,可直接退出 App 干别的;
- 银行审核完成后(操作系统处理完 I/O),会通过短信通知客户(回调机制);
- 客户全程无需“盯进度”——对应到程序中,线程发起 I/O 后立即返回,结果由回调函数处理。
2. 核心原理:异步回调 + 非阻塞
AIO 的工作流程是“异步触发”:
- 线程发起 I/O 操作(如
accept()
、read()
),并指定“回调函数”; - 线程立即返回,继续执行其他任务(非阻塞);
- 操作系统在后台完成 I/O 操作(如建立连接、读取数据);
- I/O 完成后,操作系统会唤醒线程,执行预设的回调函数(处理结果或异常)。
3. 代码示例:AIO 服务端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
public class AioServer {
public static void main(String[] args) throws IOException {
// 1. 初始化异步服务端通道
AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8082));
System.out.println("AIO 服务端启动,等待客户端连接...");
// 2. 发起异步 accept,指定回调函数
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
// 连接建立成功时触发
@Override
public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
// 继续接收下一个连接(异步操作需手动循环)
serverChannel.accept(null, this);
System.out.println("AIO 新客户端连接:" + clientChannel);
// 3. 发起异步 read,指定回调函数
ByteBuffer buffer = ByteBuffer.allocate(1024);
clientChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
// 数据读取成功时触发
@Override
public void completed(Integer readBytes, ByteBuffer buffer) {
if (readBytes > 0) {
buffer.flip();
String msg = new String(buffer.array(), 0, readBytes);
System.out.println("AIO 接收:" + msg);
// 4. 发起异步 write,回复客户端
String reply = "AIO 已收到:" + msg;
clientChannel.write(ByteBuffer.wrap(reply.getBytes()), null,
new CompletionHandler<Integer, Void>() {
@Override
public void completed(Integer result, Void attachment) {
// 回复完成后,继续读取下一批数据
buffer.clear();
clientChannel.read(buffer, buffer, this);
}
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
} else if (readBytes < 0) {
// 客户端断开连接
try {
clientChannel.close();
System.out.println("AIO 客户端断开连接");
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 读取失败时触发
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
exc.printStackTrace();
}
});
}
// 连接建立失败时触发
@Override
public void failed(Throwable exc, Void attachment) {
exc.printStackTrace();
}
});
// 防止主线程退出(AIO 操作在后台线程执行)
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4. 优缺点与适用场景
维度 |
具体说明 |
优点 |
1. 线程利用率最高:无需轮询事件,线程仅在 I/O 完成后被唤醒; |
缺点 |
1. 实现复杂:回调嵌套多(“回调地狱”),代码可读性差; |
适用场景 |
长耗时 I/O 操作,如大规模文件同步、分布式存储(实际应用较少,多被 NIO+多线程替代) |
六、三者核心差异对比表
为了让你快速掌握关键区别,整理成表格如下:
对比维度 |
BIO(同步阻塞) |
NIO(同步非阻塞) |
AIO(异步非阻塞) |
核心模型 |
1 连接 → 1 线程 |
1 线程 → N 连接(多路复用) |
线程发起后无需干预(异步回调) |
线程角色 |
线程 = 连接处理者(全程阻塞) |
线程 = 事件调度者(仅处理就绪事件) |
线程 = 结果处理器(仅回调时工作) |
关键组件 |
Socket、InputStream/OutputStream |
Channel、Buffer、Selector |
AsynchronousChannel、CompletionHandler |
阻塞点 |
accept()、read()、write() |
selector.select ()(无事件时阻塞) |
无阻塞(发起后立即返回) |
同步 / 异步 |
同步 |
同步 |
异步 |
阻塞 / 非阻塞 |
阻塞 |
非阻塞 |
非阻塞 |
线程利用率 |
极低(大量阻塞等待) |
高(少量线程处理多连接) |
极高(仅回调时占用线程) |
开发复杂度 |
低(无需处理事件) |
中(需管理 Buffer 和事件) |
高(回调嵌套,调试难) |
支持连接数 |
几十到几百(线程上限限制) |
几万到百万(多路复用支撑) |
理论百万级(但生态受限) |
典型框架 / 场景 |
早期 Tomcat、简单 TCP 服务 |
Netty、Kafka、API 网关 |
少量文件同步工具(如自研存储) |
七、实战选择建议:到底该用哪种 I/O 模型?
很多开发者会纠结 “我该选 NIO 还是 AIO?”,其实答案取决于你的业务场景,而非 “技术先进性”。以下是实战中的选择指南:
1. 优先选 BIO 的场景
- 连接数少且固定:比如内部管理系统的后端接口(日均调用量 < 1000)、本地文件读写(非高并发);
- 开发周期短,需求简单:比如快速开发一个 Demo 服务、临时数据同步脚本;
- 团队技术栈有限:若团队对 NIO 不熟悉,BIO 的低复杂度能减少 bug 率。
示例:一个公司内部的 “员工打卡数据统计服务”,每天仅需处理 500 条打卡记录,用 BIO 写一个简单的 Socket 服务即可,无需过度设计。
2. 必选 NIO 的场景
- 高并发短连接:比如即时通讯(IM)的消息推送(每秒 thousands 级连接)、API 网关(接收前端大量请求);
- 分布式中间件开发:比如实现 RPC 框架(如 Dubbo)、消息队列(如自定义轻量级 MQ);
- 需要平衡性能与易用性:NIO 虽比 BIO 复杂,但有成熟框架(Netty)封装,能快速落地,且性能满足 90% 高并发场景。
示例:开发一个支持 10 万用户在线的直播弹幕服务,用 Netty 基于 NIO 实现,既能支撑高并发,又能通过 Netty 的编解码、心跳检测等功能减少开发量。
3. 谨慎选 AIO 的场景
- 长耗时 I/O 操作:比如大文件跨服务器同步(单个文件 > 1GB,传输耗时 > 10 秒)、远程服务调用(如调用第三方 API 耗时 > 3 秒);
- 有成熟异步生态支撑:比如基于 Java 9+ 的 Flow API 或 Spring WebFlux 构建异步服务,且团队能应对回调调试问题;
- 非 Linux 环境:Windows 下 AIO 基于 IOCP 实现,比 Linux 下的 epoll 模拟更原生,可尝试用于 Windows 桌面应用的文件操作。
注意:若你的业务是 “高并发短连接”(如电商秒杀),即使 AIO 理论性能更好,也不建议选 —— 因为 Netty 基于 NIO 优化后的性能已足够支撑,且生态更完善,调试更方便。
八、常见误区解答:避开这些理解陷阱
很多开发者在学习 I/O 模型时,会因概念混淆踩坑,以下是高频误区的澄清:
1. 误区 1:“NIO 是异步 I/O”
澄清:NIO 是 “同步非阻塞”,而非异步。
- 同步的体现:线程需要主动调用 selector.select() 轮询事件,即使是非阻塞,仍需线程 “主动关注” 结果;
- 异步的关键:线程发起 I/O 后无需轮询,由操作系统主动通知(如 AIO 的 CompletionHandler)。
简单记:NIO 的 “非阻塞” 是 “等结果时能干活”,但 “同步” 是 “仍需自己盯结果”;AIO 的 “异步” 是 “不用盯,结果会主动找你”。
2. 误区 2:“AIO 性能比 NIO 好,应该优先用”
澄清:理论上 AIO 性能更好,但实战中 NIO 更实用。
- 性能瓶颈:高并发场景的瓶颈往往是 “业务逻辑处理”(如数据库操作、复杂计算),而非 I/O 模型本身;NIO 配合线程池(如 Netty 的 Worker 线程),已能将 CPU 利用率拉满;
- 生态差距:AIO 缺少成熟框架支撑,而 Netty 对 NIO 的优化(如零拷贝、内存池、避免空轮询)已非常完善,性能接近原生 AIO;
- 调试难度:AIO 的回调嵌套(“回调地狱”)会导致问题定位困难,而 NIO 的事件循环逻辑更线性,便于调试。
3. 误区 3:“用 Netty 就是用 NIO,不需要懂底层”
澄清:懂 NIO 底层能帮你避免 Netty 踩坑。
- 比如 Netty 的 ByteBuf 本质是对 NIO Buffer 的优化,若不懂 flip()/clear() 的逻辑,可能会出现 “数据读不全”“内存泄漏” 问题;
- 又如 Netty 的 Selector 配置(如 SelectorProvider 选择),若不懂 Linux 下 epoll 与 Windows 下 IOCP 的差异,可能在跨平台部署时出现性能问题。
九、总结:I/O 模型的本质是 “线程与 I/O 的协作艺术”
BIO、NIO、AIO 并非 “替代关系”,而是 Java 为不同场景设计的 “协作方案”:
- BIO 是 “简单协作”:用线程换开发效率,适合小场景;
- NIO 是 “高效协作”:用事件驱动换性能,适合高并发;
- AIO 是 “极致协作”:用异步回调换资源利用率,适合长耗时场景。
在实际开发中,不要盲目追求 “技术先进性”,而是先明确你的业务需求(连接数、耗时、开发周期),再选择最合适的模型。对于大多数后端开发者而言,深入掌握 NIO(尤其是 Netty 的使用)是性价比最高的选择 —— 它能覆盖从 “中小并发” 到 “高并发” 的大部分场景,且生态成熟,是后端进阶的核心技能之一。
更多推荐
所有评论(0)