Chapo Decoder Example
A full decoder SDK example with comments
The following script decoder relates to the hypothetical protocol named "chapo". The reason we use a made up protocol in this example rather than a pre-existing one is:
So the protocol is very simple to document.
To require and thus show off many features of the script decoder.
Description of chapo protocol:
In the fictional chapo format each message begins with a header composed of:
Bell (ascii character 7)
Message Type (only byte ASCII '8' supported)
32-bit unsigned integer - length of message body, not including header.
64-bit unsigned integer - sequence id.
The unsigned integers are encoded in network-byte order. Here is an example header:
<7: 1 byte><ASCII '8': 1 byte><103: 4 bytes><1: 8 bytes>
This indicates a message with a body of size 103 and a sequence of 1.
The message body is composed of key value pairs. Pairs are seperated by the '~' character, and keys are seperated from values by the '=' character.
For example:
M=hello~T=1234
This represents two key value pairs ("M", "hello") and ("T", 1234)
Numbers are encoded as ASCII strings in the message body.
The chapo decoder script
// Load console module for writing messages to the terminal display.
var console = vu8.load(
'console'
),
idx =
0
,
CHAPO_MSG_START =
7
,
CHAPO_MSG_TYPE =
8
,
CHAPO_HEADER_SIZE =
14
,
// uint64 sequence, start, type, uint32 size
PAIR_SEP_BYTE =
'~'
.charCodeAt(
0
),
KEY_VAL_SEP_BYTE =
'='
.charCodeAt(
0
)
// Create 4 datafield objects used for this decoder
var dfMarketId =
new
decoder.DatafieldUInt(
"script_v8.market_id"
),
dfSequence =
new
decoder.DatafieldUInt(
"script_v8.sequence"
),
dfSize =
new
decoder.DatafieldUInt(
"script_v8.size"
),
dfSymbolTicker =
new
decoder.DatafieldString(
"script_v8.symbol_ticker"
)
try
{
// Register datafields with decoder's protocol layer.
decoder.protocol.addDatafield(dfMarketId)
decoder.protocol.addDatafield(dfSequence)
decoder.protocol.addDatafield(dfSize)
decoder.protocol.addDatafield(dfSymbolTicker)
}
catch
(e) {
// C++ exceptions propogate to JavaScript exceptions...
console.log(
'chapo decoder: error adding data fields'
)
}
console.log(
'chapo decoder: loaded script'
)
////////////////////////////////////////////////////////////////////////////
// The next two functions are events called by the underlying stack API
// "on_new_decoder" is called each time a new decoder is created and must be
// defined in every script.
function on_new_decoder(dec) {
dec.idx = ++idx
// market ID is a 64-bit integer and JavaScript numbers cannot represent
// this type so decoder.UInt64 is provided for handling this type.
dec.marketId =
new
decoder.UInt64()
dec.sequence =
new
decoder.UInt64()
// dfSize is 32-bit so can use a plain JavaScript number.
dec.dfSize =
0
// Use this to signal if a message begin marker has been found.
dec.foundMsgBegin =
false
// Can associate method with decoder object for use in on_data callback.
dec.log = function(msg) {
console.log(
'chapo decoder'
,
this
.idx +
':'
, msg)
}
console.log(
'created new chapo decoder'
)
}
// "on_data" is called when a decoder receives new data and must be defined
// in every script.
// The dec.buffer attribute is a Buffer object which represents a series of
// underlying buffers as a single conceptual buffer.
// The fact that the user is dealing with multiple underlying buffers
// is hidden by the Buffer object API.
// The last buffer in the chain is not copied from the underlying
// stream until after on_data has been called (in the event that after
// such a call there is still data remaining in the buffer) allowing
// for zero-copy memory handling when possible.
function on_data(dec) {
// Use method associated with decoder object by "on_new_decoder".
// size is the number of bytes left in the buffer.
dec.log(
'new data available, reimaning buffer length '
+ dec.buffer.size())
if
(! dec.dfSize) {
parse_header(dec)
if
(! dec.dfSize)
return
}
while
(parse_message(dec)) {
dec.foundMsgBegin =
false
parse_header(dec)
if
(! dec.dfSize)
return
}
}
////////////////////////////////////////////////////////////////////////////
// helper methods from here on
// Parse chapo header.
function parse_header(dec) {
if
(! dec.foundMsgBegin) {
// Skips up to and over the character with the ascii byte given..
// returns true if the byte is found otherwise empties the buffer
// and returns false.
if
(dec.buffer.skipToAndConsumeByte(CHAPO_MSG_START))
dec.foundMsgBegin =
true
else
return
}
// Doing this is faster than dec.buffer.size() < 5, there is also:
// sizeLessThanOrEqualTo, sizeGreaterThan, sizeGreaterThanOrEqualTo
if
(dec.buffer.sizeLessThan(CHAPO_HEADER_SIZE -
1
))
return
// Javascript has no facility for dealing with characters, so passing a
// number through to isByte() is better than having to deal with expensive
// string types.
while
(! dec.buffer.isByte(CHAPO_MSG_TYPE)) {
dec.log(
'unsupported message type'
)
if
(! dec.buffer.skipToAndConsumeByte(CHAPO_MSG_START)) {
dec.foundMsgBegin =
false
return
}
if
(dec.buffer.sizeLessThan(CHAPO_HEADER_SIZE -
1
))
return
}
// This advances the current character pointer in the buffer but does not
// reclaim memory until an entire underlying buffer within the chain has
// been advanced over so it is efficient to use.
dec.buffer.advance(
1
)
// Consume a byte-encoded uint32 from the buffer using network-byte
// order. Use consumeUInt32LE to decode the integer using little-endian
// byte ordering. There is also consumeInt32 and consumeInt32LE for
// signed integers. This advances the buffer past the decoded data.
dec.dfSize = dec.buffer.consumeUInt32()
// An alternative way to do the above two operations would be:
// dec.dfSize = dec.buffer.decodeUInt32(1)
// dec.buffer.advance(5)
// Binary decode uint64 from head of buffer and advance buffer pointer
// past it.
// Can use consumeLE to decode it using little-endian byte ordering.
dec.sequence.consume(dec.buffer)
// equivalent to the above:
// dec.sequence.decode(dec.buffer, 0)
// dec.buffer.advance(8)
// There are decode*() versions of all consume*() methods. Decode is like
// consume but does not advance the buffer and takes an argument indicating
// the offset of the data to decode.
dec.log(
'found message header which indicates a payload of size '
+ dec.dfSize)
}
// Parse chapo message body. Return true if current message in buffer is
// finished with. If it is finished and parsed successfully the buffer
// will be advanced past it.
function parse_message(dec) {
if
(dec.buffer.sizeLessThan(dec.dfSize)) {
dec.log(
'chapo message split across multiple packets'
)
return
false
}
// Buffer.substring operates in the same manner as String.substring
// in JavaScript.
dec.log(
"buffer contains complete chapo message '"
+
dec.buffer.substring(
0
, dec.dfSize) +
"'"
)
var pairIdx, nextPairIdx = -
1
while
(nextPairIdx < dec.dfSize) {
pairIdx = nextPairIdx +
1
var sepIdx = dec.buffer.indexOfByteFrom(KEY_VAL_SEP_BYTE, pairIdx +
1
)
nextPairIdx = dec.buffer.indexOfByteFrom(PAIR_SEP_BYTE, sepIdx +
1
)
var key = dec.buffer.substring(pairIdx, sepIdx)
if
(
"S"
== key) {
dfSymbolTicker.set(dec.buffer.substring(sepIdx +
1
, nextPairIdx))
}
else
if
(
"3"
== key) {
// use + to get JavaScript to convert string into integer
dec.marketId.fromUInt32(+dec.buffer.substring(sepIdx +
1
, nextPairIdx))
}
}
// Set these datafields from decoder.UInt64 objects
dfMarketId.fromUInt64(dec.marketId)
dfSequence.fromUInt64(dec.sequence)
// fromUInt32 works on normal JavaScript numbers.
dfSize.fromUInt32(dec.dfSize)
// Pass through to next decoder with no data which triggers a
// collection of the datafields. nextDecoder(length) and
// nextDecoder(offset, length) are also provided for passing a
// subsection of the buffer over.
dec.nextDecoder()
// advance past current message
dec.buffer.advance(dec.dfSize)
// Make both integers 0 again.
dec.marketId.setZero()
dec.dfSize =
0
return
true
}
// Called when decoder misses data
// dec: object representing decoder that missed data
// size: size of missed data.. has type decoder.UInt64()
function on_missed_data(dec, size) {
console.log(
"missed data of length: "
+ size.toDouble())
dec.foundMsgBegin =
false
dec.buffer.reset()
}