rzaripov.kz

Простой чат на PHP с WebSocket и MySQL

Небольшой веб-чат — удачный учебный проект для понимания постоянных соединений, обмена событиями и работы с базой данных. В отличие от обычного HTTP-запроса, WebSocket позволяет серверу отправлять новые сообщения браузеру сразу после их появления, без постоянного обновления страницы и частого опроса API.

Для такой системы достаточно PHP-приложения, WebSocket-сервера, MySQL и небольшого JavaScript-клиента. PHP отвечает за авторизацию и сохранение данных, отдельный процесс поддерживает активные соединения, а браузер отображает историю и новые сообщения в режиме реального времени.

Архитектура приложения

Клиент сначала получает страницу чата через обычный HTTP-запрос. Затем JavaScript открывает WebSocket-соединение с сервером. После успешного подключения браузер может отправлять события авторизации, публикации сообщения и выхода из комнаты. Сервер принимает эти события, проверяет данные и рассылает результат нужным участникам.

MySQL используется как постоянное хранилище. В базу записываются пользователи, комнаты и сообщения, чтобы история не исчезала после перезапуска процесса. Сам WebSocket-сервер хранит только текущие соединения и временные сведения: соответствие идентификатора пользователя открытому сокету, список участников комнаты и состояние подключения.

Практичная схема состоит из нескольких частей:

Для первого прототипа можно использовать библиотеку Ratchet. Она предоставляет готовые классы для обработки открытия, закрытия и получения WebSocket-соединений, поэтому разработчику не приходится вручную реализовывать протокол и его служебные кадры.

Подготовка PHP-окружения

Проект удобно создать через Composer. После установки PHP и MySQL выполняется команда composer require cboden/ratchet. В отдельном файле конфигурации хранятся параметры подключения к базе, адрес сервера и секреты приложения. Пароли и ключи не следует помещать непосредственно в исходный код, особенно если проект размещён в GitHub.

Соединение с MySQL лучше открывать через PDO. Подготовленные выражения защищают запросы от SQL-инъекций, а режим исключений помогает не скрывать ошибки базы данных. Для чата важно установить кодировку utf8mb4, поскольку пользователи могут отправлять эмодзи, символы разных языков и специальные знаки.

$pdo = new PDO(
    'mysql:host=127.0.0.1;dbname=chat;charset=utf8mb4',
    getenv('DB_USER'),
    getenv('DB_PASSWORD'),
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
    ]
);

При обработке входящих данных сервер должен проверять тип события, длину текста, идентификатор комнаты и права пользователя. Клиентский JavaScript нельзя считать доверенным источником: любое значение из браузера может быть изменено вручную. Ограничение длины сообщения, например 2000 символами, защищает систему от случайной перегрузки и слишком больших запросов.

Структура базы данных

Для минимальной версии достаточно трёх таблиц. В users хранятся идентификатор, имя пользователя, хеш пароля и дата создания. Таблица rooms содержит название комнаты, а messages связывает текст с автором и комнатой. Время публикации лучше задавать на стороне базы или сервера, а не принимать от клиента.

Пример таблицы сообщений выглядит так:

CREATE TABLE messages (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    room_id BIGINT UNSIGNED NOT NULL,
    user_id BIGINT UNSIGNED NOT NULL,
    body VARCHAR(2000) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_room_time (room_id, created_at),
    FOREIGN KEY (room_id) REFERENCES rooms(id),
    FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Индекс по комнате и времени ускоряет загрузку последних сообщений. При большом объёме истории не стоит возвращать все записи сразу: API может выбирать последние 50 сообщений и принимать параметр для постраничной загрузки. Внешние ключи помогают не допустить сообщений, связанных с удалёнными комнатами или пользователями.

Компонент Назначение Что важно проверить
users Учётные записи и авторизация Хеширование паролей через password_hash
rooms Список каналов общения Проверка доступа участника
messages История переписки Индекс комнаты и времени
WebSocket Мгновенная доставка событий Контроль соединений и отключений
JavaScript Интерфейс пользователя Экранирование полученного текста

Реализация WebSocket-сервера

Класс обработчика Ratchet обычно реализует методы onOpen, onMessage, onClose и onError. В onOpen соединение добавляется в коллекцию активных клиентов. После авторизации сервер связывает объект соединения с идентификатором пользователя и комнатой. Когда приходит новое сообщение, обработчик проверяет JSON, сохраняет текст через PDO и отправляет событие участникам соответствующей комнаты.

Формат событий лучше сделать единообразным. Например, клиент передаёт объект {"type":"message","roomId":1,"body":"Привет"}, а сервер возвращает {"type":"message","id":25,"user":"Анна","body":"Привет","createdAt":"..."}. Поле type позволяет добавлять новые действия, не меняя весь протокол.

Важная особенность PHP WebSocket-сервера заключается в длительном времени работы процесса. Нельзя создавать новое подключение к базе при каждом небольшом действии без необходимости, а необработанное исключение не должно останавливать весь сервер. Ошибки записываются в журнал, проблемный сокет закрывается, а остальные клиенты продолжают работу.

Для запуска отдельного процесса можно использовать консольную команду, например php websocket.php. В рабочей среде процесс обычно контролируется Supervisor или systemd. Перед ним ставят Nginx, который принимает HTTPS-соединения и передаёт WebSocket-трафик через заголовки Upgrade и Connection.

Клиентская часть и защита данных

После загрузки страницы JavaScript создаёт объект WebSocket, подписывается на onopen, onmessage и onclose, а затем показывает состояние соединения. Форма отправки не должна перезагружать страницу: она проверяет пустое значение, формирует JSON и вызывает socket.send. Новое событие добавляется в список сообщений без повторной загрузки истории.

Текст сообщения необходимо безопасно вставлять в DOM. Использование innerHTML для данных от пользователя создаёт риск межсайтового скриптинга. Надёжнее применять textContent, а для сложной разметки — отдельный механизм очистки HTML с белым списком разрешённых тегов. На сервере также полезно нормализовать пробелы и отбрасывать управляющие символы.

Авторизацию можно построить на сессионной cookie или короткоживущем токене. При WebSocket-подключении сервер проверяет токен и получает пользователя из базы или кэша. Соединение разрешается только по защищённому протоколу wss, если сайт работает через HTTPS. Следует добавить ограничение частоты отправки, чтобы один клиент не мог создать тысячи событий за короткий промежуток.

Если пользователь временно потерял интернет, интерфейс показывает отключение и пытается подключиться снова с увеличивающейся задержкой. После восстановления соединения клиент запрашивает пропущенные сообщения по последнему известному идентификатору. Такой подход надёжнее, чем простое отображение новых событий, ведь во время разрыва часть данных могла прийти на сервер.

Проверка работы и дальнейшее развитие

Тестировать чат нужно в нескольких браузерах и с несколькими параллельными клиентами. Проверяются отправка сообщений, вход в разные комнаты, закрытие вкладки, повторное подключение, длинные строки и специальные символы. Отдельно стоит проверить ситуацию, когда комната или пользователь удалены между открытием страницы и отправкой события.

Для диагностики полезно вести структурированные журналы: время события, идентификатор соединения, тип операции и результат проверки. Пароли, токены и содержимое личной переписки в логи записывать не следует. Нагрузочный тест помогает понять, сколько открытых соединений выдерживает один процесс и где появляется ограничение — в CPU, памяти, базе или сетевом прокси.

Кроссплатформенный клиент для такого сервера можно реализовать отдельно: например, на Android использовать Kotlin, на iOS — Swift, а для настольных приложений выбрать Delphi и FireMonkey. При выборе технологии полезно учитывать особенности производительности и работы с сетевыми потоками, которые хорошо видны в сравнении Delphi и Swift на одинаковых алгоритмах.

Следующим этапом становятся личные диалоги, статусы «печатает», уведомления, загрузка файлов и поиск по истории. При нескольких экземплярах WebSocket-сервера понадобится общий брокер событий: Redis позволит передать сообщение процессу, в котором находится нужный получатель. Так простой прототип постепенно превращается в устойчивую систему обмена сообщениями с понятным протоколом и контролируемой нагрузкой.