Jokalari anitzeko joko bat C++-tik sarera eraman Cheerp, WebRTC eta Firebase-rekin

Sarrera

gure konpainia Leaning Teknologiak mahaigaineko aplikazio tradizionalak sarera eramateko irtenbideak eskaintzen ditu. Gure C++ konpilatzailea animatu WebAssembly eta JavaScript konbinazio bat sortzen du, biak eskaintzen dituena arakatzailearen interakzio sinplea, eta errendimendu handia.

Bere aplikazioaren adibide gisa, jokalari anitzeko joko bat sarera eramatea erabaki genuen eta aukeratu genuen Teeworlds. Teeworlds jokalari anitzeko 2D retro joko bat da, jokalarien komunitate txiki baina aktibo batekin (ni barne!). Txikia da deskargatutako baliabideei eta CPU eta GPU eskakizunei dagokienez, hautagai ezin hobea.

Jokalari anitzeko joko bat C++-tik sarera eraman Cheerp, WebRTC eta Firebase-rekin
Teeworlds arakatzailean exekutatzen

Proiektu hau esperimentatzeko erabiltzea erabaki dugu sareko kodea sarera eramateko irtenbide orokorrak. Hau normalean modu hauetan egiten da:

  • XMLHttpRequest/fetch, sarearen zatia HTTP eskaerek soilik osatzen badute, edo
  • WebSocket-ak.

Bi soluzioek zerbitzariaren osagai bat ostatzea eskatzen dute zerbitzariaren aldean, eta ez batak ez besteak ez dute onartzen garraio-protokolo gisa erabiltzeko UDP. Garrantzitsua da denbora errealeko aplikazioetarako, hala nola bideokonferentziaren software eta jokoetarako, protokolo-paketeen entrega eta ordena bermatzen duelako. TCP latentzia baxurako oztopo bihur daiteke.

Hirugarren modu bat dago: erabili sarea arakatzailetik: WebRTC.

RTCDataChannel Transmisio fidagarria eta fidagarria ez den transmisioa onartzen du (azken kasu honetan UDP garraio-protokolo gisa erabiltzen saiatzen da ahal den guztietan), eta urruneko zerbitzari batekin zein nabigatzaileen artean erabil daiteke. Horrek esan nahi du aplikazio osoa arakatzailera eraman dezakegula, zerbitzariaren osagaia barne!

Hala ere, horrek zailtasun gehigarri batekin dakar: bi WebRTC parekide komunikatu aurretik, esku-emate konplexu samarra egin behar dute konektatzeko, eta horrek hirugarrenen hainbat entitate behar ditu (seinale-zerbitzari bat eta zerbitzari bat edo gehiago). ZORDU/TXANDA).

Egokiena, sareko API bat sortu nahiko genuke WebRTC barnean erabiltzen duena, baina konexiorik ezarri behar ez duen UDP Sockets interfaze batetik ahalik eta gertuen dagoen.

Horri esker, WebRTC aprobetxatuko dugu aplikazioaren kodeari xehetasun konplexuak azaldu beharrik gabe (gure proiektuan ahalik eta gutxien aldatu nahi genuen).

Gutxieneko WebRTC

WebRTC nabigatzaileetan eskuragarri dauden API multzo bat da, audio, bideo eta datu arbitrarioen peer-to-peer transmisioa eskaintzen duena.

Parekideen arteko konexioa (nahiz eta alde batean edo bietan NAT egon) STUN eta/edo TURN zerbitzariak erabiliz ICE izeneko mekanismo baten bidez ezartzen da. Ikaskideek ICE informazioa eta kanal parametroak trukatzen dituzte SDP protokoloaren eskaintza eta erantzunaren bidez.

Aupa! Zenbat laburdura aldi berean? Azal dezagun laburki zer esan nahi duten termino hauek:

  • Session Traversal Utilities NATerako (ZORDU) β€” NAT saihesteko eta ostalariarekin datuak zuzenean trukatzeko bikotea (IP, ataka) lortzeko protokoloa. Bere zeregina burutzea lortzen badu, kideek modu independentean trukatu ditzakete datuak elkarren artean.
  • Travesal Erreleak erabiliz NAT inguruan (TXANDA) NAT zeharketarako ere erabiltzen da, baina hau inplementatzen du datuak bi parekideentzat ikusgai dagoen proxy baten bidez birbidaltzen ditu. Latentzia gehitzen du eta STUN baino ezartzea garestiagoa da (komunikazio-saio osoan aplikatzen baita), baina batzuetan aukera bakarra da.
  • Konektagarritasun interaktiboaren ezarpena (ICE) bi parekide konektatzeko ahalik eta metodo onena hautatzeko erabiltzen da parekideek zuzenean konektatzean lortutako informazioan oinarrituta, baita STUN eta TURN zerbitzariek jasotako informazioan ere.
  • Saioaren Deskribapena Protokoloa (SDP) konexio-kanalaren parametroak deskribatzeko formatua da, adibidez, ICE hautagaiak, multimedia kodekak (audio/bideo kanal baten kasuan), etab... Parekideetako batek SDP Eskaintza bat bidaltzen du, eta bigarrenak SDP Erantzun batekin erantzuten du. . . Horren ostean, kanal bat sortzen da.

Konexio hori sortzeko, kideek STUN eta TURN zerbitzarietatik jasotzen duten informazioa bildu eta elkarren artean trukatu behar dute.

Arazoa da oraindik ez dutela zuzenean komunikatzeko gaitasunik, beraz, bandaz kanpoko mekanismo bat existitu behar da datu horiek trukatzeko: seinaleztapen zerbitzari bat.

Seinale-zerbitzari bat oso erraza izan daiteke, bere lan bakarra esku-emate faseko kideen artean datuak bidaltzea delako (beheko diagraman ikusten den bezala).

Jokalari anitzeko joko bat C++-tik sarera eraman Cheerp, WebRTC eta Firebase-rekin
WebRTC esku-harremanaren sekuentzia diagrama sinplifikatua

Teeworlds sareko ereduaren ikuspegi orokorra

Teeworlds sarearen arkitektura oso erraza da:

  • Bezeroaren eta zerbitzariaren osagaiak bi programa ezberdin dira.
  • Bezeroak jokoan sartzen dira hainbat zerbitzarietako batera konektatuz, eta horietako bakoitzak joko bakarra hartzen du aldi berean.
  • Jokoaren datu-transferentzia guztiak zerbitzariaren bidez egiten dira.
  • Zerbitzari nagusi berezi bat erabiltzen da jokoaren bezeroan bistaratzen diren zerbitzari publiko guztien zerrenda biltzeko.

Datuak trukatzeko WebRTC erabiltzeari esker, jokoaren zerbitzariaren osagaia bezeroa dagoen arakatzailera transferi dezakegu. Horrek aukera paregabea ematen digu...

Zerbitzariak kendu

Zerbitzariaren logika ezak abantaila polita du: aplikazio osoa eduki estatiko gisa heda dezakegu Github Pages-en edo Cloudflareren atzean dagoen gure hardwarean, horrela deskarga azkarrak eta denbora libre handia bermatuz doan. Izan ere, haietaz ahaztu gaitezke, eta zortea badugu eta jokoa ezaguna egiten bada, orduan ez da azpiegitura modernizatu beharko.

Hala ere, sistemak funtziona dezan, oraindik kanpoko arkitektura bat erabili behar dugu:

  • STUN zerbitzari bat edo gehiago: doako hainbat aukera ditugu aukeran.
  • TURN zerbitzari bat gutxienez: hemen ez dago doako aukerarik, beraz, gurea konfigura dezakegu edo zerbitzua ordaindu. Zorionez, gehienetan konexioa STUN zerbitzarien bidez ezar daiteke (eta benetako p2p eman), baina TURN behar da ordezko aukera gisa.
  • Seinaleztapen zerbitzaria: beste bi alderdietan ez bezala, seinaleztapena ez dago estandarizatuta. Seinale-zerbitzaria benetan arduratuko dena aplikazioaren araberakoa da. Gure kasuan, konexio bat ezarri aurretik, beharrezkoa da datu kopuru txiki bat trukatzea.
  • Teeworlds Master Server: beste zerbitzariek haien existentzia iragartzeko erabiltzen dute eta bezeroek zerbitzari publikoak aurkitzeko. Beharrezkoa ez den arren (bezeroek eskuz ezagutzen duten zerbitzari batera konektatu ahal izango dira beti), atsegina izango litzateke jokalariek ausazko jendearekin jokoetan parte har dezaten.

Google-ren doako STUN zerbitzariak erabiltzea erabaki genuen, eta TURN zerbitzari bat geuk zabaldu genuen.

Erabili ditugun azken bi puntuetarako Firebase:

  • Teeworlds zerbitzari nagusia oso sinplea da inplementatzen: zerbitzari aktibo bakoitzaren informazioa (izena, IP, mapa, modua, ...) duten objektuen zerrenda gisa. Zerbitzariek beren objektua argitaratzen eta eguneratzen dute, eta bezeroek zerrenda osoa hartzen dute eta jokalariari bistaratzen diote. Zerrenda hasierako orrian ere bistaratzen dugu HTML gisa, jokalariek zerbitzarian klik egin eta jokora zuzenean eraman ahal izateko.
  • Seinalizazioa oso lotuta dago gure socketen inplementazioarekin, hurrengo atalean deskribatuta.

Jokalari anitzeko joko bat C++-tik sarera eraman Cheerp, WebRTC eta Firebase-rekin
Jokoaren barruan eta hasierako orrian zerbitzarien zerrenda

Entxufeak ezartzea

Posix UDP Socket-etatik ahalik eta hurbilen dagoen API bat sortu nahi dugu beharrezkoak diren aldaketa kopurua gutxitzeko.

Era berean, sarearen bidez datu-truke errazena izateko beharrezkoa den gutxieneko behar dena ezarri nahi dugu.

Adibidez, ez dugu benetako bideratzerik behar: pareko guztiak Firebase datu-basearen instantzia zehatz batekin lotutako "LAN birtual" berean daude.

Hori dela eta, ez dugu IP helbide esklusiborik behar: Firebase-ren gako-balio bakarrak (domeinu-izenen antzekoak) nahikoak dira parekideak modu bakarrean identifikatzeko, eta parekide bakoitzak itzuli behar den gako bakoitzari IP helbide "faltsuak" esleitzen dizkio lokalean. Honek erabat ezabatzen du IP helbide globala esleitzeko beharra, eta hori ez da hutsala.

Hona hemen inplementatu behar dugun gutxieneko APIa:

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

API sinplea eta Posix Sockets APIaren antzekoa da, baina desberdintasun garrantzitsu batzuk ditu: deiak erregistratzea, IP lokalak esleitzea eta konexio alferrak.

Deiak itzulerak erregistratzea

Jatorrizko programak blokeorik gabeko I/O erabiltzen badu ere, kodea birfactorizatu behar da web arakatzaile batean exekutatzeko.

Horren arrazoia da arakatzailearen gertaeren begizta programatik ezkutatuta dagoela (izan JavaScript edo WebAssembly).

Jatorrizko ingurunean honelako kodea idatz dezakegu

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

Gertaeren begizta ezkutatuta badago, honelako zerbait bihurtu behar dugu:

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

Tokiko IP esleipena

Gure "sareko" nodoen IDak ez dira IP helbideak, Firebase-ko gakoak baizik (honelako kateak dira: -LmEC50PYZLCiCP-vqde ).

Hau komenigarria da, ez dugulako IPak esleitzeko eta haien berezitasuna egiaztatzeko mekanismorik behar (baita bezeroa deskonektatu ondoren deuseztatzeko ere), baina askotan parekideak zenbakizko balio baten bidez identifikatzea beharrezkoa izaten da.

Funtzioak horretarako erabiltzen dira zehazki. resolve ΠΈ reverseResolve: Aplikazioak nolabait gakoaren katearen balioa jasotzen du (erabiltzailearen sarreraren bidez edo zerbitzari nagusiaren bidez), eta IP helbide bihur dezake barne erabilerarako. Gainerako APIek ere balio hori jasotzen dute kate baten ordez, sinpletasunerako.

Hau DNS bilaketaren antzekoa da, baina bezeroan lokalean egiten da.

Hau da, IP helbideak ezin dira bezero ezberdinen artean partekatu, eta nolabaiteko identifikatzaile global bat behar bada, beste modu batean sortu beharko da.

Konexio alferra

UDP-k ez du konexiorik behar, baina ikusi dugunez, WebRTC-k konexio prozesu luzea behar du bi parekideren artean datuak transferitzen hasi aurretik.

Abstrakzio maila bera eman nahi badugu, (sendto/recvfrom aldez aurretik konexiorik gabeko parekide arbitrarioekin), orduan APIaren barruan konexio "alferra" (atzeratua) egin behar dute.

Hau da "zerbitzariaren" eta "bezeroaren" arteko komunikazio arruntean gertatzen dena UDP erabiltzean, eta gure liburutegiak egin beharko lukeena:

  • Zerbitzariaren deiak bind()sistema eragileari adierazitako atakan paketeak jaso nahi dituela esateko.

Horren ordez, Firebase-n ataka ireki bat argitaratuko dugu zerbitzariaren gakoaren azpian eta bere azpizuhaitzeko gertaerak entzungo ditugu.

  • Zerbitzariaren deiak recvfrom(), ataka honetako edozein ostalaritik datozen paketeak onartuz.

Gure kasuan, ataka honetara bidalitako paketeen sarrerako ilara egiaztatu behar dugu.

Ataka bakoitzak bere ilara du, eta WebRTC datagramen hasieran iturburu eta helmuga portuak gehitzen ditugu, pakete berri bat iristen denean zein ilaratara birbidali behar dugun jakin dezagun.

Deia ez da blokeatzen, beraz, paketerik ez badago, -1 itzuli besterik ez dugu eta ezarri errno=EWOULDBLOCK.

  • Bezeroak zerbitzariaren IPa eta ataka jasotzen ditu kanpoko bitarteko batzuen bidez, eta dei egiten du sendto(). Honek barne dei bat ere egiten du. bind(), beraz, ondorengoa recvfrom() erantzuna jasoko du bind esplizituki exekutatu gabe.

Gure kasuan, bezeroak kanpotik jasotzen du kate-gakoa eta funtzioa erabiltzen du resolve() IP helbidea lortzeko.

Une honetan, WebRTC esku-ematea abiarazten dugu bi parekideak oraindik elkarren artean konektatuta ez badaude. Parekide bereko ataka ezberdinetarako konexioek WebRTC DataChannel bera erabiltzen dute.

Zeharkakoa ere egiten dugu bind()zerbitzaria hurrengoan berriro konektatu ahal izateko sendto() arrazoiren bategatik itxiko balitz.

Zerbitzariari bezeroaren konexioaren berri ematen zaio bezeroak bere SDP eskaintza idazten duenean Firebase-ko zerbitzariaren ataka-informazioaren azpian, eta zerbitzariak erantzunarekin erantzuten du bertan.

Beheko diagramak socket-eskema baten mezu-fluxuaren adibide bat erakusten du eta bezerotik zerbitzarira lehen mezuaren transmisioa:

Jokalari anitzeko joko bat C++-tik sarera eraman Cheerp, WebRTC eta Firebase-rekin
Bezeroaren eta zerbitzariaren arteko konexio-fasearen diagrama osoa

Ondorioa

Honaino irakurri baduzu, ziurrenik teoria martxan ikustea interesatuko zaizu. Jolasa joka daiteke teeworlds.leaningtech.com, saiatu!


Lankideen arteko lagunarteko partida

Sareko liburutegiaren kodea doan eskura daiteke helbidean Github. Sartu elkarrizketara gure kanalean hemen Gitter!

Iturria: www.habr.com

Gehitu iruzkin berria