C++-ից բազմախաղացող խաղ տեղափոխելը համացանց Cheerp-ի, WebRTC-ի և Firebase-ի միջոցով

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

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

Որպես դրա կիրառման օրինակ՝ մենք որոշեցինք մի քանի խաղացող խաղ տեղափոխել համացանց և ընտրեցինք Թեորվերդսս. Teeworlds-ը բազմախաղացող XNUMXD ռետրո խաղ է խաղացողների փոքր, բայց ակտիվ համայնքով (ներառյալ ես): Այն փոքր է թե՛ ներբեռնված ռեսուրսների, թե՛ պրոցեսորի և պրոցեսորի պահանջների առումով՝ իդեալական թեկնածու:

C++-ից բազմախաղացող խաղ տեղափոխելը համացանց Cheerp-ի, WebRTC-ի և Firebase-ի միջոցով
Աշխատում է Teeworlds բրաուզերում

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

  • XMLHttpՊահանջել/բերել, եթե ցանցի մասը բաղկացած է միայն HTTP հարցումներից, կամ
  • WebSockets.

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

Կա երրորդ ճանապարհ՝ օգտագործեք ցանցը զննարկիչից. WebRTC- ն.

RTCDataChannel Այն աջակցում է ինչպես հուսալի, այնպես էլ անվստահելի փոխանցում (վերջին դեպքում այն ​​փորձում է օգտագործել UDP որպես տրանսպորտային արձանագրություն, երբ հնարավոր է), և կարող է օգտագործվել ինչպես հեռավոր սերվերի, այնպես էլ բրաուզերների միջև: Սա նշանակում է, որ մենք կարող ենք ամբողջ հավելվածը տեղափոխել զննարկիչ, ներառյալ սերվերի բաղադրիչը:

Այնուամենայնիվ, սա գալիս է լրացուցիչ դժվարությամբ. մինչև WebRTC երկու հասակակիցները կարողանան հաղորդակցվել, նրանք պետք է կատարեն համեմատաբար բարդ ձեռքսեղմում միանալու համար, որը պահանջում է մի քանի երրորդ կողմի սուբյեկտներ (ազդանշանային սերվեր և մեկ կամ ավելի սերվերներ): ԱՇԽԱՏԵԼ/ԿԱՐՈՂ).

Իդեալում, մենք կցանկանայինք ստեղծել ցանցային API, որն օգտագործում է WebRTC-ն ներսից, բայց հնարավորինս մոտ է UDP Sockets ինտերֆեյսին, որը կապ հաստատելու կարիք չունի:

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

Նվազագույն WebRTC

WebRTC-ն բրաուզերներում հասանելի API-ների մի շարք է, որն ապահովում է աուդիո, վիդեո և կամայական տվյալների հավասարակցական փոխանցում:

Հասակակիցների միջև կապը հաստատվում է (նույնիսկ եթե կա NAT մեկ կամ երկու կողմերում) STUN և/կամ TURN սերվերների միջոցով ICE կոչվող մեխանիզմի միջոցով: Գործընկերները փոխանակում են ICE տեղեկատվությունը և ալիքի պարամետրերը SDP արձանագրության առաջարկի և պատասխանի միջոցով:

Վա՜յ։ Քանի՞ հապավում է միաժամանակ: Եկեք համառոտ բացատրենք, թե ինչ են նշանակում այս տերմինները.

  • Session Traversal Utilities NAT-ի համար (ԱՇԽԱՏԵԼ) — արձանագրություն՝ NAT-ը շրջանցելու և զույգ (IP, պորտ) ստանալու համար՝ ուղղակիորեն հյուրընկալողի հետ տվյալների փոխանակման համար: Եթե ​​նրան հաջողվի կատարել իր առաջադրանքը, ապա հասակակիցները կարող են ինքնուրույն փոխանակել տվյալներ միմյանց հետ:
  • Անցում, օգտագործելով ռելեներ NAT-ի շուրջ (ԿԱՐՈՂ) օգտագործվում է նաև NAT երթևեկության համար, սակայն այն իրականացնում է դա՝ փոխանցելով տվյալները վստահված անձի միջոցով, որը տեսանելի է երկու գործընկերների համար: Այն ավելացնում է ուշացում և ավելի թանկ է իրագործումը, քան STUN-ը (քանի որ այն կիրառվում է հաղորդակցության ողջ նիստի ընթացքում), բայց երբեմն դա միակ տարբերակն է:
  • Ինտերակտիվ կապի հաստատում (ICE) օգտագործվում է երկու հասակակիցների միացման լավագույն հնարավոր մեթոդը ընտրելու համար՝ հիմնված ուղղակիորեն հասակակիցների միացումից ստացված տեղեկատվության, ինչպես նաև ցանկացած թվով STUN և TURN սերվերների կողմից ստացված տեղեկատվության վրա:
  • Նստաշրջանի նկարագրության արձանագրություն (SDP- ն) միացման ալիքի պարամետրերը նկարագրելու ձևաչափ է, օրինակ՝ ICE թեկնածուներ, մուլտիմեդիա կոդեկներ (աուդիո/վիդեո ալիքի դեպքում) և այլն... Գործընկերներից մեկն ուղարկում է SDP Առաջարկ, իսկ երկրորդը պատասխանում է SDP պատասխանով։ . Դրանից հետո ստեղծվում է ալիք։

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

Խնդիրն այն է, որ նրանք դեռ ուղղակիորեն հաղորդակցվելու հնարավորություն չունեն, ուստի այս տվյալները փոխանակելու համար պետք է գոյություն ունենա տիրույթից դուրս մեխանիզմ՝ ազդանշանային սերվեր:

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

C++-ից բազմախաղացող խաղ տեղափոխելը համացանց Cheerp-ի, WebRTC-ի և Firebase-ի միջոցով
Պարզեցված WebRTC ձեռքսեղմման հաջորդականության դիագրամ

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

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

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

Տվյալների փոխանակման համար WebRTC-ի օգտագործման շնորհիվ մենք կարող ենք խաղի սերվերային բաղադրիչը փոխանցել այն բրաուզերին, որտեղ գտնվում է հաճախորդը: Սա մեզ մեծ հնարավորություն է տալիս...

Ազատվել սերվերներից

Սերվերի տրամաբանության բացակայությունը լավ առավելություն ունի. մենք կարող ենք ամբողջ հավելվածը որպես ստատիկ բովանդակություն տեղակայել Github էջերում կամ Cloudflare-ի ետևում գտնվող մեր սեփական սարքաշարի վրա՝ այդպիսով ապահովելով արագ ներբեռնումներ և բարձր գործունակություն անվճար: Իրականում մենք կարող ենք մոռանալ դրանց մասին, և եթե մեր բախտը բերի, և խաղը հայտնի դառնա, ապա ենթակառուցվածքները արդիականացնելու կարիք չեն ունենա։

Այնուամենայնիվ, որպեսզի համակարգը աշխատի, մենք դեռ պետք է օգտագործենք արտաքին ճարտարապետություն.

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

Մենք որոշեցինք օգտագործել Google-ի անվճար STUN սերվերները և ինքներս տեղադրեցինք մեկ TURN սերվեր:

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

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

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

Վարդակների իրականացում

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

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

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

Հետևաբար, մեզ պետք չեն եզակի IP հասցեներ. 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-ների նշանակում և ծույլ կապեր.

Հետադարձ զանգերի գրանցում

Նույնիսկ եթե սկզբնական ծրագիրը օգտագործում է չարգելափակող I/O, կոդը պետք է վերամշակվի, որպեսզի գործարկվի վեբ բրաուզերում:

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

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

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

Եթե ​​իրադարձությունների հանգույցը մեզ համար թաքնված է, ապա մենք պետք է այն վերածենք այսպիսի բանի.

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

Տեղական IP հանձնարարություն

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

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

Սա հենց այն է, ինչի համար օգտագործվում են գործառույթները: resolve и reverseResolveՀավելվածը ինչ-որ կերպ ստանում է ստեղնի լարային արժեքը (օգտագործողի մուտքագրման կամ հիմնական սերվերի միջոցով) և կարող է այն փոխակերպել IP հասցեի ներքին օգտագործման համար: API-ի մնացած մասը նույնպես ստանում է այս արժեքը տողի փոխարեն՝ պարզության համար:

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

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

Ծույլ կապ

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

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

Սա այն է, ինչ տեղի է ունենում «սերվերի» և «հաճախորդի» միջև սովորական հաղորդակցության ժամանակ UDP-ի օգտագործման ժամանակ, և ինչ պետք է անի մեր գրադարանը.

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

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

  • Սերվերի զանգեր recvfrom(), ընդունելով այս նավահանգստի ցանկացած հոսթից եկող փաթեթներ:

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

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

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

  • Հաճախորդը ստանում է սերվերի IP-ն և պորտը որոշ արտաքին միջոցներով և զանգեր է կատարում sendto(). Սա նաև ներքին զանգ է կատարում: bind(), հետևաբար՝ հետագա recvfrom() պատասխանը կստանա՝ առանց բացահայտորեն կատարման:

Մեր դեպքում հաճախորդը արտաքինից ստանում է լարային ստեղնը և օգտագործում ֆունկցիան resolve() IP հասցե ստանալու համար:

Այս պահին մենք սկսում ենք WebRTC ձեռքսեղմում, եթե երկու հասակակիցները դեռ կապված չեն միմյանց հետ: Նույն հասակակիցների տարբեր նավահանգիստների միացումներն օգտագործում են նույն WebRTC DataChannel-ը:

Կատարում ենք նաև անուղղակի bind()որպեսզի սերվերը կարողանա նորից միանալ հաջորդին sendto() ինչ-ինչ պատճառներով փակվելու դեպքում։

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

Ստորև բերված դիագրամը ցույց է տալիս հաղորդագրության հոսքի օրինակ վարդակից սխեմայի և հաճախորդից սերվերին առաջին հաղորդագրության փոխանցման համար.

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

Ամփոփում

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


Ընկերական հանդիպում գործընկերների միջև

Ցանցային գրադարանի կոդը անվճար հասանելի է Github. Միացե՛ք մեր ալիքի զրույցին Գիտեր!

Source: www.habr.com

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