📋 Что получится
- Мгновенная доставка сообщений
- Индикатор "печатает..."
- Статусы: отправлено → доставлено → прочитано
- Групповые чаты
- История сообщений
Любой стек: Примеры серверного кода на PHP, но концепции применимы к любому языку — Node.js, Python, Go, Ruby, Java, C# и др. Pushler.ru работает через REST API.
🏗️ Архитектура
Браузер A
Алиса
↔
Pushler.ru
WebSocket
↔
Браузер B
Борис
Ваш сервер
PHP + MySQL
Поток сообщения:
- Алиса отправляет сообщение → POST на сервер
- Сервер сохраняет в БД
- Сервер отправляет через Pushler в канал чата
- Борис получает сообщение мгновенно
🗄️ Структура базы данных
SQL
-- Чаты (комнаты)
CREATE TABLE chats (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
type ENUM('private', 'group') DEFAULT 'private',
name VARCHAR(255) NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Участники чата
CREATE TABLE chat_participants (
chat_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_read_at TIMESTAMP NULL,
PRIMARY KEY (chat_id, user_id),
FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE
);
-- Сообщения
CREATE TABLE messages (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
chat_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
content TEXT NOT NULL,
type ENUM('text', 'image', 'file') DEFAULT 'text',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE,
INDEX idx_chat_created (chat_id, created_at)
);
💻 Код сервера
Серверный код отвечает за сохранение сообщений и отправку событий в Pushler. Ниже примеры на PHP — адаптируйте под свой стек.
Установка PHP SDK
Bash
composer require pushler/php-sdk
Инициализация Pushler
PHP
<?php
// pushler.php — подключение к Pushler
require_once 'vendor/autoload.php';
use PushlerRu\PushlerClient;
// Создаём клиент Pushler
$pushler = new PushlerClient(
'key_ваш_ключ', // App Key
'secret_ваш_секрет' // App Secret
);
Отправка сообщения в чат
PHP
<?php
// send_message.php — отправка сообщения
require_once 'pushler.php';
// Получаем данные из запроса
$chatId = (int) $_POST['chat_id'];
$content = trim($_POST['content']);
$userId = getCurrentUserId(); // Ваша функция авторизации
// Проверяем доступ к чату
if (!userHasAccessToChat($userId, $chatId)) {
http_response_code(403);
echo json_encode(['error' => 'Доступ запрещён']);
exit;
}
// Сохраняем сообщение в БД
$pdo = getDbConnection();
$stmt = $pdo->prepare('
INSERT INTO messages (chat_id, user_id, content, created_at)
VALUES (?, ?, ?, NOW())
');
$stmt->execute([$chatId, $userId, $content]);
$messageId = $pdo->lastInsertId();
// Получаем данные пользователя
$user = getUserById($userId);
// Формируем данные сообщения
$message = [
'id' => $messageId,
'chat_id' => $chatId,
'user_id' => $userId,
'content' => $content,
'user' => [
'id' => $user['id'],
'name' => $user['name'],
'avatar' => $user['avatar']
],
'created_at' => date('c')
];
// Отправляем через Pushler всем участникам чата
$pushler->trigger(
"private-chat-{$chatId}",
'message:new',
['message' => $message]
);
// Уведомляем других участников (для badge/звука)
$participants = getChatParticipants($chatId);
foreach ($participants as $participantId) {
if ($participantId != $userId) {
$pushler->trigger(
"private-user-{$participantId}",
'chat:new-message',
[
'chat_id' => $chatId,
'preview' => mb_substr($content, 0, 100),
'sender' => [
'id' => $user['id'],
'name' => $user['name']
]
]
);
}
}
echo json_encode(['message' => $message]);
Авторизация приватного канала
PHP
<?php
// auth.php — авторизация каналов Pushler
require_once 'pushler.php';
header('Content-Type: application/json');
$channelName = $_POST['channel_name'];
$socketId = $_POST['socket_id'];
$userId = getCurrentUserId();
if (!$userId) {
http_response_code(401);
echo json_encode(['error' => 'Unauthorized']);
exit;
}
// Проверяем доступ к каналу
// Формат: private-chat-{chatId}
if (preg_match('/^private-chat-(\d+)$/', $channelName, $matches)) {
$chatId = (int) $matches[1];
if (!userHasAccessToChat($userId, $chatId)) {
http_response_code(403);
echo json_encode(['error' => 'Forbidden']);
exit;
}
}
// Формат: private-user-{userId} — только для своего канала
if (preg_match('/^private-user-(\d+)$/', $channelName, $matches)) {
if ((int) $matches[1] !== $userId) {
http_response_code(403);
echo json_encode(['error' => 'Forbidden']);
exit;
}
}
// Генерируем подпись для авторизации канала
$auth = $pushler->authorizeChannel($channelName, $socketId);
echo json_encode($auth);
Отметка прочитанным
PHP
<?php
// mark_read.php — отметить сообщения как прочитанные
require_once 'pushler.php';
$chatId = (int) $_POST['chat_id'];
$userId = getCurrentUserId();
// Обновляем время последнего прочтения
$pdo = getDbConnection();
$stmt = $pdo->prepare('
UPDATE chat_participants
SET last_read_at = NOW()
WHERE chat_id = ? AND user_id = ?
');
$stmt->execute([$chatId, $userId]);
// Уведомляем отправителей о прочтении
$pushler->trigger(
"private-chat-{$chatId}",
'messages:read',
[
'user_id' => $userId,
'read_at' => date('c')
]
);
echo json_encode(['success' => true]);
🌐 Код клиента (JavaScript)
Подключение SDK
HTML
<!-- Подключение из CDN -->
<script src="https://cdn.pushler.ru/sdk/pushler.min.js"></script>
<!-- Или npm: npm install @pushler/js -->
Базовая реализация чата
JavaScript
// Инициализация Pushler
const pushler = new PushlerClient({
appKey: 'key_ваш_ключ',
authEndpoint: '/auth.php' // Ваш endpoint авторизации
});
const chatId = 123;
const currentUserId = 456;
let channel = null;
// Подключение к чату
pushler.on('connected', () => {
console.log('Подключено к Pushler');
// Подписываемся на канал чата
channel = pushler.subscribe(`private-chat-${chatId}`);
// Новое сообщение
channel.on('message:new', (data) => {
// Не добавляем своё сообщение (уже добавлено оптимистично)
if (data.message.user_id !== currentUserId) {
addMessageToChat(data.message);
}
});
// Кто-то печатает
channel.on('user:typing', (data) => {
if (data.user_id !== currentUserId) {
showTypingIndicator(data.user_name);
}
});
// Сообщения прочитаны
channel.on('messages:read', (data) => {
updateMessageStatuses(data.user_id, data.read_at);
});
});
// Отправка сообщения
async function sendMessage(content) {
// Оптимистичное добавление
const tempMessage = {
id: 'temp_' + Date.now(),
user_id: currentUserId,
content: content,
created_at: new Date().toISOString(),
_pending: true
};
addMessageToChat(tempMessage);
// Отправляем на сервер
const response = await fetch('/send_message.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `chat_id=${chatId}&content=${encodeURIComponent(content)}`
});
const data = await response.json();
// Заменяем временное сообщение на реальное
replaceMessage(tempMessage.id, data.message);
}
// Индикатор "печатает"
let lastTypingSent = 0;
function handleTyping() {
const now = Date.now();
if (now - lastTypingSent > 2000) {
lastTypingSent = now;
fetch('/typing.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `chat_id=${chatId}&socket_id=${pushler.socketId}`
});
}
}
// UI функции
function addMessageToChat(message) {
const container = document.getElementById('messages');
const div = document.createElement('div');
div.className = 'message' + (message.user_id === currentUserId ? ' own' : '');
div.id = 'msg-' + message.id;
div.innerHTML = `
<div class="content">${escapeHtml(message.content)}</div>
<div class="meta">
${formatTime(message.created_at)}
${message._pending ? '🕐' : '✓'}
</div>
`;
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
Vue 3 компонент
Vue
<!-- ChatWindow.vue -->
<template>
<div class="chat-window">
<div class="chat-header">
<span class="name">{{ otherUser.name }}</span>
<span v-if="isTyping" class="typing">печатает...</span>
</div>
<div ref="messagesEl" class="messages">
<div
v-for="msg in messages"
:key="msg.id"
:class="['message', { own: msg.user_id === currentUserId }]"
>
<div class="content">{{ msg.content }}</div>
<div class="meta">
{{ formatTime(msg.created_at) }}
<span v-if="msg.user_id === currentUserId">{{ getStatus(msg) }}</span>
</div>
</div>
</div>
<div class="chat-input">
<textarea
v-model="newMessage"
v-on:input="handleTyping"
v-on:keydown.enter.exact.prevent="send"
placeholder="Сообщение..."
></textarea>
<button v-on:click="send">→</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import PushlerClient from '@pushler/js';
const props = defineProps(['chatId', 'currentUserId', 'otherUser', 'appKey']);
const messages = ref([]);
const newMessage = ref('');
const isTyping = ref(false);
const lastReadAt = ref(null);
const messagesEl = ref(null);
let pushler = null;
let channel = null;
let lastTypingSent = 0;
onMounted(async () => {
// Загружаем историю
const res = await fetch(`/messages.php?chat_id=${props.chatId}`);
messages.value = (await res.json()).messages;
// Подключаемся к Pushler
pushler = new PushlerClient({
appKey: props.appKey,
authEndpoint: '/auth.php'
});
pushler.on('connected', () => {
channel = pushler.subscribe(`private-chat-${props.chatId}`);
channel.on('message:new', (data) => {
if (data.message.user_id !== props.currentUserId) {
messages.value.push(data.message);
scrollToBottom();
}
});
channel.on('user:typing', (data) => {
if (data.user_id !== props.currentUserId) {
isTyping.value = true;
setTimeout(() => { isTyping.value = false; }, 3000);
}
});
channel.on('messages:read', (data) => {
if (data.user_id === props.otherUser.id) {
lastReadAt.value = data.read_at;
}
});
});
});
onUnmounted(() => {
channel?.unsubscribe();
pushler?.disconnect();
});
async function send() {
const content = newMessage.value.trim();
if (!content) return;
const temp = {
id: 'temp_' + Date.now(),
user_id: props.currentUserId,
content,
created_at: new Date().toISOString(),
_pending: true
};
messages.value.push(temp);
newMessage.value = '';
scrollToBottom();
const res = await fetch('/send_message.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `chat_id=${props.chatId}&content=${encodeURIComponent(content)}`
});
const data = await res.json();
const idx = messages.value.findIndex(m => m.id === temp.id);
if (idx > -1) messages.value[idx] = data.message;
}
function handleTyping() {
const now = Date.now();
if (now - lastTypingSent > 2000) {
lastTypingSent = now;
fetch('/typing.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `chat_id=${props.chatId}&socket_id=${pushler?.socketId}`
});
}
}
function getStatus(msg) {
if (msg._pending) return '🕐';
if (lastReadAt.value && new Date(msg.created_at) <= new Date(lastReadAt.value)) return '✓✓';
return '✓';
}
function formatTime(date) {
return new Date(date).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
}
function scrollToBottom() {
nextTick(() => {
if (messagesEl.value) messagesEl.value.scrollTop = messagesEl.value.scrollHeight;
});
}
</script>
⚠️ Типичные ошибки
1. Дублирование сообщений
PHP
// ❌ Неправильно — сообщение придёт и отправителю
$pushler->trigger("private-chat-{$chatId}", 'message:new', $message);
// ✅ Правильно — исключаем отправителя по socket_id
$socketId = $_POST['socket_id'] ?? null;
$pushler->trigger("private-chat-{$chatId}", 'message:new', $message, $socketId);
2. Неправильная авторизация канала
PHP
// ❌ Неправильно — не проверяем доступ
$auth = $pushler->authorizeChannel($channelName, $socketId);
echo json_encode($auth);
// ✅ Правильно — проверяем права пользователя
if (!userHasAccessToChat($userId, $chatId)) {
http_response_code(403);
exit;
}
$auth = $pushler->authorizeChannel($channelName, $socketId);
echo json_encode($auth);
3. Утечка подписок на клиенте
JavaScript
// ❌ Неправильно — не отписываемся при выходе
pushler.subscribe(`private-chat-${chatId}`);
// ✅ Правильно — отписываемся
// При закрытии чата:
channel.unsubscribe();
// При полном выходе:
pushler.disconnect();
Готовы попробовать?
Создайте бесплатный аккаунт и начните интеграцию за пару минут