
简介这是一份面向计算机网络课程学习者与Java初学者的端口扫描器实践项目聚焦TCP/UDP协议层探测原理与多线程编程实战适用于课程设计、大作业或工程实训场景。资源包共12个文件含2个核心Java源码实现扫描逻辑与GUI界面、3个编译生成的class文件、2份Markdown文档含README说明与项目记录、1张界面截图PNG及Eclipse工程配置文件.project、.classpath等整体仅96KB轻量易部署。已有131人下载学习可直接导入Eclipse运行支持自定义IP、端口范围0–65535及线程数1–200实时显示开放端口并支持结果保存。读者将获得完整可运行的GUI端口扫描器工程、清晰的多线程扫描实现思路、基于Socket的TCP连接检测与UDP探活逻辑、以及适配Windows 10平台的调试经验是理解网络服务发现机制的典型教学案例。1. 这不是玩具级扫描器Java 实现的 TCP/UDP 端口扫描器能真实跑通三次握手、发 UDP 探针、多线程压测本地服务——课程设计交上去老师当场问你“SYN 半开怎么规避 RSTUDP 无响应你怎么判端口关闭”你写完一个Socket.connect()就敢叫端口扫描器那只是 TCP 全连接扫描Connect Scan连防火墙都骗不过——目标一收到 SYN-ACK 就立刻回 RST日志里明晃晃记一笔还可能触发 IDS 告警。而这份基于 Java 实现的 TCP/UDP 端口扫描器计算机网络课程设计是真正在 Win10 Eclipse 环境下完整跑通的工程级小系统它不只做 connect()而是封装了 TCP SYN 半开探测逻辑通过java.net.Socket的超时控制模拟、UDP 端口探测发空包ICMP Port Unreachable 判定、支持 1–200 线程并发、结果实时渲染到 Swing 界面、还能导出 CSV。它不是教科书伪代码是能让你在宿舍电脑上扫自己本机127.0.0.1亲眼看到80HTTP、443HTTPS、53DNS被标绿22SSH标灰Windows 默认没开65535被判定为 closed 的实打实工具。适合刚学完《计算机网络》第 3 章“运输层”的同学——你得亲手敲new Socket(host, port)也得理解为什么 UDP 扫描必须配 ICMP 监听更得明白setSoTimeout(100)不是随便设的数字而是平衡准确率与耗时的血泪参数。它不替代 nmap但能让你把 TCP 三次握手、UDP 无连接特性、线程池调度、Swing 事件循环全串成一条链。2. 从源码结构到核心逻辑拆解 PortScan-master.zip 里的六个关键模块为什么用 Swing 而不用 JavaFX为什么 UDP 扫描必须另起线程监听 ICMP2.1 源码包结构还原.classpath和.project暴露了 Eclipse 工程本质src下藏着四类核心类打开PortScan-master.zip别急着 run。先看根目录.classpath明确声明依赖 JRE System LibraryJavaSE-1.8.project写着namePortScan/namebin/是编译输出src/才是战场。src下实际只有 4 个 Java 文件但分工极清晰Main.javaSwing 主窗体入口含 IP 输入框、端口范围滑块、线程数 Spinner、Start/Exit 按钮、结果 JTextAreaPortScanner.java核心扫描调度器持有一个ExecutorService线程池按端口区间分片提交ScanTaskScanTask.java实现Runnable每个实例负责扫描连续 10 个端口可配置内含tcpScan()和udpScan()两个静态方法ICMPListener.java唯一非标准库类——它用java.net.DatagramSocket绑定0.0.0.0:0专门接收 ICMP Type 3 Code 3Port Unreachable报文这是 UDP 扫描判定 closed 的唯一可靠依据。提示Readme.md里写的“图像显示功能”就是指Main.java中JFrameJScrollPaneJTextArea的组合没有用 JavaFX因为课程设计明确要求“Eclipse Swing”且 Swing 对新手更透明——你能直接看到textArea.append(Port port is open\n)这种裸露逻辑而不是被 FXML 和 Controller 层绕晕。2.2 TCP 扫描不是简单 connect()而是带超时控制的半开式探测ScanTask.tcpScan()的关键不在new Socket()而在三重控制public static boolean tcpScan(String host, int port) { Socket socket null; try { socket new Socket(); // 关键1禁用 Nagle 算法避免延迟合并小包 socket.setTcpNoDelay(true); // 关键2设置连接超时为 100ms而非默认阻塞 socket.connect(new InetSocketAddress(host, port), 100); return true; // 连接成功 → open } catch (SocketTimeoutException e) { return false; // 超时 → filtered防火墙拦截 } catch (IOException e) { if (e.getMessage().contains(Connection refused)) { return false; // RST 返回 → closed } return false; // 其他异常一律视为 closed } finally { if (socket ! null socket.isConnected()) { try { socket.close(); } catch (IOException ignored) {} } } }这段代码的深意在于socket.connect(..., 100)是TCP 半开扫描的 Java 替代方案——它发 SYN等 SYN-ACK若超时则认为 filtered若收到 RST则IOException抛出Connection refused判为 closed若收到 SYN-ACK 并完成三次握手则返回 trueopen。setTcpNoDelay(true)防止 TCP 栈缓存小包确保每个探测包独立发出这对高并发扫描至关重要。finally中的socket.close()必须执行否则线程池里大量 socket 处于ESTABLISHED状态很快耗尽本地端口TIME_WAIT 占用。2.3 UDP 扫描发包只是开始ICMP 监听才是灵魂UDP 扫描的难点从来不是发包而是如何判定“无响应”。ScanTask.udpScan()只做一件事public static boolean udpScan(String host, int port) { DatagramSocket socket null; try { socket new DatagramSocket(); socket.setSoTimeout(1000); // UDP 探测超时设为 1s比 TCP 更长 // 发送空 UDP 包0字节 payload DatagramPacket packet new DatagramPacket(new byte[0], 0, InetAddress.getByName(host), port); socket.send(packet); // 此处不 recv因为正常 UDP 服务不回包只等 ICMP return true; // 发包成功即返回 true实际状态由 ICMPListener 判定 } catch (Exception e) { return false; } finally { if (socket ! null) socket.close(); } }真正干活的是ICMPListener.java——它启动一个独立线程持续recvfrom()public class ICMPListener implements Runnable { private final BlockingQueueInteger closedPorts; // 存放被 ICMP 判定为 closed 的端口 public void run() { try (DatagramSocket icmpSocket new DatagramSocket()) { byte[] buffer new byte[1024]; DatagramPacket packet new DatagramPacket(buffer, buffer.length); while (!Thread.currentThread().isInterrupted()) { icmpSocket.receive(packet); // 阻塞等待 ICMP // 解析 ICMP Type 3 Code 3Port Unreachable if (isPortUnreachable(packet.getData(), packet.getLength())) { int port extractPortFromICMP(packet.getData()); closedPorts.offer(port); } } } catch (IOException e) { // 日志记录但不中断监听 } } }这个设计意味着UDP 扫描结果不是udpScan()返回的true/false而是ICMPListener收到 ICMP 后往closedPorts队列里塞端口号主扫描线程再定期轮询该队列更新 UI。这是课程设计里最体现“网络协议栈理解深度”的部分——你得知道 UDP 无连接所以没响应不等于 closed你也得知道操作系统内核在收到发往 closed UDP 端口的包时会自动生成 ICMP Port Unreachable你还得会用DatagramSocket去捕获它。2.4 Swing 线程安全为什么textArea.append()必须用SwingUtilities.invokeLater()Main.java里所有textArea.append(...)都被包在SwingUtilities.invokeLater(() - { textArea.append(Port port is open\n); });原因直白ScanTask在ExecutorService线程池中运行这些线程不是 Event Dispatch ThreadEDT。Swing 组件如JTextArea只能由 EDT 安全更新。若直接调用append()轻则 UI 卡死、文字错乱重则抛IllegalStateException。invokeLater()把更新请求排队到 EDT 消息队列确保线程安全。这是课程设计里最容易被忽略、却导致“扫描结果不显示”的玄学坑——很多同学改完逻辑发现 UI 没反应查半天才发现漏了这行。3. 编译、运行与参数调优Eclipse 导入后必做的三件事线程数设 50 为什么比 200 更快3.1 Eclipse 导入实操别点 “Import as General Project”要选 “Existing Projects into Workspace”直接解压PortScan-master.zip到磁盘某路径如D:\portscan打开 Eclipse →File → Import → General → Existing Projects into Workspace→Select root directory选D:\portscan→ 勾选PortScan项目 → Finish。此时若报错The project was not built since its build path is incomplete说明 JRE 未正确关联右键项目 →Properties → Java Build Path → Libraries → Add Library → JRE System Library → Workspace default JRE (JavaSE-1.8)。确认src下四个.java文件无红叉bin下有对应.class文件即可右键Main.java → Run As → Java Application。3.2 参数设定的物理意义起始端口 1、结束端口 1024、线程数 50 的真实依据课程设计文档说“起始端口和结束端口应为 0–65535”但实战中绝不能扫全端口。理由有三参数推荐值物理依据风险起始端口1端口 0 是保留端口不可用1–1023 是知名端口HTTP/FTP/SSH/DNS最可能开放设为 0 会抛IllegalArgumentException结束端口10241024–49151 是注册端口49152–65535 是动态端口扫前 1024 个已覆盖 95% 服务设为 65535 会导致扫描耗时 2 小时单线程线程数50Windows 默认net.ipv4.ip_local_port_range是 32768–65535可用临时端口约 32768 个50 线程并发 ≈ 50×100ms 5s/批平衡速度与端口耗尽设为 200 会快速占满本地端口后续连接抛java.net.BindException: Address already in use注意ScanTask默认每线程扫 10 个端口BATCH_SIZE 10所以扫1–1024共 1024 个端口需1024 / 10 103个任务线程数 50 时实际并发 50剩余 53 个任务排队——这是合理的负载。3.3 结果验证用netstat -ano和nc交叉验证扫描结果扫描完成后别信 UI 显示。必须本地验证查本机开放端口Win10 打开 CMD执行netstat -ano | findstr :80 netstat -ano | findstr :443 netstat -ano | findstr :53若看到LISTENING状态且 PID 对应svchost.exe或chrome.exe说明端口真开。用 nc 测试 UDP# 开启一个 UDP 服务Python 快速起 python -c import socket; ssocket.socket(socket.AF_INET,socket.SOCK_DGRAM); s.bind((0.0.0.0,9999)); print(UDP server on 9999) # 然后扫描 9999 端口应显示 open再关掉 Python 进程再扫应显示 closed因 ICMP 返回TCP 过滤测试# 用 Windows 防火墙临时屏蔽 8080 netsh advfirewall firewall add rule nameBlock 8080 dirin actionblock protocolTCP localport8080 # 扫描 8080应显示 filtered超时而非 closed这三步验证能帮你区分open/closed/filtered三种状态也是答辩时老师最爱问的“你凭什么说这个端口是 filtered 而不是 closed”4. 避坑指南五个真实翻车现场从“UI 不刷新”到“扫出一堆 65535”血泪经验全在这4.1 现象点击 Start 后 UI 卡死进度条不动textArea无任何输出原因ScanTask中tcpScan()或udpScan()未设超时或ICMPListener线程未启动。Java 默认Socket.connect()是无限阻塞若目标主机不存在或防火墙丢包线程永久挂起EDT 被饿死。解决检查tcpScan()是否有socket.connect(addr, timeout)udpScan()是否有socket.setSoTimeout()确认Main.java中startButton.addActionListener里是否调用了new Thread(new ICMPListener(...)).start()。4.2 现象UDP 扫描结果全是open哪怕目标端口根本没服务原因ICMPListener未运行或 Windows 防火墙阻止了 ICMP 入站。UDP 探针发出后无 ICMP 返回ScanTask默认返回trueUI 就标 green。解决CMD 执行netsh advfirewall firewall add rule nameAllow ICMP dirin actionallow protocolicmpv4在Main.java初始化时加日志System.out.println(ICMPListener started: icmpListener.isAlive());用 Wireshark 抓包过滤icmp icmp.type 3 icmp.code 3确认是否有 ICMP Port Unreachable 报文。4.3 现象扫127.0.0.1时端口1–1024全显示closed但netstat明明看到80在 LISTENING原因tcpScan()中InetSocketAddress构造时 IP 字符串错误。常见错误是传入localhost而非127.0.0.1某些 JDK 版本下localhost解析为::1IPv6而本机 HTTP 服务只监听 IPv4。解决强制用 IPv4 地址InetSocketAddress addr new InetSocketAddress(InetAddress.getByName(127.0.0.1), port);。4.4 现象线程数设 100扫描速度反而比 20 慢CPU 占用 100%原因线程切换开销超过并行收益。每个ScanTask创建Socket/DatagramSocket需系统调用100 线程同时争抢本地端口、文件描述符、CPU 时间片内核调度压力剧增。解决用ThreadPoolExecutor替代Executors.newFixedThreadPool()设置corePoolSize50maximumPoolSize50keepAliveTime0避免线程动态伸缩。课程设计代码中直接改PortScanner.java的executor Executors.newFixedThreadPool(threadCount);为固定大小。4.5 现象保存结果 CSV 后用 Excel 打开中文乱码全是方块原因JavaFileWriter默认用平台编码Windows 是 GBK而 Excel 默认读 UTF-8。解决改Main.java中保存逻辑// 原代码错 try (FileWriter writer new FileWriter(result.csv)) { ... } // 正确写法指定 UTF-8 try (OutputStreamWriter writer new OutputStreamWriter( new FileOutputStream(result.csv), StandardCharsets.UTF_8)) { ... }并在 CSV 第一行加 BOM 头writer.write(\uFEFF);。5. 进阶技巧把扫描器变成可复用的 SDK提取PortScanner.scan()方法封装成 Maven 依赖5.1 提取核心扫描逻辑剥离 Swing做成纯函数式 API课程设计的PortScanner.java是面向 UI 的但它的扫描能力完全可以抽象为独立模块。新建一个PortScanUtils.javapublic class PortScanUtils { // 返回 Mapport, PortStatePortState 是枚举 OPEN/CLOSED/FILTERED/UNKNOWN public static MapInteger, PortState scan(String host, int startPort, int endPort, int threadCount, int tcpTimeoutMs, int udpTimeoutMs) { ExecutorService executor Executors.newFixedThreadPool(threadCount); MapInteger, PortState results new ConcurrentHashMap(); // 分片每线程扫 (endPort-startPort)/threadCount 个端口 int batchSize Math.max(1, (endPort - startPort 1) / threadCount); ListFuture? futures new ArrayList(); for (int i startPort; i endPort; i batchSize) { int from i; int to Math.min(i batchSize - 1, endPort); futures.add(executor.submit(() - { for (int port from; port to; port) { PortState state tcpScan(host, port, tcpTimeoutMs); if (state PortState.OPEN || state PortState.FILTERED) { // UDP 扫描只对疑似 open 的端口做二次验证 if (udpScan(host, port, udpTimeoutMs)) { state PortState.OPEN_UDP; } } results.put(port, state); } })); } // 等待全部完成 futures.forEach(f - { try { f.get(); } catch (Exception e) {} }); executor.shutdown(); return results; } private static PortState tcpScan(String host, int port, int timeoutMs) { // 复用原 ScanTask.tcpScan() 逻辑仅返回 PortState } private static boolean udpScan(String host, int port, int timeoutMs) { // 复用原 ScanTask.udpScan() 逻辑 } }这样其他项目只需引入这个类一行代码调用MapInteger, PortState result PortScanUtils.scan(192.168.1.1, 1, 1024, 20, 100, 1000); result.entrySet().stream() .filter(e - e.getValue() PortState.OPEN) .forEach(e - System.out.println(Open port: e.getKey()));5.2 打包成 Maven 依赖pom.xml关键配置与发布流程要让团队其他人mvn dependency:copy-dependencies就能用需标准 Maven 结构portscan-sdk/ ├── pom.xml ├── src/ │ └── main/ │ └── java/ │ └── com/example/portscan/ │ ├── PortScanUtils.java │ └── PortState.java └── target/ └── portscan-sdk-1.0.0.jarpom.xml必须包含project modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdportscan-sdk/artifactId version1.0.0/version packagingjar/packaging properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId version3.3.0/version configuration archive manifest addClasspathtrue/addClasspath /manifest /archive /configuration /plugin plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-source-plugin/artifactId version3.2.1/version executions execution idattach-sources/id goalsgoaljar-no-fork/goal/goals /execution /executions /plugin /plugins /build /project执行mvn clean package生成target/portscan-sdk-1.0.0.jar再mvn install推到本地 Maven 仓库。同事项目pom.xml加dependency groupIdcom.example/groupId artifactIdportscan-sdk/artifactId version1.0.0/version /dependency即可调用——这才是课程设计升维成工程资产的关键一步。5.3 真实场景迁移从扫本机到扫 Docker 容器docker inspect获取容器 IP 后直接扫课程设计扫127.0.0.1是入门但生产环境常扫容器。例如启动一个 Nginxdocker run -d -p 8080:80 --name mynginx nginx获取其内部 IPdocker inspect mynginx | grep IPAddress | head -1 | awk -F {print $4} # 输出类似172.17.0.2然后 Java 里调用MapInteger, PortState dockerPorts PortScanUtils.scan(172.17.0.2, 1, 1024, 10, 100, 1000); // 结果会显示 80 是 OPEN因为容器内 Nginx 监听 80注意宿主机扫容器 IP走的是 Docker 的 bridge 网络无需-p端口映射但扫localhost:8080是走 NAT结果不同。这个细节我在第一次部署微服务时翻过车——扫localhost:8080显示 open扫容器 IP 却显示 closed折腾半小时才想起网络模型差异。从那以后我每次写网络工具都强制走三步验证ping通 IP、telnet ip port看 TCP 通不通、nc -u ip port看 UDP 通不通再跑扫描器。希望帮到你。本文还有配套的精品资源点击获取