fsockopen读不到完整响应,因fgets()按换行截断而多数二进制协议无换行,fread()又可能只读半包;必须用stream_get_meta_data($fp)['unread_bytes']检测残留或手动累积解析,否则fclose()会丢弃未读数据。

fsockopen 是入门 TCP 客户端最快路径,但别指望它自动处理粘包、超时或长连接复用;真要写服务端或 UDP,必须切到 socket 扩展。
为什么 fsockopen 读不到完整响应?
因为 fgets() 按换行符截断,而多数自定义协议(如 Modbus TCP、私有二进制协议)根本没换行;fread($fp, 1024) 又可能只读到半包——TCP 不保证一次 send() 对应一次 recv()。
- 必须用
stream_get_meta_data($fp)['unread_bytes']查缓冲区是否还有残留字节,否则下次fread()会直接返回空 - 更稳妥的做法是手动累积:循环
fread($fp, 1)或预估包头长度(如前 4 字节为 body 长度),再按需补足 - 千万别在未确认数据收完前就
fclose($fp),否则后半截数据永远丢掉
socket_create + socket_connect 怎么发 TCP 请求?
这是绕过 fsockopen 限制的底层方案,适合需要控制连接状态、复用 socket 或对接非 HTTP 协议的场景。
- 创建:$sock =
socket_create(AF_INET, SOCK_STREAM, SOL_TCP),第三个参数不能写0或IPPROTO_TCP,必须是SOL_TCP - 连接:
socket_connect($sock, '127.0.0.1', 8080),失败时socket_last_error()返回 errno,不是布尔值 - 发送后必须调
socket_set_option($sock, SOL_SOCKET, SO_SNDTIMEO, ['sec' => 5]),否则大包卡住不报错 - 接收用
socket_read($sock, 65535),它比fread更贴近原生语义,但依然不解决粘包
PHP 写 TCP 服务端为什么只能处理一个客户端?
因为 PHP 默认单进程阻塞模型:socket_accept() 后若直接 while (true) { socket_read() },整个脚本就卡死在这一个连接上,新连接连不进来。
- 最简破局法:用
socket_select()监听多个 socket(包括主 listen socket 和已 accept 的 client socket),避免阻塞在某一个上 - 每个
socket_accept()返回的新 socket 必须单独存数组,每次socket_select()前都要重置读写数组 - 别漏设
socket_set_option($client, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 3]),否则某个客户端断连不发 FIN,你的服务就永远等下去 - 没有 fork 或 pcntl 扩展时,别幻想“并发”;真要多连接,要么轮询 + select,要么交由 Swoole 等扩展接管
UDP 通信为什么发出去就没了?
fsockopen 根本不支持 UDP;必须用 socket_create(AF_INET, SOCK_DGRAM, SOL_UDP),且默认行为极度“佛系”:发完不管收没收到,收时若没数据就无限挂起。
- 发送前不用
connect(),直接socket_sendto($sock, $data, strlen($data), 0, '127.0.0.1', 9999) - 接收必须传地址引用:
socket_recvfrom($sock, $buf, 65535, 0, $ip, $port),否则$ip和$port始终为空 - 务必设接收超时:
socket_set_option($sock, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 2, 'usec' => 0]),否则丢包或对方宕机时脚本永久阻塞 - UDP 没连接状态,
socket_close()前无需shutdown(),关了就完事
真正难的从来不是“连上”,而是连上之后怎么判断数据边界、怎么容错断连、怎么不让一个坏连接拖垮整个服务——这些细节不靠抓包(tcpdump/wireshark)和 errno 日志,光看文档根本绕不出来。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











