Creating ISO8583 Server using Netty Framework

Hi, after posting about Creating ISO8583 Server using Spring Integration, in this post we are going to learn how to Create ISO8583 Server using Netty Framework. Netty is an NIO client server framework which enables quick and easy development of network applications, it is well documented and compared to spring integration before, netty only has a few dependencies.

We are going to create application called middleware-netty with only bellow dependencies:

  1. J8583: used for parse and format ISO8583 Message
  2. Netty-handler: enough to import required dependency for TCP based application
  3. Logback-classic: logging library
  4. Slf4j-api: logging library
  5. Commons-lang3: String utility library
	
    <dependencies>

	<dependency>
		<groupId>net.sf.j8583</groupId>
		<artifactId>j8583</artifactId>
		<version>1.17.0</version>
	</dependency>

	<dependency>
		<groupId>io.netty</groupId>
		<artifactId>netty-handler</artifactId>
		<version>4.2.0.RC4</version>
	</dependency>

	<dependency>
		<groupId>ch.qos.logback</groupId>
		<artifactId>logback-classic</artifactId>
		<version>1.5.18</version>
		<scope>compile</scope>
	</dependency>

	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-api</artifactId>
		<version>2.0.17</version>
	</dependency>

	<dependency>
		<groupId>org.apache.commons</groupId>
		<artifactId>commons-lang3</artifactId>
		<version>3.17.0</version>
	</dependency>

</dependencies>

    

The application design created will be as much as possible similar to Spring Integration version, the only difference is the name. In Spring integration we have Server Channel Connection Factory while in netty Server Channel. There are other component like:

  1. Serializer and Deserializer → Frame Decoder and Encoder
  2. Message Channel → Channel Pipeline
  3. Transformer → Message Decoder and Encoder
  4. ServiceActivator → Message Handler

Okay, lets create main method for starting our Server:

  • Line 2: Configure lockback.xml configuration
  • Line 4 & 5: Initiate EventLoopGroup for parent and children
  • Line 7: Creating ServerBootstrap instance
  • Line 8: Setting group for parent and children, parent group (bossGroup) used for listening incoming connection on specific port, meanwhile the child group (workerGroup) used to handle incoming requests.
  • Line 9: Setting the channel class
  • Line 10: Optional Logging Handler
  • Line 11: Define the childHandler and init channel pipeline
  • Line 16 - 20: Define the pipeline used in this project. The order of the pipeline is Frame Decoder → Message Decoder → Message Handler → Message Encoder → Frame Encoder
  • Line 25 & 26: Starting the server and listening on specific port

Since in my previous tutorial, we are using asciiSerializer which is using 4 bytes as message length indicator, so we are going to use the same specification. AsciiFrameDecoder will get the message from the stream while the Iso8583Decoder will decode from ByteBuf into CustomIsoMessage.

Netty already has class LenghFieldBasedFrameDecoder, but it's only support length indicator in hex, since we are going to use ascii length indicator we need to override getUnadjustedFrameLength method as below.

	
public class AsciiFrameDecoder extends LengthFieldBasedFrameDecoder {

	public AsciiFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment,
			int initialBytesToStrip) {
		super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip, true);
	}

	@Override
	protected long getUnadjustedFrameLength(ByteBuf buf, int offset, int length, ByteOrder order) {
		buf = buf.order(order);
		byte[] lengthBytes = new byte[length];
		buf.getBytes(offset, lengthBytes);
		String s = new String(lengthBytes, CharsetUtil.US_ASCII);
		return Long.parseLong(s);
	}
}

    

For Message Decoder we are extending ByteToMessageDecoder and all we need to do is override the decode method. This class will convert ByteBuf into CustomIsoMessage class just like Spring Integration Transformers.

	
public class Iso8583Decoder extends ByteToMessageDecoder {
	
	private final MessageFactoryUtil messageFactory = MessageFactoryUtil.getInstance();
	private static final Logger log = LoggerFactory.getLogger(Iso8583Decoder.class);
	
	@Override
	protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> out) throws Exception {
		log.debug("Iso8583Decoder START");
		if (!byteBuf.isReadable()) {
			return;
		}
		byte[] bytes = new byte[byteBuf.readableBytes()];
		byteBuf.readBytes(bytes);

		log.debug("Parsing Incoming Message {}", new String(bytes));
		final CustomIsoMessage isoMessage = messageFactory.parseMessage(bytes, 0);
		
		if (isoMessage != null) {
			log.info(isoMessage.dumpField(true, ctx.channel().id().asLongText(), 
					ctx.channel().remoteAddress().toString(), 
					System.currentTimeMillis()));
			
			out.add(isoMessage);
			
		} 
		else {
			throw new RuntimeException("Can't parse ISO8583 message");
			
		}
		log.debug("Iso8583Decoder DONE");
	}

}
    

Message Handler is like a Service Activator in Spring Integration, our code starts here. In this project we are going to use Message Handler as routing for each message handler. To determine the handler, we are going to use Message Type Indicator, Processing Code and Network Management Code.

	
public class InboundMessageHandler extends SimpleChannelInboundHandler<CustomIsoMessage> {
	
	private static final Logger log = LoggerFactory.getLogger(InboundMessageHandler.class);
	private static final MessageFactoryUtil messageFactory = MessageFactoryUtil.getInstance();
	
	@Override
	protected void channelRead0(ChannelHandlerContext ctx, CustomIsoMessage requestMsg) throws Exception {
		
		TransactionContext context = new TransactionContext();
		context.setConnectionId(ctx.channel().id().asLongText());
		context.setRequestTimestamp(System.currentTimeMillis());
		context.setReqMsg(requestMsg);
		context.setRespMsg(messageFactory.createResponse(requestMsg));
		context.setClientIpAddress(ctx.channel().remoteAddress().toString());

		IMessageHandler messageHandler = getMessageHandler(requestMsg); 
		
		if (messageHandler != null) {
			try {
				messageHandler.handle(context);
				context.setResponseTimestamp(System.currentTimeMillis());
				log.info(context.getRespMsg().dumpField(false, context.getConnectionId(), 
						context.getClientIpAddress(), context.getResponseTimestamp()));
				
			} catch (Exception e) {
				log.error("Message Handler Error", e);
				context.setResponseCode(InternalRC.GENERAL_ERROR);
			}			
		} 
		else {
			log.warn("Unknown Message Handler");
			context.setResponseCode(InternalRC.INVALID_MESSAGE);
		}
		
		// Sending Response
		ctx.writeAndFlush(context.getRespMsg());
	}

	private IMessageHandler getMessageHandler(CustomIsoMessage requestMsg) {
		
		IMessageHandler messageHandler = null;
		
		int type = requestMsg.getType();
		String processingCode = requestMsg.getString(3);
		String networkManagementCode = requestMsg.getString(70);
		log.info("Getting Message Handler for MTI [{}] DE#3 [{}] DE#70 [{}]", 
				type, processingCode, networkManagementCode);
		
		if (MTI.NETWORK_MANAGEMENT.val == type) {
			if (NetworkManagementCode.ECHO.val.equals(networkManagementCode)) {
				messageHandler = EchoMessageHandler.getInstance();
			}
			else if (NetworkManagementCode.SIGN_ON.val.equals(networkManagementCode)) {
				// TODO Sign On
			}
			else if (NetworkManagementCode.SIGN_OFF.val.equals(networkManagementCode)) {
				// TODO Sign Off
			}			
		}
		else if (MTI.TRANSACTIONAL.val == type) {
			if (ProcessingCode.INQUIRY.val.equals(processingCode)) {
				// TODO Inquiry
			}
			if (ProcessingCode.PAYMENT.val.equals(processingCode)) {
				// TODO Payment
			}
		}
		else if (MTI.ADVICE.val == type) {
			// TODO Payment Advice
		}
		else if (MTI.ADVICE_REPEAT.val == type) {
			// TODO Payment Advice Repeat
		}
		else if (MTI.REVERSE.val == type) {
			// TODO Payment Reversal
		}
		return messageHandler;
	}

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		log.error("Inbound Message Handler Error", cause);
                       ctx.close();
	}
}

    

When we are done processing the message, calling ctx.writeAndFlush(context.getRespMsg()); will put the message on the wire to be processed by the Message Encoder. Iso8583Encoder will transform CustomIsoMessage into byte to be sent to the client.

	
public class Iso8583Encoder extends MessageToByteEncoder<IsoMessage> {

	@Override
	protected void encode(ChannelHandlerContext ctx, IsoMessage isoMessage, ByteBuf out) throws Exception {
		byte[] byteBuffer = isoMessage.writeData();
        	            out.writeBytes(byteBuffer);
	}

}
    

And the last part is the Frame Encoder, this encoder will append message length indicator into the start of the message.

	
public class AsciiFrameEncoder extends LengthFieldPrepender {

	private final ByteOrder byteOrder = ByteOrder.BIG_ENDIAN;
	private final int lengthFieldLength;
	private final boolean lengthIncludesLengthFieldLength;
    
	public AsciiFrameEncoder(int lengthFieldLength, 
			boolean lengthIncludesLengthFieldLength) {
		super(lengthFieldLength, lengthIncludesLengthFieldLength);
		this.lengthFieldLength = lengthFieldLength;
		this.lengthIncludesLengthFieldLength = lengthIncludesLengthFieldLength;
	}

	@Override
	protected void encode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
		int length = msg.readableBytes();
		if (lengthIncludesLengthFieldLength) {
			length += lengthFieldLength;
		}

		checkPositiveOrZero(length, "length");
		String lenStr = StringUtils.leftPad(String.valueOf(length), lengthFieldLength, '0');
		out.add(ctx.alloc().buffer(lengthFieldLength).order(byteOrder).writeBytes(lenStr.getBytes()));
		out.add(msg.retain());
	}
}

    

Ok, that's all about Creating ISO8583 Server using Netty Framework. You can find the complete source code on my Github repository. Hope this Help!

Comments