状态模式(State):状态驱动的行为变化,C++中的TCP连接状态机
状态模式,说白了就是让对象的行为随着内部状态的改变而改变。听起来像废话?嗯,你想想看,一个TCP连接,在ESTABLISHED状态下收到数据包,应该正常处理;但在CLOSED状态下收到同样的数据包,可能直接丢弃或者返回错误。同一个操作,不同状态下的行为完全不同——这就是状态模式要解决的问题。
我个人习惯把状态模式理解为「把if-else的怪物拆成一群小精灵」。每个状态自己管好自己的事,状态切换也由状态自己决定。这样代码就不再是一坨几千行的switch-case,而是清晰的状态类体系。
状态模式的核心思想
状态模式有三个关键角色:
- Context(上下文):持有当前状态的引用,对外暴露统一的接口
- State(抽象状态):定义所有状态下需要实现的接口
- ConcreteState(具体状态):实现特定状态下的行为,并负责状态切换
我在项目中遇到过这样一个场景:一个网络库需要处理TCP连接的完整生命周期。从CLOSED到LISTEN,再到SYN_SENT、ESTABLISHED,最后到CLOSE_WAIT、TIME_WAIT……如果不用状态模式,光是维护状态转换表就够你喝一壶的。
关键点:状态模式把「状态」本身变成了对象。每个状态对象知道自己能做什么、能切换到什么状态。Context只负责委托给当前状态对象。
TCP连接状态机:一个完整的例子
我们先画一张状态转换图,看看TCP连接的生命周期长什么样:
这张图展示了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连接、订单状态、工作流 | 排序算法、压缩算法、支付方式 |
说白了,状态模式里的状态转换是「自动挡」——状态自己知道下一步该去哪。策略模式是「手动挡」——调用方决定用哪个策略。
避坑指南:我踩过的三个坑
我曾经在一个实时通信项目中大量使用状态模式,踩过不少坑。这里分享三个最典型的:
- 状态爆炸:如果状态太多(比如超过20个),状态类会变得难以维护。这时候要考虑是否能用状态表+状态机引擎来替代。
- 状态转换的循环依赖:A状态要切换到B,B状态要切换到A,结果两个状态互相include。解决方案是把状态切换逻辑集中到Context里,或者用工厂方法。
- 多线程下的状态切换:如果多个线程同时调用Context的方法,状态可能在切换过程中被读取。我建议用
std::atomic<TCPState*>或者加锁保护状态切换。
总结一下:状态模式最适合那些状态数量有限、状态转换规则明确、每个状态下的行为差异较大的场景。TCP连接状态机是教科书级的例子,但实际项目中——比如游戏角色的状态(行走、攻击、受伤)、订单系统的状态(待支付、已支付、已发货)——同样适用。
嗯,最后说一句:状态模式不是银弹。如果你的状态只有两三个,或者状态转换逻辑极其简单,用if-else反而更清晰。别为了用模式而用模式,这是很多新手容易犯的毛病。