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。

四、线上开发最终规范

  1. 所有 Socket read / write / close 必须共用同一把全局锁,禁止读写分离锁。

  2. write 写完业务数据必须 主动 flush,不依赖内核自动发送。

  3. 禁止多线程无锁并发写同一个流,必然乱序、半包、数据错乱。

  4. 网络断连判断优先依赖 read 感知,绝不信任 write 返回结果。

  5. 网络IO线程不可随意中断,服务端退出必须优雅关闭,避免RST暴力断连丢数据。

  6. 全局统一编码格式,防止两端解析乱码、数据解析失败。


五、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日上架的《金融支付架构实战指南》新书,拼多多/京东/淘宝/当当有售。

更多推荐