Kaazing WebSocket Gateway

Orange guy with box of questions

Implementing a Stomp Protocol Client in Adobe Flex

Technologies used: Adobe Flex, Stomp, Kaazing WebSocket Gateway

This document contains the following sections:

Introduction

In this tutorial you will learn how to write your own protocol client implementation using the client libraries and what you must consider before writing your own protocol implementation. The tutorial then steps you through creating a protocol implementation in Adobe Flex. In this tutorial you implement the Stomp protocol, but you can take the same approach to implement any other protocol.

Before you start, take a closer look at the technologies that you are going to be working with: client libraries, Stomp, and Adobe Flex.

Overview of Kaazing Gateway Client Libraries

Kaazing Gateway offers set of protocol-specific client libraries available for both JavaScript and Adobe Flex. Currently, the following client libraries are available:

  • ServerSentEvents--Allows clients to connect to any standards-compliant Server-sent events stream
  • WebSocket--Allows clients to open a WebSocket connection to communicate directly with a back-end service using text-based protocols (Jabber, IMAP, and so on). A connection is established by specifying a target server URL
  • ByteSocket--Allows clients to open a WebSocket connection to communicate directly with a back-end service using binary protocols, such as AMQP. The ByteSocket client library is layered on top of WebSocket
  • StompClient-- Allows clients to send and receive messages to and from a Stomp-compliant server (for example, Apache ActiveMQ or RabbitMQ with the rabbitmq-stomp adapter), using Stomp (Streaming Text Orientated Messaging Protocol)

The client libraries are implemented using a layered architecture. For example, the ByteSocket client library is layered on top of the WebSocket client library and the StompClient client library is layered on top of the ByteSocket client library. Whereas the WebSocket client library enables direct communication using text-based protocols, the ByteSocket client library goes a step further to enable client-server communication using raw TCP.

The first three client libraries--ServerSentEvents, WebSocket, and ByteSocket--can be thought of as foundational libraries that are used to implement all other protocols. For example, the Adobe Flex Stomp protocol client that you are going to implement in this tutorial is effectively an implementation of the StompClient client library.

Refer to Architectural Overview for more information about Kaazing WebSocket Gateway.

Overview of Stomp

Stomp is a simple, yet effective protocol. It provides an interoperable wire format that allows Stomp clients to communicate with almost every available message broker. Examples of message brokers that provide built-in support for Stomp are Apache ActiveMQ and RabbitMQ with the rabbitmq-stomp adapter.

Stomp offers the following client commands:

  • ABORT
  • ACK
  • BEGIN
  • COMMIT
  • CONNECT
  • DISCONNECT
  • SEND
  • SUBSCRIBE
  • UNSUBSCRIBE

Stomp offers the following server frames:

  • ERROR
  • MESSAGE
  • RECEIPT

One important Stomp concept--the frame--deserves a little bit more explanation. A frame encapsulates the unit of communication between a client and a server. The following is an example of a Stomp CONNECT frame, which is used by the client to establish a connection to a back-end system (In this example, \n represents the newline character and ^@ represents the null character):

CONNECT\n
login: <username>\n
passcode:<passcode>\n
\n
^@

As shown in the example, the frame starts with a Stomp command (CONNECT, in this case), followed by a newline character. Next are the headers in <key>:<value> pairs followed by newline characters. A blank line indicates the end of the headers and the beginning of the message body. The null character indicates the end of the frame.

Refer to the Stomp Protocol Specification for more information about Stomp and the Stomp commands.

Overview of Adobe Flex

Adobe Flex is a platform that is used for the development and deployment of Flash applications (applications that rely on the Flash plugin for display in the browser). Typically, Flash applications are used to create rich presentation layers. The Adobe Flex platform consists of the following components:

  • The MXML declarative user interface language
  • The ActionScript scripting language
  • The Adobe Flex compiler
  • Runtime services

Flash is a compiled language. The Adobe Flex compiler is used to generate Shockwave Flash (SWF) files and Shockwave Component (SWC) files from source ActionScript and MXML files. In this tutorial you are going to build a SWC component that can be consumed by a SWF application.

Implementing a Stomp Protocol Client in Adobe Flex

Before you get started, take a look at what you are going to build. The protocol client you are going to build in this tutorial is implemented in Adobe Flex (ActionScript) and compiled into a SWC library. This library can be consumed in a Flash application (SWF), like the Stomp-Driven Adobe Flex demo application that ships with the Kaazing WebSocket Gateway demo bundle. That application uses a Stomp protocol client library, like the one you are going to build in this tutorial, to communicate with a back-end Stomp-compliant server, which serves as a message broker.

Before You Start

Before you start writing a single line of code, you have to study the protocol for which you want to implement your client and consider some of the choices you have to make.

Study the Protocol

Before you get started, you must study the specification of the Protocol you want to implement. You must fully understand the format of the client and server data frames. Are all client and server commands implemented using the same wire format (this is the case in Stomp, for example) or do different commands use different formats?

Binary versus Text

You must decide on which foundational client library your protocol client should be based--WebSocket or ByteSocket. For example, Stomp can contain binary data in the message payload, so a protocol implementation for Stomp has to be based on the ByteSocket client library. Text-based protocols, like Jabber, can be based on the WebSocket protocol instead.

Once you have a good understanding of the protocol you are going to implement and you have decided which foundational client libraries you are going to base your implementation on, it is time to roll up your sleeves and get started.

Setting Up Your Development Environment

To edit the ActionScript source file you can use your favorite IDE (Eclipse is used in this tutorial). To compile the ActionScript source file you can use the Adobe Flex 3 SDK or Adobe Flex Builder. You can download the Adobe Flex 3 SDK from the Adobe Flex SDK Website.

Tip: You can configure Eclipse to use JavaScript syntax highlighting for *.AS files by associating the *.as file type with the JavaScript editor as shown in the following figure..

Configuring File Association for *.AS files in Eclipse

To organize your Adobe Flex project, create a new Eclipse project and an ActionScript source file. To do this perform the following steps in Eclipse:

  1. Select File > New > Project
  2. Select General Project and click Next
  3. Enter StompClient as the project name, choose a project location, and click Finish
  4. Select the StompClient Project and Select File > New > File
  5. Enter StompClient.as as the file name and click Finish

Note: In the Kaazing WebSocket Gateway demo bundle, the StompClient file is located in the package com.kaazing.gateway.client.protocol. For this tutorial you can use any package structure you want, just be aware that applications that consume your finished StompClient.swc component file have to import the library using the fully classified path.

Writing the Stomp Protocol Client in ActionScript

To implement the Stomp Protocol client in Adobe Flex, you must perform the following steps:

  1. Add Import Statements
  2. Add the StompClient Class and Define Variables
  3. Add the Constructor Method
  4. Add the onopen and onclose Callback Functions
  5. Add the connect Function
  6. Add the writeFrame Function
  7. Add the readFragment Function
  8. Add Functions for the Remaining Client Commands
  9. Add Callback Functions for the Remaining Server Frames
  10. Process the Remaining Server Frames

Step 1--Add Import Statements

First, you must first import the various client libraries, including the ByteSocket client library, because Stomp requires a binary transport protocol. To do this, add the following package and import statements:

package {

  import flash.events.Event;
  import com.kaazing.gateway.client.html5.ByteSocket
  import com.kaazing.gateway.client.html5.ByteBuffer
  import com.kaazing.gateway.client.html5.MessageEvent
  import com.kaazing.gateway.client.html5.Charset

  /**
   * Note: Insert the code in the next step before the closing brace
   */

}

Note: Insert the code in the next step before the closing brace to avoid compilation issues.

Step 2--Add the StompClient Class and Define Variables

Next, create the StompClient class and define the _socket variable for the ByteSocket connection that you are going to use to communicate with your back-end service. Stomp is a framed protocol and the data frames you receive from the server may be fragmented over multiple bytesocket reads. Define the _buffer variable to store the fragmented Stomp frames that you receive from the server. For convenience and readability, also add some constant values for specific byte types that you must use while reading and writing Stomp frames. The following example shows how you can create a StompClient class:

/**
* StompClient provides a socket-based JavaScript client API to communicate
* with any compatible Stomp server process.
*/
public class StompClient {

  private var _socket:ByteSocket;
  private var _buffer:ByteBuffer;

  private const NULL_BYTE:int = 0x00;
  private const LINEFEED_BYTE:int = 0x0a;
  private const COLON_BYTE:int = 0x3a;
  private const SPACE_BYTE:int = 0x20;

  /**
   * Note: Insert the code in the following steps before the closing brace
   */

}

Note: Insert the sample code in the following steps before the closing brace to avoid compilation issues.

Step 3--Add the Constructor Method

Next, add the constructor method as follows:

/**
 * Creates a new StompClient instance.
 *
 * @constructor
 */
public function StompClient() {
}

Step 4--Add the onopen and onclose Callback Functions

Next, add the onopen and onclose callback functions. You add these callback functions, because the application that uses this client implementation has to know when the initial handshake between client and server has taken place so that the client can start sending messages to the server. The reverse is true as well--the client should know when the connection is closed (for any reason) so that it can stop sending messages. The following example shows how you can add the onopen and onclose callback functions:

/**
 * The onopen handler is called when the connect handshake is completed.
 *
 * @param headers the connected message headers
 */
public var onopen:Function = function(headers:Object):void {};

/**
 * The onclose handler is called when the connection is terminated.
 */
public var onclose:Function = function():void {};

Step 5--Add the connect Function

Next, add the connect function, which is used to initialize the communication with the back-end server. The connect function takes two parameters--location and a credentials object, which contains the username and password. The location parameter is a string that contains the URL of the back-end server. As part of the connect function, you must initialize the previously defined _buffer ByteBuffer variable to start receiving data from the back-end server that you are connecting to, as well as the ByteSocket connection variable_socket.

When you add the connect function, attach the following three callback handlers: onopen, onmessage, and onclose. When the socket establishes a connection with the server, it triggers the onopen callback function. This function starts the connect handshake by sending the CONNECT frame, using the writeFrame function, which is discussed in more detail in the next step. Any time the server sends data to the client it triggers the onmessage callback function, which reads the data fragment using the readFragment function, which is discussed in more detail later. When the socket connection terminates (either gracefully or abruptly) then the socket triggers the onclose callback function which calls the Stomp client's onclose callback function. The following example shows how you can add a connect function:

/**
 * Connects to the remote STOMP server with specified credentials.
 *
 * @param location the remote STOMP server location
 * @param credentials the username, password credentials
 */
public function connect(location:String, credentials:Object):void {
  // default the username and password to empty string
  var username:String = credentials.username || "";
  var password:String = credentials.password || "";

  _socket = new ByteSocket(location);
  _socket.onopen = function():void { _writeFrame("CONNECT", {"login": username,                                      "passcode": password}); };
  _socket.onmessage = function(event:MessageEvent):void { _readFragment(event); };
  _socket.onclose = function(event:Event):void { onclose(); };

  // initialize read buffer
   _buffer = new ByteBuffer();
}

Step 6--Add the writeFrame Function

Next, you must create a function (writeFrame) that writes the frames in the way your protocol expects them. This is protocol-specific and requires that you have studied the protocol carefully. Some protocols have different frame formats for different commands, but in our Stomp example, all the frames (both client and server frames) use the same format, which makes it possible to use a single writeFrame function to write all the command frames.

To write a frame, you put bytes into a ByteBuffer (called frame in our example). A ByteBuffer is an array of byte-sized numbers. The ByteBuffer exposes information about the position for the next write; the limit, or the location at which you cannot read anymore; the capacity, or the maximum number of bytes that can be written to the buffer; and the order, or how numerical values are read from the ByteBuffer (either using the big-endian or little-endian byte order with big-endian being the default).

Just before writing the frame content in the buffer to the socket, the buffer is flipped so that it can be read. During the writing of the frames, the constants that were defined earlier are used for the special bytes. The following example shows how you can add the writeFrame function:

private function _writeFrame(command:String, headers:Object,
                             body:*=undefined):void {
  // create a new frame buffer
  var frame:ByteBuffer = new ByteBuffer();

  // build the command line
  frame.putString(command, Charset.UTF8);
  frame.put(LINEFEED_BYTE);

  // build the headers lines
  for (var key:String in headers) {
    var value:* = headers[key];
    if (typeof(value) == "string") {
      var header:String = String(value);
      frame.putString(key, Charset.UTF8);
      frame.put(COLON_BYTE);
      frame.put(SPACE_BYTE);
      frame.putString(header, Charset.UTF8);
      frame.put(LINEFEED_BYTE);
    }
  }

  // add "content-length" header for binary content
  if (body !== undefined && body.constructor === ByteBuffer) {
    frame.putString("content-length", Charset.UTF8);
    frame.put(COLON_BYTE);
    frame.put(SPACE_BYTE);
    frame.putString(String(body.remaining()), Charset.UTF8);
    frame.put(LINEFEED_BYTE);
  }

  // empty line at end of headers
  frame.put(LINEFEED_BYTE);

  // add the body (if specified)
  switch (typeof(body)) {
    case "string":
      // add as text content
      frame.putString(body, Charset.UTF8);
      break;
    case "object":
      // add as binary content
      frame.putBuffer(body);
      break;
    }

  // null terminator byte
  frame.put(NULL_BYTE);

  // flip the frame buffer
  frame.flip();

  // send the frame buffer
  _socket.postMessage(frame);
}

Step 7--Add the readFragment Function

Next, you must create a function (readFragment) that reads data fragments of the ByteSocket that is sent from the server to the client. readFragment tries to process a complete frame and retains incomplete frames in a read buffer until enough fragments arrive to form a complete frame. Once again, specific protocol knowledge is required to parse the incoming frames correctly. Since all the frames (both client and server frames) use the same format in Stomp, you can use a single readFragment function to read all the server frames.

The first frame that is sent by the server is a CONNECTED frame, which triggers the previously defined onopen callback function and signals to the client that it is now ready to start communicating. The following example shows how you can add the readFragmentfunction:

private function _readFragment(event:MessageEvent):void {
  var buffer:ByteBuffer = _buffer;
  var limit:int;

  // skip to the end of the buffer
  buffer.skip(buffer.remaining());

  // append new data to the buffer
  buffer.putBuffer(event.data);

  // prepare the buffer for reading
  buffer.flip();

  outer: while (buffer.hasRemaining()) {
    // initialize frame
    var frame:Object = { headers: {} };

    // Note: skip over empty line at start of frame
    // scenario can occur due to fragmentation
    // if Apache ActiveMQ STOMP end-of-frame newline
    // spills into the start of the next frame
    if (buffer.getAt(buffer.position) == LINEFEED_BYTE) {
      buffer.skip(1); // linefeed
    }

    // mark read progress
    buffer.mark();

    // search for command
    var endOfCommandAt:int = buffer.indexOf(LINEFEED_BYTE);
    if (endOfCommandAt == -1) {
      buffer.reset();
      break;
    }

    // read command
    limit = buffer.limit;
    buffer.limit = endOfCommandAt;
    frame.command = buffer.getString(Charset.UTF8);
    buffer.limit = limit;

    // skip linefeed byte
    buffer.skip(1);

    while(true) {
      var endOfHeaderAt:int = buffer.indexOf(LINEFEED_BYTE);

      // detect incomplete frame
      if (endOfHeaderAt == -1) {
        buffer.reset();
        break outer;
      }

      // detect header or end-of-headers
      if (endOfHeaderAt > buffer.position) {
        // non-empty line: header
        limit = buffer.limit;
        buffer.limit = endOfHeaderAt;
        var header:String = buffer.getString(Charset.UTF8);
        buffer.limit = limit;

        // process header line
        var endOfName:int = header.search(":");
        frame.headers[header.slice(0, endOfName)] = header.slice(endOfName + 1);

        // skip linefeed byte
        buffer.skip(1);
      }
    else {
      // skip linefeed byte
      buffer.skip(1);

      // empty line: end-of-headers
      var length:Number = Number(frame.headers['content-length']);
      var pattern:RegExp = /;\scharset=/;
      var contentTypeAndCharset:Array = String(frame.headers['content-type'] ||                                         "").split(pattern);

      // RabbitMQ always sends content-length header, even for text payloads
      // but then also includes content-type header with value "text/plain"
      // ActiveMQ only sends content-length for binary payloads
      // Payload is binary if content-length header is sent, and content-type
      // header is not "text/plain" (may be undefined)
      // Added additional check to look for "text/plain" instead of the exact
      // match, as the content-type value can be like "text/plain; charset=UTF-8"
      // RabbitMQ sends content-length but no content-type for ERROR messages
      // so assume text content for ERROR messages
      if (frame.command != "ERROR" && !isNaN(length) && contentTypeAndCharset[0] !=                            "text/plain") {
        // content-length specified, binary content

        // detect incomplete frame
        if (buffer.remaining() < length + 1) {
          buffer.reset();
          break outer;
        }

        // extract the frame body as byte buffer
        limit = buffer.limit;
        buffer.limit = buffer.position + length;
        frame.body = buffer.slice();
        buffer.limit = limit;
        buffer.skip(length);

        // skip null terminator, unless buffer already consumed
        if (buffer.hasRemaining()) {
          buffer.skip(1);
        }
      }
      else {
        // content-length not specified, text content

        // detect incomplete frame
        var endOfFrameAt:int = buffer.indexOf(NULL_BYTE);
        if (endOfFrameAt == -1) {
          buffer.reset();
          break outer;
        }

        // verify that UTF-8 charset is appropriate
        var charset:String = ((contentTypeAndCharset[1] as String) ||
                               "utf-8").toLowerCase();
        if (charset != "utf-8" && charset != "us-ascii") {
          throw new Error("Unsupported character set: " + charset);
        }

        // extract the frame body as null-terminated string
        frame.body = buffer.getString(Charset.UTF8);
      }


      // invoke the corresponding handler
      switch (frame.command) {
        case "CONNECTED":
          onopen(frame.headers);
          break;
          
          //insert more code here later
          
        default:
          throw new Error("Unrecognized STOMP command '" + frame.command + "'");
        }

        break;
      }
    }
  }

// compact the buffer
buffer.compact();
}

Step 8--Add Functions for the Remaining Client Commands

Next, you must define a function for each of the remaining client commands in the protocol. The functions simply have to pass the parameters they can accept to the writeFrame function. The following example shows how you can add the remaining client commands discussed in the Stomp overview:

/**
 * Disconnects from the remote STOMP server.
 */
public function disconnect():void {
  if (_socket.readyState === 1) {
    _writeFrame("DISCONNECT", {});
  }
}

/**
 * Sends a message to a specific destination at the remote STOMP Server.
 *
 * @param body the message body
 * @param destination the message destination
 * @param transactionId the transaction identifier
 * @param receiptId the message receipt identifier
 * @param headers the message headers
 */
public function send(body:String, destination:String, transactionId:String="",   receiptId:String="", headers:*=null):void {
  var headers0:* = headers || {};
  headers0["destination"] = destination;
  headers0["transaction"] = transactionId;
  headers0["receipt"] = receiptId;
  _writeFrame("SEND", headers0, body);
}

/**
 * Subscribes to receive messages delivered to a specific destination.
 *
 * @param destination the message destination
 * @param acknowledge the acknowledgment strategy
 * @param receiptId the message receipt identifier
 * @param headers the subscribe headers
 */
public function subscribe(destination:String, acknowledgement:String="",                 receiptId:String="", headers:*=null):void {
  var headers0:* = headers || {};
  headers0["destination"] = destination;
  headers0["ack"] = acknowledgement;
  headers0["receipt"] = receiptId;
  _writeFrame("SUBSCRIBE", headers0);
}

/**
 * Unsubscribes from receiving messages for a specific destination.
 *
 * @param destination the message destination
 * @param receiptId the message receipt identifier
 * @param headers the unsubscribe headers
 */
public function unsubscribe(destination:String, receiptId:String="",   headers:*=null):void {
  var headers0:* = headers || {};
  headers0["destination"] = destination;
  headers0["receipt"] = receiptId;
  _writeFrame("UNSUBSCRIBE", headers0);
}

/**
 * Begins a new transaction.
 *
 * @param id the transaction identifier
 * @param receiptId the message receipt identifier
 * @param headers the begin headers
 */
public function begin(id:String, receiptId:String="", headers:*=null):void {
  var headers0:* = headers || {};
  headers0["transaction"] = id;
  headers0["receipt"] = receiptId;
  _writeFrame("BEGIN", headers0);
}

/**
 * Commits an existing transaction.
 *
 * @param id the transaction identifier
 * @param receiptId the message receipt identifier
 * @param headers the begin headers
 */
public function commit(id:String, receiptId:String="", headers:*=null):void {
  var headers0:* = headers || {};
  headers0["transaction"] = id;
  headers0["receipt"] = receiptId;
  _writeFrame("COMMIT", headers0);
}

/**
 * Aborts an existing transaction.
 *
 * @param id the transaction identifier
 * @param receiptId the message receipt identifier
 * @param headers the begin headers
 */
public function abort(id:String, receiptId:String="", headers:*=null):void {
  var headers0:* = headers || {};
  headers0["transaction"] = id;
  headers0["receipt"] = receiptId;
  _writeFrame("ABORT", headers0);
}

/**
 * Acknowledges a received message.
 *
 * @param messageId the message identifier
 * @param transactionId the transaction identifier
 * @param receiptId the message receipt identifier
 * @param headers the acknowledgment headers 
 */
public function ack(messageId:String, transactionId:String, receiptId:String="",   headers:*=null):void {
  var headers0:* = headers || {};
  headers0["message-id"] = messageId;
  headers0["transaction"] = transactionId;
  headers0["receipt"] = receiptId;
  _writeFrame("ACK", headers0);
}

Step 9--Add Callback Functions for the Remaining Server Frames

Next, you must define a callback function for each of the remaining server frames in the protocol. The functions simply have to pass the parameters they can accept to the readFragment function. You already added the onopen and onclose callback functions earlier. The following example shows how you can add the remaining server frames discussed in the Stomp overview:

/**
 * The onmessage handler is called when a message is delivered to a subscribed
 * destination.
 *
 * @param headers the message headers
 * @param body the message body
 */
public var onmessage:Function = function(headers:Object, body:*):void {};

/**
 * The onreceipt handler is called when a message receipt is received.
 *
 * @param headers the receipt message headers
 */
public var onreceipt:Function = function(headers:Object):void {};

/**
 * The onerror handler is called when an error message is received.
 * @param headers the error message headers
 * @param body the error message body
 */
public var onerror:Function = function(headers:Object, body:String):void {};

Step 10--Process the Remaining Server Frames

Finally, you must add some switch statements to cover frames of all the command types that the server can send, For example, data fragments build up to form a frame with the command type MESSAGE and when the complete frame is available it triggers the onmessage callback function.

To process the remaining server frames, replace the following comment in the switch statement at the end of the readFragment code snippet that you inserted earlier:

//insert more code here later

With the following code:

case "MESSAGE":
  onmessage(frame.headers, frame.body);
  break;
case "RECEIPT":
  onreceipt(frame.headers);
  break;
case "ERROR":
  onerror(frame.headers, frame.body);
  break;

Compiling the Stomp Protocol Client

Now that the ActionScript code is complete, compile the ActionScript file into a SWC library. To do this, perform the following steps:

  1. Open a command prompt
  2. Navigate to the folder that contains the ActionScript file
  3. Run the following command:

ADOBE_FLEX_SDK_HOME/bin/compc -source-path . -include-classes StompClient -directory=false -library-path+=KAAZING_HOME/lib/client/flex -debug=false -output target/StompClient.swc

The library-path argument should point to the directory that contains the Kaazing Flex client library file kaazing-enterprise-gateway-client-release-version.swc. This SWC file can be found in the Kaazing Gateway demo bundle in the directory KAAZING_HOME/lib/client/flex.

Congratulations! You just finished building a Stomp protocol client in Adobe Flex!

Note: The file StompClient-completed.as, located in KAAZING_HOME/web/tutorials/stomp-protocol contains a complete version of the file for your convenience. Rename the file to StompClient.as before you compile it.

Testing the Stomp Protocol Client

To test the Stomp protocol client that you just built, you must use it in a Stomp-driven application, like the Flash demo that comes preconfigured in the Kaazing WebSocket Gateway demo bundle. To do this, you must compile the Adobe Flash application (for example, an MXML file) using the SWC file that you just built on the library path. Refer to Creating a Stomp-Driven Application in Adobe Flex for more information about compiling and running the Stomp-based Adobe Flex application.