3、Socket实战:Python Socket服务器搭建、客户端实现、粘包处理、心跳机制设计
好,咱们直接进入正题。Socket 编程,说白了就是让两台机器能互相说话。在机器人系统里,控制器和上位机之间、控制器和传感器之间,底层通信几乎都离不开 Socket。我最早接触机器人时,用的还是串口,后来换成以太网,那感觉就像从拨号上网换成了光纤——爽快多了。
这一章,咱们手把手把 Python 的 Socket 服务器和客户端搭起来。然后解决两个最头疼的问题:粘包和心跳。嗯,这两个坑我当年都踩过,而且踩得挺深。
3.1 一个最简单的 TCP 服务器
先来个最基础的。Python 的 socket 模块是标准库,直接 import 就能用。我个人习惯把服务器封装成一个类,这样后面加功能方便。
import socket
import threading
class RobotTCPServer:
def __init__(self, host='0.0.0.0', port=8888):
self.host = host
self.port = port
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.clients = []
def start(self):
self.server.bind((self.host, self.port))
self.server.listen(5)
print(f'[INFO] 服务器启动,监听 {self.host}:{self.port}')
while True:
client_sock, addr = self.server.accept()
print(f'[INFO] 新客户端连接:{addr}')
self.clients.append(client_sock)
t = threading.Thread(target=self.handle_client, args=(client_sock,))
t.start()
def handle_client(self, sock):
while True:
try:
data = sock.recv(1024)
if not data:
break
print(f'[RECV] {data.decode()}')
# 回显,方便调试
sock.sendall(b'ACK: ' + data)
except:
break
sock.close()
self.clients.remove(sock)
print('[INFO] 客户端断开')
if __name__ == '__main__':
server = RobotTCPServer()
server.start()
这段代码里有个细节:SO_REUSEADDR。我刚开始写服务器时,经常遇到端口被占用的问题,程序一重启就报错。加上这个选项,就能立刻重用端口,省心很多。
3.2 客户端实现
客户端相对简单。但要注意,机器人控制器通常作为客户端去连接上位机,所以客户端代码要能自动重连。我在项目里遇到过控制器掉电重启后,上位机还在等它连接,结果两边都傻等的情况。
import socket
import time
class RobotTCPClient:
def __init__(self, server_host, server_port):
self.server_host = server_host
self.server_port = server_port
self.sock = None
self.connect()
def connect(self):
while True:
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((self.server_host, self.server_port))
print(f'[INFO] 已连接到服务器 {self.server_host}:{self.server_port}')
break
except Exception as e:
print(f'[WARN] 连接失败,5秒后重试:{e}')
time.sleep(5)
def send(self, message):
if self.sock:
self.sock.sendall(message.encode())
response = self.sock.recv(1024)
return response.decode()
def close(self):
if self.sock:
self.sock.close()
if __name__ == '__main__':
client = RobotTCPClient('127.0.0.1', 8888)
while True:
msg = input('请输入要发送的消息:')
if msg == 'exit':
break
resp = client.send(msg)
print(f'[RESP] {resp}')
client.close()
3.3 粘包问题与解决方案
为什么会粘包?说白了,TCP 是流式协议,它不关心你一次发多少数据,只保证字节顺序正确。如果发送方连续发两条消息,接收方可能一次 recv 就把两条都读走了。这在机器人控制指令里是致命的——你发了个「前进」和「停止」,结果被当成一条指令「前进停止」,机器人就懵了。
解决粘包,常用的有三种方法:
- 固定长度法:每条消息固定长度,不够就补零。简单粗暴,但浪费带宽。
- 特殊分隔符法:比如用
\n或|结尾。但要注意消息内容里不能出现这个分隔符。 - 长度前缀法:先发一个固定长度的头部,告诉对方这条消息有多长。这是工业界最常用的方案。
我个人强烈推荐长度前缀法。来看代码:
import struct
def send_msg(sock, data: bytes):
"""发送带长度前缀的消息"""
length = len(data)
# 用4字节无符号整数表示长度
header = struct.pack('!I', length)
sock.sendall(header + data)
def recv_msg(sock):
"""接收带长度前缀的消息"""
# 先收4字节头部
header = sock.recv(4)
if not header:
return None
length = struct.unpack('!I', header)[0]
# 再收实际数据
data = b''
while len(data) < length:
chunk = sock.recv(length - len(data))
if not chunk:
return None
data += chunk
return data
3.4 心跳机制设计
心跳机制,说白了就是定期确认对方还活着。在机器人系统里,这太重要了。你想,控制器正在执行一个运动指令,突然上位机断开了,控制器如果不知道,就会一直执行下去,可能撞到人或者设备。
心跳设计有几个关键点:
- 心跳间隔:一般 3-5 秒一次。太频繁浪费带宽,太慢无法及时检测断连。
- 超时判定:连续 3 次没收到心跳,就认为连接断了。
- 心跳与业务数据分离:心跳包应该用特殊标识,不要和业务数据混在一起。
我一般这样设计心跳协议:
| 字段 | 长度 | 说明 |
|---|---|---|
| 消息类型 | 1 字节 | 0x01 表示心跳请求,0x02 表示心跳响应 |
| 时间戳 | 8 字节 | Unix 时间戳,用于计算延迟 |
| 保留字段 | 3 字节 | 预留,可扩展 |
来看心跳检测的客户端实现片段:
import threading
import time
class HeartbeatMonitor:
def __init__(self, sock, interval=3, timeout_count=3):
self.sock = sock
self.interval = interval
self.timeout_count = timeout_count
self.missed = 0
self.running = True
def start(self):
t = threading.Thread(target=self._monitor)
t.daemon = True
t.start()
def _monitor(self):
while self.running:
time.sleep(self.interval)
try:
# 发送心跳请求
heartbeat_pkt = b'\x01' + struct.pack('!Q', int(time.time())) + b'\x00\x00\x00'
self.sock.sendall(heartbeat_pkt)
# 这里假设有专门的接收线程处理响应
# 如果没收到响应,missed 会由外部线程增加
except:
self.missed += 1
if self.missed >= self.timeout_count:
print('[ALERT] 心跳超时,连接可能已断开')
self.on_timeout()
break
def on_timeout(self):
"""连接超时后的处理逻辑,比如触发重连"""
self.running = False
# 这里可以调用重连函数
3.5 综合实战:一个带心跳的机器人控制客户端
最后,咱们把上面所有东西串起来。假设我们要做一个机器人关节控制客户端,它需要:
- 连接上位机服务器
- 发送控制指令(带长度前缀防粘包)
- 定期发送心跳
- 检测到断连后自动重连
核心结构大概是这样:
class RobotController:
def __init__(self, server_addr):
self.addr = server_addr
self.sock = None
self.heartbeat = None
self.running = True
def run(self):
while self.running:
self._connect()
self.heartbeat = HeartbeatMonitor(self.sock)
self.heartbeat.start()
self._command_loop()
def _connect(self):
# 带重连的连接逻辑,参考前面的客户端代码
pass
def _command_loop(self):
# 主循环:接收用户输入或自动发送控制指令
while self.running:
cmd = input('输入指令:')
if cmd == 'quit':
self.running = False
break
send_msg(self.sock, cmd.encode())
response = recv_msg(self.sock)
print(f'响应:{response.decode()}')
嗯,这个结构我在好几个机器人项目里都用过。实际部署时,你还需要考虑日志记录、异常上报、指令队列等。但核心骨架就是上面这些。
最后说一句:Socket 编程看似简单,但真正稳定运行起来,需要处理很多边界情况。比如网络闪断、数据包乱序、缓冲区溢出等等。我建议你在开发阶段就加入详细的日志,这样出了问题能快速定位。别问我怎么知道的——都是血泪教训。