blob: a091bfa0188d2039d6f14227d037b0e22bbe0c4e [file] [log] [blame]
Websockets are a genuine way to implement push notifications,
where the server initiates the communication while the client can be idle.
Usually a HTTP communication is half-duplex and always requested by the client,
but websockets are full-duplex and only initialized by the client.
In the further communication both sites can use the websocket at any time
to send data to the other site.
To initialize a websocket connection the client sends a special HTTP request
to the server and initializes
a handshake between client and server which switches from the HTTP protocol
to the websocket protocol.
Thus both the server as well as the client must support websockets.
If proxys are used, they must support websockets too.
In this chapter we take a look on server and client, but with a focus on
the server with @emph{libmicrohttpd}.
Since version 0.9.52 @emph{libmicrohttpd} supports upgrading requests,
which is required for switching from the HTTP protocol.
Since version 0.9.74 the library @emph{libmicrohttpd_ws} has been added
to support the websocket protocol.
@heading Upgrading connections with libmicrohttpd
To support websockets we need to enable upgrading of HTTP connections first.
This is done by passing the flag @code{MHD_ALLOW_UPGRADE} to
@code{MHD_start_daemon()}.
@verbatim
daemon = MHD_start_daemon (MHD_USE_INTERNAL_POLLING_THREAD |
MHD_USE_THREAD_PER_CONNECTION |
MHD_ALLOW_UPGRADE |
MHD_USE_ERROR_LOG,
PORT, NULL, NULL,
&access_handler, NULL,
MHD_OPTION_END);
@end verbatim
@noindent
The next step is to turn a specific request into an upgraded connection.
This done in our @code{access_handler} by calling
@code{MHD_create_response_for_upgrade()}.
An @code{upgrade_handler} will be passed to perform the low-level actions
on the socket.
@emph{Please note that the socket here is just a regular socket as provided
by the operating system.
To use it as a websocket, some more steps from the following
chapters are required.}
@verbatim
static enum MHD_Result
access_handler (void *cls,
struct MHD_Connection *connection,
const char *url,
const char *method,
const char *version,
const char *upload_data,
size_t *upload_data_size,
void **ptr)
{
/* ... */
/* some code to decide whether to upgrade or not */
/* ... */
/* create the response for upgrade */
response = MHD_create_response_for_upgrade (&upgrade_handler,
NULL);
/* ... */
/* additional headers, etc. */
/* ... */
ret = MHD_queue_response (connection,
MHD_HTTP_SWITCHING_PROTOCOLS,
response);
MHD_destroy_response (response);
return ret;
}
@end verbatim
@noindent
In the @code{upgrade_handler} we receive the low-level socket,
which is used for the communication with the specific client.
In addition to the low-level socket we get:
@itemize @bullet
@item
Some data, which has been read too much while @emph{libmicrohttpd} was
switching the protocols.
This value is usually empty, because it would mean that the client
has sent data before the handshake was complete.
@item
A @code{struct MHD_UpgradeResponseHandle} which is used to perform
special actions like closing, corking or uncorking the socket.
These commands are executed by passing the handle
to @code{MHD_upgrade_action()}.
@end itemize
Depending of the flags specified while calling @code{MHD_start_deamon()}
our @code{upgrade_handler} is either executed in the same thread
as our daemon or in a thread specific for each connection.
If it is executed in the same thread then @code{upgrade_handler} is
a blocking call for our webserver and
we should finish it as fast as possible (i. e. by creating a thread and
passing the information there).
If @code{MHD_USE_THREAD_PER_CONNECTION} was passed to
@code{MHD_start_daemon()} then a separate thread is used and
thus our @code{upgrade_handler} needs not to start a separate thread.
An @code{upgrade_handler}, which is called with a separate thread
per connection, could look like this:
@verbatim
static void
upgrade_handler (void *cls,
struct MHD_Connection *connection,
void *req_cls,
const char *extra_in,
size_t extra_in_size,
MHD_socket fd,
struct MHD_UpgradeResponseHandle *urh)
{
/* ... */
/* do something with the socket `fd` like `recv()` or `send()` */
/* ... */
/* close the socket when it is not needed anymore */
MHD_upgrade_action (urh,
MHD_UPGRADE_ACTION_CLOSE);
}
@end verbatim
@noindent
This is all you need to know for upgrading connections
with @emph{libmicrohttpd}.
The next chapters focus on using the websocket protocol
with @emph{libmicrohttpd_ws}.
@heading Websocket handshake with libmicrohttpd_ws
To request a websocket connection the client must send
the following information with the HTTP request:
@itemize @bullet
@item
A @code{GET} request must be sent.
@item
The version of the HTTP protocol must be 1.1 or higher.
@item
A @code{Host} header field must be sent
@item
A @code{Upgrade} header field containing the keyword "websocket"
(case-insensitive).
Please note that the client could pass multiple protocols separated by comma.
@item
A @code{Connection} header field that includes the token "Upgrade"
(case-insensitive).
Please note that the client could pass multiple tokens separated by comma.
@item
A @code{Sec-WebSocket-Key} header field with a base64-encoded value.
The decoded the value is 16 bytes long
and has been generated randomly by the client.
@item
A @code{Sec-WebSocket-Version} header field with the value "13".
@end itemize
Optionally the client can also send the following information:
@itemize @bullet
@item
A @code{Origin} header field can be used to determine the source
of the client (i. e. the website).
@item
A @code{Sec-WebSocket-Protocol} header field can contain a list
of supported protocols by the client, which can be sent over the websocket.
@item
A @code{Sec-WebSocket-Extensions} header field which may contain extensions
to the websocket protocol. The extensions must be registered by IANA.
@end itemize
A valid example request from the client could look like this:
@verbatim
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
@end verbatim
@noindent
To complete the handshake the server must respond with
some specific response headers:
@itemize @bullet
@item
The HTTP response code @code{101 Switching Protocols} must be answered.
@item
An @code{Upgrade} header field containing the value "websocket" must be sent.
@item
A @code{Connection} header field containing the value "Upgrade" must be sent.
@item
A @code{Sec-WebSocket-Accept} header field containing a value, which
has been calculated from the @code{Sec-WebSocket-Key} request header field,
must be sent.
@end itemize
Optionally the server may send following headers:
@itemize @bullet
@item
A @code{Sec-WebSocket-Protocol} header field containing a protocol
of the list specified in the corresponding request header field.
@item
A @code{Sec-WebSocket-Extension} header field containing all used extensions
of the list specified in the corresponding request header field.
@end itemize
A valid websocket HTTP response could look like this:
@verbatim
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
@end verbatim
@noindent
To upgrade a connection to a websocket the @emph{libmicrohttpd_ws} provides
some helper functions for the @code{access_handler} callback function:
@itemize @bullet
@item
@code{MHD_websocket_check_http_version()} checks whether the HTTP version
is 1.1 or above.
@item
@code{MHD_websocket_check_connection_header()} checks whether the value
of the @code{Connection} request header field contains
an "Upgrade" token (case-insensitive).
@item
@code{MHD_websocket_check_upgrade_header()} checks whether the value
of the @code{Upgrade} request header field contains
the "websocket" keyword (case-insensitive).
@item
@code{MHD_websocket_check_version_header()} checks whether the value
of the @code{Sec-WebSocket-Version} request header field is "13".
@item
@code{MHD_websocket_create_accept_header()} takes the value from
the @code{Sec-WebSocket-Key} request header and calculates the value
for the @code{Sec-WebSocket-Accept} response header field.
@end itemize
The @code{access_handler} example of the previous chapter can now be
extended with these helper functions to perform the websocket handshake:
@verbatim
static enum MHD_Result
access_handler (void *cls,
struct MHD_Connection *connection,
const char *url,
const char *method,
const char *version,
const char *upload_data,
size_t *upload_data_size,
void **ptr)
{
static int aptr;
struct MHD_Response *response;
int ret;
(void) cls; /* Unused. Silent compiler warning. */
(void) upload_data; /* Unused. Silent compiler warning. */
(void) upload_data_size; /* Unused. Silent compiler warning. */
if (0 != strcmp (method, "GET"))
return MHD_NO; /* unexpected method */
if (&aptr != *ptr)
{
/* do never respond on first call */
*ptr = &aptr;
return MHD_YES;
}
*ptr = NULL; /* reset when done */
if (0 == strcmp (url, "/"))
{
/* Default page for visiting the server */
struct MHD_Response *response = MHD_create_response_from_buffer (
strlen (PAGE),
PAGE,
MHD_RESPMEM_PERSISTENT);
ret = MHD_queue_response (connection,
MHD_HTTP_OK,
response);
MHD_destroy_response (response);
}
else if (0 == strcmp (url, "/chat"))
{
char is_valid = 1;
const char* value = NULL;
char sec_websocket_accept[29];
if (0 != MHD_websocket_check_http_version (version))
{
is_valid = 0;
}
value = MHD_lookup_connection_value (connection,
MHD_HEADER_KIND,
MHD_HTTP_HEADER_CONNECTION);
if (0 != MHD_websocket_check_connection_header (value))
{
is_valid = 0;
}
value = MHD_lookup_connection_value (connection,
MHD_HEADER_KIND,
MHD_HTTP_HEADER_UPGRADE);
if (0 != MHD_websocket_check_upgrade_header (value))
{
is_valid = 0;
}
value = MHD_lookup_connection_value (connection,
MHD_HEADER_KIND,
MHD_HTTP_HEADER_SEC_WEBSOCKET_VERSION);
if (0 != MHD_websocket_check_version_header (value))
{
is_valid = 0;
}
value = MHD_lookup_connection_value (connection,
MHD_HEADER_KIND,
MHD_HTTP_HEADER_SEC_WEBSOCKET_KEY);
if (0 != MHD_websocket_create_accept_header (value, sec_websocket_accept))
{
is_valid = 0;
}
if (1 == is_valid)
{
/* upgrade the connection */
response = MHD_create_response_for_upgrade (&upgrade_handler,
NULL);
MHD_add_response_header (response,
MHD_HTTP_HEADER_CONNECTION,
"Upgrade");
MHD_add_response_header (response,
MHD_HTTP_HEADER_UPGRADE,
"websocket");
MHD_add_response_header (response,
MHD_HTTP_HEADER_SEC_WEBSOCKET_ACCEPT,
sec_websocket_accept);
ret = MHD_queue_response (connection,
MHD_HTTP_SWITCHING_PROTOCOLS,
response);
MHD_destroy_response (response);
}
else
{
/* return error page */
struct MHD_Response*response = MHD_create_response_from_buffer (
strlen (PAGE_INVALID_WEBSOCKET_REQUEST),
PAGE_INVALID_WEBSOCKET_REQUEST,
MHD_RESPMEM_PERSISTENT);
ret = MHD_queue_response (connection,
MHD_HTTP_BAD_REQUEST,
response);
MHD_destroy_response (response);
}
}
else
{
struct MHD_Response*response = MHD_create_response_from_buffer (
strlen (PAGE_NOT_FOUND),
PAGE_NOT_FOUND,
MHD_RESPMEM_PERSISTENT);
ret = MHD_queue_response (connection,
MHD_HTTP_NOT_FOUND,
response);
MHD_destroy_response (response);
}
return ret;
}
@end verbatim
@noindent
Please note that we skipped the check of the Host header field here,
because we don't know the host for this example.
@heading Decoding/encoding the websocket protocol with libmicrohttpd_ws
Once the websocket connection is established you can receive/send frame data
with the low-level socket functions @code{recv()} and @code{send()}.
The frame data which goes over the low-level socket is encoded according
to the websocket protocol.
To use received payload data, you need to decode the frame data first.
To send payload data, you need to encode it into frame data first.
@emph{libmicrohttpd_ws} provides several functions for encoding of
payload data and decoding of frame data:
@itemize @bullet
@item
@code{MHD_websocket_decode()} decodes received frame data.
The payload data may be of any kind, depending upon what the client has sent.
So this decode function is used for all kind of frames and returns
the frame type along with the payload data.
@item
@code{MHD_websocket_encode_text()} encodes text.
The text must be encoded with UTF-8.
@item
@code{MHD_websocket_encode_binary()} encodes binary data.
@item
@code{MHD_websocket_encode_ping()} encodes a ping request to
check whether the websocket is still valid and to test latency.
@item
@code{MHD_websocket_encode_ping()} encodes a pong response to
answer a received ping request.
@item
@code{MHD_websocket_encode_close()} encodes a close request.
@item
@code{MHD_websocket_free()} frees data returned by the encode/decode functions.
@end itemize
Since you could receive or send fragmented data (i. e. due to a too
small buffer passed to @code{recv}) all of these encode/decode
functions require a pointer to a @code{struct MHD_WebSocketStream} passed
as argument.
In this structure @emph{libmicrohttpd_ws} stores information
about encoding/decoding of the particular websocket.
For each websocket you need a unique @code{struct MHD_WebSocketStream}
to encode/decode with this library.
To create or destroy @code{struct MHD_WebSocketStream}
we have additional functions:
@itemize @bullet
@item
@code{MHD_websocket_stream_init()} allocates and initializes
a new @code{struct MHD_WebSocketStream}.
You can specify some options here to alter the behavior of the websocket stream.
@item
@code{MHD_websocket_stream_free()} frees a previously allocated
@code{struct MHD_WebSocketStream}.
@end itemize
With these encode/decode functions we can improve our @code{upgrade_handler}
callback function from an earlier example to a working websocket:
@verbatim
static void
upgrade_handler (void *cls,
struct MHD_Connection *connection,
void *req_cls,
const char *extra_in,
size_t extra_in_size,
MHD_socket fd,
struct MHD_UpgradeResponseHandle *urh)
{
/* make the socket blocking (operating-system-dependent code) */
make_blocking (fd);
/* create a websocket stream for this connection */
struct MHD_WebSocketStream* ws;
int result = MHD_websocket_stream_init (&ws,
0,
0);
if (0 != result)
{
/* Couldn't create the websocket stream.
* So we close the socket and leave
*/
MHD_upgrade_action (urh,
MHD_UPGRADE_ACTION_CLOSE);
return;
}
/* Let's wait for incoming data */
const size_t buf_len = 256;
char buf[buf_len];
ssize_t got;
while (MHD_WEBSOCKET_VALIDITY_VALID == MHD_websocket_stream_is_valid (ws))
{
got = recv (fd,
buf,
buf_len,
0);
if (0 >= got)
{
/* the TCP/IP socket has been closed */
break;
}
/* parse the entire received data */
size_t buf_offset = 0;
while (buf_offset < (size_t) got)
{
size_t new_offset = 0;
char *frame_data = NULL;
size_t frame_len = 0;
int status = MHD_websocket_decode (ws,
buf + buf_offset,
((size_t) got) - buf_offset,
&new_offset,
&frame_data,
&frame_len);
if (0 > status)
{
/* an error occurred and the connection must be closed */
if (NULL != frame_data)
{
MHD_websocket_free (ws, frame_data);
}
break;
}
else
{
buf_offset += new_offset;
if (0 < status)
{
/* the frame is complete */
switch (status)
{
case MHD_WEBSOCKET_STATUS_TEXT_FRAME:
/* The client has sent some text.
* We will display it and answer with a text frame.
*/
if (NULL != frame_data)
{
printf ("Received message: %s\n", frame_data);
MHD_websocket_free (ws, frame_data);
frame_data = NULL;
}
result = MHD_websocket_encode_text (ws,
"Hello",
5, /* length of "Hello" */
0,
&frame_data,
&frame_len,
NULL);
if (0 == result)
{
send_all (fd,
frame_data,
frame_len);
}
break;
case MHD_WEBSOCKET_STATUS_CLOSE_FRAME:
/* if we receive a close frame, we will respond with one */
MHD_websocket_free (ws,
frame_data);
frame_data = NULL;
result = MHD_websocket_encode_close (ws,
0,
NULL,
0,
&frame_data,
&frame_len);
if (0 == result)
{
send_all (fd,
frame_data,
frame_len);
}
break;
case MHD_WEBSOCKET_STATUS_PING_FRAME:
/* if we receive a ping frame, we will respond */
/* with the corresponding pong frame */
{
char *pong = NULL;
size_t pong_len = 0;
result = MHD_websocket_encode_pong (ws,
frame_data,
frame_len,
&pong,
&pong_len);
if (0 == result)
{
send_all (fd,
pong,
pong_len);
}
MHD_websocket_free (ws,
pong);
}
break;
default:
/* Other frame types are ignored
* in this minimal example.
* This is valid, because they become
* automatically skipped if we receive them unexpectedly
*/
break;
}
}
if (NULL != frame_data)
{
MHD_websocket_free (ws, frame_data);
}
}
}
}
/* free the websocket stream */
MHD_websocket_stream_free (ws);
/* close the socket when it is not needed anymore */
MHD_upgrade_action (urh,
MHD_UPGRADE_ACTION_CLOSE);
}
/* This helper function is used for the case that
* we need to resend some data
*/
static void
send_all (MHD_socket fd,
const char *buf,
size_t len)
{
ssize_t ret;
size_t off;
for (off = 0; off < len; off += ret)
{
ret = send (fd,
&buf[off],
(int) (len - off),
0);
if (0 > ret)
{
if (EAGAIN == errno)
{
ret = 0;
continue;
}
break;
}
if (0 == ret)
break;
}
}
/* This helper function contains operating-system-dependent code and
* is used to make a socket blocking.
*/
static void
make_blocking (MHD_socket fd)
{
#if defined(MHD_POSIX_SOCKETS)
int flags;
flags = fcntl (fd, F_GETFL);
if (-1 == flags)
return;
if ((flags & ~O_NONBLOCK) != flags)
if (-1 == fcntl (fd, F_SETFL, flags & ~O_NONBLOCK))
abort ();
#elif defined(MHD_WINSOCK_SOCKETS)
unsigned long flags = 0;
ioctlsocket (fd, FIONBIO, &flags);
#endif /* MHD_WINSOCK_SOCKETS */
}
@end verbatim
@noindent
Please note that the websocket in this example is only half-duplex.
It waits until the blocking @code{recv()} call returns and
only does then something.
In this example all frame types are decoded by @emph{libmicrohttpd_ws},
but we only do something when a text, ping or close frame is received.
Binary and pong frames are ignored in our code.
This is legit, because the server is only required to implement at
least support for ping frame or close frame (the other frame types
could be skipped in theory, because they don't require an answer).
The pong frame doesn't require an answer and whether text frames or
binary frames get an answer simply belongs to your server application.
So this is a valid minimal example.
Until this point you've learned everything you need to basically
use websockets with @emph{libmicrohttpd} and @emph{libmicrohttpd_ws}.
These libraries offer much more functions for some specific cases.
The further chapters of this tutorial focus on some specific problems
and the client site programming.
@heading Using full-duplex websockets
To use full-duplex websockets you can simply create two threads
per websocket connection.
One of these threads is used for receiving data with
a blocking @code{recv()} call and the other thread is triggered
by the application internal codes and sends the data.
A full-duplex websocket example is implemented in the example file
@code{websocket_chatserver_example.c}.
@heading Error handling
The most functions of @emph{libmicrohttpd_ws} return a value
of @code{enum MHD_WEBSOCKET_STATUS}.
The values of this enumeration can be converted into an integer
and have an easy interpretation:
@itemize @bullet
@item
If the value is less than zero an error occurred and the call has failed.
Check the enumeration values for more specific information.
@item
If the value is equal to zero, the call succeeded.
@item
If the value is greater than zero, the call succeeded and the value
specifies the decoded frame type.
Currently positive values are only returned by @code{MHD_websocket_decode()}
(of the functions with this return enumeration type).
@end itemize
A websocket stream can also get broken when invalid frame data is received.
Also the other site could send a close frame which puts the stream into
a state where it may not be used for regular communication.
Whether a stream has become broken, can be checked with
@code{MHD_websocket_stream_is_valid()}.
@heading Fragmentation
In addition to the regular TCP/IP fragmentation the websocket protocol also
supports fragmentation.
Fragmentation could be used for continuous payload data such as video data
from a webcam.
Whether or not you want to receive fragmentation is specified upon
initialization of the websocket stream.
If you pass @code{MHD_WEBSOCKET_FLAG_WANT_FRAGMENTS} in the flags parameter
of @code{MHD_websocket_stream_init()} then you can receive fragments.
If you don't pass this flag (in the most cases you just pass zero as flags)
then you don't want to handle fragments on your own.
@emph{libmicrohttpd_ws} removes then the fragmentation for you
in the background.
You only get the completely assembled frames.
Upon encoding you specify whether or not you want to create a fragmented frame
by passing a flag to the corresponding encode function.
Only @code{MHD_websocket_encode_text()} and @code{MHD_websocket_encode_binary()}
can be used for fragmentation, because the other frame types may
not be fragmented.
Encoding fragmented frames is independent of
the @code{MHD_WEBSOCKET_FLAG_WANT_FRAGMENTS} flag upon initialization.
@heading Quick guide to websockets in JavaScript
Websockets are supported in all modern web browsers.
You initialize a websocket connection by creating an instance of
the @code{WebSocket} class provided by the web browser.
There are some simple rules for using websockets in the browser:
@itemize @bullet
@item
When you initialize the instance of the websocket class you must pass an URL.
The URL must either start with @code{ws://}
(for not encrypted websocket protocol) or @code{wss://}
(for TLS-encrypted websocket protocol).
@strong{IMPORTANT:} If your website is accessed via @code{https://}
then you are in a security context, which means that you are only allowed to
access other secure protocols.
So you can only use @code{wss://} for websocket connections then.
If you try to @code{ws://} instead then your websocket connection will
automatically fail.
@item
The WebSocket class uses events to handle the receiving of data.
JavaScript is per definition a single-threaded language so
the receiving events will never overlap.
Sending is done directly by calling a method of the instance of
the WebSocket class.
@end itemize
Here is a short example for receiving/sending data to the same host
as the website is running on:
@verbatim
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Websocket Demo</title>
<script>
let url = 'ws' + (window.location.protocol === 'https:' ? 's' : '') + '://' +
window.location.host + '/chat';
let socket = null;
window.onload = function(event) {
socket = new WebSocket(url);
socket.onopen = function(event) {
document.write('The websocket connection has been established.<br>');
// Send some text
socket.send('Hello from JavaScript!');
}
socket.onclose = function(event) {
document.write('The websocket connection has been closed.<br>');
}
socket.onerror = function(event) {
document.write('An error occurred during the websocket communication.<br>');
}
socket.onmessage = function(event) {
document.write('Websocket message received: ' + event.data + '<br>');
}
}
</script>
</head>
<body>
</body>
</html>
@end verbatim
@noindent