Cheerp, WebRTC və Firebase ilə çox oyunçu oyunu C++-dan internetə köçürmək

Giriş

Şirkətimiz Etibarlı Texnologiyalar ənənəvi masa üstü proqramlarını internetə köçürmək üçün həllər təqdim edir. Bizim C++ kompilyatorumuz şadlanmaq hər ikisini təmin edən WebAssembly və JavaScript birləşməsini yaradır sadə brauzer qarşılıqlı əlaqəsi, və yüksək performans.

Tətbiqinə bir nümunə olaraq, bir çox oyunçu oyunu internetə köçürmək qərarına gəldik və seçdik Teeworlds. Teeworlds kiçik, lakin aktiv oyunçular icması (mən də daxil olmaqla!) ilə çox oyunçulu XNUMXD retro oyunudur. Həm yüklənmiş resurslar, həm də CPU və GPU tələbləri baxımından kiçikdir - ideal namizəd.

Cheerp, WebRTC və Firebase ilə çox oyunçu oyunu C++-dan internetə köçürmək
Teeworlds brauzerində işləyir

Biz bu layihəni sınaqdan keçirmək üçün istifadə etmək qərarına gəldik şəbəkə kodunu internetə köçürmək üçün ümumi həllər. Bu adətən aşağıdakı yollarla edilir:

  • XMLHttpRequest/getch, şəbəkə hissəsi yalnız HTTP sorğularından ibarətdirsə və ya
  • veb rozetkaları.

Hər iki həll server tərəfində server komponentinin yerləşdirilməsini tələb edir və heç biri nəqliyyat protokolu kimi istifadəyə icazə vermir. UDP. Bu, video konfrans proqramı və oyunlar kimi real vaxt proqramları üçün vacibdir, çünki o, protokol paketlərinin çatdırılmasına və sifarişinə zəmanət verir. TCP az gecikmə üçün maneə ola bilər.

Üçüncü yol var - brauzerdən şəbəkədən istifadə edin: WebRTC.

RTCDataChannel O, həm etibarlı, həm də etibarsız ötürməni dəstəkləyir (sonuncu halda, mümkün olduqda UDP-dən nəqliyyat protokolu kimi istifadə etməyə çalışır) və həm uzaq serverlə, həm də brauzerlər arasında istifadə edilə bilər. Bu o deməkdir ki, biz server komponenti daxil olmaqla bütün proqramı brauzerə köçürə bilərik!

Bununla belə, bu, əlavə çətinliklə gəlir: iki WebRTC həmyaşıdı ünsiyyət qurmazdan əvvəl, qoşulmaq üçün nisbətən mürəkkəb əl sıxma əməliyyatı həyata keçirməlidirlər ki, bu da bir neçə üçüncü tərəf qurumunu (siqnal serveri və bir və ya daha çox server) tələb edir. SUN/Dönər).

İdeal olaraq, biz daxili olaraq WebRTC-dən istifadə edən, lakin əlaqə yaratmağa ehtiyac olmayan UDP Sockets interfeysinə mümkün qədər yaxın olan şəbəkə API yaratmaq istərdik.

Bu, bizə mürəkkəb detalları tətbiq koduna (biz layihəmizdə mümkün qədər az dəyişmək istədik) məruz qoymadan WebRTC-dən faydalanmağa imkan verəcək.

Minimum WebRTC

WebRTC audio, video və ixtiyari məlumatların peer-to-peer ötürülməsini təmin edən brauzerlərdə mövcud olan API dəstidir.

ICE adlı mexanizm vasitəsilə STUN və/və ya TURN serverlərindən istifadə etməklə həmyaşıdlar arasında əlaqə qurulur (bir və ya hər iki tərəfdə NAT olsa belə). Həmyaşıdlar SDP protokolunun təklifi və cavabı vasitəsilə ICE məlumatlarını və kanal parametrlərini mübadilə edirlər.

Heyrət! Vay! Eyni anda neçə abreviatura var? Bu terminlərin nə demək olduğunu qısaca izah edək:

  • NAT üçün sessiya keçid proqramları (SUN) — NAT-dan yan keçmək və birbaşa host ilə məlumat mübadiləsi üçün bir cüt (IP, port) əldə etmək üçün protokol. Tapşırığını yerinə yetirməyi bacararsa, həmyaşıdlar bir-biri ilə müstəqil olaraq məlumat mübadiləsi edə bilərlər.
  • NAT ətrafında relelərdən istifadə edərək keçid (Dönər) NAT keçidi üçün də istifadə olunur, lakin o, hər iki həmyaşıd üçün görünən bir proxy vasitəsilə məlumatları ötürməklə bunu həyata keçirir. O, gecikmə müddəti əlavə edir və həyata keçirmək STUN-dan daha bahalıdır (çünki o, bütün rabitə seansı boyunca tətbiq olunur), lakin bəzən yeganə seçimdir.
  • İnteraktiv Əlaqənin qurulması (ICE) birbaşa birləşdirən həmyaşıdlardan əldə edilən məlumatlara, həmçinin istənilən sayda STUN və TURN serverləri tərəfindən alınan məlumatlara əsaslanaraq iki həmyaşıdın birləşdirilməsinin mümkün olan ən yaxşı üsulunu seçmək üçün istifadə olunur.
  • Sessiya Təsviri Protokolu (SDP) əlaqə kanalı parametrlərini təsvir etmək üçün formatdır, məsələn, ICE namizədləri, multimedia kodekləri (audio/video kanalı halda) və s... Həmyaşıdlardan biri SDP Təklifini göndərir, ikincisi isə SDP Cavabı ilə cavab verir. .. Bundan sonra bir kanal yaradılır.

Belə bir əlaqə yaratmaq üçün həmyaşıdlar STUN və TURN serverlərindən aldıqları məlumatları toplamalı və bir-biri ilə mübadilə etməlidirlər.

Problem ondadır ki, onların hələ birbaşa əlaqə qurmaq imkanı yoxdur, ona görə də bu məlumatların mübadiləsi üçün diapazondan kənar mexanizm mövcud olmalıdır: siqnal serveri.

Siqnal serveri çox sadə ola bilər, çünki onun yeganə işi əl sıxma mərhələsində həmyaşıdları arasında məlumatları ötürməkdir (aşağıdakı diaqramda göstərildiyi kimi).

Cheerp, WebRTC və Firebase ilə çox oyunçu oyunu C++-dan internetə köçürmək
Sadələşdirilmiş WebRTC əl sıxma ardıcıllığı diaqramı

Teeworlds Şəbəkə Modelinə Baxış

Teeworlds şəbəkə arxitekturası çox sadədir:

  • Müştəri və server komponentləri iki fərqli proqramdır.
  • Müştərilər hər birində eyni anda yalnız bir oyun olan bir neçə serverdən birinə qoşularaq oyuna daxil olurlar.
  • Oyunda bütün məlumat ötürülməsi server vasitəsilə həyata keçirilir.
  • Oyun müştərisində göstərilən bütün ictimai serverlərin siyahısını toplamaq üçün xüsusi master server istifadə olunur.

Məlumat mübadiləsi üçün WebRTC-dən istifadə sayəsində oyunun server komponentini müştərinin yerləşdiyi brauzerə köçürə bilərik. Bu bizə böyük fürsət verir...

Serverlərdən qurtulun

Server məntiqinin olmamasının gözəl üstünlüyü var: biz bütün proqramı Github Səhifələrində və ya Cloudflare arxasında öz aparatımızda statik məzmun kimi yerləşdirə bilərik, beləliklə, sürətli yükləmələri və pulsuz yüksək iş vaxtını təmin edirik. Əslində, biz onları unuda bilərik və əgər bəxtimiz gətirsə və oyun populyarlaşsa, o zaman infrastrukturun modernləşdirilməsinə ehtiyac qalmayacaq.

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

  • Bir və ya daha çox STUN serveri: Seçmək üçün bir neçə pulsuz seçimimiz var.
  • Ən azı bir TURN server: burada pulsuz seçimlər yoxdur, ona görə də ya özümüzü qura bilərik, ya da xidmət üçün ödəniş edə bilərik. Xoşbəxtlikdən, çox vaxt əlaqə STUN serverləri vasitəsilə qurula bilər (və həqiqi p2p təmin edir), lakin ehtiyat variant kimi TURN lazımdır.
  • Siqnal serveri: Digər iki aspektdən fərqli olaraq siqnalizasiya standartlaşdırılmayıb. Siqnal serverinin əslində nəyə cavabdeh olacağı proqramdan bir qədər asılıdır. Bizim vəziyyətimizdə, əlaqə qurmazdan əvvəl az miqdarda məlumat mübadiləsi aparmaq lazımdır.
  • Teeworlds Master Server: Bu, digər serverlər tərəfindən mövcudluğunu elan etmək və müştərilər tərəfindən ictimai serverləri tapmaq üçün istifadə olunur. Tələb olunmasa da (müştərilər həmişə əl ilə bildikləri serverə qoşula bilərlər), oyunçuların təsadüfi insanlarla oyunlarda iştirak edə bilməsi yaxşı olardı.

Google-un pulsuz STUN serverlərindən istifadə etmək qərarına gəldik və bir TURN serverini özümüz yerləşdirdik.

Son iki nöqtə üçün istifadə etdik Firebase:

  • Teeworlds master serveri çox sadə şəkildə həyata keçirilir: hər bir 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ı götürüb oyunçuya göstərirlər. Biz həmçinin siyahını ana səhifədə HTML olaraq göstəririk ki, oyunçular sadəcə serverə klik edib birbaşa oyuna aparıla bilsinlər.
  • Siqnalizasiya, növbəti hissədə təsvir olunan rozetkaların tətbiqi ilə sıx bağlıdır.

Cheerp, WebRTC və Firebase ilə çox oyunçu oyunu C++-dan internetə köçürmək
Oyun daxilində və ana səhifədəki serverlərin siyahısı

Soketlərin həyata keçirilməsi

Lazım olan dəyişikliklərin sayını minimuma endirmək üçün Posix UDP Sockets-ə mümkün qədər yaxın olan API yaratmaq istəyirik.

Biz həmçinin şəbəkə üzərindən ən sadə məlumat mübadiləsi üçün tələb olunan minimumu həyata keçirmək istəyirik.

Məsələn, bizim real marşrutlaşdırmaya ehtiyacımız yoxdur: bütün həmyaşıdlar xüsusi Firebase verilənlər bazası nümunəsi ilə əlaqəli eyni "virtual LAN" üzərindədir.

Buna görə də, unikal IP ünvanlarına ehtiyacımız yoxdur: unikal Firebase açar dəyərləri (domen adlarına bənzər) həmyaşıdları unikal şəkildə müəyyən etmək üçün kifayətdir və hər bir həmyaşıd yerli olaraq tərcümə edilməli olan hər bir açara "saxta" IP ünvanları təyin edir. Bu, qeyri-ciddi bir iş olan qlobal IP ünvanlarının təyin edilməsi ehtiyacını tamamilə aradan qaldırır.

Budur həyata keçirməli olduğumuz minimum API:

// 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 bir neçə mühüm fərqə malikdir: geri çağırışların qeydiyyatı, yerli IP-lərin təyin edilməsi və tənbəl əlaqələr.

Geri zənglərin qeydiyyatı

Orijinal proqram bloklanmayan I/O istifadə etsə belə, veb-brauzerdə işləmək üçün kod yenidən işlənməlidir.

Bunun səbəbi brauzerdəki hadisə dövrəsinin proqramdan gizlədilməsidir (istər JavaScript, istərsə də WebAssembly).

Doğma mühitdə biz belə kod yaza bilərik

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;
    ...
  }
  ...
}

Əgər hadisə dövrəsi bizim üçün gizlidirsə, onu belə bir şeyə çevirməliyik:

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

Yerli IP təyinatı

"Şəbəkəmizdəki" qovşaq identifikatorları IP ünvanları deyil, Firebase açarlarıdır (onlar belə görünən sətirlərdir: -LmEC50PYZLCiCP-vqde ).

Bu rahatdır, çünki bizə IP-lərin təyin edilməsi və onların unikallığının yoxlanılması (həmçinin müştəri əlaqəni kəsdikdən sonra onların atılması) mexanizminə ehtiyacımız yoxdur, lakin çox vaxt həmyaşıdları rəqəmsal dəyərlə müəyyən etmək lazımdır.

Funksiyalar məhz bunun üçün istifadə olunur. resolve и reverseResolve: Tətbiq bir şəkildə açarın sətir dəyərini alır (istifadəçi girişi və ya əsas server vasitəsilə) və onu daxili istifadə üçün IP ünvanına çevirə bilər. 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, lakin müştəridə yerli olaraq həyata keçirilir.

Yəni, IP ünvanları müxtəlif müştərilər arasında bölüşdürülə bilməz və bir növ qlobal identifikator lazımdırsa, o, fərqli bir şəkildə yaradılmalı olacaq.

Tənbəl əlaqə

UDP əlaqəyə ehtiyac duymur, lakin gördüyümüz kimi, WebRTC iki həmyaşıd arasında məlumat ötürməyə başlamazdan əvvəl uzun bir əlaqə prosesi tələb edir.

Eyni səviyyəli abstraksiyanı təmin etmək istəyiriksə, (sendto/recvfrom əvvəlcədən əlaqəsi olmayan ixtiyari həmyaşıdları ilə), onda onlar API daxilində "tənbəl" (gecikmiş) əlaqə yerinə yetirməlidirlər.

UDP-dən istifadə edərkən “server” və “müştəri” arasında normal ünsiyyət zamanı baş verənlər və kitabxanamızın etməli olduğu şey budur:

  • Server zəngləri bind()əməliyyat sisteminə müəyyən edilmiş portda paketləri qəbul etmək istədiyini bildirmək.

Bunun əvəzinə, server açarı altında Firebase-ə açıq port dərc edəcəyik və onun alt ağacında hadisələrə qulaq asacağıq.

  • Server zəngləri recvfrom(), bu portdakı istənilən hostdan gələn paketləri qəbul edir.

Bizim vəziyyətimizdə bu porta göndərilən paketlərin daxil olan növbəsini yoxlamaq lazımdır.

Hər bir portun öz növbəsi var və biz mənbə və təyinat portlarını WebRTC dataqramlarının əvvəlinə əlavə edirik ki, yeni paket gələndə hansı növbəyə yönləndiriləcəyini bilək.

Zəng bloklanmır, ona görə də paket yoxdursa, sadəcə olaraq -1 qaytarırıq və təyin edirik errno=EWOULDBLOCK.

  • Müştəri bəzi xarici vasitələrlə serverin İP və portunu alır və zəng edir sendto(). Bu da daxili zəng edir. bind(), buna görə də sonrakı recvfrom() bağlamanı açıq şəkildə yerinə yetirmədən cavab alacaq.

Bizim vəziyyətimizdə müştəri xaricdən simli açarı alır və funksiyadan istifadə edir resolve() IP ünvanı əldə etmək üçün.

Bu nöqtədə, əgər iki həmyaşıd hələ bir-birinə bağlı deyilsə, biz WebRTC əl sıxışmasına başlayırıq. Eyni həmyaşıdın müxtəlif portlarına qoşulmalar eyni WebRTC DataChannel-dən istifadə edir.

Biz də dolayı yolla həyata keçiririk bind()server növbəti dəfə yenidən qoşula bilsin sendto() nədənsə bağlansa.

Müştəri Firebase-də server port məlumatı altında SDP təklifini yazdıqda, server müştərinin əlaqəsi barədə xəbərdar edilir və server öz cavabı ilə orada cavab verir.

Aşağıdakı diaqram soket sxemi üçün mesaj axınının nümunəsini və ilk mesajın müştəridən serverə ötürülməsini göstərir:

Cheerp, WebRTC və Firebase ilə çox oyunçu oyunu C++-dan internetə köçürmək
Müştəri və server arasında əlaqə mərhələsinin tam diaqramı

Nəticə

Əgər bura qədər oxumusunuzsa, ehtimal ki, nəzəriyyəni hərəkətdə görməkdə maraqlısınız. Oyun oynana bilər teeworlds.leaningtech.com, yoxla!


Həmkarlar arasında yoldaşlıq görüşü

Şəbəkə kitabxanasının kodu burada sərbəst mövcuddur Github. Kanalımızdakı söhbətə qoşulun Gitter!

Mənbə: www.habr.com

Добавить комментарий