Pular para o conteúdo

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.

PrecisaUsePor quê
o servidor avisa, o cliente só ouveSSEHTTP comum, reconecta sozinho, passa em qualquer proxy
os dois falamWebSocketduas 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#

dataforge
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:

javascript
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 fluxoFaz
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.abertono quando o cliente fechou a aba
fluxo.enviadosquantos 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#

dataforge
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#

dataforge
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#

dataforge
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)
ChamadaFaz
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.idestado e identidade
ws.recebidos · ws.enviadoscontagem

A sala#

dataforge
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 pongdentro do receber — obrigar a tratar isso seria obrigar a conhecer o RFC
máscara do clienteo Kiln (o servidor nunca mascara)
mensagem partida em vários quadrosjuntada antes de chegar a você
recv devolvendo menos bytes do que se pediuo leitor insiste até completar
um quadro que anuncia 8 exabytesrecusado 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.

dataforge
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.