Web Socket Client

Web Socket Client

The library libwebsockets is a C library that allows opening and two-way communication via web sockets.

License and Source

Source repository is available here. libwebsockets is Open Source, license is available here, LGPL2.1 with static link exception.

Concepts

Web socket communications require a pre-defined protocol in place since both server and client must speak the same dialect. Your application can create a list of supported dialects like this:

static lws_protocols protocols[] = {
	{
		"fzco-protocol", // protocol name
		callback,        // callback for protocol
		0,               // per session data size
		0,               // receive buffer size
		0,               // user defined ID, not used by LWS
		nullptr,         // user defined pointer
		0                // transmit buffer size
	},
	{ nullptr, nullptr, 0, 0, 0, nullptr, 0 } // protocol list terminator
};

Older libwebsocket lws_protocols didn't have the transmit buffer size parameter (the last zero).

The callback reference above is a function that will handle various types of messages, here is an example:

static int callback( lws*, lws_callback_reasons reason, void* user, void* p, size_t z )
{
	switch( reason ) {
		case LWS_CALLBACK_CLIENT_ESTABLISHED:
			// Connected to server.
			break;

		case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
			// Unable to connect to server.  Check protocol?
			break;

		case LWS_CALLBACK_CLOSED:
			// Connection to server closed.
			break;

		case LWS_CALLBACK_CLIENT_RECEIVE:
			// Data of z bytes is accessible via pointer p.
			fprintf(stdout, "%s\n", (const char*)p); fflush(stdout);
			break;

		case LWS_CALLBACK_CLIENT_WRITEABLE:
			// Now is your chance to send stuff to server, use API
			// lws_write() to do so.
			break;

		default: break;
	}
	return 0;
}

Each time something happens on the socket, this callback function will fire with a reason that you respond to. Example above prints whatever server sends you after connection is established. Hopefully it is text...

Establish Connection

You'll need a server name and port to connect, the minimal code to do so is fairly simple:

int main( int argc, char** argv )
{
	lws_context_creation_info cinfo;
	memset(&cinfo, 0, sizeof(cinfo));
	cinfo.port = CONTEXT_PORT_NO_LISTEN;
	cinfo.protocols = protocols;
	cinfo.extensions = nullptr;
	cinfo.gid = -1;
	cinfo.uid = -1;
	cinfo.user = nullptr;
	cinfo.options = 0;
	
	lws_context* context = lws_create_context(&cinfo);
	if( context ) {
		// EXAMPLE: argv[1] is address and argv[2] is port number
		char origin[256];
		snprintf(origin, sizeof(origin), "%s:%s", argv[1], argv[2]);

		lws_client_connect_info ccinfo;
		memset(&ccinfo, 0, sizeof(ccinfo));
		ccinfo.context = context;
		ccinfo.address = argv[1];              // server name
		ccinfo.port = atoi(argv[2]);           // port number
		ccinfo.ssl_connection = 0;             // SSL
		ccinfo.path = "/";                     // path to access
		ccinfo.host = argv[1];                 // host name
		ccinfo.origin = origin;                // origin
		ccinfo.protocol = "fzco-protocol";     // protocol
		ccinfo.ietf_version_or_minus_one = -1; // IETF version

		lws* socket = lws_client_connect_via_info(&ccinfo);
		if( socket ) {
			bool running = true;
			while( running ) {
				lws_service(context, 0); // 0 is poll timeout
				lws_callback_on_writable_all_protocol(context,
                            &protocols[0]);
			}
		}
		lws_context_destroy(context);
	}
	return 0;
}

The code above also has an infinite loop to service the socket. Function lws_service() does the work to fire previously defined callback for your protocol. Since you can only write to the socket when it isn't receiving data, it is important to be notified when such window occurs. Function lws_callback_on_write_all_protocol() does just that. Technique I used, and I am not entirely sure if it is right, is to queue everything I want to write in a data structure. When callback fires with LWS_CALLBACK_CLIENT_WRITEABLE as the reason, I can check the queue to see if there is anything that need to go out and use lws_write() to send data to server.

Some Notes

In the protocols data structure, it is possible to define a receive buffer size. If you do, then messages will arrive in chunks of that size. If you are expecting strings then they may be chopped up into segments if length of incoming string is longer than the receive buffer size.

Earlier I mentioned that I queue out-going data up in a queue and write when protocol callback fires. If I spend too much time writing it is possible for the out-going data to collide with incoming data, and the library will print warning messages to terminal.

If C++ is an option for you, you may be interested in the implementation rtc::WebSocket included in libdatachannel with LGPLv2 (MPL2 after v.0.16) license, so it is okay to dynamically link that library. It is super easy to work with, and you get useful C++ tools like std::promise and std::future to help you with asynchronous operations.

Alternatively, you can try websocketpp. It generally works well and is header-only, but from time to time I run into problems trying to close a web socket. I think it is probably hung up on closing the TLS connection, and it was pretty frustrating when it or ASIO is not behaving.

Finally, the more recent versions of libcurl supports ws:// and wss://. I am a big fan of cURL, and it makes a lot of sense because web sockets are upgraded HTTP connections (you can establish connections with any version of libcurl provided you add the appropriate headers). But you can't always guarantee that your SDK includes a version that understands web socket and the whole idea of using a library is to avoid having to parse the network frames and deal with ping-pong, etc.

References