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

news2025/1/11 12:50:35

一、前言

        闲来无事,最近捣鼓了下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>

四、测试结果

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

@RequestBody、@Data、@Validated、@Pattern(regexp=“?“)(复习)

目录 一、注解RequestBody。 二、注解Data。 三、注解Validated、Pattern(regexp"?")。 1、完成实体参数&#xff08;对象属性&#xff09;校验。 2、NotNull、NotEmpty、Email。 一、注解RequestBody。 &#xff08;如&#xff1a;JSON格式的数据——>Java对象&…

基于YOLOv8深度学习的医学影像骨折检测诊断系统研究与实现(PyQt5界面+数据集+训练代码)

本论文深入研究并实现了一种基于YOLOV8深度学习模型的医学影像骨折检测与诊断系统&#xff0c;旨在为医学影像中的骨折检测提供高效且准确的自动化解决方案。随着医疗影像技术的快速发展&#xff0c;临床医生需要从大量复杂的医学图像中精确、快速地识别病灶区域&#xff0c;特…

69.x的平方根-力扣(LeetCode)

题目&#xff1a; 解题思路&#xff1a; 解决本题主要运用的方法是二分法&#xff0c;二分法是一种在有序数组中查找某一特定元素的搜索算法。鉴于本题满足整个序列是有序的&#xff0c;并且可以通过比较来改变区间&#xff0c;满足二分法的应用条件&#xff0c;所以采用二分法…

Notepad++--在开头快速添加行号

原文网址&#xff1a;Notepad--在开头快速添加行号_IT利刃出鞘的博客-CSDN博客 简介 本文介绍Notepad怎样在开头快速添加行号。 需求 原文件 想要的效果 方法 1.添加点号 Alt鼠标左键&#xff0c;从首行选中首列下拉&#xff0c;选中需要添加序号的所有行的首列&#xff…

新兴数据仓库设计与实践手册:从分层架构到实际应用(二)

本手册将分为三部分发布&#xff0c;以帮助读者逐步深入理解数据仓库的设计与实践。 第一部分介绍数据仓库的整体架构概述&#xff1b;第二部分深入讨论ETL在数仓中的应用理论&#xff0c;ODS层的具体实现与应用&#xff1b;第三部分将围绕DW数据仓库层、ADS层和数据仓库的整体…

java八股-SpringCloud微服务-Eureka理论

文章目录 SpringCloud架构Eureka流程Nacos和Eureka的区别是&#xff1f;CAP定理Ribbon负载均衡策略自定义负载均衡策略如何实现&#xff1f;本章小结 SpringCloud架构 Eureka流程 服务提供者向Eureka注册服务信息服务消费者向注册中心拉取服务信息服务消费者使用负载均衡算法挑…

MySQL —— explain 查看执行计划与 MySQL 优化

文章目录 explain 查看执行计划explain 的作用——查看执行计划explain 查看执行计划返回信息详解表的读取顺序&#xff08;id&#xff09;查询类型&#xff08;select_type&#xff09;数据库表名&#xff08;table&#xff09;联接类型&#xff08;type&#xff09;可用的索引…

input file结合vue3和vant实现上传图片效果,并显示上传进度百分比%

这里写自定义目录标题 采用的dom结构是input file&#xff0c;label事件绑定&#xff0c;一下为代码传入参数为uploadNum实现效果如图上传中&#xff0c;图片1上传成功&#xff0c;图片2 采用的dom结构是input file&#xff0c;label事件绑定&#xff0c;一下为代码 传入参数为…

CSS优化file控件样式

<div class"file-box"><input type"button" class"btn" value"选择文件" /><inputtype"file"class"file"id"upimg"change"previewFiles"multiple/></div><!-- Vu…

AJAX笔记 (速通精华版)

AJAX&#xff08;Asynchronous Javascript And Xml&#xff09; 此笔记来自于动力节点最美老杜 传统请求及缺点 传统的请求都有哪些&#xff1f; 直接在浏览器地址栏上输入URL。点击超链接提交 form 表单使用 JS 代码发送请求 window.open(url)document.location.href urlwi…

某校园网登录界面前端加密绕过

前言 尝试对学校校园网登录框进行爆破&#xff0c;发现密码在前端被加密了 Burp抓包 抓包信息 DDDDD2022***&upass3d5c84b6fb1dc75987884f39c05b0e6a123456782&R10&R21&para00&0MKKey123456&v6ip From表单提交上来的文本这些参数&#xff0c;DDDD是…

《生成式 AI》课程 第3講 CODE TASK执行文章摘要的机器人

课程 《生成式 AI》课程 第3講&#xff1a;訓練不了人工智慧嗎&#xff1f;你可以訓練你自己-CSDN博客 任务1:总结 1.我们希望你创建一个可以执行文章摘要的机器人。 2.设计一个提示符&#xff0c;使语言模型能够对文章进行总结。 model: gpt-4o-mini,#gpt-3.5-turbo, import…

Github客户端工具github-desktop使用教程

文章目录 1.客户端工具的介绍2.客户端工具使用感受3.仓库的创建4.初步尝试5.本地文件和仓库路径5.1原理说明5.2修改文件5.3版本号的说明5.4结合码云解释5.5版本号的查找 6.分支管理6.1分支的引入6.2分支合并6.3创建测试仓库6.4创建测试分支6.5合并分支6.6合并效果查看6.7分支冲…

python实战案例----使用 PyQt5 构建简单的 HTTP 接口测试工具

python实战案例----使用 PyQt5 构建简单的 HTTP 接口测试工具 文章目录 python实战案例----使用 PyQt5 构建简单的 HTTP 接口测试工具项目背景技术栈用户界面核心功能实现结果展示完整代码总结 在现代软件开发中&#xff0c;测试接口的有效性与响应情况变得尤为重要。本文将指导…

JavaScript的类型转换

类型转换 &#xff1a; 隐式转换和显示转换 一般的&#xff0c;默认单选框和多选框传过来的值都是字符串 JavaScript是弱数据类型&#xff1a;JavaScript不知道变量属于哪种类型&#xff0c;需要赋值了才清楚。 缺点&#xff1a;使用表单、prompt获取过来的数据默认是字符串类…

Spring Boot中使用AOP和反射机制设计一个基于redis的幂等注解,简单易懂教程

由于对于一些非查询操作&#xff0c;有时候需要保证该操作是幂等的&#xff0c;该帖子设计幂等注解的原理是使用AOP和反射机制获取方法的类、方法和参数&#xff0c;然后拼接形成一个幂等键&#xff0c;当下一次有重复操作过来的时候&#xff0c;判断该幂等键是否存放&#xff…

一文详细深入总结服务器选型

1. 题记&#xff1a; 服务器选型工作是项目规划检讨的一项非常重要的工作&#xff0c;本文详细深入总结服务器选型。 2. 服务器基础知识概览 2.1 服务器的定义与功能 2.1 .1 定义 服务器是一种高性能计算机&#xff0c;其设计目的是在网络中提供服务。它可以处理来自多个客…

接口测试用例设计的关键步骤与技巧解析!

简介 接口测试在需求分析完成之后&#xff0c;即可设计对应的接口测试用例&#xff0c;然后根据用例进行接口测试。接口测试用例的设计也需要用到黑盒测试用例设计方法&#xff0c;和测试流程与理论章节的功能测试用例设计的方法类似&#xff0c;设计过程中还需要增加与接口特…

WPF下 DataGrid加入序号列

先上代码&#xff1a; <DataGrid Name"DGV" AutoGenerateColumns"False" Grid.Row"0" Grid.Column"0" HorizontalGridLinesBrush"RoyalBlue" VerticalGridLinesBrush"Tomato" CanUserAddRows"False&qu…

WebSocket详解、WebSocket入门案例

目录 1.1 WebSocket介绍 http协议&#xff1a; webSocket协议&#xff1a; 1.2WebSocket协议&#xff1a; 1.3客户端&#xff08;浏览器&#xff09;实现 1.3.2 WebSocket对象的相关事宜&#xff1a; 1.3.3 WebSOcket方法 1.4 服务端实现 服务端如何接收客户端发送的请…