Sissejuhatus
Meie ettevõte pakub lahendusi traditsiooniliste lauaarvuti rakenduste üleviimiseks webi. Meie C++ kompilaator genereerib WebAssembly ja JavaScript'i kombinatsiooni, mis tagab nii , kui ka kõrge jõudluse.
Kuna näide selle rakendamisest otsustasime portida veebis mitme mängijaga mängu ja valisime selleks . 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.

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 kui transport protokolli. See on oluline reaalajas rakenduste, näiteks videokonverentside ja mängude jaoks, kuna kohaletoimetamise ja paketide järjekorra tagamise protokoll võivad häirida madalaid latentsuseid.
On ka kolmas tee - kasutada võrku brauserist: .
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 /).
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:
- — 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.
- 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.
- 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.
- 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).

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

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

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 , proovige kindlasti!
Sõbralik mäng kolleegide vahel
Tulemüügiteegi koodi on vabalt saadaval . Liituge meie kanaliga !
Allikas: habr.com
