Нужен чат на 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”
А как в консоли остановить сервер?
сейчас уже точно не вспомню, но насколько помню, то запущенный сервер висит запущенный (как приложение) и принимает коннекты. Чтобы его остановить — достаточно это приложение закрыть, например с помощью CTRL+C.
как запустить в фоновом режиме? если я закрываю консоль, то соединение останавливается.
Да, внизу написан один из способов, где вместо $path_to_yii (либо в эту переменную) указать путь до приложения yii. Есть способ передать ключ —daemon, но не помню точно в какой из реализаций такой вариант работает.
[…] Чат созданный в предыдущей статье необходимо было перенести на сервак. Проблема возникла когда код начали переносить на сервак с VDS на Linux. То ли VDS настроена так себе, то ли ещё чего, но проблема как оказалось такая, что скрипт запущенный из консоли и через web использовали разные папки для сессий. Допускаю, что существуют решения более грамотные, но в данном случае решение такое: […]