3、TCP协议模拟:三次握手与四次挥手的状态机模拟
TCP协议,说白了就是两台设备之间「说人话」的规矩。你想想看,如果A给B发数据,B连个「收到」都不回,那这通信还怎么搞?所以TCP搞了一套状态机,把每一次握手、每一次挥手都定义得清清楚楚。
我个人习惯在嵌入式开发中,用Python先模拟一遍TCP的状态流转。为什么?因为Python写起来快,调试也方便。等逻辑跑通了,再移植到C或者MicroPython里,心里就有底了。
3.1 三次握手:建立连接的过程
三次握手,说白了就是三次对话:
- 第一次:客户端说「嘿,我想跟你说话」(发送SYN)
- 第二次:服务器说「好的,我听到了,我也准备好了」(回复SYN+ACK)
- 第三次:客户端说「收到,那我们开始吧」(发送ACK)
我在项目中遇到过一个问题:客户端发了SYN之后,服务器没回。查了半天,发现是防火墙把SYN包给丢了。嗯,这种坑踩过一次就记住了。
核心要点:三次握手的本质是「双方都确认对方的收发能力正常」。
- 第一次:客户端确认自己的发送能力OK
- 第二次:服务器确认自己的收发能力都OK
- 第三次:客户端确认自己的接收能力OK
3.2 四次挥手:断开连接的过程
四次挥手比握手多了一次,为什么?因为TCP是全双工的,两边都要说再见。
- 第一次:客户端说「我要关了」(发送FIN)
- 第二次:服务器说「收到,但我还有数据要发」(回复ACK)
- 第三次:服务器说「我也发完了,关吧」(发送FIN)
- 第四次:客户端说「好,确认关闭」(回复ACK)
你想想看,如果服务器在第二次挥手之后直接发FIN,那客户端可能还在等数据呢。所以这个「半关闭」状态(TIME_WAIT)是必须的。
避坑指南:我曾经在做一个物联网网关时,发现设备频繁断连。查了抓包日志,发现是客户端没有正确处理TIME_WAIT状态,导致端口被占用。后来加了个超时重试机制,问题就解决了。
3.3 状态机模拟:用Python实现
下面是我写的一个简化版TCP状态机。它不处理真正的网络包,而是用字典来模拟状态流转。
import time
import threading
class TCPStateMachine:
def __init__(self, role='client'):
self.role = role
self.state = 'CLOSED'
self.state_history = []
# 状态转移表
self.transitions = {
'CLOSED': {
'SYN_SENT': 'SYN_SENT',
'LISTEN': 'LISTEN'
},
'SYN_SENT': {
'SYN_ACK_RECEIVED': 'ESTABLISHED'
},
'LISTEN': {
'SYN_RECEIVED': 'SYN_RCVD'
},
'SYN_RCVD': {
'ACK_RECEIVED': 'ESTABLISHED'
},
'ESTABLISHED': {
'FIN_SENT': 'FIN_WAIT_1',
'FIN_RECEIVED': 'CLOSE_WAIT'
},
'FIN_WAIT_1': {
'ACK_RECEIVED': 'FIN_WAIT_2',
'FIN_ACK_RECEIVED': 'TIME_WAIT'
},
'FIN_WAIT_2': {
'FIN_RECEIVED': 'TIME_WAIT'
},
'CLOSE_WAIT': {
'FIN_SENT': 'LAST_ACK'
},
'LAST_ACK': {
'ACK_RECEIVED': 'CLOSED'
},
'TIME_WAIT': {
'TIMEOUT': 'CLOSED'
}
}
def trigger(self, event):
if self.state in self.transitions:
if event in self.transitions[self.state]:
old_state = self.state
self.state = self.transitions[self.state][event]
self.state_history.append((old_state, event, self.state))
print(f"[{self.role}] {old_state} --{event}--> {self.state}")
return True
print(f"[{self.role}] 非法事件: {event} 在当前状态 {self.state}")
return False
def get_state(self):
return self.state
def get_history(self):
return self.state_history
# 模拟三次握手
def simulate_three_way_handshake():
print("=== 三次握手模拟 ===")
client = TCPStateMachine('client')
server = TCPStateMachine('server')
# 服务器开始监听
server.trigger('LISTEN')
# 客户端发送SYN
client.trigger('SYN_SENT')
print("客户端发送SYN...")
# 服务器收到SYN
server.trigger('SYN_RECEIVED')
print("服务器回复SYN+ACK...")
# 客户端收到SYN+ACK
client.trigger('SYN_ACK_RECEIVED')
print("客户端发送ACK...")
# 服务器收到ACK
server.trigger('ACK_RECEIVED')
print(f"最终状态 - 客户端: {client.get_state()}, 服务器: {server.get_state()}")
return client, server
# 模拟四次挥手
def simulate_four_way_wave(client, server):
print("\n=== 四次挥手模拟 ===")
# 客户端发起关闭
client.trigger('FIN_SENT')
print("客户端发送FIN...")
# 服务器回复ACK
server.trigger('FIN_RECEIVED')
print("服务器回复ACK...")
# 服务器也发起关闭
server.trigger('FIN_SENT')
print("服务器发送FIN...")
# 客户端回复ACK
client.trigger('FIN_ACK_RECEIVED')
print("客户端回复ACK...")
# 等待TIME_WAIT超时
client.trigger('TIMEOUT')
print(f"最终状态 - 客户端: {client.get_state()}, 服务器: {server.get_state()}")
if __name__ == "__main__":
c, s = simulate_three_way_handshake()
simulate_four_way_wave(c, s)
3.4 状态转移表
为了方便理解,我把TCP的状态转移整理成了表格。你写代码的时候可以直接参考这个表。
| 当前状态 | 触发事件 | 下一状态 | 说明 |
|---|---|---|---|
| CLOSED | 主动打开 | SYN_SENT | 客户端发起连接 |
| CLOSED | 被动打开 | LISTEN | 服务器等待连接 |
| SYN_SENT | 收到SYN+ACK | ESTABLISHED | 三次握手完成 |
| LISTEN | 收到SYN | SYN_RCVD | 服务器收到连接请求 |
| SYN_RCVD | 收到ACK | ESTABLISHED | 连接建立成功 |
| ESTABLISHED | 主动关闭 | FIN_WAIT_1 | 客户端发起挥手 |
| ESTABLISHED | 收到FIN | CLOSE_WAIT | 服务器收到关闭请求 |
| FIN_WAIT_1 | 收到ACK | FIN_WAIT_2 | 等待服务器关闭 |
| FIN_WAIT_2 | 收到FIN | TIME_WAIT | 等待2MSL |
| CLOSE_WAIT | 主动关闭 | LAST_ACK | 服务器发送FIN |
| LAST_ACK | 收到ACK | CLOSED | 连接彻底关闭 |
| TIME_WAIT | 超时 | CLOSED | 等待结束 |
3.5 实际项目中的注意事项
嗯,这里我要多说几句。你在嵌入式设备上写TCP协议时,有几个坑是绕不开的:
- 资源有限:嵌入式设备的RAM很小,别开太多连接。我一般控制在5个以内。
- 超时处理:网络不稳定时,握手可能失败。一定要加超时重试,不然设备就卡死了。
- 状态机要健壮:非法事件来了怎么办?直接丢弃还是报错?我建议记录日志然后忽略,别让设备崩溃。
小技巧:调试TCP状态机时,可以用Wireshark抓包对比。看看你的状态机跟实际网络包的状态流转是否一致。我曾经靠这个办法,半小时就定位了一个握手失败的问题。
好了,这一章的内容就到这里。TCP状态机看着复杂,其实你动手写一遍就全明白了。下一章我们会讲UDP协议,那个就简单多了——无连接,爱发不发,哈哈。