设计模式与协议解析:解释器模式解析协议、状态模式管理解析状态、工厂模式创建消息

协议解析,说白了就是让机器听懂网络里传过来的「黑话」。

我早年做物联网网关的时候,对接过十几种乱七八糟的私有协议。那时候年轻,写了一个巨大的 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 压力巨大,后来改成单例状态才解决。

避坑指南:状态模式容易把状态转移逻辑分散到各个状态类里,导致「全局视角」丢失。我建议在 Context 里维护一份状态转移图(比如用 Map 或枚举),方便调试时查看完整路径。

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. 三者如何协作?一张图说清楚

下面这张图展示了三种设计模式在协议解析中的协作关系:

三种设计模式协作解析协议 原始字节流 状态模式管理解析状态 Header → Length → Type → Payload → Checksum 解释器模式解析协议语法 FixedLengthExpression | LengthPrefixedExpression | TypeExpression 组合成抽象语法树,递归执行 工厂模式创建消息对象 根据 TYPE 字段,注册表查找并实例化 业务消息对象 ① 状态驱动 ② 语法解析 ③ 对象创建

流程很简单:

  1. 状态模式驱动解析器在不同阶段间流转
  2. 每个状态内部调用解释器模式构建的语法树来解析具体字段
  3. 解析完成后,工厂模式根据类型创建最终的消息对象

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