tcp三次握手仅含syn_sent、syn_received、established三个核心状态,应使用具名结构体+指针接收者方法实现,避免过早抽象state接口;状态转移须强序、排他、不可逆,配合uint8枚举与switch控制,每个状态只响应特定报文,并内嵌timer管理超时重传。

Go 里用结构体+方法模拟 TCP 三次握手状态机,别用 interface{} 做状态
直接说结论:TCP 三次握手只有 SYN_SENT、SYN_RECEIVED、ESTABLISHED 三个核心状态,用 Go 的具名结构体 + 指针接收者方法实现最稳,比抽象出 State 接口更直观、更难写错。
常见错误是过早抽象——定义一个 State 接口,再搞 SynSentState、SynReceivedState 等多个类型,结果每个状态都要重复实现无意义的 HandleXXX() 方法,还容易漏掉状态转移校验。
- 真实握手流程中,状态跳转有强顺序:
SYN_SENT → SYN_RECEIVED → ESTABLISHED,反向或跨步(如SYN_SENT → ESTABLISHED)必须拒绝 - 每个状态只响应特定报文:
SYN_SENT只处理SYN+ACK或超时;SYN_RECEIVED只处理ACK - 用结构体字段存当前状态(如
state uint8),比用接口变量 + 类型断言更省内存、更易调试
如何用 uint8 枚举 + switch 实现状态转移逻辑
别写一堆 if-else 嵌套判断当前状态和输入事件,用 switch 配合预定义常量,代码可读性和维护性高得多。
典型错误是把事件处理逻辑(比如收到 ACK)和状态更新混在一起,导致同一段代码既改状态又发包还清缓存,难以测试和复用。
立即学习“go语言免费学习笔记(深入)”;
- 定义状态常量:
const ( SYN_SENT uint8 = iota; SYN_RECEIVED; ESTABLISHED ) - 状态转移函数签名建议为:
func (c *Conn) handleSYNACK() error,而不是func (c *Conn) transition(event string) - 在方法内部用
switch c.state判断当前能否响应此事件,非法转移直接返回ErrInvalidStateTransition - 状态变更统一走
c.setState(newState),里面可以加日志或 panic 断言(开发期很有用)
net.Conn 不是状态机载体,得自己封装连接结构体
别试图给 net.Conn 打补丁或嵌入它来承载状态——它不暴露底层 socket 状态,也不支持你注入自定义行为。三次握手模拟必须从零封装一个 *TCPConn 类型。
常见误区是把 net.Conn 当作“已建立连接”的代理,然后在它上面硬套握手逻辑,结果收不到 SYN/SYN+ACK 这类原始报文(Go 的 net 包根本不暴露这些)。
- 模拟场景下,通常用
syscall.Socket+syscall.Sendto/Recvfrom操作 raw socket,或用gopacket构造/解析报文 - 你的
TCPConn结构体要包含:localAddr、remoteAddr、state、iss(初始序列号)、snd_nxt、rcv_nxt等必要字段 - 所有对外方法(如
Write)必须先检查c.state == ESTABLISHED,否则返回io.ErrClosedPipe或自定义错误
超时和重传不是状态本身,但必须在状态方法里触发
三次握手中,SYN_SENT 状态必须管理重传定时器,SYN_RECEIVED 要等 ACK,超时就回退到 CLOSED。这些逻辑不属于“状态”,但和状态强绑定。
容易踩的坑是把定时器塞进全局 map 或 goroutine 泛滥管理,导致连接泄漏或竞态。
- 每个
TCPConn实例内嵌一个*time.Timer字段,状态变更时Reset()或Stop() -
SYN_SENT下发 SYN 后立即timer.Reset(1 * time.Second),收到 SYN+ACK 后timer.Stop() - 定时器回调函数必须检查当前状态是否仍是预期值(比如回调执行时连接已被关闭),避免误触发
- 不要用
time.AfterFunc,它无法取消;也不要让 timer 回调直接修改状态,应发消息或调用c.onTimeout()
状态机最难的部分不是写分支逻辑,而是守住“状态不可逆”和“事件响应排他性”这两条线。哪怕只模拟客户端,也要在 SYN_SENT 里拦住重复 SYN、在 ESTABLISHED 里拒收任何 SYN 报文——这些边界检查,比主干流程更容易被跳过。










