WebSocket сообщения

WebSocket-сообщения в прикладных системах представляют собой непрерывный поток структурированных данных, передаваемых между клиентом и сервером в режиме реального времени. При росте сложности протоколов обмена возрастает необходимость строгой формализации формата сообщений, где ключевую роль играет валидация входящих и исходящих структур. Библиотека Ajv (Another JSON Schema Validator) обеспечивает компиляцию JSON Schema в высокопроизводительные функции проверки, что делает её одним из базовых инструментов для контроля WebSocket-протоколов в JavaScript-среде.

WebSocket сам по себе не накладывает ограничений на структуру сообщений. Типичная практика — использование “конверта” сообщения, в котором задаются метаданные и полезная нагрузка:

{
  "type": "message",
  "event": "chat.send",
  "payload": {
    "text": "Hello"
  },
  "requestId": "abc-123"
}

Такая структура требует строгого описания, чтобы исключить некорректные данные на границе системы. JSON Schema позволяет описать подобные сообщения декларативно.

Базовая схема для подобного сообщения:

const messageSchema = {
  type: "object",
  additionalProperties: false,
  required: ["type", "event", "payload"],
  properties: {
    type: { type: "string", enum: ["message", "error", "response"] },
    event: { type: "string" },
    requestId: { type: "string" },
    payload: { type: "object" }
  }
};

Ajv компилирует эту схему в функцию, выполняющую проверку без повторного разбора JSON Schema, что критично для потоковой обработки WebSocket-сообщений.

Инициализация Ajv и базовая компиляция схем

Создание экземпляра валидатора:

import Ajv from "ajv";

const ajv = new Ajv({
  allErrors: true,
  strict: true
});

const validateMessage = ajv.compile(messageSchema);

Компиляция выполняется один раз. Результирующая функция validateMessage используется для каждого входящего сообщения.

Обработка входящих WebSocket сообщений

При получении данных через WebSocket важно учитывать, что данные приходят в виде строки. Типичный поток обработки включает парсинг и валидацию:

ws.on("message", (raw) => {
  let data;

  try {
    data = JSON.parse(raw);
  } catch (e) {
    ws.send(JSON.stringify({
      type: "error",
      error: "INVALID_JSON"
    }));
    return;
  }

  const valid = validateMessage(data);

  if (!valid) {
    ws.send(JSON.stringify({
      type: "error",
      error: "SCHEMA_VALIDATION_FAILED",
      details: validateMessage.errors
    }));
    return;
  }

  handleMessage(data);
});

Ajv возвращает структурированный массив ошибок, содержащий путь к полю, ожидаемый тип и фактическое значение, что позволяет строить детализированные диагностические ответы.

Разделение типов сообщений через oneOf

В WebSocket-протоколах часто используются разные типы сообщений: события, ответы, ошибки, команды. Ajv поддерживает композицию схем через oneOf, что позволяет формализовать строгую типизацию сообщений:

const schema = {
  type: "object",
  oneOf: [
    {
      required: ["type", "event", "payload"],
      properties: {
        type: { const: "event" },
        event: { type: "string" },
        payload: { type: "object" }
      }
    },
    {
      required: ["type", "requestId", "result"],
      properties: {
        type: { const: "response" },
        requestId: { type: "string" },
        result: {}
      }
    },
    {
      required: ["type", "error"],
      properties: {
        type: { const: "error" },
        error: { type: "string" },
        message: { type: "string" }
      }
    }
  ]
};

Такая конструкция формирует строгий дискриминируемый союз типов сообщений, где поле type выступает дискриминатором.

Версионирование протокола сообщений

WebSocket-системы часто развиваются без возможности остановки сервера. Добавление версии протокола позволяет поддерживать обратную совместимость:

const versionedSchema = {
  type: "object",
  required: ["v", "type", "payload"],
  properties: {
    v: { type: "integer", enum: [1, 2] },
    type: { type: "string" },
    payload: { type: "object" }
  },
  oneOf: [
    {
      properties: {
        v: { const: 1 },
        type: { const: "message" }
      }
    },
    {
      properties: {
        v: { const: 2 },
        type: { enum: ["message", "event"] }
      }
    }
  ]
};

Ajv позволяет строить такие условные ветвления без потери производительности за счёт предварительной компиляции.

Производительность в потоковой обработке

В контексте WebSocket производительность валидации критична, поскольку обработка сообщений происходит в реальном времени. Ajv решает эту задачу за счёт генерации оптимизированного JavaScript-кода на этапе компиляции схемы.

Ключевые практики оптимизации:

  • компиляция схемы один раз при инициализации сервера
  • переиспользование валидаторов
  • минимизация динамической генерации схем
  • использование removeAdditional для очистки входных данных
  • включение strict режима для раннего обнаружения ошибок схемы

Пример настройки:

const ajv = new Ajv({
  allErrors: true,
  removeAdditional: "all",
  coerceTypes: true,
  strict: true
});

Валидация вложенных структур сообщений

WebSocket-протоколы часто используют сложные вложенные структуры:

const chatSchema = {
  type: "object",
  required: ["type", "payload"],
  properties: {
    type: { const: "chat" },
    payload: {
      type: "object",
      required: ["user", "message"],
      properties: {
        user: {
          type: "object",
          required: ["id", "name"],
          properties: {
            id: { type: "string" },
            name: { type: "string" }
          }
        },
        message: { type: "string", maxLength: 500 }
      }
    }
  }
};

Ajv рекурсивно валидирует вложенные структуры без дополнительного кода со стороны приложения.

Обработка ошибок валидации

Ajv формирует массив ошибок с детальной структурой:

validateMessage.errors = [
  {
    instancePath: "/payload/message",
    message: "must be string",
    keyword: "type",
    params: {
      type: "string"
    }
  }
];

Эти данные позволяют формировать диагностические события для клиента или логировать ошибки с точной привязкой к полю сообщения.

Использование форматов и пользовательских правил

Ajv поддерживает расширение через форматы и кастомные ключевые слова, что полезно для WebSocket-протоколов с идентификаторами, токенами и временными метками:

import addFormats from "ajv-formats";

addFormats(ajv);

const schema = {
  type: "object",
  required: ["timestamp"],
  properties: {
    timestamp: { type: "string", format: "date-time" }
  }
};

Кастомные проверки:

ajv.addKeyword({
  keyword: "isUserId",
  validate: (schema, data) => typeof data === "string" && data.startsWith("user_")
});

Архитектура протокольного слоя поверх WebSocket

В типичной архитектуре Ajv используется как часть промежуточного слоя между транспортом и бизнес-логикой:

  1. Получение raw WebSocket-сообщения
  2. Парсинг JSON
  3. Валидация через Ajv
  4. Маршрутизация по event или type
  5. Выполнение бизнес-логики
  6. Формирование ответа с повторной валидацией исходящего сообщения

Такая схема обеспечивает симметричность протокола и контроль на обоих направлениях передачи данных.

Валидация исходящих сообщений

Ajv применяется не только для входящих данных, но и для исходящих сообщений, что предотвращает утечки некорректных структур в клиент:

const response = {
  type: "response",
  requestId: "abc-123",
  result: { ok: true }
};

if (!validateResponse(response)) {
  throw new Error("Invalid server message structure");
}

ws.send(JSON.stringify(response));

Работа с динамическими схемами

В сложных системах схемы могут собираться динамически из модулей. Ajv поддерживает добавление схем в реестр:

ajv.addSchema(userSchema, "User");
ajv.addSchema(chatSchema, "ChatMessage");

const schema = {
  $ref: "ChatMessage"
};

Это позволяет строить модульный WebSocket-протокол с переиспользованием компонентов.

Безопасность сообщений и защита от некорректных данных

Валидация WebSocket-сообщений через Ajv выполняет функцию первой линии защиты:

  • исключение неожиданных полей
  • контроль типов данных
  • ограничение размера строк
  • предотвращение внедрения структур вне спецификации

Комбинация additionalProperties: false и строгих типов обеспечивает жёсткую фиксацию контракта между клиентом и сервером.

Интеграция с TypeScript-подходом

Хотя Ajv работает на уровне runtime, он может использоваться совместно с типизацией, где JSON Schema становится источником истины для структуры WebSocket-сообщений. Это снижает расхождение между документацией и фактической реализацией протокола.

Обработка потоков сообщений и backpressure

В высоконагруженных WebSocket-системах валидатор становится частью pipeline обработки потока. Ajv позволяет быстро отклонять некорректные сообщения до попадания в бизнес-очередь, уменьшая нагрузку на последующие стадии обработки.

Компиляция схем и повторное использование валидаторов

Каждый вызов compile создаёт отдельную функцию. В WebSocket-сервере валидаторы должны быть созданы один раз и использоваться повторно:

const validators = {
  event: ajv.compile(eventSchema),
  response: ajv.compile(responseSchema)
};

Такой подход исключает накладные расходы на повторную компиляцию при каждом сообщении.

Условные зависимости и сложные протоколы

Ajv поддерживает if/then/else, что позволяет моделировать сложные протокольные правила:

const schema = {
  type: "object",
  if: {
    properties: { type: { const: "auth" } }
  },
  then: {
    required: ["token"]
  },
  else: {
    required: ["requestId"]
  }
};

В WebSocket-системах это используется для разделения аутентифицированных и неаутентифицированных сообщений.