Skip to main content

VISÃO GERAL

SSE (Server Side Event) é o primo pobre do Websocket. É bem mais simples de implementar do que um Websocket (é um request HTTP normal), mas é unidirecional (apenas o server manda eventos). Abre-se um request "que fica aberto direto" entre o client e o server, e o server entra num loop infinito enviando dados quando quiser. Os dados enviados pelo server sempre têm um "tipo" (event), e é possível criar listeners JS pra cada evento específico. Permite, portanto, uma operação real time, e ainda é bem simples de implementar. A arquitetura fica algo assim:

HTML

<table id="tabela">
<tbody><tr class="tableModel noshow">
...
</tbody>
</table>

JS

async function startWorker() {
//CHECA SE O BROWSER TEM SUPORTE A SSE -- PRATICAMENTE GARANTIDO QUE SIM
if (!window.EventSource) {
SNFWHelpers.showMessage(10, SNFWDictionary.__t('js.interaction.browser_incompatible', 'Seu navegador não é compatível com essa funcão.'));
return;
}

//DEFINIÇÕES GERAIS
const table = $('#tabela');
const filters = {filtro01: "abc", filtro02: "def"};
const endpoint = 'endpoint/workerSSE?filters=' + encodeURIComponent(btoa(JSON.stringify(filters)));
if (table.count === 0) return;

//ABRE UMA CONEXÃO SSE COM O SERVIDOR
const eSource = new EventSource(endpoint);

//DEFINE HANDLERS PRA CADA EVENT ESPECÍFICO
eSource.addEventListener('open', function(e) {
console.log("Connection was opened.")
});
eSource.addEventListener('abort', function(e) {
console.log("Connection was closed.")
eSource.close();
});
eSource.addEventListener('error', function(e) {
if (e.readyState == EventSource.CLOSED) {
console.log("Server connection interrupted!");
}
});
eSource.addEventListener('message', function(e) {
console.log("Server sent data.")
handleIncomingData(e, table);
});
}
function handleIncomingData(e, table) {
if (e.data === '') return;
const obj = DataHandler.parseObj(e.data);

//EXTRAI OS DADOS NECESSÁRIOS DA RESPOSTA
const row = obj.row;

//ATUALIZA A TABELA COM OS NOVOS DADOS
newTr = Table.addNewLineFromModel(null, table, true);
Table.fillTableRow(newTr, row);
newTr.removeClass('dirty');
table.prepend([newTr]);
}

startWorker();

PHP

class ABC {
private readonly RequestInterface $request;
public function __construct(
private readonly SSEFlusher $sse,
private readonly MQTTClientInterface $mqtt,
) {}
public function flushMessageSSE(Message $message) : void {
$data = $message->payload;
$data = tratarDadosAplicandoFiltros($this->request, $data);
$this->sse->send(json_encode($data));
}
public function workerSSE(ServerRequestInterface $request, array $params) : never {
//GUARDA O REQUEST PARA TRATAR FILTROS
$this->request = $request;

//CONFIGURA O MQTT - TÓPICO, CALLBACK, ETC...
$this->mqtt->onMessage([$this, 'flushMessageSSE']);
$this->mqtt->subscribe('topic', qos: 0);
$mqtt->connectAndSubscribe();
ignore_user_abort(true);

//ENTRA NUM LOOP INFINITO OUVINDO MQTT E ENVIANDO KEEPALIVE A CADA 30 SEGUNDOS
try {
$keepaliveTimeout = 30;
$nextKeepalive = date('U', strtotime('+' . $keepaliveTimeout . ' seconds'));
while (true) {
if (connection_aborted()) throw new SSEJSONException('Client disconnected.');
$mqtt->loopOnce();
usleep(50_000);
if ($nextKeepalive > date('U')) continue;

$nextKeepalive = date('U', strtotime('+' . $keepaliveTimeout . ' seconds'));
$this->sse->send(data: '');
}
} catch (Exception $e) {
$this->mqtt->disconnect();
throw new SSEJSONException($e->getMessage());
}
}
}

PRÓS

  • ainda é simples de implementar
  • funcionamento em tempo real
  • geralmente é mais leve para o servidor
  • não necessariamente exige uso de MQTT no backend (pode até ser um poll escondido)
  • funciona em qualquer browser

CONTRAS

  • um pouco mais difícil de debugar
  • cada conexão aberta "segura" um worker do PHP-FPM (limite de recursos do servidor)
  • se o usuário tiver várias abas abertas, cada aba vai ter um request aberto
  • pode exigir algum tipo de middleware no backend pro worker "saber que há um novo evento" -- uma mensagem MQTT intermediária, por exemplo
  • só é possível fazer requests GET, então filtros comumente enviados via POST devem ser migrados pra URL de alguma forma