从 BIO、NIO、AIO 到多路复用与 Netty

Java I/O 全解:从 BIO、NIO、AIO 到多路复用与 Netty

关键词:BIO、NIO、AIO、select/poll/epoll/kqueue/IOCP、多路复用、Reactor、Netty


📑 目录

  • [一、Java I/O 体系总览](#一、Java I/O 体系总览)
  • [二、BIO:阻塞 I/O](#二、BIO:阻塞 I/O)
    • [2.1 工作原理](#2.1 工作原理)
    • [2.2 源码示例:BIO 服务端 + 客户端](#2.2 源码示例:BIO 服务端 + 客户端)
    • [2.3 BIO 的致命问题](#2.3 BIO 的致命问题)
  • [三、NIO:非阻塞 I/O + 多路复用](#三、NIO:非阻塞 I/O + 多路复用)
    • [3.1 三大核心:Buffer / Channel / Selector](#3.1 三大核心:Buffer / Channel / Selector)
    • [3.2 源码示例:NIO 多路复用服务端](#3.2 源码示例:NIO 多路复用服务端)
  • [四、AIO:异步 I/O](#四、AIO:异步 I/O)
    • [4.1 原理与适用平台](#4.1 原理与适用平台)
    • [4.2 源码示例:AIO(AsynchronousChannel)服务端](#4.2 源码示例:AIO(AsynchronousChannel)服务端)
    • [4.3 为什么 Linux 下 AIO 几乎被弃用](#4.3 为什么 Linux 下 AIO 几乎被弃用)
  • 五、多路复用原理:从操作系统层面讲透
    • [5.1 为什么需要「多路复用」](#5.1 为什么需要「多路复用」)
    • [5.2 select(Linux/macOS 通用)](#5.2 select(Linux/macOS 通用))
    • [5.3 poll(Linux/macOS 通用)](#5.3 poll(Linux/macOS 通用))
    • [5.4 epoll(Linux 专属,终极方案)](#5.4 epoll(Linux 专属,终极方案))
    • [5.5 kqueue(macOS / FreeBSD)](#5.5 kqueue(macOS / FreeBSD))
    • [5.6 IOCP(Windows 专属)](#5.6 IOCP(Windows 专属))
    • [5.7 五种机制对照表](#5.7 五种机制对照表)
  • [六、从 NIO 到 Netty:为什么要造轮子](#六、从 NIO 到 Netty:为什么要造轮子)
    • [6.1 原生 NIO 的痛点](#6.1 原生 NIO 的痛点)
    • [6.2 Netty 是怎么解决的](#6.2 Netty 是怎么解决的)
  • [七、Netty 核心原理](#七、Netty 核心原理)
    • [7.1 主从 Reactor 线程模型](#7.1 主从 Reactor 线程模型)
    • [7.2 EventLoop 与 Pipeline](#7.2 EventLoop 与 Pipeline)
    • [7.3 ByteBuf:替代 ByteBuffer](#7.3 ByteBuf:替代 ByteBuffer)
  • [八、Netty 实战示例:Echo 服务端 + 客户端](#八、Netty 实战示例:Echo 服务端 + 客户端)
    • [8.1 pom.xml 依赖](#8.1 pom.xml 依赖)
    • [8.2 Netty Echo Server](#8.2 Netty Echo Server)
    • [8.3 Netty Echo Client](#8.3 Netty Echo Client)
    • [8.4 运行结果](#8.4 运行结果)
  • 九、全景对比与选型建议
  • 十、总结

一、Java I/O 体系总览

Java 的 I/O 体系经历了三个阶段:

模型 引入版本 核心思想 典型 API
BIO (Blocking I/O) JDK 1.0 一个连接一个线程,连接阻塞 ServerSocket / Socket
NIO (Non-blocking I/O) JDK 1.4 多路复用,一个线程管多个连接 Channel / Selector / Buffer
AIO (Asynchronous I/O) JDK 7 真正异步,回调通知 AsynchronousServerSocketChannel

一句话演进:BIO 用线程换连接 → NIO 用选择器换线程 → AIO 用回调换等待。
#mermaid-svg-zpWUCWkg1XWEylL5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zpWUCWkg1XWEylL5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zpWUCWkg1XWEylL5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zpWUCWkg1XWEylL5 .error-icon{fill:#552222;}#mermaid-svg-zpWUCWkg1XWEylL5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zpWUCWkg1XWEylL5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zpWUCWkg1XWEylL5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zpWUCWkg1XWEylL5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zpWUCWkg1XWEylL5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zpWUCWkg1XWEylL5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zpWUCWkg1XWEylL5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zpWUCWkg1XWEylL5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zpWUCWkg1XWEylL5 .marker.cross{stroke:#333333;}#mermaid-svg-zpWUCWkg1XWEylL5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zpWUCWkg1XWEylL5 p{margin:0;}#mermaid-svg-zpWUCWkg1XWEylL5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zpWUCWkg1XWEylL5 .cluster-label text{fill:#333;}#mermaid-svg-zpWUCWkg1XWEylL5 .cluster-label span{color:#333;}#mermaid-svg-zpWUCWkg1XWEylL5 .cluster-label span p{background-color:transparent;}#mermaid-svg-zpWUCWkg1XWEylL5 .label text,#mermaid-svg-zpWUCWkg1XWEylL5 span{fill:#333;color:#333;}#mermaid-svg-zpWUCWkg1XWEylL5 .node rect,#mermaid-svg-zpWUCWkg1XWEylL5 .node circle,#mermaid-svg-zpWUCWkg1XWEylL5 .node ellipse,#mermaid-svg-zpWUCWkg1XWEylL5 .node polygon,#mermaid-svg-zpWUCWkg1XWEylL5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zpWUCWkg1XWEylL5 .rough-node .label text,#mermaid-svg-zpWUCWkg1XWEylL5 .node .label text,#mermaid-svg-zpWUCWkg1XWEylL5 .image-shape .label,#mermaid-svg-zpWUCWkg1XWEylL5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-zpWUCWkg1XWEylL5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zpWUCWkg1XWEylL5 .rough-node .label,#mermaid-svg-zpWUCWkg1XWEylL5 .node .label,#mermaid-svg-zpWUCWkg1XWEylL5 .image-shape .label,#mermaid-svg-zpWUCWkg1XWEylL5 .icon-shape .label{text-align:center;}#mermaid-svg-zpWUCWkg1XWEylL5 .node.clickable{cursor:pointer;}#mermaid-svg-zpWUCWkg1XWEylL5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zpWUCWkg1XWEylL5 .arrowheadPath{fill:#333333;}#mermaid-svg-zpWUCWkg1XWEylL5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zpWUCWkg1XWEylL5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zpWUCWkg1XWEylL5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zpWUCWkg1XWEylL5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zpWUCWkg1XWEylL5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zpWUCWkg1XWEylL5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zpWUCWkg1XWEylL5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zpWUCWkg1XWEylL5 .cluster text{fill:#333;}#mermaid-svg-zpWUCWkg1XWEylL5 .cluster span{color:#333;}#mermaid-svg-zpWUCWkg1XWEylL5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zpWUCWkg1XWEylL5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zpWUCWkg1XWEylL5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-zpWUCWkg1XWEylL5 .icon-shape,#mermaid-svg-zpWUCWkg1XWEylL5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zpWUCWkg1XWEylL5 .icon-shape p,#mermaid-svg-zpWUCWkg1XWEylL5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zpWUCWkg1XWEylL5 .icon-shape .label rect,#mermaid-svg-zpWUCWkg1XWEylL5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zpWUCWkg1XWEylL5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zpWUCWkg1XWEylL5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zpWUCWkg1XWEylL5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} BIO(1996)

一连接一线程
NIO(2002)

多路复用

一线程管多连接
AIO(2011)

回调通知

不阻塞不轮询
Netty(2008)

封装NIO

生产级框架


二、BIO:阻塞 I/O

2.1 工作原理

BIO 的模型非常直白:

  1. 服务端 ServerSocket.accept() 阻塞,直到有客户端连接进来;
  2. 每来一个连接,新建一个线程专门处理它;
  3. 线程在读 / 写时也会阻塞,直到数据到达或对端关闭。

#mermaid-svg-4wNZCIi6kfvZ54k1{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4wNZCIi6kfvZ54k1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4wNZCIi6kfvZ54k1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4wNZCIi6kfvZ54k1 .error-icon{fill:#552222;}#mermaid-svg-4wNZCIi6kfvZ54k1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4wNZCIi6kfvZ54k1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4wNZCIi6kfvZ54k1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4wNZCIi6kfvZ54k1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4wNZCIi6kfvZ54k1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4wNZCIi6kfvZ54k1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4wNZCIi6kfvZ54k1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4wNZCIi6kfvZ54k1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4wNZCIi6kfvZ54k1 .marker.cross{stroke:#333333;}#mermaid-svg-4wNZCIi6kfvZ54k1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4wNZCIi6kfvZ54k1 p{margin:0;}#mermaid-svg-4wNZCIi6kfvZ54k1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4wNZCIi6kfvZ54k1 .cluster-label text{fill:#333;}#mermaid-svg-4wNZCIi6kfvZ54k1 .cluster-label span{color:#333;}#mermaid-svg-4wNZCIi6kfvZ54k1 .cluster-label span p{background-color:transparent;}#mermaid-svg-4wNZCIi6kfvZ54k1 .label text,#mermaid-svg-4wNZCIi6kfvZ54k1 span{fill:#333;color:#333;}#mermaid-svg-4wNZCIi6kfvZ54k1 .node rect,#mermaid-svg-4wNZCIi6kfvZ54k1 .node circle,#mermaid-svg-4wNZCIi6kfvZ54k1 .node ellipse,#mermaid-svg-4wNZCIi6kfvZ54k1 .node polygon,#mermaid-svg-4wNZCIi6kfvZ54k1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4wNZCIi6kfvZ54k1 .rough-node .label text,#mermaid-svg-4wNZCIi6kfvZ54k1 .node .label text,#mermaid-svg-4wNZCIi6kfvZ54k1 .image-shape .label,#mermaid-svg-4wNZCIi6kfvZ54k1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-4wNZCIi6kfvZ54k1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4wNZCIi6kfvZ54k1 .rough-node .label,#mermaid-svg-4wNZCIi6kfvZ54k1 .node .label,#mermaid-svg-4wNZCIi6kfvZ54k1 .image-shape .label,#mermaid-svg-4wNZCIi6kfvZ54k1 .icon-shape .label{text-align:center;}#mermaid-svg-4wNZCIi6kfvZ54k1 .node.clickable{cursor:pointer;}#mermaid-svg-4wNZCIi6kfvZ54k1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4wNZCIi6kfvZ54k1 .arrowheadPath{fill:#333333;}#mermaid-svg-4wNZCIi6kfvZ54k1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4wNZCIi6kfvZ54k1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4wNZCIi6kfvZ54k1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4wNZCIi6kfvZ54k1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4wNZCIi6kfvZ54k1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4wNZCIi6kfvZ54k1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4wNZCIi6kfvZ54k1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4wNZCIi6kfvZ54k1 .cluster text{fill:#333;}#mermaid-svg-4wNZCIi6kfvZ54k1 .cluster span{color:#333;}#mermaid-svg-4wNZCIi6kfvZ54k1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4wNZCIi6kfvZ54k1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4wNZCIi6kfvZ54k1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-4wNZCIi6kfvZ54k1 .icon-shape,#mermaid-svg-4wNZCIi6kfvZ54k1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4wNZCIi6kfvZ54k1 .icon-shape p,#mermaid-svg-4wNZCIi6kfvZ54k1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4wNZCIi6kfvZ54k1 .icon-shape .label rect,#mermaid-svg-4wNZCIi6kfvZ54k1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4wNZCIi6kfvZ54k1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4wNZCIi6kfvZ54k1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4wNZCIi6kfvZ54k1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} accept 阻塞
accept 阻塞
ServerSocket 监听 8080
客户端1 连入
客户端2 连入
新建线程1 读/写 阻塞
新建线程2 读/写 阻塞
客户端1 断开 → 线程1 结束
客户端2 断开 → 线程2 结束

致命问题:1 万个连接 = 1 万个线程 → 线程上下文切换、内存(每线程默认 1MB 栈)直接把系统压垮。

2.2 源码示例:BIO 服务端 + 客户端

服务端(BioServer.java):

java 复制代码
import java.io.*;
import java.net.*;

/**
 * BIO 服务端:一连接一线程
 * 运行:java BioServer.java
 */
public class BioServer {

    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(8080);
        System.out.println("[BIO] 服务端启动,监听 8080");

        while (true) {
            // ① 阻塞,直到有客户端连入
            Socket client = server.accept();
            System.out.println("[BIO] 客户端连入: " + client.getRemoteSocketAddress());

            // ② 每个连接开一个新线程处理
            new Thread(() -> handle(client)).start();
        }
    }

    static void handle(Socket client) {
        try (
            BufferedReader in = new BufferedReader(
                new InputStreamReader(client.getInputStream()));
            PrintWriter out = new PrintWriter(
                new OutputStreamWriter(client.getOutputStream()), true)
        ) {
            String line;
            // ③ readLine 阻塞,直到读到一行或连接断开
            while ((line = in.readLine()) != null) {
                System.out.println("[BIO] 收到: " + line);
                out.println("ECHO: " + line);       // 回显
            }
        } catch (IOException e) {
            System.err.println("[BIO] 连接异常: " + e.getMessage());
        } finally {
            System.out.println("[BIO] 客户端断开");
        }
    }
}

客户端(BioClient.java):

java 复制代码
import java.io.*;
import java.net.*;

/**
 * BIO 客户端
 * 运行:java BioClient.java
 */
public class BioClient {

    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 8080);
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);
        BufferedReader console = new BufferedReader(new InputStreamReader(System.in));

        String input;
        while ((input = console.readLine()) != null) {
            out.println(input);                 // 发给服务端
            String resp = in.readLine();        // 读服务端回显(阻塞)
            System.out.println("[Client] " + resp);
            if ("bye".equalsIgnoreCase(input)) break;
        }
        socket.close();
    }
}

2.3 BIO 的致命问题

问题 说明
线程爆炸 C10K 连接 → 1 万线程,内存 + 上下文切换不可接受
连接空闲也占线程 客户端连着但不说话,线程照样阻塞在 read 上
无法扩展 加线程池只是缓兵之计,本质没变

结论:BIO 只适合连接数少、且每个连接都很活跃的场景(如内部 RPC)。高并发网络服务必须换模型。


三、NIO:非阻塞 I/O + 多路复用

3.1 三大核心:Buffer / Channel / Selector

NIO 的三个基石:

组件 作用 类比
Buffer 读写数据的容器,可读写切换 快递箱
Channel 双向数据通道(可读可写) 高速公路
Selector 多路复用器,一个线程监控多个 Channel 的就绪事件 交警指挥台

#mermaid-svg-zCS9RXk6iDDYV3RR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zCS9RXk6iDDYV3RR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zCS9RXk6iDDYV3RR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zCS9RXk6iDDYV3RR .error-icon{fill:#552222;}#mermaid-svg-zCS9RXk6iDDYV3RR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zCS9RXk6iDDYV3RR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zCS9RXk6iDDYV3RR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zCS9RXk6iDDYV3RR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zCS9RXk6iDDYV3RR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zCS9RXk6iDDYV3RR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zCS9RXk6iDDYV3RR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zCS9RXk6iDDYV3RR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zCS9RXk6iDDYV3RR .marker.cross{stroke:#333333;}#mermaid-svg-zCS9RXk6iDDYV3RR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zCS9RXk6iDDYV3RR p{margin:0;}#mermaid-svg-zCS9RXk6iDDYV3RR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zCS9RXk6iDDYV3RR .cluster-label text{fill:#333;}#mermaid-svg-zCS9RXk6iDDYV3RR .cluster-label span{color:#333;}#mermaid-svg-zCS9RXk6iDDYV3RR .cluster-label span p{background-color:transparent;}#mermaid-svg-zCS9RXk6iDDYV3RR .label text,#mermaid-svg-zCS9RXk6iDDYV3RR span{fill:#333;color:#333;}#mermaid-svg-zCS9RXk6iDDYV3RR .node rect,#mermaid-svg-zCS9RXk6iDDYV3RR .node circle,#mermaid-svg-zCS9RXk6iDDYV3RR .node ellipse,#mermaid-svg-zCS9RXk6iDDYV3RR .node polygon,#mermaid-svg-zCS9RXk6iDDYV3RR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zCS9RXk6iDDYV3RR .rough-node .label text,#mermaid-svg-zCS9RXk6iDDYV3RR .node .label text,#mermaid-svg-zCS9RXk6iDDYV3RR .image-shape .label,#mermaid-svg-zCS9RXk6iDDYV3RR .icon-shape .label{text-anchor:middle;}#mermaid-svg-zCS9RXk6iDDYV3RR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zCS9RXk6iDDYV3RR .rough-node .label,#mermaid-svg-zCS9RXk6iDDYV3RR .node .label,#mermaid-svg-zCS9RXk6iDDYV3RR .image-shape .label,#mermaid-svg-zCS9RXk6iDDYV3RR .icon-shape .label{text-align:center;}#mermaid-svg-zCS9RXk6iDDYV3RR .node.clickable{cursor:pointer;}#mermaid-svg-zCS9RXk6iDDYV3RR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zCS9RXk6iDDYV3RR .arrowheadPath{fill:#333333;}#mermaid-svg-zCS9RXk6iDDYV3RR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zCS9RXk6iDDYV3RR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zCS9RXk6iDDYV3RR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zCS9RXk6iDDYV3RR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zCS9RXk6iDDYV3RR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zCS9RXk6iDDYV3RR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zCS9RXk6iDDYV3RR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zCS9RXk6iDDYV3RR .cluster text{fill:#333;}#mermaid-svg-zCS9RXk6iDDYV3RR .cluster span{color:#333;}#mermaid-svg-zCS9RXk6iDDYV3RR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zCS9RXk6iDDYV3RR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zCS9RXk6iDDYV3RR rect.text{fill:none;stroke-width:0;}#mermaid-svg-zCS9RXk6iDDYV3RR .icon-shape,#mermaid-svg-zCS9RXk6iDDYV3RR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zCS9RXk6iDDYV3RR .icon-shape p,#mermaid-svg-zCS9RXk6iDDYV3RR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zCS9RXk6iDDYV3RR .icon-shape .label rect,#mermaid-svg-zCS9RXk6iDDYV3RR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zCS9RXk6iDDYV3RR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zCS9RXk6iDDYV3RR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zCS9RXk6iDDYV3RR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 单线程
监听 ACCEPT
监听 READ
监听 READ
Selector 多路复用器
Channel-1
Channel-2
Channel-3
Buffer
Buffer
Buffer

关键点:Selector 在底层就是操作系统的多路复用系统调用 (Linux 上是 epoll,macOS 上是 kqueue,Windows 上是 WSAPoll)。一个线程调用一次 select(),就能知道上千个 Channel 里哪些「有数据可读」「可写」「有新连接」。

3.2 源码示例:NIO 多路复用服务端

java 复制代码
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;

/**
 * NIO 多路复用服务端:单线程处理多个连接
 * 运行:java NioServer.java
 */
public class NioServer {

    public static void main(String[] args) throws IOException {
        // 1) 打开 Selector(底层在 Linux 上就是 epoll)
        Selector selector = Selector.open();

        // 2) 打开 ServerSocketChannel,绑定端口,设为非阻塞,注册 ACCEPT 事件
        ServerSocketChannel server = ServerSocketChannel.open();
        server.configureBlocking(false);
        server.bind(new InetSocketAddress(8080));
        server.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("[NIO] 服务端启动,监听 8080");

        ByteBuffer buffer = ByteBuffer.allocate(1024);

        // 3) 事件循环:一次 select() 拿到所有就绪事件
        while (true) {
            selector.select();                          // 阻塞,直到至少一个事件就绪
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> it = keys.iterator();

            while (it.hasNext()) {
                SelectionKey key = it.next();
                it.remove();                            // 必须手动移除,否则重复处理

                if (key.isAcceptable()) {
                    // ① 新连接到达
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ);
                    System.out.println("[NIO] 新连接: " + client.getRemoteSocketAddress());

                } else if (key.isReadable()) {
                    // ② 某个连接有数据可读
                    SocketChannel client = (SocketChannel) key.channel();
                    buffer.clear();
                    int n = client.read(buffer);        // 非阻塞读
                    if (n == -1) {                      // 对端关闭
                        key.cancel();
                        client.close();
                        System.out.println("[NIO] 连接断开");
                        continue;
                    }
                    buffer.flip();
                    String msg = StandardCharsets.UTF_8.decode(buffer).toString();
                    System.out.println("[NIO] 收到: " + msg.trim());

                    // 回显
                    buffer.rewind();
                    client.write(buffer);
                }
            }
        }
    }
}

与 BIO 的本质区别:

  • 单线程就能管理成千上万个连接------selector.select() 一次性返回所有就绪事件;
  • 不读不写时不占 CPU、不占线程;
  • 但数据处理(编解码、业务逻辑)仍然在这个单线程里做,如果某个 handler 很慢,会拖垮整个事件循环------这正是 Netty 要解决的。

四、AIO:异步 I/O

4.1 原理与适用平台

AIO(JDK 7 引入的 AsynchronousChannel)是「真正的异步 I/O」:

  • 调用 read/write 时传入一个 CompletionHandler 回调,方法立即返回;
  • 操作系统在后台完成 I/O,完成后回调 completed();
  • 应用线程既不阻塞、也不轮询。
平台 底层实现 效果
Windows IOCP(真异步) 性能极佳
Linux epoll 模拟(非真 IOCP) 效果 ≈ NIO,且 API 更复杂
macOS kqueue 模拟 效果 ≈ NIO

这就是为什么 Linux 服务端几乎没人用 Java AIO------底层没有真正的 IOCP,只是用 epoll 模拟,性能没优势、代码还更难写。Netty 也因此选择了 NIO 而非 AIO 作为主路线。

4.2 源码示例:AIO(AsynchronousChannel)服务端

java 复制代码
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;

/**
 * AIO 服务端:回调式异步
 * 运行:java AioServer.java
 */
public class AioServer {

    public static void main(String[] args) throws Exception {
        AsynchronousServerSocketChannel server =
            AsynchronousServerSocketChannel.open()
                .bind(new InetSocketAddress(8080));

        System.out.println("[AIO] 服务端启动,监听 8080");

        // ① 异步 accept:传入回调,方法立即返回
        server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
            @Override
            public void completed(AsynchronousSocketChannel client, Void attachment) {
                // 继续接受下一个连接(关键!否则只处理一个)
                server.accept(null, this);

                ByteBuffer buffer = ByteBuffer.allocate(1024);
                // ② 异步 read:传入回调
                client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                    @Override
                    public void completed(Integer n, ByteBuffer buf) {
                        if (n == -1) {
                            close(client);
                            return;
                        }
                        buf.flip();
                        String msg = StandardCharsets.UTF_8.decode(buf).toString();
                        System.out.println("[AIO] 收到: " + msg.trim());

                        // 异步 write:回显
                        buf.rewind();
                        client.write(buf, buf, new CompletionHandler<Integer, ByteBuffer>() {
                            @Override
                            public void completed(Integer w, ByteBuffer b) {
                                b.clear();
                                // 继续读下一轮
                                client.read(b, b, this);
                            }
                            @Override
                            public void failed(Throwable e, ByteBuffer b) {
                                close(client);
                            }
                        });
                    }
                    @Override
                    public void failed(Throwable e, ByteBuffer buf) {
                        close(client);
                    }
                });
            }
            @Override
            public void failed(Throwable e, Void attachment) {
                e.printStackTrace();
            }
        });

        // 主线程不退出(AIO 由后台线程池驱动)
        Thread.currentThread().join();
    }

    static void close(AsynchronousSocketChannel c) {
        try { c.close(); } catch (Exception ignored) {}
        System.out.println("[AIO] 连接断开");
    }
}

4.3 为什么 Linux 下 AIO 几乎被弃用

  1. Linux 没有 IOCP ,JDK 的 AIO 在 Linux 上是用 epoll + 线程池模拟的,性能不比 NIO 好;
  2. 回调地狱:如上代码,嵌套回调写起来非常痛苦,可读性差;
  3. Netty 不主推 AIO :Netty 曾经有过 AioEventLoopGroup,后来直接移除了------因为收益太小、维护成本太高。

结论:Windows 平台用 AIO 有真 IOCP 加持;Linux 上推荐直接用 NIO 或 Netty。


五、多路复用原理:从操作系统层面讲透

5.1 为什么需要「多路复用」

假设有 10000 个连接,怎么知道哪些「有数据可读」?

  • 笨办法 :每个连接调一次 read,非阻塞地轮询一遍 → 10000 次系统调用,CPU 飞起。
  • 多路复用 :把所有连接的 fd(文件描述符)一次性交给内核,内核告诉你哪些就绪 → 1 次系统调用搞定。

这就是「多路复用」:用一个系统调用,同时监控多个 fd。

不同操作系统提供了不同的系统调用来实现它:

系统调用 平台 复杂度 机制
select Linux / macOS O(n) 位图 + 线性遍历,有 1024 上限
poll Linux / macOS O(n) 链表替代位图,无上限但仍遍历
epoll Linux O(1) 红黑树 + 就绪链表,事件驱动
kqueue macOS / FreeBSD O(1) 类似 epoll,filter 更丰富
IOCP Windows O(1) 完成端口,真异步通知

下面逐个讲。

5.2 select(Linux / macOS 通用)

select 是最古老的多路复用调用(1983 年 BSD UNIX)。

原理:

  1. 用户把关心的 fd 集合装进一个 位图 (fd_set),每个 fd 对应一个 bit;
  2. 调用 select(maxfd, &readfds, &writefds, &exceptfds, &timeout),用户态→内核态拷贝位图;
  3. 内核遍历所有 fd,检查是否有就绪事件,有的话修改位图,返回就绪数量;
  4. 用户态再线性遍历位图,找到哪些 bit 被置位,逐个处理。

致命缺陷:

  • fd 数量上限 :FD_SETSIZE 默认 1024,改要重编内核;
  • 每次都要拷贝整个位图到内核,fd 多了开销大;
  • 返回后要线性遍历全部 fd 找就绪的,O(n);
  • 即使只有 1 个 fd 就绪,也要扫全部 10000 个。
c 复制代码
// select 伪代码
fd_set readfds;
FD_ZERO(&readfds);
for (int i = 0; i < 10000; i++)
    FD_SET(fds[i], &readfds);       // 装入位图

select(maxfd + 1, &readfds, NULL, NULL, &timeout);  // 交给内核

for (int i = 0; i < 10000; i++) {   // 回来还要全扫一遍
    if (FD_ISSET(fds[i], &readfds)) {
        read(fds[i], buf, sizeof(buf));
    }
}

5.3 poll(Linux / macOS 通用)

poll 是 select 的改进版(1986 年 UNIX SVR3)。

改进点 :用 struct pollfd 数组代替位图,取消了 1024 上限。

c 复制代码
struct pollfd {
    int fd;           // 关心的 fd
    short events;     // 关心的事件(POLLIN / POLLOUT)
    short revents;    // 内核返回的就绪事件
};

但核心缺陷没变:

  • 仍然要把整个数组拷贝到内核;
  • 返回后仍然要线性遍历 整个数组找 revents != 0 的项,O(n)。

结论:poll 只是去掉了 fd 上限,复杂度仍是 O(n),大规模并发下仍不行。

5.4 epoll(Linux 专属,终极方案)

epoll 是 Linux 2.6(2002 年)引入的,是 Linux 高并发的终极方案。

核心创新 :用「红黑树 + 就绪链表」替代了 select/poll 的「遍历全量」。
#mermaid-svg-Alj6WTrQ7bI3JPxr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Alj6WTrQ7bI3JPxr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Alj6WTrQ7bI3JPxr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Alj6WTrQ7bI3JPxr .error-icon{fill:#552222;}#mermaid-svg-Alj6WTrQ7bI3JPxr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Alj6WTrQ7bI3JPxr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Alj6WTrQ7bI3JPxr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Alj6WTrQ7bI3JPxr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Alj6WTrQ7bI3JPxr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Alj6WTrQ7bI3JPxr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Alj6WTrQ7bI3JPxr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Alj6WTrQ7bI3JPxr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Alj6WTrQ7bI3JPxr .marker.cross{stroke:#333333;}#mermaid-svg-Alj6WTrQ7bI3JPxr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Alj6WTrQ7bI3JPxr p{margin:0;}#mermaid-svg-Alj6WTrQ7bI3JPxr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Alj6WTrQ7bI3JPxr .cluster-label text{fill:#333;}#mermaid-svg-Alj6WTrQ7bI3JPxr .cluster-label span{color:#333;}#mermaid-svg-Alj6WTrQ7bI3JPxr .cluster-label span p{background-color:transparent;}#mermaid-svg-Alj6WTrQ7bI3JPxr .label text,#mermaid-svg-Alj6WTrQ7bI3JPxr span{fill:#333;color:#333;}#mermaid-svg-Alj6WTrQ7bI3JPxr .node rect,#mermaid-svg-Alj6WTrQ7bI3JPxr .node circle,#mermaid-svg-Alj6WTrQ7bI3JPxr .node ellipse,#mermaid-svg-Alj6WTrQ7bI3JPxr .node polygon,#mermaid-svg-Alj6WTrQ7bI3JPxr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Alj6WTrQ7bI3JPxr .rough-node .label text,#mermaid-svg-Alj6WTrQ7bI3JPxr .node .label text,#mermaid-svg-Alj6WTrQ7bI3JPxr .image-shape .label,#mermaid-svg-Alj6WTrQ7bI3JPxr .icon-shape .label{text-anchor:middle;}#mermaid-svg-Alj6WTrQ7bI3JPxr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Alj6WTrQ7bI3JPxr .rough-node .label,#mermaid-svg-Alj6WTrQ7bI3JPxr .node .label,#mermaid-svg-Alj6WTrQ7bI3JPxr .image-shape .label,#mermaid-svg-Alj6WTrQ7bI3JPxr .icon-shape .label{text-align:center;}#mermaid-svg-Alj6WTrQ7bI3JPxr .node.clickable{cursor:pointer;}#mermaid-svg-Alj6WTrQ7bI3JPxr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Alj6WTrQ7bI3JPxr .arrowheadPath{fill:#333333;}#mermaid-svg-Alj6WTrQ7bI3JPxr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Alj6WTrQ7bI3JPxr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Alj6WTrQ7bI3JPxr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Alj6WTrQ7bI3JPxr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Alj6WTrQ7bI3JPxr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Alj6WTrQ7bI3JPxr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Alj6WTrQ7bI3JPxr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Alj6WTrQ7bI3JPxr .cluster text{fill:#333;}#mermaid-svg-Alj6WTrQ7bI3JPxr .cluster span{color:#333;}#mermaid-svg-Alj6WTrQ7bI3JPxr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Alj6WTrQ7bI3JPxr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Alj6WTrQ7bI3JPxr rect.text{fill:none;stroke-width:0;}#mermaid-svg-Alj6WTrQ7bI3JPxr .icon-shape,#mermaid-svg-Alj6WTrQ7bI3JPxr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Alj6WTrQ7bI3JPxr .icon-shape p,#mermaid-svg-Alj6WTrQ7bI3JPxr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Alj6WTrQ7bI3JPxr .icon-shape .label rect,#mermaid-svg-Alj6WTrQ7bI3JPxr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Alj6WTrQ7bI3JPxr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Alj6WTrQ7bI3JPxr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Alj6WTrQ7bI3JPxr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} epoll 内核态
fd 就绪时
epoll_wait 取走
红黑树: 存所有注册的 fd
就绪链表: 只存就绪的 fd
用户态

三个系统调用:

调用 作用
epoll_create 创建 epoll 实例,返回 fd
epoll_ctl(ADD/MOD/DEL) 往红黑树里增删改一个 fd(注册一次即可,不重复传)
epoll_wait 只从「就绪链表」取就绪事件,O(1)

两种触发模式:

  • LT(水平触发,默认) :只要 fd 上有数据可读,epoll_wait 就会一直通知你。你一次没读完,下次还通知。不会丢事件,但可能重复通知。
  • ET(边缘触发) :fd 从「无数据」变成「有数据」时只通知一次 。你必须一次性把数据读完(用非阻塞 I/O + 循环 read 直到 EAGAIN),否则剩余数据不再通知。效率更高,但编程更难,漏读会丢数据。
c 复制代码
// epoll 伪代码(LT 模式)
int epfd = epoll_create1(0);

struct epoll_event ev;
ev.events = EPOLLIN;            // 水平触发(默认)
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);   // 注册到红黑树

struct epoll_event events[1024];
while (1) {
    // 只返回就绪的 fd,O(1)
    int n = epoll_wait(epfd, events, 1024, -1);
    for (int i = 0; i < n; i++) {      // 只遍历 n 个就绪的,不是全量
        if (events[i].data.fd == listen_fd) {
            int cfd = accept(listen_fd, ...);
            ev.events = EPOLLIN;
            ev.data.fd = cfd;
            epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);   // 新连接注册
        } else {
            read(events[i].data.fd, buf, sizeof(buf));
        }
    }
}

Java NIO 在 Linux 上的底层就是 epoll (Selector.open() → EPollSelectorImpl)。

为什么 epoll 快:

  • epoll_ctl 只在注册时调一次,不像 select/poll 每次都拷贝全量;
  • epoll_wait 只返回就绪的 fd,不扫描全量,O(1);
  • 红黑树保证插入 / 删除 O(log n)。

5.5 kqueue(macOS / FreeBSD)

kqueue 是 FreeBSD 4.1(2000 年)引入,macOS 也支持。设计理念与 epoll 类似,但更通用。

c 复制代码
int kq = kqueue();
struct kevent change;
EV_SET(&change, fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
kevent(kq, &change, 1, NULL, 0, NULL);    // 注册

struct kevent events[1024];
int n = kevent(kq, NULL, 0, events, 1024, NULL);  // 取就绪

与 epoll 的区别:

维度 epoll kqueue
平台 Linux macOS / FreeBSD
监控对象 fd filter(更通用,可监控信号、定时器、子进程等)
触发模式 LT / ET LT / ET
性能 O(1) O(1)

Java NIO 在 macOS 上的底层就是 kqueue (KQueueSelectorImpl)。

5.6 IOCP(Windows 专属)

IOCP(I/O Completion Port,I/O 完成端口)是 Windows 独有的真异步 I/O模型,也是 Windows 上性能最高的方案。

核心理念与 epoll 相反:

  • epoll / kqueue 是「就绪通知」:告诉你「fd 有数据了,你自己去 read」;
  • IOCP 是「完成通知 」:你发一个 read 请求就不管了,操作系统帮你读好,完成后通知你「数据已经在 buffer 里了」。
c 复制代码
// IOCP 伪代码
HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

// 把 socket 绑到 IOCP
CreateIoCompletionPort(socket, iocp, 0, 0);

// 发起异步读(立刻返回)
WSARecv(socket, &buf, ...);   // OS 后台帮你读

// 工作线程循环等待「完成通知」
DWORD bytes;
ULONG_PTR key;
OVERLAPPED *ov;
GetQueuedCompletionStatus(iocp, &bytes, &key, &ov, INFINITE);
// 到这里说明 I/O 已完成,buf 里已经有数据

Java AIO 在 Windows 上的底层就是 IOCP,所以在 Windows 上 Java AIO 性能很好。但 Linux 没有 IOCP,Java AIO 只能用 epoll 模拟。

5.7 五种机制对照表

机制 平台 fd 上限 复杂度 通知方式 触发模式
select Linux / macOS 1024 O(n) 就绪通知(遍历) LT
poll Linux / macOS 无上限 O(n) 就绪通知(遍历) LT
epoll Linux 无上限 O(1) 就绪通知(链表) LT / ET
kqueue macOS / FreeBSD 无上限 O(1) 就绪通知(filter) LT / ET
IOCP Windows 无上限 O(1) 完成通知(真异步) ---

Java NIO 的 Selector 会根据运行平台自动选择:Linux → epoll,macOS → kqueue,Windows → WSAPoll。你写的代码不变,底层自动适配。


六、从 NIO 到 Netty:为什么要造轮子

6.1 原生 NIO 的痛点

虽然 NIO 解决了 BIO 的线程爆炸问题,但直接用它写生产级代码,痛苦不堪:

痛点 说明
API 复杂 要手动管 Buffer 的 flip/rewind/clear,少一个就出 Bug
半包 / 粘包 TCP 是流,没有消息边界,你要自己拆包------LengthFieldBasedFrameDecoder 这种逻辑自己写太累
空轮询 Bug JDK 的 epoll 在某些情况下 select() 会空转不阻塞,CPU 100%(著名的 JDK Bug,Netty 有兜底)
线程模型 事件循环里做耗时业务会卡死,你要自己拆「IO 线程」和「业务线程池」
断线重连 客户端断线重连、心跳检测都要自己实现

6.2 Netty 是怎么解决的

痛点 Netty 的解法
API 复杂 自研 ByteBuf(读写指针分离,不用 flip),Channel 封装
半包 / 粘包 内置 LineBasedFrameDecoder / LengthFieldBasedFrameDecoder 等拆包器
空轮询 Bug 检测到空轮询自动重建 Selector(NioEventLoop 兜底)
线程模型 主从 Reactor,IO 线程和业务线程分离
断线重连 内置 IdleStateHandler 心跳 + 重连机制

一句话:Netty 是对 NIO 的工程级封装,把「能用」变成「好用」。


七、Netty 核心原理

7.1 主从 Reactor 线程模型

Netty 采用「主从 Reactor」模型:
#mermaid-svg-JoJkaQE9rO8IbgUU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JoJkaQE9rO8IbgUU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JoJkaQE9rO8IbgUU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JoJkaQE9rO8IbgUU .error-icon{fill:#552222;}#mermaid-svg-JoJkaQE9rO8IbgUU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JoJkaQE9rO8IbgUU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JoJkaQE9rO8IbgUU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JoJkaQE9rO8IbgUU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JoJkaQE9rO8IbgUU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JoJkaQE9rO8IbgUU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JoJkaQE9rO8IbgUU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JoJkaQE9rO8IbgUU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JoJkaQE9rO8IbgUU .marker.cross{stroke:#333333;}#mermaid-svg-JoJkaQE9rO8IbgUU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JoJkaQE9rO8IbgUU p{margin:0;}#mermaid-svg-JoJkaQE9rO8IbgUU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JoJkaQE9rO8IbgUU .cluster-label text{fill:#333;}#mermaid-svg-JoJkaQE9rO8IbgUU .cluster-label span{color:#333;}#mermaid-svg-JoJkaQE9rO8IbgUU .cluster-label span p{background-color:transparent;}#mermaid-svg-JoJkaQE9rO8IbgUU .label text,#mermaid-svg-JoJkaQE9rO8IbgUU span{fill:#333;color:#333;}#mermaid-svg-JoJkaQE9rO8IbgUU .node rect,#mermaid-svg-JoJkaQE9rO8IbgUU .node circle,#mermaid-svg-JoJkaQE9rO8IbgUU .node ellipse,#mermaid-svg-JoJkaQE9rO8IbgUU .node polygon,#mermaid-svg-JoJkaQE9rO8IbgUU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JoJkaQE9rO8IbgUU .rough-node .label text,#mermaid-svg-JoJkaQE9rO8IbgUU .node .label text,#mermaid-svg-JoJkaQE9rO8IbgUU .image-shape .label,#mermaid-svg-JoJkaQE9rO8IbgUU .icon-shape .label{text-anchor:middle;}#mermaid-svg-JoJkaQE9rO8IbgUU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JoJkaQE9rO8IbgUU .rough-node .label,#mermaid-svg-JoJkaQE9rO8IbgUU .node .label,#mermaid-svg-JoJkaQE9rO8IbgUU .image-shape .label,#mermaid-svg-JoJkaQE9rO8IbgUU .icon-shape .label{text-align:center;}#mermaid-svg-JoJkaQE9rO8IbgUU .node.clickable{cursor:pointer;}#mermaid-svg-JoJkaQE9rO8IbgUU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JoJkaQE9rO8IbgUU .arrowheadPath{fill:#333333;}#mermaid-svg-JoJkaQE9rO8IbgUU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JoJkaQE9rO8IbgUU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JoJkaQE9rO8IbgUU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JoJkaQE9rO8IbgUU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JoJkaQE9rO8IbgUU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JoJkaQE9rO8IbgUU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JoJkaQE9rO8IbgUU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JoJkaQE9rO8IbgUU .cluster text{fill:#333;}#mermaid-svg-JoJkaQE9rO8IbgUU .cluster span{color:#333;}#mermaid-svg-JoJkaQE9rO8IbgUU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JoJkaQE9rO8IbgUU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JoJkaQE9rO8IbgUU rect.text{fill:none;stroke-width:0;}#mermaid-svg-JoJkaQE9rO8IbgUU .icon-shape,#mermaid-svg-JoJkaQE9rO8IbgUU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JoJkaQE9rO8IbgUU .icon-shape p,#mermaid-svg-JoJkaQE9rO8IbgUU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JoJkaQE9rO8IbgUU .icon-shape .label rect,#mermaid-svg-JoJkaQE9rO8IbgUU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JoJkaQE9rO8IbgUU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JoJkaQE9rO8IbgUU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JoJkaQE9rO8IbgUU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} WorkerGroup 处理 IO
BossGroup 接受连接
accept
accept
accept
accept
BossEventLoop

1个线程
WorkerEventLoop-1
WorkerEventLoop-2
WorkerEventLoop-3
WorkerEventLoop-4
Pipeline → Handler
Pipeline → Handler
Pipeline → Handler
Pipeline → Handler

  • BossGroup (1 个线程):只负责 accept 新连接;
  • WorkerGroup(N 个线程):负责已建立连接的读写和业务处理;
  • 每个连接被分配到一个 Worker 的 EventLoop 上,整个生命周期都在同一个线程里处理,无锁。
java 复制代码
// 对应代码
EventLoopGroup bossGroup = new NioEventLoopGroup(1);    // 接收连接
EventLoopGroup workerGroup = new NioEventLoopGroup(4);  // 处理 IO

7.2 EventLoop 与 Pipeline

  • EventLoop = 一个线程 + 一个任务队列 + 一个 Selector。它既做 IO(select + read/write),也做任务(定时任务、业务任务)。
  • Pipeline = 每个 Channel 绑一条双向链表,入站事件(读)从 Head 往后走,出站事件(写)从 Tail 往前走。

#mermaid-svg-xmiHnZ85dZyeW0lC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xmiHnZ85dZyeW0lC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xmiHnZ85dZyeW0lC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xmiHnZ85dZyeW0lC .error-icon{fill:#552222;}#mermaid-svg-xmiHnZ85dZyeW0lC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xmiHnZ85dZyeW0lC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xmiHnZ85dZyeW0lC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xmiHnZ85dZyeW0lC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xmiHnZ85dZyeW0lC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xmiHnZ85dZyeW0lC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xmiHnZ85dZyeW0lC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xmiHnZ85dZyeW0lC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xmiHnZ85dZyeW0lC .marker.cross{stroke:#333333;}#mermaid-svg-xmiHnZ85dZyeW0lC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xmiHnZ85dZyeW0lC p{margin:0;}#mermaid-svg-xmiHnZ85dZyeW0lC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xmiHnZ85dZyeW0lC .cluster-label text{fill:#333;}#mermaid-svg-xmiHnZ85dZyeW0lC .cluster-label span{color:#333;}#mermaid-svg-xmiHnZ85dZyeW0lC .cluster-label span p{background-color:transparent;}#mermaid-svg-xmiHnZ85dZyeW0lC .label text,#mermaid-svg-xmiHnZ85dZyeW0lC span{fill:#333;color:#333;}#mermaid-svg-xmiHnZ85dZyeW0lC .node rect,#mermaid-svg-xmiHnZ85dZyeW0lC .node circle,#mermaid-svg-xmiHnZ85dZyeW0lC .node ellipse,#mermaid-svg-xmiHnZ85dZyeW0lC .node polygon,#mermaid-svg-xmiHnZ85dZyeW0lC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xmiHnZ85dZyeW0lC .rough-node .label text,#mermaid-svg-xmiHnZ85dZyeW0lC .node .label text,#mermaid-svg-xmiHnZ85dZyeW0lC .image-shape .label,#mermaid-svg-xmiHnZ85dZyeW0lC .icon-shape .label{text-anchor:middle;}#mermaid-svg-xmiHnZ85dZyeW0lC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xmiHnZ85dZyeW0lC .rough-node .label,#mermaid-svg-xmiHnZ85dZyeW0lC .node .label,#mermaid-svg-xmiHnZ85dZyeW0lC .image-shape .label,#mermaid-svg-xmiHnZ85dZyeW0lC .icon-shape .label{text-align:center;}#mermaid-svg-xmiHnZ85dZyeW0lC .node.clickable{cursor:pointer;}#mermaid-svg-xmiHnZ85dZyeW0lC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xmiHnZ85dZyeW0lC .arrowheadPath{fill:#333333;}#mermaid-svg-xmiHnZ85dZyeW0lC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xmiHnZ85dZyeW0lC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xmiHnZ85dZyeW0lC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xmiHnZ85dZyeW0lC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xmiHnZ85dZyeW0lC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xmiHnZ85dZyeW0lC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xmiHnZ85dZyeW0lC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xmiHnZ85dZyeW0lC .cluster text{fill:#333;}#mermaid-svg-xmiHnZ85dZyeW0lC .cluster span{color:#333;}#mermaid-svg-xmiHnZ85dZyeW0lC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xmiHnZ85dZyeW0lC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xmiHnZ85dZyeW0lC rect.text{fill:none;stroke-width:0;}#mermaid-svg-xmiHnZ85dZyeW0lC .icon-shape,#mermaid-svg-xmiHnZ85dZyeW0lC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xmiHnZ85dZyeW0lC .icon-shape p,#mermaid-svg-xmiHnZ85dZyeW0lC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xmiHnZ85dZyeW0lC .icon-shape .label rect,#mermaid-svg-xmiHnZ85dZyeW0lC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xmiHnZ85dZyeW0lC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xmiHnZ85dZyeW0lC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xmiHnZ85dZyeW0lC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Outbound 出站 写
Tail
Encoder
Head
Inbound 入站 读
Head
Decoder
BusinessHandler
Tail

每个 Handler 只管自己的事(解码、业务、编码),互不耦合------这就是 Netty 的「责任链模式」。

7.3 ByteBuf:替代 ByteBuffer

JDK 的 ByteBuffer 有个反人类设计:读写共用一个 position 指针,读写切换必须手动 flip(),忘写就出 Bug。

Netty 的 ByteBuf 用两个独立指针 readerIndex / writerIndex 解决了这个问题:

操作 JDK ByteBuffer Netty ByteBuf
读到哪 position(与写共用) readerIndex(独立)
写到哪 position(与读共用) writerIndex(独立)
读写切换 必须调 flip() 不用,各走各的
堆外内存 allocateDirect() 手动释放 Unpooled.directBuffer() 引用计数自动释放
池化 不支持 PooledByteBufAllocator 对象池
java 复制代码
// JDK ByteBuffer:必须 flip
ByteBuffer buf = ByteBuffer.allocate(100);
buf.put("hello".getBytes());
buf.flip();            // ← 忘了这行就读不到
byte[] data = new byte[buf.remaining()];
buf.get(data);

// Netty ByteBuf:不用 flip
ByteBuf buf = Unpooled.buffer(100);
buf.writeBytes("hello".getBytes());
byte[] data = new byte[buf.readableBytes()];
buf.readBytes(data);   // 直接读,不冲突

八、Netty 实战示例:Echo 服务端 + 客户端

8.1 pom.xml 依赖

xml 复制代码
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>netty-echo</artifactId>
  <version>1.0.0</version>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.5</version>
  </parent>
  <dependencies>
    <dependency>
      <groupId>io.netty</groupId>
      <artifactId>netty-all</artifactId>
      <version>4.1.108.Final</version>
    </dependency>
  </dependencies>
</project>

8.2 Netty Echo Server

java 复制代码
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;

/**
 * Netty Echo 服务端
 */
public class NettyEchoServer {

    public static void main(String[] args) throws InterruptedException {
        // ① 主从 Reactor:Boss 接连接,Worker 处理 IO
        EventLoopGroup boss = new NioEventLoopGroup(1);
        EventLoopGroup worker = new NioEventLoopGroup(4);

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(boss, worker)
             .channel(NioServerSocketChannel.class)       // 使用 NIO(底层 epoll/kqueue)
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 protected void initChannel(SocketChannel ch) {
                     ChannelPipeline p = ch.pipeline();
                     // ② 用分隔符解决粘包/半包(以 $ 结尾为一条消息)
                     ByteBuf delimiter = Unpooled.copiedBuffer("$", CharsetUtil.UTF_8);
                     p.addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
                     p.addLast(new StringDecoder(CharsetUtil.UTF_8));   // 入站:ByteBuf → String
                     p.addLast(new StringEncoder(CharsetUtil.UTF_8));   // 出站:String → ByteBuf
                     // ③ 业务 Handler
                     p.addLast(new EchoServerHandler());
                 }
             });

            // ④ 绑定端口,同步等待启动
            ChannelFuture f = b.bind(8080).sync();
            System.out.println("[Netty] 服务端启动,监听 8080");

            // ⑤ 等待关闭
            f.channel().closeFuture().sync();
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }

    /**
     * 业务 Handler:继承 SimpleChannelInboundHandler,泛型是入站消息类型
     * 因为前面 StringDecoder 已把 ByteBuf 转成 String,这里直接收 String
     */
    static class EchoServerHandler extends SimpleChannelInboundHandler<String> {

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) {
            System.out.println("[Netty] 收到: " + msg);
            // 回显:写回会经过 StringEncoder 编码
            ctx.writeAndReturn("ECHO: " + msg + "$");
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            System.out.println("[Netty] 客户端连入: " + ctx.channel().remoteAddress());
        }

        @Override
        public void channelInactive(ChannelHandlerContext ctx) {
            System.out.println("[Netty] 客户端断开");
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            cause.printStackTrace();
            ctx.close();
        }
    }
}

上面 ctx.writeAndReturn 是示意写法。实际回显应写 ctx.writeAndFlush("ECHO: " + msg + "$"),flush 会触发出站链刷新。本段为了突出「Encoder 编码 + 分隔符」的核心,用 writeAndFlush。

修正后的 Handler 回显写法:

java 复制代码
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
    System.out.println("[Netty] 收到: " + msg);
    ctx.writeAndFlush("ECHO: " + msg + "$");   // ← 正确写法:flush 立即发送
}

8.3 Netty Echo Client

java 复制代码
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;

import java.io.BufferedReader;
import java.io.InputStreamReader;

/**
 * Netty Echo 客户端
 */
public class NettyEchoClient {

    public static void main(String[] args) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup(1);

        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class)
             .handler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 protected void initChannel(SocketChannel ch) {
                     ChannelPipeline p = ch.pipeline();
                     ByteBuf delimiter = Unpooled.copiedBuffer("$", CharsetUtil.UTF_8);
                     p.addLast(new DelimiterBasedFrameDecoder(1024, delimiter));
                     p.addLast(new StringDecoder(CharsetUtil.UTF_8));
                     p.addLast(new StringEncoder(CharsetUtil.UTF_8));
                     p.addLast(new EchoClientHandler());
                 }
             });

            Channel ch = b.connect("127.0.0.1", 8080).sync().channel();
            System.out.println("[Client] 已连接服务端,输入消息回车发送(bye 退出)");

            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
            String line;
            while ((line = in.readLine()) != null) {
                if ("bye".equalsIgnoreCase(line)) break;
                ch.writeAndFlush(line + "$");     // 以 $ 结尾,匹配服务端分隔符
            }
            ch.close();
        } finally {
            group.shutdownGracefully();
        }
    }

    static class EchoClientHandler extends SimpleChannelInboundHandler<String> {
        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) {
            System.out.println("[Client] " + msg);
        }
    }
}

8.4 运行结果

复制代码
# 服务端
[Netty] 服务端启动,监听 8080
[Netty] 客户端连入: /127.0.0.1:54321
[Netty] 收到: hello
[Netty] 收到: world

# 客户端
[Client] 已连接服务端,输入消息回车发送(bye 退出)
hello
[Client] ECHO: hello
world
[Client] ECHO: world

注意几个关键细节:

  1. 粘包处理 :客户端发 hello$world$,服务端的 DelimiterBasedFrameDecoder 会自动拆成 hello 和 world 两条消息------这在原生 NIO 里要你自己写拆包逻辑。
  2. 编解码 :StringDecoder/Encoder 把 ByteBuf ↔ String 自动转换,Handler 里直接操作 String,不用碰 Buffer。
  3. 线程模型:Boss 1 线程接连接,Worker 4 线程处理 IO,连接数再多也不会 1 连接 1 线程。

九、全景对比与选型建议

维度 BIO NIO AIO Netty
连接模型 一连接一线程 多路复用 回调异步 多路复用(封装)
并发能力 差(千级) 高(万级) 高(万级) 高(万级)
编码难度 简单 复杂 复杂(回调地狱) 简单(Handler 链)
粘包处理 自己写 自己写 自己写 内置拆包器
空轮询 Bug 无 有(JDK Bug) 有 自动兜底
适用场景 连接少、内部通信 学习原理、轻量级 Windows 平台 生产级网络服务

选型建议:

  • 学习原理:写 NIO 代码理解多路复用;
  • 内部 RPC / 连接数少:BIO + 线程池够用;
  • 生产级网络服务 (IM、网关、游戏服务器):直接上 Netty,别自己造 NIO 轮子;
  • Windows 平台且需要极致异步:可以考虑 AIO,但社区生态不如 Netty。

十、总结

一图回顾全篇:
#mermaid-svg-ejCXg9sxK1cZr6nT{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ejCXg9sxK1cZr6nT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ejCXg9sxK1cZr6nT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ejCXg9sxK1cZr6nT .error-icon{fill:#552222;}#mermaid-svg-ejCXg9sxK1cZr6nT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ejCXg9sxK1cZr6nT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ejCXg9sxK1cZr6nT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ejCXg9sxK1cZr6nT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ejCXg9sxK1cZr6nT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ejCXg9sxK1cZr6nT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ejCXg9sxK1cZr6nT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ejCXg9sxK1cZr6nT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ejCXg9sxK1cZr6nT .marker.cross{stroke:#333333;}#mermaid-svg-ejCXg9sxK1cZr6nT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ejCXg9sxK1cZr6nT p{margin:0;}#mermaid-svg-ejCXg9sxK1cZr6nT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ejCXg9sxK1cZr6nT .cluster-label text{fill:#333;}#mermaid-svg-ejCXg9sxK1cZr6nT .cluster-label span{color:#333;}#mermaid-svg-ejCXg9sxK1cZr6nT .cluster-label span p{background-color:transparent;}#mermaid-svg-ejCXg9sxK1cZr6nT .label text,#mermaid-svg-ejCXg9sxK1cZr6nT span{fill:#333;color:#333;}#mermaid-svg-ejCXg9sxK1cZr6nT .node rect,#mermaid-svg-ejCXg9sxK1cZr6nT .node circle,#mermaid-svg-ejCXg9sxK1cZr6nT .node ellipse,#mermaid-svg-ejCXg9sxK1cZr6nT .node polygon,#mermaid-svg-ejCXg9sxK1cZr6nT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ejCXg9sxK1cZr6nT .rough-node .label text,#mermaid-svg-ejCXg9sxK1cZr6nT .node .label text,#mermaid-svg-ejCXg9sxK1cZr6nT .image-shape .label,#mermaid-svg-ejCXg9sxK1cZr6nT .icon-shape .label{text-anchor:middle;}#mermaid-svg-ejCXg9sxK1cZr6nT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ejCXg9sxK1cZr6nT .rough-node .label,#mermaid-svg-ejCXg9sxK1cZr6nT .node .label,#mermaid-svg-ejCXg9sxK1cZr6nT .image-shape .label,#mermaid-svg-ejCXg9sxK1cZr6nT .icon-shape .label{text-align:center;}#mermaid-svg-ejCXg9sxK1cZr6nT .node.clickable{cursor:pointer;}#mermaid-svg-ejCXg9sxK1cZr6nT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ejCXg9sxK1cZr6nT .arrowheadPath{fill:#333333;}#mermaid-svg-ejCXg9sxK1cZr6nT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ejCXg9sxK1cZr6nT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ejCXg9sxK1cZr6nT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ejCXg9sxK1cZr6nT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ejCXg9sxK1cZr6nT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ejCXg9sxK1cZr6nT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ejCXg9sxK1cZr6nT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ejCXg9sxK1cZr6nT .cluster text{fill:#333;}#mermaid-svg-ejCXg9sxK1cZr6nT .cluster span{color:#333;}#mermaid-svg-ejCXg9sxK1cZr6nT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ejCXg9sxK1cZr6nT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ejCXg9sxK1cZr6nT rect.text{fill:none;stroke-width:0;}#mermaid-svg-ejCXg9sxK1cZr6nT .icon-shape,#mermaid-svg-ejCXg9sxK1cZr6nT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ejCXg9sxK1cZr6nT .icon-shape p,#mermaid-svg-ejCXg9sxK1cZr6nT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ejCXg9sxK1cZr6nT .icon-shape .label rect,#mermaid-svg-ejCXg9sxK1cZr6nT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ejCXg9sxK1cZr6nT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ejCXg9sxK1cZr6nT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ejCXg9sxK1cZr6nT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 并发不够
底层 OS
异步回调
工程封装
BIO: 一连接一线程

阻塞 accept/read
NIO: 多路复用

Selector + Channel + Buffer
Linux: epoll

macOS: kqueue

Windows: IOCP/WSAPoll
AIO: CompletionHandler

Linux 用 epoll 模拟
Netty: 主从 Reactor

Pipeline + ByteBuf

拆包/心跳/重连 全内置

核心记忆点:

  1. BIO = 线程换连接,简单但不可扩展;
  2. NIO = 多路复用(Selector),一个线程管万连接,底层是 epoll/kqueue;
  3. AIO = 回调式真异步,Windows 用 IOCP,Linux 用 epoll 模拟,生态弱;
  4. 多路复用 = select(O(n),1024 上限)→ poll(O(n),无上限)→ epoll/kqueue(O(1),事件驱动)→ IOCP(完成通知,真异步);
  5. Netty = 对 NIO 的工程级封装,主从 Reactor + Pipeline + ByteBuf + 内置拆包器,是 Java 高并发网络编程的事实标准。

相关推荐
蜗牛互联网3 天前
Computer Use公共预览的安全设计:应用、动作与数据三维门禁
java·人工智能·后端·安全·策略模式
今年下半年3 天前
【Spring Boot】多种存储文件(FastDFS / MinIO / 阿里云 OSS)接入设计说明(附源码)
spring boot·分布式·中间件·简单工厂模式·策略模式
今天的砖头有点烫手啊4 天前
Java IO/NIO/AIO 演进:从 BIO 到 Netty 的底层逻辑
java·jvm·nio
Zkaisen6 天前
M4(Apple Silicon)· CVE-2020-13920 抓管理员密码 完整操作指南
策略模式
加载力科技7 天前
硬盘咔咔响是坏盘信号!赶在数据丢失前克隆换盘:不重装系统、不丢文件
windows·嵌入式硬件·系统安全·策略模式·命令模式·硬盘
名字还没想好☜22 天前
Java NIO ByteBuffer 实战:flip/clear/compact 三个绕晕人的方法与 position/limit 心智模型
java·开发语言·后端·spring·nio
滕州市燕猫虎计算机科技工作室个体工商户22 天前
java.nio.ByteBuffer的几个常用方法
nio
geovindu23 天前
java: Strategy Pattern
java·开发语言·后端·设计模式·策略模式·行为模式
geovindu24 天前
CSharp: Strategy Pattern
开发语言·后端·c#·.net·.netcore·策略模式·行为模式
沪上企服通1 个月前
代账数据的可移植性设计:开放接口、账套导出与 exit strategy 工程笔记(上海 5 家样本对照)
笔记·策略模式