Table of Contents
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
.
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.
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.
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.
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.
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.