Skip to content

Protocollo di gioco online per Minecraft basato su WebRTC ​

Questo articolo descrive un'implementazione del launcher per Minecraft che consente il gioco in rete tra diverse reti locali tramite WebRTC senza la necessità di software VPN esterno o il reindirizzamento manuale delle porte (port-forwarding) sul router.


🌐 Cos'è WebRTC? Perché sceglierlo? ​

WebRTC (Web Real-Time Communication) è una tecnologia di comunicazione in tempo reale peer-to-peer. Citazione da MDN Web Docs:

WebRTC (Web Real-Time Communications) è una tecnologia open source che consente la comunicazione in tempo reale tramite connessioni peer-to-peer.

Sebbene WebRTC sia stato originariamente progettato per lo streaming audio e video in tempo reale tra browser web, la sua architettura sottostante lo rende altamente adatto per i launcher di gioco per stabilire canali dati diretti a bassa latenza.

Confronto dei metodi di gioco online ​

La tabella seguente confronta tre modi per implementare il gioco online:

CaratteristicaWebRTCSoluzione su misura (Custom Hole Punching)Software / VPN di terze parti (es. Hamachi, Radmin)
PersonalizzazioneAltaMassima-
Difficoltà di sviluppoMedio-BassaMolto AltaMolto Bassa
Esperienza utenteEccellenteEccellenteMediocre

📑 Dettagli del protocollo e concetti di base ​

Il protocollo si compone principalmente di tre parti:

  1. Configurazione della connessione: Scambio di descrizioni di sessione (SDP) per negoziare l'evasione NAT.
  2. Canale dei metadati (Metadata): Canale di controllo per segnalare azioni della stanza, informazioni sul giocatore e stato del server LAN.
  3. Incapsulamento di Minecraft: Trasmissione del traffico TCP del gioco all'interno di canali dati binari affidabili di WebRTC.

Concetti chiave ​

  • PeerConnection: Rappresenta la connessione stabilita tra due giocatori. Gestisce l'evasione NAT, i candidati ICE e la crittografia.
  • DataChannel: Canale dati bidirezionale all'interno di una PeerConnection. Una connessione P2P può avere più canali (es. uno per i metadati e uno per ciascuna connessione di gioco attiva).
  • SDP Description: Stringa del protocollo di descrizione della sessione (Session Description Protocol) contenente dettagli sui codec, parametri di connessione e indirizzi candidati.
  • ICEServer (STUN/TURN):
    • STUN: Viene utilizzato per scoprire l'indirizzo IP pubblico del giocatore e il suo tipo di NAT. Sono ampiamente disponibili molti server STUN pubblici gratuiti (es. di Google o Tencent).
    • TURN: Server di inoltro (Relay) utilizzato se la connessione diretta fallisce (es. entrambi i giocatori si trovano dietro NAT simmetrici). L'inoltro del traffico consuma larghezza di banda e richiede un server dedicato.

🔄 Flusso del protocollo e architettura di segnalazione ​

Il diagramma seguente illustra come viene coordinata la connessione P2P, viene eseguita l'evasione NAT e viene incapsulato in modo sicuro il traffico di gioco tra due client Minecraft:

🔄 Sequenza di connessione e flusso di dati

1
Segnalazione e configurazione della stanza

L'Host invia l'offerta SDP al server di segnalazione (Lobby).
L'Ospite riceve l'offerta, la configura e restituisce una risposta SDP all'Host.

2
Evasione NAT e Hole Punching

Entrambi i launcher eseguono l'evasione NAT tramite STUN/TURN per stabilire una connessione P2P diretta. Viene aperto un canale di controllo dei metadati affidabile.

3
Rilevamento del gioco LAN

Il Minecraft Host trasmette il suo mondo LAN. Il launcher dell'host inoltra questi metadati all'Ospite.
Il Launcher dell'Ospite avvia un proxy TCP locale e lo trasmette come un gioco LAN falso al client Minecraft dell'Ospite.

4
Incapsulamento del traffico di gioco

Il client ospite si connette al proxy. L'launcher dell'ospite mappa la connessione a un nuovo DataChannel binario di WebRTC. Il launcher dell'host inoltra i pacchetti al server reale di Minecraft.

🔑 Come stabilire connessioni tra utenti ​

Le connessioni vengono stabilite tramite il modello Offer/Answer (Offerta/Risposta). Il launcher iniziatore crea un'Offer e il ricevitore restituisce un'Answer.

Flusso dell'iniziatore (Host) ​

L'iniziatore crea la PeerConnection, aggiunge il canale dei metadati e genera l'offerta SDP:

ts
// "id" rappresenta l'identificatore univoco del peer di destinazione
async function initiateConnection(id: string) {
    const connection = new PeerConnection(id, {
        iceServers: [
            { urls: "stun:stun.qq.com" }
        ],
        iceTransportPolicy: 'all'
    });

    // Creare il canale dei metadati per i comandi di controllo
    const channel = connection.createDataChannel("metadata", {
        ordered: true,
        protocol: 'metadata'
    });

    // Ascoltare le modifiche alla descrizione locale / candidati ICE
    connection.onLocalDescription((description) => {
        // Inviare questa offerta SDP al ricevitore tramite il server di segnalazione
        sendDescription(id, description);
    });

    // Se la libreria WebRTC richiede la generazione manuale:
    const offer = await connection.createOffer();
    await connection.setLocalDescription(offer);
}

Flusso del ricevitore (Ospite) ​

Il ricevitore attende l'offerta, la configura come descrizione remota e restituisce una risposta:

ts
async function onReceiveOffer(id: string, offer: Description) {
    const connection = new PeerConnection(id, {
        iceServers: [{ urls: "stun:stun.qq.com" }],
        iceTransportPolicy: 'all'
    });

    await connection.setRemoteDescription(offer);

    // Ascoltare il canale dei metadati in arrivo
    connection.onDataChannel((channel) => {
        if (channel.protocol === 'metadata') {
            console.log("Canale dei metadati stabilito!");
        }
    });

    connection.onLocalDescription((description) => {
        // Inviare questa risposta SDP all'iniziatore
        sendDescription(id, description);
    });

    const answer = await connection.createAnswer();
    await connection.setLocalDescription(answer);
}

✉️ Formato di comunicazione del canale dei metadati ​

Una volta stabilita la connessione P2P, il canale dei metadati (protocol: 'metadata') viene utilizzato per trasmettere i comandi della stanza. Il formato è una stringa JSON codificata in UTF-8:

ts
interface Message {
    type: string;
    payload: any;
}

1. Messaggi di battito cardiaco (Ping/Pong) ​

Utilizzati per misurare la latenza di rete (Ping) e mantenere attiva l'associazione della porta NAT.

  • heartbeat-ping (Inviato ogni secondo):
    json
    {
        "type": "heartbeat-ping",
        "payload": { "time": 1718000000000 }
    }
  • heartbeat-pong (Risposta immediata con lo stesso timestamp):
    json
    {
        "type": "heartbeat-pong",
        "payload": { "time": 1718000000000 }
    }

2. Messaggio di identità del giocatore ​

Condivide il profilo del giocatore (nome utente, UUID e URL di skin/mantelli) per visualizzare gli avatar nella stanza.

json
{
    "type": "identity",
    "payload": {
        "name": "Steve",
        "id": "e067c29e-29e2-498c-a1d2-e0d1655b3ff1",
        "textures": {
            "SKIN": {
                "url": "http://textures.minecraft.net/texture/...",
                "metadata": { "model": "slim" }
            }
        }
    }
}

3. Messaggio di rilevamento LAN di Minecraft ​

Inviato dall'host quando il client Minecraft locale pubblica la partita sulla rete locale.

json
{
    "type": "lan",
    "payload": {
        "motd": "Mondo di Steve",
        "port": 54321
    }
}

🎮 Incapsulamento del traffico TCP di Minecraft su DataChannel ​

Poiché Minecraft utilizza TCP per le connessioni multiplayer, ma WebRTC opera su UDP, il launcher agisce come un proxy TCP locale.

1. Proxy TCP dell'ospite (Ricevitore) ​

Al ricevimento di un messaggio lan, il launcher dell'ospite crea un server TCP locale:

ts
function startGuestProxy(motd: string, remotePort: number) {
    const proxyServer = net.createServer((tcpSocket) => {
        // Creare un canale dati binario ordinato e affidabile per questa connessione
        const gameChannel = peerConnection.createDataChannel(remotePort.toString(), {
            protocol: 'minecraft',
            ordered: true
        });

        // Inviare dati TCP dal Minecraft locale al canale dati WebRTC
        tcpSocket.on('data', (data) => {
            gameChannel.sendBinary(data);
        });

        // Scrivere dati binari WebRTC nel socket TCP locale
        gameChannel.onMessage((data) => {
            tcpSocket.write(Buffer.from(data));
        });

        tcpSocket.on('close', () => gameChannel.close());
        gameChannel.onClose(() => tcpSocket.destroy());
    });

    // Ascoltare su una porta locale casuale disponibile
    proxyServer.listen(0, () => {
        const localPort = proxyServer.address().port;
        // Trasmettere questa porta localmente in modo che il Minecraft locale la rilevi automaticamente
        broadcastLanDiscovery(motd, localPort);
    });
}

2. Ricevitore di tunnel dell'iniziatore (Host) ​

L'host ascolta i canali dati minecraft in arrivo e li reindirizza al gioco reale di Minecraft:

ts
peerConnection.onDataChannel((channel) => {
    if (channel.protocol === 'minecraft') {
        const actualGamePort = parseInt(channel.label);

        // Aprire una connessione TCP diretta al gioco Minecraft locale
        const gameSocket = net.createConnection({ port: actualGamePort });

        // Unire i dati binari grezzi tra il socket e il canale
        gameSocket.on('data', (data) => {
            channel.sendBinary(data);
        });

        channel.onMessage((data) => {
            gameSocket.write(Buffer.from(data));
        });

        gameSocket.on('close', () => gameSocket.destroy());
        channel.onClose(() => gameSocket.destroy());
    }
});

📖 Appendice e note di implementazione ​

Trasmissione di rilevamento LAN di Minecraft ​

Minecraft scopre le partite LAN ascoltando i pacchetti multicast. Il launcher trasmette le informazioni del server con il seguente formato:

[MOTD]${motd}[/MOTD][AD]${port}[/AD]

Este pacchetto deve essere inviato al gruppo multicast 224.0.2.60:4445.

Affidabilità di trasmissione ​

WebRTC consente di configurare i canali dati per garantire l'affidabilità (emulazione TCP su UDP). Gli sviluppatori devono impostare ordered: true e omettere maxPacketLifeTime e maxRetransmits per evitare la perdita di pacchetti.

Librerie WebRTC consigliate ​