Portéiert e Multiplayer-Spill vu C++ op de Web mat Cheerp, WebRTC a Firebase

Aféierung

eis Firma Leaning Technologies bitt Léisunge fir traditionell Desktop Uwendungen op de Web ze portéieren. Eis C++ Compiler cheerp generéiert eng Kombinatioun vu WebAssembly a JavaScript, déi béid ubitt einfach Browser Interaktioun, an héich Leeschtung.

Als e Beispill vu senger Uwendung hu mir décidéiert e Multiplayer-Spill op de Web ze portéieren a gewielt Teeworlds. Teeworlds ass e Multiplayer XNUMXD Retro-Spill mat enger klenger awer aktiver Gemeinschaft vu Spiller (inklusiv ech!). Et ass kleng souwuel wat erofgeluede Ressourcen wéi CPU an GPU Ufuerderunge ugeet - en ideale Kandidat.

Portéiert e Multiplayer-Spill vu C++ op de Web mat Cheerp, WebRTC a Firebase
Lafen am Teeworlds Browser

Mir hu beschloss dëse Projet ze benotzen fir ze experimentéieren allgemeng Léisunge fir Porting Netzwierkcode op de Web. Dëst gëtt normalerweis op de folgende Weeër gemaach:

  • XMLHttpRequest/fetch, wann den Netzdeel nëmmen aus HTTP-Ufroen besteet, oder
  • WebSockets.

Béid Léisunge verlaangen Hosting vun engem Serverkomponent op der Server Säit, a weder erlaabt et als Transportprotokoll ze benotzen UDP. Dëst ass wichteg fir Echtzäit Uwendungen wéi Videokonferenzsoftware a Spiller, well et d'Liwwerung an d'Bestellung vu Protokollpäck garantéiert TCP kann en Hindernis fir niddereg latency ginn.

Et gëtt en drëtte Wee - benotzt d'Netzwierk vum Browser: WebRTC.

RTCDataChannel Et ënnerstëtzt souwuel zouverlässeg an onzouverlässeg Iwwerdroung (am leschte Fall probéiert et UDP als Transportprotokoll ze benotzen wa méiglech), a ka souwuel mat engem Fernserver an tëscht Browser benotzt ginn. Dëst bedeit datt mir déi ganz Applikatioun an de Browser portéiere kënnen, och de Serverkomponent!

Wéi och ëmmer, dëst kënnt mat enger zousätzlech Schwieregkeet: ier zwee WebRTC Peer kënne kommunizéieren, musse se e relativ komplexe Handshake maachen fir ze verbannen, wat e puer Drëtt-Partei Entitéiten erfuerdert (e Signalserver an een oder méi Serveren) STUN/DREI).

Idealerweis wëlle mir e Netzwierk API erstellen deen WebRTC intern benotzt, awer sou no wéi méiglech un enger UDP Sockets Interface ass déi keng Verbindung brauch.

Dëst erlaabt eis vum WebRTC ze profitéieren ouni komplex Detailer un den Applikatiounscode auszeleeën (dee mir sou wéineg wéi méiglech an eisem Projet wollten änneren).

De Mindestlagerquote vun WebRTC

WebRTC ass eng Rei vun APIen verfügbar a Browser déi peer-to-peer Iwwerdroung vun Audio, Video an arbiträr Daten ubitt.

D'Verbindung tëscht Peer ass etabléiert (och wann et NAT op eng oder zwou Säiten ass) mat STUN an / oder TURN Serveren duerch e Mechanismus genannt ICE. Peers austauschen ICE Informatioun a Kanalparameter iwwer Offer an Äntwert vum SDP Protokoll.

Wow! Wéi vill Ofkierzungen gläichzäiteg? Loosst eis kuerz erklären wat dës Begrëffer bedeiten:

  • Sessioun Traversal Utilities fir NAT (STUN) - e Protokoll fir NAT ëmzegoen an e Paar (IP, Hafen) ze kréien fir Daten direkt mam Host auszetauschen. Wann hien et fäerdeg bréngt seng Aufgab ze kompletéieren, da kënnen d'Pers onofhängeg Daten mateneen austauschen.
  • Traversal Benotzt Relais ronderëm NAT (DREI) gëtt och fir NAT Traversal benotzt, awer et implementéiert dëst andeems d'Donnéeën duerch e Proxy weidergeleet ginn, dee fir béid Kollegen sichtbar ass. Et füügt Latenz bäi an ass méi deier fir ëmzesetzen wéi STUN (well et an der ganzer Kommunikatiounssitzung applizéiert gëtt), awer heiansdo ass et déi eenzeg Optioun.
  • Interaktive Konnektivitéit Etablissement (ICE) benotzt fir déi bescht méiglech Method ze wielen fir zwee Peer ze verbannen baséiert op Informatioun, déi direkt vun der Verbindung vun Peer kritt gëtt, souwéi Informatioun, déi vun all Zuel vu STUN- a TURN-Server kritt gëtt.
  • Sëtzung Beschreiwung Protokoll (SDP) ass e Format fir Verbindungskanalparameter ze beschreiwen, zum Beispill ICE Kandidaten, Multimedia Codecs (am Fall vun engem Audio/Video Kanal), etc ... Ee vun de Kollegen schéckt eng SDP Offer, an déi zweet reagéiert mat enger SDP Äntwert ... Duerno gëtt e Kanal erstallt.

Fir esou eng Verbindung ze kreéieren, mussen d'Peer d'Informatioun sammelen déi se vun de STUN- a TURN-Server kréien an se mateneen austauschen.

De Problem ass datt se nach net d'Fäegkeet hunn direkt ze kommunizéieren, also muss en Out-of-Band Mechanismus existéieren fir dës Donnéeën auszetauschen: e Signalserver.

E Signalserver ka ganz einfach sinn, well seng eenzeg Aarbecht ass d'Daten tëscht Kollegen an der Handshakephase weiderzebréngen (wéi am Diagramm hei ënnendrënner).

Portéiert e Multiplayer-Spill vu C++ op de Web mat Cheerp, WebRTC a Firebase
Vereinfacht WebRTC Handshake Sequenz Diagramm

Teeworlds Network Model Iwwersiicht

Teeworlds Netzwierkarchitektur ass ganz einfach:

  • De Client an de Server Komponente sinn zwee verschidde Programmer.
  • D'Clientë kommen an d'Spill andeems se mat engem vun e puer Serveren verbannen, jidderee vun deenen nëmmen ee Spill gläichzäiteg hält.
  • All Datentransfer am Spill gëtt iwwer de Server duerchgefouert.
  • E spezielle Masterserver gëtt benotzt fir eng Lëscht vun all ëffentleche Serveren ze sammelen déi am Spillclient ugewise ginn.

Dank der Benotzung vu WebRTC fir Datenaustausch kënne mir de Serverkomponent vum Spill an de Browser transferéieren wou de Client läit. Dëst gëtt eis eng super Geleeënheet ...

Kréien Server lass

De Mangel u Serverlogik huet e flotte Virdeel: mir kënnen déi ganz Applikatioun als statesche Inhalt op Github Säiten oder op eiser eegener Hardware hannert Cloudflare ofsetzen, sou datt séier Downloads an héich Uptime gratis garantéieren. Tatsächlech kënne mir iwwer si vergiessen, a wa mir Gléck hunn an d'Spill populär gëtt, da muss d'Infrastruktur net moderniséiert ginn.

Wéi och ëmmer, fir datt de System funktionnéiert, musse mir nach ëmmer eng extern Architektur benotzen:

  • Een oder méi STUN Serveren: Mir hunn e puer fräi Optiounen aus ze wielen.
  • Op d'mannst een TURN-Server: et gi keng gratis Optiounen hei, also kënne mir entweder eisen eegenen astellen oder fir de Service bezuelen. Glécklecherweis kann déi meescht vun der Zäit d'Verbindung duerch STUN-Server etabléiert ginn (a liwwert richteg p2p), awer TURN ass gebraucht als Réckfalloptioun.
  • Signaliséierungsserver: Am Géigesaz zu deenen aneren zwee Aspekter ass d'Signalisatioun net standardiséiert. Wat de Signalserver tatsächlech verantwortlech ass hänkt e bësse vun der Applikatioun of. An eisem Fall, ier Dir eng Verbindung opbaut, ass et néideg eng kleng Quantitéit un Daten auszetauschen.
  • Teeworlds Master Server: Et gëtt vun anere Servere benotzt fir hir Existenz anzeklammen a vu Clienten fir ëffentlech Serveren ze fannen. Och wann et net erfuerderlech ass (Clienten kënnen ëmmer mat engem Server konnektéieren, deen se manuell kennen), et wier flott ze hunn, sou datt d'Spiller mat zoufälleg Leit u Spiller kënne matmaachen.

Mir hunn décidéiert Google's gratis STUN Serveren ze benotzen, an hunn en TURN Server selwer ofgesat.

Fir déi lescht zwee Punkten hu mir benotzt Firebase:

  • Den Teeworlds Master Server gëtt ganz einfach ëmgesat: als Lëscht vun Objeten mat Informatioun (Numm, IP, Kaart, Modus, ...) vun all aktive Server. Serveren publizéieren an update hiren eegene Objet, a Clienten huelen déi ganz Lëscht an weisen et dem Spiller. Mir weisen och d'Lëscht op der Homepage als HTML sou datt Spiller einfach op de Server klickt an direkt an d'Spill geholl ginn.
  • D'Signalisatioun ass enk verbonne mat eiser Sockets Implementatioun, beschriwwen an der nächster Sektioun.

Portéiert e Multiplayer-Spill vu C++ op de Web mat Cheerp, WebRTC a Firebase
Lëscht vun Serveren am Spill an op der Haaptsäit

Ëmsetzung vun Sockets

Mir wëllen eng API erstellen déi sou no bei Posix UDP Sockets wéi méiglech ass fir d'Zuel vun den néideg Ännerungen ze minimiséieren.

Mir wëllen och den néidege Minimum ëmsetzen, deen néideg ass fir den einfachsten Datenaustausch iwwer d'Netz.

Zum Beispill brauche mir kee richtege Routing: all Peer sinn op deemselwechte "virtuelle LAN" verbonne mat enger spezifescher Firebase-Datebankinstanz.

Dofir brauche mir keng eenzegaarteg IP Adressen: eenzegaarteg Firebase Schlësselwäerter (ähnlech wéi Domain Nimm) si genuch fir eenzegaarteg Peer z'identifizéieren, an all Peer gëtt lokal "gefälschte" IP Adressen un all Schlëssel zou, dee muss iwwersat ginn. Dëst eliminéiert komplett de Besoin fir eng global IP Adress Uweisung, wat eng net-trivial Aufgab ass.

Hei ass de Minimum API déi mir brauchen fir ëmzesetzen:

// 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);

D'API ass einfach an ähnlech wéi d'Posix Sockets API, awer huet e puer wichteg Differenzen: logging Callbacks, zouzeschreiwen lokal IPs, a lazy Verbindungen.

Callbacks registréieren

Och wann den urspréngleche Programm net-blockéierend I/O benotzt, muss de Code refaktoréiert ginn fir an engem Webbrowser ze lafen.

De Grond dofir ass datt d'Evenementschleife am Browser vum Programm verstoppt ass (sief et JavaScript oder WebAssembly).

Am gebiertege Ëmfeld kënne mir Code esou schreiwen

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;
    ...
  }
  ...
}

Wann den Event Loop fir eis verstoppt ass, da musse mir et an esou eppes ëmsetzen:

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

Lokal IP Aufgab

D'Node IDen an eisem "Netzwierk" sinn net IP Adressen, mee Firebase Schlësselen (si sinn Saiten déi esou ausgesinn: -LmEC50PYZLCiCP-vqde ).

Dëst ass bequem well mir kee Mechanismus brauche fir IPs ze ginn an hir Eenzegaartegkeet z'iwwerpréiwen (souwéi se entsuergen nodeems de Client trennt), awer et ass dacks néideg Peer mat engem numeresche Wäert z'identifizéieren.

Dat ass genee fir wat d'Funktioune benotzt ginn. resolve и reverseResolve: D'Applikatioun kritt iergendwéi de String-Wäert vum Schlëssel (iwwer Benotzerinput oder iwwer de Master-Server), a kann se an eng IP Adress fir intern Notzung konvertéieren. De Rescht vun der API kritt och dëse Wäert amplaz vun engem String fir Einfachheet.

Dëst ass ähnlech wéi DNS Lookup, awer lokal um Client duerchgefouert.

Dat ass, IP Adressen kënnen net tëscht verschiddene Clienten gedeelt ginn, a wann eng Aart vu globalen Identifizéierer gebraucht gëtt, muss se op eng aner Manéier generéiert ginn.

Lazy Verbindung

UDP brauch keng Verbindung, awer wéi mir gesinn hunn, erfuerdert WebRTC e laange Verbindungsprozess ier et ufänke kann Daten tëscht zwee Peer ze transferéieren.

Wa mir deeselwechten Abstraktiounsniveau wëllen ubidden, (sendto/recvfrom mat arbiträre Kollegen ouni virdru Verbindung), da musse se eng "faul" (verspéit) Verbindung bannent der API maachen.

Dëst ass wat während der normaler Kommunikatioun tëscht dem "Server" an dem "Client" geschitt wann Dir UDP benotzt, a wat eis Bibliothéik soll maachen:

  • Server rifft bind()fir de Betribssystem ze soen datt et Päckchen um spezifizéierte Hafen wëll kréien.

Amplaz publizéieren mir en oppene Port op Firebase ënner dem Serverschlëssel a lauschteren no Eventer a sengem Subtree.

  • Server rifft recvfrom(), Pakete akzeptéieren, déi vun all Host op dësem Hafen kommen.

An eisem Fall musse mir d'inkommende Schlaang vu Päckchen iwwerpréiwen, déi op dësen Hafen geschéckt ginn.

All Hafen huet seng eege Schlaang, a mir fügen d'Quell- an Destinatiounshäfen un den Ufank vun den WebRTC-Datagrammen derbäi, fir datt mir wësse wéi eng Schlaang weiderfuere soll wann en neie Paket ukomm ass.

Den Uruff ass net blockéiert, also wann et keng Päck gëtt, gi mir einfach -1 zréck a setzen errno=EWOULDBLOCK.

  • De Client kritt d'IP an den Hafen vum Server duerch e puer extern Mëttelen, a rifft sendto(). Dëst mécht och en internen Uruff. bind(), also duerno recvfrom() kritt d'Äntwert ouni explizit Bindung auszeféieren.

An eisem Fall kritt de Client extern de String Schlëssel a benotzt d'Funktioun resolve() eng IP Adress ze kréien.

Zu dësem Zäitpunkt initiéieren mir e WebRTC Handshake wann déi zwee Peer nach net matenee verbonne sinn. Verbindunge mat verschiddene Ports vum selwechte Peer benotzen dee selwechte WebRTC DataChannel.

Mir maachen och indirekt bind()sou datt de Server an den nächsten nei konnektéieren kann sendto() am Fall wou et aus irgendege Grënn zou ass.

De Server gëtt iwwer d'Verbindung vum Client informéiert wann de Client seng SDP Offer ënner der Serverportinformatioun an Firebase schreift, an de Server reagéiert mat senger Äntwert do.

D'Diagramm hei ënnen weist e Beispill vu Message Flux fir e Socket Schema an d'Transmissioun vun der éischter Message vum Client op de Server:

Portéiert e Multiplayer-Spill vu C++ op de Web mat Cheerp, WebRTC a Firebase
Komplett Diagramm vun der Verbindungsphase tëscht Client a Server

Konklusioun

Wann Dir esou wäit gelies hutt, sidd Dir wahrscheinlech interesséiert d'Theorie an Handlung ze gesinn. D'Spill kann op gespillt ginn teeworlds.leaningtech.com, Versich et!


Frëndschaftsmatch tëscht de Kollegen

Den Netzwierkbibliothéikscode ass gratis verfügbar op Github. Maacht mat beim Gespréich op eisem Kanal um Gitter!

Source: will.com

Setzt e Commentaire