Чат на websocket в Yii2 | Websocket Yii2 Chat

Нужен чат на PHP, но чтобы был в реальном времени, реализация может быть разными технологиями, мне нужно было на websocket.

Сначала я использовал consik/yii2-websocket, но поскольку этот пакет использует cboden/ratchet, то решил использовать его (причина банальна, он в случае ошибок, ошибки выкидывает прямо в консоль, чего не мог предыдущий товарищ). Кроме того, если сначала поставить consik, то перед установкой cboden/ratchet необходимо consik удалить.

Зато у consik/yii2-websocket достаточно хороший рабочий пример. Итак: для начала создадим модель для работы с чатом и разместим её например daemons\MyChat.php

use app\models\ChatClient;
use app\models\ChatSupport;
use app\models\User;
use Ratchet\ConnectionInterface;
use Ratchet\MessageComponentInterface;
use yii\helpers\Console;

class MyChat implements MessageComponentInterface {
   protected $clients;

   public function __construct() {
      $this->clients = new \SplObjectStorage();
   }

   public function onOpen( ConnectionInterface $conn) {
      Console::stdout(Connection Open\n);
      //Для пользователей можно создать класс, в котором будем хранить информацию по клиентах, который будем добавлять к клиентам чата
      $user_info = new ChatClient();
      $user_info->connection = $conn;
      $this->clients->attach($user_info);
   }

   public function onMessage( ConnectionInterface $from, $msg ) {
      // Находим текущего пользователя
      $current_client = $this->findConnection($from);

      $session = \Yii::$app->session;
      $request = json_decode($msg, true);
      if (isset($request['session_id'])) {
         // Определяем по сессии текущего пользователя, для того, что это возможно было сделать
         // необходимо в экшене при открытии страницы с чатом передать в сессию ID текущего пользователя,
         // например в поле user_id, кроме этого в config/console.php необходимо включить компонент session
         $session->id = $request['session_id'];
         $user_id = $session->get('user_id');
         Console::stdout('user_id: '.$user_id.\n);
      } else {
         $request['message'] = 'Обновите страницу';
      }

      $result = ['message' => ''];
      // Выводит любые сообщение, которые получает сервер
      Console::stdout(Message: $msg\n);

      switch ($request['action']) {
         // Здесь можно фильтровать сообщения, для этого в отправке сообщений от пользователя необходимо указывать
         // action и отправлять нужно переводя в строку JSON
         case 'support':
            if ( ! empty( $request['message'] ) && $message = trim( $request['message'] ) ) {
               $support  = isset( $request['support'] ) ? $request['support'] : false;
               $receiver = isset( $request['receiver'] ) ? $request['receiver'] : null;

               // Здесь можно сохранять сообщения в БД
               $saved_message = ChatSupport::saveMessage( $message, $current_client->id, $receiver, $support );

               $message_model = $saved_message['model'];

               if ($message_model !== null) {

                  foreach ( $this->clients as $client ) {

                  //Здесь можно делать фильтр, чтобы не отправлять сообщение всем, а конкретным пользователям
      //          if ($from!= $client) {
      //             $client->send($msg);
      //          }

                     $client->connection->send( json_encode( [
                        'type'    => 'support',
                        'from'    => $current_client->name,
                        'date'    => date( 'H:i', $message_model->created_at ), // Стоит обратить внимание, что дата здесь не отформатирована с учетом timezone
                        'message' => $message,
                        'photo'   => $current_client->photo,
                        'support' => $support,
                     ] ) );
                  }
               } else {
                  // Выводит сообщение если при сохранении возникли ошибки
                  $result['message'] = $saved_message['error'];
               }
            } else {
               // В случае если сообщение пустое
               $result['message'] = 'Enter message';
            }

            break;
         case 'register':
            // Заполнение данных пользователя при подключении пользователя на основе сессии
            $client          = $this->findConnection( $from );
            $client->id      = $user_id;
            $user = User::findOne($user_id);
            if ($user !== null) {
               $client->name = $user->getNameForChat();
               $client->model = $user;
               $client->photo = $user->image;
            }
            $client->session = $request['session_id'];
            break;
      }

      $from->send(json_encode($result));
      $session->close();

   }

   public function onClose( ConnectionInterface $conn ) {
      Console::stdout(Connection Close\n);
      $client = $this->findConnection($conn);
      if ($client !== null) {
         $client_id = $client->id;
         Console::stdout(Connection $client_id Detached\n);
         $this->clients->detach($client);
      }

   }

   public function onError( ConnectionInterface $conn, \Exception $e ) {
      Console::stdout(Connection Error $e\n);
      $conn->close();
   }

   public function findConnection( ConnectionInterface $conn ) {
      // Ищем пользователя, который написал
      foreach ($this->clients as $client) {
         if ($client->connection === $conn) {
            return $client;
         }
      }
      return null;
   }
}

Далее добавить контроллер в консоль commands/ChatController.php

class ChatController extends yii\console\Controller {

   public function actionStart() {
      $app = new App('localhost', 8080);
      $app->route('/chatonline', new MyChat(), ['*']);
      $app->run();
   }

}

И запустить из консоли yii chat/start

Теперь из браузера через JS создаем соединение

function message_template(name, message, time, photo, isAuthor = false) {
    var ret = <div class='message'>message</div>;
}

function scrollBottom(el, duration = 500) {
    var top = el.prop(scrollHeight);
    el.animate({
        scrollTop: top
    }, duration);
}
var chat = null; // объявлять чат нужно здесь, чтобы можно было создавать новый объект чата при переподключении
function start(websocketServerLocation) {
    chat = new WebSocket(websocketServerLocation);
    var message_text = '';

    chat.onmessage = function(e) {
        $('#response').text('');

        var response = JSON.parse(e.data);
        if (response.type && response.type == 'support') {
            var chat_div = $('#chat');
            message_text = message_template(response.from, response.message, response.date, response.photo, response.support);
            chat_div.append(message_text);
            scrollBottom(chat_div, 500)
        } else if (response.message) {
            $('#response').text(response.message);
        }
    };

    chat.onerror = function(e) {
        // Закрываем соединение при ошибке и сообщаем об этом пользователю
        $('#response').text('Возникла ошибка.');
        chat.close();
    };

    chat.onclose = function() {
        // Если сервер перезагрузили или пропал интернет, переподключаемся при закрытии соединения
        $('#response').text('Попытка подключения...');
        setTimeout(function() {
            start(websocketServerLocation)
        }, 5000);
    };

    chat.onopen = function(e) {
        chat.send( JSON.stringify({'action' : 'register', 'session_id':'$session_id'}) );
        $('#response').text(Вы онлайн);
    };

    $('#btnSend').on('click', function() {
        var message_input = $('#message');
        if (message_input.val()) {
            var message_send = JSON.stringify({
                'action' : 'support',
                'message' : message_input.val(),
                'receiver' : '$receiver',
                'session_id': '$session_id',
                'support': true
            });
            if (chat.readyState === chat.OPEN) {
                // Если соедение не открыто
                chat.send(message_send);
            } else {
                console.warn('WS NOT CONNECTED');
            }
            message_input.val('');
        } else {
            $('#response').text('Введите сообщение');
        }
    });

    $('#message').on('keypress', function(event) {
        // отправка при нажатии CTRL+Enter
        if ((event.ctrlKey) && ((event.keyCode == 0xA) || event.keyCode == 0xD)) {
            $('#btnSend').click();
        }
    });
}

var websocketServerLocation = 'ws://localhost:8080/chatonline'; //название экшена должен совпадать с route в запускаемом через консоль экшене
start(websocketServerLocation);

scrollBottom($('#chat'));

Можно запустить в фоновом режиме

#!/bin/bash

nohup php $path_to_yii chat/start > /tmp/chat.log 2>&1 &
echo $! > save_pid.txt

5 Replies to “Чат на websocket в Yii2 | Websocket Yii2 Chat”

  • […] Чат созданный в предыдущей статье необходимо было перенести на сервак. Проблема возникла когда код начали переносить на сервак с VDS на Linux. То ли VDS настроена так себе, то ли ещё чего, но проблема как оказалось такая, что скрипт запущенный из консоли и через web использовали разные папки для сессий. Допускаю, что существуют решения более грамотные, но в данном случае решение такое: […]

  • Leave a comment

    Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.