Netty对接医疗监护仪HL7协议,使用MLLP解码器解析数据

1.介绍

医疗设备通讯中通常使用HL7协议进行通讯,底层网络层依然是TCP/IP协议,

由于TCP是字节流协议,它和HL7都并没有定义一段报文的开始和结束,所以引入了MLLP协议

MLLP即(Minimal Lower Layer Protocol),一种基于TCP/IP的通信协议,非常轻量级,

一共3个字符

(1). 起始字符(SB,Start Block):标识消息开始。

(2). 结束字符(EB,End Block):标识消息结束。

(3). 回车符(CR):用于分隔消息段或标识消息结束

一份标准的基于HL7 + MLLP协议的报文结构如下

复制代码
<VT>MSH|^~\\&|||||20061019172719||ORM^O01||P|2.3<CR>
PID|||20301||Durden^Tyler^^^Mr.||19700312|M|||88 Punchward Dr.^^Los Angeles^CA^11221^USA|||||||<CR>
PV1||O|OP^^||||4652^Paulson^Robert|||OP|||||||||9|||||||||||||||||||||||||20061019172717|20061019172718<CR>
ORC|NW|20061019172719<CR>
OBR|1|20061019172719||76770^Ultrasound: retroperitoneal^C4|||12349876<CR>
<FS><CR>

2.代码

使用Netty进行网络编程时,通常需要我们手动编写编码器,解码器来封装/解析数据

以下编码器,解码器都是经过充分生产测试,可直接复制使用,记得把包名改成自己的。

2.1 编码器

编码器非常简单,只要在报文开始和结束添加MLLP字符即可

复制代码
package com.xxx.xxx.xxx.handler;

import com.xxx.xxx.xxx.constants.MLLP;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

/**
 * MLLP 帧编码器
 *
 * @author peishan yu
 * @date 2026/06/17
 */
public class MllpFrameEncoder extends MessageToByteEncoder<byte[]> {

    @Override
    protected void encode(ChannelHandlerContext ctx, byte[] msg, ByteBuf out) {
        out.writeByte(MLLP.SB);

        out.writeBytes(msg);

        out.writeByte(MLLP.EB);
        out.writeByte(MLLP.CR);
    }
}

2.2 解码器

解码器比较复杂,需要从TCP字节流中读取数据,正确区分中报文边界,解析出合法报文

这里的maxFrameLength是关键参数,它限制了一帧之中最多传输多少字节的数据

当帧长度过长,或一直缓冲数据超过此限制时会抛出异常,丢弃异常的数据,防止出现内存泄露

通常建议设置为65536即64kb,基本能适应大部分的情况了,一条报文通常几kb左右

根据需求调整即可,必要时也可增加

复制代码
package com.xxx.xxx.xxx.handler;

import com.xxx.xxx.core.constants.MLLP;
import com.xxx.xxx.core.exception.FrameTooLongException;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

/**
 * MLLP 帧解码器 - 基于 ByteToMessageDecoder 的稳定实现
 *
 * @author peishan yu
 * @date 2026/07/27
 */
@Slf4j
public class MllpFrameDecoder extends ByteToMessageDecoder {

    private final int maxFrameLength;

    public MllpFrameDecoder(int maxFrameLength) {
        if (maxFrameLength <= 0) {
            throw new IllegalArgumentException("最大帧长度不可 <= 0, 推荐65536");
        }

        this.maxFrameLength = maxFrameLength;
    }

    private enum State {
        SKIP_UNTIL_SB,
        READ_FRAME
    }

    private State state = State.SKIP_UNTIL_SB;

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        switch (state) {
            case SKIP_UNTIL_SB:
                skipUntilStartBlock(in);
                break;
            case READ_FRAME:
                readFrameContent(in, out);
                break;
        }
    }

    private void skipUntilStartBlock(ByteBuf in) {
        while (in.isReadable()) {
            byte b = in.readByte();
            if (b == MLLP.SB) {
                state = State.READ_FRAME;
                return;
            }
        }
    }

    /**
     * 在 READ_FRAME 状态下尝试提取一个完整帧。
     * 如果数据不完整或不合法,会相应处理并可能切换回 SKIP_UNTIL_SB。
     */
    private void readFrameContent(ByteBuf in, List<Object> out) {
        int startIndex = in.readerIndex();
        int readable = in.readableBytes();

        if (readable < 2) {
            return;
        }

        int ebIndex = in.indexOf(startIndex, startIndex + readable, MLLP.EB);
        if (ebIndex == -1) {
            if (readable > maxFrameLength + 2) {
                in.skipBytes(readable);
                state = State.SKIP_UNTIL_SB;
                throw new FrameTooLongException("超过最大帧长度且未找到 EB");
            }
            return;
        }

        int crIndex = ebIndex + 1;
        if (crIndex >= startIndex + readable) {
            return;
        }
        if (in.getByte(crIndex) != MLLP.CR) {
            in.readerIndex(crIndex);
            int newReadable = in.readableBytes();
            if (newReadable > maxFrameLength + 2) {
                in.skipBytes(newReadable);
                state = State.SKIP_UNTIL_SB;
                throw new FrameTooLongException("非法结束符后的数据超过最大长度");
            }
            return;
        }

        // 4. 成功匹配 EB+CR,计算帧内容长度并校验
        int frameLength = ebIndex - startIndex;
        if (frameLength > maxFrameLength) {
            in.skipBytes(frameLength + 2);
            state = State.SKIP_UNTIL_SB;
            throw new FrameTooLongException("超过最大帧长度限制: " + frameLength);
        }

        byte[] frame = new byte[frameLength];
        in.readBytes(frame);
        in.skipBytes(2);

        out.add(frame);
        state = State.SKIP_UNTIL_SB;
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        if (cause instanceof FrameTooLongException) {
            log.error("MLLP 帧长度超限,关闭连接: {}", cause.getMessage());
            ctx.close();
        } else {
            log.error("MLLP 解码器异常", cause);
            super.exceptionCaught(ctx, cause);
        }
    }
}

2.3 MLLP协议常量字符类

复制代码
/**
 * 功能描述
 *
 * @author peishan yu
 * @date 2026/6/18 16:52
 */
public interface MLLP {

    /**
     * MLLP 帧起始符 (Start Block)
     */
    byte SB = 0x0B;

    /**
     * MLLP 帧结束符 (End Block)
     */
    byte EB = 0x1C;

    /**
     * 回车符 (Carriage Return)
     */
    byte CR = 0x0D;
}

3.使用示例

以下为一个简单的基于Netty的TCP服务端实现,支持多端口

重点:handler的顺序千万别搞乱了,消息入站时Netty依次从上至下调用,出站时反过来

如果handler的添加顺序不正确,则无法进行正常的通讯

业务处理handler要放在最后,解析到完整的MLLP帧处理自己的业务逻辑即可

复制代码
    public void start() {
        int[] serverPorts = nettyConfig.getServerPorts();
        if (ArrayUtils.isEmpty(serverPorts)) {
            log.warn("TCP Server端口未配置, 服务端未监听!");
            return;
        }

        ServerBootstrap bootstrap = new ServerBootstrap()
                .group(eventLoopFactory.getBossGroup(), eventLoopFactory.getWorkerGroup())
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 128)
                .option(ChannelOption.SO_REUSEADDR, true)
                .childOption(ChannelOption.SO_KEEPALIVE, nettyConfig.isKeepAlive())
                .childOption(ChannelOption.TCP_NODELAY, nettyConfig.isTcpNoDelay())
                .childOption(ChannelOption.SO_RCVBUF, nettyConfig.getReceiveBufferSize())
                .childOption(ChannelOption.SO_SNDBUF, nettyConfig.getSendBufferSize())
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) {
                        ChannelPipeline pipeline = ch.pipeline();

                        pipeline.addLast(new ReadTimeoutHandler(nettyConfig.getReadTimeout(), TimeUnit.MILLISECONDS));

                        // MLLP 协议处理
                        pipeline.addLast(new MllpFrameDecoder(nettyConfig.getMaxFrameLength()));
                        pipeline.addLast(new MllpFrameEncoder());

                        // 服务端会话管理器
                        pipeline.addLast(new ServerConnectionHandler(connectionManager));

                        // HL7 数据处理器
                        pipeline.addLast(new Hl7DispatchHandler(hl7MessageDispatcher));
                    }
                });

        for (int port : serverPorts) {
            try {
                ChannelFuture future = bootstrap.bind("0.0.0.0", port).sync();
                serverChannelList.add(future.channel());
            } catch (Exception e) {
                log.error("绑定核心端口 {} 失败,终止服务启动: {}", port, e.getMessage());

                stop();

                throw new RuntimeException("核心端口 " + port + " 绑定失败,服务启动终止", e);
            }
        }

        log.info("Netty服务端启动成功, 监听端口: {}", Arrays.toString(serverPorts));
    }
相关推荐
GitLqr15 天前
Java 26 终于原生支持 HTTP/3 了:告别 Netty,直接用 QUIC
java·netty·http3
唐青枫1 个月前
Java Reactor Netty 实战详解:从响应式 HTTP 到 TCP 长连接
java·netty
皮皮林5511 个月前
Netty 性能好的原因是什么?
netty
white_ant1 个月前
5-Netty WebSocket 与 HTTP
java·网络编程·netty
ywl4708120872 个月前
【Spring AI 入门07】如何具体实现低延迟大模型推理网关
大模型·netty
ywl4708120872 个月前
第二章Netty,半包读取问题
netty·nio
ps酷教程2 个月前
WebSocketFrameEncoder&WebSocketFrameDecoder源码浅析
websocket·netty
ywl4708120872 个月前
第一章Netty,如何实现I/O多路复用的功能
netty·nio·selector
ywl4708120872 个月前
第一章Netty,NIO零拷贝详解
netty·nio·selector