← Назад к вопросам

Можно ли организовать REST поверх WebSocket?

2.7 Senior🔥 111 комментариев
#REST API и HTTP#Архитектура и паттерны#Асинхронность и многопоточность

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

REST поверх WebSocket

Да, можно организовать REST-подобный API поверх WebSocket. Это называется RPC (Remote Procedure Call) или JSON-RPC протокол и часто используется для real-time приложений.

Зачем нужно

Обычный REST            REST поверх WebSocket
──────────────          ─────────────────────
HTTP connection         Одно WebSocket соединение
Один запрос-ответ       Множество операций
Новое соединение        Постоянное соединение
Ovehead на каждый       Низкий overhead
Энергозатратно         Энергоэффективно

JSON-RPC протокол

# Клиент отправляет структурированный запрос
request = {
    "jsonrpc": "2.0",
    "method": "POST /api/v1/users",  # Как REST endpoint
    "params": {
        "name": "John",
        "email": "john@example.com"
    },
    "id": 1  # Для связи request-response
}

# Сервер отправляет ответ
response = {
    "jsonrpc": "2.0",
    "result": {"id": 1, "name": "John"},
    "id": 1  # Совпадает с request id
}

Реализация в FastAPI + WebSocket

from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import Dict, Any
import json
import uuid

app = FastAPI()

class WebSocketRPCServer:
    def __init__(self):
        self.handlers = {}
        self._register_handlers()
    
    def _register_handlers(self):
        """Регистрируем обработчики как REST endpoints"""
        # POST /users
        self.handlers["POST /users"] = self.create_user
        # GET /users/{id}
        self.handlers["GET /users/{id}"] = self.get_user
        # PATCH /users/{id}
        self.handlers["PATCH /users/{id}"] = self.update_user
        # DELETE /users/{id}
        self.handlers["DELETE /users/{id}"] = self.delete_user
    
    async def create_user(self, params: Dict[str, Any]):
        """Обработчик создания пользователя"""
        # Вместо return они возвращают JSON
        user_id = str(uuid.uuid4())
        return {"id": user_id, "name": params["name"], "email": params["email"]}
    
    async def get_user(self, params: Dict[str, Any]):
        user_id = params["id"]
        # Получаем из БД
        return {"id": user_id, "name": "John", "email": "john@example.com"}
    
    async def update_user(self, params: Dict[str, Any]):
        return {"id": params["id"], "updated": True}
    
    async def delete_user(self, params: Dict[str, Any]):
        return {"id": params["id"], "deleted": True}
    
    async def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
        """Обрабатываем JSON-RPC запрос"""
        method = request.get("method")
        params = request.get("params", {})
        request_id = request.get("id")
        
        try:
            if method not in self.handlers:
                return {
                    "jsonrpc": "2.0",
                    "error": {"code": -32601, "message": f"Method {method} not found"},
                    "id": request_id
                }
            
            # Вызываем обработчик
            result = await self.handlers[method](params)
            
            return {
                "jsonrpc": "2.0",
                "result": result,
                "id": request_id
            }
        
        except Exception as e:
            return {
                "jsonrpc": "2.0",
                "error": {"code": -32603, "message": str(e)},
                "id": request_id
            }

rpc_server = WebSocketRPCServer()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    
    try:
        while True:
            # Получаем JSON-RPC запрос
            data = await websocket.receive_text()
            request = json.loads(data)
            
            # Обрабатываем
            response = await rpc_server.handle_request(request)
            
            # Отправляем ответ
            await websocket.send_text(json.dumps(response))
    
    except WebSocketDisconnect:
        print("Client disconnected")

Клиентская часть (JavaScript)

class WebSocketRPCClient {
    constructor(url) {
        this.ws = new WebSocket(url);
        this.requestId = 0;
        this.pendingRequests = new Map();
        
        this.ws.onmessage = (event) => {
            const response = JSON.parse(event.data);
            const requestId = response.id;
            
            if (this.pendingRequests.has(requestId)) {
                const { resolve, reject } = this.pendingRequests.get(requestId);
                
                if (response.error) {
                    reject(new Error(response.error.message));
                } else {
                    resolve(response.result);
                }
                
                this.pendingRequests.delete(requestId);
            }
        };
    }
    
    async request(method, params = {}) {
        const requestId = ++this.requestId;
        
        const request = {
            jsonrpc: "2.0",
            method,
            params,
            id: requestId
        };
        
        return new Promise((resolve, reject) => {
            this.pendingRequests.set(requestId, { resolve, reject });
            this.ws.send(JSON.stringify(request));
        });
    }
    
    // REST-like методы
    async post(endpoint, data) {
        return this.request(`POST ${endpoint}`, data);
    }
    
    async get(endpoint, id) {
        return this.request(`GET ${endpoint}/${id}`, { id });
    }
    
    async patch(endpoint, id, data) {
        return this.request(`PATCH ${endpoint}/${id}`, { id, ...data });
    }
    
    async delete(endpoint, id) {
        return this.request(`DELETE ${endpoint}/${id}`, { id });
    }
}

// Использование
const client = new WebSocketRPCClient('ws://localhost:8000/ws');

// REST-like API
const user = await client.post('/users', {
    name: 'John',
    email: 'john@example.com'
});
console.log(user);  // { id: '...', name: 'John', ... }

const fetchedUser = await client.get('/users', user.id);
console.log(fetchedUser);

await client.patch('/users', user.id, { name: 'Jane' });
await client.delete('/users', user.id);

Пример более сложной реализации

from fastapi import FastAPI
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Callable, Dict, Any

class AdvancedRPCServer:
    def __init__(self, db: AsyncSession):
        self.db = db
        self.methods: Dict[str, Callable] = {}
    
    def register_method(self, name: str):
        """Декоратор для регистрации метода"""
        def decorator(func):
            self.methods[name] = func
            return func
        return decorator
    
    async def handle(self, request: Dict) -> Dict:
        method = request.get("method")
        
        if method not in self.methods:
            return self._error_response(
                -32601,
                f"Method '{method}' not found",
                request.get("id")
            )
        
        try:
            handler = self.methods[method]
            result = await handler(request.get("params", {}))
            return self._success_response(result, request.get("id"))
        except Exception as e:
            return self._error_response(-32603, str(e), request.get("id"))
    
    def _success_response(self, result, request_id):
        return {"jsonrpc": "2.0", "result": result, "id": request_id}
    
    def _error_response(self, code, message, request_id):
        return {
            "jsonrpc": "2.0",
            "error": {"code": code, "message": message},
            "id": request_id
        }

# Использование
rpc = AdvancedRPCServer(db=None)

@rpc.register_method("GET /users/{id}")
async def get_user(params):
    user_id = params["id"]
    # Получаем из БД
    return {"id": user_id, "name": "John"}

@rpc.register_method("POST /users")
async def create_user(params):
    # Создаём пользователя
    return {"id": "123", "name": params["name"]}

Advantages и Disadvantages

Advantages:

  • Одно соединение для множества операций
  • Низкий overhead (нет HTTP headers на каждый запрос)
  • Real-time уведомления
  • Двусторонняя коммуникация
  • Энергоэффективнее чем polling

Disadvantages:

  • Сложнее для отладки (нельзя просто curl)
  • Кэширование сложнее (HTTP кэши не работают)
  • Масштабирование сложнее (нужно управлять соединениями)
  • Требует WebSocket поддержки (не везде)

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

✅ Используй REST + WebSocket поверх WebSocket:
- Real-time приложения (чаты, игры)
- High-frequency обновления (dashboards)
- Много операций на одном соединении
- Мобильные приложения (экономия батареи)

❌ Используй обычный REST:
- Stateless API
- Public API (CDN, кэширование)
- Простые операции
- Когда не нужна real-time

Итого

  • Да, REST можно организовать поверх WebSocket
  • JSON-RPC - стандартный протокол для этого
  • Требует больше кода чем обычный REST
  • Стоит использовать если нужна real-time и много операций
  • Сложнее отлаживать - используй DevTools WebSocket inspector