资讯动态

端口扫描原理与Python实现:从TCP connect到终端监控实战

发布时间:2026/10/13 13:05:38 来源:尧图企业网站定制
简介面向网络安全与计算机网络专业的课程设计需求此代码包实现一个基于多线程的端口监控扫描程序可对指定IP、端口及线程数量进行扫描清晰呈现扫描原理与实现细节帮助学习者站在入侵者视角审视系统脆弱点。压缩包共17个文件包含4个头文件、3个C源文件以及Visual C工程文件dsp、dsw、资源描述文件rc、rc2、ico等整体仅29KB麻雀虽小五脏俱全便于快速阅读与移植。目前已有238人学习下载。通过研读源码可掌握Socket连接、多线程管理、端口状态判定等核心步骤同时理解敏感端口监控的防御思路为后续设计更安全的系统打下基础。1. 端口扫描课程设计别急着跑代码先看它能回答三个问题拿“网络安全终端监控”做课程设计的人十有八九会在端口扫描模块上翻车——不是代码跑不通而是交上去之后被导师追问“你扫描的原理是什么”“半开连接是什么意思”“为什么有的端口明明开着却没扫出来”时答不上来。这份端口扫描代码资源核心就是一套完整可运行的扫描器实现配合状态记录和结果输出能直接嵌进终端监控系统当探测模块。它适合两类人一类是拿来做课程设计、需要现场演示和答辩的学生另一类是刚接触网络安全的开发者想搞明白 TCP connect 扫描和 SYN 扫描在实际代码里差在哪。前提是你得先搞清楚三个问题扫什么、怎么扫、扫描结果怎么被监控端使用。2. 端口扫描原理与三种实现为什么课程设计首选 TCP connect2.1 三种扫描方式对比connect、SYN 与 UDP 扫描端口扫描的核心是探测目标主机的某个 TCP/UDP 端口是否处于监听状态。常见实现有三种TCP connect 扫描是调用系统完整的 TCP 三次握手connect 成功则端口开放。它的优点是无需特殊权限普通用户就能跑Windows、Linux 下行为一致代码实现最简单适合课程设计现场演示。缺点是会产生完整的连接记录目标机的应用日志里会留下痕迹。SYN 扫描半开扫描只发送 SYN 包收到 SYNACK 即判定端口开放然后回 RST 断开不完成握手。它的优点是隐蔽性更好、速度更快但构造原始数据包需要 root 权限Windows 上实现麻烦代码里还要自己处理 IP 头和 TCP 头的校验和调试成本高。UDP 扫描则完全不同——UDP 无连接判断端口是否开放靠的是“发个空包过去看目标回不回 ICMP Port Unreachable”。不回包可能是端口开着也可能是防火墙把 ICMP 丢了误判率极高。对课程设计来说我的建议很直接核心功能用 TCP connect 扫描把 SYN 扫描作为一个“扩展功能”写在文档里说明原理和权限限制就行。道理很简单答辩时你要当场演示connect 扫描在实验室网络里稳定、可解释、不依赖 root 权限——这三个优点足够覆盖 90% 的课程设计场景。2.2 为什么 SYN 扫描不适合做课程设计的主扫描器很多人觉得 SYN 扫描“更高级”想拿它当亮点。但放进课程设计里它有几个实际麻烦第一权限问题。构造原始套接字需要管理员权限实验室的机器未必给你开 sudo。就算开了某些虚拟化环境对 raw socket 的支持也有问题代码可能在自己的机器上跑得好好的到答辩机器上直接报错。第二实现复杂度。自己构造 TCP 头要计算校验和要处理 IP 分片还要自己解析响应包这些代码量足够再开一门课了。第三防火墙干扰。现在很多主机默认开防火墙对未完成的握手直接丢包或回 RSTSYN 扫描的结果反而不如 connect 扫描可靠。所以我的建议是文档里花两页纸把 SYN 扫描的原理、优缺点、实现思路讲清楚代码部分用 TCP connect 做主力。这既能展示你懂原理又不会让答辩演示翻车。如果确实想把 SYN 扫描写在代码里用 scapy 库实现但明确标注“需 root 权限仅作为原理演示”。2.3 端口扫描的“终端监控”定位扫描器只是模块不是全部“终端监控”这个词容易被理解偏。在课程设计里它指的是对某个终端单台主机的运行状态进行监控包含端口开放情况、服务状态、响应延迟等信息。端口扫描代码在这个系统里的定位是“探测模块”——定时去扫目标主机的端口把结果记录下来供上层的监控逻辑判断服务是否正常。这意味着你要考虑的不只是“能不能扫”还有“扫完怎么用”。常见的做法是定义一个通用的扫描结果结构体包含端口号、协议、状态open/closed/filtered、响应时间把结果写入 CSV 或 JSON 文件方便后续分析和展示定时扫描时做增量记录只记录状态发生变化的端口这些在代码里都要预留接口不能只写一个“拿着端口列表去 connect”的裸脚本。下一章的代码拆解会把这些串起来。3. 扫描器核心代码拆解Socket 参数、线程池与端口调度3.1 一个能直接跑的 TCP connect 扫描器骨架下面这段代码是资源里的核心扫描器纯 Python 标准库实现没有第三方依赖拿到就能跑import socket import threading from queue import Queue # 扫描目标配置 TARGET 127.0.0.1 # 目标IP本地测试用回环地址 START_PORT 1 # 起始端口 END_PORT 1024 # 结束端口 THREAD_COUNT 100 # 线程数 TIMEOUT 1.0 # 连接超时秒数 # 结果存储list of dict results [] results_lock threading.Lock() def scan_port(port): 扫描单个端口使用TCP connect方式 try: # socket.AF_INET 表示IPv4SOCK_STREAM 表示TCP sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(TIMEOUT) # connect_ex 返回0表示连接成功非0为错误码 code sock.connect_ex((TARGET, port)) if code 0: with results_lock: results.append({port: port, status: open, service: get_service(port)}) sock.close() except socket.error as e: # 连接失败、超时等都走这里 with results_lock: results.append({port: port, status: closed, service: }) def get_service(port): 常见端口服务名映射可以用 /etc/services 扩展 common {21: ftp, 22: ssh, 23: telnet, 25: smtp, 80: http, 110: pop3, 143: imap, 443: https, 3306: mysql, 3389: rdp} return common.get(port, unknown) def worker(): 线程工作函数从队列取端口并扫描 while not port_queue.empty(): port port_queue.get() scan_port(port) port_queue.task_done() # 构造端口队列 port_queue Queue() for p in range(START_PORT, END_PORT 1): port_queue.put(p) # 启动线程池 threads [] for _ in range(THREAD_COUNT): t threading.Thread(targetworker) t.start() threads.append(t) # 等待所有任务完成 port_queue.join() # 输出结果 for r in sorted(results, keylambda x: x[port]): if r[status] open: print(f[] {r[port]:5}/tcp {r[service]})这个骨架的关键点有三个。第一connect_ex()比connect()好在不抛异常直接返回错误码省去了 try/except 嵌套判断的麻烦代码更干净。第二results_lock是必须的——多线程同时往results列表写数据不加锁会出现漏数据或者列表损坏这是 Python 多线程的经典坑。第三队列Queue()天然是线程安全的用port_queue.get()让每个线程自动领任务比手动给线程分端口范围要优雅得多端口数不是线程数的整数倍时也不会出问题。3.2 参数怎么调线程数、超时时间与端口范围上面代码里四个参数直接决定扫描行为和开销课程设计答辩时很可能被问到参数作用取值建议THREAD_COUNT并发连接数本地扫描 50~200跨网络扫描 20~50猜错了会触发对方防火墙拦截TIMEOUT单端口超时局域网 0.5~1.0 秒跨公网 2~3 秒太小会误报 closedSTART_PORT/END_PORT扫描范围课程设计扫 1~1024 够用扫全端口 1~65535 会很慢TARGET目标地址必须是你有权限测试的机器实验室环境用 127.0.0.1 或虚拟机 IP线程数不是越大越好。线程越多目标机器的 SYN 队列压力越大可能直接把对方的服务搞崩——这在答辩现场是很尴尬的事。我一般建议课程设计场景用 50 个线程起步演示的时候跑 1~1024 端口大约需要 20 秒左右节奏刚好。超时时间的坑在于connect_ex的返回码ETIMEDOUT110和ECONNREFUSED111都代表“没连上”但前者是目标不可达或防火墙丢包后者是端口确实关闭。要区分这两种情况可以把错误码打印出来。在监控场景下批次扫描完成后按“响应超时”和“连接拒绝”分类汇总能更好地判断目标状态。3.3 从裸扫描到可复用的监控模块结果结构设计课程设计的代码如果只输出一个开放端口列表导师大概率会觉得太单薄。更合理的做法是把扫描器拆成三个层次参数配置、扫描引擎、结果处理。参数配置就对应上面代码里的四个变量结果处理你至少要有三件事import json import csv from datetime import datetime def save_results(results, target): 把扫描结果存成JSON和CSV双份方便不同场景分析 timestamp datetime.now().strftime(%Y%m%d_%H%M%S) # JSON格式适合程序二次处理 with open(fscan_{target}_{timestamp}.json, w, encodingutf-8) as f: json.dump({target: target, time: timestamp, results: results}, f, ensure_asciiFalse, indent2) # CSV格式适合Excel打开做表格 with open(fscan_{target}_{timestamp}.csv, w, newline, encodingutf-8) as f: writer csv.DictWriter(f, fieldnames[port, status, service]) writer.writeheader() writer.writerows(results) # 在worker完成后调用 save_results(results, TARGET)这个设计解决的是“结果怎么被监控端消费”的问题。JSON 格式让上面说的终端监控系统可以直接读文件判断服务状态CSV 格式方便课程设计报告里直接贴表格。文件名里带目标 IP 和时间戳是习惯问题——扫描是个高频操作不区分批次的话后面根本没法回溯。4. 终端监控怎么落进代码状态判定、定时扫描与阈控4.1 端口状态就三种open、closed、filtered扫描结果不能只分“通”和“不通”因为“不通”有两种完全不同的含义。端口被防火墙拒绝返回 ICMP 不可达和端口本身没有监听在监控系统里代表的业务状态完全不同——前者可能意味着服务宕机后者可能是配置错误。# 错误码对应的实际含义 import errno def classify_port_result(code): 把connect_ex的错误码归类为三种状态 if code 0: return open elif code in (errno.ECONNREFUSED,): # 111: 端口关闭目标主动拒绝 return closed elif code in (errno.ETIMEDOUT, errno.EHOSTUNREACH, errno.ENETUNREACH): return filtered # 超时/不可达大概率被防火墙拦截 else: return unknown # 使用方式 code sock.connect_ex((target, port)) status classify_port_result(code)注意区分filtered和closed是扫描器的关键能力也是答辩的加分项。一个端口如果返回filtered说明主机在线但端口被防火墙规则挡住——这说明你需要检查防火墙配置而不是服务状态。在终端监控界面里这三种状态要用不同颜色展示绿色 open、灰色 closed、黄色 filtered。你的课程设计如果做了这个区分就已经超过 80% 的同类作业了。4.2 定时扫描与状态变化检测做监控而不是做快照终端监控要求的是“变化能发现”不是“某一刻的状态”。同一个端口上次 open、这次变成 filtered这本身就是最值得记录的信息。实现方式是加一个对比逻辑import time def monitor_loop(target, ports, interval60): 定时扫描并记录状态变化 previous_state {} while True: results run_scan(target, ports) # 调用3.1节的扫描逻辑 changes [] for r in results: key (r[port], r[protocol]) new_status r[status] old_status previous_state.get(key) if old_status is not None and old_status ! new_status: changes.append({ port: r[port], protocol: r[protocol], from: old_status, to: new_status, time: time.strftime(%Y-%m-%d %H:%M:%S) }) if changes: log_changes(changes) # 写日志或触发告警 # 打印到控制台 for c in changes: print(f[CHANGE] {c[protocol]}/{c[port]}: {c[from]} - {c[to]} {c[time]}) previous_state {r[port]: r[status] for r in results} time.sleep(interval)这个循环的关键设计在于previous_state字典保存的是上一次的完整状态而不是变化记录。这样即使监控程序中途重启只要第一次扫描的基线还在后续就能识别状态变化。实际落地时我一般会把interval设成 60 秒或 300 秒——太频繁会让目标机不堪重负太稀疏又会漏掉短时间内的状态抖动。定时扫描还有一个隐藏的作用验证扫描器自身的稳定性。如果两次相邻扫描结果不一致且不是业务变更引起的那大概率是你的扫描器有 bug——比如线程没有正常回收、socket 没有关闭导致文件描述符耗尽。这个问题在下一章的避坑里细说。5. 端口扫描常见坑与排查超时误判、线程爆炸与权限不足5.1 明明端口开着却扫不到超时时间设置过短现象在局域网内扫描一台服务器的 8080 端口服务明明正常在跑扫描结果里却是 closed 或 filtered。原因TIMEOUT设得太短connect 请求在服务器处理不过来的时候就直接超时了。局域网内 0.3 秒可能够用但目标机器负载高或开启防火墙日志审计时握手完成时间会显著变长。解决把超时时间从 0.5 秒起步逐次递增到 1.0、1.5、2.0对比结果何时稳定。注意超时时间直接影响总扫描时长——1000 个端口、100 线程、1 秒超时的情况下最坏耗时约 10 秒可以接受。另外可以在代码里加一个开关快扫模式0.3 秒超时和精准模式2 秒超时答辩时展示两者结果差异反而能体现你理解这个指标的含义。5.2 扫描到一半程序卡死线程池没有回收机制现象扫描 1~1024 端口前面几百个端口正常之后程序一直不退出CPU 占用却很高。原因port_queue.join()等待所有任务完成但如果某个线程因为 socket 异常卡在connect_ex里没返回任务队列永远不为空程序就挂住了。另一个常见原因是循环里sock.close()写成sock.close——少了括号socket 对象没被释放文件描述符被吃光。解决给scan_port加一个兜底逻辑用try/finally确保 socket 一定被关闭同时把TIMEOUT作为硬性约束传入 socket。更稳妥的做法是给线程池加超时主线程最多等END_PORT - START_PORT个端口 × 超时时间 / 线程数超过这个总时间就强制退出并报警。我在自己写扫描器时会在每次sock.connect_ex()之前打印一行调试日志到文件卡住的时候能立刻定位是哪个端口的问题。5.3 代码在 Windows 上正常、Linux 上报错现象在 Windows 上跑得好好的拷到 Linux 虚拟机里就报PermissionError或Address already in use。原因TCP connect 扫描本身不需要特殊权限但如果你尝试绑定 socket 到一个具体源端口比如sock.bind((0.0.0.0, 1024))来实现固定源端口扫描Linux 对低端口1024 以下有权限限制。另外 Linux 对 TIME_WAIT 状态的 socket 管理策略不同大量快速 connect 后再次绑定同一源端口会报错。解决扫描代码里不要自行 bind 源端口让操作系统自动分配临时端口。如果课程设计要求固定源端口就用ip_local_port_range参数扩大临时端口范围或者在网上查一下你所用 Linux 发行版对低端口绑定权限的默认策略。这个坑在答辩时很容易被问到提前准备好会显得很专业。5.4 结果里全是 closed目标开启了防火墙现象扫描得到一个“全军覆没”的结果但手动telnet端口明明是通的。原因Windows 防火墙默认拦截入站连接Linux 的 iptables 也可能有 ACCEPT 规则之外的全部 DROP。扫描器发出的 SYN 包被防火墙直接丢弃表现为超时。解决把扫描目标和扫描器放在同一台机器上测试用 127.0.0.1或者关闭目标机的防火墙做对照实验。课程设计报告里一定要写清楚“本实验在防火墙关闭环境下进行”否则结果没有可重复性。更讲究一点的做法是扫描前先 ping 一下目标确认在线再用一个已知开放的端口做校准——如果官网首页端口 80 都被判成 closed那一定是环境问题而不是代码问题。5.5 扫描结果不稳定多线程竞争导致漏端口现象同一套代码同一参数连续跑三次每次结果都不一样某些端口偶尔出现在结果里偶尔不出现。原因线程数太多目标机的 SYN 队列溢出部分连接请求被直接丢弃。这在高并发扫描时非常常见也被称作“扫描器把自己扫挂了”——你连着开 500 个线程去扫一个性能一般的服务器它可能连正常的应用请求都处理不过来了。解决降低线程数到 50 以内加一个“重试机制”对于filtered状态的端口间隔 2 秒再扫一次连续三次结果一致才写入最终报告。重试代码很简单def scan_with_retry(port, retries3, delay2): 对非open状态的端口重试排除瞬时抖动 for i in range(retries): code scan_once(port) if code 0: return open time.sleep(delay) return classify_port_result(code)这个设计也解释了为什么课程设计里“扫描速度”和“准确率”是一对矛盾——追求速度就会丢准确率追求准确率就得接受更长的扫描时间。把这对矛盾放在设计文档里讨论比代码本身更能体现水平。6. 验证端口扫描准确率本地靶场三步验证与参数调优端口扫描代码写完第一件事不是往课程设计报告里贴而是验证它的准确率。没有经过校验的扫描器只测出“能跑”是不够的——你得知道它对过滤端口判对的概率是多少。我一般用三步来做验证你也可以直接照抄到课程设计文档里。第一步搭一个最小靶场。用 Python 起三个服务分别对应 open、closed、filtered 三种预期状态# 靶场服务端在另一台终端/虚拟机运行 import socket import threading def start_open_port(port): 模拟开放端口启动一个简单的TCP服务 s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((0.0.0.0, port)) s.listen(5) print(f[] open 端口: {port}) while True: conn, addr s.accept() conn.close() def start_closed_port(port): 模拟关闭端口不需要真实动作端口无监听即closed pass # 开放 8000 和 9000 端口 threading.Thread(targetstart_open_port, args(8000,), daemonTrue).start() threading.Thread(targetstart_open_port, args(9000,), daemonTrue).start() threading.Thread(targetstart_closed_port, args(8001,)).start() # 8500 端口模拟 filtered用防火墙规则丢弃 # iptables -A INPUT -p tcp --dport 8500 -j DROP # 保持主线程运行 threading.Event().wait()第二步设计一个验证矩阵。在扫描 1~10000 端口范围内人工标注哪些端口预期是 open8000、9000哪些是 closed8001哪些是 filtered8500。然后跑扫描器统计混淆矩阵open 被判成 closed 的比率、filtered 被判成 open 的比率。我在某实验室做模拟项目X的测试时就发现了一个很有意思的现象当目标机的防火墙对未授权端口回 RST 而不是 DROP 时扫描器会把 filtered 误判成 closed——因为connect_ex返回 111而 111 在我的分类规则里属于 closed。这个误判要借助一个额外的 TCP 重传计数来判断如果收到 RST 但之前还有一个 ICMP unreachable那这个端口更可能是 filtered。第三步调参。把超时、线程数、重试次数一个个跑一遍记录准确率和扫描时长的变化表格。这里有个实际经验TCP connect 扫描在局域网内超时 0.3 秒和 2 秒的准确率几乎相同因为内网延迟小于 1 毫秒但在跨网络场景下0.3 秒超时的误判率可能高达 15%。所以你的设计文档里一定要写清楚“本扫描器适用于局域网环境”这不算承认缺陷而是明确边界。把验证结果整理成表格后整个课程设计的含金量立刻就上来了。照着这个流程走一遍你会发现自己对端口扫描的理解比单纯跑通代码要深得多。从那以后我每次写完扫描相关代码都会强制走一遍“靶场起三个状态端口、跑一次扫描、统计误判”的标准流程前前后后帮我抓出来两个隐蔽 bug。希望这个验证方法也能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑