# Базовый формат сообщений WebSocket сервера

WebSocket является двунаправленным транспортом: обе стороны в любой момент времени могут отправлять любой набор данных в формате JSON. В рамках протокола действует механизм “запрос–ответ”, при котором каждое входящее сообщение должно быть подтверждено получателем.

# Типы сообщений

Протокол определяет три типа сообщений, соответствующие перечислению MESSAGE_TYPE:

  • RESERVED = 0 – зарезервировано (не используется);
  • REQUEST = 1 – запрос;
  • RESPONSE = 2 – ответ.

На данный момент применяются только REQUEST и RESPONSE.

Любое сообщение, как от клиента, так и от сервера, содержит минимум два обязательных поля:

{
  "type": 1,
  "id": 1,
}
Поле Тип Обяз. Описание
type uint32 Да Тип сообщения: 1 для запроса (REQUEST), 2 для ответа (RESPONSE).
id uint32 Да Уникальный идентификатор сообщения, представляющий собой инкрементируемое число. Используется для связывания пар “запрос–ответ”. О том, как с ним работать см. ниже.

# Ответ на запрос сервера

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

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

{
  "type": 2,
  "id": 123456
}

Если сервер не получит ответ в течение 300 секунд, соединение считается неактивным и будет автоматически закрыто, независимо от наличия других признаков активности (например, ping/pong).

# Работа с параметром id

Поскольку в WebSocket не существует понятия запрос-ответ, а порядок входящих сообщений может быть любым, то для связывания исходящих команд и ответов на них используется параметр id. Данный параметр обязательно должен присутствовать в каждом отправляемом запросе и это же значение будет во входящем сообщении, которое является ответом на запрос. Также его можно интерпретировать как счетчик отправленных запросов для каждой из сторон. На каждый входящий запрос должен быть отправлен ответ с таким же id.

Например, клиент отправляет сообщение ping на сервер:

{
"type": 1,
"id": 1,
"method": "ping",
"payload": {
  "data": "Hello, World!"
  }
}

Сервер отвечает клиенту (обратите внимание на параметр id):

{
  "type": 2,
  "id": 1,
  "payload": {
    "some_field": true
  }
}

Другой пример. Клиент отправляет сообщение на сервер:

{
  "type": 1,
  "id": 23768,
  "method": "getCurrentTime"
}

Сервер отвечает:

{
  "type": 2,
  "id": 23768,
  "payload": {
    "data": 1724936321584
  }
}

# Таймаут и долгие операции

В том случае, если ответ на запрос не был получен за 300 секунд (5 минут) сервер считает что соединение разорвалось или клиент завис, закрывает сессию WebSocket. Для некоторых задач формирование ответа может занимать более чем 300 секунд. В таком случае будет сформировано несколько запросов-ответов.

Как пример, рассмотрим логику осуществления вызова. Человек может не брать (отклонять) вызов более 300 секунд. В таком случае мы получаем состояние вызова отдельным сообщением от сервера спустя сколько угодно долгий промежуток времени:

Запрос от клиента:

{
  "type": 1,
  "id": 4444,
  "method": "callToUser",
  "payload": {
    "callId": "john"
  }
}

Сервер сразу отвечает подтверждением создания вызова:

{
  "type": 2,
  "id": 1,
  "payload": {
    "userId": "john@as1.trueconfServer.loc#vcs"
  }
}

Спустя некоторое время, когда пользователь возьмет трубку, сервер присылает клиенту оповещение:

{
  "type": 1,
  "id": 4445,
  "method": "conferenceCreated",
  "payload": {
    "userId": "john@as1.trueconfServer.loc#vcs",
    "isP2P": true,
    "conferenceId": "477dh6731ac54e322@as1.trueconfServer.loc#vcs"
  }
}

Клиент подтверждает что получил событие начала конференции:

{
  "type": 2,
  "id": 4445
}

# Одинаковые id в запросах

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

Запрос:

{
  "type": 1,
  "id": 12,
  "method": "ping"
}

Ответ:

{
  "type": 2,
  "id": 12
}

Повторно отправляем запрос с id = 12:

{
  "type": 1,
  "id": 12,
  "method": "someTest"
}

Ответ с ошибкой errorCode = 2:

{
  "type": 2,
  "id": 12,
  "payload": {
    "errorCode": 2
  }
}

# Асинхронность выполнения запросов

Поскольку WebSocket-библиотеками предоставляются асинхронные API, то клиент или сервер могут отправлять свои ответы на запросы в любом порядке и сразу несколько штук, не дожидаясь ответа на предыдущий запрос. Главное, чтобы время отправки запроса уложилось в 5 минут:

Запрос 1:

{
  "type": 1,
  "id": 278,
  "method": "someLongTask"
}

Запрос 2:

{
  "type": 1,
  "id": 279,
  "method": "createChat",
  "payload": {
    "chatTitle": "TEST"
  }
}

Запрос 3:

{
  "type": 1,
  "id": 280,
  "method": "createChat",
  "payload": {
    "chatTitle": "TEST 2"
  }
}

Запрос 4:

{
  "type": 1,
  "id": 281,
  "method": "someFastTask"
}

Ответ 1 на запрос 4:

{
  "type": 2,
  "id": 281
}

Ответ 2 на запрос 2:

{
  "type": 2,
  "id": 279,
  "payload": {
    "chatId": "1390r4f03f"
  }
}

Ответ 3 на запрос 3:

{
  "type": 2,
  "id": 280,
  "payload": {
    "chatId": "1f4f9f9f39j9f3"
  }
}

Ответ 4 на запрос 1:

{
  "type": 2,
  "id": 278
}

# Стандартный ответ с ошибкой

На большинство запросов в случае какой-либо ошибки будет отправляться ответ следующего вида:

{
  "type": 2,
  "id": 1,
  "payload": {
    "errorCode": 200
  }
}
Категория Код Название Описание
Сетевые ошибки100CONNECTION_ERRORОшибка соединения
101CONNECTION_TIMEOUTИстекло время ожидания соединения
102TLS_ERRORОшибка TLS/SSL
103UNSUPPORTED_PROTOCOLНеподдерживаемый протокол
104ROUTE_NOT_FOUNDМаршрут не найден
Ошибки авторизации200NOT_AUTHORIZEDНе авторизован
201INVALID_CREDENTIALSНеверные учётные данные
202USER_DISABLEDПользователь отключён
203CREDENTIALS_EXPIREDУчётные данные устарели
Ошибки чатов300INTERNAL_ERRORВнутренняя ошибка сервера
301TIMEOUTТаймаут операции
302ACCESS_DENIEDДоступ запрещён
303NOT_ENOUGH_RIGHTSНедостаточно прав
304CHAT_NOT_FOUNDЧат не найден
305USER_IS_NOT_CHAT_PARTICIPANTПользователь не является участником чата
306MESSAGE_NOT_FOUNDСообщение не найдено
307UNKNOWN_MESSAGEНеизвестное сообщение
308FILE_NOT_FOUNDФайл не найден
309USER_ALREADY_IN_CHATПользователь уже состоит в чате

# Структура сообщения

Каждое сообщение, полученное в виде события (запрос от сервера), загруженное по идентификатору сообщения или полученное в истории сообщений чата соответствует объекту Envelope:

{
    "method": "sendMessage",
    "type": 1,
    "id": 3,
    "payload": {
        "chatId": "1c1230635432aa7be051e4fda53a3d5a07c8c151",
        "messageId": "dfb127d0-d174-4e11-8394-19482a98607d",
        "timestamp": 1741881175593,
        "author": {
            "id": "user@video.example.com",
            "type": 1
        },
        "isEdited": false,
        "box": {
            "id": 2,
            "position": "0"
        },
        "type": 200,
        "content": {
            "text": "What's up?",
            "parseMode": "text"
        }
    }
}

С описанием параметров вы можете ознакомиться в данном разделе.

# Хранилище сообщений "Box"

Согласно внутреннему протоколу TrueConf, каждое сообщение помещается в отдельное хранилище - Box, так называемую "коробку" (объект EnvelopeBox):

{
"id" : 1,
"position" : ""
}

В большинстве случаев в коробке будет только одно сообщение, и, для правильной сортировки сообщений достаточно учитывать параметр id. Поле id является автоматическим инкрементируемым числом, начинающимся с нуля в рамках каждого чата. В подавляющем большинстве случаев, первое сообщение в чате будет иметь Box с id = 0, второе с id = 1 и т.д. Поле position, у каждого сообщения, в таком случае, будет пустой строкой.

Однако, в некоторых случаях в одну "коробку" может попасть два и более сообщений. Это очень редкий случай возможный, например, при одновременной отправки двумя и более пользователями сообщений в чат, при этом оба этих пользователя должны испытывать проблемы сетевым соединением.

В случае попадания в одну "коробку" двух и более сообщений поле position заполняется порядковым номером этого сообщения в "коробке". Особенностью является то, что позиция сообщения в коробке является не числом, а строкой, содержащей символы 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz{}.

Для сортировки сообщений с учетом position, достаточно просто сравнить две строки с учетом их ASCII-кодов. Такое сравнение присутствует в большинстве языков программирования по умолчанию.

Сравнение трех сообщений попавших в одну "коробку" между собой:

//Message 1:
{
"id" : 15,
"position" : "A"
}
//Message 2:
{
"id" : 15,
"position" : "B"
}
//Message 3:
{
"id" : 15,
"position" : "AAA"
}

При "обычном" сравнении строк по очереди для каждого символа в строках сравнивается их ASCII порядковый номер, и, строка у которой порядковый номер больше, считается большей. В случае, если порядковые номера всех символов равны, то большей будет та строка, у которой большее количество символов.

Сравните первое и второе сообщение:

ASCII("A") = 41
ASCII("B") = 42
42 > 41

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

Сравните второе и третье сообщение:

ASCII("B") = 42
ASCII("AAA") = 41 41 41
42 < 41

Как видно, проверка не идет дальше первого символа, т.к. символ B имеет больший порядковый номер чем символ А. Таким образом, третье сообщение имеет порядковый номер меньше, чем второе.

Сравните первое и третье сообщение:

ASCII("A") = 41
ASCII("AAA") = 41 41 41
41 = 41

Порядковые номера символов в обоих строках совпадают, и в таком случае больше строкой считается та, у которой большее количество символов. Таким образом, третье сообщение имеет порядковый номер больше чем первое.

После проведенной сортировки, порядок сообщений станет таким:

//Message 1:
{
  "id": 15,
  "position": "A"
}
//Message 3:
{
"id" : 15,
"position" : "AAA"
}
//Message 2:
{
"id" : 15,
"position" : "B"
}
Обновлено: 19.07.2025