Skip to content

IO、NIO 与网络编程

这篇笔记聚焦 Java 里一条很容易碎片化的主线:

  1. IO 到底在说什么
  2. BIONIO 有什么差异
  3. 缓冲区、通道、选择器这些词分别是什么意思
  4. Java 程序如何和文件、磁盘、网络连接打交道
  5. 这些模型分别在解决什么问题

1. IO 到底是什么

IOInput / Output 的缩写,可以理解成 程序和外部世界之间交换数据的过程

这里的“外部世界”包括:

  1. 文件
  2. 磁盘
  3. 网络连接
  4. 其他进程
  5. 标准输入输出

所以 IO 的核心问题不是“会不会读文件”,而是 程序如何把数据读进来、写出去,以及在这个过程中如何平衡性能和编程模型

1.1 IO 具体在解决什么问题

如果把 IO 放到工程语境里,它主要在解决下面几类问题:

  1. 数据从哪里来,例如文件、网络包、磁盘块
  2. 数据要写到哪里去,例如文件、Socket、标准输出
  3. 线程在等待数据时,要不要一直卡住
  4. 连接数很多时,线程模型还能不能扛住
  5. 字节读进来之后,该怎么解释成字符、对象或协议消息

所以 IO 不是一个“文件 API”话题,而是一整条 程序如何和外部资源交换数据,以及交换过程中如何兼顾吞吐、延迟和编程复杂度

1.2 为什么业务代码绕不开 IO

一个后端程序哪怕完全不做前端页面,也几乎一定会碰到 IO:

  1. 读配置文件
  2. 写日志
  3. 连接数据库
  4. 收发 HTTP 请求
  5. 调用 RPC 服务
  6. 上传下载文件

从这个角度看,IO 不是 Java 的边缘知识,而是服务端程序最基础的运行主线之一。


2. 为什么 Java 里要区分 BIO、NIO 和 AIO

因为不同 IO 模型面对的问题不同。

2.1 BIO

BIO 一般指传统阻塞式 IO。

它可以看成 线程发起读写操作后,如果数据还没准备好,线程就会被阻塞等待

这种模型的优点是:

  1. 理解简单
  2. 编程模型直观

但问题是 连接数一多,线程数量和等待成本就会迅速上升

更具体一点,BIO 更适合:

  1. 连接数不高
  2. 单次请求逻辑比较直接
  3. 代码可读性优先
  4. 对超高并发连接承载没有特别强的要求

2.2 NIO

NIONew IO

它可以理解成 通过 Buffer、Channel、Selector 这些抽象,让程序能以更高效的方式处理大量连接和数据流

它的重点不只是“新的 API”,而是 更偏向非阻塞和多路复用的 IO 模型

它主要在解决的问题是 当连接非常多时,怎么避免“一个连接一个线程”带来的高成本

所以 NIO 的价值不只是“写法不同”,而是:

  1. 让一个线程能处理多个连接
  2. 减少大量线程阻塞等待的浪费
  3. 更适合高并发网络服务

2.3 AIO

AIO 一般指 Asynchronous IO,也就是异步 IO。

可以看成 发起 IO 后,当前线程不自己轮询结果,而是等系统在数据准备好后主动通知回调

它解决的问题是 进一步减少线程等待和主动管理事件的负担

不过在 Java 工程里,最常被系统化讨论和实际大规模使用的,通常还是 BIONIO

所以学习顺序通常是:

  1. 先理解阻塞式 IO 的直观模型
  2. 再理解 NIO 为什么适合高连接场景
  3. 最后再补 AIO 的位置和边界

3. 把几个最容易混的词讲清楚

很多人学 IO 困难,不是因为 API 本身特别难,而是因为几个词老是混在一起。

3.1 阻塞和非阻塞

阻塞 可以理解成 线程发起操作后,如果结果没准备好,就先停在原地等

例如读取 Socket 时,如果对方还没发数据过来,线程就卡住不往下走。

非阻塞 可以理解成 线程发起操作后,如果当前没数据,就先返回,不一直傻等

它解决的是 线程时间不要大量浪费在无意义等待上

3.2 同步和异步

同步 更强调:当前这条执行线要自己等结果、自己处理结果。

异步 更强调:把事情发出去,后面结果好了再通过通知、回调或事件机制处理。

这两个词和“阻塞 / 非阻塞”有关,但不是一回事。

更稳妥的理解是:

  1. 阻塞 / 非阻塞,更偏线程等待方式
  2. 同步 / 异步,更偏结果交付方式

3.3 多路复用

多路复用 可以理解成 一个线程同时关注多个连接或通道,哪个准备好了就处理哪个

它解决的是 连接很多时,不必每个连接都单独占一个阻塞线程

这正是 NIO 特别重要的背景。


4. Buffer、Channel、Selector 分别是什么

4.1 Buffer

Buffer 可以理解成 一块用于临时存放数据的缓冲区

在 NIO 里,读写通常不是直接面向流,而是把数据放进 Buffer,再从 Buffer 里读出。

它主要解决的是 让读写过程有一个可控的中间区,便于批量读写、状态切换和数据处理

更具体一点说,Buffer 让我们可以清楚区分:

  1. 数据从通道读进来
  2. 数据先放到缓冲区
  3. 程序再从缓冲区解析和消费

4.2 Channel

Channel 可以理解成 比传统流更接近底层传输通道的一种抽象

它解决的是 数据通过什么通道被读写

例如:

  1. 文件通道
  2. 网络 socket 通道

它主要解决的是 把“数据从哪来、往哪去”的传输路径抽象出来,并且更方便和 Buffer 配合

4.3 Selector

Selector 可以理解成 一个线程同时监听多个通道上事件的选择器

它解决的是 大量连接来了以后,不必每个连接都绑一个线程

这也是 NIO 常被拿来支撑高并发网络编程的关键原因之一。

4.4 这三个概念为什么要一起出现

可以把它们串成一句话:Channel 负责数据传输路径,Buffer 负责暂存数据,Selector 负责在很多通道里找到“现在该处理谁”。

如果没有这条主线,NIO 很容易被看成一堆零散类名。


5. 流和通道到底有什么区别

传统 IO 更常基于 Stream,也就是流。

可以这样理解:

  1. Stream:更偏顺序读写的数据流抽象
  2. Channel:更偏底层通道抽象,可配合 Buffer 做更灵活的数据处理

这两套模型不是完全对立,而是:适用于不同复杂度和性能诉求的场景。

5.1 Stream 具体解决什么问题

Stream 更适合解决:按顺序把数据读进来或写出去。

例如:

  1. 读取一个文本文件
  2. 把图片写到磁盘
  3. 顺序处理输入输出

它的优点是简单、直观。

5.2 Channel 具体解决什么问题

Channel 更适合解决:当你希望以更接近底层通道的方式处理数据,并且需要和 Buffer、Selector 配合时,如何建立统一抽象。

所以如果只是简单文件读写,Stream 往往已经够用; 但如果是高并发网络服务,Channel 往往更自然。

如果你想把 Java 里的 IO 流Stream API 这两条线彻底分开,可以接着看:


6. 文件 IO 和网络 IO 为什么要放在同一条知识线里

因为它们本质上都在回答同一个问题:程序如何和外部资源交换数据。

文件 IO 更关注:

  1. 文件读写
  2. 缓冲
  3. 编码
  4. 磁盘访问

网络 IO 更关注:

  1. 连接建立
  2. 数据收发
  3. 连接数量
  4. 并发处理模型

更具体一点:

  1. 文件 IO 更偏“数据存在那里,我怎么把它读出来”
  2. 网络 IO 更偏“数据什么时候来、连接何时就绪、线程如何高效处理”

所以学 IO 时,如果只会文件读写,不理解网络连接和阻塞模型,这条知识线还是不完整的。


7. Socket 编程到底在说什么

Socket 可以理解成 应用层进行网络通信时,一个对连接端点的编程抽象

如果用非常直白的话说:它是程序和远程程序建立连接、收发数据的一种常见方式。

在 Java 里,很多网络编程最终都会落到:

  1. Socket
  2. ServerSocket
  3. NIO 的 SocketChannel
  4. 以及更上层的框架封装

7.1 Socket 解决什么问题

Socket 解决的是:让两个程序能够基于网络连接建立通信通道,并在这个通道里持续收发字节流。

7.2 ServerSocket 解决什么问题

ServerSocket 可以理解成 服务端用来监听端口、接收客户端连接的入口

它解决的是 服务端如何等待新的客户端连进来

7.3 SocketChannel 解决什么问题

SocketChannel 是 NIO 体系里的网络通道抽象。

它解决的是 如何在 NIO 模型下,以通道方式处理网络连接,并配合 Selector 做事件驱动


8. 为什么高并发网络服务会偏爱 NIO

因为在高连接场景下,如果每个连接都配一个线程,成本通常会非常高。

NIO 更适合:

  1. 大量连接
  2. 相对较少线程
  3. 通过事件通知和多路复用处理读写

这里的 多路复用 可以理解成 一个线程同时关注多个连接,一旦哪个连接有事件,就去处理哪个

这和“一个连接一个阻塞线程”的模式差异很大。

8.1 BIO 真正卡在哪里

BIO 最大的问题不是“不能处理网络请求”,而是:连接数上来以后,大量线程都在等待 IO,就会带来线程切换、内存占用和调度成本。

8.2 NIO 真正提升在哪里

NIO 真正的提升也不是“语法更高级”,而是:

  1. 用更少线程承接更多连接
  2. 把等待数据的成本从“线程阻塞”转成“事件通知”
  3. 更适合网关、IM、RPC 框架、消息服务器这类高连接场景

8.3 这不等于 NIO 永远更好

如果系统特点是:

  1. 连接数不高
  2. 请求逻辑很简单
  3. 开发效率更重要

BIO 仍然是合理选择。


9. 编码为什么会和 IO 放在一起讨论

因为程序最终读写的不只是字节,还有“如何把字节解释成字符”。

这就是字符集和编码问题。

如果编码处理不一致,就会出现:

  1. 乱码
  2. 长度判断错误
  3. 序列化和反序列化问题

所以 IO 里不能只谈“读到了数据”,还要谈:这些字节到底该按什么规则解释。

9.1 为什么这是 IO 的一部分

因为真正的 IO 过程通常分成两层:

  1. 底层把字节读进来、写出去
  2. 上层决定这些字节代表什么内容

如果第一层没问题,第二层解释错了,业务结果一样会错。

所以很多“明明读到了但还是有问题”的情况,本质上不是 IO 没发生,而是:字节和字符之间的转换出了问题。


10. 一条更完整的主线:从 BIO 到 NIO,到底发生了什么变化

如果把这条知识线压缩成一条演进过程,可以这样理解:

  1. 最开始,程序主要面对的是“怎么把数据读进来、写出去”,所以传统 IO / Stream 模型已经够用
  2. 随着网络服务连接数越来越高,“一个连接一个线程”的阻塞模型越来越吃力
  3. 于是 Java 提供了 NIO,引入 BufferChannelSelector
  4. 这背后真正变化的,不只是 API,而是: 从顺序阻塞读写,转向面向事件和多路复用的 IO 模型

这样再回头看那些术语,就不容易碎:

  1. BIO 解决简单阻塞读写
  2. NIO 解决高连接场景下的线程成本问题
  3. Buffer 解决数据暂存和处理
  4. Channel 解决传输通道抽象
  5. Selector 解决多连接事件监听
  6. Socket 解决网络连接通信

11. 工程上最常见的几个误区

11.1 误区一:NIO 就一定比 BIO 好

不一定。

如果场景简单、连接数不高、逻辑也不复杂,BIO 的编程模型反而更直接。

11.2 误区二:会用 InputStream 就等于理解 IO

真正的问题还包括:

  1. 缓冲
  2. 编码
  3. 阻塞与非阻塞
  4. 文件和网络的差异

11.3 误区三:NIO 只是 API 写法变了

不只是。

它背后更重要的是:IO 模型和并发处理思路发生了变化。

11.4 误区四:单纯会背名词,就等于理解了 IO

如果只是记住:

  1. Buffer 是缓冲区
  2. Channel 是通道
  3. Selector 是选择器

但没有回答:

  1. 它们为什么会出现
  2. 各自在解决什么问题
  3. 为什么这些概念会一起出现

那这条知识线还是容易散。


12. 一句话总结

IO、NIO 与网络编程这条主线,本质上是在解决:

Java 程序如何和文件、磁盘、网络连接交换数据,以及在不同性能和并发要求下该选择怎样的 IO 模型。

基于 VitePress 构建的个人技术笔记。