Przejdź do głównej zawartości

🧱 Meshcore — Architektura

Warstwy

WarstwaTechnologiaRola
Transport P2PKanały danych WebRTC (natywne)Bezpośrednie dostarczanie wiadomości peer-to-peer
SygnalizacjaPubliczny broker MQTT (temat per pokój)Obecność + wymiana SDP offer/answer/ICE
Kanał wiadomościKanał danych WebRTC (n2)Wysyłka wiadomości bezpośrednio między przeglądarkami
Relay fallbackWłasny klient MQTT 3.1.1 (WSS)Dostarczanie wiadomości, gdy WebRTC nie może się połączyć (CGNAT / sieci komórkowe)
Local bridgeBroadcastChannelKarty tej samej przeglądarki (WebRTC loopback zablokowany)

Kluczowe decyzje techniczne

1. Pokój = temat sygnalizacji

Każdy peer subskrybuje n2mesh/{pokój} na publicznym brokcie MQTT i ogłasza tam swoją obecność. Peery, które zobaczą swoją obecność, nawiązują połączenie WebRTC. Zmiana pokoju = nowy temat = nowa grupa peerów.

2. Wybór oferenta bez „glare"

Dwaj peerowie mogą zobaczyć swoją obecność w tej samej chwili i obaj nie mogą wysyłać ofert (wyścig „glare" w WebRTC). Peer z leksykograficznie mniejszym id wysyła ofertę; druga strona czeka na nią:

if (state.pid < p.pid) {
dial(p.pid); // my jesteśmy oferentem
}

Każde id peera jest losowe na sesję, więc zawsze wybierany jest dokładnie jeden oferent.

3. Wiadomości sygnalizacyjne przez MQTT

Sygnalizacja jedzie tym samym tematem per pokój, co wszystko inne. Rodzaje wiadomości:

{ type: 'presence', pid, nick } // odkrywanie peerów
{ type: 'signal', from, to, data: { sdp } } // offer / answer
{ type: 'signal', from, to, data: { candidate } } // kandydat ICE
{ u, t, ts, mid } // wiadomość czatu

Pole to adresuje konkretnego peera; pozostali je ignorują. Kandydaci ICE przychodzący przed zdalnym opisem są buforowani per połączenie.

4. Relay fallback (MQTT) — dlaczego istnieje

Czysty P2P w przeglądarce nie łączy się na sieciach komórkowych: operatorzy używają CGNAT i odrzucają hole-punching, a darmowe publiczne serwery TURN, które kiedyś to mostkowały, są w 2026 martwe lub wymagają konta (Cloudflare TURN potrzebuje credencjałów z API).

Rozwiązanie: każda wiadomość jest równolegle publikowana do tematu n2mesh/{pokój} na publicznym brokcie MQTT przez WSS. Klient MQTT to własna, zero-zależnościowa implementacja MQTT 3.1.1 (~100 linii):

function mqttConnectPkt(clientId) { /* CONNECT: MQTT, v4, clean, keepalive 60 */ }
function mqttSubPkt(topic) { /* SUBSCRIBE qos 0 */ }
function mqttPubPkt(topic, p) { /* PUBLISH qos 0 */ }
function mqttUnsubPkt(topic) { /* UNSUBSCRIBE przy zmianie pokoju */ }
  • Izolacja pokoi: każda karta subskrybuje dokładny temat swojego pokoju (nie wildcard) — ruch innych pokoi nigdy do niej nie dociera.
  • Dedup po ID: każda wiadomość ma unikalny mid; Map zapamiętuje ostatnie ID na 30 s, więc wiadomość odebrana przez P2P i relay i most lokalny wyświetla się raz.
  • Kolejkowanie offline: wiadomości wysłane przed połączeniem z brokerem lądują w relay.queue (z zapamiętanym pokojem) i są flushowane po CONNACK→SUBACK.
  • Brokery: wss://broker.hivemq.com:8884 (zweryfikowany end-to-end), zapasowy wss://broker.emqx.io:8084, failover co 3 próby.
  • Keepalive: PINGREQ co 30 s, reconnect z backoffem (1 s → 15 s).

Status pokazuje, którą ścieżką działa połączenie: P2P, P2P + relay, relay mode (telefon na CGNAT), connecting….

5. Local bridge (BroadcastChannel)

Dwie karty tej samej przeglądarki nie mogą połączyć się przez WebRTC (przeglądarki blokują loopback WebRTC). Most BroadcastChannel łączy je lokalnie: karty tej samej przeglądarki wymieniają hello/msg bezpośrednio, a różne przeglądarki/urządzenia — przez kanały danych WebRTC i/lub relay.

6. Dlaczego WebTorrent został usunięty

Oryginalna wersja znajdowała peerów przez publiczne trackery WebSocket WebTorrent (tracker.webtorrent.dev, tracker.openwebtorrent.com) i przesyłała wiadomości po rozszerzonym protokole BitTorrenta. Testy na żywo (sierpień 2026) pokazały, że trackery przyjmują announce i rejestrują peerów, ale nigdy nie przekazują ofert WebRTC między nimi — dwóch klientów w tym samym roju (complete=2) otrzymało zero ofert. Przeglądarkowa wersja WebTorrent może używać tylko trackerów WebSocket (w przeglądarce nie ma UDP/DHT), więc peerowie nigdy nie mogli się odnaleźć i P2P nie działało. Zastąpienie go natywnym WebRTC + sygnalizacją przez MQTT utrzymuje aplikację w pełni bezserwerową, zero-zależnościową i działającą dziś.

Pliki

PlikZawartość
index.htmlPowłoka single-page
app.jsNetworking: WebRTC P2P + sygnalizacja/relay MQTT + local bridge
style.cssCiemny motyw aurora
.github/workflows/deploy.ymlDeploy na GitHub Pages

Przepływ danych

sender: JSON({u, t, ts, mid}) ──► dc.send(payload) (P2P data channel)
──► mqttPubPkt('n2mesh/room', p) (relay)

peer odbiera przez kanał danych LUB przez broker MQTT

receiver: handlePayload(bytes) ── dedup po mid ──► addMessage(nick, text)

odkrywanie: presence na 'n2mesh/room' ──► dial() ──► SDP/ICE przez relay ──► otwarty kanał danych

Ograniczenia

  • Brak historii — po wyjściu z pokoju grupa peerów znika (cena braku serwera).
  • Oba peery muszą być online jednocześnie — relay nie przechowuje wiadomości (QoS 0, bez retencji).
  • Tryb relay = wiadomości przechodzą przez publiczny broker — to świadomy kompromis, żeby chat działał w sieciach, gdzie P2P jest niemożliwy (CGNAT). Tryb P2P pozostaje w pełni peer-to-peer.

Repo

BartoszOsiej/Meshcore · Live