【计算机网络】第 15 篇:Java BIO、NIO 与高并发网络通信——从 Socket 理解 I/O 模型

系列: 从 0 开始学习计算机网络

适合读者: 计算机专业学生、Java 后端学习者、准备学习 Netty 的开发者

本文关键词: BIO、NIO、AIO、Socket、Channel、Buffer、Selector、Reactor、Netty

一、引言:为什么服务端连接多了,线程也跟着多?

假设你写了一个简单的 Java TCP 服务端:客户端连进来后,服务端读取客户端发送的数据,再返回响应。

最直观的实现方式是:

  1. 服务端等待客户端连接;
  2. 接受一个连接;
  3. 读取客户端数据;
  4. 处理并返回结果;
  5. 继续处理其他连接。

问题在于:如果某个客户端连接建立后一直不发送数据,服务端读取数据的线程可能就会一直等待。若每个连接都分配一个线程,大量空闲连接也会占用线程资源。

这就引出了 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 服务端采用"一个连接交给一个工作线程处理"的方式:

  1. 主线程在 accept() 处等待连接;
  2. 客户端连接到来后,accept() 返回一个 Socket;
  3. 服务端把这个 Socket 交给工作线程;
  4. 工作线程从输入流读取数据;
  5. 如果客户端暂时没有发送数据,工作线程可能阻塞在读取操作上;
  6. 客户端发送数据后,工作线程继续处理并返回响应。

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();

这段代码的执行过程:

  1. allocate(10):创建容量为 10 字节的 Buffer。
  2. put():把数据写入 Buffer,position 随写入移动。
  3. flip():把当前 position 设置为 limit,再把 position 归零,准备读取刚才写入的数据。
  4. get():从当前 position 读取数据,并推进位置。
  5. 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 服务端的基本工作流程

  1. 创建 ServerSocketChannel;
  2. 设置为非阻塞模式;
  3. 创建 Selector;
  4. 将服务端 Channel 注册到 Selector,关注 OP_ACCEPT;
  5. 调用 select() 等待事件;
  6. 取出就绪的 SelectionKey;
  7. 根据事件类型处理连接、读取或写入;
  8. 处理完成后继续等待下一批事件。

五、用 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 的核心思想是:

  1. 由事件循环等待 I/O 就绪事件;
  2. 当某个连接有可处理的事件时,分发给对应的处理逻辑;
  3. 处理逻辑读取数据、解析协议、执行后续业务;
  4. 需要写回时,再注册或处理写事件。

它把"等待事件"和"处理业务"分开组织,使一个事件循环可以管理多个连接。

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 程序如何管理网络连接:

  1. ServerSocket 和 Socket 是传统 TCP 阻塞式网络编程的常见入口。
  2. BIO 的典型实现是一个连接交给一个工作线程处理,读取数据时线程可能阻塞。
  3. NIO 通过 Channel、Buffer 和 Selector 提供非阻塞 I/O 与多路复用能力。
  4. ByteBuffer 的 flip() 用于从写入模式切换到读取模式,clear() 用于重置状态以准备下一次写入。
  5. Selector 负责报告就绪事件,应用程序仍然需要调用 Channel 完成实际读写。
  6. AIO 通过 Future 或 CompletionHandler 接收异步操作完成结果。
  7. Reactor 和 Netty 把连接事件、I/O 处理与业务逻辑组织起来,是理解高并发 Java 网络服务的重要基础。

整个系列学完后,建议继续学习什么?

计算机网络知识可以继续沿着 Java 后端方向深入:

  • Netty: 从 NIO 基础进入成熟网络框架,学习 EventLoop、ChannelPipeline、ByteBuf 和编解码器。
  • Linux 网络 I/O: 理解 select、poll、epoll,以及它们与 Java Selector 的关系。
  • 服务端性能分析: 学习线程模型、连接数、吞吐量、延迟、背压和压测方法。
  • 分布式通信: 结合 RPC、消息队列和微服务,理解网络通信在真实后端系统中的作用。

*最后记住:网络 I/O 的核心不只是把数据发出去,还要理解线程如何等待、连接如何管理,以及数据如何被安全、完整地处理。

到这里,「从 0 开始学习计算机网络」15 篇系列就全部结束了。

我是程序员大海,我们下期不见不散.

参考资料

  1. Oracle Java SE 17:ServerSocket ------ 服务端 Socket 的监听与 accept() 行为。
  2. Oracle Java SE 17:SocketChannel ------ TCP Socket Channel 的连接、读写与非阻塞连接操作。
  3. Oracle Java SE 17:ServerSocketChannel ------ 服务端 Channel 的绑定、接受连接和非阻塞行为。
  4. Oracle Java SE 17:java.nio.channels 包说明 ------ Channel、Selector 和 I/O 多路复用机制。
  5. Oracle Java SE 17:Buffer ------ position、limit、capacity、flip() 和 clear()。
  6. Oracle Java SE 17:AsynchronousSocketChannel ------ Java 异步 Socket 读写 API。
  7. Netty 4.x User Guide ------ EventLoopGroup、Channel 和 Handler 的基本使用方式。

代码说明: BIO 和 NIO 示例面向 JDK 17,不依赖第三方库。示例用于解释连接处理与 I/O 机制;实际生产系统还需要补充协议拆包、身份认证、连接超时、优雅关闭、背压和日志监控等设计。

相关推荐
geovindu2 小时前
rust: Semaphore Pattern
开发语言·后端·设计模式·rust·代理模式·企业级信号量模式·同步型模式
垆边人似月.2 小时前
RDMA 文件传输踩坑(四):从固定延时改造为 READY/ACK 协议
linux·服务器·网络
MicrosoftCloud3 小时前
计网 11|NAT 与 IP 地址转换:私有地址、NAPT 端口映射
计算机网络·考研·ip地址·408·nat·napt·私有地址
乐维_lwops3 小时前
汽车集团网管平台建设案例:乐维网管工具实现跨区域网络统一监控与链路可视化
网络·网管平台·it监控·链路·统一监控
Thneonl4 小时前
Claude Code 频繁 Compacting?发布次日我根治了 2 个痛点
后端·rust
运维行者_4 小时前
供应链安全成最大风险源,企业如何构建可控的运维体系?
运维·开发语言·网络·数据库·安全·云计算·流量监控
pt10434 小时前
Cisco设备网络安全系列-20:DevSecOps集成
网络·安全·架构
IT古董4 小时前
Modbus TCP 详解:工业设备通信协议与 MES 集成实战
网络·网络协议·tcp/ip
自由彩虹5 小时前
靶场通关15--bossplayersctf
linux·服务器·网络·安全·web安全·网络安全