Sissejuhatus
Meie ettevÔte pakub lahendusi traditsiooniliste töölauarakenduste veebiversioonide jaoks. Meie C++ kompass genereerib WebAssembly ja JavaScripti kombinatsiooni, mis tagab nii , kui ka kÔrge jÔudluse.
Kuna nĂ€ide selle kasutamisest otsustasime portida veebile jaoks mitmikmĂ€ngu ja valisime selle jaoks . 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.

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 . See on oluline reaalajas rakenduste, nagu videokonverentside tarkvara ja mÀngude jaoks, sest protokolli pakkumise ja jÀrjekorra tagamise garantiid vÔivad segada madala latentsusega.
On olemas ka kolmas tee â kasutada brauserivĂ”rku: .
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 /).
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:
- â 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.
- 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.
- 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.
- 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).

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 :
- 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.

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 callbackKohalikud 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 kutsebind(), seega jÀrgnevrecvfrom()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:

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 , proovige!
SÔbralik mÀng kolleegide vahel
VÔrgu raamatukogu kood on vabalt saadaval . Liitu meie kanaliga suhtlemiseks !
Allikas: habr.com
