Socket 工作原理
Socket 基础概念、TCP 三次握手、传统 BIO 模式下 Socket 的完整工作流程。
结合操作系统原理 + Java 代码示例 + 通俗比喻,一步步讲清 Socket 完整工作流程,区分服务端、客户端,同时结合之前的 BIO/NIO 帮你串联理解。
一、先搞懂:Socket 到底是什么
Socket 中文叫套接字,是操作系统内核提供的网络编程接口,是应用程序和 TCP/IP 协议栈之间的桥梁。
- 网络通信本质:两台主机通过 IP地址 + 端口号 定位进程,Socket 就是这个「通信端点」。
- 一组完整 TCP 连接 = 客户端Socket ↔ 服务端Socket 成对出现。
比喻:
IP 地址 = 小区地址,端口 = 住户门牌号,Socket = 入户电话线。
两台机器通信,就是两根电话线对接、传数据。
二、核心前置:TCP 三次握手(连接建立)
Socket 基于 TCP 时,底层会自动完成三次握手,这是通信的前提:
- 客户端 → 服务端:我要建立连接(SYN)
- 服务端 → 客户端:收到请求,我也准备好(SYN+ACK)
- 客户端 → 服务端:确认收到,连接正式建立(ACK)
握手完成,TCP 通道打通,双方可以收发数据。
连接断开则是四次挥手,Socket 关闭时内核自动处理。
三、传统 BIO 下 Socket 完整工作流程(最经典、最好理解)
分 服务端 和 客户端 两条链路,按执行顺序拆解,附简易 Java 伪代码。
(一)服务端流程(被动等待连接)
步骤1:创建 ServerSocket(监听端口)
- 应用程序调用系统接口,在内核中创建一个监听型 Socket;
- 绑定本机 IP + 端口(例如 0.0.0.0:9092);
- 开始监听,等待客户端接入。
Java 代码:
// 绑定 9092 端口,开始监听
ServerSocket serverSocket = new ServerSocket(9092);
步骤2:阻塞等待客户端连接 accept()
调用 serverSocket.accept(),线程进入阻塞状态:
- 此时线程卡住,什么都不做,一直等到有客户端发起连接;
- 客户端连上后,内核完成 TCP 三次握手,然后返回一个新的普通 Socket 对象。
重点:
ServerSocket 只负责「监听、接连接」,不负责收发数据;
每一个客户端连接,都会生成一个全新独立 Socket。
// 阻塞等待连接,有客户端接入才会往下执行
Socket clientSocket = serverSocket.accept();
步骤3:通过 Socket 读写数据
拿到 clientSocket 后,通过输入流、输出流和客户端通信:
getInputStream():读客户端发来的数据(阻塞)getOutputStream():向客户端写回数据
只要对方没发数据,read() 方法就会持续阻塞线程。
// 读数据(阻塞)
InputStream in = clientSocket.getInputStream();
byte[] buf = new byte[1024];
int len = in.read(buf);
// 写数据
OutputStream out = clientSocket.getOutputStream();
out.write("hello client".getBytes());
步骤4:关闭资源
通信结束,依次关闭流、Socket,释放操作系统端口和文件句柄。
in.close();
out.close();
clientSocket.close();
serverSocket.close();
BIO 服务端整体运行逻辑
创建ServerSocket → 绑定端口 → accept()阻塞等连接
↓
客户端接入 → 生成专属Socket → 新开线程处理读写(一连接一线程)
↓
读写阻塞 → 通信完成 → 关闭Socket
(二)客户端流程(主动发起连接)
客户端不需要 ServerSocket,直接创建普通 Socket 主动去连服务端。
步骤1:创建 Socket,发起连接
指定服务端IP + 端口,内核自动发起 TCP 三次握手,建立连接。
// 连接 127.0.0.1:9092 服务端
Socket socket = new Socket("127.0.0.1", 9092);
步骤2:读写数据
和服务端对称,用输入输出流收发数据:
// 发数据给服务端
OutputStream out = socket.getOutputStream();
out.write("hello server".getBytes());
// 读取服务端返回数据(阻塞)
InputStream in = socket.getInputStream();
byte[] buf = new byte[1024];
in.read(buf);
步骤3:关闭连接
in.close();
out.close();
socket.close();
四、关键特性:为什么 BIO 的 Socket 会阻塞?
这是理解瓶颈的核心:
accept()阻塞:没有客户端连接,线程一直等;read()阻塞:连接已建立,但对方没发数据,线程一直等。
结合之前饭店比喻:
accept()= 前台等客人上门;read()= 服务员等客人说话;
全程卡死,线程无法做其他事。
这就是 BIO 无法支撑高并发的根源。
五、NIO 中 Socket 的变化(过渡到 Channel)
前面说过,Java NIO 不再用传统 Socket/ServerSocket,而是改用:
ServerSocketChannel(替代 ServerSocket)SocketChannel(替代 Socket)
底层依然是操作系统 Socket,只是 Java 层做了改造:
- 可以设置 非阻塞模式:
accept()没有连接 → 直接返回,不卡线程;read()没有数据 → 直接返回,不卡线程。
- 配合
Selector多路复用:一个线程轮询上千个 Channel(底层Socket)。
工作简化流程:
ServerSocketChannel(非阻塞) → 注册到Selector
Selector轮询事件 → 有新连接 → 生成SocketChannel
SocketChannel注册到Selector → 轮询读写事件 → 非阻塞读写
本质没变:底层还是操作系统 Socket,只是调用方式、阻塞行为、线程模型改了。
六、Socket 必懂的补充知识点
1. 端口占用问题
- 一个端口同一时间只能被一个进程监听;
- Socket 正常关闭后,内核会进入
TIME_WAIT状态(默认几十秒),端口不会立刻释放,短时间内无法重启服务。
2. 文件句柄限制
操作系统中,每一个 Socket 连接都会占用一个文件句柄。
- 连接数越多,占用句柄越多;
- 系统有最大句柄上限,连接超限会直接报错:
Too many open files。
这也是高并发服务必须优化系统参数的原因。
3. 全双工通信
Socket 建立的 TCP 连接是全双工:
双方可以同时收发数据,互不干扰(输入流、输出流独立工作)。
4. 两种关闭方式
- 主动 close():正常断开连接,内核走四次挥手;
- 进程意外退出/断网:连接异常断开,内核回收资源。
七、结合 Kafka 串联总结
- Kafka 对外监听 9092 端口,底层本质也是操作系统 Socket;
- 但它不用 Java BIO 的 Socket,而是用 NIO 的
SocketChannel; - Acceptor 线程负责接收新连接(对应
ServerSocketChannel.accept()); - Processor 线程通过 Selector 管理大量
SocketChannel,非阻塞读写; - 最终所有网络通信,落脚点都是操作系统内核的 Socket 套接字。
八、一句话极简总结
- Socket 是操作系统提供的网络通信端点,基于 IP+端口定位进程;
- TCP 通信需要客户端Socket + 服务端Socket 成对,依靠三次握手建立连接;
- 传统 BIO 中 Socket 默认阻塞,一连接一线程,并发能力弱;
- NIO 的 Channel 是 Java 对底层 Socket 的新封装,支持非阻塞、多路复用,适配高并发;
- 所有 Java 网络组件(Kafka、Netty),底层都离不开操作系统 Socket。
关联文档
- TCP 粘包拆包 — TCP 粘包/拆包问题
- 20-areas/cloud-native/programming/java/Java IO 模型 — BIO/NIO 模型
- HTTP 协议 — 应用层协议