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));
    }
posted @ 2026-08-28 14:12  小鱼,  阅读(164)  评论(0)    收藏  举报