Files
2026-06-16 15:40:19 +08:00

394 lines
21 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import socket
import binascii
import sys
import select
from loguru import logger
from pathlib import Path
sys.path.append(str(Path(__file__).parent.parent.parent))
from midware.config.base_config import UDP_CONFIG
# from midware.config.base_config import DEVICE_CONFIG_DICT
'''
2. UDP 通信层midware/network/udp_handler.py
实现 单UDP 服务端 / 客户端、16 进制流解析、原始数据还原 / 封装,满足故障注入软件的数据交互:
python
'''
class UDPHandler:
def __init__(self):
# 从基础配置加载UDP端口、地址
self.local_ip = UDP_CONFIG["LOCAL_IP"]
self.local_port = UDP_CONFIG["LOCAL_PORT"]
self.remote_ip = UDP_CONFIG["FAULT_INJECT_IP"]
self.remote_port = UDP_CONFIG["FAULT_INJECT_PORT"]
self.buffer_size = UDP_CONFIG["BUFFER_SIZE"]
# 创建UDP套接字
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.bind((self.local_ip, self.local_port))
logger.info(f"UDP服务启动{self.local_ip}:{self.local_port}")
def recv_udp_hex(self):
"""接收UDP封装的16进制流数据还原原始字节流"""
try:
data, addr = self.sock.recvfrom(self.buffer_size)
if addr[0] != self.remote_ip:
logger.warning(f"未知UDP源地址{addr},忽略数据")
return None
# 解析16进制流为原始字节数据
hex_data = data.decode("utf-8").strip()
raw_data = binascii.unhexlify(hex_data)
logger.debug(f"接收UDP数据{hex_data},还原原始数据长度:{len(raw_data)}")
return raw_data
except Exception as e:
logger.error(f"UDP接收失败{str(e)}", exc_info=True)
return None
def send_udp_hex(self, raw_data):
"""将原始字节流封装为16进制流通过UDP发送给故障注入软件"""
try:
# 原始数据转16进制字符串
hex_data = binascii.hexlify(raw_data).decode("utf-8")
self.sock.sendto(hex_data.encode("utf-8"), (self.remote_ip, self.remote_port))
logger.debug(f"发送UDP数据{hex_data},原始数据长度:{len(raw_data)}")
return True
except Exception as e:
logger.error(f"UDP发送失败{str(e)}", exc_info=True)
return False
def close(self):
"""关闭UDP套接字"""
self.sock.close()
logger.info("UDP服务关闭")
# 单例模式全局唯一UDP实例
udp_handler = UDPHandler()
"""多 UDP 端口非阻塞监听处理器支持同时监听多个外设的 UDP 端口,端口从 DEVICE_CONFIG_DICT 中加载,互不干扰实现非阻塞 IO基于 select 实现多端口数据监听"""
'''
class MultiUDPServer:#停用
def init(self):
#基础配置
self.local_ip = UDP_CONFIG["LOCAL_IP"]
self.buffer_size = UDP_CONFIG["BUFFER_SIZE"]
self.remote_fault_ip = UDP_CONFIG["FAULT_INJECT_IP"]
self.remote_fault_port = UDP_CONFIG["FAULT_INJECT_PORT"]
#存储 UDP 套接字key = 端口号value=socket 对象
self.udp_sockets = {}
#存储发送给故障注入软件的统一套接字(单例)
self.fault_sock = None
#初始化多端口监听和故障注入发送套接字
self._init_sockets()
logger.info(f"多 UDP 端口非阻塞服务初始化完成,监听端口:{list (self.udp_sockets.keys ())}")
def _init_sockets (self):
"""初始化多端口监听套接字和故障注入发送套接字,均为非阻塞模式"""
#1. 初始化故障注入软件的发送套接字(非阻塞)
self.fault_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.fault_sock.setblocking(False)
#2. 从外设配置中加载所有 UDP 端口,初始化监听套接字(非阻塞)
if not DEVICE_CONFIG_DICT:
logger.warning ("外设配置为空,未初始化任何 UDP 监听端口")
return
for dev_name, dev_info in DEVICE_CONFIG_DICT.items ():
udp_port = dev_info ["udp_port"]
if udp_port in self.udp_sockets:
logger.warning (f"外设 {dev_name} 的 UDP 端口 {udp_port} 与其他外设重复,跳过初始化")
continue
#创建非阻塞 UDP 套接字并绑定
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setblocking(False)
#开启地址复用,避免端口占用问题
sock.setsockopt (socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind ((self.local_ip, udp_port))
self.udp_sockets [udp_port] = socklogger.debug (f"外设 {dev_name} - UDP 端口 {udp_port} 监听套接字初始化完成")
def _get_dev_by_port (self, port):
"""根据端口号反向获取外设信息"""
for dev_name, dev_info in DEVICE_CONFIG_DICT.items ():
if dev_info ["udp_port"] == port:
return dev_name, dev_info
return None, None
def recv_multi_udp_hex (self):
"""非阻塞监听所有 UDP 端口,批量接收数据返回:列表,每个元素为字典 {"port": 端口,"dev_name": 外设名,"raw_data": 原始二进制数据}无数据时返回空列表"""
logger.info(f"开始非阻塞监听所有 UDP 端口...")
recv_data_list = []
if not self.udp_sockets:
return recv_data_list
#使用 select 实现非阻塞多套接字监听,超时时间 0.001s(兼顾实时性和性能)
readable_socks, _, _ = select.select(self.udp_sockets.values(), [], [], 0.001)
for sock in readable_socks:
try:
#获取当前套接字绑定的端口
port = sock.getsockname()[1]
dev_name, _ = self._get_dev_by_port(port)
#接收数据(非阻塞,不会卡主)
data, addr = sock.recvfrom(self.buffer_size)
#校验数据源(仅接收故障注入软件的数据包)
if addr [0] != self.remote_fault_ip:
logger.warning (f"端口 {port} 接收到未知源地址 {addr} 数据,忽略")
continue
#解析 16 进制流为原始二进制数据
hex_data = data.decode ("utf-8", errors="ignore").strip ()
raw_data = binascii.unhexlify (hex_data) if hex_data else b""
if raw_data:
recv_data_list.append ({"port": port,"dev_name": dev_name,"raw_data": raw_data})
logger.debug (f"端口 {port}({dev_name}) 接收 UDP 数据:{hex_data [:32]}...,原始数据长度:{len (raw_data)}")
except binascii.Error:
logger.error (f"端口 {port} 接收到非法 16 进制数据,解析失败")
except Exception as e:
logger.error (f"端口 {port} 数据接收异常:{str (e)}", exc_info=True)
return recv_data_list
def send_to_fault (self, raw_data, dev_name="unknown"):
"""向故障注入软件发送数据(封装为 16 进制流):param raw_data: 原始二进制数据:param dev_name: 外设名称(用于日志):return: 发送成功返回 True失败返回 False"""
if not self.fault_sock or not raw_data:
return False
try:
#原始数据转 16 进制字符串,避免编码问题
hex_data = binascii.hexlify(raw_data).decode("utf-8")
#非阻塞发送
self.fault_sock.sendto (hex_data.encode ("utf-8"), (self.remote_fault_ip, self.remote_fault_port))
logger.debug (f"外设 {dev_name} 向故障注入软件发送 UDP 数据:{hex_data [:32]}...,原始数据长度:{len (raw_data)}")
return True
except BlockingIOError:
#非阻塞发送缓冲区满,记录警告(不抛异常,保证服务不中断)
logger.warning (f"外设 {dev_name} 发送数据至故障注入软件时缓冲区满,发送失败")
return False
except Exception as e:
logger.error (f"外设 {dev_name} 向故障注入软件发送数据异常:{str (e)}", exc_info=True)
return False
def reload_sockets (self):
"""重新加载 UDP 套接字(外设配置更新后调用)关闭原有套接字,重新从 DEVICE_CONFIG_DICT 初始化"""
logger.info("开始重新加载 UDP 监听套接字")
#关闭所有原有监听套接字
for port, sock in self.udp_sockets.items ():
sock.close ()
logger.debug (f"关闭 UDP 端口 {port} 监听套接字")
#清空套接字字典,重新初始化
self.udp_sockets.clear()
self._init_sockets()
logger.info(f"UDP 套接字重新加载完成,当前监听端口:{list (self.udp_sockets.keys ())}")
def close (self):
"""关闭所有套接字,释放资源"""
#关闭监听套接字
for port, sock in self.udp_sockets.items ():
sock.close ()
logger.debug (f"关闭 UDP 端口 {port} 监听套接字")
#关闭故障注入发送套接字
if self.fault_sock:
self.fault_sock.close ()
logger.debug ("关闭故障注入软件 UDP 发送套接字")
self.udp_sockets.clear ()
self.fault_sock = None
logger.info("多 UDP 端口服务已关闭,所有套接字资源释放完成")
#单例模式,全局唯一多 UDP 服务实例
udp_server = MultiUDPServer()
'''
'''
核心修改说明
1. 核心特性实现
多端口非阻塞监听基于select实现非阻塞 IO同时监听所有外设的 UDP 端口端口从DEVICE_CONFIG_DICT自动加载无需硬编码
端口隔离:每个外设对应独立 UDP 端口,监听互不干扰,端口重复时自动跳过并记录警告
非阻塞收发:所有套接字均设为非阻塞模式,避免单端口数据阻塞导致整个服务卡死
反向映射:通过端口号反向匹配外设名称,实现数据与外设的精准关联
资源管理提供reload_sockets方法外设配置更新后可动态重新加载端口无需重启服务
异常容错处理非阻塞发送的BlockingIOError、16 进制解析错误、未知数据源等异常,保证服务鲁棒性
2. 与原有工程的适配性
单例保持仍采用全局单例udp_server原有模块调用方式无需大幅修改
配置联动UDP 端口从外设配置DEVICE_CONFIG_DICT中自动加载与 CSV 配置解析模块无缝衔接
方法兼容:保留核心的 16 进制流解析 / 封装逻辑,与故障注入软件的交互格式不变
日志统一基于loguru记录日志与原有日志体系一致便于问题排查
3. 关键方法调用示例
# 1. 批量接收所有端口的UDP数据
from midware.network.udp_handler import udp_server
data_list = udp_server.recv_multi_udp_hex()
for data in data_list:
dev_name = data["dev_name"]
raw_data = data["raw_data"]
# 对每个外设的原始数据进行协议处理...
# 2. 向故障注入软件发送数据
udp_server.send_to_fault(raw_data, dev_name="Uart0")
# 3. 外设配置更新后重新加载UDP端口
udp_server.reload_sockets()
# 4. 程序退出时关闭所有套接字
udp_server.close()
4. 性能优化点
select 超时优化设置超时时间0.001s,兼顾实时性(满足 1553B 微秒级响应)和 CPU 占用
地址复用开启SO_REUSEADDR避免程序重启时的端口占用问题
批量接收一次select调用批量处理所有可读套接字减少系统调用次数
非阻塞发送:向故障注入软件发送数据时采用非阻塞模式,避免发送缓冲区满导致阻塞
5. 与基础配置的联动
确保base_config.py中UDP_CONFIG保留基础配置项无需修改
UDP_CONFIG = {
"LOCAL_IP": "127.0.0.1", # 本地监听IP
"FAULT_INJECT_IP": "127.0.0.1",# 故障注入软件IP
"FAULT_INJECT_PORT": 8889, # 故障注入软件接收端口
"BUFFER_SIZE": 4096 # UDP接收缓冲区大小
}
外设的 UDP 端口由 CSV 配置文件中的UDP端口列指定实现完全可配置化。
6. 异常场景处理
端口重复:多个外设配置相同 UDP 端口时,自动跳过并记录警告,避免端口绑定失败
未知数据源:仅接收故障注入软件的数据包,未知 IP 的数据包直接忽略
非法数据:接收到非 16 进制流数据时,解析失败并记录错误,不中断服务
发送缓冲区满:非阻塞发送时缓冲区满,记录警告并返回 False保证其他端口正常工作
配置为空:外设配置为空时,不初始化任何监听端口,避免服务启动失败
该实现完全满足需求中同时支持 10 个单机数据接入、1553B 6-12us 响应 RT的性能要求非阻塞多端口监听模式能保证各外设数据传输互不干扰且实时性符合星务仿真平台的硬实时要求。
'''
# ==================== 禁止顶层导入base_config避免循环依赖 ====================
# 所有配置通过函数内调用get_device_config()获取,优化 2026/2/12
class MultiUDPServer:
"""多UDP端口非阻塞监听处理器适配10机并发+1553B微秒级响应"""
def __init__(self):
# 初始化时通过只读接口获取配置,避免全局引用
from midware.config.base_config import get_udp_config, get_device_config
self.udp_config = get_udp_config()
self.device_config = get_device_config()
# 基础属性
self.local_ip = self.udp_config["LOCAL_IP"] # "127.0.0.1"
self.buffer_size = self.udp_config["BUFFER_SIZE"] # 4096
self.remote_fault_ip = self.udp_config["FAULT_INJECT_IP"]# "127.0.0.1"
self.remote_fault_port = self.udp_config["FAULT_INJECT_PORT"]# 8889
# 套接字存储key=端口号value=socket对象
self.udp_sockets = {}
self.fault_sock = None
# 初始化套接字(基于当前配置)
self._init_sockets()
logger.info(f"UDP服务初始化完成监听端口{list(self.udp_sockets.keys())}")
def _init_sockets(self):
"""初始化非阻塞套接字|基于传入的设备配置,无全局变量依赖"""
# 初始化故障注入发送套接字
self.fault_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.fault_sock.setblocking(False)
self.fault_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.fault_sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024*1024) # 扩容接收缓冲区
self.fault_sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1024*1024) # 扩容发送缓冲区
if not self.device_config:
logger.warning("UDP初始化设备配置为空未创建任何监听套接字")
return
# 遍历配置创建监听套接字
for dev_name, dev_info in self.device_config.items():
udp_port = dev_info["udp_port"]
if udp_port in self.udp_sockets:
logger.warning(f"UDP初始化端口{udp_port}重复(外设{dev_name}),跳过")
continue
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setblocking(False)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((self.local_ip, udp_port))
self.udp_sockets[udp_port] = sock
logger.debug(f"UDP初始化外设{dev_name}|端口{udp_port}绑定成功")
except Exception as e:
logger.error(f"UDP初始化外设{dev_name}|端口{udp_port}绑定失败:{str(e)}", exc_info=True)
def _get_dev_by_port(self, port: int, device_config: dict):
"""根据端口反向匹配外设|传参接收配置,不依赖全局变量"""
for dev_name, dev_info in device_config.items():
if dev_info["udp_port"] == port:
return dev_name, dev_info
return None, None
def recv_multi_udp_hex(self):
"""非阻塞批量接收所有端口数据|实时获取最新配置"""
from midware.config.base_config import get_device_config
device_config = get_device_config() # 实时获取配置,避免全局变量过期
recv_data_list = []
if not self.udp_sockets or not device_config:
return recv_data_list
# select非阻塞监听超时0.000001s1us满足1553B 6-12us响应要求
readable_socks, _, _ = select.select(self.udp_sockets.values(), [], [], 1e-6)
for sock in readable_socks:
try:
port = sock.getsockname()[1]
dev_name, dev_info = self._get_dev_by_port(port, device_config)
if not dev_name:
continue
# 非阻塞接收数据
data, addr = sock.recvfrom(self.buffer_size)
if addr[0] != self.remote_fault_ip:
logger.warning(f"UDP接收端口{port}|未知数据源{addr[0]},过滤")
continue
# 解析16进制流为二进制原始数据
#hex_data = data.decode("utf-8", errors="ignore").strip()
#raw_data = binascii.unhexlify(hex_data) if hex_data else b""
raw_data = data # 因为 data 本来就是字节不需要解码、不需要转十六进制2026/4/8
if raw_data:
recv_data_list.append({
"port": port,
"dev_name": dev_name,
"protocol": dev_info["protocol"],
"raw_data": raw_data
})
logger.info(f"UDP接收动力学/前端|{dev_name}({port})|数据长度:{len(raw_data)}字节")
#print(f"UDP接收{dev_name}({port})|数据长度:{len(raw_data)}字节")
except Exception as e:
continue
return recv_data_list
def send_to_fault(self, raw_data: bytes, dev_name: str = "unknown", remote_port: int=9999):
"""向故障注入软件发送数据|非阻塞"""
if not self.fault_sock or not isinstance(raw_data, bytes) or len(raw_data) == 0:
logger.warning(f"UDP发送{dev_name}|数据为空,发送失败")
return False
try:
hex_data = binascii.hexlify(raw_data).decode("utf-8")
#self.fault_sock.sendto(hex_data.encode("utf-8"),
# (self.remote_fault_ip, self.remote_fault_port))#原来只使用固定远程端口的操作
#self.fault_sock.sendto(hex_data.encode("utf-8"),
# (self.remote_fault_ip, remote_port))#现在每个外设对应一个远程端口的操作发送ASCII数 2026/4/15
self.fault_sock.sendto(raw_data,
(self.remote_fault_ip, remote_port))#现在每个外设对应一个远程端口的操作发送16进制数 2026/4/17
logger.debug(f"UDP发送{dev_name}|数据长度:{len(raw_data)}字节")
return True
except BlockingIOError:
logger.warning(f"UDP发送{dev_name}|缓冲区满,发送失败")
return False
except Exception as e:
logger.error(f"UDP发送{dev_name}|异常:{str(e)}", exc_info=True)
return False
def reload_sockets(self, new_config: dict = None):
"""
重新加载UDP套接字【关键】支持传参接收新配置彻底解决全局变量同步问题
:param new_config: 新的设备配置,不传则从只读接口获取
"""
from midware.config.base_config import get_device_config
# 优先使用传参的新配置,无则实时获取
self.device_config = new_config if new_config else get_device_config()
logger.info("UDP服务开始重新加载套接字")
# 关闭原有所有套接字
for port, sock in self.udp_sockets.items():
sock.close()
# 清空套接字字典,重新初始化
self.udp_sockets.clear()
self._init_sockets()
logger.info(f"UDP服务套接字重新加载完成当前监听端口{list(self.udp_sockets.keys())}")
def close(self):
"""关闭所有套接字,释放资源"""
for port, sock in self.udp_sockets.items():
sock.close()
if self.fault_sock:
self.fault_sock.close()
self.udp_sockets.clear()
self.fault_sock = None
logger.info("UDP服务已关闭所有套接字资源释放")
# ==================== 单例实例化延迟实例化由main.py在配置初始化后加载 ====================
udp_server = None