IP协议是TCP/IP协议族中最核心的协议。全部的TCP、UDP、ICMP、IGMP数据都以IP数据报的格式传输。html
IP协议是不可靠、无链接的:c++
不可靠
表示IP协议不能保证IP数据报能成功的到达目的地。IP仅提供传输服务,任何可靠性的要求都必须由上层来提供(如TCP)。若是传输过程发生错误,IP协议简单的丢弃该数据报,而后发送ICMP消息给发送端。web
无链接
表示IP协议不维护任何关于后续数据报的状态信息,每一个数据报都是相互独立的。这也说明,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字节。表示该片相对于原始数据报开始处的位置,能表示的最大偏移为 *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字节。
经过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; }
咱们将IP首部进行简化来说解IP校验和的设计原理,假设IP首部只有6个字节,第5,6字节存放校验和:
计算校验和时第5,6字节置为0,校验和等于:A+B+0,而后取反,即:
接收端收到以后校验步骤为:求校验和(不一样的是:校验和位不置0),若此时求得校验和为0,则校验经过。即:
《TCP/IP详解 卷1:协议》在线阅读地址:http://www.52im.net/topic-tcpipvol1.html