Tempo real
SSE para o servidor empurrar, WebSocket para os dois falarem, e stream para o arquivo grande.
Três mecanismos, e a escolha entre eles é quase sempre óbvia.
| Precisa | Use | Por quê |
|---|---|---|
| o servidor avisa, o cliente só ouve | SSE | HTTP comum, reconecta sozinho, passa em qualquer proxy |
| os dois falam | WebSocket | duas vias, quadro binário |
| um arquivo grande sem carregar na memória | `Kiln.stream` | cada pedaço sai enquanto o próximo é calculado |
SSE — o servidor empurra#
adopt Kiln
action progresso(fluxo):
total := 200
cycle i from 1 to total:
given not fluxo.aberto:
halt
importar_linha(i)
given i % 10 is 0:
fluxo.enviar({"feitos": i, "total": total}, tipo := "progresso")
fluxo.enviar({"ok": yes}, tipo := "fim")
server importador on 8080:
route GET "/importacao":
respond Kiln.sse(progresso)No cliente, quatro linhas:
const fonte = new EventSource('/importacao');
fonte.addEventListener('progresso', e => {
const d = JSON.parse(e.data);
barra.style.width = (d.feitos / d.total * 100) + '%';
});
fonte.addEventListener('fim', () => fonte.close());
| Método do fluxo | Faz |
|---|---|
fluxo.enviar(dados, tipo, identificador) | um evento; no se o cliente já foi |
fluxo.escrever(texto) | um pedaço cru, sem formato de evento |
fluxo.comentario() | o batimento que mantém a conexão viva |
fluxo.aberto | no quando o cliente fechou a aba |
fluxo.enviados | quantos eventos saíram |
fluxo.comentario() a cada 15 segundos evita que proxy e balanceador fechem a conexão ociosa — eles fecham tipicamente em 30 a 60. O navegador ignora o comentário.
A resposta já sai com Cache-Control: no-cache, Connection: keep-alive e X-Accel-Buffering: no. O último importa: o nginx guarda resposta em buffer por padrão, e com isso o evento só chegaria quando o buffer enchesse — o que destrói o SSE.
O formato, se você quiser montá-lo#
Kiln.evento({"n": 1}, tipo := "tick", identificador := "7")
// "id: 7\nevent: tick\ndata: {\"n\": 1}\n\n"O \n\n final não é enfeite: é ele que diz ao navegador que o evento acabou. E um texto com \n dentro leva um data: por linha — senão o evento quebraria no meio.
`Kiln.stream` — o arquivo grande#
action exportar(fluxo):
fluxo.escrever("id,valor,mes\n")
cycle linha in Banco.query(db, "SELECT id, valor, mes FROM vendas"):
fluxo.escrever($"{linha["id"]},{linha["valor"]},{linha["mes"]}\n")
route GET "/export.csv":
respond Kiln.stream(exportar, "text/csv")Um milhão de linhas sem montar o arquivo na memória. O mesmo mecanismo do SSE, sem o formato de evento.
WebSocket#
sala := Kiln.sala("chat")
action chat(req, ws):
quem := req["session"]["usuario"] ?? ws.id
sala.entrar(ws)
sala.transmitir({"entrou": quem, "agora": sala.quantos()}, exceto := ws)
persist ws.aberto:
msg := ws.receber(prazo := 60)
given msg is void or msg is "sair":
halt
sala.transmitir({"de": quem, "texto": msg})
sala.sair(ws)
sala.transmitir({"saiu": quem, "agora": sala.quantos()})
server app on 8080:
route GET "/chat":
render "chat"
Kiln.ws(app, "/ws", chat)
Kiln.ignite(app, 8080)| Chamada | Faz |
|---|---|
ws.enviar(x) | texto, ou qualquer valor — que vira JSON |
ws.enviar_json(v) | o mesmo, explícito |
ws.receber(prazo := n) | a próxima mensagem, ou void se a conexão acabou |
ws.receber_json() | já interpretada; void se não for JSON |
ws.ping() | um ping |
ws.fechar(codigo, motivo) | fecha com aperto de mão |
ws.aberto · ws.id | estado e identidade |
ws.recebidos · ws.enviados | contagem |
A sala#
sala.entrar(ws) // devolve quantos
sala.sair(ws)
sala.quantos()
sala.transmitir(mensagem, exceto := ws) // devolve para quantos chegou
sala.fechar_todos("servidor encerrando")Um soquete morto é removido em vez de levantar: um cliente que fechou a aba não pode derrubar a mensagem dos outros. E a trava protege a lista — duas threads entrando e saindo ao mesmo tempo é o caso normal, não a exceção.
O método `WS`#
A rota de WebSocket usa o método WS, que não existe em HTTP. Duas consequências boas: ela não pode ser alcançada por um GET comum, e um GET /ws continua livre para servir a página que abre a conexão.
Detalhes do protocolo que você não precisa conhecer#
| O quê | Quem trata |
|---|---|
o handshake (Sec-WebSocket-Accept) | o Kiln |
| ping e pong | dentro do receber — obrigar a tratar isso seria obrigar a conhecer o RFC |
| máscara do cliente | o Kiln (o servidor nunca mascara) |
| mensagem partida em vários quadros | juntada antes de chegar a você |
recv devolvendo menos bytes do que se pediu | o leitor insiste até completar |
| um quadro que anuncia 8 exabytes | recusado com o código 1009 |
Testar#
Kiln.test não serve para SSE nem WebSocket: ele roda tudo numa thread e não abre socket. Para esses dois é preciso um cliente do outro lado.
porta := Kiln.serve(app, 0) // segundo plano; devolve a porta
r := Web.get($"http://127.0.0.1:{porta}/importacao")
assert "event: progresso" in r["body"]
Kiln.stop(app)O que o Kiln continua não tendo#
HTTP/2 e TLS. Ele roda sobre o http.server do Python; em produção pública, ponha um nginx ou Caddy na frente — e o WebSocket atravessa proxy reverso sem configuração especial em nenhum dos dois.
Para um painel que só precisa mostrar dado fresco, a Vitrine tem V.atualizar_a_cada(n), que é por pergunta e mais simples que os dois.