lib-server
extensionThe Websocket library extension (which requires the HTTP extension) adds support for the Websocket Protocol, allowing real-time communication for web-applications (including browser based clients an).
The Websocket extension is comprised of two main parts:
[The Websocket Protocol (struct HttpProtocol
) global state].
[The Websocket Connection (struct HttpProtocol
) instance state].
The following information is a quick guide. For updated and full information, read the code and it's comments.
At the end of this file you will find a short example for a simple “Hello World” web service.
To implement the Websocket protocol in a way that is optimized for your application, such as securing limits for incoming message sizes etc', some settings are available.
These settings can be set at any time, but will only take affect after they had been set.
The global settings are available using the global Websocket
object:
extern struct { int max_msg_size; // Sets the (global) maximum websocket message size/buffer per client. unsigned char timeout; // Sets the (global) timeout for websocket connections (ping). // ... } Websocket;
max_msg_size
global propertyWebsocket messages don't have a practical limitation on their size, much like HTTP uploads. This could expose Websocket services to DoS attacks that upload large amounts of data and cause resource starvation.
The max_msg_size
allows us to set a practical global (and dynamic) limit on incoming Websocket messages.
To set a limit, set max_msg_size
to the maximum number of Bytes allowed per message.
The default limit value is ~256KB (Websocket.max_msg_size = 262144
).
Use:
Websocket.max_msg_size = 1024*1024*2 // ~2Mb
Please note that this limit is the limit per message. It is easy to stream a 200Gb file to the server by using fragmentation, i.e. uploading 128Kb at a time and having the server merge the pieces of the data together after they were received (perhaps saving them to a temporary file). This approach of using fragmentation is safer and it allows easier recovery from disconnections (by sending only the missing data).
timeout
semi-global propertyWebsocket connections are designed to persist over time, but servers, routers and all intermediaries have an interest in clearing out stale connections and preventing network errors (i.e. half closed sockets) from causing system resource starvation (the number of allowed connections on a system is always limited and each open connection, active or not, takes system resources).
This interest means that most systems have a timeout setup. If no data was sent or received during the timeout period, the connection is assumed to have died and is forcefully closed.
i.e. Heroku's, router sets the first timeout window to 30 seconds (usually the HTTP response / upgrade timeout) and 55 seconds for later timeout windows, forcing Websocket connections to close after 55 seconds of inactivity.
The timeout
semi-global property allows us to deal with both the issue of stale connections and the issue of our “host” system's timeout limits.
Instead of closing the connection automatically, the timeout
property causes a Websocket ping
to be sent. If the connection was lost, this will cause the TCP/IP connection chain to collapse (intermediaries that fail to forward the packet will shutdown the connection). Once the TCP/IP connection chain collapsed, future attempt to write
to the Websocket (including ping
packets) will fail and flag the connection for closure.
This allows us to use timeout
both to keep a connection alive and to close “dead” (half-closed) connections.
Use:
// Heroku's timeout is 55 seconds, but we should keep a safe distance. Websocket.timeout = 45
It should be noted that timeout
uses fuzzy timeout counting that might be enforced a second or two later then expected (and maybe later then the 2 second fuzzy window when the server experiences heavy load).
Also, the timeout
property isn't strictly global. Each new Websocket connection inherits the timeout
property value. Dynamic updates effect only new connections.
websocket_upgrade(...)
This macro is actually a short cut for the Websocket.upgrade
function and allows to easily pass arguments to the function.
#define websocket_upgrade(...) \ Websocket.upgrade((struct WebsocketSettings){__VA_ARGS__})
To Upgrade a connection, the minimal requirement is to pass a pointer to the HttpRequest
object and a callback that handles any on_message
The on_message
callback should expect 4 arguments:
ws_s* ws
- a pointer to the Websocket connection instance.char* buffer
- the buffer containing the Websocket message. This buffer will be automatically recycled once the on_message
callback returns.size_t size
- the amount of data present in the buffer.int is_text
- a flag indicating if the data is valid UTF-8 encoded text (is_text == 1
) or binary data (is_text==0
).All other callbacks, such as the on_open
, on_close
and on_shutdows
take only a single argument, the ws_s *
.
Other, optional, settings are defined under the WebsocketSettings
struct:
struct WebsocketSettings { /** The on_message callback will be called whenever a websocket message is received for this connection. The data received points to the websocket's message buffer and it will be overwritten once the function exits (it cannot be saved for later, but it can be copied). */ void (*on_message)(ws_s* ws, char* data, size_t size, int is_text); /** The (optional) on_open callback will be called once the websocket connection is established. */ void (*on_open)(ws_s* ws); /** The (optional) on_shutdown callback will be called if a websocket connection is still open while the server is shutting down (called before `on_close`). */ void (*on_shutdown)(ws_s* ws); /** The (optional) on_close callback will be called once a websocket connection is terminated or failed to be established. */ void (*on_close)(ws_s* ws); /** The (required) HttpRequest to be converted ("upgraded") to a websocket * connection. */ struct HttpRequest* request; /** The (optional) HttpResponse to be used for sending the upgrade response. Using this object allows cookies to be set before "upgrading" the connection. The ownership of the response object will remain unchanged - so if you have created the response object, you should free it. */ struct HttpResponse* response; /** Opaque user data. */ void* udata; };
void* Websocket.get_udata(ws_s* ws)
Returns the opaque user data associated with the websocket.
The opaque data can be set either before or after a connection was established.
void* Websocket.set_udata(ws_s* ws, void* udata)
Sets the opaque user data associated with the websocket. returns the old value, if any.
server_pt Websocket.get_server(ws_s* ws)
Returns the the server_pt
for the Server object that owns the connection. This allows access to the underlying lib-server
API.
uint64_t Websocket.get_uuid(ws_s* ws)
Returns the the connection‘s UUID. This is the same as the lib-server
assigned UUID and can be used to extract the fd
for the underlying socket (which you probably don’t want to do).
int Websocket.write(ws_s* ws, void* data, size_t size, char is_text)
Writes data to the websocket. Returns -1 on failure (0 on success).
Set the is_text
argument to 1 if the data is a valid UTF-8 encoded text string. To send binary data, set the is_text
argument to 0.
void Websocket.close(ws_s* ws)
Closes a websocket connection.
void Websocket.each(...)
Performs a task on each Websocket connection that shares the same process, except the Websocket connection pointed to by ws_originator
.
The full function arguments are:
void Websocket.each(ws_s* ws_originator, void (*task)(ws_s* ws_target, void* arg), void* arg, void (*on_finish)(ws_s* ws_originator, void* arg));
ws_originator
can be NULL if the task should be performed on all the connections, including the one scheduling the task.
long Websocket.count(ws_s* ws);
Counts the number of websocket connections in the process (forked processes are ignored).
Here's a simple “Hello World” using the Http extensions:
#include "websockets.h" // auto-includes "http.h" // Concurrency using thread? How many threads in the thread pool? #define THREAD_COUNT 4 // Concurrency using processes? (more then a single process can be used) #define PROCESS_COUNT 1 // Our websocket service - echo. void ws_echo(ws_s* ws, char* data, size_t size, int is_text) { // echos the data to the current websocket Websocket.write(ws, data, size, is_text); } // our HTTP callback - hello world. void on_request(struct HttpRequest* request) { if (request->upgrade) { // "upgrade" to the Websocket protocol, if requested websocket_upgrade(.request = request, .on_message = ws_echo); return; } struct HttpResponse* response = HttpResponse.create(request); HttpResponse.write_body(response, "Hello World!", 12); HttpResponse.destroy(response); } // Our main function will start the HTTP server int main(int argc, char const* argv[]) { start_http_server(on_request, NULL, .port = "8080", // default is "3000" .threads = THREAD_COUNT, .processes = PROCESS_COUNT ); return 0; }