ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java多线程端口扫描器:从TCP三次握手到UDP探测实战

Java多线程端口扫描器:从TCP三次握手到UDP探测实战 简介基于Java实现的TCP/UDP端口扫描器是一份面向计算机网络课程设计、毕设或大作业的完整参考项目适合Java初学者和进阶学习者也是初期项目立项的实用参考。项目基于Eclipse开发实现多线程并发扫描指定主机端口范围可自定义IP、起始端口、结束端口与线程数扫描结束后在界面中展示开放端口并支持结果保存同时包含图像显示功能方便观察扫描流程。资源压缩包共12个文件以Java源码、class字节码、Markdown说明文档及Eclipse工程配置文件为主另有运行截图辅助理解整体大小约96KB轻量易用。目前已有131人浏览学习。借助该资源读者可快速掌握多线程端口扫描器的设计思路理解TCP/UDP探测与Socket编程细节并基于现有代码扩展更多功能作为课程设计报告或初期项目开发的参考素材为网络编程实践提供可复用模板。1. Java 多线程端口扫描器不是什么黑科技是一份能跑通的课程设计做网络课设时最怕的不是不懂原理而是代码写了一堆、界面也画出来了一运行却扫啥啥不对——要么扫一遍全“关闭”要么卡到窗口转圈要么开放端口列出来自己也分不清是真是假。这份基于 Java 的 TCP/UDP 多线程端口扫描器正是可以拿来复现的完整课程设计Eclipse 直接打开就能跑填 IP、起止端口、线程数点扫描就把开放端口列在界面上扫完还能存结果。它解决的核心问题很具体在 Windows 10 Java Swing 的环境里把 TCP 的 connect 探测和 UDP 的 ICMP 探测落地成一个稳定的多线程工具。适合正在做网络课设、想搞懂端口扫描原理或者想补 Java 多线程实战的读者照着拆一遍比背十遍三次握手管用。2. 原理与选型理由TCP 走三次握手UDP 只能靠 ICMP 猜2.1 TCP 扫描的判定逻辑connect 探测与三次握手的关系TCP 端口扫描最直觉的做法是拿 Socket 去连一下目标端口。内核在处理 connect 时走的正是 TCP 三次握手的前两步客户端发 SYN服务端若开放则回 SYNACK若关闭则回 RST。对课程设计来说不需要自己拼 TCP 报文直接用 JDK 的 Socket 类把 connect 的成败当作端口开放与否的依据就行。public boolean scanTcp(String ip, int port, int timeoutMs) { try (Socket socket new Socket()) { socket.connect(new InetSocketAddress(ip, port), timeoutMs); return true; // 握手有响应端口开放 } catch (SocketTimeoutException e) { return false; // 超时可能被防火墙丢弃 } catch (IOException e) { return false; // 连接被拒绝或不可达 } }这段代码是 TCP 扫描的最小单元。connect 的第二个参数 timeoutMs 是超时毫秒数课程设计阶段在局域网内设 300 到 500 毫秒足够扫描外网主机时再放宽到 1000 毫秒。注意 try-with-resources 会自动关闭 socket不会在每个端口上遗留半开连接。这里踩过坑的人都知道如果不设超时connect 可能卡在系统默认的两三分钟上整个扫描器看起来就像死掉了。2.2 UDP 扫描判定逻辑为什么说它是“猜”UDP 是面向无连接的协议没有握手这回事。扫描 UDP 端口的标准做法是往目标端口发送一个探测包然后等待结果。判定逻辑是这样的——如果收到 ICMP Port Unreachable说明端口关闭如果收到任何 UDP 回包说明服务活着最麻烦的是第三种情况什么都没收到这可能是端口开放但不响应探测包也可能是防火墙把 ICMP 消息拦了。public int scanUdp(String ip, int port, int timeoutMs) { try (DatagramSocket socket new DatagramSocket()) { socket.setSoTimeout(timeoutMs); byte[] buf new byte[16]; DatagramPacket packet new DatagramPacket(buf, buf.length, InetAddress.getByName(ip), port); socket.send(packet); try { socket.receive(new DatagramPacket(new byte[64], 64)); return 1; // 有响应大概率开放 } catch (SocketTimeoutException e) { return 2; // 无响应开放或过滤记作“未知” } catch (PortUnreachableException e) { return 0; // ICMP 端口不可达关闭 } } catch (Exception e) { return 0; } }UDP 扫出来三种状态0 关闭、1 开放、2 未知。很多初学者在这里翻车拿到“2”就当开放去写报告其实严格地说它只能算“未证明关闭”。我一般把返回值设计成 int 而不是 boolean就是为了在界面上区分这三种情况否则全扫完所有端口都是“开放”跟实际对不上就很尴尬。2.3 为什么选 Java Swing 多线程计算机网络的课程设计选 Java 是有现实考虑的JDK 自带 Socket、DatagramSocket、ExecutorService不需要引第三方包Swing 写界面快一个 JFrame 加个 JTextArea 就能把扫描结果展示清楚跨平台性也让它在 Win10 上测试、在别的机器上演示都不至于出岔子。比用 Python 写的好处是可展示性强比用 C/C 写的好处是内存管理和线程池都不用自己造轮子。线程数设置在 0 到 200 之间之所以留到 200是为了让课程设计可以演示“多线程加速”的效果——比如单线程扫 1-1024 端口可能要好几分钟开到 50 个线程后几十秒就能跑完。3. 源码结构与核心实现校验、扫描、展示一条线3.1 项目结构先认清解压包里有哪些东西PortScan-master 解压后是标准的 Eclipse Java 工程src 和 bin 对应源码与编译输出.project 和 .classpath 直接让 Eclipse 识别项目类型Readme.md 里记录了最终版的功能说明。拿到压缩包后不要急着双击先把目录理清再按顺序读代码。工程里真正要看的类是核心入口和扫描逻辑界面类负责参数录入与结果展示。新建项目也好、直接导入到 Eclipse 也好只要 JDK 版本在 8 以上基本不需要额外配置。目录/文件作用src源码目录集中放 Java 类binEclipse 自动生成的编译输出.projectEclipse 工程描述文件.classpath类路径配置Readme.md使用说明与功能说明3.2 参数校验模块线程数、端口的合法范围先拦住课程设计最容易忽略的环节是入口校验。界面上让用户填 IP、起始端口、结束端口、线程数如果不去校验用户填了个结束端口大于起始端口或者线程数填成 99999程序要么抛异常退出要么卡死。这里有一个通用的校验方法public static void validateArgs(String ip, int startPort, int endPort, int threadCount) { if (ip null || ip.trim().isEmpty()) { throw new IllegalArgumentException(IP 地址不能为空); } if (startPort 0 || endPort 65535) { throw new IllegalArgumentException(端口范围必须在 0-65535 之间); } if (startPort endPort) { throw new IllegalArgumentException(起始端口必须小于等于结束端口); } if (threadCount 0 || threadCount 200) { throw new IllegalArgumentException(线程数必须在 1-200 之间); } }校验放在“点击扫描”事件处理的第一行能够在用户输入非法参数时立刻弹提示而不是等扫描跑了一半才出问题。这里有个设计细节端口允许从 0 开始因为端口 0 在 TCP/UDP 里有特殊含义一般不会去扫它但校验条件里放开是对的——宁可放过不可误伤。线程数上限按 200 截断防止用户把机器资源耗尽。3.3 TCP 扫描任务的执行单元界面拿到合法的参数后要把“起始端口到结束端口”这一段拆成多个探测任务。核心思路是用固定大小的线程池把每个端口作为一个任务丢进去。下面这段是任务提交的主流程也是整个多线程扫描器的心脏。ExecutorService pool Executors.newFixedThreadPool(threadCount); ListInteger openPorts Collections.synchronizedList(new ArrayList()); for (int port startPort; port endPort; port) { final int p port; pool.submit(() - { boolean isOpen tcpScanner.scanTcp(targetIp, p, timeoutMs); if (isOpen) { openPorts.add(p); SwingUtilities.invokeLater(() - resultArea.append(TCP p 开放\n)); } }); } pool.shutdown(); pool.awaitTermination(1, TimeUnit.HOURS);这段代码里要注意两个关键点。第一openPorts 用了 synchronizedList因为多个线程会同时写这个列表普通 ArrayList 会出现并发问题轻则丢数据重则报 ConcurrentModificationException。第二Swing 的界面控件不是线程安全的工作线程里直接调用 resultArea.append 会让界面刷新错乱所以要包一层 SwingUtilities.invokeLater把 UI 操作交回事件分发线程执行。3.4 UDP 扫描结果的差异化展示UDP 扫描不能像 TCP 那样直接返回 boolean得把“开放”“关闭”“未知”三种状态分别处理。界面上我建议把 TCP 和 UDP 的结果分开区域显示或者加上状态前缀避免混在一起看不见谁是谁。调用 scanUdp 后返回值 0 不显示或显示为关闭返回值 1 显示为“UDP 开放”返回值 2 显示为“UDP 未知/可能开放”。int result udpScanner.scanUdp(targetIp, p, udpTimeoutMs); if (result 1) { SwingUtilities.invokeLater(() - resultArea.append(UDP p 开放\n)); } else if (result 2) { SwingUtilities.invokeLater(() - resultArea.append(UDP p 未知无响应\n)); }如果界面只显示“开放”和“关闭”两态UDP 的“未知”会被硬塞进“开放”或者“关闭”导致结果和真实情况对不上。课程设计答辩时能把 UDP 的判定不确定性说出来反而是加分项——说明你真的理解 UDP 没有握手、只能依赖 ICMP 和超时判断的协议特性。3.5 扫描结果的保存注意路径与编码扫描完毕后把结果存成文件是这份资源里很有用的功能。保存逻辑简单但要注意编码问题Windows 记事本默认用 GBK而 Java 的 Files.write 默认按 UTF-8 写入直接用默认方式写出来的文件用记事本打开会乱码。我一般保存时强制指定编码并把路径选择器默认指到桌面。public void saveResult(File file, ListString lines) throws IOException { Files.write(file.toPath(), lines, StandardCharsets.UTF_8); }如果用户用 Windows 记事本打开乱码可以选择在保存时指定为 GBK。这个细节不做的话扫半天存下来的文件没法直接用演示时又要现场复制结果体验很糟糕。4. 多线程参数与调优线程数、超时、任务分发怎么设4.1 线程数 200 的上限是怎么来的项目约束线程数在 0-200 之间看起来是个随意范围实际上有它的合理性。每个并发 TCP 连接都要消耗一个文件描述符和一个本地临时端口Windows 默认的动态端口范围是 49152 到 65535线程数一旦开太大本地端口不够用连接就会开始排队或直接失败扫描结果自然不准。200 这个上限是基于“课程设计演示环境”定的既能看出多线程加速效果又不至于把运行扫描的这台机器拖垮。真正大规模扫描时线程数一般控制在 50-100再高收益递减。4.2 任务分配按端口均匀切分而不是等一个线程跑完线程池的提交方式决定了扫描的均匀性。最简单的做法是端口范围均分成 N 段每段丢给一个线程更平滑的做法是每端口一个任务丢到共享队列里由固定线程消费。后面这个方案负载均衡更好整体耗时也短。核心原因在于每个端口探测的耗时并不一样——开放端口响应快关闭端口要等超时均分区间会让某个线程分到一堆超时端口拖慢整体进度。int totalPorts endPort - startPort 1; int chunkSize Math.max(1, totalPorts / threadCount); for (int i 0; i threadCount; i) { int start startPort i * chunkSize; int end (i threadCount - 1) ? endPort : start chunkSize - 1; pool.submit(() - scanRange(targetIp, start, end)); }这段代码把端口范围拆成尽量均匀的区间最后一段兜底到 endPort。它比每端口提交一个任务少创建很多 Runnable 对象上下文切换开销更低。代价是如果某个区间里恰好全是超时端口尾部等待时间会比较长。对于课程设计规模的扫描两种方式都能接受但知道差别会让你调参时心里有底。4.3 超时参数与重试次数先验后调扫描的“准”和“快”是一对矛盾。TCP 超时设得太短跨网络扫描时把开放的端口误判为关闭设得太长关闭端口要排队等超时总时间暴涨。课程设计里推荐以这张表作为起点然后根据目标主机所在网络微调参数推荐值适用场景TCP connect 超时300-500ms局域网内扫描TCP connect 超时1000ms跨公网扫描UDP 等待超时1500-3000msUDP 本身依赖超时判定线程数50-100完整扫描 1-65535线程数10-20只扫少量常用端口重试策略上TCP 一般不重试一次超时就判关闭因为 connect 失败本身是有明确语义的UDP 建议对“未知”状态重试一次因为丢包和 ICMP 延迟都可能造成假阴性。重试依然无响应就维持“未知”状态不要硬改成“开放”。4.4 界面刷新与扫描线程的协作Swing 线程模型Swing 是单线程模型所有 UI 更新必须在事件分发线程里做。如果直接在扫描线程里调用 append 方法轻则界面闪烁重则抛出异常或者界面直接卡死。这个项目的正确做法是扫描任务放进后台线程UI 更新通过 SwingUtilities.invokeLater 交给事件分发线程。扫描期间“开始”按钮要 disable防止重复提交扫描任务“保存”按钮在扫描结束前也不应该可点否则可能出现保存了一半、扫描线程还在往里写结果的情况。5. 避坑与常见问题排查五条真实踩坑记录5.1 扫描本机 IP结果所有端口都“关闭”现象IP 填的是本机地址扫描后发现连 135、445 这些系统默认开放的服务端口都显示关闭。 原因Windows 防火墙默认拦截入站连接Socket 的 connect 请求被直接丢弃甚至返回超时扫描结果自然全灭。 解决把目标 IP 换成同一局域网内的其他机器先做对照验证或者给被扫描机的防火墙加一条允许规则。课程设计演示时最稳妥的办法是在两台机器之间扫而不是扫本机。如果一定要扫本机可以临时关闭防火墙做验证验证完再开回来记住这个操作要在你有控制权的机器上做。5.2 UDP 扫描结果全是一大串“开放”现象UDP 扫描跑完几百个端口都显示“开放”看着就不对劲。 原因UDP 无响应被判定成开放。很多初版代码把“没收到 ICMP 端口不可达”直接当成“端口开放”但防火墙丢弃 ICMP 消息时关闭端口同样会无响应。 解决把 UDP 状态分成开放、关闭、未知三态。无响应的统一记“未知”只有收到 UDP 回包才算“开放”只有收到 ICMP 不可达才算“关闭”。这样结果虽然保守但不会误导后续判断。5.3 线程数填到 200扫描结果反而“飘”现象同一台目标机线程数设 200 扫出来的开放端口列表跟设 50 时不一样某些端口时有时无。 原因线程过多导致本地端口耗尽后续 connect 复用处于 TIME_WAIT 状态的本地端口连接被重置加上网络层排队超时判定失真。 解决完整端口扫描时把线程数压到 64 以内常用端口小范围扫描时甚至可以只开 16 个线程。扫描速度不是只靠线程数堆出来的超时和重试策略往往影响更大。5.4 点击扫描后界面直接无响应窗口标题出现“未响应”现象点“开始”按钮之后窗口拖不动、按钮点不了看起来像死机。 原因扫描任务直接放在了按钮的事件回调里整个扫描过程塞满了事件分发线程。事件分发线程被阻塞界面当然就冻结了。 解决把扫描循环放到一个独立线程里跑按钮回调只负责收集参数、启动线程。所有 UI 更新用 SwingUtilities.invokeLater 包住。这个坑几乎人手一份排查时先看按钮监听器里有没有循环或阻塞调用。5.5 保存扫描结果用记事本打开是乱码现象保存结果文件后双击打开中文全是乱码英文正常。 原因Java 默认以 UTF-8 写文件Windows 记事本老版本默认按 GBK 解码。 解决写文件时按 GBK 输出或者保存后用支持 UTF-8 的编辑器打开。代码里保存逻辑指定 StandardCharsets.UTF_8并顺手在保存按钮旁提示用户“用 Notepad 或 VS Code 打开避免乱码”。6. 进阶用法对照实验与结果验证方法6.1 先有一组“已知答案”再来验证扫描器准不准扫描器的正确性需要基准来对照。做法是挑一台你完全可控的机器手动记录它已经监听的端口比如启动一个 Python HTTP 服务占住 8000 端口或者 Windows 共享开在 445 端口。然后让扫描器去扫这台机器看结果里是否包含这些已知端口。python -m http.server 8000 netstat -ano | findstr 8000netstat 输出与扫描器结果对得上说明 TCP 扫描逻辑基本可靠对不上优先检查超时和防火墙而不是先怀疑代码。UDP 端口的对照实验难做一点可以用一个简单的 Java/Python UDP 服务绑到某个端口比如 3333再让扫描器去扫它看是否能识别成“开放”。6.2 把布尔结果升级成状态枚举为后续功能留口子初版代码里 TCP 是 booleanUDP 是三态 int。如果想让这个项目更有嚼劲可以把 TCP 的结果也从“开放/关闭”扩成“开放/关闭/被过滤”因为 connect 超时和 connect 被拒绝在语义上并不一样。改法不复杂定义枚举 ScanState把 TCP 超时的结果标记为 FILTERED得到 RST 的标记为 CLOSED。这样扫描结果会更有层次答辩时也能多讲一层协议理解。6.3 扫描速度与准确度的平衡分段扫描比一次梭哈更稳扫 1-65535 全部端口时一次开一百个线程硬扫容易触发目标机的安全策略也容易把本机网络搞乱。我一般会按端口段分批先扫 1-1024再扫 1-1024 之外的常见端口列表最后针对特定协议扫指定的端口范围。分批的另一个好处是每一批可以用不同的超时参数——常见端口给长超时大范围端口给短超时整体速度并不会慢太多准确率却高不少。从那以后我每拿到一台机器的扫描结果第一件事就是先去看上面那三个对照端口在不在结果里——基准不过关后面分析全是空中楼阁。希望这份用心拆解的思路能帮你把这份端口扫描器课程设计真正吃透、跑通、讲明白。本文还有配套的精品资源点击获取
返回列表