O PixelHub é um quadro colaborativo distribuído em que múltiplos usuários podem pintar pixels em um canvas compartilhado de 1000×1000 células. Toda alteração realizada por qualquer participante é propagada em tempo real para todos os demais conectados, garantindo uma visão consistente e sincronizada do estado do quadro.
A arquitetura do sistema é composta por um servidor Spring Boot (back-end Java) e clientes conectados via WebSocket. A evolução da arquitetura neste trabalho consistiu na adoção da Opção A – Comunicação em Grupo (Multicast), substituindo qualquer comunicação ponto a ponto direta entre clientes por um modelo de disseminação intermediado pelo servidor.
A natureza do PixelHub exige que uma única ação de um usuário (pintar um pixel ou usar o balde de tinta) seja imediatamente visível para todos os demais participantes do quadro. Esse requisito mapeia diretamente para o modelo de Comunicação em Grupo: o remetente (o cliente que realizou a pintura) não precisa conhecer a identidade, o endereço IP ou a porta de nenhum outro cliente — ele simplesmente envia seu evento ao servidor, que age como coordenador do grupo e propaga a mensagem a todos os membros registrados.
As demais opções foram descartadas pelos seguintes motivos:
- Pub-Sub (Opção B): Adequada para cenários com múltiplos tópicos distintos e assinantes seletivos. No PixelHub, há um único "canal" de eventos de desenho, e todos os usuários são assinantes universais, tornando a sobrecarga de um broker de tópicos desnecessária.
- Filas de Mensagens (Opção C): O modelo de fila é orientado ao consumo individual de mensagens (cada mensagem é retirada da fila por um consumidor). No PixelHub, cada evento de pixel deve ser entregue a todos os clientes simultaneamente, o que contradiz a semântica de fila.
- Espaço de Tuplas (Opção D): Útil para coordenação assíncrona baseada em padrões de dados. O PixelHub requer propagação de eventos em tempo real com baixa latência, não consultas associativas a um repositório compartilhado.
A Comunicação em Grupo via WebSocket é, portanto, a abordagem que melhor se encaixa no problema: oferece disseminação eficiente a múltiplos destinatários com desacoplamento espacial completo entre os pares de clientes.
A arquitetura é organizada em quatro serviços e um controlador WebSocket:
| Componente | Responsabilidade |
|---|---|
ClientService |
Mantém o conjunto de sessões WebSocket ativas (membros do grupo) |
WebSocketService |
Implementa as primitivas de unicast e broadcast (multicast) |
BoardService |
Gerencia o estado global do quadro (1000×1000 pixels) |
UserService |
Controla login/logout e notifica todos sobre mudanças na lista de usuários |
SignalingHandler |
Ponto de entrada WebSocket; roteia eventos de desenho e balde |
O ClientService utiliza um ConcurrentHashMap.newKeySet() para armazenar as sessões WebSocket ativas. Essa estrutura é thread-safe e garante que adições e remoções de membros (conexão e desconexão de clientes) ocorram sem condições de corrida:
// ClientService.java
private final Set<WebSocketSession> clients = ConcurrentHashMap.newKeySet();
public void addClient(WebSocketSession session) { clients.add(session); }
public void removeClient(WebSocketSession session) { clients.remove(session); }Quando um novo cliente conecta (afterConnectionEstablished), ele é adicionado ao grupo e recebe imediatamente o estado atual completo do quadro via unicast, garantindo consistência na entrada:
// SignalingHandler.java
@Override
public void afterConnectionEstablished(WebSocketSession session) {
clientService.addClient(session);
webSocketService.unicast(boardService.getBoard().raw(), session);
}O método broadcast do WebSocketService implementa a disseminação de grupo. Ele itera sobre todos os membros registrados no ClientService, excluindo o próprio remetente (quando aplicável), e entrega a mensagem serializada em JSON:
// WebSocketService.java
public void broadcast(Object data, WebSocketSession sender) {
String json = objectMapper.writeValueAsString(data);
TextMessage message = new TextMessage(json);
for (WebSocketSession client : clientService.getClients()) {
if (client.equals(sender)) continue; // não reflete ao remetente
if (!client.isOpen()) continue; // ignora sessões encerradas
synchronized (client) {
client.sendMessage(message);
}
}
}O uso de synchronized (client) garante que mensagens concorrentes enviadas para a mesma sessão não se entrelacem, preservando a integridade das mensagens WebSocket.
O fluxo de um evento de pintura segue as etapas abaixo:
- O cliente envia um frame WebSocket com payload
{"type": "draw", "x": ..., "y": ..., "color": ...}. - O
SignalingHandlerdeserializa o payload e aplica a mudança aoBoardcompartilhado viaboardService.getBoard().applyDraw(draw). - O mesmo payload é disseminado a todos os outros clientes via
webSocketService.broadcast(data, session).
Para o evento de balde (bucket), o comportamento é ligeiramente diferente: após aplicar o flood-fill no estado do quadro, o servidor realiza um broadcast do estado bruto completo (board.raw()) com sender = null, garantindo que todos os clientes (incluindo o originador) recebam a nova representação consistente do quadro.
O desacoplamento espacial é comprovado pela ausência de qualquer referência entre clientes. O cliente A que pinta um pixel não conhece o endereço IP, porta ou identificador de nenhum outro cliente B, C ou D. Toda a comunicação ocorre exclusivamente entre o cliente e o servidor:
Cliente A ──────► Servidor (Grupo) ──────► Cliente B
│ └──► Cliente C
│ └──► Cliente D
└── [Board State atualizado]
O servidor age como o coordenador do grupo, e cada cliente interage apenas com ele.
A introdução do servidor como intermediário de grupo traz as seguintes sobrecargas em relação a uma hipotética comunicação direta ponto a ponto:
- Latência adicional: Cada mensagem percorre o caminho
Cliente → Servidor → Clientes. Em comunicação direta entre dois clientes na mesma rede local, esse salto extra pelo servidor acrescenta latência de rede (tipicamente de 1 a 10 ms em LANs), que é perceptível em cenários de alta frequência de pintura. - Gargalo no servidor: O broadcast é implementado com um laço sequencial sobre todas as sessões abertas. Com N clientes conectados e alta taxa de eventos, o servidor precisa serializar e enviar a mesma mensagem N-1 vezes. Para N grande (centenas de usuários), isso pode saturar a thread de I/O.
- Uso de memória: O estado completo do quadro (1000×1000 pixels com cor em string hexadecimal) é mantido em memória no
Board. O broadcast doraw()no evento de balde pode transmitir um payload significativamente maior do que um evento de pixel individual. - Complexidade de gerenciamento: O servidor precisa detectar e remover sessões encerradas (
client.isOpen()), além de sincronizar o acesso concorrente às sessões comsynchronized.
As seguintes estratégias foram adotadas ou podem ser adotadas para mitigar o overhead:
Adotadas no projeto atual:
- Serialização única da mensagem: O
WebSocketServiceserializa o objeto para JSON apenas uma vez por broadcast (String json = objectMapper.writeValueAsString(data)), reutilizando o mesmoTextMessagepara todos os destinatários. Isso evita N serializações independentes. - Estrutura thread-safe sem locks globais: O
ConcurrentHashMappara o conjunto de clientes e oCopyOnWriteArrayListpara usuários permitem leituras concorrentes sem bloqueio, reduzindo a contenção. - Delta de eventos (draw vs. raw): Eventos de pintura pixel a pixel transmitem apenas o delta (
{type, x, y, color}), evitando o envio do estado completo a cada pincelada. O estado completo só é enviado em eventos de balde ou ao entrar no quadro.
Possíveis melhorias futuras:
- Throttling e batching: Agrupar múltiplos eventos de pixels em um único frame WebSocket por intervalo de tempo (ex: 50ms) reduziria o número de mensagens enviadas em sessões de pintura intensa.
- Compressão WebSocket (permessage-deflate): Habilitar a extensão de compressão do protocolo WebSocket reduziria o volume de dados transmitidos, especialmente para o payload
raw()do balde. - Persistência do estado do Board: Atualmente o estado reside exclusivamente em memória. Uma persistência periódica (ex: Redis ou banco de dados) permitiria recuperação após reinício do servidor sem perda do quadro colaborativo.
- Horizontal scaling com broker de mensagens: Para suportar múltiplas instâncias do servidor (escalabilidade horizontal), seria necessário introduzir um broker externo (ex: Redis Pub/Sub, RabbitMQ) para sincronizar o broadcast entre instâncias — neste caso, a Opção B (Pub-Sub) complementaria a Opção A em uma arquitetura híbrida.
O sistema apresenta as seguintes características de resiliência:
- Desconexão de cliente: Quando uma sessão encerra (
afterConnectionClosed), ela é removida doClientService. O broadcast ignora sessões comclient.isOpen() == false, evitando erros de envio para conexões mortas. O quadro continua operacional para todos os demais participantes. - Exceções individuais de envio: Exceções lançadas ao enviar para um cliente específico são capturadas com
catch (Exception ignored)dentro do laço de broadcast, garantindo que uma falha em um destinatário não interrompa a entrega aos demais. - Consistência na reconexão: Ao reconectar, o cliente recebe o estado atual completo do quadro via
unicastnoafterConnectionEstablished, reestabelecendo sua visão consistente sem necessidade de replay de eventos anteriores.
O PixelHub demonstra com sucesso a adoção de Comunicação em Grupo como mecanismo de comunicação indireta. O servidor atua como coordenador do grupo, eliminando qualquer acoplamento ponto a ponto entre os clientes e garantindo o desacoplamento espacial exigido: nenhum cliente precisa conhecer a identidade ou localização de rede dos demais. A implementação é funcional, thread-safe e estruturalmente adequada ao modelo de grupo. As análises de overhead e as estratégias de mitigação propostas demonstram maturidade na compreensão dos trade-offs inerentes à comunicação indireta em sistemas distribuídos.