加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

ip首部数据结构和ip数据报

(2012-02-10 11:12:01)
标签:

杂谈

分类: linux内核之网络

IP数据报直观图

http://s10/middle/5ceeb9eagb893853a3f49&690



IP数据报首部

http://s5/middle/5ceeb9eagb89385ac1634&690

IP数据报首部内核定义

Linux 2.6.32.27里的include/Linux/ip.h里定义的

struct iphdr {

#if defined(__LITTLE_ENDIAN_BITFIELD)

         __u8         ihl:4,

                   version:4;

#elif defined (__BIG_ENDIAN_BITFIELD)

         __u8         version:4,

                ihl:4;

#else

#error       "Please fix <asm/byteorder.h>"

#endive

         __u8         tos;

         __be16     tot_len;

         __be16     id;

         __be16     frag_off;

         __u8         ttl;

         __u8         protocol;

         __sum16 check;

         __be32     saddr;

         __be32     daddr;

        

};

 

IP数据报首部解释

 

(1)版本(Iphdr->version) 占4位,指IP协议的版本。通信双方使用的IP协议版本必须一致。目前广泛使用的IP协议版本号为4(即IPv4)。关于IPv6,目前还处于草案阶段。

(2)首部长度(Iphdr->ihl)  占4位,可表示的最大十进制数值是15。请注意,这个字段所表示数的单位是32位字长(1个32位字长是4字节),因此,当IP的首部长度为1111时(即十进制的15),首部长度就达到60字节。当IP分组的首部长度不是4字节的整数倍时,必须利用最后的填充字段加以填充。因此数据部分永远在4字节的整数倍开始,这样在实现IP协议时较为方便。首部长度限制为60字节的缺点是有时可能不够用。但这样做是希望用户尽量减少开销。最常用的首部长度就是20字节(即首部长度为0101),这时不使用任何选项。

(3)区分服务(Iphdr->tos)  占8位,用来获得更好的服务。这个字段在旧标准中叫做服务类型( 服务类型字段8位: 服务类型(TOS)字段包括一个3 bit的优先权子字段(现在已被忽略),4 bit的TOS子字段和1 bit未用位但必须置0。4 bit的TOS子字段分别代表:最小时延、最大吞吐量、最高可靠性和最小费用。4 bit中只能设置其中1 bit。如果所有4 bit均为0,那么就意味着是一般服务),但实际上一直没有被使用过。1998年IETF把这个字段改名为区分服务DS(Differentiated Services)。只有在使用区分服务时,这个字段才起作用。

(4)总长度(Iphdr->tot_len) 总长度指首部和数据之和的长度,单位为字节。总长度字段为16位,因此数据报的最大长度为65535字节。在IP层下面的每一种数据链路层都有自己的帧格式,其中包括帧格式中的数据字段的最大长度,这称为最大传送单元MTU(Maximum Transfer Unit)。当一个数据报封装成链路层的帧时,此数据报的总长度(即首部加上数据部分)一定不能超过下面的数据链路层的MTU值。

利用首部长度字段和总长度字段,就可以知道 IP数据报中数据内容的起始位置和长度。由于该字段长16比特,所以IP数据报最长可达65535字.总长度字段是IP首部中必要的内容,因为一些数据链路(如以太网)需要填充一些数据以达到最小长度。尽管以太网的最小帧长为46字节,但是IP数据可能会更短。如果没有总长度字段,那么IP层就不知道46字节中有多少是IP数据报的内容。

(5)标识(Iphdr->id) 占16位。IP软件在存储器中维持一个计数器,每产生一个数据报,计数器就加1,并将此值赋给标识字段。但这个“标识”并不是序号,因为IP是无连接服务,数据报不存在按序接收的问题。当数据报由于长度超过网络的MTU而必须分片时,这个标识字段的值就被复制到所有的数据报的标识字段中。相同的标识字段的值使分片后的各数据报片最后能正确地重装成为原来的数据报。

(6)片偏移(Iphdr->frag_off) 占16位。前3位是标识位,后13位是偏移位。

前三位:

第1位是保留的,必须为0,;

第2位是“更多分片”(MF -- More Fragment)标志。除了最后一片外,其他每个组成数据报的片都要把该比特置1。

第3位是“不分片”(DF -- Don't Fragment)标志,如果将这一比特置1,IP将不对数据报进行分片,这时如果有需要进行分片的数据报到来,会丢弃此数据报并发送一个ICMP差错报文给起始端。

后13位:片偏移位

片偏移指出:较长的分组在分片后,某片在原分组中的相对位置。也就是说,相对用户数据字段的起点,该片从何处开始。片偏移以8个字节为偏移单位。这就是说,每个分片的长度一定是8字节(64位)的整数倍

 (7)生存时间(Iphdr->ttl) 占8位,生存时间字段常用的的英文缩写是TTL(Time To Live),表明是数据报在网络中的寿命。由发出数据报的源点设置这个字段。其目的是防止无法交付的数据报无限制地在因特网中兜圈子,因而白白消耗网络资源。最初的设计是以秒作为TTL的单位。每经过一个路由器时,就把TTL减去数据报在路由器消耗掉的一段时间。若数据报在路由器消耗的时间小于1秒,就把TTL值减1。当TTL值为0时,就丢弃这个数据报。

       TTL(Time to live)域是一个用于限制分组生存期的计数器。这里的计数时间单位为秒,因此最大的生存期为255秒。在每一跳上该计数器必须被递减,而且,当数据报在一台路由器上排队时间较长时,该计数器必须被多倍递减。在实践中,它只是跳计数器,当它递减到0的时候,分组被丢弃,路由器给源主机发送一个警告分组。此项特性可以避免数据报长时间地逗留在网络中,有时候当路由表被破坏之后,这种事情是有可能发生的。

(8)协议(Iphdr-> protocol) 占8位,协议字段指出此数据报携带的数据是使用何种协议,以便使目的主机的IP层知道应将数据部分上交给哪个处理过程。

       当网络层组装完成一个完整的数据报之后,它需要知道该如何对它进行处理。协议(Protocol)域指明了该将它交给哪个传输进程。TCP是一种可能,但是UDP或者其他的协议也是可能的。

(9)首部检验和(Iphdr->check) 占16位。这个字段只检验数据报的首部,但不包括数据部分。这是因为数据报每经过一个路由器,路由器都要重新计算一下首部检验和(一些字段,如生存时间、标志、片偏移等都可能发生变化)。不检验数据部分可减少计算的工作量。

       首部检验和字段(16位)是根据IP首部计算的检验和码。它不对首部后面的数据进行计算。 ICMP、IGMP、UDP和TCP在它们各自的首部中均含有同时覆盖首部和数据检验和码。

为了计算一份数据报的IP检验和,首先把检验和字段置为0。然后,对首部中每个16 bit进行二进制反码求和(整个首部看成是由一串16 bit的字组成),结果存在检验和字段中。当收到一份IP数据报后,同样对首部中每个16 bit进行二进制反码的求和。由于接收方在计算过程中包含了发送方存在首部中的检验和,因此,如果首部在传输过程中没有发生任何差错,那么接收方计算的结果应该为全1。如果结果不是全1(即检验和错误),那么IP就丢弃收到的数据报。但是不生成差错报文,由上层去发现丢失的数据报并进行重传。

(10)源地址(Iphdr->saddr) 占32位(1111 1111=255)。

(11)目的地址(Iphdr->daddr) 占32位。

 

IP数据报首部的可变部分

IP首部的可变部分就是一个可选字段。选项字段用来支持排错、测量以及安全等措施,内容很丰富。此字段的长度可变,从1个字节到40个字节不等,取决于所选择的项目。某些选项项目只需要1个字节,它只包括1个字节的选项代码。但还有些选项需要多个字节,这些选项一个个拼接起来,中间不需要有分隔符,最后用全0的填充字段补齐成为4字节的整数倍。

  增加首部的可变部分是为了增加IP数据报的功能,但这同时也使得IP数据报的首部长度成为可变的。这就增加了每一个路由器处理数据报的开销。实际上这些选项很少被使用。新的IP版本IPv6就将IP数据报的首部长度做成固定的。

  目前,这些任选项定义如下:

  (1)安全和处理限制(用于军事领域)

  (2)记录路径(让每个路由器都记下它的IP地址

  (3)时间戳(让每个路由器都记下它的IP地址和时间)

  (4)宽松的源站路由(为数据报指定一系列必须经过的IP地址)

  (5)严格的源站路由(与宽松的源站路由类似,但是要求只能经过指定的这些地址,不能经过其他的地址)

  这些选项很少被使用,并非所有主机和路由器都支持这些选项。

 

 

 

网络字节序

    4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31 bit。这种传输次序称作big endian字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。

Htons函数

  u_short PASCAL FAR htons( u_short hostshort);

  htons的功能:将一个无符号短整型数值转换为网络字节序,即大端模式(big-endian)

  参数u_short hostshort: 16位无符号整数

  返回值:TCP / IP网络字节顺序.

  htons 是把你机器上的整数转换成“网络字节序”, 网络字节序是 big-endian,也就是整数的高位字节存放在内存的低地址处。 而我们常用的 x86 CPU (intel, AMD) 电脑是 little-endian,也就是整数的低位字节放在内存的低字节处。举个例子吧。假定你的port是

  0x1234,

  在网络字节序里 这个port放到内存中就应该显示成

  addr addr+1

  0x12 0x34

  而在x86电脑上,0x1234放到内存中实际是:

  addr addr+1

  0x34 0x12

  htons 的用处就是把实际内存中的整数存放方式调整成“网络字节序”的方式。

IP数据报的分片与重组

也看了下

http://zhidao.baidu.com/question/8561666.html

http://www.techweb.com.cn/news/2009-10-26/454149.shtml

下面这个说的感觉比较全

 ip_append_data的主要任务只是创建发送网络数据的套接字缓冲区(skb),它根据输出路由查询得到的输出网络设备接口的MTU,把超过MTU长度的应用数据分割开,并创建了多个skb,放入套接字的发送缓冲队列(sk_write_queue),但它并没有为任何一个skb数据加上网络层首部,并且,随后在ip_push_pending_frames函数中,又把发送缓冲队列中的所有的skb,以一个链表的形式追加到第一个skb的end成员后面的struct skb_shared_info结构体中的frag_list上,并只为第一个skb加上了网络层首部,所以,实际上,整个应用数据还只是在一个skb中,ip_append_data这样做只是为接下来的真正的IP数据的分片作好准备。
    ip_push_pending_frames在完成了skb的组装后,把它交给了函数ip_output,ip_output又调用了函数ip_finish_output,该函数对skb的长度再次进行判断,如果长度超过输出设备的mtu的值,并且符合其它分片条件,则调用ip_fragment进行数据报的分片,否则直接调用ip_finish_output2输出到数据链路层。
    IP数据的分片涉及到IP首部中的两个字段,即结构体struct iphdr的成员frag_off,其高三位是三个标志位,第二位是不允许分片标志,置该位,表示该IP数据报不允许被分片,如果发送这样的数据报,并且数据报本身长度已经超出MTU的很制,则向发送方发一个icmp出错报文,报文类型为目的不可达(3),代码为需要进行分片但被设置了不允许分片的位(4);第三位如果置1,表示后面还有分片,置0表示本分片是一个完整的IP数据报的最后一个分片。frag_off的低13位表示本分片的第一个字节在整个IP数据报中的偏移量,单位是字节数除以8,所以需要把这13位左移3位,才是真正的偏移字节数。
    有了先前ip_append_data的工作,ip_fragment的分片工作相对简单很多。struct sk_buff的成员cb在inet域被存入了结构体struct inet_skb_parm,其定义如下:
    struct inet_skb_parm
    {
        struct ip_options   opt;
        unsigned char       flags;
#define IPSKB_FORWARDED     1
#define IPSKB_XFRM_TUNNEL_SIZE  2
#define IPSKB_XFRM_TRANSFORMED  4
#define IPSKB_FRAG_COMPLETE 8
#define IPSKB_REROUTED      16
    };
    分片完成的一个IP数据报,它的每一个skb的cb->flags被置上IPSKB_FRAG_COMPLETE标志。ip_fragment首先为frag_list列表中的每个skb的成员sk和destructor赋上跟第一个skb同样的值,使它们成为正常的skb。然后,为每个skb从第一个skb中拷贝元数据和网络层首部,并设置正确的iphdr->frag_off的值,并把它们一一输出到数据链路层。至此,网络层的数据发送工作全部完成。
    被分片后的IP数据报,其每一个分片都在网络中独立传输,所以,它们到达目的主机一般是不会同时的,并有可能乱序的。并且,它们在中间的传输路径上有可能被组装,也有可能被再次分片。由于网络层首部中frag_off的存在,使得重新正确组装成为可能。下面看看IP数据分片到达目的主机后,是如何被重新组装起来的。
    协议栈收到一个IP数据报,进入网络层的第一个函数是ip_rcv,ip_rcv对数据报进行一些正确性检查后,交给ip_rcv_finish,ip_rcv_finish查询输入路由后,交给dst_input,dst_input调用skb->dst->input,如果是本地接收的IP数据报,该函数即ip_local_deliver,ip_local_deliver一开始就检查该数据报的IP首部的frag_off成员,如果发现其低13位不为0,或者高三位中的第三位被置1,则表示这是一个IP分片(第一个分片的低13位为0,但高三位中的第三位被置1,表示后面还有分片,最后一个分片标志位置0,但低13位不为0,中间的分片,两者都不为0)。对于IP分片,该函数调用ip_defrag把它与已经收到的IP分片重组,并等待后来的IP分片,直至形成一个完整的IP数据报。
    一个完整IP数据报的全部IP分片组织存放在一个结构体struct ipq中,该结构体保存有足够的信息,等最后一个分片到达后,把它们还原成一个IP数据报。下面是struct ipq的完整定义:
    struct ipq {
        struct hlist_node list;
        struct list_head lru_list;
        u32     user;
        u32     saddr;
        u32     daddr;
        u16     id;
        u8      protocol;
        u8      last_in;
#define COMPLETE        4
#define FIRST_IN        2
#define LAST_IN         1
        struct sk_buff  *fragments;
        int     len;
        int     meat;
        spinlock_t  lock;
        atomic_t    refcnt;
        struct timer_list timer;
        struct timeval  stamp;
        int             iif;
        unsigned int    rid;
        struct inet_peer *peer;
    };
    user是一个标志,用于标识该IP分片组的来源,协议栈收到的来自网络其它主机或本地环回接口的IP分片,该标识值是IP_DEFRAG_LOCAL_DELIVER,saddr,daddr,id,protocol的值都来源于IP首部,用于确定这些IP分片确实是来自唯一的一个IP数据报。因为一台主机的协议栈可能同时跟网络中多台主机在进行通讯,所以,某一时刻,协议栈一般总有多组IP分片等待被重组,也就是说会有多个struct ipq结构的实例,多个struct ipq被组织在一个哈希表ipq_hash中,当收到一个IP分片时,首先用IP首部的相应字段计算一个哈项值,找到哈希表ipq_hash中的一项,然后去匹配上述字段完全符合的一个struct ipq,把分片加入到该ipq中即可。如果在哈希表中找不到跟当前的IP分片首部完全符合的项,则需要重新创建一个struct ipq的实例,并加到哈希表中。新创建的ipq都带有一个定时器(timer成员),超时时间缺省为IP_FRAG_TIME(30秒),如果30秒后,某一个IP数据报的分片还没有全部被收到,则这个ipq超时,超时处理函数被执行,超时处理函数会删除这个ipq,并向接收端发送一个icmp出错报文,该报文类型为超时(11),代码为在数据报组装期间生存时间为0(1)。
    得到了一个ipq后,开始把收到的分片的skb放到这个ipq中,首先检查ipq的last_in,如果它的值为COMPLETE,则表示这个分片组已经完整了,新收到的IP分片是错误的,直接扔掉。再检查新收到的skb的cb->flags,因为在发送数据报进行分片时,每一个分片的flag会被置上IPSKB_FRAG_COMPLETE标志。
    接下来检查收到的IP分片的IP_MF(frag_off的高三位中的第三位),如果为0,表示这已经是最后一个分片了,置ipq的last_in为LAST_IN,len始终被更新为当前收到的IP分片中偏移最大的那个分片的偏移值加上长度,最后如果正确,则为整个IP数据报的长度。然后把这个skb剥离网络层首部后,加入到fragments链表中,该链表以frag_off中的偏移量为顺序组织,也就是真正的IP分片的顺序,如果当前收到的这个分片的偏移量为0,则置last_in的值为FIRST_IN,meat始终被更新为当前收到的IP分片的总长,最后,如果正确,meat应该等于len。
    IP分片添加完毕后,如果meat确定等于len了,可以考虑进行重组了,函数ip_frag_reasm完成重组,它取得fragments链表,把第二个skb开始的链表又重新放到第一个skb的end后面的struct skb_shared_info结构体的frag_list链表上,并重设IP首部,一个完整的IP数据报就被重组完成了。返回前,还要释放ipq。

 

IP数据报的分片与重组小结

       根据前面看的几篇,有的说的不很准,我也没办法判别,暂时认为ip数据报的分片和重组过程如下

       发送:

       发送的时候由协议栈上到下出数据,应用层下数据到传输层,这个时候传输层可能是tcp或者是udp或者其他的协议,数据下到网络层,这里我们讨论的是网络层是IP协议的情况,然后就进行ip数据报的生成了(这个过程一个大文件的话会分成多个ip数据报么??以后还得明确下)。

       到了网络层, ip_append_data先检查ip数据报大小有没有超过MTU,如果超过,则分割开,函数把每个都分成MTU大小,最后一个分割的部分大小可能小于MTU,然后为这每个分割部分创建一个skb,分完了后就放入套接字的发送缓冲队列(sk_write_queue),但它并没有为任何一个skb数据加上网络层首部。

      然后ip_push_pending_frames这个函数出来把所有的skb都连起来,依次追加到第一个skb结构体的end指针后面,并且为第一个skb添加ip首部,接下来交给ip_output,这个函数又调离一个函数来判断是否大于MTU,(你连接在第一个skb后面的那些skb,肯定就是大于MTU的,但是只有一个skb的话,就不需要在分片),如果大于MTU,就开始调用ip_fragment进行数据报的分片,否则直接调用ip_finish_output2输出到数据链路层。

       分片完成的一个IP数据报,它的每一个skb的cb->flags被置上IPSKB_FRAG_COMPLETE标志。ip_fragment首先为frag_list列表中的每个skb的成员sk和destructor赋上跟第一个skb同样的值,使它们成为正常的skb。然后,为每个skb从第一个skb中拷贝元数据和网络层首部,并设置正确的iphdr->frag_off的值,并把它们一一输出到数据链路层。至此,网络层的数据发送工作全部完成。

接收:

   收到的ip数据报ip数据报头部信息hash到具体的struct ipq中或者重新建立一个struct ipq,对于同一个ip数据报而分来的分片ip数据报,它们的ip数据报头部信息能让它们hash到同一个ipq中,直到所有的分片到来才开始重组,不然一直等待所有的分片到来,当然,也不是一直等,ipq都带有一个定时器(timer成员),超时时间缺省为IP_FRAG_TIME(30秒),如果30秒后,某一个IP数据报的分片还没有全部被收到,则这个ipq超时,超时处理函数被执行,超时处理函数会删除这个ipq,并向接收端发送一个icmp出错报文。

来自:

http://blog.csdn.net/lishaman/article/details/6681448

http://baike.baidu.com/view/1519445.htm

http://baike.baidu.com/view/2696.htm

http://apps.hi.baidu.com/share/detail/30118020

 

0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有