WebSockets: real-time комунікація для веб-застосунків
Чати, сповіщення, multiplayer ігри — все, що потребує миттєвого зв'язку
HTTP — це «питання-відповідь». Клієнт питає, сервер відповідає. Але що робити, коли сервер хоче сам надіслати дані клієнту? Наприклад, нове повідомлення в чаті?
Клієнт постійно питає: «Є нові дані?»
- Запит кожні N секунд
- Навантаження на сервер
- Затримка = період polling
Постійне з'єднання. Дані — миттєво.
- Один handshake
- Bidirectional
- Latency: мілісекунди
Socket.IO: найпопулярніше рішення
Socket.IO — бібліотека для real-time комунікації. Надбудова над WebSocket з fallback на polling, rooms, namespaces, reconnection.
Сервер (Node.js)
// npm install socket.io express
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: {
origin: "http://localhost:3000",
methods: ["GET", "POST"]
}
});
// Зберігання користувачів онлайн
const onlineUsers = new Map();
io.on('connection', (socket) => {
console.log(`User connected: ${socket.id}`);
// Користувач приєднується до чату
socket.on('join', (userData) => {
const { username, room } = userData;
// Зберігаємо дані користувача
onlineUsers.set(socket.id, { username, room });
// Приєднання до кімнати
socket.join(room);
// Повідомлення всім у кімнаті
io.to(room).emit('user-joined', {
username,
message: `${username} приєднався до чату`,
users: getUsersInRoom(room)
});
});
// Нове повідомлення
socket.on('message', (data) => {
const user = onlineUsers.get(socket.id);
if (!user) return;
const messageData = {
id: Date.now(),
username: user.username,
text: data.text,
timestamp: new Date().toISOString()
};
// Надсилаємо всім у кімнаті
io.to(user.room).emit('message', messageData);
});
// Typing indicator
socket.on('typing', () => {
const user = onlineUsers.get(socket.id);
if (user) {
socket.to(user.room).emit('user-typing', user.username);
}
});
socket.on('stop-typing', () => {
const user = onlineUsers.get(socket.id);
if (user) {
socket.to(user.room).emit('user-stop-typing', user.username);
}
});
// Відключення
socket.on('disconnect', () => {
const user = onlineUsers.get(socket.id);
if (user) {
io.to(user.room).emit('user-left', {
username: user.username,
users: getUsersInRoom(user.room)
});
onlineUsers.delete(socket.id);
}
});
});
function getUsersInRoom(room) {
return Array.from(onlineUsers.values())
.filter(u => u.room === room)
.map(u => u.username);
}
httpServer.listen(3001, () => {
console.log('Socket.IO server running on :3001');
});
Клієнт (React)
// npm install socket.io-client
import { useEffect, useState, useRef } from 'react';
import { io } from 'socket.io-client';
const socket = io('http://localhost:3001');
function ChatRoom({ username, room }) {
const [messages, setMessages] = useState([]);
const [inputMessage, setInputMessage] = useState('');
const [typingUsers, setTypingUsers] = useState([]);
const [onlineUsers, setOnlineUsers] = useState([]);
const typingTimeoutRef = useRef(null);
useEffect(() => {
// Приєднання до кімнати
socket.emit('join', { username, room });
// Слухачі подій
socket.on('message', (message) => {
setMessages(prev => [...prev, message]);
});
socket.on('user-joined', (data) => {
setOnlineUsers(data.users);
setMessages(prev => [...prev, {
id: Date.now(),
system: true,
text: data.message
}]);
});
socket.on('user-left', (data) => {
setOnlineUsers(data.users);
});
socket.on('user-typing', (username) => {
setTypingUsers(prev =>
prev.includes(username) ? prev : [...prev, username]
);
});
socket.on('user-stop-typing', (username) => {
setTypingUsers(prev => prev.filter(u => u !== username));
});
return () => {
socket.off('message');
socket.off('user-joined');
socket.off('user-left');
socket.off('user-typing');
socket.off('user-stop-typing');
};
}, [username, room]);
const sendMessage = (e) => {
e.preventDefault();
if (!inputMessage.trim()) return;
socket.emit('message', { text: inputMessage });
setInputMessage('');
socket.emit('stop-typing');
};
const handleTyping = (e) => {
setInputMessage(e.target.value);
socket.emit('typing');
// Зупинка typing через 2 секунди неактивності
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
typingTimeoutRef.current = setTimeout(() => {
socket.emit('stop-typing');
}, 2000);
};
return (
<div className="chat-container">
<div className="users-sidebar">
<h3>Онлайн ({onlineUsers.length})</h3>
{onlineUsers.map(user => (
<div key={user}>{user}</div>
))}
</div>
<div className="messages-area">
{messages.map(msg => (
<div
key={msg.id}
className={msg.system ? 'system-message' : 'user-message'}
>
{!msg.system && <strong>{msg.username}: </strong>}
{msg.text}
</div>
))}
{typingUsers.length > 0 && (
<div className="typing-indicator">
{typingUsers.join(', ')} пише...
</div>
)}
</div>
<form onSubmit={sendMessage}>
<input
value={inputMessage}
onChange={handleTyping}
placeholder="Введіть повідомлення..."
/>
<button type="submit">Надіслати</button>
</form>
</div>
);
}
FastAPI WebSockets
Для Python-проектів FastAPI має вбудовану підтримку WebSocket.
# pip install fastapi uvicorn websockets
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from typing import Dict, List
import json
app = FastAPI()
class ConnectionManager:
"""Менеджер WebSocket з'єднань."""
def __init__(self):
# room_name -> list of websockets
self.rooms: Dict[str, List[WebSocket]] = {}
# websocket -> user info
self.users: Dict[WebSocket, dict] = {}
async def connect(self, websocket: WebSocket, room: str, username: str):
await websocket.accept()
if room not in self.rooms:
self.rooms[room] = []
self.rooms[room].append(websocket)
self.users[websocket] = {"username": username, "room": room}
# Повідомляємо всіх про нового користувача
await self.broadcast(room, {
"type": "user_joined",
"username": username,
"users": self.get_users_in_room(room)
})
def disconnect(self, websocket: WebSocket):
user = self.users.get(websocket)
if user:
room = user["room"]
self.rooms[room].remove(websocket)
del self.users[websocket]
return user
return None
async def broadcast(self, room: str, message: dict):
"""Надіслати повідомлення всім у кімнаті."""
if room in self.rooms:
for connection in self.rooms[room]:
await connection.send_json(message)
async def send_to_others(self, room: str, sender: WebSocket, message: dict):
"""Надіслати всім крім відправника."""
if room in self.rooms:
for connection in self.rooms[room]:
if connection != sender:
await connection.send_json(message)
def get_users_in_room(self, room: str) -> List[str]:
return [
self.users[ws]["username"]
for ws in self.rooms.get(room, [])
]
manager = ConnectionManager()
@app.websocket("/ws/{room}/{username}")
async def websocket_endpoint(
websocket: WebSocket,
room: str,
username: str
):
await manager.connect(websocket, room, username)
try:
while True:
data = await websocket.receive_json()
if data["type"] == "message":
await manager.broadcast(room, {
"type": "message",
"username": username,
"text": data["text"],
"timestamp": data.get("timestamp")
})
elif data["type"] == "typing":
await manager.send_to_others(room, websocket, {
"type": "typing",
"username": username
})
except WebSocketDisconnect:
user = manager.disconnect(websocket)
if user:
await manager.broadcast(user["room"], {
"type": "user_left",
"username": user["username"],
"users": manager.get_users_in_room(user["room"])
})
Real-time сповіщення
Паттерн для системи сповіщень: окремий WebSocket канал для notifications.
// Сервер: notifications.js
const notificationNamespace = io.of('/notifications');
// Зберігаємо з'єднання по user_id
const userConnections = new Map();
notificationNamespace.on('connection', (socket) => {
// Аутентифікація через token
const token = socket.handshake.auth.token;
const userId = verifyToken(token);
if (!userId) {
socket.disconnect();
return;
}
// Зберігаємо з'єднання
if (!userConnections.has(userId)) {
userConnections.set(userId, new Set());
}
userConnections.get(userId).add(socket);
// Приєднання до персональної кімнати
socket.join(`user:${userId}`);
// Надсилаємо непрочитані сповіщення
sendUnreadNotifications(userId, socket);
socket.on('mark-read', async (notificationId) => {
await markNotificationAsRead(notificationId, userId);
});
socket.on('disconnect', () => {
userConnections.get(userId)?.delete(socket);
});
});
// Функція для надсилання сповіщення користувачу
async function sendNotification(userId, notification) {
// Зберігаємо в БД
const saved = await saveNotificationToDB(userId, notification);
// Надсилаємо через WebSocket якщо онлайн
notificationNamespace.to(`user:${userId}`).emit('notification', {
id: saved.id,
type: notification.type,
title: notification.title,
body: notification.body,
link: notification.link,
createdAt: saved.createdAt
});
// Якщо офлайн — можна надіслати push notification
if (!userConnections.has(userId) || userConnections.get(userId).size === 0) {
sendPushNotification(userId, notification);
}
}
// Приклад використання з іншої частини коду
async function onNewComment(postId, commenterId, comment) {
const post = await getPost(postId);
// Сповіщаємо автора поста
if (post.authorId !== commenterId) {
await sendNotification(post.authorId, {
type: 'new_comment',
title: 'Новий коментар',
body: `${comment.authorName} прокоментував ваш пост`,
link: `/posts/${postId}#comment-${comment.id}`
});
}
}
Клієнт: React hook для сповіщень
// hooks/useNotifications.js
import { useEffect, useState, useCallback } from 'react';
import { io } from 'socket.io-client';
import { useAuth } from './useAuth';
export function useNotifications() {
const { token } = useAuth();
const [notifications, setNotifications] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
const [socket, setSocket] = useState(null);
useEffect(() => {
if (!token) return;
const newSocket = io('http://localhost:3001/notifications', {
auth: { token }
});
newSocket.on('notification', (notification) => {
setNotifications(prev => [notification, ...prev]);
setUnreadCount(prev => prev + 1);
// Показуємо toast
showToast(notification);
});
newSocket.on('unread-notifications', (data) => {
setNotifications(data.notifications);
setUnreadCount(data.count);
});
setSocket(newSocket);
return () => newSocket.close();
}, [token]);
const markAsRead = useCallback((notificationId) => {
socket?.emit('mark-read', notificationId);
setNotifications(prev =>
prev.map(n =>
n.id === notificationId ? { ...n, read: true } : n
)
);
setUnreadCount(prev => Math.max(0, prev - 1));
}, [socket]);
return { notifications, unreadCount, markAsRead };
}
Потрібна допомога з проектом?
Real-time застосунки з WebSockets -- одна з найцікавіших тем для курсової. Ми допоможемо з архітектурою, Socket.IO або FastAPI WebSocket реалізацією.
Замовити курсову з JavaScriptMultiplayer гра (концепт)
// Сервер для простої multiplayer гри
const games = new Map(); // gameId -> game state
io.on('connection', (socket) => {
socket.on('join-game', (gameId) => {
let game = games.get(gameId);
if (!game) {
game = {
id: gameId,
players: [],
state: initGameState()
};
games.set(gameId, game);
}
const player = {
id: socket.id,
x: 100,
y: 100,
color: randomColor()
};
game.players.push(player);
socket.join(gameId);
// Надсилаємо поточний стан новому гравцю
socket.emit('game-state', game);
// Повідомляємо інших про нового гравця
socket.to(gameId).emit('player-joined', player);
});
// Рух гравця
socket.on('move', (data) => {
const { gameId, x, y } = data;
const game = games.get(gameId);
if (!game) return;
const player = game.players.find(p => p.id === socket.id);
if (player) {
player.x = x;
player.y = y;
// Broadcast позиції (30 разів на секунду буде занадто часто,
// краще використовувати server-side tick rate)
socket.to(gameId).emit('player-moved', {
playerId: socket.id,
x, y
});
}
});
// Дія гравця
socket.on('action', (data) => {
const { gameId, action } = data;
const game = games.get(gameId);
if (!game) return;
// Валідація та обробка дії на сервері
const result = processAction(game, socket.id, action);
// Оновлюємо всіх про результат
io.to(gameId).emit('action-result', result);
});
});
Ідеї для курсової роботи
Функціонал: Кімнати, приватні повідомлення, typing indicator, онлайн-статус, історія.
Технології: Socket.IO, React, Node.js, MongoDB
Складність: Середня
Функціонал: Real-time редагування, курсори інших користувачів, conflict resolution.
Технології: WebSocket, CRDT/OT алгоритми, Yjs
Складність: Висока
Функціонал: Live графіки, метрики, алерти, WebSocket streaming.
Технології: FastAPI WebSocket, React, Chart.js, Redis Pub/Sub
Складність: Середня
Потрібна допомога з курсовою?
Real-time застосунки — одна з найцікавіших тем для курсової. Ми допоможемо з архітектурою, WebSocket реалізацією та оформленням.
Замовити курсову з WebSockets