<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>IO on 安橙的博客</title><link>https://blog.ans20xx.com/tags/io/</link><description>Recent content in IO on 安橙的博客</description><generator>Hugo -- 0.163.3</generator><language>zh</language><lastBuildDate>Sat, 21 Feb 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.ans20xx.com/tags/io/index.xml" rel="self" type="application/rss+xml"/><item><title>Java IO</title><link>https://blog.ans20xx.com/posts/backend/java-io/</link><pubDate>Sat, 21 Feb 2026 00:00:00 +0000</pubDate><guid>https://blog.ans20xx.com/posts/backend/java-io/</guid><description>&lt;h1 id="io-模型与概念框架"&gt;IO 模型与概念框架&lt;/h1&gt;
&lt;div
class="mindmap-container"
id="mindmap-78165234"
style="width:100%; height:860px; min-height: 860px;"
&gt;&lt;/div&gt;
&lt;textarea id="mindmap-data-78165234" style="display:none;"&gt;
- IO 模型与概念框架
- 四个基本概念
- 阻塞 vs 非阻塞
- 定义：一次调用在数据未就绪时是否立即返回
- 阻塞：调用线程进入等待，直到条件满足才返回，例如：InputStream.read()，数据没到就卡住
- 非阻塞：调用立即返回，不等数据
- 读：没数据时返回 0/-1 或抛特定异常
- 写：写不完就写一部分，返回已写字节数
- JavaNIO
- SocketChannel.configureBlocking(false) 之后，read() 不会阻塞，可能返回 0
- 阻塞非阻塞指的就是线程是否在这次调用上被卡主
- 同步 vs 异步
- 定义：结果是由谁来完成并通知
- 同步：调用负责发起&amp;#43;等待/轮询&amp;#43;拿结果，即使是非阻塞轮询，仍然是同步，因为结果获取的责任方在调用方
- 异步：调用方发起后立即返回，后续由系统/框架在操作完成时回调/事件通知取结果，调用方不需要自己轮询等待结果
- JavaNIO
- AsynchronousSocketChannel &amp;#43; CompletionHandler 是典型的 AIO 异步模型：完成时回调你
- 四象限组合
- 同步阻塞：BIO（经典 read() 卡住直到有数据）
- 同步非阻塞：NIO 非阻塞 read() &amp;#43; 你自己轮询/配合 Selector 处理就绪事件，本质仍然是调用方驱动流程
- 异步非阻塞：AIO（回调通知你“读完了/写完了“）
- 异步阻塞：理论上存在，比如发起异步后又 get() 阻塞等待，但是工程上不这么用
- BIO/NIO/AIO 的准确定义
- BIO(Blocking IO)
- API 代表：ServerSocket/Socket、InputStream/OutputStream
- 特征：accept()/read()/write() 默认阻塞
- 典型服务端模型：1 连接 1 线程（或线程池&amp;#43;阻塞读写）
- 痛点：
- 连接数上来后线程数膨胀：上下文切换、栈内存、调度开销
- 大量线程处于阻塞态，浪费资源
- NIO(Non-blocking I/O &amp;#43; Multiplexing)
- API 代表：Channel/Buffer/Selector
- 关键点：非阻塞 Channel &amp;#43; Selector 多路复用
- 本质：少量线程监听大量连接的就绪事件，再去执行非阻塞读写
- 价值：
- 连接多时不需要等比例增加线程
- 事件驱动，提升并发连接处理能力
- NIO 三件套
- Channel：数据容器，可读可写
- Buffer：数据容器
- Selector：事件分发器
- AIO(Asynchronous I/O)
- API 代表 AsynchronousServerSocketChannel/AsynchronousSocketChannel
- 特征：发起读写后立即返回，完成后通过 CompletionHandler 回调通知结果
- 工程事实：Java AIO 在不同 OS 上底层支持与效果不同；很多高性能网络主流仍然是 Netty（基于 NIO 的 Reactor &amp;#43; 工程化）
- 多路复用（IO Multiplexing）
- 核心：一个线程可以等待多个 fd 的事件
- 没有多路复用：要等待某个 socket 可读，只能在这个 socket 上阻塞 read
- 有多路复用：线程阻塞在 selector/poll/epoll_wait 上，内核告诉你那些 fd 可读/可写，再去处理
- Java NIO 的 Selector.select()=在做等事件，底层映射到 OS 的 select/poll/epoll/kqueue
- Reactor 模式（NIO 常见线程模型）
- Reactor：事件循环(event loop) &amp;#43; 分发(dispatch)
- 流程
- selector.select() 事件
- 拿到 selected keys
- 根据事件类型调用对于 handler
- 单 Reactor 单线程：简单，适合低负载
- 主从 Reactor(boss/worker)：boss 负责 accept，worker 负责读写
&lt;/textarea&gt;
&lt;h1 id="java-bio"&gt;Java BIO&lt;/h1&gt;
&lt;div
class="mindmap-container"
id="mindmap-78642315"
style="width:100%; height:860px; min-height: 860px;"
&gt;&lt;/div&gt;
&lt;textarea id="mindmap-data-78642315" style="display:none;"&gt;
- Java BIO
- IO 流体系
- 两大抽象
- 字节流：InputStream/OutputStream
- 面向原始字节
- 字符流：Reader/Writer
- 面向字符，内部会涉及字符集解码/编码
- 涉及文本语义/字符集，就优先用 Reader/Writer 或者用 InputStream &amp;#43; 明确 Charset 解码；处理二进制就用 InputStream/OutputStream
- 节点流 vs 处理流
- 节点流：直接连接数据源/目的地
- FileInputStream、FileOutputStream、Socket.getInputStream
- 处理流：包装节点流，增强能力（缓冲、转码、数据类型、对象序列化、压缩、校验）
- BufferedInputStream、InputStreamReader、DataInputStream、ObjectInputStream
- 装饰器模式：不改原类，通过包装叠加功能
- Buffered 缓冲为什么快
- 不加缓冲为什么慢
- 许多 read()/write() 最终都会调用 OS 层的 read(2)/write(2) 系统调用，系统开销不小
- 不缓冲：每读 1KB 就要系统调用一次，次数巨大
- 加缓冲：一次系统调用读 8KB/64KB 放到用户态缓冲区，后续小读都在内存中完成，降低系统调用次数
- BufferedInputStream/BufferedOutputStream
- BufferedInputStream：内部有 byte[] buffer（默认 8KB），先批量从底层读，再按需给你
- BufferedOutputStream：先写入 buffer，满了再 flush 到底层，减少了 write 的系统调用次数
- flush 是什么
- 把用户缓冲区里的数据推下去
- flush() != 一定落盘
- 字符集与桥接流
- InputStreamReader/OutputStreamWriter
- InputStreamReader：字节 -&amp;gt; 字符（解码），需要 Charset
- OutputStreamWriter：字符 -&amp;gt; 字节（编码），需要 Charset
- 典型用法
- 读文本
- BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(f), UTF_8))
- 写文本
- BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f), UTF_8))
- 为什么不能随便 new String(byte[])
- 不指定 charset，会走平台默认编码，导致乱码
- 常见功能处理流
- DataInputStream/DataOutputStream（理解二进制协议）
- 能按固定类型读写：writeInt/readInt，writeLong/readLong
- 用途：自定义二进制协议、文件格式
- DataInputStream 读写的是 Java 规定格式，跨语言要慎重，更常见的跨语言协议是 protobuf、msgpack 等
- ObjectInputStream/ObjectOutputStream
- Java 原生序列化：实现 Serializable
- 问题：性能一般、版本兼容差、安全风险
- 不建议用于 RPC/外部输入，内部常见简单可用，生产用 protobuf/kryo/json 等
- PrintWriter/PrintStream（输出遍历但注意编码/flush）
- System.out 是 PrintStream
- PrintWriter 可选 autoFlush（遇到 println/printf/format 自动 flush）
- PrintWriter 默认吞异常
- 资源管理与异常
- try-with-resources
- 自动 close，且 close 顺序与声明顺序相反
- close 过程中异常会作为 suppressed 异常附加到主屏幕
- close vs flush
- close vs flush
- close() 会隐式 flush()，不能依赖进程崩溃前的 close
- 网络场景：写完重要数据要主动 flush 或者使用协议层 ack。
- BIO 与网络 IO：Socket 阻塞读写的关键点
- 阻塞读到集中返回情况
- InputStream.read(byte[])
- 返回 &amp;gt;0：读到的字节数
- 返回 -1：对端关闭输出
- 可能一直阻塞：对端不发数据也不关
- 半包/粘包在 BIO 也存在
- BIO 只是阻塞，TCP 仍然是字节流，read() 不保证一条业务消息完整到达
&lt;/textarea&gt;
&lt;h1 id="nio-基础-bufferchannel"&gt;NIO 基础 Buffer/Channel&lt;/h1&gt;
&lt;div
class="mindmap-container"
id="mindmap-21486573"
style="width:100%; height:860px; min-height: 860px;"
&gt;&lt;/div&gt;
&lt;textarea id="mindmap-data-21486573" style="display:none;"&gt;
- NIO 基础
- NIO 的核心心智模型：Buffer&amp;#43;Channel
- 在 NIO 中，数据永远在 Buffer 里转
- 读：Channel.read(ByteBuffer) 把数据写进 buffer
- 写：Channel.write(ByteBuffer) 从 buffer 读出数据写到 channel
- Channel 是管道，Buffer 是货箱，NIO 必须管理 buffer 状态
- ByteBuffer 属性与状态
- 关键属性
- capacity：容量，创建后固定
- position：当前位置（下次读/写从这里开始）
- limit：边界（读/写不能超过它）
- mark：标记位置（可选）
- remaing()：limit - position
- 两种模式
- 写模式：准备往 buffer 里放数据（例如从 channel read 进来）
- position 向后走，limit 通常等于 capacity
- 读模式：准备从 buffer 里取数据（例如写到 channel 或解析协议）
- position 向后走，limit 表示可读数据的末尾
- 三个最重要的方法
- flip：写模式 -&amp;gt; 读模式
- 把 limit 设置为当前 position，再把 position 设置为 0
- 我写完了，现在从头读刚写的数据
- clear：读完后重置为写模式（不清数据，只重置指针）
- position = 0; limit = capacity; mark = -1
- 不关心旧数据了，直接当成空 buffer 继续写
- compact：读了一部分，保留剩余未读数据，并切换为写模式
- 把未读数据 [position,limit) 移动到开头
- position=remaining;limit=capacity
- 我还剩一点没处理，先挪到前面，下次继续在后面写新数据
- HeapBuffer vs DirectBuffer
- Heap ByteBuffer（堆内）
- ByteBuffer.allocate(n)
- 好处：分配快，受 JVM GC 管理，排查简单
- 坏处：做网络 IO 时需要把数据从堆复制到内核缓冲区
- Direct ByteBuffer（堆外）
- ByteBuffer.allocate(n)
- 好处：减少一次拷贝，适合大块网络 IO，频繁 IO。
- 坏处
- 分配/释放更贵
- 受 MaxDirectMemorySize 限制
- 回收依赖 Cleaner/GC 时机，不是=null 就立刻释放
- Channel：FileChannel 与 SocketChannel
- FileChannel
- 来源
- FileInputStream.getChannel()
- FileOutputStream.getChannel()
- 常用能力
- read(ByteBuffer)/write(ByteBuffer)
- position()/position(long)：随机读写位置
- size() 文件大小
- force(boolean metaData)：把修改刷到存储设备
- transferTo/transferFrom：为后续零拷贝铺垫
- SocketChannel
- 支持 configureBlocking(false) NIO 非阻塞基石
- NIO 文件读写的标准流程
- FileChannel 读取
- buffer.clear() 进入写模式
- int n = channel.read(buffer) 把数据写进 buffer
- buffer.flip() 进入读模式
- 从 buffer 读取处理
- 循环直到 n == -1
&lt;/textarea&gt;
&lt;h1 id="nio-进阶-selector多路复用"&gt;NIO 进阶 Selector/多路复用&lt;/h1&gt;
&lt;div
class="mindmap-container"
id="mindmap-41753268"
style="width:100%; height:860px; min-height: 860px;"
&gt;&lt;/div&gt;
&lt;textarea id="mindmap-data-41753268" style="display:none;"&gt;
- NIO 进阶 Selector/多路复用
- Selector/SelectionKey/Channel：三者关系与生命周期
- 三件套职责
- Selector：事件复用器，阻塞在 select() 等待就绪事件，把就绪的 key 放进 selectedKeys 集合
- SelectableChannel：可注册到 Selector 的通道
- SelectionKey：一次注册的凭证/句柄，包含
- channel()：对于 channel
- selector()：归属 selector
- interestOps()：你关心那些事件
- readyOps()：当前就绪了哪些事件
- attachment()：绑定业务状态
- 注册与事件的基本规则
- channel.register(selector, OP_READ, attachment)：把 channel 注册给 selector，并声明兴趣事件
- interestOps 是订阅，可以动态改
- readyOps 是本轮 select 的结果，不能直接改
- key 的取消与资源释放
- key.cancel()：从 selector 的 key 集合中移除
- 移动要同时 close channel：channel.close()，否则 fd 泄露
- 被 cancel 的 key 不会立刻从所有集合消失，通常在下一次 select 时清理
- 多路复用：就绪事件语义
- 就绪不是完成
- OP_READ 就绪：表示现在读不阻塞，不保证读到一条完整的业务消息
- OP_WRITE 就绪：表示现在写更可能成功，不保证一次 write 写完你要写的所有数据
- 各个事件含义
- OP_ACCEPT：ServerSocketChannel 有新连接可以 accept
- OP_CONNECT：客户端连接建立过程完成（非阻塞 connect）
- OP_READ：SocketChannel 可读
- OP_WRITE：SocketChannel 可写（注意：很多时候“总是可写”）
- Reactor 模式：从单线程到主从 Reactor
- 单线程 Reactor
- 一个线程做三件事
- selector.select() 等待事件
- 遍历 selectedKeys
- 按事件类型分发处理：accept/read/write
- 优点：简单
- 缺点：业务处理慢会阻塞整个事件循环
- 主从 Reactor（boss/worker）
- boss：只负责 accept，把新 SocketChannel 分发到 worker 的 selector
- worker：负责读写与协议解析、业务派发
&lt;/textarea&gt;
&lt;h1 id="netty"&gt;Netty&lt;/h1&gt;
&lt;div
class="mindmap-container"
id="mindmap-16542738"
style="width:100%; height:860px; min-height: 860px;"
&gt;&lt;/div&gt;
&lt;textarea id="mindmap-data-16542738" style="display:none;"&gt;
- Netty
- 解决什么问题
- 原生 NIO 的痛点
- 需要自己管理：Selector 轮询、Key 生命周期、半包/粘包、写不完与 OP_WRITE 开关
- Buffer 管理复杂：flip/compact、输入输出缓冲复用、DirectBuffer 回收时机
- 性能与稳定性：Selector 空轮询、内存膨胀、背压、线程隔离
- Netty 提供的工程化能力
- 线程模型（EventLoopGroup &amp;#43; Reactor）成熟
- Pipeline/Handler 体系：解码、业务、编码职责分离
- ByteBuf：读写指针友好、池化、零拷贝视图、引用计数
- 编解码器、背压、写队列、水位线、内存泄露检测等工具链
- 线程模型
- EventLoop
- EventLoop = 一个线程 &amp;#43; 一个 Selector &amp;#43; 一组 Channel
- EventLoop 负责
- 处理 IO 事件
- 执行任务队列
- 同一个 Channel 的所有 Handler 回调默认都在通一个 EventLoop 线程执行，一致性，减少锁
- EventLoopGroup
- bossGroup：负责 accept（ServerSocketChannel）
- workerGroup：负责已建立连接的读写（SocketChannel）
- 为什么少量线程能抗大量连接
- 事件驱动：线程主要阻塞在 epoll/kqueue 上，事件就绪才处理
- 连接多不会导致线程线性增长
- Channel、Future、Promise：异步编程语义
- ChannelFuture
- Netty 大量 API 返回 ChannelFuture，比如 bind、connect、writeAndFlush，表示操作已发起，完成会通知
- future.addListener(...)：完成回调（推荐）
- future.sync()：阻塞等待
- Promise
- 可写的 future，用于自己控制完成/失败
- Pipeline/Handler：职责分离
- Pipeline 是什么
- 每个 Channel 都有一条 ChannelPipeline
- Inbound（入站）：数据从 socket 进来 -&amp;gt; 解码 -&amp;gt; 业务处理
- Outbound（出站）：业务写出 -&amp;gt; 编码 -&amp;gt; flush 到 socket
- Handler 分类
- ChannelInboundHandler：处理入站事件（channelRead、channelActive…）
- ChannelOutboundHandler：处理出站事件（write、flush…）
- ChannelDuplexHandler：双向
- SimpleChannelInboundHandler&amp;lt;T&amp;gt;：自动释放入站消息（配合 ByteBuf 引用计数要理解）
- ByteBuf
- 为什么 ByteBuf 比 ByteBuffer 友好
- 两个指针：readerIndex/writerIndex
- 不需要 flip：写完读直接通过读指针移动
- 支持切片/复制语义清晰
- 三种常见 ByteBuf
- Heap：堆内
- Direct：堆外
- CompositeByteBuf：多个 buffer 组合成一个逻辑 buffer（减少拷贝）
&lt;/textarea&gt;
&lt;h1 id="selectpollepoll"&gt;select/poll/epoll&lt;/h1&gt;
&lt;div
class="mindmap-container"
id="mindmap-28631457"
style="width:100%; height:860px; min-height: 860px;"
&gt;&lt;/div&gt;
&lt;textarea id="mindmap-data-28631457" style="display:none;"&gt;
- select/poll/epoll
- 简介
- 都是操作系统提供的 I/O 多路复用机制
- 让一个线程同时等待很多文件描述符的可读/可写/异常事件，然后再去对就绪的 fd 做 read/write
- 术语
- fd：内核里对打开的文件/套接字等资源的编号。网络 socket 也是 fd。
- 就绪：表示现在读/写大概率不会阻塞，不等于你一次就能读到完整业务包/一次写完所有数据
- select：最老牌的多路复用
- 工作方式：把三组 fd（读/写/异常） 集合传给内核，内核扫描那些就绪，然后返回就绪集合。
- 特点：
- fd 数量上限：select 受到 FD_SETSIZE 限制，常见 1024
- 每次调用要拷贝与扫描：用户态要把位图/集合传入内核，内核要线性扫描
- 集合会被修改：返回时就绪集合是被改过的，下次调用要重新准备集合
- 适用：
- fd 少，跨平台，简单场景。
- poll：对 select 的数组版改良
- 工作方式：把 fd 列表放在一个 pollfd[] 数组传递给内核，内核扫描并在数组里标记那些就绪。
- 相比 select 改进
- 不收 FD_SETSIZE 位图限制
- 表达更灵活
- 缺点
- 仍然是 O(n) 扫描
- 每次调用仍然需要把数组从用户态传给内核
- epoll：为海量连接而生
- epoll 是 Linux 的高性能事件通知机制，核心思想是：把关注列表留在内核里，避免每次把大集合传来传去，返回就绪列表而不是让自己全量扫描
- 关键点
- 关注列表在内核维护：避免每轮 select/poll 大集合拷贝&amp;#43;扫描
- epoll_wait 返回的是就绪事件列表，只处理这些 fd，更接近 O(就绪数)
- LT vs ET 触发模式
- LT (Level-Triggered 水平触发，默认)
- 只要还可读/可写，就会持续通知你
- 编程更简单：读不完下次还会通知你
- ET (Edge-Triggered，边缘触发)
- 只有从不可读 -&amp;gt; 可读的边缘变化才通知一次
- 必须把数据一次性读到不能再读，否则可能丢通知导致卡住
- 性能更高，但对实现要求更严格（非阻塞&amp;#43;循环读/写）
&lt;/textarea&gt;</description></item></channel></rss>