Kandideerime mitme kasutajaga mÀngu C++-st veebiversiooni Cheerp, WebRTC ja Firebase abil

Sissejuhatus

Meie ettevÔte Leaning Technologies pakub lahendusi traditsiooniliste töölauarakenduste veebiversioonide jaoks. Meie C++ kompass Cheerp genereerib WebAssembly ja JavaScripti kombinatsiooni, mis tagab nii lihtsa suhtluse brauseriga, kui ka kÔrge jÔudluse.

Kuna nĂ€ide selle kasutamisest otsustasime portida veebile jaoks mitmikmĂ€ngu ja valisime selle jaoks Teeworlds. Teeworlds on kahemÔÔtmeline retro-mitmik mĂ€ng vĂ€ikese, kuid aktiivse mĂ€ngijate kogukonnaga (sealhulgas mina!). See on vĂ€ike nii alla laadimise ressurssides kui ka CPU ja GPU nĂ”uetes — ideaalne kandidaat.

Kandideerime mitme kasutajaga mÀngu C++-st veebiversiooni Cheerp, WebRTC ja Firebase abil
Teeworlds, mis töötab brauseris

Otsustasime kasutada seda projekti, et eksperimenteerida ĂŒldiste lahendustega vĂ”rgu koodi portimiseks veebisse. Tavaliselt teostatakse seda jĂ€rgmiste viisidega:

  • XMLHttpRequest/fetch, kui vĂ”rguosa koosneb ainult HTTP-pĂ€ringutest, vĂ”i
  • WebSockets.

MÔlemad lahendused nÔuavad, et serveri komponent oleks serveripoolse hostimisega, ja kumbki ei luba kasutada transportprotokollina UDP. See on oluline reaalajas rakenduste, nagu videokonverentside tarkvara ja mÀngude jaoks, sest protokolli pakkumise ja jÀrjekorra tagamise garantiid TCP vÔivad segada madala latentsusega.

On olemas ka kolmas tee — kasutada brauserivĂ”rku: WebRTC.

RTCDataChannel toetab nii usaldusvÀÀrset kui ka usaldusvÀÀrset edastamist (viimasel juhul pĂŒĂŒab see vĂ”imalusel kasutada transpordiprotokollina UDP-d) ning vĂ”ib töötada nii kaugserveriga kui ka brauserite vahel. See tĂ€hendab, et saame portida kogu rakenduse brauserisse, sealhulgas serverikomponendi!

Siiski on sellega seotud lisaraskus: enne kui kaks WebRTC peer'i saavad andmeid vahetada, peavad nad lĂ€bima suhteliselt keerulise „kĂ€tlemise“ protseduuri ĂŒhendamiseks, milleks on vajalik mitu kolmanda osapoole erinevat ĂŒksust (signaaliserver ja ĂŒks vĂ”i mitu serverit STUN/TURN).

Ideaalis tahaksime luua vĂ”rguteabe, mis kasutab siseselt WebRTC-d, kuid on nii lĂ€hedane UDP Sockets liidesele, et see ei nĂ”ua ĂŒhenduse loomist.

See aitab meil kasutada WebRTC eeliseid, ilma et peaksime rakenduse koodi keerulisi ĂŒksikasju paljastama (mida soovisime oma projektis vĂ€hem muuta).

Minimaalne WebRTC

WebRTC on brauserites olemasolev API komplekt, mis vÔimaldab peer-to-peer heli, video ja mis tahes andmete edastamist.

Ühendus peer’ide vahel luuakse (isegi kui NAT on ĂŒhelt vĂ”i kahelt poolt) STUN ja/vĂ”i TURN serverite abil, kasutades mehhanismi nimega ICE. Peer’id vahetavad ICE teavet ja kanaliparameetreid SDP protokolli pakkumise ja vastuse kaudu.

Vau! Nii palju lĂŒhendeid korraga. Selgiteks lĂŒhidalt, mida need mĂ”isted tĂ€hendavad:

  • Session Traversal Utilities for NAT (STUN) — protokoll NAT-i ĂŒletamiseks ja (IP, port) pĂĄru hankimiseks, et andmeid otse hostiga vahetada. Kui see suudab oma ĂŒlesande tĂ€ita, saavad peer’id omavahel andmeid iseseisvalt vahetada.
  • Traversal Using Relays around NAT (TURN) kasutatakse ka NAT-i ĂŒletamiseks, kuid see teeb seda, suunates andmeid kaudu vaheproksi, mis on mĂ”lemale peerile nĂ€htav. See lisab viivituse ja on tĂ€itmisel kulukam kui STUN (kuna see rakendatakse kogu suhtlusseansi vĂ€ltel), kuid mĂ”nikord on see ainus vĂ”imalik variant.
  • Interaktiivne Ühenduse Loomine (ICE) kasutatakse kahe peer'i parima ĂŒhendusviisi valimiseks, tuginedes teabele, mis on saadud otsese ĂŒhenduse loomisel peer'ide vahel, ning teabele, mis on saadud igasugustelt STUN ja TURN serveritelt.
  • Seansi Kirjeldamise Protokoll (SDP) on ĂŒhenduskanali parameetrite, nĂ€iteks ICE kandidaatidest, meedia koodekidest (heli/videokanali puhul) jne, kirjeldamise formaat. Üks peer saadab SDP pakkumise ("pakkumine"), teine vastab SDP vastusega ("vastus"). PĂ€rast seda luuakse kanal.

Selle ĂŒhenduse loomiseks peavad peer'id koguma teavet, mille nad on saanud STUN ja TURN serveritelt, ja vahetama seda omavahel.

Probleem on selles, et neil pole veel vÔimalust andmeid otse vahetada, seega peab andmete vahetamiseks olema olemas vaheline mehhanism: signaaliserver.

Signaalserver vĂ”ib olla vĂ€ga lihtne, kuna selle ainus ĂŒlesanne on edastada andmeid paaride vahel „kĂ€tlemise” etapis (nagu on kujutatud alloleval skeemil).

Kandideerime mitme kasutajaga mÀngu C++-st veebiversiooni Cheerp, WebRTC ja Firebase abil
Lihtsustatud WebRTC kÀtlemise jÀrjestuse skeem

Teeworldsi vĂ”rgu mudeli ĂŒlevaade

Teeworldsi vÔrgu arhitektuur on vÀga lihtne:

  • Kliendi ja serveri komponendid on kaks erinevat programmi.
  • Kliendid liituvad mĂ€nguga, ĂŒhendudes ĂŒhe mitmest serverist, millest igaĂŒks hostib korraga ainult ĂŒhte mĂ€ngu.
  • KĂ”ik mĂ€ngus sisalduvad andmed edastatakse serveri kaudu.
  • Eraldi meistriserverit kasutatakse kĂ”igi avalike serverite nimekirja kogumiseks, mis kuvatakse mĂ€ngu kliendis.

Andmete vahetamiseks WebRTC kasutamine vÔimaldab meil viia mÀngu serveri komponendi brauserisse, kus klient asub. See annab meile suurepÀrase vÔimaluse


Vabaneda serveritest

Serveri loogika puudumine toob kaasa meeldiva eelise: saame rakenduse paigutada staatilise sisuna GitHub Pages'i vÔi oma seadmetesse Cloudflare'i tagant, kindlustades seelÀbi tasuta kiire laadimise ja kÔrge tööaja. Tegelikult vÔiksime neist tÀielikult unustada, ning kui meil vedama hakkab ja mÀng muutub populaarseks, siis infrastruktuuri uuendama ei pea.

Kuid sĂŒsteemi tĂ”husaks töötamiseks peame siiski kasutama vĂ€list arhitektuuri:

  • Üks vĂ”i mitu STUN serverit: meil on valida mitme tasuta variandi vahel.
  • KĂ”ik vĂ€hemalt ĂŒks TURN server: siin tasuta variante ei ole, seega vĂ”ime kas seada oma vĂ”i maksta teenuse eest. Õnneks saab suurema osa ajast ĂŒhenduse luua STUN serverite kaudu (tagades tĂ”elise p2p), kuid TURN on vajalik varuvĂ”imalusena.
  • Signaaliserver: erinevalt teistest kaht aspektist ei ole signaalimise standardiseerimist. See, mille eest signaaliserver tegelikult vastutab, sĂ”ltub osade kaupa rakendusest. Meie puhul tuleb enne ĂŒhenduse loomist vahetada vĂ€ike andmemaht.
  • Teeworldsi meistriserver: seda kasutavad teised serverid oma olemasolu teatamiseks ning kliendid avalike serverite leidmiseks. Kuigi see ei ole kohustuslik (kliendid saavad alati manuaalselt ĂŒhenduda tuttava serveriga), oleks see kasulik, et mĂ€ngijad saaksid osaleda mĂ€ngudes juhuslike inimestega.

Otsustasime kasutada Google'i tasuta STUN-servereid, ning ĂŒhe TURN-serveri seadistasime ise.

Kahe viimase punkti jaoks kasutasime Firebase:

  • Teeworldsi meistriserver on vĂ€ga lihtsalt rakendatud: see on objektiloetelu, mis sisaldab teavet (nimi, IP, kaart, reĆŸiim jne) iga aktiivse serveri kohta. Serverid avaldavad ja uuendavad oma objekti ning kliendid saavad kogu loendi ning kuvavad selle mĂ€ngijale. Samuti kuvame loendi avalehel HTMLina, et mĂ€ngijad saaksid lihtsalt serverile klikkida ja otse mĂ€ngu siseneda.
  • Signaalimine on tihedalt seotud meie soklite rakendusega, mida kirjeldatakse jĂ€rgmises jaotises.

Kandideerime mitme kasutajaga mÀngu C++-st veebiversiooni Cheerp, WebRTC ja Firebase abil
Serverite loend mÀngus ja avalehel

Soklite rakendamine

Soovime luua API, mis oleks vÔimalikult lÀhedane Posix UDP sokkidele, et minimeerida vajalike muudatuste arvu.

Samuti soovime rakendada vajalikku miinimumi, mis on vajalik kĂ”ige lihtsama andmevahetuse jaoks ĂŒle vĂ”rgu.

NĂ€iteks me ei vaja tĂ”elist suunamist: kĂ”ik peer’id asuvad ĂŒhes „virtuaalses LANis“, mis on seotud konkreetse Firebase'i andmebaasi eksemplariga.

SeetĂ”ttu ei ole meil unikaalsed IP-aadressid vajalikud: piisab, kui kasutada unikaalseid Firebase vĂ”tme vÀÀrtusi (sarnaselt domeeninimedele), ning iga peer mÀÀrab igale vĂ”tmele kohalikult "vale" IP-aadressi, mida tuleb konverteerida. See vabastab meid globaalsete IP-aadresside mÀÀramisest, mis on mitte triviaalne ĂŒlesanne.

Siin on minimaalne API, mille me 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 Sockets API-le, kuid on mitmeid olulisi erinevusi: tagasikutsumiste registreerimine, kohalike IP-de mÀÀramine ja "laiska" ĂŒhendamine.

Tagasikutsumiste registreerimine

Isegi kui algne programm kasutab mittesummutavat I/O-d, tuleb kood veebibrauseris töötamiseks refaktoorida.

Selle pĂ”hjuseks on see, et brauseris on sĂŒndmuste tsĂŒkkel programmist varjatud (ĂŒkskĂ”ik kas see on JavaScript vĂ”i WebAssembly).

Kohalikus keskkonnas saame koodi kirjutada jÀrgmiselt

while(running) {
  select(...); // oota I/O sĂŒndmusi
  while(true) {
    int r = readfrom(...); // proovi lugeda
    if (r < 0 && errno == EWOULDBLOCK) // enam andmeid ei ole kÀttevÔetav
      break;
    ...
  }
  ...
}

Kui sĂŒndmuste tsĂŒkkel on meie jaoks varjatud, tuleb see muuta millegi sarnaseks:

auto cb = []() { // see 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

Kohalikud IP-aadressid

Meie 'vÔrgus' olevate sÔlmede identifikaatorid ei ole IP-aadressid, vaid Firebase'i vÔtmed (need on stringid, mis nÀevad vÀlja sellised: -LmEC50PYZLCiCP-vqde ).

See on mugav, sest me ei vaja mehhanismi IP-de mÀÀramiseks ja nende unikaalsuse kontrollimiseks (samuti nende utiliseerimiseks pĂ€rast kliendi lahtiĂŒhendamist), kuid tihti on vajalik identifitseerida peer'e numbriliste vÀÀrtuste jĂ€rgi.

Just selle jaoks kasutatakse funktsioone resolve ja reverseResolve: rakendus saab tĂ”lkida vĂ”tme stringi vÀÀrtuse (kasutaja sisendi kaudu vĂ”i master-serveri kaudu) ja vĂ”ib selle sisemiselt IP-aadressiks muuta. API ĂŒlejÀÀnud osa saab samuti selle vÀÀrtuse stringina, et lihtsustada kasutamist.

See on sarnane DNS-otsingule, kuid toimub kliendi kohal.

See tĂ€hendab, et IP-aadressid ei saa olla erinevate klientide jaoks ĂŒhised ja kui on vajalik mingi globaalne identifikaator, tuleb see genereerida muul viisil.

Laisk ĂŒhendus

UDP ei nĂ”ua ĂŒhendust, kuid nagu me oleme nĂ€inud, enne andmete edastamise alustamist kahe peer'i vahel nĂ”uab WebRTC pikka ĂŒhendusprotsessi.

Kui soovime tagada sama abstraktsiooni taseme, (sendto/recvfrom ilma eelneva ĂŒhenduseta juhuslike peer'idega), peame teostama "laiska" (edasilĂŒkatud) ĂŒhendamist API sees.

Nii toimub tavaline andmevahetus "serveri" ja "kliendi" vahel UDP kasutamisel ning mida peaks meie teek tegema:

  • Server kutsub vĂ€lja bind(), et teavitada operatsioonisĂŒsteemi soovist saada pakette mÀÀratud porti.

Selle asemel avaldame Firebase'is avatud portaali serveri vĂ”tme all ja kuulame sĂŒndmusi selle alampuus.

  • Server kutsub vĂ€lja recvfrom(), et vĂ”tta vastu pakette, mis saadetakse sellele portile mistahes hostilt.

Meie puhul tuleb kontrollida sissetulevat pakettide jÀrjekorda, mis on saadetud sellele portile.

Igal pordil on oma jÀrjekord ning me lisame WebRTC datagrammidele algusesse alg- ja lÔpp-pordid, et teada, kuhu suunata uus pakett.

VÀljakutse on mitteblokeeriv, seega, kui pakette pole, tagastame lihtsalt -1 ja mÀÀrame errno=EWOULDBLOCK.

  • Kliendil on vĂ€liste allikate kaudu IP ja serveri port, ning ta kutsub sendto(). Samuti toimub see ajal sisemine kutse bind(), seega jĂ€rgnev recvfrom() saab vastuse ilma selge bind-kutsumise protsessita.

Meie puhul saab klient vÀliselt stringi vÔtme ja kasutab funktsiooni resolve() IP-aadresse saamiseks.

Selles etapis alustame WebRTC "kĂ€tlemist", kui kaks peer-i veel ei ole ĂŒksteisega ĂŒhendatud. Ühendused ĂŒhe peer-i erinevatele portidele kasutavad sama WebRTC DataChannelit.

Me teostame ka kaudset bind(), et server saaks jÀrgmises sendto() kui see mingil pÔhjusel suletakse.

Serverile antakse teada kliendi ĂŒhendusest, kui klient salvestab oma SDP pakkumise serveri porti informatsiooniga Firebase'is, ja server vastab seal oma vastusega.

Alltoodud skeemil on nÀidatud sÔnumite liikumine socket-skeemi ja kliendi jaoks serveri esimese sÔnumi edastamine:

Kandideerime mitme kasutajaga mÀngu C++-st veebiversiooni Cheerp, WebRTC ja Firebase abil
Kliendi ja serveri vahelise ĂŒhenduse etapi tĂ€ielik skeem

KokkuvÔte

Kui olete lÔpuni lugenud, siis on teil ilmselt huvi nÀha teooriat praktikas. MÀngu saab mÀngida aadressil teeworlds.leaningtech.com, proovige!


SÔbralik mÀng kolleegide vahel

VÔrgu raamatukogu kood on vabalt saadaval Github. Liitu meie kanaliga suhtlemiseks Gitter!

Allikas: habr.com

Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster