Introduction to Emscripten Sockets
~ 5 Minute Read.
Since we’re working on our multiplayer WebXR game at Vhite Rabbit, I needed to somehow connect to some server to exchange some data via sockets from web assembly.
Our server uses the Poco library (because I had already used it with pyromania), hence I will go into detail with that a bit also.
Beware that to fit this into the scope of a daily blog post, I copied quite a bit of code that I had already written. That code makes use of some Corrade library features and you will have to adapt that if you don’t use that. I can highly recommend it, though.
Sockets are not WebSockets
The first thing you need to realize is that while websockets are sockets, the reverse does not hold. They are _not_ equivalent. WebSockets exchange a handshake via http requests, which the server needs to handle correctly.
You can therefore not connect to any arbitrary server using websockets. The server must support it. You have two ways of doing so: either have the server support websockets or use websockify which I understand as a proxy to another server, handling the websocket layer on top of it. 1
To set up a server that supports WebSockets with Poco, check out their example.
“binary” subprotocol
Emscripten uses the binary subprotocol of WebSockets. You will need to handle that at application level by setting the appropriate header for the WebSocket response like this:
// Emscripten uses the binary subprotocol response.set("Sec-WebSocket-Protocol", "binary"); Poco::Net::WebSocket ws(request, response);
Sockets in Emscripten
First thing you’ll want to do is enably the nifty SOCKET_DEBUG
flag for debug builds. In
CMake you would do this by adding the following lines:
# Some handy debug flags set_property(TARGET your-target APPEND_STRING PROPERTY LINK_FLAGS_DEBUG " -s SOCKET_DEBUG=1")
Since emscripten sockets are a wrapper around WebSockets to mimic the Linux system sockets API,
you can pretty much use them like that. Be aware, though, that the connection will not immediately
succeed, but will return EINPROGRESS
which means you should wait for the connection to be completed.
Connecting
You can now start by connecting a socket like in the following code snippet: 2
#include <errno.h> /* EINPROGRESS, errno */ #include <sys/types.h> /* timeval */ #include <sys/socket.h> #include <arpa/inet.h> #include <fcntl.h> #include <unistd.h> /* ... */ struct Socket::SocketData { int socket; }; /* class Socket { ... private: SocketData _socket; bool _connected; }; */ void Socket::connect(const std::string& host, int port) { _data->socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); if(_data->socket == -1) { Error() << "Socket::connect(): failed to create socket"; return; } fcntl(_data->socket, F_SETFL, O_NONBLOCK); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(port); if(inet_pton(AF_INET, host.c_str(), &addr.sin_addr) != 1) { Error() << "Socket::connect(): inet_pton failed"; return; } const int res = ::connect(_data->socket, (struct sockaddr *)&addr, sizeof(addr)); if(res == -1) { if(errno == EINPROGRESS) { Debug() << "Socket::connect(): Connection in progress for fd:" << _data->socket; /* Wait for connection to complete */ fd_set sockets; FD_ZERO(&sockets); FD_SET(_data->socket, &sockets); /* You should probably do other work instead of busy waiting on this... or set a timeout or something */ while(select(_data->socket + 1, nullptr, &sockets, nullptr, nullptr) <= 0) {} connected = true; } else { Error() << "Socket::connect(): connection failed"; return; } } else { connected = true; } }
I’m using Corrade::Utility::Debug output here, but you may use whatever you prefer of course.
Sending
Sending also matches how you would use sockets on desktop. As I’m not a net programming expert, I advise you to seek out other tutorials and resources if you need to build something more stable with proper error handling.
void Socket::send(Containers::ArrayView<char> data) { CORRADE_ASSERT(_connected, "Socket::send(): socket not connected", ); const int ret = ::send(_data->socket, data, data.size(), 0); if(ret == -1) { Error() << "Socket::send(): send failed"; close(); return; } }
Receiving
Similar to sending, this is not too special. I’m using Corrade::Containers::Array and ArrayView here to handle the buffers.
Containers::ArrayView<char> Socket::receive(Containers::Array<char>& dest, int timeout) { CORRADE_ASSERT(_connected, "Socket::receive(): socket not connected", {}); /* Wait timeout milliseconds to receive data */ fd_set sockets; FD_ZERO(&sockets); FD_SET(_data->socket, &sockets); timeval t{0, timeout*1000}; int ret = select(_data->socket + 1, &sockets, nullptr, nullptr, (timeout == -1) ? nullptr : &t); if(ret == 0) { /* Timeout */ return nullptr; } else if(ret < 0) { Utility::Error() << "Socket::receive(): select failed"; return nullptr; } ret = recv(_data->socket, dest.data(), dest.size(), 0); if(ret < 0) { Utility::Error() << "Socket::receive(): recv failed"; return nullptr; } return dest.prefix(ret); }
Closing
Closing the socket when you’re done is a simple matter of:
void Socket::close() { if(_data->socket != -1) ::close(_data->socket); }
Conclusion
WebSocket connections need to be handled differently on the server side. Once that’s done, emscripten has a nice wrapper around websockets to that you can use them as you would use normal sockets.
As a result the only documentation I found on emscripten sockets was the tests they had. Instead check out the Linux Programmer’s Manual if you need any help.
I hope this helped out someone, it did take us at Vhite Rabbit a while to figure out. Special thanks go to Andrea Capobianco who spent quite some time on debugging the above code.
- 1
- Disclaimer: I have no experience with websockify.
- 2
- Which will not trivially compile, but you’ll figure it out from here easily.
Written in 60 minutes, not edited yet and the read time is a bold guess.