IP 协议是 TCP/IP 协议族中最核心的协议。所有的 TCP、UDP、ICMP、IGMP 数据都以 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 数据报的校验和字段置为 0;
把首部看成以 16 位为单位的数字组成,依次进行二进制反码求和;
把求和得到的结果取反。
将第 2、3 步得到的 2 个字节数据存入首部校验和。
把首部看成以 16 位为单位的数字组成,依次进行二进制反码求和;
把求和得到的结果取反码。
如果结果为 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> unsigned short GetChecksum (unsigned short * ip_header, int size) { assert (sizeof (unsigned short ) == 2 ); unsigned long checksum = 0 ; while (size > 1 ) { checksum += *ip_header; ip_header++; size -= 2 ; } if (size == 1 ) { checksum += *(unsigned char *)ip_header; } checksum = (checksum >> 16 ) + (checksum & 0xffff ); checksum += (checksum >> 16 ); return (unsigned short )(~checksum); } int main () { unsigned char ip_header[20 ] = { 0x45 , 0x00 , 0x00 , 0x1c , 0x50 , 0xaa , 0x00 , 0x00 , 0xff , 0x01 , 0xf1 , 0x7a , 0xc0 , 0xa8 , 0x2e , 0x55 , 0xee , 0x73 , 0x9c , 0x4a }; ip_header[10 ] = 0x00 ; ip_header[11 ] = 0x00 ; unsigned short checksum = GetChecksum ((unsigned short *)ip_header, sizeof (ip_header)); printf ("%02hhx %02hhx\n" , *(char *)(&checksum), *((char *)(&checksum) + 1 )); ip_header[10 ] = *(char *)(&checksum); ip_header[11 ] = *((char *)(&checksum) + 1 ); 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 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; } S_un; #define s_addr S_un.S_addr #define s_host S_un.S_un_b.s_b2 #define s_net S_un.S_un_b.s_b1 #define s_imp S_un.S_un_w.s_w2 #define s_impno S_un.S_un_b.s_b4 #define s_lh S_un.S_un_b.s_b3 } IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;
struct sockaddr_in 1 2 3 4 5 6 7 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 struct sockaddr { u_short sa_family; char sa_data[14 ]; };
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 ); } 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 自动添加了水印。