Chapter 2. C API

Table of Contents

1. The Client API
1.1. Structures
1.2. Functions
1.3. Summary
2. Common Facilities
2.1. Error Handling
2.2. Tracing
2.3. Memory Management
3. The Message API
4. The Server API
4.1. Architecture
4.2. Core Server
4.3. WebSocket Server
4.4. Message Server
5. RPC API
6. The Server API
6.1. Core Server
6.2. WebSocket Server
6.3. Message Server

There are two APIs in the library: the Client API and the Server API. Both are built around WebSocket constructs: frames, messages and connections, as you would expect. Both are designed to be simple and intuitive, managing all the painful detail work behind the scenes so that you can focus on building the application. As mentioned in the introduction, the two APIs are built from completely different networking designs, each suited to their particular context. The client architecture is designed for single connections and operates synchronously. The server architecture is designed for many concurrent connections and operates asynchronously. The client architecture builds on native operating networking services whereas the server architecture is built on libuv.

1. The Client API

1.1. Structures

There are three objects in the C API: connection (vws_cnx), frame (vws_frame) and message (vws_msg). Frames have the following binary form:

            /* An RFC 6455 message frame
            **
            **    0                   1                   2                   3
            **    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
            **   +-+-+-+-+-------+-+-------------+-------------------------------+
            **   |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
            **   |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
            **   |N|V|V|V|       |S|             |   (if payload len==126/127)   |
            **   | |1|2|3|       |K|             |                               |
            **   +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
            **   |     Extended payload length continued, if payload len == 127  |
            **   + - - - - - - - - - - - - - - - +-------------------------------+
            **   |                               | Masking-key, if MASK set to 1 |
            **   +-------------------------------+-------------------------------+
            **   | Masking-key (continued)       |          Payload Data         |
            **   +-------------------------------- - - - - - - - - - - - - - - - +
            **   :                     Payload Data continued ...                :
            **   + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
            **   |                     Payload Data continued ...                |
            **   +---------------------------------------------------------------+
            */

While frames are a concrete object in the standard, messages are not. Messages are expressed in terms of frames. Specifically, a message is one or more frames, terminated by a frame with the FIN bit set. So a message can consist of a single frame with the FIN bit set. Alternately, a message can consist of multiple frames, the last of which has the FIN bet set. In this latter case, the first frame contains the frame type (TEXT or BINARY). Subsequent frames in the chain are of type CONTINUATION (another frame type). The last frame composing the message will have the FIN bit set, signifying the end of the message. This covers the structural part of the API.

1.2. Functions

The functional part of the API operates on these object — connections, frames and messages. Thus the functions are divided into three parts, based on the object they operate on. Of necessity the first object you must deal with is the connection. You must make a connection before you can send and receive anything. To this end there is the vws_connect() vws_disconnect() functions. You start by creating a connection object by calling vws_cnx_new(). Once you have that you can connect and disconnect. The following illustrates this process:

#include <vws/websocket.h>

int main()
{
    // 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);

    //
    // { Do stuff: send and receive messages/frames }
    //

    // Disconnect
    vws_disconnect(cnx);

    // Free the connection
    vws_cnx_free(cnx);

    return 0;
}

The vws_socket_set_timeout() is thrown in there for illustration purposes. This is how you can adjust your timeout value. If you want no timeout at all (blocking indefinitely until something happens on the socket), you can set the timeout to -1. Additionally, the vws_socket_is_connected() is the way to tell if your socket is still connected. Should you receive an error or a timeout when attempting to send or receive, you can use this function to determine if the connection has been terminated. Finally there is the vws_socket_is_connected() function which terminates the connection. The connection object can be reused to connect to another host. If you are done with it then you must call vws_cnx_free() to free its memory.

Windows Timeout Limitation

There is a limitation to the timeout value of -1 in Windows with respect to making connections. The timeout value is used in two places in the code: (1) setsockopt() which affects the timeout of connecting and (2) in poll() which affects timeout of socket reads and writes. Using -1 for poll() works the same on all platforms, causing it to block indefinitely. With respect to setsockopt(), the code internally sets it to zero when making this call to achive blocking. That is to say, when it sees a timeout of -1, when calling setsockopt() it must pass it a value of zero in order to make the socket blocking in this respect. It makes this adjustment internally. However this does not work on Windows. This effectively disables blocking. The only solution is to use the maximum value which is determined by the Windows DWORD type — a signed 32-bit integer. So in the case of Windows when you set the timeout to -1, the library internally uses the max DWORD value (4294967295) which sets the blocking timeout to 136.17 years. So while this workaround is limited, should your program need to block for more than 136.17 years, you will likely not be around to see the bug in practice.

Next comes messages. The vws_msg_send_text(), vws_msg_send_binary(), vws_msg_send_data() and vws_msg_recv() deal in messages but work in terms of frames. The send functions send messages out as a single frame. The receive function collects incoming frames until it gets one with the FIN bit set. Then it concatenates the data from all frames in the sequence, forming a single message which it provides as the return value. The following illustrates using this API to send and receive messages:

    // Send a TEXT message
    vws_msg_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, "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);
    }

If you need more fine-grained control, you can deal directly in frames if you need to. There are equivalent functions to send and receive frames: vws_frame_send_text(), vws_frame_send_binary(), vws_frame_send_data() and vws_frame_recv(). The following illustrates using this API to send and receive frames (which is virtually identical to messages):

    // Send a TEXT frame
    vws_frame_send_text(cnx, "Hello, world!");

    // Receive frame
    vws_frame* reply = vws_frame_recv(cnx);

    if (reply == NULL)
    {
        // There was no message received and it resulted in timeout
    }
    else
    {
        // Free message
        vws_frame_free(reply);
    }

    // Send a BINARY frame
    vws_frame_send_binary(cnx, "Hello, world!", 14);

    // Receive frame
    reply = vws_frame_recv(cnx);

    if (reply == NULL)
    {
        // There was no message received and it resulted in timeout
    }
    else
    {
        // Free frame
        vws_frame_free(reply);
    }

If you need even more control over that regarding your frame construction, you can use vws_frame_send_data() and vws_frame_send(). The first function takes data and a frame type, also referred to as the opcode (types are defined in the frame_type_t enum). This will create a data frame of that type containing the data passed in and then send it out on the wire (with the FIN bit set). This works for creating single TEXT, BINARY, PING and PONG frames. However if you want to create a message spanning multiple frames, you will need to use vws_frame_send(), which allows you to modify the frame attributes explicitly to your liking doing things like setting the FIN but to zero. You first create a frame using vws_frame_new(). Once you have modified the frame to your liking, you send it out with vws_frame_send(). The following code illustrates using these functions to create a message spanning two frames:

    const char* data;
    int size;

    // Create first frame of two in message
    data = "Lorem ipsum";
    size = 11;
    vws_frame* f = vws_frame_new(data, size, BINARY_FRAME);

    // Modify frame to flip FIN off
    f->fin = 0;

    // Send frame. Note: this function takes ownership of frame and frees it. Do
    // NOT attempt to use frame pointer after making this call (without
    // allocating a new one).
    vws_frame_send(c, f);

    // Send the next frame as continuation. The FIN bit is automatically set to
    // 1, completing message.
    content = " dolor sit amet";
    size    = 15;
    vws_frame_send_data(cnx, data, size, CONTINUATION_FRAME)

As frames arrive they are put on a receive queue. The vws_frame_recv() function checks this queue and returns the first one it finds. If the queue is empty then it intiates a socket read and waits until the a frame arrives. If no frame arrives within the given timeout, it will return NULL. The vws_msg_recv() function works by the exact same logic but with messages.

Warning: Mixing frame and message functions

If you use both the frame and message functions together you will need to be careful how you use them. If for example you pull a frame off the queue via vws_frame_recv() and it happens to be part of a series of frames composing a message, and then you call vws_msg_recv() to pull a message, you will get a corrupted message because you manually pulled one of its frame out off the queue which should have been there as part of the message. So if you use both of these functions together you will need to pay attention to the frames you pull and take note of the message state based on that frame’s type. If you see that that frame is part of a series of frames containing a message, then you will need to manually pull the remaining frames until you reach the last (FIN) frame. Once you are in that state then you can resume using vws_msg_recv() to pull messages as the queue is in a consistent state.

1.3. Summary

That covers the Client API with respect to connections, frames and messages. Putting it all together, the following example illustrates the basic usage:

#include <vws/websocket.h>

int main()
{
    // 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.trace = 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, "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);
    }

    // Disconnect
    vws_disconnect(cnx);

    // Free the connection
    vws_cnx_free(cnx);

    return 0;
}

In summary, what you see here is a synchronous (blocking) client connection model with an optional timeout. This keeps the API simple and intuitive. Typically from the client-side this is what you want. The underlying socket read (vws_socket_read()) and write (vws_socket_write()) operations are implemented using the operating systems’s poll() facility, which provides the combination of blocking and timeout. As such, you can send messages/frames and easily wait for responses to arrive with the option of a timeout for error detection. Concurrency in this model would be employed with threads rather than non-blocking I/O. Multiple connections can operate independently within the threads they are created in. The only limitation is that a connection should only be used within the thread it was created in, as it relies on a thread-local variable (vrtql) for error handling and tracing.