Skip to content

IP协议

IP协议是TCP/IP协议族中最为核心的协议,TCP、UDP、ICMP、IGMP数据都以IP数据报格式传输(IPv4、IPv6)

提供了一个无状态, 无连接, 不可靠的协议

无状态: 所有IP数据报的发送、传输和接收都是相互独立、没有上下文关系的。这种服务最大的缺点就是无法处理乱序和重复的IP数据报。

无连接: IP协议不会记录对方的信息, 所以上层发送信息的时候都需要提供对方的IP地址

不可控: 不能保证这一个数据会发送给对方, 发送失败的时候会通知上层协议, 不会试图重发

image-20240705213126673

image-20240705220945235

IP地址在整个发送的过程中是不会变化的, 是用于确认路标的

img

IP数据报格式

  • IP数据报的首部长度和数据长度都是可变长的,但总是4字节的整数倍。
  • 对于IPv4,4位版本字段是4。
  • 4位首部长度的数值是以4字节为单位的,最小值为5,也就是说首部长度最小是4x5=20字节,也就是不带任何选项的IP首部,4位能表示的最大值是15,也就是说首部长度最大是60字节。
  • 8位TOS字段有3个位用来指定IP数据报的优先级(目前已经废弃不用),还有4个位表示可选的服务类型(最小延迟、最大?吐量、最大可靠性、最小成本),还有一个位总是0。
  • 总长度是整个数据报(包括IP首部和IP层payload)的字节数。
  • 每传一个IP数据报,16位的标识加1,可用于分片和重新组装数据报。
  • 3位标志

image-20240705225852731

  • 13位分片偏移是分片相对原始IP数据报开始处(仅指数据部分)的偏移。实际的偏移值是该值左移3位(乘8)后得到的。由于这个原因,除了最后一个IP分片外,每个IP分片的数据部分的长度必须是8的整数倍(这样才能保证后面的IP分片拥有一个合适的偏移量)。
  • TTL(Time to live)是这样用的:源主机为数据包设定一个生存时间,比如64,每过一个路由器就把该值减1,如果减到0就表示路由已经太长了仍然找不到目的主机的网络,就丢弃该包,因此这个生存时间的单位不是秒,而是跳(hop)。
  • 协议字段指示上层协议是TCP、UDP、ICMP还是IGMP。
  • 然后是校验和,只校验IP首部,数据的校验由更高层协议负责。IPv4的IP地址长度为32位。

默认的时候最大的传输单元是1500字节, 如果比这一个数据大的话, 需要进行分片, 这一个长度是加上IP首部以后的, 实际可以发送的数字最大长度为1480

实际使用的时候需要进行分片的是UDP协议, 使用TCP协议的时候会在传输层进行分片

LWIP

分片

  1. 申请一个pbuf, 需要预留一个54字节的头部(20TCP头部(传输层) + 20IP头部(网络层) + 14网络层头部)
  2. 使用函数ip4_frag进行分层, 这一个函数会在ip4_output_if_src函数里面进行调用

ip4_output_if_src函数首先会添加IP层的首部, 使用struct ip_hdr这一个结构体进行初始化头部信息, 初始化以后根据发送数据的长度使用ip4_frag分层以后发送, 或直接使用etharp_output进行发送

c
struct pbuf_custom {
  /** The actual pbuf */
  struct pbuf pbuf;
  /** This function is called when pbuf_free deallocates this pbuf(_custom) */
  pbuf_free_custom_fn custom_free_function;
};

struct pbuf_custom_ref {
  /** 'base class' */
  struct pbuf_custom pc;
  /** pointer to the original pbuf that is referenced */
  struct pbuf *original;
};
  • 先申请一个保存分片IP首部的pbuf:rambuf
  • 然后再申请一个pbuf_custom_ref数据结构的伪pbuf:pcr
  • 然后把未分片的IP报文的pbuf对应分片的数据区地址给到pcr->pc->pbuf->payload,共享数据区内存嘛。
  • 然后把分片IP首部的pbuf和分片IP的数据pbuf拼接起来:pbuf_cat(rambuf, pcr->pc->pbuf->payload);,这样就组成了分片的IP报文了。

image-20240705225852731

重装

由于到达的顺序是不确定的, 所以后发送的分组可能前到达

  1. 接收到的分片暂存起来
  2. 对分片进行排序
  3. 所有分片接收完成后,再将数据递交给传输层处理
c
/** IP reassembly helper struct.
 * This is exported because memp needs to know the size.
 */
struct ip_reassdata {
  struct ip_reassdata *next;	//指向下一个需要组合的IP数据包
  struct pbuf *p;			   //指向pbuf分片
  struct ip_hdr iphdr;		   //IP数据报首部
  u16_t datagram_len;		   //获取的数据的长度
  u8_t flags;				  //是否获取最后一个数据包
  u8_t timer;				  //超时时间
};

这一个结构体表示一个正在重装的IP数据报, 收到分组的时候, 会把pbuf连接起来, 到ip_reassdata里面的pbuf表, 所有的pbuf都获取以后才会上传

image-20240707212137747

c
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;

使用这一个结构体进行强转, 所有的数据获取以后把第一个pbuf的偏移指向(重新填充)IP首部, 其余的指向数据

使用函数ip4_reass进行重装

  • 首先获取IP首部, 从这里获取数据长度以及数据的数据的偏移量
  • 从重装链表里面获取对应的reassdatagrams(通过判断ip首部是不是一样的), 没有找到的话获取一个新的
  • 如果这一个pbuf是第一个的话(偏移是0), 会使用iphdr记录他的ip首部
  • 如果获取的是最后一个分组, 会使用他的偏移以及他的长度计算总长度
  • 使用ip_reass_chain_frag_into_datagram_and_validate函数把pbuf插入对应的链表位置, 以及把ip_reass_helper初始化, 同时会在这一个函数里面判断这一个pbuf的数据的位置是不是有问题的, 是的话进行丢弃, 在获取到最后一个分组以后, 会在获取分片的时候判断所有的分片是不是已经连续了, 是的话返回IP_REASS_VALIDATE_TELEGRAM_FINISHED
  • 在获取全部接收以后会把第一个pbuf的ip首部复原, 之后把全部的pbuf的偏移指向数据, 然后把所有的pbuf关联起来
  • 之后释放ipr, 但是不释放pbuf

这一个过程是在ip4_input函数里面调用的, 获取到这一个完整的数据以后, 记录一下IP首部的信息, 然后把第一个的IP首部数据偏移减去, 根据首部的信息进行不同的处理

image-20240707230608202

image-20240707230652445

image-20240707230730602