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
