Kandime mitmikmängu C++-st veebis Cheerp, WebRTC ja Firebase abil

Sissejuhatus

Meie ettevõte Leaning Technologies pakub lahendusi traditsiooniliste lauaarvuti rakenduste üleviimiseks webi. Meie C++ kompilaator Cheerp genereerib WebAssembly ja JavaScript'i kombinatsiooni, mis tagab nii lihtsa interaktsiooni brauseriga, kui ka kõrge jõudluse.

Kuna näide selle rakendamisest otsustasime portida veebis mitme mängijaga mängu ja valisime selleks Teeworlds. Teeworlds on mitme mängijaga kahe mõõtmega retro mäng, millel on väike, kuid aktiivne mängijaskond (sealhulgas mina!). See on väike nii allalaadimise mahult kui ka CPU ja GPU nõudmistes - ideaalne kandidaat.

Kandime mitmikmängu C++-st veebis Cheerp, WebRTC ja Firebase abil
Brauseris töötav Teeworlds

Otsustasime kasutada seda projekti, et katsetada üldisi lahendusi võrgu koodi portimiseks veebis. Tavalised teostusviisid on järgmised:

  • XMLHttpRequest/fetch, kui võrgukoht koosneb ainult HTTP-päringutest, või
  • WebSockets.

Mõlemad lahendused nõuavad serverikomponendi majutamist serveripool, ja kumbki ei võimalda kasutada UDPkui transport protokolli. See on oluline reaalajas rakenduste, näiteks videokonverentside ja mängude jaoks, kuna kohaletoimetamise ja paketide järjekorra tagamise protokoll TCP võivad häirida madalaid latentsuseid.

On ka kolmas tee - kasutada võrku brauserist: WebRTC.

RTCDataChannel toetab nii usaldusväärset kui ka usaldusvõimetut edastamist (viimasel juhul püütakse võimaluse korral kasutada transportprotokollina UDP-d) ning seda saab rakendada nii kaugserveriga kui ka brauserite vahel. See tähendab, et saame portida kogu rakenduse brauserisse, sealhulgas serverikomponendi!

Siiski on sellega seotud täiendav keerukus: enne kui kaks WebRTC peer'i saavad andmeid vahetada, peavad nad läbima suhteliselt keerulise „käepigistuse“ (handshake) protseduuri ühenduse loomiseks, mille jaoks on vajalik mitme kolmanda osapoole üksuse kaasamine (signaaliserver ja üks või mitu STUN/TURN).

Idealjuhul sooviksime luua võrguliidese, mis kasutab sisemiselt WebRTC-d, kuid on nii lähedane UDP socketi liidesele, mille jaoks ei ole ühenduse loomine vajalik.

See võimaldab meil kasutada WebRTC eeliseid, ilma et peaksime paljastama rakenduse koodi keerulisi üksikasju (mida soovisime oma projektis võimalikult vähe muuta).

Minimaalne WebRTC

WebRTC on brauserite olev API-de kogum, mis võimaldab peer-to-peer heli, videot ja mis tahes andmete edastamist.

Ühendus kahe peer'i vahel luuakse (isegi kui NAT on kummalgi või kummalgi pool) STUN ja/või TURN serverite abil mehhanismi nimega ICE kaudu. Peer'id vahetavad ICE teavet ja kanalite parameetreid läbi SDP protokolli offer ja answer.

Vau! Kui palju akronüüme korraga. Selgitatagem lühidalt, mida need mõisted tähendavad:

  • Session Traversal Utilities for NAT (STUN) — protokoll NAT-i ületamiseks ja (IP, port) paari saamiseks, et vahetada andmeid otse hostiga. Kui see suudab oma ülesande täita, saavad peer'id omavahel andmeid vahetada.
  • Traversal Using Relays around NAT (TURN) kasutatakse samuti NAT-i ületamiseks, kuid see teostab seda suunates andmeid läbi proksi, mis on mõlemale peer'ile nähtav. See lisab viivituse ja on kallim täita kui STUN (kuna rakendatakse kogu suhtlemise seansi vältel), kuid mõnikord on see ainus võimalik variant.
  • Interactive Connectivity Establishment (ICE) kasutatakse kahe peer'i parima ühendusmeetodi valimiseks, tuginedes teabele, mis on saadud otse peer'ide ühendamisel, ning teabele, mis on saadud mõnelt STUN ja TURN serverilt.
  • Session Description Protocol (SDP) on ühenduskanali parameetrite, nagu ICE kandidaadid, meedia kodeerijad (heli/video kanali puhul) jne, kirjeldamise formaat... Üks peer saadab SDP Offer («ettepanek»), teine vastab SDP Answer («vastus»). Pärast seda luuakse kanal.

Selle ühenduse loomiseks peavad peer'id koguma teabe, mille nad said STUN ja TURN serveritelt, ja vahetama selle omavahel.

Probleem on selles, et neil pole hetkel võimalust andmeid otse vahetada, seega peab andmete vahetamiseks olema olemas väline mehhanism: signalisatsiooniserver.

Signalisatsiooniserver võib olla väga lihtne, sest selle ainus ülesanne on andmete suunamine peer'ide vahel „kätlemise“ faasis (nagu on näidatud alloleval skeemil).

Kandime mitmikmängu C++-st veebis Cheerp, WebRTC ja Firebase abil
Lihtsustatud skeem WebRTC kätlemise järjestusest

Teeworldsi võrgu mudeli ülevaade

Teeworldsi võrgueelarhitektuur on väga lihtne:

  • Kliendi ja serveri komponendid on kaks erinevat programmi.
  • Kliendid astuvad mängu, ühendudes ühe mitmest serverist, millest igaüks hostib korraga ainult ühte mängu.
  • Kogu mängu andmete edastus toimub serveri kaudu.
  • Eriline meistriserver kogub nimekirja kõigist avalikest serveritest, mis kuvatakse mängu kliendis.

WebRTC kasutamine andmete vahetamiseks võimaldab meil viia mängu serveri komponendi brauserisse, kus klient asub. See annab meile suurepärase võimaluse…

Vabane serveritest

Serveri loogika puudumisel on meeldiv eelis: saame rakenduse üles seada staatilise sisuna Github Pages'ile või oma seadmetele Cloudflare'i taga, tagades seeläbi tasuta kiire laadimisaja ja kõrge tööaja. Tegelikult saame neist unustada ja kui meil veab ning mäng muutub populaarseks, ei pea me infrastruktuuri uuendama.

Kuid süsteemi toimimiseks peame ikkagi kasutama välist arhitektuuri:

  • Üks või mitu STUN serverit: meil on valida mitmete tasuta variantide vahel.
  • Vähemalt üks TURN server: siin ei ole tasuta variante, seega võime kas seadistada oma või maksta teenuse eest. Õnneks saab enamikul juhtudel ühenduse luua STUN serverite kaudu (tagades tõelise p2p), kuid TURN on vajalik varuvariantina.
  • Signaaliserver: erinevalt kahest teisest aspektist ei ole signalisatsioon standardiseeritud. See, mille eest signaaliserver tõeliselt vastutab, sõltub osaliselt rakendusest. Meie puhul tuleb enne ühenduse loomist vahetada väike hulk andmeid.
  • Teeworldsi meistriserver: seda kasutatakse teiste serverite poolt oma olemasolu teavitamiseks ja klientide poolt avalike serverite otsimiseks. Kuigi see ei ole kohustuslik (kliendid saavad alati ühenduda tuntud serveriga käsitsi), oleks hea, kui see oleks olemas, et mängijad saaksid osaleda mängudes juhuslike inimestega.

Oleme otsustanud kasutada Google'i tasuta STUN servereid ja ühe TURN serveri oleme seadistanud ise.

Viimaste kahe punkti jaoks kasutasime Firebase:

  • Teeworldsi mahitusserver on ellu viidatud väga lihtsalt: see on objektide nimekiri, mis sisaldab iga aktiivse serveri teavet (nimi, IP, kaart, režiim, …). Serverid avaldavad ja uuendavad oma objekti, samas kui kliendid võtavad kogu nimekirja ja kuvavad selle mängijale. Kuvame nimekirja ka kodulehel HTMLina, et mängijad saaksid lihtsalt serverile klõpsata ja otse mängu minna.
  • Signalisatsioon on tihedalt seotud meie soketite elluviimisega, mis on kirjeldatud järgmises jaotises.

Kandime mitmikmängu C++-st veebis Cheerp, WebRTC ja Firebase abil
Serverite loetelu mängus ja kodulehel

Sokettide elluviimine

Soovime luua API, mis on võimalikult lähedane Posix UDP soketitele, et minimeerida vajalike muudatuste hulka.

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

Näiteks ei vaja me tõelist marsruutimist: kõik peers asuvad ühes "virtuaalses LANis", mis on seotud konkreetse Firebase andmebaasi instantsiga.

Seetõttu ei vaja me ainulaadseid IP-aadresse: piisab, kui kasutada peerside unikaalsete Firebase võtmearvude (sarnane domeeninimedele) põhjal ainulaadset tuvastamist ning iga peer määrab iga võtme jaoks kohalikult "vale" IP-aadressi, mille tuleb teisendada. See vabastab meid täielikult globaalsete IP-aadresside määramise vajadusest, mis on mitte trivialne ülesanne.

Siin on minimaalne API, mille 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 sarnaneb Posix sokettide APIle, kuid tal on mõned olulised erinevused: tagasikutsumise registreerimine, kohalike IP-de määramine ja "liskontakt".

Tagasikutsumise registreerimine

Ieven kui algne programm kasutab mittesaggivat sisend-väljundit, tuleb koodi veebibrauseris käivitamiseks refaktoreerida.

Selle põhjuseks on see, et brauseri sündmuste tsükkel on programmilt varjatud (olgu see siis JavaScript või WebAssembly).

Kohalikus keskkonnas saame kirjutada koodi järgmiselt

while(running) {
  select(...); // oota I/O sündmusi
  while(true) {
    int r = readfrom(...); // proovi lugeda
    if (r < 0 && errno == EWOULDBLOCK) // ei rohkem andmeid saadaval
      break;
    ...
  }
  ...
}

Kui sündmuste tsükkel on meie jaoks varjatud, tuleb see muuta millekski selliseks:

auto cb = []() { // see kutsub välja, kui uus teave on saadaval
  while(true) {
    int r = readfrom(...); // proovi lugeda
    if (r < 0 && errno == EWOULDBLOCK) // ei rohkem andmeid saadaval
      break;
    ...
  }
  ...
};
recvCallback(cb); // registreeri tagasikutsumine

Kohalike IP-de määramine

Sõlmide identifikaatorid meie "võrgus" ei ole IP-aadressid, vaid Firebase'i võtmed (need on stringid, mis näevad välja nii: -LmEC50PYZLCiCP-vqde ).

See on mugav, kuna me ei vaja mehhanismi IP-de määramiseks ja nende unikaalsuse kontrollimiseks (ka nende utiliseerimine pärast kliendi väljalülitamist), kuid sageli on vajalik sünkroniseerida peer'e numbrilise väärtuse järgi.

Just selleks kasutatakse funktsioone resolve ja reverseResolve: rakendus saab somehow stringi võtme väärtuse (kasutaja sisendi või meister-serveri kaudu) ja suudab selle sisendada IP-aadressiks sisemiselt kasutamiseks. Puuduvad ka API teised osad selle lihtsuse huvides saavad selle väärtuse stringi asemel.

See meenutab DNS-i otsingut, ainult et see toimub kliendi kohapeal.

Teisisõnu, IP-aadresse ei saa jagada erinevate klientide vahel, ja kui on vajalik mingi globaalne identifikaator, siis tuleb see genereerida muul viisil.

Laineühendus

UDP ei vaja ühendust, kuid nagu oleme näinud, peab WebRTC enne kahe peer'i andmevahetuse alustamist läbi viima pika ühendamisprotsessi.

Kui soovime tagada sama abstraktsiooni taseme, (sendto/recvfrom suvaliste peer'idega eelneva ühendamiseta), siis peame API-s läbi viima "laisk" (edasi lükatud) ühendamise.

Nii toimub tavapärane andmevahetus "serveri" ja "kliendi" vahel UDP kasutamisel ning mida peab meie teek täitma:

  • Server kutsub üles bind(), et teavitada operatsioonisüsteemi, et soovib saada pakette määratud sadamasse.

Selle asemel avaldame Firebase'is avatud sadama serveri võtme all ja kuulame selle aladressi sündmusi.

  • Server kutsub üles recvfrom(), võttes vastu pakette, mis tulevad mistahes hostilt selle sadama sisse.

Meie juhul peame kontrollima sissetulevate pakettide järjekorda, mis on saadetud sellele sadamale.

Igal sadamal on oma järjekord, ja me lisame WebRTC datagrammidesse alg- ja lõppsadama, et teada, kuhu suunata uus pakk, kui see saabub.

Kutse on mittesurmav, seega kui pakette ei ole, siis tagastame lihtsalt -1 ja seadistame errno=EWOULDBLOCK.

  • Klient saab mitmete väliste vahendite kaudu serveri IP ja sadama ning kutsub välja sendto(). Samuti tehakse sisemine kutse bind(), seega järgnev recvfrom() saab vastuse ilma selge 'bind' teostamiseta.

Kliendile antakse stringi võti ja kasutatakse funktsiooni resolve() IP-aadressi saamiseks.

Sellel etapil alustame WebRTC "käepigistust", kui kaks peer'it ei ole veel omavahel ühendatud. Erinevatele pordile ühendused kasutavad sama WebRTC DataChannel'i.

Samuti teeme kaudse bind(), et server saaks ühenduse järgmisel korral taastada, sendto() kui see mingil põhjusel suleti.

Server saab teate kliendi ühendusest, kui klient salvestab oma SDP pakkumise serveri sadama teabe alla Firebase'is ning server vastab seal samas oma vastusega.

Alloleval skeemil on näidatud sõnumite liikumise näide soketite skeemi ja esimesed sõnumid kliendilt serverisse edastamisel:

Kandime mitmikmängu C++-st veebis Cheerp, WebRTC ja Firebase abil
Täielik ühenduse loomise etapi skeem kliendi ja serveri vahel

Kokkuvõte

Kui olete lõpuni lugenud, siis ilmselt huvitab teid teooria praktikas. Mängu saab mängida aadressil teeworlds.leaningtech.com, proovige kindlasti!


Sõbralik mäng kolleegide vahel

Tulemüügiteegi koodi on vabalt saadaval Github. Liituge meie kanaliga Gitter!

Allikas: habr.com

Osta usaldusväärne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid | ProHoster