Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Separating protocol parser and handler in Java

Tags:

java

parsing

I am working with a simple, binary protocol. Each packet consists of 10 bytes. The first byte specifies the packet type. There are many (~50) packet types used.

I want to write a general parser for this protocol which is independent of the handling of packets. So the parser should detect the packet type and put the data into an instance of the appropriate packet class, which holds the protocol data. E.g., considering the classes below: When parser detects packet type 1 --> new Type1() and read raw bytes and set temperature and humidity. Similarly for packet type 2 and all the other packet types.

class Packet {
  byte[] raw;
}

class Type1 extends Packet {
  int temperature;
  int humidity;
}

class Type2 extends Packet {
  DateTime sunrise;
  DateTime sunset;
}

Since there are so many packet types but each application only uses very few, it should be possible to register for certain types before parsing starts. All other packets types are ignored.

I am planning to have a PacketParser for each packet type. Probably, I need a handler class for each type as well. E.g.:

abstract class Type1Parser {
  abstract void handle(Type1 packet);
}

class Type1Parser extends PacketParser {
  //how to use/set handler? how to pass packet to handler?
  static public Type1Handler type1Handler = null;

  @override
  void parse(Packet input) {
    if(type1Handler == null)
      return;
    Type1 packet = new Type1(input);
    packet.temperature = byteToInt(input.raw, 0, 3);
    packet.humidity = byteToInt(input.raw, 4, 7);

    type1Handler.handle(packet);
  }
}

How to connect parser and handler? Above a naive approach: The program needs to implement Type1Handler and set the static variable Type1Parser.type1Handler.

Then the main parser can look like this:

class MainParser {
   Type1Parser type1 = new Type1Parser();
   Type2Parser type2 = new Type2Parser();
   ...
   void parse(byte[] packet) {
     switch(packet[0]) {
       case 1: type1.parse(packet); break;
       case 2: type2.parse(packet); break;
       ...
     }
   }
}

However, this seems to be 1) a lot of very similar lines of code 2) a lot of overhead, since all packet parser are instantiated and for each packet parse() is called, even if no handler is registered.

Any ideas how to improve this code?

Note: The parsing should be transparent to the program. Parsing code should stay inside the "parsing library". So ideally, the program only "knows" classes TypeXHandler and TypeX.

like image 723
Jack Miller Avatar asked Jul 10 '14 20:07

Jack Miller


1 Answers

There is no perfect answer to this design question, and I don't wish to pretend that mine is, but hopefully my instinctual approach to this problem teaches you things you didn't already know! The main missing component from your code that I see is Generics:

public interface Parser<T extends Packet> {
  T parse(Packet packet);
}

public interface Handler<T extends Packet> {
  void handle(T packet);
}

This way, you can use lazy static initialization to manage which packet types you are aware of. I won't flesh out the code entirely here, but to give you an idea:

public class TypeRegistry {
  private static Map<Integer, TypeHandlerBundle<?>> typeHandlerBundles;

  static <T> register(int typeNum, Class<T> clazz, Parser<T> parser, Handler<T> handler) {
    // Make bundle, add to map
  }

  ... void parse(Packet packet) {
    if (typeHandlerBundles.containsKey((int) packet[0])) {
      TypeHandlerBundle<?> bundle = typeHandlerBundles.get((int) packet[0]);
      bundle.parseAndHandle(packet);
    }
  } 
}

public class TypeHandlerBundle<T extends Packet> {
  ...
  private final Parser<T> parser;
  private final Handler<T> handler;

  ... void parseAndHandle(Packet packet) {
    T parsedPacket = parser.parse(packet);
    handler.handle(parsedPacket);
  }
}

...

public class Type1Processor {
  static {
    TypeRegistry.register(1, Type1.class, TYPE1_PARSER, TYPE1_HANDLER);
  }

  // Definition of constants, implementation, etc.
  // ...
}

===

Things I omitted: Qualifiers, lower level implementation, Error-checking, Synchronization, main method, etc. Depending on your set-up, static initialization might not be the right way to call TypeRegistry.register, so you could instead consider a properties file that lists the classes (ugh, but has its merits), or a hard-coded sequence of calls in your main method.

Since Parser and Handler are functional interfaces here, don't forget that you can implement them with lambdas! You can save tons of lines of code that way.

like image 196
torquestomp Avatar answered Sep 21 '22 09:09

torquestomp