Կատարում ենք բազմապպատակի խաղի տեղափոխումը C++-ից վեբի վրա Cheerp, WebRTC և Firebase-ով

Ներածություն

Մեր ընկերությունը Leaning Technologies առաջարկում է ավանդական desktop-հավելվածների վեբ տեղափոխման լուծումներ։ Մեր C++ կոմպիլատորը Cheerp արտադրում է WebAssembly և JavaScript-ին ընդհանրացնող կոմպոզիցիա, որը ապահովում է ինչպես հեշտ հաքերություն բրաուզերների հետ, այնպես էլ բարձր կատարողականություն։

Երբեք դրվագեցրել ենք մեր փորձը որպես անդրադարձ, մենք որոշեցինք տեղափոխել բազմաբնույթ խաղը վեբի վրա և ընտրեցինք Teeworlds։ Teeworlds-ը բազմապպատակի երկչափ ռետրո խաղ է՝ փոքր, բայց ակտիվ խաղացողների համայնքով (այդ թվում՝ ես!)։ Այն փոքր է՝ ինչպես ներբեռնվող ռեսուրսների, այնպես էլ CPU և GPU պահանջների տեսանկյունից՝ իդեալական թեկնածու։

Կատարում ենք բազմապպատակի խաղի տեղափոխումը C++-ից վեբի վրա Cheerp, WebRTC և Firebase-ով
Բրաուզերում աշխատող Teeworlds

Մենք որոշեցինք օգտագործել այս նախագիծն՝ փորձարկելու վեբի վրա ցանցային կոդի տեղափոխման ընդհանուր լուծումները։Համարյա միշտ այս գործընթացը կատարվում է այս կերպ.

  • XMLHttpRequest/fetch, եթե ցանցային մասը միայն HTTP հարցումներով է կազմված, կամ
  • WebSockets.

Երկու լուծումներ էլ պահանջում են հոստինգային սերվայն կազմավորել սերվերի կողմից, և ոչ մեկը թույլ չի տալիս օգտագործել որպես փոխադրման պրոտոկոլ. UDPԴա կարևոր է իրական ժամանակի հավելվածների համար, ինչպես տեսանյութի կոնֆերանսների ծրագիրն ու խաղերը, քանի որ տվյալների առաքման և կարգի պոտենցիալները կարող են միանգամից խանգարել ցածր ուշացումների համար. TCP Կա նաև երրորդ ճանապարհ՝ օգտագործել բրաուզերից ցանցը:

RTCDataChannel WebRTC.

դրանք ապահովում են և՛ հուսալի, և՛ ոչ հուսալի փոխանցում (առաջին դեպքերում նա մինչև հնարավորինս կատարում է UDP փոխադրանքը), և կարող է կիրառվել թե հեռավոր սերվերի և թե բրաուզերների միջև. Այսինքն՝ մենք կարող ենք տեղափոխել բրաուզեր՝ ողջ հավելվածը, այդ թվում նաև սերվերային կոմպոնենտը! Սակայն այս համակարգի հետ մեկ այլ դժվարություն է կապված. Արդեն առաջ, երբ երկու WebRTC կատակասերները կարող են փոխանակել տվյալներ, սովորաբար նրանց պետք է կատարել հարաբերականորեն բարդ "գրկախառնությունից" (handshake) անցնելու գործընթաց, որի համար պահանջվում է մի քանի արտաքին մարմիներ (հաղորդագրական սերվեր և մի կամ մի քանի սերվեր

STUN TURN/Կատարյալ թվարկել, մենք կցանկանայինք ստեղծել ցանցային API, որը կիրառվում է WebRTC ներսում, բայց որքան հնարավոր է մոտ UDP Sockets ինտերֆեյսին, որը չի պահանջում միանալ:).

Սա կհնարավորի օգտագործել WebRTC-ի առավելությունները՝ առանց դիմելու համալիր մանրամասներին հավելվածի կոդում (որը նախագծում մենք ցանկանում էինք նվազագույնի հասցնել):

Минимальный WebRTC

WebRTC-ը առաջարկվող API-ծ набор браузерах обеспечивают передачу peer-to-peer звука, видео и произвольных данных.

Պիրսերի միջև կապը կտեղակայվի (նույն ժամին, եթե NAT կա մեկ կամ երկու կողմերում) STUN և/կամ TURN սերվերի միջոցով՝ ICE կոչվող մեխանիզմի միջոցով: Պիրսերը փոխանակվում են ICE տեղեկություններով և ալիքների պարամետրերով через предложение и ответ протокола SDP.

WebRTC-ը ունենում է արդեն գոյություն ունեցող API-ների հավաքածու, որը ապահովում է peer-to-peer բոեր, տեսակի և առաջարկվող տվյալների փոխանցումը: Պիրսերի միջև կապը ուղարկվում է (այդ մասին NAТ-ի առկայության դեպքում, մի կողմում կամ երկուսն էլ) STUN և/կամ TURN սերվերների միջոցով ICE կոչվող մեխանիզմի միջոցով: Պիրսերը փոխանակվում են ICE տեղեկություններով և ալիքների պարամետրերով SDPքի առաջարկության և պատասխանների միջոցով.

Վայ! Ի՞նչքան շատ հապավումներ մեկ անգամում։ Եկեք կարճ բացատրենք, թե ինչ են նշանակում այս հասկացությունները:

Այդպիսի մի կապ ստեղծելու համար կողմերը պետք է հավաքեն այն տեղեկատվությունը, որը ստացվել է STUN և TURN սերվերներից, և փոխանակվեն այն միմյանց հետ:

Մ problémն այն է, որ նրանք դեռ չունեն անմիջական տվյալ փոխանակելու հնարավորություն, ուստի այդ տվյալները փոխանակելու համար պետք է լինի արտանետային մեխանիզմ՝ ազդակային սերվեր:

Ամսաոր կողային սերվեր կարող է լինել շատ պարզ, քանի որ նրա միակ առաքելությունն է՝ տվյալները հղել կողմերի միջև «մանկապախաղի» փուլում (նմանեցված նորմանկով):

Կատարում ենք բազմապպատակի խաղի տեղափոխումը C++-ից վեբի վրա Cheerp, WebRTC և Firebase-ով
WebRTC պակասման «մանկապախաղի» համառոտ սքեմ

Teeworlds ցանցային մոդելի ակնարկ

Teeworlds-ի ցանցային ճարտարապետությունը շատ պարզ է:

  • Հաճախորդի և սերվերի բաղադրիչները երկու տարբեր ծրագրեր են:
  • Հաճախորդները խաղին են մտնում՝ միանալով քանիցս սերվերներին, յուրաքանչյուրին, որը միաժամանակ հյուրընկալում է միայն մեկ խաղ:
  • Խաղի համընդհանուր տվյալների փոխանցումը կատարվում է սերվերի միջոցով:
  • Հատուկ մի վերականգնում սերվեր օգտագործվում է բոլոր հանրային սերվերների ցանկը հավաքելու համար, որոնք ցուցադրվում են խաղի հաճախորդում:

Այսպիսով, WebRTC օգտագործելու շնորհիվ մենք կարող ենք տեղափոխել խաղի սերվերային բաղադրիչը զննում՝ որտեղ գտնվում է հաճախորդը։ Դա մեզ տալիս է հրաշալի հնարավորություն ...

Անհրաժեշտությունից ազատվելու համար սերվերներից

Սերվերային տրամաբանության բացակայությունը հաճելի առավելություն ունի. մենք կարող ենք ներդնել ամբողջ ծրագրարը որպես ստատիկական բովանդակություն Github Pages-ում կամ մեր սարքավորումները Cloudflare-ի տակ, այդպիսով անվճար ապահովելով արագ загрузка և բարձր uptime: Հաճախ է, կարելի է մոռանալ դրանց մասին, և եթե մեզ հաջողված է, և խաղը հայտնի դառնա, ապա ենթակառուցվածքը վերաքարևորել անհրաժեշտություն չի լինի.

Այդուհանդերձ, համակարգը աշխատելու համար մենք ստիպված կլինենք օգտագործել արտաքին ճարտարապետություն:

  • Մեկ կամ մի քանի STUN սերվերներ. մենք ունենք մի քանի անվճար տարբերակների ընտրություն:
  • Վրացական TURN ծառ: այստեղ անվճար տարբերակներ չկան, այնպես որ մենք կարող ենք կամ մեր սեփականը կազմաձևել, կամ վճարել ծառայության համար: Բախտի վրա, ամենից շատ ժամանակ կապը կարելի է ստեղծել STUN սերվերների միջոցով (և իրական p2p ապահովել), բայց TURN-ը անհրաժեշտ է որպես պահուստային տարբերակ:
  • Նշանակման սերվեր. երեք այլ ասպեկտների համեմատ, նշումը չի ստանդարտացվել: Այն, ինչի համար նշման սերվերը իրականում պատասխանատվություն ունի, ինչ-որ չափով կախված է հավելվածից: Մեր դեպքում, կապը ստեղծելուց առաջ անհրաժեշտ է փոխանակվել փոքր քանակությամբ տվյալներով:
  • Teeworlds մաստեր սերվեր. այն օգտագործվում է այլ սերվերների կողմից սեփական առկայության մասին հաղորդելու համար և հաճախորդների կողմից հանրային սերվերներ որոնելու համար: Սակայն այն պարտադիր չէ (հաճախորդները միշտ կարող են ծանոթ սերվերին միանալ ձեռքով), լավ կլիներ ունենալ այն, որպեսզի խաղացողները կարողանան մասնակցել խաղերին պատահական մարդկանց հետ:

Մենք որոշեցինք օգտագործել Google-ի անվճար STUN սերվերները, իսկ մեկ TURN սերվեր սահմանեցինք ինքնուրույն:

Վերջին երկու կետերի համար մենք օգտագործեցինք Firebase:

  • Teeworlds մաստեր սերվերը շատ պարզ է իրականացված՝ որպես տվյալների օբյեկտների ցանկ, որոնք պարունակում են յուրաքանչյուր ակտիվ սերվերի մասին տեղեկություն (անուն, IP, քարտ, ռեժիմ և այլն): Սերվերները հրապարակում և թարմացնում են իրենց սեփական օբյեկտը, իսկ հաճախորդները վերցնում են ամբողջ ցանկը և ցուցադրում այն խաղացողին: Նաեւ մենք ցուցադրում ենք այդ ցանկը գլխավոր էջում որպես HTML, որպեսզի խաղացողները պարզապես սեղմեն սերվեր և անմիջապես մտնեն խաղ:
  • Նշումը սերտորեն կապված է մեր ս็ուակայնների իրականացման հետ, որը նկարագրված է հաջորդ բաժնում:

Կատարում ենք բազմապպատակի խաղի տեղափոխումը C++-ից վեբի վրա Cheerp, WebRTC և Firebase-ով
Խաղի ներսում և գլխավոր էջում սերվերների ցանկ

Սոկետների իրականացում

Մենք ցանկանում ենք ստեղծել API, որքան հնարավոր է մոտ Posix UDP Sockets-ին, որպեսզի նվազեցնենք անհրաժեշտ փոփոխությունների քանակը:

Նաեւ մենք ցանկանում ենք իրականացնել անհրաժեշտ նվազագույնը, որը պահանջվում է պարզ տվյալներ փոխանակելու համար:

Օրինակ, մեզ իրական երթուղավորում չի պետք: Բոլոր պիրերը գտնվում են մեկ «տարրալանջային LAN»-ում, որը կապված է կոնկրետ Firebase տվյալների բազայի օրինակին:

Այսպիսով, մեզ անհրաժեշտ չեն եզակի IP հասցեներ. պիրերի.unique ճանաչման համար բավական է օգտագործել եզակի Firebase բանալիի արժեքները (համեմատաբար դոմենային անունների), իսկ յուրաքանչյուր պիր տեղական տրվում է «աճեցված» IP հասցեներ յուրաքանչյուր բանալիի, որը պետք է փոխակերպվի: Սա բոլորովին պատրաստում է մեզ գլոբալ IP հասցեների նշանակման անհրաժեշտությունից, ինչը ոչ թե պարզ խնդիր է:

Այստեղ նվազագույն 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-ն պարզ է և նման է Posix Sockets API-ին, սակայն ունի մի քանի կարևոր տարբերություններ: կանչերի գրանցում, տեղի IP-ների մակնշում և «յոթ երկրներից» միացում.

Կանչերի գրանցում

Ա même եթե նախնական ծրագիրը օգտագործում է ոչ ապ挡ող մուտք/ռուսա, ծանուցարանները կոդը պետք է վերև կույելվական լինի ծանոթ добиться в веб-բրաուզերի:

Այս հանգամանքը պայմանավորված է նրանով, որ բրաուզերի իրադարձությունների ցիկլը թաքնված է ծրագրից (որն է JavaScript կամ WebAssembly):

Տեղական միջավայրում մենք կարող ենք գրել կոդը նման կերպ

while(running) {
  select(...); // սպասել I/O իրադարձությունների 
  while(true) {
    int r = readfrom(...); // փորձել կարդալ
    if (r < 0 && errno == EWOULDBLOCK) // լրիվ տվյալներ չունենք
      break;
    ...
  }
  ...
}

Եթե մեր համար իրադարձությունների ցիկլը թաքնված է, մենք պետք է այն փոխենք նման բաների:

auto cb = []() { // սա կանչվի, երբ նոր տվյալներ հասանելի լինեն
  while(true) {
    int r = readfrom(...); // փորձել կարդալ
    if (r < 0 && errno == EWOULDBLOCK) // լրիվ տվյալներ չունենք
      break;
    ...
  }
  ...
};
recvCallback(cb); // գրանցել կ callback-ը

Տեղական IP-ների մակնշում

Մեր «ցանցի» հանգույցների նույնականացնողները չեն հանդիսանում IP-հասցեներ, այլ Firebase բանալիներ (այսինքն տողեր, որոնք նայում են այսպես: -LmEC50PYZLCiCP-vqde ).

Սա հարմար է, որովհետև մեզ պետք չէ IP-ների մակնշման մեխանիզմ և նրանց միանձնության ստուգում (և նրանց վերամշակումը կ cliente-երի անջատումից), բայց հաճախ անհրաժեշտ է նույնականացնել հզորները թվային արժեքով:

Իրենց համար այդ ֆունկցիաները դուրս էին resolve և reverseResolve: բջջային ծրագրումը ինչ-որ կերպ ստանում է բանալիի տողային արժեք (մուտքերից կամ հիմնական սերվերից), և կարող է փոխել այն IP-հասցեներին ներքին օգտագործման համար: API-ի մյուս մասը նույնպես հեշտացմամբ ստանում է այդ արժեքը փոխարենը:

Սա նման է DNS որոնման, բայց կատարվում է հաճախորդի մոտ:

Այդ պատճառով, IP-հասցեները չեն կարող լինել ընդհանուր տարբեր հաճախորդների համար, և եթե անհրաժեշտ է որևէ գլոբալ նույնականացման, ապա այն պետք է ստեղծվի այլ եղանակով:

Յոթ երկրներից« միացում

UDP միացումներ չեն պահանջում, սակայն, ինչպես մենք տեսանք, որպեսզի տվյալներ փոխանցվեն երկու հսկիչների միջև, WebRTC պահանջում է երկար միացման գործընթաց:

Եթե ցանկանում ենք ապահովել նույն մակարդակի աբսթրակցիա (sendto/recvfrom ուզում ենք այս ծանր անձնակազմերի մեջ առանց նախնական միացման), ապա մենք պետք է «յոթ երկրներ» (հետաձգված) անել API-ի ներսում:

Այս գործարանը տեղի է ունենում սովորական տվյալների փոխանակման դեպքում «սերվերի» և «հաճախորդի» միջև UDP օգտագործելու դեպքում, և ինչը պետք է իրականացվի մեր գրադարան:

  • Սերվերը կանչում է bind(), որպեսզի զեկուցի գործառնության համակարգին, որ ուզում է ստանալ փաթեթներ նշված պորտում:

Իրականում, մենք հրապարակում ենք բաց պորտը Firebase-ում սերվերի բանալիի տակ և կլսենք իր ենթաբաժնում իրադարձություններ:

  • Սերվերը կանչում է recvfrom(), ընդունելով այդ պորտում փաթեթներ, որոնք հոսում են ցանկացած հոսանքի:

Մեր դեպքում պետք է ստուգել երեխաների ներկա հերթը, որը ուղարկվում է այդ պորտում:

Յուրաքանչյուր նավահանգիստ ունի իր հերթը, եւ մենք ավելացնում ենք WebRTC-ի տաղամասերին սկիզբը եւ վերջը, որպեսզի իմանանք, թե որ հերթին ուղղորդել նոր փաթեթը ժամանելու դեպքում:

Դիզայնը ոչ արգելափակող է, այնպես որ, եթե փաթեթներ չունենք, պարզապես վերադարձնում ենք -1 եւ սահմանում errno=EWOULDBLOCK.

  • Հաճախորդը ինչ-որ արտաքին եղանակներով ստանում է IP եւ սերվերի նավահանգիստը եւ կատարում է sendto(). Ընդ որում, կատարվում է ներքին զանգ bind(), այդ պատճառով սահուն recvfrom() կստանան պատասխան առանց ակնհայտ կատարելու bind:

Մեր դեպքում հաճախորդը արտաքին կերպով ստանում է ՀԱՆԴԻՍԱԿԻ բանալին եւ օգտագործում է resolve() IP հասցեն ստանալու համար:

Այս փուլում սկսում ենք WebRTC-ի «ձեռքի սեղմումը», եթե երկու զույգերը դեռ չեն միավորված միմյանց հետ: Բազմաթիվ նավահանգիստներով մեկ զույգն օգտագործում է նույն WebRTC DataChannel-ը:

Այնպես էլ կատարում ենք անուղղակի bind(), որպեսզի սերվերը կարողանա վերականգնել կապը հաջորդ sendto() հնարավորության դեպքում, եթե այն ինչ-որ պատճառներով փակված լինի:

Սերվերը տեղեկացվում է հաճախորդի միացման մասին, երբ հաճախորդը իր SDP առաջարկը գրանցում է սերվերի նավահանգստի տեղեկանքի տակ Firebase-ում, եւ սերվեր հ respondsպատասխանում է այնտեղ:

Ներքեւում ներկայացված սխեմայում նայում է մեսիջների շարժը սերվերի եւ հաճախորդի միջեւ առաջին հաղորդագրության հանձնումի համար:

Կատարում ենք բազմապպատակի խաղի տեղափոխումը C++-ից վեբի վրա Cheerp, WebRTC և Firebase-ով
Ամբողջ սխեման կապի փուլը հաճախորդի եւ սերվերի միջեւ:

Ավարտ

Եթե դուք վերջացնում եք կարդալ, ապա ձեզ հավանաբար հետաքրքրում է տեսնել տեսությունը գործնականում: Խաղը կարելի է խաղալ teeworlds.leaningtech.com, փորձեք:


Անհրաժեշտ հանդիպում աշխատակիցների միջեւ:

Ձեռնարկի ցանցային գրադարանը ազատ է հասանելի Github. Միացեք մեր ալիքով Gitter!

Ընտանիք: habr.com

Գնել հուսալի հյուրընկալում DDoS պաշտպանությամբ, VPS VDS սերվերներով 🔥 Գնել հուսալի հյուրընկալում DDoS պաշտպանությամբ, VPS VDS սերվերներով | ProHoster