Mitme mängijaga mängu teisaldamine C++-st veebi Cheerpi, WebRTC ja Firebase'i abil

Sissejuhatus

meie firma Kalduvad tehnoloogiad pakub lahendusi traditsiooniliste töölauarakenduste teisaldamiseks veebi. Meie C++ kompilaator rõõmustama genereerib WebAssembly ja JavaScripti kombinatsiooni, mis pakub mõlemat lihtne brauseri interaktsioonja suure jõudlusega.

Selle rakenduse näitena otsustasime portida mitme mängijaga mängu veebi ja valisime Teeworlds. Teeworlds on mitme mängijaga XNUMXD retromäng väikese, kuid aktiivse mängijate kogukonnaga (ka mina!). See on väike nii allalaaditud ressursside kui ka CPU ja GPU nõuete poolest – ideaalne kandidaat.

Mitme mängijaga mängu teisaldamine C++-st veebi Cheerpi, WebRTC ja Firebase'i abil
Töötab Teeworldsi brauseris

Otsustasime seda projekti katsetamiseks kasutada üldised lahendused võrgukoodi teisaldamiseks veebi. Tavaliselt tehakse seda järgmistel viisidel:

  • XMLHttpRequest/Ftch, kui võrguosa koosneb ainult HTTP päringutest või
  • WebSockets.

Mõlemad lahendused nõuavad serveripoolset serverikomponendi hostimist ja kumbki ei võimalda kasutada transpordiprotokollina UDP. See on oluline reaalajas kasutatavate rakenduste jaoks, nagu videokonverentsitarkvara ja mängud, kuna see tagab protokollipakettide kohaletoimetamise ja järjestuse TCP võib takistada madalat latentsust.

On kolmas viis - kasutage võrku brauserist: WebRTC.

RTCDataChannel See toetab nii usaldusväärset kui ka ebausaldusväärset edastamist (viimasel juhul proovib võimalusel kasutada UDP-d transpordiprotokollina) ning seda saab kasutada nii kaugserveriga kui ka brauserite vahel. See tähendab, et saame portida brauserisse kogu rakenduse, sealhulgas serverikomponendi!

Sellega kaasneb aga täiendav raskus: enne kui kaks WebRTC partnerit saavad suhelda, peavad nad ühenduse loomiseks sooritama suhteliselt keeruka käepigistuse, mis nõuab mitut kolmanda osapoole olemit (signaalserver ja üks või mitu serverit URMASTUS/Pöörake).

Ideaalis sooviksime luua võrgu API, mis kasutab sisemiselt WebRTC-d, kuid on võimalikult lähedal UDP Socketsi liidesele, mis ei vaja ühendust looma.

See võimaldab meil kasutada WebRTC-d, ilma et peaksime rakenduse koodile (mida soovisime oma projektis võimalikult vähe muuta) keerukaid üksikasju paljastada.

Minimaalne WebRTC

WebRTC on brauserites saadaolevate API-liidete komplekt, mis pakub heli-, video- ja suvaliste andmete võrdõiguslikku edastamist.

Ühendus partnerite vahel luuakse (isegi kui ühel või mõlemal poolel on NAT) STUN- ja/või TURN-serverite abil mehhanismi nimega ICE kaudu. Partnerid vahetavad ICE teavet ja kanali parameetreid SDP-protokolli pakkumise ja vastuse kaudu.

Vau! Mitu lühendit korraga? Selgitame lühidalt, mida need terminid tähendavad:

  • Seansi läbimise utiliidid NAT-i jaoks (URMASTUS) — protokoll NAT-ist möödahiilimiseks ja paari (IP, port) hankimiseks andmete vahetamiseks otse hostiga. Kui tal õnnestub oma ülesanne täita, saavad eakaaslased üksteisega iseseisvalt andmeid vahetada.
  • Läbimine NAT-i ümber asuvate releede abil (Pöörake) kasutatakse ka NAT-i läbimiseks, kuid see rakendab seda, edastades andmed mõlemale partnerile nähtava puhverserveri kaudu. See lisab latentsust ja selle rakendamine on kallim kui STUN (kuna seda rakendatakse kogu suhtlusseansi jooksul), kuid mõnikord on see ainus võimalus.
  • Interaktiivse ühenduvuse loomine (ICE) kasutatakse parima võimaliku meetodi valimiseks kahe kaaslase ühendamiseks, tuginedes otse ühendavatelt partneritelt saadud teabele, samuti teabele, mis on saadud suvalise arvu STUN- ja TURN-serverite poolt.
  • Seansi kirjelduse protokoll (SDP) on ühenduskanali parameetrite kirjeldamise formaat, näiteks ICE kandidaadid, multimeediumikoodekid (heli/video kanali puhul) jne... Üks partneritest saadab SDP Offer’i ja teine ​​vastab SDP vastusega .. Pärast seda luuakse kanal.

Sellise ühenduse loomiseks peavad partnerid koguma STUN- ja TURN-serveritelt saadud teavet ja vahetama seda omavahel.

Probleem on selles, et neil ei ole veel võimalust otse suhelda, seega peab nende andmete vahetamiseks olema ribaväline mehhanism: signaaliserver.

Signaaliserver võib olla väga lihtne, kuna selle ainus ülesanne on kätlemise faasis kaaslaste vahel andmeid edastada (nagu on näidatud alloleval diagrammil).

Mitme mängijaga mängu teisaldamine C++-st veebi Cheerpi, WebRTC ja Firebase'i abil
Lihtsustatud WebRTC käepigistuse järjestusskeem

Teeworldsi võrgumudeli ülevaade

Teeworldsi võrguarhitektuur on väga lihtne:

  • Kliendi- ja serverikomponendid on kaks erinevat programmi.
  • Kliendid sisenevad mängu, ühendades ühe mitmest serverist, millest igaüks majutab korraga ainult ühte mängu.
  • Kogu mängu andmeedastus toimub serveri kaudu.
  • Spetsiaalset peaserverit kasutatakse kõigi mängukliendis kuvatavate avalike serverite loendi kogumiseks.

Tänu WebRTC kasutamisele andmevahetuseks saame mängu serverikomponendi üle kanda brauserisse, kus klient asub. See annab meile suurepärase võimaluse...

Vabanege serveritest

Serveriloogika puudumisel on üks tore eelis: saame kogu rakenduse juurutada staatilise sisuna Github Pagesile või oma riistvarale Cloudflare'i taga, tagades nii kiire allalaadimise ja kõrge tööaja tasuta. Tegelikult võime need unustada ja kui meil veab ja mäng populaarseks muutub, siis ei pea taristut kaasajastama.

Süsteemi toimimiseks peame siiski kasutama välist arhitektuuri:

  • Üks või mitu STUN-serverit: meil on mitu tasuta valikut.
  • Vähemalt üks TURN-server: siin pole tasuta valikuid, nii et saame kas ise seadistada või teenuse eest maksta. Õnneks saab enamasti ühenduse luua STUN-serverite kaudu (ja pakkuda tõelist p2p-d), kuid varuvalikuna on vaja TURN-i.
  • Signaaliserver: erinevalt kahest teisest aspektist ei ole signaalimine standardiseeritud. See, mille eest signaaliserver tegelikult vastutab, sõltub mõnevõrra rakendusest. Meie puhul on enne ühenduse loomist vaja vahetada väike hulk andmeid.
  • Teeworlds Master Server: seda kasutavad teised serverid oma olemasolu reklaamimiseks ja kliendid avalike serverite leidmiseks. Kuigi see pole nõutav (kliendid saavad alati käsitsi ühenduse luua serveriga, millest nad teavad), oleks tore omada, et mängijad saaksid juhuslike inimestega mängudes osaleda.

Otsustasime kasutada Google'i tasuta STUN-servereid ja juurutasime ise ühe TURN-serveri.

Kahe viimase punkti puhul kasutasime Firebase:

  • Teeworldsi peaserver on rakendatud väga lihtsalt: objektide loendina, mis sisaldab teavet (nimi, IP, kaart, režiim jne) iga aktiivse serveri kohta. Serverid avaldavad ja värskendavad oma objekti ning kliendid võtavad kogu loendi ja kuvavad selle mängijale. Samuti kuvame loendi avalehel HTML-ina, et mängijad saaksid lihtsalt serveril klõpsata ja otse mängu suunata.
  • Signaalimine on tihedalt seotud meie pistikupesade rakendamisega, mida kirjeldatakse järgmises jaotises.

Mitme mängijaga mängu teisaldamine C++-st veebi Cheerpi, WebRTC ja Firebase'i abil
Serverite loend mängu sees ja avalehel

Pistikupesade rakendamine

Soovime luua API, mis oleks võimalikult lähedal Posixi UDP Socketsidele, et minimeerida vajalike muudatuste arvu.

Samuti soovime rakendada vajalikku miinimumi, mis on vajalik lihtsaimaks andmevahetuseks üle võrgu.

Näiteks me ei vaja tegelikku marsruutimist: kõik partnerid on samas "virtuaalses LAN-is", mis on seotud konkreetse Firebase'i andmebaasi eksemplariga.

Seetõttu ei vaja me kordumatuid IP-aadresse: unikaalsetest Firebase'i võtmeväärtustest (sarnaselt domeeninimedele) piisab sarnaste partnerite kordumatuks tuvastamiseks ja iga partner määrab kohalikult "võlts" IP-aadressid igale võtmele, mida tuleb tõlkida. See välistab täielikult globaalse IP-aadressi määramise vajaduse, mis on mittetriviaalne ülesanne.

Siin on minimaalne API, mida peame rakendama:

// 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 on lihtne ja sarnane Posix Socketsi API-ga, kuid sellel on mõned olulised erinevused. tagasihelistamiste logimine, kohalike IP-de määramine ja laisad ühendused.

Tagasihelistuste registreerimine

Isegi kui algne programm kasutab mitteblokeerivat I/O-d, tuleb kood veebibrauseris töötamiseks ümber teha.

Põhjus on selles, et brauseris olev sündmuste silmus on programmi eest peidetud (olgu selleks JavaScript või WebAssembly).

Natiivses keskkonnas saame sellist koodi kirjutada

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

Kui sündmuste ahel on meie jaoks peidetud, peame selle muutma millekski selliseks:

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

Kohalik IP määramine

Meie "võrgu" sõlme ID-d ei ole IP-aadressid, vaid Firebase'i võtmed (need on stringid, mis näevad välja järgmised: -LmEC50PYZLCiCP-vqde ).

See on mugav, kuna me ei vaja mehhanismi IP-de määramiseks ja nende unikaalsuse kontrollimiseks (nagu ka nende kõrvaldamiseks pärast kliendi lahtiühendamist), kuid sageli on vaja partnerid numbrilise väärtuse järgi tuvastada.

See on täpselt see, milleks funktsioone kasutatakse. resolve и reverseResolve: rakendus võtab kuidagi vastu võtme stringi väärtuse (kasutaja sisendi või peaserveri kaudu) ja saab selle sisekasutuseks IP-aadressiks teisendada. Ka ülejäänud API saab selle väärtuse lihtsuse huvides stringi asemel.

See sarnaneb DNS-i otsinguga, kuid tehakse kliendil kohapeal.

See tähendab, et IP-aadresse ei saa erinevate klientide vahel jagada ja kui on vaja mingit globaalset identifikaatorit, tuleb see genereerida teistmoodi.

Laisk ühendus

UDP ei vaja ühendust, kuid nagu nägime, vajab WebRTC pikka ühendusprotsessi, enne kui saab hakata andmeid kahe kaaslase vahel edastama.

Kui tahame pakkuda sama abstraktsioonitaset, (sendto/recvfrom suvaliste partneritega ilma eelneva ühenduseta), peavad nad API sees looma "laiska" (viivitatud) ühenduse.

See juhtub tavapärase suhtluse ajal "serveri" ja "kliendi" vahel UDP kasutamisel ning mida meie teek peaks tegema:

  • Serveri kõned bind()et öelda operatsioonisüsteemile, et ta soovib määratud pordis pakette vastu võtta.

Selle asemel avaldame serverivõtme all Firebase'i avatud pordi ja kuulame selle alampuu sündmusi.

  • Serveri kõned recvfrom(), mis võtab vastu pakette, mis tulevad selle pordi mis tahes hostilt.

Meie puhul peame kontrollima sellesse porti saadetud pakettide sissetulevat järjekorda.

Igal pordil on oma järjekord ning me lisame lähte- ja sihtpordid WebRTC datagrammide algusesse, et teaksime, millisesse järjekorda uue paketi saabudes edasi suunata.

Kõne on mitteblokeeriv, nii et kui pakette pole, tagastame lihtsalt -1 ja seadistame errno=EWOULDBLOCK.

  • Klient saab serveri IP ja pordi mõne välise vahendiga ning helistab sendto(). See teeb ka sisekõne. bind(), seega järgnev recvfrom() saab vastuse ilma sidumist selgesõnaliselt käivitamata.

Meie puhul saab klient stringi võtme väljastpoolt ja kasutab funktsiooni resolve() IP-aadressi saamiseks.

Siinkohal algatame WebRTC käepigistuse, kui kaks partnerit pole veel üksteisega ühendatud. Ühendused sama kaaslase erinevate portidega kasutavad sama WebRTC DataChanneli.

Teostame ka kaudset bind()et server saaks järgmisel korral uuesti ühenduse luua sendto() juhuks, kui see mingil põhjusel suletakse.

Serverit teavitatakse kliendi ühendusest, kui klient kirjutab Firebase'is serveri pordi teabe alla oma SDP-pakkumise ja server vastab seal oma vastusega.

Allolev diagramm näitab pistikupesa skeemi sõnumivoo näidet ja esimese sõnumi edastamist kliendilt serverisse:

Mitme mängijaga mängu teisaldamine C++-st veebi Cheerpi, WebRTC ja Firebase'i abil
Kliendi ja serveri vahelise ühenduse etapi täielik diagramm

Järeldus

Kui olete nii kaugele lugenud, olete tõenäoliselt huvitatud teooriast praktikas. Mängu saab edasi mängida teeworlds.leaningtech.com, proovi seda!


Sõbralik mäng kolleegide vahel

Võrguteegi kood on vabalt saadaval aadressil Github. Liituge vestlusega meie kanalil aadressil gitter!

Allikas: www.habr.com

Lisa kommentaar