{"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\/pt\/blog\/administrirovanie\/portiruem-mnogopolzovatelskuyu-igru-s-s-na-veb-c-cheerp-webrtc-i-firebase","title":{"rendered":"Portando o jogo multiplayer de C++ para a web com Cheerp, WebRTC e Firebase","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<h2>Introdu\u00e7\u00e3o<\/h2>\n<p>\nNossa empresa <noindex><a rel=\"nofollow\" href=\"https:\/\/leaningtech.com\">Leaning Technologies<\/a><\/noindex> fornece solu\u00e7\u00f5es para a portabilidade de aplicativos desktop tradicionais para a web. Nosso compilador C++ <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerp-meta\">Cheerp<\/a><\/noindex> gera uma combina\u00e7\u00e3o de WebAssembly e JavaScript, o que assegura tanto <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerp-meta\/wiki\/Cheerp-Tutorial%3A-Mixed-mode-C++-to-WebAssembly-and-JavaScript\">uma intera\u00e7\u00e3o simples com o navegador<\/a><\/noindex>, quanto um alto desempenho.<\/p>\n<p>Como exemplo de sua aplica\u00e7\u00e3o, decidimos portar um jogo multiplayer para a web e escolhemos <noindex><a rel=\"nofollow\" href=\"https:\/\/www.teeworlds.com\/\"><strong>Teeworlds<\/strong><\/a><\/noindex>. Teeworlds \u00e9 um jogo retro 2D multiplayer com uma comunidade de jogadores pequena, mas ativa (incluindo eu!). \u00c9 pequeno tanto em termos de recursos para download quanto em requisitos de CPU e GPU \u2014 um candidato ideal.<\/p>\n<p><img decoding=\"async\" alt=\"Portando o jogo multiplayer de C++ para a web com Cheerp, WebRTC e Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/30bff36ee1158ddea2e76d8b11e48d2b.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Teeworlds rodando no navegador<\/i><br \/>\n<noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><br \/>\nDecidimos usar este projeto para experimentar com <strong>solu\u00e7\u00f5es gerais para portar c\u00f3digo de rede para a web<\/strong>. Normalmente, isso \u00e9 realizado das seguintes maneiras:<\/p>\n<ul>\n<li><strong>XMLHttpRequest\/fetch<\/strong>, se a parte de rede consiste apenas em requisi\u00e7\u00f5es HTTP, ou<\/li>\n<li><strong>WebSockets<\/strong>.<\/li>\n<\/ul>\n<p>\nAmbas as solu\u00e7\u00f5es exigem hospedar um componente de servidor do lado do servidor, e nenhuma delas permite usar como protocolo de transporte. <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/User_Datagram_Protocol\">UDP<\/a><\/noindex>Isso \u00e9 importante para aplica\u00e7\u00f5es em tempo real, como softwares para videoconfer\u00eancias e jogos, pois as garantias de entrega e ordem dos pacotes do protocolo <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Transmission_Control_Protocol\">TCP<\/a><\/noindex> podem interferir em baixas lat\u00eancias.<\/p>\n<p>H\u00e1 tamb\u00e9m um terceiro caminho \u2014 usar a rede a partir do 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> suporta tanto transmiss\u00e3o confi\u00e1vel quanto n\u00e3o confi\u00e1vel (neste \u00faltimo caso, ele tenta usar como protocolo de transporte o UDP sempre que poss\u00edvel), e pode ser utilizado tanto com um servidor remoto quanto entre navegadores. <strong>Isso significa que podemos portar toda a aplica\u00e7\u00e3o para o navegador, incluindo o componente do servidor!<\/strong><\/p>\n<p>No entanto, isso traz uma dificuldade adicional: antes que dois pares WebRTC possam trocar dados, eles precisam passar por um procedimento relativamente complexo de \"aperto de m\u00e3o\" (handshake) para se conectar, o que requer v\u00e1rias entidades externas (servidor de sinaliza\u00e7\u00e3o e um ou mais 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>Idealmente, gostar\u00edamos de criar uma API de rede que dentro utilize WebRTC, mas o mais pr\u00f3ximo poss\u00edvel da interface de Sockets UDP, que n\u00e3o requer uma conex\u00e3o estabelecida.<\/p>\n<p>Isso nos permitir\u00e1 aproveitar as vantagens do WebRTC sem a necessidade de expor os detalhes complexos ao c\u00f3digo da aplica\u00e7\u00e3o (que em nosso projeto gostar\u00edamos de modificar o m\u00ednimo poss\u00edvel).<\/p>\n<h1>WebRTC m\u00ednimo<\/h1>\n<p>\nWebRTC \u00e9 um conjunto de APIs dispon\u00edvel nos navegadores que permite a transmiss\u00e3o de \u00e1udio, v\u00eddeo e dados arbitr\u00e1rios de peer-to-peer.<\/p>\n<p>A conex\u00e3o entre os pares \u00e9 estabelecida (mesmo com NAT em um ou ambos os lados) atrav\u00e9s de servidores STUN e\/ou TURN por meio de um mecanismo chamado ICE. Os pares trocam informa\u00e7\u00f5es de ICE e par\u00e2metros de canais atrav\u00e9s do protocolo SDP com oferta e resposta.<\/p>\n<p>Uau! Quantas siglas de uma s\u00f3 vez. Vamos explicar brevemente o que esses conceitos significam:<\/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> \u00e9 um protocolo para contornar o NAT e obter um par (IP, porta) para trocar dados diretamente com o host. Se conseguir realizar sua fun\u00e7\u00e3o, os pares poder\u00e3o trocar dados entre si.<\/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> tamb\u00e9m \u00e9 usado para contornar o NAT, mas o faz redirecionando os dados atrav\u00e9s de um proxy, vis\u00edvel para ambos os pares. Isso adiciona lat\u00eancia e \u00e9 mais custoso em termos de execu\u00e7\u00e3o do que o STUN (porque \u00e9 utilizado durante toda a sess\u00e3o de comunica\u00e7\u00e3o), mas \u00e0s vezes \u00e9 a \u00fanica op\u00e7\u00e3o poss\u00edvel.<\/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> \u00e9 utilizado para selecionar a melhor maneira poss\u00edvel de conectar dois pares com base nas informa\u00e7\u00f5es obtidas atrav\u00e9s da conex\u00e3o direta dos pares, bem como as informa\u00e7\u00f5es adquiridas por qualquer n\u00famero de servidores 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> \u00e9 um formato para descrever os par\u00e2metros do canal de conex\u00e3o, como os candidatos de ICE, codecs de m\u00eddia (no caso de um canal de \u00e1udio\/v\u00eddeo), etc. Um dos pares envia uma oferta SDP (\"offer\"), e o outro responde com uma resposta SDP (\"answer\"). Ap\u00f3s isso, o canal \u00e9 criado.<\/li>\n<\/ul>\n<p>\nPara criar tal conex\u00e3o, os pares precisam coletar as informa\u00e7\u00f5es que conseguiram dos servidores STUN e TURN e troc\u00e1-las entre si.<\/p>\n<p>O problema \u00e9 que eles ainda n\u00e3o t\u00eam a capacidade de trocar dados diretamente, ent\u00e3o deve existir um mecanismo de sinaliza\u00e7\u00e3o off-band: o servidor de sinaliza\u00e7\u00e3o.<\/p>\n<p>O servidor de sinaliza\u00e7\u00e3o pode ser muito simples, pois sua \u00fanica tarefa \u00e9 redirecionar dados entre os pares durante a etapa de \"aperto de m\u00e3os\" (como mostrado no diagrama abaixo).<\/p>\n<p><img decoding=\"async\" alt=\"Portando o jogo multiplayer de C++ para a web com Cheerp, WebRTC e Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/0fc39ba5e0942bc19183e8cf06c60868.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Diagrama simplificado da sequ\u00eancia de \"aperto de m\u00e3os\" do WebRTC<\/i><\/p>\n<h1>Vis\u00e3o geral do modelo de rede Teeworlds<\/h1>\n<p>\nA arquitetura de rede do Teeworlds \u00e9 muito simples:<\/p>\n<ul>\n<li>Os componentes do cliente e do servidor s\u00e3o dois programas diferentes.<\/li>\n<li>Os clientes entram em jogo conectando-se a um dos v\u00e1rios servidores, cada um dos quais hospeda apenas um jogo de cada vez.<\/li>\n<li>Toda a transmiss\u00e3o de dados no jogo \u00e9 feita atrav\u00e9s do servidor.<\/li>\n<li>Um servidor-mestre especial \u00e9 utilizado para coletar a lista de todos os servidores p\u00fablicos, que s\u00e3o exibidos no cliente de jogo.<\/li>\n<\/ul>\n<p>\nGra\u00e7as ao uso do WebRTC para troca de dados, podemos transferir o componente de servidor do jogo para o navegador, onde est\u00e1 o cliente. Isso nos oferece uma oportunidade maravilhosa\u2026<\/p>\n<h1>Desfazer-se dos servidores<\/h1>\n<p>\nA aus\u00eancia de l\u00f3gica de servidor tem a vantagem agrad\u00e1vel: podemos implantar toda a aplica\u00e7\u00e3o como conte\u00fado est\u00e1tico no Github Pages ou em nosso pr\u00f3prio hardware atr\u00e1s do Cloudflare, garantindo assim downloads r\u00e1pidos e alta disponibilidade de forma gratuita. Basicamente, podemos esquecer deles, e se tivermos sorte e o jogo se tornar popular, n\u00e3o ser\u00e1 necess\u00e1rio atualizar a infraestrutura.<\/p>\n<p>No entanto, para que o sistema funcione, ainda teremos que usar uma arquitetura externa:<\/p>\n<ul>\n<li>Um ou mais servidores STUN: temos v\u00e1rias op\u00e7\u00f5es gratuitas \u00e0 escolha.<\/li>\n<li>Pelo menos um servidor TURN: aqui n\u00e3o h\u00e1 op\u00e7\u00f5es gratuitas, ent\u00e3o podemos ou configurar o nosso ou pagar pelo servi\u00e7o. Felizmente, a maior parte do tempo a conex\u00e3o pode ser estabelecida atrav\u00e9s de servidores STUN (assegurando p2p verdadeiro), mas o TURN \u00e9 necess\u00e1rio como uma alternativa de reserva.<\/li>\n<li>Servidor de sinaliza\u00e7\u00e3o: ao contr\u00e1rio dos outros dois aspectos, a sinaliza\u00e7\u00e3o n\u00e3o \u00e9 padronizada. O que o servidor de sinaliza\u00e7\u00e3o realmente far\u00e1 depende do aplicativo. No nosso caso, \u00e9 necess\u00e1rio trocar um pequeno volume de dados antes de estabelecer a conex\u00e3o.<\/li>\n<li>Servidor-mestre Teeworlds: ele \u00e9 usado por outros servidores para notificar sua exist\u00eancia e pelos clientes para encontrar servidores p\u00fablicos. Embora n\u00e3o seja obrigat\u00f3rio (os clientes sempre podem se conectar manualmente a um servidor que conhecem), seria bom t\u00ea-lo para que os jogadores possam participar de jogos com pessoas aleat\u00f3rias.<\/li>\n<\/ul>\n<p>\nDecidimos usar os servidores STUN gratuitos da Google, e implantamos um servidor TURN n\u00f3s mesmos.<\/p>\n<p>Para os dois \u00faltimos pontos, utilizamos <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Firebase\">Firebase<\/a><\/noindex>:<\/p>\n<ul>\n<li>O mestre servidor Teeworlds \u00e9 implementado de forma muito simples: como uma lista de objetos contendo informa\u00e7\u00f5es (nome, IP, mapa, modo, ...) de cada servidor ativo. Os servidores publicam e atualizam seu pr\u00f3prio objeto, enquanto os clientes capturam toda a lista e a exibem para o jogador. Tamb\u00e9m exibimos a lista na p\u00e1gina inicial como HTML, para que os jogadores possam simplesmente clicar no servidor e entrar no jogo diretamente.<\/li>\n<li>A sinaliza\u00e7\u00e3o est\u00e1 intimamente relacionada \u00e0 nossa implementa\u00e7\u00e3o de soquetes, descrita na pr\u00f3xima se\u00e7\u00e3o.<\/li>\n<\/ul>\n<p>\n<img decoding=\"async\" alt=\"Portando o jogo multiplayer de C++ para a web com Cheerp, WebRTC e Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/5e30bc22643f8baecf850053bf69e130.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Lista de servidores dentro do jogo e na p\u00e1gina inicial<\/i><\/p>\n<h1>Implementa\u00e7\u00e3o de soquetes<\/h1>\n<p>\nQueremos criar uma API o mais pr\u00f3ximo poss\u00edvel dos Soquetes UDP Posix, para minimizar o n\u00famero de altera\u00e7\u00f5es necess\u00e1rias.<\/p>\n<p>Al\u00e9m disso, queremos implementar o m\u00ednimo necess\u00e1rio para a troca simples de dados pela rede.<\/p>\n<p>Por exemplo, n\u00e3o precisamos de roteamento real: todos os peers est\u00e3o em uma 'LAN virtual' ligada a uma inst\u00e2ncia espec\u00edfica do banco de dados Firebase.<\/p>\n<p>Portanto, n\u00e3o precisamos de endere\u00e7os IP exclusivos: para a identifica\u00e7\u00e3o \u00fanica dos peers, basta usar valores de chave exclusivos do Firebase (semelhante a nomes de dom\u00ednio), e cada peer localmente atribui endere\u00e7os IP 'falsos' a cada chave que precisa ser convertida. Isso nos livra completamente da necessidade de atribui\u00e7\u00e3o global de endere\u00e7os IP, o que \u00e9 uma tarefa n\u00e3o trivial.<\/p>\n<p>Aqui est\u00e1 a API m\u00ednima que precisamos 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>\nA API \u00e9 simples e semelhante \u00e0 API Posix Sockets, mas possui algumas diferen\u00e7as importantes: <strong>registro de callbacks, atribui\u00e7\u00e3o de IPs locais e conex\u00e3o 'pregui\u00e7osa'<\/strong>.<\/p>\n<h2>Registro de callbacks<\/h2>\n<p>\nMesmo que o programa original utilize E\/S n\u00e3o bloqueante, para execu\u00e7\u00e3o em um navegador, o c\u00f3digo precisa ser refatorado.<\/p>\n<p>A raz\u00e3o para isso \u00e9 que o loop de eventos no navegador est\u00e1 oculto para o programa (seja JavaScript ou WebAssembly).<\/p>\n<p>No ambiente nativo, podemos escrever o c\u00f3digo desta forma<\/p>\n<pre><code class=\"cpp\">while(running) {\n  select(...); \/\/ aguardar eventos de E\/S\n  while(true) {\n    int r = readfrom(...); \/\/ tentar ler\n    if (r &lt; 0 &amp;&amp; errno == EWOULDBLOCK) \/\/ n\u00e3o h\u00e1 mais dados dispon\u00edveis\n      break;\n    ...\n  }\n  ...\n}<\/code><\/pre>\n<p>\nSe o loop de eventos est\u00e1 oculto para n\u00f3s, precisamos transform\u00e1-lo em algo como:<\/p>\n<pre><code class=\"cpp\">auto cb = []() { \/\/ isso ser\u00e1 chamado quando novos dados estiverem dispon\u00edveis\n  while(true) {\n    int r = readfrom(...); \/\/ tentar ler\n    if (r &lt; 0 &amp;&amp; errno == EWOULDBLOCK) \/\/ n\u00e3o h\u00e1 mais dados dispon\u00edveis\n      break;\n    ...\n  }\n  ...\n};\nsomeCallback(cb); \/\/ registrar o callback<\/code><\/pre>\n<p><\/p>\n<h2>Atribui\u00e7\u00e3o de IPs locais<\/h2>\n<p>\nOs identificadores de n\u00f3s em nossa \"rede\" n\u00e3o s\u00e3o endere\u00e7os IP, mas chaves do Firebase (s\u00e3o strings que se parecem com: <code>-LmEC50PYZLCiCP-vqde<\/code> ).<\/p>\n<p>Isso \u00e9 conveniente, pois n\u00e3o precisamos de um mecanismo para atribuir IPs e verificar sua exclusividade (bem como descart\u00e1-los ap\u00f3s a desconex\u00e3o do cliente), mas muitas vezes \u00e9 necess\u00e1rio identificar pares por um valor num\u00e9rico.<\/p>\n<p>\u00c9 exatamente para isso que s\u00e3o utilizadas as fun\u00e7\u00f5es <code>resolve<\/code> e <code>reverseResolve<\/code>: o aplicativo de alguma forma obt\u00e9m o valor da chave em string (por meio da entrada do usu\u00e1rio ou por meio de um servidor mestre) e pode convert\u00ea-lo em um endere\u00e7o IP para uso interno. O restante da API tamb\u00e9m recebe, por simplicidade, esse valor em vez da string.<\/p>\n<p>Isso \u00e9 semelhante a uma consulta DNS, exceto que \u00e9 realizada localmente no cliente.<\/p>\n<p>Ou seja, os endere\u00e7os IP n\u00e3o podem ser compartilhados entre diferentes clientes, e se um identificador global for necess\u00e1rio, ele precisar\u00e1 ser gerado de outra maneira.<\/p>\n<h2>Conex\u00e3o pregui\u00e7osa<\/h2>\n<p>\nUDP n\u00e3o requer conex\u00e3o, mas, como vimos, antes de come\u00e7ar a transmitir dados entre dois pares, o WebRTC exige um longo processo de conex\u00e3o.<\/p>\n<p>Se quisermos manter o mesmo n\u00edvel de abstra\u00e7\u00e3o, (<code>sendto<\/code>\/<code>recvfrom<\/code> com pares arbitr\u00e1rios sem conex\u00e3o pr\u00e9via), devemos realizar uma conex\u00e3o \"pregui\u00e7osa\" (delayed) dentro da API.<\/p>\n<p>Isso \u00e9 o que acontece durante a troca t\u00edpica de dados entre um \"servidor\" e um \"cliente\" ao usar UDP, e o que nossa biblioteca deve executar:<\/p>\n<ul>\n<li>O servidor chama <code>bind()<\/code>, para informar ao sistema operacional que deseja receber pacotes na porta especificada.<\/li>\n<\/ul>\n<p>\nEm vez disso, vamos publicar uma porta aberta no Firebase sob a chave do servidor e escutar os eventos em seu sub\u00e1rvore.<\/p>\n<ul>\n<li>O servidor chama <code>recvfrom()<\/code>, recebendo nesse porta pacotes provenientes de qualquer host.<\/li>\n<\/ul>\n<p>\nNo nosso caso, precisamos verificar a fila de entrada de pacotes enviados para essa porta.<\/p>\n<p>Cada porta tem sua pr\u00f3pria fila, e adicionamos nos datagramas WebRTC as portas de origem e destino, para saber em qual fila redirecionar quando um novo pacote chegar.<\/p>\n<p>A chamada \u00e9 n\u00e3o bloqueante, portanto, se n\u00e3o houver pacotes, simplesmente retornamos -1 e definimos <code>errno=EWOULDBLOCK<\/code>.<\/p>\n<ul>\n<li>O cliente obt\u00e9m IP e a porta do servidor por meios externos e chama <code>sendto()<\/code>. Tamb\u00e9m ocorre uma chamada interna, <code>bind()<\/code>, portanto, o subsequente <code>recvfrom()<\/code> receber\u00e1 uma resposta sem a execu\u00e7\u00e3o expl\u00edcita de bind.<\/li>\n<\/ul>\n<p>\nNeste caso, o cliente recebe externamente uma chave de string e utiliza a fun\u00e7\u00e3o <code>resolve()<\/code> para obter o endere\u00e7o IP.<\/p>\n<p>Neste est\u00e1gio, come\u00e7amos o 'handshake' do WebRTC, caso os dois pares ainda n\u00e3o estejam conectados entre si. Conex\u00f5es em diferentes portas de um mesmo par utilizam o mesmo DataChannel do WebRTC.<\/p>\n<p>Al\u00e9m disso, realizamos uma indireta <code>bind()<\/code>, para que o servidor possa restaurar a conex\u00e3o na pr\u00f3xima <code>sendto()<\/code> caso ela tenha sido encerrada por algum motivo.<\/p>\n<p>O servidor \u00e9 notificado da conex\u00e3o do cliente quando o cliente registra sua oferta SDP sob a informa\u00e7\u00e3o da porta do servidor no Firebase, e o servidor responde l\u00e1 mesmo com sua resposta.<\/p>\n<p>\nNo esquema mostrado abaixo, \u00e9 apresentado um exemplo do fluxo de mensagens para o esquema de sockets e a transmiss\u00e3o da primeira mensagem do cliente para o servidor:<\/p>\n<p><img decoding=\"async\" alt=\"Portando o jogo multiplayer de C++ para a web com Cheerp, WebRTC e Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/7dc48e816c09e33daf28c307cad0c258.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Esquema completo da etapa de conex\u00e3o entre o cliente e o servidor.<\/i><\/p>\n<h1>Conclus\u00e3o<\/h1>\n<p>\nSe voc\u00ea chegou at\u00e9 aqui, deve estar interessado em ver a teoria em a\u00e7\u00e3o. O jogo pode ser jogado em <noindex><a rel=\"nofollow\" href=\"https:\/\/teeworlds.leaningtech.com\">teeworlds.leaningtech.com<\/a><\/noindex>, experimente!<\/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>Partida amig\u00e1vel entre colegas.<\/i><\/p>\n<p>O c\u00f3digo da biblioteca de rede est\u00e1 dispon\u00edvel livremente em <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerpnet\">Github<\/a><\/noindex>. Junte-se \u00e0 conversa no nosso canal em <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.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\/pt\/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=\"pt_PT\" \/>\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\/pt\/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\udd47Portando um jogo multiplayer de C++ para a web com Cheerp, WebRTC e Firebase | ProHoster","description":"Introdu\u00e7\u00e3o Nossa empresa Leaning Technologies oferece solu\u00e7\u00f5es para portar aplicativos desktop tradicionais para a web.","canonical_url":"https:\/\/prohoster.info\/pt\/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":"pt_PT","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\/pt\/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\/pt\/wp-json\/wp\/v2\/posts\/38370","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/pt\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/pt\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/pt\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/pt\/wp-json\/wp\/v2\/comments?post=38370"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/pt\/wp-json\/wp\/v2\/posts\/38370\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/prohoster.info\/pt\/wp-json\/wp\/v2\/media\/28801"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/pt\/wp-json\/wp\/v2\/media?parent=38370"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/pt\/wp-json\/wp\/v2\/categories?post=38370"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/pt\/wp-json\/wp\/v2\/tags?post=38370"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}