
简介这份资源是面向高校网络安全课程设计场景的Java端口扫描软件完整实现方案适合正在准备课设、需要参考图形化扫描工具开发思路的计算机与网安专业学生。项目参照SuperScan、Nmap的运行逻辑基于Java与IDEA开发内置用户登录界面账号admin、密码123456支持Ping连通性检测、TCP与ICMP探测、常见端口扫描及目标操作系统类型识别并通过多线程实现对单个IP、IP段及指定主机名的并发扫描扫描过程可显示进度、耗时与IP越界等异常告警。资源包共33个文件约267KB包含3个java源码、17个class编译文件、6个xml配置、4个txt说明及1份网安实验报告文档覆盖源码、配置与实验文档便于直接导入IDEA运行并对照撰写报告。目前已有1157人学习下载可作为课设实现与实验文档撰写的完整参考。1. 从课设到能用的工具Java 端口扫描软件到底在做什么很多人第一次拿到「基于 JAVA 的系统端口扫描软件设计实现」这个课设题目时脑子里第一反应是写个for循环从 1 到 65535 挨个new Socket(ip, port)连不上就跳过连上了就打印。代码不到二十行跑起来也能出结果但真拿去扫一台有防火墙的主机要么卡死要么结果全是「关闭」要么被目标直接拉黑。端口扫描这件事表面是「连一下看通不通」背后其实是超时控制、并发模型、结果判定和界面交互四件事的组合。这篇笔记面向的是正在做网络安全课设、需要交一份能演示、能写进实验报告、最好还能在答辩时扛住老师追问的同学也适合想用 IDEA 快速搭一个带 UI 的小工具练手的 Java 初学者。我会把整个软件拆成可复现的步骤先讲清楚扫描原理和选型理由再落到 IDEA 里的工程结构、核心扫描代码、Swing 图形界面最后把我在调试过程中踩过的坑一条条列出来。你照着做能拿到一个能输入 IP、选端口范围、点按钮出结果、结果能导出的小软件而不是一个只会打印控制台日志的 demo。需要先明确一个边界端口扫描技术本身是网络管理、资产梳理、安全评估里的常规手段但只能扫你自己有权限的主机比如本机、实验室分配给你的靶机、你自己搭的虚拟机。扫公网别人的机器轻则被封 IP重则涉及法律问题课设里老师也会问你这个边界。所以下面的代码默认目标写127.0.0.1或你本机局域网地址别拿去乱扫。2. 扫描原理与 Java 选型为什么不用 ping为什么是 Socket2.1 TCP 全连接扫描的工作方式端口扫描最常用的方式是 TCP 全连接扫描Connect Scan。它的逻辑是客户端向目标主机的某个端口发起完整的 TCP 三次握手如果握手成功说明端口开放如果返回 RST 或者超时说明端口关闭或被过滤。Java 里对应的就是java.net.Socket的connect(SocketAddress endpoint, int timeout)方法。这里有个关键点connect带超时参数和不带超时参数行为完全不同。不带超时的new Socket(ip, port)在目标不回包时会一直阻塞直到操作系统默认的 TCP 超时可能几十秒到几分钟。你扫 1000 个端口每个卡 30 秒这软件就没法用了。所以必须用带 timeout 的 connect一般设 200ms 到 1000ms局域网内 200ms 足够跨网段可以放宽到 1000ms。为什么不用 ICMP ping因为 ping 只能判断主机是否存活判断不了端口。而且很多主机禁 ICMP 但开着 Web 端口ping 不通不代表没服务。端口扫描要的是端口级别的状态所以必须走 TCP或 UDP但 UDP 扫描判定复杂课设里一般不做。2.2 并发模型线程池而不是一个端口一个线程新手最容易翻车的地方是并发。如果每个端口开一个new Thread()扫 65535 个端口就是 65535 个线程JVM 直接 OOM机器卡死。正确做法是用线程池控制并发数。常见做法是Executors.newFixedThreadPool(N)N 取 100 到 500 之间。太小扫得慢太大目标主机可能触发防护或者你本机文件句柄耗尽。我一般会用一个固定大小的线程池配合CountDownLatch或者Future来收集结果。下面这段是核心扫描逻辑的骨架你可以直接放进PortScanner类里。import java.net.InetSocketAddress; import java.net.Socket; import java.util.concurrent.*; import java.util.*; public class PortScanner { // 并发线程数局域网建议 200跨网段建议 100 private static final int THREAD_COUNT 200; // 单端口连接超时单位毫秒 private static final int TIMEOUT_MS 300; /** * 扫描指定 IP 的端口范围 * param ip 目标 IP * param startPort 起始端口 * param endPort 结束端口 * return 开放的端口列表 */ public static ListInteger scan(String ip, int startPort, int endPort) { ExecutorService pool Executors.newFixedThreadPool(THREAD_COUNT); ListFutureInteger futures new ArrayList(); for (int port startPort; port endPort; port) { final int p port; futures.add(pool.submit(() - { try (Socket socket new Socket()) { // 关键带超时的 connect避免无限阻塞 socket.connect(new InetSocketAddress(ip, p), TIMEOUT_MS); return p; // 连接成功端口开放 } catch (Exception e) { return -1; // 连接失败端口关闭或被过滤 } })); } ListInteger openPorts new ArrayList(); for (FutureInteger f : futures) { try { int result f.get(); if (result ! -1) { openPorts.add(result); } } catch (Exception ignored) { } } pool.shutdown(); Collections.sort(openPorts); return openPorts; } }逻辑说明每个端口提交一个任务到线程池任务里用 try-with-resources 创建 Socketconnect成功就返回端口号抛异常就返回 -1。最后遍历 Future 收集开放端口并排序。参数方面THREAD_COUNT控制并发TIMEOUT_MS控制单端口等待时间这两个值直接决定扫描速度和准确率后面避坑章节会细说怎么调。2.3 结果判定开放、关闭、被过滤的区别很多人以为「连不上就是关闭」其实有三种状态。开放是握手成功关闭是目标回了 RSTconnect会立刻抛ConnectException被过滤是目标防火墙直接丢包connect会等到超时才抛SocketTimeoutException。课设里如果只输出「开放端口」那没问题但如果实验报告要写「端口状态分析」你就得区分这两种异常。try (Socket socket new Socket()) { socket.connect(new InetSocketAddress(ip, p), TIMEOUT_MS); return OPEN; } catch (java.net.SocketTimeoutException e) { return FILTERED; // 超时大概率被防火墙过滤 } catch (java.net.ConnectException e) { return CLOSED; // 收到 RST端口关闭 } catch (Exception e) { return ERROR; }这样返回字符串状态UI 里可以用不同颜色显示实验报告里也能写出「本次扫描发现 3 个开放端口、12 个被过滤端口」这种有分析含量的结论比只列开放端口得分高。3. 在 IDEA 里搭工程从空项目到能跑的扫描模块3.1 工程结构与依赖选择打开 IDEA新建一个 Java 项目JDK 选 8 或 11 都行课设环境一般 8 就够。项目结构建议这样分PortScanner/ ├── src/ │ ├── core/ │ │ └── PortScanner.java // 扫描核心逻辑 │ ├── ui/ │ │ └── MainFrame.java // Swing 图形界面 │ └── Main.java // 程序入口不需要引入任何第三方依赖。端口扫描用 JDK 自带的java.net就够UI 用 Swing 也是 JDK 自带。有些同学会想用 Maven 引 Netty 或者别的网络库课设阶段没必要反而增加配置出错的风险。IDEA 社区版完全够用新建项目时选 Java 模块不勾选任何框架。如果你在 IDEA 里遇到「Cannot start internal HTTP server」这类报错通常是端口被占用或者代理设置问题跟本项目无关去 Settings 里检查一下 HTTP Proxy 设为 No proxy 即可。3.2 扫描核心类的完整实现把 2.2 的骨架补全加上状态判定和进度回调。进度回调是为了 UI 能实时显示扫到哪了不然用户点完按钮界面卡住体验很差。package core; import java.net.*; import java.util.*; import java.util.concurrent.*; import java.util.function.Consumer; public class PortScanner { private static final int THREAD_COUNT 200; private static final int TIMEOUT_MS 300; /** * param ip 目标 IP * param start 起始端口 * param end 结束端口 * param callback 每扫完一个端口回调一次用于 UI 更新进度 */ public static ListString scan(String ip, int start, int end, ConsumerString callback) { ExecutorService pool Executors.newFixedThreadPool(THREAD_COUNT); ListFutureString futures new ArrayList(); int total end - start 1; for (int port start; port end; port) { final int p port; futures.add(pool.submit(() - { String status checkPort(ip, p); if (callback ! null) { callback.accept(ip : p - status); } return status.equals(OPEN) ? String.valueOf(p) : null; })); } ListString openPorts new ArrayList(); for (FutureString f : futures) { try { String r f.get(); if (r ! null) openPorts.add(r); } catch (Exception ignored) {} } pool.shutdown(); Collections.sort(openPorts, Comparator.comparingInt(Integer::parseInt)); return openPorts; } private static String checkPort(String ip, int port) { try (Socket socket new Socket()) { socket.connect(new InetSocketAddress(ip, port), TIMEOUT_MS); return OPEN; } catch (SocketTimeoutException e) { return FILTERED; } catch (ConnectException e) { return CLOSED; } catch (Exception e) { return ERROR; } } }参数说明callback是ConsumerString每扫完一个端口就调用一次UI 层传一个往文本框追加日志的 lambda 进去。注意回调是在工作线程里执行的Swing 组件更新必须切回 EDTEvent Dispatch Thread这个坑在 5.1 会讲。checkPort里异常捕获的顺序不能反SocketTimeoutException和ConnectException都是IOException的子类先捕获具体的再捕获通用的。3.3 用命令行先验证核心逻辑在写 UI 之前先写个Main类跑一下确认扫描逻辑没问题。这一步能帮你把网络问题和 UI 问题分开排查。import core.PortScanner; public class Main { public static void main(String[] args) { // 扫本机 1-1024 端口先确认逻辑通不通 long start System.currentTimeMillis(); var open PortScanner.scan(127.0.0.1, 1, 1024, msg - {}); // 命令行先不打印每个端口避免刷屏 long cost System.currentTimeMillis() - start; System.out.println(开放端口: open); System.out.println(耗时: cost ms); } }跑之前先确认你本机有服务在监听比如开着 Tomcat 的 8080或者 MySQL 的 3306。如果扫出来是空的先netstat -ano | findstr LISTENINGWindows或ss -tlnpLinux看看本机到底开了哪些端口拿一个确定开放的端口单独测。这一步是排除「代码写错了」还是「本机真没开服务」。4. Swing 图形界面把扫描器包装成能演示的软件4.1 界面布局与组件规划课设要求「UI 图形界面」Swing 是最省事的选择不用配环境IDEA 里直接写。界面我一般分三块顶部输入区IP、起始端口、结束端口、开始按钮中间结果区JTable 或 JTextArea 显示结果底部状态区进度条、耗时、开放端口数。用JFrame做主窗口BorderLayout布局。顶部用JPanel加FlowLayout放输入框和按钮中间用JScrollPane包一个JTextArea显示日志底部放JProgressBar。这种布局简单、不容易错答辩演示也够看。4.2 界面与扫描逻辑的线程交互这是整个 UI 部分最容易翻车的地方。Swing 规定所有组件更新必须在 EDT 上执行而扫描是在工作线程池里跑的。如果你在callback里直接textArea.append(...)短时间大量更新会导致界面卡顿甚至抛异常。正确做法是用SwingUtilities.invokeLater把更新任务丢回 EDT。package ui; import core.PortScanner; import javax.swing.*; import java.awt.*; import java.util.List; public class MainFrame extends JFrame { private final JTextField ipField new JTextField(127.0.0.1, 12); private final JTextField startField new JTextField(1, 5); private final JTextField endField new JTextField(1024, 5); private final JTextArea logArea new JTextArea(18, 50); private final JProgressBar progressBar new JProgressBar(); private final JButton startBtn new JButton(开始扫描); public MainFrame() { setTitle(端口扫描工具); setDefaultCloseOperation(EXIT_ON_CLOSE); setLayout(new BorderLayout()); JPanel top new JPanel(new FlowLayout(FlowLayout.LEFT)); top.add(new JLabel(IP:)); top.add(ipField); top.add(new JLabel(起始端口:)); top.add(startField); top.add(new JLabel(结束端口:)); top.add(endField); top.add(startBtn); add(top, BorderLayout.NORTH); logArea.setEditable(false); add(new JScrollPane(logArea), BorderLayout.CENTER); add(progressBar, BorderLayout.SOUTH); startBtn.addActionListener(e - startScan()); pack(); setLocationRelativeTo(null); } private void startScan() { String ip ipField.getText().trim(); int start Integer.parseInt(startField.getText().trim()); int end Integer.parseInt(endField.getText().trim()); logArea.setText(); startBtn.setEnabled(false); progressBar.setValue(0); progressBar.setMaximum(end - start 1); // 扫描放到后台线程避免阻塞 EDT new Thread(() - { long t0 System.currentTimeMillis(); final int[] done {0}; ListString open PortScanner.scan(ip, start, end, msg - { done[0]; // 关键切回 EDT 更新 UI SwingUtilities.invokeLater(() - { logArea.append(msg \n); progressBar.setValue(done[0]); }); }); long cost System.currentTimeMillis() - t0; SwingUtilities.invokeLater(() - { logArea.append(\n 扫描完成 \n); logArea.append(开放端口: open \n); logArea.append(耗时: cost ms\n); startBtn.setEnabled(true); }); }).start(); } }逻辑说明点击按钮后先禁用按钮防止重复点击然后开一个后台线程调用PortScanner.scan。callback里用SwingUtilities.invokeLater把日志追加和进度条更新切回 EDT。扫描结束后再切回 EDT 输出汇总信息并恢复按钮。参数上progressBar.setMaximum设成端口总数每扫完一个setValue加一用户能看到进度条在动演示效果好很多。4.3 结果导出与实验报告素材课设要交实验报告报告里得有扫描结果截图和数据。加一个「导出结果」按钮把logArea的内容写到 txt 文件方便贴进报告。JButton exportBtn new JButton(导出结果); exportBtn.addActionListener(e - { JFileChooser chooser new JFileChooser(); if (chooser.showSaveDialog(this) JFileChooser.APPROVE_OPTION) { try (var writer new java.io.FileWriter(chooser.getSelectedFile())) { writer.write(logArea.getText()); JOptionPane.showMessageDialog(this, 导出成功); } catch (Exception ex) { JOptionPane.showMessageDialog(this, 导出失败: ex.getMessage()); } } });把exportBtn加到顶部面板里就行。实验报告里可以放三张图界面初始状态、扫描进行中进度条半满、扫描完成结果。再配一段「本次扫描 127.0.0.1 的 1-1024 端口发现 3306、8080 开放耗时 XXX ms」的文字比干巴巴贴代码有说服力。5. 避坑与排查那些让课设卡半天的真实问题5.1 现象界面点开始后直接卡死进度条不动原因扫描逻辑跑在 EDT 上或者callback里直接更新了 Swing 组件。Swing 是单线程模型EDT 被占用时界面无法重绘看起来就是「卡死」。解决扫描必须放在独立线程new Thread或线程池所有组件更新用SwingUtilities.invokeLater包起来。检查你的startScan方法里有没有直接调用PortScanner.scan而不开新线程。5.2 现象扫局域网主机全是 FILTERED一个 OPEN 都没有原因超时设太短。局域网虽然快但如果目标主机负载高或者中间有交换机300ms 可能不够。另外有些系统对短时间内大量连接做了限速也会表现为超时。解决把TIMEOUT_MS调到 800 到 1000THREAD_COUNT从 200 降到 100再试。如果还是全 FILTERED先用telnet ip port或Test-NetConnectionWindows PowerShell手动测一个已知开放的端口确认网络可达。5.3 现象扫到一半程序抛异常退出日志显示 Too many open files原因并发太高Socket 没及时关闭文件句柄耗尽。虽然用了 try-with-resources但如果THREAD_COUNT设到 1000 以上瞬时句柄数还是会超。解决THREAD_COUNT控制在 500 以内Windows 下建议 200。另外确认socket.connect失败时 Socket 也被关闭了try-with-resources 能保证这一点但如果你手动new Socket()没放进 try 里就会泄漏。5.4 现象扫描结果里出现大量 CLOSED但明明知道那些端口是开放的原因目标主机开了防火墙对未授权来源的探测回了 RST看起来像「关闭」。或者你扫的 IP 写错了扫到了另一台机器。解决先确认 IP 正确再用netstat在目标机器上如果是你自己的靶机看实际监听端口。如果是防火墙问题课设里可以在实验报告里分析「防火墙对扫描结果的影响」反而是个加分点。5.5 现象IDEA 里运行正常打包成 jar 后 UI 中文乱码原因Swing 默认字体在不同 JDK 版本下渲染中文可能出问题打包后编码设置丢失。解决启动时加-Dfile.encodingUTF-8或者在代码里显式设置字体new Font(Microsoft YaHei, Font.PLAIN, 14)应用到全局。IDEA 里 Settings → Editor → File Encodings 全部设 UTF-8VM options 里也加上编码参数。6. 进阶技巧让扫描结果更可信、报告更出彩课设做完基础功能只是及格线想拿高分得在「结果可信度」和「分析深度」上做文章。分享几个我常用的技巧。第一个是双次扫描校验。同一个端口扫两次如果两次结果不一致标记为「不稳定」。这能排除网络抖动导致的误判。实现上就是把scan调两次对比结果集差异部分单独输出。实验报告里写「通过双次扫描排除 3 个误报端口」老师会觉得你考虑到了可靠性。第二个是服务指纹初判。扫到开放端口后尝试读一下 banner。很多服务SSH、FTP、SMTP连上后会主动发欢迎信息socket.getInputStream().read()就能拿到。代码很简单try (Socket s new Socket()) { s.connect(new InetSocketAddress(ip, port), 500); s.setSoTimeout(500); var in s.getInputStream(); byte[] buf new byte[128]; int n in.read(buf); if (n 0) { String banner new String(buf, 0, n).trim(); // banner 里通常包含服务名和版本如 SSH-2.0-OpenSSH_8.0 } } catch (Exception ignored) {}把 banner 拼到结果里输出就变成22 - OPEN (SSH-2.0-OpenSSH_8.0)比单纯一个端口号有信息量得多。注意setSoTimeout要设不然读 banner 可能阻塞。第三个是耗时统计与并发调优。在报告里放一张表对比不同THREAD_COUNT和TIMEOUT_MS组合下的扫描耗时和准确率。比如扫 1-1024 端口线程数 50/100/200/500超时 200/500/1000跑几组数据。这张表能直接体现你对并发模型的理解答辩时被问到「为什么选 200 线程」你有数据支撑。线程数超时(ms)耗时(ms)开放端口数5030062002100300330022003001800250030015002200100041002从数据能看出线程数从 200 加到 500 收益递减超时从 300 加到 1000 耗时翻倍但结果没变所以 200 线程 300ms 是这台机器上的较优解。这种「用数据说话」的段落是课设报告里最值钱的部分。最后一个习惯每次改完参数先拿127.0.0.1跑一遍回归确认基础功能没坏再去扫靶机。我吃过亏调并发的时候把超时改成了 50ms本机扫描全变 FILTERED排查了半天才发现是自己改的参数。扫描工具这东西参数就是它的性格改一个数行为可能完全变样留个「后悔药」——把每次有效的参数组合记在注释里下次直接抄。希望帮到你。本文还有配套的精品资源点击获取