网络协议 -- IP协议

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

IP协议是不可靠、无链接的:c++

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

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

1、IP首部

IP数据报的格式如图:
这里写图片描述数组

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

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

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

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

  • 16位标识:惟一地标识主机发送的每一份数据报。IP数据报的最大长度可达65535字节,但大多数链路层都会对它进行分片。因为TCP自己会把用户数据分红若干片,所以这个字段通常来讲不会影响到TCP。spa

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

  • 13位偏移:用于IP数据报分片。单位为8字节。表示该片相对于原始数据报开始处的位置,能表示的最大偏移为 2 13 2^{13} *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字节。

2、IP首部校验和

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

3、使用代码计算校验和

经过wireshark抓取一帧数据报,如图:
这里写图片描述

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

#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;

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

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

	// 第4步:将第二、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;
}

4、IP校验和的设计原理

咱们将IP首部进行简化来说解IP校验和的设计原理,假设IP首部只有6个字节,第5,6字节存放校验和:
这里写图片描述

计算校验和时第5,6字节置为0,校验和等于:A+B+0,而后取反,即:
这里写图片描述

接收端收到以后校验步骤为:求校验和(不一样的是:校验和位不置0),若此时求得校验和为0,则校验经过。即:
这里写图片描述

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