用Python标准库实现简易IP扫描工具:socket与线程池实战 这次我们来看一个基于 Python 的简易 IP 扫描工具设计与实现。它不需要依赖 Nmap、Masscan 这类重型工具只用 Python 自带的标准库就能完成局域网主机发现、端口开放检查、服务类型识别和结果导出。整个项目的核心价值不是“再造一个 Nmap”而是把网络编程的基础知识点串起来socket 连接、IP 地址解析、并发控制、结果结构化输出都是实际工程里每天都会用到的东西。这个工具的设计目标很克制能扫单个 IP、CIDR 网段、起始结束范围用 TCP connect 方式探测端口用线程池提升速度最后把结果打印到控制台或导出为 CSV。对运维来说可以快速盘点一个网段里哪些设备的哪些端口是开放的对开发者来说这是理解扫描器原理和 Python 并发编程的一个完整练手项目。先给出一个判断这个项目不需要 GPU不需要大型模型文件也不需要管理员权限。只要一台能跑 Python 3.8 以上版本的机器Windows、Linux、macOS 都能运行。本文会从环境准备、核心代码实现、功能测试、性能观察、常见问题排查几个角度把这个扫描工具完整拆开。代码示例可以直接复制到本地跑建议在你自己管理的测试网络里验证。1. 简易IP扫描工具核心能力速览能力项说明项目类型网络探测 / 轻量端口扫描工具开发语言Python 3.8推荐 3.10 以上依赖库仅 Python 标准库零第三方依赖扫描方式TCP connect 扫描为主可选 Ping 预筛选目标输入单个 IP、CIDR 网段、起始 IP 范围并发控制ThreadPoolExecutor 线程池线程数可配置输出方式控制台结果列表、CSV 结果导出权限要求普通用户可运行不需要 root/管理员权限适用场景局域网设备盘点、端口状态检查、网络编程学习从这张表能看出来这个工具的特点是“轻、简、可扩展”。它不做 SYN 半开扫描不猜操作系统不自动调用漏洞库。边界很清楚只解决“哪些主机在线、哪些端口开放、端口大概是什么服务”这三个问题。如果之后要上生产环境完全可以在这份代码基础上加 HTTP 接口、任务队列、Web 前端扩展路径是通的。2. 适用场景与使用边界2.1 适合谁用第一类读者是运维和网络管理员。内网设备一多靠手动 ping 和 telnet 逐个测试不现实这个工具可以把一段 /24 网段里开放了 22、80、443、3389、8080 等端口的设备快速列出来做资产台账的初步梳理。第二类读者是 Python 开发者。这个项目虽然简单但涉及 ipaddress 模块、socket 编程、线程池、命令行参数解析、CSV 文件写入都是后端开发常用的技能点。把代码读懂、改一遍比单独看语法教程要扎实得多。第三类读者是安全测试方向的学习者。注意这里只讨论“理解扫描原理”这一层TCP 三次握手是什么、为什么 connect_ex 返回值是 0 代表端口开放、为什么需要超时控制。所有测试只能在授权环境下进行。2.2 使用边界与合规要求IP 扫描不是无边界的技术。以下几点必须明确只能扫描你拥有、管理或获得明确授权的网络设备。在未授权情况下扫描他人网络轻则被防火墙和 IDS 记录告警重则涉及法律风险。在单位或学校网络里使用要提前确认是否符合内部管理规定。扫描会产生网络流量不要在核心业务链路上使用过大并发避免影响正常服务。这篇文章里的所有代码仅用于本地测试环境、学习网络编程原理和个人运维场景。商用或上线前建议找法务和技术负责人一起评估合规边界。3. 环境准备与项目结构3.1 运行环境检查这个项目对系统几乎没有特殊要求。建议使用 Python 3.10 或更高版本原因是类型标注和异常处理体验更好。检查方式python --versionLinux 或 macOS 下如果默认不是 Python 3改用python3 --versionWindows 下要注意如果在命令行找不到 python可以到 Python 官网安装并在安装时勾选“Add Python to PATH”。3.2 项目目录结构建议按下面的结构组织ip_scanner/ ├── ip_scanner.py ├── requirements.txt └── output/当前版本只用标准库所以 requirements.txt 里可以只写一行注释说明不需要安装任何依赖。output 目录用来存放导出的 CSV 扫描结果避免和源码混在一起。3.3 是否需要安装第三方库从工具设计上看不推荐为了扫描功能引入 requests、scapy、nmap 等第三方库。原因有两个一是标准库足够完成任务零依赖让工具在任何机器上都能直接跑二是作为学习项目用标准库实现更能帮助你理解底层原理。后续如果要做 Web 界面或更复杂的协议识别再按需引入 FastAPI、Scapy 等库也不迟。4. 简易IP扫描工具核心代码实现整个工具可以拆成 5 个模块参数解析、目标 IP 解析、端口加载、TCP 扫描、结果输出。下面从最重要的函数开始逐个实现。4.1 目标 IP 解析模块需要支持三种输入形式单个 IP、CIDR 网段、起始到结束范围。Python 的 ipaddress 模块天生支持这些操作。import ipaddress def parse_targets(target: str) - list: 解析目标输入支持单个IP、CIDR网段、起始-结束范围。 result [] target target.strip() # 单个 IP if target.count(-) 0 and / not in target: ipaddress.ip_address(target) # 校验 IP 格式 return [target] # CIDR 网段例如 192.168.1.0/24 if / in target: network ipaddress.ip_network(target, strictFalse) return [str(ip) for ip in network.hosts()] # 起始-结束范围例如 192.168.1.10-192.168.1.20 if - in target: start, end target.split(-) start_ip ipaddress.ip_address(start.strip()) end_ip ipaddress.ip_address(end.strip()) current start_ip while current end_ip: result.append(str(current)) current 1 return result raise ValueError(f无法解析目标: {target})需要注意network.hosts()返回的是网段内可用主机地址会自动排除网络地址和广播地址。比如 192.168.1.0/24 会返回 192.168.1.1 到 192.168.1.254共 254 个地址。如果目标少部分设备使用了 .0 或 .255 这样的特殊地址可以在后续版本里加参数控制这里先保持默认行为。4.2 端口列表加载模块命令行传入的端口支持单端口、逗号分隔列表和范围三种写法。例如-p 22,80,443表示 3 个端口-p 1-1000表示连续 1000 个端口。def load_ports(port_str: str) - list: 解析端口参数支持 22,80,443 和 1-1000 两种写法。 ports set() for part in port_str.split(,): part part.strip() if not part: continue if - in part: start, end part.split(-) ports.update(range(int(start), int(end) 1)) else: ports.add(int(part)) return sorted(ports)用 set 去重避免同一个端口被重复扫描。返回值用 sorted 排序输出结果更整齐。4.3 TCP connect 端口扫描模块TCP connect 扫描的原理很简单向目标的某个端口发起 TCP 连接也就是完成一次三次握手。如果端口开放操作系统会完成握手connect_ex返回 0如果端口关闭目标会返回 RSTconnect_ex返回非 0 错误码如果目标不可达或防火墙丢弃了包就会一直等到超时。import socket def tcp_connect_scan(host: str, port: int, timeout: float) - bool: 对指定IP和端口发起TCP连接返回端口是否开放。 try: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.settimeout(timeout) return sock.connect_ex((host, port)) 0 except OSError: return False这里使用with语句管理 socket自动释放文件描述符。connect_ex比connect更适合扫描场景因为调用connect_ex不会因为连接失败抛出异常而是把错误码作为返回值返回逻辑更清晰。4.4 Ping 预筛选模块在一个大网段里很多 IP 可能根本没有设备在线。如果直接对这些 IP 做端口扫描每个端口都要等到超时浪费大量时间。可以先用 Ping 做一轮主机存活筛选只对在线主机做端口扫描。import platform import subprocess def host_is_alive(host: str, timeout_ms: int 1000) - bool: 通过系统ping命令检测主机是否在线兼容Windows和Linux。 system platform.system().lower() if system windows: cmd [ping, -n, 1, -w, str(timeout_ms), host] else: timeout_sec max(1, timeout_ms // 1000) cmd [ping, -c, 1, -W, str(timeout_sec), host] try: result subprocess.run( cmd, stdoutsubprocess.DEVNULL, stderrsubprocess.DEVNULL, timeouttimeout_ms / 1000 2, ) return result.returncode 0 except Exception: return False需要注意Windows 的-w参数单位是毫秒Linux 的-W参数单位是秒。代码里做了平台判断同时把stdout和stderr丢弃避免命令输出刷屏。如果主机禁用了 ICMP即使在线也可能 ping 不通所以 ping-first 只作为可选的预筛开关不能完全替代端口扫描判断。4.5 并发扫描与结果汇总TCP 连接扫描是典型的 IO 密集型任务每个连接都在等待网络响应CPU 基本闲着。用线程池可以把大量等待时间重叠起来让几十个连接同时进行速度提升非常明显。from concurrent.futures import ThreadPoolExecutor, as_completed def scan_worker(host: str, ports: list, timeout: float) - list: 扫描单个主机的所有端口返回开放端口列表。 open_ports [] for port in ports: if tcp_connect_scan(host, port, timeout): open_ports.append(port) return open_ports这里的粒度是“一个线程负责一台主机的所有端口”。如果主机数量很多线程数就等于同时扫描的主机数量如果主机很少但端口很多可以把粒度改成“一个线程负责一个主机端口对”并发度更高。作为简易工具先用主机级粒度代码更直观。4.6 服务类型识别与 Banner 抓取知道端口开放还不够最好能猜出端口对应的服务。常见端口和服务的映射关系是固定的SERVICE_MAP { 21: ftp, 22: ssh, 23: telnet, 25: smtp, 53: dns, 80: http, 110: pop3, 135: msrpc, 139: netbios, 143: imap, 443: https, 445: microsoft-ds, 993: imaps, 995: pop3s, 1433: mssql, 1521: oracle, 3306: mysql, 3389: rdp, 5432: postgresql, 6379: redis, 8080: http-proxy, 8443: https-alt, 27017: mongodb, } def guess_service(port: int) - str: return SERVICE_MAP.get(port, unknown)如果需要对开放端口做更细的识别可以抓取 Banner。很多服务在客户端连接后会主动发送一段欢迎信息或版本信息。但不同协议差异很大HTTP 服务需要先发请求头HTTPS 需要 TLS 握手所以 Banner 抓取只作为可选功能。def fetch_banner(host: str, port: int, timeout: float 2.0) - str: 尝试获取服务bannerHTTP端口需要发送请求头。 try: with socket.create_connection((host, port), timeouttimeout) as sock: if port in (80, 8080, 8000): sock.sendall( bHEAD / HTTP/1.0\r\nHost: host.encode() b\r\n\r\n ) else: sock.sendall(b\r\n) data sock.recv(1024) return data.decode(utf-8, errorsignore).strip() except OSError: return 注意443、8443 这类 HTTPS 端口因为需要 TLS 握手上面这个函数抓不到内容这属于正常现象不代表服务异常。Banner 抓取只适合教学和授权环境下的服务识别不建议在生产环境大批量开启因为它会显著增加扫描时间。4.7 主程序入口与参数设计主程序需要接收目标 IP、端口列表、超时时间、并发线程数、Ping 预筛选开关、Banner 抓取开关、输出文件路径等参数。使用 argparse 标准库解析命令行参数import argparse import csv import sys def parse_args(): parser argparse.ArgumentParser(description简易IP扫描工具) parser.add_argument(-t, --target, requiredTrue, help目标IP、CIDR或范围例如 192.168.1.0/24) parser.add_argument(-p, --ports, default22,80,443,3389,8080, help端口列表或范围) parser.add_argument(--timeout, typefloat, default0.5, help单端口超时时间单位秒) parser.add_argument(--threads, typeint, default50, help并发线程数) parser.add_argument(--ping-first, actionstore_true, help先ping筛选在线主机) parser.add_argument(--banner, actionstore_true, help对开放端口抓取banner) parser.add_argument(-o, --output, help导出CSV结果文件路径) return parser.parse_args()主流程的逻辑是解析目标 - 可选 Ping 筛选 - 线程池并发扫描 - 打印结果 - 可选导出 CSV。def main(): args parse_args() hosts parse_targets(args.target) ports load_ports(args.ports) if args.ping_first: print(f[*] Ping 预筛选共 {len(hosts)} 个地址) alive_hosts [] with ThreadPoolExecutor(max_workersargs.threads) as executor: future_map { executor.submit(host_is_alive, host): host for host in hosts } for future in as_completed(future_map): if future.result(): alive_hosts.append(future_map[future]) hosts alive_hosts print(f[*] 预筛后剩余 {len(hosts)} 个在线主机) print(f[*] 开始扫描 {len(hosts)} 个主机端口: {ports}) results [] with ThreadPoolExecutor(max_workersargs.threads) as executor: future_map { executor.submit(scan_worker, host, ports, args.timeout): host for host in hosts } for future in as_completed(future_map): host future_map[future] try: open_ports future.result() except Exception as exc: print(f[!] {host} 扫描异常: {exc}, filesys.stderr) continue if open_ports: results.append((host, open_ports)) print_results(results) if args.output: write_csv(results, args.output, args.banner)结果打印函数和 CSV 导出函数如下def print_results(results: list): if not results: print([*] 未发现开放端口) return print(f[] 发现 {len(results)} 台主机存在开放端口) for host, open_ports in sorted(results): print(f[] {host}) for port in open_ports: print(f {port:5}/tcp {guess_service(port)}) def write_csv(results: list, output_file: str, enable_banner: bool False): with open(output_file, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([host, port, service, banner]) for host, open_ports in results: for port in open_ports: banner if enable_banner: banner fetch_banner(host, port) writer.writerow([host, port, guess_service(port), banner]) print(f[*] 结果已导出到 {output_file}) if __name__ __main__: main()把上面 4.1 到 4.7 的代码按顺序整合到一个ip_scanner.py文件里就是一个可以直接运行的完整工具。只需要在文件顶部补全import ipaddress、import socket、import platform、import subprocess这些标准库导入。5. 功能测试与效果验证工具写完以后先在最小范围内验证再扩展到整个网段。下面给出一套通用的验证流程。5.1 测试单 IP 端口扫描先扫本机回环地址确认工具基本逻辑正常python ip_scanner.py -t 127.0.0.1 -p 22,80,443,8080 --timeout 1如果本机没有开放这些端口可能会看到“未发现开放端口”。如果本机恰好启动了某个 Web 服务输出里会看到类似这样的结果[] 发现 1 台主机存在开放端口 [] 127.0.0.1 8080/tcp http-proxy判断成功的标准程序正常退出控制台输出了每个主机的开放端口列表。如果本机所有端口都不开至少看不到报错和异常堆栈。5.2 测试网段扫描确认单 IP 正常后扫描局域网内的一个 /24 网段python ip_scanner.py -t 192.168.1.0/24 -p 22,80,443,3389 --timeout 0.5 --threads 100这里要特别提醒192.168.1.0/24 这个网段地址要改成你自己测试环境对应的网段绝对不要直接拿去扫别人的网段。扫描结果会列出所有 22、80、443、3389 端口开放的主机。这些端口对应 SSH、HTTP、HTTPS、RDP是资产盘点时最常关注的入口。5.3 测试端口范围扫描需要看某个主机开放了哪些端口时可以扫一段范围python ip_scanner.py -t 192.168.1.10 -p 1-1000 -o output/scan_result.csv这个命令只扫一台主机的 1 到 1000 端口单端口超时 0.5 秒最坏情况下理论耗时约 500 秒如果用多线程只扫单台主机就不会加速因为默认线程粒度是主机级。这里建议把-p范围控制在常用端口或使用--threads配合多主机扫描。5.4 测试 Ping 预筛选python ip_scanner.py -t 192.168.1.0/24 -p 22,80 --ping-first可以看到程序先输出 Ping 预筛结果再输出剩余主机数量和最终扫描结果。Ping 预筛能大幅缩短扫网段的时间但前提是目标主机没有禁用 ICMP。5.5 测试 CSV 导出扫描结束后用文本编辑器或表格软件打开output/scan_result.csv确认包含以下字段host,port,service,banner 192.168.1.10,80,http, 192.168.1.10,443,https, 192.168.1.12,22,ssh,CSV 导出正常说明结构化输出没有问题后续可以把这个文件交给其他脚本做二次分析比如生成资产清单或绘制网络拓扑。6. 批量任务与工程化扩展6.1 当前工具的批量能力这个工具本身不提供 HTTP API也没有内置任务队列但通过命令行参数已经具备了最基础的批量能力一次性传入一个 /24 网段就是对 254 个 IP 的批量扫描传入 1-1000 端口范围就是对单个主机的批量端口测试。对于临时盘点任务命令行的批处理能力足够。6.2 多网段批量扫描如果需要一次扫描多个独立网段可以用一个外层脚本循环调用。例如在 Linux 下for target in 192.168.1.0/24 192.168.2.0/24 192.168.10.0/24; do python ip_scanner.py -t $target -p 22,80,443 \ --timeout 0.5 --threads 100 \ -o output/${target}_result.csv done每个网段的结果单独保存文件名带上网段标识方便后续归类和清理。6.3 扩展为 HTTP 接口如果要做成服务化扫描可以在核心函数基础上封装一个 Web 接口。这里给一个 FastAPI 扩展思路实际使用需要额外安装 fastapi 和 uvicorn# 扩展思路示例需要安装 fastapi 和 uvicorn from fastapi import FastAPI app FastAPI() app.post(/scan) def run_scan(target: str, ports: str 22,80,443): host_list parse_targets(target) port_list load_ports(ports) result {} for host in host_list: open_ports scan_worker(host, port_list, timeout0.5) if open_ports: result[host] open_ports return {target: target, open_ports: result}注意这只是扩展方向不是当前项目自带功能。服务化以后要额外考虑接口鉴权、任务限流、扫描结果隔离否则任何能访问接口的人都能发起扫描风险很大。7. 资源占用与性能观察7.1 资源占用分析这个工具运行时只占很少的内存主要开销来自每个线程持有的 socket 文件描述符。线程数越高同时建立的 TCP 连接就越多占用的文件描述符也越多。Linux 下默认文件描述符限制通常为 1024如果线程数设置过高可能出现Too many open files错误。遇到这种情况降低--threads或调整系统 ulimit 限制即可。7.2 耗时估算方法扫描总耗时主要由三个因素决定目标主机数、每个主机的端口数、超时时间。在最坏情况下所有端口都超时扫描耗时可以近似为总耗时 ≈ 主机数 × 端口数 × 超时时间 / 并发线程数举例扫描 254 个主机、5 个端口、单端口超时 0.5 秒、并发 50 线程最坏耗时约为254 × 5 × 0.5 / 50 ≈ 12.7 秒实际耗时通常会小于这个值因为端口关闭时目标会立即返回 RST不需要等满超时端口开放时也能快速识别。这个公式可以用来预估大范围扫描的代价帮助决定是否需要先做 Ping 预筛选。7.3 如何优化扫描速度对内网主机先做 Ping 预筛选把离线主机过滤掉。端口扫描时优先扫常用端口不要一上来就1-65535。超时时间控制在 0.3 到 1 秒之间。太短会漏报太长会拖慢整体速度。线程数并不是越大越好。在内网里 100 到 200 线程通常已经很快再大可能导致网络丢包和防火墙误判。如果需要扫端口全范围建议把“主机级并发”改成“主机端口对级并发”这是下一步优化的重点方向。8. 常见问题与排查方法问题现象可能原因排查方式解决方案扫描结果全部显示没有开放端口目标主机离线、防火墙丢弃连接、超时时间过短先手动 ping 目标再用 telnet 测单个端口调大--timeout开启--ping-first确认主机是否在线提示Name or service not known目标域名无法解析检查域名拼写和 DNS 配置改用 IP 地址或先解析出 IP 再扫描提示Too many open files线程数过高文件描述符耗尽查看当前 ulimit 限制降低--threads或调整系统文件描述符限制程序卡住很久没有输出端口超时设置过长目标数量多观察 CPU 和网络连接数调低--timeout增加--ping-first预筛Ping 预筛把所有主机都过滤掉了目标主机禁用了 ICMP 协议手动执行 ping 命令测试关闭--ping-first直接用 TCP 扫描判断主机状态Banner 抓取内容为空服务不主动发 banner或需要先发送协议数据用 curl 或 nc 手动连接测试针对特定协议扩展 fetch_banner 的处理逻辑CSV 文件用 Excel 打开乱码CSV 编码问题用文本编辑器查看原始字节确保以 UTF-8 编码读取或用 pandas 指定编码打开扫描速度太慢并发线程数过低、目标范围过大查看日志中的扫描耗时增大--threads缩小端口范围先 Ping 预筛同一个 IP 出现在多次扫描结果里网段解析和参数重复检查目标输入写法用ipaddress解析结果去重或统一使用 CIDR 写法9. 最佳实践与合规提醒9.1 使用规范第一次使用建议按下面的顺序操作先用-t 127.0.0.1扫本机确认工具能跑通。再扫自己管理的单个设备确认端口识别和结果输出正常。最后再扩展到整个网段并提前评估扫描耗时。每次扫描前先确认目标网络属于自己或已获得授权。不要用这个工具去探测不熟悉的网络更不要把它接入自动化系统对公网反复扫描。9.2 结果管理扫描结果属于敏感信息。建议扫描结果文件单独放在 output 目录不要提交到公开代码仓库。CSV 文件里包含 IP 和端口信息接收人要限定范围。每次扫描结果带上日期和扫描命令记录方便追溯。不需要长期保存的结果及时删除。9.3 代码扩展方向当前工具只解决了“发现开放端口”的问题后续可以从这几个方向继续扩展增加 JSON 输出格式方便接任务系统。增加主机名反查和 MAC 地址识别完善资产台账。增加 Web 前端把扫描结果可视化。增加扫描任务定时调度自动感知新上线设备。把主机级并发改成主机端口对级并发提高单主机全端口扫描效率。如果在扩展过程中引入了第三方库再把 requirements.txt 补全记录依赖版本。10. 总结与下一步这个简易 IP 扫描工具最值得尝试的点是用纯标准库实现了一个能实际使用的网络工具。它不依赖 Nmap也没有复杂的安装流程代码量控制在两百行以内却覆盖了 IP 解析、socket 连接、线程池并发、参数解析、CSV 导出这些高频知识点。建议你拿到代码后先跑通127.0.0.1的单 IP 扫描再在自己管理的局域网里做一次小范围验证。最容易踩的坑是超时时间设置太短导致漏报以及线程数太高导致文件描述符不足。把这两个参数调整好工具基本就能稳定使用了。下一步的扩展路径也很清晰如果你想做资产盘点就加上 hostname 反查和 CSV 归档如果你想做网络编程练习就尝试把并发粒度从主机级改成主机端口对级如果你想做服务化能力就参考第 6 节的 FastAPI 思路加接口鉴权和任务队列。从这份代码继续往上长比从空白开始写要快得多。