介于最近TCP/IP比较火,那我就加一把吧~~~
本文主要是结合《TCP/IP协议栈.第一卷》来分析AVRNET(貌似不叫这个名字,姑且这样称呼吧,因为代码里面木有给名字)的几百行的TCP/IP协议栈。TCP/IP有自己的数据包格式,那就是“帧”,而帧的构成比较复杂,不同的层之间一层一层的封装。所以我在看书的时候在想如何实现,而在看代码实现的时候却又忘了我需要帧的哪些flag来让后识别是ARP?还是TCP?还是UDP,主要是有点多啊,记不住。
所以,本文就把两者结合一下,个人比较喜欢这种学习方式。
本文把这几个都放一块了,所以像老太太的裹脚布,如果木有耐心,可以看我的博客,那里是分开的。学习TCP/IP没多久,并非科班出身,那么长的帖子肯定有错误,并且因为本文是菜鸟,所以站在菜鸟的视角学习(好吧,就是菜鸟的网络学习笔记),老鸟可能觉得有点啰嗦,请老鸟鞭笞。再次申明:本贴是科普贴。
源代码是野火的基础上改的,参考xukai871105的帖子写的。
强烈建议自己下代码看,我在代码里面都加了注释,然后可以参考着看,因为实在太臭太长了,我自己都受不鸟了。
理论上只要STM32都ok的,看看具体要改啥:
1. web_server.c
看下SPI的接口:
* 硬件连接: ------------------------------------
* |PB13 :ENC28J60-INT (没用到)|
* |PA6-SPI1-MISO:ENC28J60-SO |
* |PA7-SPI1-MOSI:ENC28J60-SI |
* |PA5-SPI1-SCK :ENC28J60-SCK |
* |PA4-SPI1-NSS :ENC28J60-CS |
* |PE1 :ENC28J60-RST (没用) |
* ------------------------------------
复制代码
IP,PORT,MAC
/* mac地址和ip地址在局域网内必须唯一,否则将与其他主机冲突,导致连接不成功 */
static unsigned char mymac[6] = {0x54,0x55,0x58,0x10,0x00,0x24};
static unsigned char myip[4] = {172,16,22,120};
/* ip地址(或者是DNS的名字,如果有DNS服务器的话),ip地址必须以"/"结尾 */
static char baseurl[]="http://172.16.22.120/";
/* tcp/www服务器监听端口号,范围为:1-254 */
static unsigned int mywwwport =80;
/* udp服务器 监听端口号,即本地(开发板)端口号 */
static unsigned int myudpport =1200;
static unsigned int pcudpport =4001;
/* 发送数据缓冲区 */
#define BUFFER_SIZE 1500
static unsigned char buf[BUFFER_SIZE+1];
/* 密码,不能大于9个字符(只有密码的前5位会被检测),(字符限定为:a-z,0-9) */
static char password[]="123456";
复制代码
2. enc28j60.h
ENC28J60片选信号线:
#define ENC28J60_CS GPIO_Pin_11 /* ENC28J60片选线 */
#define ENC28J60_CSL() GPIOA->BRR = ENC28J60_CS; /* 拉低片选 */
#define ENC28J60_CSH() GPIOA->BSRR = ENC28J60_CS; /* 拉高片选 */
复制代码
其他IO啥的自己对着看吧。
ARP部分
IP & ICMP部分
UDP部分
TCP部分
看看到右边的滚动条那么短~⊙﹏⊙b汗
===================================================ARP===========================================================
1. ARP的简介
Address Resolution Protocol-地址解析协议
ARP为IP地址到对应的硬件地址之间提供动态映射。从逻辑Internet地址到对应的物理硬件地址需要进行翻译。这就是ARP的功能。ARP的功能是在32 bit的IP地址和采用不同网络技术的硬件地址之间提供动态映射。转载
都会进行以下这些步骤。这些步骤的序号如图 4 - 2所示。
1) 应用程序FTP客户端调用函数gethostbyname(3)把主机名(bsdi)转换成32 bit的IP地址。这个函数在DNS(域名系统)中称作解析器,我们将在第1 4章对它进行介绍。这个转换过程或者使用DNS,或者在较小网络中使用一个静态的主机文件(/etc/hosts) 。
2) FTP客户端请求TCP用得到的IP地址建立连接。
3) TCP发送一个连接请求分段到远端的主机,即用上述 IP地址发送一份IP数据报(在第1 8章我们将讨论完成这个过程的细节) 。
4) 如果目的主机在本地网络上(如以太网、令牌环网或点对点链接的另一端) ,那么IP数据报可以直接送到目的主机上。如果目的主机在一个远程网络上,那么就通过 IP选路函数来确定位于本地网络上的下一站路由器地址,并让它转发 IP数据报。在这两种情况下,IP数据报都是被送到位于本地网络上的一台主机或路由器。
5) 假定是一个以太网,那么发送端主机必须把 32 bit的IP地址变换成48 bit的以太网地址。从逻辑Internet地址到对应的物理硬件地址需要进行翻译。这就是 ARP的功能。ARP本来是用于广播网络的,有许多主机或路由器连在同一个网络上。
6) ARP发送一份称作ARP请求的以太网数据帧给以太网上的每个主机。这个过程称作广播,如图 4 - 2中的虚线所示。 ARP请求数据帧中包含目的主机的IP地址(主机名为bsdi) ,其意思是“如果你是这个IP地址的拥有者,请回答你的硬件地址。 ”
7) 目的主机的ARP层收到这份广播报文后,识别出这是发送端在寻问它的 IP地址,于是发送一个ARP应答。这个ARP应答包含IP地址及对应的硬件地址。
8) 收到ARP应答后,使ARP进行请求—应答交换的IP数据报现在就可以传送了。
9) 发送IP数据报到目的主机。
3. ARP的分组格式
? 以太网报头中的前两个字段是以太网的源地址和目的地址。目的地址为全 1的特殊地址是广播地址。电缆上的所有以太网接口都要接收广播的数据帧。
? 两个字节长的以太网帧类型表示后面数据的类型。对于 A R P请求或应答来说,该字段的值为0 x 0 8 0 6。
? 硬件类型字段表示硬件地址的类型。它的值为 1即表示以太网地址。
? 协议类型字段表示要映射的协议地址类型。它的值为 0 x 0 8 0 0即表示I P地址。它的值与包含I P数据报的以太网数据帧中的类型字段的值相同,这是有意设计的(参见图 2 - 1)。
? 接下来的两个1字节的字段,硬件地址长度和协议地址长度分别指出硬件地址和协议地址的长度,以字节为单位。对于以太网上I P地址的ARP请求或应答来说,它们的值分别为6和4。
? 操作字段(op)指出四种操作类型,它们是 ARP请求(值为1) 、ARP应答(值为2) 、RARP请求(值为3)和R ARP应答(值为4) (我们在第5章讨论RARP) 。这个字段必需的,因为ARP请求和ARP应答的帧类型字段值是相同的。
? 接下来的四个字段是发送端的硬件地址(在本例中是以太网地址) 、发送端的协议地址(IP地址) 、目的端的硬件地址和目的端的协议地址。注意,这里有一些重复信息:在以太网的数据帧报头中和ARP请求数据帧中都有发送端的硬件地址。
对于一个ARP请求来说,除目的端硬件地址外的所有其他的字段都有填充值。当系统收到一份目的端为本机的 ARP请求报文后,它就把硬件地址填进去,然后用两个目的端地址分别替换两个发送端地址,并把操作字段置为 2,最后把它发送回去。
--------------------------------以上内容整理于《TCP/IP协议详解:卷1》----------------------------
理是那个那个理,但是过于抽象了,不过是基础,看完上面再看实现,那感觉很爽的~~~
------------------------------------------以下内容产生于代码及分析--------------------------------------
4. ARP的宏定义实现
以太网协议而非802.3协议,看ETH命名的头名字就晓得了,地址位置可以结合两个header算算就出来了
// ******* ARP *******
//ARP包长度
#define ETH_ARP_PACKET_LEN 28
//硬件地址长度值
#define ETHTYPE_ARP_L_V 0x06
//协议地址长度值
#define ETHTYPE_ARP_PROTOCOL_SIZE_V 0x04
//操作码位置 2字节
#define ETH_ARP_OPCODE_H_P 0x14
#define ETH_ARP_OPCODE_L_P 0x15
//ARP请求操作码值
#define ETH_ARP_OPCODE_REQUEST_V 0x0001
#define ETH_ARP_OPCODE_REQUEST_H_V 0x00
#define ETH_ARP_OPCODE_REQUEST_L_V 0x01
//ARP响应操作码值
#define ETH_ARP_OPCODE_REPLY_V 0x0002
#define ETH_ARP_OPCODE_REPLY_H_V 0x00
#define ETH_ARP_OPCODE_REPLY_L_V 0x02
// 发送者源硬件地址位置 6字节
#define ETH_ARP_SRC_MAC_P 0x16
//发送者源IP地址位置 4字节
#define ETH_ARP_SRC_IP_P 0x1c
//目标硬件地址位置 6字节
#define ETH_ARP_DST_MAC_P 0x20
//目标IP地址位置 4字节
#define ETH_ARP_DST_IP_P 0x26
复制代码
5. ARP的实现函数
以太网的header在ARP的header之前,很简单的,介绍先。
配置以太网的头,为14字节:6字节目的mac地址+6字节源mac地址+2字节协议类型,如图4-3
// 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++;
}
}
复制代码
在判断为arp请求之后,填充以太网的头之后响应arp请求
void make_arp_answer_from_request(unsigned char *buf)
{
unsigned char i = 0;
//配置以太网的头,为14字节:6字节目的mac地址+6字节源mac地址+2字节协议类型
make_eth(buf);
buf[ETH_ARP_OPCODE_H_P] = ETH_ARP_OPCODE_REPLY_H_V; //arp 响应
buf[ETH_ARP_OPCODE_L_P] = ETH_ARP_OPCODE_REPLY_L_V;
// 后面的ARP_DEBUG插入此处即可。
// fill the mac addresses:
while(i < sizeof(mac_addr))
{
buf[ETH_ARP_DST_MAC_P + i] = buf[ETH_ARP_SRC_MAC_P + i];
buf[ETH_ARP_SRC_MAC_P + i] = macaddr;
i++;
}
i = 0;
//fill the ipv4 addresses
while(i < sizeof(ipv4_addr))
{
buf[ETH_ARP_DST_IP_P + i] = buf[ETH_ARP_SRC_IP_P + i];
buf[ETH_ARP_SRC_IP_P + i] = ipaddr;
i++;
}
// eth+arp is 42 bytes:
enc28j60PacketSend(ETH_HEADER_LEN + ETH_ARP_PACKET_LEN, buf);
}
复制代码
当然,响应ARP请求的前提是你得确定有人向你发出ARP请求(下面那个函数就是了),并且这个人是谁,你是要知道的(通过发送者的IP和MAC地址),这个很容易,本协议是将地址放在几个全局变量里面的,大家就都知道了,虽然全局变量用起来很爽,但是对模块化以及后期维护带来的不便也是很大的。
检查是否为合法的eth,并且只接受发给本机的arp数据,此函数在上面那个函数之前被调用,再下面的代码就是演示的例程
//检查是否为合法的eth,并且只接受发给本机的arp数据
unsigned char eth_type_is_arp_and_my_ip(unsigned char *buf, unsigned int len)
{
unsigned char i = 0;
// 帧长度不得小于以太网的最小帧长度值,即46-除以太网头和CRC检测
if(len < MIN_FRAMELEN)
{
return(0);
}
if(buf[ETH_TYPE_H_P] != ETHTYPE_ARP_H_V || buf[ETH_TYPE_L_P] != ETHTYPE_ARP_L_V)
{
return(0);
}
//不是发给本机IP地址的不接收,那么如此说来,我在这里可以设定监听其他IP的信息!
while(i < sizeof(ipv4_addr))
{
if(buf[ETH_ARP_DST_IP_P + i] != ipaddr)
{
return(0);
}
i++;
}
return(1);
}
复制代码
以上函数在别人向你发送任何请求之前都将被调用一次(原因是本协议只是实现了对IP和ARP的响应),所以需要在一个while死循环或者RTOS的一个thread/task/process里面。如下所示:
上层调用示例代码
/*
此部分为一部分代码
*/
/*do something initial */
while(1)
{
// get the next new packet:
plen = enc28j60PacketReceive(BUFFER_SIZE, buf);
// plen will be unequal to zero if there is a valid packet (without crc error)
if(plen==0)
{
continue;
}
// check if ip packets are for us:
if(eth_type_is_ip_and_my_ip(buf,plen)==0)
{
//丢弃本次获取的数据,再接下一个
continue;
}
// arp is broadcast if unknown but a host may also
// verify the mac address by sending it to
// a unicast address.
//这里就是ARP的响应了,如果我们在这里加入串口调试,
//就可以将谁在向我发送arp请求的数据打印到串口
//当然加一个选择宏,放在函数里面更方便一点
if(eth_type_is_arp_and_my_ip(buf,plen))
{
make_arp_answer_from_request(buf);
continue;
}
/*do other things */
}
复制代码
6. ARP实验调式
嗯,接者来看看在make_arp_answer_from_request函数里面加入串口调试信息来输出arp请求者的ip和mac地址。
加入到 make_arp_answer_from_request中的调试代码,用于输出ARP请求者的信息
#ifdef ARP_DEBUG
printf("ARP请求者IP地址 : \r\n");
while(i < sizeof(ipv4_addr))
{
printf("%d", buf[ETH_ARP_SRC_IP_P + i]);
if(i != sizeof(ipv4_addr) - 1) // 加入判断只是为了输出的形式好看点
{
printf(".");
}
else
{
printf("\r\n");
}
i++;
}
i = 0;
printf("ARP请求者MAC地址 :\r\n");
while(i < sizeof(mac_addr))
{
printf("%x", buf[ETH_ARP_SRC_MAC_P + i]);
if(i != sizeof(mac_addr) - 1)
{
printf(":");
}
else
{
printf("\r\n");
}
i++;
}
i = 0;
#endif
复制代码
PC端:测试arp请求需要先执行“arp -d”清楚本地的arp-ip对应列表,这样PC机才会发送ARP请求
ps:enj28j60的MAC地址是软件设定的,所以就不打码了。
[size=13.333333969116211px]但是在实验过程中发现如下情况是不会调用make_arp_answer_from_request函数的:PC机本地有arp-ip列表,就是ping过一次以后ping第二次的就不会去响应了,这个本来就是要如此的,希望结果就是这样的,你要问我是谁(MAC地址是多少),然后我告诉你一次,你记住就ok了么。但是问题出在我们的协议栈实现代码里面,本协议栈在通过eth_type_is_arp_and_my_ip判断了是发给本机的数据包之后,就会调用arp响应的,没有一个对以太网header的类型的判断啊。怎么会自己变的智能了呢?- if(eth_type_is_arp_and_my_ip(buf, plen))
- {
- make_arp_answer_from_request(buf);
- continue;
- }
[color=rgb(51, 102, 153) !important]复制代码
那么问题出在哪里呢?
1. 难道是进入make_arp_answer_from_request 函数,但是没有发送出去么?当然也不会在enc28j60PacketSend(ETH_HEADER_LEN + ETH_ARP_PACKET_LEN, buf); 里面,因为没有判别信息传递进去。(当然发不发,只要如函数之后就会有打印信息输出到串口)
2. 那么就是eth_type_is_arp_and_my_ip 函数返回为0了;
但是ping命令还是可以得到响应的,哦,对了,还有ICMP(我本来就用的ping,都忘了,放在IP层),那么就好解释了,第一次是由ARP+ICMP响应,第二次及之后的只有ICMP响应。所以现象还是符合原理解释的。
但是,我用网页去刷新和点亮LED等,走的肯定是IP包,ARP还是不响应。
回去看代码,当时对自己就无语了
if(buf[ETH_TYPE_H_P] != ETHTYPE_ARP_H_V || buf[ETH_TYPE_L_P] != ETHTYPE_ARP_L_V)
是ARP啊,我以为是对IP和ARP都会响应,而且人家的函数名是eth_type_is_arp_and_my_ip ,我自己把它想成eth_type_is_arp_and_ip ,╮(╯▽╰)╭
代码就在那里,自己SB了~~~
--------------------------------------------以上是我边想边实验边写的过程-----------------------------------------------
硬件环境:STM32+ENC28J60
软件环境:MDK4.70a
TCP/IP协议栈:开发者的网站已经关闭了,也没有命名~~~
给出作者信息:
/*********************************************
* modified: 2007-08-08
* Author : awake
* Copyright: GPL V2
* http://www.icdev.com.cn/?2213/
* Host chip: ADUC7026
**********************************************/
===================================================ARP END========================================================
===================================================IP & ICMP=======================================================
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宏定义实现- // ******* 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
[color=rgb(51, 102, 153) !important]复制代码
4. IP函数实现
以太网的header在IP的header之前,很简单的,介绍先。
配置以太网的头,为14字节:6字节目的mac地址+6字节源mac地址+2字节协议类型- // 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++;
- }
- }
[color=rgb(51, 102, 153) !important]复制代码
展开之后如下所示,其在以太网帧中的位置与之前的宏定义是一一对应的。
IP与ARP一样,需要判定是不是发给本机的(eth_type_is_ip_and_my_ip函数),还有与填充make_eth 函数一样需要填充函数(make_ip函数),此外还有填充其他杂七杂八和16位首部校验和函数(fill_ip_hdr_checksum函数)- //判定过程与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)
- {
- 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++;
- }
- fill_ip_hdr_checksum(buf);
- }
[color=rgb(51, 102, 153) !important]复制代码
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
没错,不完全验证两种方法的结果是一致的,不是科班出身,感觉上有啥子理论来说明某种顺序的调换在对二进制运算结果方面的影响是无关的。- 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);
- }
[color=rgb(51, 102, 153) !important]复制代码
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地址部分。- // ******* 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);
- }
[color=rgb(51, 102, 153) !important]复制代码
8 ICMP实验调试- 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);
- }
[color=rgb(51, 102, 153) !important]复制代码
在程序的无线循环中,需要层层进行查询。其实就是各种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。
如果按照注释里面改正之后~~~
|
|
|