PHP 7.2 使用 stream_socket_server 时句柄泄漏主因是 socket 资源未显式 fclose,尤其在异常、断连、超时等分支遗漏关闭;需通过 lsof 或 /proc/pid/fd 统计 fd 数对比活跃连接数确认,重点检查监听 socket、客户端连接及 stream_select 中残留引用,并用日志跟踪、try-finally 和 register_shutdown_function 防御性清理。

PHP 7.2 使用 stream_socket_server 构建 TCP 服务时,句柄泄漏(file descriptor leak)往往表现为进程打开的 socket 数持续上涨、lsof -p $PID | wc -l 值不断增大,最终触发系统限制(如 Too many open files 错误)。问题核心不是函数本身有 Bug,而是资源生命周期管理缺失——PHP 不会自动关闭未显式释放的 socket 流,尤其在异常分支、连接中断、超时未处理等场景下极易遗漏。
确认是否真存在句柄泄漏
不要仅凭内存增长判断,句柄泄漏需直接观测文件描述符数量:
- 运行
lsof -p <php_pid> | grep "IPv4\|IPv6\|sock" | wc -l,对比连接数与实际活跃客户端数;若前者远大于后者(比如 500 个句柄但只有 10 个活跃连接),基本可判定泄漏 - 检查系统限制:
ulimit -n和/proc/<pid>/limits中Max open files值,确认是否接近上限 - 用
cat /proc/<pid>/fd/ | wc -l快速获取当前 fd 总数,比 lsof 更轻量
重点排查未关闭的 socket 类型
泄漏通常发生在三类 socket 上,需逐类验证:
-
监听 socket($socket)被重复赋值或未 close:确保
stream_socket_server返回的监听句柄只创建一次,且服务退出前调用fclose($socket);避免在重启逻辑中反复 new 而不 close -
已接受的客户端连接($conn)未正确清理:每个
stream_socket_accept返回的 $conn 必须在连接结束时显式fclose($conn)+unset($conn);特别注意fread返回空字符串(断连)、false(错误)或超时后,仍要执行关闭 -
stream_select 中残留的 $read 数组引用:示例代码中常把 $master 全量复制给 $read,但若某连接已关闭却未从 $master 中移除,下次循环时 $read 仍含无效 resource,
stream_select可能失败或跳过清理逻辑
加日志和断言快速定位泄漏点
在关键路径插入轻量级跟踪,不依赖外部工具:
立即学习“PHP免费学习笔记(深入)”;
- 每次
stream_socket_accept成功后,记录句柄 ID:echo "[ACCEPT] ".(int)$conn.PHP_EOL; - 每次
fclose($conn)前打印:echo "[CLOSE] ".(int)$conn.PHP_EOL; - 在循环顶部统计当前 $master 中有效句柄数:
array_filter($master, 'is_resource'),若该数只增不减,说明有连接没进 fclose 分支 - 对每个 $conn 执行
stream_get_meta_data($conn),检查timed_out、eof、blocked状态,辅助判断是否该关未关
防御性编码规范(必须落地)
避免靠“记得关”来防泄漏,用结构化写法固化清理逻辑:
- 所有
stream_socket_accept结果必须立即加入 $master,并在同一个作用域内配对try...finally或显式检查后关闭 - 每次
stream_select返回后,遍历 $read 时,先用is_resource($read[$i])判定有效性,再操作;否则可能对已 fclose 的 resource 再次读写 - 连接处理逻辑中,任何
break、continue、return或异常抛出前,确保已调用fclose并unset对应句柄 - 使用
register_shutdown_function注册兜底清理函数,遍历 $master 中所有 is_resource 的句柄并 fclose,防止脚本意外终止
句柄泄漏本质是资源所有权不清晰。只要每个 socket 从 accept 到 close 的路径唯一、可追踪、无分支遗漏,问题就能收敛。不需要复杂工具,靠日志+计数+强制清理三步就能准确定位并修复。



















