Админка
пожалуйста подождите

Введение

WebSockets обеспечивают двунаправленные постоянные соединения между клиентами и серверами, что критически важно для функций реального времени, таких как чат, уведомления, live-обновления и совместное редактирование. В отличие от модели HTTP «запрос—ответ» WebSockets позволяют серверу мгновенно отправлять данные клиентам. В этом руководстве рассматриваются практические паттерны реализации WebSocket.

Основы WebSocket

Как работают WebSockets

  1. Клиент инициирует HTTP-запрос на upgrade
  2. Сервер принимает запрос и выполняет upgrade соединения
  3. Начинается двунаправленное взаимодействие
  4. Любая сторона может отправлять сообщения в любое время
  5. Соединение сохраняется до явного закрытия

Когда использовать WebSockets

Используйте WebSocketsИспользуйте HTTP/REST
Обновления в реальном времениДанные по модели «запрос—ответ»
Чат-приложенияCRUD-операции
Live-уведомленияЗагрузка файлов
Совместное редактированиеРедкие обновления
ИгрыСтатический контент

Node.js с Socket.IO

Настройка сервера

// server.js
import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: {
origin: 'http://localhost:3000',
methods: ['GET', 'POST'],
},
});
// Обработка соединения
io.on('connection', (socket) => {
console.log('Client connected:', socket.id);
// Обработка событий от клиента
socket.on('message', (data) => {
console.log('Message received:', data);
// Отправка всем клиентам
io.emit('message', data);
// Или отправка в конкретную room
// io.to('room1').emit('message', data);
});
// Присоединиться к room
socket.on('join_room', (room) => {
socket.join(room);
console.log(`${socket.id} joined ${room}`);
socket.to(room).emit('user_joined', { userId: socket.id });
});
// Покинуть room
socket.on('leave_room', (room) => {
socket.leave(room);
socket.to(room).emit('user_left', { userId: socket.id });
});
// Обработка отключения
socket.on('disconnect', () => {
console.log('Client disconnected:', socket.id);
});
});
httpServer.listen(3001, () => {
console.log('Server running on port 3001');
});

Клиент (React)

// hooks/useSocket.js
import { useEffect, useState, useCallback } from 'react';
import { io } from 'socket.io-client';
const SOCKET_URL = 'http://localhost:3001';
export function useSocket() {
const [socket, setSocket] = useState(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const socketInstance = io(SOCKET_URL, {
transports: ['websocket'],
});
socketInstance.on('connect', () => {
setIsConnected(true);
console.log('Connected to server');
});
socketInstance.on('disconnect', () => {
setIsConnected(false);
console.log('Disconnected from server');
});
setSocket(socketInstance);
return () => {
socketInstance.disconnect();
};
}, []);
const sendMessage = useCallback((event, data) => {
if (socket) {
socket.emit(event, data);
}
}, [socket]);
return { socket, isConnected, sendMessage };
}
// components/Chat.jsx
import { useState, useEffect } from 'react';
import { useSocket } from '../hooks/useSocket';
export function Chat({ room }) {
const { socket, isConnected, sendMessage } = useSocket();
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
useEffect(() => {
if (!socket) return;
// Присоединиться к room
socket.emit('join_room', room);
// Прослушивание сообщений
socket.on('message', (message) => {
setMessages((prev) => [...prev, message]);
});
socket.on('user_joined', ({ userId }) => {
setMessages((prev) => [...prev, { system: true, text: `${userId} joined` }]);
});
return () => {
socket.emit('leave_room', room);
socket.off('message');
socket.off('user_joined');
};
}, [socket, room]);
const handleSend = () => {
if (input.trim()) {
sendMessage('message', { room, text: input, timestamp: Date.now() });
setInput('');
}
};
return (
<div className="chat">
<div className="status">
{isConnected ? '🟢 Connected' : '🔴 Disconnected'}
</div>
<div className="messages">
{messages.map((msg, i) => (
<div key={i} className={msg.system ? 'system' : 'message'}>
{msg.text}
</div>
))}
</div>
<div className="input">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
placeholder="Type a message..."
/>
<button onClick={handleSend}>Send</button>
</div>
</div>
);
}

PHP с Ratchet

Настройка сервера

composer require cboden/ratchet
// src/ChatServer.php
<?php
namespace App;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class ChatServer implements MessageComponentInterface
{
protected $clients;
protected $rooms = [];
public function __construct()
{
$this->clients = new \SplObjectStorage;
}
public function onOpen(ConnectionInterface $conn)
{
$this->clients->attach($conn);
echo "New connection: {$conn->resourceId}\n";
}
public function onMessage(ConnectionInterface $from, $msg)
{
$data = json_decode($msg, true);
switch ($data['type']) {
case 'join':
$this->joinRoom($from, $data['room']);
break;
case 'message':
$this->broadcastToRoom($from, $data['room'], $data);
break;
}
}
protected function joinRoom(ConnectionInterface $conn, $room)
{
if (!isset($this->rooms[$room])) {
$this->rooms[$room] = new \SplObjectStorage;
}
$this->rooms[$room]->attach($conn);
// Уведомить остальных
foreach ($this->rooms[$room] as $client) {
if ($client !== $conn) {
$client->send(json_encode([
'type' => 'user_joined',
'userId' => $conn->resourceId,
]));
}
}
}
protected function broadcastToRoom(ConnectionInterface $from, $room, $data)
{
if (!isset($this->rooms[$room])) return;
$message = json_encode($data);
foreach ($this->rooms[$room] as $client) {
$client->send($message);
}
}
public function onClose(ConnectionInterface $conn)
{
$this->clients->detach($conn);
// Удалить из всех rooms
foreach ($this->rooms as $room => $clients) {
if ($clients->contains($conn)) {
$clients->detach($conn);
}
}
echo "Connection closed: {$conn->resourceId}\n";
}
public function onError(ConnectionInterface $conn, \Exception $e)
{
echo "Error: {$e->getMessage()}\n";
$conn->close();
}
}
// bin/server.php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use App\ChatServer;
$server = IoServer::factory(
new HttpServer(
new WsServer(
new ChatServer()
)
),
8080
);
echo "WebSocket server running on port 8080\n";
$server->run();

Laravel Broadcasting

Конфигурация

// config/broadcasting.php
'connections' => [
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'useTLS' => true,
],
],
],

Трансляция событий

// app/Events/MessageSent.php
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;
class MessageSent implements ShouldBroadcast
{
use InteractsWithSockets, SerializesModels;
public $message;
public $user;
public function __construct($message, $user)
{
$this->message = $message;
$this->user = $user;
}
public function broadcastOn()
{
return new PrivateChannel('chat.' . $this->message->room_id);
}
public function broadcastWith()
{
return [
'id' => $this->message->id,
'text' => $this->message->text,
'user' => [
'id' => $this->user->id,
'name' => $this->user->name,
],
'created_at' => $this->message->created_at->toIso8601String(),
];
}
}

Трансляция из контроллера

// app/Http/Controllers/ChatController.php
public function sendMessage(Request $request)
{
$message = Message::create([
'room_id' => $request->room_id,
'user_id' => auth()->id(),
'text' => $request->text,
]);
broadcast(new MessageSent($message, auth()->user()))->toOthers();
return response()->json($message);
}

Клиент с Laravel Echo

import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
const echo = new Echo({
broadcaster: 'pusher',
key: process.env.PUSHER_APP_KEY,
cluster: process.env.PUSHER_APP_CLUSTER,
forceTLS: true,
});
// Слушать на private channel
echo.private(`chat.${roomId}`)
.listen('MessageSent', (e) => {
console.log('New message:', e);
addMessage(e);
});
// Presence channel (показывает, кто онлайн)
echo.join(`chat.${roomId}`)
.here((users) => {
console.log('Users in room:', users);
})
.joining((user) => {
console.log('User joined:', user);
})
.leaving((user) => {
console.log('User left:', user);
});

Аутентификация

Socket.IO с JWT

// Сервер
io.use((socket, next) => {
const token = socket.handshake.auth.token;
try {
const user = jwt.verify(token, process.env.JWT_SECRET);
socket.user = user;
next();
} catch (err) {
next(new Error('Authentication failed'));
}
});
// Клиент
const socket = io(SOCKET_URL, {
auth: {
token: localStorage.getItem('token'),
},
});

Масштабирование с Redis

Socket.IO с Redis Adapter

import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));

Это позволяет нескольким серверам Socket.IO совместно использовать события.

Обработка ошибок и переподключение

// Клиент
const socket = io(SOCKET_URL, {
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
timeout: 20000,
});
socket.on('connect_error', (error) => {
console.error('Connection error:', error);
});
socket.on('reconnect', (attemptNumber) => {
console.log('Reconnected after', attemptNumber, 'attempts');
});
socket.on('reconnect_failed', () => {
console.error('Reconnection failed');
// Показать ошибку пользователю
});

Рекомендации

  1. Используйте rooms для адресной отправки сообщений вместо рассылки всем
  2. Реализуйте heartbeats для обнаружения «зависших» соединений
  3. Добавьте аутентификацию для предотвращения несанкционированного доступа
  4. Корректно обрабатывайте переподключение на стороне клиента
  5. Используйте Redis adapter для горизонтального масштабирования
  6. Ограничивайте размер сообщений для предотвращения злоупотреблений
  7. Реализуйте rate limiting для отправки сообщений

Заключение

WebSockets обеспечивают коммуникацию в реальном времени, необходимую для современных приложений. Используйте Socket.IO для кроссбраузерной совместимости, Laravel Broadcasting для приложений на Laravel или Ratchet для PHP-серверов. Для использования в production реализуйте корректную аутентификацию, обработку ошибок и стратегии масштабирования.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
Зетка Интерактив