Vairāku spēlētāju spēles pārneÅ”ana no C++ uz tÄ«mekli, izmantojot Cheerp, WebRTC un Firebase

Ievads

mÅ«su kompānija Leaning tehnoloÄ£ijas nodroÅ”ina risinājumus tradicionālo darbvirsmas lietojumprogrammu pārneÅ”anai uz tÄ«mekli. MÅ«su C++ kompilators uzmundrināt Ä£enerē WebAssembly un JavaScript kombināciju, kas nodroÅ”ina abus vienkārÅ”a pārlÅ«kprogrammas mijiedarbÄ«ba, un augstu veiktspēju.

Kā tās pielietojuma piemēru mēs nolēmām vairāku spēlētāju spēli portēt tÄ«meklÄ« un izvēlējāmies Teeworlds. Teeworlds ir vairāku spēlētāju XNUMXD retro spēle ar nelielu, bet aktÄ«vu spēlētāju kopienu (tostarp es!). Tas ir mazs gan lejupielādēto resursu, gan CPU un GPU prasÄ«bu ziņā ā€“ ideāls kandidāts.

Vairāku spēlētāju spēles pārneÅ”ana no C++ uz tÄ«mekli, izmantojot Cheerp, WebRTC un Firebase
Darbojas pārlūkprogrammā Teeworlds

Mēs nolēmām izmantot Å”o projektu, lai eksperimentētu vispārÄ«gi risinājumi tÄ«kla koda pārneÅ”anai uz tÄ«mekli. To parasti veic Ŕādos veidos:

  • XMLHttpRequest/fetch, ja tÄ«kla daļa sastāv tikai no HTTP pieprasÄ«jumiem, vai
  • WebSockets.

Abiem risinājumiem ir nepiecieÅ”ama servera komponenta mitināŔana servera pusē, un neviens no tiem neļauj izmantot kā transporta protokolu UDP. Tas ir svarÄ«gi reāllaika lietojumprogrammām, piemēram, videokonferenču programmatÅ«rai un spēlēm, jo ā€‹ā€‹tas garantē protokolu pakeÅ”u piegādi un secÄ«bu. TCP var kļūt par Ŕķērsli zemam latentumam.

Ir treŔais veids - izmantojiet tīklu no pārlūkprogrammas: WebRTC.

RTCDataChannel Tā atbalsta gan uzticamu, gan neuzticamu pārraidi (pēdējā gadījumā tā mēģina izmantot UDP kā transporta protokolu, kad vien iespējams), un to var izmantot gan ar attālo serveri, gan starp pārlūkprogrammām. Tas nozīmē, ka mēs varam pārnest visu lietojumprogrammu uz pārlūkprogrammu, ieskaitot servera komponentu!

Tomēr tas rada papildu grÅ«tÄ«bas: pirms divi WebRTC lÄ«dzinieki var sazināties, tiem ir jāveic salÄ«dzinoÅ”i sarežģīts rokasspiediens, lai izveidotu savienojumu, kam nepiecieÅ”amas vairākas treŔās puses entÄ«tijas (signalizācijas serveris un viens vai vairāki serveri Apdullināt/Pagrieziens).

Ideālā gadÄ«jumā mēs vēlētos izveidot tÄ«kla API, kas iekŔēji izmanto WebRTC, bet ir pēc iespējas tuvāk UDP Sockets saskarnei, kurai nav jāizveido savienojums.

Tas ļaus mums izmantot WebRTC priekÅ”rocÄ«bas, nepakļaujot sarežģītas detaļas lietojumprogrammas kodam (ko mēs savā projektā vēlējāmies mainÄ«t pēc iespējas mazāk).

Minimālais WebRTC

WebRTC ir pārlūkprogrammās pieejama API kopa, kas nodroŔina vienādranga audio, video un patvaļīgu datu pārraidi.

Savienojums starp vienaudžiem tiek izveidots (pat ja vienā vai abās pusēs ir NAT), izmantojot STUN un/vai TURN serverus, izmantojot mehānismu, ko sauc par ICE. Vienādrangi apmainās ar ICE informāciju un kanālu parametriem, izmantojot SDP protokola piedāvājumu un atbildi.

Oho! Cik saÄ«sinājumu vienlaikus? ÄŖsi paskaidrosim, ko nozÄ«mē Å”ie termini:

  • Sesijas apceļoÅ”anas utilÄ«tas NAT (Apdullināt) ā€” protokols NAT apieÅ”anai un pāra (IP, porta) iegÅ«Å”anai datu apmaiņai tieÅ”i ar resursdatoru. Ja viņam izdodas paveikt savu uzdevumu, vienaudži var patstāvÄ«gi apmainÄ«ties ar datiem savā starpā.
  • Pāreja, izmantojot relejus ap NAT (Pagrieziens) tiek izmantots arÄ« NAT ŔķērsoÅ”anai, taču tas tiek Ä«stenots, pārsÅ«tot datus caur starpniekserveri, kas ir redzams abiem vienaudžiem. Tas palielina latentumu, un to ievieÅ”ana ir dārgāka nekā STUN (jo tas tiek lietots visas saziņas sesijas laikā), taču dažreiz tā ir vienÄ«gā iespēja.
  • InteraktÄ«vas savienojamÄ«bas izveide (ICE) izmanto, lai izvēlētos labāko iespējamo metodi divu vienaudžu savienoÅ”anai, pamatojoties uz informāciju, kas iegÅ«ta no vienaudžiem tieÅ”i, kā arÄ« informāciju, ko saņem jebkurÅ” STUN un TURN serveru skaits.
  • Sesijas apraksta protokols (SDP) ir formāts savienojuma kanāla parametru aprakstÄ«Å”anai, piemēram, ICE kandidāti, multivides kodeki (audio/video kanāla gadÄ«jumā) utt... Viens no vienaudžiem nosÅ«ta SDP piedāvājumu, bet otrs atbild ar SDP Answer .. Pēc tam tiek izveidots kanāls.

Lai izveidotu Ŕādu savienojumu, vienaudžiem ir jāapkopo informācija, ko viņi saņem no STUN un TURN serveriem, un jāapmainās ar to savā starpā.

Problēma ir tā, ka viņiem vēl nav iespējas tieÅ”i sazināties, tāpēc Å”o datu apmaiņai ir jābÅ«t ārpusjoslas mehānismam: signalizācijas serverim.

Signalizācijas serveris var bÅ«t ļoti vienkārÅ”s, jo tā vienÄ«gais uzdevums ir pārsÅ«tÄ«t datus starp vienaudžiem ā€œrokasspiedienaā€ fāzes laikā (kā parādÄ«ts tālāk esoÅ”ajā diagrammā).

Vairāku spēlētāju spēles pārneÅ”ana no C++ uz tÄ«mekli, izmantojot Cheerp, WebRTC un Firebase
VienkārŔota WebRTC rokasspiediena secības diagramma

Teeworlds tīkla modeļa pārskats

Teeworlds tīkla arhitektūra ir ļoti vienkārŔa:

  • Klienta un servera komponenti ir divas dažādas programmas.
  • Klienti spēlē ievadiet savienojumu ar vienu no vairākiem serveriem, no kuriem katrs vienlaikus mitina tikai vienu spēli.
  • Visa spēles datu pārsÅ«tÄ«Å”ana tiek veikta caur serveri.
  • ÄŖpaÅ”s galvenais serveris tiek izmantots, lai apkopotu visu publisko serveru sarakstu, kas tiek parādÄ«ti spēles klientā.

Pateicoties WebRTC izmantoÅ”anai datu apmaiņai, mēs varam pārsÅ«tÄ«t spēles servera komponentu uz pārlÅ«kprogrammu, kurā atrodas klients. Tas mums sniedz lielisku iespēju...

Atbrīvojieties no serveriem

Servera loÄ£ikas trÅ«kumam ir jauka priekÅ”rocÄ«ba: mēs varam izvietot visu lietojumprogrammu kā statisku saturu Github Pages vai mÅ«su paÅ”u aparatÅ«rā aiz Cloudflare, tādējādi nodroÅ”inot ātru lejupielādi un ilgu darbÄ«bas laiku bez maksas. PatiesÄ«bā par tiem varam aizmirst, un, ja paveiksies un spēle kļūs populāra, tad infrastruktÅ«ra nebÅ«s jāmodernizē.

Tomēr, lai sistēma darbotos, mums joprojām ir jāizmanto ārēja arhitektūra:

  • Viens vai vairāki STUN serveri: mums ir vairākas bezmaksas iespējas, no kurām izvēlēties.
  • Vismaz viens TURN serveris: Å”eit nav bezmaksas iespēju, tāpēc mēs varam iestatÄ«t paÅ”i vai samaksāt par pakalpojumu. Par laimi, lielāko daļu laika savienojumu var izveidot, izmantojot STUN serverus (un nodroÅ”ināt patiesu p2p), taču TURN ir nepiecieÅ”ama kā rezerves opcija.
  • Signalizācijas serveris: atŔķirÄ«bā no pārējiem diviem aspektiem signalizācija nav standartizēta. Tas, par ko patiesÄ«bā bÅ«s atbildÄ«gs signalizācijas serveris, ir nedaudz atkarÄ«gs no lietojumprogrammas. MÅ«su gadÄ«jumā pirms savienojuma izveides ir nepiecieÅ”ams apmainÄ«ties ar nelielu datu apjomu.
  • Teeworlds Master Server: to izmanto citi serveri, lai reklamētu savu esamÄ«bu, un klienti, lai atrastu publiskos serverus. Lai gan tas nav nepiecieÅ”ams (klienti vienmēr var izveidot savienojumu ar serveri, par kuru viņi zina manuāli), bÅ«tu jauki, ja spēlētāji varētu piedalÄ«ties spēlēs ar nejauÅ”iem cilvēkiem.

Mēs nolēmām izmantot Google bezmaksas STUN serverus un paÅ”i izvietojām vienu TURN serveri.

Par pēdējiem diviem punktiem mēs izmantojām Firebase:

  • Teeworlds galvenais serveris ir ieviests ļoti vienkārÅ”i: kā objektu saraksts, kas satur informāciju (nosaukums, IP, karte, režīms, ...) par katru aktÄ«vā servera. Serveri publicē un atjaunina savu objektu, un klienti ņem visu sarakstu un parāda to atskaņotājam. Mēs arÄ« parādām sarakstu sākumlapā HTML formātā, lai spēlētāji varētu vienkārÅ”i noklikŔķināt uz servera un tikt novirzÄ«ti tieÅ”i uz spēli.
  • Signalizācija ir cieÅ”i saistÄ«ta ar mÅ«su kontaktligzdu ievieÅ”anu, kas aprakstÄ«ta nākamajā sadaļā.

Vairāku spēlētāju spēles pārneÅ”ana no C++ uz tÄ«mekli, izmantojot Cheerp, WebRTC un Firebase
Serveru saraksts spēles iekÅ”ienē un mājaslapā

Kontaktligzdu ievieŔana

Mēs vēlamies izveidot API, kas ir pēc iespējas tuvāk Posix UDP Sockets, lai samazinātu nepiecieÅ”amo izmaiņu skaitu.

Tāpat vēlamies ieviest nepiecieÅ”amo minimumu vienkārŔākai datu apmaiņai tÄ«klā.

Piemēram, mums nav nepiecieÅ”ama reāla marÅ”rutÄ“Å”ana: visi lÄ«dzÄ«gie ir tajā paŔā "virtuālajā LAN", kas saistÄ«ts ar konkrētu Firebase datu bāzes gadÄ«jumu.

Tāpēc mums nav vajadzÄ«gas unikālas IP adreses: unikālas Firebase atslēgas vērtÄ«bas (lÄ«dzÄ«gas domēna nosaukumiem) ir pietiekamas, lai unikāli identificētu lÄ«dziniekus, un katrs partneris lokāli pieŔķir ā€œviltusā€ IP adreses katrai atslēgai, kas jātulko. Tas pilnÄ«bā novērÅ” globālās IP adreses pieŔķirÅ”anas nepiecieÅ”amÄ«bu, kas ir nenozÄ«mÄ«gs uzdevums.

Tālāk ir norādīts minimālais API, kas mums jāievieŔ:

// 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 ir vienkārÅ”a un lÄ«dzÄ«ga Posix Sockets API, taču tai ir dažas svarÄ«gas atŔķirÄ«bas: Atzvanu reÄ£istrÄ“Å”ana, vietējo IP pieŔķirÅ”ana un slinki savienojumi.

Atzvanu reģistrēŔana

Pat ja sākotnējā programma izmanto nebloķējoÅ”u I/O, kods ir jāpārveido, lai tas darbotos tÄ«mekļa pārlÅ«kprogrammā.

Iemesls tam ir tas, ka notikumu cilpa pārlūkprogrammā ir paslēpta no programmas (vai tā būtu JavaScript vai WebAssembly).

Vietējā vidē mēs varam rakstÄ«t Ŕādu kodu

while(running) {
  select(...); // wait for I/O events
  while(true) {
    int r = readfrom(...); // try to read
    if (r < 0 && errno == EWOULDBLOCK) // no more data available
      break;
    ...
  }
  ...
}

Ja notikuma cikls mums ir paslēpts, mums tas jāpārvērÅ” par kaut ko lÄ«dzÄ«gu:

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

Vietējā IP pieŔķirÅ”ana

MÅ«su "tÄ«kla" mezglu ID nav IP adreses, bet gan Firebase atslēgas (tās ir virknes, kas izskatās Ŕādi: -LmEC50PYZLCiCP-vqde ).

Tas ir ērti, jo mums nav nepiecieÅ”ams mehānisms IP pieŔķirÅ”anai un to unikalitātes pārbaudei (kā arÄ« atbrÄ«voÅ”anai no tiem pēc klienta atvienoÅ”anas), bet bieži vien ir nepiecieÅ”ams identificēt lÄ«dziniekus pēc skaitliskās vērtÄ«bas.

TieÅ”i Å”im nolÅ«kam Ŕīs funkcijas tiek izmantotas. resolve Šø reverseResolve: Lietojumprogramma kaut kādā veidā saņem atslēgas virknes vērtÄ«bu (izmantojot lietotāja ievadi vai galveno serveri) un var pārvērst to par IP adresi iekŔējai lietoÅ”anai. ArÄ« pārējā API daļa vienkārŔības labad saņem Å”o vērtÄ«bu, nevis virkni.

Tas ir lÄ«dzÄ«gs DNS uzmeklÄ“Å”anai, bet tiek veikts lokāli klientam.

Tas ir, IP adreses nevar koplietot starp dažādiem klientiem, un, ja ir nepiecieÅ”ams kaut kāds globālais identifikators, tas bÅ«s jāģenerē citā veidā.

Slinks savienojums

UDP nav nepiecieÅ”ams savienojums, taču, kā mēs redzējām, WebRTC ir nepiecieÅ”ams ilgstoÅ”s savienojuma process, pirms tas var sākt datu pārsÅ«tÄ«Å”anu starp diviem vienaudžiem.

Ja mēs vēlamies nodroÅ”ināt tādu paÅ”u abstrakcijas lÄ«meni, (sendto/recvfrom ar patvaļīgiem vienaudžiem bez iepriekŔēja savienojuma), tad tiem API iekÅ”ienē ir jāveic ā€œslinksā€ (aizkavēts) savienojums.

LÅ«k, kas notiek parastas saziņas laikā starp ā€œserveriā€ un ā€œklientuā€, izmantojot UDP, un tas, kas jādara mÅ«su bibliotēkai:

  • Servera zvani bind()lai paziņotu operētājsistēmai, ka tā vēlas saņemt paketes norādÄ«tajā portā.

Tā vietā mēs publicēsim atvērtu portu uz Firebase zem servera atslēgas un klausÄ«simies notikumus tā apakÅ”kokā.

  • Servera zvani recvfrom(), pieņemot paketes, kas nāk no jebkura saimniekdatora Å”ajā portā.

Mūsu gadījumā mums ir jāpārbauda uz Ŕo portu nosūtīto pakeŔu ienākoŔā rinda.

Katram portam ir sava rinda, un mēs pievienojam avota un mērķa portus WebRTC datagrammu sākumam, lai mēs zinātu, uz kuru rindu pārsūtīt, kad pienāk jauna pakete.

Zvans ir nebloķējoÅ”s, tāpēc, ja nav pakeÅ”u, mēs vienkārÅ”i atgriežam -1 un iestatām errno=EWOULDBLOCK.

  • Klients saņem servera IP un portu ar ārējiem lÄ«dzekļiem un zvana sendto(). Tas arÄ« veic iekŔēju zvanu. bind(), tāpēc turpmāk recvfrom() saņems atbildi, nepārprotami neizpildot saistÄ«Å”anu.

Mūsu gadījumā klients ārēji saņem virknes atslēgu un izmanto funkciju resolve() lai iegūtu IP adresi.

Å ajā brÄ«dÄ« mēs uzsākam WebRTC rokasspiedienu, ja abi partneri vēl nav savienoti viens ar otru. Savienojumi ar dažādiem viena un tā paÅ”a partnera portiem izmanto vienu un to paÅ”u WebRTC DataChannel.

Mēs veicam arÄ« netieÅ”u bind()lai serveris varētu atkārtoti izveidot savienojumu nākamajā sendto() ja kāda iemesla dēļ tas slēgts.

Serveris tiek informēts par klienta savienojumu, kad klients Firebase zem servera porta informācijas ieraksta savu SDP piedāvājumu, un serveris atbild ar savu atbildi tur.

Tālāk redzamajā diagrammā ir parādÄ«ts ligzdas shēmas ziņojumu plÅ«smas piemērs un pirmā ziņojuma pārsÅ«tÄ«Å”ana no klienta uz serveri:

Vairāku spēlētāju spēles pārneÅ”ana no C++ uz tÄ«mekli, izmantojot Cheerp, WebRTC un Firebase
Pilnīga diagramma savienojuma fāzei starp klientu un serveri

Secinājums

Ja esat izlasījis tik tālu, jūs, iespējams, interesē teorija darbībā. Spēli var spēlēt tālāk teeworlds.leaningtech.com, Pamēģini!


Draudzības spēle starp kolēģiem

Tīkla bibliotēkas kods ir brīvi pieejams vietnē GitHub. Pievienojieties sarunai mūsu kanālā plkst skala!

Avots: www.habr.com

Pievieno komentāru