php:使用socket函数创建WebSocket服务

一、前言

闲来无事,最近捣鼓了下websocket,但是不希望安装第三方类库,所以打算用socket基础函数创建个服务。

二、构建websocket服务端

php 复制代码
<?php

class SocketService
{
    // 默认的监听地址和端口
    private $address  = '0.0.0.0';
    private $port = 8083;
    private $_sockets;

    /**
     * 构造函数,初始化地址和端口
     *
     * @param string $address 监听的地址,默认 '0.0.0.0'
     * @param int $port 监听的端口,默认 8083
     */
    public function __construct($address = '', $port = '')
    {
        if (!empty($address)) {
            $this->address = $address;
        }
        if (!empty($port)) {
            $this->port = $port;
        }
    }

    /**
     * 初始化服务,创建套接字并开始监听
     */
    public function service()
    {
        // 获取 TCP 协议号
        $tcp = getprotobyname("tcp");

        // 创建 TCP 套接字
        $sock = socket_create(AF_INET, SOCK_STREAM, $tcp);

        // 设置套接字选项,允许地址重用
        socket_set_option($sock, SOL_SOCKET, SO_REUSEADDR, 1);

        // 如果创建失败,抛出异常
        if ($sock < 0) {
            throw new Exception("failed to create socket: " . socket_strerror($sock) . "\n");
        }

        // 绑定地址和端口
        socket_bind($sock, $this->address, $this->port);

        // 开始监听
        socket_listen($sock, $this->port);

        echo "listen on $this->address $this->port ... \n";
        
        // 保存套接字
        $this->_sockets = $sock;
    }

    /**
     * 运行 WebSocket 服务
     * 
     * 该方法会进入一个无限循环,处理所有客户端连接
     */
    public function run()
    {
        // 启动服务
        $this->service();
        
        // 存储客户端套接字
        $clients[] = $this->_sockets;

        // 无限循环监听客户端连接
        while (true) {
            $changes = $clients;
            $write = NULL;
            $except = NULL;

            // 监听可读的套接字
            socket_select($changes, $write, $except, NULL);

            // 处理每个连接的套接字
            foreach ($changes as $key => $_sock) {
                // 判断是否是新连接
                if ($this->_sockets == $_sock) {
                    // 接受新连接
                    if (($newClient = socket_accept($_sock)) === false) {
                        die('failed to accept socket: ' . socket_strerror($_sock) . "\n");
                    }

                    // 读取客户端发送的数据
                    $line = trim(socket_read($newClient, 1024));

                    // 执行 WebSocket 握手
                    $this->handshaking($newClient, $line);

                    // 获取客户端 IP
                    socket_getpeername($newClient, $ip);

                    // 将新连接的客户端保存
                    $clients[$ip] = $newClient;

                    // 输出客户端 IP 和消息
                    echo "Client ip:{$ip}   \n";
                    echo "Client msg:{$line} \n";
                } else {
                    // 处理已连接的客户端消息
                    socket_recv($_sock, $buffer, 2048, 0);

                    // 解码接收到的消息
                    $msg = $this->message($buffer);

                    // 在这里处理业务逻辑
                    echo "{$key} client msg: {$msg}\n";

                    // 等待用户输入响应
                    fwrite(STDOUT, 'Please input a argument:');
                    $response = trim(fgets(STDIN));

                    // 发送响应给客户端
                    $this->send($_sock, $response);

                    echo "{$key} response to Client: {$response}\n";
                }
            }
        }
    }

    /**
     * WebSocket 握手处理
     * 
     * @param resource $newClient 新连接的客户端套接字
     * @param string $line 接收到的握手请求头
     * @return int 返回写入的字节数
     */
    public function handshaking($newClient, $line)
    {
        $headers = array();
        $lines = preg_split("/\r\n/", $line);

        // 解析请求头
        foreach ($lines as $line) {
            $line = chop($line);
            if (preg_match('/\A(\S+): (.*)\z/', $line, $matches)) {
                $headers[$matches[1]] = $matches[2];
            }
        }

        // 获取客户端的 Sec-WebSocket-Key
        $secKey = $headers['Sec-WebSocket-Key'];

        // 生成 Sec-WebSocket-Accept
        $secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));

        // 构造握手响应
        $upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
            "Upgrade: websocket\r\n" .
            "Connection: Upgrade\r\n" .
            "WebSocket-Origin: $this->address\r\n" .
            "WebSocket-Location: ws://$this->address:$this->port/websocket/websocket\r\n" .
            "Sec-WebSocket-Accept:$secAccept\r\n\r\n";

        // 发送握手响应
        return socket_write($newClient, $upgrade, strlen($upgrade));
    }

    /**
     * 解析接收到的 WebSocket 消息
     * 
     * @param string $buffer 接收到的 WebSocket 数据
     * @return string 解码后的消息
     */
    public function message($buffer)
    {
        $len = $masks = $data = $decoded = null;
        $len = ord($buffer[1]) & 127;

        // 根据消息长度处理掩码和数据
        if ($len === 126) {
            $masks = substr($buffer, 4, 4);
            $data = substr($buffer, 8);
        } else if ($len === 127) {
            $masks = substr($buffer, 10, 4);
            $data = substr($buffer, 14);
        } else {
            $masks = substr($buffer, 2, 4);
            $data = substr($buffer, 6);
        }

        // 解码消息
        for ($index = 0; $index < strlen($data); $index++) {
            $decoded .= $data[$index] ^ $masks[$index % 4];
        }

        return $decoded;
    }

    /**
     * 发送 WebSocket 消息给客户端
     * 
     * @param resource $newClient 新连接的客户端套接字
     * @param string $msg 要发送的消息
     * @return int 返回写入的字节数
     */
    public function send($newClient, $msg)
    {
        // 封装消息为 WebSocket 数据帧
        $msg = $this->frame($msg);

        // 发送数据帧
        socket_write($newClient, $msg, strlen($msg));
    }

    /**
     * 将消息封装为 WebSocket 数据帧
     * 
     * @param string $s 要封装的消息
     * @return string 封装后的 WebSocket 数据帧
     */
    public function frame($s)
    {
        $a = str_split($s, 125);
        
        if (count($a) == 1) {
            return "\x81" . chr(strlen($a[0])) . $a[0];
        }
        
        $ns = "";
        foreach ($a as $o) {
            $ns .= "\x81" . chr(strlen($o)) . $o;
        }

        return $ns;
    }

    /**
     * 关闭 WebSocket 连接
     * 
     * @return bool 返回是否成功关闭
     */
    public function close()
    {
        return socket_close($this->_sockets);
    }
}

// 创建并运行 WebSocket 服务
$sock = new SocketService();
$sock->run();

三、构建websocket客户端

接下来写个前端页面,测试服务端是否正常,代码如下:

html 复制代码
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <title>WebSocket</title>
  </head>
  <body>
    <input id="text" value="">
    <input type="submit" value="发送" onclick="start()">
    <input type="submit" value="关闭" onclick="close()">
    <div id="msg"></div>

    <script>
      /**
       * WebSocket的连接状态代码:
       * 0: 未连接
       * 1: 已连接,可以通讯
       * 2: 正在关闭
       * 3: 已关闭或无法打开
       */
      
      // 创建WebSocket实例
      var webSocket = new WebSocket("ws://127.0.0.1:8083");

      // 监听错误事件
      webSocket.onerror = function (event) {
        onError(event);
      };

      // 监听连接成功事件
      webSocket.onopen = function (event) {
        onOpen(event);
      };

      // 监听消息事件
      webSocket.onmessage = function (event) {
        onMessage(event);
      };

      // 监听关闭事件
      webSocket.onclose = function (event) {
        onClose(event);
      };

      // 错误处理函数
      function onError(event) {
        document.getElementById("msg").innerHTML = "<p>连接错误</p>";
        console.log("错误: " + event.data);
      }

      // 连接成功后的回调函数
      function onOpen(event) {
        console.log("连接成功: " + sockState());
        document.getElementById("msg").innerHTML = "<p>已连接到服务</p>";
      }

      // 处理接收到的消息
      function onMessage(event) {
        console.log("接收到消息");
        document.getElementById("msg").innerHTML += "<p>响应: " + event.data + "</p>";
      }

      // 连接关闭后的回调函数
      function onClose(event) {
        document.getElementById("msg").innerHTML = "<p>连接已关闭</p>";
        console.log("关闭连接: " + sockState());
        webSocket.close();
      }

      // 获取WebSocket连接状态
      function sockState() {
        var status = ['未连接', '已连接,可以通讯', '正在关闭', '已关闭或无法打开'];
        return status[webSocket.readyState];
      }

      // 发送消息函数
      function start(event) {
        console.log(webSocket);
        var msg = document.getElementById('text').value;
        document.getElementById('text').value = ''; // 清空输入框
        console.log("发送消息: " + sockState());
        console.log("消息内容: " + msg);
        webSocket.send("msg=" + msg); // 发送消息
        document.getElementById("msg").innerHTML += "<p>请求: " + msg + "</p>";
      }

      // 关闭连接
      function close(event) {
        webSocket.close();
      }
    </script>
  </body>
</html>

四、测试结果

出现已连接到服务,代表成功连接。

相关推荐
hopetomorrow12 分钟前
学习路之PHP--使用GROUP BY 发生错误 SELECT list is not in GROUP BY clause .......... 解决
开发语言·学习·php
网络安全-杰克19 分钟前
网络安全概论
网络·web安全·php
不是二师兄的八戒21 分钟前
本地 PHP 和 Java 开发环境 Docker 化与配置开机自启
java·docker·php
怀澈12223 分钟前
高性能服务器模型之Reactor(单线程版本)
linux·服务器·网络·c++
耗同学一米八1 小时前
2024 年河北省职业院校技能大赛网络建设与运维赛项样题二
运维·网络·mariadb
skywalk81631 小时前
树莓派2 安装raspberry os 并修改成固定ip
linux·服务器·网络·debian·树莓派·raspberry
C++忠实粉丝1 小时前
计算机网络socket编程(3)_UDP网络编程实现简单聊天室
linux·网络·c++·网络协议·计算机网络·udp
黑客Ela2 小时前
网络安全中常用浏览器插件、拓展
网络·安全·web安全·网络安全·php
qdprobot2 小时前
ESP32桌面天气摆件加文心一言AI大模型对话Mixly图形化编程STEAM创客教育
网络·人工智能·百度·文心一言·arduino
hakesashou3 小时前
Python中常用的函数介绍
java·网络·python