{"id":38370,"date":"2019-10-31T22:23:18","date_gmt":"2019-10-31T19:23:18","guid":{"rendered":"https:\/\/prohoster.info\/blog\/portiruem-mnogopolzovatelskuyu-igru-s-s-na-veb-c-cheerp-webrtc-i-firebase\/"},"modified":"2019-10-31T22:23:18","modified_gmt":"2019-10-31T19:23:18","slug":"portiruem-mnogopolzovatelskuyu-igru-s-s-na-veb-c-cheerp-webrtc-i-firebase","status":"publish","type":"post","link":"https:\/\/prohoster.info\/da\/blog\/administrirovanie\/portiruem-mnogopolzovatelskuyu-igru-s-s-na-veb-c-cheerp-webrtc-i-firebase","title":{"rendered":"Vi porterer et multiplayer-spil fra C++ til web med Cheerp, WebRTC og Firebase","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<h2>Introduktion<\/h2>\n<p>\nVores virksomhed <noindex><a rel=\"nofollow\" href=\"https:\/\/leaningtech.com\">Leaning Technologies<\/a><\/noindex> tilbyder l\u00f8sninger til portering af traditionelle desktop-applikationer til web. Vores C++-kompilator <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerp-meta\">Cheerp<\/a><\/noindex> genererer en kombination af WebAssembly og JavaScript, hvilket sikrer b\u00e5de <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerp-meta\/wiki\/Cheerp-Tutorial%3A-Mixed-mode-C++-to-WebAssembly-and-JavaScript\">nem interaktion med browseren<\/a><\/noindex>, og h\u00f8j ydeevne.<\/p>\n<p>Som et eksempel p\u00e5 dens anvendelse har vi besluttet at portere et multiplayer-spil til web og valgte derfor <noindex><a rel=\"nofollow\" href=\"https:\/\/www.teeworlds.com\/\"><strong>Teeworlds<\/strong><\/a><\/noindex>. Teeworlds er et multiplayer 2D retro-spil med et lille, men aktivt spillerf\u00e6llesskab (inklusive mig!). Det er lille b\u00e5de hvad ang\u00e5r downloadst\u00f8rrelse og krav til CPU og GPU - en ideel kandidat.<\/p>\n<p><img decoding=\"async\" alt=\"Vi porterer et multiplayer-spil fra C++ til web med Cheerp, WebRTC og Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/30bff36ee1158ddea2e76d8b11e48d2b.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Teeworlds, der k\u00f8rer i browseren<\/i><br \/>\n<noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><br \/>\nVi besluttede at bruge dette projekt til at eksperimentere med <strong>generelle l\u00f8sninger til portering af netv\u00e6rkskode til web<\/strong>. Dette kan normalt g\u00f8res p\u00e5 f\u00f8lgende m\u00e5der:<\/p>\n<ul>\n<li><strong>XMLHttpRequest\/fetch<\/strong>, hvis den netv\u00e6rksbaserede del kun best\u00e5r af HTTP-anmodninger, eller<\/li>\n<li><strong>WebSockets<\/strong>.<\/li>\n<\/ul>\n<p>\nBegge l\u00f8sninger kr\u00e6ver, at serverkomponenten hostes p\u00e5 serversiden, og ingen af dem tillader brug af <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/User_Datagram_Protocol\">UDP<\/a><\/noindex>som transportprotokol. Dette er vigtigt for realtidsapplikationer, s\u00e5som videokonference-software og spil, eftersom garanti for levering og r\u00e6kkef\u00f8lge af pakker i protokollen <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Transmission_Control_Protocol\">TCP<\/a><\/noindex> kan v\u00e6re en hindring for lav latens.<\/p>\n<p>Der findes ogs\u00e5 en tredje metode - at bruge netv\u00e6rket fra browseren: <noindex><a rel=\"nofollow\" href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Glossary\/WebRTC\"><strong>WebRTC<\/strong><\/a><\/noindex>.<\/p>\n<p><noindex><a rel=\"nofollow\" href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/RTCDataChannel\"><strong>RTCDataChannel<\/strong><\/a><\/noindex> underst\u00f8tter b\u00e5de p\u00e5lidelig og up\u00e5lidelig transmission (i sidstn\u00e6vnte tilf\u00e6lde fors\u00f8ger det, n\u00e5r det er muligt, at bruge UDP som transportprotokol), og kan anvendes b\u00e5de med en fjernserver og mellem browsere. <strong>Det betyder, at vi kan portere hele applikationen til browseren, inklusive serverkomponenten!<\/strong><\/p>\n<p>Men der er en ekstra udfordring: F\u00f8r to WebRTC-noder kan udveksle data, skal de gennemg\u00e5 en ret kompliceret 'handshake'-procedure for at oprette forbindelse, hvilket kr\u00e6ver flere eksterne enheder (signaleringsserver og en eller flere <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/STUN\">STUN<\/a><\/noindex>\/<noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Traversal_Using_Relays_around_NAT\">TURN<\/a><\/noindex>).<\/p>\n<p>Ideelt set vil vi gerne skabe et netv\u00e6rks-API, der internt bruger WebRTC, men s\u00e5 t\u00e6t p\u00e5 UDP-socket-gr\u00e6nsefladen som muligt, som ikke kr\u00e6ver oprettelse af forbindelse.<\/p>\n<p>Det vil tillade os at udnytte fordelene ved WebRTC uden at skulle afsl\u00f8re de komplekse detaljer for applikationskoden (som vi \u00f8nskede at \u00e6ndre s\u00e5 lidt som muligt i vores projekt).<\/p>\n<h1>Minimal WebRTC<\/h1>\n<p>\nWebRTC er et s\u00e6t API'er til r\u00e5dighed i browsere, der muligg\u00f8r peer-to-peer overf\u00f8rsel af lyd, video og vilk\u00e5rlige data.<\/p>\n<p>Forbindelsen mellem brugererne etableres (selv i tilf\u00e6lde af NAT p\u00e5 den ene eller begge sider) ved hj\u00e6lp af STUN- og\/eller TURN-servere gennem en mekanisme kaldet ICE. Brugerne udveksler ICE-oplysninger og kanalparametre via tilbud og svar i SDP-protokollen.<\/p>\n<p>Wow! S\u00e5 mange forkortelser p\u00e5 \u00e9n gang. Lad os kort forklare, hvad disse begreber betyder:<\/p>\n<ul>\n<li><noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/STUN\"><strong>Session Traversal Utilities for NAT<\/strong> (<strong>STUN<\/strong>)<\/a><\/noindex> er en protokol til at omg\u00e5 NAT og f\u00e5 et par (IP, port) til at udveksle data direkte med v\u00e6rten. Hvis den lykkes med sin opgave, kan brugerne udveksle data med hinanden selv.<\/li>\n<li><noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Traversal_Using_Relays_around_NAT\"><strong>Traversal Using Relays around NAT<\/strong> (<strong>TURN<\/strong>)<\/a><\/noindex> bruges ogs\u00e5 til at omg\u00e5 NAT, men den implementerer det ved at omdirigere data gennem en proxy, der er synlig for begge brugere. Den tilf\u00f8jer forsinkelse og er dyrere at udf\u00f8re end STUN (fordi den anvendes hele kommunikationssessionen), men nogle gange er det den eneste mulige l\u00f8sning.<\/li>\n<li><noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Interactive_Connectivity_Establishment\"><strong>Interactive Connectivity Establishment<\/strong> (<strong>ICE<\/strong>)<\/a><\/noindex> bruges til at v\u00e6lge den bedste mulige m\u00e5de at forbinde to brugere p\u00e5 baseret p\u00e5 oplysninger, der modtages under direkte forbindelse mellem brugerne samt oplysninger, der modtages fra det antal STUN- og TURN-servere.<\/li>\n<li><noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Session_Description_Protocol\"><strong>Session Description Protocol<\/strong> (<strong>SDP<\/strong>)<\/a><\/noindex> er et format til beskrivelse af parametre for tilslutningskanalen, s\u00e5som ICE-kandidater, mediecodec (i tilf\u00e6lde af lyd-\/videokanalen) osv. En af brugerne sender en SDP-tilbud (\"offer\"), og den anden svarer med en SDP-svar (\"answer\"). Herefter oprettes kanalen.<\/li>\n<\/ul>\n<p>\nFor at oprette en s\u00e5dan forbindelse skal brugerne indsamle oplysningerne, de modtog fra STUN- og TURN-serverne, og udveksle dem med hinanden.<\/p>\n<p>Problemet er, at de endnu ikke har mulighed for at udveksle data direkte, s\u00e5 der skal findes en off-band mekanisme: en signalserver.<\/p>\n<p>Signalserveren kan v\u00e6re meget simpel, fordi dens eneste opgave er at omdirigere data mellem brugerne i \"h\u00e5ndtryks\" fasen (som vist i diagrammet nedenfor).<\/p>\n<p><img decoding=\"async\" alt=\"Vi porterer et multiplayer-spil fra C++ til web med Cheerp, WebRTC og Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/0fc39ba5e0942bc19183e8cf06c60868.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Forenklet diagram over WebRTC's h\u00e5ndtrykssekvens<\/i><\/p>\n<h1>Oversigt over Teeworlds netv\u00e6rksmodel<\/h1>\n<p>\nTeeworlds netv\u00e6rksarkitektur er meget enkel:<\/p>\n<ul>\n<li>Klient- og serverkomponenter er to separate programmer.<\/li>\n<li>Kunderne tr\u00e6der ind i spillet ved at tilslutte sig en af flere servere, hvor hver server kun hoster \u00e9t spil ad gangen.<\/li>\n<li>Al dataoverf\u00f8rsel i spillet foreg\u00e5r gennem serveren.<\/li>\n<li>En s\u00e6rskilt master-server bruges til at samle en liste over alle offentlige servere, som vises i spilklienten.<\/li>\n<\/ul>\n<p>\nVed at bruge WebRTC til dataudveksling kan vi flytte den serverkomponent, som spillet kr\u00e6ver, til browseren, hvor klienten befinder sig. Dette giver os en fremragende mulighed...<\/p>\n<h1>At slippe for servere<\/h1>\n<p>\nManglen p\u00e5 serverlogik har den behagelige fordel, at vi kan deployere hele applikationen som statisk indhold p\u00e5 Github Pages eller p\u00e5 vores eget udstyr bag Cloudflare, hvilket dermed giver os hurtige indl\u00e6sninger og h\u00f8j oppetid gratis. I praksis kan vi glemme dem, og hvis vi er heldige og spillet bliver popul\u00e6rt, vil vi ikke skulle opgradere infrastrukturen.<\/p>\n<p>Men for at systemet kan fungere, skal vi stadig bruge en ekstern arkitektur:<\/p>\n<ul>\n<li>En eller flere STUN-servere: vi har flere gratis muligheder at v\u00e6lge imellem.<\/li>\n<li>Mindst \u00e9n TURN-server: her findes der ingen gratis muligheder, s\u00e5 vi kan enten konfigurere vores egen eller betale for en tjeneste. Heldigvis kan forbindelsen stort set altid etableres gennem STUN-servere (og sikre \u00e6gte p2p), men TURN er n\u00f8dvendig som en backup.<\/li>\n<li>Signalserver: i mods\u00e6tning til de to andre aspekter er signalering ikke standardiseret. Hvad signalserveren egentlig skal h\u00e5ndtere, afh\u00e6nger delvist af applikationen. I vores tilf\u00e6lde er det n\u00f8dvendigt at udveksle et lille datam\u00e6ngde f\u00f8r forbindelsen oprettes.<\/li>\n<li>Teeworlds master-server: den anvendes af andre servere til at informere om deres eksistens og af klienter til at finde offentlige servere. Selvom den ikke er obligatorisk (klienter kan altid forbinde til en kendt server manuelt), ville det v\u00e6re godt at have den, s\u00e5 spillere kan deltage i spil med tilf\u00e6ldige personer.<\/li>\n<\/ul>\n<p>\nVi har besluttet at bruge Googles gratis STUN-servere, mens vi selv har sat en TURN-server op.<\/p>\n<p>For de to sidste punkter brugte vi <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Firebase\">Firebase<\/a><\/noindex>:<\/p>\n<ul>\n<li>Teeworlds master server er implementeret p\u00e5 en meget simpel m\u00e5de: som en liste over objekter, der indeholder information (navn, IP, kort, tilstand, \u2026) om hver aktiv server. Serverne publicerer og opdaterer deres eget objekt, mens klienterne henter hele listen og viser den til spilleren. Vi viser ogs\u00e5 listen p\u00e5 startsiden som HTML, s\u00e5 spillere let kan klikke p\u00e5 serveren og komme direkte ind i spillet.<\/li>\n<li>Signalisering er n\u00e6rt relateret til vores implementering af sockets, som beskrives i n\u00e6ste afsnit.<\/li>\n<\/ul>\n<p>\n<img decoding=\"async\" alt=\"Vi porterer et multiplayer-spil fra C++ til web med Cheerp, WebRTC og Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/5e30bc22643f8baecf850053bf69e130.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Serverliste indeni spillet og p\u00e5 startsiden<\/i><\/p>\n<h1>Implementering af sockets<\/h1>\n<p>\nVi \u00f8nsker at skabe et API, der er s\u00e5 t\u00e6t p\u00e5 Posix UDP Sockets som muligt for at minimere antallet af n\u00f8dvendige \u00e6ndringer.<\/p>\n<p>Vi \u00f8nsker ogs\u00e5 at implementere det n\u00f8dvendige minimum for den enkleste dataudveksling over netv\u00e6rket.<\/p>\n<p>For eksempel har vi ikke brug for \u00e6gte routings: alle peers befinder sig i et 'virtuel LAN', der er forbundet til et specifikt Firebase-database-eksperiment.<\/p>\n<p>Derfor har vi ikke brug for unikke IP-adresser: for at identificere peers unikt er det tilstr\u00e6kkeligt at bruge unikke v\u00e6rdier af Firebase-n\u00f8gler (svarende til dom\u00e6nenavne), og hver peer tildeler lokalt 'falske' IP-adresser til hver n\u00f8gle, der skal omdannes. Dette fjerner fuldst\u00e6ndigt behovet for global tildeling af IP-adresser, hvilket er en ikke-triviel opgave.<\/p>\n<p>Her er det minimale API, vi skal implementere:<\/p>\n<pre><code class=\"cpp\">\/\/ Create and destroy a socket\nint socket();\nint close(int fd);\n\/\/ Bind a socket to a port, and publish it on Firebase\nint bind(int fd, AddrInfo* addr);\n\/\/ Send a packet. This lazily create a WebRTC connection to the \n\/\/ peer when necessary\nint sendto(int fd, uint8_t* buf, int len, const AddrInfo* addr);\n\/\/ Receive the packets destined to this socket\nint recvfrom(int fd, uint8_t* buf, int len, AddrInfo* addr);\n\/\/ Be notified when new packets arrived\nint recvCallback(Callback cb);\n\/\/ Obtain a local ip address for this peer key\nuint32_t resolve(client::String* key);\n\/\/ Get the peer key for this ip\nString* reverseResolve(uint32_t addr);\n\/\/ Get the local peer key\nString* local_key();\n\/\/ Initialize the library with the given Firebase database and \n\/\/ WebRTc connection options\nvoid init(client::FirebaseConfig* fb, client::RTCConfiguration* ice);<\/code><\/pre>\n<p>\nAPI'et er enkelt og ligner API'et for Posix Sockets, men har nogle vigtige forskelle: <strong>registrering af callback-funktioner, tildeling af lokale IP-adresser og 'lazy' forbindelse<\/strong>.<\/p>\n<h2>Registrering af callback-funktioner<\/h2>\n<p>\nSelv hvis den oprindelige program bruger ikke-blokerende input-output, skal koden refaktoreres for at k\u00f8re i en webbrowser.<\/p>\n<p>\u00c5rsagen til dette er, at begivenhedsl\u00f8kken i browseren er skjult for programmet (uanset om det er JavaScript eller WebAssembly).<\/p>\n<p>I et natives milj\u00f8 kan vi skrive koden s\u00e5dan her<\/p>\n<pre><code class=\"cpp\">while(running) {\n  select(...); \/\/ vent p\u00e5 I\/O begivenheder\n  while(true) {\n    int r = readfrom(...); \/\/ pr\u00f8v at l\u00e6se\n    if (r &lt; 0 &amp;&amp; errno == EWOULDBLOCK) \/\/ ikke flere data tilg\u00e6ngelige\n      break;\n    ...\n  }\n  ...\n}<\/code><\/pre>\n<p>\nHvis begivenhedsl\u00f8kken er skjult for os, skal den omdannes til noget som:<\/p>\n<pre><code class=\"cpp\">auto cb = []() { \/\/ denne vil blive kaldt, n\u00e5r nye data er tilg\u00e6ngelige\n  while(true) {\n    int r = readfrom(...); \/\/ pr\u00f8v at l\u00e6se\n    if (r &lt; 0 &amp;&amp; errno == EWOULDBLOCK) \/\/ ikke flere data tilg\u00e6ngelige\n      break;\n    ...\n  }\n  ...\n};\nrecvCallback(cb); \/\/ registrer callback&#039;en<\/code><\/pre>\n<p><\/p>\n<h2>Tildeling af lokale IP-adresser<\/h2>\n<p>\nNoderne i vores \"netv\u00e6rk\" identificeres ikke ved IP-adresser, men ved Firebase-n\u00f8gler (de strenge, der ser s\u00e5dan ud: <code>-LmEC50PYZLCiCP-vqde<\/code> ).<\/p>\n<p>Det er praktisk, fordi vi ikke har brug for en mekanisme til at tildele IP og kontrollere deres unikke identitet (og ogs\u00e5 deres bortskaffelse efter frakobling af klienten), men det er ofte n\u00f8dvendigt at identificere peers ved numeriske v\u00e6rdier.<\/p>\n<p>Netop til dette bruges funktionerne <code>resolve<\/code> og <code>reverseResolve<\/code>: Applikationen f\u00e5r p\u00e5 en eller anden m\u00e5de den strenge v\u00e6rdi af n\u00f8glen (gennem brugerinput eller gennem masterserveren), og kan konvertere den til en IP-adresse til intern brug. Resten af API'et f\u00e5r ogs\u00e5 for nemhed i stedet for strengen denne v\u00e6rdi.<\/p>\n<p>Det minder om DNS-opslag, men udf\u00f8res lokalt hos klienten.<\/p>\n<p>Det vil sige, at IP-adresser ikke kan v\u00e6re f\u00e6lles for forskellige klienter, og hvis der er behov for en global identifikator, skal den genereres p\u00e5 en anden m\u00e5de.<\/p>\n<h2>Lazy connection<\/h2>\n<p>\nUDP kr\u00e6ver ikke en forbindelse, men som vi har set, kr\u00e6ver WebRTC en lang forbindelsesproces, f\u00f8r data kan overf\u00f8res mellem to peers.<\/p>\n<p>Hvis vi vil sikre det samme abstraheringsniveau, (<code>sendto<\/code>\/<code>recvfrom<\/code> til vilk\u00e5rlige peers uden forudg\u00e5ende forbindelse), skal vi udf\u00f8re en \"lazy\" (forsinket) forbindelse inden for API'et.<\/p>\n<p>Her er, hvad der sker ved almindelig dataudveksling mellem \"serveren\" og \"klienten\", n\u00e5r der bruges UDP, og hvad vores biblioteker skal udf\u00f8re:<\/p>\n<ul>\n<li>Serveren kalder <code>bind()<\/code>, for at informere operativsystemet om, at den vil modtage pakker p\u00e5 den angivne port.<\/li>\n<\/ul>\n<p>\nI stedet offentligg\u00f8r vi en \u00e5ben port i Firebase under serverens n\u00f8gle og lytter efter begivenheder i dens undertr\u00e6.<\/p>\n<ul>\n<li>Serveren kalder <code>recvfrom()<\/code>, modtager pakker til denne port, der kommer fra enhver v\u00e6rt.<\/li>\n<\/ul>\n<p>\nI vores tilf\u00e6lde skal vi kontrollere den indkommende pakkequeue, der sendes til denne port.<\/p>\n<p>Hver port har sin egen k\u00f8, og vi tilf\u00f8jer i starten af WebRTC-datagrammerne kilde- og destinationsportene for at vide, hvilken k\u00f8 vi skal omdirigere en ny pakke til, n\u00e5r den ankommer.<\/p>\n<p>Opkaldet er ikke-blokerende, s\u00e5 hvis der ikke er pakker, returnerer vi bare -1 og s\u00e6tter <code>errno=EWOULDBLOCK<\/code>.<\/p>\n<ul>\n<li>Klienten f\u00e5r serverens IP og port ved hj\u00e6lp af nogle eksterne metoder, og kalder <code>sendto()<\/code>. Der udf\u00f8res ogs\u00e5 et internt kald, s\u00e5 den efterf\u00f8lgende <code>bind()<\/code>f\u00e5r svar uden eksplicit at udf\u00f8re bind. <code>recvfrom()<\/code> f\u00e5r svar uden at binde det eksplicit.<\/li>\n<\/ul>\n<p>\nI vores tilf\u00e6lde f\u00e5r klienten en strengn\u00f8gle eksternt og bruger funktionen <code>resolve()<\/code> for at f\u00e5 IP-adressen.<\/p>\n<p>P\u00e5 dette tidspunkt begynder vi \"h\u00e5ndtrykket\" WebRTC, hvis de to peers endnu ikke er forbundet med hinanden. Forbindelser til forskellige porte p\u00e5 \u00e9n peer bruger den samme DataChannel WebRTC.<\/p>\n<p>Vi udf\u00f8rer desuden en indirekte <code>bind()<\/code>, s\u00e5 serveren kan genoprette forbindelsen i den n\u00e6ste <code>sendto()<\/code> i tilf\u00e6lde af, at den af en eller anden grund er blevet lukket.<\/p>\n<p>Serveren bliver informeret om klientens forbindelse, n\u00e5r klienten registrerer sit SDP-offer under serverens portinformation i Firebase, og serveren svarer med sit svar der.<\/p>\n<p>\nDiagrammet nedenfor viser et eksempel p\u00e5 bev\u00e6gelsen af meddelelser for socket-skemaet og overf\u00f8rslen af den f\u00f8rste meddelelse fra klienten til serveren:<\/p>\n<p><img decoding=\"async\" alt=\"Vi porterer et multiplayer-spil fra C++ til web med Cheerp, WebRTC og Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/7dc48e816c09e33daf28c307cad0c258.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Den komplette skematiske forbindelse mellem klienten og serveren<\/i><\/p>\n<h1>Konklusion<\/h1>\n<p>\nHvis du er n\u00e5et hertil, er du sikkert interesseret i at se teorien i praksis. Du kan spille spillet p\u00e5 <noindex><a rel=\"nofollow\" href=\"https:\/\/teeworlds.leaningtech.com\">teeworlds.leaningtech.com<\/a><\/noindex>, pr\u00f8v det!<\/p>\n<p><center><iframe loading=\"lazy\" width=\"560\" height=\"315\" src=\"https:\/\/gfycat.com\/ifr\/newjaggedcoyote\" frameborder=\"0\" allowfullscreen><\/iframe><\/center><br \/>\n<i>En venlig match mellem kolleger<\/i><\/p>\n<p>Koden til netv\u00e6rksbiblioteket er frit tilg\u00e6ngelig p\u00e5 <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerpnet\">Github<\/a><\/noindex>. Deltag i samtalen p\u00e5 vores kanal p\u00e5 <noindex><a rel=\"nofollow\" href=\"https:\/\/gitter.im\/leaningtech\/cheerp\">Gitter<\/a><\/noindex>!<br \/>\n<br \/>Kilde: <a content=\"nofollow\" rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/post\/468031\/\">habr.com<\/a><\/p>","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"excerpt":{"rendered":"<p>\u0412\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u041d\u0430\u0448\u0430 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u044f Leaning Technologies \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u043f\u043e \u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044e \u0442\u0440\u0430\u0434\u0438\u0446\u0438\u043e\u043d\u043d\u044b\u0445 desktop-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439 \u0432 \u0432\u0435\u0431. \u041d\u0430\u0448 \u043a\u043e\u043c\u043f\u0438\u043b\u044f\u0442\u043e\u0440 C++ Cheerp \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442 \u0441\u043e\u0447\u0435\u0442\u0430\u043d\u0438\u0435 WebAssembly \u0438 JavaScript, \u0447\u0442\u043e \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0435\u0442 \u0438 \u043f\u0440\u043e\u0441\u0442\u043e\u0435 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0441 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u043e\u043c, \u0438 \u0432\u044b\u0441\u043e\u043a\u0443\u044e \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c. \u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u043f\u0440\u0438\u043c\u0435\u0440\u0430 \u0435\u0433\u043e \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043c\u044b \u0440\u0435\u0448\u0438\u043b\u0438 \u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u043b\u044f \u0432\u0435\u0431\u0430 \u043c\u043d\u043e\u0433\u043e\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0443\u044e \u0438\u0433\u0440\u0443 \u0438 \u0432\u044b\u0431\u0440\u0430\u043b\u0438 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e Teeworlds. Teeworlds \u2014 \u044d\u0442\u043e \u043c\u043d\u043e\u0433\u043e\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0430\u044f \u0434\u0432\u0443\u0445\u043c\u0435\u0440\u043d\u0430\u044f \u0440\u0435\u0442\u0440\u043e-\u0438\u0433\u0440\u0430 [&hellip;]<\/p>\n","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"author":1,"featured_media":28801,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[688],"tags":[],"class_list":["post-38370","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-administrirovanie"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.3 - aioseo.com -->\n\t<meta name=\"description\" content=\"\u0412\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u041d\u0430\u0448\u0430 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u044f Leaning Technologies \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u043f\u043e \u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044e \u0442\u0440\u0430\u0434\u0438\u0446\u0438\u043e\u043d\u043d\u044b\u0445 desktop-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439 \u0432 \u0432\u0435\u0431.\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"Yuri Gagarin\"\/>\n\t<link rel=\"canonical\" href=\"https:\/\/prohoster.info\/da\/blog\/administrirovanie\/portiruem-mnogopolzovatelskuyu-igru-s-s-na-veb-c-cheerp-webrtc-i-firebase\" \/>\n\t\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.3\" \/>\n\t\t<meta property=\"og:locale\" content=\"da_DK\" \/>\n\t\t<meta property=\"og:site_name\" content=\"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"\ud83e\udd47\u041f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u043c \u043c\u043d\u043e\u0433\u043e\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0443\u044e \u0438\u0433\u0440\u0443 \u0441 \u0421++ \u043d\u0430 \u0432\u0435\u0431 c Cheerp, WebRTC \u0438 Firebase | ProHoster\" \/>\n\t\t<meta property=\"og:description\" content=\"\u0412\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u041d\u0430\u0448\u0430 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u044f Leaning Technologies \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u043f\u043e \u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044e \u0442\u0440\u0430\u0434\u0438\u0446\u0438\u043e\u043d\u043d\u044b\u0445 desktop-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439 \u0432 \u0432\u0435\u0431.\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/prohoster.info\/da\/blog\/administrirovanie\/portiruem-mnogopolzovatelskuyu-igru-s-s-na-veb-c-cheerp-webrtc-i-firebase\" \/>\n\t\t<meta property=\"og:image\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:secure_url\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:width\" content=\"350\" \/>\n\t\t<meta property=\"og:image:height\" content=\"350\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2019-10-31T19:23:18+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2019-10-31T19:23:18+00:00\" \/>\n\t\t<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<meta property=\"article:author\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"\ud83e\udd47Vi porterer et flerspiller-spil fra C++ til web med Cheerp, WebRTC og Firebase | ProHoster","description":"Introduktion Vores virksomhed Leaning Technologies tilbyder l\u00f8sninger til portering af traditionelle desktop-applikationer til web.","canonical_url":"https:\/\/prohoster.info\/da\/blog\/administrirovanie\/portiruem-mnogopolzovatelskuyu-igru-s-s-na-veb-c-cheerp-webrtc-i-firebase","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":null,"og:locale":"da_DK","og:site_name":"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b","og:type":"article","og:title":"\ud83e\udd47\u041f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u043c \u043c\u043d\u043e\u0433\u043e\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0443\u044e \u0438\u0433\u0440\u0443 \u0441 \u0421++ \u043d\u0430 \u0432\u0435\u0431 c Cheerp, WebRTC \u0438 Firebase | ProHoster","og:description":"\u0412\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u041d\u0430\u0448\u0430 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u044f Leaning Technologies \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0440\u0435\u0448\u0435\u043d\u0438\u044f \u043f\u043e \u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044e \u0442\u0440\u0430\u0434\u0438\u0446\u0438\u043e\u043d\u043d\u044b\u0445 desktop-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439 \u0432 \u0432\u0435\u0431.","og:url":"https:\/\/prohoster.info\/da\/blog\/administrirovanie\/portiruem-mnogopolzovatelskuyu-igru-s-s-na-veb-c-cheerp-webrtc-i-firebase","og:image":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:secure_url":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:width":350,"og:image:height":350,"article:published_time":"2019-10-31T19:23:18+00:00","article:modified_time":"2019-10-31T19:23:18+00:00","article:publisher":"https:\/\/www.facebook.com\/prohoster","article:author":"https:\/\/www.facebook.com\/prohoster"},"aioseo_meta_data":{"post_id":"38370","title":null,"description":null,"keywords":null,"keyphrases":null,"primary_term":null,"canonical_url":null,"og_title":null,"og_description":null,"og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":null,"og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"","isEnabled":true},"graphs":[]},"schema_type":null,"schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":null,"robots_max_videopreview":null,"robots_max_imagepreview":"large","priority":null,"frequency":null,"local_seo":null,"seo_analyzer_scan_date":"2026-01-23 21:44:19","breadcrumb_settings":null,"limit_modified_date":false,"reviewed_by":null,"ai":null,"created":"2021-03-01 01:10:24","updated":"2026-01-23 21:44:19","focus_keyword":null,"additional_keywords":null,"truseo_locale":null},"gt_translate_keys":[{"key":"link","format":"url"}],"_links":{"self":[{"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/posts\/38370","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/comments?post=38370"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/posts\/38370\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/media\/28801"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/media?parent=38370"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/categories?post=38370"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/da\/wp-json\/wp\/v2\/tags?post=38370"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}