PHP写WebSocket客户端?客户很多人第一反应是“PHP不是干这个的”。但现实是客户,当你的客户服务端是Go或Node,而业务后台偏偏是客户PHP时,你总得有个办法去推送消息或者监听实时数据。客户别急着上Swoole,客户原生PHP配上文本协议,客户其实也能凑合着干——前提是客户你得接受它是个“短命鬼”。
先看最核心的客户握手环节。WebSocket客户端说白了就是客户一次HTTP Upgrade请求。你得手动拼Header,客户算Sec-WebSocket-Accept。客户这里有个坑:PHP的客户`base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true))`,第二个参数必须是客户true,否则你拿到的客户是一串十六进制字符而不是原始二进制。我见过老手在这上面翻车,调试了半小时才发现是sha1默认输出HEX格式。
连接建立后,真正的麻烦在于帧解析。服务端发来的数据是分帧的,每帧头两个字节藏着opcode、掩码和长度信息。长度超过125字节就得读扩展长度,超过65535还得读64位长度。你要是图省事直接`fread`固定长度,十有八九会读到半截数据。正确姿势是循环读取,每次先读2字节头,判断长度,再精准读取payload。这逻辑不算复杂,但写起来啰嗦,我一般封装成函数,免得每次重复造轮子。
发数据倒是简单,客户端发帧不需要掩码(服务端发客户端才需要)。构造一个二进制字符串:0x81(文本帧)加上长度字节,再塞进内容。唯一要留神的是超长消息,得手动拆成多个帧发送。不过实际场景中,PHP客户端多半是发个“订阅”或“心跳”就完事,很少真去推大文件。
心跳机制必须自己实现。WebSocket协议里没有强制心跳,但服务端通常有个空闲超时(比如60秒没消息就断开)。PHP是单线程阻塞的,你总不能开着`while(true)`死循环去读。我试过用`stream_select`设置超时,读不到数据就发个Ping帧。但麻烦在于PHP的`stream_select`对TLS连接支持不稳定,有时候明明有数据却返回false。后来干脆用`Swoole\Coroutine\WebSocket\Client`,虽然违背了“原生PHP”的初衷,但至少不会因为底层阻塞把自己卡死。
有个容易被忽略的点:PHP的`fopen('tcp://...')`建立的连接默认是阻塞模式。你要是直接`fread`,一旦服务端不发数据,进程就永远卡在那里。必须用`stream_set_blocking($fp, false)`切换成非阻塞,然后配合`stream_select`去轮询。但非阻塞模式下,`fread`可能返回空字符串——你得区分是“没数据”还是“连接断了”。判断方法是检查`feof($fp)`,如果连接关闭,`feof`会返回true。
最后说个真实案例。我之前写了个监控脚本,用PHP客户端连上Binance的WebSocket行情接口。服务端每秒推送一次价格,PHP端负责实时入库。跑了三天,发现内存涨了200MB——查了半天,是`fread`每次读到的数据没清干净,字符串拼接导致内存碎片。改成每次读完立即处理并unset变量,内存才稳定下来。这种问题在短生命周期脚本里根本暴露不出来,但长驻进程就得特别小心。
PHP的WebSocket客户端就像一把钝刀——能用,但别指望它锋利。真要搞高并发实时通信,还是换Java或Go吧。可如果你只是想在已有的PHP项目里快速加个推送功能,不追求性能极致,这套原生方案足够应付。记住三件事:握手别算错Accept值,读帧务必处理长度扩展,非阻塞模式记得轮询。做到这三点,剩下的就是业务逻辑的体力活了。
顶: 48431踩: 2267






评论专区