LwIP从入门到放弃之(六)---网际协议IP

欲穷千里目,更上一层楼!感谢各位读者的喜爱,你们的支持就是我不断更新的动力。

IP时整个TCP/IP协议的核心,传输层协议,如UDP和TCP都需要IP来提供服务。常见的广域路由器就工作在IP层,他们负责将IP数据包从源主机送至目的主机,主机间通过IP地址来实现的。

IP协议的基础知识我这里就不做过多讲解,直接从IP数据包开始。

1. 数据包

1.1 数据包组成结构

在以太网中,IP数据包和ARP报文都是封装在以太网数据帧中发送的,IP数据包通常由两部分组成,即IP首部和数据。见下图:
在这里插入图片描I述
第一个字段是4bit的版本号(VER),包含了创建数据报所使用的IP协议版本信息,例如对

于IPv4,该值为4,对于IPv6,该值为6。目前,普遍使用的版本为IPv4。

接下来的4bit字段用于记录首部长度,这个长度以字为单位。所以对于不含任何选项字段的IP首部,则该长度值为5(5x4=20字节);由于该字段最大值为15,所以其能描述的最大IP首部长度为15x4=60字节。

再下来是一个8bit的服务类型字段(Type
Of Service,TOS),该字段主要用于描述当前IP数

据报急需的服务类型,如最小延时、最大吞吐量、最高可靠性、最小费用等。路由器在转发数据报时,可以根据这个字段的值来为数据报选择最合理的路由路径。

16位的总长度字段描述了整个IP数据报(IP首部和数据区)的总字节数。理论上说,IP数据报总长度最大可达65535字节,但在实际应用中,底层链路可不允许这么大的数据包出现在链路上。

16位标识字段用于标识IP层发送出去的每一份IP数据报,每发送一份报文,则该值加1,数据包被分片时,该字段会被复制到每一个分片中。在接收端,会使用这个字段值来组装所有分片为一个完整的数据报。

3位标志字段的定义如下:第一位保留为今后使用;第二位是不分片位,当该位被置位时,IP数据报在发送或转发过程中,不能进行分片,在这种情况下,如果这个数据报由于太大而不能被放在任何物理网络中进行发送,那么这个数据报将会被丢弃,当该位为0时,IP层将在需要的时候对数据报进行分片处理;第三位表示更多分片位,当该位被置1时,说明该分片不是某个数据报的最后一个分片,当该位为0时,表示该分片是某个数据报的最后一个分片,或者是某个数据报的唯一分片。

13位片偏移字段表示当前分片所携带的数据在整个数据报中的相对位置(以8字节为单位)。目的站必须收到从0偏移量到最高偏移量的所有分片,才能将分片重装为一个完整的数据报。每个分片在网络中单独传输,它们到达终点的顺序可能会各不相同,但在目的端,将按照分片的偏移量来顺序的组织各个分片。

生存时间(TTL)字段描述该IP数据报最多能被转发的次数,每经过一次转发,该值会减1,当该值为0时,路由器会丢弃掉分组,同时一个ICMP差错报文会被返回至源主机。为每个数据报设置一个生存时间很有意义,它保证了数据报不会在网络中无休止的飘荡,可被看成是网络自动防范故障的一种机制。

8位协议字段和以太网数据帧中的协议类型字段功能相似,不过这里它用来描述该IP数据报中的数据是来自于哪个上层协议,例如,该值为1表示ICMP协议,为2表示IGMP协议,为6表示TCP协议,为17表UDP协议。事实上,该字段的值也间接的指出了IP数据报的数据区域中数据的格式,因为每一种上层协议都使用了一种独立的数据格式。

16位首部校验和只针对IP首部做校验,它并不关心其内部数据在传输过程中出错与否。

1.2 数据结构

为了方便对IP数据报首部字段进行读取或写入操作,在LwIP中定义了一个名为ip_hdr的结构体来描述数据报首部,如下代码所示:

struct ip_hdr {

PACK_STRUCT FIELD(u16_t _v_hl_tos);//前三个字段:版本号+首部长度+服务类型
PACK_STRUCT_FIELD(u16_t _len);//总长度
PACK_STRUCT FIELD(ul6_t _id);//标识字段
PACK_STRUCT_FIELD(u16_t _offset);//3位标志位和13位片偏移字段
#define IP_RF 0x8000//标志位第一位(保留位)掩码
#define IP_DF 0x4000//标志位第二位(不分片标志)掩码
#define IP_MF 0x2000//标志位第三位(更多分片位)掩码
#define IP_OFFMASK 0x1fff//13位片偏移字段的掩码
PACK_STRUCT_FIELD(ul6_t _ttl_proto);//TTL字段+协议字段
PACK_STRUCT_FIELD(ul6_t _chksum);//首部校验和字段
PACK_STRUCT_FIELD(struct ip_addr src);//源IP地址
PACK_STRUCT_FIELD(struct ip addr_dest);//目的IP地址
} PACK_STRUCT STRUCT;

2. IP层输出

当传输层协议(TCP或UDP)

要发送数据时,它们会将数据按照自己的格式组装在一个pbuf中,并将payload指针指向协议首部,然后调用IP层的数据报发送函数ip_output发送数据。函数ip_output的调用者需要为它提供数据报首部中的目的IP地址、源IP地址、协议类型、TTL等重要信息,它主要的工作是根据目的IP地址为该数据报选择一个合适的网络接口,然后调用函数ip_output_if将数据报发送出去。ip_output_if函数源代码如下所示:

//函数功能:填写IP首部中的各个字段值(不处理选项字段),并发送数据报

//参数说明:netif为发送数据报的网络接口结构

err_t ip_output_if(struct pbuf *p,  struct ip_addr *src, struct ip_addr *dest,u8 _t ttl, u8_t tos, u8_t proto, struct netif *netif)
{
struct ip_hdr *iphdr;
static ul6_t ip_id =0;//静态变量,记录IP数据报的编号(标识字段)
if(dest!=IP_HDRINCL){//dest不为IP_HDRINCL,说明pbuf中未填写IP首部
ul6_t ip_hlen = IP_HLEN;//宏IPHLEN为默认的IP首部长度,20
if (pbuf header(p, IP_HLEN)){//移动payload指针,指向pbuf中的IP首部
return ERR_BUF;//失败,返回pbuf空间错误
}

iphdr=p->payload;//iphdr指向数据报首部
IPH_TTL_SET(iphdr, ttl);//填写TTL字段
IPH_PROTO_SET(iphdr, proto);//填写协议字段
ip_addr_set(&(iphdr->dest), dest);//填写目的IP地址
IPH_VHLTOS_SET(iphdr,4,ip_hlen/4,tos);//填写版本号+首部长度+服务类型
IPH_LEN_SET(iphdr, htons(p->tot_len);//填写数据报总长度
IPH_OFFSET_SET(iphdr,0);//填写标志位和片偏移字段,都为0
IPH_ID_SET(iphdr, htons(p_id));//填写标识字段
++ip_id;//数据报编号值加.1

if(ip_addr_isany(src)){//若src为空,则将源IP地址填写为网络接口的IP地址
ip_addr_set(&(iphdr->src),&(netif->ip_addr));
}else{           //若src不为空,则直接填写源IP地址
ip_addr_set(&(iphdr->src), src);
}
IPH_CHKSUM_SET(iphdr,0);//清0校验和字段
IPH_CHKSUM_SET(iphdr, inet_chksum(iphdr,iphlen);//计算并填写首部校验和
}else{             //如果pbuf中已经填写IP首部,这里不需要填写任何字段
iphdr= p->payload;
dest= &(iphdr>dest);//只是用变量dest记录下数据报中的目的IP地址
//下面开始发送IP数据报,分为三种情况来处理
if (ip_addr_cmp(dest, &nctif->ip_addr)){//如果目的IP地址是本网卡的地址,
return netif_loop_output(netif, p;dest);//则调用环回输入
}
if(netif->mtu&& (p->tot_len>netif->mt))){//如果数据报太大(大于接口的mtu)
return ip_fag(p,netif,dest);//则调用函数ip_frag对数据报分片发送
}
//剩下的情况,直接调用接口注册的output函数发送数据报
return netif->output(netif, p, dest);
}

函数ip_output_ifip_output二者间的关系,它们中的前者都是完成数据包的组装和发送工作,而后者的功能是通过调用前者来实现的。函数ip_output_if在组装好IP数据报后,需要根据目的IP地址的情况来选择不同的发送方式:首先判断目的IP地址是否为本地的地址,若是,则说明可能是个环回接口的数据,我们直接调用环回接口数据报发送函数netif_loop_output发送数据报;其次,需要考虑数据报分片的问题,当网络接口的mtu值不为0,且数据报总长度值大于mtu时,需要对数据报进行分片发送,这是通过调用函数ip_frag来实现的,这个函数后续讲解,它会对数据报进行分片,并调用netif->output来完成发送工作,数据的分片工作必须由IP层来完成最后,若前两种情况都不满足,则调用网卡结构注册的output函数来完成数据报的发送工作,这里我们的这个函数应该为etharp_output,它会解析MAC地址,组装以太网帧并发送。

3. IP层输入

在上一章中,以太网数据帧经过ethernet_input函数,根据不同的数据类型分发给IP层或者ARP层模块处理,分发给IP模块是通过调用ip_input函数完成的。当然在递交前,ethernet_input已经将数据包pbuf的指针payload指向了IP数据包的首部。

所有传递给协议栈上层的数据报都需要经过ip_input,来分析下它应该实现的功能首先,相关字段的检验是必须的,例如长度、校验和、版本号等,对于那些不合法的数据报可以直接丢弃;接下来可以判断数据报是否是发给本地的,这可以通过和本地IP地址进行比较(若有多个网络接口,则需要比较所有的IP地址),对于不是给本地的数据报,可以使用其他网络接口将该数据报转发出去(当在有多个网络接口的情况下);如果数据报是以分片的形式出现,且是发送给本地的,则此时ip_input有义务将分片组装成为完整的数据报,而这应该是整个数据报处理过程中最复杂的部分了;最后,若一个数据报到来或者分片经过重组后,得到了一个完整的数据报,此时,需要将该数据报递交到上层协议,例如UDP、TCP等。

关于ip_input的源代码较长,这里就不赘述了,读者感兴趣可以去我的主页下载。

注:LwIP协议栈源代码我已上传,需要的小伙伴欢迎下载:

https://download.csdn.net/download/rgxiwei/15724471

本系列内容参考朱升林的《嵌入式网络那些事:LWIP协议深度剖析于实战演练》

  • 0
    点赞
  • 1
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

房东的猫爱喵喵喵

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值