Skip to content

Latest commit

 

History

History
208 lines (157 loc) · 6.22 KB

WS.md

File metadata and controls

208 lines (157 loc) · 6.22 KB

Example: WebSocket Echo Server

A simple WebSocket echo server in three easy steps:

  1. Define some WebSocket event callbacks
  2. Add a default HTTP handler
  3. Create and start the server

Fin!

Let's have a peek:

1. WebSocket Protocol Callbacks

To start things off, we'll define our callback functions for the following WebSockets events:

  • connect (analogous to onopen)
  • receive (analogous to onmessage)
  • close (analogous to onclose)

Connect Callback

The connection callback takes a single argument, a ymo_ws_session_t (all WebSockets event callbacks provide the session as the first argument — see API docs for more info). On success, the callback should return YMO_OKAY. Otherwise, the connection is closed, and the return value is used to set errno.

NOTE: Yimmo sends all payload data (i.e. message bodies) using Apache-esque "bucket brigades" (ymo_bucket_t).

static ymo_status_t test_ws_connect_cb(ymo_ws_session_t* session)
{
    ymo_log_info("New WebSocket session: %p!", (void*)session);

    /* Encapsulate a string literal in a bucket: */
    ymo_bucket_t* my_msg = YMO_BUCKET_FROM_REF("Hello!", 6);

    /* Send a text-type message from with the FIN bit set: */
    return ymo_ws_session_send(
            session, YMO_WS_FLAG_FIN | YMO_WS_OP_TEXT, my_msg);
}

Receive Callback

This is invoked whenever a message is received from the client. In addition to the session object, we get flags (the first 8 bits of the RFC6455 message frame), data (the raw data as const char*), and len the total payload length. On success, the callback should return YMO_OKAY. Otherwise, the connection is closed, with the return value being used to set errno.

static ymo_status_t test_ws_recv_cb(
        ymo_ws_session_t* session,
        void*             user_data,
        uint8_t flags,
        const char*       data,
        size_t len)
{
    if( data && len ) {
        if( flags & YMO_WS_OP_TEXT ) {
            ymo_log_info("Recv from %p: \"%.*s\"",
                    (void*)session, (int)len, data);
        }
    } else {
        ymo_log_info(
                "Got a message with a zero length payload "
                "(allowed by RFC-6455!)");
    }

    return ymo_ws_session_send(
            session, flags, YMO_BUCKET_FROM_CPY(data, len));
}

Heads up! — Message Framing:

Since we're just echoing the payload back, we don't bother checking or setting the OP_TYPE or FIN bits — we just copy them back, as-is, to the client the way they were recieved.

If you wanted to do something with the complete message — e.g. parse some JSON payload with a non-streaming parser, etc — you'd want to check the FIN bit and buffer the incoming frames until you got to the end, or use a buffered connection adapter.

Close Callback

The close callback is invoked when the WebSocket session has been terminated (by either the client or server).

WARNING: The ymo_ws_session_t* passed in as a parameter here cannot be meaningfully dereferenced after this function returns!

static void test_ws_close_cb(ymo_ws_session_t* session, void* user_data)
{
    ymo_log_info("Session %p closed!", (void*)session);
    return;
}

2. Add an HTTP callback for non-upgrade requests

For kicks, we add a simple HTTP callback to serve a plain-text 200 response to any HTTP 1.0, 1.1 requests which do not attempt a connection upgrade or HTTP2 upgrade requests (which are defaulted back to HTTP 1.1 via ymo_http2_no_upgrade_handler in main):

For a deeper dive into what's going on here, see the HTTP module docs.

static ymo_status_t test_http_callback(
        ymo_http_session_t* session,
        ymo_http_request_t* request,
        ymo_http_response_t* response,
        void* user_data)
{
    ymo_http_response_insert_header(response, "content-type", "text/plain");
    ymo_bucket_t* content = YMO_BUCKET_FROM_REF("OK", 2);
    ymo_http_response_set_status_str(response, "200 OK");
    ymo_http_response_body_append(response, content);
    ymo_http_response_finish(response);
    return YMO_OKAY;
}

3. Create and Start the Server!

What we do here is:

  1. Get a handle to the default libev event loop using ev_default_loop.
  2. Invoke ymo_proto_ws_create to create a new instance of the libyimmo WebSockets protocol, registering our connect, receive, and close callbacks.
  3. Add some HTTP upgrade handlers:
    • ymo_ws_http_upgrade_handler is the default HTTP->RFC6455 upgrade handler (you can roll your own, if you like; see the ymo_http module API docs for more info).
    • ymo_http2_no_upgrade_handler is a "fallback" handler which reverts HTTP2 upgrade attempts to HTTP/1.1
  4. Instantiate a libyimmo HTTP server using ymo_http_simple_init, registering our test HTTP callback as the principle handler and our "upgrade handler" chain (which contains the WebSockets upgrade handler
    • protocol).
  5. Run the thing!
int main(int argc, char** argv)
{
    ymo_log_init();

    /* Say hello */
    issue_startup_msg();

    struct ev_loop* loop = ev_default_loop(0);

    /* Create a websocket protocol object: */
    ymo_proto_t* ws_proto = ymo_proto_ws_create(
            YMO_WS_SERVER_DEFAULT,
            &test_ws_connect_cb,
            &test_ws_recv_cb,
            &test_ws_close_cb);

    /* Set up an array of HTTP "Upgrade" upgrade_handlers: */
    ymo_http_upgrade_handler_t* upgrade_handlers[] = {
        ymo_ws_http_upgrade_handler(ws_proto), /* WS upgrade handler */
        ymo_http2_no_upgrade_handler(),        /* HTTP2 --> HTTP/1.1. */
        NULL,                                  /* sentinel */
    };

    /* Create the HTTP server: */
    ymo_server_t* http_srv = ymo_http_simple_init(
            loop, HTTP_PORT, &test_http_callback, upgrade_handlers, NULL);

    /* Run it! */
    ymo_server_start(http_srv, loop);
    if( http_srv ) {
        ev_run(ev_default_loop(0), 0);
        ymo_log(YMO_LOG_INFO, "Loop exited. Shutting down!");
        ymo_server_free(http_srv);
    } else {
        ymo_log(YMO_LOG_ERROR, "Server failed to start!");
    }
    return 0;
}