本章开始讲解 ICMP 协议,通过学习 ICMP 协议,我们可以顺便掌握 Raw Sockets,中译名为原始套接字。在学习 ICMP 前,还需要简单的了解一下 IP 协议。
IP 协议是 TCP/IP 协议族中最核心的协议,TCP、UDP、ICMP 等众多协议需要依赖它工作。
1. IP 首部
1.1 IP 首部格式
图1 IP 首部
IP 首部相比 TCP 首部要简单的多。因为 TCP 是可靠协议,所以需要更多的字段。
1.2 IP 首部结构体
该结构体定义在 netinet/ip.h
中。不过为了方便学习,我将该结构体定义在了 unp/include/ip.h
,写程序的时候,只要引用该头文件即可。
需要注意该结构体中所有数据都是网络字节序。另一方面,需要注意大小端机器中有些字段摆放的位置是不一样的。结构体中用宏定义 __BYTE_ORDER
的值来判断。比如在小端机器中,第一个字节的高 4 位表示的是版本号,低 4 位表示的是首部长度;而在大端机器中恰好相反。
结构体中有些成员后面有一个冒号,后面跟着数字,相关知识请搜索“位域”,本文就不讲解了。
// IP首部数据结构 // 都是网络字节序 struct ip{ // 主机字节序判断 #if __BYTE_ORDER == __LITTLE_ENDIAN uint8_t ip_hl:4; // 首部长度 uint8_t ip_v:4; // 版本 #endif #if __BYTE_ORDER == __BIG_ENDIAN uint8_t ip_v:4; uint8_t ip_hl:4; #endif uint8_t ip_tos; // 服务类型 uint16_t ip_len; // 总长度 uint16_t ip_id; // 标识符 uint16_t ip_off; // 标志和片偏移 uint8_t ip_ttl; // 生存时间 uint8_t ip_p; // 协议 uint16_t ip_sum; // 校验和 struct in_addr ip_src; // 32位源ip地址 struct in_addr ip_dst; // 32位目的ip地址 // 可选项、数组起始部分 };
2. 字段解释
- 4 位版本号
- 目前协议版本号是 4,即 IPv4.
- 4 位首部长度
- 它的单位是 4 字节。比如如果首部长度是 0x05,则首部长度就是 20 字节。
- 服务类型(TOS)
- 不同的应用程序,可能有不能的需求,有的需要大呑吐量,有的要低时延,有的要可靠性高等等,该字段可以设置不同的值,来满足不同应用程序的需求。如今,大多数 TCP/IP 协议都不使用该字段了,将其设置为 0.
- 总长度字段
- 单位是 1 字节。利用总长度字段和首部长度,可以计算出 IP 数据报中内容的起始位置和长度(总长度减首部长度)。因为该字段为 16 bit,这意味着 IP 数据报总长度可以达到 65535 字节。
- 标识字段
-
该字段能够唯一标识主机发送的每一个完整的数据报。所谓一个完整的数据报,是指两种情况:
- 如果 IP 数据报不分片,它本身就是完整的
- 如果 IP 数据报被分片,则所有标识号相同的数据报可以组装成一个完整的数据报。
- 3 位标识
-
这三个标志位分别是:
- RF (Reservd Fragment,保留位,总是为 0)
- DF (Do Not Fragment,不分片,没有分片)
- MF (More Fragment,还有更多分片)
- TTL 字段
- 该字段表示该数据报最多可以经过多少路由器。当该字段为 0 时,数据报就被丢弃。
- 首部检验和
- 只计算 IP 首部的检验和,该算法名为二进制反码和(ones-complement sum)。为了计算一份数据报的 IP 检验和,首先把检验和字段设置成 0,然后对首部每 16 bit 进行二进制反码求和。
- 8 位协议
-
表示 IP 数据报的数据部分承载的是什么协议。比如该值为 6 时,则 IP 数据报承载 TCP 协议,如果是 17,则承载 UDP 协议,如果是 1,则承载 ICMP 协议。
回忆 socket 函数的第三个参数 —— protocol,在创建 IP 数据报时,内核会将该值填入到 8 位协议字段或者该套接字只接收 protocol 协议的 IP 数据报。
你可以在 netinet/in.h 文件中找到所有协议代号。
- 源 IP 地址和目的 IP 地址
- 每一个 IP 数据报都包含这两个字段,它标识了发送者和接收者的 IP 地址。
- 可选项
-
该选项可以不存在。如果有的话,通常有下面几种:
- 安全和处理限制
- 记录路径
- 时间戳
- 宽松的源站路由
- 严格的源站路由
3. 总结
- 掌握 IP 首部各个字段