{"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\/nl\/blog\/administrirovanie\/portiruem-mnogopolzovatelskuyu-igru-s-s-na-veb-c-cheerp-webrtc-i-firebase","title":{"rendered":"We porteren een multiplayer game van C++ naar web met Cheerp, WebRTC en Firebase","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<h2>Inleiding<\/h2>\n<p>\nOns bedrijf <noindex><a rel=\"nofollow\" href=\"https:\/\/leaningtech.com\">Leaning Technologies<\/a><\/noindex> biedt oplossingen voor het porteren van traditionele desktopapplicaties naar het web. Onze C++ compiler <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerp-meta\">Cheerp<\/a><\/noindex> genereert een combinatie van WebAssembly en JavaScript, wat zorgt voor <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerp-meta\/wiki\/Cheerp-Tutorial%3A-Mixed-mode-C++-to-WebAssembly-and-JavaScript\">eenvoudige interactie met de browser<\/a><\/noindex>, en hoge prestaties.<\/p>\n<p>Als voorbeeld van zijn toepassing hebben we besloten om een multiplayer game voor het web te porteren en hebben we gekozen voor <noindex><a rel=\"nofollow\" href=\"https:\/\/www.teeworlds.com\/\"><strong>Teeworlds<\/strong><\/a><\/noindex>. Teeworlds is een multiplayer retro 2D-spel met een kleine, maar actieve gemeenschap van spelers (inclusief ik!). Het is klein in termen van downloadgrootte en CPU- en GPU-eisen \u2014 een perfecte kandidaat.<\/p>\n<p><img decoding=\"async\" alt=\"We porteren een multiplayer game van C++ naar web met Cheerp, WebRTC en Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/30bff36ee1158ddea2e76d8b11e48d2b.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Teeworlds dat draait in de browser<\/i><br \/>\n<noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><br \/>\nWe hebben besloten om dit project te gebruiken om te experimenteren met <strong>algemene oplossingen voor het porteren van netwerkcode naar het web<\/strong>. Gewoonlijk gebeurt dit op de volgende manieren:<\/p>\n<ul>\n<li><strong>XMLHttpRequest\/fetch<\/strong>, als het netwerkonderdeel alleen uit HTTP-verzoeken bestaat, of<\/li>\n<li><strong>WebSockets<\/strong>.<\/li>\n<\/ul>\n<p>\nBeide oplossingen vereisen het hosten van een servercomponent aan de serverzijde, en geen van beiden laat gebruik maken van <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/User_Datagram_Protocol\">UDP<\/a><\/noindex>. Dit is belangrijk voor real-time applicaties, zoals videoconferentiesoftware en games, omdat de garanties voor levering en volgorde van pakketten van het protocol <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Transmission_Control_Protocol\">TCP<\/a><\/noindex> een belemmering kunnen vormen voor lage latentie.<\/p>\n<p>Er is ook een derde manier \u2014 gebruik maken van de netwerkfunctionaliteit van de browser: <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> ondersteunt zowel betrouwbare als onbetrouwbare transmissie (in het laatste geval probeert het indien mogelijk UDP als transportprotocol te gebruiken), en kan zowel met een externe server als tussen browsers worden gebruikt. <strong>Dit betekent dat we de hele applicatie, inclusief de servercomponent, naar de browser kunnen porteren!<\/strong><\/p>\n<p>Echter, dit brengt een extra uitdaging met zich mee: voordat twee WebRTC peers gegevens kunnen uitwisselen, moeten ze een relatief complexe 'handshake'-procedure uitvoeren om te verbinden, waarvoor verschillende externe entiteiten nodig zijn (signaalserver en een of meer <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>Idealiter willen we een netwerklast API cre\u00ebren die WebRTC intern gebruikt, maar zo dicht mogelijk bij de interface van UDP Sockets staat, waarvoor geen verbinding hoeft te worden gemaakt.<\/p>\n<p>Dit stelt ons in staat om de voordelen van WebRTC te benutten zonder de complexe details van de applicatiecode (die we in ons project zo min mogelijk wilden wijzigen) openbaar te maken.<\/p>\n<h1>Minimale WebRTC<\/h1>\n<p>\nWebRTC is een set API's die beschikbaar zijn in browsers en die peer-to-peer overdracht van audio, video en willekeurige gegevens mogelijk maakt.<\/p>\n<p>De verbinding tussen de peers wordt tot stand gebracht (zelfs als er NAT aan \u00e9\u00e9n of beide zijden aanwezig is) met behulp van STUN en\/of TURN-servers via een mechanisme genaamd ICE. Peers wisselen ICE-informatie en kanaalparameters uit via het offer- en antwoordprotocol van SDP.<\/p>\n<p>Wauw! Wat een hoop afkortingen in \u00e9\u00e9n keer. Laten we kort uitleggen wat deze termen betekenen:<\/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> is een protocol voor het omzeilen van NAT en het verkrijgen van een paar (IP, poort) voor directe gegevensuitwisseling met de host. Als het erin slaagt zijn doel te bereiken, kunnen peers direct gegevens met elkaar uitwisselen.<\/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> wordt ook gebruikt om NAT te omzeilen, maar doet dit door gegevens via een proxy om te leiden die voor beide peers zichtbaar is. Dit voegt vertraging toe en is kostbaarder in uitvoering dan STUN (omdat het tijdens de hele communicatiesessie wordt toegepast), maar soms is dit de enige mogelijkheid.<\/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> wordt gebruikt om de best mogelijke manier te kiezen om twee peers te verbinden op basis van de informatie die is verkregen via de directe verbinding tussen de peers, evenals informatie verkregen van een of meer STUN- en TURN-servers.<\/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> is een formaat voor het beschrijven van de parameters van de verbindingskanaal, zoals ICE-kandidaten, multimedia codecs (in het geval van audio\/video), enz. Een van de peers stuurt een SDP Offer ('aanbod'), en de andere reageert met een SDP Answer ('antwoord'). Hierna wordt het kanaal aangemaakt.<\/li>\n<\/ul>\n<p>\nOm zo'n verbinding te maken, moeten de peers de informatie verzameld van de STUN- en TURN-servers en deze uitwisselen.<\/p>\n<p>Het probleem is dat ze nog geen directe gegevensuitwisseling kunnen doen, dus er moet een off-band mechanisme zijn: een signaalserver.<\/p>\n<p>Een signaalserver kan heel eenvoudig zijn, omdat zijn enige taak het doorsturen van gegevens tussen de peers in de 'handshake'-fase is (zoals weergegeven in het onderstaande schema).<\/p>\n<p><img decoding=\"async\" alt=\"We porteren een multiplayer game van C++ naar web met Cheerp, WebRTC en Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/0fc39ba5e0942bc19183e8cf06c60868.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Vereenvoudigd schema van de WebRTC 'handdruk'-volgorde<\/i><\/p>\n<h1>Overzicht van het netwerkmodel van Teeworlds<\/h1>\n<p>\nDe netwerkarchitectuur van Teeworlds is zeer eenvoudig:<\/p>\n<ul>\n<li>De client- en servercomponenten zijn twee verschillende programma's.<\/li>\n<li>Klanten nemen deel aan het spel door verbinding te maken met een van de verschillende servers, elk van die slechts \u00e9\u00e9n spel tegelijk host.<\/li>\n<li>Alle gegevensoverdracht in het spel gebeurt via de server.<\/li>\n<li>Een speciale masterserver wordt gebruikt om een lijst met alle openbare servers te verzamelen, die in de spelclient worden weergegeven.<\/li>\n<\/ul>\n<p>\nDoor WebRTC te gebruiken voor gegevensuitwisseling, kunnen we de servercomponent van het spel naar de browser verplaatsen, waar de client zich bevindt. Dit biedt ons een prachtige mogelijkheid...<\/p>\n<h1>Af te zien van servers<\/h1>\n<p>\nHet ontbreken van serverlogica heeft het mooie voordeel dat we de gehele applicatie als statische inhoud op Github Pages of op onze eigen hardware achter Cloudflare kunnen implementeren, waardoor we ons gratis snelle laadtijden en hoge uptime kunnen verzekeren. In wezen kunnen we ze vergeten, en als we geluk hebben en het spel populair wordt, hoeven we de infrastructuur niet te upgraden.<\/p>\n<p>Echter, om het systeem te laten werken, moeten we nog steeds externe architectuur gebruiken:<\/p>\n<ul>\n<li>Een of meerdere STUN-servers: we hebben de keuze uit verschillende gratis opties.<\/li>\n<li>Minimaal \u00e9\u00e9n TURN-server: er zijn geen gratis opties, dus we kunnen ofwel onze eigen instellen of betalen voor een dienst. Gelukkig kan de verbinding meestal via STUN-servers worden ingesteld (en echte p2p bieden), maar TURN is nodig als back-up.<\/li>\n<li>Signaalserver: in tegenstelling tot de andere twee aspecten is signalering niet gestandaardiseerd. Wat de signaalserver daadwerkelijk zal doen, hangt in zekere zin af van de applicatie. In ons geval moet er voor het tot stand brengen van een verbinding een kleine hoeveelheid gegevens worden uitgewisseld.<\/li>\n<li>Masterserver van Teeworlds: deze wordt door andere servers gebruikt om hun bestaan aan te geven en door clients om openbare servers te vinden. Hoewel het niet verplicht is (clients kunnen altijd handmatig verbinding maken met een bekende server), is het goed om het te hebben zodat spelers kunnen deelnemen aan spellen met willekeurige mensen.<\/li>\n<\/ul>\n<p>\nWe hebben ervoor gekozen om de gratis STUN-servers van Google te gebruiken, en we hebben zelf \u00e9\u00e9n TURN-server opgezet.<\/p>\n<p>Voor de laatste twee punten hebben we gebruikt <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Firebase\">Firebase<\/a><\/noindex>:<\/p>\n<ul>\n<li>De masterserver van Teeworlds is heel eenvoudig ge\u00efmplementeerd: als een lijst met objecten die informatie (naam, IP, kaart, modus, ...) van elke actieve server bevatten. Servers publiceren en updaten hun eigen object, terwijl clients de hele lijst ophalen en deze aan de speler tonen. We tonen ook de lijst op de startpagina als HTML, zodat spelers gewoon op de server kunnen klikken en direct in het spel kunnen komen.<\/li>\n<li>Signalisatie is nauw verbonden met onze implementatie van sockets, die in het volgende gedeelte wordt beschreven.<\/li>\n<\/ul>\n<p>\n<img decoding=\"async\" alt=\"We porteren een multiplayer game van C++ naar web met Cheerp, WebRTC en Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/5e30bc22643f8baecf850053bf69e130.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>De lijst met servers binnen het spel en op de startpagina<\/i><\/p>\n<h1>Implementatie van sockets<\/h1>\n<p>\nWe willen een API cre\u00ebren die zo dicht mogelijk bij Posix UDP Sockets ligt, om het aantal noodzakelijke wijzigingen te minimaliseren.<\/p>\n<p>Daarnaast willen we het noodzakelijke minimum implementeren dat vereist is voor de eenvoudigste gegevensuitwisseling via het netwerk.<\/p>\n<p>Bijvoorbeeld, we hebben geen echte routering nodig: alle peers bevinden zich in \u00e9\u00e9n 'virtueel LAN', gekoppeld aan een specifieke instantie van de Firebase-database.<\/p>\n<p>Daarom hebben we geen unieke IP-adressen nodig: voor unieke identificatie van peers is het voldoende om unieke waarden van de Firebase-sleutels te gebruiken (vergelijkbaar met domeinnamen), en elke peer kent lokaal 'nep' IP-adressen toe aan elke sleutel die moet worden omgezet. Dit elimineert volledig de noodzaak voor globale toewijzing van IP-adressen, wat geen triviale taak is.<\/p>\n<p>Hier is de minimale API die we moeten implementeren:<\/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>\nDe API is eenvoudig en lijkt op de API van Posix Sockets, maar heeft een aantal belangrijke verschillen: <strong>registratie van callback-functies, toewijzing van lokale IP's en 'lui' verbindingen<\/strong>.<\/p>\n<h2>Registratie van callback-functies<\/h2>\n<p>\nZelfs als de oorspronkelijke programma gebruik maakt van niet-blokkerende invoer\/uitvoer, moet de code worden gerefactoreerd voor gebruik in een webbrowser.<\/p>\n<p>De reden hiervoor is dat de event loop in de browser verborgen is voor het programma (of het nu JavaScript of WebAssembly is).<\/p>\n<p>In een native omgeving kunnen we de code op deze manier schrijven<\/p>\n<pre><code class=\"cpp\">while(running) {\n  select(...); \/\/ wacht op I\/O gebeurtenissen\n  while(true) {\n    int r = readfrom(...); \/\/ probeer te lezen\n    if (r &lt; 0 &amp;&amp; errno == EWOULDBLOCK) \/\/ geen gegevens meer beschikbaar\n      break;\n    ...\n  }\n  ...\n}<\/code><\/pre>\n<p>\nAls de event loop voor ons verborgen is, moeten we deze omzetten in iets dat hierop lijkt:<\/p>\n<pre><code class=\"cpp\">auto cb = []() { \/\/ dit zal worden aangeroepen wanneer nieuwe gegevens beschikbaar zijn\n  while(true) {\n    int r = readfrom(...); \/\/ probeer te lezen\n    if (r &lt; 0 &amp;&amp; errno == EWOULDBLOCK) \/\/ geen gegevens meer beschikbaar\n      break;\n    ...\n  }\n  ...\n};\nrecvCallback(cb); \/\/ registreer de callback<\/code><\/pre>\n<p><\/p>\n<h2>Toepassing van lokale IP's<\/h2>\n<p>\nDe knooppunten in ons 'netwerk' zijn geen IP-adressen, maar Firebase-sleutels (dit zijn strings die er als volgt uitzien: <code>-LmEC50PYZLCiCP-vqde<\/code> ).<\/p>\n<p>Dit is handig omdat we geen mechanisme nodig hebben voor het toewijzen van IP's en de controle van hun uniciteit (en hun verwijdering na het uitschakelen van de klant), maar het is vaak nodig om peers te identificeren op basis van numerieke waarde.<\/p>\n<p>Daarom worden de functies <code>resolve<\/code> en <code>reverseResolve<\/code>: de applicatie krijgt op de een of andere manier de stringwaarde van de sleutel (door invoer van de gebruiker of via de master-server) en kan deze omzetten in een IP-adres voor intern gebruik. De rest van de API ontvangt ook deze waarde in plaats van de string voor de eenvoud.<\/p>\n<p>Dit lijkt op een DNS-zoekopdracht, alleen wordt deze lokaal bij de klant uitgevoerd.<\/p>\n<p>Dat betekent dat IP-adressen niet gedeeld kunnen worden tussen verschillende klanten, en als er een globale identifier nodig is, deze op een andere manier gegenereerd moet worden.<\/p>\n<h2>Lui verbinding maken<\/h2>\n<p>\nUDP vereist geen verbinding, maar zoals we eerder hebben gezien, vereist WebRTC een lang verbindingsproces voordat het dataverkeer tussen twee peers kan beginnen.<\/p>\n<p>Als we hetzelfde abstractieniveau willen garanderen, (<code>sendto<\/code>\/<code>recvfrom<\/code> met willekeurige peers zonder voorafgaande verbinding), dan moeten we 'lui' (uitgestelde) verbinding maken binnen de API.<\/p>\n<p>Dit is wat er gebeurt bij normale gegevensuitwisseling tussen een 'server' en 'client' bij gebruik van UDP, en wat onze bibliotheek zou moeten uitvoeren:<\/p>\n<ul>\n<li>De server roept <code>bind()<\/code>, om het besturingssysteem te informeren dat het pakketten op de opgegeven poort wil ontvangen.<\/li>\n<\/ul>\n<p>\nIn plaats daarvan publiceren we een open poort in Firebase onder de serversleutel en luisteren we naar gebeurtenissen in zijn subtree.<\/p>\n<ul>\n<li>De server roept <code>recvfrom()<\/code>, waarmee we pakketten vanaf elke host naar deze poort accepteren.<\/li>\n<\/ul>\n<p>\nIn ons geval moeten we de inkomende wachtrij van pakketten controleren die naar deze poort zijn gestuurd.<\/p>\n<p>Elke poort heeft zijn eigen wachtrij, en we voegen de oorspronkelijke en eindpoorten van de WebRTC-datagrammen aan het begin toe, zodat we weten in welke wachtrij een nieuw pakket moet worden omgeleid wanneer het binnenkomt.<\/p>\n<p>De aanroep is niet-blokkerend, dus als er geen pakketten zijn, retourneren we gewoon -1 en stellen we <code>errno=EWOULDBLOCK<\/code>.<\/p>\n<ul>\n<li>De klant verkrijgt op een of andere externe manier het IP en de poort van de server en roept <code>sendto()<\/code>. Ook wordt bij dit proces een interne aanroep gedaan, zodat de daaropvolgende <code>bind()<\/code>een antwoord ontvangt zonder expliciet een bind uit te voeren. <code>recvfrom()<\/code> ontvangen een antwoord zonder expliciete uitvoering van bind.<\/li>\n<\/ul>\n<p>\nIn ons geval ontvangt de klant extern een string sleutel en gebruikt de functie <code>resolve()<\/code> om het IP-adres te verkrijgen.<\/p>\n<p>In deze fase beginnen we met de \"handshake\" van WebRTC, als de twee peers nog niet met elkaar verbonden zijn. Verbindingen op verschillende poorten van \u00e9\u00e9n peer gebruiken dezelfde WebRTC DataChannel.<\/p>\n<p>Ook voeren we een indirecte <code>bind()<\/code>, zodat de server de verbinding in de volgende <code>sendto()<\/code> kan herstellen, indien deze om welke reden dan ook is verbroken.<\/p>\n<p>De server wordt op de hoogte gesteld van de verbinding van de klant wanneer de klant zijn SDP-aanbod onder de serverpoortinformatie in Firebase opslaat, en de server daar ook op reageert met zijn antwoord.<\/p>\n<p>\nIn de onderstaande afbeelding wordt een voorbeeld van de berichtbewegingen voor de socketschema en de overdracht van het eerste bericht van de klant naar de server weergegeven:<\/p>\n<p><img decoding=\"async\" alt=\"We porteren een multiplayer game van C++ naar web met Cheerp, WebRTC en Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/7dc48e816c09e33daf28c307cad0c258.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Het volledige schema van de verbindingsfase tussen de klant en de server<\/i><\/p>\n<h1>Conclusie<\/h1>\n<p>\nAls je tot het einde hebt gelezen, ben je waarschijnlijk benieuwd om de theorie in actie te zien. Je kunt het spel spelen op <noindex><a rel=\"nofollow\" href=\"https:\/\/teeworlds.leaningtech.com\">teeworlds.leaningtech.com<\/a><\/noindex>, probeer het eens!<\/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>Een vriendschappelijke wedstrijd tussen collega\u2019s<\/i><\/p>\n<p>De code van de netwerkbibliotheek is vrij beschikbaar op <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerpnet\">Github<\/a><\/noindex>. Neem deel aan het gesprek op ons kanaal op <noindex><a rel=\"nofollow\" href=\"https:\/\/gitter.im\/leaningtech\/cheerp\">Gitter<\/a><\/noindex>!<br \/>\n<br \/>Bron: <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\/nl\/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=\"nl_NL\" \/>\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\/nl\/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\udd47We porteren een multiplayer game van C++ naar web met Cheerp, WebRTC en Firebase | ProHoster","description":"Inleiding Ons bedrijf Leaning Technologies biedt oplossingen voor het porteren van traditionele desktopapplicaties naar het web.","canonical_url":"https:\/\/prohoster.info\/nl\/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":"nl_NL","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\/nl\/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\/nl\/wp-json\/wp\/v2\/posts\/38370","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/comments?post=38370"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/posts\/38370\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/media\/28801"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/media?parent=38370"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/categories?post=38370"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/nl\/wp-json\/wp\/v2\/tags?post=38370"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}