基于NicheStack协议栈的网络例程分析及客户端程序设计
一、摘要
分析基于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端)的网络通信。最后,可以根据自己的需要修改客户端和服务器端程序。