Real-time WebSocket Socket.IO Chat

WebSockets: real-time комунікація для веб-застосунків

Чати, сповіщення, multiplayer ігри — все, що потребує миттєвого зв'язку

28 лютого 2026 | 25 хв читання

HTTP — це «питання-відповідь». Клієнт питає, сервер відповідає. Але що робити, коли сервер хоче сам надіслати дані клієнту? Наприклад, нове повідомлення в чаті?

HTTP (Polling)

Клієнт постійно питає: «Є нові дані?»

  • Запит кожні N секунд
  • Навантаження на сервер
  • Затримка = період polling
WebSocket

Постійне з'єднання. Дані — миттєво.

  • Один handshake
  • Bidirectional
  • Latency: мілісекунди
HTTP Polling: Client ───[GET /messages]───► Server Client ◄──────[empty]──────── Server Client ───[GET /messages]───► Server Client ◄──────[empty]──────── Server Client ───[GET /messages]───► Server Client ◄───[new message]───── Server WebSocket: Client ═══════════════════════ Server ◄──────[message]─────── ───────[message]──────► ◄──────[message]─────── ◄──────[message]─────── (одне з'єднання, миттєва доставка)

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 реалізацією.

Замовити курсову з JavaScript

Multiplayer гра (концепт)

// Сервер для простої 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

Потрібна допомога з роботою?

Замовте професійне виконання — без передоплати, оплата після демонстрації!

Курсова з JavaScript Курсова: Веб-додаток