Pular para o conteúdo principal

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:

classic-http-vs-websoket

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 no path.

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:

server/services/ws/room/post.js
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()}`)
}

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:

server/services/ws/room/put.js
_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.

server/services/ws/room/delete.js
_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.

server/services/ws/room/get.js
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.

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)
)
)

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:

/server/services/room/participant/new.js
// O UID enviado é obtido diretamente no request.
const 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

/server/services/room/participant/new.js
_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 da minha-app para o nome correspondente da aplicação Netuno. Se houver _ no nome da aplicação então no endereço do Host será substituído por -, por exemplo, a aplicação minha_app passa a ter o valor do Host assim minha-app.local.netu.no.