Thursday, July 26, 2012

Using Netty - Lessons learned

We were building a ISO-8583 equivalent card transaction simulator using the powerful Netty framework.The simplicity of the netty design is a great wow factor. Also most of the complexities of NIO are abstracted by the framework by concepts such as Channel, ChannelBuffer, ChannelEvent, Decoders/Encoders, etc.

While we were using the LengthFieldBasedFrameDecoder class, we faced an intriguing problem. The socket client was sending the length of the record in the first 2 bytes. For e.g. "11abcdefghijk"
But strangely, the length derived by the  BigEndianHeapChannelBuffer was something different.
To get to the bottom of this, we enabled debugging and saw the raw buffer byte array that reached the server.

The buffer array for "11abcdefghijk" was [49, 49, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107]
The first 2 bytes contained the unicode charset number of the character '1'. Hence the lenght was also encoded from a character into bytes using the default charset, which resulted in a very large number.. Obviously the LengthFieldBasedFrameDecoder failed to decode the message.

To get around this problem, we had to send the first 2 bytes without any characted encoding. We achieved this using unicode escape sequences for the lenght field - i.e. \u0000 and \u000b; essentially 0 and 11.
The buffer array for "\u0000\u000babcdefghijk" was [0, 11, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107]

Now the  netty decoder would work. But this approach has a serious flaw...for 2 chars - \u000A and \u000D. These chars correspond to \n (linefeed) and \r (carriage return) and hence would not compile itself as the Unicode escapes are pre-processed before the compiler is run.

Hence the best approach is to write the lenght field to the socket stream without using any charset encoding (write raw bytes) and then write the record as a string.

An alternate workaround is to write your own decoder, which too is very simple. 

Given below is a sample decoder that extracts the first 2 chars of a string for the length and returns the buffer upstream.

-----------------------------------------------------------------
import java.util.logging.Logger;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;
import org.jboss.netty.util.CharsetUtil;

public class NarenLengthFieldDecoder extends FrameDecoder {

    private static final Logger logger = Logger.getLogger(NarenLengthFieldDecoder.class.getName());

    public int lengthFieldLength = 2; // some default value

    public NarenLengthFieldDecoder(int length) {
        this.lengthFieldLength = length;
    }

    @Override
    protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception {
        // wait until the length prefix is available.
        if (buffer.readableBytes() < lengthFieldLength) {
            // return null to inform frame decoder that frame is not yet
            // complete and to continue reading data
            return null;
        }

        // read length field...create a byteArray for the length field and copy
        // the first bytes into it..
        byte[] array = new byte[lengthFieldLength];
        buffer.readBytes(array); // Imp: readBytes also forwards the readerIndex

        // Mark the current buffer position before reading the length field
        // because the whole frame might not be in the buffer yet.
        // We will reset the buffer position to the marked position if
        // there's not enough bytes in the buffer.
        buffer.markReaderIndex();

        int dataLength = getLength(array);// length of the record

        // wait until the whole data is available.
        if (buffer.readableBytes() < dataLength) {
            // The whole bytes were not received yet - return null.
            // This method will be invoked again when more packets are received
            // and appended to the buffer.

            // Reset to the marked position to read the length field again next
            // time.
            buffer.resetReaderIndex();
            return null;
        }

        // forward remaining buffer to higher up handlers
         // There's enough bytes in the buffer. Read it.
         ChannelBuffer frame = buffer.readBytes(dataLength);
         // Successfully decoded a frame.  Return the decoded frame.
         return frame;
    }

    /**
     * Returns the first 2 or 3 sequence of bytes...that specify the length of
     * the record
     * 
     * @param array
     *            The byte array containing the length
     * @return An integer representing the length of the record
     */
    public int getLength(byte[] array) {
        String temp = new String(array, CharsetUtil.ISO_8859_1);

        int length = 0;
        try {
            length = Integer.parseInt(temp);
        } catch (Exception ex) {
            logger.info("Could not parse the length field of the record >>>" + temp);
        }
        return length;
    }
}