IP 协议负责将数据报从源主机发送到目标主机,通过 IP 地址作为唯一识别码,简单来说,不同主机之间的 IP地址是不一样的,在发送数据报的过程中, IP 协议还可能对数据报进行分片处理,同时在接收数据报的时候还可能需要对分片的数据报进行重装等等。

IP地址

IP 地址是软件地址,不是硬件地址,硬件 MAC 地址是存储在网卡中的, 应用于本地网络中寻找目标主机。而 IP 地址能让一个网络中的主机能够与另一个网络中的主机进行通信,无需理会这些主机之间的 MAC地址。
在分类编址中,设计者把所有的 IP 地址划分为 5 大类,分别为 A、 B、 C、 D、 E 五类,每一类地址都觉定了其中 IP 地址的一部分组成。

特殊IP地址

受限广播地址

广播通信是一对所有的通信方式, 受限广播地址用于定义整个互联网, 如果设备想使IP 数据报被整个网络所接收,就发送这个目的地址全为 1 的广播包,但这样会给整个互联网带来灾难性的负担, 所以在任何情况下,路由器都会禁止转发目的地址为255.255.255.255 的广播数据包, 因此这样的数据包仅会出现在本地网络中(局域网) ,255.255.255.255 这个地址指本网段内的所有主机, 相当于“房子里面的人都听着”通知所有主机。
注意:此处不要与以太网的广播地址(255-255-255-255-255-255)混淆了。

直接广播地址

直接广播地址是主机号全为 1 而得到的地址,广播地址代表本网络内的所有主机,使用该地址可以向网络内的所有主机发送数据,比如一个 IP 地址是 192.168.0.181,这是 C 类地址,所以它的主机号只有一个字节,那么对主机号全取 1 得到一个广播地址 192.168.0.255,向这个地址发送数据就能让同一网络下的所有主机接收到。
A、 B、 C 三类地址的广播地址结构如下:
A 类地址的广播地址为: XXX.255.255.255(XXX 为 A 类地址的第一个字节取值范围)。
B类地址的广播地址为: XXX. XXX.255.255(XXX 为 B 类地址的前两个字节取值范围)。
C 类地址的广播地址为: XXX. XXX. XXX.255(XXX 为 C 类地址的前三个字节取值范围)。
注意:这个地址在 IP 数据报中只能作为目的地址。另外,直接广播地址使一个网段中可分配给设备的地址数减少了 1 个。

多播地址

多播地址用在一对多的通信中,即一个发送者,多个接收者, 不论接受者员数量的多少, 发送者只发送一次数据包。 多播地址属于分类编址中的 D 类地址, D 类地址只能用作目的地址,而不能作为主机中的源地址。

环回地址

127 网段的所有地址都称为环回地址,主要用来测试网络协议是否工作正常的作用。比如在电脑中使用 ping 命令去 ping 127.1.1.1 就可以测试本地 TCP/IP 协议是否正常。用通俗的话表示,就是“我自己”,不能以 127 网段中的 IP 地址作为主机地址,因此 A 类地址又少了一个可用网络号

本网络本主机

IP 地址 32bit 全为 0 的地址(0.0.0.0)表示的是本网络本主机, 这个 IP 地址在 IP 数据报中只能用作源 IP 地址,这发生在当设备启动时但又不知道自己的 IP 地址情况下。在使用 DHCP 分配 IP 地址的网络环境中,这样的地址是很常见的, 主机为了获得一个可用的IP 地址,就给 DHCP 服务器发送 IP 数据报,并用这样的地址(0.0.0.0) 作为源地址,目的地址为 255.255.255.255(因为主机这时还不知道 DHCP 服务器的 IP 地址) ,然后 DHCP服务器就会知道这个主机暂时没有 IP 地址,那么就会分配一个 IP 给这个主机。

局域网和广域网、互联网

举个例子, 如果你家在某网络运营商办理了网络服务, 可能他们将送你一个光猫, 然后你拿着这个光猫就能上网了, 你可能觉得需要多人使用网络, 所以你又去买了一个无线路由器, 让家人也能连接到网络上, 那么这个家就是一个局域网, 然后局域网的通信是通过运营商连接到广域网上的, 示意图具体见图

这个示意图虽然简单,却把 LAN、 WAN、 Internet 三者全包含了。无线路由器把电脑、手机等设备连接到局域网 LAN 上,并分配 IP 地址,即局域网 IP,我们可以称之为 LAN-IP,
LAN-IP 所到之处,就是局域网的范围,像我们电脑的 IP 地址(192.168.0.xxx)就是一个局域网 IP,而路由器的地址就是运营商给我们的一个 IP 地址,这个 IP 地址是有效的,可以看做是 WAN-IP(实际上这个 IP 地址也是运营商的局域网 IP 地址(相对运营商来说),这个地址在运营商中转换成一个真正的广域网 IP 地址,但是这些事情我们暂时无需理会,只要把路由器的 IP 地址看做是 WAN-IP 即可) 。
而运营商是一个边界,国家与国家之间有边界,网络之间也有边界, 运营商就是局域网 LAN 与广域网 WAN 的边界。局域网 LAN-IP 可以在局域网内部有效,但是无法跨越边界进入广域网中, LAN-IP 是路由器分配给我们的 IP,那么我们想要跨越边界进入广域网中, 就需要将 LAN-IP 变成有效的的 IP 地址,也就是 WAN-IP,那么在路由器中就需要将IP 地址进行转换,完成 LAN-IP<—>WAN-IP 地址转换(NAT),关于地址转换的内容我们稍后讲解统一换成特别通行证才可以在广域网里继续邀游。
当持有 WAN-IP 的 IP 包顺利到达下一个边界 Internet Gateway,这是通往互联网Internet 的最后一道关卡,即边界。左边是广域网,右边是互联网, 也需要做 WAN-IP 与Global-IP(互联网公共 IP)的转换才能进入互联网中,我们知道这种概念即可,无需过多深入

网络地址转换(NAT)

NAT 英文全称是“Network Address Translation”,中文意思是“网络地址转换”,它是一个 IETF(Internet Engineering Task Force, Internet 工程任务组)标准,允许一个整体机构以一个公用 IP(Internet Protocol)地址出现在 Internet 上。它是一种把内部私有网络地址(IP 地址)翻译成合法网络 IP 地址的技术。
具有 NAT 功能的路由器必须拥有一个内部地址与一个外部地址,内部地址是为了与局域网的用户通信而使用的, 它使用一个特定的内部 IP 地址,如 192.168.0.1(也是局域网的网关),外部地址是与广域网进行通信而使用的,这是一个有效的 IP 地址,通常为运营商分配给我们,假设运营商分配给我们的 IP 地址是一个 C 类网络地址 223.166.166.66, 假设我们电脑上的 IP 地址是 192.168.0.181,端口号是 5555。
那么在局域网的电脑想要与外网进行通信的时候,比如我们想访问百度(假设百度的IP 地址是 123.125.115.110, 端口号是 80) ,那么电脑就会把这些数据报发送到路由器中,请求路由器帮忙转发, 假设这些数据报格式简化为:
(源 IP 地址,端口号,目标 IP 地址,端口号)
(192.168.0.181, 5555, 123.125.115.110, 80)

具有 NAT 功能的路由器会在内部维护一个 NAT 转换表,当路由器收到局域网的 IP 数据报时,就会为这个数据报分配一个路由器内部的 NAT 端口,假设为 6666,并且路由器会将原始 IP 数据报中源 IP 地址与端口号(192.168.0.181, 5555)转换成一个有效 IP 地址与端口号(223.166.166.66, 6666), 然后转换后的路由器发出的数据就是:
(223.166.166.66, 6666, 123.125.115.110, 80)

当百度响应这个数据报的时候,就会返回一个响应信息,它返回的目标 IP 地址就是我们路由器的有效 IP 地址和端口号(223.166.166.66, 6666),那么路由器收到的数据报就是:
(123.125.115.110, 80, 223.166.166.66, 6666)

当路由器收到这个数据报之后,会在 NAT 转换表中查找端口号为 6666 的连接,并且把数据报中的目标 IP 地址与端口号(223.166.166.66, 6666)转换成局域网内我们电脑的IP 地址与端口号(192.168.0.181, 5555),并且将数据报转发到我们的主机上,那么我们电脑上收到的数据报就是:
(123.125.115.110, 80, 192.168.0.181, 5555)

这样子的数据报对于电脑来说,是很正常的通信方式,但是它并不知道路由器已经转换过这些数据报的 IP 地址与端口内容,所以 NAT 对于所有用户来说是透明的,通过这样子的两次转换,局域网就实现了与广域网的通信,在 NAT 转换中, NAT 端口号是一个关键的因素,路由器应及时为每个连接分配唯一的端口号,并且要及时回收那些不使用的端口号。

IP数据报

IP 数据报与 ARP 报文都是一种报文格式,都有自己的组织形式,与 ARP 报文一样,由两部分组成,一部分是 IP 首部,另一部分是数据区域,一个 IP 数据报的首部长度是不定的,通常为 20~60 字节,根据选项决定。而数据区域理论上可以多达65535 个字节,但是很少有数据是那么大的,并且受限于网卡硬件。

版本(号) ,占据 4bit 空间。 这个字段规定了数据报的 IP 协议版本, 对于 IPv4,该值为 4; 对于 IPv6,该值为 6。
首部长度字段占据 4bit 空间,用于记录 IP 首部的数据的长度,为什么需要记录首部长度呢?因为 IP 首部中包含了一些可变的数据选项,故需要这 4bit 记录首部的长度,以便区分数据部分的起始位置,当然啦, 4bit 的部首长度单位是字,只有这样子才能最大记录 60个字节的数据(15*4=60)。
服务类型(TOS)占据 8bit 空间, 服务类型(TOS)包含在 IPv4 首部中,以便使不同类型的 IP 数据报(例如,一些特别要求低时延、高吞吐量或可靠性的数据报)能相互区别开来。提供特定等级的服务是一个由路由器管理员决定的策略问题,简单来说就路由器根据是这个字段的值来为数据报提供(选择)最合理的路径。
数据报长度字段占据 16bit 空间。这是 IP 数据报的总长度(首部加上数据区域),以字节为单位。因为该字段长为 16bit,所以整个 IP 数据报的理论最大长度为 65535 字节,然而,数据报很少有超过 1500 字节的,这是因为底层链路硬件不允许那么大的数据报出现在链路上,以太网数据帧的最大长度为 1500 个字节,当有一个很大的 IP 数据报出现的时候,就需要进行分片处理;而如果 IP 数据报的数据很少的时候,比如少于 46 个字节,那么在以太网进行发送数据的时候会填充一定的字节以满足以太网帧最小长度,那么在接收方就要根据这个字段的内容进行提取有效数据。
标识、标志、 分片偏移量这三个字段与 IP 数据报分片有关
标识字段用于表示 IP 层发送出去的每一份 IP 数据报,在发送每一份报文,该值加 1,在分片的时候,该字段会被复制到每个分片数据报中,在目标接收主机中,使用该字段判断这些数据是否属于同一个 IP 数据报。
**标志位(3bit)**的定义如下:第一位保留未用;第二位是不分片标志位,如果该位为 1,则表示 IP 数据报在发送的过程中不允许进行分片,如果这个 IP 数据报的大小超过链路层能承载的大小,这个 IP 数据报将被丢弃,如果该位为 0 则表示 IP 层在必要的时候可以对其进行分片处理;第三位为更多分片位,如果为 1 则表示该分片数据报不是整个 IP 数据报的最后一个分片,如果为 0 则表示是整个 IP 数据报的最后一个分片。
分片偏移量占据 13bit 空间,表示当前分片所携带的数据在整个 IP 数据报中的相对偏移位置(以 8 字节为单位),目标主机必须受到以 0 偏移量开始到最高偏移量的所有分片,才能将分片进行重装为一个完整的 IP 数据报,并且重装 IP 数据报的依据就是分片的偏移量
生存时间(Time-To-Live, TTL) ,该字段用来确保数据报不会永远在网络中循环(例如由于长时间的路由选择环路) 。每当 IP 数据报由一台路由器处理时,该字段的值减 1,若 TTL 字段减为 0,则该数据报必须丢弃,同时会返回一个 ICMP 差错报文给源主机,这样子数据就不会永远在网络中漂流而占据资源。
上层协议字段占据 8bit 空间。该字段仅在一个 IP 数据报到达其最终目的地才会有用。该字段的值指示了 IP 数据报的数据部分应交给哪个特定的传输层协议。例如,值为 6 表明数据部分要交给 TCP,而值为 17 表明数据要交给 UDP。在 IP 数据报中的协议号所起的作用,类似于运输层报文段中端口号字段所起的作用。协议字段是将网络层与运输层绑定到一起的粘合剂,而端口号是将运输层和应用层绑定到一起的粘合剂,此处了解这个概念即可。
首部检验和字段占据 16bit 空间。首部检验和用于帮助路由器检测收到的 IP 数据报首部是否发生错误,而对应 IP 数据报中的数据区域校验那是上层协议处理的事情。首部检验和是这样计算的:将首部中的每 2 个字节当作一个数,用反码运算对这些数求和,该和的反码(被称为因特网检验和)存放在检验和字段中。路由器要对每个收到的 IP 数据报计算其首部检验和,如果数据报首部中携带的检验和与计算得到的检验和不一致,则表示出现错误,路由器一般会丢弃检测出错误的 IP 数据报。注意了: IP 数据报在到达每个路由器上都必须重新计算检验和并再次存放到原处,因为 TTL 字段以及可能的选项字段会改变。
源 IP 地址与目标 IP 地址就不用过多解释了,源主机在生成 ip 数据包的时候会在源 IP地址字段中插入它的 IP 地址,在目标 IP 地址字段中插入其想要发送的最终目标 IP 地址。
选项字段占据 0~40 个字节。它允许 IP 首部被扩展,首部选项在实际中还是比较少使用的,因此在每个 IP 数据报首部中必须存在的字段是不包括选项字段的,这样能够节约开销,如果与选项字段就添加,而如果没有就无需理会。
数据区域(也可以称之为有效载荷), 这是 IP 数据报的最后的一个字段,也是最重要的内容,因为有数据区域才会有数据报首部的存在, 在大多数情况下, IP 数据报中的数据字段包含要交付给目标 IP 地址的运输层(TCP 协议或 UDP 协议) ,当然, 数据区域也可承载其他类型的报文,如 ICMP 报文。

IP 数据报封装在以太网帧的格式具体见

IP数据报的数据结构

为了描述 IP 数据报首部的信息, LwIP 定义了一个 ip_hdr 的结构体作为描述 IP 数据报首部, 同时还定义了很多获取 IP 数据报首部的宏定义与设置 IP 数据报首部的宏定义

PACK_STRUCT_BEGIN
/* The IPv4 header */
struct ip_hdr {/* version / header length */PACK_STRUCT_FLD_8(u8_t _v_hl); /* 版本 / 首部长度 *//* type of service */PACK_STRUCT_FLD_8(u8_t _tos); /* 服务类型 *//* total length */PACK_STRUCT_FIELD(u16_t _len); /* 数据报总长度 *//* identification */PACK_STRUCT_FIELD(u16_t _id); /* 标识字段 *//* fragment offset field */PACK_STRUCT_FIELD(u16_t _offset); /* 标志与偏移 */
#define IP_RF 0x8000U        /* reserved fragment flag *//* 保留的标志位 */
#define IP_DF 0x4000U        /* don't fragment flag *//* 不分片标志位 */
#define IP_MF 0x2000U        /* more fragments flag *//* 更多分片标志 */
#define IP_OFFMASK 0x1fffU   /* mask for fragmenting bits *//* 用于分段的掩码 *//* time to live */PACK_STRUCT_FLD_8(u8_t _ttl); /* 生存时间 *//* protocol*/PACK_STRUCT_FLD_8(u8_t _proto); /* 上层协议*//* checksum */PACK_STRUCT_FIELD(u16_t _chksum); /* 校验和 *//* source and destination IP addresses */PACK_STRUCT_FLD_S(ip4_addr_p_t src); /* 源 IP 地址与目标 IP 地址 */PACK_STRUCT_FLD_S(ip4_addr_p_t dest);
} PACK_STRUCT_STRUCT;
PACK_STRUCT_END
#ifdef PACK_STRUCT_USE_INCLUDES
#  include "arch/epstruct.h"
#endif/* Macros to get struct ip_hdr fields: */
#define IPH_V(hdr)  ((hdr)->_v_hl >> 4) //获取协议版本
#define IPH_HL(hdr) ((hdr)->_v_hl & 0x0f) //获取首部长度(字)
#define IPH_HL_BYTES(hdr) ((u8_t)(IPH_HL(hdr) * 4))  //获取获取首部长度字节
#define IPH_TOS(hdr) ((hdr)->_tos) //获取服务类型
#define IPH_LEN(hdr) ((hdr)->_len) //获取数据报长度
#define IPH_ID(hdr) ((hdr)->_id) //获取数据报标识
#define IPH_OFFSET(hdr) ((hdr)->_offset) //获取分片标志位+偏移量
#define IPH_OFFSET_BYTES(hdr) ((u16_t)((lwip_ntohs(IPH_OFFSET(hdr)) & IP_OFFMASK) * 8U)) //获取偏移量大小(字节)
#define IPH_TTL(hdr) ((hdr)->_ttl) //获取生存时间
#define IPH_PROTO(hdr) ((hdr)->_proto) //获取上层协议
#define IPH_CHKSUM(hdr) ((hdr)->_chksum) //获取校验和/* Macros to set struct ip_hdr fields: */
#define IPH_VHL_SET(hdr, v, hl) (hdr)->_v_hl = (u8_t)((((v) << 4) | (hl))) //设置版本号跟首部长度
#define IPH_TOS_SET(hdr, tos) (hdr)->_tos = (tos) //设置服务类型
#define IPH_LEN_SET(hdr, len) (hdr)->_len = (len) //设置数据报总长度
#define IPH_ID_SET(hdr, id) (hdr)->_id = (id) //设置标识
#define IPH_OFFSET_SET(hdr, off) (hdr)->_offset = (off) //设置分片标志与偏移量
#define IPH_TTL_SET(hdr, ttl) (hdr)->_ttl = (u8_t)(ttl) //设置生存时间
#define IPH_PROTO_SET(hdr, proto) (hdr)->_proto = (u8_t)(proto) //设置上层协议
#define IPH_CHKSUM_SET(hdr, chksum) (hdr)->_chksum = (chksum) //设置校验和

IP数据报分片

从 IP 首部我们就知道 IP 数据报分片这个概念,也知道不是每个底层网卡都能承载每个 IP 数据报长度的报文,例如以太网帧最大能承载 1500 个字节的数据,而某些广域网链路的帧可承载不超过 576 字节的数据。一个链路层帧能承载的最大数据量叫做最大传送单元(Maximum Transmission Unit, MTU)。
分片处理是将 IP 数据报中的数据分片成两个或更多个较小的 IP 数据报,用单独的链路层帧封装这些较小的 IP 数据报;然后向输出链路上发送这些帧, 每个这些较小的数据报都称为分片,由于 IP 数据报的分片偏移量是用 8 的整数倍记录的,所以每个数据报中的分片数据大小也必须是 8 的整数倍
所有分片数据报在其到达目标主机的传输层之前需要在 IP 层完成重新组装(也称之为重装) 。 IPv4 协议的设计者觉得如果在每个 IP 层中组装分片数据包,那么将严重影响路由器的性能,例如一台路由器, 在收到数据分片后又进行重装,然后再转发,这样子的处理是万万不可的,所以 IPv4 的设计者决定将数据报的重新组装工作放到端系统中,而不是放到网络路由器中,什么是端系统呢?简单来说就是数据包中的目标 IP 地址的主机,在这台机器上的 IP 层进行数据分片的重装,这样子数据分片可以任意在各个路由之间进行转发,而路由器就无需理会数据分片是在哪里重装,只要数据分片不是给路由器的,那么就将其转发出去即可,当然,这样子的处理就会是的每个数据分片到达目标 IP 地址的主机时间是不一样的。
一个主机打算发送 4000 字节的 IP 数据报(20 字节 IP 首部加上 3980 字节 IP 数据区域,假设没有 IP 数据报首部选项字段) , 且该数据报必须通过一条 MTU 为 1500 字节的以太网链路。这就意味着源始 IP 数据报中 3980 字节数据必须被分配为 3 个独立的数据报分片(其中的每个分片也是一个 IP 数据报)。假定初始 IP 数据报贴上的标识号为 666, 那么第一个分片的数据报总大小为 1500 字节(1480 字节数据大小+20 字节 IP 数据报首部) ,分片偏移量为 0,第二个分片的数据报大小也为 1500 字节,分片偏移量为 185(185*8=1480),第三个分片的数据报大小为 1040(3980-1480-1480+20),分片偏移量为370(185+185) 。

/*** Fragment an IP datagram if too large for the netif.** Chop the datagram in MTU sized chunks and send them in order* by pointing PBUF_REFs into p.** @param p ip packet to send* @param netif the netif on which to send* @param dest destination ip address to which to send** @return ERR_OK if sent successfully, err_t otherwise*/
err_t
ip4_frag(struct pbuf *p, struct netif *netif, const ip4_addr_t *dest)
{struct pbuf *rambuf;
#if !LWIP_NETIF_TX_SINGLE_PBUFstruct pbuf *newpbuf;u16_t newpbuflen = 0;u16_t left_to_copy;
#endifstruct ip_hdr *original_iphdr;struct ip_hdr *iphdr;const u16_t nfb = (u16_t)((netif->mtu - IP_HLEN) / 8);u16_t left, fragsize;u16_t ofo;int last;u16_t poff = IP_HLEN;u16_t tmp;int mf_set;original_iphdr = (struct ip_hdr *)p->payload;iphdr = original_iphdr;if (IPH_HL_BYTES(iphdr) != IP_HLEN) {/* ip4_frag() does not support IP options */return ERR_VAL;}LWIP_ERROR("ip4_frag(): pbuf too short", p->len >= IP_HLEN, return ERR_VAL);/* Save original offset */tmp = lwip_ntohs(IPH_OFFSET(iphdr));ofo = tmp & IP_OFFMASK;/* already fragmented? if so, the last fragment we create must have MF, too */mf_set = tmp & IP_MF;left = (u16_t)(p->tot_len - IP_HLEN);while (left) {/* Fill this fragment */fragsize = LWIP_MIN(left, (u16_t)(nfb * 8));#if LWIP_NETIF_TX_SINGLE_PBUFrambuf = pbuf_alloc(PBUF_IP, fragsize, PBUF_RAM);if (rambuf == NULL) {goto memerr;}LWIP_ASSERT("this needs a pbuf in one piece!",(rambuf->len == rambuf->tot_len) && (rambuf->next == NULL));poff += pbuf_copy_partial(p, rambuf->payload, fragsize, poff);/* make room for the IP header */if (pbuf_add_header(rambuf, IP_HLEN)) {pbuf_free(rambuf);goto memerr;}/* fill in the IP header */SMEMCPY(rambuf->payload, original_iphdr, IP_HLEN);iphdr = (struct ip_hdr *)rambuf->payload;
#else /* LWIP_NETIF_TX_SINGLE_PBUF *//* When not using a static buffer, create a chain of pbufs.* The first will be a PBUF_RAM holding the link and IP header.* The rest will be PBUF_REFs mirroring the pbuf chain to be fragged,* but limited to the size of an mtu.*/rambuf = pbuf_alloc(PBUF_LINK, IP_HLEN, PBUF_RAM);if (rambuf == NULL) {goto memerr;}LWIP_ASSERT("this needs a pbuf in one piece!",(rambuf->len >= (IP_HLEN)));SMEMCPY(rambuf->payload, original_iphdr, IP_HLEN);iphdr = (struct ip_hdr *)rambuf->payload;left_to_copy = fragsize;while (left_to_copy) {struct pbuf_custom_ref *pcr;u16_t plen = (u16_t)(p->len - poff);LWIP_ASSERT("p->len >= poff", p->len >= poff);newpbuflen = LWIP_MIN(left_to_copy, plen);/* Is this pbuf already empty? */if (!newpbuflen) {poff = 0;p = p->next;continue;}pcr = ip_frag_alloc_pbuf_custom_ref();if (pcr == NULL) {pbuf_free(rambuf);goto memerr;}/* Mirror this pbuf, although we might not need all of it. */newpbuf = pbuf_alloced_custom(PBUF_RAW, newpbuflen, PBUF_REF, &pcr->pc,(u8_t *)p->payload + poff, newpbuflen);if (newpbuf == NULL) {ip_frag_free_pbuf_custom_ref(pcr);pbuf_free(rambuf);goto memerr;}pbuf_ref(p);pcr->original = p;pcr->pc.custom_free_function = ipfrag_free_pbuf_custom;/* Add it to end of rambuf's chain, but using pbuf_cat, not pbuf_chain* so that it is removed when pbuf_dechain is later called on rambuf.*/pbuf_cat(rambuf, newpbuf);left_to_copy = (u16_t)(left_to_copy - newpbuflen);if (left_to_copy) {poff = 0;p = p->next;}}poff = (u16_t)(poff + newpbuflen);
#endif /* LWIP_NETIF_TX_SINGLE_PBUF *//* Correct header */last = (left <= netif->mtu - IP_HLEN);/* Set new offset and MF flag */tmp = (IP_OFFMASK & (ofo));if (!last || mf_set) {/* the last fragment has MF set if the input frame had it */tmp = tmp | IP_MF;}IPH_OFFSET_SET(iphdr, lwip_htons(tmp));IPH_LEN_SET(iphdr, lwip_htons((u16_t)(fragsize + IP_HLEN)));IPH_CHKSUM_SET(iphdr, 0);
#if CHECKSUM_GEN_IPIF__NETIF_CHECKSUM_ENABLED(netif, NETIF_CHECKSUM_GEN_IP) {IPH_CHKSUM_SET(iphdr, inet_chksum(iphdr, IP_HLEN));}
#endif /* CHECKSUM_GEN_IP *//* No need for separate header pbuf - we allowed room for it in rambuf* when allocated.*/netif->output(netif, rambuf, dest);IPFRAG_STATS_INC(ip_frag.xmit);/* Unfortunately we can't reuse rambuf - the hardware may still be* using the buffer. Instead we free it (and the ensuing chain) and* recreate it next time round the loop. If we're lucky the hardware* will have already sent the packet, the free will really free, and* there will be zero memory penalty.*/pbuf_free(rambuf);left = (u16_t)(left - fragsize);ofo = (u16_t)(ofo + nfb);}MIB2_STATS_INC(mib2.ipfragoks);return ERR_OK;
memerr:MIB2_STATS_INC(mib2.ipfragfails);return ERR_MEM;
}

整个函数是比较复杂的,主要是循环处理数据报的分片,主要是处理偏移量与分片标志,拷贝原始数据的部分到分片空间中并发送出去,然后填写 IP 数据报首部的其他字段,如果是分片的最后一个数据报,则修改标志位并且发送出去,发送完成则释放分片空间。

IP数据报发送

IP 协议是网络层的主要协议,在上层传输协议(如 TCP/UDP) 需要发送数据时,就会将数据封装起来,然后传递到 IP 层, IP 层首先会根据上层协议的目标 IP 地址选择一个合适的网卡进行发送数据,当 IP 协议获得数据后将其封装成 IP 数据报的格式,填写 IP 数据报首部对应的各个字段,如目标 IP 地址、源 IP 地址、协议类型、生存时间等重要信息。
最后在 IP 层通过回调函数 netif->output(即 etharp_output()函数)将 IP 数据报投递给 ARP协议,再调用网卡底层发送函数进行发送,这样子自上而下的数据就发送出去, IP 协议以目标 IP 地址作为目标主机的身份地址。
不过 IP 数据报发送流程是比较麻烦的,因为它嵌套了很多子函数,但是最终调用ip4_output_if _src()函数进行发送数据(此处不讲解带有选项字段的操作) , IP 数据报发送相关的源码具体见代码

/*** Simple interface to ip_output_if. It finds the outgoing network* interface and calls upon ip_output_if to do the actual work.** @param p the packet to send (p->payload points to the data, e.g. nextprotocol header; if dest == LWIP_IP_HDRINCL, p already includes anIP header and p->payload points to that IP header)* @param src the source IP address to send from (if src == IP4_ADDR_ANY, the*         IP  address of the netif used to send is used as source address)* @param dest the destination IP address to send the packet to* @param ttl the TTL value to be set in the IP header* @param tos the TOS value to be set in the IP header* @param proto the PROTOCOL to be set in the IP header** @return ERR_RTE if no route is found*         see ip_output_if() for more return values*///传输层调用
err_t
ip4_output(struct pbuf *p, const ip4_addr_t *src, const ip4_addr_t *dest,u8_t ttl, u8_t tos, u8_t proto)
{struct netif *netif;LWIP_IP_CHECK_PBUF_REF_COUNT_FOR_TX(p);if ((netif = ip4_route_src(src, dest)) == NULL) { //根据目标 IP 地址找到对应的网卡发送数据LWIP_DEBUGF(IP_DEBUG, ("ip4_output: No route to %"U16_F".%"U16_F".%"U16_F".%"U16_F"\n",ip4_addr1_16(dest), ip4_addr2_16(dest), ip4_addr3_16(dest), ip4_addr4_16(dest)));IP_STATS_INC(ip.rterr);return ERR_RTE;}return ip4_output_if(p, src, dest, ttl, tos, proto, netif);
}
err_t
ip4_output_if(struct pbuf *p, const ip4_addr_t *src, const ip4_addr_t *dest,u8_t ttl, u8_t tos,u8_t proto, struct netif *netif)
{#if IP_OPTIONS_SENDreturn ip4_output_if_opt(p, src, dest, ttl, tos, proto, netif, NULL, 0);
}err_t
ip4_output_if(struct pbuf *p, const ip4_addr_t *src, const ip4_addr_t *dest,u8_t ttl, u8_t tos,u8_t proto, struct netif *netif)
{const ip4_addr_t *src_used = src;if (dest != LWIP_IP_HDRINCL) {if (ip4_addr_isany(src)) { //如果源 IP 地址是 0src_used = netif_ip4_addr(netif); //填写网卡的 IP 地址}}return ip4_output_if_src(p, src_used, dest, ttl, tos, proto, netif);
}
err_t
ip4_output_if_src(struct pbuf *p, const ip4_addr_t *src, const ip4_addr_t *dest,u8_t ttl, u8_t tos,u8_t proto, struct netif *netif)
{struct ip_hdr *iphdr;ip4_addr_t dest_addr;LWIP_ASSERT_CORE_LOCKED();LWIP_IP_CHECK_PBUF_REF_COUNT_FOR_TX(p);MIB2_STATS_INC(mib2.ipoutrequests);/* Should the IP header be generated or is it already included in p? *//* 如果 dest 不为 LWIP_IP_HDRINCL,表示 pbuf 中未填写 IP 数据报首部 */if (dest != LWIP_IP_HDRINCL) {u16_t ip_hlen = IP_HLEN;/* generate IP header */if (pbuf_add_header(p, IP_HLEN)) { /* 调整数据区域指针以指向 IP 数据报首部 */LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_output: not enough room for IP header in pbuf\n"));IP_STATS_INC(ip.err);MIB2_STATS_INC(mib2.ipoutdiscards);return ERR_BUF;}iphdr = (struct ip_hdr *)p->payload; //将数据首部区域强制转换成 IP 数据报首部的 ip_hdr 数据结构LWIP_ASSERT("check that first pbuf can hold struct ip_hdr",(p->len >= sizeof(struct ip_hdr)));IPH_TTL_SET(iphdr, ttl); //填写生存时间IPH_PROTO_SET(iphdr, proto); //填写上层协议/* dest cannot be NULL here */ip4_addr_copy(iphdr->dest, *dest); /* 填写目标 IP 地址 */IPH_VHL_SET(iphdr, 4, ip_hlen / 4); //填写版本号+首部长度IPH_TOS_SET(iphdr, tos); //填写服务类型IPH_LEN_SET(iphdr, lwip_htons(p->tot_len)); //填写数据报总长度IPH_OFFSET_SET(iphdr, 0); //填写标志位和分片偏移量IPH_ID_SET(iphdr, lwip_htons(ip_id)); //填写标识++ip_id; //标识加一//填写源 IP 地址if (src == NULL) {ip4_addr_copy(iphdr->src, *IP4_ADDR_ANY4);} else {/* src cannot be NULL here */ip4_addr_copy(iphdr->src, *src);}IPH_CHKSUM_SET(iphdr, 0); //不进行校验和检查} else { //如果已经填写了 IP 首部/* IP header already included in p */if (p->len < IP_HLEN) { //如果数据报总长度小于 IP 数据报首部长度LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_output: LWIP_IP_HDRINCL but pbuf is too short\n"));IP_STATS_INC(ip.err);MIB2_STATS_INC(mib2.ipoutdiscards);return ERR_BUF;}iphdr = (struct ip_hdr *)p->payload; //将数据首部区域强制转换成 IP 数据报首部的 ip_hdr 数据结构ip4_addr_copy(dest_addr, iphdr->dest); //利用 des 变量记录 IP 数据报中的目标 IP 地址dest = &dest_addr;}IP_STATS_INC(ip.xmit);LWIP_DEBUGF(IP_DEBUG, ("ip4_output_if: %c%c%"U16_F"\n", netif->name[0], netif->name[1], (u16_t)netif->num));ip4_debug_print(p);#if IP_FRAG/* don't fragment if interface has mtu set to 0 [loopif] */if (netif->mtu && (p->tot_len > netif->mtu)) {return ip4_frag(p, netif, dest); //分片处理}
#endif /* IP_FRAG */LWIP_DEBUGF(IP_DEBUG, ("ip4_output_if: call netif->output()\n"));return netif->output(netif, p, dest);
}

在上层应用一般不会调用 ip4_output()这些函数,而是通过代码清单 所示的带参宏进行直接发送数据包,因为这些函数层层封装,直接调用底层的函
数效率会更高,比如 UDP 协议就是通过调用 ip_output_if_src()将数据包发送到 IP 层,因此
IP 数据报发送流程示意图具体见图。

 #define ip_output(p, src, dest, ttl, tos, proto) \ip4_output(p, src, dest, ttl, tos, proto)#define ip_output_if(p, src, dest, ttl, tos, proto, netif) \ip4_output_if(p, src, dest, ttl, tos, proto, netif)#define ip_output_if_src(p, src, dest, ttl, tos, proto, netif) \ip4_output_if_src(p, src, dest, ttl, tos, proto, netif)#define ip_output_hinted(p, src, dest, ttl, tos, proto, netif_hint) \ip4_output_hinted(p, src, dest, ttl, tos, proto, netif_hint)#define ip_output_if_hdrincl(p, src, dest, netif) \ip4_output_if(p, src, LWIP_IP_HDRINCL, 0, 0, 0, netif)#define ip_route(src, dest) \ip4_route_src(src, dest)#define ip_netif_get_local_ip(netif, dest) \ip4_netif_get_local_ip(netif)

IP数据报接收

一个 IP 数据报从网卡进入到 IP 协议, 是通过ethernet_input()函数再到 ip4_input()函数进入 IP 协议中被处理,对于 IPv4 版本的协议,所有的 IP 数据报都需要经过 ip4_input()才能进入 IP 协议中,相对于 IP 数据报发送,接收的流程会更加复杂,因为对于所有输入的 IP 数据报,内核都要确认这些数据报是否是给自己的,并且还要保证这些数据报的格式必须是正确的;如不是给本地的数据报, IP 层还要将这些数据报进行转发或者丢弃,当然,如果对于分片的数据报, IP 层还需要负责将其组装起来,并且校验组装完成的数据报是否完整,如果不完整则丢弃它(关于组装数据报的部分比较麻烦,我们也无需了解太多,只要知道即可) ,当数据报是正确的, IP 层就会递交给上层协议(如 UDP 协议、 TCP 协议), ip4_input()源码具体见代码清单

err_t
ip4_input(struct pbuf *p, struct netif *inp)
{const struct ip_hdr *iphdr;struct netif *netif;u16_t iphdr_hlen;u16_t iphdr_len;
#if IP_ACCEPT_LINK_LAYER_ADDRESSING || LWIP_IGMPint check_ip_src = 1;
#endif /* IP_ACCEPT_LINK_LAYER_ADDRESSING || LWIP_IGMP */
#if LWIP_RAWraw_input_state_t raw_status;
#endif /* LWIP_RAW */LWIP_ASSERT_CORE_LOCKED();IP_STATS_INC(ip.recv);MIB2_STATS_INC(mib2.ipinreceives);/* identify the IP header */iphdr = (struct ip_hdr *)p->payload; /* 识别 IP 数据报首部 *///如果不是 IPv4,删除 pbuf,返回 ERR_OK。if (IPH_V(iphdr) != 4) {LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_WARNING, ("IP packet dropped due to bad version number %"U16_F"\n", (u16_t)IPH_V(iphdr)));ip4_debug_print(p);pbuf_free(p);IP_STATS_INC(ip.err);IP_STATS_INC(ip.drop);MIB2_STATS_INC(mib2.ipinhdrerrors);return ERR_OK;}#ifdef LWIP_HOOK_IP4_INPUTif (LWIP_HOOK_IP4_INPUT(p, inp)) {/* the packet has been eaten */return ERR_OK;}
#endif/* obtain IP header length in bytes */iphdr_hlen = IPH_HL_BYTES(iphdr);/* obtain ip length in bytes */iphdr_len = lwip_ntohs(IPH_LEN(iphdr));/* Trim pbuf. This is especially required for packets < 60 bytes. */if (iphdr_len < p->tot_len) {pbuf_realloc(p, iphdr_len);}/* header length exceeds first pbuf length, or ip length exceeds total pbuf length? */if ((iphdr_hlen > p->len) || (iphdr_len > p->tot_len) || (iphdr_hlen < IP_HLEN)) {if (iphdr_hlen < IP_HLEN) {LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS,("ip4_input: short IP header (%"U16_F" bytes) received, IP packet dropped\n", iphdr_hlen));}if (iphdr_hlen > p->len) {LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS,("IP header (len %"U16_F") does not fit in first pbuf (len %"U16_F"), IP packet dropped.\n",iphdr_hlen, p->len));}if (iphdr_len > p->tot_len) {LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS,("IP (len %"U16_F") is longer than pbuf (len %"U16_F"), IP packet dropped.\n",iphdr_len, p->tot_len));}/* free (drop) packet pbufs */pbuf_free(p);IP_STATS_INC(ip.lenerr);IP_STATS_INC(ip.drop);MIB2_STATS_INC(mib2.ipindiscards);return ERR_OK;}/* copy IP addresses to aligned ip_addr_t */ip_addr_copy_from_ip4(ip_data.current_iphdr_dest, iphdr->dest);ip_addr_copy_from_ip4(ip_data.current_iphdr_src, iphdr->src);/* match packet against an interface, i.e. is this packet for us? */if (ip4_addr_ismulticast(ip4_current_dest_addr())) {if ((netif_is_up(inp)) && (!ip4_addr_isany_val(*netif_ip4_addr(inp)))) {netif = inp;} else {netif = NULL;}
#endif /* LWIP_IGMP */} else {/* start trying with inp. if that's not acceptable, start walking thelist of configured netifs. */if (ip4_input_accept(inp)) {netif = inp;} else {netif = NULL;
#if !LWIP_NETIF_LOOPBACK || LWIP_HAVE_LOOPIF/* Packets sent to the loopback address must not be accepted on an* interface that does not have the loopback address assigned to it,* unless a non-loopback interface is used for loopback traffic. */if (!ip4_addr_isloopback(ip4_current_dest_addr()))
#endif /* !LWIP_NETIF_LOOPBACK || LWIP_HAVE_LOOPIF */{#if !LWIP_SINGLE_NETIFNETIF_FOREACH(netif) {if (netif == inp) {/* we checked that before already */continue;}if (ip4_input_accept(netif)) {break;}}
#endif /* !LWIP_SINGLE_NETIF */}}}{if ((ip4_addr_isbroadcast(ip4_current_src_addr(), inp)) ||(ip4_addr_ismulticast(ip4_current_src_addr()))) {/* packet source is not valid */LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_TRACE | LWIP_DBG_LEVEL_WARNING, ("ip4_input: packet source is not valid.\n"));/* free (drop) packet pbufs */pbuf_free(p);IP_STATS_INC(ip.drop);MIB2_STATS_INC(mib2.ipinaddrerrors);MIB2_STATS_INC(mib2.ipindiscards);return ERR_OK;}}/* packet not for us? */if (netif == NULL) {/* packet not for us, route or discard */LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_TRACE, ("ip4_input: packet not for us.\n"));{IP_STATS_INC(ip.drop);MIB2_STATS_INC(mib2.ipinaddrerrors);MIB2_STATS_INC(mib2.ipindiscards);}pbuf_free(p);return ERR_OK;}/* packet consists of multiple fragments? */if ((IPH_OFFSET(iphdr) & PP_HTONS(IP_OFFMASK | IP_MF)) != 0) {LWIP_DEBUGF(IP_DEBUG, ("IP packet is a fragment (id=0x%04"X16_F" tot_len=%"U16_F" len=%"U16_F" MF=%"U16_F" offset=%"U16_F"), calling ip4_reass()\n",lwip_ntohs(IPH_ID(iphdr)), p->tot_len, lwip_ntohs(IPH_LEN(iphdr)), (u16_t)!!(IPH_OFFSET(iphdr) & PP_HTONS(IP_MF)), (u16_t)((lwip_ntohs(IPH_OFFSET(iphdr)) & IP_OFFMASK) * 8)));/* reassemble the packet*/p = ip4_reass(p);/* packet not fully reassembled yet? */if (p == NULL) {return ERR_OK;}iphdr = (const struct ip_hdr *)p->payload;}/* send to upper layers */LWIP_DEBUGF(IP_DEBUG, ("ip4_input: \n"));ip4_debug_print(p);LWIP_DEBUGF(IP_DEBUG, ("ip4_input: p->len %"U16_F" p->tot_len %"U16_F"\n", p->len, p->tot_len));ip_data.current_netif = netif;ip_data.current_input_netif = inp;ip_data.current_ip4_header = iphdr;ip_data.current_ip_header_tot_len = IPH_HL_BYTES(iphdr);#if LWIP_RAW/* raw input did not eat the packet? */raw_status = raw_input(p, inp);if (raw_status != RAW_INPUT_EATEN)
#endif /* LWIP_RAW */{pbuf_remove_header(p, iphdr_hlen); /* Move to payload, no check necessary. */switch (IPH_PROTO(iphdr)) {#if LWIP_UDPcase IP_PROTO_UDP:
#if LWIP_UDPLITEcase IP_PROTO_UDPLITE:
#endif /* LWIP_UDPLITE */MIB2_STATS_INC(mib2.ipindelivers);udp_input(p, inp);break;
#endif /* LWIP_UDP */
#if LWIP_TCPcase IP_PROTO_TCP:MIB2_STATS_INC(mib2.ipindelivers);tcp_input(p, inp);break;
#endif /* LWIP_TCP */
#if LWIP_ICMPcase IP_PROTO_ICMP:MIB2_STATS_INC(mib2.ipindelivers);icmp_input(p, inp);break;
#endif /* LWIP_ICMP */
#if LWIP_IGMPcase IP_PROTO_IGMP:igmp_input(p, inp, ip4_current_dest_addr());break;
#endif /* LWIP_IGMP */default:
#if LWIP_RAWif (raw_status == RAW_INPUT_DELIVERED) {MIB2_STATS_INC(mib2.ipindelivers);} else
#endif /* LWIP_RAW */{#if LWIP_ICMP/* send ICMP destination protocol unreachable unless is was a broadcast */if (!ip4_addr_isbroadcast(ip4_current_dest_addr(), netif) &&!ip4_addr_ismulticast(ip4_current_dest_addr())) {pbuf_header_force(p, (s16_t)iphdr_hlen); /* Move to ip header, no check necessary. */icmp_dest_unreach(p, ICMP_DUR_PROTO);}
#endif /* LWIP_ICMP */LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("Unsupported transport protocol %"U16_F"\n", (u16_t)IPH_PROTO(iphdr)));IP_STATS_INC(ip.proterr);IP_STATS_INC(ip.drop);MIB2_STATS_INC(mib2.ipinunknownprotos);}pbuf_free(p);break;}}/* @todo: this is not really necessary... */ip_data.current_netif = NULL;ip_data.current_input_netif = NULL;ip_data.current_ip4_header = NULL;ip_data.current_ip_header_tot_len = 0;ip4_addr_set_any(ip4_current_src_addr());ip4_addr_set_any(ip4_current_dest_addr());return ERR_OK;
}

《lwip学习7》-- IP协议相关推荐

  1. 互联网协议学习笔记-----IP协议与传输协议

    欢迎参考阅读,作者水平有限,如有帮助,不幸荣光 1.What's a protocol? protocols define format, order of messages sent and rec ...

  2. TCP/IP协议:最大传输单元MTU 路径MTU

    最大传输单元MTU 以太网和8 0 2 . 3对数据帧的长度都有一个限制,其最大值分别是1 5 0 0和1 4 9 2字节.链路层的这个特性称作M T U,最大传输单元.不同类型的网络大多数都有一个上 ...

  3. LWIP学习笔记(1)---LWIP简介

    LWIp综述 阅读:嵌入式网络那些事:LWIP协议深度剖析于实战演练 总结 LWIP是TCP/IP协议的一种独立 简单的实现,设计目的在于:保证嵌入式产品拥有完整的TCP/IP功能的同时,又能保证协议 ...

  4. SOME/IP协议详解「总目录」

    SOME/IP协议详解「总目录」 欢迎大家来到雪云飞星的<SOME/IP协议详解>,开始前博主先列出本专栏学习的大纲,同时这也可以作为大家学习SOME/IP协议知识点的参考.下面蓝字都是传 ...

  5. ZYNQ -Lwip和TCP/IP简介

    TCP/IP TCP/IP 通信协议是对计算机必须遵守的规则的描述,只有遵守这些规则,计算机之间才能进行通信.浏览器与服务器都在使用 TCP/IP 协议, E-Mail 使用 TCP/IP 协议,电子 ...

  6. 《lwip学习6》-- ARP协议

    初始ARP 地址解析协议(Address Resolution Protocol, ARP)是通过解析 IP 地址得到数据链路层地址的,是一个在网络协议包中极其重要的网络传输协议,它与网卡有着极其密切 ...

  7. TCP/IP详解学习笔记(3)-IP协议,ARP协议,RARP协议

    把这三个协议放到一起学习是因为这三个协议处于同一层,ARP协议用来找到目标主机的Ethernet网卡Mac地址,IP则承载要发送的消息.数据链路层可以从ARP得到数据的传送信息,而从IP得到要传输的数 ...

  8. TCP/IP协议学习笔记

    TCP/IP详解学习笔记(1)-基本概念 为什么会有TCP/IP协议 在世界上各地,各种各样的电脑运行着各自不同的操作系统为大家服务,这些电脑在表达同一种信息的时候所使用的方法是千差万别.就好像圣经中 ...

  9. 《TCP/IP详解》学习笔记(三):IP协议、ARP协议

    把这三个协议放到一起学习是因为这三个协议处于同一层,ARP 协议用来找到目标主机的 Ethernet 网卡 Mac 地址,IP 则承载要发 送的消息.数据链路层可以从 ARP 得到数据的传送信息,而从 ...

最新文章

  1. django_4数据库3——admin
  2. mysql 录入窗体设计_在Access中,可用于设计输入界面的对象是   A)窗体 B)报表 C)查询 D)表...
  3. (Mark)操作系统原理
  4. Soj题目分类 python代码)
  5. 持续演进,克服“REST缺乏”
  6. Linux下pwn从入门到放弃,pwn从入门到放弃第六章——简单ROP
  7. Ubuntu18.04 unzip解压zip文件乱码的解决方法
  8. 【编程之美】2.21 只考加法的面试题
  9. mysql 编译安装与rpm安装的区别_编译安装与RPM安装的区别
  10. influxdb数据过期_为什么腾讯QQ的大数据平台选择了InfluxDB数据库?
  11. Express框架的安装通信测试 - 讲解篇
  12. 学习 Python 的 14 张思维导图
  13. python私有化方法_Python 私有化
  14. PHP开发中的中文编码问题
  15. 小说自动采集+【深度seo优化】+自适应=小说网站源码
  16. 新浪离职员工写给新浪各位高层的信
  17. 云顶之弈服务器维护多长时间,英雄联盟3.17更新维护时间介绍 云顶之弈什么时候可以玩_18183云顶之弈专区...
  18. POJ-3376 Finding Palindromes
  19. Aras Innovator: TOC category的视图
  20. 微信小程序 position: absolute位置错乱问题

热门文章

  1. Require.js用法
  2. 2022微信红包封面制作过程--这样,那样,最后
  3. GridView 使用介绍
  4. 全覆盖路径规划思想(2)
  5. 2549 自然数和分解
  6. 怎样解决 -- 电脑点击右键反应慢 ?
  7. 计算机网络国培总结,信息技术国培学习心得体会
  8. 1088: 获取出生日期(多实例测试)
  9. python实例-----名片管理系统
  10. mysql insert into on_MySQL之INSERT INTO ON DUPLICATE KEY UPDATE用法详解 | 夕辞