# Базовый формат сообщений 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
}
}
Категория | Код | Название | Описание |
---|---|---|---|
Сетевые ошибки | 100 | CONNECTION_ERROR | Ошибка соединения |
101 | CONNECTION_TIMEOUT | Истекло время ожидания соединения | |
102 | TLS_ERROR | Ошибка TLS/SSL | |
103 | UNSUPPORTED_PROTOCOL | Неподдерживаемый протокол | |
104 | ROUTE_NOT_FOUND | Маршрут не найден | |
Ошибки авторизации | 200 | NOT_AUTHORIZED | Не авторизован |
201 | INVALID_CREDENTIALS | Неверные учётные данные | |
202 | USER_DISABLED | Пользователь отключён | |
203 | CREDENTIALS_EXPIRED | Учётные данные устарели | |
Ошибки чатов | 300 | INTERNAL_ERROR | Внутренняя ошибка сервера |
301 | TIMEOUT | Таймаут операции | |
302 | ACCESS_DENIED | Доступ запрещён | |
303 | NOT_ENOUGH_RIGHTS | Недостаточно прав | |
304 | CHAT_NOT_FOUND | Чат не найден | |
305 | USER_IS_NOT_CHAT_PARTICIPANT | Пользователь не является участником чата | |
306 | MESSAGE_NOT_FOUND | Сообщение не найдено | |
307 | UNKNOWN_MESSAGE | Неизвестное сообщение | |
308 | FILE_NOT_FOUND | Файл не найден | |
309 | USER_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"
}