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() 阻塞等待,但是工程上不这么用
    • 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 + 工程化)
    • 多路复用(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
      • 节点流 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:字节 -> 字符(解码),需要 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,会走平台默认编码,导致乱码
    • 常见功能处理流
      • 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[])
          • 返回 >0:读到的字节数
          • 返回 -1:对端关闭输出
          • 可能一直阻塞:对端不发数据也不关
      • 半包/粘包在 BIO 也存在
        • BIO 只是阻塞,TCP 仍然是字节流,read() 不保证一条业务消息完整到达

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 表示可读数据的末尾
      • 三个最重要的方法
        • flip:写模式 -> 读模式
          • 把 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

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:负责读写与协议解析、业务派发

Netty

  • Netty
    • 解决什么问题
      • 原生 NIO 的痛点
        • 需要自己管理:Selector 轮询、Key 生命周期、半包/粘包、写不完与 OP_WRITE 开关
        • Buffer 管理复杂:flip/compact、输入输出缓冲复用、DirectBuffer 回收时机
        • 性能与稳定性:Selector 空轮询、内存膨胀、背压、线程隔离
      • Netty 提供的工程化能力
        • 线程模型(EventLoopGroup + Reactor)成熟
        • Pipeline/Handler 体系:解码、业务、编码职责分离
        • ByteBuf:读写指针友好、池化、零拷贝视图、引用计数
        • 编解码器、背压、写队列、水位线、内存泄露检测等工具链
    • 线程模型
      • EventLoop
        • EventLoop = 一个线程 + 一个 Selector + 一组 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 进来 -> 解码 -> 业务处理
        • Outbound(出站):业务写出 -> 编码 -> flush 到 socket
      • Handler 分类
        • ChannelInboundHandler:处理入站事件(channelRead、channelActive…)
        • ChannelOutboundHandler:处理出站事件(write、flush…)
        • ChannelDuplexHandler:双向
        • SimpleChannelInboundHandler:自动释放入站消息(配合 ByteBuf 引用计数要理解)
    • ByteBuf
      • 为什么 ByteBuf 比 ByteBuffer 友好
        • 两个指针:readerIndex/writerIndex
        • 不需要 flip:写完读直接通过读指针移动
        • 支持切片/复制语义清晰
      • 三种常见 ByteBuf
        • Heap:堆内
        • Direct:堆外
        • CompositeByteBuf:多个 buffer 组合成一个逻辑 buffer(减少拷贝)

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,边缘触发)
          • 只有从不可读 -> 可读的边缘变化才通知一次
          • 必须把数据一次性读到不能再读,否则可能丢通知导致卡住
          • 性能更高,但对实现要求更严格(非阻塞+循环读/写)