一 导读
在上一章 ip数据输入中,输入的ip分组会被判断是否是一个被分片的分组。若是一个分片的ip数据,则需要将分片暂存起来,等接收完所有的分片再将分片重装成一个完整的ip数据传递给传输层。
这章就介绍lwip如何实现分片的重装。
二,重装数据结构
由于ip分组在网络传输过程中到达目的地点的时间是不确定的,所以后面的分组可能比前面的分组先达到目的地点。
如图所示,分片A,B,C代表一个完整的ip数据报,他们的目的地址都是192.168.1.1,片偏移的单位是字(32bit),总长度的单位是字节,最后MF标志位表示其后是否有数据。
A分片的片偏移为0,说明它是ip数据的第一个分片,数据长度为1420-20=1400.(20字节ip首部)。B分片片偏移为 1400/8=175;C分片MF为0,说明它是最后一个分片。注意ABC 三分片到达目的站的时间是不确定的。
图中只为方便表示,具体ip首部不是图中所示的位置。
为此,我们需要将接收到的分组先暂存起来,等所有的分组都接收完成,再将数据传递给上层。在lwip中,有专门的结构体负责缓存这些分组。
结合示意图理解结构体成员的意思。
//重装数据结构体
struct ip_reassdata {
struct ip_reassdata *next;
struct pbuf *p; //ip数据报pbuf链
struct ip_hdr iphdr; //ip数据报首部(即第一个分组的ip首部)
u16_t datagram_len; //完整ip数据报大小
u8_t flags; //标志是否最后一个分组
u8_t timer; //超时间隔
};
在lwip中会维持一条ip_reassdata链,每一个ip_reassdata结构体代表一个正在重装的ip数据报。当接收到分组时,会将分组的pbuf连接到对应ip_reassdata里的pbuf链中。当该ip数据报接收完整后才递交上层。
在上图中,ip分片首部的8个字节的数据被修改成一个结构体ip_reass_helper,该结构体是用于连接pbuf与判断数据是否完整。定义如下(后面详细分析):
PACK_STRUCT_BEGIN
struct ip_reass_helper {
PACK_STRUCT_FIELD(struct pbuf *next_pbuf);
PACK_STRUCT_FIELD(u16_t start); //该分组数据起始序号
PACK_STRUCT_FIELD(u16_t end); //数据结束序号
} PACK_STRUCT_STRUCT;
PACK_STRUCT_END
三,代码分析
1, 分片处理
该函数先检查分片大小是否超出限制,从重装数据报链中找到对应的数据报,将分片插入数据报,根据插入结果决定是否重新组装pbuf成一个完整的ip数据报,递交上层。在理解分片重新组装pbuf时,多结合上面的示意图理解。
//将输入的分组插入对应reassdata的pbuf链,若重装完成则返回完整数据的pbuf
struct pbuf *
ip4_reass(struct pbuf *p)
{
struct pbuf *r;
struct ip_hdr *fraghdr; //输入分片的首部
struct ip_reassdata *ipr; //分片对应的重装数据报
struct ip_reass_helper *iprh; //pbuf中被强制转换的8个字节,用于指向下一个pbuf与分组起始和结束
u16_t offset, len, clen; //offset:片偏移;len:分片长度;clen:分片的pbuf数量
int valid; //分片插入链表的结果
int is_last; //最后一个分片
fraghdr = (struct ip_hdr*)p->payload; //获取分组首部
//ip首部不正常
if ((IPH_HL(fraghdr) * 4) != IP_HLEN) {
goto nullreturn;
}
//得到分组的片偏移量(相对于0的偏移量)
offset = (lwip_ntohs(IPH_OFFSET(fraghdr)) & IP_OFFMASK) * 8;
len = lwip_ntohs(IPH_LEN(fraghdr)) - IPH_HL(fraghdr) * 4; //获取分组的数据长度
clen = pbuf_clen(p); //获取输入分组的pbuf数量
//若将输入报文的pbuf加上reassdatagrams链表所有的pbuf的数量超出限制
if ((ip_reass_pbufcount + clen) > IP_REASS_MAX_PBUFS) {
#if IP_REASS_FREE_OLDEST
//则释放掉最老的reassdatagrams,并再次检查长度是否超出
if (!ip_reass_remove_oldest_datagram(fraghdr, clen) ||
((ip_reass_pbufcount + clen) > IP_REASS_MAX_PBUFS))
#endif /* IP_REASS_FREE_OLDEST */
{
//长度还是超出,返回错误
/* @todo: send ICMP time exceeded here? */
/* drop this pbuf */
goto nullreturn;
}
}
//遍历分组重装链表,找到该分组对应的reassdatagrams
for (ipr = reassdatagrams; ipr != NULL; ipr = ipr->next) {
//找到对应的reassdatagrams,退出循环 此时ipr不为空
if (IP_ADDRESSES_AND_ID_MATCH(&ipr->iphdr, fraghdr)) {
break;
}
}
if (ipr == NULL) {
//未找到其对应的ip数据报,新建一个数据报
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);
}
}
//到此,我们为输入分组找到了对应的数据报
is_last = (IPH_OFFSET(fraghdr) & PP_NTOHS(IP_MF)) == 0; //是否是最后一个分组.MF=0
if (is_last) {
u16_t datagram_len = (u16_t)(offset + len); //计算数据报长度
//u16 类型内存溢出
if ((datagram_len < offset) || (datagram_len > (0xFFFF - IP_HLEN))) {
goto nullreturn;
}
}
/* @todo: trim pbufs if fragments are overlapping */
//将分片插入链表并检查重装是否完成
valid = ip_reass_chain_frag_into_datagram_and_validate(ipr, p, is_last);
//插入失败
if (valid == IP_REASS_VALIDATE_PBUF_DROPPED) {
goto nullreturn;
}
//到此,分片的插入完成
//ip_reass_pbufcount增加clen个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;
ipr->datagram_len += IP_HLEN; //设置数据报总长度 数据长度+首部长度20字节
r = ((struct ip_reass_helper*)ipr->p->payload)->next_pbuf; //保存第二个pbuf(后面有用)
fraghdr = (struct ip_hdr*)(ipr->p->payload); //fraghdr现在是第一个pbuf的payload,指向ip数据报的首部(我们需要填充这个首部)
SMEMCPY(fraghdr, &ipr->iphdr, IP_HLEN); //结合示意图。用之前保存在iphadr中的首部数据填充pbuf首部
IPH_LEN_SET(fraghdr, lwip_htons(ipr->datagram_len)); //设置数据报的长度
IPH_OFFSET_SET(fraghdr, 0); //设置片偏移为0
IPH_CHKSUM_SET(fraghdr, 0);
/* @todo: do we need to set/calculate the correct checksum? */
#if CHECKSUM_GEN_IP
IF__NETIF_CHECKSUM_ENABLED(ip_current_input_netif(), NETIF_CHECKSUM_GEN_IP) {
IPH_CHKSUM_SET(fraghdr, inet_chksum(fraghdr, IP_HLEN)); //设置校验和
}
#endif /* CHECKSUM_GEN_IP */
p = ipr->p; //p是数据报第一个pbuf
//将同一数据报中分片的pbuf链连接连接在一起
while (r != NULL) {
iprh = (struct ip_reass_helper*)r->payload; //iprh->next_pbuf指向下一个分片
pbuf_header(r, -IP_HLEN); //将下一个分片第一个pbuf的payload后移,指向数据区,隐藏掉ip首部(我们只需要一个ip首部)
pbuf_cat(p, r); //将分片的pbuf链 连接到p上
r = iprh->next_pbuf; //获取下一个分片的pbuf链
}
//确定ipr_prev,并删除ipr
if (ipr == reassdatagrams) {
ipr_prev = NULL;
} else {
for (ipr_prev = reassdatagrams; ipr_prev != NULL; ipr_prev = ipr_prev->next) {
if (ipr_prev->next == ipr) {
break;
}
}
}
ip_reass_dequeue_datagram(ipr, ipr_prev); //从链表中删除ipr
ip_reass_pbufcount -= pbuf_clen(p); //重装pbuf数量减少
MIB2_STATS_INC(mib2.ipreasmoks);
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:
LWIP_DEBUGF(IP_REASS_DEBUG,("ip4_reass: nullreturn\n"));
IPFRAG_STATS_INC(ip_frag.drop);
pbuf_free(p);
return NULL;
}
2,分组插入
上面提到的将分组插入数据报,其实现也是一个重要的函数。该函数将分片插入链表,并检查链表中的数据是否完整,若数据完整则返回1,在ip4_reass()中就会将完整的数据交给上层。
其中valid
值标志数据是否完整。valid初始为1,在分组插入链表的过程中,通过检查分组的数据之间是否连续来修改valid,若有一处的分组不连续,那么数据报肯定不完整,valid为0,只有遍历完链表后valid仍为1且最后一个分组收到时,才说明数据报完整。
在这个函数中,要注意ip_reass_helper
这个结构体的作用。该结构体的start和end是判断数据是否连续的关键。
//将分片插入分组链
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; //offset:分片片偏移;len:分片数据长度
struct ip_hdr *fraghdr;
int valid = 1; //数据连续标志。初始化valid为1,当出现前后分组数据不连续时,valid为0,说明数据报不完整
fraghdr = (struct ip_hdr*)new_p->payload; //获取当前分片的首部
len = lwip_ntohs(IPH_LEN(fraghdr)) - IPH_HL(fraghdr) * 4; //计算len:分片的数据长度
offset = (lwip_ntohs(IPH_OFFSET(fraghdr)) & IP_OFFMASK) * 8; //计算分片的片偏移
iprh = (struct ip_reass_helper*)new_p->payload; //将分片首部的8个字节的数据转换成ip_reass_helper
iprh->next_pbuf = NULL;
iprh->start = offset; //填充iprh的开始序号
iprh->end = offset + len; //结束序号
//遍历分组链表中的分组的第一个pbuf的ip_reass_helper结构体(序号从小到大)
//并在合适的位置插入new_pbuf
for (q = ipr->p; q != NULL;) {
//分组的ip_reass_helper,重要!用于确定分组插入位置
iprh_tmp = (struct ip_reass_helper*)q->payload;
if (iprh->start < iprh_tmp->start) {
//输入分组start小于当前分组,可插入
iprh->next_pbuf = q;
if (iprh_prev != NULL) {
//前面的pbuf与当前的pbuf数据内容出现覆盖
if ((iprh->start < iprh_prev->end) || (iprh->end > iprh_tmp->start)) {
goto freepbuf;
}
iprh_prev->next_pbuf = new_p;
//前一个pbuf的结尾不等当前pbuf的结尾序号,说明两个pbuf数据不是连续的
if (iprh_prev->end != iprh->start) {
valid = 0;
}
} else {
//当前分片偏移最小
//若当前分片出现覆盖,退出
if (iprh->end > iprh_tmp->start) {
goto freepbuf;
}
ipr->p = new_p; //插入链表的最前
}
break;
} else if (iprh->start == iprh_tmp->start) {
//接收到重复的分组
goto freepbuf;
} else if (iprh->start < iprh_tmp->end) {
//分片的内容与iprh_temp的内容重叠,退出
goto freepbuf;
} else {
//iprh->start > iprh_tmp->end
//检查数据报是否连续
if (iprh_prev != NULL) {
if (iprh_prev->end != iprh_tmp->start) {
valid = 0; //不连续
}
}
}
q = iprh_tmp->next_pbuf; //检查下一个分片
iprh_prev = iprh_tmp;
}
if (q == NULL) {
//若q为null,输入分组序号最高 则说明分组应该在链表的最末
if (iprh_prev != NULL) {
iprh_prev->next_pbuf = new_p; //插入链尾
//判断是否连续
if (iprh_prev->end != iprh->start) {
valid = 0;
}
} else {
ipr->p = new_p; //到这里只能是链表为null
}
}
//若最后一个分组已经收到
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;
}
/* If we come here, not all fragments were received, yet! */
return IP_REASS_VALIDATE_PBUF_QUEUED; /* not yet valid! */
#if IP_REASS_CHECK_OVERLAP
freepbuf:
ip_reass_pbufcount -= pbuf_clen(new_p);
pbuf_free(new_p);
return IP_REASS_VALIDATE_PBUF_DROPPED;
#endif /* IP_REASS_CHECK_OVERLAP */
}