A Websocket's Echo Server in Catalyst

Overview

This past year Catalyst got the basics of support for long / persistent connections so that you could build realtime applications over cometd and websockets. Lets build the classic websockets echo server.

Introduction

"The websockets specification defines an API that enables web pages to use the WebSockets protocol for two-way communication with a remote host." - http://www.websocket.org/aboutwebsocket.html.

One of the basic examples you find on the websockets website is the Echo server which you can see over here: http://www.websocket.org/echo.html Any web framework which claims support should provide a version, just to show off the basics. Let's do it in Catalyst!

The Controller

    package MyApp::Controller::Root;

    use base  'Catalyst::Controller';
    use Protocol::WebSocket::Handshake::Server;
    use AnyEvent::Handle;




    sub index :Path(/)
    {
      my ($self, $c) = @_;
      my $url = $c->uri_for_action($self->action_for('ws'));

      $url->scheme('ws');
      $c->stash(websocket_url => $url);
      $c->forward($c->view('HTML'));
    }

    sub ws :Path(/echo)
    {
      my ($self, $c) = @_;
      my $io = $c->req->io_fh;
      my $hs = Protocol::WebSocket::Handshake::Server
        ->new_from_psgi($c->req->env);

      $hs->parse($io);

      my $hd = AnyEvent::Handle->new(fh => $io);
      $hd->push_write($hs->to_string);
      $hd->push_write($hs->build_frame(buffer => "Echo Initiated")->to_bytes);

      $hd->on_read(sub {
        (my $frame = $hs->build_frame)->append($_[0]->rbuf);
        while (my $message = $frame->next) {
          $message = $hs->build_frame(buffer => $message)->to_bytes;
          $hd->push_write($message);
        }
      });
    }

    __PACKAGE__->config( namespace => '');

So there's a controller with jsut two actions. Lets look at the first:

    sub index :Path(/)
    {
      my ($self, $c) = @_;
      my $url = $c->uri_for_action($self->action_for('ws'));

      $url->scheme('ws');
      $c->stash(websocket_url => $url);
      $c->forward($c->view('HTML'));
    }

All we are doing here is creating a single URL in the stash and sending it on to the view. This is the URL of the websockets handler (which is why we need to change the schema to 'ws'.

We are not using the standard 'renderview' actionclass in this trivial application so I go ahead and forward on the view. Personally I am not a fan of the renderview actionclass and quite often just handle it like this.

Lets look at the view (warning Javascript ahead for the unwilling).

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <title>WebSocket Echo Client</title>
      <meta charset="UTF-8" />
      <script>
        "use strict";
        window.addEventListener("load", function(event) {
          var status = document.getElementById("status");
          var url = document.getElementById("url");
          var open = document.getElementById("open");
          var close = document.getElementById("close");
          var send = document.getElementById("send");
          var text = document.getElementById("text");
          var message = document.getElementById("message");
          var socket;

          status.textContent = "Not Connected";
          url.value = "[% websocket_url %]";
          close.disabled = true;
          send.disabled = true;

          // Create a new connection when the Connect button is clicked
          open.addEventListener("click", function(event) {
            open.disabled = true;
            socket = new WebSocket(url.value);

            socket.addEventListener("open", function(event) {
              close.disabled = false;
              send.disabled = false;
              status.textContent = "Connected";
            });

            // Display messages received from the server
            socket.addEventListener("message", function(event) {
              message.textContent = "Server Says: " + event.data;
            });

            // Display any errors that occur
            socket.addEventListener("error", function(event) {
              message.textContent = "Error: " + event;
              console.log("my object: %o", event);
            });

            socket.addEventListener("close", function(event) {
              open.disabled = false;
              status.textContent = "Not Connected";
            });
          });

          // Close the connection when the Disconnect button is clicked
          close.addEventListener("click", function(event) {
            close.disabled = true;
            send.disabled = true;
            message.textContent = "";
            socket.close();
          });

          // Send text to the server when the Send button is clicked
          send.addEventListener("click", function(event) {
            socket.send(text.value);
            text.value = "";
          });
        });
      </script>
    </head>
    <body>
      Status: <span id="status"></span><br />
      URL: <input id="url" /><br />
      <input id="open" type="button" value="Connect" />&nbsp;
      <input id="close" type="button" value="Disconnect" /><br />
      <input id="send" type="button" value="Send" />&nbsp;
      <input id="text" /><br />
      <span id="message"></span>
    </body>
    </html>

As this is a Catalyst article, I am not going to step through this line by line, but to cover it in general I am using the plainest Javascript here (yes if we used a Javascript framework there'd be less code, but I don't want to obscure the point with yet another framework). I associate various callbacks as event handlers for the most important bits of DOM here. The biggest part is the bit that opens the websocket and does the send and receive. That gets hooked up to the 'ws' action. Lets look at that.

    sub ws :Path(/echo)
    {
      my ($self, $c) = @_;
      my $io = $c->req->io_fh;
      my $hs = Protocol::WebSocket::Handshake::Server
        ->new_from_psgi($c->req->env);

      $hs->parse($io);

      my $hd = AnyEvent::Handle->new(fh => $io);
      $hd->push_write($hs->to_string);
      $hd->push_write($hs->build_frame(buffer => "Echo Initiated")->to_bytes);

      $hd->on_read(sub {
        (my $frame = $hs->build_frame)->append($_[0]->rbuf);
        while (my $message = $frame->next) {
          $message = $hs->build_frame(buffer => $message)->to_bytes;
          $hd->push_write($message);
        }
      });
    }

Since websocket support is new in Catalyst there's not a lot of pretty helpers and shortcut methods. So we need to call Protocol::WebSocket to initiate the websocket, and then we create a 'on_read' handler to listen on the socket, and the we add to the write queue whatver shows up. Lets break it down a bit:

      my ($self, $c) = @_;
      my $io = $c->req->io_fh;
      my $hs = Protocol::WebSocket::Handshake::Server
        ->new_from_psgi($c->req->env);




The key to the magic is the Catalyst::Request method io_fh. This gives you low level access to the underlying psgix.io socket, and is also a flag to Catalyst so that it never tries to finalize the body (since you are never really closing the connection). We then create a handshake server object via Protocol::WebSocket::Handshake::Server. Now, this isnt' a running server, but rather the server's side of the Websocket protocol. Since we are the server, that's the one we need (and there's a client side protocol as well, go check out the full docs of Protocol::WebSocket for more). this $hs object parses the information coming across the websocket and presents it in a manner that is easy to use in Perl code, and it also does the opposite, it takes some Perl data and preps it to send across the open websocket.

     $hs->parse($io);

This the the handshake server parsing the start of the socket, which if you looked at it would like a lot of HTTP but with a few extra bits to say we'd like to upgrade the connection to a websocket (it does this using the documented HTTP/1.1 Upgrade header, so yes the people that did HTTP 1.1 all those years ago were really thinking ahead!)

      my $hd = AnyEvent::Handle->new(fh => $io);
      $hd->push_write($hs->to_string);
      $hd->push_write($hs->build_frame(buffer => "Echo Initiated")->to_bytes);

This converts the raw $io socket to socket resembling a bidirection filehandle with a clear API we can use to attache read and write events. We then initiate ther websocket protocol upgrade and send out first frew frames of data. If you go back to look at the Javascript end of things:

    // Display messages received from the server
    socket.addEventListener("message", function(event) {
      message.textContent = "Server Says: " + event.data;
    });

Here's the message event that happens when those frames are sent across.

The next bit is the code that listens to the read event, which happens when frames are pushed across the websocket from the browser to the server:

      $hd->on_read(sub {
        (my $frame = $hs->build_frame)->append($_[0]->rbuf);
        while (my $message = $frame->next) {
          $message = $hs->build_frame(buffer => $message)->to_bytes;
          $hd->push_write($message);
        }
      });

The on_read associates a callback to handle any incoming messages. We use the handshake server to decode the frames and then we turn around and take that decoded message and put it right back on the write queue. So when someone sends a message from the browser, we decode it, and send it back across the same websocket up the the browser. Thus completing the echo!

So, that's really it!

Summary

Although support for techniques like websockets is still rather new to Catalyst you can still use it for testing and for helping us figure out where to go next. So, go play!

Code associated with this article:

https://github.com/perl-catalyst/2013-Advent-Staging/tree/master/Websocket-Echo

Author

John Napiorkowski jjnapiork@cpan.org