BIO、Select、Poll、Epoll 四大IO模型详解笔记

BIO、Select、Poll、Epoll 四大IO模型详解笔记(通俗比喻+原理+对比+实战场景)

一、前置基础概念

1. 核心名词解释

  1. IO(输入/输出):网络读写、文件读写等数据交互操作,网络IO就是客户端与服务端的数据通信。
  2. Socket(套接字):网络连接的载体,一条客户端与服务端的长连接/短连接对应一个Socket。
  3. 线程:操作系统最小执行单元,创建、切换线程会消耗CPU资源,大量线程会引发上下文切换,导致性能暴跌。
  4. 长连接:连接建立后持续保持,服务端可主动推送数据(直播、聊天、游戏、弹幕必备)。
  5. 短连接:一次请求+一次响应后立即断开,普通网页、接口请求常用。
  6. IO多路复用一个线程/少量线程,同时监控大量Socket连接,是高并发网络服务的核心技术,Linux下主流实现为selectpollepoll
  7. 上下文切换:CPU在线程之间来回切换执行任务,频繁切换会产生巨大性能开销。
  8. 轮询:循环逐个检查每个连接是否有数据请求,无效轮询会浪费大量CPU。
  9. 事件回调:连接有数据/事件时主动通知程序,无需主动轮询,是Epoll高效的核心。

2. 场景引入

小型项目(几十在线用户)用简单IO模型即可;数万长连接高并发场景(直播、IM、游戏网关),必须使用IO多路复用,否则服务器会被大量连接拖垮。下文用饭店服务统一比喻,方便理解。

二、四大IO模型逐类解析

(一)BIO 阻塞IO(传统单连接单线程)

1. 工作原理

每建立一条客户端连接,服务端就单独创建一个线程专门处理该连接。线程全程阻塞,连接无数据时也不会释放,一直等待客户端请求。

2. 饭店比喻

饭店每来一桌客人(一条连接),就专门分配一名服务员(一个线程)全程陪同。客人不点菜、不说话,服务员也必须原地等待,不能服务其他桌。

3. 优缺点

  • 优点:实现简单,逻辑直白,适合并发量极小(几十人以内)的单体项目。
  • 缺点:
    1. 连接数 = 线程数,上万连接就会创建上万线程,CPU频繁上下文切换,资源耗尽;
    2. 大量空闲连接会占用线程资源,资源利用率极低。

4. 实战案例

早期小型管理系统、单机简易接口服务,在线用户少,并发压力小,使用BIO完全够用。
致命问题:一旦用于直播、IM等上万长连接场景,服务器直接卡死。


(二)Select(第一代IO多路复用)

1. 工作原理

使用单个线程监控所有Socket连接。线程会循环遍历全部连接,逐个检查是否有读写事件(有无请求),有则处理,无则继续轮询。

2. 饭店比喻

整个饭店只雇一名总服务员,拿着所有桌号清单,从第一桌走到最后一桌,挨个询问“是否需要服务”。哪怕99%客人都在挂机休息,也必须走完完整轮询。

3. 核心限制&缺点

  1. 连接数量有上限:Linux系统下默认最大监听连接数有限,无法支撑超大并发;
  2. 全量轮询,效率低下:连接越多,轮询耗时越长,空闲连接越多,无效CPU消耗越大;
  3. 仅告知“有事件”,不定位具体哪条连接,轮询无法避免。

4. 适用场景

中等并发连接,对性能要求不高的服务,目前基本被淘汰。


(三)Poll(第二代IO多路复用,Select升级版)

1. 工作原理

整体轮询逻辑和Select一致,仅解除连接数量上限,依然是单线程全量遍历所有连接。

2. 饭店比喻

还是一名总服务员轮询所有餐桌,不再限制餐桌总数,但依旧需要一桌一桌挨个询问,没有改变“无效轮询”的本质。

3. 优缺点

  • 优点:突破Select的连接数限制,可监听更多连接;
  • 缺点:保留轮询短板,连接越多、CPU无效消耗越严重,高并发下性能依旧拉胯。

4. 现状

部分老旧系统沿用,新项目基本不使用。


(四)Epoll(Linux第三代IO多路复用,工业级高并发首选)

1. 核心工作原理

彻底抛弃主动轮询,采用事件通知+回调机制
内核为每个连接绑定“事件监听”,当连接有读写、数据到达等事件时,主动通知应用线程,线程只处理有事件的连接,空闲连接完全不占用CPU。

2. 饭店比喻(经典易懂)

给每一桌客人安装呼叫铃

  1. 服务员平时休息待命,不用来回走动轮询;
  2. 客人需要服务(有IO事件),按下呼叫铃;
  3. 服务员收到通知,精准前往对应桌号处理业务;
  4. 处理完毕继续休息,全程无无效跑动。

3. 核心三大优势

  1. 无连接数上限:理论可支持百万级长连接,适配互联网高并发场景;
  2. 无无效轮询:仅处理活跃连接,百万空闲连接几乎不消耗CPU;
  3. 事件精准通知:内核直接告知具体哪个连接、发生什么事件,定位效率拉满。

4. 补充说明

  • 平台限制:Epoll是Linux系统专属;Windows系统对应技术为IOCP(完成端口);
  • 主流底层依赖:Redis、Nginx、Node.js、Netty、IM网关、直播弹幕服务器等高并发中间件/框架,底层均基于Epoll实现。

5. 实战案例

  1. 直播弹幕服务器:数十万用户同时建立长连接,绝大部分时间处于空闲状态,Epoll用少量线程管理全部连接,仅在用户发弹幕时触发处理;
  2. Redis服务:海量客户端连接,依靠Epoll实现高并发网络读写;
  3. 微信/QQ IM网关:支撑亿级长连接,核心IO模型为Epoll。

三、四大模型横向对比表

IO模型 核心模式 线程模型 连接上限 CPU消耗 适用场景
BIO 一连接一线程 多线程 极低(线程限制) 高(上下文切换) 小型项目、低并发短连接
Select 单线程全量轮询 单线程多路复用 有上限 中高(无效轮询) 老旧中等并发系统
Poll 单线程全量轮询 单线程多路复用 无上限 中高(无效轮询) 过渡型系统,逐步淘汰
Epoll 事件回调、按需处理 单/少量线程 百万级无上限 极低(仅处理活跃连接) 高并发长连接:直播、IM、网关、中间件

四、关键问题解答(面试高频)

1. 为什么BIO无法支撑上万长连接?

上万连接会创建上万线程,操作系统线程资源有限,且大量线程切换会耗尽CPU,大部分线程还处于空闲等待状态,资源严重浪费。

2. Select/Poll 和 Epoll 最本质区别?

Select/Poll:主动轮询,挨个检查所有连接;
Epoll:被动事件通知,有事件才处理,空闲连接完全无视。

3. Epoll 为什么是高并发长连接的标配?

长连接场景下绝大多数连接长期空闲,Epoll 规避无效轮询和多线程开销,用极低资源承载海量连接。

4. Windows 可以使用 Epoll 吗?

不可以,Epoll 仅Linux;Windows 平台使用 IOCP(IO完成端口),功能与Epoll对等。

五、业务选型指南

  1. 小型CRUD项目、低并发短连接:普通BIO即可,开发简单,无需复杂优化;
  2. 中等并发、老旧系统:历史项目维持Select/Poll,新项目不推荐;
  3. 高并发、海量长连接(直播、聊天、游戏、网关、中间件):Linux环境优先Epoll,Windows使用IOCP;
  4. Java开发建议:使用Netty框架(底层封装Epoll,屏蔽底层差异,开箱即用)。

六、思维拓展(原文引申)

  1. BIO 对应工作状态:机械分工,一人盯一件事,大量时间空耗,资源利用率低;
  2. Select/Poll 对应工作状态:看似不停忙碌,实则大量无效工作,效率低下;
  3. Epoll 对应工作状态:聚焦核心事件,无事休整、有事精准出击,拒绝无效内卷,是高效做事的思路。

七、整体总结

  1. 从BIO到Epoll,演进核心是用更少线程,管理更多连接,消灭无效CPU消耗
  2. IO多路复用是高并发网络编程的基石,Epoll 凭借事件驱动成为Linux下的最优解;
  3. 技术选型贴合业务:短连接低并发不用过度设计,海量长连接必须选用Epoll这类高性能模型。

补充:Java NIO、Netty 底层 Epoll 实战代码示例

前置说明:

  1. Java NIO 对应操作系统 IO 多路复用:Windows 用 Select,Linux 默认自动适配 Epoll;
  2. JDK NIO 原生 Selector 在 Linux 底层封装 epoll;
  3. Netty 默认自动检测 Linux 环境,启用 Epoll 原生传输EpollEventLoopGroup),性能优于 JDK 普通 NIO Selector;
  4. BIO = java.net.ServerSocket;NIO = java.nio.channels;Netty 封装 NIO/Epoll。

一、基础名词补充

  1. Channel:NIO 里的 Socket 连接,对应 Epoll 监听的文件描述符 fd;
  2. Selector:多路复用器,底层 Linux 封装 epoll_create/epoll_wait;
  3. SelectionKey:注册事件(可读、可写、连接就绪),对应 epoll 事件回调;
  4. EpollEventLoopGroup:Netty Linux 专属,直接调用系统 epoll 原生 API,无 JDK 层封装损耗。

二、原生 JDK NIO(底层 Epoll)极简服务端代码

运行环境:Linux,JDK8+,底层自动使用 epoll。

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NioEpollServer {
    public static void main(String[] args) throws IOException {
        // 1. 创建多路复用器Selector,Linux底层调用epoll_create
        Selector selector = Selector.open();

        // 2. 开启服务通道
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(8888));
        // 设置非阻塞(Epoll必须非阻塞IO)
        serverChannel.configureBlocking(false);

        // 3. 将服务通道注册到selector,监听【连接就绪事件】
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("NIO服务启动,端口8888,底层使用Epoll");

        // 4. 循环事件轮询(底层调用epoll_wait,阻塞等待事件)
        while (true) {
            // 阻塞等待有事件到达,无事件则休眠,不消耗CPU(Epoll核心优势)
            selector.select();

            // 获取所有产生事件的连接
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iter = keys.iterator();

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

                if (key.isAcceptable()) {
                    // 事件1:有新客户端连接
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel client = server.accept();
                    client.configureBlocking(false);
                    // 客户端通道注册到selector,监听读事件
                    client.register(selector, SelectionKey.OP_READ);
                    System.out.println("新客户端接入:" + client.getRemoteAddress());
                } else if (key.isReadable()) {
                    // 事件2:客户端有数据可读(epoll触发EPOLLIN事件)
                    SocketChannel client = (SocketChannel) key.channel();
                    ByteBuffer buf = ByteBuffer.allocate(1024);
                    int len = client.read(buf);
                    if (len <= 0) {
                        // 客户端断开,关闭通道
                        key.cancel();
                        client.close();
                        continue;
                    }
                    buf.flip();
                    String msg = new String(buf.array(), 0, len);
                    System.out.println("收到客户端消息:" + msg);
                    // 回写数据
                    client.write(ByteBuffer.wrap("服务端已收到消息\n".getBytes()));
                }
            }
        }
    }
}

代码对应 Epoll 底层逻辑

  1. Selector.open() → 系统调用 epoll_create 创建 epoll 实例;
  2. channel.register()epoll_ctl 把 socket fd 添加到 epoll 监听列表;
  3. selector.select() → 阻塞调用 epoll_wait,无IO事件时线程休眠;
  4. 客户端发数据 → 内核主动触发事件,select() 立刻返回,只遍历有事件连接,无无效轮询。

局限

JDK NIO 的 Selector 是跨平台封装,Linux 虽底层是 epoll,但存在一层 Java 堆/内核缓冲区拷贝,性能不如 Netty 原生 Epoll。

三、Netty 原生 Epoll 高并发服务端代码(Linux专属最优方案)

Netty 区分两套实现:

  1. 跨平台 NIO:NioEventLoopGroup(Windows/Linux通用,底层Linux走epoll)
  2. Linux 高性能专属:EpollEventLoopGroup 直接调用 epoll 原生系统API,零多余封装,IM、网关、直播推荐使用。

Maven依赖

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.100.Final</version>
</dependency>

Netty Epoll 服务端完整示例

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;

public class NettyEpollServer {
    public static void main(String[] args) throws InterruptedException {
        // 1. 使用Epoll专属线程池(仅Linux可用,底层原生epoll)
        // bossGroup:处理连接建立;workerGroup:处理读写事件
        EpollEventLoopGroup bossGroup = new EpollEventLoopGroup(1);
        EpollEventLoopGroup workerGroup = new EpollEventLoopGroup(4);

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    // 指定Epoll服务通道,直接绑定系统epoll
                    .channel(EpollServerSocketChannel.class)
                    .childHandler(new SimpleServerHandler());

            // 绑定8888端口启动服务
            ChannelFuture future = bootstrap.bind(8888).sync();
            System.out.println("Netty Epoll服务启动成功,端口8888");
            // 阻塞等待服务关闭
            future.channel().closeFuture().sync();
        } finally {
            // 优雅关闭线程池
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    // 自定义业务处理器
    static class SimpleServerHandler extends ChannelInboundHandlerAdapter {
        // 客户端发送数据触发(对应epoll可读事件)
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            ByteBuf buf = (ByteBuf) msg;
            byte[] data = new byte[buf.readableBytes()];
            buf.readBytes(data);
            String content = new String(data);
            System.out.println("Netty收到消息:" + content);

            // 回复客户端
            ctx.writeAndFlush(ctx.alloc().buffer().writeBytes("Epoll服务响应成功\n".getBytes()));
            super.channelRead(ctx, msg);
        }

        // 客户端断开连接
        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("客户端连接断开");
            super.channelInactive(ctx);
        }
    }
}

关键说明

  1. EpollEventLoopGroup + EpollServerSocketChannel = Linux 原生 epoll,跳过 JDK NIO 中间层,性能更高;
  2. 少量工作线程(示例4个)即可管理上万/百万长连接,完全依托 epoll 事件驱动;
  3. 空闲长连接不会占用CPU,只有发送消息时触发回调处理。

四、三种IO模型代码效果对比

1. BIO 代码缺陷对比(一连接一线程)

// BIO 伪代码,不推荐长连接场景
ServerSocket server = new ServerSocket(8888);
while(true){
    // 每来一个连接新建线程
    Socket socket = server.accept();
    new Thread(() -> {
        while(true){
            socket.getInputStream().read(); // 线程阻塞卡死,占用资源
        }
    }).start();
}

上万长连接会创建上万线程,CPU上下文切换爆满,和Epoll形成鲜明差距。

2. JDK NIO Selector vs Netty Epoll

实现 底层 性能 适用场景
BIO 阻塞单连接线程 极差,并发受限 本地小型工具
JDK NIO Selector Linux epoll(封装层) 中等 跨平台简单服务
Netty EpollEventLoop Linux原生epoll API 最优 IM网关、直播、百万长连接、中间件

五、线上生产落地案例结合代码

案例1:直播弹幕网关(Netty Epoll)

直播间十万用户长连接,99%时间空闲,仅发弹幕时有IO事件:

  1. 使用 EpollEventLoopGroup,4个工作线程承载10万连接;
  2. 无弹幕时 epoll 阻塞休眠,CPU占用低于5%;
  3. 用户发送弹幕触发可读事件,线程批量处理推送至直播间;
    若改用BIO,10万连接需要10万线程,服务器直接OOM卡死。

案例2:Redis底层epoll逻辑

Redis C语言直接调用epoll API,思路和Netty Epoll一致:

  1. 注册所有客户端fd到epoll;
  2. 无命令时阻塞等待事件;
  3. 客户端发送指令触发事件,单线程处理读写,支撑十万客户端连接。

六、核心总结

  1. Linux环境下,JDK NIO Selector底层封装epoll,实现多路复用,替代BIO多线程;
  2. Netty 提供专属 EpollEventLoopGroup,直接调用系统epoll,是Java高并发长连接最优方案;
  3. 代码核心特征:全部通道设置非阻塞,依靠事件通知,放弃循环轮询,海量空闲连接几乎零CPU消耗;
  4. 开发规范:Linux生产环境网关、IM、消息推送服务强制使用 Netty Epoll 模式。
posted @ 2026-06-15 23:05  堭鍙銤  阅读(28)  评论(0)    收藏  举报