Úvod
Naša spoločnosť poskytuje riešenia na portovanie tradičných desktopových aplikácií na web. Náš kompilátor C++ generuje kombináciu WebAssembly a JavaScript, čo zabezpečuje , a vysoký výkon.
Ako príklad jeho použitia sme sa rozhodli portovať na web multiplayerovú hru a vybrali sme pre tento projekt . Teeworlds je multiplayerová 2D retro hra s malou, ale aktívnou komunitou hráčov (medzi nimi aj ja!). Je malá ako z hľadiska sťahovaných zdrojov, tak aj požiadaviek na CPU a GPU — ideálny kandidát.

Fungujúca v prehliadači Teeworlds
Sme sa rozhodli využiť tento projekt na experimentovanie s generickými riešeniami na portovanie sieťového kódu na web. Zvyčajne sa to vykonáva nasledujúcimi spôsobmi:
- XMLHttpRequest/fetch, ak sieťová časť pozostáva len z HTTP požiadaviek, alebo
- WebSockets.
Obe riešenia vyžadujú hosťovanie serverového komponentu na strane servera a žiadne z nich neumožňuje používať ako prenosový protokol . To je dôležité pre aplikácie v reálnom čase, ako je softvér na videokonferencie a hry, pretože záruky dodania a poradia paketov protokolu môžu byť prekážkou pre nízke latencie.
Existuje aj tretia cesta — používanie siete z prehliadača: .
podporuje ako spoľahlivý, tak aj nespoľahlivý prenos (v poslednom prípade sa snaží použiť ako prenosový protokol UDP, ak je to možné), a môže byť použitý aj s vzdialeným serverom, aj medzi prehliadačmi. To znamená, že môžeme portovať do prehliadača celú aplikáciu, vrátane serverového komponentu!
Avšak s tým sa spája dodatočná ťažkost: predtým, ako si dva WebRTC peer môžu vymieňať dáta, musia prejsť relatívne zložitým procesom „podania rúk“ (handshake) na pripojenie, na čo sú potrebné niektoré externé entity (signálny server a jeden alebo viac serverov /).
Ideálne by sme chceli vytvoriť sieťové API, vo vnútri používajúce WebRTC, ale čo najviac podobné rozhraniu UDP Sockets, ktoré nepotrebuje vytvárať pripojenie.
To nám umožní využiť výhody WebRTC bez potreby zverejňovať zložité detaily kódu aplikácie (ktorý sme v našom projekte chceli zmeniť čo najmenej).
Minimálne WebRTC
WebRTC je sada API dostupná v prehliadačoch, ktorá umožňuje prenos peer-to-peer zvuku, videa a rôznych dát.
Spojenie medzi peer-ami sa vytvára (aj v prípade, že sú prítomné NAT z jednej alebo oboch strán) pomocou serverov STUN a/alebo TURN prostredníctvom mechanizmu nazývaného ICE. Peer-i si vymieňajú informácie ICE a parametre kanálov prostredníctvom ponúk a odpovedí protokolu SDP.
Wow! Koľko skratiek naraz. Poďme krátko vysvetliť, čo tieto pojmy znamenajú:
- — protokol na obídenie NAT a získanie páru (IP, port) na výmenu dát priamo s hostiteľom. Ak sa mu podarí splniť svoju úlohu, peer-i si môžu samostatne vymieňať dáta.
- tiež sa používa na obídenie NAT, ale realizuje to presmerovaním dát cez proxy, ktorá je viditeľná obom peer-om. Pridáva oneskorenie a je nákladnejší na vykonanie ako STUN (pretože sa používa počas celej relácie), ale niekedy je to jediná možná možnosť.
- sa používa na výber najlepšieho možného spôsobu spojenia dvoch peer-ov na základe informácií získaných pri priamom spojení peer-ov, ako aj informácií získaných z akéhokoľvek množstva serverov STUN a TURN.
- — je to formát na opis parametrov pripojovacieho kanála, ako sú ICE kandidáti, multimediálne kodeky (v prípade audio/video kanála), atď. Jeden z peer-ov posiela SDP ponuku ("ponuku"), a druhý odpovedá SDP odpoveďou ("odpoveďou"). Potom sa vytvorí kanál.
Aby sa takéto spojenie vytvorilo, peer-om je potrebné zhromaždiť informácie, ktoré získali od serverov STUN a TURN, a vymeniť si ich navzájom.
Problém je v tom, že zatiaľ nemajú možnosť vymieňať si dáta priamo, a preto musí existovať offline mechanizmus: signálny server.
Signálny server môže byť veľmi jednoduchý, pretože jeho jedinou úlohou je presmerovanie dát medzi peer-ami v etape "podania ruky" (ako je znázornené na schéme nižšie).

Zjednodušená schéma postupnosti "podania ruky" WebRTC
Prehľad sieťového modelu Teeworlds
Sieťová architektúra Teeworlds je veľmi jednoduchá:
- Komponenty klienta a servera sú dve rôzne programy.
- Klienti sa zapájajú do hry pripojením k jednému z viacerých serverov, z ktorých každý hostí len jednu hru naraz.
- Celá výmena dát v hre prebieha prostredníctvom servera.
- Špeciálny master-server slúži na zhromažďovanie zoznamu všetkých verejných serverov, ktoré sa zobrazujú v hernom klienskom programe.
Vďaka využitiu WebRTC na výmenu dát môžeme presunúť serverovú komponenent do prehliadača, kde sa nachádza klient. To nám dáva skvelú príležitosť...
Zbaviť sa serverov
Absencia serverovej logiky má príjemnú výhodu: môžeme nasadiť celú aplikáciu ako statický obsah na Github Pages alebo na vlastnom hardvéri za Cloudflare, čím si zabezpečíme rýchle načítanie a vysokú dostupnosť bez nákladov. V podstate na ne môžeme zabudnúť a ak budeme mať šťastie, že hra sa stane populárnou, nebude potrebné modernizovať infraštruktúru.
Avšak, aby systém fungoval, budeme aj tak musieť použiť externú architektúru:
- Jeden alebo viac STUN serverov: máme na výber z niekoľkých bezplatných možností.
- Aspoň jeden TURN server: tu nie sú k dispozícii bezplatné možnosti, takže si môžeme buď nastaviť svoj, alebo platiť za službu. Našťastie väčšinu času sa pripojenie dá nadviazať cez STUN servery (a zabezpečiť pravé p2p), ale TURN je potrebný ako záložná alternatíva.
- Signálny server: na rozdiel od dvoch ďalších aspektov nie je signalizovanie štandardizované. To, za čo vlastne bude zodpovedný signálny server, do určitej miery závisí od aplikácie. V našom prípade je potrebné vymeniť si malé množstvo dát pred nadviazaním spojenia.
- Master-server Teeworlds: používajú ho iné servery na upozornenie na svoju existenciu a klienti na vyhľadávanie verejných serverov. Hoci nie je povinný (klienti sa vždy môžu manuálne pripojiť k známemu serveru), bolo by dobré ho mať, aby hráči mohli súťažiť v hrách s náhodnými ľuďmi.
Rozhodli sme sa použiť bezplatné STUN servery od spoločnosti Google, zatiaľ čo jeden TURN server sme si nasadili sami.
Na dva posledné body sme použili :
- Master server Teeworlds je implementovaný veľmi jednoducho: ako zoznam objektov, ktorý obsahuje informácie (meno, IP, mapa, režim, …) každého aktívneho servera. Servery publikujú a aktualizujú svoj vlastný objekt, zatiaľ čo klienti berú celý zoznam a zobrazujú ho hráčovi. Zoznam tiež zobrazuje na domovskej stránke ako HTML, aby hráči mohli jednoducho kliknúť na server a priamo sa dostať do hry.
- Signalizácia je úzko spojená s našou implementáciou socketov, opísanou v nasledujúcej časti.

Zoznam serverov v hre a na domovskej stránke
Implementácia socketov
Chceme vytvoriť API, čo najbližšie k Posix UDP Sockets, aby sme minimalizovali počet potrebných zmien.
Takisto chceme implementovať nevyhnutné minimum, potrebné na najjednoduchšiu výmenu údajov v sieti.
Napríklad, nepotrebujeme skutočné smerovanie: všetky peerové zariadenia sú v jednej „virtuálnej LAN“, spojenej s konkrétnym inštančným databázovým systémom Firebase.
Preto nepotrebujeme jedinečné IP adresy: na jedinečnú identifikáciu peerov stačí použiť jedinečné hodnoty kľúčov Firebase (podobne ako doménové mená), a každý peer lokálne priraďuje „falošné“ IP adresy každému kľúču, ktorý je potrebné previesť. To nás úplne zbavuje potreby globálneho priraďovania IP adries, čo je netriviálna úloha.
Tu je minimálne API, ktoré musíme implementovať:
// 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 je jednoduché a podobné API Posix Sockets, ale má niekoľko dôležitých rozdielov: registrácia spätných volaní, priradenie lokálnych IP a „lenivá“ konektivita.
Registrácia spätných volaní
Aj keď pôvodný program používa neblokujúci vstup/výstup, pre spustenie v webovom prehliadači je potrebné kód refaktorovať.
Dôvodom je to, že slučka udalostí v prehliadači je skrytá pred programom (či už je to JavaScript alebo WebAssembly).
V natívnom prostredí môžeme písať kód takto
while(running) {
select(...); // čakajte na I/O udalosti
while(true) {
int r = readfrom(...); // pokus o čítanie
if (r < 0 && errno == EWOULDBLOCK) // nie sú k dispozícii ďalšie údaje
break;
...
}
...
}Ak je slučka udalostí pre nás skrytá, musíme ju premeniť na niečo podobné:
auto cb = []() { // toto sa zavolá, keď sú nové údaje k dispozícii
while(true) {
int r = readfrom(...); // pokus o čítanie
if (r < 0 && errno == EWOULDBLOCK) // nie sú k dispozícii ďalšie údaje
break;
...
}
...
};
recvCallback(cb); // registrácia spätného volaniaPriradenie lokálnych IP
Identifikátory uzlov v našej ‘sieti’ nie sú IP adresy, ale Firebase kľúče (to sú reťazce, ktoré vyzerajú takto: -LmEC50PYZLCiCP-vqde ).
Je to pohodlné, pretože nepotrebujeme mechanizmus na priradenie IP a kontrolu ich jedinečnosti (ako aj ich likvidáciu po odpojení klienta), ale veľmi často je potrebné identifikovať peer podľa číselnej hodnoty.
Na tento účel sa používajú funkcie resolve a reverseResolve: aplikácia získa reťazcovú hodnotu kľúča (cez vstup používateľa alebo cez majstrovský server) a môže ju previesť na IP adresu na vnútorné použitie. Zvyšok API pre jednoduchosť tiež získa túto hodnotu namiesto reťazca.
Je to podobné ako DNS vyhľadávanie, ale vykonáva sa lokálne u klienta.
To znamená, že IP adresy nemôžu byť zdieľané medzi rôznymi klientmi, a ak potrebujeme nejaký globálny identifikátor, bude ho potrebné generovať iným spôsobom.
Lenivé pripojenie
UDP nevyžaduje pripojenie, ale, ako sme videli, skôr ako začneme prenášať údaje medzi dvoma peerami, WebRTC vyžaduje dlhý proces pripojenia.
Ak chceme zabezpečiť rovnakú úroveň abstrakcie, (sendto/recvfrom s ľubovoľnými peerami bez predchádzajúceho pripojenia), musíme vykonať „lenivé“ (oneskorené) pripojenie v rámci API.
Toto je to, čo sa deje pri bežnej výmene údajov medzi „serverom“ a „klientom“ v prípade použitia UDP, a čo by mala naša knižnica vykonávať:
- Server volá
bind(), aby informoval operačný systém, že chce prijímať pakety na zadaný port.
Namiesto toho zverejníme otvorený port vo Firebase pod kľúčom servera a budeme počúvať udalosti v jeho podstromu.
- Server volá
recvfrom(), prijímajúci na tento port pakety pochádzajúce od akéhokoľvek hostiteľa.
V našom prípade je potrebné kontrolovať prichádzajúcu frontu paketov, ktoré boli odoslané na tento port.
Každý port má svoju vlastnú frontu, a pridávame na začiatok dátagramov WebRTC pôvodné a cieľové porty, aby sme vedeli, do ktorej fronty presmerovať nový paket pri jeho príchode.
Volanie je nebločka, takže ak nie sú pakety, jednoducho vrátime -1 a nastavíme errno=EWOULDBLOCK.
- Klient získa externými prostriedkami IP a port servera a volá
sendto(). Tiež pritom prebieha vnútorné volaniebind(), takže nasledujúcirecvfrom()získa odpoveď bez explicitného vykonania bind.
V našom prípade klient externým spôsobom získava reťazový kľúč a používa funkciu resolve() na získanie IP adresy.
V tejto fáze začíname „podanie ruky“ WebRTC, ak sa dva peer ešte navzájom nespojili. Pripojenia na rôzne porty jedného peer používajú rovnaký DataChannel WebRTC.
Taktiež vykonávame nepriamy bind(), aby server mohol obnoviť pripojenie v nasledujúcom sendto() v prípade, že sa z nejakého dôvodu uzavrie.
Server je informovaný o pripojení klienta, keď klient zapisuje svoj SDP offer pod informáciou o porte servera vo Firebase, a server tam odpovedá svojím odpovedom.
Na nižšie uvedenom schéme je ukážka prenosu správ pre schému soketov a prenos prvého správy od klienta k serveru:

Úplná schéma fázy pripojenia medzi klientom a serverom
Záver
Ak ste dočítali až do konca, pravdepodobne vás zaujíma pozrieť sa na teóriu v praxi. Hru si môžete zahrať na , vyskúšajte!
Priateľský zápas medzi kolegami
Kód sieťovej knižnice je voľne dostupný na . Pridajte sa k diskusii na našom kanáli v !
Zdroj: habr.com
