1. Introduction

In the following tutorial, we will learn how to use websockets by creating a simple chat-room application. The tutorial will be divided in three parts:
- the first part will focus on setting up a Crails+Comet application for the tutorial.
- the second part will be targeted at server-side web sockets with Crails.
- the third part will be targeted at client-side web sockets with Comet.
Before we dwelve into websockets, let's create our development environment using the crails comment line interface:

1.1 Creating a Crails+Comet application

First things first, let's create a simple crails application:

$ crails new -n chatroom -p html

This will create a basic application with html support. Next, we need to add a comet client, which we will create using the comet plugin:

$ crails plugins comet install

1.2 Creating a controller and layout

Even though we'll be making a Single-Page-Application using Comet, we still need to provide a route to serve a basic html file which will provide a layout including the required stylesheets and javascript files. Let's start with creating the controller:

$ crails scaffold controller -n home

Now, we will create a simple default layout:

$ crails scaffold layout

The default options for the layout generator will create the layouts/application layout. The next step is to add an action to our home controller that will render our layout. First, update the header to add thehomepage action:

app/controllers/home.hpp
#pragma once
#include "app/controllers/application.hpp"

class HomeController : public ApplicationController
{
public:
  HomeController(Crails::Context&);

  void initialize() override;
  void finalize() override;

  // Add our new action here:
  void homepage();
protected:
};

Then, let's implement the homepage action in the controller's source file:

app/controllers/home.cpp
#include "home.hpp"
#include <crails/params.hpp>

...

void HomeController::homepage()
{
  render("layouts/application");
}

This might seem unorthodox... and it is. Usually, you would render a view within a layout. In this context, we are directly rendering the layout as if it were a simple view. This is absolutely possible, and it fits our current needs better, as rendering the views will be handled by the client rather than the server.

To finish up our homepage route, we need to reference it in the router:

app/routes.cpp
#include <crails/router.hpp>
#include "app/controllers/home.hpp" // adds the include for our HomeController

void Crails::Router::initialize(void)
{
  match_action("GET", "/", HomeController, homepage); // references the homepage action as the root action ("/")
}

1.3 Serving the Comet application from the layout

Last step, we need to serve the JavaScript for our comet application. This is done using the lib/assets.hpp header, a generated file which provides the definitive URIs towards your application assets.

In order to take a peek at what this header will look like, let's try building your application:

$ crails build

Everything should go smoothly, and the lib/assets.hpp file should've been generated. Let's check it out:

lib/assets.hpp
#ifndef APPLICATION_ASSETS_HPP
#define APPLICATION_ASSETS_HPP
namespace Assets
{
#ifndef __CHEERP_CLIENT__
  extern const char* application_js;
#endif
  extern const char* stylesheets_bootstrap_bootstrap_scss;
}
#endif

See the application_js variable ? That's what we're interested about. This variable will contain the path for our asset from the web. This is what we need to add to our layout to serve our comet application. Let's go and add a <script> in our layout's header:

app/views/layouts/application.html
#include "lib/assets.hpp"

const char* @yield = nullptr;
// END LINKING
<html>
  <head>
    <%= tag("link", {{"rel","stylesheet"},{"href",Assets::stylesheets_bootstrap_bootstrap_scss}}) %>

    <!-- Add the following script tag: -->
    <%= tag("script", {{"src",Assets::application_js}}) yields %><% yields-end %>

  </head>
  <body>
    <% if (yield != nullptr) do %>
      <%= yield %>
    <% end %>
  </body>
</html>

And that's all there is to it ! Our setup is ready : we can now go on and dwelve into WebSockets !

2. WebSocket server with Crails

2.1 A simple echo websocket

To familiarize with the WebSocket API, let's start by writing a simple echo websocket. It's a websocket that will merely send back everything that it receives.

Let's start by scaffolding our websocket class:

app/controllers/echo_web_socket.hpp
#pragma once
#include <crails/websocket.hpp>

class EchoWebSocket : public Crails::WebSocket
{
public:
  EchoWebSocket(Crails::Context& context) : Crails::WebSocket(context)
  {
  }
};

All we need to create a valid websocket is to inherit Crails::WebSocket and create a constructor that passes a context object to the superclass.

This websocket, however, does nothing yet. We will now override the received method to send back any received messages:

app/controllers/echo_web_socket.hpp
#pragma once
#include <crails/websocket.hpp>

class EchoWebSocket : public Crails::WebSocket
{
public:
  EchoWebSocket(Crails::Context& context) : Crails::WebSocket(context)
  {
  }

  void received(const std::string& message, Crails::WebSocket::MessageType type) override
  {
    send(message, type);
  }
};

Simple stuff ! And we're almost done: we now need to register our WebSocket object in the router, so that it can wire requests towards it. Edit the routes as following:

app/routes.cpp
#include <crails/router.hpp>
#include "app/controllers/home.hpp"
#include "app/controllers/echo_web_socket.hpp" // adds the include for our EchoWebSocket

void Crails::Router::initialize(void)
{
  match_action("GET", "/", HomeController, homepage);
  match_websocket("/echo", EchoWebSocket, read); // references our websocket at "/echo"
}

Matching websockets routes is very similar to controller routes. Like for controllers, you also need to specify an action as the third parameter of a route: the action specified will trigger right after the WebSocket handshake is completed. In this case, we've used read as our action.

The read action is provided by Comet::WebSocket: you don't have to implement it yourself. It is basically what you would expect from reading on a socket: it asynchronously waits for the client to send data, and calls received (which we've implemented earlier) once a message has been received.

2.2 Chatroom server

Let's capitalize on our newly aquired knowledge by writing a simple chatroom server. Our goal will be to echo each message received from one websocket to all other websockets connected to the chatroom.

To that end, we will need to make a chatroom storing all the connected websockets. It will need to be a thread-safe chatroom, as distincts clients could connect, disconnect and emit messages simultaneously.

Let's have a go at it:

app/controllers/chat_room.hpp
#pragma once
#include <crails/websocket.hpp>
#include <crails/utils/singleton.hpp>
#include <mutex>

class ChatRoom
{
  SINGLETON(ChatRoom) // singleton helper from the framework
  typedef std::shared_ptr<Crails::WebSocket> WebSocketPtr;
  typedef std::list<WebSocketPtr>            Clients;
public:
  void add_client(Crails::WebSocket& socket)
  {
    std::lock_guard<std::mutex> guard(clients_mutex);

    clients.push_back(socket.shared_from_this());
  }

  void remove_client(const Crails::WebSocket& socket)
  {
    Clients::iterator it;

    std::lock_guard<std::mutex> guard(clients_mutex);
    it = std::find(clients.begin(), clients.end(), socket.shared_from_this());
    if (it != clients.end())
      clients.erase(it);
  }

  void send_message(const Crails::WebSocket& emitter, const std::string& message)
  {
    std::lock_guard<std::mutex> guard(clients_mutex);

    for (WebSocketPtr client : clients)
    {
      if (client.get() != &emitter)
        client->send(message, Crails::WebSocket::TextMessage);
    }
  }

private:
  std::mutex clients_mutex;
  Clients clients;
};

In this ChatRoom class, we use std::mutex to ensure thread-safety with our client list. In the send_message method, we loop over all our connected clients, and send a message to everyone except the WebSocket who emitted the message in the first place.

Note that we used the SINGLETON macro, meaning we need to instantiate the singleton somwehere, ideally somewhere that won't be subjected to potential concurrency issues. We'll do it in our main function:

app/main.cpp
...
#include "controllers/chat_room.hpp"

int main()
{
  SingletonInstantiator<ChatRoom> chat_room; // just add this line to instantiate the singleton
  ...
}

Let's now write a new WebSocket class that will use this chatroom:

app/controllers/chat_client.hpp
#pragma once
#include "chat_room.hpp"

class ChatClient : public Crails::WebSocket
{
  std::string nickname;
public:
  ChatClient(Crails::Context& context) : Crails::WebSocket(context)
  {
    // First, we'll use the request parameters to let participants set a nickname
    nickname = context.params["nickname"].defaults_to<std::string>("anonymous");
  }

  // We need a method to handle the arrival of a new participant:
  void connect()
  {
    ChatRoom::singleton::get()->send_message(*this, "<i>" + nickname + " joined the room.</i>");
    ChatRoom::singleton::get()->add_client(*this);
    read();
  }

  // Next, we need to broadcast received messages to the room
  void received(const std::string& message, Crails::WebSocket::MessageType) override
  {
    ChatRoom::singleton::get()->send_message(*this, "<b>" + nickname + ":</b> " + message);
    read();
  }

  // Finally, we remove the participant from the room when the socket gets disconnected
  void disconnected() const override
  {
    ChatRoom::singleton::get()->send_message(*this, "<i>" + nickname + " left the room.</i>");
    ChatRoom::singleton::get()->remove_client(*this);
  }
};

Lastly, we add a route to reach our chatroom:

app/routes.cpp
#include <crails/router.hpp>
#include "app/controllers/home.hpp"
#include "app/controllers/echo_web_socket.hpp"
#include "app/controllers/chat_client.hpp" // add the ChatClient header

void Crails::Router::initialize(void)
{
  match_action("GET", "/", HomeController, homepage);
  match_websocket("/echo", EchoWebSocket, read);
  match_websocket("/chatroom/:nickname", ChatClient, connect); // add a new route
}

3. WebSocket client with Comet

We'll now take care of the client part. We'll design it using Comet.cpp. If you haven't used Comet yet, check out the Getting started with Comet tutorial.

Before all else, move to the client's directory at app/client: the comet command line interface should always run from this folder, or one of its descendents.

$ cd app/client

3.1 WebSocket

Let's start with a chat room object: it will use a WebSocket to send and receive messages, and store the history of messages received. Create the following file:

app/client/chat_room_client.hpp
#pragma once
#include <comet/websocket.hpp>

class ChatRoomClient
{
  std::unique_ptr<Comet::WebSocket> socket;
  std::string nickname;
  std::size_t maximum_history_size = 50;
  std::list<std::string> history;
public:
  Comet::Signal<> history_updated;

  const std::list<std::string>& get_history() const { return history; }

  void connect(const std::string& nickname_)
  {
    nickname = nickname_;
    socket.reset(new Comet::WebSocket("ws://localhost:3001/chatroom/" + nickname));
    socket->message_received.connect(std::bind(&ChatRoom::add_message, this, std::placeholders::_1));
  }

  void add_message(const std::string& message)
  {
    history.push_back(message);
    if (history.size() > maximum_history_size)
      history.erase(history.begin());
    history_updated.trigger();
  }

  void send_message(const std::string& message)
  {
    socket->send(message);
    add_message("<b>" + nickname + ": </b>" + message);
  }
};

For receiving messages, the WebSocket object comes with a message_received signal, which we connect to the add_message method right after instantiating the WebSocket in the connect method.

For sending messages, we use the send method, which takes std::string as its parameter, as seen in the send_message method.

3.2 Overloading WebSocket

While it is possible to use Comet::WebSocket directly for simple use cases, if we want to handle other events from websockets, such as connection or disconnection, we will have to create a class that inherits Comet::WebSocket and overrides the on_open, on_close, on_error methods:

app/client/chat_web_socket.hpp
#pragma once
#include <comet/websocket.hpp>

class ChatWebSocket : public Comet::WebSocket
{
public:
  Comet::Signal<bool> connection_changed;

  ChatWebSocket(Comet::String url) : Comet::WebSocket(url)
  {
  }

  void on_open(client::Event*) override
  {
    connection_changed.trigger(true);
  }

  void on_close(client::Event*) override
  {
    connection_changed.trigger(false);
  }

  void on_error(client::ErrorEvent*) override
  {
  }
};

In this example, we create a class that provides a signal when the state of the connection changed. Let's integrate this signal in our ChatRoomClient object:

app/chat_room_client.hpp
#pragma once
#include "chat_web_socket.hpp"

class ChatRoomClient
{
  std::unique_ptr<ChatWebSocket> socket; // update the socket's classname
  std::string nickname;
  std::size_t maximum_history_size = 50;
  std::list<std::string> history;
public:
  Comet::Signal<> history_updated;

  const std::list<std::string>& get_history() const { return history; }

  void connect(const std::string& nickname_)
  {
    nickname = nickname_;
    socket.reset(new ChatWebSocket("ws://localhost:3001/chatroom/" + nickname));
    socket->message_received.connect(std::bind(&ChatRoomClient::add_message, this, std::placeholders::_1));
    socket->connection_changed.connect(std::bind(&ChatRoomClient::on_connection_changed, this, std::placeholders::_1));
  }

  void on_connection_changed(bool connected)
  {
    add_message(connected ? "You are now connected." : "You are not connected anymore.");
  }

  void add_message(const std::string& message)
  {
    history.push_back(message);
    if (history.size() > maximum_history_size)
      history.erase(history.begin());
    history_updated.trigger();
  }

  void send_message(const std::string& message)
  {
    socket->send(message);
    add_message(nickname + ": " + message);
  }
};

By adding a listener for the connection_changed event, we will now display a message when the user connects or disconnects.

3.3 Chat room views

3.3.1 Connect view

We've got the backend ready, let's move on to the frontend. We'll start by creating a connection room, where users can pick their nicknames. Run the following command in your app/client folder:

$ comet scaffold view -n ConnectView

And update the generated app/views/connect_view.html file as following:

app/client/views/connect_view.html
<template>
  <head>
    <script>virtual void on_submit() = 0;</script>
  </head>
  <body>
    <form submit.trigger="on_submit()">
      <div class="form-group">
        <input type="text" class="form-control" ref="nickname_input" />
        <input type="submit" class="btn btn-primary mb-3" />
      </div>
    </form>
  </body>
</template>

This view has a text input that will be referenced as nickname_input, and declares the on_submit method, called when the form sends a submit event. The method needs an implementation, so let's add one in the view's header:

app/client/views/connect_view.hpp
#pragma once
#include "templates/connect_view.hpp"

class ConnectView : public HtmlTemplate::ConnectView
{
public:
  Comet::Signal<std::string> accepted;

  void on_submit()
  {
    accepted.trigger(
      nickname_input.value<std::string>()
    );
  }
};

3.3.2 Chatroom controller

We'll now set up a controller to display the connection view, and eventually the chatroom. Run the following command:

$ comet scaffold controller -n ChatRoom

Let us now add an action to that controller that will manage our connection view. Open the header at app/client/controllers/chat_room.hpp, and declare the connect method as such :

app/controllers/chat_room.hpp
#pragma once
#include <comet/mvc/controller.hpp>

class ChatRoomController : public Comet::Controller
{
public:
  ChatRoomController(const Comet::Params&);

  void connect();
};

Let's now implement it in the source file:

app/client/controllers/chat_room.cpp
#include "chat_room.hpp"
#include "../views/connect_view.hpp"

using namespace std;

unique_ptr<ConnectView> connect_view;

void ChatRoomController::connect()
{
  Comet::body.empty();
  connect_view.reset(new ConnectView);
  connect_view->append_to(Comet::body);
}

...

This will replace the content of the document's body with a freshly instantiated ConnectView. when the action is called. Let's add a route to the client router:

app/client/routes.cpp
#include <comet/router.hpp>
#include "controllers/chat_room.hpp"

void Comet::Router::initialize()
{
  match_action("/", ChatRoomController, connect);
}

3.3.3 Connection

Now that we have a form to enter our nickname, let's use it to implement connection with the ChatRoom object we created in 3.1. Let's add a second route for connection:

app/client/routes.cpp
#include <comet/router.hpp>
#include "controllers/chat_room.hpp"

void Comet::Router::initialize()
{
  match_action("/", ChatRoomController, connect);
  match_action("/chatroom/:nickname", ChatRoomController, chatroom);
}

We'll add the chatroom action to our controller source:

app/client/controllers/chat_room.cpp
#include "chat_room.hpp"
#include "../views/connect_view.hpp"
#include "../chat_room_client.hpp"

using namespace std;

unique_ptr<ChatRoomClient> chatroom_client;
unique_ptr<ConnectView> connect_view;

void ChatRoomController::connect()
{
  Comet::body.empty();
  connect_view.reset(new ConnectView);
  connect_view->append_to(Comet::body);
}

void ChatRoomController::chatroom()
{
  chatroom_client.reset(new ChatRoomClient);
  chatroom_client->connect(params["nickname"]);
}
...

Let's also update the connect method so that submitting the form trigger the chatroom action:

app/client/controllers/chat_room.cpp
#include "../application.hpp"

...

void ChatRoomController::connect()
{
  Comet::body.empty();
  connect_view.reset(new ConnectView);
  connect_view->append_to(Comet::body);
  connect_view->accepted.connect([](const std::string& nickname)
  {
    Application::get().router.navigate("/chatroom/" + nickname);
  });
}

...

3.3.4 Chat view

Last, but not least: the chat view. We'll be creating a view that displays the message history and input. Run the following command in your app/client directory:

$ comet scaffold view -n ChatRoomView -p 'std::vector<std::string>*/history'

Open the generated file at app/client/views/chat_room_view.html, and modify it as following:

app/client/views/chat_room_view.html
<template>
  <head>
    <attribute name="history" type="const std::list<std::string>*" value="nullptr" />
    <script>
      Comet::Signal<std::string> send_message;
    </script>
  </head>
  <body>
    <div repeat.for="line of [const std::list<std::string>]*history">
      <span text.bind="line"></span>
    </div>
    <form submit.trigger="send_message.trigger(message_input.value<std::string>())">
      <div class="input-group">
        <input type="text" class="form-control" ref="message_input" />
        <input type="submit" class="btn btn-primary mb-3" />
      </div>
    </form>
  </body>
</template>

This view consists of two parts: the first one is a repeater, which will repeat the <div> element for each line in the history. The second one is a form, much like the one we've seen in the connection view.

We're nearly done. We have to render the chat view from the chatroom action we've created earlier. Open app/client/controllers/chat_room.hpp and update the chatroom method as such:

app/client/controllers/chat_room.cpp
#include "../views/chat_room_view.hpp"

...
unique_ptr<ChatRoomView> chatroom_view;
...

void ChatRoomController::chatroom()
{
  Comet::body.empty();
  chatroom_view.reset(new ChatRoomView);
  chatroom_client.reset(new ChatRoomClient);
  chatroom_client->history_updated.connect(
    std::bind(&ChatRoomView::trigger_binding_updates, chatroom_view.get())
  );
  chatroom_view->send_message.connect([](const std::string& message)
  {
    chatroom_client->send_message(message);
  });
  chatroom_view->set_history(&chatroom_client->get_history());
  chatroom_client->connect(params["nickname"]);
  chatroom_view->bind_attributes();
  chatroom_view->append_to(Comet::body);
}

And this is it: the history attribute from the view is now a pointer to the history in our ChatRoomClient object, and the view bindings will all update whenever the history_updated signal is sent.
On the other side, when the send_message signal gets called on the view, it will call send_message on the ChatRoomClient object, and it will clear the message input.

All done. Now, build, launch the server, and profit:

$ crails build
$ build/server