Ratchet PHP WAMP-React/ZeroMQ-特定用户广播

Ratchet PHP WAMP-React/ZeroMQ-特定用户广播,php,websocket,zeromq,ratchet,reactphp,Php,Websocket,Zeromq,Ratchet,Reactphp,注意:这与使用消息组件界面不同。我使用的是WampServerInterface,所以这个问题与该部分相关。我需要一个带有代码示例和解释的答案,因为我可以看到这对将来的其他人有帮助 尝试对单个用户进行循环推送 我正在使用Ratchet和ZeroMQ的WAMP部分,目前我有一个工作版本的 我正在尝试执行以下操作: zeromq服务器已启动并正在运行,准备记录订阅者和取消订阅者 用户通过websocket协议在浏览器中进行连接 启动一个循环,将数据发送给请求数据的特定用户 当用户断开连接时,该用户

注意:这与使用
消息组件界面
不同。我使用的是
WampServerInterface
,所以这个问题与该部分相关。我需要一个带有代码示例和解释的答案,因为我可以看到这对将来的其他人有帮助

尝试对单个用户进行循环推送 我正在使用Ratchet和ZeroMQ的WAMP部分,目前我有一个工作版本的

我正在尝试执行以下操作:

  • zeromq服务器已启动并正在运行,准备记录订阅者和取消订阅者
  • 用户通过websocket协议在浏览器中进行连接
  • 启动一个循环,将数据发送给请求数据的特定用户
  • 当用户断开连接时,该用户数据的循环停止
我有第(1)点和第(2)点,但我的问题是第三点:

首先:我如何才能只向每个特定用户发送数据?广播将数据发送给每个人,除非“主题”最终可能是单个用户ID

其次:我有一个很大的安全问题。如果我正在从客户端发送想要订阅的用户ID,这似乎是我需要的,那么用户可以将变量更改为另一个用户的ID,并返回他们的数据

第三:我必须运行一个单独的php脚本,其中包含zeromq的代码,以启动实际的循环。我不确定这是否是最好的方法,我宁愿让它完全在代码库中工作,而不是单独的php文件。这是我需要整理的一个主要领域

下面的代码显示了我当前拥有的内容

仅从控制台运行的服务器 我直接键入
php-bin/push-server.php
来运行它。出于调试目的,订阅和取消订阅将输出到此终端

$loop   = React\EventLoop\Factory::create();
$pusher = Pusher;

$context = new React\ZMQ\Context($loop);
$pull = $context->getSocket(ZMQ::SOCKET_PULL);
$pull->bind('tcp://127.0.0.1:5555');
$pull->on('message', array($pusher, 'onMessage'));

$webSock = new React\Socket\Server($loop);
$webSock->listen(8080, '0.0.0.0'); // Binding to 0.0.0.0 means remotes can connect
$webServer = new Ratchet\Server\IoServer(
    new Ratchet\WebSocket\WsServer(
        new Ratchet\Wamp\WampServer(
            $pusher
        )
    ),
    $webSock
);

$loop->run();
通过WebSocket发送数据的推进器 我省略了一些无用的东西,集中讨论了
onMessage()
onSubscribe()
方法

public function onSubscribe(ConnectionInterface $conn, $topic) 
{
    $subject = $topic->getId();
    $ip = $conn->remoteAddress;

    if (!array_key_exists($subject, $this->subscribedTopics)) 
    {
        $this->subscribedTopics[$subject] = $topic;
    }

    $this->clients[] = $conn->resourceId;

    echo sprintf("New Connection: %s" . PHP_EOL, $conn->remoteAddress);
}

public function onMessage($entry) {
    $entryData = json_decode($entry, true);

    var_dump($entryData);

    if (!array_key_exists($entryData['topic'], $this->subscribedTopics)) {
        return;
    }

    $topic = $this->subscribedTopics[$entryData['topic']];

    // This sends out everything to multiple users, not what I want!!
    // I can't send() to individual connections from here I don't think :S
    $topic->broadcast($entryData);
}
在循环中开始使用上述Pusher代码的脚本 这是我的问题-这是一个单独的php文件,希望将来可以集成到其他代码中,但目前我不确定如何正确使用它。我是否从会话中获取用户ID?我仍然需要从客户端发送它

// Thought sessions might work here but they don't work for subscription
session_start();
$userId = $_SESSION['userId'];

$loop   = React\EventLoop\Factory::create();

$context = new ZMQContext();
$socket = $context->getSocket(ZMQ::SOCKET_PUSH, 'my pusher');
$socket->connect("tcp://localhost:5555");

$i = 0;
$loop->addPeriodicTimer(4, function() use ($socket, $loop, $userId, &$i) {

   $entryData = array(
       'topic'     => 'subscriptionTopicHere',
       'userId'    => $userId
    );
    $i++;

    // So it doesn't go on infinitely if run from browser
    if ($i >= 3)
    {
        $loop->stop();
    }

    // Send stuff to the queue
    $socket->send(json_encode($entryData));
});
最后是要订阅的客户端js 结论 上述方法可行,但我确实需要弄清楚以下几点:

  • 如何向单个用户发送单个消息?当他们访问在JS中启动websocket连接的页面时,我是否也应该启动在PHP(zeromq)中将内容推入队列的脚本?这就是我现在手动做的,感觉不对

  • 当从JS订阅用户时,从会话中获取用户id并从客户端发送该id是不安全的。这可能是伪造的。请告诉我有一个更简单的方法,如果有,怎么做


要发送给特定用户,您需要路由器-经销商模式,而不是发布-订阅模式。本指南第3章对此进行了解释。如果您使用的是ZMQ v4.0,那么安全性是在有线级别处理的,因此在应用程序中看不到它。它仍然需要一些工作,除非您使用CZMQ绑定,它提供了一个身份验证框架(zauth)

基本上,要进行身份验证,需要在上安装一个处理程序inproc://zeromq.zap.01,并通过该套接字响应请求。用于RFC的Google ZeroMQ ZAP;核心libzmq/tests/test_security_curve.cpp程序中还有一个测试用例

注意:我在这里的回答不包括对ZeroMQ的引用,因为我不再使用它了。但是,如果需要的话,我相信您将能够找到如何使用ZeroMQ和这个答案

使用JSON 首先也是最重要的是,and声明要订阅的主题必须是字符串。我在这里有点作弊,但我仍然遵守规范:我将通过JSON

{
    "topic": "subject here",
    "userId": "1",
    "token": "dsah9273bui3f92h3r83f82h3"
}
JSON仍然是一个字符串,但它允许我传递更多数据来代替“主题”,PHP在另一端执行
JSON\u decode()
也很简单。当然,您应该验证您是否确实收到了JSON,但这取决于您的实现

那么我在这里经过什么,为什么?
  • 主题
主题是用户订阅的主题。您可以使用它来决定将哪些数据传递回用户

  • 用户ID
显然是用户的ID。必须使用下一部分验证此用户是否存在并允许其订阅:

  • 代币
这应该是一个随机生成的标记,在PHP中生成,并传递给JavaScript变量。当我说“一次使用”时,我的意思是每次重新加载页面时(以及扩展为每次HTTP请求时),JavaScript变量中都应该有一个新的令牌。此令牌应根据用户ID存储在数据库中

然后,一旦发出websocket请求,您就将令牌和用户id与数据库中的令牌和用户id进行匹配,以确保用户确实是他们所说的用户,并且他们没有与JS变量混淆

注意:在事件处理程序中,您可以使用
$conn->remoteAddress
获取连接的IP,因此如果有人试图恶意连接,您可以阻止他们(记录他们或其他)

为什么这样做有效? 它之所以有效,是因为每次通过新连接时,唯一令牌确保任何用户都无法访问任何其他人的订阅数据

服务器 下面是我用来运行循环和事件处理程序的内容。我正在创建循环,做所有的装饰
{
    "topic": "subject here",
    "userId": "1",
    "token": "dsah9273bui3f92h3r83f82h3"
}
$loop = Factory::create();

new IoServer(
    new WsServer(
        new WampServer(
            new EventHandler($loop) // This is my class. Pass in the loop!
        )
    ),
    $webSock
);

$loop->run();
class EventHandler implements WampServerInterface, MessageComponentInterface
{
    /**
     * @var \React\EventLoop\LoopInterface
     */
    private $loop;

    /**
     * @var array List of connected clients
     */
    private $clients;

    /**
     * Pass in the react event loop here
     */
    public function __construct(LoopInterface $loop)
    {
        $this->loop = $loop;
    }

    /**
     * A user connects, we store the connection by the unique resource id
     */
    public function onOpen(ConnectionInterface $conn)
    {
        $this->clients[$conn->resourceId]['conn'] = $conn;
    }

    /**
     * A user subscribes. The JSON is in $subscription->getId()
     */
    public function onSubscribe(ConnectionInterface $conn, $subscription)
    {
        // This is the JSON passed in from your JavaScript
        // Obviously you need to validate it's JSON and expected data etc...
        $data = json_decode(subscription->getId());
        
        // Validate the users id and token together against the db values
        
        // Now, let's subscribe this user only
        // 5 = the interval, in seconds
        $timer = $this->loop->addPeriodicTimer(5, function() use ($subscription) {
            $data = "whatever data you want to broadcast";
            return $subscription->broadcast(json_encode($data));
        });

        // Store the timer against that user's connection resource Id
        $this->clients[$conn->resourceId]['timer'] = $timer;
    }

    public function onClose(ConnectionInterface $conn)
    {
        // There might be a connection without a timer
        // So make sure there is one before trying to cancel it!
        if (isset($this->clients[$conn->resourceId]['timer']))
        {
            if ($this->clients[$conn->resourceId]['timer'] instanceof TimerInterface)
            {
                $this->loop->cancelTimer($this->clients[$conn->resourceId]['timer']);
            }
        }
    
        unset($this->clients[$conn->resourceId]);
    }

    /** Implement all the extra methods the interfaces say that you must use **/
}