状态模式(State):状态驱动的行为变化,C++中的TCP连接状态机

状态模式,说白了就是让对象的行为随着内部状态的改变而改变。听起来像废话?嗯,你想想看,一个TCP连接,在ESTABLISHED状态下收到数据包,应该正常处理;但在CLOSED状态下收到同样的数据包,可能直接丢弃或者返回错误。同一个操作,不同状态下的行为完全不同——这就是状态模式要解决的问题。

我个人习惯把状态模式理解为「把if-else的怪物拆成一群小精灵」。每个状态自己管好自己的事,状态切换也由状态自己决定。这样代码就不再是一坨几千行的switch-case,而是清晰的状态类体系。

状态模式的核心思想

状态模式有三个关键角色:

  • Context(上下文):持有当前状态的引用,对外暴露统一的接口
  • State(抽象状态):定义所有状态下需要实现的接口
  • ConcreteState(具体状态):实现特定状态下的行为,并负责状态切换

我在项目中遇到过这样一个场景:一个网络库需要处理TCP连接的完整生命周期。从CLOSEDLISTEN,再到SYN_SENTESTABLISHED,最后到CLOSE_WAITTIME_WAIT……如果不用状态模式,光是维护状态转换表就够你喝一壶的。

关键点:状态模式把「状态」本身变成了对象。每个状态对象知道自己能做什么、能切换到什么状态。Context只负责委托给当前状态对象。

TCP连接状态机:一个完整的例子

我们先画一张状态转换图,看看TCP连接的生命周期长什么样:

TCP连接状态机 CLOSED LISTEN SYN_SENT ESTABLISHED CLOSE_WAIT TIME_WAIT LAST_ACK passive open active open / SYN SYN+ACK SYN+ACK received close / FIN close / FIN timeout ACK received → CLOSED 回到 CLOSED

这张图展示了TCP连接的主要状态和转换路径。每个箭头代表一个事件(比如收到SYN、发送FIN),每个节点代表一个稳定状态。状态模式要做的就是:把每个节点变成一个类,把箭头变成类里的方法。

C++实现:从抽象到具体

我们先定义抽象状态接口。每个状态都需要处理三类事件:连接请求、数据收发、断开连接。

// 前向声明
class TCPConnection;

// 抽象状态
class TCPState {
public:
    virtual ~TCPState() = default;

    virtual void open(TCPConnection& conn) = 0;
    virtual void close(TCPConnection& conn) = 0;
    virtual void send(TCPConnection& conn, const std::string& data) = 0;
    virtual void receive(TCPConnection& conn, const std::string& data) = 0;

protected:
    // 状态切换的辅助方法
    void changeState(TCPConnection& conn, TCPState* state);
};

接下来是Context类——TCPConnection。它持有当前状态的指针,所有操作都委托给状态对象。

class TCPConnection {
public:
    TCPConnection() : m_state(TCPClosed::getInstance()) {}

    void open()  { m_state->open(*this); }
    void close() { m_state->close(*this); }
    void send(const std::string& data) { m_state->send(*this, data); }
    void receive(const std::string& data) { m_state->receive(*this, data); }

    void changeState(TCPState* state) {
        m_state = state;
        std::cout << "State changed to: " << state->name() << std::endl;
    }

private:
    TCPState* m_state;
};

小技巧:状态对象通常设计为单例。因为状态本身不保存实例变量,所有状态相关的数据都在Context里。这样可以避免反复创建销毁状态对象。

具体状态实现:以ESTABLISHED为例

我们挑两个典型状态看看。首先是ESTABLISHED状态——这是TCP连接最活跃的阶段。

class TCPEstablished : public TCPState {
public:
    static TCPState* getInstance() {
        static TCPEstablished instance;
        return &instance;
    }

    void open(TCPConnection& conn) override {
        std::cout << "Error: connection already established" << std::endl;
    }

    void close(TCPConnection& conn) override {
        std::cout << "Closing connection..." << std::endl;
        // 发送FIN包,进入CLOSE_WAIT
        changeState(conn, TCPCloseWait::getInstance());
    }

    void send(TCPConnection& conn, const std::string& data) override {
        std::cout << "Sending data: " << data << std::endl;
        // 实际发送数据...
    }

    void receive(TCPConnection& conn, const std::string& data) override {
        std::cout << "Received data: " << data << std::endl;
        // 处理接收到的数据...
    }

    std::string name() const override { return "ESTABLISHED"; }
};

再看CLOSED状态——这是连接的初始和最终状态。

class TCPClosed : public TCPState {
public:
    static TCPState* getInstance() {
        static TCPClosed instance;
        return &instance;
    }

    void open(TCPConnection& conn) override {
        std::cout << "Initiating connection..." << std::endl;
        // 发送SYN包,进入SYN_SENT
        changeState(conn, TCPSynSent::getInstance());
    }

    void close(TCPConnection& conn) override {
        std::cout << "Error: connection already closed" << std::endl;
    }

    void send(TCPConnection& conn, const std::string& data) override {
        std::cout << "Error: cannot send data in CLOSED state" << std::endl;
    }

    void receive(TCPConnection& conn, const std::string& data) override {
        std::cout << "Error: cannot receive data in CLOSED state" << std::endl;
    }

    std::string name() const override { return "CLOSED"; }
};

注意:状态模式容易犯的一个错误是——在状态对象里保存了Context的引用或数据。千万别这么做!状态对象应该是无状态的(stateless),所有可变数据都应该放在Context里。否则多线程环境下你会哭的。

状态模式 vs 策略模式:别搞混了

这两个模式结构很像,但意图完全不同。我经常被问到这个问题,这里统一说清楚:

维度 状态模式 策略模式
核心意图 对象行为随内部状态改变 算法可替换、可独立变化
状态/策略切换 状态自己决定切换(自动) 客户端决定切换(手动)
状态/策略数量 有限、已知、固定 可扩展、可新增
典型场景 TCP连接、订单状态、工作流 排序算法、压缩算法、支付方式

说白了,状态模式里的状态转换是「自动挡」——状态自己知道下一步该去哪。策略模式是「手动挡」——调用方决定用哪个策略。

避坑指南:我踩过的三个坑

我曾经在一个实时通信项目中大量使用状态模式,踩过不少坑。这里分享三个最典型的:

  1. 状态爆炸:如果状态太多(比如超过20个),状态类会变得难以维护。这时候要考虑是否能用状态表+状态机引擎来替代。
  2. 状态转换的循环依赖:A状态要切换到B,B状态要切换到A,结果两个状态互相include。解决方案是把状态切换逻辑集中到Context里,或者用工厂方法。
  3. 多线程下的状态切换:如果多个线程同时调用Context的方法,状态可能在切换过程中被读取。我建议用std::atomic<TCPState*>或者加锁保护状态切换。

总结一下:状态模式最适合那些状态数量有限、状态转换规则明确、每个状态下的行为差异较大的场景。TCP连接状态机是教科书级的例子,但实际项目中——比如游戏角色的状态(行走、攻击、受伤)、订单系统的状态(待支付、已支付、已发货)——同样适用。

嗯,最后说一句:状态模式不是银弹。如果你的状态只有两三个,或者状态转换逻辑极其简单,用if-else反而更清晰。别为了用模式而用模式,这是很多新手容易犯的毛病。


专注资料整理