WebSocket
Мгновенный обмен сообщениями между сервером и клиентом.
Введение
С помощью WebSocket можно установить постоянное соединение между сервером и браузером клиента.
Это означает, что в любой момент браузер может использовать установленное через WebSocket соединение для отправки данных на сервер без обязательного получения ответа. То же самое относится к серверу, который может связываться с браузером клиента, отправляя данные немедленно.
Обмен происходит мгновенно, и в любой момент можно передавать данные в обе стороны независимо и сразу.
Это очень полезно для выполнения операций в реальном времени с мгновенной связью: это как чат, беседа между браузером и сервером, когда в любой момент любой участник может отправить сообщение, а другой его получит.
Например, это широко используется — от чатов в целом до игр; мы также применяем это для отображения изменений данных в реальном времени, то есть когда есть необходимость в максимально быстрой связи.
В упрощённом виде разница между классической связью HTTP и WebSocket:

На стороне сервера создаётся endpoint — адрес, позволяющий устанавливать соединения через WebSocket.
Сервер может отправлять данные только в конкретное соединение, то есть можно отправлять новые сообщения конкретному участнику чата.
Также сервер может выполнять broadcast — отправку данных всем активным соединениям по определённому
адресу, то есть когда сообщение отправляется всем участникам чата, находящимся в комнате или
группе.
Активация и настройка
Чтобы активировать и настроить WebSocket в вашем приложении Netuno, необходимо отредактировать файл конфигурации приложения для используемой среды, например:
📂 config/_development.json📂 config/_production.json
Добавьте и настройте следующие параметры:
...
"ws": {
"hosts": [...], "<-- НЕОБЯЗАТЕЛЬНО": "подробнее в документации ниже.",
"endpoints": [
{
"name": "obshchiy",
"enabled": true,
"sendTimeout": 10000,
"idleTimeout": 0,
"maxText": 15000,
"public": "/ws/obshchiy",
"path": "/",
"service": "/services/ws/obshchiy"
},
{
"name": "komnata",
"enabled": true,
"sendTimeout": 10000,
"idleTimeout": 0,
"maxText": 15000,
"public": "/ws/komnata",
"path": "/{uid}",
"service": "/services/ws/komnata"
}
]
},
...
endpoints
Это массив, позволяющий определить, что сервер будет поддерживать WebSocket через несколько указанных публичных адресов (endpoints), то есть каждый endpoint — это публичный адрес, поддерживающий приём соединений через WebSocket из браузеров.
name
Идентификационное имя endpoint.
enabled
Позволяет включать и отключать endpoint; значение по умолчанию — true (активен).
sendTimeout
Максимальное время для отправки сообщения клиентам; значение по умолчанию — 60000 миллисекунд, то есть
1 минута.
idleTimeout
Максимальное время неактивности соединения; значение по умолчанию — 300000 миллисекунд, то есть 5 минут.
maxText
Ограничение в байтах для максимальной длины сообщения; значение по умолчанию — 1048576, что эквивалентно 1 мегабайту.
public
Определяет базу публичного адреса, который позволит принимать соединения через WebSocket из браузеров.
Итоговый адрес состоит из значения настройки public, за которым следует то, что определено в path.
path
Определяет конечную часть пути публичного адреса; может быть динамическим, что позволяет, например, создавать несколько отдельных каналов, например это полезно для создания нескольких комнат.
Внимание:
pathвсегда должен начинаться с/, а в конфигурации подключения на фронтенде очень важно указывать слэш именно так, как определено вpath.
На фронтенде итоговый адрес будет public + path, см. пример:
...
"public": "/ws/obshchiy",
"path": "/",
...
Тогда итоговый адрес на фронтенде будет /ws/obshchiy/, со слэшем в конце.
service
Адрес базы бэкенд-сервиса приложения, который обрабатывает соединения и связь через WebSocket.
hosts
Опционально можно определить хосты, на которых доступны наши endpoints, то есть при подключении в HTTP-заголовке приходит адрес Host; учитывается только информация о домене или IP этого хоста, а указание порта отбрасывается — это используется для сопоставления и поиска соответствующего endpoint.
Поэтому endpoint для корректной работы должен сочетать
hostиpublic+pathследующим образом:
ws://[host]:9000[public][path]В продакшене с NGINX, обратным прокси и SSL это должно выглядеть так:
wss://[host][public][path]
Netuno определяет некоторые локальные домены с префиксом имени приложения, которые автоматически
связываются как значения по умолчанию для хостов endpoints; кроме того, для приложения в разработке также добавляются
localhost и 127.0.0.1 к хостам по умолчанию, поэтому определение хостов необязательно — эта конфигурация
выполняется автоматически.
Следующие хосты определяются автоматически для endpoints, если имя приложения — moeapp:
- Этот направляет напрямую на локальный IP 127.0.0.1:
moeapp.local.netu.no
- Для работы этих хостов необходимо зарегистрировать их в файле hosts операционной системы:
moeapp.localhostmoeapp.local
Если имя приложения — moe_prilozhenie, то автоматическими будут следующие хосты:
- Этот направляет напрямую на локальный IP 127.0.0.1:
moe-prilozhenie.local.netu.no
- Для работы этих хостов необходимо зарегистрировать их в файле hosts операционной системы:
moe-prilozhenie.localhostmoe-prilozhenie.local
В домене не допускается
_(подчёркивание), а в имени приложений не допускается-(дефис), поэтому снаружи мы используем-(дефис) в адресе, но Netuno автоматически преобразует-(дефис) в_(подчёркивание), чтобы найти нужное приложение.
Приложение по умолчанию или приложение, указанное при запуске сервера Netuno, дополнительно имеет следующие хосты:
localhost127.0.0.1
Для определения пользовательских хостов используем следующие настройки:
...
"ws": {
"endpoints": [
{
"hosts": ["moy-domen1.com", "www.moy-domen1.com"],
...
},
{
"hosts": ["moy-domen2.com", "www.moy-domen2.com"],
...
}
]
},
...
Если все endpoints работают для одних и тех же хостов, можно определить их глобально так:
...
"ws": {
"hosts": ["moy-domen.com", "www.moy-domen1.com"],
"endpoints": [
...
]
},
...
Убедитесь, что в итоговом адресе WebSocket всегда используется домен или IP, являющийся допустимым хостом, иначе это не будет работать, потому что endpoint не удастся найти внутри.
Сервис для эндпоинтов
POST = новое соединение
Сервис, настроенный для метода POST, выполняется при установке нового соединения, например:
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
const uidKomnaty = _ws.path().getString("uid")
const dbKomnaty = _db.get("komnata", uidKomnaty)
if (dbKomnaty == null) {
_log.warn(`Недопустимая комната ${uidKomnaty}.`)
_ws.close()
} else {
_log.info(`Новая сессия WebSocket: ${_ws.sessionId()}`)
}
const uidKomnaty = _ws.path().getString("uid")
const dbKomnaty = _db.get("komnata", uidKomnaty)
if (dbKomnaty == null) {
_log.warn(`Недопустимая комната ${uidKomnaty}.`)
_ws.close()
} else {
_log.info(`Новая сессия WebSocket: ${_ws.sessionId()}`)
}
uidKomnaty = _ws.path().getString("uid")
dbKomnaty = _db.get("komnata", uidKomnaty)
if dbKomnaty == null:
_log.warn(f"Недопустимая комната {uidKomnaty}.")
_ws.close()
else:
_log.info(f"Новая сессия WebSocket: {_ws.sessionId()}")
uidKomnaty = _ws.path().getString("uid")
dbKomnaty = _db.get("komnata", uidKomnaty)
if dbKomnaty == null
_log.warn("Недопустимая комната #{uidKomnaty}.")
_ws.close()
else
_log.info("Новая сессия WebSocket: #{_ws.sessionId()}")
end
val uidKomnaty = _ws.path().getString("uid")
val dbKomnaty = _db.get("komnata", uidKomnaty)
if (dbKomnaty == null) {
_log.warn("Недопустимая комната ${uidKomnaty}.")
_ws.close()
} else {
_log.info("Новая сессия WebSocket: ${_ws.sessionId()}")
}
def uidKomnaty = _ws.path().getString("uid")
def dbKomnaty = _db.get("komnata", uidKomnaty)
if (dbKomnaty == null) {
_log.warn("Недопустимая комната ${uidKomnaty}.")
_ws.close()
} else {
_log.info("Новая сессия WebSocket: ${_ws.sessionId()}")
}
Обычно
_ws.sessionId()сохраняется в базе данных для использования в будущих коммуникациях.
PUT = новое сообщение
Если полученное сообщение не в формате JSON или если это JSON, который не в поддерживаемом формате для выполнения
конкретного сервиса (service), тогда будет выполнен этот универсальный сервис:
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
_log.info(`Сессия ${_ws.sessionId()} отправила это сообщение: ${_ws.message()}`)
_log.info(`Сессия ${_ws.sessionId()} отправила это сообщение: ${_ws.message()}`)
_log.info(f"Сессия {_ws.sessionId()} отправила это сообщение: {_ws.message()}")
_log.info("Сессия #{_ws.sessionId()} отправила это сообщение: #{_ws.message()}")
_log.info("Сессия ${_ws.sessionId()} отправила это сообщение: ${_ws.message()}")
_log.info("Сессия ${_ws.sessionId()} отправила это сообщение: ${_ws.message()}")
DELETE = соединение закрыто
Выполняется при отключении браузера клиента.
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
_log.info(`Сессия ${_ws.sessionId()} отключилась.`)
_log.info(`Сессия ${_ws.sessionId()} отключилась.`)
_log.info(f"Сессия {_ws.sessionId()} отключилась.")
_log.info("Сессия #{_ws.sessionId()} отключилась.")
_log.info("Сессия ${_ws.sessionId()} отключилась.")
_log.info("Сессия ${_ws.sessionId()} отключилась.")
GET = непрерывный поток сообщений
Запускается при установке нового соединения браузера клиента, и выполнение продолжается до завершения соединения.
То есть сервис для HTTP-метода GET непрерывно получает сообщения, приходящие от клиента
в stream, то есть это открытый и непрерывный поток сообщений, который может как принимать, так и отправлять.
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
if (_ws.isStream()) {
while (_ws.awaitStream()) {
if (_ws.isBinaryStreamed()) {
_log.info("Получено новое бинарное сообщение: "+ _convert.textFromBytes(_ws.binaryStreamed()))
} else if (_ws.isTextStreamed()) {
_log.info("Получено новое текстовое сообщение: "+ _ws.textStreamed())
}
_ws.sendText("Ок! Сообщение успешно получено.")
}
}
if (_ws.isStream()) {
while (_ws.awaitStream()) {
if (_ws.isBinaryStreamed()) {
_log.info("Получено новое бинарное сообщение: "+ _convert.textFromBytes(_ws.binaryStreamed()))
} else if (_ws.isTextStreamed()) {
_log.info("Получено новое текстовое сообщение: "+ _ws.textStreamed())
}
_ws.sendText("Ок! Сообщение успешно получено.")
}
}
if _ws.isStream():
while _ws.awaitStream():
if _ws.isBinaryStreamed():
_log.info("Получено новое бинарное сообщение: "+ _convert.textFromBytes(_ws.binaryStreamed()))
elif _ws.isTextStreamed():
_log.info("Получено новое текстовое сообщение: "+ _ws.textStreamed())
_ws.sendText("Ок! Сообщение успешно получено.")
if _ws.isStream()
while _ws.awaitStream()
if _ws.isBinaryStreamed()
_log.info("Получено новое бинарное сообщение: "+ _convert.textFromBytes(_ws.binaryStreamed()))
elsif _ws.isTextStreamed()
_log.info("Получено новое текстовое сообщение: "+ _ws.textStreamed())
end
_ws.sendText("Ок! Сообщение успешно получено.")
end
end
if (_ws.isStream()) {
while (_ws.awaitStream()) {
if (_ws.isBinaryStreamed()) {
_log.info("Получено новое бинарное сообщение: "+ _convert.textFromBytes(_ws.binaryStreamed()))
} else if (_ws.isTextStreamed()) {
_log.info("Получено новое текстовое сообщение: "+ _ws.textStreamed())
}
_ws.sendText("Ок! Сообщение успешно получено.")
}
}
if (_ws.isStream()) {
while (_ws.awaitStream()) {
if (_ws.isBinaryStreamed()) {
_log.info("Получено новое бинарное сообщение: "+ _convert.textFromBytes(_ws.binaryStreamed()))
} else if (_ws.isTextStreamed()) {
_log.info("Получено новое текстовое сообщение: "+ _ws.textStreamed())
}
_ws.sendText("Ок! Сообщение успешно получено.")
}
}
if с _ws.isStream() проверяет, действительно ли это внутренний запрос, управляемый WebSocket, для
выполнения stream.
В while с _ws.awaitStream() происходит ожидание прихода нового потока сообщений, то есть ожидание
поступления нового сообщения. Как только появляется новое сообщение, выполняется внутренний код while.
_ws.awaitStream()работает как бесконечный цикл в ожидании нового сообщения.
Затем во внутренних ifs цикла while проверяется тип сообщения: бинарное оно или текстовое; в обоих
случаях содержимое этих сообщений выводится в логи. Обратите внимание, что может использоваться любой формат
сообщений, и их можно обрабатывать любым удобным способом.
Наконец, в _ws.sendText клиенту отправляется ответное сообщение, подтверждающее получение.
Каждое выполнение сервиса
GETбудет отдельным активным соединением.
Когда соединение с endpoint WebSocket завершается, выполнение этого сервиса завершается внутренне.
Связь
sendService
Выполняет сервис и отправляет сгенерированный вывод конкретному клиенту, подключённому через WebSocket, используя ID
соединения клиента (sessionId).
Пример вызова сервиса
Код ниже можно использовать в сервисе метода PUT endpoint (service); обратите внимание, что используется
_ws.sessionId(), который получает ID сессии соединения клиента. Если идентификатор сессии
сохранён в базе данных, sendService можно выполнить в любом другом сервисе в любой
момент.
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
const uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), // Идентификация текущего соединения WebSocket.
_val.map() // Параметризация сервиса, который будет выполнен.
.set("method", "POST")
.set("service", "/services/komnata/uchastnik/novyy")
.set(
"data", // Параметры, которые будут переданы в сервис и получены через _req.
_val.map()
.set("uid", uid)
)
)
const uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), // Идентификация текущего соединения WebSocket.
_val.map() // Параметризация сервиса, который будет выполнен.
.set("method", "POST")
.set("service", "/services/komnata/uchastnik/novyy")
.set(
"data", // Параметры, которые будут переданы в сервис и получены через _req.
_val.map()
.set("uid", uid)
)
)
uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), # Идентификация текущего соединения WebSocket.
_val.map() # Параметризация сервиса, который будет выполнен.
.set("method", "POST")
.set("service", "/services/komnata/uchastnik/novyy")
.set(
"data", # Параметры, которые будут переданы в сервис и получены через _req.
_val.map()
.set("uid", uid)
)
)
uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), # Идентификация текущего соединения WebSocket.
_val.map() # Параметризация сервиса, который будет выполнен.
.set("method", "POST")
.set("service", "/services/komnata/uchastnik/novyy")
.set(
"data", # Параметры, которые будут переданы в сервис и получены через _req.
_val.map()
.set("uid", uid)
)
)
val uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), // Идентификация текущего соединения WebSocket.
_val.map() // Параметризация сервиса, который будет выполнен.
.set("method", "POST")
.set("service", "/services/komnata/uchastnik/novyy")
.set(
"data", // Параметры, которые будут переданы в сервис и получены через _req.
_val.map()
.set("uid", uid)
)
)
final uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), // Идентификация текущего соединения WebSocket.
_val.map() // Параметризация сервиса, который будет выполнен.
.set("method", "POST")
.set("service", "/services/komnata/uchastnik/novyy")
.set(
"data", // Параметры, которые будут переданы в сервис и получены через _req.
_val.map()
.set("uid", uid)
)
)
То есть в конфигурации path, настроенный в endpoint, имеет значение /{uid}, что позволяет клиенту
подключаться через публичный адрес:
/ws/komnata/d641f095-30eb-4025-a39c-b2e3e497eab7
Тогда значением uid будет d641f095-30eb-4025-a39c-b2e3e497eab7.
После этого будет выполнен сервис, находящийся по URL /services/komnata/uchastnik/novyy, и будет передан uid.
uid можно получить в коде выполняемого сервиса как параметр HTTP-запроса, см. пример:
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
// UID, отправленный в запросе, получается напрямую в request.
const uid = _req.getString('uid')
// UID, отправленный в запросе, получается напрямую в request.
const uid = _req.getString('uid')
# UID, отправленный в запросе, получается напрямую в request.
uid = _req.getString('uid')
# UID, отправленный в запросе, получается напрямую в request.
uid = _req.getString('uid')
// UID, отправленный в запросе, получается напрямую в request.
val uid = _req.getString('uid')
// UID, отправленный в запросе, получается напрямую в request.
final uid = _req.getString('uid')
broadcastService
Чтобы выполнить broadcast, нужно имя endpoint WebSocket, определить путь (path), если применимо, и
сервис, который будет выполнен.
Вывод сервиса будет отправлен всем активным соединениям на указанном
endpoint.
Пример широковещательной рассылки
- JavaScript
- TypeScript
- Python
- Ruby
- Kotlin
- Groovy
_ws.broadcastService(
"admin", // Имя endpoint WebSocket.
"/", // Путь, настроенный в endpoint (path).
_val.map() // Параметризация сервиса, который будет выполнен.
.set("service", "/services/komnata/uchastnik/spisok")
)
_ws.broadcastService(
"admin", // Имя endpoint WebSocket.
"/", // Путь, настроенный в endpoint (path).
_val.map() // Параметризация сервиса, который будет выполнен.
.set("service", "/services/komnata/uchastnik/spisok")
)
_ws.broadcastService(
"admin", # Имя endpoint WebSocket.
"/", # Путь, настроенный в endpoint (path).
_val.map() # Параметризация сервиса, который будет выполнен.
.set("service", "/services/komnata/uchastnik/spisok")
)
_ws.broadcastService(
"admin", # Имя endpoint WebSocket.
"/", # Путь, настроенный в endpoint (path).
_val.map() # Параметризация сервиса, который будет выполнен.
.set("service", "/services/komnata/uchastnik/spisok")
)
_ws.broadcastService(
"admin", // Имя endpoint WebSocket.
"/", // Путь, настроенный в endpoint (path).
_val.map() // Параметризация сервиса, который будет выполнен.
.set("service", "/services/komnata/uchastnik/spisok")
)
_ws.broadcastService(
"admin", // Имя endpoint WebSocket.
"/", // Путь, настроенный в endpoint (path).
_val.map() // Параметризация сервиса, который будет выполнен.
.set("service", "/services/komnata/uchastnik/spisok")
)
В этом примере содержимое вывода, сгенерированного сервисом komnata/uchastnik/spisok, будет отправлено всем, кто
подключён к endpoint admin.
WS-клиент - NPM
Для удобной интеграции с фронтендом предоставляется модуль NPM:
Команда установки:
bun install @netuno/ws-client
import _ws from '@netuno/ws-client';
_ws.config({
url: 'ws://localhost:9000/ws/primer/',
servicesPrefix: '/services',
autoReconnect: true,
connect: (sobytie) => {
console.info('WebSocket подключён', sobytie);
},
close: (sobytie) => {
console.warn('WebSocket закрыт', sobytie);
},
error: (oshibka) => {
console.error('Ошибка WebSocket', oshibka);
},
message: (dannye, sobytie) => {
console.debug('Сообщение WebSocket', dannye);
}
});
Убедитесь, что URL правильно сочетает значения, настроенные в endpoint для
hostиpublic+path, следующим образом:
ws://[host]:9000[public][path]А URL в продакшене с интеграцией NGINX, обратного прокси и SSL, вероятно, будет таким:
wss://[host][public][path]
Затем можно инициировать соединение:
_ws.connect();
Выполнение сервиса через WebSocket
Мы можем выполнять сервисы REST API через соединение WebSocket; это означает, что не будет выполняться никакой HTTP-запрос, поэтому данные запроса отправляются как сообщение, а ответ сервиса получается как принятое обратно сообщение.
Перед началом нужно создать наблюдатель ответа сервиса, например:
const ssylkaNaNablyudatelya = _ws.addListener({
method: 'GET', // Необязательно; пропустите, чтобы слушать все методы.
service: 'put/k/moemu/servisu/zdes',
success: (dannye) => {
console.log('Ответ сервиса, выполненного через WebSocket:', dannye);
},
fail: (oshibka)=> {
console.log('Сервис, выполненный через WebSocket, завершился ошибкой:', oshibka);
}
});
Теперь можно отправить запрос на выполнение сервиса; ответ сервиса будет получен в наблюдателе, созданном выше. Выполняем сервис так:
_ws.sendService({
method: 'GET', // Необязательно; запросы используют GET, если не указан.
service: 'put/k/moemu/servisu/zdes',
data: {
message: 'Привет...'
}
});
Чтобы удалить добавленный наблюдатель, используйте:
_ws.removeListener(ssylkaNaNablyudatelya);
А чтобы закрыть соединение WebSocket:
_ws.close();
Обратный прокси NGINX
В NGINX обратный прокси можно настроить так:
location /ws {
proxy_pass http://localhost:9000;
proxy_set_header Host moe-prilozhenie.local.netu.no;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_http_version 1.1;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
add_header X-Frame-Options "";
}
В строке 3, где определяется
Host, частьmoe-prilozhenieнужно заменить на соответствующее имя приложения Netuno. Если в имени приложения есть_, то в адресеHostон заменяется на-, например, приложениеmoe_prilozhenieбудет иметь значениеHostmoe-prilozhenie.local.netu.no.
Перезапустите NGINX, чтобы конфигурация вступила в силу.
Домены, настроенные в NGINX, то есть параметр server_name, не обязательно должны быть в hosts
endpoint приложения Netuno, потому что в конфигурации прокси выше Host в HTTP-заголовке изменяется
на стандартный адрес, который всегда автоматически настраивается Netuno, если префикс с именем
приложения указан правильно.
Если сервер NGINX использует порт 80, итоговый URL будет:
ws://[host][public][path]
А с SSL/HTTPS, если сервер NGINX использует порт 443, итоговый URL будет:
wss://[host][public][path]