Inasambaza mchezo wa wachezaji wengi kutoka C++ hadi kwenye wavuti kwa Cheerp, WebRTC na Firebase

Utangulizi

kampuni yetu Teknolojia za Leaning hutoa suluhu za kusambaza programu za kompyuta za mezani kwenye wavuti. Mkusanyaji wetu wa C++ furaha inazalisha mchanganyiko wa WebAssembly na JavaScript, ambayo hutoa zote mbili mwingiliano rahisi wa kivinjari, na utendaji wa juu.

Kama mfano wa matumizi yake, tuliamua kuweka mchezo wa wachezaji wengi kwenye wavuti na tukachagua Teeworlds. Teeworlds ni mchezo wa retro wa 2D wenye wachezaji wengi na jamii ndogo lakini inayofanya kazi ya wachezaji (pamoja na mimi!). Ni ndogo kwa suala la rasilimali zilizopakuliwa na mahitaji ya CPU na GPU - mgombea bora.

Inasambaza mchezo wa wachezaji wengi kutoka C++ hadi kwenye wavuti kwa Cheerp, WebRTC na Firebase
Inaendesha katika kivinjari cha Teeworlds

Tuliamua kutumia mradi huu kufanya majaribio suluhu za jumla za kuhamisha msimbo wa mtandao kwenye wavuti. Hii kawaida hufanywa kwa njia zifuatazo:

  • XMLHttpRequest/fetch, ikiwa sehemu ya mtandao inajumuisha maombi ya HTTP pekee, au
  • Mifuko ya Wavuti.

Suluhu zote mbili zinahitaji kupangisha kijenzi cha seva kwenye upande wa seva, na wala hairuhusu kutumika kama itifaki ya usafiri UDP. Hii ni muhimu kwa programu za wakati halisi kama vile programu na michezo ya mikutano ya video, kwa sababu inahakikisha uwasilishaji na mpangilio wa pakiti za itifaki. TCP inaweza kuwa kikwazo kwa latency ya chini.

Kuna njia ya tatu - tumia mtandao kutoka kwa kivinjari: WebRTC.

RTCDataChannel Inaauni upitishaji unaotegemewa na usioaminika (katika hali ya mwisho inajaribu kutumia UDP kama itifaki ya usafirishaji wakati wowote inapowezekana), na inaweza kutumika kwa seva ya mbali na kati ya vivinjari. Hii ina maana kwamba tunaweza kuhamisha programu nzima kwa kivinjari, ikiwa ni pamoja na sehemu ya seva!

Walakini, hii inakuja na ugumu wa ziada: kabla ya wenzao wawili wa WebRTC kuwasiliana, wanahitaji kufanya kusalimiana kwa mikono kwa kiasi kikubwa ili kuunganisha, ambayo inahitaji vyombo kadhaa vya tatu (seva ya kuashiria na seva moja au zaidi. STUN/TAFADHALI).

Kwa hakika, tungependa kuunda API ya mtandao inayotumia WebRTC ndani, lakini iko karibu iwezekanavyo na kiolesura cha Soketi cha UDP ambacho hakihitaji kuanzisha muunganisho.

Hii itaturuhusu kuchukua fursa ya WebRTC bila kufichua maelezo changamano kwa msimbo wa maombi (ambayo tulitaka kubadilisha kidogo iwezekanavyo katika mradi wetu).

Kiwango cha chini cha WebRTC

WebRTC ni seti ya API zinazopatikana katika vivinjari ambazo hutoa uwasilishaji wa data kutoka kwa wenzao wa sauti, video na data kiholela.

Muunganisho kati ya programu zingine umeanzishwa (hata kama kuna NAT kwa upande mmoja au pande zote mbili) kwa kutumia seva za STUN na/au TURN kupitia utaratibu unaoitwa ICE. Wenzake hubadilishana maelezo ya ICE na vigezo vya kituo kupitia ofa na jibu la itifaki ya SDP.

Lo! Vifupisho vingapi kwa wakati mmoja? Hebu tueleze kwa ufupi maana ya maneno haya:

  • Huduma za Kupitia Kipindi cha NAT (STUN) - itifaki ya kupitisha NAT na kupata jozi (IP, bandari) ya kubadilishana data moja kwa moja na mwenyeji. Ikiwa ataweza kukamilisha kazi yake, basi wenzi wanaweza kubadilishana data kwa uhuru na kila mmoja.
  • Kusafiri kwa Kutumia Relay karibu na NAT (TAFADHALI) pia hutumika kwa upitishaji wa NAT, lakini hutekeleza hili kwa kusambaza data kupitia proksi ambayo inaonekana kwa rika zote mbili. Inaongeza muda wa kusubiri na ni ghali zaidi kutekeleza kuliko STUN (kwa sababu inatumika katika kipindi chote cha mawasiliano), lakini wakati mwingine ni chaguo pekee.
  • Uanzishaji wa Muunganisho wa Maingiliano (ICE) kutumika kuchagua njia bora zaidi ya kuunganisha wenzao wawili kulingana na taarifa zilizopatikana kutoka kwa kuunganisha wenzao moja kwa moja, pamoja na taarifa iliyopokelewa na idadi yoyote ya seva za STUN na TURN.
  • Itifaki ya Maelezo ya Kikao (SDP) ni muundo wa kuelezea vigezo vya kituo cha uunganisho, kwa mfano, wagombea wa ICE, codecs za multimedia (katika kesi ya kituo cha sauti / video), nk ... Mmoja wa wenzao anatuma Ofa ya SDP, na wa pili anajibu kwa Jibu la SDP. .. Baada ya hayo, kituo kinaundwa.

Ili kuunda muunganisho kama huo, wenzao wanahitaji kukusanya habari wanayopokea kutoka kwa seva za STUN na TURN na kuzibadilisha na kila mmoja.

Shida ni kwamba bado hawana uwezo wa kuwasiliana moja kwa moja, kwa hivyo utaratibu wa nje wa bendi lazima uwepo ili kubadilishana data hii: seva ya kuashiria.

Seva ya kuashiria inaweza kuwa rahisi sana kwa sababu kazi yake pekee ni kusambaza data kati ya programu zingine katika awamu ya kupeana mkono (kama inavyoonyeshwa kwenye mchoro hapa chini).

Inasambaza mchezo wa wachezaji wengi kutoka C++ hadi kwenye wavuti kwa Cheerp, WebRTC na Firebase
Mchoro uliorahisishwa wa kupeana mkono wa WebRTC

Muhtasari wa Muundo wa Mtandao wa Teeworlds

Usanifu wa mtandao wa Teeworlds ni rahisi sana:

  • Vipengele vya mteja na seva ni programu mbili tofauti.
  • Wateja huingia kwenye mchezo kwa kuunganisha kwenye mojawapo ya seva kadhaa, ambayo kila moja hupangisha mchezo mmoja tu kwa wakati mmoja.
  • Uhamisho wote wa data kwenye mchezo unafanywa kupitia seva.
  • Seva kuu maalum hutumiwa kukusanya orodha ya seva zote za umma ambazo zinaonyeshwa kwenye mteja wa mchezo.

Shukrani kwa matumizi ya WebRTC kwa ubadilishanaji wa data, tunaweza kuhamisha sehemu ya seva ya mchezo hadi kwenye kivinjari ambapo mteja yuko. Hii inatupa fursa nzuri ...

Ondoa seva

Ukosefu wa mantiki ya seva una faida nzuri: tunaweza kusambaza programu nzima kama yaliyomo tuli kwenye Kurasa za Github au kwenye maunzi yetu nyuma ya Cloudflare, na hivyo kuhakikisha upakuaji wa haraka na wakati wa juu bila malipo. Kwa kweli, tunaweza kusahau juu yao, na ikiwa tuna bahati na mchezo unakuwa maarufu, basi miundombinu haitastahili kuwa ya kisasa.

Walakini, ili mfumo ufanye kazi, bado tunapaswa kutumia usanifu wa nje:

  • Seva moja au zaidi za STUN: Tuna chaguo kadhaa za bure za kuchagua.
  • Angalau seva moja ya TURN: hakuna chaguo za bure hapa, kwa hivyo tunaweza kusanidi yetu au kulipia huduma. Kwa bahati nzuri, mara nyingi muunganisho unaweza kuanzishwa kupitia seva za STUN (na kutoa p2p halisi), lakini TURN inahitajika kama chaguo mbadala.
  • Seva ya Kuashiria: Tofauti na vipengele vingine viwili, utoaji wa ishara haujasanifishwa. Kile ambacho seva ya kuashiria itawajibika inategemea kwa kiasi fulani programu. Kwa upande wetu, kabla ya kuanzisha uunganisho, ni muhimu kubadilishana kiasi kidogo cha data.
  • Seva Kuu ya Teeworlds: Inatumiwa na seva zingine kutangaza uwepo wao na wateja kupata seva za umma. Ingawa haihitajiki (wateja wanaweza kuunganisha kila wakati kwenye seva wanayoijua wenyewe), itakuwa vyema kuwa nayo ili wachezaji waweze kushiriki katika michezo na watu bila mpangilio.

Tuliamua kutumia seva za STUN zisizolipishwa za Google, na tukatumia seva moja ya TURN sisi wenyewe.

Kwa pointi mbili za mwisho tulizotumia Moto:

  • Seva kuu ya Teeworlds inatekelezwa kwa urahisi sana: kama orodha ya vitu vyenye taarifa (jina, IP, ramani, hali, ...) ya kila seva inayotumika. Seva huchapisha na kusasisha kifaa chao, na wateja huchukua orodha nzima na kuionyesha kwa kichezaji. Pia tunaonyesha orodha kwenye ukurasa wa nyumbani kama HTML ili wachezaji waweze kubofya seva na kupelekwa moja kwa moja kwenye mchezo.
  • Kuashiria kunahusiana kwa karibu na utekelezaji wa soketi zetu, zilizoelezewa katika sehemu inayofuata.

Inasambaza mchezo wa wachezaji wengi kutoka C++ hadi kwenye wavuti kwa Cheerp, WebRTC na Firebase
Orodha ya seva ndani ya mchezo na kwenye ukurasa wa nyumbani

Utekelezaji wa soketi

Tunataka kuunda API ambayo iko karibu na Soketi za UDP za Posix iwezekanavyo ili kupunguza idadi ya mabadiliko yanayohitajika.

Pia tunataka kutekeleza kiwango cha chini kinachohitajika kwa ubadilishanaji rahisi wa data kwenye mtandao.

Kwa mfano, hatuhitaji uelekezaji halisi: wenzi wote wako kwenye "LAN ya mtandaoni" inayohusishwa na mfano maalum wa hifadhidata ya Firebase.

Kwa hivyo, hatuhitaji anwani za kipekee za IP: thamani za kipekee za funguo za Firebase (sawa na majina ya vikoa) zinatosha kutambua programu zingine kwa njia ya kipekee, na kila rika huweka anwani za IP "bandia" kwa kila ufunguo unaohitaji kutafsiriwa. Hii inaondoa kabisa hitaji la ugawaji wa anwani ya IP ya kimataifa, ambayo ni kazi isiyo ya kawaida.

Hapa kuna API ya chini tunayohitaji kutekeleza:

// 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 ni rahisi na sawa na API ya Soketi za Posix, lakini ina tofauti chache muhimu: kukata simu, kupeana IP za ndani, na viunganisho vya uvivu.

Kusajili Wito

Hata kama programu asili inatumia I/O isiyozuia, lazima msimbo ufanyike upya ili kuendeshwa katika kivinjari.

Sababu ya hii ni kwamba kitanzi cha tukio kwenye kivinjari kimefichwa kutoka kwa programu (iwe JavaScript au WebAssembly).

Katika mazingira ya asili tunaweza kuandika nambari kama hii

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

Ikiwa kitanzi cha tukio kimefichwa kwetu, basi tunahitaji kukibadilisha kuwa kitu kama hiki:

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

Mgawo wa IP wa ndani

Vitambulisho vya nodi kwenye "mtandao" wetu sio anwani za IP, lakini funguo za Firebase (ni kamba zinazoonekana kama hii: -LmEC50PYZLCiCP-vqde ).

Hii ni rahisi kwa sababu hatuhitaji utaratibu wa kukabidhi IP na kuangalia upekee wao (pamoja na kuziondoa baada ya mteja kukatwa), lakini mara nyingi ni muhimu kutambua programu zingine kwa thamani ya nambari.

Hii ndio hasa kazi zinazotumiwa. resolve ΠΈ reverseResolve: Programu kwa namna fulani hupokea thamani ya mfuatano wa ufunguo (kupitia ingizo la mtumiaji au kupitia seva kuu), na inaweza kuibadilisha kuwa anwani ya IP kwa matumizi ya ndani. API iliyosalia pia hupokea thamani hii badala ya mfuatano wa unyenyekevu.

Hii ni sawa na utafutaji wa DNS, lakini inafanywa ndani ya mteja.

Hiyo ni, anwani za IP haziwezi kushirikiwa kati ya wateja tofauti, na ikiwa aina fulani ya kitambulisho cha kimataifa inahitajika, italazimika kuzalishwa kwa njia tofauti.

Uunganisho wa uvivu

UDP haihitaji muunganisho, lakini kama tulivyoona, WebRTC inahitaji mchakato mrefu wa muunganisho kabla ya kuanza kuhamisha data kati ya programu zingine mbili.

Ikiwa tunataka kutoa kiwango sawa cha uondoaji, (sendto/recvfrom na wenzao wa kiholela bila muunganisho wa hapo awali), basi lazima wafanye muunganisho "wavivu" (uliocheleweshwa) ndani ya API.

Hiki ndicho kinachotokea wakati wa mawasiliano ya kawaida kati ya "seva" na "mteja" wakati wa kutumia UDP, na kile ambacho maktaba yetu inapaswa kufanya:

  • Simu za seva bind()kuwaambia mfumo wa uendeshaji kwamba inataka kupokea pakiti kwenye bandari maalum.

Badala yake, tutachapisha mlango wazi kwa Firebase chini ya ufunguo wa seva na kusikiliza matukio katika mti mdogo wake.

  • Simu za seva recvfrom(), inakubali pakiti zinazotoka kwa seva pangishi yoyote kwenye mlango huu.

Kwa upande wetu, tunahitaji kuangalia foleni inayoingia ya pakiti zilizotumwa kwenye bandari hii.

Kila mlango una foleni yake, na tunaongeza chanzo na bandari lengwa mwanzoni mwa hifadhidata za WebRTC ili tujue ni foleni gani ya kusambaza pakiti mpya inapowasili.

Simu haizuii, kwa hivyo ikiwa hakuna pakiti, tunarudi tu -1 na kuweka errno=EWOULDBLOCK.

  • Mteja hupokea IP na bandari ya seva kwa njia fulani za nje, na simu sendto(). Hii pia hufanya simu ya ndani. bind(), kwa hiyo baadae recvfrom() itapokea jibu bila kutekeleza ufungaji kwa uwazi.

Kwa upande wetu, mteja nje anapokea ufunguo wa kamba na hutumia kazi resolve() kupata anwani ya IP.

Katika hatua hii, tunaanzisha kupeana mkono kwa WebRTC ikiwa rika hili bado halijaunganishwa. Viunganisho kwenye milango tofauti ya programu zingine hutumia WebRTC DataChannel sawa.

Pia tunafanya kazi zisizo za moja kwa moja bind()ili seva iweze kuunganisha tena katika ijayo sendto() ikiwa imefungwa kwa sababu fulani.

Seva huarifiwa kuhusu muunganisho wa mteja mteja anapoandika ofa yake ya SDP chini ya maelezo ya mlango wa seva katika Firebase, na seva hujibu kwa majibu yake hapo.

Mchoro ulio hapa chini unaonyesha mfano wa mtiririko wa ujumbe kwa mpango wa soketi na upitishaji wa ujumbe wa kwanza kutoka kwa mteja hadi kwa seva:

Inasambaza mchezo wa wachezaji wengi kutoka C++ hadi kwenye wavuti kwa Cheerp, WebRTC na Firebase
Mchoro kamili wa awamu ya uunganisho kati ya mteja na seva

Hitimisho

Ikiwa umesoma hadi hapa, huenda ungependa kuona nadharia hiyo ikitenda kazi. Mchezo unaweza kuchezwa teeworlds.leaningtech.com, jaribu!


Mechi ya kirafiki kati ya wenzake

Msimbo wa maktaba ya mtandao unapatikana bila malipo Github. Jiunge na mazungumzo kwenye chaneli yetu kwa Gitter!

Chanzo: mapenzi.com

Kuongeza maoni