系列: 从 0 开始学习计算机网络
适合读者: 计算机专业学生、Java 后端学习者、准备学习 Netty 的开发者
本文关键词: BIO、NIO、AIO、Socket、Channel、Buffer、Selector、Reactor、Netty
一、引言:为什么服务端连接多了,线程也跟着多?
假设你写了一个简单的 Java TCP 服务端:客户端连进来后,服务端读取客户端发送的数据,再返回响应。
最直观的实现方式是:
- 服务端等待客户端连接;
- 接受一个连接;
- 读取客户端数据;
- 处理并返回结果;
- 继续处理其他连接。
问题在于:如果某个客户端连接建立后一直不发送数据,服务端读取数据的线程可能就会一直等待。若每个连接都分配一个线程,大量空闲连接也会占用线程资源。
这就引出了 Java 网络编程中经常出现的几个词:
- BIO: 常用来指传统的阻塞式 I/O 编程方式;
- NIO: Java New I/O,提供 Channel、Buffer、Selector 等能力,可用于非阻塞 I/O 和 I/O 多路复用;
- AIO: Java 异步 I/O,通过 Future 或完成回调接收异步操作结果。
本文不再重复 TCP 握手、HTTP、WebSocket 或 NAT,而是从 Java 的 Socket 编程出发,理解服务端如何管理多个连接,以及 Netty 为什么会使用事件循环模型。
二、先理解 Socket:Java 程序如何建立 TCP 通信?
Socket 可以理解为应用程序使用传输层通信能力时操作的端点抽象。对于 TCP 服务端,Java 最常见的两个类是:
ServerSocket:在指定端口监听连接,并接受客户端连接;Socket:表示一条已经建立的 TCP 连接,程序可以通过它的输入流和输出流收发字节。
最简单的服务端逻辑如下:
java
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket socket = serverSocket.accept();
// 通过 socket 获取输入流和输出流
}
其中:
new ServerSocket(8080):创建并绑定一个监听端口为 8080 的服务端 Socket。accept():等待客户端连接。没有连接到来时,调用线程会阻塞等待。Socket:accept()成功返回后,代表与某个客户端建立的连接。
注意:监听 Socket 和连接 Socket 不是同一个概念。 ServerSocket 负责接收连接请求;每次 accept() 成功后,都会得到一个用于和对应客户端通信的 Socket。
三、BIO:一个常见连接对应一个工作线程
BIO 是 Blocking I/O 的常见简称。它的典型特点是:当线程执行某些 I/O 操作而当前条件不满足时,线程会等待,直到操作可以继续或发生超时、异常等情况。
3.1 BIO 服务端的工作过程
一种常见的 BIO 服务端采用"一个连接交给一个工作线程处理"的方式:
- 主线程在
accept()处等待连接; - 客户端连接到来后,
accept()返回一个Socket; - 服务端把这个
Socket交给工作线程; - 工作线程从输入流读取数据;
- 如果客户端暂时没有发送数据,工作线程可能阻塞在读取操作上;
- 客户端发送数据后,工作线程继续处理并返回响应。

3.2 一个可运行的 BIO 回声服务端
下面的例子使用 JDK 17,不依赖第三方框架。客户端发送一行文本,服务端读取后返回一行 echo 响应。
java
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class BioEchoServer {
public static void main(String[] args) throws IOException {
int port = 18080;
ThreadPoolExecutor pool = new ThreadPoolExecutor(
8,
32,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.AbortPolicy()
);
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("BIO server started on port " + port);
while (true) {
// 没有客户端连接时,accept() 会阻塞等待
Socket client = serverSocket.accept();
try {
// 一个工作任务负责处理一条连接
pool.execute(() -> handleClient(client));
} catch (RejectedExecutionException e) {
// 线程池和队列都无法接收任务时,关闭刚接收的连接
client.close();
}
}
} finally {
pool.shutdown();
}
}
private static void handleClient(Socket client) {
try (
Socket socket = client;
BufferedReader reader = new BufferedReader(
new InputStreamReader(
socket.getInputStream(),
StandardCharsets.UTF_8
)
);
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream(),
StandardCharsets.UTF_8
)
)
) {
String line;
// readLine() 在没有读到换行符或流结束时会继续等待
while ((line = reader.readLine()) != null) {
writer.write("echo: " + line);
writer.newLine();
writer.flush();
}
} catch (IOException e) {
System.err.println("Client connection closed: " + e.getMessage());
}
}
}
3.3 重点理解这段代码
第一处:accept()
java
Socket client = serverSocket.accept();
它负责等待新连接。没有客户端连接时,执行它的主线程会阻塞。
第二处:线程池
java
pool.execute(() -> handleClient(client));
连接被接受后,主线程不需要亲自读取客户端数据,而是把处理任务交给线程池。
这里使用有界队列,避免任务无限制堆积。线程池和队列都满时,代码关闭新接收的连接,而不是继续无条件创建线程。
第三处:readLine()
java
while ((line = reader.readLine()) != null) {
readLine() 会等待一行数据结束。客户端如果一直保持连接但不发送换行符,工作线程就可能持续等待。
这正是 BIO 连接处理方式需要关注的问题:线程可能被长时间占用在 I/O 等待上。
3.4 BIO 的问题在哪里?
假设有很多客户端连接,其中不少连接只是保持在线,暂时没有发送数据。
如果采用一连接一工作线程的处理方式,那么大量工作线程可能都在等待输入。线程本身需要内存、调度和上下文切换等资源,线程数量过多会增加系统开销。
但要注意:
- BIO 并不是"不能处理并发";
- 使用线程池可以控制并发工作线程数量;
- 真正的问题是:当连接数远大于可用工作线程数时,连接处理能力和资源使用会受到限制;
- BIO 也适合连接规模较小、业务逻辑简单的场景。
四、NIO:用 Channel、Buffer 和 Selector 管理连接
NIO 是 Java New I/O 的简称。它从 JDK 1.4 开始提供,核心组件包括:
| 组件 | 作用 | 可以先怎么理解 |
|---|---|---|
| Channel | 与文件、Socket 等 I/O 实体进行数据读写 | 数据传输通道 |
| Buffer | 存放读入或待写出的数据 | 临时数据容器 |
| Selector | 监控注册到它的多个 Channel 是否有感兴趣的 I/O 操作就绪 | 事件监听器 |
需要纠正一个常见误区:NIO 不等于所有操作都自动非阻塞。 SocketChannel 等可选择通道默认可以处于阻塞模式;只有调用 configureBlocking(false) 配置为非阻塞模式,才能注册到 Selector 进行多路复用。
4.1 Channel:负责数据读写
在 TCP 服务端中,常用两个类:
ServerSocketChannel:监听并接受新的客户端连接;SocketChannel:负责与已经连接的客户端交换数据。
例如:
java
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8081));
SocketChannel clientChannel = serverChannel.accept();
这里的 accept() 仍然可能阻塞,因为新创建的 Channel 默认处于阻塞模式。后面使用 Selector 时,需要把它配置为非阻塞模式。
4.2 Buffer:数据读写的临时容器
在传统 BIO 中,我们经常通过输入流、输出流读写数据;NIO 的 Channel 通常通过 Buffer 与程序交换数据。
最常用的是 ByteBuffer。
一个 Buffer 需要理解四个属性:
| 属性 | 含义 |
|---|---|
| capacity | 容量,Buffer 最多能容纳多少元素 |
| position | 当前读写位置 |
| limit | 当前允许读写的边界 |
| mark | 可选标记位置,用于之后恢复 |
初学阶段最重要的是 position、limit 和 capacity。
put、flip、get、clear 是什么关系?
java
ByteBuffer buffer = ByteBuffer.allocate(10);
// 写入数据
buffer.put("hello".getBytes(StandardCharsets.UTF_8));
// 切换为读取模式
buffer.flip();
// 读取数据
while (buffer.hasRemaining()) {
byte b = buffer.get();
System.out.print((char) b);
}
// 清空状态,准备下一次写入
buffer.clear();
这段代码的执行过程:
allocate(10):创建容量为 10 字节的 Buffer。put():把数据写入 Buffer,position随写入移动。flip():把当前position设置为limit,再把position归零,准备读取刚才写入的数据。get():从当前position读取数据,并推进位置。clear():将position设为 0、limit设为capacity,准备再次写入。
注意:clear() 只是重置 Buffer 的位置和边界,并不会把底层字节逐个清零。

4.3 Selector:一个线程关注多个连接的 I/O 就绪事件
Selector 是 NIO 中非常关键的组件。
它允许一个线程注册多个非阻塞 Channel,并关注指定的 I/O 事件。例如:
OP_ACCEPT:服务端 Socket 已经可以接受新连接;OP_CONNECT:客户端连接操作已经就绪;OP_READ:Channel 已经可以进行读取;OP_WRITE:Channel 已经可以进行写入。
Selector 并不是替应用程序把所有数据自动读取完,而是告诉应用程序:哪些 Channel 的哪些操作已经就绪,接下来可以处理了。
可以把它理解为一个前台调度员:
- BIO:每个工作人员盯着自己的一个窗口,有人来就处理;没人来时可能一直等待。
- NIO:一个调度员观察多个窗口,哪个窗口有事情就通知程序去处理。
4.4 NIO 服务端的基本工作流程
- 创建
ServerSocketChannel; - 设置为非阻塞模式;
- 创建
Selector; - 将服务端 Channel 注册到 Selector,关注
OP_ACCEPT; - 调用
select()等待事件; - 取出就绪的
SelectionKey; - 根据事件类型处理连接、读取或写入;
- 处理完成后继续等待下一批事件。
五、用 Java NIO 写一个非阻塞 Echo 服务端
下面的代码使用 JDK 17 自带的 NIO API,不需要引入 Netty 或其他第三方依赖。
为了突出 Selector 的工作方式,这个服务端实现一个简单的字节回显功能:客户端发送什么字节,服务端就把读到的字节写回去。
5.1 NIO 服务端代码
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class NioEchoServer {
public static void main(String[] args) throws IOException {
int port = 8081;
try (
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open()
) {
// 1. 设置非阻塞模式
serverChannel.configureBlocking(false);
// 2. 绑定端口
serverChannel.bind(new InetSocketAddress(port));
// 3. 注册 OP_ACCEPT 事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO server started on port " + port);
// 4. 事件循环
while (true) {
selector.select();
Iterator<SelectionKey> iterator =
selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (!key.isValid()) {
continue;
}
try {
if (key.isAcceptable()) {
acceptConnection(serverChannel, selector);
} else if (key.isReadable()) {
readData(key);
} else if (key.isWritable()) {
writeData(key);
}
} catch (IOException e) {
closeKey(key);
}
}
}
}
}
// 接受客户端连接
private static void acceptConnection(
ServerSocketChannel serverChannel,
Selector selector
) throws IOException {
SocketChannel clientChannel = serverChannel.accept();
if (clientChannel == null) {
return;
}
// 客户端 Channel 也必须设置为非阻塞模式
clientChannel.configureBlocking(false);
// 每个客户端连接关联一个 Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 注册读事件,并把 Buffer 作为附件保存
clientChannel.register(
selector,
SelectionKey.OP_READ,
buffer
);
}
// 读取客户端数据
private static void readData(SelectionKey key) throws IOException {
SocketChannel clientChannel =
(SocketChannel) key.channel();
ByteBuffer buffer =
(ByteBuffer) key.attachment();
// 准备写入新读到的数据
buffer.clear();
int readBytes = clientChannel.read(buffer);
if (readBytes == -1) {
closeKey(key);
return;
}
if (readBytes == 0) {
return;
}
// 切换为读取模式,准备把刚读到的字节写回客户端
buffer.flip();
clientChannel.write(buffer);
// 如果一次没有写完,暂时关注 OP_WRITE
if (buffer.hasRemaining()) {
key.interestOps(
(key.interestOps() & ~SelectionKey.OP_READ)
| SelectionKey.OP_WRITE
);
} else {
// 数据已经全部写出,准备下一次读取
buffer.clear();
}
}
// 继续写出上一次没有写完的数据
private static void writeData(SelectionKey key) throws IOException {
SocketChannel clientChannel =
(SocketChannel) key.channel();
ByteBuffer buffer =
(ByteBuffer) key.attachment();
clientChannel.write(buffer);
if (!buffer.hasRemaining()) {
// 写完后恢复 Buffer 写入状态
buffer.clear();
// 取消写事件关注,重新关注读事件
key.interestOps(
(key.interestOps() & ~SelectionKey.OP_WRITE)
| SelectionKey.OP_READ
);
}
}
// 关闭连接
private static void closeKey(SelectionKey key) {
key.cancel();
try {
key.channel().close();
} catch (IOException ignored) {
}
}
}
5.1.1 用同一个 Socket 客户端验证两个服务端
BIO 和 NIO 服务端都使用 TCP,因此可以用同一个普通 Socket 客户端连接它们。这里发送一行文本,客户端等待服务端返回换行结束的响应。
java
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class EchoClient {
public static void main(String[] args) throws IOException {
if (args.length != 1) {
System.out.println("Usage: java EchoClient <port>");
return;
}
int port = Integer.parseInt(args[0]);
try (
Socket socket = new Socket("127.0.0.1", port);
BufferedReader reader = new BufferedReader(
new InputStreamReader(
socket.getInputStream(),
StandardCharsets.UTF_8
)
);
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
socket.getOutputStream(),
StandardCharsets.UTF_8
)
)
) {
writer.write("hello");
writer.newLine();
writer.flush();
System.out.println(reader.readLine());
}
}
}
将三个 Java 文件放在同一个目录,使用 JDK 17 编译:
bash
javac --release 17 BioEchoServer.java NioEchoServer.java EchoClient.java
先启动 BIO 服务端,再在另一个终端运行客户端:
bash
java BioEchoServer
java EchoClient 18080
测试 NIO 时,先停止 BIO 服务端,再启动 NIO 服务端并连接它:
bash
java NioEchoServer
java EchoClient 8081
客户端通过 readLine() 等待一行响应,因此服务端回显的数据中需要包含换行符。这个客户端只用于本地教学验证,不是生产级客户端。
5.2 按执行顺序理解关键代码
第一步:创建 Selector 和服务端 Channel
java
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
Selector 用于管理注册到它的 Channel;ServerSocketChannel 用于监听客户端连接。
第二步:设置非阻塞模式
java
serverChannel.configureBlocking(false);
如果没有设置为非阻塞模式,就不能将这个可选择 Channel 注册到 Selector。
第三步:注册连接事件
java
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
告诉 Selector:关注服务端是否有新连接可以接受。
第四步:等待事件
java
selector.select();
当前线程会等待,直到至少有一个已注册的通道发生感兴趣的就绪事件,或者发生其他唤醒情况。
注意:select() 本身可以阻塞等待。这并不与 NIO 的非阻塞 Channel 矛盾。非阻塞指的是具体 Channel 的读写操作不会因为暂时没有数据而一直阻塞;Selector 则负责等待事件发生。
第五步:判断事件类型
java
if (key.isAcceptable()) {
acceptConnection(serverChannel, selector);
} else if (key.isReadable()) {
readData(key);
} else if (key.isWritable()) {
writeData(key);
}
isAcceptable():有新连接可以接受;isReadable():可以尝试读取数据;isWritable():可以继续写出数据。
第六步:处理部分写入
非阻塞写操作不保证一次写完全部数据。代码在 Buffer 还有剩余字节时,注册 OP_WRITE,等下一次写就绪事件到来后继续发送。
这里还暂时取消了 OP_READ,避免旧数据尚未写完时又把同一个 Buffer 清空并覆盖。写完后再恢复 OP_READ。
5.3 一个重要提醒:TCP 是字节流,不是消息队列
这个 Echo 示例只是把每次读取到的字节原样写回,并没有定义完整的应用层消息协议。
TCP 提供的是有序字节流。一次 read() 可能只读到一部分数据,也可能读到多段连续数据;不能假设一次读取就对应客户端的一次 send()。
如果要实现聊天协议、命令协议或 HTTP 解析,需要自行设计消息边界,例如:
- 固定长度消息;
- 分隔符协议,例如以换行符结束;
- 长度字段协议,例如消息头中先声明正文长度。
这也是为什么真实网络框架除了处理 I/O,还需要处理拆包、粘包、协议编解码和缓冲区管理。
六、BIO、NIO、AIO 到底有什么区别?
先看一张对比表:
| 对比维度 | BIO | NIO(Selector 多路复用) | AIO |
|---|---|---|---|
| 常见 API | Socket、ServerSocket |
SocketChannel、ServerSocketChannel、Selector |
AsynchronousSocketChannel、AsynchronousServerSocketChannel |
| 常见 I/O 方式 | 阻塞式 | 非阻塞 + 就绪事件通知 | 异步操作完成通知 |
| 数据处理 | 流 | Channel + Buffer | 异步 Channel + Buffer |
| 线程组织 | 常见一连接一工作线程 | 少量事件循环线程管理多个连接 | 发起操作后由 Future 或回调接收完成结果 |
| 适用场景 | 简单、连接规模较小的服务 | 连接较多、需要 I/O 多路复用的服务 | 需要异步完成通知的 I/O 场景 |
6.1 阻塞 I/O 和非阻塞 I/O
阻塞 I/O:
线程调用读取方法后,如果当前没有可读数据,线程会等待。
非阻塞 I/O:
线程调用读取方法后,如果当前没有数据可读,方法可以立即返回,不必一直等到数据到来。
在 Java NIO 中,非阻塞 SocketChannel.read() 在当前没有数据可读时可以返回 0;如果连接已经到达输入流末尾,则返回 -1。
6.2 同步 I/O 和异步 I/O
这是另一个维度,不能与阻塞、非阻塞简单画等号。
- 同步 I/O: 应用程序发起读写后,需要由应用程序在相应调用流程中取得 I/O 结果。
- 异步 I/O: 应用程序发起操作后可以先返回,等操作完成后通过 Future 或回调收到完成结果。
Java NIO Selector 的典型工作方式是:Selector 告诉应用程序某个 Channel 已经就绪,应用程序再调用 read() 或 write()。因此,I/O 多路复用不等于异步 I/O 完成通知。
6.3 AIO 是怎么工作的?
Java 提供了 AsynchronousSocketChannel 等异步通道。可以使用 Future 获取结果,也可以传入 CompletionHandler,在操作完成或失败时接收通知。
简化示意:
java
channel.read(
buffer,
null,
new CompletionHandler<Integer, Void>() {
@Override
public void completed(Integer result, Void attachment) {
System.out.println("读取操作完成,字节数:" + result);
}
@Override
public void failed(Throwable exc, Void attachment) {
System.err.println("读取失败:" + exc.getMessage());
}
}
);
这里的 read() 是发起一次异步读取操作,completed() 在操作完成后被调用,failed() 在操作失败时被调用。
注意:回调中拿到的读取字节数也可能是 -1,表示已经到达输入流末尾;如果需要持续读取,还要在回调中根据结果和连接状态决定是否发起下一次读取。

七、Reactor 模型:NIO 如何组织高并发网络服务?
理解了 Selector 之后,可以再认识一个常见的服务端设计模式:Reactor(反应器)模式。
Reactor 的核心思想是:
- 由事件循环等待 I/O 就绪事件;
- 当某个连接有可处理的事件时,分发给对应的处理逻辑;
- 处理逻辑读取数据、解析协议、执行后续业务;
- 需要写回时,再注册或处理写事件。
它把"等待事件"和"处理业务"分开组织,使一个事件循环可以管理多个连接。
7.1 为什么不能在 I/O 线程里执行很慢的业务?
假设一个 NIO EventLoop 负责管理很多客户端连接。如果其中一个连接触发了读取事件,而处理代码在 EventLoop 线程里执行了一个很慢的数据库查询,那么这个线程就无法及时处理其他连接的事件。
因此,真实服务端通常会区分:
- I/O 线程: 处理连接、读写数据、协议编解码等相对短小的工作;
- 业务线程池: 执行可能耗时的业务逻辑,例如数据库访问、复杂计算或调用外部服务。
这不是说所有业务都必须切换线程。线程切换也有成本,需要根据业务耗时和并发情况设计。
7.2 Netty 与 NIO 有什么关系?
Netty 是 Java 生态中常见的高性能网络通信框架。它在 NIO 等传输实现之上封装了连接管理、事件循环、缓冲区、编解码器和处理器链等能力。
在典型的 Netty TCP 服务端中,常见结构包括:
- Boss EventLoopGroup: 负责接受客户端连接;
- Worker EventLoopGroup: 负责处理已接入连接上的 I/O 事件;
- ChannelPipeline: 按顺序组织多个 ChannelHandler,处理入站和出站事件;
- EventLoop: 负责某个 Channel 注册后的 I/O 事件和相关任务。
并不是一个连接就一定对应一个独立线程。一个 EventLoop 通常可以处理多个 Channel,具体线程数量和分配方式由 EventLoopGroup 的实现与配置决定。
如果 Handler 中执行长时间阻塞操作,就可能拖慢同一个 EventLoop 所管理的其他连接。因此,Netty 开发中要避免在 I/O 事件线程里执行耗时阻塞任务,必要时将其交给专门的业务线程池。
八、实际开发中应该如何选择?
不要简单认为 NIO 一定比 BIO 快,或者 AIO 一定比 NIO 高级。
可以从以下角度判断:
| 场景 | 可以优先考虑 | 原因 |
|---|---|---|
| 简单 TCP 工具、连接量较小 | BIO | 代码直观,开发和调试成本较低 |
| 需要维护大量长连接 | NIO / Netty | 事件循环与 I/O 多路复用有助于减少大量线程等待 |
| 需要异步 I/O 完成通知 | AIO API | 可以使用 Future 或 CompletionHandler 表达异步完成 |
| 复杂网络协议、生产级通信服务 | Netty 等成熟框架 | 已封装连接管理、编解码、事件分发和资源管理 |
真正的性能还取决于:
- 业务逻辑是否耗 CPU;
- 数据库和外部服务是否成为瓶颈;
- 连接数、消息大小和消息频率;
- 线程池、缓冲区和内存使用;
- 操作系统、JDK 和网络环境;
- 是否正确处理背压、超时和异常。
如果瓶颈是数据库查询,单纯把 BIO 换成 NIO 并不能让数据库变快。如果业务线程被大量阻塞,应该先确认阻塞发生在哪里,再决定是否调整线程模型。
九、常见误区
误区 1:NIO 就是完全不阻塞
不准确。NIO 的 Channel 可以配置为阻塞或非阻塞模式;使用 Selector 多路复用时,通常把可选择 Channel 配置为非阻塞。Selector.select() 本身仍然可以阻塞等待事件。
误区 2:Selector 会自动帮我们把数据读取出来
错误。Selector 主要负责通知哪些注册的 I/O 操作已经就绪,应用程序还需要通过 Channel 执行读写。
误区 3:一次 read() 就能读到一条完整消息
错误。TCP 是字节流,没有应用层消息边界。应用程序必须自行设计和解析消息格式。
误区 4:NIO 就是异步 I/O
不准确。Selector 提供的是就绪事件通知,应用程序随后仍然要调用读写方法;Java 的 AsynchronousSocketChannel 才是异步通道 API。
误区 5:用了 NIO,就不需要线程池
错误。NIO 可以减少 I/O 等待场景中对大量线程的依赖,但业务处理、数据库访问、复杂计算仍可能需要独立的执行资源。
误区 6:NIO 一定比 BIO 性能好
不一定。NIO 的事件循环、Buffer 管理和状态维护会增加代码复杂度。对于连接规模小、业务简单的程序,BIO 可能更容易维护。是否有性能收益,需要结合实际负载进行测试。
十、本篇总结:把 Java 网络 I/O 串起来
本篇是整个计算机网络系列的最后一篇。重点不是记住几个 API 名称,而是理解 Java 程序如何管理网络连接:
ServerSocket和Socket是传统 TCP 阻塞式网络编程的常见入口。- BIO 的典型实现是一个连接交给一个工作线程处理,读取数据时线程可能阻塞。
- NIO 通过 Channel、Buffer 和 Selector 提供非阻塞 I/O 与多路复用能力。
ByteBuffer的flip()用于从写入模式切换到读取模式,clear()用于重置状态以准备下一次写入。- Selector 负责报告就绪事件,应用程序仍然需要调用 Channel 完成实际读写。
- AIO 通过 Future 或 CompletionHandler 接收异步操作完成结果。
- Reactor 和 Netty 把连接事件、I/O 处理与业务逻辑组织起来,是理解高并发 Java 网络服务的重要基础。
整个系列学完后,建议继续学习什么?
计算机网络知识可以继续沿着 Java 后端方向深入:
- Netty: 从 NIO 基础进入成熟网络框架,学习 EventLoop、ChannelPipeline、ByteBuf 和编解码器。
- Linux 网络 I/O: 理解 select、poll、epoll,以及它们与 Java Selector 的关系。
- 服务端性能分析: 学习线程模型、连接数、吞吐量、延迟、背压和压测方法。
- 分布式通信: 结合 RPC、消息队列和微服务,理解网络通信在真实后端系统中的作用。
*最后记住:网络 I/O 的核心不只是把数据发出去,还要理解线程如何等待、连接如何管理,以及数据如何被安全、完整地处理。
到这里,「从 0 开始学习计算机网络」15 篇系列就全部结束了。
我是程序员大海,我们下期不见不散.
参考资料
- Oracle Java SE 17:ServerSocket ------ 服务端 Socket 的监听与
accept()行为。 - Oracle Java SE 17:SocketChannel ------ TCP Socket Channel 的连接、读写与非阻塞连接操作。
- Oracle Java SE 17:ServerSocketChannel ------ 服务端 Channel 的绑定、接受连接和非阻塞行为。
- Oracle Java SE 17:java.nio.channels 包说明 ------ Channel、Selector 和 I/O 多路复用机制。
- Oracle Java SE 17:Buffer ------
position、limit、capacity、flip()和clear()。 - Oracle Java SE 17:AsynchronousSocketChannel ------ Java 异步 Socket 读写 API。
- Netty 4.x User Guide ------ EventLoopGroup、Channel 和 Handler 的基本使用方式。
代码说明: BIO 和 NIO 示例面向 JDK 17,不依赖第三方库。示例用于解释连接处理与 I/O 机制;实际生产系统还需要补充协议拆包、身份认证、连接超时、优雅关闭、背压和日志监控等设计。