排查网络故障时你通常怎么知道某个网段内有哪些设备在线翻 DHCP 租约、问运维同事、拿手机挨个 ping如果手头只有一台普通电脑最朴素的办法是在命令行里输入ping 192.168.1.1再手动改成192.168.1.2不断点下一条……几百个地址扫下来不仅费时间结果还全凭肉眼判断很难变成一份可靠的数据。这个痛点非常常见。无论是网络管理员收紧防火墙后想验证端口连通性还是开发同学在测试环境排查“服务为什么联不上”你都需要一个快速、可重复、可输出报告的设备发现手段。nmap 当然能做但很多场景下你不想开重型工具也不想在业务脚本里依赖外部命令。这时候用 Python 写一个简易 IP 扫描工具既能吃透底层原理又能按自己的业务逻辑定制结果输出。我的判断是一个“能用”的 IP 扫描器并不复杂Python 标准库就能完成。核心难点不在于代码量而在于你如何选择探测协议、如何设置并发和超时、如何避免把“探测失败”误读成“主机离线”。如果你之前只会用集成工具、没仔细想过底层机制这篇文章正好帮你补齐。全文会从三种主流扫描方式讲起再给出一套完整可运行的脚本覆盖网段解析、并发探测、结果输出和常见排错。整份代码基于 Python 标准库不需要安装第三方包复制到本地即可运行。开篇先强调一件事请确认你拥有所扫描网段的管理权限或明确授权。本文讨论的是网络运维和排障场景不是也不应该被用来做未授权探测。1. 这篇文章真正要解决的问题1.1 谁需要这个工具第一个典型场景是网络资产梳理。公司或家庭网络中路由器、打印机、摄像头、开发板、NAS 各自占了一个 IP时间久了没人记得清楚。想要快速列出当前网段有哪些在线设备IP 扫描器是最直接的答案。第二个场景是端口连通性验证。防火墙策略调整、安全组规则修改、服务端口更换之后你需要确认“外网到这台服务器 443 端口是否已经放通”“内网 22 端口能否正常访问”。对整个网段跑一遍端口探测比一台台连上去看要高效得多。基于 Python 实现最大的优势是可以直接嵌入到已有的自动化脚本或监控系统里。第三个场景是故障排查辅助。服务器不响应了第一件事不是登录机房而是确认它是否还活着、关键端口是否还在监听。一个轻量扫描器可以在几秒内给出网段级视图帮助缩小问题范围。1.2 为什么不直接用 nmapnmap 确实非常强大几乎可以做你想做的所有探测。但“强大”本身也意味着学习成本和不必要的信息噪音。在只想知道“哪些主机在线、哪些端口开放”时nmap 的输出需要花时间过滤它的 OS 指纹、脚本引擎和服务版本探测虽然有用但并不是每个场景都必要。更现实的问题是不是每台生产服务器都允许安装 nmap。很多公司对安全工具有严格的白名单管理但你自己的 Python 脚本反而更容易审批和移植。把扫描逻辑写进业务代码里也远比每次调用外部命令、再解析文本输出更可靠。1.3 简易扫描器的定位这篇文章实现的工具定位是“轻量、可定制、可嵌入业务脚本”。它不做漏洞识别、不做操作系统指纹、不做隐蔽探测只做三件事输入网段、并发探测、输出结果。它和 nmap 不是替代关系而是互补关系。你要是需要深度安全分析请直接上 nmap你要是需要内网设备巡检、端口连通性回归、定时上报资产在线情况用这个简易工具更顺手。2. IP 扫描的底层原理与主流实现方式在写代码之前先弄清楚一台主机“在线”在协议层面意味着什么。不同的探测方式结论的可靠性完全不同。2.1 ICMP Ping 探测ICMP 扫描是最常用的连通性检查方式。它的原理是发送 ICMP Echo Request 报文如果目标主机存活且不禁用 ICMP 协议它会回一个 Echo Reply。调用系统ping命令是最简单的实现方式跨平台时要注意参数差异Windows 用-n 1 -w 1Linux 和 macOS 用-c 1 -W 1。ICMP 扫描的局限也很明显。很多服务器出于安全考虑会丢弃 ICMP防火墙也常常默认不放行 ping。主机明明在线但 ping 不通的情况在公网和严格隔离的内网里很常见。因此ping 的返回值只能作为参考不能作为“主机离线”的最终依据。在 Python 里调subprocess执行系统 ping 命令可以快速验证一个小网段但它不适合作为唯一探测手段。2.2 TCP Connect 扫描TCP Connect 扫描利用的是 TCP 三次握手。客户端向目标 IP 的某个端口发起连接请求如果端口正在监听三次握手就会完成connect_ex()返回 0如果端口关闭连接会被拒绝。由于它走的是完整握手流程普通用户权限即可运行不需要构造原始报文所以这是标准库最容易实现、也是结果最直观的方案。它的代价是会在目标主机上留下连接日志并且依赖目标至少开放一个可被探测到的端口。如果一台主机把所有端口都关闭了TCP 扫描会把它的状态判断成“离线”虽然它实际上是活的。这是理解扫描结果时必须注意的“假阴性”问题。2.3 ARP 扫描ARP 扫描只工作在同一个二层广播域内。原理是广播 ARP 请求“谁有这个 IP 地址”对应主机收到后会回一个 ARP 应答告诉你它的 MAC 地址。这种方式速度快、准确率高因为几乎所有主机都不会主动屏蔽 ARP 协议。但它的适用范围很窄跨网段扫描无效需要scapy或系统命令arping配合而且普通 Python 标准库难以直接构造 ARP 报文。三种方式对比如下扫描方式协议适用网络速度准确度主要限制ICMP PingICMP任意可达网络快中主机禁用 ICMP 时严重误判TCP ConnectTCP任意可达网络中高依赖目标开放端口会留下连接日志ARP 扫描ARP同一局域网二层极快高只能发现本网段需要第三方库从工程角度看一个简易扫描器应当以 TCP Connect 为主要探测方式ICMP 作为辅助。后面给出的完整脚本也遵循这个设计只要目标的任何一个常见端口可以建立 TCP 连接就认为它在线。3. 环境准备与前置条件3.1 Python 版本与依赖建议使用 Python 3.8 或更高版本。脚本中会用到的模块包括socket、concurrent.futures、ipaddress、argparse、json、subprocess、platform、time全部属于标准库不需要执行pip install。从代码简洁和部署成本看这是 Python 相对其他语言做扫描工具的一大优势。3.2 运行平台差异脚本在 Windows、Linux、macOS 上都可以运行。主扫描逻辑不依赖系统命令只调用 socket 接口因此不存在跨平台兼容问题。唯一可能涉及平台差异的是 ICMP 辅助函数需要根据platform.system()的结果构造不同的ping参数代码中已经做了处理。3.3 权限边界普通用户运行 TCP Connect 扫描不需要管理员权限因为socket建立连接是常规网络操作。但如果后续扩展成 ICMP 原始套接字或 ARP 扫描在部分系统上需要 root 权限。无论权限如何扫描的目标网段必须是当前项目或个人有权管理的范围否则扫描行为本身就可能构成未授权访问这是底线问题。3.4 建议的测试环境推荐先在虚拟机构建的隔离测试网段或者自己家路由器下方的局域网里运行。以192.168.1.0/24这类私有网段做练习既能验证脚本逻辑又不会影响他人。正式使用前再向网络负责人确认扫描范围和频率。4. 核心流程拆解一个完整的扫描脚本可以拆成四个阶段解析网段、并发探测、收集结果、输出报告。下面逐个说明设计思路。4.1 解析目标网段并生成候选 IP用户输入的一般是 CIDR 格式比如192.168.1.0/24。用ipaddress.ip_network(args.network, strictFalse)可以解析任意合法网段。这里的strictFalse有一个实用价值当用户输入192.168.1.1/24时脚本不会报错而是自动按192.168.1.0/24处理。network.hosts()会返回该网段内所有可用主机地址自动剔除网络地址和广播地址。对/24网段这个序列长度是 254恰好可以交给线程池并发处理。如果你使用/31或/32等特殊掩码hosts()的行为在不同 Python 版本中略有差异但日常使用不受影响。4.2 探测主机在线状态探测策略已经明确优先使用 TCP Connect 方式扫描一组预设的常用端口。只要其中任意一个端口连接成功就认为该 IP 在线否则标记为离线。这种策略比单纯 ping 可靠因为很多只开特定端口的主机依然能被发现。端口列表需要结合业务场景选择。内网服务器常见开放端口包括 SSH22、远程桌面3389、文件共享445/139、数据库3306/5432公网入口通常关心 HTTP80和 HTTPS443。默认值不能覆盖所有情况所以脚本要把端口列表设计成可配置参数。4.3 并发执行与超时控制ThreadPoolExecutor是这里最合适的并发模型。socket 连接是典型的 IO 密集型操作会等待网络超时。线程在等待响应时不会长时间占用 CPU因此线程数可以设置得比较大。推荐范围是 50 到 200。如果线程数过大反而可能耗尽本机文件描述符或者短时间在目标网络上制造大量连接请求引起误报和网络拥塞。超时时间同样关键。同网段扫描一般设置为0.3到0.5秒跨网段或公网扫描建议放宽到1秒左右。超时设太短弱网环境下容易漏报设太长整个扫描耗时线性上升。4.4 结果收集与结构化输出线程池的任务提交后用as_completed()异步获取结果并按 IP 排序后输出。控制台打印用于人工查看同时支持将结果写入 JSON 或文本文件方便后续接入资产系统或监控平台。脚本设计上应该把“扫描结果”和“结果展示”分开数据存成结构化 JSON 是工程化的基本要求。5. 完整示例代码实现下面分三段给出代码ICMP 辅助函数、TCP 端口探测函数、完整的主扫描脚本。前两个可以单独复用最后一个是完整工具。5.1 ICMP Ping 辅助函数# 文件名ping_utils.py import subprocess import platform def ping_host(ip: str, count: int 1) - bool: 调用系统 ping 命令检测目标 IP 是否响应 ICMP 请求。 注意目标禁 ping 时返回 False不代表主机离线。 system platform.system().lower() if system windows: cmd [ping, -n, str(count), -w, 1000, ip] else: cmd [ping, -c, str(count), -W, 1, ip] ret subprocess.run( cmd, stdoutsubprocess.DEVNULL, stderrsubprocess.DEVNULL, checkFalse, ) return ret.returncode 0这段代码的核心是根据操作系统拼接不同的 ping 参数。subprocess.DEVNULL的作用是丢弃 ping 命令的输出因为我们只关心返回码。需要强调的是ping_host返回 False 并不等于主机不在线也可能是目标禁 ping 或防火墙丢弃了 ICMP。因此它在完整脚本里是辅助验证而不是主要判断依据。5.2 TCP 端口探测函数# 文件名tcp_utils.py import socket def check_tcp_port(ip: str, port: int, timeout: float 0.5) - bool: 检测目标 IP 的指定 TCP 端口是否可连接。 连接成功返回 True否则返回 False。 sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) try: return sock.connect_ex((ip, port)) 0 except socket.error: return False finally: sock.close()connect_ex()是这里最关键的 API。普通connect()在连接失败时会抛出异常connect_ex()则把错误码作为返回值返回0 表示成功。这样就不需要写 try/except 处理每一种连接错误代码会更干净。每个 socket 都必须设置超时时间否则遇到不响应端口时整个线程会一直卡住。5.3 完整主扫描脚本# 文件名simple_ip_scanner.py 简易 IP 扫描工具。 用法示例 python simple_ip_scanner.py 192.168.1.0/24 python simple_ip_scanner.py 192.168.1.0/24 -p 80,443,22,3389 python simple_ip_scanner.py 192.168.1.0/24 -t 50 -o result.json import argparse import ipaddress import json import socket import subprocess import platform import sys import time from concurrent.futures import ThreadPoolExecutor, as_completed def parse_args(): parser argparse.ArgumentParser(description简易 IP 扫描工具) parser.add_argument(network, help目标网段支持 CIDR例如 192.168.1.0/24) parser.add_argument( -p, --ports, default80,443,22,3389,445,139,53, help需要探测的端口列表逗号分隔支持范围例如 80,443,22-25, ) parser.add_argument( -t, --threads, typeint, default100, help并发线程数默认 100, ) parser.add_argument( -o, --output, defaultNone, help结果输出文件支持 .json 或 .txt, ) parser.add_argument( --timeout, typefloat, default0.5, help单次连接超时时间秒默认 0.5, ) return parser.parse_args() def parse_ports(port_str: str) - list: 解析端口参数支持逗号和短横线范围。 ports set() for item in port_str.split(,): item item.strip() if not item: continue if - in item: start, end item.split(-, 1) if int(start) int(end): continue for port in range(int(start), int(end) 1): ports.add(port) else: ports.add(int(item)) return sorted(ports) def ping_host(ip: str) - bool: 辅助调用系统 ping仅作为在线状态参考。 system platform.system().lower() if system windows: cmd [ping, -n, 1, -w, 1000, ip] else: cmd [ping, -c, 1, -W, 1, ip] ret subprocess.run( cmd, stdoutsubprocess.DEVNULL, stderrsubprocess.DEVNULL, checkFalse, ) return ret.returncode 0 def check_tcp_port(ip: str, port: int, timeout: float) - bool: TCP Connect 探测指定端口。 sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) try: return sock.connect_ex((ip, port)) 0 except socket.error: return False finally: sock.close() def scan_one_ip(ip: str, ports: list, timeout: float) - dict: 扫描单个 IP逐一探测端口列表记录所有开放端口。 open_ports [] for port in ports: if check_tcp_port(ip, port, timeout): open_ports.append(port) if open_ports: return {ip: ip, status: online, open_ports: open_ports} return {ip: ip, status: offline, open_ports: []} def main(): args parse_args() try: network ipaddress.ip_network(args.network, strictFalse) except ValueError: print(f网段格式错误{args.network}请使用如 192.168.1.0/24 的格式) sys.exit(1) ports parse_ports(args.ports) if not ports: print(请至少指定一个需要探测的端口) sys.exit(1) host_list [str(host) for host in network.hosts()] print(f开始扫描 {network}共 {len(host_list)} 个 IP) print(f探测端口{ports}) print(f并发线程数{args.threads}连接超时{args.timeout}s) start_time time.time() results [] with ThreadPoolExecutor(max_workersargs.threads) as executor: future_map { executor.submit(scan_one_ip, ip, ports, args.timeout): ip for ip in host_list } for future in as_completed(future_map): ip future_map[future] try: result future.result() except Exception as exc: result {ip: ip, status: error, error: str(exc)} results.append(result) elapsed time.time() - start_time results.sort(keylambda x: ipaddress.ip_address(x[ip])) online_list [r for r in results if r[status] online] offline_list [r for r in results if r[status] offline] error_list [r for r in results if r[status] error] print(f扫描完成耗时 {elapsed:.2f} 秒) print(f主机总数{len(results)}在线{len(online_list)} f离线{len(offline_list)}异常{len(error_list)}) for r in online_list: print(f {r[ip]} - 在线开放端口{r[open_ports]}) if args.output: with open(args.output, w, encodingutf-8) as f: if args.output.endswith(.json): json.dump(results, f, ensure_asciiFalse, indent2) else: for r in results: f.write( f{r[ip]} {r[status]} f{ .join(map(str, r[open_ports]))}\n ) print(f结果已保存到 {args.output}) if __name__ __main__: main()5.4 脚本关键逻辑讲解参数解析部分让脚本具备了基本 CLI 工具形态。network是必填位置参数-p、-t、-o、--timeout是可选参数。端口解析函数支持80,443,22-25这样的写法实际项目中扫描连续小范围端口时很有用。scan_one_ip的逻辑是遍历端口列表并记录所有开放端口。根据结果决定主机状态有任意端口开放则在线否则离线。这里没有提前break所以能记录一个 IP 上所有被探测到的开放端口信息量更大代价是扫描所有端口的时间会稍长。线程池的构建方式是一个字典future_map用future作为键、ip作为值。这样在处理结果时能知道当前结果属于哪个 IP。as_completed()会按完成顺序返回 future不保证原始 IP 顺序所以最后用ipaddress.ip_address(x[ip])做一次排序保证输出结果稳定。结果排序这一步容易被新手忽略。多线程扫描的返回顺序是乱的如果不排序同一网段连续两次扫描的输出顺序可能不同后续人工对比会很痛苦。6. 运行结果与效果验证6.1 运行命令把脚本保存为simple_ip_scanner.py在命令行进入文件所在目录执行下面这条命令python simple_ip_scanner.py 192.168.1.0/24 -p 80,443,22,3389 -t 100 -o result.json这条命令的含义是扫描192.168.1.0/24网段探测80、443、22、3389四个端口使用 100 个并发线程结果写入result.json。6.2 预期输出由于实际网络环境差异很大下面给的是典型输出格式具体数值会随环境变化开始扫描 192.168.1.0/24共 254 个 IP 探测端口[22, 80, 443, 3389] 并发线程数100连接超时0.5s 扫描完成耗时 4.83 秒 主机总数254在线12离线242异常0 192.168.1.1 - 在线开放端口[80, 443] 192.168.1.101 - 在线开放端口[22, 3389]你只需要重点看两件事第一在线主机数量是否合理第二在线主机的开放端口是否和已知业务一致。如果在线数量为 0基本可以断定是网络路由、防火墙或超时设置问题需要按第 7 节排查。6.3 验证方法先用一个明确已知在线的 IP 验证函数层逻辑。比如你的路由器通常是192.168.1.1单独跑一个端口探测from tcp_utils import check_tcp_port print(check_tcp_port(192.168.1.1, 80, timeout1))如果能返回True说明网络路径和 socket 逻辑没问题。然后再对整个网段扫描。最后可以用nmap -sn 192.168.1.0/24或其他网管工具做交叉核对看扫描出的在线主机列表是否一致。多次扫描结果稳定的网段才是可信的资产清单。6.4 失败时的第一排查方向如果整个网段全部离线先去 ping 网关地址确认本机和目标网段之间的路由通不通。路由通但扫描无结果再检查目标主机防火墙是否放行指定端口、超时时间是否过短。不要一上来就怀疑 Python 代码有 bug端口探测类工具“扫不到”绝大多数是网络策略问题。7. 常见问题与排查思路问题现象可能原因排查方式解决方案扫描结果全部离线目标网段不可达防火墙拦截或网段不存在ping网关地址并检查路由表确认网段归属调整防火墙规则后再试扫描速度过慢超时时间过长线程数太少统计单次扫描耗时将--timeout调到 0.3-0.5 秒适当提高--threads在线主机数量偏少端口列表未覆盖目标开放端口查看已知主机实际监听端口用-p参数补充端口列表结果抖动前后两次不一致网络丢包服务响应不稳定对同一网段连续扫描两次对比增加连接重试或适当提高超时时间并发过高导致误报线程数过多引发文件描述符耗尽或网络拥塞观察本机连接数将线程数控制在 50-200 之间程序提示端口格式错误端口参数包含非法字母或范围颠倒检查命令行参数改用80,443或22-100的格式ICMP 辅助函数在 Linux 上无响应系统未安装 ping 命令或权限不足直接在终端执行ping -c 1 127.0.0.1安装 iputils-ping或改用主 TCP 扫描逻辑这里补充说明“结果抖动”的处理建议。网络扫描是一次采样丢包会造成假离线。工程上处理方式是扫描两轮取“至少有一轮在线”的主机或者直接对特定主机做三次重试。简易脚本里没有内置重试逻辑但你可以把check_tcp_port包装成带重试的函数重试间隔不宜过短1 秒左右即可。还有一个非常典型的误判把防火墙丢包当成端口关闭。很多防火墙会对未开放端口直接发送 RST 或静默丢弃connect_ex()返回非 0。这种情况下脚本报告“端口关闭”是正确的但它不代表主机离线。因此端口关闭不等于主机离线离线结论必须基于所有端口都连接失败。8. 最佳实践与工程建议8.1 扫描策略要从小到大第一次在新网段运行先扫一个/30或/28小网段。比如192.168.1.0/30只有 2 个可用主机你可以在很短时间内验证脚本行为和预期一致。跑通小网段后再扩展到/24。直接扫大网段时一旦所有人都显示离线很难判断是脚本问题还是网络问题。8.2 端口列表按业务场景定制内网资产盘点建议使用22,3389,445,139,53这些都是主机管理和文件服务高频端口。公网入口探测则关注80,443。办公网里的打印机、摄像头和 IoT 设备可能只开放自定义端口如果这类设备在巡检范围内先把它们的端口采集出来加进参数。8.3 并发和超时不是越大越好100 个线程、0.5 秒超时对/24网段是兼顾速度和准确度的经验值。并发调到 500 以上时本机 socket 资源会迅速耗尽扫描触发的网络包也可能被上游防火墙限速。判断方法很简单如果扫描报告的在线主机数明显低于真实值先降低并发数而不是提高。8.4 输出结果必须结构化控制台打印适合人看但不适合程序消费。建议日常巡检时固定使用-o result.json生成的 JSON 数组里每条记录包含ip、status、open_ports。你可以在后续任务里用jq命令或 Python 脚本处理这些文件生成在线率报表甚至导入 CMDB 资产系统。8.5 扫描结果入库前要经过判定不要把一次扫描结果直接当作最终资产清单。建议连续扫描两到三次取多次结果中都在线的主机作为稳定设备取出现过的主机作为待观察设备。网络设备、电脑休眠、移动设备断开 WiFi都会造成在线状态波动一次扫描只能代表某一时刻的快照。8.6 安全边界必须清晰这一点不需要讲太复杂。扫描工具的使用边界等同于你对目标网络的管理边界。个人实验用虚拟机隔离网段工作场景向网络负责人申请并说明扫描范围和频率。脚本本身没有任何隐蔽能力所有 TCP 连接都会在目标主机上留下记录这一点在使用前就应该知道。8.7 后续扩展方向扫描端口成功后可以用 socket 接收 banner 信息做简单的服务识别。例如连接 SSH 端口后服务端通常会返回版本字符串这能帮助区分“端口开放”和“业务正常”。更完整的扩展可以做成定时任务结合发信服务或企业 IM 机器人在设备掉线时自动告警。9. 总结与后续学习方向一个简易 IP 扫描器麻雀虽小五脏俱全。通过这个项目你至少能收获四项能力理解 ICMP、TCP、ARP 三种探测方式各自的适用边界掌握ipaddress模块对网段的解析方式学会用ThreadPoolExecutor做 IO 密集型并发任务体会到结构化输出在运维工具里的重要性。真正用起来之后你会发现最耗时的不是扫描本身而是如何把扫描结果整理成一份可信的设备清单。设备在线状态是动态的网络策略是不断变化的任何一次扫描都只是某个时间点的快照。理解了这一点你就理解了网络运维工具设计的一条基本准则工具负责采集数据判断永远需要结合业务上下文。写完代码之后建议先在自己的局域网或虚拟测试环境里跑一遍对照路由器管理页面里的设备列表逐项核对。等确认扫描结果和真实情况一致再逐步接入你管理的正式环境。后续想要挑战更深的内容可以从这三个方向入手对开放端口做服务指纹识别、把扫描器改造成 HTTP API 服务、结合 ARP 表生成网络拓扑图。每一条路都能把现在的简易脚本扩展成一个更完整的网络管理基础组件。