How to Implement a Protocol Client
This document contains the following sections:
- Introduction
- WebSocket and Protocol Frame Boundaries in Stream-Based Data
- Implementing a STOMP Protocol Client Overview
- Implementing a STOMP Protocol Client in Adobe Flex
Introduction
The goal of this tutorial is to explain how to handle WebSocket frame boundaries in a client for any framed protocol, and how to ensure your client can handle WebSocket frame boundaries in stream-based data correctly. To describe how to create a protocol client that can handle WebSocket frame boundaries, this tutorial describes the code required in a STOMP protocol client that communicates with the Gateway to access a back-end STOMP-compliant server.The steps in this tutorial show you how to use the Kaazing WebSocket Gateway client libraries to create a Shockwave Component (SWC) that can be consumed by a Shockwave Flash (SWF) application, and how to build a STOMP protocol client library to communicate with a back-end STOMP-compliant server, that serves as a message broker. This tutorial does not walk you through creating a finished client, and does not include code for a Graphical User Interface (GUI).
WebSocket and Protocol Frame Boundaries in Stream-Based Data
The Gateway frames the TCP-based protocol data frames it receives from the back-end server in WebSocket frames and sends these to protocol client applications. The TCP-based protocol data is therefore split across multiple WebSocket frames when it arrives at the protocol client. Protocol client applications must then assemble the WebSocket frames to obtain the TCP-based protocol data within each WebSocket frame (in the WebSocket frame payload) and reconstitute that data. If the protocol data is raw bytes, then the application must be able to handle raw bytes.
One common example of a protocol client that can manage WebSocket frames is a web browser. When the Gateway proxies TCP-based protocol traffic to web browsers over WebSocket it frames the TCP frames from the back-end server in WebSocket frames. These WebSocket frames are sent to the web browser that must then parse the WebSocket frames to obtain the payload of those frames (the TCP-based protocol data stored in each frame) and reconstitute the TCP-based protocol data.
If a protocol client application does not anticipate frame boundaries and protocol parsing, then when it receives partial TCP data messages across WebSocket frames it will be unable to reconstitute these messages correctly. This type of error can occur in the following scenario:
- Back-end server. The back-end server sends and receives data with the Gateway using a TCP-based protocol. The TCP traffic is sent to the Gateway in TCP data frames and fragmented as needed.
- The Gateway. The Gateway acts as a proxy between the TCP back-end server and clients, and uses the WebSocket protocol to communicate with clients. The Gateway frames the TCP-based protocol data frames in WebSocket frames and sends these to the client application.
- Protocol client application. The protocol client application communicating with the Gateway assumes that it is receiving complete messages and that no fragmentation of the TCP-based protocol has occurred.
As a result, the protocol client application does not attempt to identify WebSocket frame boundaries and reconstitute frame payloads and communication fails. This error can occur in situations where the Gateway is deployed to extend a specific TCP-based protocol over the Web using WebSocket. In some cases, developers might not notice framing issues during development because they are only streaming single messages locally in their development environment.
The protocol client application example provided in this tutorial is intended to help developers understand how to write protocol clients that manage protocol frame boundaries when communicating with the Gateway. For example, in the code samples provided in this tutorial, you will create a function (readFragment) that reads data fragments sent from the server to the client. readFragment tries to process a complete frame from the fragments and retains incomplete frames in a read buffer until enough fragments arrive to form a complete frame. You will also create a function (writeFrame) that writes the frames in the way the protocol expects them.
Notes:- For detailed information about frames and fragmentation with WebSocket, see RFC 6455, Section 5.
- Clients using WebSocket must support receiving both fragmented and unfragmented messages.
Implementing a STOMP Protocol Client Overview
In this tutorial you will learn how to write your own protocol client implementation using the Kaazing WebSocket Gateway 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, review the technologies that you are going to be working with: client libraries, STOMP, and Adobe Flex.
Overview of Kaazing WebSocket Gateway Client Libraries
The Gateway offers set of protocol-specific client libraries available for JavaScript, Java, Adobe Flex, .NET and Silverlight. 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
The client libraries are implemented using a layered architecture. For example, the ByteSocket client library is layered on top of the WebSocket client library. The WebSocket client library enables direct communication using text-based protocols, and the ByteSocket client library goes a step further to enable client-server communication using raw TCP.
The three client libraries—ServerSentEvents, WebSocket, and ByteSocket—can be thought of as foundational libraries that are used to implement all other protocols.
Overview of STOMP
STOMP (Simple Text Orientated Messaging Protocol) is a simple, yet effective protocol that 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. The example code in this tutorial accounts for the differences in how ActiveMQ and RabbitMQ identify content types. STOMP is language-agnostic, meaning clients and brokers developed in different languages can send and receive messages.
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 STOMP frame—deserves a little bit more explanation. A STOMP 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
Implementing a STOMP Protocol Client in Adobe Flex
Flash is a compiled language. The Adobe Flex compiler is used to generate Shockwave Flash (SWF) files and Shockwave Component (SWC) files from ActionScript and MXML source files. In this tutorial you are going to build a SWC library that can be consumed in a Flash application (SWF), such as a STOMP-Driven Adobe Flex application.
Before You Start
Important: 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, study the specification of the protocol you want to implement. You must fully understand the format of the client and server data frames. For example, are all client and server commands implemented using the same wire format in your protocol as with STOMP, 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.
Setting Up Your Development Environment
To edit the ActionScript source file you can use your favorite IDE or text editor. To compile the ActionScript source file you can use the Adobe Flex 4.6 SDK or Adobe Flash Builder. You can download the Adobe Flex 4.6 SDK or Adobe Flash Builder.
If you use Adobe Flash Builder, create a new project and an ActionScript source file using the following steps in Adobe Flash Builder:
- Select File, click New, and then click Flex Project.
- Enter StompClient in Project name, choose a project location, and click Finish.
- Select File, click New, and then click ActionScript File.
- Enter StompClient in File name, and click Finish.
Writing the STOMP Protocol Client in ActionScript
To implement the STOMP Protocol client in Adobe Flex, you must perform the following steps:
- Add Import Statements
- Add the StompClient Class and Define Variables
- Add the Constructor Method
- Add the onopen and onclose Callback Functions
- Add the connect Function
- Add the writeFrame Function
- Add the readFragment Function
- Add Functions for the Remaining Client Commands
- Add Callback Functions for the Remaining Server Frames
- Process the Remaining Server Frames
Step 1: Add Import Statements
First, you must first import the various client libraries from GATEWAY_HOME/lib/flash (using the import method available in Adobe Flash Builder), 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.ByteSocket import com.kaazing.gateway.client.ByteBuffer import com.kaazing.gateway.client.MessageEvent import com.kaazing.gateway.client.Charset /** * Note: Insert the code in the next step before the closing brace */ }Important: Insert the code in the next step before the closing brace above (}) to avoid compilation errors.
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 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 */ }Important: Insert the sample code in the following steps before the closing brace above (}) to avoid compilation errors.
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 in order that the application that uses this client implementation knows when the initial handshake between client and server has taken place, and 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 that 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.send(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 readFragment function:
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 (line 138 in Add the readFragment Function):
//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:
- Open a command prompt.
- Navigate to the folder that contains the ActionScript file.
- Run the following command:
ADOBE_FLEX_SDK_HOME/bin/compc -source-path . -include-classes StompClient -directory=false -library-path+=GATEWAY_HOME/lib/client/flash -debug=false -output target/StompClient.swc
The library-path argument should point to the directory that contains the Kaazing WebSocket Gateway Flash client library file: GATEWAY_HOME/lib/client/flash/com.kaazing.gateway.client.swc
Congratulations! You just finished building a STOMP protocol SWC library in Adobe Flex!
Testing the STOMP Protocol Client
This tutorial is intended to show you how to handle frame boundaries and did not take you through the complete process of creating an application.