Построение real-time коммуникации с PHP
Методы организации чата на PHP
Создание чата требует обработки событий в реальном времени. PHP, традиционно работающий по схеме запрос-ответ, может быть адаптирован для этой задачи с помощью различных технологий. Ниже рассматриваются несколько подходов, от простого опроса до WebSocket.
Как сделать чат с помощью периодических AJAX-запросов?
Простой метод: клиент каждые N секунд отправляет запрос к PHP-скрипту, который возвращает новые сообщения. Сервер проверяет время последнего запроса и отдает данные.
// server.php
session_start();
$lastId = $_GET['lastId'] ?? 0;
$messages = getMessagesSince($lastId);
echo json_encode($messages);создать чат php (создание чата на php)
На клиенте: setInterval(() => fetch(`server.php?lastId=${lastId}`).then(r => r.json()).then(update), 2000);
Проблемы:
- Высокая нагрузка на сервер при большом числе пользователей.
- Задержка до 2 секунд.
- Лишний трафик.
Каким образом реализовать чат без постоянных запросов, но с ожиданием ответа?
Long Polling: клиент отправляет запрос, сервер держит соединение открытым до появления нового сообщения или таймаута. После ответа клиент сразу отправляет новый запрос.
// longpoll.php
$start = time();
while (time() - $start < 30) {
$new = checkNewMessages();
if ($new) {
echo json_encode($new);
exit;
}
usleep(500000); // 0.5 sec
}
echo json_encode([]);
Проблемы: ресурсы на удержание соединений, возможные таймауты.
Как применить Server-Sent Events для чата на PHP?
SSE позволяет серверу отправлять данные клиенту через одно долгое соединение. PHP-скрипт устанавливает заголовки и в цикле отправляет события.
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
$lastId = $_SERVER['HTTP_LAST_EVENT_ID'] ?? 0;
while (true) {
$msgs = getNewMessages($lastId);
if ($msgs) {
foreach ($msgs as $m) {
echo "id: {$m['id']}\n";
echo "data: " . json_encode($m) . "\n\n";
$lastId = $m['id'];
}
}
ob_flush(); flush();
sleep(1);
}
Проблемы: поддержка только односторонней связи, ограничение на количество одновременных соединений.
Каким образом создать двухсторонний чат в реальном времени на PHP с помощью WebSocket?
WebSocket обеспечивает постоянное двустороннее соединение. Для PHP используется библиотека Ratchet. Установка через Composer: composer require cboden/ratchet.
Создаем класс чата:
// Chat.php
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class Chat implements MessageComponentInterface {
protected $clients;
public function __construct() {
$this->clients = new \SplObjectStorage;
}
public function onOpen(ConnectionInterface $conn) {
$this->clients->attach($conn);
echo "New connection\n";
}
public function onMessage(ConnectionInterface $from, $msg) {
foreach ($this->clients as $client) {
if ($from !== $client) {
$client->send($msg);
}
}
}
public function onClose(ConnectionInterface $conn) {
$this->clients->detach($conn);
echo "Closed\n";
}
public function onError(ConnectionInterface $conn, \Exception $e) {
$conn->close();
echo $e->getMessage();
}
}
Запускаем сервер:
// server.php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
require 'vendor/autoload.php';
$server = IoServer::factory(
new HttpServer(
new WsServer(
new Chat()
)
),
8080
);
$server->run();
Клиентский JavaScript:
// client.js
const ws = new WebSocket('ws://localhost:8080');
ws.onmessage = function(e) {
// отобразить сообщение
};
function sendMessage(text) {
ws.send(text);
}
Пояснения: WebSocket требует отдельного процесса (демона). На сервере должен быть открыт порт 8080, возможны проблемы с брандмауэром или хостингом, поддерживающим только CGI.
Типичные ошибки и их устранение:
- Ошибка соединения (1006) - часто из-за блокировки порта фаерволом или неправильного пути WebSocket.
- Сервер не отвечает - убедиться, что процесс Ratchet запущен (php server.php).
- Проблемы с SSL - для wss требуется SSL-сертификат, настройка через ReactPHP.
Расширенные примеры реализации чата на PHP с WebSocket
Пример 1: Сохранение сообщений в базу данных MySQL
// ChatDB.php
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use PDO;
class ChatDB implements MessageComponentInterface {
private $clients;
private $db;
public function __construct() {
$this->clients = new \SplObjectStorage;
$this->db = new PDO('mysql:host=localhost;dbname=chat', 'user', 'pass');
}
public function onMessage(ConnectionInterface $from, $msg) {
$data = json_decode($msg, true);
// сохраняем
$stmt = $this->db->prepare("INSERT INTO messages (user, text, time) VALUES (?, ?, NOW())");
$stmt->execute([$data['user'], $data['text']]);
// рассылаем всем
foreach ($this->clients as $client) {
$client->send($msg);
}
}
// ... остальные методы
}
Пример 2: Авторизация через токен при открытии соединения
// В onOpen проверяем GET-параметр token
public function onOpen(ConnectionInterface $conn) {
$query = $conn->httpRequest->getUri()->getQuery();
parse_str($query, $params);
$token = $params['token'] ?? '';
if (!validateToken($token)) {
$conn->close();
return;
}
$this->clients->attach($conn);
}
Клиент подключается: new WebSocket('ws://localhost:8080?token=secret')
Пример 3: Отправка изображений через base64
// Клиент отправляет JSON: {"type":"image","data":"data:image/png;base64,..."}
// Сервер просто ретранслирует всем, либо сохраняет файл
$data = json_decode($msg, true);
if ($data['type'] === 'image') {
// можно сохранить файл
$file = 'uploads/' . uniqid() . '.png';
file_put_contents($file, base64_decode(explode(',', $data['data'])[1]));
$data['url'] = $file;
$msg = json_encode($data);
}
// рассылка
Результат: в чате появляется ссылка на изображение или если отображать сразу через data URI, то прямо в сообщении.
Пример 4: Ограничение истории (последние 50 сообщений)
// При подключении нового клиента отправляем ему последние 50 сообщений из БД
public function onOpen(ConnectionInterface $conn) {
// ... авторизация
$stmt = $this->db->query("SELECT * FROM messages ORDER BY id DESC LIMIT 50");
$history = $stmt->fetchAll(PDO::FETCH_ASSOC);
$conn->send(json_encode(['type' => 'history', 'messages' => array_reverse($history)]));
$this->clients->attach($conn);
}
Пример 5: Использование ReactPHP для масштабирования (неблокирующий ввод-вывод)
// Замена IoServer на ReactPHP event loop
$loop = React\EventLoop\Factory::create();
$webSock = new React\Socket\Server('0.0.0.0:8080', $loop);
$webServer = new Ratchet\Server\IoServer(
new Ratchet\Http\HttpServer(
new Ratchet\WebSocket\WsServer(
new Chat()
)
),
$webSock
);
$loop->run();
Результат: более высокая производительность, возможность интегрировать другие асинхронные задачи.