棘轮存储用户连接并在服务器实例之外发送消息

2024-02-25

我一直在跟随教程here http://socketo.me/并使棘轮服务器正常工作。

我的聊天课程目前或多或少与教程相同,因此没有必要在这里展示这一点,因为我的问题更多是关于实施策略.

在我附加的问题中,用户正在寻找如何获取特定用户的连接对象。在最上面的答案解决方案中,跟踪资源 ID 似乎是实现此目的的方法。

例如,当创建连接时,会有此代码。

public function onOpen(ConnectionInterface $conn) {
        // Store the new connection to send messages to later
        $this->clients[$conn->resourceId] = $conn;
        echo "New connection! ({$conn->resourceId})\n";
    }

这会创建一个成员变量clients存储所有连接,您现在只需通过 ID 引用它即可发送消息。这个客户然而是一个例子 ConnectionInterface $conn

然后,要发送消息,您只需使用下面的代码,输入客户端的 id 作为数组键。很简单。

$client = $this->clients[{{insert client id here}}];
$client->send("Message successfully sent to user.");

正如我们所知,Ratchet 在服务器上作为脚本运行,事件循环永无止境。

我正在运行一个 Symfony 项目,其中服务器实例之外当用户在系统中执行特定操作时运行棘轮代码,我需要它向连接到服务器的特定客户端发送消息。

我不知道该怎么做,因为客户是 ConnectionInterface 的实例并在用户首次通过 WebSocket 连接时创建。如何以这种方式向特定客户端发送消息?

这是我想要实现的目标的视觉效果。

参考:

如何获取特定用户的连接对象? https://stackoverflow.com/questions/17583903/how-to-get-the-connection-object-of-a-specific-user


我即将发布的解决方案涵盖了在 Web 浏览器上从服务器到客户端通信的整个过程,包括使 Websocket 服务器在后台运行的方法(使用或不使用 docker)。

Step 1:

假设您已经通过 Composer 安装了 Ratchet,请在项目中创建一个名为 bin 的文件夹,并将文件命名为“startwebsocketserver.php”(或您想要的任何名称)

Step 2:

将以下代码复制到其中。

<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use React\Socket\Server;
use React\EventLoop\Factory;

use WebSocketApp\Websocketserver;
use WebSocketApp\Htmlserver;
use WebSocketApp\Clientevent;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Ratchet\App;

require dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/bootstrap/bootstrap.php';

$websocketserver = new Websocketserver();

$dispatcher = new EventDispatcher(); //@JA - This is used to maintain communication between the websocket and HTTP Rest API Server
$dispatcher->addListener('websocketserver.updateclient', array($websocketserver, 'updateClient'));

//// 1. Create the event loop
$loop = Factory::create();

//// 2. Create websocket servers
$webSock = new Server($loop);
new IoServer(
    new HttpServer(
        new WsServer( $websocketserver )
    ),
    $webSock
);
$webSock->listen('8080', '0.0.0.0');

$app = new App( 'localhost', 6677, '0.0.0.0',$loop );
$app->route( '/', new Htmlserver(), [ '*' ] );//@JA - Allow any origins for last parameter

$app->run();

请注意,在我的示例中,我使用引导文件来加载数据库。如果您不使用数据库或其他方法,请忽略它。为了这个答案的目的,我将假设 Doctrine 2 作为数据库。

这段代码的作用是在同一代码库中同时创建一个 HTTP 服务器和一个 WebSocket 服务器。我正在使用$app->route方法,因为您可以为 HTTP 服务器添加进一步的路由来组织 API 调用,以从 PHP Web 服务器与 WebSocket 服务器通信。

$loop 变量包括应用程序循环中的 Websocket 服务器以及 HTTPServer。

Step 3:

在您的项目目录中创建一个名为 websockets 的文件夹。在其中创建另一个名为 WebSocketApp 的文件夹。现在在其中创建 3 个空文件。

客户端事件.php html服务器.php Websocket服务器.php

接下来我们将一一研究这些文件。未能按此顺序创建这些目录将导致 Composer Autoload PSR-0 无法找到它们。

您可以更改名称,但请确保相应地编辑您的作曲家文件。

Step 4:

在您的composer.json 文件中,确保它看起来像这样。

{
    "require": {
        "doctrine/orm": "^2.5",
        "slim/slim": "^3.0",
        "slim/twig-view": "^2.1",
        "components/jquery": "*",
        "components/normalize.css": "*",
        "robloach/component-installer": "*",
        "paragonie/random_compat": "^2.0",
        "twilio/sdk": "^5.5",
        "aws/aws-sdk-php": "^3.22",
        "mailgun/mailgun-php": "^2.1",
        "php-http/curl-client": "^1.7",
        "guzzlehttp/psr7": "^1.3",
        "cboden/ratchet": "^0.3.6"
    },
    "autoload": {
        "psr-4": {
            "app\\":"app",
            "Entity\\":"entities"
        },
        "psr-0": {
            "WebSocketApp":"websockets"
        },
        "files": ["lib/utilities.php","lib/security.php"]
    }
}

就我而言,我使用的是doctrine & slim,重要的部分是“自动加载”部分。这一部分尤其重要。

"psr-0": {
            "WebSocketApp":"websockets"
        },

这将自动加载 WebSocketApp 命名空间中 websockets 文件夹中的所有内容。 psr-0 假设代码将按命名空间的文件夹进行组织,这就是为什么我们必须在 websockets 内添加另一个名为 WebSocketApp 的文件夹。

Step 5:

在 htmlserver.php 文件中将此...

<?php
namespace WebSocketApp;
use Guzzle\Http\Message\RequestInterface;
use Guzzle\Http\Message\Response;
use Guzzle\Http\Message\Request;
use Ratchet\ConnectionInterface;
use Ratchet\Http\HttpServerInterface;

class Htmlserver implements HttpServerInterface {
    protected $response;

    public function onOpen( ConnectionInterface $conn, RequestInterface $request = null ) {
        global $dispatcher;

        $this->response = new Response( 200, [
            'Content-Type' => 'text/html; charset=utf-8',
        ] );

        $query = $request->getQuery();
        parse_str($query, $get_array);//@JA - Convert query to variables in an array

        $json = json_encode($get_array);//@JA - Encode to JSON

        //@JA - Send JSON for what you want to do and the token representing the user & therefore connected user as well.
        $event = new ClientEvent($json);
        $dispatcher->dispatch("websocketserver.updateclient",$event);

        $this->response->setBody('{"message":"Successfully sent message to websocket server")');
        echo "HTTP Connection Triggered\n";
        $this->close( $conn );
    }

    public function onClose( ConnectionInterface $conn ) {
        echo "HTTP Connection Ended\n";
    }

    public function onError( ConnectionInterface $conn, \Exception $e ) {
        echo "HTTP Connection Error\n";
    }

    public function onMessage( ConnectionInterface $from, $msg ) {
        echo "HTTP Connection Message\n";
    }

    protected function close( ConnectionInterface $conn ) {
        $conn->send( $this->response );
        $conn->close();
    }
}

该文件的目的是通过基本 HTTP 简化与 WebSocket 服务器的通信,稍后我将展示从 PHP Web 服务器使用 cURL 的演示。我设计它是为了使用 Symfony 的事件系统并通过查看查询字符串并将其转换为 JSON 字符串来将消息传播到 WebSocket 服务器。如果您愿意,它也可以保留为数组,但就我而言,我需要 JSON 字符串。

Step 6:

接下来在 clientevent.php 中放置此代码...

<?php
namespace WebSocketApp;

use Symfony\Component\EventDispatcher\Event;

use Entity\User;
use Entity\Socket;

class Clientevent extends Event
{
    const NAME = 'clientevent';

    protected $user; //@JA - This returns type Entity\User

    public function __construct($json)
    {
        global $entityManager;

        $decoded = json_decode($json,true);
        switch($decoded["command"]){
            case "updatestatus":
                //Find out what the current 'active' & 'busy' states are for the userid given (assuming user id exists?)
                if(isset($decoded["userid"])){
                    $results = $entityManager->getRepository('Entity\User')->findBy(array('id' => $decoded["userid"]));
                    if(count($results)>0){
                        unset($this->user);//@JA - Clear the old reference
                        $this->user = $results[0]; //@JA - Store refernece to the user object
                        $entityManager->refresh($this->user); //@JA - Because result cache is used by default, this will make sure the data is new and therefore the socket objects with it
                    }
                }
                break;
        }
    }

    public function getUser()
    {
        return $this->user;
    }
}

请注意,User 和 Socket 实体是我根据 Doctrine 2 创建的实体。您可以使用您喜欢的任何数据库。就我而言,我需要根据数据库中的登录令牌从 PHP Web 服务器向特定用户发送消息。

Clientevent 假设 JSON 字符串为'{"command":"updatestatus","userid":"2"}'

您可以根据自己的喜好进行设置。

Step 7:

在 Websocketserver.php 文件中将此...

<?php
namespace WebSocketApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Symfony\Component\EventDispatcher\Event;

use Entity\User;
use Entity\Authtoken;
use Entity\Socket;

class Websocketserver implements MessageComponentInterface {
    protected $clients;

    public function updateClient(Event $event)
    {
       $user = $event->getUser();//@JA - Get reference to the user the event is for.

       echo "userid=".$user->getId()."\n";
       echo "busy=".($user->getBusy()==false ? "0" : "1")."\n";
       echo "active=".($user->getActive()==false ? "0" : "1")."\n";

       $json["busy"]    = ($user->getBusy()==false ? "0" : "1");
       $json["active"]  = ($user->getActive()==false ? "0" : "1");

       $msg = json_encode($json);

       foreach($user->getSockets() as $socket){
            $connectionid = $socket->getConnectionid();
            echo "Sending For ConnectionID:".$connectionid."\n";
            if(isset($this->clients[$connectionid])){
                $client = $this->clients[$connectionid];
                $client->send($msg);
            }else{
                echo "Client is no longer connected for this Connection ID:".$connectionid."\n";
            }
       }
    }

    public function __construct() {
        $this->clients = array();
    }

    public function onOpen(ConnectionInterface $conn) {
        // Store the new connection to send messages to later
        $this->clients[$conn->resourceId] = $conn;
        echo "New connection! ({$conn->resourceId})\n";
    }

    public function onMessage(ConnectionInterface $from, $msg) {
        global $entityManager;

        echo sprintf('Connection %d sending message "%s"' . "\n", $from->resourceId, $msg);

        //@JA - First step is to decode the message coming from the client.  Use token to identify the user (from cookie or local storage)
        //@JA - Format is JSON {token:58d8beeb0ada3:4ffbd272a1703a59ad82cddc2f592685135b09f2,message:register}
        $json = json_decode($msg,true);
        //echo 'json='.print_r($json,true)."\n";
        if($json["message"] == "register"){
            echo "Registering with server...\n";

            $parts = explode(":",$json["token"]);

            $selector = $parts[0];
            $validator = $parts[1];

            //@JA - Look up records in the database by selector.
            $tokens = $entityManager->getRepository('Entity\Authtoken')->findBy(array('selector' => $selector, 'token' => hash('sha256',$validator)));

            if(count($tokens)>0){
                $user = $tokens[0]->getUser();
                echo "User ID:".$user->getId()." Registered from given token\n";
                $socket = new Socket();
                $socket->setUser($user);
                $socket->setConnectionid($from->resourceId);
                $socket->setDatecreated(new \Datetime());

                $entityManager->persist($socket);
                $entityManager->flush();
            }else{
                echo "No user found from the given cookie token\n";
            }

        }else{
            echo "Unknown Message...\n";
        }     
    }

    public function onClose(ConnectionInterface $conn) {
        global $entityManager;

        // The connection is closed, remove it, as we can no longer send it messages
        unset($this->clients[$conn->resourceId]);

        //@JA - We need to clean up the database of any loose ends as well so it doesn't get full with loose data
        $socketResults = $entityManager->getRepository('Entity\Socket')->findBy(array('connectionid' => $conn->resourceId));
        if(count($socketResults)>0){
            $socket = $socketResults[0];
            $entityManager->remove($socket);
            $entityManager->flush();
            echo "Socket Entity For Connection ID:".$conn->resourceId." Removed\n";
        }else{
            echo "Was no socket info to remove from database??\n";
        }

        echo "Connection {$conn->resourceId} has disconnected\n";
    }

    public function onError(ConnectionInterface $conn, \Exception $e) {
        echo "An error has occurred: {$e->getMessage()}\n";

        $conn->close();
    }
}

这是需要解释的最复杂的文件。首先,有一个受保护的变量客户端,用于存储与该棘轮 Websocket 服务器建立的每个连接。它是在 onOpen 事件中创建的。

接下来,Web 浏览器客户端将在 onMessage 事件中注册自己以接收消息。我使用 JSON 协议完成此操作。一个例子是我使用的格式的代码,特别是我使用他们的 cookie 中的令牌来识别我的系统中的用户以及简单的注册消息。

我简单地在这个函数中查看数据库,看看是否有 authToken 与 cookie 一起使用。

如果数据库中的 Socket 表有写入 $from->resourceId

这是棘轮用来跟踪该特定连接编号的编号。

接下来,在 onClose 方法中请注意,我们必须确保在连接关闭时删除我们创建的条目,以便数据库不会填充不必要的额外数据。

最后注意 updateClient 函数是一个 symfony 事件,它是从我们之前做的 HtmlServer 触发的。

这就是实际将消息发送到客户端 Web 浏览器的内容。首先,如果用户打开了许多网络浏览器来创建不同的连接,我们会循环访问与该用户相关的所有已知套接字。 Doctrine 通过 $user->getSockets() 使这变得容易,你必须决定最好的方法来做到这一点。

然后您只需输入 $client->send($message) 即可将消息发送到 Web 浏览器。

Step 8:

最后在你的网络浏览器的 JavaScript 中添加类似这样的内容。

var hostname = window.location.hostname; //@JA - Doing it this way will make this work on DEV and LIVE Enviroments
    var conn = new WebSocket('ws://'+hostname+':8080');
    conn.onopen = function(e) {
        console.log("Connection established!");
        //@JA - Register with the server so it associates the connection ID to the supplied token
        conn.send('{"token":"'+$.cookie("ccdraftandpermit")+'","message":"register"}');
    };

    conn.onmessage = function(e) {
        //@JA - Update in realtime the busy and active status
        console.log(e.data)
        var obj = jQuery.parseJSON(e.data);
        if(obj.busy == "0"){
            $('.status').attr("status","free");
            $('.status').html("Free");
            $(".unbusy").css("display","none");
        }else{
            $('.status').attr("status","busy");
            $('.status').html("Busy");
            $(".unbusy").css("display","inline");
        }
        if(obj.active == "0"){
            $('.startbtn').attr("status","off");
            $('.startbtn').html("Start Taking Calls");
        }else{
            $('.startbtn').attr("status","on");
            $('.startbtn').html("Stop Taking Calls");
        }
    };

我的演示展示了使用 JSON 来回传递信息的简单方法。

Step 9:

为了从 PHP Web 服务器发送消息,我在辅助函数中做了类似的事情。

function h_sendWebsocketNotificationToUser($userid){
    //Send notification out to Websocket Server
    $ch = curl_init(); 
    curl_setopt($ch, CURLOPT_URL, "http://localhost/?command=updatestatus&userid=".$userid); 
    curl_setopt($ch, CURLOPT_PORT, 6677);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
    $output = curl_exec($ch); 
    curl_close($ch); 
}

这将尝试随时发送特定用户的 updateStatus 消息。

Step 10:

没有第10步你就完成了!好吧,不完全是......为了在后台运行网络服务器,我使用 Docker,这使得它很容易。只需使用以下命令执行网络服务器即可。

docker exec -itd draftandpermit_web_1 bash -c "cd /var/www/callcenter/livesite; php bin/startwebsocketserver.php"

或与您的情况相当的东西。这里的关键是我正在使用的 -d 选项,它在后台运行它。即使您再次运行该命令,它也不会生成两个实例,这很漂亮。关闭服务器超出了本文的范围,但如果您找到了一个好的方法来做到这一点,请修改或评论这个答案。

另外,不要忘记在 docker-compose 文件上正确打开端口。我为我的项目做了类似的事情。

ports: 
            - "80:80"
            - "8080:8080"
            - "6060:80"
            - "443:443"
            - "6677:6677"
            #This is used below to test on local machines, just portforward this on your router.
            - "8082:80"

请记住 8080 由 WebSockets 使用,因此它必须完全通过。

如果您对实体和数据库结构感到好奇,我在这里使用的是附图。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

棘轮存储用户连接并在服务器实例之外发送消息 的相关文章

  • Magento 中的子域 htaccess 问题

    public html www domain com public html subdomain subdomain domain com public html htaccess public html subdomain htacces
  • 如何检查号码是否是巴基斯坦用户的手机号码而不是固定电话号码

    我所做的是从开头删除 92 或 0092 并使用以下代码检查它是否是巴基斯坦人的有效手机号码 if preg match 3 0 4 0 9 number 1 Pakistani mobile number else not a pakis
  • 压缩 zend Framework 2 的 html 输出

    我目前正在 PHP 5 4 4 上使用 Zend Framework 2 beta 开发个人 web 应用程序以用于自学目的 我想知道是否可以在 html 输出发送到浏览器之前拦截它 以便通过删除所有不必要的空格来缩小它 我怎样才能在ZF2
  • 在会话 cookie 中存储大量数据会产生什么影响?

    谁能解释一下在会话中存储大量数据的缺点或给我指出一些阅读材料 我也很感兴趣在会话中存储数据和从数据文件读取数据之间是否有任何区别 如果您在会话中存储大量数据 则输入 输出性能会下降 因为会有大量读取 写入 默认情况下 PHP 中的会话存储在
  • 覆盖供应商自动加载编辑器

    有没有办法让您创建的自动加载文件在调用供应商自动加载之前运行 我们似乎遇到了 SimpleSAML 的自动加载覆盖我们创建的自动加载文件之一的问题 我是 Composer 的新手 似乎无法在网上找到任何解决方案 我尝试将我们的自动加载文件包
  • session_regenerate_id 没有创建新的会话 id

    我有一个脚本 旨在完成当前会话并开始新的会话 我使用了一段代码 它在我的开发计算机上运行良好 但是 当我将其发布到生产服务器时 会话 ID 始终保持不变 以下是我重新启动会话的代码 session start SESSION array P
  • Symfony2中如何获取所有post参数? [复制]

    这个问题在这里已经有答案了 我想获取a的所有post参数symfony http symfony com Form I used all parameter this gt get request gt getParameterHolder
  • Node.js 中的 PHP exit()/die() 等价物是什么

    什么是 PHP die http www php net manual de function die php http www php net manual de function die php 在 Node js 中等效吗 https
  • 创建即用型 symfony 2 应用程序 zip

    我创建了一个 symfomy 应用程序包 可用于从 Android 应用程序收集崩溃报告 对于那些对 Android 和 ACRA 感兴趣的人 https github com marvinlabs acra server https gi
  • Android GCM 服务器的 API 密钥

    我有点困惑我应该为 GCM 服务器使用哪个 API 密钥 在文档中它说使用 android api 密钥 这对我不起作用并且总是给出未经授权的 http developer android com google gcm gs html ht
  • 使用PHP套接字发送和接收数据

    我正在尝试通过 PHP 套接字发送和接收数据 一切正常 但是当我尝试发送数据时 PHP 不发送任何内容 Wireshark 告诉我发送的数据长度为 0 我正在使用这段代码
  • 使用 DOJO 自动完成文本框

    我正在寻找一种使用 DOJO 进行文本框自动建议的简单方法 我将查询的数据库表 使用 PHP 脚本 以 JSON 形式返回 有超过 100 000 条记录 因此这确实不应该采用 FilteringSelect 或 ComboBox 的形式
  • 如何用javascript正确读取php cookies

    考虑这个 php 和 javascript 代码 然后我在控制台中看到的是 utma 111872281 291759993 1444771465 1445374822 1445436904 4 utmz 111872281 1444771
  • 如何在 HTML / Javascript 页面中插入 PHP 下拉列表

    好吧 这是我的第二篇文章 请接受我是一个完全的新手 愿意学习 花了很多时间在各个网站上寻找答案 而且我几乎已经到达了我需要到达的地方 至少在这一点上 我有一个网页 其中有许多 javascript 函数 这些函数一起使用 google 地图
  • 如何在没有引用的情况下复制对象?

    PHP5 OOP 有据可查对象通过引用传递 http php net manual en language oop5 references php默认情况下 如果这是默认的 在我看来 有一种非默认的方式可以在没有参考的情况下进行复制 如何
  • Zend Framework Zend_Form 装饰器: 位于按钮元素内部?

    我有一个像这样创建的按钮元素 submit new Zend Form Element Button submit submit gt setLabel My Button submit gt setDecorators array Vie
  • 如何在 codeigniter 查询中使用 FIND_IN_SET?

    array array classesID gt 6 this gt db gt select gt from this gt table name gt where array gt order by this gt order by q
  • 为什么 PHP 中不允许“传统”类型提示?

    刚刚发现类型提示 http php net manual en language oop5 typehinting phpPHP 中允许 但不适用于整数 字符串 布尔值或浮点数 为什么 PHP 不允许对整数 字符串等类型进行类型提示 从 P
  • mysqli bind_param 中的 NULL 是什么类型?

    我正在尝试将参数绑定到 INSERT INTO MySQLi 准备好的语句 如果该变量存在 否则插入 null 然后我知道 type variable i corresponding variable has type integer d
  • PHP cURL 在本地工作,在 AWS 服务器上出现错误 77

    最新更新 脚本作为管理员用户通过 SSH shell 作为 php script php 成功运行 当由 nginx 用户运行时 curl 命令无法执行 https 请求 所以我猜测这是nginx用户无法正确使用curl的问题 我已经检查了

随机推荐