Java 网络编程中,Socket BIO 是所有网络通信的基石,也是面试高频考点。大部分开发者只会写调用代码,但不懂内核缓冲区、读写线程安全、FIN/RST 断连、延迟报错、进程退出机制,导致线上频发数据错乱、数据丢失、Connection reset、Broken pipe 等问题。
本文汇总 bind / listen / accept / connect / read / write / close 七大核心方法机制、返回值、异常场景、线程安全问题、内核行为。
一、核心前置底层认知
1.1 为什么 write 延迟报错、read 实时感知?
-
write 感知断连:依赖真实网络 ACK 校验
-
write 仅把数据拷贝到内核发送缓冲区,属于内存操作,不立即发包
-
缓冲区未满、未 flush 时,多次 write 均假性成功,不感知断连
-
只有缓冲区满 / 主动 flush / 再次IO,内核真实发包等待 ACK 才会报错
-
-
read 感知断连:依赖内核本地状态
-
read 不发包,仅读取本地内核接收缓冲区
-
对端断开后,内核本地直接标记连接死亡
-
只要调用 read,立刻感知状态,无延迟
-
1.2 FIN 与 RST 核心区别(解决 Connection reset 所有疑惑)
-
FIN(优雅关闭):代码主动 close()、进程正常结束;内核发完缓冲区剩余数据,走四次挥手,温柔断连,不丢数据。
-
RST(暴力关闭) :进程强退、kill-9、主线程提前退出、断网;内核直接丢弃缓冲区数据、不走四次挥手、强制复位连接,对端报 Connection reset。
1.3 线程安全终极结论
-
Java 上层 IO 流:全部非线程安全
-
操作系统内核系统调用:全部线程安全(串行执行)
-
Socket 读写、关闭共用同一个文件句柄,必须全局同一把锁,读写分离锁依然不安全
二、Socket 七大核心方法完整对照表(终极版·可直接面试背诵)
| 函数 | 核心处理机制 | 正常返回 | 异常 / 特殊返回 |
|---|---|---|---|
| read() | 1.从内核缓冲区读取数据; 2.非线程安全; 3.多线程并发无锁共用流,读写指针错乱、数据被拆分割裂; 4.调用 close 会触发 onclose,使得未处理的数据直接结束。 5.read多线程同时 read (),一定会出现:线程 A 读到一半,线程 B 读到另一半。数据彻底混乱、错乱、无法使用 | 成功读到字节:返回 > 0 的实际字节数 | 1. 对端正常 close (四次挥手发 FIN):返回 -1 2. 对端 kill-9 / 断网 / 进程强退 (RST):抛SocketException:Connection reset 3. 本地已经执行 close:抛IOException:Stream closed |
| write(buf) | 1.数据先写入 OS 内核缓冲区,不立即发网线; 2.非线程安全; 3.多线程无锁并发写入,数据穿插半包错乱; 4.缓冲区数据依赖 flush 落地发送5.close缓冲区未刷出残留数据直接丢失 6.只是把数据写入内核缓冲区,不保证一定发送成功,也不保证立刻发送。close会丢失在缓冲区尚未外发的数据 7.多个write中间可能会乱序,如写buf1,写buf2, 可能是buf1和buf2 各一部分 | void 无返回 | 1. 对端正常 close:首次 write 不报错,下次 I/O (write/read) 抛 Broken pipe (Windows:Software caused connection abort) 2. 对端 kill-9 / 强制断开:下次 I/O 抛Connection reset 3. 本地已 close:抛IOException:Stream closed |
| close() | 1.关闭上层流 + 底层 TCP 连接;2.JDK 标准 IO 流幂等支持多次调用,内置 closed 标记位 | void 无返回 | 1.多次重复调用无异常、无副作用; 2.仅包装流特殊实现极少数场景抛异常(Socket 原生流不会) |
| bind() | 1.服务端专属操作,将 Socket 与本机 IP+端口绑定; 2.内核注册端口占用信息,标识端口归属当前进程; 3.必须在 listen/accept 之前执行; 4.非线程安全,不支持重复绑定; 5.客户端一般无需手动 bind | void 无返回 | 1.端口已被占用:抛 BindException; 2.IP 非法/不可绑定:抛 SocketException;3.Socket 已关闭:抛 IOException; 4.重复绑定端口报错 |
| listen() | 1.服务端专用,将主动套接字转为被动监听状态; 2.内核创建半连接队列、全连接队列,管理 TCP 握手连接; 3.仅开启监听能力,不建立连接; 4.必须先 bind 再 listen; 5.重复调用无意义、非线程安全 | void 无返回 | 1.未绑定端口直接调用:抛 IOException; 2.端口异常/被占用:BindException; 3.Socket 已关闭:抛 IOException |
| accept() | 1.阻塞方法,从全连接队列取出已完成三次握手的客户端连接; 2.每次返回全新通信 Socket,原 ServerSocket 继续监听; 3.多线程并发 accept 触发惊群效应,一个连接仅被一个线程获取; 4.只获取连接,不处理业务数据 | 返回新 Socket 对象(客户端通信通道) | 1.监听 Socket 被 close:抛 IOException: Socket closed; 2.阻塞等待被线程中断:抛 SocketException; 3.底层文件句柄(fd)失效/被系统回收:抛 IO 异常 |
| connect() | 1.客户端专属,主动向服务端发起 TCP 三次握手; 2.阻塞执行,握手成功才返回; 3.非线程安全,多线程并发 connect 会造成套接字状态混乱; 4.连接成功后不可重复调用; 5.连接失败后当前 Socket 彻底失效,不可复用 | void 无返回 | 1.服务端未监听端口:抛 Connection refused; 2.握手超时:SocketTimeoutException; 3.目标主机不可达:ConnectException; 4.已连接/已关闭状态调用:抛 IOException |
三、关键异常通俗详解
3.1 accept 三大异常通俗解释
-
1. 监听Socket被close → Socket closed:服务端 ServerSocket 已经关闭,还在继续 accept,直接报错。
-
2. 阻塞被中断 → SocketException :线程正在 accept 阻塞等连接,外部调用
interrupt()强行唤醒线程,阻塞被打断报错。 -
3. 文件描述符失效 → IO异常:底层 Socket 句柄被系统回收、进程销毁、资源释放,通道已经不存在,继续操作报错。
3.2 Connection reset 根本原因
不是 close() 导致!是进程暴力退出导致 RST 断连。
主线程提前结束、子线程还在IO、kill-9强杀进程、网络突然断开 → 内核不走优雅FIN挥手,直接RST强制断连 → 对端read抛 Connection reset。
3.3 Broken pipe 根本原因
对端已经正常 close(FIN 关闭),本地第一次 write 不报错 (只写缓冲区),第二次IO操作触发真实发包,检测到对方已断开,抛出 Broken pipe。
四、线上开发最终规范
-
所有 Socket read / write / close 必须共用同一把全局锁,禁止读写分离锁。
-
write 写完业务数据必须 主动 flush,不依赖内核自动发送。
-
禁止多线程无锁并发写同一个流,必然乱序、半包、数据错乱。
-
网络断连判断优先依赖 read 感知,绝不信任 write 返回结果。
-
网络IO线程不可随意中断,服务端退出必须优雅关闭,避免RST暴力断连丢数据。
-
全局统一编码格式,防止两端解析乱码、数据解析失败。
五、NIO 核心函数(和 BIO 一一对应)
|----------------------------------|---------------------------------------------------------------------------------------------------------------|-------------------------------------|---------------------------------------------------------------------------------------------------------------|
| 函数 | 核心处理机制 | 正常返回 | 异常 / 特殊返回 |
| Channel.read() | 1. 从内核缓冲区读取数据到 ByteBuffer; 2. 非线程安全; 3. 多线程并发读会导致指针错乱、数据分裂; 4.Channel 关闭后读写立即终止; 5. 支持非阻塞模式,无数据直接返回 0 | 读到字节数 > 0;无数据返回 0;对端关闭返回 -1 | 1. 对端断开 RST:抛 IOException: Connection reset; 2.Channel 已关闭:抛 ClosedChannelException;3. 非阻塞模式被中断:抛 IOException |
| Channel.write() | 1. 数据写入 OS 内核发送缓冲区,不立即发网卡; 2. 非线程安全; 3. 多线程无锁写会出现数据穿插、半包; 4. 不保证发送成功; 5. 关闭时缓冲区未发数据会丢失; 6. 非阻塞模式可能写不完,返回实际写入量 | 返回实际写入字节数(非阻塞可能 < 数据长度) | 1. 对端已关闭:下次 IO 抛 Broken pipe; 2.RST 断开:抛 Connection reset; 3.Channel 关闭:抛 ClosedChannelException |
| Channel.close() | 1. 关闭通道 + 释放文件描述符; 2. 支持多次调用,幂等; 3. 关闭后无法再读写; 4. 触发底层 TCP FIN 关闭;进程强退仍会发 RST | void 无返回 | 多次关闭无异常;极少数场景抛 IO 异常 |
| SocketChannel.bind() | 1. 绑定本地 IP + 端口; 2. 服务端必须调用; 3. 非线程安全; 4. 重复绑定报错 | void 无返回 | 端口被占用:BindException;已关闭:ClosedChannelException |
| ServerSocketChannel.bind() | 1. 服务端绑定 IP + 端口; 2. 设置监听队列长度; 3. 开启监听模式 | void 无返回 | 端口占用:BindException;参数非法:IllegalArgumentException |
| ServerSocketChannel.accept() | 1. 非阻塞模式:无连接立即返回 null; 2. 阻塞模式:等待连接; 3. 返回全新 SocketChannel; 4. 多线程并发会出现惊群; 5. 非线程安全 | 有连接返回 SocketChannel;无连接(非阻塞)返回 null | 1. 通道已关闭:ClosedChannelException;2. 阻塞被中断:IOException |
| SocketChannel.connect() | 1. 发起 TCP 三次握手; 2. 非阻塞模式会立即返回,需用 finishConnect; 3. 非线程安全; 4. 连接失败通道失效 | 阻塞:成功返回 true;非阻塞:返回 false(需后续确认) | 1. 连接拒绝:Connection refused; 2. 超时:SocketTimeoutException; 3. 通道关闭:ClosedChannelException |
| Selector.select() | 1. 阻塞等待通道就绪(读 / 写 / 连接); 2. 多路复用核心;3. 单线程管理成千上万个连接; 4. 支持设置超时时间 | 返回就绪通道数量 | 1. 线程被中断:抛 IOException; 2.Selector 已关闭:抛 ClosedSelectorException |
| Selector.wakeup() | 1. 立刻唤醒阻塞在 select () 的线程;2. 无副作用,可多次调用; 3. 多路复用必备打断机制 | void 无返回 | Selector 关闭:抛 ClosedSelectorException |
附录:对端正常关闭后,第一次 write 不抛异常,第二次才抛的原理
一句话核心原因
第一次 write:数据发出去了,但内核只是 "收到了 RST 标记",不会立刻抛异常。 第二次 write:内核知道连接已死,直接抛 Broken pipe!
完整流程(一步一步看懂)
前提
-
对端正常调用了
socket.close()(发送 FIN 包) -
对你来说,TCP 是全双工的(收、发是两条独立通道)
第一步:对端关闭连接
对端调用 close() → 向你发送 FIN 包 → 意思是:"我不发数据了,但我还能收你发的数据"
此时:
-
你的读通道 关闭 → 下次
read()返回 -1 -
你的写通道还开着 → 你不知道对方已关闭
第二步:你第一次 write(不抛异常)
你调用 write() → 数据进入内核缓冲区 → 内核把数据发给对端 → 对端回复 RST 包("我已经关了,别发了")
内核收到 RST,但不会立刻抛异常! 它只会悄悄标记连接:已失效。
所以: 第一次 write 成功返回,不抛异常!
第三步:你第二次 write(抛异常)
你再次调用 write() → 内核检查:连接已标记失效! → 直接抛出:
Broken pipe(Linux) Software caused connection abort(Windows)
总结
第一次 write:负责把数据发出去,顺便发现连接挂了。 第二次 write:知道挂了,直接拒绝并抛异常。
用生活比喻秒懂
对方挂了电话(FIN),你还在说话:
-
第一句:话说出去了,听到忙音(RST)
-
第二句:你知道已经挂断,说不出去了,直接报错
对技术感兴趣的朋友,推荐阅读我今年5月11日上架的《金融支付架构实战指南》新书,拼多多/京东/淘宝/当当有售。