Giriş
Bizim şirkət ənənəvi masaüstü tətbiqlərini vebə keçirmək üçün həllər təqdim edir. Bizim C++ kompilyatorumuz WebAssembly və JavaScript birləşməsi yaradır, bu da təmin edir , 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 — 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.

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 az gecikmələrə mane ola bilər. Üçüncü yol var — brauzerdən istifadə etmək:
RTCDataChannel .
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 /).
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:
- — a protocol for traversing NAT and obtaining a pair (IP, port) for exchanging data directly with the host. If it succeeds, peers can exchange data with each other independently.
- is also used for NAT traversal, but it does this by redirecting data through a proxy visible to both peers. It adds latency and is more resource-intensive than STUN (as it is applied throughout the entire communication session), but sometimes it is the only feasible option.
- is used to select the best possible way to connect two peers based on information obtained during the direct connection of peers, as well as data collected from any number of STUN and TURN servers.
- — this is a format for describing connection channel parameters, such as ICE candidates, media codecs (in the case of audio/video channels), etc. One peer sends an SDP Offer, and the other responds with an SDP Answer. After this, a channel is created.
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).

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 :
- 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.

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çirinYerli 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 resolve və reverseResolve: 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.

Ə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 Kolleqalar arasında dostluq matçı.
Şəbəkə kitabxanasının kodu pulsuz olaraq mövcuddur.
Bizim kanala qoşulun. Gitter !
Mənbə: habr.com
