recv()返回0表示对端关闭写端而非错误,应停止recv()但继续send()直至数据发完再close()或shutdown(SHUT_WR);shutdown可精确控制半关闭方向,select/epoll仍会报告可读因FIN在缓冲区。

recv() 返回 0 表示对端已关闭写端,不是错误
很多初学者看到 recv() 返回 0 就 panic,立刻 close() 自己的 socket,结果丢掉本可继续发送的数据。这恰恰违背了半关闭的设计意图:TCP 允许一端调用 shutdown(fd, SHUT_WR) 关闭写端,但仍能接收对方后续发来的数据。
正确做法是把返回 0 当作「对方不会再发新数据」的信号,进入只写模式:
- 停止调用
recv(),但保持 socket 描述符有效 - 继续用
send()发送剩余响应(比如 HTTP 的 body 或 FIN 包) - 等所有待发数据写完后,再调用
close()或shutdown(fd, SHUT_WR)
shutdown() 比 close() 更精确控制半关闭方向
close() 是终局操作:它会同时关闭读写端,并在引用计数归零时真正释放 socket。而 shutdown() 只作用于当前 socket,且能指定方向:
-
shutdown(fd, SHUT_RD):丢弃后续到达的数据,recv()立即返回 0(极少用) -
shutdown(fd, SHUT_WR):发送 FIN,通知对方“我不会再发”,但还能recv() -
shutdown(fd, SHUT_RDWR)等价于先 SHUT_WR 再 SHUT_RD,但不等于close()(文件描述符仍存在)
典型场景:HTTP/1.0 服务端处理完请求后,调用 shutdown(fd, SHUT_WR) 发送响应头+body,然后等待客户端 ACK;期间若客户端还发了 RST,send() 可能返回 -1 + EPIPE,需检查 errno 而非直接 crash。
select()/epoll_wait() 不会因半关闭自动触发可读事件
半关闭后,对端发 FIN,本地 TCP 栈会把 FIN 放入接收缓冲区——但这个 FIN 不是“数据”,select() 或 epoll_wait() 仍会报告 socket 可读,因为缓冲区有内容(哪怕只有 FIN 标志)。此时 recv() 才返回 0。
常见误区是以为“可读事件 = 有应用层数据”,结果在事件循环里盲目 recv(),直到返回 0 才反应过来对方关了写端。更健壮的做法:
- 每次
recv()后检查返回值:== 0→ 半关闭完成,转入只写逻辑 且errno == EAGAIN || errno == EWOULDBLOCK→ 正常,继续等待事件且其他 errno(如ECONNRESET)→ 连接异常,应清理资源
SO_LINGER 配置会影响半关闭的语义和延迟
默认情况下,调用 close() 会立即返回,内核在后台发送 FIN 并等待 ACK。但如果设置了 SO_LINGER 且 l_onoff=1, l_linger=0,close() 会直接发送 RST,强制终止连接——这会破坏半关闭流程,对方收不到 FIN,recv() 可能返回 -1 + ECONNRESET 而非 0。
想确保半关闭行为可预测,建议:
- 避免全局设置
SO_LINGER,尤其不要设为 0 - 若需控制 TIME_WAIT 时长,改用
net.ipv4.tcp_fin_timeout系统参数 - 主动半关闭务必用
shutdown(fd, SHUT_WR),而非依赖close()的副作用
半关闭真正的复杂点不在 API 调用顺序,而在于状态机要覆盖「对方先关」「自己先关」「双方都关」「中间断连」四种组合,且每种都要区分是干净关闭还是异常中断。业务逻辑越重,越容易在 send/recv 交错时漏掉 0 值判断。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











