结论:句柄泄漏主因是stream_socket_accept返回的客户端socket未显式fclose;服务端socket长期存活不泄漏,但每个客户端连接必须手动关闭,否则fd持续累积。

直接结论:句柄泄漏几乎一定是 stream_socket_accept 返回的客户端连接没 fclose,而不是服务端 socket 本身的问题。
为什么 stream_socket_accept 的返回值必须显式 fclose
PHP 的 stream_socket_server 创建的服务端 socket 是长期存活的,它本身不会随连接数增长而泄漏;但每次 stream_socket_accept 返回的客户端 socket 资源(本质是 fd)必须手动关闭。PHP 不会自动在连接断开或脚本结束时帮你关——尤其在常驻进程(如 CLI 循环监听)中,fclose($client) 漏掉一次,就多一个打开的 fd。
- 常见漏关场景:客户端异常断开(
feof或fread返回空但没触发fclose)、处理逻辑抛出异常跳过清理、stream_select后忘记对可读 socket 做完整读取 + 关闭 - 注意:
fclose($client)必须在真正完成通信后调用,不能在fread一次就关——TCP 是流式协议,可能还有未读完的数据或 FIN 包 - 验证方法:启动服务后,用
lsof -p $(pgrep -f "php your_server.php") | grep IPv4 | wc -l查看 IPv4 类型 fd 数量,持续增长即泄漏
如何快速确认哪个连接没关
别靠日志猜,用系统工具直击 fd 分布:
- 查当前所有 socket fd 路径:
lsof -p $(pgrep -f "php your_server.php") | grep sock—— 如果大量显示can't identify protocol或重复 IP:port 对,基本就是未关闭的客户端连接 - 统计 fd 类型分布:
lsof -p $(pgrep -f "php your_server.php") | awk '{print $5}' | sort | uniq -c | sort -nr,重点关注IPv4和sock行数是否远超预期并发数 - 配合
strace -p $(pgrep -f "php your_server.php") -e trace=close,recvfrom,sendto观察是否有close()调用缺失(需 root 权限)
stream_select 循环里怎么安全释放客户端资源
典型错误是只判断 feof 就 fclose,但 TCP 连接可能处于半关闭状态(对方只发了 FIN),此时 feof 为 false,fread 却返回空字符串,继续循环会导致该 socket 被永久挂起。
- 正确做法:每次
fread($client, 1024)后检查$message === false || $message === '',再结合feof($client)和!is_resource($client)综合判断 - 务必在
fclose($client)后从$clients数组中移除该 resource,避免后续stream_select再次把它加入$read集合(否则stream_select可能报Warning: stream_select(): unable to select [Bad file descriptor]) - 示例判断逻辑:
if ($message === false || (strlen($message) === 0 && feof($client))) { fclose($client); $clients = array_values(array_filter($clients, function($c) use ($client) { return $c !== $client; })); }
容易被忽略的隐性泄漏点
除了主循环里的客户端 socket,还有几个地方常被遗漏:
-
stream_socket_server创建失败时,$errno/$errstr不影响 fd,但若你在失败后仍尝试stream_socket_accept($socket)($socket 为 null),PHP 会报错且不释放底层资源 - 使用
stream_set_timeout或stream_set_blocking设置后,未重置就进入下一轮循环,可能导致某些 socket 状态异常,fclose失效 - CLI 模式下 SIGTERM 信号未捕获,进程被 kill -15 时,正在处理的 client socket 来不及
fclose—— 建议注册pcntl_signal(SIGTERM, function() { exit(0); });并在退出前遍历关闭所有已知$clients
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











