WebSocket
Mensagens instantâneas entre o servidor e o cliente.
Introdução
Com WebSocket é possível estabelecer uma conexão permanente entre o servidor e o navegador do cliente.
Isto quer dizer que a qualquer momento o navegador poderá utilizar a conexão estabelecida via WebSocket para enviar dados para o servidor, sem ser obrigatório obter uma resposta. O mesmo acontece para o servidor que poderá contactar o navegador do cliente enviando dados de forma imediata.
A comunicação é realizada instantaneamente e a qualquer momento é possível transitar dados de um lado para o outro de forma independente e imediata.
É muito útil para realizar operações de realtime com comunicações imediatas, é como se fosse um chat, um bate-papo entre o navegador e o servidor, a qualquer momento qualquer um pode enviar uma mensagem e o outro recebe.
Por exemplo, é muito utilizado desde soluções de chats no geral até jogos, utilizamos também para apresentar alteração de dados ao vivo, ou seja, quando existe a necessidade de realizar comunicação o mais imediata possível.
De forma simplificada a diferença entre a comunicação clássica HTTP e o WebSocket:
Do lado do servidor é criado um endpoint
, endereço que permite estabelecer conexões com WebSocket.
O servidor tem a capacidade de enviar dados apenas para uma conexão específica, ou seja, é possível enviar novas mensagens para um participante de um chat em específico.
Também o servidor pode fazer broadcast
que é o envio de dados para todas as conexões ativas num determinado
endereço, ou seja, quando é enviado uma mensagem para todos os participantes do chat que estão em uma sala ou em um
grupo.
Ativação e Configuração
Para ativar e configurar o WebSocket na sua aplicação Netuno é necessário editar o arquivo de configuração da aplicação referente ao ambiente que está sendo utilizado, como:
📂 config/_development.json
📂 config/_production.json
Insira e ajuste os seguintes parâmetros:
...
"ws": {
"endpoints": [
{
"name": "pool",
"enabled": true,
"sendTimeout": 10000,
"idleTimeout": 0,
"maxText": 15000,
"public": "/ws/pool",
"path": "/",
"service": "/services/ws/pool"
},
{
"name": "room",
"enabled": true,
"sendTimeout": 10000,
"idleTimeout": 0,
"maxText": 15000,
"public": "/ws/room",
"path": "/{uid}",
"service": "/services/ws/room"
}
]
},
...
endpoints
É um array que permite definir que o servidor vai suportar WebSocket através dos múltiplos endereços públicos específicados (endpoints), então cada endpoint é um endereço público que suporta receber conexões via WebSockets a partir dos navegadores.
name
Nome de identificação do endpoint.
enabled
Permite ativar e desativar o endpoint, o padrão é true
(ativo).
sendTimeout
Limite de tempo máximo para enviar uma mensagem para os clientes, o padrão é 60000
milissegundos, ou seja, equivale
a 1 minuto.
idleTimeout
Limite de tempo máximo para a inatividade na conexão, o padrão é 300000
milissegundos, ou seja, equivale a 5 minutos.
maxText
Limite em bytes para o tamanho máximo do comprimento da mensagem, o padrão é 1048576
equivalente a 1 megabyte.
public
Define a base do endereço público que permitirá receber as conexões via WebSocket vinda dos navegadores.
O endereço final é constituído pelo valor da configuração public
seguido pelo que está definido no path
.
path
Define a parte final do caminho do endereço público, pode ser dinâmico, o que permite criar por exemplos múltiplos canais separados, por exemplo é útil para criar múltiplas salas.
Atenção: O
path
deve sempre iniciar com/
, e na configuração de conexão no front-end é muito importante conter a barra exatamente como definido nopath
.
No front-end o endereço final será o public
+ path
, veja o exemplo:
...
"public": "/ws/pool",
"path": "/",
...
Então o endereço final no front-end será /ws/pool/
, com a barra no fim.
service
Endereço da base de serviço de back-end da aplicação que processa as conexões e a comunicação via WebSocket.
Serviço para os Endpoints
POST = nova conexão
É executado o serviço configurado com o método POST
quando uma nova conexão é estabelecida, por exemplo:
- JavaScript
- Python
- Ruby
- Kotlin
- Groovy
const uidRoom = _ws.path().getString("uid")
const dbRoom = _db.get("room", uidRoom)
if (dbRoom == null) {
_log.warn(`Invalid room ${uidRoom}.`)
_ws.close()
} else {
_log.info(`New WebSocket Session: ${_ws.sessionId()}`)
}
uidRoom = _ws.path().getString("uid")
dbRoom = _db.get("room", uidRoom)
if dbRoom == null:
_log.warn(f"Invalid room {uidRoom}.")
_ws.close()
else:
_log.info(f"New WebSocket Session: {_ws.sessionId()}")
uidRoom = _ws.path().getString("uid")
dbRoom = _db.get("room", uidRoom)
if dbRoom == null
_log.warn("Invalid room #{uidRoom}.")
_ws.close()
else
_log.info("New WebSocket Session: #{_ws.sessionId()}")
end
val uidRoom = _ws.path().getString("uid")
val dbRoom = _db.get("room", uidRoom)
if (dbRoom == null) {
_log.warn("Invalid room ${uidRoom}.")
_ws.close()
} else {
_log.info("New WebSocket Session: ${_ws.sessionId()}")
}
def uidRoom = _ws.path().getString("uid")
def dbRoom = _db.get("room", uidRoom)
if (dbRoom == null) {
_log.warn("Invalid room ${uidRoom}.")
_ws.close()
} else {
_log.info("New WebSocket Session: ${_ws.sessionId()}")
}
Normalmente o
_ws.sessionId()
é guardado em base de dados para ser utilizado em futuras comunicações.
PUT = nova mensagem
Se a mensagem recebida não for em formato JSON, ou se for um JSON que não está no formato suportado para execução de
um serviço específico (service
), então será executado este serviço genérico:
- JavaScript
- Python
- Ruby
- Kotlin
- Groovy
_log.info(`The session ${_ws.sessionId()} sent this message: ${_ws.message()}`)
_log.info(f"The session {_ws.sessionId()} sent this message: {_ws.message()}")
_log.info("The session #{_ws.sessionId()} sent this message: #{_ws.message()}")
_log.info("The session ${_ws.sessionId()} sent this message: ${_ws.message()}")
_log.info("The session ${_ws.sessionId()} sent this message: ${_ws.message()}")
DELETE = fechou a conexão
É executado quando acontece uma desconexão do navegador do cliente.
- JavaScript
- Python
- Ruby
- Kotlin
- Groovy
_log.info(`Session ${_ws.sessionId()} has disconnected.`)
_log.info(f"Session {_ws.sessionId()} has disconnected.")
_log.info("Session #{_ws.sessionId()} has disconnected.")
_log.info("Session ${_ws.sessionId()} has disconnected.")
_log.info("Session ${_ws.sessionId()} has disconnected.")
GET = fluxo contínuo de mensagens
Inicia a execução quando acontece uma nova conexão do navegador do cliente, e a execução é mantida até finalizar a conexão.
Ou seja, o serviço para o método HTTP GET
fica recebendo as mensagens que estão vindo do cliente continuamente
em stream
, ou seja, é um fluxo aberto e contínuo de mensagens que tando pode receber como enviar.
- JavaScript
- Python
- Ruby
- Kotlin
- Groovy
if (_ws.isStream()) {
while (_ws.awaitStream()) {
if (_ws.isBinaryStreamed()) {
_log.info("Nova mensagem binária recebida: "+ _convert.textFromBytes(_ws.binaryStreamed()))
} else if (_ws.isTextStreamed()) {
_log.info("Nova mensagem de texto recebida: "+ _ws.textStreamed())
}
_ws.sendText("Ok! A mensagem foi recebida com sucesso.")
}
}
if _ws.isStream():
while _ws.awaitStream():
if _ws.isBinaryStreamed():
_log.info("Nova mensagem binária recebida: "+ _convert.textFromBytes(_ws.binaryStreamed()))
elif _ws.isTextStreamed():
_log.info("Nova mensagem de texto recebida: "+ _ws.textStreamed())
_ws.sendText("Ok! A mensagem foi recebida com sucesso.")
if _ws.isStream()
while _ws.awaitStream()
if _ws.isBinaryStreamed()
_log.info("Nova mensagem binária recebida: "+ _convert.textFromBytes(_ws.binaryStreamed()))
elsif _ws.isTextStreamed()
_log.info("Nova mensagem de texto recebida: "+ _ws.textStreamed())
end
_ws.sendText("Ok! A mensagem foi recebida com sucesso.")
end
end
if (_ws.isStream()) {
while (_ws.awaitStream()) {
if (_ws.isBinaryStreamed()) {
_log.info("Nova mensagem binária recebida: "+ _convert.textFromBytes(_ws.binaryStreamed()))
} else if (_ws.isTextStreamed()) {
_log.info("Nova mensagem de texto recebida: "+ _ws.textStreamed())
}
_ws.sendText("Ok! A mensagem foi recebida com sucesso.")
}
}
if (_ws.isStream()) {
while (_ws.awaitStream()) {
if (_ws.isBinaryStreamed()) {
_log.info("Nova mensagem binária recebida: "+ _convert.textFromBytes(_ws.binaryStreamed()))
} else if (_ws.isTextStreamed()) {
_log.info("Nova mensagem de texto recebida: "+ _ws.textStreamed())
}
_ws.sendText("Ok! A mensagem foi recebida com sucesso.")
}
}
O if
com o _ws.isStream()
faz com que verifique se realmente é um pedido interno gerido pelo WebSocket para
realizar o stream.
No while
com o _ws.awaitStream()
fica aguardando que um novo fluxo de mensagem chegue, ou seja, aguarda a
chegada de uma nova mensagem. Assim que houver uma nova mensagem é executado o código interno do while
.
O
_ws.awaitStream()
funciona como um loop infinito aguardando uma nova mensagem.
Então nos ifs
internos do while
é verificado o tipo de mensagem, se a mensagem é binária ou de texto, em ambos
os casos é apresentado nos logs o conteúdo destas mensagens. Repare que qualquer formato de mensagem pode ser
utilizado, podendo ser processadas de qualquer maneira que seja conveniente.
Finalmente no _ws.sendText
é enviada uma mensagem de resposta ao cliente, confirma a recepção.
Cada execução do serviço
GET
será uma conexão de ativa.
Quando a conexão com o endpoint do WebSocket for finalizada então a execução deste serviço é finalizada internamente.
Comunicação
sendService
Executa o serviço e envia o output gerado para um cliente específico conectado através do WebSocket, utiliza o ID de
conexão do cliente (sessionId
).
Exemplo da Chamada de Serviço
O código abaixo pode ser utilizado no serviço de método PUT do endpoint (service
), repare que está sendo
utilizado o _ws.sessionId()
que obtém o ID da sessão de conexão do cliente, se o identificador de sessão
estiver guardado em base de dados o sendService
poderia ser executado em qualquer outro serviço a qualquer
momento.
- JavaScript
- Python
- Ruby
- Kotlin
- Groovy
const uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), // Identificação da conexão atual do WebSocket.
_val.map() // Parametrização do serviço que será executado.
.set("method", "POST")
.set("service", "/services/room/participant/new")
.set(
"data", // Parâmetros que serão passados para o serviço e recebidos através do _req.
_val.map()
.set("uid", uid)
)
)
uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), # Identificação da conexão atual do WebSocket.
_val.map() # Parametrização do serviço que será executado.
.set("method", "POST")
.set("service", "/services/room/participant/new")
.set(
"data", # Parâmetros que serão passados para o serviço e recebidos através do _req.
_val.map()
.set("uid", uid)
)
)
uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), # Identificação da conexão atual do WebSocket.
_val.map() # Parametrização do serviço que será executado.
.set("method", "POST")
.set("service", "/services/room/participant/new")
.set(
"data", # Parâmetros que serão passados para o serviço e recebidos através do _req.
_val.map()
.set("uid", uid)
)
)
val uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), // Identificação da conexão atual do WebSocket.
_val.map() // Parametrização do serviço que será executado.
.set("method", "POST")
.set("service", "/services/room/participant/new")
.set(
"data", // Parâmetros que serão passados para o serviço e recebidos através do _req.
_val.map()
.set("uid", uid)
)
)
def uid = _ws.path().getString("uid")
_ws.sendService(
_ws.sessionId(), // Identificação da conexão atual do WebSocket.
_val.map() // Parametrização do serviço que será executado.
.set("method", "POST")
.set("service", "/services/room/participant/new")
.set(
"data", // Parâmetros que serão passados para o serviço e recebidos através do _req.
_val.map()
.set("uid", uid)
)
)
Ou seja, na configuração o path
que está configurado no endpoint
tem o valor /{uid}
, assim permite o cliente
conectar através do endereço público:
/ws/room/d641f095-30eb-4025-a39c-b2e3e497eab7
Então o valor do uid
será d641f095-30eb-4025-a39c-b2e3e497eab7
.
Após será executado o serviço que fica na URL /services/room/participant/new
e o valor do uid
é passado.
O uid
pode ser obtido no código do serviço que é executado, como um parâmetro do pedido HTTP, veja um exemplo:
- JavaScript
- Python
- Ruby
- Kotlin
- Groovy
// O UID enviado é obtido diretamente no request.
const uid = _req.getString('uid')
# O UID enviado é obtido diretamente no request.
uid = _req.getString('uid')
# O UID enviado é obtido diretamente no request.
uid = _req.getString('uid')
// O UID enviado é obtido diretamente no request.
val uid = _req.getString('uid')
// O UID enviado é obtido diretamente no request.
def uid = _req.getString('uid')
broadcastService
Para realizar o broadcast é preciso o nome do endpoint do WebSocket, definir o caminho (path
) se aplicável, e o
serviço que será executado.
O output do serviço será enviado para todas as conexões ativas no
endpoint
especificado.
Exemplo de Broadcast
- JavaScript
- Python
- Ruby
- Kotlin
- Groovy
_ws.broadcastService(
"admin", // Nome do endpoint do WebSocket.
"/", // Caminho configurado no endpoint (path).
_val.map() // Parametrização do serviço que será executado.
.set("service", "/services/room/participant/list")
)
_ws.broadcastService(
"admin", # Nome do endpoint do WebSocket.
"/", # Caminho configurado no endpoint (path).
_val.map() # Parametrização do serviço que será executado.
.set("service", "/services/room/participant/list")
)
_ws.broadcastService(
"admin", # Nome do endpoint do WebSocket.
"/", # Caminho configurado no endpoint (path).
_val.map() # Parametrização do serviço que será executado.
.set("service", "/services/room/participant/list")
)
_ws.broadcastService(
"admin", // Nome do endpoint do WebSocket.
"/", // Caminho configurado no endpoint (path).
_val.map() // Parametrização do serviço que será executado.
.set("service", "/services/room/participant/list")
)
_ws.broadcastService(
"admin", // Nome do endpoint do WebSocket.
"/", // Caminho configurado no endpoint (path).
_val.map() // Parametrização do serviço que será executado.
.set("service", "/services/room/participant/list")
)
Neste exemplo o conteúdo de output gerado pelo serviço room/participant/list
será enviado para todos que estiverem
conectados no endpoint admin
.
WS Client - NPM
Para realizar facilmente a integração com o frontend, é disponibilizado o módulo do NPM:
Comando de instalação:
pnpm install @netuno/ws-client
_ws.config({
url: 'ws://localhost:9000/ws/example',
servicesPrefix: '/services',
method: 'GET',
autoReconnect: true,
connect: (event) => {
console.info('WebSocket Connected', event);
},
close: (event) => {
console.warn('WebSocket Closed', event);
},
error: (error) => {
console.error('WebSocket Error', error);
},
message: (data, event) => {
console.debug('WebSocket Message', data);
}
});
A seguir pode iniciar a conexão:
_ws.connect();
Executar Serviço via WebSocket
Podemos executar serviços da API REST através da conexão WebSocket, isso quer dizer que não será realizado nenhum pedido HTTP, então os dados do pedido são enviados como mensagem, e a resposta do serviço é obtida como uma mensagem recebida de volta.
Antes de iniciar devemos criar um observador da resposta do serviço, por exemplo:
const listenerRef = _ws.addListener({
method: 'GET', // Opcional, por padrão é GET.
service: 'caminho/do/meu/servico/aqui',
success: (data) => {
console.log('Resposta do serviço executado via WebSocket:', data);
},
fail: (error)=> {
console.log('O serviço executado via WebSocket falhou:', error);
}
});
Agora podemos enviar um pedido de execução do serviço, sendo que a resposta do serviço será obtida no observador criado acima, executamos o serviço assim:
_ws.sendService({
method: 'GET', // Opcional, outro método HTTP pode ser utilizado.
service: 'caminho/do/meu/servico/aqui',
data: {
message: 'Olá...'
}
});
Para remover o observador adicionado, utilize:
_ws.removeListener(listenerRef);
E para fechar a conexão WebSocket:
_ws.close();
NGINX Proxy Reverso
No NGINX o proxy reverso pode ser configurado desta forma:
location /ws {
proxy_pass http://localhost:9000;
proxy_set_header Host minha-app.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 "";
}
Na linha 3 onde define o
Host
deve ser ajustado a parte daminha-app
para o nome correspondente da aplicação Netuno. Se houver_
no nome da aplicação então no endereço doHost
será substituído por-
, por exemplo, a aplicaçãominha_app
passa a ter o valor doHost
assimminha-app.local.netu.no
.