news 2026/11/2 2:03:29

从零手搓ICMP探测:Python原始套接字、校验和与TTL逐跳实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零手搓ICMP探测:Python原始套接字、校验和与TTL逐跳实践

简介:面向网络编程初学者、网络管理员与协议分析爱好者的ICMP数据包构造实践资料,围绕ICMP报文结构、类型与代码字段含义展开,并说明将其封装进IP数据报的基本思路,帮助读者从代码层面区分差错报告报文与查询报文,理解回显请求、回显应答等典型场景的实现要点。压缩包为rar格式,共3个文件,以1个cpp源码、1个h头文件和1个txt说明文档为主,源码用于呈现报文构造过程,头文件承载结构声明,说明文档补充背景与使用提示,整体约5KB,体量轻便,适合作为协议学习与实验对照的小型示例。已有1609人学习,具备一定参考热度。读者可借此获得ICMP报文构造的代码框架、字段组织与结构定义参考,并延伸理解IP封装、报文校验及抓包比对等排错思路,适合边阅读源码边结合网络抓包工具核对报文细节,夯实对TCP/IP底层通信机制的认识。

1. ICMP 数据包构造:先别急着用现成库,手搓一遍才知道坑在哪

线上有台机器ping不通,但业务端口连得上,运维说“网络是好的”,开发说“我这边超时”。这种扯皮场景里,现成的 ping 工具只丢给你一句超时,什么信息都没有。这个 ICMP 数据包构造的小项目,就是把这层黑匣子拆开:用 Python 标准库自己拼一个 8 字节 ICMP 头、算反码校验和、走原始套接字发出去,再把回来的报文一层层剥开看。

它适合三类人:想搞明白 ping 和 traceroute 底层到底发了什么的人;要给自研监控加一条轻量探测链路的人;以及想练手struct二进制打包的老哥。整个工程不依赖第三方库,几十行核心代码就能跑,剩下的都是把这件事做得更稳的脚手架。

2. 报文结构与工程骨架:8 字节头决定了一切

ICMP 看着神秘,拆开就是一个定长头加一段载荷。Echo Request 的头部固定 8 字节,后面跟任意长度的 data,接收方原样回显。真正容易翻车的地方不在字段本身,而在字节序、校验和时机和标识符取值这三件事上。

2.1 ICMP Echo 的字段布局与 struct 格式串

先把字段摆清楚。Echo Request(type=8)和 Echo Reply(type=0)共用同一套布局:

偏移长度字段说明
01type8 请求,0 应答,11 超时
11codeEcho 场景固定 0
22checksum覆盖整个 ICMP 报文的反码和
42identifier用来匹配请求与应答,常见取进程号
62sequence序号,逐包递增
8Ndata载荷,可放时间戳或自定义内容

对应的格式串是!BBHHH。!表示网络字节序(大端),这很重要——不管你跑在 x86 还是 ARM 上,线上传输的永远是高位在前。B是无符号单字节,H是无符号双字节。很多新手第一次写会漏掉!,在本机自测时因为收发都在同一台机器上,字节序一致反而看不出问题,一跨机器就全乱。

工程里我把报文相关的纯函数收在一个模块里,不碰 socket,方便单元测试:

# probe/packet.py import struct ICMP_ECHO_REQUEST = 8 ICMP_ECHO_REPLY = 0 ICMP_TIME_EXCEEDED = 11 # 8 字节定长头,! 强制网络字节序 ICMP_HEADER_FMT = "!BBHHH" ICMP_HEADER_LEN = struct.calcsize(ICMP_HEADER_FMT) # 8 def build_echo_request(ident: int, seq: int, payload: bytes = b"") -> bytes: """构造一个 ICMP Echo Request,校验和先填 0 占位""" header = struct.pack( ICMP_HEADER_FMT, ICMP_ECHO_REQUEST, # type 0, # code 0, # checksum 占位 ident & 0xFFFF, # identifier,截断到 16 位 seq & 0xFFFF, # sequence ) return header + payload

这个函数只负责拼,不算校验和。为什么不在这里一步到位?因为校验和要覆盖“头 + 载荷”整体,如果先算头再拼载荷,算出来的值一定是错的。工程上更稳妥的做法是先拼完整包、再回填校验和,这个顺序在下一节展开。

ident & 0xFFFF这个截断也别省。os.getpid()在某些系统上返回值可能超过 65535,struct.pack遇到超范围的值会直接抛struct.error,报错信息还很含糊,排查起来浪费时间。

2.2 目录结构与模块职责划分

仓库不大,但分层是清晰的。报文构造、校验和、套接字收发、TTL 探测、命令行入口各自独立,单测只覆盖前两层,不需要 root 权限就能跑:

icmp-probe/ ├── probe/ │ ├── __init__.py │ ├── packet.py # 报文构造与解析,纯函数 │ ├── checksum.py # 反码求和校验和 │ ├── sender.py # raw socket 创建、发送、收包 │ ├── tracer.py # TTL 递增探测逻辑 │ └── cli.py # 命令行入口与参数解析 ├── tests/ │ └── test_packet.py # 不依赖网络的单元测试 ├── requirements.txt # 实际上为空,仅标准库 └── README.md

这么拆的理由很实际:packet.py和checksum.py是纯计算,可以在没有管理员权限的 CI 环境里跑;sender.py需要CAP_NET_RAW或者 root,本地开发时才用得上;tracer.py是策略层,负责“TTL 从 1 加到多少、每跳等多久”,跟底层报文无关。三层的边界一旦划清,后面加 IPv6 或者加自定义载荷,改动范围就很可控。

requirements.txt留空不是偷懒,是刻意的约束——原始套接字这部分,标准库的socket已经够用,引入第三方抓包库反而会把“报文长什么样”这层又遮回去。如果只是想快速验证,用现成库没问题;但要理解构造过程,从struct.pack起步更直接。

2.3 报文构造函数的逐参数说明

把build_echo_request的每个参数掰开说。ident的作用是让接收方知道这个回复属于哪个请求。同一台机器上可能同时跑着系统自带的 ping 和你的脚本,如果两边用了相同的 ident 和 seq,收包时就会互相抢对方的应答,出现“明明发出去了却收到一个莫名其妙的回复”这种玄学现象。常见做法是取os.getpid() & 0xFFFF,进程号天然区分不同实例。

seq是从 1 开始递增的计数器,作用是在连续发包时对齐请求与应答,同时用来算丢包率。这里有个细节:seq在 traceroute 场景下不只是序号,它还要和 TTL 绑定,因为每一跳可能返回多个应答(多路径),得靠 seq 反查这一跳发了几个包。

payload是载荷。系统自带的 ping 默认填一段递增字节,我们这里建议填发送时刻的时间戳:

# 载荷放 8 字节 double 时间戳,收到回包后反解算 RTT import struct, time payload = struct.pack("!d", time.time())

这么做的好处是不依赖本地记录的发送时间表。回包里的载荷原样带回,收到后struct.unpack("!d", payload)就能算出往返延迟,即使程序中间重启、或者多个探测并发,也不会串。代价是载荷从 8 字节起步,比默认的 56 字节小,但完全不影响 ICMP 语义。

构造完成后,报文长度 = 8 + len(payload)。这个长度在发送时不用显式告诉 socket,sendto会按 bytes 长度处理;但在解析回包时要用到,因为要判断“收到的数据够不够长、载荷在哪”。

3. 校验和与收发链路:从 checksum 到 recvfrom 的完整闭环

报文拼好了不等于能发出去。ICMP 的校验和是强制字段,算错一个字节,内核或者对端会直接丢弃,你在应用层看到的现象就是“发了没反应”,连个错误码都不给。这一章把校验和算法、原始套接字的创建参数、回包解析三段串起来。

3.1 反码求和校验和的实现与常见写法对比

ICMP 校验和用的是 16 位反码求和:把报文按 16 位切块累加,进位回卷到低位,最后取反。实现上有几个容易写错的点,我按踩过的顺序列:

# probe/checksum.py def checksum(data: bytes) -> int: """16 位反码求和,返回可直接填入头部的校验和""" # 奇数长度补一个 0 字节,保证按 16 位切 if len(data) % 2: data += b"\x00" total = 0 for i in range(0, len(data), 2): # 大端拼成 16 位字,与网络字节序一致 word = (data[i] << 8) | data[i + 1] total += word # 进位回卷:高 16 位加到低 16 位,可能要卷两次 while total >> 16: total = (total & 0xFFFF) + (total >> 16) return (~total) & 0xFFFF

第一个坑是奇数长度。ICMP 头固定 8 字节,载荷如果是奇数长度(比如你塞了个 5 字节字符串),整体就是奇数,不补零会越界或者漏算最后一字节。补的零只在计算时用,不发出去。

第二个坑是回卷次数。用while而不是一次if,是因为累加过程中可能产生多次进位。虽然实践中两次回卷就收敛,但写if在极端数据下会漏掉高位,校验和就错了。

第三个坑是取反后的掩码。Python 的~作用于整数会得到负数,& 0xFFFF把它拉回 16 位无符号。这一步漏了,struct.pack会直接报错,还算好排查;真正麻烦的是某些实现里用~total + 1写成了补码,跟反码和是两个东西,发出去的包对端一律不回。

算校验和的时机也要注意。正确顺序是先拼“头 + 载荷”的完整字节串,校验和字段填 0,整体算一遍,再把结果回填到偏移 2 的位置:

def finalize(packet: bytes) -> bytes: """packet 必须是 checksum 字段为 0 的完整报文""" csum = checksum(packet) # 头部 8 字节,checksum 位于偏移 2,长度 2 return packet[:2] + struct.pack("!H", csum) + packet[4:]

这里用切片替换而不是重新pack整个头,是为了避免把 type、code、ident、seq 再抄一遍出错。字节串不可变,切片拼接会生成新对象,开销可以忽略。

顺便说个对照:如果哪天你改用抓包库构造报文,校验和通常由库自动算。好处是省事,坏处是你不知道它算错了该怎么查。手写一遍之后,遇到“包发出去了但对端没反应”,第一反应就是抓包看校验和字段是不是 0 或者明显异常,这个直觉很值钱。

3.2 raw socket 创建与发包参数

原始套接字的创建有三个参数,每个都有讲究:

# probe/sender.py import socket, os, time from .packet import build_echo_request, ICMP_HEADER_FMT, ICMP_HEADER_LEN from .checksum import checksum def send_echo(host: str, seq: int, timeout: float = 2.0): ident = os.getpid() & 0xFFFF payload = struct.pack("!d", time.time()) # 1) 先拼完整包,校验和占位 0 raw = build_echo_request(ident, seq, payload) # 2) 整体算校验和并回填 csum = checksum(raw) packet = raw[:2] + struct.pack("!H", csum) + raw[4:] # AF_INET + SOCK_RAW + IPPROTO_ICMP 是固定组合 with socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) as s: s.settimeout(timeout) # 第三个参数是端口,ICMP 无端口概念,填 0 s.sendto(packet, (host, 0))

AF_INET指定 IPv4。SOCK_RAW表示我们要自己构造协议头,内核只负责加 IP 头。IPPROTO_ICMP告诉内核按 ICMP 协议注册,这样内核才知道把哪些回包投递到这个套接字。

sendto的地址是个二元组(host, 0),端口写 0 不是随便填的——ICMP 没有端口概念,这个位置在内核里被忽略,但 API 要求必须给。有人会填 1 或者 80,反正不生效,写 0 语义最清楚。

还有一个隐藏参数:IP_TTL。默认值通常是 64,ping 场景无所谓,traceroute 场景必须每发一个包改一次,下一章细说。修改方式是s.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, ttl),注意选项级别是IPPROTO_IP而不是IPPROTO_ICMP,写错了会抛OSError。

3.3 回包解析:剥掉 IP 头再拆 ICMP

recvfrom收上来的数据,在 Linux 上是带 IP 头的完整 IP 包,不是纯 ICMP。第一步得先算 IP 头长度:

def parse_reply(data: bytes): # IP 头第一个字节低 4 位是首部长度,单位是 4 字节 ihl = (data[0] & 0x0F) * 4 icmp = data[ihl:] if len(icmp) < ICMP_HEADER_LEN: return None itype, code, chksum, ident, seq = struct.unpack( ICMP_HEADER_FMT, icmp[:ICMP_HEADER_LEN] ) payload = icmp[ICMP_HEADER_LEN:] return {"type": itype, "code": code, "ident": ident, "seq": seq, "payload": payload}

(data[0] & 0x0F) * 4这行的意思是:IP 头首字节高 4 位是版本号(4),低 4 位是首部长度,单位是 32 位字,所以乘 4 得到字节数。最小值 20,带选项时可能到 60。写死 20 是最常见的错误,在本机自测时通常没选项、恰好 20 字节,看着能跑;一旦经过某些中间设备加了选项,解析就整体错位,读出来的 type 可能是任意值。

拿到 ICMP 部分后,用同一个格式串拆头,剩下的就是载荷。校验和字段这里一般不再校验,因为内核在投递给原始套接字之前通常已经验证过一遍;如果要自己验,把整个 ICMP 部分丢进checksum(),结果应该是 0。

过滤环节不能省。原始套接字会收到本机所有 ICMP 流量,包括系统 ping、其他进程的探测、甚至路由器的不可达通知。判断标准是type == 0 and ident == 我的 ident and seq == 我发的 seq,三条件同时满足才算自己的回包。只看 type 不看 ident,多进程并发时必然串包。

4. 从 ping 到 traceroute:用 TTL 把路径一段段逼出来

ping 是一条直线:发一个、收一个、算时间。traceroute 是沿着路径逐跳试探,靠的是 IP 头里那个 TTL 字段。同一个套接字、同一套报文构造逻辑,只是把 TTL 从 1 开始递增,就能把中间经过的每一跳逼出来。

4.1 TTL 与 setsockopt 的配合

TTL 的机制很朴素:每经过一个路由器减 1,减到 0 时路由器丢弃这个包,并回一个 ICMP 超时报文(type=11,code=0)。所以把 TTL 设成 1,第一个路由器就会回超时,它的地址就是第一跳;设成 2,第二个路由器回,以此类推。

代码上就是在发送前改套接字选项:

# probe/tracer.py import socket, time from .packet import build_echo_request, ICMP_HEADER_FMT, ICMP_HEADER_LEN from .checksum import checksum def probe_hop(host: str, ttl: int, seq: int, timeout: float = 2.0): ident = 0x1234 # traceroute 场景下可用固定值 payload = struct.pack("!d", time.time()) raw = build_echo_request(ident, seq, payload) csum = checksum(raw) packet = raw[:2] + struct.pack("!H", csum) + raw[4:] with socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) as s: s.settimeout(timeout) # 关键:每跳改一次 TTL,必须在 sendto 之前 s.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, ttl) start = time.perf_counter() s.sendto(packet, (host, 0)) try: while True: data, addr = s.recvfrom(1500) ihl = (data[0] & 0x0F) * 4 icmp = data[ihl:] itype, code = icmp[0], icmp[1] rtt = (time.perf_counter() - start) * 1000 if itype == 11: # 超时报文:拿到的是中间路由器的地址 return {"hop": ttl, "addr": addr[0], "rtt": rtt, "done": False} if itype == 0 and icmp[4:6] == struct.pack("!H", ident): # Echo Reply:到达目标 return {"hop": ttl, "addr": addr[0], "rtt": rtt, "done": True} except socket.timeout: return {"hop": ttl, "addr": None, "rtt": None, "done": False}

setsockopt必须在sendto之前调用,这个顺序不能反。套接字选项影响的是后续发送的包,改晚了就是对已经发出去的包做无用功,现象是“明明设了 TTL 却一直收到目标主机的回复”,第一次遇到会怀疑人生。

timeout设 2 秒是经验值。跨运营商链路偶尔会有几百毫秒的抖动,设 0.5 秒会产生大量假丢包;设 5 秒则遇到不通的跳时整轮探测被拖得很长。2 秒在大多数场景下够用,真要优化可以按跳自适应。

4.2 超时报文 type=11 的识别与 RTT 统计

回包处理里最关键的一个分支是itype == 11。超时报文的载荷里嵌着原始 IP 头和原始 ICMP 头的前 8 字节,理论上可以从中反解出我们当初发的 ident 和 seq,用来精确匹配是哪个探测触发的超时。但实现上有个现实问题:不同系统回填的原始报文长度不一致,有的只带 8 字节,解析逻辑要写得非常防御。

更省事的做法是靠 TTL 本身来区分——每个 TTL 值一次只发一个包,收到任何 type=11 就归到当前这一跳。代价是并发度低,探测一轮要按跳串行。如果要提速,就得改成“一个 TTL 发多个包 + 从载荷里反解 seq”,复杂度直接上一个台阶。

RTT 的计算点也要注意。start是在sendto之前打的,包含了内核处理时间,通常几十微秒,可以忽略。但如果你的程序里还在循环内做日志落盘、DNS 反解这类慢操作,就一定得把start打在紧贴sendto的位置,不然测出来的是“业务耗时”而不是“网络耗时”。

统计层面,每跳返回一个结构体,最后汇总成表。丢包的跳(addr is None)通常显示成*,连续多跳都超时一般说明中间某段做了 ICMP 限速或者直接丢弃,这时候继续加大 TTL 也没用,得换个探测方式。

4.3 批量探测的循环控制与超时处理

把单跳函数串成完整 traceroute 的主循环,有几个控制点:

def trace(host: str, max_hops: int = 30): seq = 1 for ttl in range(1, max_hops + 1): result = probe_hop(host, ttl, seq) seq += 1 if result["addr"] is None: print(f"{ttl:>2} * 请求超时") continue print(f"{ttl:>2} {result['addr']:<15} {result['rtt']:.2f} ms") if result["done"]: print("已到达目标") break

max_hops默认 30 是常见上限,够覆盖绝大多数公网路径。不设上限的风险是遇到路由环路时无限打下去,TTL 一直加,包一直发,脚本挂在那里不动。

seq在主循环里递增而不是每跳重置,是为了在日志和抓包里能唯一标识每一个发出的包。如果按跳重置,出问题时你会看到一堆 seq=1 的包,根本分不清是第几跳发的。

continue那行的逻辑值得单独说。某一跳超时不代表结束,可能只是那台路由器不响应 ICMP,继续加 TTL 后面的跳往往还能通。真正要终止的条件只有两个:收到 Echo Reply(到达目标),或者 TTL 到上限。把“超时”当终止条件是最容易犯的逻辑错误,会让 traceroute 在第一台不回应的小路由器那儿就断掉,看起来像是“网络只有一跳”。

5. 避坑与排查:那些在 root 权限下也发不出去的包

这套代码逻辑本身不长,但真正跑通往往要跟环境搏斗几轮。下面几条是实际调试时最常撞上的,按“现象 → 原因 → 解决”记录。

5.1 现象:PermissionError,提示不允许的操作

原因:创建SOCK_RAW套接字需要CAP_NET_RAW能力,普通用户直接调用会被拒绝。Linux 下报PermissionError: [Errno 1] Operation not permitted,Windows 下则是OSError: [WinError 10013]。

解决:Linux 用sudo跑,或者给 Python 解释器单独赋能力。Windows 需要用管理员权限打开终端。注意别在容器里想当然——容器默认的能力集通常包含NET_RAW,但有些加固过的运行时会把它去掉,症状和宿主机权限不足一模一样,得进容器capsh --print看一眼才确认。

5.2 现象:能发出去,但永远收不到回包,超时

原因:最常见的是校验和算错,包被对端静默丢弃;其次是防火墙或者安全组把 ICMP 入站挡了。两者现象完全一致,都是“发了没反应”。

解决:先在本机ping 127.0.0.1验证构造逻辑,本机回环不经过防火墙,能收到说明校验和没问题;收不到就回头查checksum()。本机通了、外网不通,基本就是链路或者策略问题,换目标、抓包确认。

5.3 现象:解析出来的 type 是乱值,或者程序直接下标越界

原因:把 IP 头长度写死成 20 字节。部分网络路径会给 IP 头加选项,实际长度 24 或 28,此时data[20:]切出来的不是 ICMP 头。

解决:统一用(data[0] & 0x0F) * 4动态计算,并且在切之前判断len(data) >= ihl。解析函数里加长度防御,比事后抓包猜要省事得多。

5.4 现象:Windows 上收到回包解析全乱,Linux 上却正常

原因:两个平台对原始套接字的语义不一样。Linux 上IPPROTO_ICMP收上来的是完整 IP 包,带 IP 头;Windows 上收上来的通常直接从 ICMP 头开始,没有 IP 头。同一段data[ihl:]在 Windows 上会把 ICMP 头切掉 20 字节。

解决:在parse_reply里加平台判断,或者更稳的办法是看数据特征——判断data[9] == 1(IP 头里的协议字段是 ICMP)来决定要不要剥 IP 头。别用sys.platform硬编码,同一平台不同版本的行为也可能有差异。

5.5 现象:多进程同时跑,收到的回包对不上号

原因:ident 冲突。两个进程如果都用了同一个固定 ident(比如都写死 0x1234),内核投递回包时不会区分,谁先recvfrom谁拿到。

解决:ident 至少取os.getpid() & 0xFFFF,再进一步可以叠加随机数。判断回包归属时同时比对 ident 和 seq,只比一个都不够。

6. 二次开发:把探测能力塞进自己的监控里

这套代码跑通之后,最自然的改造方向是把它从“命令行小工具”变成“监控系统里的一个探测器”。我的做法是保留packet.py和checksum.py不动,把sender.py外面包一层采集器,输出结构化的探测结果,供上层消费。

第一个改造点是并发模型。现在的实现是一个套接字、串行发收,探测 30 个目标要等 30 轮超时,最坏情况能拖到分钟级。改成每个目标一个线程、每个线程独立套接字,互不干扰,总耗时降到单轮超时。线程数按目标数量控制,超过 50 个就上线程池限流,避免把本机的文件描述符耗光。

第二个改造点是载荷设计。现在塞的是 8 字节时间戳,够算 RTT。如果要区分“哪个业务线发起的探测”,可以把载荷扩成时间戳 + 4 字节业务 ID,回包解析时一并取出来。这里要注意载荷长度会改变报文总长,校验和必须重算,别拿旧的缓存值直接用。

第三个改造点是结果落地。探测函数只返回字典,落库、打点、告警都放在外层。这样单测好写,也方便替换存储。表结构大概长这样:

字段类型说明
targetstring探测目标地址
tsint64探测发起时间戳,毫秒
rtt_msfloat往返延迟,超时为 NULL
lostbool本次是否丢包
src_ipstring回包来源地址,用于发现路径漂移

验证改造有没有跑偏,我一般走三步。第一步,ping 127.0.0.1和ping 网关各跑一轮,确认基础链路和校验和没问题。第二步,对着一个已知会丢 ICMP 的目标跑,确认程序是把结果标记成丢包而不是抛异常退出。第三步,抓包比对——用系统自带的抓包工具看发出的 type、code、checksum、ident、seq 五个字段,跟你代码里算的是否一致,这一步能一次性排掉绝大多数“逻辑对但结果不对”的问题。

还有一个容易被忽略的点:长期运行的探测器一定要有资源回收。原始套接字用完必须close(),用with语句最省心。我曾经写过一个每分钟探测一次的常驻脚本,忘了关套接字,跑了两天之后进程里堆了几千个句柄,最后socket()直接抛Too many open files,日志里还看不出任何异常,排查了大半天才发现是个with漏写。从那以后我每次写这种循环发包的代码,都强制先在本地跑 200 轮压测,看句柄数是否稳定,再往线上放。希望这些经验能帮到你,少走几段弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 2:03:23

Linux实战100例:命令排障、systemd与cron服务化

简介&#xff1a;《linux实战100例》面向Linux初学者、初中级运维与开发人员&#xff0c;聚焦日常命令使用、服务配置和故障排查&#xff0c;帮助读者通过典型案例建立从操作到定位问题的完整思路。包内共204个文件&#xff0c;以195个HTML技术文档为主体&#xff0c;辅以PDF、…

作者头像 李华
网站建设 2026/11/2 2:01:20

C++实现单链表按k值重新排序的方法

前言 「按 k 值重新排序」这个说法本身有歧义。常见的有三种理解&#xff1a;一是以 k 为分界&#xff0c;把小于 k 的节点排在前面、其余排在后面&#xff0c;保持各自相对顺序&#xff1b;二是把链表旋转 k 位&#xff1b;三是每 k 个节点为一组做反转。「按 k 值」强调的是节…

作者头像 李华
网站建设 2026/11/2 2:01:05

PLC花式喷水池控制课设:四花样时序与梯形图实现

简介&#xff1a;这份资源是面向自动化、电气控制相关专业学生及PLC初学者的课程设计参考资料&#xff0c;围绕基于PLC的花式喷水池控制系统展开&#xff0c;帮助读者理解从硬件配置到程序调试的完整设计思路。压缩包内共1个doc文档&#xff0c;约956KB&#xff0c;以文字报告形…

作者头像 李华
网站建设 2026/11/2 2:00:49

C++实现二叉树及堆的示例代码

前言 二叉树和堆经常被放在同一章讲&#xff0c;因为它们都呈现「一个节点最多两个孩子」的形状&#xff0c;但两者的约束完全不同。二叉树只要求每个节点至多两个子节点&#xff0c;节点之间没有大小关系&#xff1b;堆在此之上还要求「父节点不大于&#xff08;或不小于&…

作者头像 李华
网站建设 2026/11/2 2:00:49

动作标注不够时,物理AI 基座模型 从视频学潜动作

【具身AGI导读】动作标注不够的时候&#xff0c;要换的不是数据的来源&#xff0c;而是监督信号的形式。一条动作路线值不值得跟&#xff0c;得看它留下的表示往哪儿用&#xff0c;以及机器人执行那一端还差几步。真机遥操作能给出精确的动作&#xff0c;代价是贵、慢&#xff…

作者头像 李华
网站建设 2026/11/2 2:00:49

一人公司(OPC)风险地图:有限责任何时失效、财务边界怎么守、个人兜底底线在哪(2026)——系列文章之五

【摘要】有限责任是持续举证的特权&#xff0c;不是注册发放的证书。 287件涉一人公司债权纠纷判决中股东自证清白成功0件&#xff0c;已生效人格否认判决里财产混同因素占比超60%。拆解有限责任失效的三类触发路径&#xff0c;给出财务分离动作清单与个人兜底框架&#xff0c;…

作者头像 李华