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));
}