{"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\/es\/blog\/administrirovanie\/portiruem-mnogopolzovatelskuyu-igru-s-s-na-veb-c-cheerp-webrtc-i-firebase","title":{"rendered":"Estamos portando un juego multijugador de C++ a la web con Cheerp, WebRTC y Firebase","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<h2>Introducci\u00f3n<\/h2>\n<p>\nNuestra empresa <noindex><a rel=\"nofollow\" href=\"https:\/\/leaningtech.com\">Leaning Technologies<\/a><\/noindex> ofrece soluciones para portar aplicaciones desktop tradicionales a la web. Nuestro compilador C++ <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerp-meta\">Cheerp<\/a><\/noindex> genera una combinaci\u00f3n de WebAssembly y JavaScript, lo que proporciona tanto <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerp-meta\/wiki\/Cheerp-Tutorial%3A-Mixed-mode-C++-to-WebAssembly-and-JavaScript\">una interacci\u00f3n sencilla con el navegador<\/a><\/noindex>, como un alto rendimiento.<\/p>\n<p>Como ejemplo de su aplicaci\u00f3n, decidimos portar un juego multijugador a la web y elegimos para ello <noindex><a rel=\"nofollow\" href=\"https:\/\/www.teeworlds.com\/\"><strong>Teeworlds<\/strong><\/a><\/noindex>. Teeworlds es un juego retro multijugador en 2D con una comunidad de jugadores peque\u00f1a pero activa (\u00a1incluy\u00e9ndome a m\u00ed!). Es peque\u00f1o tanto en t\u00e9rminos de recursos descargables como de requisitos de CPU y GPU: un candidato ideal.<\/p>\n<p><img decoding=\"async\" alt=\"Estamos portando un juego multijugador de C++ a la web con Cheerp, WebRTC y Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/30bff36ee1158ddea2e76d8b11e48d2b.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Teeworlds, que funciona en el navegador<\/i><br \/>\n<noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><br \/>\nDecidimos utilizar este proyecto para experimentar con <strong>soluciones generales para portar c\u00f3digo de red a la web<\/strong>. Generalmente, esto se realiza de las siguientes maneras:<\/p>\n<ul>\n<li><strong>XMLHttpRequest\/fetch<\/strong>, si la parte de red consiste solo en solicitudes HTTP, o<\/li>\n<li><strong>WebSockets<\/strong>.<\/li>\n<\/ul>\n<p>\nAmbas soluciones requieren alojar un componente del servidor del lado del servidor, y ninguna de ellas permite usar como protocolo de transporte <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/User_Datagram_Protocol\">UDP<\/a><\/noindex>. Esto es importante para aplicaciones en tiempo real, como software de videoconferencias y juegos, porque las garant\u00edas de entrega y orden de paquetes del protocolo <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Transmission_Control_Protocol\">TCP<\/a><\/noindex> pueden interferir con las bajas latencias.<\/p>\n<p>Tambi\u00e9n existe un tercer camino: usar la red desde el navegador: <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> soporta tanto la transmisi\u00f3n confiable como la no confiable (en el \u00faltimo caso, intenta usar UDP como protocolo de transporte cuando sea posible), y puede usarse tanto con un servidor remoto como entre navegadores. <strong>Esto significa que podemos portar toda la aplicaci\u00f3n al navegador, incluido el componente del servidor.<\/strong><\/p>\n<p>Sin embargo, esto presenta una dificultad adicional: antes de que dos pares de WebRTC puedan intercambiar datos, necesitan completar un procedimiento de \u00abapret\u00f3n de manos\u00bb relativamente complejo para conectarse, lo que requiere varias entidades externas (servidor de se\u00f1alizaci\u00f3n y uno o varios servidores <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>En un mundo ideal, nos gustar\u00eda crear una API de red que use WebRTC internamente, pero que sea lo m\u00e1s cercana posible a la interfaz de UDP Sockets, que no requiere establecer una conexi\u00f3n.<\/p>\n<p>Esto nos permitir\u00e1 aprovechar las ventajas de WebRTC sin necesidad de revelar los complejos detalles en el c\u00f3digo de la aplicaci\u00f3n (que en nuestro proyecto queremos modificar lo menos posible).<\/p>\n<h1>WebRTC M\u00ednimo<\/h1>\n<p>\nWebRTC es un conjunto de API disponibles en los navegadores que permite la transmisi\u00f3n de audio, video y datos arbitrarios entre pares.<\/p>\n<p>La conexi\u00f3n entre pares se establece (incluso en presencia de NAT de uno o ambos lados) mediante servidores STUN y\/o TURN a trav\u00e9s de un mecanismo llamado ICE. Los pares intercambian informaci\u00f3n ICE y par\u00e1metros de canal a trav\u00e9s de la oferta y respuesta del protocolo SDP.<\/p>\n<p>\u00a1Vaya! Cu\u00e1ntas abreviaturas hay en una sola vez. Vamos a explicar brevemente qu\u00e9 significan estos conceptos:<\/p>\n<ul>\n<li><noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/STUN\"><strong>Utilidades de recorrido de sesi\u00f3n para NAT<\/strong> (<strong>STUN<\/strong>)<\/a><\/noindex> - es un protocolo para sortear NAT y obtener un par (IP, puerto) para intercambiar datos directamente con el host. Si logra cumplir su tarea, los pares pueden intercambiar datos entre s\u00ed de forma independiente.<\/li>\n<li><noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Traversal_Using_Relays_around_NAT\"><strong>Recorrido utilizando rel\u00e9s alrededor de NAT<\/strong> (<strong>TURN<\/strong>)<\/a><\/noindex> tambi\u00e9n se utiliza para evitar NAT, pero lo implementa redirigiendo datos a trav\u00e9s de un proxy, visible para ambos pares. A\u00f1ade latencia y es m\u00e1s costoso en ejecuci\u00f3n que STUN (porque se aplica durante toda la sesi\u00f3n de comunicaci\u00f3n), pero a veces es la \u00fanica opci\u00f3n posible.<\/li>\n<li><noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Interactive_Connectivity_Establishment\"><strong>Establecimiento de conectividad interactivo<\/strong> (<strong>ICE<\/strong>)<\/a><\/noindex> se utiliza para elegir el mejor posible m\u00e9todo de conexi\u00f3n entre dos pares basado en la informaci\u00f3n obtenida durante la conexi\u00f3n directa de pares, as\u00ed como en la informaci\u00f3n obtenida de cualquier n\u00famero de servidores STUN y TURN.<\/li>\n<li><noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Session_Description_Protocol\"><strong>Protocolo de descripci\u00f3n de sesi\u00f3n<\/strong> (<strong>SDP<\/strong>)<\/a><\/noindex> - es un formato para describir los par\u00e1metros del canal de conexi\u00f3n, como los candidatos ICE, c\u00f3decs multimedia (en el caso de un canal de audio\/video), etc... Uno de los pares env\u00eda una Oferta SDP y el otro responde con una Respuesta SDP. Despu\u00e9s de esto, se crea el canal.<\/li>\n<\/ul>\n<p>\nPara crear tal conexi\u00f3n, los pares necesitan reunir la informaci\u00f3n que han obtenido de los servidores STUN y TURN e intercambiarla entre s\u00ed.<\/p>\n<p>El problema es que a\u00fan no tienen la capacidad de intercambiar datos directamente, por lo que debe existir un mecanismo de se\u00f1alizaci\u00f3n: un servidor de se\u00f1alizaci\u00f3n.<\/p>\n<p>Un servidor de se\u00f1alizaci\u00f3n puede ser muy simple, porque su \u00fanica tarea es redirigir datos entre los pares en la etapa de \"apret\u00f3n de manos\" (como se muestra en el esquema a continuaci\u00f3n).<\/p>\n<p><img decoding=\"async\" alt=\"Estamos portando un juego multijugador de C++ a la web con Cheerp, WebRTC y Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/0fc39ba5e0942bc19183e8cf06c60868.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Esquema simplificado de la secuencia de \"apret\u00f3n de manos\" de WebRTC<\/i><\/p>\n<h1>Descripci\u00f3n del modelo de red de Teeworlds<\/h1>\n<p>\nLa arquitectura de red de Teeworlds es muy simple:<\/p>\n<ul>\n<li>Los componentes del cliente y el servidor son dos programas distintos.<\/li>\n<li>Los clientes entran en el juego conect\u00e1ndose a uno de varios servidores, cada uno de los cuales aloja solo un juego a la vez.<\/li>\n<li>Toda la transmisi\u00f3n de datos en el juego se realiza a trav\u00e9s del servidor.<\/li>\n<li>Un maestro de servidor especial se utiliza para recopilar la lista de todos los servidores p\u00fablicos, que se muestran en el cliente del juego.<\/li>\n<\/ul>\n<p>\nGracias al uso de WebRTC para el intercambio de datos, podemos trasladar el componente del servidor del juego al navegador, donde se encuentra el cliente. Esto nos brinda una excelente oportunidad\u2026<\/p>\n<h1>Deshacernos de los servidores<\/h1>\n<p>\nLa falta de l\u00f3gica del servidor tiene una agradable ventaja: podemos desplegar toda la aplicaci\u00f3n como contenido est\u00e1tico en Github Pages o en nuestro propio hardware detr\u00e1s de Cloudflare, asegur\u00e1ndonos de cargas r\u00e1pidas y alta disponibilidad sin costo. En esencia, podr\u00edamos olvidarnos de ellos, y si tenemos suerte y el juego se convierte en popular, no ser\u00e1 necesario actualizar la infraestructura.<\/p>\n<p>Sin embargo, para que el sistema funcione, a\u00fan necesitaremos utilizar una arquitectura externa:<\/p>\n<ul>\n<li>Uno o varios servidores STUN: tenemos varias opciones gratuitas disponibles.<\/li>\n<li>Al menos un servidor TURN: aqu\u00ed no hay opciones gratuitas, as\u00ed que podemos configurar el nuestro o pagar por el servicio. Afortunadamente, la mayor parte del tiempo la conexi\u00f3n se podr\u00e1 establecer a trav\u00e9s de servidores STUN (y asegurar un verdadero p2p), pero TURN es necesario como opci\u00f3n de respaldo.<\/li>\n<li>Servidor de se\u00f1alizaci\u00f3n: a diferencia de los otros dos aspectos, la se\u00f1alizaci\u00f3n no est\u00e1 estandarizada. Lo que realmente har\u00e1 el servidor de se\u00f1alizaci\u00f3n depende en parte de la aplicaci\u00f3n. En nuestro caso, es necesario intercambiar un peque\u00f1o volumen de datos antes de establecer la conexi\u00f3n.<\/li>\n<li>Maestro de servidor de Teeworlds: es utilizado por otros servidores para avisar de su existencia y por los clientes para buscar servidores p\u00fablicos. Aunque no es obligatorio (los clientes siempre pueden conectarse manualmente a un servidor conocido), ser\u00eda bueno tenerlo para que los jugadores puedan participar en juegos con personas al azar.<\/li>\n<\/ul>\n<p>\nHemos decidido utilizar los servidores STUN gratuitos de Google, y hemos desplegado uno TURN por nuestra cuenta.<\/p>\n<p>Para los dos \u00faltimos puntos, utilizamos <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Firebase\">Firebase<\/a><\/noindex>:<\/p>\n<ul>\n<li>El maestro servidor de Teeworlds est\u00e1 implementado de manera muy sencilla: como una lista de objetos que contienen informaci\u00f3n (nombre, IP, mapa, modo, \u2026) de cada servidor activo. Los servidores publican y actualizan su propio objeto, mientras que los clientes toman toda la lista y la muestran al jugador. Tambi\u00e9n exhibimos la lista en la p\u00e1gina de inicio como HTML, para que los jugadores puedan simplemente hacer clic en el servidor y entrar directamente al juego.<\/li>\n<li>La se\u00f1alizaci\u00f3n est\u00e1 estrechamente relacionada con nuestra implementaci\u00f3n de sockets, que se describe en la siguiente secci\u00f3n.<\/li>\n<\/ul>\n<p>\n<img decoding=\"async\" alt=\"Estamos portando un juego multijugador de C++ a la web con Cheerp, WebRTC y Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/5e30bc22643f8baecf850053bf69e130.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Lista de servidores dentro del juego y en la p\u00e1gina de inicio<\/i><\/p>\n<h1>Implementaci\u00f3n de sockets<\/h1>\n<p>\nQueremos crear una API que se asemeje lo m\u00e1s posible a los Sockets UDP de Posix, para minimizar el n\u00famero de cambios necesarios.<\/p>\n<p>Tambi\u00e9n queremos implementar el m\u00ednimo necesario requerido para el intercambio de datos m\u00e1s b\u00e1sico por la red.<\/p>\n<p>Por ejemplo, no necesitamos un enrutamiento real: todos los pares est\u00e1n en una \"LAN virtual\" conectada a una instancia espec\u00edfica de la base de datos de Firebase.<\/p>\n<p>Por lo tanto, no necesitamos direcciones IP \u00fanicas: para la identificaci\u00f3n \u00fanica de los pares, basta con usar valores \u00fanicos de las claves de Firebase (similar a los nombres de dominio), y cada par asigna localmente direcciones IP \"falsas\" a cada clave que necesita ser traducida. Esto nos libera completamente de la necesidad de asignaciones globales de direcciones IP, que es una tarea no trivial.<\/p>\n<p>Esta es la API m\u00ednima que necesitamos implementar:<\/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>\nLa API es simple y similar a la de los Sockets Posix, pero tiene algunas diferencias importantes: <strong>registro de callbacks, asignaci\u00f3n de IP locales y conexi\u00f3n \"perezosa\"<\/strong>.<\/p>\n<h2>Registro de callbacks<\/h2>\n<p>\nIncluso si el programa original utiliza entrada-salida no bloqueante, para ejecutarse en un navegador web, el c\u00f3digo necesita ser reestructurado.<\/p>\n<p>La raz\u00f3n de esto es que el ciclo de eventos en el navegador est\u00e1 oculto para el programa (ya sea JavaScript o WebAssembly).<\/p>\n<p>En un entorno nativo, podemos escribir el c\u00f3digo de esta manera<\/p>\n<pre><code class=\"cpp\">while(running) {\n  select(...); \/\/ espera eventos de I\/O\n  while(true) {\n    int r = readfrom(...); \/\/ intenta leer\n    if (r &lt; 0 &amp;&amp; errno == EWOULDBLOCK) \/\/ no hay m\u00e1s datos disponibles\n      break;\n    ...\n  }\n  ...\n}<\/code><\/pre>\n<p>\nSi el ciclo de eventos est\u00e1 oculto para nosotros, entonces necesitamos convertirlo en algo as\u00ed:<\/p>\n<pre><code class=\"cpp\">auto cb = []() { \/\/ esto se llamar\u00e1 cuando haya nuevos datos disponibles\n  while(true) {\n    int r = readfrom(...); \/\/ intenta leer\n    if (r &lt; 0 &amp;&amp; errno == EWOULDBLOCK) \/\/ no hay m\u00e1s datos disponibles\n      break;\n    ...\n  }\n  ...\n};\nrecvCallback(cb); \/\/ registrar el callback<\/code><\/pre>\n<p><\/p>\n<h2>Asignaci\u00f3n de IP locales<\/h2>\n<p>\nLos identificadores de los nodos en nuestra \u00abred\u00bb no son direcciones IP, sino claves de Firebase (son cadenas que se ven as\u00ed: <code>-LmEC50PYZLCiCP-vqde<\/code> ).<\/p>\n<p>Esto es conveniente, ya que no necesitamos un mecanismo para asignar IP y verificar su unicidad (as\u00ed como su eliminaci\u00f3n tras desconectar al cliente), pero a menudo es necesario identificar pares por un valor num\u00e9rico.<\/p>\n<p>Para esto se utilizan las funciones <code>resolve<\/code> y <code>reverseResolve<\/code>: la aplicaci\u00f3n de alguna manera obtiene el valor de la clave como una cadena (ya sea mediante la entrada del usuario o a trav\u00e9s de un servidor maestro), y puede convertirlo en una direcci\u00f3n IP para uso interno. El resto de la API tambi\u00e9n, por simplicidad, recibe este valor en lugar de una cadena.<\/p>\n<p>Esto es similar a una b\u00fasqueda DNS, solo que se realiza localmente en el cliente.<\/p>\n<p>Es decir, las direcciones IP no pueden ser compartidas entre diferentes clientes, y si se necesita un identificador global, tendr\u00e1 que generarse de otra manera.<\/p>\n<h2>Conexi\u00f3n perezosa<\/h2>\n<p>\nUDP no requiere conexi\u00f3n, pero, como hemos visto, antes de comenzar a transferir datos entre dos pares, WebRTC requiere un proceso de conexi\u00f3n prolongado.<\/p>\n<p>Si queremos proporcionar el mismo nivel de abstracci\u00f3n, (<code>sendto<\/code>\/<code>recvfrom<\/code> con pares arbitrarios sin conexi\u00f3n previa), debemos realizar una conexi\u00f3n \u00abperezosa\u00bb (pospuesta) dentro de la API.<\/p>\n<p>Esto es lo que ocurre durante el intercambio de datos entre el \u00abservidor\u00bb y el \u00abcliente\u00bb en el caso de usar UDP, y lo que debe hacer nuestra biblioteca:<\/p>\n<ul>\n<li>El servidor llama <code>bind()<\/code>, para informar al sistema operativo que desea recibir paquetes en el puerto especificado.<\/li>\n<\/ul>\n<p>\nEn su lugar, publicamos un puerto abierto en Firebase bajo la clave del servidor y escuchamos eventos en su sub\u00e1rbol.<\/p>\n<ul>\n<li>El servidor llama <code>recvfrom()<\/code>, recibiendo en este puerto paquetes que llegan desde cualquier host.<\/li>\n<\/ul>\n<p>\nEn nuestro caso, necesitamos verificar la cola de entrada de paquetes enviados a este puerto.<\/p>\n<p>Cada puerto tiene su propia cola, y a\u00f1adimos al principio de los datagramas WebRTC los puertos de origen y destino, para saber a qu\u00e9 cola redirigir cuando llega un nuevo paquete.<\/p>\n<p>La llamada es no bloqueante, por lo que si no hay paquetes, simplemente devolvemos -1 y establecemos <code>errno=EWOULDBLOCK<\/code>.<\/p>\n<ul>\n<li>El cliente obtiene de alguna manera la IP y el puerto del servidor, y llama a <code>sendto()<\/code>. Tambi\u00e9n se realiza una llamada interna, <code>bind()<\/code>, por lo que el siguiente <code>recvfrom()<\/code> recibir\u00e1 una respuesta sin la ejecuci\u00f3n expl\u00edcita de bind.<\/li>\n<\/ul>\n<p>\nEn nuestro caso, el cliente obtiene externamente una clave de cadena y utiliza la funci\u00f3n <code>resolve()<\/code> para obtener la direcci\u00f3n IP.<\/p>\n<p>En esta etapa, comenzamos el \u00abapret\u00f3n de manos\u00bb de WebRTC, si los dos pares a\u00fan no est\u00e1n conectados entre s\u00ed. Las conexiones a diferentes puertos de un mismo par utilizan el mismo DataChannel de WebRTC.<\/p>\n<p>Tambi\u00e9n realizamos una conexi\u00f3n indirecta <code>bind()<\/code>, para que el servidor pueda restablecer la conexi\u00f3n en el siguiente <code>sendto()<\/code> en caso de que se cierre por alguna raz\u00f3n.<\/p>\n<p>El servidor es notificado de la conexi\u00f3n del cliente cuando este guarda su oferta SDP bajo la informaci\u00f3n del puerto del servidor en Firebase, y el servidor responde all\u00ed mismo con su respuesta.<\/p>\n<p>\nEn el diagrama mostrado a continuaci\u00f3n se muestra un ejemplo del movimiento de mensajes para el esquema de sockets y la transmisi\u00f3n del primer mensaje del cliente al servidor:<\/p>\n<p><img decoding=\"async\" alt=\"Estamos portando un juego multijugador de C++ a la web con Cheerp, WebRTC y Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/7dc48e816c09e33daf28c307cad0c258.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Diagrama completo de la etapa de conexi\u00f3n entre el cliente y el servidor<\/i><\/p>\n<h1>Conclusi\u00f3n<\/h1>\n<p>\nSi has llegado hasta aqu\u00ed, seguramente te interesa ver la teor\u00eda en acci\u00f3n. Puedes jugar en <noindex><a rel=\"nofollow\" href=\"https:\/\/teeworlds.leaningtech.com\">teeworlds.leaningtech.com<\/a><\/noindex>, \u00a1pru\u00e9balo!<\/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>Partido amistoso entre colegas<\/i><\/p>\n<p>El c\u00f3digo de la biblioteca de red est\u00e1 disponible de forma gratuita en <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerpnet\">Github<\/a><\/noindex>. \u00danete a la conversaci\u00f3n en nuestro canal en <noindex><a rel=\"nofollow\" href=\"https:\/\/gitter.im\/leaningtech\/cheerp\">Gitter<\/a><\/noindex>!<br \/>\n<br \/>Fuente: <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\/es\/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=\"es_ES\" \/>\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\/es\/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\udd47Portamos un juego multijugador de C++ a la web con Cheerp, WebRTC y Firebase | ProHoster","description":"Introducci\u00f3n Nuestra empresa Leaning Technologies proporciona soluciones para la portaci\u00f3n de aplicaciones de escritorio tradicionales a la web.","canonical_url":"https:\/\/prohoster.info\/es\/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":"es_ES","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\/es\/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\/es\/wp-json\/wp\/v2\/posts\/38370","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/comments?post=38370"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/posts\/38370\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/media\/28801"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/media?parent=38370"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/categories?post=38370"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/es\/wp-json\/wp\/v2\/tags?post=38370"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}