从 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 通用)

pollselect 的改进版(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 上的底层就是 epollSelector.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 上的底层就是 kqueueKQueueSelectorImpl)。

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 复杂 要手动管 Bufferflip/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 检测到空轮询自动重建 SelectorNioEventLoop 兜底)
线程模型 主从 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 会自动拆成 helloworld 两条消息------这在原生 NIO 里要你自己写拆包逻辑。
  2. 编解码StringDecoder/EncoderByteBuf ↔ 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 高并发网络编程的事实标准。

相关推荐
cui_hao_nan1 天前
大模型负责“聪明“,策略模式负责“执行“:智能配图系统如何自动选择图片来源
策略模式·ai开发
阿萨德528号4 天前
npm 包发布实战指南:从零发布、更新迭代到版本治理
前端·npm·策略模式
AI人工智能+电脑小能手5 天前
大白话说Java设计模式-26-策略模式(业务实战篇)
java·spring·设计模式·策略模式·支付系统·算法切换
coding4u5 天前
Mac 多桌面(Spaces)的正确用法:你的屏幕可以大三倍
macos·docker·计算机外设·文件管理·策略模式
AI人工智能+电脑小能手5 天前
大白话说Java设计模式-27-策略模式(源码剖析篇)
java·spring·设计模式·策略模式·源码分析·事务管理·comparator
小此方5 天前
Linux加餐(一):藏在Linux中的设计模式(一)策略模式与日志
linux·设计模式·策略模式
coding4u14 天前
Mac 外接显示器完全指南:从插上线到调好每个参数
macos·计算机外设·文件管理·显示器·策略模式·quick look扩展
神奇霸王龙16 天前
AgentDesk 配置 Codex+ selltoken 中转 API 实测教程(2026 年 8 月更新)
人工智能·ai·ai编程·策略模式·codex·agentdesk
Bill FANG20 天前
从原生Java NIO到Netty重构:充电桩Socket服务端高性能改造实战
java·重构·nio