PHP原生Socket实现WebSocket全双工通信详解

小鹿同学
小鹿同学
发布时间:2026-10-13 | 3人浏览过
原创

在PHP中实现WebSocket需要手动处理TCP连接、HTTP握手升级及数据帧编解码。本文将通过分析WebSocke协议原理,结合PHP原生Socket API,展示如何从零搭建一个支持多连接的WebSocket服务器,并以一个简易聊天室为例,完整演示握手、心跳、消息分发与断连处理的实际编码过程。

结果或目标

掌握在PHP环境中使用原生 socket_*() 函数集建立持久化长连接,理解WebSocket协议的握手流程(Upgrade Header交换)及数据帧格式,能够独立编写具备多用户管理能力的Socket服务端脚本。

准备条件

  • 安装了 sockets 扩展的PHP环境(通常Linux默认安装,Windows需开启extension=php_sockets.dll)。
  • 具备基础的TCP/IP网络知识,理解C/S架构与服务端/客户端角色。
  • 本地运行终端用于执行PHP后台脚本,以及现代浏览器用于前端测试。

真实操作顺序

第1步:理解WebSocket连接原理

HTTP是请求-响应模式,由客户端发起;而WebSocket允许服务端主动推送数据。建立WebSocket连接前,需完成一次HTTP握手:

  1. 客户端请求:发送带有 Upgrade: websocket 和 Sec-WebSocket-Key 的请求头。
  2. 服务端响应:收到请求后,将 Sec-WebSocket-Key 进行特定算法加密(SHA1 + Magic String),生成 Sec-WebSocket-Accept 返回给客户端,状态码为101 Switching Protocols。

第2步:PHP服务端Socket初始化

服务端需创建一个流式Socket(SOCK_STREAM),绑定IP和端口并进入监听状态。

function WebSocket($address, $port) {
    // 创建IPv4 TCP Socket
    $server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
    
    // 设置允许地址重用,避免重启时报错
    socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
    
    // 绑定地址与端口
    socket_bind($server, $address, $port);
    
    // 开始监听,最大挂起连接数为10
    socket_listen($server, 10);
    
    return $server;
}

第3步:实现主循环与连接管理

PHP脚本需保持运行状态,通过 socket_select() 阻塞等待I/O事件。需维护两个数组:$sockets(所有活跃fd)和 $users(记录连接状态、是否握手等信息)。

function run() {
    while (true) {
        $changes = $this->sockets;
        $write = NULL;
        $except = NULL;
        
        // 阻塞直到有连接建立或数据到达
        socket_select($changes, $write, $except, NULL);
        
        foreach ($changes as $sock) {
            // 新连接接入
            if ($sock == $this->master) {
                $client = socket_accept($this->master);
                $key = uniqid();
                $this->sockets[] = $client;
                $this->users[$key] = [
                    'socket' => $client,
                    'shou' => false // 标记未握手
                ];
            } else {
                // 接收数据
                $len = 0;
                $buffer = '';
                do {
                    $l = socket_recv($sock, $buf, 1000, 0);
                    $len += $l;
                    $buffer .= $buf;
                } while ($l == 1000);
                
                // 查找对应用户
                $k = $this->search($sock);
                
                // 长度小于7视为断开或无效数据
                if ($len < 7) {
                    $this->send2($k);
                    continue;
                }
                
                // 未握手则执行握手逻辑
                if (!$this->users[$k]['shou']) {
                    $this->woshou($k, $buffer);
                } else {
                    // 已握手,解析数据帧
                    $decodedData = $this->uncode($buffer, $k);
                    if ($decodedData !== false) {
                        // 业务处理与推送
                        $this->handleMessage($k, $decodedData);
                    }
                }
            }
        }
    }
}

第4步:完成握手验证

核心在于处理 Sec-WebSocket-Key。必须严格遵循RFC 6455规范进行计算。

function woshou($k, $buffer) {
    // 提取Key
    $buf = substr($buffer, strpos($buffer, 'Sec-WebSocket-Key:') + 18);
    $key = trim(substr($buf, 0, strpos($buf, "\r\n")));
    
    // RFC 6455 规定的Magic String
    $magicString = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
    
    // 计算Accept Key
    $newKey = base64_encode(sha1($key . $magicString, true));
    
    // 构造响应头
    $response = "HTTP/1.1 101 Switching Protocols\r\n";
    $response .= "Upgrade: websocket\r\n";
    $response .= "Connection: Upgrade\r\n";
    $response .= "Sec-WebSocket-Accept: " . $newKey . "\r\n\r\n";
    
    socket_write($this->users[$k]['socket'], $response, strlen($response));
    $this->users[$k]['shou'] = true;
}

第5步:数据帧解码(Masking Removal)

WebSocket客户端发往服务器的数据必须进行掩码(Mask)处理,服务端接收时需去除掩码还原原始Payload。

function uncode($str, $key) {
    $msg = unpack('H*', $str);
    $head = substr($msg[1], 0, 2);
    
    // FIN bit + Opcode (0x81 for text)
    if (hexdec($head[1]) < 8 || hexdec($head[1]) > 15) {
        return false; // 未知类型
    }
    
    // 解析长度与掩码
    $maskLen = substr($msg[1], 2, 2);
    $len = hexdec($maskLen);
    
    $isMasked = $len >= 126;
    if ($len == 126) {
        // 16位长度
        $len = hexdec(substr($msg[1], 4, 4));
        $maskOffset = 10;
    } elseif ($len == 127) {
        // 64位长度
        $len = hexdec(substr($msg[1], 4, 16));
        $maskOffset = 22;
    } else {
        $maskOffset = 8;
    }
    
    if ($isMasked) {
        // 获取Masking-Key
        $mKey = [$maskOffset+1, $maskOffset+2, $maskOffset+3, $maskOffset+4];
        $dataStart = $maskOffset + 5;
        $rawData = substr($msg[1], $dataStart);
        $decoded = '';
        $r = 0;
        // XOR去掩码
        for ($i = 0; $i < strlen($rawData); $i++) {
            $decoded .= chr(hexdec(substr($rawData, $i*2, 2)) ^ hexdec(substr($msg[1], $mKey[($i % 4)]*2, 2)));
            $i++; // consume next byte of data
        }
        return $decoded;
    }
    return false;
}

完成判断

当PHP脚本启动后,若能成功绑定端口且无报错输出,并在浏览器控制台看到WebSocket ReadyState变为1,即表示底层通道已连通。

原文有依据的排错

  • 端口被占用:确保 socket_set_option 中 SO_REUSEADDR 设为1,否则脚本重启时可能因TIME_WAIT状态失败。
  • Huge Data:原生Socket处理大文本需注意内存泄漏,生产环境建议引入Redis作为发布订阅中间件解耦。
  • 中文乱码:确保JSON序列化及传输过程中统一使用UTF-8,前端JS需正确解码URL参数。

简要总结

通过上述步骤,我们使用PHP原生API实现了WebSocket的核心握手与数据收发。此方法适合轻量级内部系统或学习原理,对于高并发场景建议转向Swoole等异步框架。以上就是PHP中使用原生Socket实现WebSocket通讯的详细教程,更多关于后端实时通信技术的资料请关注本站其它相关文章!

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表