Portearje fan in multiplayer-spiel fan C ++ nei it web mei Cheerp, WebRTC en Firebase

Ynlieding

ús bedriuw Leaning Technologies biedt oplossingen foar it portearjen fan tradisjonele buroblêdapplikaasjes nei it web. Us C ++ kompilator cheerp genereart in kombinaasje fan WebAssembly en JavaScript, dy't beide leveret ienfâldige browser ynteraksje, en hege prestaasjes.

As foarbyld fan syn tapassing, wy besletten in port in multiplayer spultsje oan it web en keas Teeworlds. Teeworlds is in multiplayer XNUMXD retro-spiel mei in lytse, mar aktive mienskip fan spilers (ynklusyf my!). It is lyts sawol yn termen fan ynladen boarnen as CPU- en GPU-easken - in ideale kandidaat.

Portearje fan in multiplayer-spiel fan C ++ nei it web mei Cheerp, WebRTC en Firebase
Rint yn 'e Teeworlds-blêder

Wy besletten om dit projekt te brûken om mei te eksperimintearjen algemiene oplossings foar it porten fan netwurkkoade nei it web. Dit wurdt normaal dien op 'e folgjende manieren:

  • XMLHttpRequest/fetch, as it netwurkdiel allinich bestiet út HTTP-oanfragen, of
  • web sockets.

Beide oplossingen fereaskje it hostjen fan in serverkomponint oan 'e serverkant, en net ien makket it mooglik om te brûken as ferfierprotokol UDP. Dit is wichtich foar real-time applikaasjes lykas software foar fideokonferinsjes en spultsjes, om't it de levering en folchoarder fan protokolpakketten garandearret TCP kin in hinder wurde foar lege latency.

D'r is in tredde manier - brûk it netwurk fan 'e browser: WebRTC.

RTCDataChannel It stipet sawol betroubere as ûnbetroubere oerdracht (yn it lêste gefal besiket it UDP as ferfierprotokol wannear mooglik te brûken), en kin sawol brûkt wurde mei in tsjinner op ôfstân as tusken browsers. Dit betsjut dat wy de hiele applikaasje kinne portearje nei de browser, ynklusyf de serverkomponint!

Dit komt lykwols mei in ekstra muoite: foardat twa WebRTC-peers kinne kommunisearje, moatte se in relatyf komplekse handshake útfiere om te ferbinen, wat ferskate entiteiten fan tredden fereasket (in sinjaaltsjinner en ien of mear servers STUN/BOCHT).

Ideal wolle wy in netwurk API meitsje dy't WebRTC yntern brûkt, mar is sa ticht mooglik by in UDP Sockets-ynterface dy't gjin ferbining hoecht te meitsjen.

Dit sil ús tastean om te profitearjen fan WebRTC sûnder komplekse details te bleatstelle oan 'e applikaasjekoade (dy't wy sa min mooglik yn ús projekt feroarje woene).

It minimalisearre bedriuw fan WebRTC

WebRTC is in set fan API's beskikber yn browsers dy't peer-to-peer oerdracht fan audio, fideo en willekeurige gegevens leveret.

De ferbining tusken peers wurdt oprjochte (sels as der NAT oan ien of beide kanten) mei help fan STUN en / of TURN tsjinners troch in meganisme neamd ICE. Peers wikselje ICE-ynformaasje en kanaalparameters út fia oanbod en antwurd fan it SDP-protokol.

Wow! Hoefolle ôfkoartings tagelyk? Litte wy koart útlizze wat dizze termen betsjutte:

  • Sesje Traversal Utilities foar NAT (STUN) - in protokol foar it omgean fan NAT en it krijen fan in pear (IP, poarte) foar it útwikseljen fan gegevens direkt mei de host. As it slagget om syn taak te foltôgjen, dan kinne peers selsstannich gegevens mei elkoar útwikselje.
  • Traversal Mei help fan relais om NAT (BOCHT) wurdt ek brûkt foar NAT-traversal, mar it ymplementearret dit troch it trochstjoeren fan gegevens fia in proxy dy't sichtber is foar beide peers. It foeget latency ta en is djoerder om te ymplementearjen dan STUN (om't it wurdt tapast yn 'e heule kommunikaasje sesje), mar soms is it de ienige opsje.
  • Ynteraktive Konnektivität Oprjochting (IIS) brûkt om de bêste mooglike metoade te selektearjen foar it ferbinen fan twa peers basearre op ynformaasje krigen fan it direkt ferbinen fan peers, lykas ek ynformaasje ûntfongen troch in oantal STUN- en TURN-tsjinners.
  • Sesje Beskriuwing Protokol (RDS) is in opmaak foar it beskriuwen fan ferbiningskanaalparameters, bygelyks ICE-kandidaten, multimedia-codecs (yn it gefal fan in audio-/fideokanaal), ensfh. .. Hjirnei wurdt in kanaal makke.

Om sa'n ferbining te meitsjen, moatte peers de ynformaasje sammelje dy't se krije fan 'e STUN- en TURN-tsjinners en it mei-inoar útwikselje.

It probleem is dat se noch net de mooglikheid hawwe om direkt te kommunisearjen, dus moat der in out-of-band-meganisme bestean om dizze gegevens te wikseljen: in sinjaaltsjinner.

In sinjaaltsjinner kin heul ienfâldich wêze, om't har iennichste taak is om gegevens troch te stjoeren tusken peers yn 'e handshake-faze (lykas werjûn yn it diagram hjirûnder).

Portearje fan in multiplayer-spiel fan C ++ nei it web mei Cheerp, WebRTC en Firebase
Simplified WebRTC handshake sequence diagram

Teeworlds Network Model Oersjoch

Teeworlds netwurkarsjitektuer is heul ienfâldich:

  • De client- en serverkomponinten binne twa ferskillende programma's.
  • Klanten geane it spultsje yn troch te ferbinen mei ien fan ferskate servers, dy't elk mar ien spultsje tagelyk host.
  • Alle gegevens oerdracht yn it spul wurdt útfierd fia de tsjinner.
  • In spesjale masterserver wurdt brûkt om in list te sammeljen fan alle iepenbiere servers dy't werjûn wurde yn 'e spielklient.

Mei tank oan it brûken fan WebRTC foar gegevens útwikseling, kinne wy ​​oerdrage de server komponint fan it spul nei de blêder dêr't de klant leit. Dit jout ús in geweldige kâns ...

Krij de tsjinners kwyt

It ûntbrekken fan serverlogika hat in moai foardiel: wy kinne de heule applikaasje ynsette as statyske ynhâld op Github-siden of op ús eigen hardware efter Cloudflare, en soargje dus foar rappe downloads en hege uptime fergees. Eins kinne wy ​​har ferjitte, en as wy gelok hawwe en it spultsje populêr wurdt, dan hoecht de ynfrastruktuer net te modernisearjen.

Om it systeem lykwols te wurkjen, moatte wy noch in eksterne arsjitektuer brûke:

  • Ien of mear STUN-tsjinners: Wy hawwe ferskate fergese opsjes om út te kiezen.
  • Op syn minst ien TURN-tsjinner: d'r binne hjir gjin fergese opsjes, dus wy kinne ús eigen ynstelle of betelje foar de tsjinst. Gelokkich kin de measte fan 'e tiid de ferbining wurde oprjochte fia STUN-tsjinners (en jouwe wiere p2p), mar TURN is nedich as in fallback-opsje.
  • Sinjaaltsjinner: Oars as de oare twa aspekten is sinjalearring net standerdisearre. Wêr't de sinjaaltsjinner eins ferantwurdlik foar sil wêze, hinget wat ôf fan 'e applikaasje. Yn ús gefal, foardat jo in ferbining meitsje, is it nedich om in lyts bedrach fan gegevens te wikseljen.
  • Teeworlds Master Server: It wurdt brûkt troch oare servers om har bestean te advertearjen en troch kliïnten om iepenbiere servers te finen. Hoewol it net nedich is (kliïnten kinne altyd ferbine mei in server wêrfan se manuell witte), soe it moai wêze om te hawwen sadat spilers meidwaan kinne oan spultsjes mei willekeurige minsken.

Wy besletten de fergese STUN-servers fan Google te brûken, en hawwe sels ien TURN-tsjinner ynset.

Foar de lêste twa punten hawwe wy brûkt Firebase:

  • De masterserver fan Teeworlds wurdt heul ienfâldich ymplementearre: as in list mei objekten mei ynformaasje (namme, IP, kaart, modus, ...) fan elke aktive tsjinner. Servers publisearje en fernije har eigen objekt, en kliïnten nimme de hiele list en werjaan it oan de spiler. Wy ek werjaan de list op 'e thússide as HTML sadat spilers kinne gewoan klikke op de tsjinner en wurde nommen direkt nei it spul.
  • Sinjalearring is nau besibbe oan ús sockets ymplemintaasje, beskreaun yn de folgjende paragraaf.

Portearje fan in multiplayer-spiel fan C ++ nei it web mei Cheerp, WebRTC en Firebase
List fan servers binnen it spultsje en op 'e thússide

Utfiering fan sockets

Wy wolle in API meitsje dy't sa ticht by Posix UDP Sockets mooglik is om it oantal wizigingen nedich te minimalisearjen.

Wy wolle ek it nedige minimum ymplementearje dat nedich is foar de ienfâldichste gegevensútwikseling oer it netwurk.

Wy hawwe bygelyks gjin echte routing nedich: alle peers binne op itselde "firtuele LAN" ferbûn mei in spesifike Firebase-database-eksimplaar.

Dêrom hawwe wy gjin unike IP-adressen nedich: unike Firebase-kaaiwearden (lykas domeinnammen) binne genôch om peers unyk te identifisearjen, en elke peer jout lokaal "falske" IP-adressen ta oan elke kaai dy't oerset wurde moat. Dit elimineert folslein de needsaak foar globale IP-adres tawizing, dat is in net-triviale taak.

Hjir is de minimale API dy't wy moatte ymplementearje:

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

De API is ienfâldich en fergelykber mei de Posix Sockets API, mar hat in pear wichtige ferskillen: logging callbacks, tawize lokale IPs, en luie ferbinings.

Registrearje Callbacks

Sels as it orizjinele programma net-blokkearjende I/O brûkt, moat de koade refaktorearre wurde om yn in webblêder te rinnen.

De reden hjirfoar is dat de barrenslus yn 'e browser ferburgen is foar it programma (it is JavaScript of WebAssembly).

Yn 'e native omjouwing kinne wy ​​koade sa skriuwe

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

As de barrenslus foar ús ferburgen is, dan moatte wy it omsette yn soksawat:

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

Lokale IP opdracht

De node-ID's yn ús "netwurk" binne gjin IP-adressen, mar Firebase-kaaien (it binne stringen dy't der sa útsjen: -LmEC50PYZLCiCP-vqde ).

Dit is handich, om't wy gjin meganisme nedich hawwe foar it tawizen fan IP's en it kontrolearjen fan har unykheid (lykas it fersmiten nei't de kliïnt los is), mar it is faaks nedich om peers te identifisearjen troch in numerike wearde.

Dit is krekt wêr't de funksjes foar brûkt wurde. resolve и reverseResolve: De applikaasje ûntfangt op ien of oare manier de tekenrige wearde fan 'e kaai (fia brûkersynput of fia de masterserver), en kin it omsette yn in IP-adres foar yntern gebrûk. De rest fan 'e API ûntfangt ek dizze wearde ynstee fan in tekenrige foar ienfâld.

Dit is fergelykber mei DNS-opsykjen, mar útfierd lokaal op 'e kliïnt.

Dat is, IP-adressen kinne net dield wurde tusken ferskate kliïnten, en as in soarte fan globale identifier nedich is, sil it op in oare manier generearre wurde moatte.

Lazy ferbining

UDP hat gjin ferbining nedich, mar lykas wy hawwe sjoen, fereasket WebRTC in lang ferbiningsproses foardat it kin begjinne mei it oerdragen fan gegevens tusken twa peers.

As wy itselde nivo fan abstraksje wolle leverje, (sendto/recvfrom mei willekeurige peers sûnder foarôfgeande ferbining), dan moatte se in "luie" (fertrage) ferbining útfiere binnen de API.

Dit is wat bart by normale kommunikaasje tusken de "tsjinner" en de "kliïnt" by it brûken fan UDP, en wat ús bibleteek moat dwaan:

  • Tsjinner ropt bind()om it bestjoeringssysteem te fertellen dat it pakketten op 'e oantsjutte poarte ûntfange wol.

Ynstee dêrfan sille wy in iepen poarte nei Firebase publisearje ûnder de serverkaai en harkje nei eveneminten yn syn subtree.

  • Tsjinner ropt recvfrom(), pakketten akseptearje dy't komme fan elke host op dizze poarte.

Yn ús gefal moatte wy de ynkommende wachtrige fan pakketten kontrolearje nei dizze poarte.

Elke poarte hat in eigen wachtrige, en wy foegje de boarne- en bestimmingsporten ta oan it begjin fan 'e WebRTC-datagrammen, sadat wy witte hokker wachtrige nei trochstjoere as in nij pakket komt.

De oprop is net-blokkearjend, dus as d'r gjin pakketten binne, jouwe wy gewoan -1 werom en set errno=EWOULDBLOCK.

  • De kliïnt ûntfangt de IP en haven fan 'e tsjinner troch guon eksterne middels, en ropt sendto(). Dit makket ek in ynterne oprop. bind(), dêrom folgjende recvfrom() sil it antwurd ûntfange sûnder eksplisyt bining út te fieren.

Yn ús gefal ûntfangt de kliïnt ekstern de tekenrige kaai en brûkt de funksje resolve() om in IP-adres te krijen.

Op dit punt begjinne wy ​​in WebRTC-handshake as de twa peers noch net mei elkoar ferbûn binne. Ferbinings mei ferskate havens fan deselde peer brûke deselde WebRTC DataChannel.

Wy dogge ek yndirekt bind()sadat de tsjinner kin opnij ferbine yn de folgjende sendto() yn gefal it sluten om ien of oare reden.

De tsjinner wurdt op 'e hichte brocht fan' e ferbining fan 'e kliïnt as de kliïnt syn SDP-oanbod skriuwt ûnder de tsjinnerportynformaasje yn Firebase, en de tsjinner reagearret dêr mei syn antwurd.

It diagram hjirûnder lit in foarbyld sjen fan berjochtstream foar in socketskema en de oerdracht fan it earste berjocht fan 'e kliïnt nei de tsjinner:

Portearje fan in multiplayer-spiel fan C ++ nei it web mei Cheerp, WebRTC en Firebase
Folsleine diagram fan de ferbining faze tusken client en tsjinner

konklúzje

As jo ​​​​sa fier hawwe lêzen, binne jo wierskynlik ynteressearre om de teory yn aksje te sjen. It spul kin spile wurde op teeworlds.leaningtech.com, besykje it!


Freonlike wedstriid tusken kollega's

It netwurk biblioteek koade is frij beskikber op Github. Doch mei oan it petear op ús kanaal op Gitter!

Boarne: www.habr.com

Add a comment