分析TCP/IP协议栈代码之IP & ICMP(STM32平台)

1. IP介绍
IP是TCP/IP协议族中最为核心的协议。大家,如TCP、UDP、ICMP及IGMP数据,都是在IP数据报格式基础上再封装一层再来传输的(见图1 - 4)。


不可靠(unreliable)的意思是它不能保证 IP数据报能成功地到达目的地。 IP仅提供最好的传输服务。如果发生某种错误时,如某个路由器暂时用完了缓冲区, IP有一个简单的错误处理算法:丢弃该数据报,然后发送 ICMP消息报给信源端。任何要求的可靠性必须由上层来提供(如TCP) 。
无连接(connectionless)这个术语的意思是I P并不维护任何关于后续数据报的状态信息。每个数据报的处理是相互独立的。这也说明, IP数据报可以不按发送顺序接收。如果一信源向相同的信宿发送两个连续的数据报(先是 A,然后是B) ,每个数据报都是独立地进行路由选择,可能选择不同的路线,因此B可能在A到达之前先到达。

2. IP首部
I P数据报的格式如图3 - 1所示。普通的IP首部长为20个字节,除非含有选项字段。

分析图3 - 1中的首部。最高位在左边,记为0 bit;最低位在右边,记为31 bit。4个字节的32 bit值以下面的次序传输:首先是0~7 bit,其次8~15 bit,然后1 6~23 bit,最后是24~31 bit。这种传输次序称作 big endian字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作 网络字节序
目前的 协议版本号是4,因此IP有时也称作IPv4。
服务类型(TOS)字段包括一个3 bit的优先权子字段(现在已被忽略) ,4 bit的TO S子字段和1 bit未用位但必须置0。4 bit的TO S分别代表:最小时延、最大吞吐量、最高可靠性和最小费用。
总长度字段是指整个I P数据报的长度,以字节为单位。
标识字段唯一地标识主机发送的每一份数据报。通常每发送一份报文它的值就会加 1。在大多数从伯克利派生出来的系统中,每发送一个I P数据报,I P层都要把一个内核变量的值加1,不管交给IP的数据来自哪一层。内核变量的初始值根据系统引导时的时间来设置。
TTL(time-to-live)生存时间字段设置了数据报可以经过的最多路由器数。TTL的初始值由源主机设置(通常为3 2或6 4) ,一旦经过一个处理它的路由器,它的值就减去1。当该字段的值为0时,数据报就被丢弃,并发送 ICMP报文通知源主机。
协议字段可以识别是哪个协议向I P传送数据。
首部检验和字段是根据IP首部计算的检验和码。它不对首部后面的数据进行计算。 ICMP、IGMP、UDP和TCP在它们各自的首部中均含有同时覆盖首部和数据检验和码。
目前,这些 任选项定义如下:
• 安全和处理限制(用于军事领域,详细内容参见 RFC 1108[Kent 1991])
• 记录路径(让每个路由器都记下它的IP地址,见7 . 3节)
• 时间戳(让每个路由器都记下它的IP地址和时间,见7 . 4节)
• 宽松的源站选路(为数据报指定一系列必须经过的 IP地址,见8 . 5节)
• 严格的源站选路(与宽松的源站选路类似,但是要求只能经过指定的这些地址,不能经过其他的地址) 。
------------------------------------------以上内容整理于《TCP/IP协议详解:卷1》--------------------------------------
------------------------------------------以下内容产生于代码及分析--------------------------------------
3. IP宏定义实现
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ******* IP *******
//IP首部长度
#define IP_HEADER_LEN    20

//IP版本号位置 以太网首部2+6+6,与下面那个在用的时候上区别下
#define IP_HEADER_LEN_VER_P 0xe

//IP版本号位置 以太网首部2+6+6
#define IP_P 0xe
//IP 16位标志位置
#define IP_FLAGS_P 0x14
//IP 生存时间位置
#define IP_TTL_P 0x16
//IP协议类型位置,如ICMP,TCP,UDP 1个字节
#define IP_PROTO_P 0x17
//首部校验和
#define IP_CHECKSUM_P 0x18
// IP源地址位置 14+12
#define IP_SRC_P 0x1a
// IP目标地址位置 14+12+4
#define IP_DST_P 0x1e

//IP总长度
#define IP_TOTLEN_H_P 0x10
#define IP_TOTLEN_L_P 0x11

//协议类型
#define IP_PROTO_ICMP_V 0x01
#define IP_PROTO_TCP_V 0x06
#define IP_PROTO_UDP_V 0x11

4. IP函数实现
以太网的header在IP的header之前,很简单的,介绍先。

 配置以太网的头,为14字节:6字节目的mac地址+6字节源mac地址+2字节协议类型
1
2
3
4
5
6
7
8
9
10
11
12
13
// make a return eth header from a received eth packet
void make_eth( unsigned  char *buf)
{
     unsigned  char  i =  0;

     //copy the destination mac from the source and fill my mac into src
     while(i <  sizeof(mac_addr))
    {
        buf[ETH_DST_MAC + i] = buf[ETH_SRC_MAC + i];
        buf[ETH_SRC_MAC + i] = macaddr[i];
        i++;
    }
}

展开之后如下所示,其在以太网帧中的位置与之前的宏定义是一一对应的。

IP与ARP一样,需要判定是不是发给本机的( eth_type_is_ip_and_my_ip函数),还有与填充 make_eth  函数一样需要填充函数( make_ip函数),此外还有填充其他杂七杂八和16位首部校验和函数( fill_ip_hdr_checksum函数)
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//判定过程与eth_type_is_arp_and_my_ip类似
unsigned  char  eth_type_is_ip_and_my_ip( unsigned  char *buf,  unsigned   int len)
{
     unsigned  char  i =  0;

     //eth+ip+udp header is 42
     if(len < MIN_FRAMELEN)
    {
         return( 0);
    }

     if(buf[ETH_TYPE_H_P] != ETHTYPE_IP_H_V || buf[ETH_TYPE_L_P] != ETHTYPE_IP_L_V)
    {
         return( 0);
    }

     if(buf[IP_HEADER_LEN_VER_P] != 0x45)
    {
         // must be IP V4 and 20 byte header
         return( 0);
    }

     while(i <  sizeof(ipv4_addr))
    {
         if(buf[IP_DST_P + i] != ipaddr[i])
        {
             return( 0);
        }

        i++;
    }

     return( 1);
}
//下面那个ip填充函数调用它,主要是补充填充和校验和
void fill_ip_hdr_checksum( unsigned  char *buf)
{
     unsigned   int ck;
     // clear the 2 byte checksum
    buf[IP_CHECKSUM_P] =  0;
    buf[IP_CHECKSUM_P +  1] =  0;
    buf[IP_FLAGS_P] = 0x40;  // don't fragment
    buf[IP_FLAGS_P +  1] =  0// fragement offset
    buf[IP_TTL_P] =  64// ttl
     // calculate the checksum:
     //校验和计算,在下下面那个函数里面,输入参数的含义下面看就晓得了
    ck = checksum(&buf[IP_P], IP_HEADER_LEN,  0);
    buf[IP_CHECKSUM_P] = ck >>  8;
    buf[IP_CHECKSUM_P +  1] = ck & 0xff;
}

// make a return ip header from a received ip packet
//与以太网填充函数类似,填充ip地址
void make_ip( unsigned  char *buf)
{
     unsigned  char  i =  0;

     while(i <  sizeof(ipv4_addr))
    {
        buf[IP_DST_P + i] = buf[IP_SRC_P + i];
        buf[IP_SRC_P + i] = ipaddr[i];
        i++;
    }

    fill_ip_hdr_checksum(buf);
}
5. IP校验和实现
校验和函数式如何得出校验和值的呢?看《TCP/IP协议详解:卷1》里面咋说的吧。
        ”为了计算一份数据报的 IP检验和,首先把检验和字段置为 0。然后,对首部中每个 16 bit进行二进制反码求和(整个首部看成是由一串 16 bit的字组成) ,结果存在检验和字段中。当收到一份I P数据报后,同样对首部中每个16 bit进行二进制反码的求和。由于接收方在计算过程中包含了发送方存在首部中的检验和,因此,如果首部在传输过程中没有发生任何差错,那么接收方计算的结果应该为全 1。如果结果不是全1(即检验和错误) ,那么I P就丢弃收到的数据报。但是不生成差错报文,由上层去发现丢失的数据报并进行重传。
        ICMP、IGMP、UDP和TCP都采用相同的检验和算法,尽管TCP和UDP除了本身的首部和数据外,在IP首部中还包含不同的字段。在RFC 1071[Braden, Borman and Patridge 1988]中有关于如何计算Internet检验和的实现技术。由于路由器经常只修改 TTL段(减1) ,因此当路由器转发一份报文时可以增加它的检验和,而不需要对 IP整个首部进行重新计算。 RFC1141[Mallory and Kullberg 1990]为此给出了一个很有效的方法。“

        但是本协议栈的实现顺序上与以上说的略有不同,《TCP/IP协议详解:卷1》是先反码再求和,本协议栈里面是先求和再反码,当然都是按照16bit单位的单元来的。 那结果一样么?
比如:
11101010 01010100
10000000 11111110
先反码再求和:
取反
00010101 10101011
01111111 00000001
求和
10010100 10101100
先求和再反码:
求和
1 01101011 01010010
将进位加置最后来保持16位(下面的代码如是说)
01101011 01010011
取反
10010100 10101100
没错,不完全验证两种方法的结果是一致的,不是科班出身,感觉上有啥子理论来说明某种顺序的调换在对二进制运算结果方面的影响是无关的。
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
unsigned   int checksum( unsigned  char * buf,  unsigned   int len, unsigned  char  type)
{
     // type 0=ip 
     //      1=udp
     //      2=tcp
     unsigned  long sum =  0;
    
     //if(type==0){
     //        // do not add anything
     //}
     if(type== 1)
    {
        sum+=IP_PROTO_UDP_V;  // protocol udp
         // the length here is the length of udp (data+header len)
         // =length given to this function - (IP.scr+IP.dst length)
        sum+=len- 8// = real tcp len
    }
     if(type== 2)
    {
        sum+=IP_PROTO_TCP_V; 
         // the length here is the length of tcp (data+header len)
         // =length given to this function - (IP.scr+IP.dst length)
        sum+=len- 8// = real tcp len
    }
     // build the sum of 16bit words
     while(len > 1)
    {
        sum += 0xFFFF & (*buf<<8|*(buf+ 1));
        buf+= 2;
        len-= 2;
    }
     // if there is a byte left then add it (padded with zero)
     if (len)
    {
        sum += (0xFF & *buf)<< 8;
    }
     // now calculate the sum over the bytes in the sum
     // until the result is only 16bit long
     while (sum>> 16)
    {
        sum = (sum & 0xFFFF)+(sum >>  16);
    }
     // build 1's complement:
     return( ( unsigned   int) sum ^ 0xFFFF);
}
------------------------------------------------------------------------------------------------------
6. ICMP简介
注意:ICMP在TCP/IP分层上与IP属于同一层,因此放在与IP一块,但是ICMP是封装在IP数据报里面的。

ICMP:Internet Control Messages Protocol, 网间控制报文协议

ICMP报文的格式如图6 - 2所示。所有报文的前4个字节都是一样的,但是剩下的其他字节则互不相同。下面我们将逐个介绍各种报文格式。类型字段可以有1 5个不同的值,以描述特定类型的 ICMP报文。某些ICMP报文还使用代码字段的值来进一步描述不同的条件。
检验和字段覆盖整个ICMP报文。使用的算法与I P首部检验和算法相同。ICMP的检验和是必需的。


7. ICMP宏定义及函数实现

虽然ICMP具有很多的子协议,但是其中最著名的要数ping程序,即ICMP回显请求和应答报文。通过使用ping命令来判断报文是否可以到达目标地址。ICMP的实现是一个逐步遵守规则的过程,即向固定的字节填充数据,其实本协议栈也就实现了这个ping。
“ping”这个名字源于声纳定位操作。 Ping程序由Mike Muuss编写,目的是为了测试另一 台主机是否可达。该程序发送一份 ICMP回显请求报文给主机,并等待返回 ICMP回显应答。

当返回ICMP回显应答时,要打印出序列号和TTL,并计算往返时间(TTL位于 IP首部中 的生存时间字段)。
ICMP回显应答需要做好两步,第一步检查IP首部中的协议类型是否为ICMP报文;第二,检查ICMP首部中的ICMP类型是 否为ICMP请求,如果是则生成ICMP回显应答并通过以太网驱动芯片发送。为了便于调试,在接收到ICMP回显请求时通过串口 输出发起方的IP地址,ping命令发起方的IP地址存在于IP首部中的源IP地址部分。
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ******* ICMP *******
//回显应答
#define ICMP_TYPE_ECHOREPLY_V  0
//回显请求
#define ICMP_TYPE_ECHOREQUEST_V  8
//ICMP类型
#define ICMP_TYPE_P 0x22
//ICMP首部校验和
#define ICMP_CHECKSUM_P 0x24

void make_echo_reply_from_request( unsigned  char * buf, unsigned   int len)
{
    make_eth(buf);
    make_ip(buf);
      //ICMP_DEBUG插入此处
    buf[ICMP_TYPE_P]=ICMP_TYPE_ECHOREPLY_V;    //回显应答
     // we changed only the icmp.type field from request(=8) to reply(=0).
     // we can therefore easily correct the checksum:
     if (buf[ICMP_CHECKSUM_P] > (0xff-0x08))
    {
        buf[ICMP_CHECKSUM_P+ 1]++;
    }
    buf[ICMP_CHECKSUM_P]+=0x08;
     //
    enc28j60PacketSend(len,buf);
}


8 ICMP实验调试
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void make_echo_reply_from_request( unsigned  char * buf, unsigned   int len)
{
         int i= 0;
    make_eth(buf);
    make_ip(buf);
         #ifdef ICMP_DEBUG
        printf( "ping命令发起者的IP地址 : \r\n");
       while(i< sizeof(ipv4_addr))
    {
             //注意这里是IP_SRC_P,不是ARP包了,因为包的类型变了
            printf( "%d",buf[IP_SRC_P+i]); /*这里错了,应该是IP_DST_P,why?看函数名,好了,看了串口的输出才看出来的*/
             if(i!= sizeof(ipv4_addr)- 1)
                printf( ".");
             else
                printf( "\r\n");
            i++;
        }
        i= 0;
         #endif
    buf[ICMP_TYPE_P]=ICMP_TYPE_ECHOREPLY_V;    //回送应答
     // we changed only the icmp.type field from request(=8) to reply(=0).
     // we can therefore easily correct the checksum:
     if (buf[ICMP_CHECKSUM_P] > (0xff-0x08))
    {
        buf[ICMP_CHECKSUM_P+ 1]++;
    }
    buf[ICMP_CHECKSUM_P]+=0x08;
     //
    enc28j60PacketSend(len,buf);
}
在程序的无线循环中,需要层层进行查询。其实就是各种if语句来判定以太网帧的那些个标记为是不是所要找的类型。
1.查询以太网中是否有数据,若无数据则返回。
2.保存源MAC地址,待返回时使用。
3.查询是否为ARP报文并返回ARP报文
4.保存源IP地址,待返回时使用。
6.查询是否为IP报文,若非IP报文返回。
5.查询是否为ICMP报文并返回ICMP回显应答。

这里可以修改 fill_ip_hdr_checksum函数里面的 ttl(time to live)值,来确定IP包的生存周期,就是经过几个路由之后被丢弃掉。修改为以下的125。


如果按照注释里面改正之后~~~


  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值