1)实验平台:正点原子开拓者FPGA 开发板
2)摘自《开拓者 Nios II开发指南》关注官方微信号公众号,获取更多资料:正点原子
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/index.html
第二十二章基于NicheStack的UDP实验
上一章我们使用三速以太网IP核搭建了NicheStack的底层硬件框架,并使用Nios II SBT
for Eclipse自带的“Simple Socket Server”例程演示了使用telnet服务控制FPGA开发板上
的led灯。本章我们将进一步了解NicheStack,并实现一个简单的UDP服务。本章分为以下几个
部分:
22.1 简介
22.2 实验任务
22.3 硬件设计
22.4 软件设计
22.5 下载验证
简介
UDP是User Datagram Protocol的简称,中文名是用户数据报协议。是OSI(Open System
Interconnection,开放式系统互联) 参考模型中一种无连接的传输层协议,提供面向事务的
简单不可靠信息传送服务。UDP 用来支持那些需要在计算机之间传输数据的网络应用,包括网
络视频会议系统在内的众多的客户/服务器模式的网络应用都需要使用UDP协议。UDP协议从问
世至今已经被使用了很多年,虽然其最初的光彩已经被一些类似协议所掩盖,但是即使是在今
天UDP仍然不失为一项非常实用和可行的网络传输层协议。
UDP 数据报格式如下图:
图 22.1.1 UDP数据报格式
端口号表示发送和接收进程,UDP协议使用端口号为不同的应用保留各自的数据传输通道,
UDP和TCP协议都是采用端口号对同一时刻内多项应用同时发送和接收数据,而数据接收方则通
过目标端口接收数据。有的网络应用只能使用预先为其预留或注册的静态端口;而另外一些网
络应用则可以使用未被注册的动态端口。因为UDP报头使用两个字节存放端口号,所以端口号
的有效范围是从0到65535。一般来说,大于49151的端口号都代表动态端口。
数据报的长度是指包括报头和数据部分在内的总字节数。因为报头的长度是固定的,所以
该域主要被用来计算可变长度的数据部分(又称为数据负载)。数据报的最大长度根据操作环
境的不同而各异。从理论上说,包含报头在内的数据报的最大长度为 65535 字节。
UDP协议使用报头中的校验和来保证数据的安全。校验和首先在数据发送方通过特殊的算
法计算得出,在传递到接收方之后,还需要再重新计算。如果某个数据报在传输过程中被第三
方篡改或者由于线路噪音等原因受到损坏,发送和接收方的校验计算和将不会相符,由此UDP
协议可以检测是否出错。关于UDP协议及其与IP协议的关系的详细介绍可参考《开拓者FPGA开
发指南》第四十二章的《以太网通信实验》。
下面我们来看看如何用NicheStack进行UDP通信。使用NicheStack进行UPD通信的过程大致
如下图所示:
图 22.1.2 UDP通信过程
整个过程可分为以下几个步骤:
UDP服务端:
1) 创建UDP套接字(使用socket()函数)
2) 将套接字与服务器地址绑定(使用bind()函数)
3) 接收客户端发送的数据(使用recvfrom()函数)
4) 向客户端发送数据(使用sendto()函数)
5) 回到第3步(如果继续服务)
6) 关闭UDP服务(如果需要关闭服务),关闭socket描述符并退出(使用close()函数)
UDP客户端:
1) 创建UDP套接字(使用socket()函数)
2) 发送数据或请求给服务器(使用sendto()函数)
3) 接收来自服务器的数据或响应(使用recvfrom()函数)
4) 处理回复并在必要时返回步骤2
5) 关闭套接字描述符并退出(使用close()函数)
涉及到的函数说明如下:
socket()函数原型:
long socket(int family, int type, int proto)
作用:在指定的域中创建未绑定的套接字,返回套接字文件描述符
参数family:用于设置网络通信的域,socket根据这个参数选择信息协议的族,常用的值
为AF_INET(用于IPv4)和AF_INET6(用于IPv6)
参数type:指定创建的socket类型,值SOCK_STREAM用于TCP通信、值SOCK_DGRAM用于UDP
通信。
参数proto:指明套接字使用的协议,0表示使用地址系列的默认协议,置0即可。
返回值:成功:非负的文件描述符
失败:-1
bind()函数原型:
int bind (long s, struct sockaddr * addr, int addrlen)
作用:将地址分配给未绑定的套接字。
参数s:套接字文件描述符,通过socket()函数获得
参数addr:需要绑定的IP和端口
参数addrlen:addr结构体的大小
返回值:成功:0
失败:-1
recvfrom()函数原型:
int recvfrom(BSD_SOCKET s, void * buf, BSD_SIZE_T len, int flags,
struct sockaddr * from, int * fromlen)
作用:从套接字接收消息。
参数s:套接字文件描述符,通过socket()函数获得
参数buf:用于接收数据的应用程序缓冲区
参数len:接收缓冲区的大小
参数flags:用于修改套接字行为的位或标志,一般填0即可
参数from:返回包含源地址的结构
参数from len:表示第五个参数所指向内容的长度
返回值:成功:返回接收成功的数据长度
失败:-1
sendto()函数原型:
int sendto (long s, char * buf, int len, int flags,
struct sockaddr * to, int tolen)
参数s:套接字文件描述符,通过socket()函数获得
参数buf:包含要发送的数据的应用程序缓冲区
参数len:发送缓冲区的大小
参数flags:用于修改套接字行为的位或标志,一般填0即可
参数to:包含目的地址的结构
参数tolen:目的地址结构的大小
返回值:成功:返回发送成功的数据长度
失败:-1
close()函数原型:
int close(long s)
close函数比较简单,只要填入socket()函数返回的文件描述符即可。
需要说明的是这些函数在NicheStack中基本上都是宏定义到另一个函数的,这里的函数原
型以最终使用的函数为准。
实验任务
本章的实验任务是使用NicheStack实现简单的UDP服务器,其功能是将网络调试助手发送
给开发板的数据环回至网络调试助手。
硬件设计
本章的UDP实验硬件部分可以基于《基于NicheStack的简单socket服务器实验》,无需修
改。
软件设计
软件设计部分与上一章《基于NicheStack的简单socket服务器实验》的区别不大,可以在
上一章的软件设计部分的基础上修改。
我们打开《基于NicheStack的简单socket服务器实验》的软件工程,在qsys_eth_bsp的
iniche目录下有如图 22.4.1所示的目录层。inc目录包含系统调用宏定义文件alt_syscall.h
和Altera InterNiche器件服务源文件alt_iniche_dev.h。src目录主要包含NicheStack的协议
栈实现源文件,其中ip目录具有完整大小的IP系列堆栈协议(ARP、ICMP、UDP)源文件、tcp
目录包含TCP和套接字源文件、net目录具有NicheStack和NicheLite共有的网络支持软件(包
括pktalloc、queue、macloop、slip和dhcp)源文件、misclib目录包含面向字符的用户界面
(CUI)与IP地址解析代码以及其它类似的额外功能、nios2目录则包含用于支持在Altera的
Nios-II平台上运行NicheStack协议栈的代码。
图 22.4.1 NicheStack目录层次
当购买了除TCP/IP堆栈之外的其它InterNiche产品时,将出现其它目录,如ftp(FTP客户
端和服务器代码)、tftp(TFTP客户端和服务器代码)、telnet(Telnet服务器代码)等,这
些目录都是Altera的NicheStack组件自带的。
知道NicheStack的目录层次后,我们可以从这些目录中了解其底层的实现,有兴趣的可以
研究,这里我们重点在于如何使用。
现在我们将该工程修改为UDP服务器工程。
由于本实验不需要led,所以我们将led.c文件删除。重命名simple_socket_server.c为
udp_server.c。并将其内容替换如下:
1 #include <stdio.h>
2 #include <string.h>
3 #include <ctype.h>
4
5 /* MicroC/OS-II definitions */
6 #include "includes.h"
7
8 /* Simple Socket Server definitions */
9 #include "simple_socket_server.h"
10 #include "alt_error_handler.h"
11
12 /* Nichestack definitions */
13 #include "ipport.h"
14 #include "tcpport.h"
15
16 /*
17 * sss_handle_msg()
18 *
19 * 接收UDP客户端发送过来的信息, 并将接收到的信息环回给UDP客户端 ,同时打印信息到控制台
20 *
21 */
22 void sss_handle_msg(SSSConn* conn)
23 {
24 int len, rx_code;
25 struct sockaddr_in incoming_addr;
26
27 while(1){
28 memset(conn->rx_buffer, 0, SSS_RX_BUF_SIZE);
29 len = sizeof(incoming_addr);
30 rx_code = recvfrom(conn->fd, conn->rx_buffer, SSS_RX_BUF_SIZE, 0,
31 (struct sockaddr *)&incoming_addr ,&len); //接收客户端发送过来的信息
32 if(rx_code == -1){
33 printf("recieve data fail!");
34 return;
35 } //判断是否接收错误
36 conn->rx_wr_pos = conn->rx_buffer; //将接收到的信息传递给rx_wr_pos指针
37 printf("client ip : %s", inet_ntoa(incoming_addr.sin_addr)); //打印客户端IP
38 printf("client msg: %s",conn->rx_buffer); //打印客户端发过来的信息
39 sendto(conn->fd, conn->rx_wr_pos, strlen(conn->rx_wr_pos), 0,
40 (struct sockaddr *)&incoming_addr ,len); //发送信息给客户端
41 }
42 }
43
44 /*
45 * SSSSimpleSocketServerTask()
46 *
47 * 定义SSSSimpleSocketServerTask任务函数,即UDP服务任务函数
48 *
49 */
50 void SSSSimpleSocketServerTask()
51 {
52 int socketfd;
53 struct sockaddr_in addr;
54 static SSSConn conn;
55
56 if ((socketfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) //创建socket接口
57 alt_NetworkErrorHandler(EXPANDED_DIAGNOSIS_CODE,"[sss_task] Socket creation failed");
58
59 addr.sin_family = AF_INET; //地址家族:IPv4
60 addr.sin_port = htons(SSS_PORT); //端口由SSS_PORT宏定义,并转换为网络字节序
61 addr.sin_addr.s_addr = INADDR_ANY; //通配地址,由内核指定
62
63 if ((bind(socketfd,(struct sockaddr *)&addr,sizeof(addr))) < 0) //绑定socket
64 alt_NetworkErrorHandler(EXPANDED_DIAGNOSIS_CODE,"[sss_task] Bind failed");
65
66 printf("[sss_task] UDP Server on port %d", SSS_PORT);
67
68 conn.fd = socketfd;
69
70 sss_handle_msg(&conn);
71 close(conn.fd);
72 }
SSSConn为结构体,用于管理SSS的每一个连接,定义如下:
图 22.4.2 SSSConn为结构体
其中的enum用于TCP连接的状态指示,这里我们未使用到,在TCP客户服务中会使用。
从 udp_server.c 中 我 们 看 到 , 创 建 UDP 服 务 器 的 过 程 同 图 22.1.2 表 示 的 一 致 。
SSSSimpleSocketServerTask()任务函数即UDP服务任务函数,需要注意的是recvfrom()函数
是阻塞调用的,直到接收到数据后才往下执行。inet_ntoa()函数是将IP地址转换为标准的
点分十进制显示。第60行的宏htons调用是因为不同机器的字节排列方式不一样,也就是俗称
的大端和小端排序,所以为了方便网络通信,统一先转换成网络字节序。
更改完成后,我们找到SSS_PORT宏定义的地方,也就是simple_socket_server.h文件的第
131行,将其值改为8080,即使用8080端口进行UDP服务。
最后我们打开iniche_init.c文件,删除第94行和第97行的两个函数(这两个函数在
udp_server.c中因未用到已经删除),函数名如下:
图 22.4.3 删除不需要的函数
经过以上修改后,软件设计部分就完成了。
下载验证
讲完了软件工程,接下来我们就将该实验下载至我们的开拓者开发板进行验证。
首先我们用一根网线将开发板和电脑进行连接,然后连接JTAG和电源,开发板上电后我们
在Quartus II软件中将qsys_eth.sof文件下载至我们的开拓者开发板,qsys_eth.sof下载完成
后,我们就将qsys_eth.elf文件系统下载至我们的开拓者开发板,下载完成后,控制台打印如
下信息:
图 22.5.1 启动UDP服务信息
现在,我们打开网络调试助手,设置如下图所示:
图 22.5.2 打开网络调试助手
打开网络调试助手后,协议类型选择:UDP;本地主机地址选择:本地连接的IP地址(在
这里是192.168.1.89);本地主机端口号:8080;设置完成后点击【打开】按钮。如下图所示:
图 22.5.3 设置发送相关信息
远程主机选择:192.168.1.234 : 8080(UDP服务器的IP地址和端口号),网络调试助手
打开后,在发送文本框中输入数据“正点原子”并点击发送,如下图所示:
图 22.5.4 环回结果
可以看到网络调试助手接收到了UDP服务端返回的信息。与此同时,控制台打印如下信息:
图 22.5.5 控制台打印接收到的信息