C 语言实现 ping 程序
实验介绍
本项目学习用 C 语言实现 ping 命令。通过本项目中更深入地理解 TCP/IP 协议,掌握 C 语言进行网络编程的技巧方法。
一、ping 命令使用的相关 TCP/IP 协议介绍
ping 命令是用来查看网络上另一个主机系统的网络连接是否正常的一个工具。例如,执行 ping 127.0.0.1 会得到如下结果:
由上面的执行结果可以看到,ping 命令执行后显示出被测试系统主机名和相应 IP 地址、返回给当前主机的 ICMP 报文顺序号、ttl 生存时间和往返时间 rtt(单位是毫秒,即千分之一秒)。
要真正了解 ping 命令实现原理,就要了解 ping 命令所使用到的 TCP/IP 协议:ICMP 协议。
ICMP 是(Internet Control Message Protocol)Internet 控制报文协议。它是 TCP/IP 协议族的一个子协议,用于在 IP 主机、路由器之间传递控制消息。
控制消息有:目的不可达消息,超时信息,重定向消息,时间戳请求和时间戳响应消息,回显请求和回显应答消息。
ping 命令使用回显请求和回显应答消息。具体表现是向网络上的另一个主机系统发送 ICMP 报文,如果指定系统得到了报文,它将把报文一模一样地传回给发送者。
下面来看下回显请求和回显应答消息 ICMP 报文格式:
回显请求报文其中类型为 0,代码为 0。
回显应答报文其中类型为 8,代码为 0。
校验和字段:包括数据在内的整个 ICMP 协议数据包的校验和,具体实现方法会在下面详细介绍。
标识符字段:用于唯一标识 ICMP 报文,本项目使用程序的进程 id。因为如果同时在两个命令行终端执行 ping 命令的话,每个 ping 命令都会接收到所有的回显应答,所以需要根据标识符来判断回显应答是否应该接收。
序号字段:ICMP 报文的序号。
数据字段:也就是报文,本项目中我们将发送报文的时间戳放入数据字段,这样当接收到该报文应答的时候可以取出发送时间戳,将接收应答的时间戳减去发送时间戳就是报文往返时间(rtt)。提前预告一下,这里使用gettimeofday()
API函数获取时间戳,详细介绍会在函数介绍部分说明。
ICMP 报文 C 语言实现可以用下面的数据结构表示:
struct icmp{
unsigned char type; // 类型
unsigned char code; // 代码
unsigned short checksum; // 校验和
unsigned short id; // 标识符
unsigned short sequence; // 序号
struct timeval timestamp; // 时间戳
};
unsigned char 正好一字节,也就是 8bit,unsigned short 二字节,也就是 16bit,unsigned int4 字节(32bit),不过上面没用到。其中 struct timeval 类型可能有人不清楚,不过没关系,函数部分说明。
系统发送ICMP报文时会将ICMP报文作为IP的数据,也就是放入IP报文格式的数据字段,IP报文格式如下图所示:
注意上面4位首部长度和16位总长度,4位首部长度不包括数据字段,并且单位是4字节。16位总长度包括数据字段,单位是1字节。
IP报文首部C语言实现可以用下面的数据结构表示:
struct ip{
unsigned char version:4; // 版本
unsigned char hlen:4; // 首部长度
unsigned char tos; // 服务类型
unsigned short len; // 总长度
unsigned short id; // 标识符
unsigned short offset; // 标志和片偏移
unsigned char ttl; // 生存时间
unsigned char protocol; // 协议
unsigned short checksum; // 校验和
struct in_addr ipsrc; // 32位源ip地址
struct in_addr ipdst; // 32位目的ip地址
};
struct in_addr
类型会函数介绍部分说明。
注意ICMP协议是IP层的一个协议,这里有人可能不清楚为什么ICMP协议是IP层的。
这是因为ICMP报文在发送给报文接收方时可能要经过若干子网,会牵涉到路由选择等问题,所以ICMP报文需通过IP协议来发送,是IP层的。
二、地址信息表示
当我们编写网络应用程序时,必然要使用地址信息指定数据传输给网络上哪个主机,那么地址信息应该包含哪些呢?
1.地址族,基于IPv4的地址族还是IPv6的地址族。
2.IP地址。
3.端口号。
为了便于记录地址信息,系统定义了如下结构体:
struct sockaddr_in{
sa_family_t sin_family; // 地址族
uint16_t sin_port; // 端口号
struct in_addr sin_addr; // 32位IP地址
char sin_zero[8]; // 不使用
};
其中