239 lines
8.3 KiB
Python
239 lines
8.3 KiB
Python
"""
|
||
Socket客户端
|
||
"""
|
||
import socket
|
||
import json
|
||
import time
|
||
from voice_drone.core.configuration import SYSTEM_SOCKET_SERVER_CONFIG
|
||
from voice_drone.core.command import Command
|
||
from voice_drone.logging_ import get_logger
|
||
|
||
logger = get_logger("socket.client")
|
||
|
||
|
||
class SocketClient:
|
||
# 初始化Socket客户端
|
||
def __init__(self, config: dict):
|
||
self.host = config.get("host")
|
||
self.port = config.get("port")
|
||
self.connect_timeout = config.get("connect_timeout")
|
||
self.send_timeout = config.get("send_timeout")
|
||
self.reconnect_interval = float(config.get("reconnect_interval") or 3.0)
|
||
# max_retries:-1 表示断线后持续重连并发送,直到成功(不视为致命错误)
|
||
_mr = config.get("max_retries", -1)
|
||
try:
|
||
self.max_reconnect_attempts = int(_mr)
|
||
except (TypeError, ValueError):
|
||
self.max_reconnect_attempts = -1
|
||
|
||
self.sock = None
|
||
self.connected = False
|
||
|
||
# 连接到socket服务器
|
||
def connect(self) -> bool:
|
||
if self.connected and self.sock is not None:
|
||
return True
|
||
try:
|
||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
self.sock.settimeout(self.connect_timeout)
|
||
self.sock.connect((self.host, self.port))
|
||
self.sock.settimeout(self.send_timeout)
|
||
self.connected = True
|
||
print(
|
||
f"[SocketClient] 连接成功: {self.host}:{self.port}",
|
||
flush=True,
|
||
)
|
||
logger.info("Socket 已连接 %s:%s", self.host, self.port)
|
||
return True
|
||
|
||
except socket.timeout:
|
||
logger.warning(
|
||
"Socket 连接超时 host=%s port=%s timeout=%s",
|
||
self.host,
|
||
self.port,
|
||
self.connect_timeout,
|
||
)
|
||
print(
|
||
"[SocketClient] connect: 连接超时 "
|
||
f"(host={self.host!r}, port={self.port!r}, timeout={self.connect_timeout!r})",
|
||
flush=True,
|
||
)
|
||
self._cleanup()
|
||
return False
|
||
except ConnectionRefusedError as e:
|
||
logger.warning("Socket 连接被拒绝: %s", e)
|
||
print(
|
||
f"[SocketClient] connect: 连接被拒绝: {e!r}",
|
||
flush=True,
|
||
)
|
||
self._cleanup()
|
||
return False
|
||
except OSError as e:
|
||
logger.warning("Socket connect OSError (%s): %s", type(e).__name__, e)
|
||
print(
|
||
f"[SocketClient] connect: OSError ({type(e).__name__}): {e!r}",
|
||
flush=True,
|
||
)
|
||
self._cleanup()
|
||
return False
|
||
except Exception as e:
|
||
print(
|
||
f"[SocketClient] connect: 未预期异常 ({type(e).__name__}): {e!r}",
|
||
flush=True,
|
||
)
|
||
self._cleanup()
|
||
return False
|
||
|
||
# 断开与socket服务器的连接
|
||
def disconnect(self) -> None:
|
||
self._cleanup()
|
||
|
||
# 清理资源
|
||
def _cleanup(self) -> None:
|
||
if self.sock is not None:
|
||
try:
|
||
self.sock.close()
|
||
except Exception:
|
||
pass
|
||
self.sock = None
|
||
self.connected = False
|
||
|
||
# 确保连接已建立
|
||
def _ensure_connected(self) -> bool:
|
||
if self.connected and self.sock is not None:
|
||
return True
|
||
|
||
return self.connect()
|
||
|
||
# 发送命令
|
||
def send_command(self, command) -> bool:
|
||
print("[SocketClient] 正在发送命令…", flush=True)
|
||
|
||
if not self._ensure_connected():
|
||
logger.warning(
|
||
"Socket 未连接且 connect 失败,跳过本次发送 host=%s port=%s",
|
||
self.host,
|
||
self.port,
|
||
)
|
||
print(
|
||
"[SocketClient] 未连接或 connect 失败,跳过发送",
|
||
flush=True,
|
||
)
|
||
return False
|
||
|
||
try:
|
||
command_dict = command.to_dict()
|
||
json_str = json.dumps(command_dict, ensure_ascii=False)
|
||
|
||
# 添加换行符(根据 JSON格式说明.md,命令以换行符分隔)
|
||
message = json_str + "\n"
|
||
|
||
# 发送数据
|
||
self.sock.sendall(message.encode("utf-8"))
|
||
print("[SocketClient] sendall 成功", flush=True)
|
||
return True
|
||
|
||
except socket.timeout:
|
||
logger.warning("Socket send 超时,将断开以便重连")
|
||
print("[SocketClient] send_command: socket 超时", flush=True)
|
||
self._cleanup()
|
||
return False
|
||
except ConnectionResetError as e:
|
||
logger.warning("Socket 连接被重置(将重连): %s", e)
|
||
print(
|
||
f"[SocketClient] send_command: 连接被重置 ({type(e).__name__}): {e!r}",
|
||
flush=True,
|
||
)
|
||
self._cleanup()
|
||
return False
|
||
except BrokenPipeError as e:
|
||
logger.warning("Socket 管道破裂(将重连): %s", e)
|
||
print(
|
||
f"[SocketClient] send_command: 管道破裂 ({type(e).__name__}): {e!r}",
|
||
flush=True,
|
||
)
|
||
self._cleanup()
|
||
return False
|
||
except OSError as e:
|
||
# 断网、对端关闭等:可恢复,不当作未捕获致命错误
|
||
logger.warning("Socket send OSError (%s): %s(将重连)", type(e).__name__, e)
|
||
print(
|
||
f"[SocketClient] send_command: OSError ({type(e).__name__}): {e!r}",
|
||
flush=True,
|
||
)
|
||
self._cleanup()
|
||
return False
|
||
except Exception as e:
|
||
logger.warning(
|
||
"Socket send 异常 (%s): %s(将重连)", type(e).__name__, e
|
||
)
|
||
print(
|
||
f"[SocketClient] send_command: 异常 ({type(e).__name__}): {e!r}",
|
||
flush=True,
|
||
)
|
||
self._cleanup()
|
||
return False
|
||
|
||
# 发送命令并重试
|
||
def send_command_with_retry(self, command) -> bool:
|
||
"""失败后清理连接并按 reconnect_interval 重试;max_retries=-1 时直到发送成功。"""
|
||
unlimited = self.max_reconnect_attempts < 0
|
||
cap = max(1, self.max_reconnect_attempts) if not unlimited else None
|
||
attempt = 0
|
||
while True:
|
||
attempt += 1
|
||
self._cleanup()
|
||
if self.send_command(command):
|
||
if attempt > 1:
|
||
print(
|
||
f"[SocketClient] 重试后发送成功(第 {attempt} 次)",
|
||
flush=True,
|
||
)
|
||
logger.info("Socket 重连后命令已发送(第 %s 次尝试)", attempt)
|
||
return True
|
||
|
||
if not unlimited and cap is not None and attempt >= cap:
|
||
logger.warning(
|
||
"Socket 已达 max_retries=%s,本次命令未送达,稍后可再试",
|
||
self.max_reconnect_attempts,
|
||
)
|
||
print(
|
||
"[SocketClient] 已达最大重试次数,本次命令未送达(可稍后重试)",
|
||
flush=True,
|
||
)
|
||
return False
|
||
|
||
# 无限重试时每 10 次打一条日志,避免刷屏
|
||
if unlimited and attempt % 10 == 1:
|
||
logger.warning(
|
||
"Socket 发送失败,%ss 后第 %s 次重连重试…",
|
||
self.reconnect_interval,
|
||
attempt,
|
||
)
|
||
print(
|
||
f"[SocketClient] 发送失败,{self.reconnect_interval}s 后重试 "
|
||
f"(第 {attempt} 次)",
|
||
flush=True,
|
||
)
|
||
time.sleep(self.reconnect_interval)
|
||
|
||
# 上下文管理器入口
|
||
def __enter__(self):
|
||
self.connect()
|
||
return self
|
||
|
||
# 上下文管理器出口
|
||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||
self.disconnect()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
from voice_drone.core.configuration import SYSTEM_SOCKET_SERVER_CONFIG
|
||
from voice_drone.core.command import Command
|
||
|
||
config = SYSTEM_SOCKET_SERVER_CONFIG
|
||
client = SocketClient(config)
|
||
client.connect()
|
||
command = Command.create("takeoff", 1)
|
||
client.send_command(command)
|
||
client.disconnect() |