<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          IP協(xié)議源碼分析

          共 8745字,需瀏覽 18分鐘

           ·

          2021-02-12 11:55

          IP協(xié)議?是網(wǎng)絡的最重要部分,毫不夸張地說,正是因為有?IP協(xié)議?才有了互聯(lián)網(wǎng)。而?IP協(xié)議?最重要的是?IP地址,IP地址?就好像我們的家庭住址一樣,用于其他人方便找到我們的位置。

          當然,這篇文章并不是介紹?IP協(xié)議?的原理,有關?IP協(xié)議?的原理可以參考經(jīng)典的書籍《TCP/IP協(xié)議詳解》,而這篇文章主要介紹的是 Linux 內核怎么實現(xiàn)?IP協(xié)議

          IP協(xié)議簡介

          雖然我們不會對?IP協(xié)議?做詳細介紹,但是還是作個簡單的介紹吧,不然直接分析代碼有點唐突。

          如果有一臺計算機A和一臺計算機B,計算機A想與計算機B通訊的話怎么辦?

          如果計算機A與計算機B是直連的,那么計算機A可以直接發(fā)送信息給計算機B,如下圖:


          所以,如果是直連的話,問題很容易解決。但是,大多數(shù)情況下,互聯(lián)網(wǎng)上的計算機都不是直連的。試想一下,如果在中國的電腦如果要與美國的電腦發(fā)送消息,那是不可能直接通過一條網(wǎng)線連接的。

          那么,互聯(lián)網(wǎng)上的計算機之間是通過什么連接的的?答案就是?路由器。如下圖:


          由于在互聯(lián)網(wǎng)中,計算機與計算機之間不是直連的,所以兩臺計算機之間要通訊的話不能直接發(fā)送消息,因為不同的計算機之間不知道對方的位置。

          那么,有什么辦法解決呢?我們現(xiàn)實生活中,房子都有一個固定的地址,如:廣東省廣州市天河區(qū)林和西路98號,我們可以通過這個地址找到對應的房子。所以,對應互聯(lián)網(wǎng)上的計算機,我們也可以人為的為其編上地址,名為?IP地址。

          IP地址?由一個 32 位的整型數(shù)字表示,學過計算機科學的同學都指定,一個 32 位的整型數(shù)字能夠表示的范圍為:0 ~ 4294967295。所以,IP地址?理論上能夠支持 4294967296 臺計算機上網(wǎng)(但事實上遠遠少于這個數(shù),因為有很多地址用于特殊用途)。

          但是,32 位的整型數(shù)字對人類的記憶不太友好,所以,又將這個 32 位的整型數(shù)字分成 4 個 8 位的整型數(shù)字,然后用??將他們連接起來,如下圖:


          所以,IP地址?表示的范圍如下:

          0.0.0.0 ~ 255.255.255.255

          我們可以在?Windows?系統(tǒng)的網(wǎng)絡設置處查看到本機的?IP地址,如下圖:


          有了?IP地址?后,就可以為互聯(lián)網(wǎng)上的每臺計算機設置?IP地址,如下圖:


          這樣,為每臺計算機設置好?IP地址?后,不同計算機之間就可以通過?IP地址?來進行通訊。比如,計算機A想與計算機D通訊,那么就可以通過向?IP地址?為?11.11.1.1?的地址發(fā)送消息。

          通信過程如下:

          • 計算A把自己的?IP地址(源IP地址)?與計算機B的?IP地址(目標IP地址)?封裝成數(shù)據(jù)包,然后發(fā)送到路由器A。

          • 路由器A接收到計算機A的數(shù)據(jù)包,發(fā)現(xiàn)目標?IP地址?不在同一個網(wǎng)段(也就是連接不同路由器的),就會從?路由表?中找到合適的下一跳?路由器,把數(shù)據(jù)包轉發(fā)出去(也就是路由B)。

          • 路由器B接收到路由器A的數(shù)據(jù)后,從數(shù)據(jù)包中獲取到目標?IP地址?為?11.11.1.1,知道此?IP地址?是計算機D的?IP地址,所以就把數(shù)據(jù)轉發(fā)給計算機D。

          由于每個路由器都知道連著自己的所有計算機的?IP地址(稱為路由表),所以路由器與路由器之間可以通過?路由協(xié)議?交換路由表的信息,這樣就可以從路由表信息中查找到?IP地址?對應的下一跳路由器。

          IP協(xié)議?就介紹到這里了,更詳細的原理可以參考其他資料。

          IP頭部

          由于向網(wǎng)絡中的計算機發(fā)送數(shù)據(jù)時,必須指定對方的?IP地址(目標IP地址)?和本機的?IP地址(源IP地址),所以需要在發(fā)送的數(shù)據(jù)包添加?IP協(xié)議?頭部。IP協(xié)議?頭部的格式如下圖所示:


          從上圖可以看出,除了?目標IP地址?和?源IP地址?外,還有其他一些字段,這些字段都是為了實現(xiàn)?IP協(xié)議?而定義的。下面我們來介紹一下?IP頭部?各個字段的作用:

          • 版本:占 4 個位。表示?IP協(xié)議?的版本,如果是?IPv4?的話,那么固定為 4。

          • 頭部長度:占 4 個位。表示?IP頭部?的長度,單位為字(即 4 個字節(jié))。由于其最大值為 15,所以?IP頭部?最長為 60 字節(jié)(15 * 4)。

          • 服務類型:占 8 個位。定義不同的服務類型,可以為 IP 數(shù)據(jù)包提供不同的服務。本文不涉及這個字段,所以不作詳細介紹。

          • 總長度:占 16 個位。表示整個 IP 數(shù)據(jù)包的長度,包括數(shù)據(jù)與?IP頭部,所以 IP 數(shù)據(jù)包的最大長度為 65535 字節(jié)。

          • ID:占 16 個位。用于標識不同的 IP 數(shù)據(jù)包。該字段和?Flags?和?分片偏移量?字段配合使用,對較大的 IP 數(shù)據(jù)包進行分片操作,關于 IP 數(shù)據(jù)包分片功能后面會介紹。

          • 標記(Flags):占 3 個位。該字段第一位不使用,第二位是?DF(Don't Fragment)?位,DF?位設為 1 時表明路由器不能對該數(shù)據(jù)包分段。第三位是?MF(More Fragments)?位,表示當前分片是否為 IP 數(shù)據(jù)包的最后一個分片,如果是最后一個分片,就設置為 0,否則設置為 1。

          • 分片偏移量:占 13 個位。表示當前分片位于 IP 數(shù)據(jù)包分片組中的位置,接收端靠此來組裝還原 IP 數(shù)據(jù)包。

          • 生存期(TTL):占 8 個位。當 IP 數(shù)據(jù)包進行發(fā)送時,先會對該字段賦予某個特定的值。當 IP 數(shù)據(jù)包經(jīng)過沿途每一個路由器時,每個沿途的路由器會將該 IP 數(shù)據(jù)包的 TTL 值減少 1。如果 TTL 減少至為 0 時,則該 IP 數(shù)據(jù)包會被丟棄。這個字段可以防止由于路由環(huán)路而導致 IP 數(shù)據(jù)包在網(wǎng)絡中不停被轉發(fā)。

          • 上層協(xié)議:占 8 個位。標識了上層所使用的協(xié)議,例如常用的 TCP,UDP 等。

          • 校驗和:占 16 個位。用于對 IP 頭部的正確性進行檢測,但不包含數(shù)據(jù)部分。因為每個路由器要改變 TTL 的值,所以路由器會為每個通過的 IP 數(shù)據(jù)包重新計算這個值。

          • 源 IP 地址與目標 IP 地址:這兩個字段都占 32 個位。標識了這個 IP 數(shù)據(jù)包的?源IP地址?和?目標IP地址。

          • IP選項:長度可變,最多包含 40 字節(jié)。選項字段很少被使用,所以本文不會介紹。

          IP頭部?結構在內核中的定義如下:

          struct iphdr {    __u8    version:4,            ihl:4;    __u8    tos;    __u16   tot_len;    __u16   id;    __u16   frag_off;    __u8    ttl;    __u8    protocol;    __u16   check;    __u32   saddr;    __u32   daddr;    /*The options start here. */};

          IP頭部?結構的各個字段與上圖的所展示的字段是一一對應的。

          雖然?IP頭部?看起來好像很復雜,但如果按每個字段所支持的功能來分析,就會豁然開朗。一個被添加上?IP頭部?的數(shù)據(jù)包如下圖所示:


          當然,除了?IP頭部?外,在一個網(wǎng)絡數(shù)據(jù)包中還可能包含一些其他協(xié)議的頭部,比如?TCP頭部,以太網(wǎng)頭部?等,但由于這里只分析?IP協(xié)議,所以只標出了?IP頭部

          接下來,我們通過源碼來分析 Linux 內核是怎么實現(xiàn)?IP協(xié)議?的,我們主要分析 IP 數(shù)據(jù)包的發(fā)送與接收過程。

          IP數(shù)據(jù)包的發(fā)送

          要發(fā)送一個 IP 數(shù)據(jù)包,可以通過兩個接口來完成:ip_queue_xmit()?和?ip_build_xmit()。第一個主要用于 TCP 協(xié)議,而第二個主要用于 UDP 協(xié)議。

          我們主要分析?ip_queue_xmit()?這個接口,ip_queue_xmit()?代碼如下:

          int ip_queue_xmit(struct sk_buff *skb){    struct sock *sk = skb->sk;    struct ip_options *opt = sk->protinfo.af_inet.opt;    struct rtable *rt;    struct iphdr *iph;
          rt = (struct rtable *)__sk_dst_check(sk, 0); // 是否有路由信息緩存 if (rt == NULL) { u32 daddr; u32 tos = RT_TOS(sk->protinfo.af_inet.tos)|RTO_CONN|sk->localroute;
          daddr = sk->daddr; if(opt && opt->srr) daddr = opt->faddr;
          // 通過目標IP地址獲取路由信息 if (ip_route_output(&rt, daddr, sk->saddr, tos, sk->bound_dev_if)) goto no_route; __sk_dst_set(sk, &rt->u.dst); // 設置路由信息緩存 }
          skb->dst = dst_clone(&rt->u.dst); // 綁定數(shù)據(jù)包的路由信息 ... // 獲取數(shù)據(jù)包的IP頭部指針 iph = (struct iphdr *)skb_push(skb, sizeof(struct iphdr)+(opt?opt->optlen:0));
          // 設置 版本 + 頭部長度 + 服務類型 *((__u16 *)iph) = htons((4<<12)|(5<<8)|(sk->protinfo.af_inet.tos & 0xff));
          iph->tot_len = htons(skb->len); // 設置總長度 iph->frag_off = 0; // 分片偏移量 iph->ttl = sk->protinfo.af_inet.ttl; // 生命周期 iph->protocol = sk->protocol; // 上層協(xié)議(如TCP或者UDP等) iph->saddr = rt->rt_src; // 源IP地址 iph->daddr = rt->rt_dst; // 目標IP地址
          skb->nh.iph = iph; ... // 調用 ip_queue_xmit2() 進行下一步的發(fā)送操作 return NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev, ip_queue_xmit2);}

          ip_queue_xmit()?函數(shù)的參數(shù)是要發(fā)送的數(shù)據(jù)包,其類型為?sk_buff。在內核協(xié)議棧中,所有要發(fā)送的數(shù)據(jù)都是通過?sk_buff?結構來作為載體的。ip_queue_xmit()?函數(shù)主要完成以下幾個工作:

          • 首先調用?__sk_dst_check()?函數(shù)獲取路由信息緩存,如果路由信息還沒被緩存,那么以?目標IP地址?作為參數(shù)調用?ip_route_output()?函數(shù)來獲取路由信息,并且設置路由信息緩存。路由信息一般包含發(fā)送數(shù)據(jù)的設備對象(網(wǎng)卡設備)和下一跳路由的?IP地址。

          • 綁定數(shù)據(jù)包的路由信息。

          • 獲取數(shù)據(jù)包的?IP頭部?指針,然后設置?IP頭部?的各個字段的值,如代碼注釋所示,可以對照?IP頭部?結構圖來分析。

          • 調用?ip_queue_xmit2()?進行下一步的發(fā)送操作。

          我們接著分析?ip_queue_xmit2()?函數(shù)的實現(xiàn),代碼如下:

          static inline int ip_queue_xmit2(struct sk_buff *skb){    struct sock *sk = skb->sk;    struct rtable *rt = (struct rtable *)skb->dst;    struct net_device *dev;    struct iphdr *iph = skb->nh.iph;    ...    // 如果數(shù)據(jù)包的長度大于設備的最大傳輸單元, 那么進行分片操作    if (skb->len > rt->u.dst.pmtu)        goto fragment;
          if (ip_dont_fragment(sk, &rt->u.dst)) // 如果數(shù)據(jù)包不能分片 iph->frag_off |= __constant_htons(IP_DF); // 設置 DF 標志位為1
          ip_select_ident(iph, &rt->u.dst); // 設置IP數(shù)據(jù)包的ID(標識符)
          // 計算 IP頭部 的校驗和 ip_send_check(iph);
          skb->priority = sk->priority; return skb->dst->output(skb); // 把數(shù)據(jù)發(fā)送出去(一般為 dev_queue_xmit)
          fragment: ... ip_select_ident(iph, &rt->u.dst); return ip_fragment(skb, skb->dst->output); // 進行分片操作}

          ip_queue_xmit2()?函數(shù)主要完成以下幾個工作:

          • 判斷數(shù)據(jù)包的長度是否大于最大傳輸單元(最大傳輸單元 Maximum Transmission Unit,MTU?是指在傳輸數(shù)據(jù)過程中允許報文的最大長度),如果大于最大傳輸單元,那么就調用?ip_fragment()?函數(shù)對數(shù)據(jù)包進行分片操作。

          • 如果數(shù)據(jù)包不能進行分片操作,那么設置?DF(Don't Fragment)?位為 1。

          • 設置 IP 數(shù)據(jù)包的 ID(標識符)。

          • 計算?IP頭部?的校驗和。

          • 通過網(wǎng)卡設備把數(shù)據(jù)包發(fā)送出去,一般通過調用?dev_queue_xmit()?函數(shù)。

          ip_queue_xmit2()?函數(shù)會繼續(xù)設置?IP頭部?其他字段的值,然后調用?dev_queue_xmit()?函數(shù)把數(shù)據(jù)包發(fā)送出去。

          當然還要判斷發(fā)送的數(shù)據(jù)包長度是否大于最大傳輸單元,如果大于最大傳輸單元,那么就需要對數(shù)據(jù)包進行分片操作。數(shù)據(jù)分片是指把要發(fā)送的數(shù)據(jù)包分割成多個以最大傳輸單元為最大長度的數(shù)據(jù)包,然后再把這些數(shù)據(jù)包發(fā)送出去。

          IP數(shù)據(jù)包的接收

          IP數(shù)據(jù)包的接收是通過?ip_rcv()?函數(shù)完成的,當網(wǎng)卡接收到數(shù)據(jù)包后,會上送到內核協(xié)議棧的鏈路層,鏈路層會根據(jù)鏈路層協(xié)議(如以太網(wǎng)協(xié)議)解析數(shù)據(jù)包。然后再將解析后的數(shù)據(jù)包通過調用?ip_rcv()?函數(shù)上送到網(wǎng)絡層的?IP協(xié)議,ip_rcv()?函數(shù)的實現(xiàn)如下:

          int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt){    struct iphdr *iph = skb->nh.iph; // 獲取數(shù)據(jù)包的IP頭部
          if (skb->pkt_type == PACKET_OTHERHOST) // 如果不是發(fā)送給本機的數(shù)據(jù)包, 則丟掉這個包 goto drop; ... // 判斷數(shù)據(jù)包的長度是否合法 if (skb->len < sizeof(struct iphdr) || skb->len < (iph->ihl<<2)) goto inhdr_error;
          // 1. 判斷頭部長度是否合法 // 2. IP協(xié)議的版本是否合法 // 3. IP頭部的校驗和是否正確 if (iph->ihl < 5 || iph->version != 4 || ip_fast_csum((u8 *)iph, iph->ihl) != 0) goto inhdr_error;
          { __u32 len = ntohs(iph->tot_len); if (skb->len < len || len < (iph->ihl<<2)) // 數(shù)據(jù)包的長度是否合法 goto inhdr_error;
          __skb_trim(skb, len); }
          // 繼續(xù)調用 ip_rcv_finish() 函數(shù)處理數(shù)據(jù)包 return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);
          inhdr_error: IP_INC_STATS_BH(IpInHdrErrors);drop: kfree_skb(skb);out: return NET_RX_DROP;}

          ip_rcv()?函數(shù)的主要工作就是驗證?IP頭部?各個字段的值是否合法,如果不合法就將數(shù)據(jù)包丟棄,否則就調用?ip_rcv_finish()?函數(shù)繼續(xù)處理數(shù)據(jù)包。

          ip_rcv_finish()?函數(shù)的實現(xiàn)如下:

          static inline int ip_rcv_finish(struct sk_buff *skb){    struct net_device *dev = skb->dev;    struct iphdr *iph = skb->nh.iph;
          // 根據(jù)源IP地址、目標IP地址和服務類型查找路由信息 if (skb->dst == NULL) { if (ip_route_input(skb, iph->daddr, iph->saddr, iph->tos, dev)) goto drop; } ...
          // 如果是發(fā)送給本機的數(shù)據(jù)包將會調用 ip_local_deliver() 處理 return skb->dst->input(skb);}

          ip_rcv_finish()?函數(shù)的實現(xiàn)比較簡單,首先以?源IP地址、目標IP地址?和?服務類型?作為參數(shù)調用?ip_route_input()?函數(shù)查找對應的路由信息。

          然后通過調用路由信息的?input()?方法處理數(shù)據(jù)包,如果是發(fā)送給本機的數(shù)據(jù)包?input()?方法將會指向?ip_local_deliver()?函數(shù)。

          我們接著分析?ip_local_deliver()?函數(shù):

          int ip_local_deliver(struct sk_buff *skb){    struct iphdr *iph = skb->nh.iph;
          // 如果是一個IP數(shù)據(jù)包的分片 if (iph->frag_off & htons(IP_MF|IP_OFFSET)) { skb = ip_defrag(skb); // 將分片組裝成真正的數(shù)據(jù)包,如果成功將會返回組裝后的數(shù)據(jù)包 if (!skb) return 0; }
          // 繼續(xù)調用 ip_local_deliver_finish() 函數(shù)處理數(shù)據(jù)包 return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL, ip_local_deliver_finish);}

          ip_local_deliver()?函數(shù)首先判斷數(shù)據(jù)包是否是一個分片,如果是分片的話,就調用?ip_defrag()?函數(shù)對分片進行重組操作。重組成功的話,會返回重組后的數(shù)據(jù)包。接著調用?ip_local_deliver_finish()?對數(shù)據(jù)包進行處理。

          ip_local_deliver_finish()?函數(shù)的實現(xiàn)如下:

          static inline int ip_local_deliver_finish(struct sk_buff *skb){    struct iphdr *iph = skb->nh.iph;
          skb->h.raw = skb->nh.raw + iph->ihl*4; // 設置傳輸層頭部(如TCP/UDP頭部)
          { int hash = iph->protocol & (MAX_INET_PROTOS - 1); // 傳輸層協(xié)議對應的hash值 struct sock *raw_sk = raw_v4_htable[hash]; struct inet_protocol *ipprot; int flag; ...
          // 通過hash值找到傳輸層協(xié)議的處理函數(shù) ipprot = (struct inet_protocol *)inet_protos[hash]; flag = 0;
          if (ipprot != NULL) { if (raw_sk == NULL && ipprot->next == NULL && ipprot->protocol == iph->protocol) { // 調用傳輸層的數(shù)據(jù)包處理函數(shù)處理數(shù)據(jù)包 return ipprot->handler(skb, (ntohs(iph->tot_len) - iph->ihl*4)); } else { flag = ip_run_ipprot(skb, iph, ipprot, (raw_sk != NULL)); } } ... } return 0;}

          ip_local_deliver_finish()?函數(shù)的主要工作就是根據(jù)上層協(xié)議(傳輸層)的類型,然后從?inet_protos?數(shù)組中找到其對應的數(shù)據(jù)包處理函數(shù),然后通過此數(shù)據(jù)包處理函數(shù)處理數(shù)據(jù)。

          也就是說,IP層對數(shù)據(jù)包的正確性驗證完成和重組后,會將數(shù)據(jù)包上送給傳輸層去處理。對于?TCP協(xié)議?來說,數(shù)據(jù)包處理函數(shù)對應的是?tcp_v4_rcv()


          瀏覽 25
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  任我操在线视频 | 日逼视频软件 | 日韩又大又粗精品 | 99久久香蕉视频 | 98无码人妻精品一区二区三区 |