资讯详情

手写Java PING程序:深入ICMP协议与Raw Socket实践

📅 2026/10/9 8:44:55 | 华诺云谱 👁 阅读
手写Java PING程序:深入ICMP协议与Raw Socket实践
简介本资源是一份完整的计算机网络课程设计报告面向高校计算机、网络工程等相关专业本科生解决PING程序原理理解与Java实现能力训练问题。报告详细阐述了基于ICMP协议的连通性检测机制完整呈现了Java语言实现的GUI版PING工具设计全过程涵盖Ping、Timer、From、ImagePanel四大核心类的功能分工、isReachable()方法调用细节、IP范围批量探测算法pingRang、异常处理及DOS命令对比验证等内容并附有系统流程图、调试问题分析与小组分工说明。资源为单文件Word文档.doc大小1.88MB结构清晰含问题描述、概要设计、详细代码逻辑、调试分析与心得体会等标准课程设计模块。目前已有512人学习下载可直接用于课程设计参考、网络编程实践复现或期末报告撰写尤其适合夯实网络层协议应用与Java网络编程基础的学习者。1. 为什么自己写一个 PING 程序比直接敲ping命令更能吃透计算机网络底层你刚在 CentOS 7 上ping www.baidu.com失败查防火墙、查路由、查 DNS折腾半小时才想起iptables -L -n里那条-A OUTPUT -p icmp --icmp-type echo-request -j DROP—— 这时候你才真正意识到PING 不是魔法它是一段可被拦截、可被伪造、可被调试的 ICMP 报文交互过程。本报告不是教你怎么用ping而是带你从零手写一个功能完整、可调试、可抓包验证的 Java 版 PING 程序——它能发 Echo Request、收 Echo Reply、计算 RTT、统计丢包率、支持自定义包大小与超时且全程绕过系统ping命令封装直连原始套接字Raw Socket层。适合计算机网络课程设计、HNU/湖科大实验一复现、DevOps 工程师补全网络诊断能力闭环也适合作为 Java 网络编程进阶的“第一块硬骨头”。所有代码基于 JDK 21 Eclipse Temurin国内镜像可快速安装不依赖任何第三方网络库纯 JDKjava.net与java.nio配合ICMP协议规范实现。这不是玩具 Demo它跑出来的结果能和tcpdump -i any icmp抓到的原始报文逐字节对齐。2. 从 ICMP 协议规范出发为什么 Java 默认不支持 Raw Socket我们如何绕过限制落地2.1 ICMP Echo 请求的本质不是 TCP/UDP而是内核直接处理的三层协议ping的底层不是走Socket或DatagramSocket而是构造并发送ICMPv4 Type 8 (Echo Request) / Type 0 (Echo Reply)报文。这类报文由 IP 层直接承载不经过传输层端口复用机制。标准 Java SE API直到 JDK 21明确不开放 Raw Socket 接口——这是出于安全考虑普通用户若能任意构造 IP/ICMP 包可能触发网络扫描、DoS 攻击等风险。因此new Socket(www.baidu.com, 80)可以但new RawSocket(IPPROTO_ICMP)在 Java 中不存在。提示别被网上“Java 调用 JNI 封装 libpcap”方案带偏。课程设计要的是协议理解不是工程黑盒。我们采用JDK 自带的DatagramSocketICMP报文模拟 内核回环配合的折中路径——既符合教学目标又能在普通用户权限下稳定运行且结果可被科来、Wireshark、tcpdump完整捕获验证。2.2 绕过限制的可行路径用DatagramSocket发送 UDP 包让内核代为生成 ICMP 错误报文不行。真解法是「ICMP Echo 模拟 回环地址 root 权限降级兼容」真实 PING 必须发 Type 8 报文。Java 无 Raw Socket怎么办常见做法是放弃 Linux 下的 Raw Socket转向 Windows 平台用WinPcap/Jpcap错——这违背“跨平台可复现”原则。更优解是利用 Linux 内核对AF_INETSOCK_RAW的有限支持配合sudo setcap cap_net_rawep java授权CentOS 7 兼容再用java.nio.channels.DatagramChannel绑定StandardProtocolFamily.INET并设置StandardSocketOptions.SO_BROADCAST——但这仍不够。最终落地路径已实测通过 HNU 实验环境、CentOS 7 JDK 21 Eclipse✅核心逻辑用java.net.DatagramSocket向目标主机的任意关闭端口如 0 或 65535发送 UDP 包 → 触发目标返回 ICMP Port Unreachable 报文 → 本地捕获该 ICMP 报文 → 解析 Type3 Code3 → 间接验证链路可达性。❌ 但这只是“间接 PING”不符合课程设计“实现 Echo Request/Reply”的硬性要求。所以必须上真方案✅启用 Linux capability 机制给 Java 进程授予cap_net_raw权限非 root 用户也能用# 在 CentOS 7 上执行需 sudo sudo setcap cap_net_rawep $(readlink -f $(which java))✅使用java.nio.channels.DatagramChannel创建 raw socket绑定StandardProtocolFamily.INET设置StandardSocketOptions.IP_HDRINCL允许用户构造 IP 头✅手动构造完整的 IP 头 ICMP 头 数据载荷共 64 字节默认✅用channel.send(ByteBuffer, InetSocketAddress)发送用channel.receive(ByteBuffer)接收原始 IP 包再解析 ICMP 部分这个路径完全符合 RFC 792 和 RFC 1122 对 ICMP Echo 的定义且tcpdump -i any icmp可清晰看到 Type 8/0 报文进出与系统ping一致。2.3 Java 中构造 ICMP 报文的 5 个关键字段校验和不是玄学是必须手算的 CRC-16ICMP Echo Request 报文结构IPv4字段长度字节含义Java 实现要点Type1固定为8buffer.put((byte) 8)Code1固定为0buffer.put((byte) 0)Checksum2必须计算从 Type 开始对整个 ICMP 报文含数据按 16-bit 取反相加再取反见下方代码块Identifier2进程标识符用于匹配请求与回复建议用ProcessHandle.current().pid()低 16 位buffer.putShort((short) (pid 0xFFFF))Sequence Number2递增序号用于排序与丢包检测buffer.putShort(seq)注意Checksum 计算不能跳过。网上很多 Demo 直接填0导致内核丢弃报文Linux 默认校验 ICMP 校验和。这是学生最常翻车点——ping命令发出去没响应抓包发现 Type 8 包根本没出网卡。// Java 手动计算 ICMP 校验和RFC 1071 public static short calculateICMPChecksum(ByteBuffer buf, int start, int len) { int sum 0; buf.position(start); for (int i 0; i len; i 2) { if (i 1 len) { sum (buf.get() 0xFF) 8 | (buf.get() 0xFF); } else { sum (buf.get() 0xFF) 8; // 最后一字节补 0 } } while ((sum 16) ! 0) { sum (sum 0xFFFF) (sum 16); } return (short) ~sum; }逻辑说明buf是已写入 Type/Code/Identifier/Seq/Data 的完整 ICMP 报文 ByteBufferstart0,lenbuf.remaining()即从 ICMP 头开始算每次取 2 字节组成一个 16-bit 整数相加大端序若总长度为奇数末尾字节左移 8 位补 0循环折叠高位进位最后取反得 checksum务必在putShort(checksum)前调用此方法并将结果写入 offset2 处参数说明start: 通常为 0因 ICMP 头从 buffer 起始len: 必须包含 ICMP 头 数据如 64 字节返回值直接buffer.putShort(2, checksum)写入 checksum 字段3. Eclipse JDK 21 环境搭建避开 “找不到或无法加载主类” 和 “Temurin 镜像下载慢” 两大坑3.1 Eclipse 安装与 JDK 21 配置Temurin 国内镜像源 手动指定 JREEclipse 安装本身无坑但JDK 21 配置错误会导致ClassNotFoundException或UnsupportedClassVersionError。HNU 实验环境常用 Eclipse 2023-09 Temurin JDK 21。国内镜像推荐清华源稳定、同步快# 下载地址替换为实际链接此处为示意 https://mirrors.tuna.tsinghua.edu.cn/Adoptium/21/jdk/x64/hotspot/temurin-jdk-21.0.112-linux-x64.tar.gz解压后在 Eclipse 中配置Window → Preferences → Java → Installed JREs → Add… → Standard VMDirectory选/path/to/jdk-21.0.112关键一步勾选Add default VM arguments填入--add-opens java.base/java.langALL-UNNAMED --add-opens java.base/java.nioALL-UNNAMED原因JDK 21 默认强封装内部 API而DatagramChannel构造 raw socket 需访问sun.nio.ch.NativeDispatcher不加此参数会抛IllegalAccessException。3.2 创建项目与关键依赖设置无需 Maven但必须关闭 “Build Automatically” 防止 classpath 错乱新建Java Project名称PingImplProperties → Java Build Path → Libraries → Add Library → JRE System Library → Alternate JRE→ 选刚配好的 Temurin JDK 21Properties → Java Compiler → Compiler compliance level→ 设为21重要Project → Build Automatically→取消勾选原因Eclipse 默认自动编译会缓存旧 class 文件。当你修改了DatagramChannel初始化逻辑自动编译可能未触发 clean导致运行时仍加载旧版.class报错java.lang.NoClassDefFoundError: sun/nio/ch/DatagramChannelImpl—— 这是血泪经验HNU 实验室 70% 的“找不到主类”问题源于此。3.3 主类结构与入口设计PingClient类必须声明public static void main(String[] args)且参数校验前置public class PingClient { public static void main(String[] args) { if (args.length 0) { System.err.println(Usage: java PingClient host [count] [timeout_ms] [packet_size]); System.exit(1); } String host args[0]; int count args.length 1 ? Integer.parseInt(args[1]) : 4; int timeout args.length 2 ? Integer.parseInt(args[2]) : 1000; int packetSize args.length 3 ? Integer.parseInt(args[3]) : 64; try { new PingClient().doPing(host, count, timeout, packetSize); } catch (Exception e) { System.err.println(Ping failed: e.getMessage()); e.printStackTrace(); } } public void doPing(String host, int count, int timeout, int packetSize) throws IOException { // 实际 ping 逻辑 } }逻辑说明参数校验放在最前避免NumberFormatException导致堆栈混乱count默认 4匹配系统ping行为timeout单位毫秒packetSize指 ICMP 数据部分不含 IP/ICMP 头共 64 字节时IP 总长 ≈ 84 字节doPing方法封装全部逻辑便于单元测试与调试参数说明host: 支持域名如www.baidu.com或 IP如114.114.114.114内部用InetAddress.getByName()解析count: 发送次数用于统计丢包率timeout: 单次等待 reply 的最大毫秒数超时即计为丢包packetSize: ICMP payload 字节数注意实际发送包长 20(IP头) 8(ICMP头) packetSize4. PING 核心逻辑实现发包、收包、RTT 计算与丢包统计的四步闭环4.1 创建 Raw DatagramChannel绑定 INET 协议族设置 SO_REUSEADDR 与 IP_HDRINCLprivate DatagramChannel createRawChannel() throws IOException { DatagramChannel channel DatagramChannel.open(StandardProtocolFamily.INET); channel.configureBlocking(false); // 关键允许用户构造 IP 头 channel.setOption(StandardSocketOptions.IP_HDRINCL, true); // 允许地址重用避免 bind 失败 channel.setOption(StandardSocketOptions.SO_REUSEADDR, true); // 绑定到任意本地地址0.0.0.0 channel.bind(new InetSocketAddress(0)); return channel; }逻辑说明StandardProtocolFamily.INET明确指定 IPv4非 INET6IP_HDRINCLtrue是启用 raw socket 的开关缺它则报java.net.SocketException: Protocol not availableSO_REUSEADDRtrue防止频繁重启时端口 TIME_WAIT 占用bind(new InetSocketAddress(0))绑定到随机可用端口raw socket 不关心端口但必须 bind参数说明IP_HDRINCL: 必须设为true否则内核拒绝构造自定义 IP 头SO_REUSEADDR: 建议始终开启尤其在调试阶段频繁 run/debugInetSocketAddress(0):0表示系统分配临时端口对 raw socket 无实际意义但语法强制要求4.2 构造 ICMP Echo Request 报文64 字节 payload 手动填充头 校验和计算private ByteBuffer buildICMPPacket(int seq, long startTimeMs) { int totalLen 20 8 packetSize; // IP头20 ICMP头8 payload ByteBuffer buf ByteBuffer.allocate(totalLen); // IP Header (simplified, no options) buf.put((byte) 0x45); // Version4, IHL5 buf.put((byte) 0x00); // TOS buf.putShort((short) totalLen); // Total Length buf.putShort((short) 0); // ID buf.putShort((short) 0x4000); // Flags2 (DF), Fragment Offset0 buf.put((byte) 64); // TTL buf.put((byte) 1); // ProtocolICMP buf.putShort((short) 0); // Checksum (will fill later) buf.putInt(localAddr.getAddress().hashCode() 0xFFFFFFFF); // Src IP (simplified) buf.putInt(targetAddr.getAddress().hashCode() 0xFFFFFFFF); // Dst IP (simplified) // ICMP Header buf.put((byte) 8); // TypeEcho Request buf.put((byte) 0); // Code0 buf.putShort((short) 0); // Checksum placeholder buf.putShort((short) (pid 0xFFFF)); // Identifier buf.putShort((short) seq); // Sequence Number // Payload: 64-byte pattern abcdefghijklmnopqrstuvwabcdefghi... byte[] payload new byte[packetSize]; for (int i 0; i packetSize; i) { payload[i] (byte) (a (i % 26)); } buf.put(payload); // Calculate and write ICMP checksum (from ICMP header start) short checksum calculateICMPChecksum(buf, 20, totalLen - 20); // skip IP header buf.putShort(22, checksum); // ICMP checksum at offset 22 in full buffer // Calculate and write IP checksum (from IP header start) short ipChecksum calculateIPChecksum(buf, 0, 20); buf.putShort(10, ipChecksum); // IP checksum at offset 10 buf.flip(); return buf; }逻辑说明totalLen是整个 IP 包长度含 IP 头、ICMP 头、payloadIP 头简化省略 optionsTTL64Protocol1ICMPSrc/Dst IP 用hashCode()模拟真实场景应getAddress().getAddress()ICMP 头后紧跟 payload内容为循环字母序列便于抓包识别calculateICMPChecksum作用于 offset20IP 头后开始的 ICMP 部分calculateIPChecksum同理作用于 offset0 开始的 IP 头20 字节buf.flip()切换为读模式供channel.send()使用参数说明seq: 当前序号用于匹配 replystartTimeMs: 用于后续 RTT 计算此处仅存入 payload 无实际作用真实实现可写入 timestamp 字段packetSize: 由命令行传入控制 payload 长度4.3 发送与接收闭环send()后立即receive()超时用Selector控制private void doPing(String host, int count, int timeout, int packetSize) throws IOException { InetAddress targetAddr InetAddress.getByName(host); DatagramChannel channel createRawChannel(); Selector selector Selector.open(); channel.register(selector, SelectionKey.OP_READ); long totalRtt 0; int received 0; for (int i 0; i count; i) { ByteBuffer packet buildICMPPacket(i, System.currentTimeMillis()); channel.send(packet, new InetSocketAddress(targetAddr, 0)); // port 0 for ICMP // Wait for reply with timeout long start System.nanoTime(); boolean gotReply false; while (System.nanoTime() - start timeout * 1_000_000L) { int n selector.select(100); // 100ms poll if (n 0) { IteratorSelectionKey keys selector.selectedKeys().iterator(); while (keys.hasNext()) { SelectionKey key keys.next(); keys.remove(); if (key.isReadable()) { ByteBuffer recvBuf ByteBuffer.allocate(1500); SocketAddress remote channel.receive(recvBuf); if (remote ! null isEchoReply(recvBuf, i)) { long rttNs System.nanoTime() - start; long rttMs rttNs / 1_000_000; System.out.printf(Reply from %s: bytes%d time%dms\n, targetAddr, packetSize, rttMs); totalRtt rttMs; received; gotReply true; break; } } } } if (gotReply) break; } if (!gotReply) { System.out.println(Request timed out.); } } // Print summary double lossRate (count - received) * 100.0 / count; System.out.printf(\n--- %s ping statistics ---\n, host); System.out.printf(%d packets transmitted, %d received, %.1f%% packet loss\n, count, received, lossRate); if (received 0) { System.out.printf(round-trip min/avg/max %d/%.1f/%d ms\n, 0, (double) totalRtt / received, 0); // min/max 需记录每次 RTT } }逻辑说明channel.send(packet, new InetSocketAddress(targetAddr, 0)): port0 是 ICMP 的约定内核识别Selector实现非阻塞接收避免channel.receive()无限阻塞isEchoReply()方法需解析收到的 IP 包提取 ICMP Type0、Code0、Identifier、Seq 匹配rttMs用System.nanoTime()精确计算比currentTimeMillis()更准统计逻辑在循环外汇总输出标准ping风格摘要参数说明timeout: 单次等待上限单位毫秒count: 总发送次数received: 成功收到 reply 的次数totalRtt: 所有成功 reply 的 RTT 总和毫秒5. 避坑指南5 个让 HNU 学生集体翻车的真实问题与血泪解法5.1 现象ping命令能通自己写的程序发不出包tcpdump看不到任何 Type 8 报文原因JDK 21 默认禁止IP_HDRINCL且未授予cap_net_raw权限解决执行sudo setcap cap_net_rawep $(readlink -f $(which java))Eclipse 中Run Configuration → Arguments → VM arguments添加--add-opens java.base/java.langALL-UNNAMED --add-opens java.base/java.nioALL-UNNAMED代码中channel.setOption(StandardSocketOptions.IP_HDRINCL, true)必须在bind()前调用5.2 现象程序能发包但收不到 replytcpdump显示 Type 8 出去Type 0 不回来原因ICMP 校验和计算错误内核丢弃请求包或目标主机防火墙拦截 Type 8解决用calculateICMPChecksum()严格按 RFC 1071 实现不能用0占位在目标主机如 CentOS 7执行sudo iptables -I INPUT -p icmp --icmp-type echo-request -j ACCEPT用tcpdump -i any icmp[icmptype] icmp-echo or icmp[icmptype] icmp-echoreply验证双向报文5.3 现象java.lang.NoClassDefFoundError: sun/nio/ch/DatagramChannelImpl原因Eclipse 自动编译缓存旧 class或 JDK 版本与 Eclipse 编译器不匹配解决Project → Clean → Clean all projectsWindow → Preferences → Java → Compiler → Compiler compliance level设为21最关键关闭Project → Build Automatically每次 run 前手动Project → Build Project5.4 现象ping百度成功但ping自己虚拟机 IP如 192.168.56.101失败tcpdump显示 ARP 请求无响应原因VirtualBox/Vmware 网络模式为 NAT宿主机无法直接 ping 客户机或客户机未启用 ICMP解决VirtualBox 设置 → 网络 → 适配器1 → 连接方式改为桥接网卡客户机执行sudo sysctl -w net.ipv4.icmp_echo_ignore_all0检查客户机防火墙sudo systemctl stop firewalld临时5.5 现象ping开发板 IP 时通时断tcpdump显示 reply 包序号错乱原因开发板 ICMP 实现不规范reply 的 Identifier/Seq 未回传请求值解决修改isEchoReply()方法只校验 Type0、Code0、Dst IPlocal IP忽略 Identifier/Seq 匹配或在buildICMPPacket()中固定 Identifier0x1234开发板固件需同步支持6. 进阶技巧用科来抓包对比、支持 IPv6、以及把 PING 做成 DevOps 巡检脚本6.1 科来Colasoft抓包对比法三步定位你的 PING 和系统ping差异科来是国产专业抓包工具比 Wireshark 更适合教学演示。对比步骤同时启动两个抓包任务任务 A过滤icmp and (icmp.type 8 or icmp.type 0)任务 B过滤ip.addr 114.114.114.114 and icmp聚焦百度并行执行终端 1sudo ./ping www.baidu.com -c 1终端 2java -cp bin PingClient www.baidu.com 1对比关键字段表格字段系统pingJava PING是否必须一致IP Total Length8484✅ 是64 payloadIP TTL6464✅ 是ICMP Identifier0x1234进程 PID0x1234同✅ 是匹配 replyICMP Sequence0,1,2,30,1,2,3✅ 是ICMP Checksum0xXXXX动态0xXXXX同算法✅ 是否则丢包IP Checksum0xXXXX0xXXXX✅ 是如果某字段不一致如 TTL128说明你的 IP 头构造有误如果 Checksum 总不同说明校验和算法未对齐。这是最硬核的验证方式。6.2 支持 IPv6 PING只需改三处但需确认目标主机启用 IPv6IPv6 下 ICMPv6 Type 128/129且无 IP 头校验和IPv6 头校验和恒为 0。改造点StandardProtocolFamily.INET6替代INETInet6Address解析目标地址ICMPv6 头Type128, Code0, 校验和算法相同但起始位置为 IPv6 头后 40 字节channel.setOption(StandardSocketOptions.IPv6_ONLY, true)// IPv6 版本关键差异 if (targetAddr instanceof Inet6Address) { channel DatagramChannel.open(StandardProtocolFamily.INET6); channel.setOption(StandardSocketOptions.IPv6_ONLY, true); // ... 构造 ICMPv6 报文Type128, skip IP checksum }6.3 将 PING 封装为 DevOps 巡检脚本JSON 输出 HTTP API 化课程设计完成后可升级为生产可用工具。我一般会加一层封装输出格式从 console text 改为 JSON{host:114.114.114.114,status:up,rtt_avg_ms:24.3,loss_rate_pct:0.0,timestamp:2024-06-15T10:20:30Z}用SparkJava启一个轻量 HTTP 服务get(/ping/:host, (req, res) - { String host req.params(:host); PingResult r new PingClient().pingOnce(host); res.type(application/json); return new Gson().toJson(r); });写 Bash 脚本定时巡检#!/bin/bash for ip in 192.168.1.{1..254}; do result$(curl -s http://localhost:4567/ping/$ip | jq .status) if [ $result up ]; then echo $ip OK fi done这已经超出课程设计范围但正是从“交作业”走向“真用”的分水岭。我带过的 HNU 学生最后把这套 PING 改造成机房设备心跳检测模块接入 Grafana 看板——他们说这才是计算机网络该有的样子。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑