设计模式与协议解析:解释器模式解析协议、状态模式管理解析状态、工厂模式创建消息
协议解析,说白了就是让机器听懂网络里传过来的「黑话」。
我早年做物联网网关的时候,对接过十几种乱七八糟的私有协议。那时候年轻,写了一个巨大的 switch-case 来解析消息类型,结果代码膨胀到我自己都不想看。后来重构时用了三种设计模式组合,才把这块彻底理顺了。
今天咱们就聊聊这个组合:解释器模式负责解析协议语法,状态模式管理解析过程中的状态流转,工厂模式负责创建最终的消息对象。
1. 场景回顾:一个典型的协议解析流程
假设我们要解析一个简单的自定义协议,报文格式如下:
HEADER(4字节) + LENGTH(2字节) + TYPE(1字节) + PAYLOAD(N字节) + CHECKSUM(1字节)
解析过程大致分几步:
- 读取头部,验证魔数
- 读取长度字段,确定载荷大小
- 读取类型字段,决定消息种类
- 读取载荷,按类型做反序列化
- 校验校验和
你看,这每一步其实都对应一个「状态」。而且协议语法本身有嵌套结构,比如某些类型字段里还包含子协议。这时候解释器模式就派上用场了。
2. 解释器模式:把协议语法变成可执行的规则
解释器模式的核心思想是:为语言中的每一条语法规则定义一个类。解析时,把这些类组合成一棵抽象语法树,然后递归执行。
我习惯把协议字段的解析规则抽象成一个个「表达式」。比如:
// 抽象表达式
public interface Expression {
int interpret(Context ctx);
}
// 终端表达式:读取固定字节
public class FixedLengthExpression implements Expression {
private int length;
public FixedLengthExpression(int length) { this.length = length; }
@Override
public int interpret(Context ctx) {
byte[] data = ctx.readBytes(length);
// 处理 data...
return data.length;
}
}
// 非终端表达式:读取长度字段后再读取载荷
public class LengthPrefixedExpression implements Expression {
private Expression lengthExpr;
private Expression payloadExpr;
public LengthPrefixedExpression(Expression lengthExpr, Expression payloadExpr) {
this.lengthExpr = lengthExpr;
this.payloadExpr = payloadExpr;
}
@Override
public int interpret(Context ctx) {
int len = lengthExpr.interpret(ctx);
ctx.setExpectedLength(len);
return payloadExpr.interpret(ctx);
}
}
这样做的好处是:新增一种协议字段,只需要加一个 Expression 子类,不用改解析器的主流程。我在项目中遇到过一种奇葩协议,同一个字段在不同版本里长度不一样。用解释器模式,我只需要在构建语法树时传入不同的终端表达式就行,解析器本身纹丝不动。
3. 状态模式:管理解析过程中的状态流转
解析协议时,解析器内部的状态是不断变化的:
- 刚收到数据时,处于 HEADER 状态
- 头部验证通过后,进入 LENGTH 状态
- 长度读取完毕,进入 TYPE 状态
- 类型确定后,进入 PAYLOAD 状态
- 载荷读取完毕,进入 CHECKSUM 状态
用状态模式来管理,每个状态就是一个独立的对象:
public interface ParserState {
ParserState handle(Context ctx, byte[] data);
}
public class HeaderState implements ParserState {
@Override
public ParserState handle(Context ctx, byte[] data) {
if (validateHeader(data)) {
ctx.consumeBytes(4);
return new LengthState();
}
throw new ProtocolException("Invalid header");
}
}
public class LengthState implements ParserState {
@Override
public ParserState handle(Context ctx, byte[] data) {
int len = readLength(data);
ctx.setPayloadLength(len);
ctx.consumeBytes(2);
return new TypeState();
}
}
// 其他状态类似...
解析器主循环就变得极其简洁:
public class ProtocolParser {
private ParserState currentState = new HeaderState();
public void feed(byte[] data) {
while (data.hasRemaining()) {
currentState = currentState.handle(ctx, data);
}
}
}
嗯,这里要注意:状态对象本身是无状态的,所有临时数据都放在 Context 里。这样状态对象可以复用,避免频繁创建对象。我曾经踩过这个坑——每次状态切换都 new 一个新对象,结果 GC 压力巨大,后来改成单例状态才解决。
4. 工厂模式:根据类型创建消息对象
解析完载荷后,我们需要根据 TYPE 字段创建具体的消息对象。这就是工厂模式的用武之地。
我一般用简单工厂 + 注册表的方式:
public class MessageFactory {
private static Map<Byte, Class<? extends Message>> registry = new HashMap<>();
static {
registry.put((byte) 0x01, LoginMessage.class);
registry.put((byte) 0x02, HeartbeatMessage.class);
registry.put((byte) 0x03, DataMessage.class);
}
public static Message createMessage(byte type, byte[] payload) {
Class<? extends Message> clazz = registry.get(type);
if (clazz == null) {
throw new UnsupportedOperationException("Unknown type: " + type);
}
// 通过反射或构造器创建实例
return clazz.getConstructor(byte[].class).newInstance(payload);
}
}
这样做的好处是:新增一种消息类型,只需要注册一个类,解析器不用改。我在项目中遇到过一种情况,同一个类型在不同版本里对应不同的消息结构。解决方案也很简单:在注册时带上版本号作为 key 的一部分。
5. 三者如何协作?一张图说清楚
下面这张图展示了三种设计模式在协议解析中的协作关系:
流程很简单:
- 状态模式驱动解析器在不同阶段间流转
- 每个状态内部调用解释器模式构建的语法树来解析具体字段
- 解析完成后,工厂模式根据类型创建最终的消息对象
6. 完整代码骨架
最后给一个整合后的代码骨架,方便你理解三者如何粘合:
public class ProtocolEngine {
private ParserState state = new HeaderState();
private Context ctx = new Context();
public Message parse(byte[] rawData) {
ctx.setRawData(rawData);
// 状态驱动解析
while (!state.isFinal()) {
state = state.handle(ctx);
}
// 解释器解析载荷
Expression syntax = buildSyntaxTree(ctx.getType());
syntax.interpret(ctx);
// 工厂创建消息
return MessageFactory.createMessage(ctx.getType(), ctx.getPayload());
}
private Expression buildSyntaxTree(byte type) {
// 根据类型构建不同的语法树
// 例如:Login 消息 = FixedLength(4) + LengthPrefixed(2, ...)
return new LengthPrefixedExpression(
new FixedLengthExpression(2),
new FixedLengthExpression(ctx.getPayloadLength())
);
}
}
说实话,这个组合我用了好几年,从物联网到即时通讯,从私有协议到标准协议,基本都能套用。你想想看,如果你的解析器还在用大段的 if-else 和 switch-case,不妨试试这个思路。
公众号:蓝海资料掘金营,微信deep3321