Python struct.error报错解析、struct.unpack功能及ICMP请求实现问询
Hey there, let's break down your questions one by one!
1. 解析
struct.error: bad char in struct format 错误 这个错误本质是你传给struct.pack()或struct.unpack()的格式字符串里包含了不被struct模块支持的字符,或者字符组合不符合规则。常见触发场景有这些:
- 格式字符串出现struct定义外的字符:比如误写
"!XH",其中X不是合法格式符(合法的如H无符号短整型、I无符号整型、s字符串等)。 - 字节序标记(
!/</>/=)后跟无效字符:比如"!123H"写法错误,数量应放在格式符前,正确写法是"!2H"表示2个无符号短整型。 - 格式符大小写误用:虽然大部分格式符大小写区分有符号/无符号,但使用完全不存在的大小写变体也会报错。
排查时直接拆分格式字符串,对照struct模块的格式符列表逐一检查,就能快速定位问题。
2. 解析
struct.unpack("!%sH" % (len(data) / 2), data) 的功能 这条语句的作用可以拆成三个清晰部分:
!:指定按照**网络字节序(大端模式)**解包,这是网络通信通用的字节序标准,保证跨平台兼容性。"%sH" % (len(data)/2):动态生成格式字符串——比如data长度为4字节时,会变成"2H",表示要解出2个无符号短整型(每个H占2字节)。- 整体调用:将二进制数据
data按上述规则解包,返回包含N个无符号短整型的元组,其中N等于len(data)/2。
⚠️ 重要注意:该语句要求len(data)必须是偶数,否则会触发另一个struct错误(每个H需要2字节空间)。举个例子,如果data是b'\x00\x01\x00\x02',解包后会得到(1, 2)。
3. 完成ICMP请求消息的Python实现
基于你给出的代码片段,我们需要补全create_packet()和nonprintable_to_dots()函数,同时完善接收逻辑。以下是符合ICMP协议标准的完整可运行实现:
import socket import os import struct import sys from functools import reduce def nonprintable_to_dots(): # 创建字节翻译表:将不可打印ASCII字符转为'.',可打印字符保留原样 return bytes([ord('.') if i < 32 or i > 126 else i for i in range(256)]) def calculate_checksum(data): # 遵循RFC 1071标准计算ICMP包校验和 if len(data) % 2 != 0: data += b'\x00' # 数据长度为奇数时补空字节,保证按16位块处理 # 将数据拆分为16位无符号整数 words = struct.unpack('!%dH' % (len(data)//2), data) # 累加所有16位块 checksum = reduce(lambda a, b: a + b, words, 0) # 处理溢出:将高16位与低16位相加,直到无溢出 checksum = (checksum >> 16) + (checksum & 0xffff) checksum += checksum >> 16 # 返回按位取反的16位结果(网络字节序) return ~checksum & 0xffff def create_packet(code): # ICMP回显请求的类型固定为8,代码使用传入参数 icmp_type = 8 icmp_code = code checksum = 0 # 先填0,后续计算真实校验和 # 用进程ID低16位作为标识符,方便匹配对应响应 identifier = os.getpid() & 0xffff sequence = 1 # 序列号可自定义,这里固定为1 # 构造不带校验和的ICMP头部 header = struct.pack('!BBHHH', icmp_type, icmp_code, checksum, identifier, sequence) # 自定义数据部分,可根据需求修改 data = b'Hello from ICMP Request!' # 计算整个ICMP包(头部+数据)的校验和 checksum = calculate_checksum(header + data) # 重新构造带正确校验和的头部 header = struct.pack('!BBHHH', icmp_type, icmp_code, checksum, identifier, sequence) # 返回完整ICMP包 return header + data def step4(code, server, port): try: # 创建原始ICMP套接字,需要管理员/root权限 s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.getprotobyname('icmp')) except PermissionError: print("Error: 需要管理员权限才能创建原始套接字,请使用sudo运行此脚本") sys.exit(1) CHARMAP = nonprintable_to_dots() packet = create_packet(int(code)) print(f"发送ICMP回显请求到 {server}...") # ICMP无需端口,port填0即可 s.sendto(packet, (server, port)) while True: # 接收响应,缓冲区1600字节足够容纳IP+ICMP包 msg, addr = s.recvfrom(1600) # IP头部固定20字节(无选项),后续为ICMP包 ip_header = msg[:20] icmp_packet = msg[20:] # 解析IP头部获取源IP ip_src = socket.inet_ntoa(ip_header[12:16]) # 解析ICMP头部前8字节 icmp_type, icmp_code, _, identifier, sequence = struct.unpack('!BBHHH', icmp_packet[:8]) # 匹配我们发送的请求:ICMP类型为0(回显响应)且标识符一致 if icmp_type == 0 and identifier == (os.getpid() & 0xffff): print(f"\n收到来自 {ip_src} 的ICMP回显响应") # 处理响应数据,转换不可打印字符并解码 response_data = icmp_packet[8:].translate(CHARMAP).decode('ascii') print(f"响应数据内容: {response_data}") break s.close() # 测试调用示例:发送请求到谷歌公共DNS,code设为0 if __name__ == "__main__": step4(0, '8.8.8.8', 0)
关键细节说明:
nonprintable_to_dots():生成翻译表,避免响应数据中的不可打印字符出现乱码,方便查看。calculate_checksum():ICMP协议要求必须校验和,该函数严格遵循RFC标准,确保目标主机能正确识别请求。create_packet():构造标准ICMP回显请求包,包含头部和自定义数据部分。step4():补全接收与解析逻辑,通过进程ID匹配响应,确保只处理对应自身发送请求的回复。
内容的提问来源于stack exchange,提问作者thmasker
相关产品推荐
相关产品推荐

