Porting un ghjocu multiplayer da C++ à u web cù Cheerp, WebRTC è Firebase

Introduzione

a nostra cumpagnia Leaning Technologies furnisce soluzioni per portà l'applicazioni di desktop tradiziunali à u web. U nostru compilatore C++ accurtà genera una cumminazione di WebAssembly è JavaScript, chì furnisce i dui interazzione simplice cù u navigatore, è altu rendiment.

Comu esempiu di a so applicazione, avemu decisu di portà un ghjocu multiplayer à u web è hà sceltu Teeworlds. Teeworlds hè un ghjocu retro multiplayer XNUMXD cù una piccula ma attiva cumunità di ghjucatori (cumpresu mè!). Hè chjuca in termini di risorse scaricate è esigenze CPU è GPU - un candidatu ideale.

Porting un ghjocu multiplayer da C++ à u web cù Cheerp, WebRTC è Firebase
Esecuzione in u navigatore Teeworlds

Avemu decisu di utilizà stu prughjettu per sperimentà suluzioni generale per portà u codice di rete à u web. Questu hè generalmente fattu in i seguenti modi:

  • XMLHttpRequest/fetch, Se a parte di a rete hè custituita solu di richieste HTTP, o
  • sockets web.

E duie soluzioni necessitanu l'ospitu di un cumpunente di u servitore da u latu di u servitore, è nè permette l'usu cum'è un protocolu di trasportu UDP. Questu hè impurtante per l'applicazioni in tempu reale cum'è u software di videoconferenza è i ghjochi, perchè guarantisci a consegna è l'ordine di pacchetti di protokollu. TCP pò esse un ostaculu à a bassa latenza.

Ci hè un terzu modu - aduprà a reta da u navigatore: WebRTC.

RTCDataChannel Supporta a trasmissione affidabile è inaffidabile (in l'ultimu casu prova d'utilizà UDP cum'è un protokollu di trasportu sempre chì hè pussibule), è pò esse usatu sia cù un servitore remotu sia trà i navigatori. Questu significa chì pudemu portà tutta l'applicazione à u navigatore, cumpresu u cumpunente di u servitore!

Tuttavia, questu vene cun una difficultà addiziale: prima chì dui parenti WebRTC ponu cumunicà, anu bisognu di fà una stretta di manu relativamente cumplessa per cunnette, chì richiede parechje entità di terzu (un servitore di signalazione è unu o più servitori). STUN/GIRU).

Ideale, vulemu creà una API di rete chì usa WebRTC internamente, ma hè u più vicinu pussibule à una interfaccia UDP Sockets chì ùn hà micca bisognu di stabilisce una cunnessione.

Questu ci permetterà di prufittà di WebRTC senza avè da espose dettagli cumplessi à u codice di l'applicazione (chì vulemu cambià u più pocu pussibule in u nostru prughjettu).

WebRTC minimu

WebRTC hè un inseme di API dispunibili in i navigatori chì furnisce a trasmissione peer-to-peer di audio, video è dati arbitrarie.

A cunnessione trà i pari hè stabilitu (ancu s'ellu ci hè NAT da unu o dui lati) utilizendu i servitori STUN è / o TURN attraversu un mecanismu chjamatu ICE. Peers scambianu l'infurmazioni ICE è i paràmetri di u canali via l'offerta è a risposta di u protocolu SDP.

Wow! Quante abbreviazioni in una volta? Spieghemu brevemente ciò chì significanu sti termini:

  • Session Traversal Utilities per NAT (STUN) - un protokollu per scaccià NAT è ottene una coppia (IP, portu) per scambià dati direttamente cù l'ospite. S'ellu riesce à compie u so compitu, i pari ponu scambià dati indipindentamente cù l'altri.
  • Traversale Utilizendu Relè intornu à NAT (GIRU) hè ancu utilizatu per a traversata NAT, ma implementa questu trasmettendu dati attraversu un proxy chì hè visibile à i dui pari. Aghjunghje latenza è hè più caru di implementà cà STUN (perchè hè appiicata in tutta a sessione di cumunicazione), ma qualchì volta hè l'unica opzione.
  • Stabilimentu di cunnessione interattiva (GHIACCIO) usatu pi selezziunà lu megghiu metudu pussibili di culligamentu dui pari basatu nant'à infurmazione ottenuta da cunnessi parenti direttamente, oltri infurmazione ricevutu da ogni numeru di servori STUN è TURN.
  • Session Description Protocol (RDS) hè un furmatu per discrizzione di i paràmetri di u canali di cunnessione, per esempiu, candidati ICE, codecs multimedia (in u casu di un canale audio / video), etc... Unu di i pari manda una SDP Offerta, è u sicondu risponde cù una Risposta SDP. . . Dopu questu, un canale hè creatu.

Per creà una tale cunnessione, i pari bisognu di cullà l'infurmazioni chì ricevenu da i servitori STUN è TURN è scambià cù l'altri.

U prublema hè ch'elli ùn anu micca ancu a capacità di cumunicà direttamente, cusì deve esse un mecanismu fora di banda per scambià sti dati : un servitore di signalazione.

Un servitore di signalazione pò esse assai simplice perchè u so solu travagliu hè di trasmette dati trà i pari in a fase di handshake (cum'è mostra in u diagramma sottu).

Porting un ghjocu multiplayer da C++ à u web cù Cheerp, WebRTC è Firebase
Diagramma di sequenza di handshake WebRTC simplificatu

Panoramica di u mudellu di Teeworlds Network

L'architettura di rete di Teeworlds hè assai simplice:

  • I cumpunenti di u cliente è di u servitore sò dui prugrammi diffirenti.
  • I clienti entranu in u ghjocu cunnettendu à unu di parechji servitori, ognuna di quali ospita solu un ghjocu à u tempu.
  • Tuttu u trasferimentu di dati in u ghjocu hè realizatu attraversu u servitore.
  • Un servitore maestru speciale hè utilizatu per cullà una lista di tutti i servitori publichi chì sò visualizati in u cliente di ghjocu.

Grazie à l'usu di WebRTC per u scambiu di dati, pudemu trasfiriri u cumpunente di u servitore di u ghjocu à u navigatore induve u cliente hè situatu. Questu ci dà una grande opportunità ...

Sbarazza di i servitori

A mancanza di logica di u servitore hà un bonu vantaghju: pudemu implementà tutta l'applicazione cum'è cuntenutu staticu nantu à e Pagine Github o nantu à u nostru propiu hardware daretu à Cloudflare, assicurendu cusì scaricamenti veloci è un altu uptime gratuitamente. In fatti, pudemu scurdà di elli, è se avemu furtunatu è u ghjocu diventa populari, allora l'infrastruttura ùn deve esse mudernizzata.

Tuttavia, per u sistema per travaglià, avemu sempre aduprà una architettura esterna:

  • Unu o più servitori STUN: Avemu parechje opzioni libere da sceglie.
  • Almenu un servitore TURN: ùn ci sò micca opzioni gratuiti quì, cusì pudemu stabilisce u nostru propiu o pagà per u serviziu. Fortunatamente, a maiò parte di u tempu a cunnessione pò esse stabilita per i servitori STUN (è furnisce u veru p2p), ma TURN hè necessariu cum'è una opzione di fallback.
  • Servitore di signalazione: A cuntrariu di l'altri dui aspetti, a signalazione ùn hè micca standardizzata. Ciò chì u servitore di signalazione serà in realtà rispunsevuli di dipende un pocu di l'applicazione. In u nostru casu, prima di stabilisce una cunnessione, hè necessariu di scambià una piccula quantità di dati.
  • Teeworlds Master Server: Hè utilizatu da altri servitori per publicità a so esistenza è da i clienti per truvà servitori publichi. Ancu s'ellu ùn hè micca necessariu (i clienti ponu sempre cunnette à un servitore chì cunnosci manualmente), saria bellu d'avè cusì chì i ghjucatori ponu participà à ghjochi cù persone aleatorii.

Avemu decisu di utilizà i servitori STUN gratuiti di Google, è avemu implementatu un servitore TURN.

Per l'ultimi dui punti avemu usatu Firebase:

  • U servitore maestru Teeworlds hè implementatu assai simplice: cum'è una lista di l'uggetti chì cuntenenu infurmazioni (nome, IP, mappa, modu, ...) di ogni servitore attivu. I servitori publicanu è aghjurnanu u so propiu ughjettu, è i clienti piglianu a lista sana è a mostra à u lettore. Avemu ancu vede a lista in a pagina di casa cum'è HTML per chì i ghjucatori ponu simpricimenti cliccà nantu à u servitore è esse purtatu direttamente à u ghjocu.
  • A signalazione hè strettamente ligata à a nostra implementazione di sockets, descritta in a sezione dopu.

Porting un ghjocu multiplayer da C++ à u web cù Cheerp, WebRTC è Firebase
Lista di servitori in u ghjocu è in a pagina di casa

Implementazione di sockets

Vulemu creà una API chì hè u più vicinu à Posix UDP Sockets quantu pussibule per minimizzà u numeru di cambiamenti necessarii.

Vulemu ancu implementà u minimu necessariu necessariu per u scambiu di dati più simplice nantu à a reta.

Per esempiu, ùn avemu micca bisognu di routing reale: tutti i pari sò nantu à a listessa "LAN virtuale" assuciata cù una istanza di basa di dati Firebase specifica.

Dunque, ùn avemu micca bisognu di indirizzi IP unichi: i valori unichi di chjave Firebase (simili à i nomi di duminiu) sò abbastanza per identificà unicu parenti, è ogni peer assigna lucale indirizzi IP "falsi" à ogni chjave chì deve esse traduttu. Questu elimina completamente a necessità di l'assignazione di l'indirizzu IP globale, chì hè un compitu micca trivial.

Eccu l'API minima chì avemu bisognu di implementà:

// Create and destroy a socket
int socket();
int close(int fd);
// Bind a socket to a port, and publish it on Firebase
int bind(int fd, AddrInfo* addr);
// Send a packet. This lazily create a WebRTC connection to the 
// peer when necessary
int sendto(int fd, uint8_t* buf, int len, const AddrInfo* addr);
// Receive the packets destined to this socket
int recvfrom(int fd, uint8_t* buf, int len, AddrInfo* addr);
// Be notified when new packets arrived
int recvCallback(Callback cb);
// Obtain a local ip address for this peer key
uint32_t resolve(client::String* key);
// Get the peer key for this ip
String* reverseResolve(uint32_t addr);
// Get the local peer key
String* local_key();
// Initialize the library with the given Firebase database and 
// WebRTc connection options
void init(client::FirebaseConfig* fb, client::RTCConfiguration* ice);

L'API hè simplice è simile à l'API Posix Sockets, ma hà uni pochi di differenzi impurtanti: logging callbacks, assigning IPs lucali, è cunnessione lazy.

Registrazione di Callbacks

Ancu s'è u prugramma uriginale usa l'I / O senza bloccu, u codice deve esse refactored per eseguisce in un navigatore web.

U mutivu di questu hè chì u ciclu di l'avvenimentu in u navigatore hè oculatu da u prugramma (sia JavaScript o WebAssembly).

In l'ambiente nativu pudemu scrive codice cusì

while(running) {
  select(...); // wait for I/O events
  while(true) {
    int r = readfrom(...); // try to read
    if (r < 0 && errno == EWOULDBLOCK) // no more data available
      break;
    ...
  }
  ...
}

Se u ciclu di l'avvenimentu hè oculatu per noi, allora avemu bisognu di trasfurmà in qualcosa cum'è questu:

auto cb = []() { // this will be called when new data is available
  while(true) {
    int r = readfrom(...); // try to read
    if (r < 0 && errno == EWOULDBLOCK) // no more data available
      break;
    ...
  }
  ...
};
recvCallback(cb); // register the callback

Assignazione IP locale

L'ID di i nodi in a nostra "rete" ùn sò micca indirizzi IP, ma chjavi Firebase (sò strings chì pareanu cusì: -LmEC50PYZLCiCP-vqde ).

Questu hè cunvenutu perchè ùn avemu micca bisognu di un mecanismu per assignà l'IPs è cuntrollà a so unicità (cum'è a dispusizione di elli dopu chì u cliente disconnects), ma hè spessu necessariu d'identificà i pari per un valore numericu.

Questu hè esattamente ciò chì e funzioni sò aduprate. resolve и reverseResolve: L'applicazione riceve in qualchì modu u valore di stringa di a chjave (via l'input di l'utilizatori o via u servitore maestru), è pò cunvertisce in un indirizzu IP per l'usu internu. U restu di l'API riceve ancu stu valore invece di una stringa per simplicità.

Questu hè simile à a ricerca DNS, ma realizata in u locu nantu à u cliente.

Vale à dì, l'indirizzi IP ùn ponu esse spartuti trà i diversi clienti, è se un tipu d'identificatore globale hè necessariu, duverà esse generatu in una manera diversa.

Cunnessione lazy

UDP ùn hà micca bisognu di cunnessione, ma cum'è avemu vistu, WebRTC richiede un longu prucessu di cunnessione prima di pudè inizià a trasferimentu di dati trà dui pari.

Se vulemu furnisce u listessu livellu di astrazione, (sendto/recvfrom cù peers arbitrarii senza cunnessione previa), allora devenu fà una cunnessione "lazy" (ritardata) in l'API.

Questu hè ciò chì succede durante a cumunicazione normale trà u "servitore" è u "cliente" quandu si usa UDP, è ciò chì a nostra biblioteca deve fà:

  • Chjama di u servitore bind()per dì à u sistema operatore chì vole riceve pacchetti nantu à u portu specificatu.

Invece, publicheremu un portu apertu à Firebase sottu a chjave di u servitore è ascoltemu l'avvenimenti in u so subtree.

  • Chjama di u servitore recvfrom(), accettendu i pacchetti chì venenu da qualsiasi host in stu portu.

In u nostru casu, avemu bisognu di verificà a fila entrata di pacchetti mandati à stu portu.

Ogni portu hà a so propria fila, è aghjunghjemu i porti d'origine è di destinazione à l'iniziu di i datagrammi WebRTC in modu chì sapemu quale fila per trasmette quandu un novu pacchettu ghjunghje.

A chjama ùn hè micca bluccata, perchè s'ellu ùn ci hè micca pacchetti, avemu solu vultà -1 è stabilisce errno=EWOULDBLOCK.

  • U cliente riceve l'IP è u portu di u servitore per certi mezi esterni, è chjama sendto(). Questu hè ancu una chjama interna. bind(), dunque dopu recvfrom() riceverà a risposta senza eseguisce esplicitamente bind.

In u nostru casu, u cliente riceve esternamente a chjave di stringa è usa a funzione resolve() pè ottene un indirizzu IP.

À questu puntu, avemu principiatu un WebRTC handshake se i dui pari ùn sò micca cunnessi l'un à l'altru. Cunnessioni à diversi porti di u stessu paru utilizanu u listessu WebRTC DataChannel.

Avemu ancu realizatu indirettu bind()cusì chì u servitore pò ricunnisce in u prossimu sendto() in casu ch'ella sia chjusa per qualchì mutivu.

U servitore hè notificatu di a cunnessione di u cliente quandu u cliente scrive a so offerta SDP sottu l'infurmazione di u portu di u servitore in Firebase, è u servitore risponde cù a so risposta quì.

U diagramma sottu mostra un esempiu di flussu di messagiu per un schema di socket è a trasmissione di u primu messagiu da u cliente à u servitore:

Porting un ghjocu multiplayer da C++ à u web cù Cheerp, WebRTC è Firebase
Schema cumpletu di a fase di cunnessione trà u cliente è u servitore

cunchiusioni

Sè avete lettu finu à quì, probabilmente site interessatu à vede a teoria in azzione. U ghjocu pò esse ghjucatu teeworlds.leaningtech.com, pruvate !


Partita amichevule trà i culleghi

U codice di a biblioteca di a rete hè dispunibule liberamente à Github. Unisci a cunversazione nant'à u nostru canale à Gitter!

Source: www.habr.com

Add a comment