Flytja fjölspilunarleik frá C++ yfir á vefinn með Cheerp, WebRTC og Firebase

Inngangur

fyrirtækið okkar Hallandi tækni býður upp á lausnir til að flytja hefðbundin skrifborðsforrit yfir á vefinn. C++ þýðandinn okkar hress býr til blöndu af WebAssembly og JavaScript, sem veitir hvort tveggja einföld vafrasamskipti, og mikil afköst.

Sem dæmi um notkun þess ákváðum við að flytja fjölspilunarleik á vefinn og völdum Teeworlds. Teeworlds er fjölspilunar XNUMXD retro leikur með litlu en virku samfélagi leikmanna (þar á meðal ég!). Það er lítið bæði hvað varðar niðurhalað auðlindir og CPU og GPU kröfur - tilvalinn frambjóðandi.

Flytja fjölspilunarleik frá C++ yfir á vefinn með Cheerp, WebRTC og Firebase
Keyrir í Teeworlds vafranum

Við ákváðum að nota þetta verkefni til að gera tilraunir með almennar lausnir til að flytja netkóða á vefinn. Þetta er venjulega gert á eftirfarandi hátt:

  • XMLHttpRequest/fetch, ef nethlutinn samanstendur aðeins af HTTP beiðnum, eða
  • vefinnstungur.

Báðar lausnirnar krefjast hýsingar á miðlarahluta á miðlarahlið og hvorug leyfir notkun sem flutningssamskiptareglur UDP. Þetta er mikilvægt fyrir rauntímaforrit eins og myndbandsfundahugbúnað og leiki, vegna þess að það tryggir afhendingu og röð samskiptapakka TCP getur orðið hindrun fyrir litla leynd.

Það er þriðja leiðin - notaðu netið úr vafranum: WebRTC.

RTCDataChannel Það styður bæði áreiðanlega og óáreiðanlega sendingu (í síðara tilvikinu reynir það að nota UDP sem flutningssamskiptareglur þegar mögulegt er), og er hægt að nota það bæði með ytri netþjóni og á milli vafra. Þetta þýðir að við getum flutt allt forritið í vafrann, þar á meðal miðlarahlutann!

Hins vegar fylgir þessu fleiri erfiðleikar: áður en tveir WebRTC jafningjar geta átt samskipti þurfa þeir að framkvæma tiltölulega flókið handaband til að tengjast, sem krefst nokkurra þriðja aðila (merkjaþjóns og einn eða fleiri netþjóna STUN/HLUTIÐ).

Helst viljum við búa til net API sem notar WebRTC innbyrðis, en er eins nálægt og hægt er UDP Sockets viðmóti sem þarf ekki að koma á tengingu.

Þetta gerir okkur kleift að nýta WebRTC án þess að þurfa að afhjúpa flóknar upplýsingar fyrir forritskóðanum (sem við vildum breyta eins lítið og mögulegt er í verkefninu okkar).

Lágmarks WebRTC

WebRTC er sett af API sem eru fáanleg í vöfrum sem veita jafningjasendingu á hljóði, myndböndum og handahófskenndum gögnum.

Tengingin milli jafningja er komið á (jafnvel þótt NAT sé á annarri eða báðum hliðum) með því að nota STUN og/eða TURN netþjóna í gegnum vélbúnað sem kallast ICE. Jafnaldrar skiptast á ICE-upplýsingum og rásarbreytum með tilboði og svari SDP-samskiptareglunnar.

Vá! Hversu margar skammstafanir í einu? Við skulum útskýra í stuttu máli hvað þessi hugtök þýða:

  • Session Traversal Utilities fyrir NAT (STUN) — samskiptareglur til að komast framhjá NAT og fá par (IP, tengi) til að skiptast á gögnum beint við hýsilinn. Ef honum tekst að klára verkefni sitt geta jafnaldrar skiptst á gögnum sjálfstætt sín á milli.
  • Yfirferð með því að nota liða í kringum NAT (HLUTIÐ) er einnig notað til að fara yfir NAT, en það útfærir þetta með því að senda gögn áfram í gegnum umboð sem er sýnilegt báðum jafningjum. Það bætir við biðtíma og er dýrara í framkvæmd en STUN (vegna þess að það er notað í gegnum alla samskiptalotuna), en stundum er það eini kosturinn.
  • Gagnvirk tengslastofnun (ICE) notað til að velja bestu mögulegu aðferðina til að tengja saman tvo jafningja út frá upplýsingum sem fengnar eru frá því að tengja jafningja beint, svo og upplýsingum sem berast hvaða fjölda STUN og TURN netþjóna sem er.
  • Fundarlýsing bókun (RDS) er snið til að lýsa breytum tengirásar, td ICE frambjóðendum, margmiðlunarmerkjamáli (ef um er að ræða hljóð-/myndrás), osfrv... Einn jafningjanna sendir SDP tilboð og sá annar svarar með SDP svari . . . Eftir þetta er rás búin til.

Til að búa til slíka tengingu þurfa jafnaldrar að safna upplýsingum sem þeir fá frá STUN og TURN netþjónum og skiptast á þeim.

Vandamálið er að þeir hafa ekki enn getu til að hafa bein samskipti, þannig að það verður að vera fyrir hendi utan bands til að skiptast á þessum gögnum: merkjaþjónn.

Merkjaþjónn getur verið mjög einfaldur vegna þess að eina hlutverk hans er að senda gögn á milli jafningja í handabandi áfanganum (eins og sýnt er á skýringarmyndinni hér að neðan).

Flytja fjölspilunarleik frá C++ yfir á vefinn með Cheerp, WebRTC og Firebase
Einfölduð WebRTC handabandsröð skýringarmynd

Teeworlds Network Model Yfirlit

Teeworlds netarkitektúr er mjög einfalt:

  • Biðlara- og miðlarahlutirnir eru tvö mismunandi forrit.
  • Viðskiptavinir fara inn í leikinn með því að tengjast einum af nokkrum netþjónum, sem hver um sig hýsir aðeins einn leik í einu.
  • Allur gagnaflutningur í leiknum fer fram í gegnum netþjóninn.
  • Sérstakur aðalþjónn er notaður til að safna lista yfir alla opinbera netþjóna sem eru sýndir í leikjaþjóninum.

Þökk sé notkun WebRTC fyrir gagnaskipti getum við flutt miðlarahluta leiksins yfir í vafrann þar sem viðskiptavinurinn er staðsettur. Þetta gefur okkur frábært tækifæri...

Losaðu þig við netþjóna

Skortur á rökfræði miðlara hefur góðan kost: við getum sett allt forritið upp sem kyrrstætt efni á Github síðum eða á eigin vélbúnaði á bak við Cloudflare, þannig að tryggja hratt niðurhal og mikinn spennutíma ókeypis. Reyndar getum við gleymt þeim og ef við erum heppin og leikurinn verður vinsæll, þá þarf ekki að nútímavæða innviðina.

Hins vegar, til að kerfið virki, verðum við samt að nota ytri arkitektúr:

  • Einn eða fleiri STUN netþjónar: Við höfum nokkra ókeypis valkosti til að velja úr.
  • Að minnsta kosti einn TURN netþjónn: það eru engir ókeypis valkostir hér, svo við getum annað hvort sett upp okkar eigin eða borgað fyrir þjónustuna. Sem betur fer er oftast hægt að koma á tengingu í gegnum STUN netþjóna (og veita sanna p2p), en TURN er þörf sem varavalkostur.
  • Merkjaþjónn: Ólíkt hinum tveimur þáttunum er merkjasending ekki staðlað. Hvað merkjaþjónninn mun í raun bera ábyrgð á fer nokkuð eftir forritinu. Í okkar tilviki, áður en tenging er komið á, er nauðsynlegt að skiptast á litlu magni af gögnum.
  • Teeworlds Master Server: Hann er notaður af öðrum netþjónum til að auglýsa tilvist sína og af viðskiptavinum til að finna opinbera netþjóna. Þó að það sé ekki krafist (viðskiptavinir geta alltaf tengst netþjóni sem þeir þekkja handvirkt), þá væri gaman að hafa það þannig að leikmenn geti tekið þátt í leikjum með handahófi.

Við ákváðum að nota ókeypis STUN netþjóna Google og settum sjálf upp einn TURN netþjón.

Fyrir síðustu tvo punktana sem við notuðum Firebase:

  • Teeworlds aðalþjónninn er útfærður á mjög einfaldan hátt: sem listi yfir hluti sem innihalda upplýsingar (nafn, IP, kort, háttur, ...) af hverjum virkum netþjóni. Servers birta og uppfæra eigin hlut og viðskiptavinir taka allan listann og sýna spilaranum. Við birtum líka listann á heimasíðunni sem HTML svo leikmenn geta einfaldlega smellt á netþjóninn og farið beint í leikinn.
  • Merkjasending er nátengd innstungunum okkar, sem lýst er í næsta kafla.

Flytja fjölspilunarleik frá C++ yfir á vefinn með Cheerp, WebRTC og Firebase
Listi yfir netþjóna inni í leiknum og á heimasíðunni

Útfærsla á innstungum

Við viljum búa til API sem er eins nálægt Posix UDP Sockets og hægt er til að lágmarka fjölda breytinga sem þarf.

Við viljum einnig innleiða nauðsynlega lágmarkskröfur fyrir einfaldasta gagnaskipti yfir netið.

Til dæmis þurfum við ekki raunverulega leið: allir jafnaldrar eru á sama „raunverulegu staðarneti“ sem tengist tilteknu Firebase gagnagrunnstilviki.

Þess vegna þurfum við ekki einstök IP vistföng: einstök Firebase lykilgildi (svipað og lénsheiti) nægja til að auðkenna jafningja á einkvæman hátt og hver jafningi úthlutar á staðnum „falsar“ IP tölur á hvern lykil sem þarf að þýða. Þetta útilokar algjörlega þörfina fyrir úthlutun IP-tölu á heimsvísu, sem er ekki léttvægt verkefni.

Hér er lágmarks API sem við þurfum að innleiða:

// 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 er einfalt og svipað og Posix Sockets API, en hefur nokkra mikilvæga mun: skráningu svarhringinga, úthluta staðbundnum IP-tölum og lata tengingar.

Skráning svarhringinga

Jafnvel þó að upprunalega forritið noti I/O sem ekki hindrar, verður að breyta kóðanum til að keyra í vafra.

Ástæðan fyrir þessu er sú að atburðarlykkjan í vafranum er falin fyrir forritinu (hvort sem það er JavaScript eða WebAssembly).

Í innfæddu umhverfi getum við skrifað kóða eins og þennan

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

Ef atburðarlykkjan er falin fyrir okkur, þá þurfum við að breyta henni í eitthvað á þessa leið:

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

Staðbundin IP úthlutun

Hnútauðkennin í „netinu“ okkar eru ekki IP tölur, heldur Firebase lyklar (þeir eru strengir sem líta svona út: -LmEC50PYZLCiCP-vqde ).

Þetta er þægilegt vegna þess að við þurfum ekki kerfi til að úthluta IP-tölum og athuga sérstöðu þeirra (ásamt því að farga þeim eftir að viðskiptavinurinn aftengir), en oft er nauðsynlegt að bera kennsl á jafningja með tölugildi.

Þetta er nákvæmlega það sem aðgerðirnar eru notaðar til. resolve и reverseResolve: Forritið fær einhvern veginn strenggildi lykilsins (með inntak notanda eða í gegnum aðalþjóninn), og getur breytt því í IP tölu til innri notkunar. Restin af API fær einnig þetta gildi í stað strengs til einföldunar.

Þetta er svipað og DNS leit, en framkvæmt á staðnum á biðlaranum.

Það er að segja að ekki er hægt að deila IP tölum á milli mismunandi viðskiptavina og ef þörf er á einhvers konar alþjóðlegu auðkenni þarf að búa það til á annan hátt.

Latur tenging

UDP þarf ekki tengingu, en eins og við höfum séð þarf WebRTC langt tengingarferli áður en það getur byrjað að flytja gögn á milli tveggja jafningja.

Ef við viljum veita sama útdráttarstig, (sendto/recvfrom með handahófskenndum jafningjum án fyrri tengingar), þá verða þeir að framkvæma „lata“ (seinkaða) tengingu inni í API.

Þetta er það sem gerist við venjuleg samskipti milli „þjónsins“ og „viðskiptavinarins“ þegar UDP er notað og það sem bókasafnið okkar ætti að gera:

  • Þjónn hringir bind()að segja stýrikerfinu að það vilji taka á móti pökkum á tilgreindu tengi.

Í staðinn munum við birta opna gátt til Firebase undir miðlaralyklinum og hlusta á atburði í undirtré hans.

  • Þjónn hringir recvfrom(), tekur við pökkum sem koma frá hvaða vél sem er á þessari höfn.

Í okkar tilviki þurfum við að athuga komandi biðröð pakka sem sendar eru á þessa höfn.

Hver höfn hefur sína eigin biðröð og við bætum uppruna- og áfangagáttum við upphaf WebRTC gagnaskránna svo að við vitum í hvaða röð á að framsenda þegar nýr pakki kemur.

Símtalið er ekki læst, þannig að ef það eru engir pakkar skilum við einfaldlega -1 og stillum errno=EWOULDBLOCK.

  • Viðskiptavinurinn fær IP og gátt miðlarans með einhverjum utanaðkomandi hætti og hringir sendto(). Þetta hringir einnig innra símtal. bind(), því síðari recvfrom() mun fá svarið án þess að framkvæma bindingu.

Í okkar tilviki fær viðskiptavinurinn strengjalykilinn að utan og notar aðgerðina resolve() til að fá IP tölu.

Á þessum tímapunkti hefjum við WebRTC handaband ef jafnaldrarnir tveir eru ekki ennþá tengdir hver öðrum. Tengingar við mismunandi tengi á sama jafningja nota sömu WebRTC DataChannel.

Við framkvæmum líka óbeint bind()svo að þjónninn geti tengst aftur í næsta sendto() ef það lokaðist af einhverjum ástæðum.

Miðlarinn fær tilkynningu um tengingu viðskiptavinarins þegar viðskiptavinurinn skrifar SDP tilboð sitt undir miðlaragáttarupplýsingarnar í Firebase og þjónninn bregst við með svari sínu þar.

Skýringarmyndin hér að neðan sýnir dæmi um skilaboðaflæði fyrir falskerfi og sendingu fyrstu skilaboðanna frá biðlaranum til netþjónsins:

Flytja fjölspilunarleik frá C++ yfir á vefinn með Cheerp, WebRTC og Firebase
Heill skýringarmynd af tengingarfasa milli viðskiptavinar og netþjóns

Ályktun

Ef þú hefur lesið þetta langt hefurðu líklega áhuga á að sjá kenninguna í verki. Hægt er að spila leikinn á teeworlds.leaningtech.com, reyna það!


Vináttuleikur samstarfsmanna

Kóðinn fyrir netsafnið er ókeypis aðgengilegur á GitHub. Taktu þátt í samtalinu á rásinni okkar kl Gitter!

Heimild: www.habr.com

Bæta við athugasemd