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

一、前言

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

    

二、构建websocket服务端

<?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客户端

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

<!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>

四、测试结果

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/920068.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【YOLOv8】安卓端部署-1-项目介绍

【YOLOv8】安卓端部署-1-项目介绍 1 什么是YOLOv81.1 YOLOv8 的主要特性1.2 YOLOv8分割模型1.2.1 YOLACT实例分割算法之计算掩码1.2.1.1 YOLACT 的掩码原型与最终的掩码的关系1.2.1.2 插值时的目标检测中提取的物体特征1.2.1.3 coefficients&#xff08;系数&#xff09;作用1.…

(十八)JavaWeb后端开发案例——会话/yml/过滤器/拦截器

目录 1.业务逻辑实现 1.1 登录校验技术——会话 1.1.1Cookie 1.1.2session 1.1.3JWT令牌技术 2.参数配置化 3.yml格式配置文件 4.过滤器Filter 5.拦截器Interceptor 1.业务逻辑实现 Day10-02. 案例-部门管理-查询_哔哩哔哩_bilibili //Controller层/*** 新增部门*/Pos…

数字IC后端设计实现之Innovus place报错案例 (IMPSP-9099,9100三种解决方案)

最近吾爱IC社区星球会员问到跑place_opt_design时会报错退出的情况。小编今天把这个错误解决办法分享给大家。主要分享三个方法&#xff0c;大家可以根据自己的实际情况来选择。 数字IC后端低功耗设计实现案例分享(3个power domain&#xff0c;2个voltage domain) **ERROR: (I…

麒麟网络负载均衡与高可用方案实践

安装 teamd 包。 yum -y install teamd Copy 一、配置TEAMING 查看两个网卡信息 ifconfig Copy 注意&#xff1a;根据实际网卡设备名称情况调整代码&#xff01;不同环境下网卡名称略有不同&#xff01; 根据查询的结果&#xff0c;两张网卡设备名称分别为 enp0s2 和 enp…

【SpringBoot】26 实体映射工具(MapStruct)

Gitee 仓库 https://gitee.com/Lin_DH/system 介绍 现状 为了让应用程序的代码更易于维护&#xff0c;通常会将项目进行分层。在《阿里巴巴 Java 开发手册》中&#xff0c;推荐分层如下图所示&#xff1a; 每层都有对应的领域模型&#xff0c;即不同类型的 Bean。 DO&…

深述C++模板类

1、前言 函数模板是通用函数的描述&#xff0c;类模板是通用类的描述&#xff0c;使用任意类型来描述类的定义。和函数模板有很多相似的地方&#xff0c;关于函数模板可以看我之前写过的一篇文章&#xff1a;简述C函数模板。这里就不过多赘述。 2、模板类的基本概念 模板类的…

利用Python爬虫获取1688搜索词推荐:技术与实践

在电子商务领域&#xff0c;关键词的选择对于产品的曝光和销售至关重要。1688作为中国领先的B2B电子商务平台&#xff0c;提供了丰富的搜索词推荐功能&#xff0c;帮助商家优化关键词策略。本文将详细介绍如何使用Python编写爬虫程序&#xff0c;获取1688平台的搜索词推荐&…

Flink Lookup Join(维表 Join)

Lookup Join 定义&#xff08;支持 Batch\Streaming&#xff09; Lookup Join 其实就是维表 Join&#xff0c;比如拿离线数仓来说&#xff0c;常常会有用户画像&#xff0c;设备画像等数据&#xff0c;而对应到实时数仓场景中&#xff0c;这种实时获取外部缓存的 Join 就叫做维…

从Stream的 toList() 和 collect(Collectors.toList()) 方法看Java的不可变流

环境 JDK 21Windows 11 专业版IntelliJ IDEA 2024.1.6 背景 在使用Java的Stream的时候&#xff0c;常常会把流收集为List。 假设有List list1 如下&#xff1a; var list1 List.of("aaa", "bbbbbb", "cccc", "d", "eeeee&qu…

wsl虚拟机中的dockers容器访问不了物理主机

1 首先保证wsl虚拟机能够访问宿主机IP地址&#xff0c;wsl虚拟机通过vEthernet (WSL)的地址访问&#xff0c;着意味着容器也要通过此IP地址访问物理主机。 2 遇到的问题&#xff1a;wsl虚拟机中安装了docker&#xff0c;用在用到docker容器内的开发环境&#xff0c;但是虚拟机…

华为VPN技术

1.启动设备 2.配置IP地址 [FW1]int g1/0/0 [FW1-GigabitEthernet1/0/0]ip add 192.168.1.254 24 [FW1-GigabitEthernet1/0/0]int g1/0/1 [FW1-GigabitEthernet1/0/1]ip add 100.1.1.1 24 [FW1-GigabitEthernet1/0/1]service-manage ping permit [FW2]int g1/0/0 [FW2-Gi…

【Swift】运算符

文章目录 术语赋值运算符算数运算符基本四则算术运算符求余运算符一元负号运算符一元正号运算符 比较运算符三元运算符空合运算符区间运算符闭区间运算符半开区间运算符单侧区间运算符 逻辑运算符逻辑非运算符逻辑与运算符逻辑或运算符逻辑运算符组合计算 位运算符运算符优先级…

二手手机回收小程序,一键便捷高效回收

随着科技的不断升级&#xff0c;智能手机也在快速进行更新换代&#xff0c;出现了大量的闲置手机&#xff0c;这为二手手机市场提供了巨大的发展空间&#xff01; 经过手机回收市场的快速发展&#xff0c;二手手机回收已经成为了消费者的新选择&#xff0c;既能够减少手机的浪…

网安瞭望台第2期:零日漏洞密集爆发、2024年常见网络安全漏洞类型及分析

国内外要闻 Ubuntu 服务器 Needrestart 软件包惊现严重安全漏洞 近日&#xff0c;Ubuntu 服务器&#xff08;自 21.04 版本起默认安装&#xff09;的 Needrestart 软件包被曝存在多个可追溯至数十年前的安全漏洞。这些漏洞允许本地攻击者在无需用户交互的情况下获取根…

反转链表、链表内指定区间反转

反转链表 给定一个单链表的头结点pHead&#xff08;该头节点是有值的&#xff0c;比如在下图&#xff0c;它的val是1&#xff09;&#xff0c;长度为n&#xff0c;反转该链表后&#xff0c;返回新链表的表头。 如当输入链表{1,2,3}时&#xff0c;经反转后&#xff0c;原链表变…

AWTK 最新动态:支持鸿蒙系统(HarmonyOS Next)

HarmonyOS是全球第三大移动操作系统&#xff0c;有巨大的市场潜力&#xff0c;在国产替代的背景下&#xff0c;机会多多&#xff0c;AWTK支持HarmonyOS&#xff0c;让AWTK开发者也能享受HarmonyOS生态的红利。 AWTK全称为Toolkit AnyWhere&#xff0c;是ZLG倾心打造的一套基于C…

CSS+JQuery 实现弹力球效果,碰到屏幕边框弹回

实现弹力球效果&#xff0c;碰到屏幕边框弹回&#xff0c;效果如下 代码如下&#xff1a; <img src"../image/ball.png" alt"" class"ball"> <style>.ball {position: fixed;top: 50vh;left: 50vw;width: 15vw;height: 15vw;border…

银河麒麟V10-SP1-x86_64离线安装Docker

由于要推广信创&#xff0c;需要把Milvus向量数据库从别的平台迁移到信创平台上&#xff0c;为了能顺利迁移&#xff0c;在迁移前需要做一系列用到的功能软件的安装与运行的测试&#xff0c;由于Milvus向量数据库依赖于Docker运行&#xff0c;以及工作性质的要求&#xff0c;只…

vue2 webpack分包实现首屏加载优化

项目打包后得到的vendor.js文件过大&#xff0c;进行拆包以减少文件的大小&#xff0c;具体实现如下&#xff1a; webpack3.x使用new webpack.optimize.CommonsChunkPlugin打包文件分割优化加载 修改项目build内的webpack.prod.conf.js文件&#xff0c;将项目中的需要拆的文件…

125.验证回文串-力扣(LeetCode)

题目&#xff1a; 解题思路&#xff1a; 首先进行移除非字母数字字符&#xff0c;并将大写字符转换为小写字符的操作。这个过程中&#xff0c;主要利用快慢指针的方式来进行移除操作&#xff0c;通过加32将大写字符转换为小写字符。完成后&#xff0c;将前一半的数据与后一半的…