IP 协议是 TCP/IP 协议族中最核心的协议。所有的 TCP、UDP、ICMP、IGMP 数据都以 IP 数据报的格式传输。

IP 协议是不可靠、无连接的:

  • 不可靠表示 IP 协议不能保证 IP 数据报能成功的到达目的地。IP 仅提供传输服务,任何可靠性的要求都必须由上层来提供(如 TCP)。如果传输过程发生错误,IP 协议简单的丢弃该数据报,然后发送 ICMP 消息给发送端。

  • 无连接表示 IP 协议不维护任何关于后续数据报的状态信息,每个数据报都是相互独立的。这也说明,IP 数据报可能不是按照发送顺序被接收到的,很有可能后发送的数据被先收到。

一、IP 首部

IP 数据报的格式如图:

  • 4 位版本:标识目前采用的 IP 协议的版本号。IPv4 为 0100, IPv6 为 0110

  • 4 位首部长度:用于标识首部的长度,单位为4 字节,所以首部的最大长度为15*4字节=60字节

  • 8 位服务类型:包括 3bit 的优先权字段(已被忽略),4bit 的 TOS 字段,1bit 的始终为 0 的未使用位。

  • 16 位总长度(字节数):整个 IP 数据报的长度。数据报中数据内容的长度=总长度 - 首部长度

  • 16 位标识:唯一地标识主机发送的每一份数据报。IP 数据报的最大长度可达 65535 字节,但大多数链路层都会对它进行分片。由于 TCP 本身会把用户数据分成若干片,因此这个字段一般来说不会影响到 TCP。

  • 3 位标志:用于 IP 数据报分片。该字段第 1bit 不使用,第 2bit 是 DF(Don't Fragment)位,DF 位设为 1 时表明 IP 不对该数据包分片。第 3bit 是 MF(More Fragments)位,当对数据包分片时,除了最后一片外,其他每个组成数据报的片都要把此位设为 1。

  • 13 位偏移:用于 IP 数据报分片。单位为 8 字节。表示该片相对于原始数据报开始处的位置,能表示的最大偏移为*8=65536 字节。

另外,数据报被分片之后,每个片的总长度要更改为该片的长度值。IP 层分片是透明的,但是即使只丢失一片数据也要重传整个数据报,因为 IP 层本身没有超时重传的机制。

  • 8 位生存时间(TTL):设置数据报可以经过的最多路由器数量,每经过一个路由器,该值就减去 1。当该值为 0 时,数据报就被丢弃。通常初始值为 32 或 64.

  • 8 位协议:表示上层传输层所用的协议类型。1 表示 ICMP 协议,2 表示 IGMP 协议,6 表示 TCP 协议,17 表示 UDP 协议。

  • 16 位首部校验和:用于对 IP 首部的正确性进行校验,但不包括数据部分,这点不同于 TCP 和 UDP 的首部校验和。

  • 32 位源 IP 地址:发送端的 32bit 的 IP 地址。

  • 32 位目的 IP 地址:接收端的 32bit 的 IP 地址。

  • 选项:可变长度的可选信息。如果首部不含“选项字段”,则 IP 首部长度为 20 字节。

二、IP 首部校验和

  • 发送端对 IP 数据报的校验和的计算步骤:
  1. 把 IP 数据报的校验和字段置为 0;
  2. 把首部看成以 16 位为单位的数字组成,依次进行二进制反码求和;
  3. 把求和得到的结果取反。
  4. 将第 2、3 步得到的 2 个字节数据存入首部校验和。
  • 接收端对 IP 数据报的校验和的校验步骤:
  1. 把首部看成以 16 位为单位的数字组成,依次进行二进制反码求和;
  2. 把求和得到的结果取反码。
  3. 如果结果为 0,则表示检验和校验通过,IP 报文没有被修改过。

三、使用代码计算校验和

通过 wireshark 抓取一帧数据报,如图:

以该数据报的 IP 首部为基础,使用 C++代码来验证 IP 首部校验和的计算步骤和校验步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include <assert.h>


// GetChecksum函数用于实现上面所说的计算步骤中的第2步、第3步:
// 把首部看成以16位(2字节)为单位的数字组成,依次进行二进制反码求和,
// 但实际的算法实现上需要考虑取和溢出时的改进计算方法(见函数内部注释)
//
unsigned short GetChecksum(unsigned short* ip_header, int size) {
assert(sizeof(unsigned short) == 2);

// 为什么使用unsigned long(4字节)?
// 因为虽然首部校验和只占16位(2个字节),但执行“以16位(2字节)为单位的二进制反码数据”求和操作得到的checksum可能会超过16位(2字节),
// 所以这里用4个字节的unsigned long来接收相加得到的结果,后面再进行处理。
//
unsigned long checksum = 0;

while (size > 1) {
checksum += *ip_header; // 因为都是正数,所以反码与原码相同;故直接相加求和
ip_header++; // ip_header为unsigned short类型的指针每次按2个字节相加

size -= 2;
}
// 执行到这:checksum = 0x2850c

// IP首部如果不包含“选项”字段,则为20字节,偶数;如果包含了“选项”,则字节数就可能为奇数了,
// 这里针对字节数为奇数的情况进行处理。
// 注:示例main函数中构造的ip_header不含有“选项”
//
if (size == 1) {
checksum += *(unsigned char*)ip_header;
}

// 因为上面相加之后的结果大于2个字节,所以执行额外的处理步骤:
// checksum >> 16 右移16位
// 即除以2的16次方(0xffff),就是去除右边的2个字节,如:0x2850c >> 16 = 0x2
//
// checksum & 0xffff 位运算,得到后2个字节
// 如:0x2850c & 0xffff = 0x850c
//
// checksum = 0x2 + 0x850c = 0x850e
//
checksum = (checksum >> 16) + (checksum & 0xffff);

// 假如还大于2个字节,再次将多余的字节和checksum相加。
checksum += (checksum >> 16);

// 求和得到的结果的取反
return (unsigned short)(~checksum);
}


int main()
{
// 将上面wirkshark抓的数据包的IP头部,使用char数组,按字节构造出来
//
unsigned char ip_header[20] = {
0x45, // 4位版本+4位首部长度
0x00, // 8位服务类型(TOS)
0x00, 0x1c, // 16位总长度(字节数)
0x50, 0xaa, // 16位标识
0x00, 0x00, // 3位标志+13位片偏移
0xff, // 8位生存时间(TTL)
0x01, // 8位协议
0xf1, 0x7a, // 16位首部校验和
0xc0, 0xa8, 0x2e, 0x55, // 32位源IP地址
0xee, 0x73, 0x9c, 0x4a // 32位目的IP地址
};


// 第1步:把IP数据包的校验和字段置为0;
//
ip_header[10] = 0x00;
ip_header[11] = 0x00;

// 第2、3步计算校验和
//
unsigned short checksum = GetChecksum((unsigned short*)ip_header, sizeof(ip_header));

printf("%02hhx %02hhx\n", *(char*)(&checksum), *((char*)(&checksum) + 1));

// 第4步:将第2、3步得到的2个字节数据存入首部校验和
//
ip_header[10] = *(char*)(&checksum);
ip_header[11] = *((char*)(&checksum) + 1);


// 模拟接收到IP包之后,对IP首部的校验和进行校验
//
unsigned short checksum_check = GetChecksum((unsigned short*)ip_header, sizeof(ip_header));

if (checksum_check == 0) {
printf("checksum check successful!\n");
}
else {
printf("checksum check failed!\n");
}

return 0;
}

四、IP 校验和的设计原理

我们将 IP 首部进行简化来讲解 IP 校验和的设计原理,假设 IP 首部只有 6 个字节,第 5,6 字节存放校验和:

计算校验和时第 5,6 字节置为 0,校验和等于:A+B+0,然后取反,即:

接收端收到之后校验步骤为:求校验和(不同的是:校验和位不置 0),若此时求得校验和为 0,则校验通过。即:

五、IP 地址相关操作

本节介绍在网络编程中涉及到的与 IP 地址相关的操作

struct in_addr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// sizeof(in_addr) == sizeof(ULONG) == 4
//
typedef struct in_addr {
union {
struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { USHORT s_w1,s_w2; } S_un_w;
ULONG S_addr; // 4个字节,按网络字节序列存储
// 可以使用inet_addr函数将IP格式字符串转为网络字节序列的整数。
} S_un;
// 定义的一些宏,方便访问结构体成员
#define s_addr S_un.S_addr /* can be used for most tcp & ip code */
#define s_host S_un.S_un_b.s_b2 // host on imp
#define s_net S_un.S_un_b.s_b1 // network
#define s_imp S_un.S_un_w.s_w2 // imp
#define s_impno S_un.S_un_b.s_b4 // imp #
#define s_lh S_un.S_un_b.s_b3 // logical host
} IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;

struct sockaddr_in

1
2
3
4
5
6
7
// sizeof(sockaddr_in) == 16
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};

struct sockaddr

1
2
3
4
5
6
// sizeof(sockaddr) == 16
//
struct sockaddr {
u_short sa_family; /* address family */
char sa_data[14]; /* up to 14 bytes of direct address */
};

5.1 转换函数

webrtc 中的IPAddress类和SocketAddress类,对网络地址的操作进行了很好的封装,值得参考。

5.1.1 IP 字符串 -> 整数

1
2
3
unsigned long inet_addr(
_In_ const char *cp
);

将类似127.0.0.1这样的 IP 字符串转换为网络字节序列的整数

5.1.2 整数 -> IP 字符串

1
2
3
char* FAR inet_ntoa(
_In_ struct in_addr in
);

将 in_addr(也可以理解为网络字节序列整数)转换为 IP 字符串。

5.2 字节序列转换

1
2
3
4
5
6
htons
htonl
ntohs
ntohl
htonll
ntohll

对整数(short、long、longlong)进行网络字节序列和主机字节序列间的转换操作。
以 htons 为例:
h是 host 的首字母,表示主机字节序列;
n是 network 的首字母,表示网络字节序列;
s代表 short;
所以 htons 的功能是,将 short 从主机字节序列转为网络字节序列。

字节序列可以参考:http://blog.csdn.net/china_jeffery/article/details/78401731

5.3 获取本机 IP 地址

5.3.1 使用 gethostbyname

这种方式有一个弊端:只能获取一个网卡的 IP 地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned long GetLocalIPv4Address() {
char hostname[MAX_PATH] = { 0 };
gethostname(hostname, MAX_PATH);
struct hostent FAR* lpHostEnt = gethostbyname(hostname);
if (lpHostEnt == NULL) {
return htonl(0x7f000001); //127.0.0.1
}

LPSTR lpAddr = lpHostEnt->h_addr_list[0];

struct in_addr addr;
memcpy(&addr, lpAddr, 4);

return addr.s_addr;
}

5.3.2 使用 GetAdaptersInfo

该方式可以获取本机多块网卡的信息(不限于 IP 地址)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <windows.h>
#include <Iphlpapi.h>
#include <string>
#include <vector>
#pragma comment(lib,"Iphlpapi.lib")

bool GetLocalAddress(std::vector<std::string> &ip_list) {
PIP_ADAPTER_INFO pIpAdapterInfo = new IP_ADAPTER_INFO();
unsigned long stSize = sizeof(IP_ADAPTER_INFO);
int nRet = GetAdaptersInfo(pIpAdapterInfo, &stSize);

if (ERROR_BUFFER_OVERFLOW == nRet) {
delete pIpAdapterInfo;
pIpAdapterInfo = (PIP_ADAPTER_INFO)new BYTE[stSize];
nRet = GetAdaptersInfo(pIpAdapterInfo, &stSize);
}

if (ERROR_SUCCESS != nRet) {
if (pIpAdapterInfo) {
delete pIpAdapterInfo;
}
return false;
}

while (pIpAdapterInfo) {
IP_ADDR_STRING *pIpAddrString = &(pIpAdapterInfo->IpAddressList);
switch (pIpAdapterInfo->Type) {
case MIB_IF_TYPE_OTHER:
case MIB_IF_TYPE_ETHERNET:
case MIB_IF_TYPE_TOKENRING:
case MIB_IF_TYPE_FDDI:
case MIB_IF_TYPE_PPP:
case MIB_IF_TYPE_LOOPBACK:
case MIB_IF_TYPE_SLIP: {
std::string address = pIpAddrString->IpAddress.String;
if ("0.0.0.0" == address)
break;
ip_list.push_back(address);
break;
}
default:
break;
}
pIpAdapterInfo = pIpAdapterInfo->Next;
}

if (pIpAdapterInfo) {
delete pIpAdapterInfo;
}

return true;
}

《TCP/IP 详解 卷 1:协议》在线阅读地址:http://www.52im.net/topic-tcpipvol1.html

文章图片带有“CSDN”水印的说明:
由于该文章和图片最初发表在我的CSDN 博客中,因此图片被 CSDN 自动添加了水印。