IO 模型与概念框架
- IO 模型与概念框架
- 四个基本概念
- 阻塞 vs 非阻塞
- 定义:一次调用在数据未就绪时是否立即返回
- 阻塞:调用线程进入等待,直到条件满足才返回,例如:InputStream.read(),数据没到就卡住
- 非阻塞:调用立即返回,不等数据
- 读:没数据时返回 0/-1 或抛特定异常
- 写:写不完就写一部分,返回已写字节数
- JavaNIO
- SocketChannel.configureBlocking(false) 之后,read() 不会阻塞,可能返回 0
- 阻塞非阻塞指的就是线程是否在这次调用上被卡主
- 同步 vs 异步
- 定义:结果是由谁来完成并通知
- 同步:调用负责发起+等待/轮询+拿结果,即使是非阻塞轮询,仍然是同步,因为结果获取的责任方在调用方
- 异步:调用方发起后立即返回,后续由系统/框架在操作完成时回调/事件通知取结果,调用方不需要自己轮询等待结果
- JavaNIO
- AsynchronousSocketChannel + CompletionHandler 是典型的 AIO 异步模型:完成时回调你
- 四象限组合
- 同步阻塞:BIO(经典 read() 卡住直到有数据)
- 同步非阻塞:NIO 非阻塞 read() + 你自己轮询/配合 Selector 处理就绪事件,本质仍然是调用方驱动流程
- 异步非阻塞:AIO(回调通知你“读完了/写完了“)
- 异步阻塞:理论上存在,比如发起异步后又 get() 阻塞等待,但是工程上不这么用
- 阻塞 vs 非阻塞
- BIO/NIO/AIO 的准确定义
- BIO(Blocking IO)
- API 代表:ServerSocket/Socket、InputStream/OutputStream
- 特征:accept()/read()/write() 默认阻塞
- 典型服务端模型:1 连接 1 线程(或线程池+阻塞读写)
- 痛点:
- 连接数上来后线程数膨胀:上下文切换、栈内存、调度开销
- 大量线程处于阻塞态,浪费资源
- NIO(Non-blocking I/O + Multiplexing)
- API 代表:Channel/Buffer/Selector
- 关键点:非阻塞 Channel + Selector 多路复用
- 本质:少量线程监听大量连接的就绪事件,再去执行非阻塞读写
- 价值:
- 连接多时不需要等比例增加线程
- 事件驱动,提升并发连接处理能力
- NIO 三件套
- Channel:数据容器,可读可写
- Buffer:数据容器
- Selector:事件分发器
- AIO(Asynchronous I/O)
- API 代表 AsynchronousServerSocketChannel/AsynchronousSocketChannel
- 特征:发起读写后立即返回,完成后通过 CompletionHandler 回调通知结果
- 工程事实:Java AIO 在不同 OS 上底层支持与效果不同;很多高性能网络主流仍然是 Netty(基于 NIO 的 Reactor + 工程化)
- BIO(Blocking IO)
- 多路复用(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) + 分发(dispatch)
- 流程
- selector.select() 事件
- 拿到 selected keys
- 根据事件类型调用对于 handler
- 单 Reactor 单线程:简单,适合低负载
- 主从 Reactor(boss/worker):boss 负责 accept,worker 负责读写
- 四个基本概念
Java BIO
- Java BIO
- IO 流体系
- 两大抽象
- 字节流:InputStream/OutputStream
- 面向原始字节
- 字符流:Reader/Writer
- 面向字符,内部会涉及字符集解码/编码
- 涉及文本语义/字符集,就优先用 Reader/Writer 或者用 InputStream + 明确 Charset 解码;处理二进制就用 InputStream/OutputStream
- 字节流:InputStream/OutputStream
- 节点流 vs 处理流
- 节点流:直接连接数据源/目的地
- FileInputStream、FileOutputStream、Socket.getInputStream
- 处理流:包装节点流,增强能力(缓冲、转码、数据类型、对象序列化、压缩、校验)
- BufferedInputStream、InputStreamReader、DataInputStream、ObjectInputStream
- 装饰器模式:不改原类,通过包装叠加功能
- 节点流:直接连接数据源/目的地
- 两大抽象
- Buffered 缓冲为什么快
- 不加缓冲为什么慢
- 许多 read()/write() 最终都会调用 OS 层的 read(2)/write(2) 系统调用,系统开销不小
- 不缓冲:每读 1KB 就要系统调用一次,次数巨大
- 加缓冲:一次系统调用读 8KB/64KB 放到用户态缓冲区,后续小读都在内存中完成,降低系统调用次数
- 许多 read()/write() 最终都会调用 OS 层的 read(2)/write(2) 系统调用,系统开销不小
- BufferedInputStream/BufferedOutputStream
- BufferedInputStream:内部有 byte[] buffer(默认 8KB),先批量从底层读,再按需给你
- BufferedOutputStream:先写入 buffer,满了再 flush 到底层,减少了 write 的系统调用次数
- flush 是什么
- 把用户缓冲区里的数据推下去
- flush() != 一定落盘
- 不加缓冲为什么慢
- 字符集与桥接流
- InputStreamReader/OutputStreamWriter
- InputStreamReader:字节 -> 字符(解码),需要 Charset
- OutputStreamWriter:字符 -> 字节(编码),需要 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,会走平台默认编码,导致乱码
- InputStreamReader/OutputStreamWriter
- 常见功能处理流
- 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 默认吞异常
- DataInputStream/DataOutputStream(理解二进制协议)
- 资源管理与异常
- try-with-resources
- 自动 close,且 close 顺序与声明顺序相反
- close 过程中异常会作为 suppressed 异常附加到主屏幕
- close vs flush
- close vs flush
- close() 会隐式 flush(),不能依赖进程崩溃前的 close
- 网络场景:写完重要数据要主动 flush 或者使用协议层 ack。
- close vs flush
- try-with-resources
- BIO 与网络 IO:Socket 阻塞读写的关键点
- 阻塞读到集中返回情况
- InputStream.read(byte[])
- 返回 >0:读到的字节数
- 返回 -1:对端关闭输出
- 可能一直阻塞:对端不发数据也不关
- InputStream.read(byte[])
- 半包/粘包在 BIO 也存在
- BIO 只是阻塞,TCP 仍然是字节流,read() 不保证一条业务消息完整到达
- 阻塞读到集中返回情况
- IO 流体系
NIO 基础 Buffer/Channel
- NIO 基础
- NIO 的核心心智模型:Buffer+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 表示可读数据的末尾
- 写模式:准备往 buffer 里放数据(例如从 channel read 进来)
- 三个最重要的方法
- flip:写模式 -> 读模式
- 把 limit 设置为当前 position,再把 position 设置为 0
- 我写完了,现在从头读刚写的数据
- clear:读完后重置为写模式(不清数据,只重置指针)
- position = 0; limit = capacity; mark = -1
- 不关心旧数据了,直接当成空 buffer 继续写
- compact:读了一部分,保留剩余未读数据,并切换为写模式
- 把未读数据 [position,limit) 移动到开头
- position=remaining;limit=capacity
- 我还剩一点没处理,先挪到前面,下次继续在后面写新数据
- flip:写模式 -> 读模式
- 关键属性
- HeapBuffer vs DirectBuffer
- Heap ByteBuffer(堆内)
- ByteBuffer.allocate(n)
- 好处:分配快,受 JVM GC 管理,排查简单
- 坏处:做网络 IO 时需要把数据从堆复制到内核缓冲区
- Direct ByteBuffer(堆外)
- ByteBuffer.allocate(n)
- 好处:减少一次拷贝,适合大块网络 IO,频繁 IO。
- 坏处
- 分配/释放更贵
- 受 MaxDirectMemorySize 限制
- 回收依赖 Cleaner/GC 时机,不是=null 就立刻释放
- Heap ByteBuffer(堆内)
- 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 非阻塞基石
- FileChannel
- NIO 文件读写的标准流程
- FileChannel 读取
- buffer.clear() 进入写模式
- int n = channel.read(buffer) 把数据写进 buffer
- buffer.flip() 进入读模式
- 从 buffer 读取处理
- 循环直到 n == -1
- FileChannel 读取
- NIO 的核心心智模型:Buffer+Channel
NIO 进阶 Selector/多路复用
- 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:负责读写与协议解析、业务派发
- 单线程 Reactor
- Selector/SelectionKey/Channel:三者关系与生命周期
Netty
- Netty
- 解决什么问题
- 原生 NIO 的痛点
- 需要自己管理:Selector 轮询、Key 生命周期、半包/粘包、写不完与 OP_WRITE 开关
- Buffer 管理复杂:flip/compact、输入输出缓冲复用、DirectBuffer 回收时机
- 性能与稳定性:Selector 空轮询、内存膨胀、背压、线程隔离
- Netty 提供的工程化能力
- 线程模型(EventLoopGroup + Reactor)成熟
- Pipeline/Handler 体系:解码、业务、编码职责分离
- ByteBuf:读写指针友好、池化、零拷贝视图、引用计数
- 编解码器、背压、写队列、水位线、内存泄露检测等工具链
- 原生 NIO 的痛点
- 线程模型
- EventLoop
- EventLoop = 一个线程 + 一个 Selector + 一组 Channel
- EventLoop 负责
- 处理 IO 事件
- 执行任务队列
- 同一个 Channel 的所有 Handler 回调默认都在通一个 EventLoop 线程执行,一致性,减少锁
- EventLoopGroup
- bossGroup:负责 accept(ServerSocketChannel)
- workerGroup:负责已建立连接的读写(SocketChannel)
- 为什么少量线程能抗大量连接
- 事件驱动:线程主要阻塞在 epoll/kqueue 上,事件就绪才处理
- 连接多不会导致线程线性增长
- EventLoop
- Channel、Future、Promise:异步编程语义
- ChannelFuture
- Netty 大量 API 返回 ChannelFuture,比如 bind、connect、writeAndFlush,表示操作已发起,完成会通知
- future.addListener(…):完成回调(推荐)
- future.sync():阻塞等待
- Promise
- 可写的 future,用于自己控制完成/失败
- ChannelFuture
- Pipeline/Handler:职责分离
- Pipeline 是什么
- 每个 Channel 都有一条 ChannelPipeline
- Inbound(入站):数据从 socket 进来 -> 解码 -> 业务处理
- Outbound(出站):业务写出 -> 编码 -> flush 到 socket
- Handler 分类
- ChannelInboundHandler:处理入站事件(channelRead、channelActive…)
- ChannelOutboundHandler:处理出站事件(write、flush…)
- ChannelDuplexHandler:双向
- SimpleChannelInboundHandler
:自动释放入站消息(配合 ByteBuf 引用计数要理解)
- Pipeline 是什么
- ByteBuf
- 为什么 ByteBuf 比 ByteBuffer 友好
- 两个指针:readerIndex/writerIndex
- 不需要 flip:写完读直接通过读指针移动
- 支持切片/复制语义清晰
- 三种常见 ByteBuf
- Heap:堆内
- Direct:堆外
- CompositeByteBuf:多个 buffer 组合成一个逻辑 buffer(减少拷贝)
- 为什么 ByteBuf 比 ByteBuffer 友好
- 解决什么问题
select/poll/epoll
- 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 大集合拷贝+扫描
- epoll_wait 返回的是就绪事件列表,只处理这些 fd,更接近 O(就绪数)
- LT vs ET 触发模式
- LT (Level-Triggered 水平触发,默认)
- 只要还可读/可写,就会持续通知你
- 编程更简单:读不完下次还会通知你
- ET (Edge-Triggered,边缘触发)
- 只有从不可读 -> 可读的边缘变化才通知一次
- 必须把数据一次性读到不能再读,否则可能丢通知导致卡住
- 性能更高,但对实现要求更严格(非阻塞+循环读/写)
- LT (Level-Triggered 水平触发,默认)
- 简介