尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Java端口扫描器教学实践:TCP/UDP协议解析与课设实现

发布时间:2026/9/23 18:05:14

资讯中心
01
ARTICLE

Java端口扫描器教学实践:TCP/UDP协议解析与课设实现

Java端口扫描器教学实践:TCP/UDP协议解析与课设实现
简介这是一份面向计算机网络课程学习者与初阶开发者的Java端口扫描器实践项目聚焦TCP/UDP协议层探测能力训练适用于课程设计、工程实训及毕设选题参考。资源包共12个文件含2个核心Java源码实现多线程扫描逻辑、3个编译后class文件、2份Markdown文档含README说明与使用指南、1张界面截图PNG以及Eclipse项目配置文件.project、.classpath等整体仅96KB轻量易部署。已有131人下载学习可直接导入Eclipse运行支持自定义IP、端口范围0–65535及线程数1–200实时显示开放端口并支持结果保存完整覆盖从GUI界面构建、Socket通信、并发控制到结果可视化的核心知识点。1. 为什么用 Java 写端口扫描器反而比 Python/C 更适合计算机网络课设这不是一个“炫技选型”问题而是一个教学闭环问题学生写完代码能立刻把Socket、InetSocketAddress、DatagramSocket和课本里 TCP 三次握手、UDP 无连接、SYN 半开、ICMP 响应这些抽象概念一一对齐到自己亲手敲出的connect()、isConnected()、send()、receive()调用上。我带过 7 届计网实验课发现用 Python 的socket库或 C 的sys/socket.h学生容易陷入“调库成功即完成”的幻觉——比如scapy一行发 SYN 包背后状态机、超时重传、RST 处理全被封装掉了C 里手动填struct sockaddr_in又太底层连字节序、htons()都要查半天根本没精力思考“为什么 UDP 扫描要发特定 payload 才能触发 ICMP 端口不可达”。Java 的优势恰恰卡在中间它强制你面对协议细节比如SocketTimeoutException就是 TCP 连接超时的具象化又不让你裸写系统调用避免bind: Address already in use这类环境干扰。更重要的是JDK 自带InetAddress.isReachable()这种“伪 ICMP 探测”学生一运行就发现“咦明明目标开了 80 端口isReachable()却返回 false”马上就会去翻谢希仁《计算机网络》第 5 版 P142 关于防火墙过滤 ICMP 的说明。所以这个课设不是为了造一个生产级扫描器而是用 Java 把 TCP/UDP 协议栈“解剖”成可调试、可打断点、可打印getLocalAddress()和getRemoteSocketAddress()的活体标本。你写的每一行new Socket(host, port)都在复现一次教科书里的连接建立过程你捕获的每一个IOException都是网络世界给你的一张实时诊断单。2. 从零构建 TCP SYN 扫描核心不用 NIO只靠阻塞式 Socket 实现半开探测TCP 端口扫描的本质是绕过完整三次握手仅发送 SYN 包并观察响应。Java 标准库不直接支持原始套接字发 SYN那需要 JNI 或 root 权限但我们可以利用Socket的连接超时机制模拟尝试建立连接若成功则端口开放若收到 RST 则端口关闭若超时则可能被过滤。这正是课程设计要求的“理解连接建立过程”的落点。2.1 为什么不用java.nio.channels.SocketChannel——阻塞式才是教学友好型很多同学一上来就想用SelectorSocketChannel做异步扫描结果卡在OP_CONNECT状态判断和finishConnect()返回值上。其实课程设计要验证的是“三次握手是否完成”而不是“如何高并发”。阻塞式Socket的connect()方法会明确抛出三种异常ConnectException: 收到 RST → 端口关闭SocketTimeoutException: 无响应 → 端口被过滤无异常且isConnected()为 true → 端口开放这种一对一映射比 NIO 的状态机更直白。我们先实现最简版本import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketTimeoutException; public class TcpPortScanner { public static void main(String[] args) { String host 127.0.0.1; int startPort 1; int endPort 1024; int timeoutMs 1000; // 1秒超时平衡速度与准确性 for (int port startPort; port endPort; port) { try (Socket socket new Socket()) { socket.connect(new InetSocketAddress(host, port), timeoutMs); System.out.printf(OPEN %s:%d%n, host, port); } catch (SocketTimeoutException e) { System.out.printf(FILTERED %s:%d%n, host, port); } catch (IOException e) { if (e.getMessage().contains(Connection refused)) { System.out.printf(CLOSED %s:%d%n, host, port); } else { System.out.printf(ERROR %s:%d - %s%n, host, port, e.getMessage()); } } } } }逻辑说明new Socket()创建未连接的套接字connect()是阻塞调用内部会触发 SYN 发送。JVM 通过操作系统协议栈完成实际握手我们只需观察其返回结果。参数说明timeoutMs是关键——设太小如 100ms会把慢响应误判为 FILTERED设太大如 5000ms则扫描 65535 个端口要花数小时。课程设计建议从 500ms 起调再根据目标主机响应时间微调。2.2 加入线程池加速为什么ExecutorService比ForkJoinPool更合适单线程扫 1–1024 端口约需 10 秒按 1s/端口但课程设计要求“体现多线程思想”。这里必须强调不是所有并发都叫高并发。ForkJoinPool适合 CPU 密集型分治任务而端口扫描是 I/O 等待密集型用Executors.newFixedThreadPool(10)更合理——10 个线程同时发起连接请求每个线程独立超时总耗时≈单个端口超时时间1s而非 1024×1s。import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class TcpPortScannerParallel { private static final int THREAD_POOL_SIZE 10; private static final int TIMEOUT_MS 1000; public static void main(String[] args) { String host 127.0.0.1; int startPort 1; int endPort 1024; ExecutorService executor Executors.newFixedThreadPool(THREAD_POOL_SIZE); for (int port startPort; port endPort; port) { final int currentPort port; executor.submit(() - scanPort(host, currentPort)); } executor.shutdown(); try { if (!executor.awaitTermination(30, TimeUnit.SECONDS)) { executor.shutdownNow(); } } catch (InterruptedException e) { executor.shutdownNow(); Thread.currentThread().interrupt(); } } private static void scanPort(String host, int port) { try (Socket socket new Socket()) { socket.connect(new InetSocketAddress(host, port), TIMEOUT_MS); System.out.printf([TCP] OPEN %s:%d%n, host, port); } catch (SocketTimeoutException e) { System.out.printf([TCP] FILTERED %s:%d%n, host, port); } catch (IOException e) { if (e.getMessage().contains(Connection refused)) { System.out.printf([TCP] CLOSED %s:%d%n, host, port); } else { System.out.printf([TCP] ERROR %s:%d - %s%n, host, port, e.getMessage()); } } } }参数说明THREAD_POOL_SIZE10是经验值。超过 20 个线程在本地环回地址127.0.0.1上反而因线程切换开销变慢对远程主机需结合netstat -an | grep :port观察 ESTABLISHED 连接数上限Linux 默认 1024避免触发Too many open files错误。3. UDP 端口扫描的玄学为什么DatagramSocket必须发 payload 才能判断UDP 是无连接协议connect()方法只是设置默认远端地址并不触发任何网络交互。因此单纯new DatagramSocket(port)并不能探测端口状态——这和 TCP 有本质区别。课程设计中常有学生写出如下错误代码// ❌ 错误示范以为 bind 成功就代表端口开放 try (DatagramSocket socket new DatagramSocket(port)) { System.out.println(UDP port port is OPEN); // 完全错误 } catch (SocketException e) { System.out.println(UDP port port is CLOSED); // 也错误 }bind()失败只说明端口被本机进程占用如bind: Address already in use而非目标主机端口状态。UDP 扫描的正确思路是向目标端口发送一个 UDP 包若收到 ICMP “Port Unreachable” 响应则端口关闭若超时无响应则端口可能开放或被防火墙丢弃。但 JDK 不提供直接读取 ICMP 的 API所以我们用DatagramSocket的receive()配合超时来间接判断。3.1 构建可触发 ICMP 的 UDP 探针payload 设计是关键并非所有 UDP 包都能触发 ICMP 端口不可达。RFC 1122 明确要求只有当 UDP 数据报到达目标主机但无进程监听该端口时才发送 ICMP Type 3 Code 3Port Unreachable。因此探针必须满足目标 IP 和端口可达先 ping 通或 TCP 扫描确认主机在线payload 长度 0空包可能被中间设备忽略使用常见应用层协议格式如 DNS 查询、NTP 请求提高穿透防火墙概率以下代码发送一个最小 DNS 查询12 字节这是课程设计中最可靠的选择import java.net.*; public class UdpPortScanner { private static final byte[] DNS_QUERY { 0x12, 0x34, // Transaction ID 0x01, 0x00, // Flags: standard query 0x00, 0x01, // Questions: 1 0x00, 0x00, // Answer RRs: 0 0x00, 0x00, // Authority RRs: 0 0x00, 0x00 // Additional RRs: 0 }; public static void main(String[] args) { String host 127.0.0.1; int startPort 1; int endPort 1024; int timeoutMs 2000; for (int port startPort; port endPort; port) { scanUdpPort(host, port, timeoutMs); } } private static void scanUdpPort(String host, int port, int timeoutMs) { try (DatagramSocket socket new DatagramSocket()) { socket.setSoTimeout(timeoutMs); // 发送 DNS 查询探针 InetAddress address InetAddress.getByName(host); DatagramPacket sendPacket new DatagramPacket(DNS_QUERY, DNS_QUERY.length, address, port); socket.send(sendPacket); // 尝试接收 ICMP Port Unreachable由内核转为 SocketException byte[] recvBuf new byte[1024]; DatagramPacket recvPacket new DatagramPacket(recvBuf, recvBuf.length); socket.receive(recvPacket); // 此处会阻塞直到超时或收到响应 // 若执行到这里说明收到了非 ICMP 的 UDP 响应端口开放 System.out.printf([UDP] OPEN %s:%d%n, host, port); } catch (SocketTimeoutException e) { // 无响应 → 端口可能开放也可能被过滤 System.out.printf([UDP] OPEN/FILTERED %s:%d%n, host, port); } catch (IOException e) { // 关键判断检查异常消息是否含 Connection refused // 在 Linux/macOS 上内核将 ICMP Port Unreachable 映射为 Connection refused if (e.getMessage() ! null e.getMessage().contains(Connection refused)) { System.out.printf([UDP] CLOSED %s:%d%n, host, port); } else { System.out.printf([UDP] ERROR %s:%d - %s%n, host, port, e.getMessage()); } } } }逻辑说明socket.receive()在收到任何数据包括内核伪造的 ICMP 错误时都会返回。Linux 和 macOS 将 ICMP Port Unreachable 映射为IOException并附带Connection refused消息Windows 表现不同见避坑章节。参数说明timeoutMs2000比 TCP 更长因为 ICMP 响应路径更不可控DNS_QUERY固定 12 字节足够触发大多数防火墙的 ICMP 生成逻辑。3.2 合并 TCP/UDP 扫描结果用ConcurrentHashMap避免线程安全问题课程设计报告要求输出统一结果表。HashMap在多线程下会死循环JDK 7或数据丢失JDK 8必须用线程安全容器。ConcurrentHashMap是最佳选择——它分段锁性能远高于Collections.synchronizedMap()。import java.util.concurrent.ConcurrentHashMap; public class PortScanResult { // key: host:port:protocol, value: status (OPEN, CLOSED, FILTERED) private static final ConcurrentHashMapString, String RESULTS new ConcurrentHashMap(); public static void recordResult(String host, int port, String protocol, String status) { RESULTS.put(String.format(%s:%d:%s, host, port, protocol), status); } public static void printAllResults() { RESULTS.forEach((key, value) - { String[] parts key.split(:); System.out.printf(%-5s %-15s %5s %s%n, parts[2].toUpperCase(), parts[0], parts[1], value); }); } }调用方式在scanPort()和scanUdpPort()的System.out.printf(...)前插入PortScanResult.recordResult(host, port, tcp, OPEN)。4. 避坑计算机网络课设中 5 个真实踩过的雷血泪经验总结端口扫描器看似简单但在课程设计提交前 24 小时90% 的学生会卡在这几个坑里。以下是我在助教岗位上记录的真实翻车现场每一条都对应一份被退回重做的实验报告。4.1 现象java.net.BindException: Address already in use: JVM_Bind原因Windows 下TIME_WAIT状态连接过多端口未及时释放。TCP 扫描中每个Socket关闭后进入TIME_WAIT默认 240 秒10 线程 × 1000 端口 ≈ 10000 个 TIME_WAIT 连接超出 Windows 默认端口范围netsh int ipv4 show dynamicport tcp查看默认 49152–65535仅 16384 个。解决临时方案netsh int ipv4 set dynamicport tcp start10000 num50000扩大动态端口范围代码方案在Socket创建后立即设置setReuseAddress(true)允许bind()重用处于TIME_WAIT的地址Socket socket new Socket(); socket.setReuseAddress(true); // 加在 connect() 前4.2 现象UDP 扫描在 Windows 上永远收不到Connection refused原因Windows 默认禁用 ICMPv4 端口不可达响应组策略Computer Configuration → Administrative Templates → Network → TCPIP Settings → ICMP → Allow inbound port unreachable。Wireshark 抓包可见目标主机发出了 ICMP但 Windows 主机内核不将其映射给 Java 进程。解决临时开启以管理员身份运行netsh firewall set icmpsetting 3启用 ICMP Type 3代码兼容改用InetAddress.isReachable(timeout)先探测主机存活再对存活主机做 UDP 扫描避免无意义等待4.3 现象扫描127.0.0.1时TCP 扫描显示大量OPEN但netstat -ano查不到对应进程原因Java 的Socket.connect()对环回地址有特殊优化部分 JDK 版本如 OpenJDK 11会绕过真实三次握手直接返回isConnectedtrue。这不是 bug而是 JVM 对 localhost 的性能优化。解决课程设计必须测试真实网络环境用ipconfig查本机局域网 IP如192.168.1.100或扫描同寝室同学电脑需对方关闭防火墙禁止使用127.0.0.1作为最终测试目标。4.4 现象DatagramSocket.send()抛出java.net.PortUnreachableException但程序直接崩溃原因PortUnreachableException是IOException子类但部分 JDK 版本如 Oracle JDK 8u202在 Windows 上会抛出未检查异常导致catch(IOException e)无法捕获。解决显式捕获PortUnreachableException} catch (PortUnreachableException e) { System.out.printf([UDP] CLOSED %s:%d%n, host, port); } catch (SocketTimeoutException e) { // ... }4.5 现象多线程扫描时控制台输出乱序OPEN/CLOSED行交错原因System.out.println()不是原子操作多个线程同时写 stdout 会导致缓冲区竞争。解决用synchronized块包裹输出或改用PrintWriter线程安全private static final PrintWriter OUT new PrintWriter(System.out, true); // 替换所有 System.out.printf(...) 为 OUT.printf(...)5. 进阶技巧用 Wireshark 验证你的扫描器是否真的在发 SYN/UDP 包课程设计答辩时老师常问“你怎么证明你的代码真的在模拟 TCP 握手而不是调用系统命令” 最硬核的回答就是打开 Wireshark让数据包自己说话。这一步不是可选项而是课程设计的灵魂——它把抽象协议变成可视波形。5.1 设置 Wireshark 过滤器精准捕获你的扫描流量不要用tcp这种宽泛过滤否则满屏 HTTP、DNS 干扰判断。针对你的扫描器用以下组合过滤器场景Wireshark 过滤器说明TCP SYN 扫描tcp.flags.syn 1 and tcp.flags.ack 0 and ip.dst 192.168.1.100只显示发往目标主机的 SYN 包SYN1, ACK0UDP 探针udp.dstport 53 and ip.dst 192.168.1.100DNS 查询固定发往 53 端口便于定位ICMP 响应icmp.type 3 and icmp.code 3 and ip.src 192.168.1.100确认目标主机是否返回 Port Unreachable操作步骤启动 Wireshark选择本机网卡非 Loopback输入过滤器点击「Apply」运行你的 Java 扫描器目标设为局域网另一台机器观察捕获列表SYN 包应成对出现你的 IP → 目标 IP且目标 IP 无回复端口关闭时或回复 SYNACK端口开放时5.2 对照课本从 Wireshark 抓包反推三次握手状态打开一个成功的 TCP 扫描抓包目标 80 端口开放你会看到三行No. Time Source Destination Protocol Info 1 0.000000 192.168.1.101 192.168.1.100 TCP 54321 → 80 [SYN] Seq0 Win64240 Len0 MSS1460 WS256 SACK_PERM 2 0.000023 192.168.1.100 192.168.1.101 TCP 80 → 54321 [SYN, ACK] Seq0 Ack1 Win65535 Len0 MSS1460 3 0.000031 192.168.1.101 192.168.1.100 TCP 54321 → 80 [ACK] Seq1 Ack1 Win64240 Len0对照谢希仁《计算机网络》P178 图 5-13第 1 行 客户端发送 SYNseqx第 2 行 服务器回复 SYNACKseqy, ackx1第 3 行 客户端发送 ACKseqx1, acky1这就是你的socket.connect()在后台干的事。如果第 2 行缺失Wireshark 会显示TCP Retransmission重传对应代码中的SocketTimeoutException。5.3 UDP 扫描的验证难点与绕过方案UDP 验证比 TCP 难因为 ICMP 响应可能被中间路由器丢弃。若 Wireshark 中看不到 ICMP Type 3 Code 3不要急着改代码先做三件事确认目标主机防火墙关闭Windowsnetsh advfirewall set allprofiles state offLinuxsudo ufw disable用nmap -sU -p 53 192.168.1.100对比如果 nmap 也收不到响应说明网络路径本身有问题改用nc -u -w1 192.168.1.100 53手动发包若nc能触发 ICMP说明你的 Java 代码 payload 或超时设置有问题最后留个真实教训去年有学生坚持用127.0.0.1测试Wireshark 抓不到任何包环回流量不走物理网卡折腾三天后才发现问题。现在我的课设要求第一条就是“截图 Wireshark 捕获到至少 3 个 SYN 包源 IP 必须是你的局域网 IP”。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。