252 lines
10 KiB
Python
252 lines
10 KiB
Python
import socket
|
|
import threading
|
|
import concurrent.futures
|
|
import tkinter as tk
|
|
from tkinter import scrolledtext, messagebox
|
|
from typing import Iterable, Optional, List
|
|
|
|
|
|
class PortScanner:
|
|
"""
|
|
PortScanner: 在 Tkinter 的 ScrolledText 中显示端口检测结果的工具类。
|
|
|
|
构造:
|
|
scanner = PortScanner(result_box)
|
|
|
|
方法:
|
|
test_connect(ip, port, timeout=1.0)
|
|
- 直接测试单个 ip:port 是否可连通(立即返回 bool,并在结果框显示一行结果)
|
|
|
|
start_range_scan(ip, start_port, end_port, timeout=1.0, max_workers=100)
|
|
- 并发扫描端口范围 [start_port, end_port](包含端口边界)
|
|
- 扫描结果会实时写入 result_box
|
|
|
|
start_list_scan(ip, ports: Iterable[int], timeout=1.0, max_workers=100)
|
|
- 并发扫描给定端口列表
|
|
|
|
stop_scan()
|
|
- 尝试中止正在进行的扫描(设置停止标志,后续任务检测到后会停止提交或返回)
|
|
|
|
注意:
|
|
- 使用 TCP 连接测试(socket.connect),适合服务端口检测。
|
|
- GUI 写入通过 self._append_text(...) 调度到主线程,保证线程安全。
|
|
"""
|
|
|
|
def __init__(self, result_box: scrolledtext.ScrolledText):
|
|
self.result_box = result_box
|
|
|
|
# 扫描控制状态
|
|
self._stop_flag = False # 外部调用 stop_scan() 会把此标志设为 True
|
|
self._scan_thread: Optional[threading.Thread] = None
|
|
self._executor: Optional[concurrent.futures.ThreadPoolExecutor] = None
|
|
|
|
# 用于统计(可选)
|
|
self._total = 0
|
|
self._done = 0
|
|
self._open_ports: List[int] = []
|
|
|
|
# -------------------------
|
|
# 辅助方法:线程安全地向结果框写文本
|
|
# -------------------------
|
|
def _append_text(self, text: str):
|
|
"""
|
|
把文本插入到 result_box。因为可能从子线程调用,所以用 after 调度到主线程执行。
|
|
"""
|
|
try:
|
|
# schedule on main thread immediately
|
|
self.result_box.after(0, lambda: (self.result_box.insert(tk.END, text), self.result_box.see(tk.END)))
|
|
except Exception:
|
|
# 在极少数情况下(例如 result_box 已销毁),捕获异常避免崩溃
|
|
pass
|
|
|
|
# -------------------------
|
|
# 单端口测试
|
|
# -------------------------
|
|
def test_connect(self, ip: str, port: int, timeout: float = 1.0) -> bool:
|
|
"""
|
|
立即测试单个 ip:port 是否可以 TCP 连接。
|
|
- 返回 True(可连通)或 False(不可连通)
|
|
- 同时把结果写入 result_box(通过主线程调度)
|
|
"""
|
|
addr = (ip, int(port))
|
|
status = False
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.settimeout(timeout)
|
|
sock.connect(addr)
|
|
status = True
|
|
sock.close()
|
|
except Exception:
|
|
status = False
|
|
|
|
# 输出结果(在 GUI 中显示)
|
|
self._append_text(f"{ip}:{port} {'✅ 开放' if status else '❌ 关闭/不可达'}\n")
|
|
return status
|
|
|
|
# -------------------------
|
|
# 并发单端口任务(内部使用)
|
|
# -------------------------
|
|
def _scan_single_port(self, ip: str, port: int, timeout: float) -> str:
|
|
"""
|
|
线程池中运行的单个端口检测任务,返回一行结果字符串。
|
|
任务必须尽量短小(快速返回),并在开始前检查 stop_flag。
|
|
"""
|
|
if self._stop_flag:
|
|
return "" # 为空表示不输出
|
|
|
|
try:
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
sock.settimeout(timeout)
|
|
sock.connect((ip, port))
|
|
sock.close()
|
|
result = f"{ip}:{port} ✅ 开放\n"
|
|
# 记录到本地开放端口列表(线程安全地追加)
|
|
self._open_ports.append(port)
|
|
except Exception:
|
|
result = f"{ip}:{port} ❌ 关闭/不可达\n"
|
|
|
|
# 更新进度计数(最好在主线程更新显示,这里在子线程更新计数)
|
|
self._done += 1
|
|
return result
|
|
|
|
# -------------------------
|
|
# 并发扫描(范围或列表)
|
|
# -------------------------
|
|
def start_range_scan(self, ip: str, start_port: int, end_port: int, timeout: float = 0.8, max_workers: int = 200):
|
|
"""
|
|
并发扫描端口范围 [start_port, end_port](含两端)。
|
|
结果实时写入 result_box。单次扫描在后台线程中运行(不会阻塞主线程)。
|
|
"""
|
|
# 参数校验(基本)
|
|
try:
|
|
start_port = int(start_port); end_port = int(end_port)
|
|
except Exception:
|
|
messagebox.showwarning("输入错误", "起始端口和结束端口必须为整数")
|
|
return
|
|
if start_port < 1 or end_port > 65535 or start_port > end_port:
|
|
messagebox.showwarning("输入错误", "端口范围不合法(1-65535 且 起始<=结束)")
|
|
return
|
|
|
|
ports = list(range(start_port, end_port + 1))
|
|
self.start_list_scan(ip, ports, timeout=timeout, max_workers=max_workers)
|
|
|
|
def start_list_scan(self, ip: str, ports: Iterable[int], timeout: float = 0.8, max_workers: int = 200):
|
|
"""
|
|
并发扫描指定的端口列表。
|
|
- ip: 目标 IP(字符串)
|
|
- ports: 可迭代的端口集合(如 list、range 等)
|
|
- timeout: 单端口连接超时(秒)
|
|
- max_workers: 最大并发数(线程池大小)
|
|
"""
|
|
# 防止重复启动
|
|
if self._scan_thread and self._scan_thread.is_alive():
|
|
messagebox.showinfo("提示", "已有扫描任务在运行,请先停止后再启动新的扫描。")
|
|
return
|
|
|
|
# 将 ports 转为列表并进行基本校验
|
|
try:
|
|
ports_list = [int(p) for p in ports]
|
|
except Exception:
|
|
messagebox.showwarning("输入错误", "端口列表包含非法值")
|
|
return
|
|
if not ports_list:
|
|
messagebox.showwarning("输入错误", "端口列表为空")
|
|
return
|
|
for p in ports_list:
|
|
if p < 1 or p > 65535:
|
|
messagebox.showwarning("输入错误", f"端口 {p} 不在合法范围 1-65535")
|
|
return
|
|
|
|
# 重置状态
|
|
self._stop_flag = False
|
|
self._open_ports = []
|
|
self._total = len(ports_list)
|
|
self._done = 0
|
|
|
|
# 清空 result_box 并输出起始信息(主线程调度)
|
|
self._append_text(f"开始并发端口扫描:目标 {ip},共 {self._total} 个端口\n")
|
|
|
|
# 后台线程用于管理线程池与结果收集,确保 GUI 不阻塞
|
|
def manager():
|
|
# 根据任务数自适应限制并发数
|
|
actual_workers = max(1, min(max_workers, self._total))
|
|
self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=actual_workers)
|
|
|
|
# 提交任务
|
|
futures = {self._executor.submit(self._scan_single_port, ip, port, timeout): port for port in ports_list}
|
|
|
|
try:
|
|
# as_completed 会在每个 future 完成时迭代返回
|
|
for fut in concurrent.futures.as_completed(futures):
|
|
if self._stop_flag:
|
|
# 如果外部发出停止信号,尽量取消未开始的 future(cancel 返回 True 表示取消成功)
|
|
# 线程池中的任务可能已在运行, cancel 只能取消未开始的任务
|
|
break
|
|
|
|
try:
|
|
line = fut.result()
|
|
except Exception as e:
|
|
line = f"{ip}:{futures.get(fut)} 错误: {e}\n"
|
|
|
|
if line:
|
|
# 把结果写回 GUI(通过 _append_text 安全调度)
|
|
self._append_text(line)
|
|
|
|
# 可选:显示进度(例如:done/total)
|
|
self._append_text(f"进度: {self._done}/{self._total}\n")
|
|
|
|
# 如果 stop_flag 已设,尝试取消尚未开始的任务
|
|
if self._stop_flag:
|
|
for f in futures:
|
|
f.cancel()
|
|
|
|
finally:
|
|
# 关闭线程池
|
|
if self._executor:
|
|
self._executor.shutdown(wait=False)
|
|
self._executor = None
|
|
|
|
# 最终输出总结信息(开放端口列表)
|
|
if not self._stop_flag:
|
|
self._append_text("\n端口扫描完成。\n")
|
|
else:
|
|
self._append_text("\n端口扫描已停止。\n")
|
|
|
|
if self._open_ports:
|
|
self._append_text(f"开放端口: {sorted(self._open_ports)}\n")
|
|
else:
|
|
self._append_text("未发现开放端口。\n")
|
|
|
|
# 启动后台管理线程
|
|
self._scan_thread = threading.Thread(target=manager, daemon=True)
|
|
self._scan_thread.start()
|
|
|
|
# -------------------------
|
|
# 停止扫描
|
|
# -------------------------
|
|
def stop_scan(self):
|
|
"""
|
|
请求停止正在进行的扫描:设置停止标志并尝试关闭线程池。
|
|
- 已提交并正在运行的 socket.connect 调用无法被立即中断(但后续任务会被取消)
|
|
- stop_scan 尽快返回,实际终止需要等待正在运行的任务完成或超时
|
|
"""
|
|
if not (self._scan_thread and self._scan_thread.is_alive()):
|
|
messagebox.showinfo("提示", "当前没有正在运行的扫描任务。")
|
|
return
|
|
|
|
self._append_text("\n正在停止扫描,请稍候...\n")
|
|
self._stop_flag = True
|
|
|
|
# 尝试立即关闭线程池(不等待正在运行任务)
|
|
if self._executor:
|
|
try:
|
|
self._executor.shutdown(wait=False)
|
|
except Exception:
|
|
pass
|
|
|
|
# -------------------------
|
|
# 可选:返回当前扫描状态(供外部查询)
|
|
# -------------------------
|
|
def is_scanning(self) -> bool:
|
|
return bool(self._scan_thread and self._scan_thread.is_alive())
|