IP协议是TCP/IP协议族中最为核心的协议,TCP、UDP、ICMP、IGMP数据都以IP数据报格式传输(IPv4、IPv6)。
IP协议指网际互联协议,Internet Protocol,为上层提供了无连接、不可靠的服务;
无连接:IP通讯双方都不长久的维持对方的任何信息;这表明每次上层协议发送数据都需要指定IP地址。
不可靠:IP协议不能把IP数据报准确到达接收端,只能尽力;一旦发送失败,就会通知上层协议,但是不会重发。
IP寻址:在主机A和主机B不处于同一网段时,就会由主机A发送数据到网关,也就是路由器中,路由器经过解封装,获得目标的IP地址的网络号(IP地址分为网络号和主机号),进行发送;
路由表,也及时路由选择,包含三个信息:目标网络,下一跳地址以及子网掩码;通过路由表来把数据发送到相应的路由器中;
分片和重组就是本节课的重点,大于MTU就会需要这两个操作来获取完整数据(MTU也就是1500字节)。
IP协议会与以下三个协议配套使用:ARP、ICMP以及IGMP协议(正点原子的课程中没有涉及)
物理地址,也就是MAC地址,是数据链路层和物理层使用的地址;而IP地址是网络层及以上层使用的地址。
IP地址由网络号和主机号构成,根据位的不同,可分为ABCDE五类地址。
总结:
可以看出,这里是采用IPv4;IP协议的首部一共有20字节;第三个是差分服务区域;然后总长度是52字节;标识位;标志位;偏移量(一般为8的倍数);生存时间;上层协议,是TCP;校验和;源和目标的IP地址。
identification:
标识一个分片(例如经过分片,4000字节的数据就要分为1500、1500、1060三片,分片后的数据包identification字段是一样的);之所以是1060,是因为每一片最多1500字节的信息,还需要包含IP的结构体大小20字节,也就是说每一片最多带传输数据1480字节;
fragment offset:
偏移量,计算的是payload部分;
payload就是去掉二层头部和三层头部后的内容;
1500字节的分片,就是IP+payload的大小;
加上E2,整个数据包抓包看到的大小是1514字节。
数据大小为3980,所有的标识是一样的,代表是同一个数据,经过分片;然后标志位前两个都是1,代表不是最后一个分片,直到最后一个分片变成0;偏移量每一个都不一样;总长度最后一片就是3980-1480-1480+20的结果。
这个就是之前课程有讲的pbuf的内存申请出来的格式,其中tot_len就可以直接置3960,payload指向pbuf结构体结束之后的地址再向后偏移54个字节处;
payload指针向上偏移20个字节(这一过程调用pbuf_add_header(p, TCP_HLEN)),该段留作TCP的首部;tot_len变为3960+20;
payload继续上移20字节(这一过程调用pbuf_add_header(p, IP_HLEN)),留作IP头部;tot_len变为3960+20+20;
源码和对应的注释如下:
err_t
ip4_frag(struct pbuf *p, struct netif *netif, const ip4_addr_t *dest)
{
struct pbuf *rambuf;
#if !LWIP_NETIF_TX_SINGLE_PBUF
struct pbuf *newpbuf;
u16_t newpbuflen = 0;
u16_t left_to_copy;
#endif
struct ip_hdr *original_iphdr;
struct ip_hdr *iphdr;
/* 第一步:nfb = (1500 - 20)/8 = 185 */
const u16_t nfb = (u16_t)((netif->mtu - IP_HLEN) / 8);
u16_t left, fragsize;
u16_t ofo;
int last;
/* poff = 20 */
u16_t poff = IP_HLEN;
u16_t tmp;
int mf_set;
/* 第二步:获取IP首部信息 */
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;
}
/* 第四步:tmp = 保存偏移量的字段数值 = 0 */
tmp = lwip_ntohs(IPH_OFFSET(iphdr));
/* 第五步:ofo = 0 & 0x1fffU = 0 */
ofo = tmp & IP_OFFMASK;
/* 第六步:mf_set = 0 & 0x2000U = 0 */ */
mf_set = tmp & IP_MF;
/* 第七步:left = p->tot_len - 20
1:4000 - 20 = 3980 */
left = (u16_t)(p->tot_len - IP_HLEN);
/* 第八步:left > 0 ? */
while (left) {
/* 第九步:分片大小fragsize = left < (nfb * 8) ? left : (nfb * 8)
1:3980 < 1480 ? 3980 : 1480 fragsize = 1480
2:2500 < 1480 ? 2500 :1480 fragsize = 1480
3:1020 < 1480 ? 1020 :1480 fragsize = 1020 */
fragsize = LWIP_MIN(left, (u16_t)(nfb * 8));
/* 第十步:申请pbuf内存---用来描述分片 */
rambuf = pbuf_alloc(PBUF_LINK, IP_HLEN, PBUF_RAM);
if (rambuf == NULL) {
goto memerr;
}
/* 第十一步:把IP首部复制到rambuf当中 */
SMEMCPY(rambuf->payload, original_iphdr, IP_HLEN);
/* iphdr指向rambuf->payload */
iphdr = (struct ip_hdr *)rambuf->payload;
/* left_to_copy = 分片大小fragsize
1:left_to_copy = 1480
2:left_to_copy = 1480
3:left_to_copy = 1020 */
left_to_copy = fragsize;
while (left_to_copy) {
struct pbuf_custom_ref *pcr;
/* plen = p->len - poff
1:4000 - 20 = 3980 plen = 3980
2:4000 - 1500 = 2500 plen = 2500
3:4000 - 2980 = 1020 plen = 1020 */
u16_t plen = (u16_t)(p->len - poff);
/* 新的pbuf大小 newpbuflen = left_to_copy < plen ? left_to_copy : plen
1:newpbuflen = 1480
2:newpbuflen = 1480
3:newpbuflen = 1020 */
newpbuflen = LWIP_MIN(left_to_copy, plen);
/* 是否为空 */
if (!newpbuflen) {
poff = 0;
p = p->next;
continue;
}
/* 申请一个新的结构pbuf_custom_ref */
pcr = ip_frag_alloc_pbuf_custom_ref();
if (pcr == NULL) {
pbuf_free(rambuf);
goto memerr;
}
/* 镜像pbuf(p),尽管我们可能不需要它的全部。 */
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;
/* rambuf与newpbuf链接 */
pbuf_cat(rambuf, newpbuf);
/* 1:left_to_copy = 1480 - 1480 = 0
2:left_to_copy = 1480 - 1480 = 0
3:left_to_copy = 1020 - 1020 = 0 */
left_to_copy = (u16_t)(left_to_copy - newpbuflen);
if (left_to_copy) {
poff = 0;
p = p->next;
}
}
/* 1:poff = 20 + 1480 = 1500
2:poff = 1500 + 1480 = 2980
3:poff = 2980 + 1020 = 4000 */
poff = (u16_t)(poff + newpbuflen);
/* 1:last = (3980 <= 1500 - 20 ) = 0
2:last = (2500 <= 1500 - 20 ) = 0
3:last = (1020 <= 1500 - 20 ) = 1 */
last = (left <= netif->mtu - IP_HLEN);
/* 设置新的偏移量和MF标志 */
tmp = (IP_OFFMASK & (ofo));
if (!last || mf_set) {
/* 最后一个片段有MF设置 */
tmp = tmp | IP_MF;
}
/* 设置该分片的IP首部信息 */
IPH_OFFSET_SET(iphdr, lwip_htons(tmp));
IPH_LEN_SET(iphdr, lwip_htons((u16_t)(fragsize + IP_HLEN)));
IPH_CHKSUM_SET(iphdr, 0);
/* 发送该分片IP数据包 */
netif->output(netif, rambuf, dest);
IPFRAG_STATS_INC(ip_frag.xmit);
/* 释放内存 */
pbuf_free(rambuf);
/* 1:left = 3980 - 1480 = 2500
2:left = 2500 - 1480 = 1020
3:left = 1020 - 1020 = 0 */
left = (u16_t)(left - fragsize);
/* 1:ofo = 0 + 185 = 185
2:ofo = 185 + 185 = 370
3:ofo = 370 + 185 = 555 */
ofo = (u16_t)(ofo + nfb);
}
MIB2_STATS_INC(mib2.ipfragoks);
return ERR_OK;
memerr:
MIB2_STATS_INC(mib2.ipfragfails);
return ERR_MEM;
}
其中,有关于nbf,也就是IP首部的片偏移,是以8个字节为单位进行统计的;
由于IP分组在网络中的传输到达目的地点的时间是不确定的,所以后面的分组可能比前面的分组先到达目的地。
这是接收一个IP数据包的示例,如果没接收全,会把IP首部以8字节为代价强制转换为如下3个字段,其中next_pbuf会互相进行链接;其中如果三个分片都接收到了,就会释放掉ip_reassdata这个结构体数据;然后IP首部就会重新重装,并且pbuf的payload会指向每一个分片的IP包;pbuf的next就会相互链接起来。
其中,8字节为代价的强制转换的结构体如下所示:
以上结构体,就使得IP包没接收完整时,可以把多个IP分片以链表形式链接。
struct pbuf *
ip4_reass(struct pbuf *p)
{
struct pbuf *r;
struct ip_hdr *fraghdr;
struct ip_reassdata *ipr;
struct ip_reass_helper *iprh;
u16_t offset, len, clen;
u8_t hlen;
int valid;
int is_last;
IPFRAG_STATS_INC(ip_frag.recv);
MIB2_STATS_INC(mib2.ipreasmreqds);
/**************************计算分片参数******************************/
/* 获取该分片的IP首部 */
fraghdr = (struct ip_hdr *)p->payload;
if (IPH_HL_BYTES(fraghdr) != IP_HLEN) {
LWIP_DEBUGF(IP_REASS_DEBUG, ("ip4_reass: IP options currently not supported!\n"));
IPFRAG_STATS_INC(ip_frag.err);
goto nullreturn;
}
/* offset = 该分片的偏移量
1:0
2:185
3:370 */
offset = IPH_OFFSET_BYTES(fraghdr);
/* len = 该分片的总长度
1:1500
2:1500
3:1040 */
len = lwip_ntohs(IPH_LEN(fraghdr));
/* hlen = 首部长度 = 20 */
hlen = IPH_HL_BYTES(fraghdr);
if (hlen > len) {
goto nullreturn;
}
/* len = 数据长度
1:len = 1500 - 20 = 1480
2:len = 1500 - 20 = 1480
3:len = 1040 - 20 = 1020 */
len = (u16_t)(len - hlen);
/* clen = 1 (获取输入分组的 pbuf 数量) */
clen = pbuf_clen(p);
/* 若将输入报文的 pbuf 加上 reassdatagrams 链表所有的 pbuf 的数量超出限制 */
if ((ip_reass_pbufcount + clen) > IP_REASS_MAX_PBUFS) {
#if IP_REASS_FREE_OLDEST
if (!ip_reass_remove_oldest_datagram(fraghdr, clen) ||
((ip_reass_pbufcount + clen) > IP_REASS_MAX_PBUFS))
#endif /* IP_REASS_FREE_OLDEST */
{
/* 省略..... */
goto nullreturn;
}
}
/* 遍历分组重装链表,找到该分组对应的 reassdatagrams */
for (ipr = reassdatagrams; ipr != NULL; ipr = ipr->next) {
/* IP数据报首部是否一致 */
if (IP_ADDRESSES_AND_ID_MATCH(&ipr->iphdr, fraghdr)) {
/* 省略代码 */
break;
}
}
/* 未找到其对应的重装链表,新建一个重装链表 */
if (ipr == NULL) {
ipr = ip_reass_enqueue_new_datagram(fraghdr, clen);
if (ipr == NULL) {
goto nullreturn;
}
} else {/* 找到对应的重装链表,若输入分组的片偏移为 0,则是数据报的第一个分组 */
if (((lwip_ntohs(IPH_OFFSET(fraghdr)) & IP_OFFMASK) == 0) &&
((lwip_ntohs(IPH_OFFSET(&ipr->iphdr)) & IP_OFFMASK) != 0)) {
/* 复制输入分组的 ip 首部到 ipr 的 iphdr 成员,由它的首部作为完整数据报的 ip 首部 */
SMEMCPY(&ipr->iphdr, fraghdr, IP_HLEN);
}
}
/* 是否是最后一个分组.MF = 0 */
is_last = (IPH_OFFSET(fraghdr) & PP_NTOHS(IP_MF)) == 0;
if (is_last) {
/* 计算数据报长度 */
u16_t datagram_len = (u16_t)(offset + len);
/* 超过 65535 字节了, IP 报不能这么长 */
if ((datagram_len < offset) || (datagram_len > (0xFFFF - IP_HLEN))) {
goto nullreturn_ipr;
}
}
/**************************把分片插入重装链表******************************/
/* 将分片插入对应重装链表 */
valid = ip_reass_chain_frag_into_datagram_and_validate(ipr, p, is_last);
/* 注意:如果valid = IP_REASS_VALIDATE_PBUF_DROPPED表示还没有收到所有的碎片! */
if (valid == IP_REASS_VALIDATE_PBUF_DROPPED) {
goto nullreturn_ipr;
}
/**************************重装完成执行的代码******************************/
/* 到此,分片的插入完成,统计一下有多少个PBUF */
ip_reass_pbufcount = (u16_t)(ip_reass_pbufcount + clen);
/* 最后一片 */
if (is_last) {
/* 计算数据报长度 */
u16_t datagram_len = (u16_t)(offset + len);
/* 总长度保存在结构体 */
ipr->datagram_len = datagram_len;
/* 设置数据报标志 */
ipr->flags |= IP_REASS_FLAG_LASTFRAG;
}
/* 若所有数据报已完成重装,构建一个存放完整ip数据报的pbuf链(填充首部,连接 pbuf),
并将ipr从reassdatagrams链表删除 */
if (valid == IP_REASS_VALIDATE_TELEGRAM_FINISHED) {
struct ip_reassdata *ipr_prev;
/* 设置数据报总长度:数据长度+首部长度20字节 */
u16_t datagram_len = (u16_t)(ipr->datagram_len + IP_HLEN);
/* 指向第二个pbuf(后面有用) */
r = ((struct ip_reass_helper *)ipr->p->payload)->next_pbuf;
/* fraghdr现在是第一个pbuf的 payload,指向ip数据报的首部(我们需要填充这个首部) */
fraghdr = (struct ip_hdr *)(ipr->p->payload);
SMEMCPY(fraghdr, &ipr->iphdr, IP_HLEN);
IPH_LEN_SET(fraghdr, lwip_htons(datagram_len));
IPH_OFFSET_SET(fraghdr, 0);
IPH_CHKSUM_SET(fraghdr, 0);
/* 保存第一个头 PBUF */
p = ipr->p;
/* 将同一重装数据链表中分片的pbuf链接在一起 */
while (r != NULL) {
/* iprh->next_pbuf指向下一个分片 */
iprh = (struct ip_reass_helper *)r->payload;
/* 将下一个分片第一个 pbuf 的 payload 后移,指向数据区,隐藏掉 ip 首部
(我们只需要一个 ip 首部) */
pbuf_remove_header(r, IP_HLEN);
/* 将分片的 pbuf 链连接到 p 上 */
pbuf_cat(p, r);
/* 获取下一个分片的 pbuf 链 */
r = iprh->next_pbuf;
}
/* 如果 ipr 是第一个,则前面 ipr_prev 为空,因为前面没有了 */
if (ipr == reassdatagrams) {
ipr_prev = NULL;
} else {/* 如果不是,找到 ipr_prev 的下一个是 ipr ,用于后面的释放 */
for (ipr_prev = reassdatagrams; ipr_prev != NULL; ipr_prev = ipr_prev->next) {
if (ipr_prev->next == ipr) {
break;
}
}
}
/* 释放ipr */
ip_reass_dequeue_datagram(ipr, ipr_prev);
/* 因为已经完成,算一下这个 P 有多少个 PBUF */
clen = pbuf_clen(p);
/* 因为完整了,要传到上层,因此要减去 */
ip_reass_pbufcount = (u16_t)(ip_reass_pbufcount - clen);
MIB2_STATS_INC(mib2.ipreasmoks);
/* Return the pbuf chain */
return p;
}
/* the datagram is not (yet?) reassembled completely */
LWIP_DEBUGF(IP_REASS_DEBUG, ("ip_reass_pbufcount: %d out\n", ip_reass_pbufcount));
return NULL;
nullreturn_ipr:
LWIP_ASSERT("ipr != NULL", ipr != NULL);
if (ipr->p == NULL) {
LWIP_ASSERT("not firstalthough just enqueued", ipr == reassdatagrams);
ip_reass_dequeue_datagram(ipr, NULL);
}
nullreturn:
LWIP_DEBUGF(IP_REASS_DEBUG, ("ip4_reass: nullreturn\n"));
IPFRAG_STATS_INC(ip_frag.drop);
pbuf_free(p);
return NULL;
}
static int
ip_reass_chain_frag_into_datagram_and_validate(struct ip_reassdata *ipr, struct pbuf *new_p, int is_last)
{
struct ip_reass_helper *iprh, *iprh_tmp, *iprh_prev = NULL;
struct pbuf *q;
u16_t offset, len;
u8_t hlen;
struct ip_hdr *fraghdr;
int valid = 1;
/* 获取该分片的IP首部 */
fraghdr = (struct ip_hdr *)new_p->payload;
/* len = 该分片的总长度
1:1500
2:1500
3:1040 */
len = lwip_ntohs(IPH_LEN(fraghdr));
/* 获取该分片的首部长度(20) */
hlen = IPH_HL_BYTES(fraghdr);
/* 首部长度大于分片总长度? */
if (hlen > len) {
return IP_REASS_VALIDATE_PBUF_DROPPED;
}
/* len:数据大小
1:1500 - 20 = 1480
2:1500 - 20 = 1480
3:1040 - 20 = 1020 */
len = (u16_t)(len - hlen);
/* offset = 该分片的偏移量
1:0
2:185
3:370 */
offset = IPH_OFFSET_BYTES(fraghdr);
/* 在new_p->payload指向的地址附加一个ip_reass_helper结构体 */
iprh = (struct ip_reass_helper *)new_p->payload;
iprh->next_pbuf = NULL;
/* offset = 该分片的偏移量
1:0
2:185
3:370 */
iprh->start = offset;
/* offset = 该分片的偏移量
1:0 + 1480 = 1048
2:185 + 1480 = 1665
3:370 + 1020 = 1396 */
iprh->end = (u16_t)(offset + len);
/* 这个数据报无效 */
if (iprh->end < offset) {
return IP_REASS_VALIDATE_PBUF_DROPPED;
}
/* 遍历分组重装链表 */
for (q = ipr->p; q != NULL;) {
/* 指向第一个分片的ip_reass_helper,重要!用于确定分组插入位置 */
iprh_tmp = (struct ip_reass_helper *)q->payload;
/* 确认插入的数据报和重装链表的第一个分片顺序 */
if (iprh->start < iprh_tmp->start) {
/* 如果插入分片的数据起始序号小于重装链表的第一个分片的数据起始序号
插入分片的指针指向这个分片 */
iprh->next_pbuf = q;
if (iprh_prev != NULL) {
/* not the fragment with the lowest offset */
#if IP_REASS_CHECK_OVERLAP
if ((iprh->start < iprh_prev->end) || (iprh->end > iprh_tmp->start)) {
return IP_REASS_VALIDATE_PBUF_DROPPED;
}
#endif
iprh_prev->next_pbuf = new_p;
if (iprh_prev->end != iprh->start) {
valid = 0;
}
} else {
#if IP_REASS_CHECK_OVERLAP
if (iprh->end > iprh_tmp->start) {
return IP_REASS_VALIDATE_PBUF_DROPPED;
}
#endif
/* 插入的分片与重装链表的第一个分片调换位置 */
ipr->p = new_p;
}
break;
} else if (iprh->start == iprh_tmp->start) {/* 接收到重复的分组 */
/* 掉弃该分组 */
return IP_REASS_VALIDATE_PBUF_DROPPED;
#if IP_REASS_CHECK_OVERLAP
} else if (iprh->start < iprh_tmp->end) {/* 分片的内容与 iprh_temp 的内容重叠 */
/* 掉弃该分组 */
return IP_REASS_VALIDATE_PBUF_DROPPED;
#endif /* IP_REASS_CHECK_OVERLAP */
} else {
/* 检查数据报是否连续 */
if (iprh_prev != NULL) {
if (iprh_prev->end != iprh_tmp->start) {
/* 不连续 */
valid = 0;
}
}
}
/* q = NULL */
q = iprh_tmp->next_pbuf;
/* iprh_prev指向第一个分片 */
iprh_prev = iprh_tmp;
}
/* 若 q 为 null,输入分组序号最高 则说明分组应该在链表的最末 */
if (q == NULL) {
if (iprh_prev != NULL) {
/* 第一个分片的next_pbuf指针指向插入的分片 */
iprh_prev->next_pbuf = new_p;
/* 判断是否连续 */
if (iprh_prev->end != iprh->start) {
valid = 0;
}
} else {
/* 到这里只能是链表为 null */
ipr->p = new_p;
}
}
/* 最后一个分片处理的代码段 */
if (is_last || ((ipr->flags & IP_REASS_FLAG_LASTFRAG) != 0)) {
/* 输入分片与它前面的分片的数据连续无断点 */
if (valid) {
/* 检查数据报首部是否为 null 或者偏移是不是 0 */
if ((ipr->p == NULL) || (((struct ip_reass_helper *)ipr->p->payload)->start != 0)) {
/* 以上情况说不对 */
valid = 0;
} else {
/* 检查输入分组之后的分组是否也是连续的 */
iprh_prev = iprh;
/* 从输入分组之后开始遍历检查 */
q = iprh->next_pbuf;
while (q != NULL) {
iprh = (struct ip_reass_helper *)q->payload;
if (iprh_prev->end != iprh->start) {
/* 出现不连续的数据 */
valid = 0;
/* 跳出循环。不用再检查了 */
break;
}
iprh_prev = iprh;
q = iprh->next_pbuf;
}
if (valid) {
LWIP_ASSERT("sanity check", ipr->p != NULL);
LWIP_ASSERT("sanity check",
((struct ip_reass_helper *)ipr->p->payload) != iprh);
LWIP_ASSERT("validate_datagram:next_pbuf!=NULL",
iprh->next_pbuf == NULL);
}
}
}
/* 返回 1 说明数据报重装完成 */
return valid ? IP_REASS_VALIDATE_TELEGRAM_FINISHED : IP_REASS_VALIDATE_PBUF_QUEUED;
}
/* 如果我们来到这里,还没有收到所有的碎片! */
return IP_REASS_VALIDATE_PBUF_QUEUED;
}
这一章内容比较多,总体就对着源码,能够看懂这几个示意图就可以了,源码比较多,大概看一看就好,理清整个传输逻辑比较重要!
这里所有的IP包数据都代表着TCP首部+实际传输数据。
以上就是当前已经学习过的lwIP相关的协议框架,包括了网卡驱动、IP协议以及ARP协议,以太网DMA以及MAC内核的内容。