|
本帖最后由 xukai871105 于 2013-2-14 22:16 编辑
1前言
UDP协议全称为用户数据协议,是一种简单有效的运输协议。和以太网首部和IP首部相似,UDP首部也有自身的数据结构定义。从运输协议开始引入端口的概念,端口相当于一个应用程序的标识符。相对于TCP协议而言,UDP协议简单的多。本文将实现UDP协议,并通过几个简单的案例说明UDP的使用。
1.2 相关资料
ENC28J60学习笔记链接
http://www.amobbs.com/thread-5519381-1-1.html
AVRNET学习笔记 ETHERNET和ARP部分
http://www.amobbs.com/thread-5519452-1-1.html
AVRNET学习笔记 IP和ICMP部分
http://www.amobbs.com/thread-5519494-1-1.html
AVRNET项目(国外)
http://www.avrportal.com/?page=avrnet
AVR webserver项目(国外)
http://www.tuxgraphics.org/electronics/200611/article06111.shtml#0lfindex0
2 UDP部分实现
UDP功能的实现可分为UDP首部填充,UDP缓冲区填充和UDP报文查询。UDP首部填充是一个按部就班的过程,即填充源端口,目标端口,长度和校验和。UDP缓冲区填充即往UDP负载部分逐个填充数据。UDP报文查询功能即匹配本机UDP端口号,并进行函数处理。为了实现这些功能,首先需要以下宏定义。需要注意的是以太网传输协议中数据被以大端的形式保存,即低地址存放了高字节。例如端口号的高字节存放在了0x22的位置,而端口号的低字节存放在了0x23的位置。- // UDP默认端口号
- #define UDP_AVR_PORT_V 3000
- #define UDP_AVR_PORT_H_V (UDP_AVR_PORT_V>>8)
- #define UDP_AVR_PORT_L_V (UDP_AVR_PORT_V&0xff)
- // 源端口
- #define UDP_SRC_PORT_H_P 0x22
- #define UDP_SRC_PORT_L_P 0x23
- // 目标端口
- #define UDP_DST_PORT_H_P 0x24
- #define UDP_DST_PORT_L_P 0x25
- // UDP负载长度
- #define UDP_LENGTH_H_P 0x26
- #define UDP_LENGTH_L_P 0x27
- // UDP校验和
- #define UDP_CHECKSUM_H_P 0x28
- #define UDP_CHECKSUM_L_P 0x29
- // UDP负载起始地址
- #define UDP_DATA_P 0x2A
复制代码 2.1 UDP首部填充
UDP首部填充中需要明确UDP的端口号,AVRNET项目中通过常数宏定义实现
#define UDP_AVR_PORT_V 3000
#define UDP_AVR_PORT_H_V (UDP_AVR_PORT_V>>8)
#define UDP_AVR_PORT_L_V (UDP_AVR_PORT_V&0xff)
从这段代码中可以看出,AVRNET的UDP端口号为3000。- void udp_generate_header ( BYTE *rxtx_buffer, WORD_BYTES dest_port, WORD_BYTES length )
- {
- WORD_BYTES ck;
- // 默认端口号 3000
- rxtx_buffer[UDP_SRC_PORT_H_P] = UDP_AVR_PORT_H_V;
- rxtx_buffer[UDP_SRC_PORT_L_P] = UDP_AVR_PORT_L_V;
- // 目标端口地址
- rxtx_buffer[UDP_DST_PORT_H_P] = dest_port.byte.high;
- rxtx_buffer[UDP_DST_PORT_L_P] = dest_port.byte.low;
- // 负载长度
- rxtx_buffer[UDP_LENGTH_H_P] = length.byte.high;
- rxtx_buffer[UDP_LENGTH_L_P] = length.byte.low;
- // 计算校验和
- rxtx_buffer[UDP_CHECKSUM_H_P] = 0;
- rxtx_buffer[UDP_CHECKSUM_L_P] = 0;
- // length+8 for source/destination IP address length (8-bytes)
- ck.word = software_checksum ( (BYTE*)&rxtx_buffer[IP_SRC_IP_P], length.word+8, length.word+IP_PROTO_UDP_V);
- rxtx_buffer[UDP_CHECKSUM_H_P] = ck.byte.high;
- rxtx_buffer[UDP_CHECKSUM_L_P] = ck.byte.low;
- }
复制代码 2.2 UDP负载长度查询
UDP首部中包含UDP长度的描述字节,长度占有两个字节并以大端格式保存,由于宏定义的提示作用,弱化了大端格式的影响。长度中也包括了UDP首部的长度,UDP首部的长度为固定的8字节。- WORD udp_get_dlength( BYTE *rxtx_buffer )
- {
- WORD length = 0;
- // 获得UDP长度
- length = rxtx_buffer[UDP_LENGTH_H_P] << 8 | rxtx_buffer[UDP_LENGTH_L_P];
- // 去除首部长度
- length = length - 8;
-
- return length;
- }
复制代码 2.3 UDP负载区填充
UDP负载去填充即在UDP首部之后填充有用的数据。在这段真实负载之前包括了UDP首部,IP首部和以太网首部,分别占用了8字节,20字节和14字节。UDP负载的起始地址通过宏由UDP_DATA_P定义。对于AVR单片机的特点,为了尽一切可能节约内存使用率,在向负载区填充数据时用到了两个函数,udp_puts_data函数操作的是BYTE*类型的数据,而udp_puts_data_p操作的为PGM_P类型数据,即位于FLASH中的数据,需要通过pgm_read_byte取出。而其他类型的CPU,例如STM却没有该功能,则使用udp_puts_data即可。- WORD udp_puts_data ( BYTE *rxtx_buffer, BYTE *data, WORD offset )
- {
- while( *data )
- {
- rxtx_buffer[ UDP_DATA_P + offset ] = *data++;
- offset++;
- }
- return offset;
- }
- WORD udp_puts_data_p ( BYTE *rxtx_buffer, PGM_P data, WORD offset )
- {
- BYTE ch;
-
- while( (ch = pgm_read_byte(data++)) )
- {
- rxtx_buffer[ UDP_DATA_P + offset ] = ch;
- offset++;
- }
- return offset;
- }
复制代码 2.4 UDP报文查询
UDP报文的查询需要匹配接收数据包中的UDP端口号,若匹配成功则可对输入数据包进行处理,这些处理包括解析数据包的数据格式,分析出控制命令或查询命令。也可以通过udp_puts_data向发送缓冲区中填写响应数据。接着逐步生成以太网首部,IP首部和UDP首部,以太网首部中包含目标MAC地址,IP首部中包含目标IP地址,UDP首部中包含目标端口号。- BYTE udp_receive ( BYTE *rxtx_buffer, BYTE *dest_mac, BYTE *dest_ip )
- {
- WORD dlength = 0;
- // udp负载长度
- WORD udp_loadlen = 0;
- // 匹配UDP协议 UDP端口号
- if ( rxtx_buffer[IP_PROTO_P] != IP_PROTO_UDP_V || rxtx_buffer[UDP_DST_PORT_H_P] != UDP_AVR_PORT_H_V || rxtx_buffer[ UDP_DST_PORT_L_P ] != UDP_AVR_PORT_L_V )
- return 0;
-
- // 加入处理函数
- // 生成以太网首部
- eth_generate_header (rxtx_buffer, (WORD_BYTES){ETH_TYPE_IP_V}, dest_mac );
- // 生成IP首部
- ip_generate_header (rxtx_buffer, (WORD_BYTES){sizeof(IP_HEADER)+sizeof(UDP_HEADER)+dlength}, IP_PROTO_UDP_V, dest_ip );
- // 生成UDP首部
- udp_generate_header (rxtx_buffer, (WORD_BYTES){(rxtx_buffer[UDP_SRC_PORT_H_P]<<8)|rxtx_buffer[UDP_SRC_PORT_L_P]}, (WORD_BYTES){sizeof(UDP_HEADER)+dlength});
- // 发送所有首部和UDP负载数据
- enc28j60_packet_send ( rxtx_buffer, sizeof(ETH_HEADER)+sizeof(IP_HEADER)+sizeof(UDP_HEADER)+dlength );
- return 1;
- }
复制代码 3 实验
实验部分主要是为了验证UDP协议,通过PC机上的网络调试软件开辟一个PC机的UDP端口,假定端口号为3001,IP地址为192.168.1.102;AVR的UDP默认端口号为3000,IP地址为192.168.1.105。
3.1 程序结构
在运行UDP程序之前,需要运行ARP,IP和ICMP各部分,并保存发起发的MAC地址和IP地址。- /* 获得新的IP报文 */
- plen = enc28j60_packet_receive( (BYTE*)&rxtx_buffer, MAX_RXTX_BUFFER );
- if(plen==0) return;
- /* 保存客服端的MAC地址 */
- memcpy ( (BYTE*)&client_mac, &rxtx_buffer[ ETH_SRC_MAC_P ], sizeof(MAC_ADDR) );
- /* 检查该报文是不是ARP报文 */
- if ( arp_packet_is_arp( rxtx_buffer, (WORD_BYTES){ARP_OPCODE_REQUEST_V} ) )
- {
- /* 向客户端返回ARP报文 */
- arp_send_reply ( (BYTE*)&rxtx_buffer, (BYTE*)&client_mac );
- return;
- }
- /* 保存客服端的IP地址 */
- memcpy ( (BYTE*)&client_ip, &rxtx_buffer[ IP_SRC_IP_P ], sizeof(IP_ADDR) );
- /* 检查该报文是否为IP报文 */
- if ( ip_packet_is_ip ( (BYTE*)&rxtx_buffer ) == 0 )
- {
- return;
- }
-
- /* 如果是ICMP报文 向发起方返回数据 */
- if ( icmp_send_reply ( (BYTE*)&rxtx_buffer, (BYTE*)&client_mac, (BYTE*)&client_ip ) )
- {
- return;
- }
-
- // 进行UDP处理
- if (udp_receive ( (BYTE *)&rxtx_buffer, (BYTE *)&client_mac, (BYTE *)&client_ip ))
- {
- return;
- }
复制代码 3.2 源IP和源端口
使用网路调试助手发送一个名称,例如UDP。在仿真环境中通过串口打印出发起方的IP地址和端口号,例如PC机的端口号设定为3001,PC机的IP地址为192.168.1.102。在udp_receive函数中需要判断UDP端口号和目标IP地址是否匹配,若匹配即可加入以下代码。首先获得UDP的负载长度,使用memcpy命令复制到udp_recbuf中,接着通过串口打印源IP地址,该参数位于IP首部,源端口号,该参数位于UDP首部中。- // 获得UDP负载长度
- udp_loadlen = udp_get_dlength(rxtx_buffer);
-
- // 复制UDP负载
- memcpy(udp_recbuf,(char*)&rxtx_buffer[UDP_DATA_P],udp_loadlen);
-
- #ifdef UDP_DEBUG
- printf("UDP Message!\n");
- printf("Send Form:%d.%d.%d.%d ",\
- rxtx_buffer[IP_SRC_IP_P+0],rxtx_buffer[IP_SRC_IP_P+1],\
- rxtx_buffer[IP_SRC_IP_P+2],rxtx_buffer[IP_SRC_IP_P+3]);
- printf("Port:%d\n",(rxtx_buffer[UDP_SRC_PORT_H_P] << 8) | rxtx_buffer[UDP_SRC_PORT_L_P]);
- printf("Reccive:%s\n",udp_recbuf);
- #endif
复制代码
图 网络调试助手
网络调试助手使用时,先设定协议类型为UDP,并修改本机端口号改为3000,点击连接。然后修改目标主机的IP地址,改为AVR的IP地址,目标端口号改为3000.最后填入数据点击发送。
图 UDP 输出结果
可以看出,PC机先发送UDP命令之前先发送了一个ARP请求,找出AVR的MAC地址,接着发送UDP数据包。这个实验可以验证UDP接收数据包正确。
3.3 返回Hello UDP
接着稍微修改一下上面的程序,接收到UDP数据包之后,在负载数据之前加入Hello字符,如果输入的为xukai871105,则返回Hello xukai871105。可以通过网络调试助手看到返回的结果。使用strcpy函数把Hello复制到udp_sendbuf数组中,接着使用strcat把udp_recbuf中的字符串连接到udp_sendbuf之后,最后调用udp_puts_date填充到发送缓冲区中,udp_puts_date的最后一个参数为缓冲区的起始字节,第一次填入时应使用0。- // 获得UDP负载长度
- udp_loadlen = udp_get_dlength(rxtx_buffer);
-
- // 复制UDP负载
- memcpy(udp_recbuf,(char*)&rxtx_buffer[UDP_DATA_P],udp_loadlen);
-
- #ifdef UDP_DEBUG
- printf("UDP Message!\n");
- printf("Send Form:%d.%d.%d.%d ",\
- rxtx_buffer[IP_SRC_IP_P+0],rxtx_buffer[IP_SRC_IP_P+1],\
- rxtx_buffer[IP_SRC_IP_P+2],rxtx_buffer[IP_SRC_IP_P+3]);
- printf("Port:%d\n",(rxtx_buffer[UDP_SRC_PORT_H_P] << 8) | rxtx_buffer[UDP_SRC_PORT_L_P]);
- printf("Reccive:%s\n",udp_recbuf);
- #endif
-
- // 复制Hello
- strcpy(udp_sendbuf,"Hello ");
- // 连接数据
- strcat(udp_sendbuf,udp_recbuf);
- dlength = udp_puts_data ( rxtx_buffer,(BYTE*)udp_sendbuf, 0 );
复制代码
图 UDP返回结果
通过前面两个实验可以证明UDP的接收和发送工作正常。
3.4 LED控制
验证了UDP的发送和接收,可以通过定义一组指令实现LED的控制。AVRNET项目中有两个LED指示灯,设计这样一组指令打开或关闭LED,例如
led1=1 打开LED1
led1=0 关闭LED1
led2=1 打开LED2
led2=0 关闭LED2
具体代码如下。通过memcmp比较UDP数据包的中前4个字符是否为led1,如果两个字符串相等,则进行led1的控制处理,在根据第5个字节(从0开始)判断操作是打开还是关闭。- // 获得UDP负载长度
- udp_loadlen = udp_get_dlength(rxtx_buffer);
-
- // 复制UDP负载
- memcpy(udp_recbuf,(char*)&rxtx_buffer[UDP_DATA_P],udp_loadlen);
-
- #ifdef UDP_DEBUG
- printf("UDP Message!\n");
- printf("Send Form:%d.%d.%d.%d ",\
- rxtx_buffer[IP_SRC_IP_P+0],rxtx_buffer[IP_SRC_IP_P+1],\
- rxtx_buffer[IP_SRC_IP_P+2],rxtx_buffer[IP_SRC_IP_P+3]);
- printf("Port:%d\n",(rxtx_buffer[UDP_SRC_PORT_H_P] << 8) | rxtx_buffer[UDP_SRC_PORT_L_P]);
- printf("Reccive:%s\n",udp_recbuf);
- #endif
-
- // 命令解析 led1=1
- if( !memcmp((char*)&udp_recbuf,"led1",4) )
- {
- // led1 = 1
- if( udp_recbuf[5] == '1')
- {
- LED_PORT &= ~_BV ( LED_PIN1 );
- }
- else
- {
- LED_PORT |= _BV( LED_PIN1 );
- }
- dlength = udp_puts_data_p ( rxtx_buffer, PSTR("led1 command"), 0 );
- }
复制代码
图 通过led=1命令控制LED点亮
4总结
UDP是一个非常简答有效的协议。协议占用的Flash和RAM的空间较小,LED控制实例调试时占用的RAM空间为409字节,仅占用了AVRNET使用的ATmega32内存空间的20%。研究完UDP协议之后便可研究分析TCP协议,在熟悉的TCP协议的基础上才可实践HTTP制作WEB网页。使用嵌入式实践WEB服务器之前还需要练习静态网页动态网页的制作方法,熟悉HTTP请求格式等等工作。
最后附上工程代码!
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有帐号?注册
x
阿莫论坛20周年了!感谢大家的支持与爱护!!
曾经有一段真挚的爱情摆在我的面前,我没有珍惜,现在想起来,还好我没有珍惜……
|