This is a robust and performance-oriented WebSockets library written in C. It provides a simple yet flexible API for building WebSocket clients and servers. It supports all standard WebSocket features including text and binary messages, ping/pong frames, control frames and includes built-in OpenSSL support.
The motivation behind the project is to have a portable WebSockets client library under a permissive license (MIT) which feels like a traditional socket API (blocking with optional timeout) which can also provide a foundation for some additional messaging features similar to (but lighter weight than) AMQP and MQTT.
The code compiles and runs on Linux, FreeBSD, NetBSD, OpenBSD, OS X, Illumos/Solaris and Windows. It is fully commented and well-documented. Furthermore, the code is under a permissive license (MIT) allowing its use in commercial (closed-source) applications. The build system (CMake) includes built-in support to for cross-compiling from Linux/BSD to Windows, provided MinGW compiler and tools are installed on the host system.
There are two parts to the library: a client-side component and an optional server-side component. The two are built from completely different networking designs, each suited to their particular use-cases. The client architecture is designed for single connections and operates synchronously, waiting for responses from the server. The server architecture is designed for many concurrent connections and operates asynchronously.
The client-side API is simple and flexible. Connections wait (block) for
responses and can employ a timeout in which to take action if a response does
not arrive in a timely manner (or at all). The API is threadsafe in so far as
each connection must be maintained in its own thread. All global structures and
common services (error-reporting and tracing) use thread-local variables. The
API runs atop the native operating system’s networking facilities, using
poll()
and thus no additional libraries are required.
The server-side API implements a non-blocking, multiplexing, multithreaded
server atop libuv
. The server consists of a main
networking thread and a pool of worker threads that process the data. The
networking thread runs the libuv
loop to handle socket I/O and evenly
distributes incoming data to the worker threads via a synchronized queue. The
worker threads process the data and optionally send back replies via a separate
queue. The server takes care of all the WebSocket protocol serialization and
communication between the the network and worker threads. Developers only need
to focus on the actual message processing logic to service incoming messages.
The requirement of libuv
is what makes the server component optional. While
libuv
runs on every major operating system, it is not expected to be a
requirement of this library, as its original intent was to provide client-side
connections only. Thus if you want use the server-side API, you simply add a
configuration switch -DBUILD_SERVER=1
at build time to include the code
WebSockets significantly enhance the capabilities of web applications compared to standard HTTP or raw TCP connections. They enable real-time data exchange with reduced latency due to the persistent connection, making them ideal for applications like live chat, gaming, real-time trading, and live sports updates.
Several large-scale applications and platforms utilize WebSockets today, including Slack, WhatsApp, and Facebook for real-time messaging. WebSockets are also integral to the functionality of collaborative coding platforms like Microsoft's Visual Studio Code Live Share. On the server-side, many popular software systems support WebSocket, including Node.js, Apache, and Nginx.
WebSockets emerged in the late 2000’s in response to the growing need for real-time, bidirectional communication in web applications. The goal was to provide a standardized way for web servers to send content to browsers without being prompted by the user, and vice versa. In December 2011 they were standardized by the Internet Engineering Task Force (IETF) in RFC 6455. They now enjoy wide support and integration in modern browsers, smartphones, IoT devices and server software. They have become a fundamental technology in modern web applications.
Unlike traditional HTTP connections, which are stateless and unidirectional, WebSocket connections are stateful and bidirectional. The connection is established through an HTTP handshake (HTTP Upgrade request), which is then upgraded to a WebSocket connection if the server supports it. The connection remains open until explicitly closed, enabling low-latency data exchange.
The WebSockets protocol communicates through a series of data units called frames. Each WebSocket frame has a maximum size of 2^64 bytes (but the actual size limit may be smaller due to network or system constraints). There are several types of frames, including text frames, binary frames, continuation frames, and control frames.
Text frames contain Unicode text data, while binary frames carry binary data. Continuation frames allow for larger messages to be broken down into smaller chunks. Control frames handle protocol-level interactions and include close frames, ping frames, and pong frames. The close frame is used to terminate a connection, ping frames are for checking the liveness of the connection, and pong frames are responses to ping frames.
For working examples beyond that shown here, see the test_websocket.c
file in
the src/test
directory. After building the project, step into that directory
and run ./server
which starts a simple websocket server. Then run
test_websocket
.
The WebSockets API is built solely upon WebSockets constructs: frames, messages and connections, as you would expect. It intuitively follows the concepts and structure laid out in the standard. The following is a basic example of the Websockets API:
#include <vws/websocket.h>
int main(int argc, const char* argv[])
{
// Create connection object
vws_cnx* cnx = vws_cnx_new();
// Set connection timeout to 2 seconds (the default is 10). This applies
// both to connect() and to read operations (i.e. poll()).
vws_socket_set_timeout((vws_socket*)cnx, 2);
// Connect. This will automatically use SSL if "wss" scheme is used.
cstr uri = "ws://localhost:8181/websocket";
if (vws_connect(cnx, uri) == false)
{
printf("Failed to connect to the WebSocket server\n");
vws_cnx_free(cnx);
return 1;
}
// Can check connection state this way. Should always be true here as we
// just successfully connected.
assert(vws_socket_is_connected((vws_socket*)cnx) == true);
// Enable tracing. This will dump frames to the console in human-readable
// format as they are sent and received.
vws.tracelevel = VT_PROTOCOL;
// Send a TEXT frame
vws_frame_send_text(cnx, "Hello, world!");
// Receive websocket message
vws_msg* reply = vws_msg_recv(cnx);
if (reply == NULL)
{
// There was no message received and it resulted in timeout
}
else
{
// Free message
vws_msg_free(reply);
}
// Send a BINARY message
vws_msg_send_binary(cnx, (ucstr)"Hello, world!", 14);
// Receive websocket message
reply = vws_msg_recv(cnx);
if (reply == NULL)
{
// There was no message received and it resulted in timeout
}
else
{
// Free message
vws_msg_free(reply);
}
vws_disconnect(cnx);
return 0;
}
The Messaging API is built on top of the WebSockets API. While WebSockets
provide a mechanism for real-time bidirectional communication, it doesn't
inherently offer things like you would see in more heavyweight message protocols
like AMQP. The Messaging API provides a small step in that direction, but
without the heft. It mainly provides a more structured message format with
built-in serialization. The message structure includes two maps (hashtables of
string key/value pairs) and a payload. One map, called routing
, is designed to
hold routing information for messaging applications. The other map, called
headers
, is for application use. The payload can hold both text and binary
data.
The message structure operates with a higher-level connection API which works
atop the native WebSocket API. The connection API mainly adds support to send
and receive the messages, automatically handling serialization and
deserialization on and off the wire. It really just boils down to send()
and
receive()
calls which operate with these messages.
Messages can be serialized in two formats: JSON and MessagePack. Both formats can be sent over the same connection on a message-by-message basis. That is, the connection is able to auto-detect each incoming message's format and deserialize accordingly. Thus connections support mixed-content messages: JSON and MessagePack.
The following is a basic example of using the high-level messaging API.
#include <vws/message.h>
int main()
{
// Create connection object
vws_cnx* cnx = vws_cnx_new();
// Connect. This will automatically use SSL if "wss" scheme is used.
cstr uri = "ws://localhost:8181/websocket";
if (vws_connect(cnx, uri) == false)
{
printf("Failed to connect to the WebSocket server\n");
vws_cnx_free(cnx);
return 1;
}
// Enable tracing. This will dump frames to the console in human-readable
// format as they are sent and received.
vws.tracelevel = VT_PROTOCOL;
// Create
vrtql_msg* request = vrtql_msg_new();
vrtql_msg_set_routing(request, "key", "value");
vrtql_msg_set_header(request, "key", "value");
vrtql_msg_set_content(request, "payload");
// Send
if (vrtql_msg_send(cnx, request) < 0)
{
printf("Failed to send: %s\n", vws.e.text);
vrtql_msg_free(request);
vws_cnx_free(cnx);
return 1;
}
// Receive
vrtql_msg* reply = vrtql_msg_recv(cnx);
if (reply == NULL)
{
// There was no message received and it resulted in timeout
}
else
{
// Free message
vrtql_msg_free(reply);
}
// Cleanup
vrtql_msg_free(request);
// Diconnect
vws_disconnect(cnx);
// Free the connection
vws_cnx_free(cnx);
return 0;
}
The server API is layered and works the same for all media formats: binary data, WebSockets, HTTP and VRTQL messages. Once you understand how the basic API works, everything else is intuitive. The API is simple and designed to make it very easy for you to focus on processing messages. There are only a few steps required to create a server. You create a server instance, define a processing function to handle incoming data, and send data back to the remote peer. We will take each of these in turn.
You create a server instance using the vrtql_svr_new()
function. This takes
three arguments: the number of worker threads, the connection backlog, and the
maximum message queue size. If you set the latter two arguments to zero, it will
use the default values. Next, you create a processing function. The signature of
this function varies according to the server. For the core server, which deals
with unstructured data, this signature is given by the vrtql_svr_process_data
callback. It takes a two arguments. The first argument is a vrtql_svr_data
instance, created on the heap, which simply holds a blob of data. It is up to
your processing function to make sense of that data and respond accordingly.
The second argument is the worker thread context, which allows you to create
application-specific envrionment for processing. Since the server uses a thread
pool of worker threads for processing, you may want to have some context or
environment for your processing available. This is the job of the worker_ctor
,
worker_ctor_data
and worker_dtor
members. The worker_ctor
constructs the
user-defined context which is passed into the processing function as the last
argument. The worker_ctor_data
is user-defined data passed into the
worker_ctor
function to assist setting up the environment. Finally the
worker_dtor
is called on worker thread shutdown and passed the context
returned by worker_ctor
for cleanup. All of these are optional, but if you
have any processing that requires things like dedicated database connections or
other environmental resources specific to the thread envrionment, they can be
very useful.
If you need to send data back to the peer, you do so using
vrtql_svr_send()
. With all these things in place, you call vrtql_svr_run()
to start the server.
The following illustrates writing a basic echo server:
#include <vws/server.h>
cstr server_host = "127.0.0.1";
int server_port = 8181;
/* Stuff you want allocated in every worker thread for you to do your
** application-specific processing.
*/
type struct my_env
{
void* thingy;
} my_env;
void process(vws_svr_data* req, void* ctx)
{
vws.trace(VL_INFO, "process (%p)", req);
// Instance of your my_env for this specific worker thread
my_env* env = (my_env*)ctx;
vws_tcp_svr* server = req->server;
//> Prepare the response: echo the data back
// Allocate memory for the data to be sent in response
char* data = (char*)vws.malloc(req->size);
// Copy the request's data to the response data
strncpy(data, req->data, req->size);
// Create response
vws_svr_data* reply;
reply = vws_svr_data_own(req->server, req->cid, (ucstr)data, req->size);
// Free request
vws_svr_data_free(req);
if (vws.tracelevel >= VT_APPLICATION)
{
vws.trace(VL_INFO, "process(%lu): %i bytes", reply->cid, reply->size);
}
// Send reply. This will wakeup network thread.
vws_tcp_svr_send(reply);
}
// Allocate context for worker thread
void* worker_thread_startup(void* data)
{
// Worker thread specific initialization
vrtql_svr* server = (vrtql_svr*)data;
my_env* env = (my_env*)malloc(sizeof(my_env));
env->thingy = malloc(1);
return env;
}
// Deallocate context for worker thread
void worker_thread_shutdown(void* data)
{
// Worker thread specific cleanup
my_env* env = (my_env*)data;
free(env->thingy);
free(env);
}
int main(int argc, const char* argv[])
{
// Run server with 10 worker threads, default TCP listen backlog (128) and
// default work queue size (1024 pending requests).
vrtql_svr* server = vrtql_svr_new(10, 0, 0);
server->on_data_in = process;
vws.tracelevel = VT_THREAD;
// Worker thread context
server->worker_ctor = worker_thread_startup;
server->worker_ctor_data = server;
server->worker_dtor = worker_thread_shutdown;
// Run
vrtql_svr_run(server, server_host, server_port);
// Shutdown
vrtql_svr_stop(server);
uv_thread_join(&server_tid);
vrtql_svr_free(server);
}
Writing a WebSocket server is even simpler. It follows the same pattern but uses
vws_svr_new()
to create the server. The processing function signature is given
by the vws_svr_process_msg
callback. Rather than using unstructured data, it
operates on WebSocket messages.
The following illustrates writing a WebSocket server:
#include <vws/server.h>
cstr server_host = "127.0.0.1";
int server_port = 8181;
// Server function to process messages. Runs in context of worker thread.
void process(vws_svr* s, vws_cid_t cid, vws_msg* m, void* ctx)
{
vws.trace(VL_INFO, "process_message (%ul) %p", cid, m);
// Echo back. Note: You should always set reply messages format to the
// format of the connection.
// Create reply message
vws_msg* reply = vws_msg_new();
// Use same format
reply->opcode = m->opcode;
// Copy content
vws_buffer_append(reply->data, m->data->data, m->data->size);
// Send. We don't free message as send() does it for us.
s->send(s, cid, reply, NULL);
// Clean up request
vws_msg_free(m);
}
int main(int argc, const char* argv[])
{
// Setup
vws_svr* server = vws_svr_new(10, 0, 0);
server->process = process;
// Run
vrtql_tcp_svr_run((vrtql_svr*)server, server_host, server_port);
// Shutdown
vrtql_svr_stop((vrtql_svr*)server);
uv_thread_join(&server_tid);
vws_svr_free(server);
vws_cleanup();
}
Additionally, the framework includes operating as a pure HTTP server. If HTTP
requests come in which are not WebSocket upgrades, the framework will attempt to
pass the request to a user-defined handler process_http
. If this is defined
then that callback will be invoked with the HTTP request passed to it. The
following is an example of running a pure HTTP server.
#include <vws/server.h>
cstr server_host = "127.0.0.1";
int server_port = 8181;
// Server function to process HTTP messages. Runs in context of worker thread.
bool process(vws_svr* s, vws_cid_t cid, vws_http_msg* msg, void* ctx)
{
vws_tcp_svr* server = (vws_tcp_svr*)s;
if (vws.tracelevel >= VT_APPLICATION)
{
vws.trace(VL_INFO, "server: process (%ul) %p", cid, msg);
}
cstr response = "HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: 12\r\n"
"\r\n"
"Hello world";
// Allocate memory for the data to be sent in response
char* data = (char*)vws.strdup(response);
// Create response
vws_svr_data* reply = vws_svr_data_own(server, cid, (ucstr)data, strlen(data));
// Send reply. This will wakeup network thread.
vws_tcp_svr_send(reply);
return true;
}
int main(int argc, const char* argv[])
{
// Setup
vws_svr* server = vws_svr_new(10, 0, 0);
server->process_http = process;
// Run
vrtql_tcp_svr_run((vrtql_svr*)server, server_host, server_port);
// Shutdown
vrtql_svr_stop((vrtql_svr*)server);
uv_thread_join(&server_tid);
vws_svr_free(server);
vws_cleanup();
}
This can run in tandem with the WebSocket server as well. As long as you provide the appropriate callbacks, the framework will call the corresponding callback based on the context.
Finally, the Message API server works in exactly the same way. The only
difference is that it operates on vrtql_msg
messages.
The following illustrates creating a Message server:
#include <vws/server.h>
cstr server_host = "127.0.0.1";
int server_port = 8181;
// Server function to process messages. Runs in context of worker thread.
void process(vws_svr* s, vws_cid_t cid, vrtql_msg* m, void* ctx)
{
vrtql_msg_svr* server = (vrtql_msg_svr*)s;
vws.trace(VL_INFO, "process (%lu) %p", cid, m);
// Echo back. Note: You should always set reply messages format to the
// format of the connection.
// Create reply message
vrtql_msg* reply = vrtql_msg_new();
reply->format = m->format;
// Copy content
ucstr data = m->content->data;
size_t size = m->content->size;
vws_buffer_append(reply->content, data, size);
// Send. We don't free message as send() does it for us.
server->send(s, cid, reply, NULL);
// Clean up request
vrtql_msg_free(m);
}
int main(int argc, const char* argv[])
{
// Setup
vrtql_msg_svr* server = vrtql_msg_svr_new(10, 0, 0);
server->process = process;
// Run
vrtql_svr_run((vrtql_svr*)server, server_host, server_port);
// Shutdown
vrtql_svr_stop((vrtql_svr*)server);
uv_thread_join(&server_tid);
vrtql_msg_svr_free(server);
vws_cleanup();
}
Full documentation is located here. Source code annotation is located here.
- Written in C for maximum portability
- Runs on all major operating systems
- OpenSSL support built in
- Thread safe
- Liberal license allowing use in closed-source applications
- Simple, intuitive API.
- Handles complicated tasks like socket-upgrade on connection, PING requests, proper shutdown, frame formatting/masking, message sending and receiving.
- Well tested with extensive unit tests
- Well documented
- Provides a high-level API for messaging applications supporing both JSON and MessagePack serialization formats within same connection.
- Includes native Ruby C extension with RDoc documentaiton.
In order to build the code you need CMake version 3.0 or higher on your system.
Build as follows:
$ git clone https://github.com/vrtql/websockets.git
$ cd websockets
$ cmake .
$ make
$ sudo make install
Consider deactivating or activating the server side API, depending on your use case:
$ cmake . -DBUILD_SERVER=0
(instead of previous cmake .
command)
The Ruby extension can be built as follows:
$ git clone https://github.com/vrtql/websockets.git
$ cd src/ruby
$ cmake .
$ make
$ make gem
$ sudo gem install vrtql-ws*.gem
Alternately, without using gem
:
cd websockets-ruby/src/ruby/ext/vrtql/ws/
ruby extconf.rb
make
make install
The RDoc documentaton is located here.
You must have the requisite MinGW compiler and tools installed on your system. For Debian/Devuan you would install these as follows:
$ apt-get install mingw-w64 mingw-w64-tools mingw-w64-common \
g++-mingw-w64-x86-64 mingw-w64-x86-64-dev
You will need to have OpenSSL for Windows on your system as well. If you don't
have it you can build as follows. First download the version you want to
build. Here we will use openssl-1.1.1u.tar.gz
as an example. Create the
install directory you intend to put OpenSSL in. For example:
$ mkdir ~/mingw
Build OpenSSL. You want to ensure you set the --prefix
to the directory you
specified above. This is where OpenSSL will install to.
$ cd /tmp
$ tar xzvf openssl-1.1.1u.tar.gz
$ cd openssl-1.1.1u
$ ./Configure --cross-compile-prefix=x86_64-w64-mingw32- \
--prefix=~/mingw shared mingw64 no-tests
$ make
$ make DESTDIR=~/mingw install
Now within the websockets
project. Modify the CMAKE_FIND_ROOT_PATH
in the
config/windows-toolchain.cmake
file to point to where you installed
OpenSSL. In this example it would be ~/mingw/openssl
(you might want to use
full path). Then invoke CMake as follows:
$ cmake -DCMAKE_TOOLCHAIN_FILE=config/windows-toolchain.cmake
Then build as normal
$ make
We welcome contributions! Please fork this repository, make your changes, and submit a pull request. We'll review your changes and merge them if they're a good fit for the project.
This project and all third party code used by it is licensed under the MIT License. See the LICENSE file for details.