Socket
→ 返回 计算机网络
Socket(套接字)是操作系统提供的网络通信抽象接口,应用程序通过 Socket API 发送和接收数据,底层可基于 TCP 或 UDP。
Socket 与协议的关系
应用程序
│ Socket API(send / recv)
▼
操作系统内核
├── TCP Socket → TCP → IP → 网卡
└── UDP Socket → UDP → IP → 网卡
Socket 是应用层与传输层之间的编程接口,屏蔽底层协议细节。
TCP Socket 通信流程
服务端 客户端
socket() socket()
bind(ip, port)
listen()
accept() ◄─────────────────── connect(server_ip, port)
│ 三次握手完成 │
recv() ◄──────── send(data) ─────────┤
send(data) ──────────────────► recv()
close() close()
Java 示例
TCP 服务端
ServerSocket serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept(); // 阻塞等待客户端
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
String line;
while ((line = in.readLine()) != null) {
out.println("Echo: " + line);
}
socket.close();TCP 客户端
Socket socket = new Socket("127.0.0.1", 8080);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
out.println("Hello Server");
String response = in.readLine();
socket.close();Python 示例
TCP 服务端
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 8080))
server.listen(5)
conn, addr = server.accept()
data = conn.recv(1024)
conn.send(b'Echo: ' + data)
conn.close()UDP
# 发送方
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(b'Hello', ('127.0.0.1', 9090))
# 接收方
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('0.0.0.0', 9090))
data, addr = sock.recvfrom(1024)常用 Socket 选项
| 选项 | 说明 |
|---|---|
SO_REUSEADDR | 允许重用 TIME_WAIT 端口,服务重启时常用 |
SO_KEEPALIVE | 开启 TCP 保活探测,检测连接是否存活 |
TCP_NODELAY | 禁用 Nagle 算法,减少小数据包延迟 |
SO_TIMEOUT | 设置 recv 超时时间 |
SO_SNDBUF / SO_RCVBUF | 调整发送/接收缓冲区大小 |
BIO / NIO / AIO
| 模型 | 说明 | Java 对应 |
|---|---|---|
| BIO(阻塞 IO) | 每个连接一个线程,连接多时线程开销大 | Socket / ServerSocket |
| NIO(非阻塞 IO) | 单线程通过 Selector 管理多路连接 | java.nio、Netty |
| AIO(异步 IO) | 操作系统完成 IO 后回调通知 | AsynchronousSocketChannel |
高并发场景推荐 NIO 框架(Netty),避免 BIO 的线程资源浪费。
粘包与拆包
TCP 是字节流协议,无消息边界,可能出现:
- 粘包:多个小包合并为一次 recv
- 拆包:一个大包分多次 recv 到达
常用解决方案:
| 方案 | 说明 | 示例 |
|---|---|---|
| 固定长度 | 每条消息固定 N 字节 | 简单协议 |
| 分隔符 | 用特定字符(\n、\0)标记消息结尾 | Redis 协议、HTTP |
| 长度前缀 | 头部 4 字节写入消息体长度,先读头再读体 | Netty LengthFieldBasedFrameDecoder |
| 自定义协议帧 | 魔数 + 版本 + 消息类型 + 长度 + 体 | Dubbo、自研 RPC |
┌──────────┬──────────┬──────────┬──────────────┐
│ Magic │ Version │ Type │ Length Body│
│ 2 bytes │ 1 byte │ 1 byte │ 4 bytes ... │
└──────────┴──────────┴──────────┴──────────────┘
IO 多路复用
单线程同时监听多个 Socket,避免 BIO 每连接一线程的开销:
| 机制 | 系统 | 说明 | 上限 |
|---|---|---|---|
select | 跨平台 | 遍历 fd_set,O(n) | 1024 fd |
poll | Linux/Mac | 链表替代数组,O(n) | 无硬限制 |
epoll | Linux | 事件驱动,O(1),边缘/水平触发 | 百万级 fd |
kqueue | macOS/BSD | 类似 epoll | 百万级 fd |
IOCP | Windows | 真异步完成端口 | — |
epoll 两种触发模式:
- LT(水平触发,默认):只要缓冲区有数据就持续通知,未读完下次仍通知,易用
- ET(边缘触发):只在状态变化时通知一次,必须一次性读完,性能更高但编程复杂
TCP 连接状态
LISTEN → SYN_RCVD → ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED
↑
CLOSED → SYN_SENT → ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED
TIME_WAIT:主动关闭方在 2MSL(约 60s)内保持,防止最后 ACK 丢失重传。大量 TIME_WAIT 说明服务端主动关闭连接,可开启 SO_REUSEADDR 和 net.ipv4.tcp_tw_reuse。
CLOSE_WAIT:被动关闭方收到 FIN 后未调用 close(),通常是代码 bug(连接泄漏)。
TCP 半关闭
close() 关闭读写两个方向;shutdown() 可单独关闭一个方向:
socket.shutdownOutput(); // 发送 FIN,告知对端我不再发数据,但仍可接收用于:客户端写完数据后半关闭,等待服务端处理完毕再返回结果,再全关闭。
Unix Domain Socket
同一台机器的进程间通信,使用文件路径而非 IP:Port,性能优于 TCP(无需 TCP/IP 协议栈):
# 服务端
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind('/tmp/myapp.sock')
server.listen(5)
# 客户端
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
client.connect('/tmp/myapp.sock')常见场景:Nginx 与 PHP-FPM、Docker daemon、MySQL 本地连接。
心跳机制
长连接空闲时,中间路由/防火墙可能断开连接而两端不知晓:
| 方案 | 说明 |
|---|---|
| TCP KeepAlive | 操作系统级,默认 2 小时探测,可调整(SO_KEEPALIVE + TCP_KEEPIDLE) |
| 应用层心跳 | 每隔 N 秒发送 Ping 包,对端回 Pong;超时未回视为断线重连 |
应用层心跳更可靠(能检测到应用假死),推荐在 Netty 等框架中实现。