资讯动态

Java端口扫描工具:Swing图形界面实现TCP连通性诊断

发布时间:2026/10/21 12:27:31 来源:尧图企业网站定制
1. 项目概述一个能真正“看见”网络连接状态的Java端口扫描工具你有没有遇到过这样的场景在本地调试一个Web服务浏览器死活打不开localhost:8080或者部署完数据库应用日志里反复报“Connection refused”又或者帮同事排查远程服务器连通性ping通了但telnet就是连不上——这时候你最需要的不是一句“网络应该没问题”而是一个能快速告诉你“80端口开着但3306被防火墙挡了”的可视化工具。这个标题“java实现端口扫描设计UI界面”说的正是这样一个东西它不是教科书里的理论Demo而是一个能装进U盘、双击就跑、输入IP和端口范围就能出结果的实用小工具。核心关键词是Java端口扫描、Swing UI界面、TCP连接探测、网络连通性诊断。它解决的是开发、运维、测试甚至网管日常中最琐碎却最耗时的问题——确认目标机器上哪些端口是真实可访问的。适合三类人刚学完Java网络编程想落地练手的新手需要快速验证服务部署状态的后端开发者以及不熟悉命令行但又要处理基础网络问题的技术支持人员。我做过不下二十个类似的小工具从纯命令行到Web版再到Electron打包最后发现——对绝大多数一线场景来说一个轻量、无依赖、带图形界面的Java桌面程序反而是最稳、最快、最不容易出幺蛾子的选择。它不追求炫酷动画只求一次点击三秒出结果绿色免安装关机重启后还能用。2. 整体设计思路与技术选型逻辑2.1 为什么是Java而不是Python或Go很多人第一反应是“端口扫描用Python写几行socket.connect()不就完了为啥还要搞Java”这问题问得特别实在也恰恰是设计起点。我们来算一笔账Python确实快nmap脚本一写就跑但它的“快”是开发快不是交付快。你打包成exe给客户动辄50MB起步还常因PyInstaller版本兼容性导致运行报错Go编译出来是单文件但Windows下默认会弹UAC权限提示普通用户看到“是否允许此应用对设备进行更改”就懵了。而Java呢JRE在99%的开发机、测试机、甚至很多生产服务器上早已预装。你只需要一个不到200KB的jar包双击即启不弹任何系统警告所有UI渲染、线程调度、异常捕获都由JVM兜底。我曾在一个某高校实验室的老旧Windows 7机器上部署过同类工具——那台机器连Chrome都打不开但Java 8 JRE还在跑着几个教学用的Applet我们的扫描工具照样丝滑运行。这不是技术情怀是现实妥协后的最优解交付成本最低用户门槛最低故障面最小。2.2 为什么用Swing而不是JavaFX或Web界面标题明确写了“设计UI界面”这就排除了纯控制台方案。那为什么选Swing先说结论Swing是Java生态里唯一一个“写一次到处能看懂”的GUI框架。JavaFX虽然更现代但自Java 11起就被移出JDK标准库你得额外打包javafx.controls.jar等一堆依赖稍有不慎就ClassNotFoundExceptionWeb界面比如用Spring Boot Thymeleaf看似时髦但要开HTTP服务、配端口、防跨域一个简单扫描工具硬生生变成微型Web服务器安全审计时还得解释“为啥要开8081端口”。而Swing呢它是JDK自带的从Java 1.2活到今天所有组件源码公开文档齐全连JTable怎么自定义单元格渲染器都有十几种成熟方案。更重要的是它的线程模型极其清晰所有UI操作必须在Event Dispatch ThreadEDT中执行。这反而成了优势——当你启动扫描线程时UI不会卡死进度条能实时刷新按钮能及时置灰/恢复这种确定性在复杂异步场景下比“看起来高级”的框架更可靠。我试过用JavaFX重写同一套逻辑光是解决Platform.runLater()和后台线程数据同步就多花了两天最后效果和Swing并无二致。所以选Swing不是守旧是用最可控的工具做最不可控的事网络IO。2.3 扫描策略为什么只做TCP Connect扫描不做SYN扫描标题没限定扫描类型但实际实现中我们必须做取舍。网络层有两类主流扫描SYN扫描半开扫描和TCP Connect扫描全连接扫描。SYN扫描效率高、隐蔽性强但它需要原始套接字Raw Socket在Windows上需管理员权限在Linux上需CAP_NET_RAW能力普通用户双击运行直接失败。而TCP Connect扫描呢它调用的是操作系统标准的connect()系统调用只要用户有网络权限这几乎是默认的就能跑。它的缺点是速度慢一点、会在目标机器留下连接日志但对于内网诊断、本地服务验证这类场景日志根本不是问题而“能不能跑起来”才是生死线。实测数据在局域网内扫描1-1000端口TCP Connect平均耗时4.2秒SYN扫描理论快3倍但加上权限检查、异常降级逻辑最终代码量翻倍稳定性反而下降。所以我们选择牺牲理论性能换取100%的可用性。这也是为什么所有商业级网络诊断工具如SolarWinds Ping Sweep的免费版默认都是TCP Connect——因为用户要的是结果不是论文分数。2.4 架构分层三层分离让代码像乐高一样可替换整个程序按职责划分为清晰三层UI层View纯Swing组件只负责展示、接收用户输入IP、端口范围、扫描按钮不碰任何网络逻辑。所有事件监听器如ActionListener只做一件事把输入参数打包扔给Controller。控制层Controller这是心脏。它接收UI参数校验合法性比如IP格式、端口范围是否越界然后创建并启动扫描任务线程。关键点在于它用SwingWorker封装后台任务确保doInBackground()里跑耗时扫描done()里更新UI完美避开Swing线程安全陷阱。模型层Model包含PortScanner核心类它只干一件事——给定一个IP和端口返回PortStatus枚举OPEN/CLOSED/FILTERED/TIMEOUT。这个类完全独立于UI你可以把它抽出来写个JUnit测试或者集成进Spring Boot的REST接口里零改造成本。这种分层不是为了炫技是为后续扩展留活路。比如某天你想加UDP扫描只需新增一个UdpPortScanner实现类Controller里加个判断分支UI层几乎不用动再比如你想把结果导出为Excel只用在Controller的done()方法里加几行Apache POI代码UI层连重绘都不需要。我见过太多把connect()直接写在button.addActionListener()里的Demo改一行代码整个UI线程就卡死——那不是工具是定时炸弹。3. 核心细节解析与实操要点3.1 IP与端口范围校验别让错误输入毁掉第一印象UI界面再漂亮输错IP地址或端口范围点开始就报红错用户体验直接归零。所以校验不是锦上添花是底线。我们分两步做第一步IP地址格式强校验不能只用正则^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$。这正则能匹配999.999.999.999因为25[0-5]部分没限制首位不能为0。正确做法是用InetAddress.getByName(ipString)尝试解析捕获UnknownHostException。这个方法底层会做完整语义校验比如127.0.0.1合法127.0.0.256直接抛异常。代码片段如下private boolean isValidIP(String ip) { try { InetAddress.getByName(ip); return true; } catch (UnknownHostException e) { return false; } }为什么不用Inet4Address因为我们要兼容IPv6getByName自动识别而Inet4Address强制IPv4::1这种本地回环v6地址就挂了。第二步端口范围逻辑校验用户可能输入1-65535也可能手抖输成65535-1甚至abc-def。校验逻辑必须覆盖起始端口和结束端口必须是数字起始端口 ≤ 结束端口端口号必须在1-65535范围内0是保留端口65536溢出。这里有个易错点Integer.parseInt()遇到非数字字符串会抛NumberFormatException但如果你在try-catch里只捕获这个异常而忘了处理null输入比如用户清空了文本框就会NullPointerException。所以完整校验函数要先判空private boolean isValidPortRange(String startStr, String endStr) { if (startStr null || endStr null || startStr.trim().isEmpty() || endStr.trim().isEmpty()) { return false; } try { int start Integer.parseInt(startStr.trim()); int end Integer.parseInt(endStr.trim()); return start 1 end 65535 start end; } catch (NumberFormatException e) { return false; } }这个函数返回false时UI必须给出明确提示比如端口范围无效请输入1-65535之间的整数且起始端口不能大于结束端口而不是笼统的“输入错误”。3.2 扫描线程管理如何避免“点十次扫十次”的灾难UI按钮点击事件里如果直接new Thread(() - scan()).start()用户狂点十次后台就起十个线程同时扫同一个IPCPU飙升结果混乱还可能触发目标机器的连接限制。必须做线程节流。我们用SwingWorker它天然支持取消和状态监听。关键代码结构private SwingWorkerVoid, PortScanResult currentWorker; private void startScan() { // 如果已有扫描在运行先取消 if (currentWorker ! null !currentWorker.isDone()) { currentWorker.cancel(true); // 可选重置UI状态如按钮文字变回开始扫描 scanButton.setText(开始扫描); } // 创建新Worker currentWorker new PortScanWorker(ip, startPort, endPort); currentWorker.addPropertyChangeListener(evt - { if (state.equals(evt.getPropertyName())) { SwingWorker.StateValue state (SwingWorker.StateValue) evt.getNewValue(); if (state SwingWorker.StateValue.STARTED) { scanButton.setText(正在扫描...); scanButton.setEnabled(false); } else if (state SwingWorker.StateValue.DONE) { scanButton.setText(开始扫描); scanButton.setEnabled(true); } } }); currentWorker.execute(); }这里有两个精妙设计一是cancel(true)会中断线程但Socket.connect()本身是阻塞IO中断不一定立即生效所以我们在PortScanner的scanPort方法里必须配合Thread.interrupted()做主动退出public PortStatus scanPort(String ip, int port, int timeoutMs) { Socket socket null; try { socket new Socket(); // 设置连接超时避免卡死 socket.connect(new InetSocketAddress(ip, port), timeoutMs); return PortStatus.OPEN; } catch (IOException e) { if (Thread.currentThread().isInterrupted()) { return PortStatus.CANCELLED; // 自定义状态 } // 根据异常类型判断 if (e instanceof SocketTimeoutException) { return PortStatus.TIMEOUT; } else if (e.getMessage().toLowerCase().contains(connection refused)) { return PortStatus.CLOSED; } else { return PortStatus.FILTERED; } } finally { if (socket ! null !socket.isClosed()) { try { socket.close(); } catch (IOException ignored) {} } } }二是PropertyChangeListener监听state属性动态更新按钮文字和状态。这样用户点一次UI立刻反馈“正在扫描”再点无效体验干净利落。3.3 连接超时与并发控制速度与稳定的平衡术扫描速度取决于两个参数单个端口的connect()超时时间和同时扫描的端口数量并发度。设得太短丢包率高的网络会把开放端口误判为TIMEOUT设得太长扫1000个端口要等十几分钟。并发度同理开100个线程一起扫局域网飞快但扫公网IP可能被目标防火墙限速甚至拉黑。我们采用自适应策略超时时间默认设为2000ms2秒。这是经验值——内网延迟通常10ms公网骨干网50ms2秒足够覆盖99%的正常响应。如果用户勾选“快速模式”则降为500ms牺牲少量准确性换速度勾选“精准模式”升至5000ms专治高延迟链路。并发度固定为10。为什么是10因为Socket对象创建和销毁有开销线程切换也有成本。实测数据并发1→5耗时线性下降5→10下降趋缓10→20耗时几乎不变但内存占用翻倍。10是个甜点值兼顾速度与资源。核心扫描循环代码// 使用ExecutorService控制并发 private final ExecutorService executor Executors.newFixedThreadPool(10); public ListPortScanResult scanRange(String ip, int startPort, int endPort, int timeoutMs) { ListFuturePortScanResult futures new ArrayList(); for (int port startPort; port endPort; port) { final int currentPort port; FuturePortScanResult future executor.submit(() - { PortStatus status scanner.scanPort(ip, currentPort, timeoutMs); return new PortScanResult(currentPort, status); }); futures.add(future); } // 收集结果支持中断 ListPortScanResult results new ArrayList(); for (FuturePortScanResult future : futures) { try { if (Thread.currentThread().isInterrupted()) { break; } results.add(future.get(10, TimeUnit.SECONDS)); // 防止单个future卡死 } catch (InterruptedException | ExecutionException | TimeoutException e) { // 记录异常但不中断整个扫描 } } return results; }注意future.get(10, TimeUnit.SECONDS)的超时这是第二道保险——即使scanPort内部没响应外部也会强制超时防止整个扫描队列被一个坏端口拖垮。3.4 UI渲染优化让上千行扫描结果不卡死扫描结果少时JTable直接塞数据没问题。但扫1-65535全端口6万行DefaultTableModel往里addRow()UI直接冻结10秒。必须用懒加载分页。我们不真的一次性加载所有数据而是后台扫描时结果存入ConcurrentLinkedQueuePortScanResultJTable绑定一个自定义TableModel它只维护当前可视区域的几百行数据滚动时TableModel动态从队列里取数据填充。但这样太重。更轻量的方案是扫描完成后只显示前100个开放端口其余状态汇总成统计卡片。UI上放一个JTabbedPane左边是JTable只显示OPEN端口右边是JPanel堆叠的统计卡片OPEN: 23个,CLOSED: 1200个,TIMEOUT: 5个。JTable的数据源用DefaultTableModel但只addRow()那些OPEN的结果。代码极简private void updateResultTable(ListPortScanResult results) { DefaultTableModel model (DefaultTableModel) resultTable.getModel(); model.setRowCount(0); // 清空 for (PortScanResult r : results) { if (r.getStatus() PortStatus.OPEN) { model.addRow(new Object[]{r.getPort(), r.getStatus().getDesc()}); } } }这样哪怕扫6万个端口JTable最多显示几百行UI永远流畅。用户真要查某个特定端口加个搜索框JTable自带TableRowSorter一行代码搞定过滤。4. 实操过程与核心环节实现4.1 从零搭建项目结构Maven依赖与目录规划我们用Maven管理依赖pom.xml核心配置如下project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdport-scanner-ui/artifactId version1.0-SNAPSHOT/version packagingjar/packaging properties maven.compiler.source11/maven.compiler.source maven.compiler.target11/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies !-- JavaFX不是必需的这里不引入 -- !-- 所有Swing和网络功能JDK自带无需额外依赖 -- /dependencies build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId version3.3.0/version configuration archive manifest mainClasscom.example.scanner.MainFrame/mainClass addClasspathtrue/addClasspath /manifest /archive /configuration /plugin /plugins /build /project注意没有添加任何第三方依赖。Swing、java.net、java.util.concurrent全是JDK标准库。项目目录结构极简src/ ├── main/ │ ├── java/ │ │ └── com/example/scanner/ │ │ ├── MainFrame.java // 主窗口入口 │ │ ├── controller/ // Controller层 │ │ │ └── ScanController.java │ │ ├── model/ // Model层 │ │ │ ├── PortScanner.java │ │ │ ├── PortStatus.java │ │ │ └── PortScanResult.java │ │ └── view/ // View层 │ │ └── ScannerPanel.java │ └── resources/ // 图标、配置等 └── test/ // 单元测试这种结构新人拉下代码mvn clean package生成的target/port-scanner-ui-1.0-SNAPSHOT.jar双击就运行毫无障碍。4.2 主窗口MainFrame实现Swing的“Hello World”式严谨MainFrame继承JFrame但绝不只是setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)了事。它要处理窗口生命周期public class MainFrame extends JFrame { private ScannerPanel mainPanel; public MainFrame() { setTitle(Java端口扫描器 v1.0); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); // 关键 addWindowListener(new WindowAdapter() { Override public void windowClosing(WindowEvent e) { // 询问用户是否退出避免扫描中误关 int result JOptionPane.showConfirmDialog( MainFrame.this, 扫描正在进行中确定要退出吗, 确认退出, JOptionPane.YES_NO_OPTION ); if (result JOptionPane.YES_OPTION) { System.exit(0); } } }); mainPanel new ScannerPanel(); add(mainPanel); // 设置窗口大小和居中 pack(); // 先pack让组件计算最佳尺寸 setLocationRelativeTo(null); // 居中 setMinimumSize(new Dimension(600, 400)); // 设最小尺寸防缩太小 setVisible(true); } public static void main(String[] args) { // Swing线程安全所有UI操作必须在EDT中 SwingUtilities.invokeLater(MainFrame::new); } }这里三个细节决定专业度setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE)禁用默认关闭自己接管避免用户扫到一半点叉号进程没了但线程还在后台跑造成资源泄漏。WindowAdapter里windowClosing事件中用JOptionPane二次确认且只在扫描进行中才弹——这需要ScanController暴露一个isScanning()方法供查询。SwingUtilities.invokeLater()包裹构造函数确保MainFrame实例化在EDT中这是Swing铁律跳过它某些UI组件如JProgressBar可能渲染异常。4.3 扫描面板ScannerPanel布局用GridBagLayout拿捏像素级精准Swing布局管理器里BorderLayout太粗放FlowLayout太随意GridLayout太死板。GridBagLayout是唯一能精确控制每个组件位置、大小、对齐的方案。ScannerPanel布局如下--------------------------------------------------- | [IP地址:] [127.0.0.1] | | [端口范围:] [1] - [1000] | | [ ] 快速模式 [ ] 精准模式 [开始扫描] [停止] | | | | ----------------------------------------------- | | | 开放端口列表 | | | | Port | Status | | | | |------|--------| | | | | 22 | OPEN | | | | | ... | ... | | | | ----------------------------------------------- | | [统计: OPEN: 3, CLOSED: 997, TIMEOUT: 0] | ---------------------------------------------------对应代码精简版public class ScannerPanel extends JPanel { private JTextField ipField; private JTextField startPortField; private JTextField endPortField; private JCheckBox fastModeBox; private JCheckBox accurateModeBox; private JButton startButton; private JButton stopButton; private JTable resultTable; private JLabel statusLabel; public ScannerPanel() { setLayout(new GridBagLayout()); GridBagConstraints gbc new GridBagConstraints(); // IP行 gbc.gridx 0; gbc.gridy 0; gbc.anchor GridBagConstraints.WEST; add(new JLabel(IP地址:), gbc); gbc.gridx 1; gbc.fill GridBagConstraints.HORIZONTAL; gbc.weightx 1.0; ipField new JTextField(127.0.0.1, 15); add(ipField, gbc); // 端口范围行 gbc.gridy 1; gbc.gridx 0; gbc.fill GridBagConstraints.NONE; gbc.weightx 0; add(new JLabel(端口范围:), gbc); gbc.gridx 1; gbc.fill GridBagConstraints.NONE; startPortField new JTextField(1, 5); add(startPortField, gbc); gbc.gridx 2; add(new JLabel(-), gbc); gbc.gridx 3; endPortField new JTextField(1000, 5); add(endPortField, gbc); // 模式与按钮行 gbc.gridy 2; gbc.gridx 0; gbc.gridwidth 2; fastModeBox new JCheckBox(快速模式); add(fastModeBox, gbc); gbc.gridx 2; gbc.gridwidth 1; accurateModeBox new JCheckBox(精准模式); add(accurateModeBox, gbc); gbc.gridx 3; gbc.fill GridBagConstraints.NONE; gbc.weightx 0; startButton new JButton(开始扫描); add(startButton, gbc); gbc.gridx 4; stopButton new JButton(停止); add(stopButton, gbc); // 结果表格 gbc.gridy 3; gbc.gridx 0; gbc.gridwidth 5; gbc.fill GridBagConstraints.BOTH; gbc.weighty 1.0; String[] columns {端口, 状态}; resultTable new JTable(new DefaultTableModel(columns, 0)); JScrollPane scrollPane new JScrollPane(resultTable); add(scrollPane, gbc); // 统计标签 gbc.gridy 4; gbc.fill GridBagConstraints.NONE; gbc.weighty 0; statusLabel new JLabel(准备就绪); add(statusLabel, gbc); // 初始化事件监听 initEventListeners(); } private void initEventListeners() { startButton.addActionListener(e - { // 触发扫描逻辑 }); stopButton.addActionListener(e - { // 触发取消逻辑 }); } }GridBagLayout的gbc.weightx和gbc.weighty控制组件拉伸权重gbc.fill控制填充方式gbc.gridwidth控制跨列。这套代码无论窗口怎么拉伸IP输入框始终占宽按钮始终右对齐表格始终填满剩余空间——这才是专业UI该有的样子。4.4 扫描结果可视化用不同颜色区分端口状态JTable默认所有行一个色但端口状态有四种OPEN绿色、CLOSED红色、TIMEOUT橙色、FILTERED灰色。我们需要自定义TableCellRenderer。不重写整个渲染器而是继承DefaultTableCellRenderer只改getTableCellRendererComponent方法public class PortStatusRenderer extends DefaultTableCellRenderer { Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if (value instanceof PortStatus) { PortStatus status (PortStatus) value; switch (status) { case OPEN: setBackground(Color.GREEN); setForeground(Color.WHITE); break; case CLOSED: setBackground(Color.RED); setForeground(Color.WHITE); break; case TIMEOUT: setBackground(new Color(255, 165, 0)); // 橙色 setForeground(Color.BLACK); break; case FILTERED: setBackground(Color.LIGHT_GRAY); setForeground(Color.BLACK); break; default: setBackground(table.getBackground()); setForeground(table.getForeground()); } } return this; } }然后在ScannerPanel初始化resultTable后设置渲染器resultTable.getColumnModel().getColumn(1).setCellRenderer(new PortStatusRenderer());这样表格第二列状态列就自动按状态上色。用户一眼扫过去绿色块就是能连上的端口红色块是明确拒绝橙色块是网络不通——信息密度瞬间提升这才是UI该干的事。4.5 打包与分发一个jar包全平台通行Maven打包后target/port-scanner-ui-1.0-SNAPSHOT.jar就是最终产物。但直接双击Windows可能用记事本打开Mac可能报“已损坏”。解决方案Windows写一个launch.bat内容就一行java -jar port-scanner-ui-1.0-SNAPSHOT.jar。用户双击bat自动调用系统JRE。Mac写一个launch.command首行加#!/bin/bash第二行java -jar port-scanner-ui-1.0-SNAPSHOT.jar然后终端执行chmod x launch.command。Linux同Macchmod x launch.sh。更进一步可以用jpackageJDK 14生成原生安装包jpackage --input target/ --name PortScanner --main-jar port-scanner-ui-1.0-SNAPSHOT.jar --main-class com.example.scanner.MainFrame它会生成Windows的.msi、Mac的.dmg、Linux的.deb图标、菜单项、卸载程序全配好。但对小工具来说一个jar包一个启动脚本已经足够轻量。我给某公司IT部门做的版本就是发一个zip包里面scanner.jar和start.bat他们拷到几十台测试机上五秒搞定没人问“怎么装Java”。5. 常见问题与排查技巧实录5.1 典型问题速查表问题现象可能原因排查步骤解决方案点击“开始扫描”无反应按钮不变化startButton的ActionListener未正确绑定或initEventListeners()未被调用1. 在initEventListeners()开头加System.out.println(Listener init)2. 检查ScannerPanel构造函数末尾是否调用了它确保initEventListeners()在add()组件之后调用且ActionListener中startScan()方法不为空扫描结果显示全为TIMEOUT但telnet ip port能通单个端口超时时间设得太短或目标机器防火墙拦截了Java进程的连接请求1. 将超时时间临时改为5000ms2. 在目标机器上用netstat -an | findstr :端口确认端口确实在监听3. 关闭目标防火墙测试调高超时时间确认目标服务绑定的是0.0.0.0所有网卡而非127.0.0.1仅本地扫描过程中UI卡死进度条不动SwingWorker未正确使用或scanPort()方法在EDT中被直接调用1. 检查startScan()中是否用了SwingWorker.execute()2. 在scanPort()开头加System.out.println(Thread: Thread.currentThread().getName())确认输出不是AWT-EventQueue-0确保所有耗时操作都在SwingWorker.doInBackground()中UI更新在done()中扫描结果里出现大量FILTERED但预期是CLOSED目标机器防火墙如Windows Defender主动丢弃了连接请求未返回RST包1. 在目标机器上临时关闭防火墙2. 用nmap -sT ip对比结果FILTERED是防火墙行为属正常现象无需修复若需区分可增加ICMP探测Ping但会增加复杂度jar包双击无反应命令行运行报UnsupportedClassVersionError编译用的JDK版本高于用户机器JRE版本如用JDK 17编译用户只有JRE 81. 用户运行java -version2. 开发者mvn compile -Dmaven.compiler.source8 -Dmaven.compiler.target8在pom.xml中显式指定maven.compiler.source和maven.compiler.target为85.2 我踩过的三个深坑与独家避坑技巧坑一Socket未正确关闭导致“Too many open files”现象连续扫描十几次后程序报java.io.IOException: Too many open files系统级错误。原因Socket.connect()抛异常时socket对象可能已部分创建但finally块里socket.close()没执行比如socket变量是null或close()本身抛异常。避坑技巧永远用try-with-resources。修改scanPort方法public PortStatus scanPort(String ip, int port, int timeoutMs) { try (Socket socket new Socket()) { // 自动close socket.connect(new InetSocketAddress(ip, port), timeoutMs); return PortStatus.OPEN; } catch (IOException e) { // 异常处理逻辑不变 } }try-with-resources保证无论connect()成功与否socket.close()都会被调用彻底杜绝句柄泄漏。坑二JTable在SwingWorker.done()中更新崩溃现象扫描完成done()里调用model.addRow()偶尔抛java.lang.ArrayIndexOutOfBoundsException。原因JTable的DefaultTableModel不是线程安全的done()虽在EDT中但SwingWorker的publish()/process()机制可能引发竞态。避坑技巧用SwingUtilities.invokeLater()二次包裹UI更新。即使已在EDT也加一层保险Override protected void done() { try { ListPortScanResult results get(); SwingUtilities.invokeLater(() - { updateResultTable(results); statusLabel.setText(扫描完成共 results.size() 个端口); }); } catch (Exception e) { e.printStackTrace(); } }这行代码成本几乎为零但

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑