Çox istifadəçilidir oyununu C++-dan Cheerp, WebRTC və Firebase ilə vebə keçiririk

Giriş

Bizim şirkət Leaning Technologies ənənəvi masaüstü tətbiqlərini vebə keçirmək üçün həllər təqdim edir. Bizim C++ kompilyatorumuz Cheerp WebAssembly və JavaScript birləşməsi yaradır, bu da təmin edir brauzer ilə sadə qarşılıqlı əlaqə, və yüksək performans.

Tətbiqinin bir nümunəsi kimi veb üçün çox istifadəçilidir oyunu keçirmək qərarına gəldik və bunun üçün seçdik Teeworlds. Teeworlds — kiçik, amma aktiv oyunçu icması olan çox istifadəçilidir iki ölçülü retro oyundur (ora mən də daxildir!). Yüklənən resurslar və CPU ilə GPU tələbləri baxımından kiçikdir — ideal namizəd.

Çox istifadəçilidir oyununu C++-dan Cheerp, WebRTC və Firebase ilə vebə keçiririk
Teeworlds brauzerdə işləyir

Bu layihəni istifadə edərək veb üçün şəbəkə kodunun keçidinə dair ümumi həlləri sınaqdan keçirmək qərarına gəldik . Adətən bu aşağıdakı üsullarla həyata keçirilir:XMLHttpRequest/fetch

  • , əgər şəbəkə hissəsi yalnız HTTP sorğularından ibarətdirsə, və yaWebSockets
  • Hər iki həll server komponentini server tərəfində yerləşdirməyi tələb edir və heç biri transport protokolu kimi istifadə etməyə imkan vermir..

Bu, videokonfrans proqramları və oyunlar kimi real vaxt tətbiqləri üçün vacibdir, çünki protokolların çatdırılma və paket sırası zəmanətləri UDPaz gecikmələrə mane ola bilər. TCP Üçüncü yol var — brauzerdən istifadə etmək:

RTCDataChannel WebRTC.

etibarlı və etibarsız ötürülməni dəstəkləyir (sonuncu halda UDP-nin transport protokolu kimi istifadə etməyə çalışır), həm də həm uzaq serverlə, həm də brauzerlər arasında istifadə oluna bilər. Bu, deməkdir ki, biz brauzerdə bütün tətbiqi, o cümlədən server komponentini keçirmək mümkündür! Lakin bununla bağlı əlavə çətinlik var: iki WebRTC piri məlumat mübadiləsi edə bilmək üçün əlaqə üçün bir neçə xarici varlığın (siqnal serveri və bir və ya bir neçə serverin)

STUN TURN/İdeal olaraq, biz WebRTC-dən istifadə edərək şəbəkə API-sini yaratmaq istəyərdik, lakin mümkün qədər UDP Sockets interfeysinə yaxın, bunun üçün əlaqə yaratmaq lazım deyil.).

Bu, WebRTC-nin üstünlüklərindən istifadə etməyə imkan tanıyacaq, tətbiq kodunun mürəkkəb detallarını açıqlamağa ehtiyac olmadan (biz layihəmizdə mümkün qədər az dəyişiklik etmək istəyirdik).

Minimal WebRTC

WebRTC — brauzerlərdə mövcud olan peer-to-peer səs, video və istənilən məlumatların ötürülməsini təmin edən API-lərdən ibarətdir.

Pirlər arasında əlaqə (bir və ya hər iki tərəfdə NAT olmasına baxmayaraq) STUN və ya TURN serverləri vasitəsilə ICE adlanan mexanizm ilə qurulur. Pirlər ICE məlumatları və kanal parametrlərini SDP protokolunun təklif və cavab qrafiki ilə mübadilə edirlər.

Соединение между пирами устанавливается (даже в случае наличия NAT с одной или обеих сторон) при помощи серверов STUN и/или TURN через механизм под названием ICE. Пиры обмениваются информацией ICE и параметрами каналов через offer и answer протокола SDP.

Wow! That's a lot of abbreviations at once. Let's briefly explain what these terms mean:

To establish such a connection, peers need to gather information they received from STUN and TURN servers and exchange it with each other.

The problem is that they currently have no way to exchange data directly, so there must be an out-of-band mechanism: a signaling server.

The signaling server can be very simple, as its only task is to redirect data between peers during the "handshake" phase (as shown in the diagram below).

Çox istifadəçilidir oyununu C++-dan Cheerp, WebRTC və Firebase ilə vebə keçiririk
Simplified WebRTC handshake sequence diagram

Overview of the Teeworlds network model

The network architecture of Teeworlds is very simple:

  • The client and server components are two separate programs.
  • Clients join the game by connecting to one of several servers, each of which hosts only one game at a time.
  • All data transfer in the game occurs through the server.
  • A special master server is used to gather a list of all public servers that are displayed in the game client.

By using WebRTC for data exchange, we can move the server component of the game to the browser where the client is located. This gives us a wonderful opportunity...

To eliminate servers

The absence of server logic has the nice advantage that we can deploy the entire application as static content on Github Pages or on our own infrastructure behind Cloudflare, thus providing fast loading and high uptime for free. Essentially, we can forget about them, and if we're lucky and the game becomes popular, we won't need to upgrade the infrastructure.

Ancaq sistemin işləməsi üçün hələ də xarici arxitekturadan istifadə etməliyik:

  • Bir və ya bir neçə STUN serveri: bir neçə pulsuz variantdan seçmək imkanımız var.
  • Ən azı bir TURN serveri: burada pulsuz variant yoxdur, buna görə ya özümüz qura bilərik, ya da xidmətə pul ödəməliyik. Xoşbəxtlikdən, əksər vaxtlarda bağlantını STUN serverləri vasitəsilə qurmaq mümkün olacaq (və həqiqi p2p təmin edə biləcək), lakin TURN ehtiyat variant kimi lazımdır.
  • Sinyal serveri: digər iki aspektdən fərqli olaraq, sinyalizasiya standartlaşdırılmayıb. Sinyal serverinin həqiqətən nə ilə məşğul olacağı bir növ tətbiqdən asılıdır. Bizim halımızda, əlaqəni qurmazdan əvvəl az miqdarda məlumat mübadiləsi etmək lazımdır.
  • Teeworlds master serveri: digər serverlər tərəfindən öz mövcudluğuna dair xəbərdarlıqlar üçün istifadə olunur və müştərilər tərəfindən ictimai serverləri axtarmaq üçün. O, mütləq deyil (müştərilər bilikdə olduqları serverə əl ilə qoşula bilərlər), lakin oyunçuların təsadüfi insanlarla oyunlara qoşulmalarını asanlaşdırmaq üçün olması yaxşıdır.

Google-un pulsuz STUN serverlərindən istifadə etməyə qərar verdik, TURN serverini isə özümüz quraşdırdıq.

Son iki bənd üçün biz istifadə etdik Firebase:

  • Teeworlds master serveri olduqca sadə şəkildə reallaşdırılıb: hər aktiv serverin məlumatlarını (ad, IP, xəritə, rejim, ...) ehtiva edən obyektlərin siyahısı kimi. Serverlər öz obyektlərini dərc edir və yeniləyir, müştərilər isə bütün siyahını alır və oyunçulara göstərirlər. Həmçinin, biz siyahını ana səhifədə HTML formatında göstəririk ki, oyunçular sadəcə serverə klikləyərək oyuna daxil olsunlar.
  • Sinyalizasiya bizim aşağıda təsvir edilən soket reallaşmamızla sıx bağlıdır.

Çox istifadəçilidir oyununu C++-dan Cheerp, WebRTC və Firebase ilə vebə keçiririk
Oyun içindəki və ana səhifədəki serverlərin siyahısı

Soketlərin reallaşması

Mümkün qədər Posix UDP Sockets-a yaxın bir API yaratmaq istəyirik ki, lazım olan dəyişikliklərin sayını minimuma endirək.

Eyni zamanda, şəbəkə üzrə ən sadə məlumat mübadiləsi üçün lazım olan minimumları reallaşdırmaq istəyirik.

Məsələn, bizə real marşrutlaşdırma lazım deyil: bütün peer-lər müəyyən bir Firebase verilənlər bazası instansiyası ilə bağlı «virtual LAN» içindədir.

Nəticədə, bizə unikal IP ünvanları lazım deyil: peer-ləri unikal şəkildə tanımaq üçün unikal Firebase açar dəyərlərinin istifadəsi kifayətdir (domen adlarına bənzər şəkildə) və hər peer müvafiq açara «saxta» IP ünvanları təyin edir ki, bu da tamamilə qlobal IP ünvanlarının ayrılanmasını aradan qaldırır, bu isə əhəmiyyətli bir məsələdir.

Bizim reallaşdırmalı olduğumuz minimal API budur:

// 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 sadədir və Posix Sockets API-yə bənzəyir, lakin bəzi mühüm fərqliliklərə malikdir: geri çağırma qeydiyyatı, yerli IP-lərin təyin edilməsi və "tənbəl" bağlantı.

Geri çağırma qeydiyyatı

Əgər əsas proqram bloklanmamış giriş-çıxış istifadə edirsə, veb brauzerində işə salmaq üçün kodu yenidən təşkil etmək lazımdır.

Bunun səbəbi budur ki, brauzerdəki hadisə döngüsü proqramdan (JavaScript və ya WebAssembly olsun) gizlidir.

Təbiət mühitində kodu belə yaza bilirik

while(running) {
  select(...); // I/O hadisələrini gözləyin
  while(true) {
    int r = readfrom(...); // oxumağa çalışın
    if (r < 0 && errno == EWOULDBLOCK) // artıq məlumat yoxdur
      break;
    ...
  }
  ...
}

Əgər hadisə döngüsü bizdən gizlidirsə, onu belə bir şeyə çevirməliyik:

auto cb = []() { // yeni məlumat mövcud olduqda çağırılacaq
  while(true) {
    int r = readfrom(...); // oxumağa çalışın
    if (r < 0 && errno == EWOULDBLOCK) // artıq məlumat yoxdur
      break;
    ...
  }
  ...
};
recvCallback(cb); // geri çağırmanı qeydiyyatdan keçirin

Yerli IP-lərin təyini

Bizim "şəbəkəmizdəki" düyünlər IP ünvanları deyil, Firebase açarlarıdır (bunlar belə görünən sətirlərdir: -LmEC50PYZLCiCP-vqde ).

Bu, rahatdır, çünki IP-ləri təyin etmək və onların unikal olduğunu yoxlamaq üçün mexanizmə ehtiyac yoxdur (həmçinin müştəri söndürüldükdən sonra onların utilizasiyası), lakin tez-tez peer-ləri sayısal dəyər əsasında identifikasiya etmək lazımdır.

Bunun üçün funksiyalar istifadə olunur resolvereverseResolve: tətbiq bir şəkildə açarın sətir dəyərini alır (istifadəçi daxil etməsi ilə və ya master server vasitəsilə) və bunu daxili istifadə üçün IP ünvanına çevirə bilir. API-nin qalan hissəsi də sadəlik üçün sətir əvəzinə bu dəyəri alır.

Bu, DNS axtarışına bənzəyir, yalnız müştəri tərəfində yerinə yetirilir.

Yəni, IP ünvanları fərqli müştərilər üçün ortaq ola bilməz, və əgər qlobal bir identifikator lazımdırsa, onu başqa üsulla generasiya etməliyik.

Tənbəl bağlantı

UDP üçün bağlantı tələb olunmur, lakin gördüyümüz kimi, iki peer arasında məlumat mübadiləsinə başlamaq üçün WebRTC uzun bir bağlantı prosesi tələb edir.

Eyni abstraksiya səviyyəsini təmin etmək istəyiriksə, (sendto/recvfrom arbitrary peers ilə əvvəlcədən bağlantı olmadan), API daxilində "tənbəl" (əziləcək) bağlantı həyata keçirməliyik.

İki "server" və "müştəri" arasında adi məlumat mübadiləsi zamanı baş verən budur və kitabxanamızın yerinə yetirməsi lazım olanı:

  • Server çağırır bind(), əməliyyat sisteminə göstərmək üçün ki, müəyyən bir portda paketləri qəbul etmək istəyir.

Bunun əvəzinə biz server açarı altında Firebase-də açıq portu yayımlayacağıq və onun altından hadisələri dinləyəcəyik.

  • Server çağırır recvfrom(), bu porta istənilən ev sahiblərindən gələn paketləri qəbul edərək.

Bizim halda, bu porta göndərilən paketlərin gələn növbəsini yoxlamaq lazımdır.

Hər bir portun öz növbəsi var və biz WebRTC datagramlarına giriş və çıxış portlarını əlavə edirik ki, yeni paket gəldikdə hansı növbəyə yönləndiriləcəyini bilək.

Çağrı bloklanmır, ona görə də paket olmadıqda sadəcə -1 qaytarırıq və sorğu edirik. errno=EWOULDBLOCK.

  • Müştəri xarici vasitələrlə serverin IP və portunu alır və çağırır. sendto(). Həmçinin, burada daxili bir çağırış həyata keçirilir. bind(), buna görə də sonrakı recvfrom() cavab alacaq, açıq şəkildə bind yerinə yetirilmədən.

Bizim vəziyyətdə müştəri xarici şəkildə açar simvolunu alır və funksiyası ilə istifadə edir. resolve() IP ünvanını əldə etmək üçün.

Bu mərhələdə, əgər iki pir hələ bir-birinə qoşulmayıbsa, WebRTC "əllə sıxma" prosesi başlayır. Eyni pir üçün müxtəlif portlara qoşulmalar eyni WebRTC DataChannel istifadə edir.

Həmçinin biz dolayı bir bind()yerinə yetiririk ki, server gələcəkdə ola biləcək bağlanma səbəbindən qoşulmanı bərpa etsin. sendto() Server müştərinin qoşulduğunu bildirir, müştəri öz SDP təklifini serverin portu məlumatları ilə Firebase-də yazdığı zaman, server də orada öz cavabını verir.

Aşağıdakı cədvəldə müştəri-server arasında mesajların hərəkətinin bir nümunəsi göstərilir.

Müştəri və server arasında qoşulma mərhələsinin tam sxemi.

Çox istifadəçilidir oyununu C++-dan Cheerp, WebRTC və Firebase ilə vebə keçiririk
Əgər sona qədər oxudunuzsa, yəqin ki, nəzəriyyəni iş başında görmək istəyirsiniz. Oyun oynamaq olar.

Nəticə

teeworlds.leaningtech.com , cəhd edin!Kolleqalar arasında dostluq matçı.


Şəbəkə kitabxanasının kodu pulsuz olaraq mövcuddur.

Bizim kanala qoşulun. GithubGitter Kubernetes Web View-in veb interfeysi (və Kubernetes üçün digər web UI-nin qısa icmalı) haqqında elan.!

Mənbə: habr.com

DDoS qoruması olan saytlara etibarlı hosting satın alın, VPS VDS serverlər 🔥 DDoS qoruması olan saytlara etibarlı hosting satın alın, VPS VDS serverlər | ProHoster