一、摘要
分析基于 NicheStack 协议栈的网络例程,重点分析了 simple_socket_server.c 文件,阐述网络通信的过程,最后,完成了基于 C#的上位机网络通信应用程序。
二、实验平台
软件平台:Quartus II 9.0 + Nios II 9.0、Visual Studio2010
硬件平台:DIY_DE2
三、基于 NicheStack 协议栈的网络例程分析
首先,明确两个概念:
服务器端:FPGA 端,
客户端:PC 端。
1、工程文件解读
本例程需要的工程文件有以下几种,下面对其意义及作用做了说明:
(1)alt_error_handler.h、alt_error_handler.c:错误类型句柄文件;
(2)dm9000a_regs.h、dm9000a.h、dm9000a.c:DM9000A 的驱动;
(3)network_utilities.h、network_utilities.c:设置 IP,设置 MAC;
(4)simple_socket_server.h、simple_socket_server.c:工程的主体程序,包括任务调度优先级、缺省 IP 设置、套接字、各种任务调度等等工作;
(5)led.c:LED、七段数码管显示程序;
(6)iniche_init.c:程序主函数。
例程的主程序理解起来就较为简单,其过程为:初始化 uC/OS II 系统的时间,外部任务创建,包括设备初始化,套接字创建等等,之后,程序开始运行。而具体完成套接字任务的是 simple_socket_server.c 文件,下面就此作出解读。
2、simple_socket_server.c 文件解读
首先看几个函数:
void SSSCreateTasks(void);
套接字任务的创建。创建 2 个任务:LED 和七段数码管显示任务。
void sss_reset_connection(SSSConn* conn);
连接初始化。
void sss_send_menu(SSSConn* conn);
发送菜单。连接建立后,回传给客户端菜单信息。
void sss_handle_accept(int listen_socket, SSSConn* conn);
套接字连接函数。侧重网络连接。
void sss_exec_command(SSSConn* conn);
命令函数。显示 LED、七段数码管。
void sss_handle_receive(SSSConn* conn);
套接字接收数据函数。侧重接收数据。
void SSSSimpleSocketServerTask();
套接字任务主函数。
其中,套接字任务函数里面,给出了相关函数的调用顺序:
下面着重分析一下接收数据函数:
void sss_handle_receive(SSSConn* conn) { int data_used = 0, rx_code = 0; INT8U *lf_addr; conn->rx_rd_pos = conn->rx_buffer; conn->rx_wr_pos = conn->rx_buffer; printf("[sss_handle_receive] processing RX data\n"); while(conn->state != CLOSE) { /* Find the Carriage return which marks the end of the header */ lf_addr = strchr(conn->rx_buffer, '\n'); if(lf_addr) { /* go off and do whatever the user wanted us to do */ printf("TEST\n"); sss_exec_command(conn); } /* No newline received Then ask the socket for data */ else { rx_code = recv(conn->fd, conn->rx_wr_pos, SSS_RX_BUF_SIZE - (conn->rx_wr_pos - conn->rx_buffer) -1, 0); if(rx_code > 0) { conn->rx_wr_pos += rx_code; /* Zero terminate so we can use string functions */ *(conn->rx_wr_pos+1) = 0; } } /* * When the quit command is received, update our connection state so that * we can exit the while() loop and close the connection */ conn->state = conn->close CLOSE : READY; /* Manage buffer */ data_used = conn->rx_rd_pos - conn->rx_buffer; memmove(conn->rx_buffer, conn->rx_rd_pos, conn->rx_wr_pos - conn->rx_rd_pos); conn->rx_rd_pos = conn->rx_buffer; conn->rx_wr_pos -= data_used; memset(conn->rx_wr_pos, 0, data_used); } printf("[sss_handle_receive] closing connection\n"); close(conn->fd); sss_reset_connection(conn); return; }
程序主要采用指针操作的形式,首先看指针的定义及初始化:
simple_socket_server.h 中:
INT8U rx_buffer[SSS_RX_BUF_SIZE]; INT8U *rx_rd_pos; /* position we've read up to */ INT8U *rx_wr_pos; /* position we've written up to */
simple_socket_server.c 中:
conn->rx_rd_pos = conn->rx_buffer; conn->rx_wr_pos = conn->rx_buffer;
显然,conn->rx_buffer 是指向数组 rx_buffer 的首地址,初始化后,读指针 rx_rd_pos 和写指针 rx_wr_pos 也指向了数组 rx_buffer 的首地址,如下图所示。
第 14 行:
lf_addr = strchr(conn->rx_buffer, '\n');
这里是搜索得到的字符串中是否有转义符’\n’,即查询客户端输入数据后,是否有回车。如果有’\n’,则返回一个非零值;否则,返回 0。因此,除了在客户端发送数据外,还要跟一个’\n’。
第 24~36 行:
else { rx_code = recv(conn->fd, conn->rx_wr_pos, SSS_RX_BUF_SIZE - (conn->rx_wr_pos - conn->rx_buffer) -1, 0); if(rx_code > 0) { conn->rx_wr_pos += rx_code; /* Zero terminate so we can use string functions */ *(conn->rx_wr_pos+1) = 0; } }
这里用到 recv 函数,函数原型为:
int recv( SOCKET s,char FAR *buf,int len, int flags);
第一个参数 指定接收端套接字描述符;
第二个参数指明 一个缓冲区,该缓冲区用来存放 recv 函数接收到的数据;
第三个参数指明 buf 的长度;
第四个参数一般置 0。
根据上面的定义,可以得知:
(1)conn->fd 为接收端套接字描述符,
(2)conn->rx_wr_pos 为一个缓冲区,即使用 rx_buffer 数组,且从首地址开始存储数据,
(3)SSS_RX_BUF_SIZE – (conn->rx_wr_pos – conn->rx_buffer) -1 为缓冲区的长度,这里值得揣摩的是,程序初始化后,缓冲区长度为 SSS_RX_BUF_SIZE – 1,分析后可以得知,少的一个字节是为转义符’\n’预留。
recv 函数的返回值是,一次传输完成后,rx_buffer 接收到的字节数。由于网卡是 16bit 模式,所以,接收到的数据是以 2 个字节为单位,即 rx_code 为 2 的整数倍。
下面的 if 语句就比较简单了,如果有新的数据收到,则写指针 rx_wr_pos 向数组 rx_buffer 移动 rx_code 个字节,并把下一个字节里面的内容清零,以方便存储转义符’\n’。
第 45~50 行:
data_used = conn->rx_rd_pos - conn->rx_buffer; memmove(conn->rx_buffer, conn->rx_rd_pos, conn->rx_wr_pos - conn->rx_rd_pos); conn->rx_rd_pos = conn->rx_buffer; conn->rx_wr_pos -= data_used; memset(conn->rx_wr_pos, 0, data_used);
存储区管理:
data_used = conn->rx_rd_pos - conn->rx_buffer;
把已经读取的数据的字节数赋值给 data_used
memmove(conn->rx_buffer, conn->rx_rd_pos, conn->rx_wr_pos - conn->rx_rd_pos);
这里用到了拷贝字符串函数 memmove(memmove 与 memcpy 的区别可参考相关文档),memmove 的函数原型为:
void *memmove(void *dest, const void *src, size_t n);
*dest 为目标位置,
*src 为原位置,
n 为要拷贝的字符串字节数。
由一个图示来解释程序中的 memmove 语句,如下图所示。
左右侧分别为运行内存管理语句前后存储区状态。其中,
memmove(conn->rx_buffer, conn->rx_rd_pos, conn->rx_wr_pos - conn->rx_rd_pos);
为拷贝存储区内容,
conn->rx_rd_pos = conn->rx_buffer;
和
conn->rx_wr_pos -= data_used;
为重新调整读指针和写指针指向。最后,
memset(conn->rx_wr_pos, 0, data_used);
memset 函数给一段内存赋初值,从 rx_wr_pos 所指的地址开始的 data_used 个字节。
总结一下 void sss_handle_receive(SSSConn* conn)函数的处理过程:如果客户端输入了数据,但没有结束字符’\n’,则服务器端一直存储数据;如果客户端输入了数据,且有结束字符’\n’,则服务器端进入命令函数,随后进行相关显示内容,并通过内存管理,重新调整读指针和写指针的指向。
四、基于 C#的 PC 端网络应用程序
前面博文介绍了 PC 端 CMD 下输入 telent 命令的方式,这里应用 Visual Studio 2010 完成基于 C#的客户端应用程序设计,便于设计符合自己要求的客户端应用程序。其界面如下:
注:本例程来自网络,后经调试使用。
五、总结
该篇博文分析了服务器端(FPGA 端)网络任务处理过程,并完成服务器端与客户端(PC 端)的网络通信。最后,可以根据自己的需要修改客户端和服务器端程序。
扫码关注尚为网微信公众号
原创文章,作者:sunev,如若转载,请注明出处:https://www.sunev.cn/embedded/32.html