news 2026/9/23 19:29:31

Java端口扫描器:TCP/UDP双协议实现与Swing线程解耦

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java端口扫描器:TCP/UDP双协议实现与Swing线程解耦

简介:这是一份面向计算机网络课程学习者与初阶开发者的Java端口扫描器实践项目,聚焦TCP/UDP协议层探测能力训练,适用于课程设计、工程实训及毕设选题参考。资源包共12个文件,含2个核心Java源码(实现多线程扫描逻辑)、3个编译后class文件、2份Markdown文档(含README说明与使用指南)、1张界面截图PNG,以及Eclipse项目配置文件(.project、.classpath等),整体仅96KB,轻量易部署。已有131人学习下载,体现其作为入门级网络编程案例的实用价值。读者可直接运行复现完整扫描流程:输入目标IP与端口范围、设置线程数(1–200)、实时获取开放端口列表并支持结果保存;代码结构清晰,含图形化界面与模块化功能封装,便于理解Socket通信、线程池调度及Swing UI设计等关键知识点。

1. 这不是玩具级扫描器:一个能跑通 TCP SYN+UDP ICMP 回显、支持线程数精细调控的 Java 端口扫描器(课程设计级但可真实复现)

你试过在 Win10 上用 Java 写一个端口扫描器,结果刚起 5 个线程就卡死、扫到 80 端口却显示“closed”、UDP 扫描永远返回 timeout——不是代码写错了,是缺了三样东西:TCP 连接超时的分级控制逻辑、UDP 响应包的 ICMP 错误码解析机制、以及 Swing 界面线程与扫描线程的真正解耦。这个 PortScan-master 项目,就是把这三块硬骨头啃下来的课程设计成品:它不依赖任何第三方网络库(纯 JDKjava.net+java.nio.channels),完整实现 TCP Connect 扫描(含三次握手状态捕获)、UDP 端口探测(结合 ICMP port unreachable 判定)、多线程并发控制(线程池 + 任务队列 + 扫描进度回调),且所有参数(IP、端口范围、线程数)都在 GUI 实时可调、结果实时刷新。它适合计算机网络课设答辩、Java 多线程实战入门、甚至作为渗透测试原理教学的可视化教具——因为你能看到每一行socket.connect()的阻塞时间、每一条DatagramSocket.receive()的等待逻辑、每一个 SwingSwingUtilities.invokeLater()如何避免界面冻结。别被“课程设计”四个字骗了,它的 TCP 扫描模块已实测通过127.0.0.1:3306(MySQL)、192.168.1.1:80(家用路由器)、localhost:22(SSH);UDP 模块在 Wireshark 下验证过 ICMP Type 3 Code 3(Port Unreachable)的精准捕获。如果你正卡在“Java 怎么做非阻塞 UDP 扫描”或“Swing 更新列表时总抛IllegalStateException”,这份源码就是你的后悔药。


2. 从零拆解核心模块:TCP Connect 扫描器的三次握手状态机与超时分级策略

2.1 TCP 扫描不是简单new Socket(host, port):为什么必须手动控制 connect() 超时?

Java 原生Socket.connect(SocketAddress, timeout)在 timeout 后会抛出SocketTimeoutException,但问题在于:这个 timeout 是整个连接建立过程的总耗时,无法区分“SYN 发出后没回 SYN-ACK”和“SYN-ACK 收到但 ACK 未确认”的状态。而真实网络中,防火墙可能丢弃 SYN 包(无响应)、也可能放行 SYN 但拦截 SYN-ACK(导致客户端一直等)。PortScan-master 的解决方案是:SocketChannel配合Selector实现非阻塞 connect,并监听 OP_CONNECT 事件——这才是逼近 TCP 协议栈底层的写法。

// src/scan/TcpScanner.java 关键片段 SocketChannel channel = SocketChannel.open(); channel.configureBlocking(false); InetSocketAddress address = new InetSocketAddress(ip, port); SelectionKey key = channel.register(selector, SelectionKey.OP_CONNECT); channel.connect(address); // 非阻塞发起连接 // 在主循环中轮询 selector while (selector.select(100) > 0) { Iterator<SelectionKey> keys = selector.selectedKeys().iterator(); while (keys.hasNext()) { SelectionKey k = keys.next(); keys.remove(); if (k.isConnectable()) { SocketChannel ch = (SocketChannel) k.channel(); try { if (ch.finishConnect()) { // 成功完成三次握手 openPorts.add(port); k.cancel(); ch.close(); } else { // 连接失败(如被拒绝) k.cancel(); ch.close(); } } catch (IOException e) { // Connection refused / No route to host 等明确错误 k.cancel(); ch.close(); } } } }

逻辑说明:这段代码绕开了Socket的黑匣子,直接用 NIO 暴露 TCP 状态。ch.finishConnect()返回 true 表示三次握手完成(SYN→SYN-ACK→ACK 全部成功),此时端口开放;若抛出IOException(如Connection refused),说明目标端口有服务但拒绝连接(仍属开放);若selector.select()超时后finishConnect()仍返回 false,则判定为 filtered(被防火墙过滤)。
参数说明selector.select(100)中的100是毫秒级轮询间隔,决定了扫描精度——值越小响应越快但 CPU 占用越高;实际项目中建议设为 50~200ms,平衡速度与负载。

2.2 线程池如何避免“创建 100 个 Socket 导致系统资源耗尽”?

课程设计常犯的错误是:为每个端口新建一个Thread,结果线程数一设大就 OOM。PortScan-master 采用ThreadPoolExecutor+LinkedBlockingQueue组合,关键在于队列容量与拒绝策略的协同设计

// src/scan/PortScanner.java 初始化线程池 private static final int MAX_THREADS = 200; private static final int QUEUE_CAPACITY = 1000; // 队列最大容纳 1000 个待扫描端口任务 private static final ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, // 核心线程数 = 用户设置的线程数(如 50) MAX_THREADS, // 最大线程数 = 200(防突发) 60L, TimeUnit.SECONDS, // 空闲线程存活时间 new LinkedBlockingQueue<>(QUEUE_CAPACITY), // 有界队列! new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由主线程执行任务 );

为什么用有界队列?无界队列(如new LinkedBlockingQueue<>())会导致任务无限堆积,内存爆满;有界队列强制触发拒绝策略。这里选CallerRunsPolicy是血泪经验:当队列满、线程达上限时,新任务由 Swing 主线程执行——虽然会短暂卡 UI,但绝对不崩溃、不丢任务、不 OOM,比AbortPolicy(直接抛异常)或DiscardPolicy(静默丢弃)更适合课程设计场景。
参数安全边界corePoolSize来自用户输入(GUI 输入框),但代码中做了校验:Math.max(1, Math.min(200, userThreads)),确保不会传入 0 或超大值。

2.3 Swing 界面如何实时更新扫描结果而不炸掉?

新手常写listModel.addElement(port)在工作线程里,结果抛IllegalStateException: safe to modify only from AWT event dispatch thread。PortScan-master 的解法是:所有 UI 更新必须走SwingUtilities.invokeLater(),且封装成原子操作

// src/gui/MainFrame.java 中的回调方法 public void addOpenPort(int port) { SwingUtilities.invokeLater(() -> { DefaultListModel<Integer> model = (DefaultListModel<Integer>) listOpenPorts.getModel(); if (!model.contains(port)) { // 避免重复添加 model.addElement(port); // 同步滚动到底部 listOpenPorts.ensureIndexIsVisible(model.size() - 1); } }); } // src/scan/TcpScanner.java 中调用 if (isPortOpen) { mainFrame.addOpenPort(port); // 跨线程安全调用 }

关键细节invokeLater()是异步投递,不阻塞扫描线程;model.contains(port)防止同一端口因重试被多次添加;ensureIndexIsVisible()让新端口自动滚动可见——这些才是课程设计答辩时老师会追问的“为什么这么写”。


3. UDP 扫描的玄学破解:ICMP 错误码解析与“无响应即开放”的反直觉逻辑

3.1 UDP 扫描为什么不能只看DatagramSocket.receive()是否超时?

TCP 有连接状态,UDP 是无连接的。new DatagramSocket().send(packet)发送后,如果目标端口无服务,操作系统会返回 ICMP Destination Unreachable(Type=3, Code=3);如果有服务,通常不回复任何包。但 Java 的DatagramSocket默认无法捕获 ICMP 错误——它只管 UDP 层。PortScan-master 的破局点是:启用 socket 的SO_TIMEOUT,并在 catchSocketTimeoutException后,结合 ICMP 报文分析做二次判定

// src/scan/UdpScanner.java 核心逻辑 DatagramSocket socket = new DatagramSocket(); socket.setSoTimeout(2000); // 设置 2 秒超时 try { socket.send(packet); // 关键:尝试接收响应(哪怕服务不回包,OS 可能发 ICMP) byte[] buffer = new byte[1024]; DatagramPacket response = new DatagramPacket(buffer, buffer.length); socket.receive(response); // 此处可能收到应用层响应,也可能被 ICMP 中断 // 收到包 → 端口开放(或有服务) openPorts.add(port); } catch (SocketTimeoutException e) { // 超时 → 两种可能:1. 端口关闭(OS 发 ICMP)2. 端口开放但服务不回包 // 需要抓包验证 ICMP,但课程设计简化处理:标记为 "open|filtered" uncertainPorts.add(port); } catch (IOException e) { // 如果 e.getMessage() 包含 "Connection refused",说明收到 ICMP Port Unreachable if (e.getMessage().toLowerCase().contains("connection refused")) { // 明确判定为 closed closedPorts.add(port); } else { uncertainPorts.add(port); } } finally { socket.close(); }

为什么Connection refused能判断 ICMP?DatagramSocket尝试向一个无服务的 UDP 端口发送数据时,Linux/Win10 内核会向该 socket 抛出IOException,message 为"Connection refused"——这正是内核将 ICMP Type 3 Code 3 映射到 Java 异常的结果。这是 JDK 对底层 ICMP 的隐式翻译,无需 JNI 或 pcap 库。
参数陷阱setSoTimeout(2000)的 2000ms 是经验阈值。太短(如 500ms)会误判高延迟网络下的开放端口;太长(如 5000ms)则扫描慢。课程设计推荐 1500~2500ms。

3.2 UDP 扫描结果为何分三级:open / closed / open|filtered?

PortScan-master 的 GUI 结果面板有三列:Open PortsClosed PortsUncertain Ports。这不是偷懒,而是严格遵循 Nmap 的分类逻辑:

类型判定条件典型场景
Openreceive()成功收到数据包DNS(53)、DHCP(67/68)、SNMP(161)等主动响应服务
ClosedIOExceptionmessage 含"Connection refused"目标主机存在,但该 UDP 端口无服务监听
Open|FilteredSocketTimeoutException且无其他异常防火墙丢弃 UDP 包(无 ICMP 返回),无法区分开放或过滤

教学价值:这个三级分类恰恰是计算机网络课程的核心考点——它暴露了 UDP 协议的不可靠性本质,也解释了为什么nmap -sU扫描比-sT慢且结果模糊。在答辩时,你可以指着Uncertain Ports列说:“老师,这正是谢希仁《计算机网络》第 5 版 P189 提到的‘UDP 扫描的固有局限性’。”

3.3 如何验证 UDP 扫描结果的真实性?Wireshark 抓包对照表

光看程序输出不够,必须用 Wireshark 验证。以下是 PortScan-master 扫描127.0.0.1:53(本地 DNS)时的抓包对照:

时间Wireshark 显示PortScan-master 日志逻辑对应
0.000sUDP 127.0.0.1:50234 → 127.0.0.1:53Sending UDP probe to port 53程序发出探测包
0.001sUDP 127.0.0.1:53 → 127.0.0.1:50234Received response on port 53receive()成功 →Open
0.005sICMP 127.0.0.1 → 127.0.0.1: Destination unreachable (Port unreachable)IOException: Connection refused on port 54Connection refusedClosed

实操提示:在 Wireshark 过滤栏输入ip.addr == 127.0.0.1 && (udp || icmp)即可聚焦本地 UDP/ICMP 流量。注意观察UDP包的Length字段(通常为 0,因探测包无 payload)和ICMPType/Code字段——这才是判定 closed 的黄金证据。


4. 避坑指南:五个让课程设计当场翻车的致命细节与血泪修复方案

4.1 现象:点击“开始扫描”后界面完全冻结,CPU 占用 100%,5 分钟后才弹出“扫描完毕”

原因:GUI 线程(AWT Event Dispatch Thread)被阻塞。常见于新手把TcpScanner.scan()直接写在button.addActionListener()里,且未用SwingWorker或线程池,导致 Swing 主线程陷入while (port <= endPort)循环。

解决

  • ✅ 正确做法:button.addActionListener(e -> new ScanTask().execute());,其中ScanTask extends SwingWorker<Void, String>,在doInBackground()中调用扫描逻辑,在process()中更新 UI。
  • ❌ 错误写法:executor.submit(() -> { scan(); });但未用SwingUtilities.invokeLater()更新界面,导致listModel.addElement()抛异常并静默失败。

4.2 现象:扫描192.168.1.1(路由器)时,TCP 扫描显示 22、80 开放,但 UDP 扫描全为Uncertain

原因:家用路由器默认禁用 ICMP Port Unreachable 响应(安全策略),导致 UDP 探测包发出后既无应用层响应,也无 ICMP 错误,全部超时归为Uncertain

解决

  • ✅ 方案一:改用nmap -sU --max-retries 3 192.168.1.1对比验证,确认是设备策略而非代码问题;
  • ✅ 方案二:在代码中增加提示:“UDP 扫描结果受目标主机 ICMP 策略影响,建议结合 TCP 扫描交叉验证”;
  • ❌ 不要强行调低setSoTimeout()到 500ms——只会增加误判率。

4.3 现象:扫描127.0.0.1时,80 端口显示closed,但浏览器能正常访问http://localhost

原因127.0.0.1的 80 端口服务(如 Apache)可能绑定在::1(IPv6)而非0.0.0.0(IPv4),而 JavaInetSocketAddress默认解析为 IPv4 地址,导致连接被拒绝。

解决

  • ✅ 强制指定 IPv4:InetAddress.getByName("127.0.0.1")替代InetAddress.getByName("localhost")
  • ✅ 或在SocketChannel.open()后,channel.bind(new InetSocketAddress(InetAddress.getByName("0.0.0.0"), 0))显式绑定任意 IPv4 地址。

4.4 现象:保存结果文件时,中文路径报java.io.FileNotFoundException: ??.txt (系统找不到指定的文件)

原因FileWriter默认使用平台默认编码(Windows 是 GBK),但JFileChooser返回的路径含中文,若文件名含中文且未指定编码,写入时乱码导致路径无效。

解决

  • ✅ 正确写法:new FileWriter(file, StandardCharsets.UTF_8)
  • ✅ 并在保存前校验路径:if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
  • ❌ 不要用new FileWriter(file)——这是 Windows 下中文路径的隐形炸弹。

4.5 现象:Eclipse 运行时报错Exception in thread "main" java.lang.NoClassDefFoundError: javafx/application/Application

原因:项目.classpath文件引用了 JavaFX 库,但 JDK 11+ 已移除 JavaFX,且 Eclipse 默认 JRE 未配置 JavaFX SDK。

解决

  • ✅ 删除.classpath<classpathentry kind="lib" path="lib/jfxrt.jar"/>行;
  • ✅ 确认src/gui/MainFrame.java使用的是javax.swing.*(Swing),而非javafx.scene.*(JavaFX);
  • ✅ 若真需 JavaFX,下载 OpenJFX 并在 Eclipse → Properties → Java Build Path → Libraries → Add Library → User Library 中添加。

5. 进阶技巧:用扫描日志反推网络拓扑,以及三个让答辩加分的实操验证法

5.1 从扫描日志发现隐藏的网络结构:端口分布模式即拓扑指纹

PortScan-master 生成的result.txt不只是端口列表,它是网络设备的“X 光片”。我带学生做课程设计时,让他们对三类目标扫描并对比日志:

目标类型典型开放端口模式拓扑含义教学价值
家用路由器(192.168.1.1)TCP: 22, 80, 5000; UDP: 53, 1900 (UPnP)LAN 边界网关,提供 NAT、DHCP、Web 管理理解 SOHO 设备协议栈分层
Windows 10 主机(192.168.1.100)TCP: 135, 139, 445, 3389; UDP: 137, 138SMB/CIFS 文件共享、远程桌面、NetBIOS关联《操作系统》进程通信章节
Linux 服务器(192.168.1.200)TCP: 22, 80, 443, 3306; UDP: 53, 67SSH、Web、数据库、DNS、DHCP 服务验证 TCP/IP 协议族服务端口标准分配

操作步骤

  1. 用 PortScan-master 扫描三台设备(确保在同一局域网);
  2. result.txt导出,用 Excel 按“目标 IP”分组,统计每类端口出现频次;
  3. 制作热力图:横轴为端口号(0~1024),纵轴为设备类型,色块深浅表示开放概率。
    你会发现:135-139端口几乎只出现在 Windows 日志中,3306只在 Linux 服务器出现——这就是协议栈实现差异的实证。

5.2 三个答辩必演的验证实验:让老师当场点头的硬核操作

实验一:TCP 扫描的“三次握手”可视化验证
  • 步骤:启动 PortScan-master,设置 IP=127.0.0.1,端口范围21-23,线程数1
  • 同时打开 Wireshark,过滤tcp.port == 21 || tcp.port == 22 || tcp.port == 23
  • 点击扫描,观察 Wireshark 中每个端口的 TCP 流:21(FTP)应有完整三次握手;22(SSH)同理;23(Telnet)若未开启,则只有 SYN 包发出,无 SYN-ACK 返回。
  • 答辩话术:“老师,您看这里——21 端口的Seq=0, Ack=0, Flags=[S]是 SYN,紧接着Seq=0, Ack=1, Flags=[SA]是 SYN-ACK,最后Seq=1, Ack=1, Flags=[A]是 ACK,三次握手完成,程序判定开放。而 23 端口只有第一个包,证明服务未启用。”
实验二:UDP 扫描的 ICMP 错误码捕获演示
  • 步骤:关闭本机 DNS 服务(services.msc→ Stop “DNS Client”);
  • 扫描127.0.0.1:53,观察 PortScan-master 输出Closed Ports中是否包含 53;
  • Wireshark 过滤icmp && ip.src == 127.0.0.1,找到对应 ICMP 包,展开Internet Control Message ProtocolType: 3 (Destination unreachable)Code: 3 (Port unreachable)
  • 答辩话术:“这个 ICMP Type 3 Code 3 就是 UDP 扫描判定 closed 的依据,它由操作系统内核生成,不是应用层协议,所以 Java 无需解析原始 IP 包——JDK 已帮我们映射到异常消息。”
实验三:线程数对扫描速度的量化影响测试
  • 步骤:固定目标127.0.0.1,端口范围1-1024,分别用线程数1/10/50/100扫描;
  • 记录每次耗时(程序日志末尾的Scan completed in X ms);
  • 绘制折线图:横轴线程数,纵轴耗时(ms),你会看到1→10速度飙升,10→50增速放缓,50→100几乎持平甚至变慢(线程调度开销反超收益)。
  • 答辩话术:“这验证了 Amdahl 定律——并行加速有上限。当线程数超过 CPU 核心数,上下文切换成本吞噬了并发收益。我们的线程数上限设为 200,是为应对 I/O 密集型场景(网络延迟主导),而非 CPU 密集型。”

5.3 从那以后我每次重构网络工具,都强制走一遍“三屏验证法”

所谓三屏验证法,是我带学生做课程设计时定下的铁律:第一屏看代码逻辑(是否符合 RFC)、第二屏看 Wireshark 抓包(是否符合协议行为)、第三屏看终端日志(是否符合预期输出)。比如改 UDP 扫描超时时间,我不只改setSoTimeout(2000),还要:

  • 第一屏确认DatagramSocket创建、发送、接收三步无遗漏;
  • 第二屏在 Wireshark 看2000ms内是否真有 ICMP 返回;
  • 第三屏检查result.txtUncertain Ports数量是否随超时值增大而减少。
    这看似繁琐,但能避开 90% 的“代码跑通但协议不对”的玄学 bug。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/23 19:14:53

高级人工智能训练师实战:从数据工程到模型评测的完整路径

简介&#xff1a;《高级人工智能训练师》是一份聚焦店小蜜智能客服配置优化的PDF学习资料&#xff0c;面向电商客服主管、AI训练师及店铺运营人员&#xff0c;尤其适合准备高级人工智能训练师认证或正为店小蜜后台调优发愁的读者。资源包共1个PDF文件&#xff0c;容量仅861KB&a…

作者头像 李华