O WebSocket
protocolo, descrito na especificação RFC 6455 fornece uma forma de troca de dados entre o navegador e o servidor através de uma ligação persistente. Os dados podem ser passados em ambas as direcções como “pacotes”, sem quebrar a ligação e pedidos HTTP adicionais.
WebSocket é especialmente bom para serviços que requerem troca contínua de dados, por exemplo, jogos online, sistemas de negociação em tempo real e assim por diante.
Um exemplo simples
Para abrir uma ligação websocket, precisamos de criar new WebSocket
usando o protocolo especial ws
na url:
let socket = new WebSocket("ws://javascript.info");
Também há encriptado wss://
protocolo. É como HTTPS para websockets.
wss://
O protocolo wss://
não só é encriptado, mas também mais fiável.
Isso porque ws://
os dados não são encriptados, visíveis para qualquer intermediário. Os antigos servidores proxy não sabem sobre WebSocket, podem ver cabeçalhos “estranhos” e abortar a ligação.
Por outro lado, wss://
é WebSocket sobre TLS, (o mesmo que HTTPS é HTTP sobre TLS), a camada de segurança do transporte encripta os dados no remetente e descriptografa no receptor. Assim, os pacotes de dados são passados encriptados através de proxies. Não conseguem ver o que está dentro e deixam-nos passar.
Após a tomada ser criada, devemos ouvir os eventos nela contidos. Existem totalmente 4 eventos:
-
open
– ligação estabelecida, -
message
– dados recebidos, -
error
– erro de soquete, -
close
– ligação fechada.
…E se quisermos enviar algo, então socket.send(data)
fará isso.
Aqui está um exemplo:
let socket = new WebSocket("wss://javascript.info/article/websocket/demo/hello");socket.onopen = function(e) { alert(" Connection established"); alert("Sending to server"); socket.send("My name is John");};socket.onmessage = function(event) { alert(` Data received from server: ${event.data}`);};socket.onclose = function(event) { if (event.wasClean) { alert(` Connection closed cleanly, code=${event.code} reason=${event.reason}`); } else { // e.g. server process killed or network down // event.code is usually 1006 in this case alert(' Connection died'); }};socket.onerror = function(error) { alert(` ${error.message}`);};
Para efeitos de demonstração, existe um pequeno servidor servidor.js escrito em Node.js, para o exemplo acima, a correr. Responde com “Olá do servidor, John”, depois espera 5 segundos e fecha a ligação.
Assim verá eventos open
message
close
.
Na verdade é isso, já podemos falar WebSocket. Muito simples, não é?
Agora vamos falar mais a fundo.
Abrir um websocket
Quando new WebSocket(url)
é criado, começa a ligar-se imediatamente.
Durante a ligação o navegador (usando cabeçalhos) pergunta ao servidor: “Você suporta Websocket? E se o servidor responder “sim”, então a conversa continua no protocolo WebSocket, que não é de todo HTTP.
Aqui está um exemplo de cabeçalhos de browser para pedido feito por new WebSocket("wss://javascript.info/chat")
.
GET /chatHost: javascript.infoOrigin: https://javascript.infoConnection: UpgradeUpgrade: websocketSec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==Sec-WebSocket-Version: 13
-
Origin
– a origem da página do cliente, por exemplohttps://javascript.info
. Os objectos do WebSocket são de origem cruzada por natureza. Não há cabeçalhos especiais ou outras limitações. Os servidores antigos são incapazes de lidar com WebSocket de qualquer forma, pelo que não há problemas de compabitividade. MasOrigin
cabeçalho é importante, pois permite ao servidor decidir se deve ou não falar do WebSocket com este website. -
Connection: Upgrade
– sinais de que o cliente gostaria de alterar o protocolo. -
Upgrade: websocket
– o protocolo pedido é “websocket”. -
Sec-WebSocket-Key
– uma chave gerada aleatoriamente pelo navegador para segurança. -
Sec-WebSocket-Version
– versão do protocolo WebSocket, 13 é a actual.
Não podemos usar XMLHttpRequest
ou fetch
para fazer este tipo de pedido HTTP, porque não é permitido JavaScript para definir estes cabeçalhos.
Se o servidor concordar em mudar para WebSocket, deverá enviar o código 101 de resposta:
101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=
Aqui Sec-WebSocket-Accept
Sec-WebSocket-Key
, recodificado usando um algoritmo especial. O navegador usa-o para se certificar de que a resposta corresponde ao pedido.
Depois, os dados são transferidos usando o protocolo WebSocket, veremos a sua estrutura (“frames”) em breve. E isso não é HTTP.
Extensões e sub-protocolos
Podem existir cabeçalhos adicionais Sec-WebSocket-Extensions
e Sec-WebSocket-Protocol
que descrevem extensões e sub-protocolos.
por exemplo:
-
Sec-WebSocket-Extensions: deflate-frame
significa que o navegador suporta compressão de dados. Uma extensão é algo relacionado com a transferência dos dados, funcionalidade que estende o protocolo WebSocket. O cabeçalhoSec-WebSocket-Extensions
é enviado automaticamente pelo navegador, com a lista de todas as extensões que suporta. -
Sec-WebSocket-Protocol: soap, wamp
significa que gostaríamos de transferir não apenas quaisquer dados, mas os dados em protocolos SOAP ou WAMP (“The WebSocket Application Messaging Protocol”). Os subprotocolos WebSocket estão registados no catálogo da IANA. Assim, este cabeçalho descreve formatos de dados que vamos utilizar.
Este cabeçalho opcional é definido usando o segundo parâmetro de new WebSocket
. Este é o conjunto de subprotocolos, por exemplo, se quisermos utilizar SOAP ou WAMP:
let socket = new WebSocket("wss://javascript.info/chat", );
O servidor deve responder com uma lista de protocolos e extensões que concorda em utilizar.
Por exemplo, o pedido:
GET /chatHost: javascript.infoUpgrade: websocketConnection: UpgradeOrigin: https://javascript.infoSec-WebSocket-Key: Iv8io/9s+lYFgZWcXczP8Q==Sec-WebSocket-Version: 13Sec-WebSocket-Extensions: deflate-frameSec-WebSocket-Protocol: soap, wamp
Resposta:
101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=Sec-WebSocket-Extensions: deflate-frameSec-WebSocket-Protocol: soap
Aqui o servidor responde que suporta a extensão “deflate-frame”, e apenas SOAP dos subprotocolos solicitados.
Transferência de dados
Comunicação WebSocket consiste em “frames” – fragmentos de dados, que podem ser enviados de ambos os lados, e podem ser de vários tipos:
- “text frames” – contêm dados de texto que as partes enviam umas às outras.
- “frames de dados binários” – contêm dados binários que as partes enviam umas às outras.
- “frames de ping/pong” são utilizados para verificar a ligação, enviados a partir do servidor, o navegador responde a estes automaticamente.
li> há também “frame de fecho de ligação” e algumas outras frames de serviço.
No browser, trabalhamos directamente apenas com frames de texto ou binários.
WebSocket .send()
o método pode enviar tanto texto como dados binários.
uma chamada socket.send(body)
permite body
em string ou num formato binário, incluindo Blob
ArrayBuffer
, etc. Não são necessárias definições: basta enviá-lo em qualquer formato.
Quando recebemos os dados, o texto vem sempre como string. E para os dados binários, podemos escolher entre Blob
e ArrayBuffer
formatos.
Isso é definido por socket.binaryType
propriedade, é "blob"
por defeito, por isso os dados binários vêm como Blob
objectos.
Blob é um objecto binário de alto nível, integra-se directamente com <a>
<img>
e outras etiquetas, de modo que é um padrão são. Mas para o processamento binário, para aceder a bytes de dados individuais, podemos alterá-lo para "arraybuffer"
:
socket.binaryType = "arraybuffer";socket.onmessage = (event) => { // event.data is either a string (if text) or arraybuffer (if binary)};
Limitação da taxa
Imagine, a nossa aplicação está a gerar muitos dados para enviar. Mas o utilizador tem uma ligação de rede lenta, talvez numa Internet móvel, fora de uma cidade.
Podemos chamar socket.send(data)
repetidamente. Mas os dados serão armazenados em buffer (armazenados) na memória e enviados apenas tão rapidamente quanto a velocidade da rede o permita.
O socket.bufferedAmount
armazena quantos bytes permanecem armazenados em buffer neste momento, à espera de serem enviados através da rede.
Podemos examiná-lo para ver se o socket está realmente disponível para transmissão.
// every 100ms examine the socket and send more data// only if all the existing data was sent outsetInterval(() => { if (socket.bufferedAmount == 0) { socket.send(moreData()); }}, 100);
Fechamento da ligação
Normalmente, quando uma parte quer fechar a ligação (tanto o browser como o servidor têm direitos iguais), enviam um “frame fechado de ligação” com um código numérico e uma razão textual.
O método para isso é:
socket.close(, );
-
code
é um código especial de fecho de WebSocket (opcional) -
reason
é uma string que descreve o motivo do fecho (opcional)
então a outra parte em close
manipulador de eventos obtém o código e o motivo, e.g.:
// closing party:socket.close(1000, "Work complete");// the other partysocket.onclose = event => { // event.code === 1000 // event.reason === "Work complete" // event.wasClean === true (clean close)};
Valores de código mais comuns:
-
1000
– o fecho padrão, normal (utilizado se nãocode
fornecido), -
1006
– nenhuma forma de definir tal código manualmente, indica que a ligação foi perdida (sem frame de fecho).
Há outros códigos como:
-
1001
– a festa está a ir-se embora, por exemplo o servidor está a fechar, ou um browser deixa a página, -
1009
– a mensagem é demasiado grande para processar, -
1011
– erro inesperado no servidor, - …e assim por diante.
A lista completa pode ser encontrada em RFC6455, §7.4.1.
códigos WebSocket são um pouco como códigos HTTP, mas diferentes. Em particular, quaisquer códigos inferiores a 1000
são reservados, haverá um erro se tentarmos definir tal código.
// in case connection is brokensocket.onclose = event => { // event.code === 1006 // event.reason === "" // event.wasClean === false (no closing frame)};
Estado de ligação
Para obter o estado de ligação, adicionalmente há socket.readyState
propriedade com valores:
-
0
– “CONEXÃO”: a ligação ainda não foi estabelecida, -
1
– “ABERTO”: comunicando, -
2
– “FECHADO”: a ligação está a fechar, -
3
– “FECHADO”: a ligação está fechada.
Chat exemplo
Vejamos um exemplo de chat usando o navegador WebSocket API e o módulo Node.js WebSocket https://github.com/websockets/ws. Vamos prestar a principal atenção ao lado do cliente, mas o servidor também é simples.
HTML: precisamos de um <form>
para enviar mensagens e um <div>
para mensagens recebidas:
<!-- message form --><form name="publish"> <input type="text" name="message"> <input type="submit" value="Send"></form><!-- div with messages --><div></div>
De JavaScript queremos três coisas:
- Abrir a ligação.
- Na submissão do formulário –
socket.send(message)
para a mensagem. - Na mensagem recebida – anexá-la a
div#messages
.
Aqui está o código:
let socket = new WebSocket("wss://javascript.info/article/websocket/chat/ws");// send message from the formdocument.forms.publish.onsubmit = function() { let outgoingMessage = this.message.value; socket.send(outgoingMessage); return false;};// message received - show the message in div#messagessocket.onmessage = function(event) { let message = event.data; let messageElem = document.createElement('div'); messageElem.textContent = message; document.getElementById('messages').prepend(messageElem);}
O código do lado do servidor está um pouco fora do nosso alcance. Aqui usaremos Node.js, mas não é necessário. Outras plataformas também têm os seus meios para trabalhar com WebSocket.
O algoritmo do lado do servidor será:
- Criar
clients = new Set()
– um conjunto de tomadas. - Para cada tomada aceite, adicioná-lo ao conjunto
clients.add(socket)
e configurarmessage
ouvinte de eventos para receber as suas mensagens. - Quando uma mensagem recebida: iterar sobre clientes e enviá-la a todos.
- Quando uma ligação é fechada:
clients.delete(socket)
. - WebSockets não têm limitações de origem cruzada.
- são bem suportados em browsers.
- podem enviar/receber cordas e dados binários.
-
socket.send(data)
, -
socket.close(, )
. -
open
, -
message
, -
error
,
const ws = new require('ws');const wss = new ws.Server({noServer: true});const clients = new Set();http.createServer((req, res) => { // here we only handle websocket connections // in real project we'd have some other code here to handle non-websocket requests wss.handleUpgrade(req, req.socket, Buffer.alloc(0), onSocketConnect);});function onSocketConnect(ws) { clients.add(ws); ws.on('message', function(message) { message = message.slice(0, 50); // max message length will be 50 for(let client of clients) { client.send(message); } }); ws.on('close', function() { clients.delete(ws); });}
Aqui está o exemplo de trabalho:
Também é possível descarregá-lo (botão superior direito no iframe) e executá-lo localmente. Só não se esqueça de instalar Node.js e npm install ws
antes de correr.
Summary
WebSocket é uma forma moderna de ter ligações browser-servidor persistentes.
O API é simples.
Métodos:
Eventos:
, close
.
WebSocket por si só não inclui a reconexão, autenticação e muitos outros mecanismos de alto nível. Assim, existem bibliotecas cliente/servidor para isso, e também é possível implementar estas capacidades manualmente.
Socket às vezes, para integrar o WebSocket no projecto existente, as pessoas executam o servidor WebSocket em paralelo com o servidor HTTP principal, e partilham uma única base de dados. Pedidos para utilização do WebSocket wss://ws.site.com
, um subdomínio que leva ao servidor WebSocket, enquanto https://site.com
vai para o principal servidor HTTP.
Seguramente, outras formas de integração também são possíveis.