{"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\/en\/blog\/administrirovanie\/portiruem-mnogopolzovatelskuyu-igru-s-s-na-veb-c-cheerp-webrtc-i-firebase","title":{"rendered":"We are porting a multiplayer game from C++ to the web with Cheerp, WebRTC, and Firebase","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>\nOur company <noindex><a rel=\"nofollow\" href=\"https:\/\/leaningtech.com\">Leaning Technologies<\/a><\/noindex> provides solutions for porting traditional desktop applications to the web. Our C++ compiler <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerp-meta\">Cheerp<\/a><\/noindex> generates a combination of WebAssembly and JavaScript, which ensures both <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerp-meta\/wiki\/Cheerp-Tutorial%3A-Mixed-mode-C++-to-WebAssembly-and-JavaScript\">easy interaction with the browser<\/a><\/noindex>, and high performance.<\/p>\n<p>As an example of its application, we decided to port a multiplayer game to the web and chose <noindex><a rel=\"nofollow\" href=\"https:\/\/www.teeworlds.com\/\"><strong>Teeworlds<\/strong><\/a><\/noindex>. Teeworlds is a multiplayer 2D retro game with a small but active player community (including myself!). It is small in terms of downloadable resources as well as CPU and GPU requirements \u2014 an ideal candidate.<\/p>\n<p><img decoding=\"async\" alt=\"We are porting a multiplayer game from C++ to the web with Cheerp, WebRTC, and Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/30bff36ee1158ddea2e76d8b11e48d2b.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Running in the browser, Teeworlds<\/i><br \/>\n<noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><br \/>\nWe decided to use this project to experiment with <strong>general solutions for porting network code to the web<\/strong>. This is usually accomplished in the following ways:<\/p>\n<ul>\n<li><strong>XMLHttpRequest\/fetch<\/strong>, if the network part consists only of HTTP requests, or<\/li>\n<li><strong>WebSockets<\/strong>.<\/li>\n<\/ul>\n<p>\nBoth solutions require hosting a server component on the server side, and neither allows using <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/User_Datagram_Protocol\">UDP<\/a><\/noindex>as a transport protocol. This is important for real-time applications, such as video conferencing software and games, because the delivery and order guarantees of the protocol <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Transmission_Control_Protocol\">TCP<\/a><\/noindex> can hinder low latency.<\/p>\n<p>There is also a third way \u2014 using the browser network: <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> supports both reliable and unreliable transmission (in the latter case, it attempts to use UDP as the transport protocol whenever possible), and can be used both with a remote server and between browsers. <strong>This means we can port the entire application to the browser, including the server component!<\/strong><\/p>\n<p>However, this comes with an additional difficulty: before two WebRTC peers can exchange data, they need to go through a relatively complex 'handshake' procedure for connection, which requires several third-party entities (a signaling server and one or more <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 an ideal scenario, we would like to create a network API that internally uses WebRTC, but as close as possible to the UDP Sockets interface, which does not require a connection to be established.<\/p>\n<p>This will allow us to leverage the advantages of WebRTC without needing to disclose the complex details of the application code (which we wanted to change as little as possible in our project).<\/p>\n<h1>Minimum WebRTC<\/h1>\n<p>\nWebRTC is a set of APIs available in browsers that enables peer-to-peer transmission of audio, video, and arbitrary data.<\/p>\n<p>The connection between peers is established (even in the presence of NAT on one or both sides) through STUN and\/or TURN servers via a mechanism called ICE. Peers exchange ICE information and channel parameters through the offer and answer of the SDP protocol.<\/p>\n<p>Wow! That's a lot of acronyms at once. Let's briefly explain what these terms mean:<\/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> \u2014 a protocol for bypassing NAT and obtaining a pair (IP, port) for directly exchanging data with the host. If it succeeds in its task, peers can exchange data with each other.<\/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> also used to bypass NAT, but it does this by relaying data through a proxy visible to both peers. It adds latency and is more resource-intensive than STUN (because it is used throughout the communication session), but sometimes it is the only possible option.<\/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> is used to select the best possible method for connecting two peers based on information obtained from direct peer connections as well as data received from any number of STUN and TURN servers.<\/li>\n<li><noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Session_Description_Protocol\"><strong>Session Description Protocol<\/strong> (<strong>SDP<\/strong>)<\/a><\/noindex> is a format for describing the parameters of a connection channel, such as ICE candidates, media codecs (for audio\/video channels), etc. One peer sends an SDP Offer, while the other responds with an SDP Answer. After this, a channel is established.<\/li>\n<\/ul>\n<p>\nTo create such a connection, peers need to gather the information they received from STUN and TURN servers and exchange it with each other.<\/p>\n<p>The problem is that they currently cannot exchange data directly, so there must be an out-of-band mechanism: a signaling server.<\/p>\n<p>The signaling server can be very simple, as its only task is to redirect data between peers during the 'handshake' phase (as shown in the diagram below).<\/p>\n<p><img decoding=\"async\" alt=\"We are porting a multiplayer game from C++ to the web with Cheerp, WebRTC, and Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/0fc39ba5e0942bc19183e8cf06c60868.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>Simplified sequence of the WebRTC 'handshake'<\/i><\/p>\n<h1>Overview of the Teeworlds network model<\/h1>\n<p>\nThe network architecture of Teeworlds is quite simple:<\/p>\n<ul>\n<li>Client and server components are two different programs.<\/li>\n<li>Clients join the game by connecting to one of several servers, each of which hosts only one game at a time.<\/li>\n<li>All data transfer in the game is done through the server.<\/li>\n<li>A special master server is used to collect a list of all public servers that are displayed in the game client.<\/li>\n<\/ul>\n<p>\nBy using WebRTC for data exchange, we can move the game's server component into the browser where the client is located. This provides us with a great opportunity\u2026<\/p>\n<h1>To eliminate servers<\/h1>\n<p>\nThe lack of server logic has a nice advantage: we can deploy the entire application as static content on Github Pages or on our own equipment behind Cloudflare, thereby ensuring fast load times and high uptime for free. Essentially, we might forget about them, and if we are lucky and the game becomes popular, we won't have to upgrade the infrastructure.<\/p>\n<p>However, for the system to work, we still need to use external architecture:<\/p>\n<ul>\n<li>One or more STUN servers: we have a choice of several free options.<\/li>\n<li>At least one TURN server: there are no free options here, so we can either set up our own or pay for a service. Fortunately, most of the time, connections can be established through STUN servers (ensuring true P2P), but TURN is necessary as a backup.<\/li>\n<li>Signaling server: unlike the other two aspects, signaling is not standardized. What the signaling server will actually be responsible for depends somewhat on the application. In our case, a small amount of data needs to be exchanged before establishing a connection.<\/li>\n<li>Teeworlds master server: it is used by other servers to announce their existence and by clients to find public servers. Although it is not mandatory (clients can always connect to a known server manually), it would be good to have it so that players can join games with random people.<\/li>\n<\/ul>\n<p>\nWe decided to use Google's free STUN servers, and we deployed one TURN server ourselves.<\/p>\n<p>For the last two points, we used <noindex><a rel=\"nofollow\" href=\"https:\/\/en.wikipedia.org\/wiki\/Firebase\">Firebase<\/a><\/noindex>:<\/p>\n<ul>\n<li>The master server for Teeworlds is implemented very simply: as a list of objects containing information (name, IP, map, mode, etc.) for each active server. Servers publish and update their own object, while clients retrieve the entire list and display it to the player. We also display the list on the homepage as HTML so players can simply click on the server to join the game.<\/li>\n<li>Signal handling is closely tied to our implementation of sockets, which is described in the next section.<\/li>\n<\/ul>\n<p>\n<img decoding=\"async\" alt=\"We are porting a multiplayer game from C++ to the web with Cheerp, WebRTC, and Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/5e30bc22643f8baecf850053bf69e130.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>The list of servers within the game and on the homepage<\/i><\/p>\n<h1>Socket Implementation<\/h1>\n<p>\nWe want to create an API that is as close to Posix UDP Sockets as possible to minimize the necessary changes.<\/p>\n<p>We also want to implement the bare minimum required for the simplest data exchange over the network.<\/p>\n<p>For example, we do not need true routing: all peers exist in a single 'virtual LAN' connected to a specific instance of the Firebase database.<\/p>\n<p>Therefore, we do not need unique IP addresses: to uniquely identify peers, it's sufficient to use unique Firebase key values (similar to domain names), and each peer locally assigns 'fake' IP addresses to each key that needs to be transformed. This completely eliminates the need for global IP address assignments, which is a non-trivial task.<\/p>\n<p>Here is the minimal API that we need to implement:<\/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>\nThe API is simple and resembles the Posix Sockets API, but has several important differences: <strong>callback registration, local IP assignment, and 'lazy' connection<\/strong>.<\/p>\n<h2>Callback Registration<\/h2>\n<p>\nEven if the original program uses non-blocking I\/O, the code needs to be refactored to run in a web browser.<\/p>\n<p>The reason for this is that the event loop in the browser is hidden from the program (whether it\u2019s JavaScript or WebAssembly).<\/p>\n<p>In a native environment, we can write code like this<\/p>\n<pre><code class=\"cpp\">while(running) {\n  select(...); \/\/ wait for I\/O events\n  while(true) {\n    int r = readfrom(...); \/\/ try to read\n    if (r &lt; 0 &amp;&amp; errno == EWOULDBLOCK) \/\/ no more data available\n      break;\n    ...\n  }\n  ...\n}<\/code><\/pre>\n<p>\nIf the event loop is hidden from us, we need to turn it into something like this:<\/p>\n<pre><code class=\"cpp\">auto cb = []() { \/\/ this will be called when new data is available\n  while(true) {\n    int r = readfrom(...); \/\/ try to read\n    if (r &lt; 0 &amp;&amp; errno == EWOULDBLOCK) \/\/ no more data available\n      break;\n    ...\n  }\n  ...\n};\nrecvCallback(cb); \/\/ register the callback<\/code><\/pre>\n<p><\/p>\n<h2>Purpose of local IPs<\/h2>\n<p>\nNode identifiers in our 'network' are not IP addresses, but Firebase keys (these are strings that look like: <code>-LmEC50PYZLCiCP-vqde<\/code> ).<\/p>\n<p>This is convenient because we don\u2019t need a mechanism to assign IPs and check their uniqueness (as well as their recycling after a client disconnects), but it is often necessary to identify peers by numerical value.<\/p>\n<p>This is exactly what the functions <code>resolve<\/code> and <code>reverseResolve<\/code>: the application somehow obtains the string value of the key (through user input or through a master server), and can convert it into an IP address for internal use. The rest of the API also receives this value instead of the string for simplicity.<\/p>\n<p>This is similar to a DNS lookup, except it is performed locally by the client.<\/p>\n<p>That is, IP addresses cannot be shared among different clients, and if a global identifier is needed, it must be generated in another way.<\/p>\n<h2>Lazy connection<\/h2>\n<p>\nUDP does not require a connection, but as we have seen, before starting to transfer data between two peers, WebRTC requires a lengthy connection process.<\/p>\n<p>If we want to provide the same level of abstraction, (<code>sendto<\/code>\/<code>recvfrom<\/code> with arbitrary peers without prior connection), we must perform 'lazy' (deferred) connection within the API.<\/p>\n<p>Here\u2019s what happens during a standard data exchange between a 'server' and a 'client' when using UDP, and what our library needs to perform:<\/p>\n<ul>\n<li>The server calls <code>bind()<\/code>, to inform the operating system that it wants to receive packets on a specified port.<\/li>\n<\/ul>\n<p>\nInstead, we will publish the open port in Firebase under the server key and listen for events in its subtree.<\/p>\n<ul>\n<li>The server calls <code>recvfrom()<\/code>, receiving packets sent from any host to this port.<\/li>\n<\/ul>\n<p>\nIn our case, we need to check the incoming packet queue sent to this port.<\/p>\n<p>Each port has its own queue, and we add the source and destination ports to the beginning of WebRTC datagrams so we know which queue to redirect a new packet to upon arrival.<\/p>\n<p>The call is non-blocking, so if there are no packets, we simply return -1 and set <code>errno=EWOULDBLOCK<\/code>.<\/p>\n<ul>\n<li>The client obtains the server's IP and port by some external means and calls <code>sendto()<\/code>. An internal call is also made at this point, so the subsequent <code>bind()<\/code>request proceeds. <code>recvfrom()<\/code> will receive a response without explicit binding execution.<\/li>\n<\/ul>\n<p>\nIn our case, the client externally obtains a string key and uses the function <code>resolve()<\/code> to obtain the IP address.<\/p>\n<p>At this stage, we initiate the WebRTC handshake if the two peers are not yet connected to each other. Connections to different ports of a single peer use the same WebRTC DataChannel.<\/p>\n<p>We also perform an indirect <code>bind()<\/code>, so that the server can restore the connection in the next <code>sendto()<\/code> in case it was closed for any reason.<\/p>\n<p>The server is notified of the client's connection when the client records its SDP offer under the server port information in Firebase, and the server responds there with its own response.<\/p>\n<p>\nThe diagram below shows an example of message flow for the socket scheme and the transmission of the first message from the client to the server:<\/p>\n<p><img decoding=\"async\" alt=\"We are porting a multiplayer game from C++ to the web with Cheerp, WebRTC, and Firebase\" src=\"\/wp-content\/uploads\/2019\/09\/7dc48e816c09e33daf28c307cad0c258.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<i>The complete connection stage scheme between the client and the server.<\/i><\/p>\n<h1>Conclusion<\/h1>\n<p>\nIf you have read this far, you are probably interested in seeing the theory in action. You can play the game on <noindex><a rel=\"nofollow\" href=\"https:\/\/teeworlds.leaningtech.com\">teeworlds.leaningtech.com<\/a><\/noindex>, give it a try!<\/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>A friendly match between colleagues.<\/i><\/p>\n<p>The code for the networking library is freely available at <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/leaningtech\/cheerpnet\">Github<\/a><\/noindex>. Join the conversation on our channel at <noindex><a rel=\"nofollow\" href=\"https:\/\/gitter.im\/leaningtech\/cheerp\">Gitter<\/a><\/noindex>!<br \/>\n<br \/>Source: <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.2 - 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\/en\/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.2\" \/>\n\t\t<meta property=\"og:locale\" content=\"en_US\" \/>\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\/en\/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\udd47Porting a multiplayer game from C++ to the web with Cheerp, WebRTC, and Firebase | ProHoster","description":"Introduction Our company Leaning Technologies provides solutions for porting traditional desktop applications to the web.","canonical_url":"https:\/\/prohoster.info\/en\/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":"en_US","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\/en\/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\/en\/wp-json\/wp\/v2\/posts\/38370","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/comments?post=38370"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/posts\/38370\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/media\/28801"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/media?parent=38370"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/categories?post=38370"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/tags?post=38370"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}