3、Python基础回顾:面向对象编程、socket编程基础、异常处理机制、日志模块配置
好,咱们进入第三讲。说实话,很多做DoIP测试的朋友,Python基础是有的,但一到写框架就卡壳。为啥?因为面向对象、socket、异常、日志这几个东西,单独拎出来都会,合在一起就懵了。
今天我就带你把这几块串起来。记住,咱们的目标不是学Python,而是用Python搞定DoIP测试。
3.1 面向对象编程:把DoIP报文封装成“活物”
面向对象,说白了就是“把数据和操作数据的方法打包在一起”。我在做DoIP测试时,最烦的就是到处散落着报文解析的代码。后来我学乖了——把每个DoIP报文都封装成一个类。
核心思想:DoIP报文有固定的结构——协议版本、负载类型、负载长度、实际负载。这些字段天然适合用对象来管理。
class DoIPMessage:
def __init__(self, protocol_version=0x02, payload_type=0x0001, payload=b''):
self.protocol_version = protocol_version
self.payload_type = payload_type
self.payload_length = len(payload)
self.payload = payload
def encode(self):
"""把对象变成字节流,方便发送"""
header = struct.pack('!BH', self.protocol_version, self.payload_type)
length_field = struct.pack('!I', self.payload_length)
return header + length_field + self.payload
@classmethod
def decode(cls, raw_data):
"""从字节流还原出对象"""
protocol_version = raw_data[0]
payload_type = struct.unpack('!H', raw_data[1:3])[0]
payload_length = struct.unpack('!I', raw_data[3:7])[0]
payload = raw_data[7:7+payload_length]
return cls(protocol_version, payload_type, payload)
你看,这样一封装,发送和接收都变得特别清爽。我个人习惯是,每个DoIP报文类型都对应一个子类,比如路由激活报文、诊断报文,都继承自这个基类。
我的经验:刚开始写框架时,别急着搞继承。先把一个类写稳了,再考虑抽象。我见过太多人一上来就设计三层继承,结果改一个地方崩一片。
3.2 Socket编程基础:DoIP的“血管”
DoIP跑在TCP/IP上,所以socket编程是基本功。但注意,DoIP不是简单的“发-收”模式,它有连接建立、保活、超时重连这些讲究。
为什么会这样?因为车上的ECU可能随时休眠,也可能突然重启。你的测试脚本必须能处理这些“不靠谱”。
import socket
import time
class DoIPSocket:
def __init__(self, target_ip, target_port=13400):
self.target_ip = target_ip
self.target_port = target_port
self.sock = None
self.timeout = 5 # 秒
def connect(self):
"""建立TCP连接,带重试机制"""
retry_count = 3
for i in range(retry_count):
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.sock.connect((self.target_ip, self.target_port))
print(f"连接成功: {self.target_ip}:{self.target_port}")
return True
except socket.timeout:
print(f"第{i+1}次连接超时,重试中...")
time.sleep(1)
except Exception as e:
print(f"连接异常: {e}")
break
return False
def send_message(self, doip_msg):
"""发送DoIP报文"""
if not self.sock:
raise ConnectionError("socket未连接")
raw_data = doip_msg.encode()
self.sock.sendall(raw_data)
def receive_message(self):
"""接收DoIP报文,处理粘包问题"""
# 先读7字节头部
header = self.sock.recv(7)
if len(header) < 7:
return None
payload_length = struct.unpack('!I', header[3:7])[0]
# 再读负载
payload = b''
while len(payload) < payload_length:
chunk = self.sock.recv(payload_length - len(payload))
if not chunk:
break
payload += chunk
return DoIPMessage.decode(header + payload)
避坑指南:我曾经在项目里吃过“粘包”的亏。DoIP报文是流式的,recv一次不一定能拿到完整报文。所以一定要先读固定长度的头部,再根据头部里的长度字段去读负载。这个顺序不能乱。
3.3 异常处理机制:别让脚本“裸奔”
测试脚本跑着跑着突然崩了,这种事你遇到过吧?我早期写脚本时,异常处理就是print("出错了"),然后exit。后来被坑惨了——有一次半夜跑回归测试,脚本在凌晨3点崩了,第二天早上才发现,一晚上白跑了。
所以,异常处理要分层、要精细。
class DoIPError(Exception):
"""DoIP基础异常类"""
pass
class ConnectionError(DoIPError):
"""连接相关异常"""
pass
class TimeoutError(DoIPError):
"""超时异常"""
pass
class ProtocolError(DoIPError):
"""协议解析异常"""
pass
def safe_send_receive(socket_obj, doip_msg, max_retries=3):
"""带异常处理的发送接收函数"""
for attempt in range(max_retries):
try:
socket_obj.send_message(doip_msg)
response = socket_obj.receive_message()
if response is None:
raise TimeoutError("接收超时,未收到响应")
return response
except socket.timeout:
print(f"第{attempt+1}次尝试超时")
if attempt == max_retries - 1:
raise TimeoutError("重试耗尽,放弃")
time.sleep(2)
except ProtocolError as e:
print(f"协议错误: {e}")
# 记录错误报文,方便事后分析
log_error(f"协议错误: {e}, 报文: {doip_msg}")
raise
except Exception as e:
print(f"未知错误: {e}")
# 尝试重连
socket_obj.connect()
continue
我的习惯:自定义异常类一定要继承自一个基础异常。这样你在最外层可以统一捕获DoIPError,而不用管具体是哪种错误。另外,异常信息里一定要带上上下文——哪个报文、哪个步骤、什么时间。不然查问题就像大海捞针。
3.4 日志模块配置:给测试过程“录像”
日志这东西,平时觉得没用,出问题时就真香了。我见过有人用print来记录测试过程,结果脚本一多,满屏的print,根本分不清是哪条消息。
Python自带的logging模块,配置好了非常好用。我一般这样配:
import logging
import logging.handlers
import os
from datetime import datetime
def setup_logger(log_dir="logs", log_level=logging.DEBUG):
"""配置日志系统"""
# 创建日志目录
os.makedirs(log_dir, exist_ok=True)
# 日志文件名带时间戳
log_file = os.path.join(log_dir, f"doip_test_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")
# 创建logger
logger = logging.getLogger("DoIPTest")
logger.setLevel(log_level)
# 文件处理器:记录所有级别
file_handler = logging.handlers.RotatingFileHandler(
log_file, maxBytes=10*1024*1024, backupCount=5, encoding='utf-8'
)
file_handler.setLevel(logging.DEBUG)
# 控制台处理器:只记录INFO及以上
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# 定义格式
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# 添加处理器
logger.addHandler(file_handler)
logger.addHandler(console_handler)
return logger
# 使用示例
logger = setup_logger()
logger.info("DoIP测试开始")
logger.debug(f"发送报文: {doip_msg.encode().hex()}")
logger.warning("ECU响应超时,准备重试")
logger.error("连接断开,测试终止")
关键配置说明:
| 配置项 | 推荐值 | 原因 |
|---|---|---|
| 日志级别 | DEBUG(文件)/ INFO(控制台) | 文件记录细节,控制台只看关键信息 |
| 文件轮转 | 10MB,保留5个备份 | 防止日志文件撑爆磁盘 |
| 格式 | 含时间、文件名、行号 | 方便定位问题来源 |
| 编码 | utf-8 | 避免中文乱码 |
避坑指南:我曾经犯过一个低级错误——在循环里反复调用setup_logger,结果每次调用都新增一个handler,日志重复打印。正确的做法是:在模块级别只调用一次,或者用logger.hasHandlers()判断一下。
3.5 把它们串起来:一个完整的DoIP测试片段
好了,前面四个知识点都讲完了。咱们把它们组合起来,看看实际怎么用。
def test_doip_routing_activation():
"""测试DoIP路由激活功能"""
logger = setup_logger()
socket_obj = DoIPSocket("192.168.1.100")
try:
# 连接
if not socket_obj.connect():
logger.error("连接失败,测试终止")
return False
# 构造路由激活请求
activation_req = DoIPMessage(
protocol_version=0x02,
payload_type=0x0005, # 路由激活
payload=bytes([0x01, 0x00, 0x00, 0x00]) # 激活类型
)
# 发送并接收
logger.info("发送路由激活请求")
response = safe_send_receive(socket_obj, activation_req)
# 验证响应
if response.payload_type == 0x0006: # 路由激活响应
logger.info("路由激活成功")
return True
else:
logger.warning(f"意外的响应类型: 0x{response.payload_type:04x}")
return False
except DoIPError as e:
logger.error(f"DoIP测试失败: {e}")
return False
except Exception as e:
logger.critical(f"未预期的异常: {e}", exc_info=True)
return False
finally:
socket_obj.sock.close()
logger.info("连接已关闭")
你看,这样写出来的代码,可读性强、健壮性好、出了问题也好查。这就是面向对象 + socket + 异常 + 日志组合起来的效果。
嗯,这一讲就到这里。下一讲咱们开始真正搭建DoIP自动化测试框架的骨架。到时候你会发现,今天打的基础,全都能用上。