{"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\/it\/blog\/administrirovanie\/portiruem-mnogopolzovatelskuyu-igru-s-s-na-veb-c-cheerp-webrtc-i-firebase","title":{"rendered":"Portiamo un gioco multiplayer da C++ al web con Cheerp, WebRTC e Firebase","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<h2>Introduzione<\/h2>\n<p>\nLa nostra azienda <noindex><a rel=\"nofollow\" href=\"https:\/\/leaningtech.com\">Leaning Technologies<\/a><\/noindex> offre soluzioni per la portabilit\u00e0 delle applicazioni desktop tradizionali sul web. Il nostro compilatore C++ <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerp-meta\">Cheerp<\/a><\/noindex> genera una combinazione di WebAssembly e JavaScript, che assicura sia <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerp-meta\/wiki\/Cheerp-Tutorial%3A-Mixed-mode-C++-to-WebAssembly-and-JavaScript\">una semplice interazione con il browser<\/a><\/noindex>, sia alte prestazioni.<\/p>\n<p>Come esempio del suo utilizzo, abbiamo deciso di portare sul web un gioco multiplayer e abbiamo scelto per questo <noindex><a rel=\"nofollow\" href=\"https:\/\/www.teeworlds.com\/\"><strong>Teeworlds<\/strong><\/a><\/noindex>. Teeworlds \u00e8 un gioco retro 2D multiplayer con una comunit\u00e0 di giocatori piccola ma attiva (tra cui io!). \u00c8 leggero sia in termini di risorse scaricabili che di requisiti per la CPU e la GPU, rendendolo un candidato ideale.<\/p>\n<p><img decoding=\"async\" alt=\"Portiamo un gioco multiplayer da C++ al web con Cheerp, WebRTC e Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/30bff36ee1158ddea2e76d8b11e48d2b.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Teeworlds funzionante nel browser<\/i><br \/>\n<noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><br \/>\nAbbiamo deciso di utilizzare questo progetto per sperimentare con <strong>soluzioni generali per la portabilit\u00e0 del codice di rete sul web<\/strong>. Di solito, ci\u00f2 avviene nei seguenti modi:<\/p>\n<ul>\n<li><strong>XMLHttpRequest\/fetch<\/strong>, se la parte di rete consiste unicamente in richieste HTTP, oppure<\/li>\n<li><strong>WebSockets<\/strong>.<\/li>\n<\/ul>\n<p>\nEntrambe le soluzioni richiedono di ospitare un componente server sul lato server, e nessuna delle due consente di utilizzare come protocollo di trasporto <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/User_Datagram_Protocol\">UDP<\/a><\/noindex>. Questo \u00e8 importante per le applicazioni in tempo reale, come il software per videoconferenze e i giochi, perch\u00e9 le garanzie di consegna e ordine dei pacchetti del protocollo <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Transmission_Control_Protocol\">TCP<\/a><\/noindex> possono interferire con le basse latenze.<\/p>\n<p>Esiste anche una terza via: utilizzare la rete dal 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> supporta sia la trasmissione affidabile che quella non affidabile (in quest'ultimo caso tenta di utilizzare come protocollo di trasporto l'UDP), e pu\u00f2 essere utilizzato sia con un server remoto che tra browser. <strong>Questo significa che possiamo portare nel browser l'intera applicazione, inclusi il componente server!<\/strong><\/p>\n<p>Tuttavia, ci\u00f2 comporta una difficolt\u00e0 aggiuntiva: prima che due peer WebRTC possano scambiarsi dati, devono eseguire una procedura relativamente complessa di \"handshake\" per connettersi, per la quale \u00e8 necessario coinvolgere diverse entit\u00e0 esterne (server di segnalazione e uno o pi\u00f9 server <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>In ideale, vorremmo creare un'API di rete che utilizzi WebRTC internamente, ma quanto pi\u00f9 possibile simile all'interfaccia dei socket UDP, che non richiede l'instaurazione di una connessione.<\/p>\n<p>Questo ci permetter\u00e0 di sfruttare i vantaggi di WebRTC senza dover rivelare dettagli complessi nel codice dell'applicazione (che nel nostro progetto volevamo modificare il meno possibile).<\/p>\n<h1>WebRTC Minimo<\/h1>\n<p>\nWebRTC \u00e8 un insieme di API disponibili nei browser che consente il trasferimento di audio, video e dati arbitrari peer-to-peer.<\/p>\n<p>La connessione tra i peer viene stabilita (anche nel caso di NAT da una o entrambe le parti) tramite server STUN e\/o TURN attraverso un meccanismo chiamato ICE. I peer scambiano informazioni ICE e parametri dei canali tramite l'offerta e la risposta del protocollo SDP.<\/p>\n<p>Wow! Quante abbreviazioni in una volta. Spieghiamo brevemente cosa significano questi concetti:<\/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> \u00e8 un protocollo per bypassare il NAT e ottenere una coppia (IP, porta) per scambiare dati direttamente con l'host. Se riesce a fare il suo lavoro, i peer possono scambiarsi dati tra di loro.<\/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> viene utilizzato anch'esso per bypassare il NAT, ma lo fa reindirizzando i dati attraverso un proxy visibile a entrambi i peer. Aggiunge latenza e richiede pi\u00f9 risorse rispetto a STUN (perch\u00e9 viene applicato durante l'intera sessione di comunicazione), ma a volte \u00e8 l'unica opzione possibile.<\/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> viene utilizzato per scegliere il modo migliore per connettere i due peer sulla base delle informazioni ottenute durante la connessione diretta tra i peer e delle informazioni ricevute da un numero qualsiasi di server STUN e TURN.<\/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> \u00e8 un formato per descrivere i parametri del canale di connessione, ad esempio i candidati ICE, i codec multimediali (nel caso di canali audio\/video), ecc... Uno dei peer invia un SDP Offer (\u00abofferta\u00bb), e l'altro risponde con un SDP Answer (\u00abrisposta\u00bb). Dopo di che viene creato il canale.<\/li>\n<\/ul>\n<p>\nPer creare una tale connessione, i peer devono raccogliere le informazioni ricevute dai server STUN e TURN e scambiarsi queste informazioni.<\/p>\n<p>Il problema \u00e8 che al momento non hanno la possibilit\u00e0 di scambiarsi dati direttamente, quindi deve esistere un meccanismo di signaling esterno: il server di signaling.<\/p>\n<p>Il server di signaling pu\u00f2 essere molto semplice, perch\u00e9 il suo unico compito \u00e8 reindirizzare i dati tra i peer nella fase di \u00abhandshake\u00bb (come mostrato nello schema sottostante).<\/p>\n<p><img decoding=\"async\" alt=\"Portiamo un gioco multiplayer da C++ al web con Cheerp, WebRTC e Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/0fc39ba5e0942bc19183e8cf06c60868.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Schema semplificato della sequenza di \u00abhandshake\u00bb WebRTC<\/i><\/p>\n<h1>Panoramica del modello di rete Teeworlds<\/h1>\n<p>\nL'architettura di rete di Teeworlds \u00e8 molto semplice:<\/p>\n<ul>\n<li>I componenti client e server sono due programmi distinti.<\/li>\n<li>I clienti entrano nel gioco connettendosi a uno dei diversi server, ognuno dei quali ospita solo una partita alla volta.<\/li>\n<li>Tutta la trasmissione dei dati nel gioco avviene tramite il server.<\/li>\n<li>Un master server speciale \u00e8 utilizzato per raccogliere l'elenco di tutti i server pubblici, che vengono visualizzati nel client di gioco.<\/li>\n<\/ul>\n<p>\nGrazie all'uso di WebRTC per lo scambio di dati, possiamo spostare il componente server del gioco nel browser, dove si trova il client. Questo ci offre un'opportunit\u00e0 fantastica\u2026<\/p>\n<h1>Eliminare i server<\/h1>\n<p>\nL'assenza di logica server ha un piacevole vantaggio: possiamo distribuire l'intera applicazione come contenuto statico su Github Pages o su hardware nostro dietro Cloudflare, garantendo cos\u00ec caricamenti veloci e un elevato uptime gratuitamente. In sostanza, sar\u00e0 possibile dimenticarsene e, se saremo fortunati e il gioco diventer\u00e0 popolare, non dovremo modernizzare l'infrastruttura.<\/p>\n<p>Tuttavia, affinch\u00e9 il sistema funzioni, dovremo comunque utilizzare un'architettura esterna:<\/p>\n<ul>\n<li>Uno o pi\u00f9 server STUN: abbiamo a disposizione diverse opzioni gratuite.<\/li>\n<li>Almeno un server TURN: qui non ci sono opzioni gratuite, quindi possiamo impostare il nostro server o pagare un servizio. Fortunatamente, la maggior parte delle volte la connessione pu\u00f2 essere stabilita tramite server STUN (e garantire un vero p2p), ma TURN \u00e8 necessario come opzione di riserva.<\/li>\n<li>Server di segnalazione: a differenza degli altri due aspetti, la segnalazione non \u00e8 standardizzata. Ci\u00f2 di cui si occuper\u00e0 effettivamente il server di segnalazione dipende in parte dall'applicazione. Nel nostro caso, prima di stabilire una connessione \u00e8 necessario scambiare una piccola quantit\u00e0 di dati.<\/li>\n<li>Master server di Teeworlds: viene utilizzato da altri server per notificare la propria esistenza e dai client per cercare server pubblici. Anche se non \u00e8 obbligatorio (i client possono sempre connettersi manualmente a un server che conoscono), sarebbe utile averlo cos\u00ec i giocatori possono partecipare a partite con persone casuali.<\/li>\n<\/ul>\n<p>\nAbbiamo deciso di utilizzare i server STUN gratuiti di Google, mentre un server TURN l'abbiamo installato noi stessi.<\/p>\n<p>Per i due ultimi punti abbiamo utilizzato <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Firebase\">Firebase<\/a><\/noindex>:<\/p>\n<ul>\n<li>Il master server di Teeworlds \u00e8 implementato in modo molto semplice: come un elenco di oggetti che contengono informazioni (nome, IP, mappa, modalit\u00e0, ...) di ciascun server attivo. I server pubblicano e aggiornano il proprio oggetto, mentre i client prendono l'intero elenco e lo mostrano al giocatore. Inoltre, mostriamo l'elenco sulla homepage come HTML, in modo che i giocatori possano semplicemente cliccare sul server e accedere subito al gioco.<\/li>\n<li>La segnalazione \u00e8 strettamente correlata alla nostra implementazione dei socket, descritta nella sezione successiva.<\/li>\n<\/ul>\n<p>\n<img decoding=\"async\" alt=\"Portiamo un gioco multiplayer da C++ al web con Cheerp, WebRTC e Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/5e30bc22643f8baecf850053bf69e130.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Elenco dei server all'interno del gioco e sulla homepage<\/i><\/p>\n<h1>Implementazione dei socket<\/h1>\n<p>\nVogliamo creare un'API il pi\u00f9 simile possibile ai socket UDP Posix, per ridurre al minimo il numero di modifiche necessarie.<\/p>\n<p>Vogliamo anche implementare il minimo indispensabile necessario per il pi\u00f9 semplice scambio di dati in rete.<\/p>\n<p>Ad esempio, non abbiamo bisogno di una vera e propria instradamento: tutti i peer sono su una 'LAN virtuale' collegata a un'istanza specifica del database Firebase.<\/p>\n<p>Pertanto, non abbiamo bisogno di indirizzi IP unici: per l'identificazione univoca dei peer \u00e8 sufficiente utilizzare valori di chiave Firebase univoci (simile ai nomi di dominio), e ogni peer assegna localmente indirizzi IP 'falsi' a ciascuna chiave che deve essere trasformata. Questo ci libera completamente dalla necessit\u00e0 di assegnare indirizzi IP globali, il che \u00e8 un compito non banale.<\/p>\n<p>Ecco l'API minima che dobbiamo implementare:<\/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>\nL'API \u00e8 semplice e simile all'API dei socket Posix, ma presenta alcune differenze importanti: <strong>registrazione dei callback, assegnazione di IP locali e connessione 'pigra'<\/strong>.<\/p>\n<h2>Registrazione dei callback<\/h2>\n<p>\nAnche se il programma originale utilizza I\/O non bloccante, per l'esecuzione nel browser web il codice deve essere rifattorizzato.<\/p>\n<p>La ragione \u00e8 che il ciclo degli eventi nel browser \u00e8 nascosto al programma (sia esso JavaScript o WebAssembly).<\/p>\n<p>In ambiente nativo possiamo scrivere il codice in questo modo<\/p>\n<pre><code class=\"cpp\">while(running) {\n  select(...); \/\/ aspetta eventi I\/O\n  while(true) {\n    int r = readfrom(...); \/\/ prova a leggere\n    if (r &lt; 0 &amp;&amp; errno == EWOULDBLOCK) \/\/ non ci sono pi\u00f9 dati disponibili\n      break;\n    ...\n  }\n  ...\n}<\/code><\/pre>\n<p>\nSe il ciclo degli eventi \u00e8 nascosto, allora bisogna trasformarlo in qualcosa del genere:<\/p>\n<pre><code class=\"cpp\">auto cb = []() { \/\/ questo verr\u00e0 chiamato quando sono disponibili nuovi dati\n  while(true) {\n    int r = readfrom(...); \/\/ prova a leggere\n    if (r &lt; 0 &amp;&amp; errno == EWOULDBLOCK) \/\/ non ci sono pi\u00f9 dati disponibili\n      break;\n    ...\n  }\n  ...\n};\nrecvCallback(cb); \/\/ registra il callback<\/code><\/pre>\n<p><\/p>\n<h2>Assegnazione di IP locali<\/h2>\n<p>\nGli identificatori dei nodi nella nostra \u00abrete\u00bb non sono indirizzi IP, ma chiavi Firebase (queste sono stringhe che sembrano cos\u00ec: <code>-LmEC50PYZLCiCP-vqde<\/code> ).<\/p>\n<p>\u00c8 comodo perch\u00e9 non abbiamo bisogno di un meccanismo per assegnare IP e verificarne l'unicit\u00e0 (e anche il loro smaltimento dopo la disconnessione del client), ma spesso \u00e8 necessario identificare i peer in base a un valore numerico.<\/p>\n<p>Questo \u00e8 esattamente ci\u00f2 per cui vengono utilizzate le funzioni <code>resolve<\/code> e <code>reverseResolve<\/code>: l'applicazione in qualche modo ottiene il valore stringa della chiave (tramite l'input dell'utente o un server master), e pu\u00f2 convertirlo in un indirizzo IP per utilizzo interno. Anche il resto dell'API per semplicit\u00e0 riceve invece questo valore al posto della stringa.<\/p>\n<p>Questo \u00e8 simile a una ricerca DNS, ma avviene localmente sul client.<\/p>\n<p>Cio\u00e8, gli indirizzi IP non possono essere condivisi tra diversi client, e se \u00e8 necessario un qualche identificatore globale, dovr\u00e0 essere generato in un altro modo.<\/p>\n<h2>Connessione pigra<\/h2>\n<p>\nUDP non richiede una connessione, ma, come abbiamo visto, prima di iniziare a trasmettere dati tra due peer, WebRTC richiede un lungo processo di connessione.<\/p>\n<p>Se vogliamo fornire lo stesso livello di astrazione, (<code>sendto<\/code>\/<code>recvfrom<\/code> a peer qualsiasi senza una connessione preliminare), dobbiamo eseguire una connessione \u00abpigra\u00bb (ritardata) all'interno dell'API.<\/p>\n<p>Ecco cosa accade durante uno scambio normale di dati tra un \u00abserver\u00bb e un \u00abclient\u00bb nel caso di utilizzo di UDP, e cosa deve fare la nostra libreria:<\/p>\n<ul>\n<li>Il server chiama <code>bind()<\/code>, per informare il sistema operativo che desidera ricevere pacchetti sulla porta specificata.<\/li>\n<\/ul>\n<p>\nInvece, pubblicheremo una porta aperta in Firebase sotto la chiave del server e ascolteremo gli eventi nel suo sottoalbero.<\/p>\n<ul>\n<li>Il server chiama <code>recvfrom()<\/code>, accettando su questa porta i pacchetti in arrivo da qualsiasi host.<\/li>\n<\/ul>\n<p>\nNel nostro caso, \u00e8 necessario controllare la coda in ingresso dei pacchetti inviati a questa porta.<\/p>\n<p>Ogni porta ha la sua coda, e aggiungiamo all'inizio dei datagrammi WebRTC le porte sorgente e destinazione per sapere in quale coda reindirizzare quando arriva un nuovo pacchetto.<\/p>\n<p>La chiamata \u00e8 non bloccante, quindi se non ci sono pacchetti, restituiamo semplicemente -1 e impostiamo <code>errno=EWOULDBLOCK<\/code>.<\/p>\n<ul>\n<li>Il client ottiene tramite mezzi esterni l'IP e la porta del server e chiama <code>sendto()<\/code>. Viene eseguita anche una chiamata interna, quindi il successivo <code>bind()<\/code>ricever\u00e0 una risposta senza necessit\u00e0 di eseguire esplicitamente il bind. <code>recvfrom()<\/code> ricever\u00e0 una risposta senza un'esecuzione esplicita del bind.<\/li>\n<\/ul>\n<p>\nNel nostro caso, il cliente riceve esternamente una chiave in formato stringa e utilizza la funzione <code>resolve()<\/code> per ottenere l'indirizzo IP.<\/p>\n<p>A questo punto iniziamo il \"handshake\" di WebRTC, se i due peer non sono gi\u00e0 connessi tra loro. Le connessioni a porte diverse di un singolo peer utilizzano lo stesso DataChannel WebRTC.<\/p>\n<p>Inoltre, eseguiamo un'indiretta <code>bind()<\/code>, in modo che il server possa ripristinare la connessione nel successivo <code>sendto()<\/code> nel caso in cui essa si chiuda per qualche motivo.<\/p>\n<p>Il server viene informato della connessione del cliente quando il cliente registra la sua offerta SDP sotto le informazioni sulla porta del server in Firebase, e il server risponde anch'esso con la sua risposta.<\/p>\n<p>\nNello schema mostrato qui sotto \u00e8 illustrato un esempio del flusso dei messaggi per lo schema dei socket e la trasmissione del primo messaggio dal cliente al server:<\/p>\n<p><img decoding=\"async\" alt=\"Portiamo un gioco multiplayer da C++ al web con Cheerp, WebRTC e Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/7dc48e816c09e33daf28c307cad0c258.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Schema completo della fase di connessione tra cliente e server<\/i><\/p>\n<h1>Conclusione<\/h1>\n<p>\nSe sei arrivato fino a qui, probabilmente ti interesser\u00e0 vedere la teoria in azione. Puoi giocare a <noindex><a rel=\"nofollow\" href=\"https:\/\/teeworlds.leaningtech.com\">teeworlds.leaningtech.com<\/a><\/noindex>, prova!<\/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>Partita amichevole tra colleghi<\/i><\/p>\n<p>Il codice della libreria di rete \u00e8 liberamente disponibile su <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerpnet\">Github<\/a><\/noindex>. Unisciti alla nostra comunit\u00e0 nel nostro canale di <noindex><a rel=\"nofollow\" href=\"https:\/\/gitter.im\/leaningtech\/cheerp\">Gitter<\/a><\/noindex>!<br \/>\n<br \/>Fonte: <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.1.1 - 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\/it\/blog\/administrirovanie\/portiruem-mnogopolzovatelskuyu-igru-s-s-na-veb-c-cheerp-webrtc-i-firebase\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.1.1\" \/>\n\t\t<meta property=\"og:locale\" content=\"it_IT\" \/>\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\/it\/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\udd47Portiamo un gioco multiplayer da C++ al web con Cheerp, WebRTC e Firebase | ProHoster","description":"Introduzione La nostra azienda Leaning Technologies fornisce soluzioni per il porting di tradizionali applicazioni desktop nel web.","canonical_url":"https:\/\/prohoster.info\/it\/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":"it_IT","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\/it\/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\/it\/wp-json\/wp\/v2\/posts\/38370","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/comments?post=38370"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/posts\/38370\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/media\/28801"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/media?parent=38370"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/categories?post=38370"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/tags?post=38370"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}