Golang MLLP Library
Apr 26, 2016
4 minutes read

While you're reading this, keep in mind that I'm available for hire! If you've got a JavaScript project getting out of hand, or a Golang program that's more "stop" than "go," feel free to get in touch with me. I might be able to help you. You can find my resume here.

MLLP stands for “Minimum Lower Layer Protocol”. It’s from a time before IP, TCP, UDP, and even UFC were big things, and it kind of shows. It still sees widespread use in medical institutions however, being the de facto default transport for HL7v2 communications. A little while ago I wrote a library in Go for consuming and producing its message streams. You can find it at fknsrs.biz/p/mllp (godoc). I’ve been using this library in production for about six months now, and there haven’t been any significant changes to it in that time.

First off, MLLP is a framing protocol. What I mean by that is that it specifies a mechanism for message-oriented communication over a stream- oriented transport. This is useful if you have a message format with no clear start/end markers, such as HL7v2. With something like JSON, you can just pump messages back-to-back down the line and begin parsing the next one when the current one finishes. With some protocols, however, it’s not so easy to tell when the “end” is. At least not without a ton of context and some questionable heuristics (I’m looking at you, MPEG2TS).

The concept underlying MLLP is pretty simple. Put a special byte at the start of each message (0x0b, vertical tab), and put a special value at the end of each message (0x1c, 0x0d, file separator, carriage return). Everything in between is payload data. What happens if you put 0x1c, 0x0d in the middle of the payload? Bad things. I did mention that it comes from a simpler time, where software people weren’t yet scarred by a half-century of error-laden, exception-spewing systems. Oh, also - this is just what the spec says. If you want to work with real-world systems, you’ll want to shove an 0x0d in before the trailing value. Why? Probably because in 1991 someone did it by accident and now everyone does it. Hoorayyyyy computers.

Producing messages is really easy. Just shove your content inside the header and trailer values, and you’re good to go. No problems there. This is the whole function from my library:

func (w Writer) WriteMessage(b []byte) error {
  if _, err := w.w.Write([]byte{0x0b}); err != nil {
    return stackerr.Wrap(err)
  }

  for len(b) > 0 {
    n, err := w.w.Write(b)
    if err != nil {
      return stackerr.Wrap(err)
    }

    b = b[n:]
  }

  if _, err := w.w.Write([]byte{0x0d, 0x1c, 0x0d}); err != nil {
    return stackerr.Wrap(err)
  }

  return nil
}

Easy stuff.

Reading a message is a tiny bit more complicated, but still not so bad. Basically, the process goes like this:

  1. peek one byte into the stream, make sure it’s 0x0b
  2. read everything you find until you encounter 0x1c
  3. make sure the last byte of the content (the byte before 0x1c) is 0x0d
  4. read one more byte from the stream, make sure it also is 0x0d

You just do that in a loop, and bail out if any of the checks fail. My code for reading a message looks like this:

func (r Reader) ReadMessage() ([]byte, error) {
  c, err := r.b.ReadByte()
  if err != nil {
    return nil, stackerr.Wrap(err)
  }

  if c != byte(0x0b) {
    return nil, ErrInvalidHeader(stackerr.Newf("invalid header found; expected 0x0b but got %02x", c))
  }

  d, err := r.b.ReadBytes(byte(0x1c))
  if err != nil {
    return nil, stackerr.Wrap(err)
  }

  if len(d) < 2 {
    return nil, ErrInvalidContent(stackerr.Newf("content including boundary should be at least two bytes long; instead was %d", len(d)))
  }

  if d[len(d)-2] != 0x0d {
    return nil, ErrInvalidBoundary(stackerr.Newf("content should end with 0x0d; instead was %02x", d[len(d)-2]))
  }

  t, err := r.b.ReadByte()
  if err != nil {
    return nil, stackerr.Wrap(err)
  }
  if t != byte(0x0d) {
    return nil, ErrInvalidTrailer(stackerr.Newf("invalid trailer found; expected 0x0d but got %02x", t))
  }

  return d[0 : len(d)-2], nil
}

If you find yourself working with hospital systems, you’ll probably have to get familiar with MLLP. I hope this short run-down of how it works and how it’s used can help.

Join me next time, when I talk about the HL7 protocol itself.


Back to posts