当 ICMP 首部 type = 8, code = 0,该 ICMP 是回显请求报文。当 type = 0, code = 0 时,是回显应答报文。
1. 回显请求与应答报文
1.1 首部格式
图1 ICMP 回显请求与应答报文首部
当 ICMP 报文是回显请求与应答报文时,我们可以看到首部的第 4、5 两个字节是标识符字段,第 6、7 两个字节是序号字段。
1.2 结构体
该结构体定义在 unp/program/include/icmp.h
中。
// icmp 回显报文头部 struct icmp_echo { uint8_t icmp_type; uint8_t icmp_code; uint16_t icmp_cksum; uint16_t icmp_id; uint16_t icmp_seq; char icmp_data[0]; };
2. 回显请求与应答过程
应用进程可以发送一个 type = 8, code = 0 的 ICMP 回显请求报文给目标主机,目标主机收到该 ICMP 报文后,将该报文的 type 字段值改为 0(回显应答报文),原封不动的发送回去。
图2 ICMP 回显请求与应答
如图 2,主机 A 上的进程发送一个回显请求报文(type = 8),同时附带数据 "hello"
,发送到主机 B. 主机 B 收到该报文后,主动发送回显应答给主机 A.
3. 程序设计
3.1 本文程序路径
本文使用的程序托管在 gitos 上:http://git.oschina.net/ivan_allen/unp
如果你已经 clone 过这个代码了,请使用 git pull
更新一下。本节程序所使用的程序路径是 unp/program/icmp/icmpecho
.
3.2 设计思路
之前已经练习过接收 ICMP 报文了,但是我们还没有写过如何发送 ICMP 报文,其实发送也很简单。不过要注意,要不要我们自己构造 IP 数据报?
回答:默认情况下,我们不需要自己构造,直接构造好 IP 数据报的内容发出去就行了。如果你想自己构造,可以使用 setsockotp 函数指定选项 IP_HDRINCL,像下面这样:
int onoff = 1; setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL, &onoff, sizeof(onoff));
在这里,我们并不想自己构造 IP 数据报,因此也不用开启 IP_HDRINCL 选项。所以我们只需要构造一个 ICMP 回显请求报文就行了。
char sendbuf[4096]; struct icmp_echo *icmp_echo = (struct icmp_echo*)sendbuf; // 开始填充 icmp_echo icmp_echo->icmp_type = 8; // 类型为 8,表示回显请求 icmp_echo->icmp_code = 0; icmp_echo->icmp_cksum = 0; // 注意,这个字段一定要正确填写 icmp_echo->icmp_id = getpid() & 0xffff; // 这个字段随便你填,我就填一个进程的 id 号。 icmp_echo->icmp_seq = 0; // 这个字段也是随便你填什么的。 strcpy(icmp_echo->data, "hello"); // 计算整个 icmp 报文的长度(首部长度 + 数据部分长度) icmplen = sizeof(struct icmp_echo) + strlen("hello"); // 所有字段填写完成后,就开始计算校验和 icmp_echo->icmp_cksum = cksum((unsigned short*)sendbuf, icmplen);
如果校验和填不对,这个 IP 数据报会直接被你的主机一声不吭的丢弃。校验和的算法叫 one’s complement sum,中译名为二进制反码求和,如果你感兴趣可以自行搜索它的算法。这里直接给出代码:
unsigned short cksum(unsigned short *addr, int len){ unsigned int sum = 0; while(len > 1){ sum += *addr++; len -= 2; } // 处理剩下的一个字节 if(len == 1){ sum += *(unsigned char*)addr; } // 将32位的高16位与低16位相加 sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); return (unsigned short) ~sum; }
3.3 程序框架(伪代码)
void handler(int sig) { // 每隔一秒发送一个回显请求报文给目标主机 sender(); alarm(1); } int main() { registSignal(SIGALRM, handler); alarm(1); recver(); } void sender() { // 构造 ICMP 回显请求报文发送给目标主机 // ... } void recver() { // 接收 IP 数据报,并解析出 ICMP 报文 // 过滤出 ICMP 回显应答报文(type = 0, code = 0) // 打印 ICMP 报文中的数据部分 }
当然你也可以使用线程的方式发送 ICMP 报文……无所谓啦。
4. 实验
程序 icmp/icmpecho
每隔一秒发送一个 ICMP 请求报文给目标主机 baidu.com,同时接收 baidu.com 发送回来的回显应答报文,然后将接收到的回显数据打印出来。
图1 发送回显请求与接收回显应答
从上面的结果可以看到,我们的程序给主机 baidu.com 发送了一个回显请求,然后又接收到了 baidu.com 的回显应答。
5. 总结
很开心,我们离 ping 命令的实现非常接近了,你完全可以将本程序稍作修改,它就是一个 ping 命令。
- 掌握 ICMP 回显报文的构造
- 掌握发送 ICMP 报文的方法