有没有一种干净的方法来使用 symfony 2.6 设置 websocket

Posted

技术标签:

【中文标题】有没有一种干净的方法来使用 symfony 2.6 设置 websocket【英文标题】:Is there a clean way to setup websocket with symfony 2.6 【发布时间】:2015-05-17 17:45:27 【问题描述】:

我使用 symfony 2 开发了一个网站。 直到现在它还没有使用 ajax。当您发布新评论时,页面已刷新。

我添加了一个 ajax 层,因此表单无需刷新即可提交。

每次我发布新帖子时,我都希望所有允许在线查看该帖子的用户都能在他们的时间线中收到该帖子。

这就是我现在所做的:

我创建了一个事件 NewPostAdded 和一个订阅者渲染到 html 帖子(希望我将它发送给客户端并且客户端将 $('.timeline').prepend(post) )

我正在寻找一种方法来实现将启动 websocket 服务器的 symfony2 命令。我的订阅者将能够将帖子+数据(是否公开?否则允许查看的用户 ID 列表)推送到该服务器。然后,该服务器将拥有一个在线用户列表,如果该帖子是公开的,它将推送给其他所有人,它会将其推送给正确的在线用户。

我在使用 symfony 2.1 + redis + nodejs + socket.io 之前已经这样做了,但这显然是不可维护的,而且由于这个网站不是针对大受众的,所以我想保持简单。

这是我的 2 个问题:

是否有一个捆绑包让您觉得简单,提供了一种多合一的简单方法来编写具有收入事件侦听器(接收来自 symfony 的帖子)和事件发送者(将帖子发送给用户)的服务器,添加事件侦听器客户端(准备使用资产添加以便能够编写客户端代码)对服务器消息做出反应?

有没有办法在客户端使用除用户 ID 之外的其他东西来“验证”websocket 服务器上的用户,以避免用户更改客户端代码中的 ID 以接收他们不应该查看的帖子?

【问题讨论】:

查看事件源(sse),从“裸”php 中使用它比使用套接字更容易,并且可以使用与 http 相同的基本例程随时推出。它无法接收,但由于这个限制,实现起来非常简单,而且您可以随时使用 ajax 从客户端发送... 【参考方案1】:

是的,您可以使用 php web socket Ratchet "http://socketo.me/",您可以将其用作 composer 包https://packagist.org/packages/cboden/ratchet

你应该创建一个服务应用程序,然后你应该创建一个控制台命令来运行套接字应用程序。

您应该生成一个秘密令牌,而不是 userId,例如 userId 的 md5 哈希 + 一些用于检查套接字连接是否可信的密钥。

更新:

composer.json

...
"cboden/ratchet": "0.3.*",
...

然后你应该创建一个新的包,比如说“WebSocketBundle”

    创建服务应用程序 WebSocketBundle/WebSocket/WebSocketApplication.php

    namespace MyApp\WebSocketBundle\WebSocket;

    use Ratchet\ConnectionInterface;
    use Ratchet\MessageComponentInterface;

    class WebSocketApplication implements MessageComponentInterface
    
        protected $container;
        protected $clients;
        protected $redis;

        public function __construct($container)
        
            $this->clients = [];
            $this->container = $container;
            $this->redis = $container->get('snc_redis.something'); // your redis service
        

        public function onMessage(ConnectionInterface $from, $msg)
        
            $messageData = $this->decodeJSONAndCheckMessage($from, $msg);

            // here you must pass a token in messageData and implement own function to check is the token valid 
            $loginResult = $this->userLogin($from, $messageData);

            if ($loginResult instanceof Success)  // my custom success class message
                $this->handleMessage($from, $messageData);
             else 
                $this->onError($from, new \Exception('Cannot login a user.'));
            
        

        // some strategy wrapper
        private function handleMessage($from, $messageData)
        
            $message = $messageData->message;
            if (method_exists($this, $message)) 
                try 
                    $this->$message($from, $messageData);
                 catch (Exception $ex) 
                    $this->onError($from, $ex);
                
             else 
                $this->onError($from, new \Exception(sprintf('Unknown method "%s"', $message)));
            
        

        // you can use here post new message action
        private function eventStartSomething($from, $messageData)
        
            if (!$messageData->somethingId) 
                $this->onError($from, new \Exception('Bad parameters'));
                return;
            
            $scope = [];
            $scope['clients'][$from->resourceId] = null;

            // I need socket for something only limited amount of time, you can implement here own logic
            $this->redis->setex($messageData->somethingId, 600, serialize($scope));

            $this->clients[$from->resourceId]['scope'] = $messageData->eventId;
            $this->logMessage($from, 'started new something with Id: ' . $messageData->somethingId);
            $from->send($this->getResultOKMessage());
        

        private function eventGetSomething($from, $messageData)
        
            $scopeKey = $this->redis->get($messageData->somethingId);
            if (!$scopeKey) 
                $this->onError($from, new \Exception('Bad or expired something ' . $messageData->somethingId));
                return;
            
            if (!$this->checkForClientInScope($from->resourceId, $messageData->eventId)) 
                if ($this->assignClientToScope($from->resourceId, $messageData->eventId)) 
                    $this->sendMessageToScope($from, $messageData->eventId, $this->getScopeWatchMessage($from, $messageData->eventId));
                    $from->send($this->getScopeWatchMessage($from, $messageData->eventId));
                
            
        

        private function assignClientToScope($clienResourseId, $scopeId)
        
            $result = false;
            $scopeKey = $this->redis->get($scopeId);
            if (!$scopeKey) 
                return $result;
            
            $scope = unserialize($scopeKey);
            if (!array_key_exists($clienResourseId, $scope['clients'])) 

            // I need socket for something only limited amount of time, you can implement here own logic
                $this->redis->setex($scopeId, 600, serialize($scope));

                if (array_key_exists($clienResourseId, $this->clients)) 
                    $this->clients[$clienResourseId]['scope'] = $scopeId;
                
                $result = true;
            
            return $result;
        

        private function sendMessageToScope($from, $scopeId, $message)
        
            $scopeKey = $this->redis->get($scopeId);
            if (!$scopeKey) 
                $this->onError($from, new \Exception('Bad or expired event ' . $scopeId . ' for sending message'));
                return;
            
            $scope = unserialize($scopeKey);

            foreach ($scope['clients'] as $clientResourceId => $remoteAddress) 
                if (array_key_exists($clientResourceId, $this->clients) &&
                        $this->clients[$clientResourceId]['connection'] != $from) 
                    $this->clients[$clientResourceId]['connection']->send($message);
                
            
        

        public function onClose(ConnectionInterface $conn)
        
            if (isset($this->clients[$conn->resourceId]['scope'])) 
                $scopeId = $this->clients[$conn->resourceId]['scope'];
                $this->removeClientFromScope($conn->resourceId);
                $this->sendMessageToScope($conn, $scopeId, $this->getScopeWatchMessage($conn, $scopeId));
            
            unset($this->clients[$conn->resourceId]);
            $this->logMessage($conn, 'Connection closed.');
        

        public function onError(ConnectionInterface $conn, \Exception $e)
        
            echo date("Y-m-d H:i:s") . ":" . "WebSocket error::" . $e->getMessage() . " resourceId:" . $conn->resourceId . ". remoteAddress:" . $conn->remoteAddress . "\n";
            $conn->send($this->getErrorMessage($e->getMessage()));
        

        public function onOpen(ConnectionInterface $conn)
        
            $this->clients[$conn->resourceId]['connection'] = $conn;
            $this->logMessage($conn, 'New connection.');
        
    
    控制台命令:

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
use Ratchet\Server\ioserver;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use Snc\RedisBundle\Session\Storage\Handler\RedisSessionHandler;
use Ratchet\Session\SessionProvider;
use MyApp\WebSocketBundle\WebSocket\WebSocketApplication;

class ListenCommand extends ContainerAwareCommand

    protected function configure()
    
        $this->setName('myapp:websocket:listen')
            ->setDescription('Listen for websocket requests ')
            ->addOption('port', 'p', InputOption::VALUE_REQUIRED, 'The port to listen on', 8000)
            ->addOption('interface', 'i', InputOption::VALUE_REQUIRED, 'The interface to listen on', '0.0.0.0');
    

    protected function execute(InputInterface $input, OutputInterface $output)
    
        $redis = $this->getContainer()->get('snc_redis.default');
        $application = new WebSocketApplication($this->getContainer());
        $server = IoServer::factory(
            new HttpServer(
                new WsServer(
                    new SessionProvider(
                        $application,
                        new RedisSessionHandler($redis)
                    )
                )
            ),
            $input->getOption('port'),
            $input->getOption('interface')
        );
        echo "Listening on: ".$input->getOption('interface').":".$input->getOption('port')."\n";
        $server->run();

    

【讨论】:

所以我可以通过将它添加到我的 composer.json 中来在 symfony2 中使用这个包? 你有任何在 symfony2 中实现的例子吗? 是的,但不能在这里发布完整的代码案例保密协议,但会尝试向您展示一些关键点 太好了,我现在就试试这个!感谢您的帮助

以上是关于有没有一种干净的方法来使用 symfony 2.6 设置 websocket的主要内容,如果未能解决你的问题,请参考以下文章

Symfony 2:依赖注入和特征

Symfony2 在 YAML 配置中使用 PHP 类常量?

哪里可以找到 Symfony2 干净的骨架?

有没有一种简单的方法来使用 LDAP 配置 Docker Private Registry 2.0?

有没有一种有效的方法来使用正则表达式从 HTML 字符串中提取数据? [复制]

有没有一种很好的跨平台方法来使用 CMake 包含外部 C++ 库?