为什么 Socket 的 read/write 可能只读写一部分?如何处理半包读写?
简化版
Socket 的 read 和 write 不保证一次读写完整业务数据。read 返回当前内核缓冲区里可读的字节数,write 只表示部分数据被写入发送缓冲区。正确做法是循环读写、维护应用层缓冲区,并处理 EAGAIN、连接关闭和超时。
详细版
阻塞 IO 下,read 可能因为对端只发送了一部分、内核缓冲区暂时只有一部分而返回短读;非阻塞 IO 下,暂时没有数据会返回 EAGAIN。write 也可能只写入一部分,因为发送缓冲区空间不足。
因此发送大消息时要记录已发送偏移,等 socket 可写后继续发送;接收时要把多次读取的数据拼进 buffer,再按协议解析完整消息。网络编程里“短读短写”是常态,不能把一次系统调用当成一次业务消息。
完整版教学
一、read 读的是内核缓冲区
应用调用 read(fd, buf, 4096),意思是最多读 4096 字节,不是必须读满 4096 字节。内核缓冲区当前有多少可读数据,会影响返回值。
socket receive buffer: 1200 bytes
app read 4096
return 1200
如果对端关闭连接,read 返回 0。这个 0 是重要信号,表示 EOF,而不是“这次没数据”。
二、write 写的是发送缓冲区
write 成功返回 n,只代表 n 字节已经交给内核发送缓冲区,不代表对端已经收到。发送缓冲区空间不足时,write 可能只接受一部分数据。
want write: 10KB
send buffer free: 3KB
write returns: 3KB
remaining: 7KB
如果程序没有保存剩余 7KB,下游收到的就是不完整数据。这个 bug 在压力上来后才容易暴露。
三、阻塞和非阻塞下的差异
阻塞 socket 在没有数据时可能挂起等待,非阻塞 socket 会立即返回 EAGAIN 或 EWOULDBLOCK。但两者都可能短读短写。
| 模式 | 没数据可读 | 缓冲区写不下 | 是否可能短读写 |
|---|---|---|---|
| 阻塞 | 等待 | 等待或写部分 | 可能 |
| 非阻塞 | EAGAIN | EAGAIN 或写部分 | 可能 |
非阻塞编程通常配合 epoll/kqueue,把“现在不能继续”交给事件循环等待下一次可读可写。
四、接收侧如何处理
接收侧要维护一个输入 buffer。每次可读事件到来,就循环读到 EAGAIN,把数据追加进 buffer,然后按协议解析完整帧。
read -> append input buffer -> decode frames -> keep incomplete tail
如果协议是 4 字节长度头,buffer 不足 4 字节时等待;够 4 字节但 body 不够时继续等待;够一条完整消息时交给业务并从 buffer 移除。
五、发送侧如何处理
发送侧要维护输出队列或 output buffer。业务要发送 100KB,不应假设一次写完。写了多少就推进偏移,剩余部分等下次可写事件继续。
offset = 0
while offset < len(data):
n = write(fd, data[offset:])
offset += n
非阻塞场景下不能在 while 里死等,遇到 EAGAIN 要注册写事件,等内核通知可写后再继续。
六、为什么要注意背压
如果对端读得慢,你一直往 output buffer 塞数据,内存会不断增长。背压就是在下游处理不过来时,让上游慢下来或拒绝更多数据。
peer slow -> send buffer full -> app output buffer grows -> memory risk
生产服务会设置单连接输出缓冲区上限、全局连接内存上限和超时策略。超过阈值时关闭慢连接,比拖垮整个进程更可取。
七、错误和关闭语义
read=0 表示对端关闭写方向;ECONNRESET 表示连接被重置;EPIPE 或 SIGPIPE 可能出现在向已关闭连接写数据时。健壮程序必须区分这些情况。
| 返回/错误 | 含义 | 常见处理 |
|---|---|---|
| read 0 | 对端关闭 | 清理连接 |
| EAGAIN | 暂时不可读/写 | 等待事件 |
| ECONNRESET | 连接重置 | 关闭并记录 |
| EPIPE | 写已断连接 | 忽略 SIGPIPE 并清理 |
记忆钩子:Socket 系统调用处理的是内核缓冲区进度,不是业务消息进度。
排查线上半包问题时,可以记录每次读写返回的字节数、应用层 frame 长度、buffer 剩余长度和连接状态。只看业务日志里的“协议解析失败”,很难判断是短读、越界长度还是连接提前关闭。
read_n=512 frame_len=1024 buffered=512 state=reading_body
八、常见误区与追问
- 误区:write 成功代表对方已经收到。 write 只是写入本机发送缓冲区。
- 误区:read 返回少于期望长度就是网络异常。 短读是正常现象,要继续读取。
- 误区:阻塞 IO 不会短写。 缓冲区和信号中断等情况仍可能导致短写。
- 追问:read 返回 0 代表什么? 对端关闭了连接的写方向。
- 追问:非阻塞遇到 EAGAIN 怎么办? 暂停本次读写,等待下一次可读/可写事件。
- 追问:如何避免 output buffer 无限增长? 设置水位线、超时和慢连接关闭策略。
- 追问:SIGPIPE 怎么处理? 忽略 SIGPIPE 或用 send flags,按错误码清理连接。
九、加强记忆
半包读写要从“系统调用只推进一部分字节”来记。接收侧用 input buffer 和协议解码器拼完整消息,发送侧用 output buffer 和偏移量续写剩余数据。遇到 EAGAIN 就交回事件循环,遇到关闭和错误就清理连接。能讲清短读、短写和背压,才算真正理解 Socket 编程。