☰
Java端口扫描课设源码详解:多线程+Swing+TCP/ICMP探测
2026/10/1 5:52:53 网站建设 项目流程

简介:端口扫描是网络安全评估与运维中的基础技术,通过探测目标主机的开放端口来识别服务与潜在风险。实现时通常需结合ICMP存活性探测与TCP连接检测,并利用多线程并发提升效率。在实际工程中,如何平衡扫描速度与资源占用、如何避免UI卡顿,是开发者常面临的挑战。Java凭借跨平台特性与丰富的网络、并发及Swing GUI库,常被用于教学与课设实践。本文以一个完整的Java端口扫描课设为例,解析其源码结构、多线程线程池设计、IP段解析及界面交互逻辑,并总结常见坑点与优化方向,帮助学习者快速掌握可落地的实现思路。

1. 这份 Java 端口扫描课设资源值不值得下:先看它解决什么问题

端口扫描是网络安全课设里几乎绕不开的一个题目,但真正动手写的时候才发现坑不少:要处理 TCP 和 ICMP 两种探测方式、要判断主机是否在线、还要应付多线程同时扫多台主机不卡界面。这套基于 Java 的端口扫描软件,正好把这些全部打包好了——IDEA 直接打开就能跑,登录账号 admin、密码 123456,自带图形界面、扫描进度条、异常告警窗口,还附一份写好的实验报告。适合正在做网安课设但又不想从零开始搭的学生,也适合想快速看一遍“端口扫描 + 多线程 + Swing 界面”完整代码结构的 Java 学习者。下面我按实际拆项目的顺序,把源码结构、扫描原理、UI 逻辑和踩过的坑一条条讲清楚。

2. 拆解源码与核心探测机制:src 里到底放了哪些关键类

2.1 先看工程全景:IDEA 打开前认识这几个文件与目录

解压之后你会看到一堆东西,不少第一次接触的人会懵。我用表格把每个文件的用途列清楚,你心里就有底了。

文件/目录作用说明
scan.imlIDEA 模块文件识别项目用的,别删,打开工程靠它
.ideaIDEA 工程配置目录包含运行配置、编码设置等,整个目录拷走也不影响
src源码目录所有 Java 代码都在这,后面重点拆它
out编译输出目录IDEA 编译后生成的 class 文件,工具会自动生成
网安实验报告.doc课设报告包含设计思路、核心代码、截图,答辩前改一改就能交
账号密码.txt登录凭据说明写着 admin / 123456

用 IDEA 打开的时候选Open,定位到解压后的目录,选scan.iml或直接选根目录都行。常见做法是等 IDEA 右下角索引跑完,然后直接运行主类。如果你打开后发现编码乱码,检查一下 IDEA 右下角文件编码是不是 UTF-8,这个资源按 UTF-8 写的可能性最大,出现乱码基本是 IDEA 默认 GBK 在作怪,切回 UTF-8 重启就好。

out目录是我要提醒你注意的:它本质上是编译产物,IDEA 每编译一次会刷新。如果运行报错说类找不到,先执行一次Build -> Rebuild Project,不要手动去动out里的文件,越动越乱。

2.2 TCP 与 ICMP 探测:为什么 ping 不通就直接判不在线

这个课设的需求第一条很明确:先用 ping 测试连通性,ping 不通直接显示“主机不在网络”。这背后的逻辑和 nmap / SuperScan 的思路一致——ICMP 探测用来判断主机是否存活,TCP connect 扫描用来判断端口是否开放。两者分工不同,先讲存活判断再做端口探测,能省下大量无效的连接尝试。

// 主机存活检测:基于 ICMP 的简化实现 public boolean isHostAlive(String host) { try { // 关键参数:timeout 设为 3000ms // 注意:此方法依赖系统 ping 命令,Windows 和 Linux 参数基本一致 Process process = Runtime.getRuntime().exec("ping -n 1 -w 3000 " + host); // 等命令执行完,看返回码 int exitCode = process.waitFor(); return exitCode == 0; } catch (Exception e) { // 异常统一按不可达处理,不把异常抛到上层 return false; } }

这段代码的核心逻辑是:调用系统 ping 命令,退出码为 0 说明有 ICMP 回包,主机在线;非 0 则离线。-n 1是只发一个包,-w 3000是等待 3000 毫秒。如果你要扫的是 Linux 目标,-n 1要改成-c 1,这是个常见的坑,后面避坑章节会再讲。生产环境更推荐用java.net.InetAddress.isReachable(),但课设场景下直接调系统命令反而更直观,答辩时也好解释。

主机在线后才会进入端口探测。下面这段是 TCP connect 扫描的核心,也是整个工具最关键的循环逻辑:

// 端口探测:遍历端口列表,尝试建立 TCP 连接 public void scanPort(String host, int port) { try (Socket socket = new Socket()) { // 连接超时设 1500ms,比 ping 超时短 // 太长会导致整轮扫描时间成倍增长,太短容易误报 socket.connect(new InetSocketAddress(host, port), 1500); // 能连上说明端口开放,记录结果 System.out.println("端口 " + port + " 开放"); } catch (IOException e) { // 连接失败:可能端口关闭,也可能被防火墙过滤 System.out.println("端口 " + port + " 关闭或过滤"); } }

这里的try-with-resources写法保证了 Socket 用完自动关闭,避免句柄泄漏。connect的第二个参数是超时时间,我一般习惯设 1500ms——比 ping 的 3000ms 短,因为端口扫描要探测的端口往往几十上百个,单个超时太长会拖垮总时长。你如果扫的是校园局域网,1000ms 也够;扫跨网段的公网 IP,建议调到 2000ms 以上。

2.3 操作系统指纹识别:从端口开放情况反推系统类型

需求第二条要求识别目标操作系统类型。真正意义上的 OS 指纹识别靠的是 TCP 栈特征差异,比如初始 TTL 值、窗口大小,但课设里通常用端口特征来反推,实现简单,演示效果也够。常见做法是维护一张“端口特征到系统类型”的映射表,扫描结束后做比对。

端口特征系统推断说明
139、445、3389 开放WindowsSMB 和远程桌面是 Windows 常驻服务
22、53、80 开放Linux/UnixSSH、DNS、HTTP 在 Linux 服务器更常见
161、162 开放网络设备/开启了 SNMP 的系统很多网络设备默认开 SNMP
没有明显特征未知建议多看几个高端口再下结论

代码实现上,最简单的思路是维护一个列表,扫描完后遍历比对:

// 系统识别:根据端口开放列表做简单判定 public String guessOs(List<Integer> openPorts) { if (openPorts.contains(445) && openPorts.contains(3389)) { return "Windows"; } else if (openPorts.contains(22) && openPorts.contains(53)) { return "Linux/Unix"; } else { return "未知"; } }

这个判定比较粗糙,但课设报告里完全够用。你在答辩时可以把原理说明白:Windows 默认开放 139/445 做文件共享,而 Linux 服务器几乎不会开放 445;3389 是 Windows 远程桌面的默认端口,Linux 上即使装了 xrdp 也很少用默认端口。把这段逻辑讲清楚,比盲目追求精确识别更能拿分。

3. 多线程扫描的落地写法:同时扫多台主机还不卡死 UI

3.1 线程池选型:为什么用 ExecutorService 而不是裸 new Thread

需求第三条明确要求“使用多线程实现能同时扫描多台主机”。很多课设代码喜欢直接new Thread()一把梭,但这样有两个问题:一是线程数量不受控制,扫一个 C 段 254 个 IP,每个 IP 扫 100 个端口,可能瞬间创建上万个线程,直接导致系统资源耗尽;二是线程管理混乱,没有统一的终止和异常处理机制。我在这个项目里用的是线程池,核心参数这样设:

import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; // 创建固定大小线程池,避免无限制创建线程拖垮系统 ExecutorService executor = Executors.newFixedThreadPool(10); for (String ip : ipList) { // 每个 IP 的扫描任务封装成一个 Runnable executor.submit(new ScanTask(ip)); } // 所有任务提交完后,不再接收新任务,执行完自动关闭 executor.shutdown();

newFixedThreadPool(10)表示同时最多 10 个扫描线程在跑,其余任务排队等待。为什么是 10 而不是 CPU 核数?端口扫描是 IO 密集型操作,线程主要在等待 Socket 连接超时,CPU 基本闲着,所以线程数可以大于 CPU 核数。但也不能太贪,我之前试过开 50 个线程扫局域网,结果目标主机和交换机先扛不住,丢包率飙升,扫描结果反而不准。10 到 20 是局域网扫描的合理区间。

shutdown()的语义要搞清楚:它不是强制终止,而是拒绝新任务入队、等已提交任务跑完。如果你想中断正在跑的任务,需要保存Future对象再调用cancel(true),这个进阶操作在最后一部分讲。

3.2 给 UI 回传进度:工作线程不能直接碰界面组件

多线程扫描必然要往界面上更新进度条和扫描时间,这里有个大坑:Swing 组件不是线程安全的,在工作线程里直接调progressBar.setValue()轻则界面卡顿,重则抛InterruptedException或直接死锁。正确做法是把 UI 更新操作丢回事件分发线程(EDT):

// 扫描线程中更新进度,必须通过 SwingUtilities.invokeLater 转交 EDT SwingUtilities.invokeLater(new Runnable() { @Override public void run() { // 这里的代码运行在 EDT 上,可以安全更新界面 progressBar.setValue(currentCount * 100 / totalCount); timeLabel.setText("已用时间:" + elapsedTime + " 秒"); } });

invokeLater的原理是把 Runnable 对象放入 Swing 事件队列,由 EDT 按顺序执行。这样扫描线程只管计算结果,界面刷新统一交给 EDT,两边不打架。还有一个变体叫invokeAndWait,它是阻塞等待界面更新完成才返回,扫描场景不要用它,因为如果 EDT 正在处理别的任务,你的工作线程会被卡住,反而拖慢扫描。

另外提醒一点:扫描到一半用户点“停止”,需要用一个volatile boolean标志位来控制。扫描循环里每处理完一个端口就检查一次标志位,为 true 就 break。只靠executor.shutdown()是停不下来的,它只是不接收新任务,已提交的活儿照跑。

3.3 解析单个 IP 和 IP 段:边界判断要先于扫描执行

需求要求既能扫单个 IP,也能扫一段范围。IP 段解析这段代码是整个工具里最容易出错的,也是异常告警窗口最常触发的地方。我把解析逻辑抽成一个独立方法,扫描前先调它做校验:

// 解析 IP 范围,返回所有待扫描 IP 列表 public List<String> parseIpRange(String startIp, String endIp) { List<String> result = new ArrayList<>(); String[] startParts = startIp.split("\\."); String[] endParts = endIp.split("\\."); // 先做基础格式校验,长度不为 4 直接抛异常 if (startParts.length != 4 || endParts.length != 4) { throw new IllegalArgumentException("IP 格式不合法"); } int start = ipToInt(startIp); int end = ipToInt(endIp); // 核心边界判断:起始 IP 大于结束 IP,视为越界 if (start > end) { throw new IllegalArgumentException("IP 地址范围出界"); } // 防止扫描范围过大,超过 65536 个地址直接拒绝 if (end - start > 65535) { throw new IllegalArgumentException("扫描范围过大,请缩小 IP 段"); } for (int i = start; i <= end; i++) { result.add(intToIp(i)); } return result; } // IP 字符串转 int,方便比较大小和遍历 private int ipToInt(String ip) { String[] parts = ip.split("\\."); return (Integer.parseInt(parts[0]) << 24) | (Integer.parseInt(parts[1]) << 16) | (Integer.parseInt(parts[2]) << 8) | Integer.parseInt(parts[3]); }

这里把 IP 转成 int 再比较大小,比逐段比较字符串要干净得多。ipToInt用了位移运算,左移 24 位处理第一段,依次类推。两个边界判断很关键:start > end是需求里明确要弹出“IP 地址范围出界”告警的场景;end - start > 65535是我自己加的保险,不然有人拿0.0.0.0到255.255.255.255来扫,程序会卡到天荒地老。捕获到IllegalArgumentException后在 UI 层弹窗提示,这就实现了需求里的“异常告警窗口”。

4. UI 图形界面与实验报告:登录校验、结果表格和告警窗口

4.1 登录页面:admin/123456 的校验逻辑放在哪里

这套资源的登录逻辑非常简单,没有连数据库,用户名密码是硬编码在代码里的。课设演示场景下这样做完全合理,答辩时老师问起来,你反而能说清楚“这是为了演示方便,生产环境需要换成数据库或配置文件校验”。登录模块的代码大致是下面这样:

// 登录按钮的点击事件:校验账号密码 loginButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String username = usernameField.getText().trim(); String password = passwordField.getText().trim(); // 硬编码校验,注意 trim() 去掉首尾空格 // 防止用户不小心输入空格导致登录失败 if ("admin".equals(username) && "123456".equals(password)) { // 登录成功:关闭登录窗,打开主界面 dispose(); new MainFrame().setVisible(true); } else { // 登录失败:弹窗提示,不清空输入框方便用户修改 JOptionPane.showMessageDialog(LoginFrame.this, "账号或密码错误", "登录失败", JOptionPane.ERROR_MESSAGE); } } });

trim()是必须写的,用户复制粘贴账号时很容易带进空格,不处理就会莫名登录失败。登录失败弹窗用的是JOptionPane.showMessageDialog,第二个参数是展示内容,第三个是标题,第四个是消息类型。这里用ERROR_MESSAGE会在弹窗左上角显示红色错误图标,演示效果更明显。登录成功后的dispose()只释放当前窗口的资源,不会影响 JVM 退出,接下来创建MainFrame主窗口实例并让它可见。

4.2 扫描结果表格与进度条:JTable 数据填充的典型套路

主界面的表格展示用的是JTable + DefaultTableModel。JTable本身只负责显示,数据都放在DefaultTableModel里。扫描结果是一行一行动态加进去的,我一般会写这样的方法:

// 添加一行扫描结果到表格 private void addScanResult(String ip, int port, String status) { // 注意:此方法必须由 EDT 调用,扫描线程里要包一层 invokeLater DefaultTableModel model = (DefaultTableModel) resultTable.getModel(); // addRow 接收一个 Object 数组,每个元素对应一列 model.addRow(new Object[]{ip, port, status, currentTime()}); } // 格式化当前时间,用于"扫描时间"列 private String currentTime() { return new SimpleDateFormat("HH:mm:ss").format(new Date()); }

addRow的参数是Object[]数组,数组长度要和表格列数一致。这里四列分别是 IP、端口、状态、时间。currentTime()里SimpleDateFormat是线程不安全的,但因为这个方法只在 UI 线程被调用,所以没这个问题。如果你以后想把这段代码改到多线程环境,记得用ThreadLocal<SimpleDateFormat>包一层。

进度条和扫描时间的更新逻辑在 3.2 已经讲过了,核心就一句话:所有 UI 更新必须经SwingUtilities.invokeLater转交 EDT。还有个小细节:扫描过程中要把“开始扫描”按钮禁用,扫描结束再启用,不然用户连点两次会启动两个扫描任务,结果表格里数据会乱。

4.3 异常告警需求与实验报告对照:IP 出界提示在项目中的位置

实验报告这份文档是打包资源里容易被忽略但实际很有价值的部分。你不用重新写了,但要检查它和代码的逻辑是否一致。我做课设时习惯把需求点和代码位置列成一张对照表,答辩前照着过一遍:

课设需求点代码对应位置报告对应章节
ping 测连通状态isHostAlive()方法系统设计-连通性检测模块
端口扫描与 OS 识别scanPort()+guessOs()核心功能实现
多线程同时扫描ExecutorService线程池系统优化-多线程设计
IP 范围与异常告警parseIpRange()抛异常 + 弹窗异常处理模块
图形界面与进度显示Swing组件 + 进度条界面设计

报告中如果某些截图和当前代码界面不一致,问题不大,但建议重新跑一遍程序截几张新图替换进去。老师重点关注的是设计思路和核心代码逻辑,尤其是多线程和 Socket 部分。报告里这块如果写得偏薄,你可以在“系统设计”一节里补充一段文字,说明线程池为什么选固定大小而不是缓存线程池,这样显得你真正理解了自己的项目。

5. 端口扫描避坑指南:五个我在课设里反复踩的坑

5.1 扫描结果大量误报“端口关闭”,其实是系统 ping 命令参数不同

现象:在 Windows 上跑得好好的,换到 Linux 环境,所有主机都显示“不在网络”。原因:ping命令参数不一样——Windows 用-n指定发包数,Linux 用-c。代码里写死ping -n 1,在 Linux 上执行会报“Unknown option”,退出码非 0,主机就被误判为离线。解决:先判断当前系统再拼命令参数,或者干脆用 Java 自带的InetAddress.isReachable(3000)替代系统 ping。课设演示一般在 Windows 上做,但你得知道这个坑,答辩时老师可能会问“换到 Linux 能不能跑”。

5.2 扫描过程中界面卡死,进度条一动不动

现象:点“开始扫描”后,整个窗口变成白板,鼠标转圈,几秒后恢复但结果一次性全冒出来。原因:扫描 Socket 连接的过程直接写在了按钮的ActionListener里,这个监听器本身运行在 EDT 上。你在 EDT 上做耗时操作,界面刷新事件全部排队等,看起来就是卡死。解决:扫描任务必须丢给线程池执行,EDT 只负责接收结果并刷新界面。记住一句话:任何可能超过 100ms 的操作都不能放在 EDT 上,Socket 连接超时动辄一两秒,铁定卡死。

5.3 阿里云或本机服务器扫出来全部端口关闭,怀疑代码写错

现象:扫某个固定 IP,结果全是关闭状态,但这个 IP 上的 Web 服务明明能访问。原因:目标主机有防火墙,默认丢弃未允许的 TCP 连接请求。Socket 连接超时后,状态既不是“开放”也不是“明确关闭”,而是“被过滤”。代码把所有连接失败统一算作关闭,掩盖了真实情况。解决:把异常情况单独归类。SocketTimeoutException表示包被丢了(可能是防火墙过滤),ConnectionRefusedException表示端口确实关闭。你可以在 UI 上增加“过滤”状态,或者至少打印日志区分。演示时优先扫本机回环127.0.0.1,绕过防火墙干扰。

5.4 IP 段解析没做范围限制,扫描任务提交了几百万个

现象:程序内存飙升,线程池队列爆满,电脑风扇狂转,最后直接 OOM。原因:parseIpRange没有做上限判断。有人输入192.168.1.1到192.168.255.255,地址数量多达六万多个,每个地址再扫几十个端口,任务总数瞬间到百万级。解决:解析时加上限判断,超过 65535 个地址直接抛异常提示缩小范围。这个我在 3.3 的代码里已经写了,算是用血泪换来的经验。

5.5 主机名解析卡住整个 UI,输入无法解析的域名直接假死

现象:在“主机名”输入框填了一个不存在的域名,点扫描后界面假死,关都关不掉。原因:InetAddress.getByName()做 DNS 解析时可能会阻塞好几秒,这个调用发生在扫描流程开头,如果放在主线程里就会卡住界面。解决:把主机名解析也丢到后台线程,或者设置一个解析超时。比较简单的方式是用InetAddress.getByName(host)配合线程池的Future.get(2, TimeUnit.SECONDS)实现超时控制,时间到了还解析不出来就给用户提示“主机名无法解析”。

6. 验证与进阶:用本机回环地址把扫描结果跑准

拿到资源后,第一步验证我会建议你直接走这条流程:启动程序 → 用 admin/123456 登录 → 在“单 IP 扫描”栏输入127.0.0.1→ 端口范围填1-1000→ 开始扫描。本机回环地址不会经过防火墙过滤,能最真实地反映代码逻辑。如果你本机开了 Web 服务,80 端口应该显示开放;装了 MySQL,3306 也会开放。系统识别结果可能出现“Windows”或“未知”,这取决于你是否开放了 445 端口,但扫描本身跑通即可,后续再换局域网内其他机器验证。

验证完基础流程,我建议做两个进阶改造,这两个点也是答辩时能加分的地方。第一个是把操作系统识别从硬编码改成外部配置,用 properties 文件维护端口特征映射表,这样以后要加新的系统类型不用改代码,只改配置。第二个是把 TCP 扫描从Socket换成 NIO 的SocketChannel非阻塞模式,配合Selector实现单线程管理大量连接,扫描速度能提升好几倍。这个改动不需要重写整个项目,把scanPort里的“逐个连接”换成“批量注册连接事件”即可,代码量大约增加 40 行。NIO 端口扫描是生产级工具的常见实现,你在报告的“系统优化”一节加上这段描述,整个项目的工程含量会明显不一样。

我从一开始接到这个题目,到后来给学生讲这个项目,每一次都会强制走一遍“先看报告、再跑通主流程、最后加一个自己的改进点”的流程。因为课设答辩的风险点从来不是代码能不能跑,而是老师一问“你做了什么优化”就卡壳。建议你至少亲手改一个参数、加一个功能,哪怕只是把超时时间从 1500ms 改成 1000ms 并验证结果差异,都比原封不动交上去更有底气。希望这篇拆解帮到你,动手跑一遍,这半个小时花得值。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询