本文可以说是在我之前写的《单片机驱动DM9000网卡芯片(详细调试过程)》基础上增加了一些协议代码来实现具体UDP通信传输。在这里我重新强调一下,上篇文章是在介绍如何调硬件,目的是为了让应用程序可以使用这个芯片。而具体的使用就是使用三个函数:初始化、数据包发送和数据包接收。数据包接收是否基于中断还需要用户根据需要自行设置。总之,我们可以通过上篇文章了解到,对硬件的调试可以得到这三个有用的函数。
不同网卡芯片的驱动可能略有不同。这里不一一例举,所以首先需要说明的是,本文所讲的内容主要是如何用C代码来实现协议,并利用数据包发送、接收函数来实现通讯,基本是与硬件无关的。除中断外,本文唯一与硬件相关的地方就是大端或小端格式,这也在之前的文章中有提到过,在本文涉及到的地方会再次说明。也就是说使用不同网卡芯片都可以应用本文所写的代码。其次,本文所写出的协议部分是已经过简化的,代码较少,不需要操作系统支持。但仅能实现数据的收发,而且没有验证可靠性(需要时可自行验证)。适合用在资源有限的单片机系统中,或者需要用网络代替RS232通信的情况,当然也可以在操作系统中使用。可根据情况来选择或增减。
顺便提一下资源要求:ram最好大于2KB,实在不行也得要1KB(需要一定技巧,传输的数据内容很少,不需要全部读出数据包的情况);flash或rom4KB以上,基本的单片机都能达到;可用IO怎么也得有12个,控制个一般的芯片也需要这么多的。
在进行正文之前,我再啰嗦几句,本文是讲协议的实现。这里的协议部分可以从《TCP/IP协议 第一卷 —— 协议》这本书中看到最详细最权威的讲解,如果有兴趣研究协议的话可以参考这本书(网上可以找到电子版的)。下面进入主题。
1、UDP通讯的实现过程简述
涉及到协议部分,很多人会感觉摸不清头绪,不知如何下手。所以看一看上面说的那本书还是很有帮助的。当然看了以下部分,你也会对协议有些了解的。
(1)初始化网卡芯片和其他外设(在网卡驱动部分已经做好了,这里重新说了一遍);
(2)arp通讯获得目标机地址信息;
(3)udp通讯收发数据(利用IP协议作媒介)。
看到这会不会有些失望呢,可实际上udp通讯就是这么简单的。
UDP是User Datagram Protocol的简称,中文名是用户数据包协议,是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务(这句话是在网上抄的)。
说的再简单点,udp通讯与RS232一样,只管将数据发送出去而不管对方是否正确的接收到了。在一些简单应用中,我们似乎也不大关心数据是否被对方正确的接收到了,因为传输过程中数据包损坏的情况也不多,肯定能被正确的接收到。或者我们有其他的办法去验证。这和TCP协议不一样,TCP协议是可靠的链接,发送一次数据需要三次握手来反复确认数据被正确无误的接收到了,否则会重新发送一遍,实现起来比较繁琐。有点跑题,不过看到这里应该可以明白udp协议是一种很简单的网络通讯协议。
简单解释一下上面三个过程。初始化不用解释了。ARP通讯是整个网络传输的开始,而且只需要运行一次。在《单片机驱动DM9000网卡芯片(详细调试过程)【下】》最后的部分已经讲清楚了。主要是解释第三条,udp通讯收发数据。
OSI参考模型中arp协议属于链路层(最底层),ip协议比arp协议高一层属于网络层(这一层还包括icmp和igmp协议),在往上一层是运输层,包括tcp协议和udp协议。但是按我的理解,从数据包格式的角度看,我把arp协议与ip协议放在同一级别,我们接收到的数据包中的前几十个字节用来判断是arp协议还是ip协议,也就是说这两个协议是互补相容的(下面程序中会做个过滤,数据包只接收arp或ip协议,最后处理的数据包中只能是arp协议或者是ip协议)。这种互补相容的协议同样也适用于tcp协议和udp协议上。既然有互不相容的协议,那么也就有相容的协议了,这种相容的协议就是指一个协议必须依赖于另一种协议才能实现,udp协议就是这样。我们可以这样理解,ip协议就像一件外套,udp协议好比一件衬衫,而真正的数据可以看做是穿衣服的人。穿衣服的人先穿上衬衫再穿外套,这两个协议之间的关系就是这样:udp协议将数据包起来,ip协议又将udp协议连同其中的数据一起包起来。也就是说,实际的数据经过udp协议的包装,在经过ip协议的包装之后才能发送出去。虽然看起来有些繁琐,但实际计算机端就是这样识别数据的。所谓的包装就是在被包装数据前加上一小段首部数据,一般几十个字节左右。
2、ARP协议的实现
这部分内容在《单片机驱动DM9000网卡芯片(详细调试过程)【下】》的后半部分已经讲过,这里为了完整性再重复一次。
在写所有协议之前,有些全局变量需要事先设定一下,如ip地址、mac地址等信息。另外,统一规定一下我们的单片机系统为“基板”,计算机端为“上位机”,以下叫起来方便。再规定一下:char型是8位,short型是16位,long型是32位。OK!
/********************************************/
unsigned char my_macaddr[6] = { 0x00, 0x0a, 0x00, 0x01, 0x02, 0x03 };//基板上mac地址,这里随便写6个字节。
unsigned char my_ipaddr[4] = { 192, 168, 1, 207 };//基板上ip地址,根据网关写入合适值。
unsigned char server_macaddr[6] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };//上位机mac地址,通过arp协议获取后改变。
unsigned char server_ipaddr[4] = {192, 168, 1, 122};//上位机ip地址,看自己的电脑,这个应该会,不会就问问旁边的人吧。
unsigned char transmit_buffer[2048] = { 0 };//发送数据包缓存区
unsigned char receive_buffer[2048] = { 0 };//接收数据包缓存区
/*如果为了节省ram,这两个区可以共用一个(因为发送和接收数据可以分开进行,而且一般网卡芯片内部都会有自己独立的接收和发送缓存的),容量也可以改小,写成“unsigned char data_buffer[1000] = { 0 };”我的ram比较多,所以用了两个区,而且占用很大空间。*/
unsigned long send_packet_length = 0;//发送数据包长度
unsigned long receive_packet_length = 0;//接收数据包长度
/*如果上面的数据缓存区共用一个的话,这两个存储数据包长度的变量也可以共用一个,写成“packet_length”*/
/********************************************/
这部分是全局变量,需要在所有函数之前定义。
在写arp协议之前先定义两个结构体:
//以太网地址首部,我们用的网络就是以太网
struct eth_hdr {
unsigned char d_mac[6]; //目标mac地址
unsigned char s_mac[6]; //源mac地址
unsigned short type; //协议类型,判断arp协议还是ip协议
};
//ARP首部=以太网首部+arp协议首部
struct arp_hdr {
struct eth_hdr ethhdr; //以太网首部结构体,如上
unsigned short hwtype; //硬件地址,“1”表示mac地址
unsigned short protocol; //协议地址,“0x0800”表示ip地址
unsigned char hwlen; //硬件地址长度,mac地址为6
unsigned char protolen; //协议地址,ip地址为4
unsigned short opcode; //操作码“1”表示arp请求,“2”表示arp应答
unsigned char smac[6]; //源mac地址
unsigned char sipaddr[4]; //源ip地址
unsigned char dmac[6]; //目标mac地址
unsigned char dipaddr[4]; //目标ip地址
};
#define ETH_TRBUF ((struct eth_hdr *)&transmit_buffer[0])
#define ETH_REBUF ((struct eth_hdr *)&receive_buffer[0])
/*宏定义均是为了书写方便,以以太网首部的方式指向数据包缓存
#define ARP_TRBUF ((struct arp_hdr *)&transmit_buffer[0])
#define ARP_REBUF ((struct arp_hdr *)&receive_buffer[0])
接下来是arp协议的代码,具体含义在前一篇文章中有,这里对arp实现原理不做详细介绍了。另外,本文认为数据包发送和接收函数已经像前篇文章中那样写好了:
void sendpacket(unsigned char* datas, unsigned long length);//发送数据包
//要发送的数据包存放在“transmit_buffer[]”中。
unsigned short receivepacket(unsigned char* datas);//接收数据包,返回协议类型(arp或ip)
//接收到的数据包存放在“receive_buffer[]”中。可以与发送数据包公用。
//arp请求
void arp_request(void)
{
memcpy(ARP_TRBUF->ethhdr.d_mac, server_macaddr, 6);
memcpy(ARP_TRBUF->ethhdr.s_mac, my_macaddr, 6);
/*用到上面的函数,在文件中要写上“#include ”*/
ARP_TRBUF->ethhdr.type = 0x0806;//arp协议,此处按大端格式
/*注意!这里涉及到大端格式和小端格式问题,小端格式需要写成0x0608,也就是16位的高低两个字节转换一下,这个很重要,一定要弄清楚自己处理器的编译器的存储格式。我记得51、AVR等单片机的编译器是小端格式,所以要高低字节交换。我这里和下面的程序中都按大端格式写*/
ARP_TRBUF->hwtype = 1;//此处按大端格式
ARP_TRBUF->protocol = 0x0800;//ip协议,此处按大端格式
ARP_TRBUF->hwlen = 6;
ARP_TRBUF->protolen = 4;
ARP_TRBUF->opcode = 1;//arp请求,此处按大端格式。。。
/*以下凡是16位宽的数据我都按大端格式处理不在重复啦*/
memcpy(ARP_TRBUF->smac, my_macaddr, 6);
memcpy(ARP_TRBUF->sipaddr, my_ipaddr, 4);
memcpy(ARP_TRBUF->dipaddr, server_ipaddr, 4);
send_packet_length = 42;//14字节以太网首部+28字节arp首部=42字节
sendpacket(tr_data, send_packet_length);
}
//arp应答
void arp_reply(void)
{
//memcpy(ARP_TRBUF->ethhdr.d_mac, server_macaddr, 6);
//memcpy(ARP_TRBUF->ethhdr.s_mac, my_macaddr, 6);
ARP_TRBUF->ethhdr.type = 0x0806;
ARP_TRBUF->hwtype = 1;
ARP_TRBUF->protocol = 0x0800;
ARP_TRBUF->hwlen = 6;
ARP_TRBUF->protolen = 4;
ARP_TRBUF->opcode = 2;
//memcpy(ARP_TRBUF->smac, my_macaddr, 6);
memcpy(ARP_TRBUF->sipaddr, my_ipaddr, 4);
memcpy(ARP_TRBUF->dipaddr, server_ipaddr, 4);
send_packet_length = 42;//14+28=42
sendpacket(transmit_buffer, send_packet_length);
}
//arp处理
unsigned long arp_process(unsigned long len)
{
if(len < 42)return ARP_BAD_PACKET;//ARP_BAD_PACKET 这个宏自己随便定义个数,表示数据包是损坏的
if(ARP_REBUF->opcode == 1) //判断是否是arp请求包,记得这里也是大端格式,小端格式就变成0x0100了
{
if(ARP_REBUF->dipaddr[0] == my_ipaddr[0] &&//判断是否是基板的mac地址
ARP_REBUF->dipaddr[1] == my_ipaddr[1] &&
ARP_REBUF->dipaddr[2] == my_ipaddr[2] &&
ARP_REBUF->dipaddr[3] == my_ipaddr[3])
{
memcpy(ARP_TRBUF->ethhdr.d_mac, ARP_REBUF->ethhdr.s_mac, 6);
memcpy(ARP_TRBUF->ethhdr.s_mac, ARP_REBUF->ethhdr.d_mac, 6);
memcpy(ARP_TRBUF->dmac, ARP_REBUF->ethhdr.s_mac, 6);
memcpy(ARP_TRBUF->smac, ARP_REBUF->ethhdr.d_mac, 6);
arp_reply();//发送arp应答
return ARP_REPLYTHEREQUEST;/*ARP_REPLYTHEREQUEST 这个宏也是自己随便定义的数,表示接收到的是arp请求数据包包*/
}
else
{
return ARP_NOTMYIP;//ARP_NOTMYIP 自定义宏,表示接收到的数据包不是基板的,放弃掉。
}
}
else if(ARP_REBUF->opcode == 2) //判断是arp应答包
{
if(ARP_REBUF->dipaddr[0] == my_ipaddr[0] &&
ARP_REBUF->dipaddr[1] == my_ipaddr[1] &&
ARP_REBUF->dipaddr[2] == my_ipaddr[2] &&
ARP_REBUF->dipaddr[3] == my_ipaddr[3])
{
memcpy(server_macaddr, ARP_REBUF->smac, 6);
return ARP_SAVETHEREPLY;//自定义宏,表示接收到arp应答,并且将上位机mac地址存储好了
}
else
{
return ARP_NOTMYIP;
}
}
else
{
return ARP_UNKNOWN;//自定义宏,表示数据包出错
}
}
arp协议到这里就完成了。包含三个函数:
void arp_request(void);
void arp_reply(void);
unsigned long arp_process(unsigned long len);
arp协议具体原理可参考《单片机驱动DM9000网卡芯片(详细调试过程)【下】》,也可参考前面介绍的书。
3、IP协议的实现
ip协议用途很重要,但对于本文仅使用udp协议来说,它只是用于装载udp协议的。
首先,定义ip协议的首部结构体和一些必要的全局变量。
//ip首部=以太网首部+ip协议首部
struct ip_hdr {
struct eth_hdr ethhdr;//以太网首部
unsigned char vhl, //ip协议的版本和首部长度。固定写法0x45,表示IP4,首部长5*32bit(20字节)
tos;//TOS(0)(可不关心)
unsigned short len,//整个ip数据包长度,包括ip首部
ipid,//ip标识,也可以看做是ip计数器
ipoffset;//掩码和偏移(可不关心)
unsigned char ttl,//ip包的生命周期(写成固定值,可不关心)
proto;//ip包装载的协议类型, "1"表示ICMP, "2"表示IGMP, "6"表示TCP, "17"表示UDP
unsigned short ipchksum; //首部校验和
unsigned char srcipaddr[4], //源ip地址
destipaddr[4]; //目标ip地址
};
从这个首部看,ip协议首部结构体包含34个字节,其中ip首部有20个字节。根据首部可以确定一些信息,比如ip包中转载的是什么协议、ip地址、ip长度等。ip协议与arp协议的一个不同点是,ip协议中包含了一个首部校验和。这样的校验和在其他协议中也都有,而且计算方法也相同。校验和的作用是判断该协议被校验和检验过的区域(ip协议只检验首部的20个字节)是否正确,进而判断该数据包是否损坏。关于这个校验和的操作,有些芯片内部可以自动计算出来,有些却不能。为了统一,稍后会介绍校验和的计算方法。
本文所涉及的ip协议除了装载其他协议外,没有其他用途,因此也并无很复杂的原理。接下来继续介绍涉及到ip协议的函数。这里再加一些宏和变量:
#define IP_TRBUF ((struct ip_hdr *)&transmit_buffer[0])
#define IP_REBUF ((struct ip_hdr *)&receive_buffer[0])
//宏定义:为书写方便,以ip协议首部的方式指向发送和接收的数据
unsigned char m_ipid = 0;
/*这是一个全局变量,可以看做是个发送ip协议包的计数器,专门用来填充ip协议首部结构体中的ipid成员*/
接下来会完成两个函数,ip协议处理函数和ip协议数据包发送函数。前者用来完成ip协议的数据包分析和处理,后者用来发送一个ip协议数据包。具体函数如下:
//ip协议处理函数
unsigned long ip_process(void)
{
//判断是否是基板的ip地址
if( ( IP_REBUF->destipaddr[0] != my_ipaddr[0] )||
( IP_REBUF->destipaddr[1] != my_ipaddr[1] )||
( IP_REBUF->destipaddr[2] != my_ipaddr[2] )||
( IP_REBUF->destipaddr[3] != my_ipaddr[3] ) )
{
return IP_NOTMYIP;//自定义宏,表示不是基板的ip包,放弃该包。
}
if(IP_REBUF->vhl != 0x45)
{
return IP_BED_PACKET;//自定义宏,表示此ip包已经损坏
}
//我们仅做了以上两个简单判断,其实可以做更详细判断,但这些已经足够了
if(ip_chksum(&(IP_REBUF->vhl)) != 0)//校验和函数“ip_chksum()”在后面介绍,这里暂时标记一下
{
return IP_BED_PACKET;
}
/*这里将接收到的完整数据包长度减掉20字节ip协议首部和14字节以太网首部,剩余字节数作为ip协议中数据内容的字节数*/
receive_packet_length = IP_REBUF->len - 34;
return IP_REBUF->proto;//返回ip包中包含的协议类型,以供后面程序继续处理
}
//发送ip数据包
void ip_send(unsigned long len)//参数为ip协议包中数据部分的字节数(这个数据部分包括udp协议)
{
//以太网首部填充
memcpy(IP_TRBUF->ethhdr.d_mac, server_macaddr, 6);
memcpy(IP_TRBUF->ethhdr.s_mac, my_macaddr, 6);
IP_TRBUF->ethhdr.type = 0x0800;//ip协议代码,大端格式
// IP header
len += 34;//更新数据包长度,增加ip首部和以太网首部共34字节
IP_TRBUF->vhl = 0x45;//固定写法,ip版本和首部长度,我们的网络貌似还没有用到IP6
IP_TRBUF->tos = 0x00;//可不关心,写0就行
IP_TRBUF->len = (unsigned short)(len - 14);//ip包总长度,包括ip首部和数据(不包括以太网首部)
IP_TRBUF->ipid = m_ipid;//ip包标识,也就是计数器,每次发完加1就行。
m_ipid++;
IP_TRBUF->ipoffset = 0x00;//可不关心
IP_TRBUF->ttl = 255;//可不关心,表示此包可以经过路由器的个数,填为最大值
IP_TRBUF->proto = 17;//表示ip包中装载着udp协议(大端格式),6为tcp协议
memcpy(IP_TRBUF->srcipaddr, my_ipaddr, 4);//基板ip地址
memcpy(IP_TRBUF->destipaddr, server_ipaddr, 4);//上位机ip地址
IP_TRBUF->ipchksum = 0;//校验和字段在计算结果之前用0填充,并参与计算
IP_TRBUF->ipchksum = ip_chksum(&(IP_TRBUF->vhl));//校验和的填充和检验的计算方法相同,用同一函数
sendpacket(transmit_buffer, len);//发送这个数据包
}
/*******************************************
注释:值得注意的是,在这个函数中,要发送的数据包长度我用了传递参数的办法。而之前我又用了个全局变量来存放要发送的数据包长度,arp协议的实现,我使用了全局变量来传递要发送的数据包长度。其实这两个办法都可行,由于这两个函数我不是在同一个时间写的,实现方法上有些不同,现在看来是有点冗余,大家可以根据自己的书写习惯选择其中的一种方法,我就不在这里修改程序了。
*******************************************/
由于这里ip协议仅仅用于装载udp协议,所以暂时没有办法发包来验证。
4、校验和
上节ip协议中提到过校验和的概念,并且用到了一个函数“ip_chksum()”,下面简单介绍一下校验和及其实现方法。
校验和机制是一种检验数据包完整性的方法,也就是查看一下接收到的包是否坏损(数据出错)。其原理也比较简单,在发送数据包时,将校验和计算的结果放上在指定位置上。数据包发送到接收端后,接收端在发送端检验和的基础上,再进行一次检验和的计算,如果结果全“0”,则表示正确,否则表示数据包已经损坏(一般损坏的数据包就直接丢弃了)。
校验和采用一种很巧妙且简单的数学方法进行计算,使发送端和接收端校验和的计算方法相同,而且根据计算结果可以正确判断数据包中被校验的数据区域是否有损坏(但不能确定是哪个数据是损坏的)。计算方法如下:校验和占用16位(2字节)。首先,以0x0000为起始将待被验证的数据以16位宽依次取出与之相加,如果溢出就将结果再加1。依次累加下去直到完成所有数据的检验。如果数据是奇数个字节(即最后一位数据不是16位宽,只有8位)那么将此数据扩展到16位宽(高8位填充0)后再相加。这样就得到了一个基本的校验和结果。最后,将此结果取反后便得到最终可用的校验和了。说了这么多,用一句最简单的话概括:数据求和后取反(或数据取反后求和)。
下面看一下具体函数。校验和通过两部分函数组成,求和函数和取反函数,求和函数做通用校验用,取反函数被各个协议调用:
// 校验和计算函数
unsigned short chksum(unsigned short *sdata, unsigned long len)
//参数为需要校验的数据部分地址指针(16位宽对齐),数据长度(以字节为单位)
{
unsigned short sum = 0;//定义一个16为宽以0x0000为开始的基数
for(sum = 0; len > 1; len -= 2)//用于求和累加的循环
{
sum += *sdata;
if( sum < *sdata )//判断溢出
sum++;
sdata++;
}
//数据长度是奇数个字节的情况
/*if(len == 1)//小端格式
{
sum += (*sdata & 0xff);
if(sum < (*sdata & 0xff))
sum++;
}*/
if(len == 1)//大端格式,根据自己处理器情况选择
{
sum += (*sdata & 0xff00);
if(sum < (*sdata & 0xff00))
sum++;
}
return sum;//返回16位宽求和累加结果
}
// IP协议首部校验和
unsigned short ip_chksum(void* sdata)
{
return ~chksum((unsigned short*)sdata, 20);
}// UDP首部校验和
unsigned short udp_chksum(void* sdata, unsigned long len)
{
unsigned short sum = 0;
unsigned short tem = 0;
// UDP首部和数据部分校验和计算
sum = chksum((unsigned short*)sdata, len);
// UDP伪首部校验和计算
// 小端格式
/*tem = (((unsigned short)my_ipaddr[1] << 8) & 0xff00) | ((unsigned short)my_ipaddr[0] & 0xff);
sum += tem;
if(sum < tem)sum++;
tem = (((unsigned short)my_ipaddr[3] << 8) & 0xff00) | ((unsigned short)my_ipaddr[2] & 0xff);
sum += tem;
if(sum < tem)sum++;
tem = (((unsigned short)server_ipaddr[1] << 8) & 0xff00) | ((unsigned short)server_ipaddr[0] & 0xff);
sum += tem;
if(sum < tem)sum++;
tem = (((unsigned short)server_ipaddr[3] << 8) & 0xff00) | ((unsigned short)server_ipaddr[2] & 0xff);
sum += tem;
if(sum < tem)sum++;
tem = ((unsigned short)IP_UDP << 8) & 0xff00;
sum += tem;
if(sum < tem)sum++;
tem = (((unsigned short)len & 0xff) << 8) | (((unsigned short)len & 0xff00) >> 8);
sum += tem;
if(sum < tem)sum++;*/
//大端格式,根据自己的处理器选择
tem = (((unsigned short)my_ipaddr[0] << 8) & 0xff00) | ((unsigned short)my_ipaddr[1] & 0xff);
sum += tem;
if(sum < tem)sum++;
tem = (((unsigned short)my_ipaddr[2] << 8) & 0xff00) | ((unsigned short)my_ipaddr[3] & 0xff);
sum += tem;
if(sum < tem)sum++;
tem = (((unsigned short)server_ipaddr[0] << 8) & 0xff00) | ((unsigned short)server_ipaddr[1] & 0xff);
sum += tem;
if(sum < tem)sum++;
tem = (((unsigned short)server_ipaddr[2] << 8) & 0xff00) | ((unsigned short)server_ipaddr[3] & 0xff);
sum += tem;
if(sum < tem)sum++;
tem = (unsigned short)IP_UDP & 0xff;
sum += tem;
if(sum < tem)sum++;
tem = (unsigned short)len;
sum += tem;
if(sum < tem)sum++;
return ~sum;
}
udp协议的校验和计算似乎比较复杂。首先,udp协议校验和计算覆盖udp首部和udp数据(tcp协议也一样),这与ip协议的校验和不同,但计算方法相同。其次,udp协议(和tcp协议)的校验和还要包含一个12字节的“伪首部”,伪首部包括:32位源ip地址、32位目的ip地址、8位0、8位协议(udp为17)和16位的udp长度(首部加数据的字节数)。这个伪首部在数据包内并不是真实存在的,仅仅是在计算校验和时参与计算而已。关于udp的伪首部及其校验和的实现就简单介绍到这,这一部分即使没有看懂也没关系,因为udp校验和是可选的,在80年代以后,计算机厂商在默认条件下关闭了udp校验和的功能,以提高udp协议的速度。对于我们来说,更可以避免这种繁琐的校验计算来提高发送和接收数据包的速度。当然,为了保证数据的准确性,你也可以按上面方法计算。需要注意的是,tcp协议的校验和是必选的,不可以省略。
这样,我们得到了以上三个函数:
unsigned short chksum(unsigned short *sdata, unsigned long len);
unsigned short ip_chksum(void* sdata);
unsigned short udp_chksum(void* sdata, unsigned long len);
其中后两个函数是我们需要在各自协议中用到的,在ip协议部分用到了ip_chksum函数。这两个函数是这样使用的:发送数据包时,首先将需要校验的部分填写好,再将校验和的位置填充0,调用该协议的校验和函数并将返回结果填充在校验和的位置;接收数据包时,直接调用函数校验该协议,如果函数返回“0x0000”表示校验成功,否则表示数据损坏。
值得注意的是,我们在发送和接收数据包的时候都要进行校验和的计算,但用到的函数都是同一个。我想看到这里,大家脑袋里会有许多问号,用这个函数计算的校验和到底是个什么结果呢?为什么发送和接收都用同一个函数就能完成呢?
我先来解释一下用这个函数所产生的结果及其原理。发送数据包时,将校验和位置0后在利用函数校验,会得到一个16位宽的校验和数据(数值本身看不出有任何意义)。当接收端再用相同的算法重新计算一遍这些相同的数据后,神奇的是,如果数据完好结果会是全0,否则得不到全0结果。至于为什么,我觉得从数学角度看,通过一堆推导可以验证这一结论的,我们就当这是定理好了。如果好奇,你可以自己找一些数来手动算一下。
上面所讲的校验和计算函数“chksum()”是个通用函数,其它协议也可以根据自己的规则来调用它。这里为了简单,我只给出udp协议的校验和函数。
5、UDP协议的实现
好像说了很多铺垫才刚刚进入主题。如果大家觉得arp协议有点复杂,ip协议也不简单,那么你会发现udp协议要简单得多!
与之前一样,为udp协议定义一些全局变量和结构体等。
// UDP协议首部
struct udp_hdr {
struct ip_hdr iphdr; // IP协议首部结构体
unsigned short sport, // UDP源端口
dport, // UDP目的端口
len, // UDP包中数据部分长度(字节数)
udpchksum; // UDP校验和
};
udp协议首部长度仅有8个字节,分4个部分,且均以16位宽对齐。其中,udp协议涉及到了端口的概念,我简单解释一下端口的作用。我们知道通过ip地址可以确定是哪个网卡在工作(或者说是哪个计算机在工作),而端口表示在某个ip地址里哪一个软件在工作,也就是说端口是软件的地址。TCP/IP协议中有个“插口”的概念,这个插口就是指由“ip地址:端口”所组成的联合地址,它可以确定所接收到的数据包是哪个计算机里的哪个软件发过来的,也就是说“插口”这种地址可以精确到正在运行的软件。端口是由16位宽的数值组成的,在网络中有规定,哪个端口号代表着什么意思。当然,大部分的端口号是公开的,可以在自己的软件中使用。其实,在非操作系统环境下,这个端口的作用并不大,我们一般不大关心到底是那个软件在给我们发数据,因为正常情况下都是唯一的一个软件在给我们发数据。如果一台计算机里有多个软件可以给我们发数据,这个端口就有意义了。这里我们用下面的全局变量来表示端口号,初始化为0,方便更改。
unsigned short udp_sport = 0;
unsigned short udp_dport = 0;
下面定义的宏可以方便在数据缓存中使用udp首部。
#define UDP_TRBUF ((struct udp_hdr *)&transmit_buffer[0])
#define UDP_REBUF ((struct udp_hdr *)&receive_buffer[0])
完成以上定义和声明,我们开始写udp协议代码,并验证。
// 发送udp数据包
void udp_send(unsigned long len)//参数是udp包中数据部分的长度(字节数)
{
UDP_TRBUF->sport = udp_sport;
UDP_TRBUF->dport = udp_dport;//源地址和目的地址均设为0,这里不关心这个值
UDP_TRBUF->len = len;
UDP_TRBUF->udpchksum = 0;//检验和,上面讲过喽
UDP_TRBUF->udpchksum = udp_chksum(&(UDP_TRBUF->sport), len + 8);//这句可选
ip_send(len + 8);//调用ip发送函数,将数据包发出去
}
发送udp数据包代码很简单吧,只要填充好udp首部即可。当然,ip包发送函数也要正确。下面写个简单的main函数验证一下。
关于以下调试过程的注释:调试程序我习惯于用上串口,可以很方便的在上位机上显示调试信息,所我在main函数里加上个几个串口相关函数。这些函数仅仅用于调试,而且与处理器相关,用到时我只给出函数用途而不在写代码了。另外,上面提到的对芯片初始化等操作,在main函数里省略了,这里只挑选最关键的代码来验证。需要定义的宏和全局变量,在上面已经提到过,main函数里也不在重复。
#define DATA_TRBUF (unsigned char *)&transmit_buffer[28]
void main(void)
{
unsigned short tem;//定义几个临时变量
/***********
一些初始化等操作在此处进行,这里省略
************/
//首先通过arp协议寻找上位机,这一步在以前的文章中介绍过,这里再简单写一下
while(1)
{
arp_request();//发出arp请求
while(/*等待中断标志或某种标识来判断是否有数据包被接收到,没有数据包时为真,有数据包收到时为假*/);
//上面的语句会根据不同的网卡芯片和不同的处理器,判断方式会有所不同。
tem = receivepacket();
if(tem == 0x0806)//判断是否是arp协议
{
if(ARP_SAVETHEREPLY == arp_process(receive_packet_length))break;
//判断是否是arp应答,如果是则存储上位机mac地址后退出循环
}
for(a = 5000000; a > 0; a--);//适当的延时1秒左右
}
send_packet_length = 4;//填写数据长度(字节数)
DATA_TRBUF[0] = 0xaa;//将数据填充到发送数据缓存区
DATA_TRBUF[1] = 0xbb;
DATA_TRBUF[2] = 0xcc;
DATA_TRBUF[3] = 0xdd;//填充四个数据”0xaa 0xbb 0xcc 0xdd”
udp_send(send_packet_length);//发送udp数据包
while(1);//程序停止,查看结果
}
上面main函数利用udp发送了四个数。实际上,main函数的作用就是将要发送的数据放到发送数据的缓存中并且指定好数据部分的长度,真正发送数据包的工作都是由前面讲到的函数来完成的。至于如何把数据放到缓存和放多少数据是由自己的程序决定的。为了简单易懂,我直接向发送数据缓存中填写了四个数值。
这里向大家推荐个软件“IRIS”用于网络调试,可以很方便的查看所有经过网卡的数据包,我们利用这个工具查看我们发出的数据包。如图1所示:
图1 验证发送udp数据包
PS:请不要问我要这个软件,因为这是收费的软件,我也是试用的,不过用这个软件查看数据包很方便,需要的话还是自己想办法吧。
图中在“capture”栏中的右上部分是接收到的数据包(我只过滤了基板收发的数据包)。共显示三个,依次是基板发出的arp请求、上位机发出的arp应答和基板发出的包含四个数据的udp包。这与我们所写的main函数中的顺序是一致的。在“capture”栏中左半部分是当前选中数据包的解析,我选中的是第三个udp数据包,可以看到解析出的数据包结构:mac首部、ip首部、udp首部和4字节数据。这样便可以验证我们发送的udp数据包是没有错误的。另外,在红圈处我们看到udp校验和,这与ip的校验和表示的不同,我们看ip首部中校验和部分后面注释了一个“correct”标记表示校验正确,但udp的校验和没有这个标记。这并不是说udp校验和计算错误了,而是说上位机没有对udp校验和做验证,也就验证了udp校验和是可选的,在默认情况下是关闭的。
上半部分完