We port a multiplayer game from C ++ to the web with Cheerp, WebRTC and Firebase

Introduction



Our company Leaning Technologies provides solutions for porting traditional desktop applications to the web. Our C ++ Cheerp compiler generates a combination of WebAssembly and JavaScript, which provides both easy browser interaction and high performance.



As an example of its application, we decided to port a multiplayer game for the web and chose Teeworlds for this. Teeworlds is a multi-player, two-dimensional retro game with a small but active community of players (including me!). It is small in terms of both downloadable resources and CPU and GPU requirements - an ideal candidate.









Works in the Teeworlds browser



We decided to use this project to experiment with general solutions for porting network code to the web . This is usually done in the following ways:





Both solutions require hosting the server component on the server side, and none of them allows you to use UDP as the transport protocol. This is important for real-time applications such as video conferencing and gaming software, because delivery guarantees and TCP packet ordering can interfere with low latencies.



There is a third way - use the network from a browser: WebRTC .



RTCDataChannel supports both reliable and unreliable transmission (in the latter case, if possible, it tries to use UDP as the transport protocol), and can be used both with a remote server and between browsers. This means that we can port the entire application to the browser, including the server component!



However, this is an additional difficulty: before the two WebRTC peers can exchange data, they need to perform a relatively complicated handshake procedure for connection, which requires several third-party entities (a signal server and one or more STUN / TURN servers).



Ideally, we would like to create a network API internally using WebRTC, but as close as possible to the UDP Sockets interface, which does not need to establish a connection.



This will allow us to take advantage of WebRTC without the need to disclose complex details to the application code (which we wanted to change as little as possible in our project).



Minimum WebRTC



WebRTC is a set of APIs available in browsers that provides peer-to-peer audio, video, and arbitrary data transfer.



The connection between the peers is established (even if there is NAT on one or both sides) using the STUN and / or TURN servers through a mechanism called ICE. Peers exchange ICE information and channel parameters through the SDP offer and answer protocol.



Wow! How many abbreviations at a time. Let's briefly explain what these concepts mean:





To create such a connection, peers need to collect the information they received from the STUN and TURN servers and exchange it with each other.



The problem is that they do not yet have the ability to exchange data directly, so there must be an out-of-band mechanism for exchanging this data: a signal server.



A signal server can be very simple, because its only task is to redirect data between peers at the “handshake” stage (as shown in the diagram below).









WebRTC Simplified Handshake Sequence



Teeworlds Network Model Overview



The network architecture of Teeworlds is very simple:





Thanks to the use of WebRTC for data exchange, we can transfer the server component of the game to the browser where the client is located. It gives us a great opportunity ...



Get rid of servers



The 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, thus ensuring fast downloads and high uptime for free. In fact, it will be possible to forget about them, and if we are lucky and the game becomes popular, then the infrastructure will not have to be modernized.



However, for the system to work, we still have to use an external architecture:





We decided to use Google’s free STUN servers, and deployed one TURN server on our own.



For the last two points we used Firebase :











List of servers inside the game and on the home page



Socket Implementation



We want to create an API as close as possible to Posix UDP Sockets to minimize the number of changes needed.



We also want to implement the necessary minimum required for the simplest data exchange over the network.



For example, we do not need real routing: all peers are in the same “virtual LAN” associated with a specific instance of the Firebase database.



Therefore, we do not need unique IP addresses: for unique identification of peers, it is sufficient to use unique values ​​of Firebase keys (similar to domain names), and each peer locally assigns “fake” IP addresses to each key that needs to be converted. This completely eliminates the need for a global IP address assignment, which is a non-trivial task.



Here is the minimum API we need to implement:



// Create and destroy a socket int socket(); int close(int fd); // Bind a socket to a port, and publish it on Firebase int bind(int fd, AddrInfo* addr); // Send a packet. This lazily create a WebRTC connection to the // peer when necessary int sendto(int fd, uint8_t* buf, int len, const AddrInfo* addr); // Receive the packets destined to this socket int recvfrom(int fd, uint8_t* buf, int len, AddrInfo* addr); // Be notified when new packets arrived int recvCallback(Callback cb); // Obtain a local ip address for this peer key uint32_t resolve(client::String* key); // Get the peer key for this ip String* reverseResolve(uint32_t addr); // Get the local peer key String* local_key(); // Initialize the library with the given Firebase database and // WebRTc connection options void init(client::FirebaseConfig* fb, client::RTCConfiguration* ice);
      
      





The API is simple and similar to the Posix Sockets API, but it has several important differences: registering callbacks, assigning local IPs and a lazy connection .



Callback Registration



Even if the source program uses non-blocking I / O, the code needs to be refactored to run in a web browser.



The reason for this is that the event loop in the browser is hidden from the program (be it JavaScript or WebAssembly).



In a native environment, we can write code this way



 while(running) { select(...); // wait for I/O events while(true) { int r = readfrom(...); // try to read if (r < 0 && errno == EWOULDBLOCK) // no more data available break; ... } ... }
      
      





If the event loop is hidden for us, then we need to turn it into something like this:



 auto cb = []() { // this will be called when new data is available while(true) { int r = readfrom(...); // try to read if (r < 0 && errno == EWOULDBLOCK) // no more data available break; ... } ... }; recvCallback(cb); // register the callback
      
      





Local IP Assignment



The identifiers of the nodes in our “network” are not IP addresses, but Firebase keys (these are lines that look like this: -LmEC50PYZLCiCP-vqde



).



This is convenient because we do not need a mechanism to assign IPs and verify their uniqueness (as well as their disposal after disconnecting the client), but it is often necessary to identify peers by a numerical value.



For this, the resolve



and reverseResolve



functions are used: the application somehow gets the string value of the key (through user input or through the master server), and can convert it to an IP address for internal use. The rest of the API also receives this value instead of a string for simplicity.



This is similar to a DNS lookup, only performed locally at the client.



That is, IP addresses cannot be shared between different clients, and if you need some kind of global identifier, you will have to generate it in a different way.



Lazy mix



UDP does not need a connection, but, as we saw, before starting data transfer between two peers, WebRTC requires a long connection process.



If we want to provide the same level of abstraction, ( sendto



/ recvfrom



with arbitrary peers without first connecting), then we must make a “lazy” (delayed) connection inside the API.



Here is what happens during the normal exchange of data between the "server" and the "client" in the case of using UDP, and what our library should do:





Instead, we will publish the open port in Firebase under the server key and listen to events in its subtree.





In our case, we need to check the incoming queue of packets sent to this port.



Each port has its own queue, and we add the source and destination ports to the beginning of the WebRTC datagrams to know which queue to redirect when a new packet arrives.



The call is non-blocking, so if there are no packets, we simply return -1 and set errno=EWOULDBLOCK



.





In our case, the client externally receives the string key and uses the resolve()



function to obtain the IP address.



At this point, we begin the “handshake” of WebRTC if the two peers are not yet connected to each other. Connections to different ports of the same peer use the same DataRannel WebRTC.



We also do indirect bind()



so that the server can reconnect in the next sendto()



in case it sendto()



for some reason.



The server is notified of the client connecting when the client writes its SDP offer under the server port information in Firebase, and the server responds with its own response.






The diagram below shows an example of the movement of messages for a socket scheme and the transmission of the first message from the client to the server:









Complete connection step diagram between client and server



Conclusion



If you have read to the end, then you are probably interested in looking at the theory in action. The game can be played on teeworlds.leaningtech.com , try it!





Friendly match between colleagues



The network library code is freely available on Github . Join chatting on our channel in Gitter !



All Articles