基于NicheStack协议栈的网络例程分析及客户端程序设计

一、摘要

分析基于NicheStack协议栈的网络例程,重点分析了simple_socket_server.c文件,阐述网络通信的过程,最后,完成了基于C#的上位机网络通信应用程序。

 

二、实验平台

软件平台:Quartus II 9.0 + Nios II 9.0Visual 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.cLED、七段数码管显示程序;

(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();

套接字任务主函数。

其中,套接字任务函数里面,给出了相关函数的调用顺序:

 

下面着重分析一下接收数据函数:

  1. void sss_handle_receive(SSSConn* conn)
  2. {
  3.   int data_used = 0, rx_code = 0;
  4.   INT8U *lf_addr; 
  5.   
  6.   conn->rx_rd_pos = conn->rx_buffer;
  7.   conn->rx_wr_pos = conn->rx_buffer;
  8.   
  9.   printf("[sss_handle_receive] processing RX data\n");
  10.   
  11.   while(conn->state != CLOSE)
  12.   {
  13.     /* Find the Carriage return which marks the end of the header */
  14.     lf_addr = strchr(conn->rx_buffer, '\n');
  15.       
  16.     if(lf_addr)
  17.     {
  18.       /* go off and do whatever the user wanted us to do */
  19.       printf("TEST\n");
  20.       
  21.       sss_exec_command(conn);
  22.     }
  23.     /* No newline received? Then ask the socket for data */
  24.     else
  25.     {
  26.       rx_code = recv(conn->fd, conn->rx_wr_pos, 
  27.         SSS_RX_BUF_SIZE - (conn->rx_wr_pos - conn->rx_buffer) -1, 0);
  28.           
  29.      if(rx_code > 0)
  30.       {
  31.         conn->rx_wr_pos += rx_code;
  32.         
  33.         /* Zero terminate so we can use string functions */
  34.         *(conn->rx_wr_pos+1) = 0;
  35.       }
  36.     }
  37.     /* 
  38.      * When the quit command is received, update our connection state so that
  39.      * we can exit the while() loop and close the connection
  40.      */
  41.     conn->state = conn->close ? CLOSE : READY;
  42.     /* Manage buffer */
  43.     data_used = conn->rx_rd_pos - conn->rx_buffer;
  44.     memmove(conn->rx_buffer, conn->rx_rd_pos, 
  45.        conn->rx_wr_pos - conn->rx_rd_pos);
  46.     conn->rx_rd_pos = conn->rx_buffer;
  47.     conn->rx_wr_pos -= data_used;
  48.     memset(conn->rx_wr_pos, 0, data_used);
  49.   }
  50.   printf("[sss_handle_receive] closing connection\n");
  51.   close(conn->fd);
  52.   sss_reset_connection(conn);
  53.   
  54.   return;
  55. }

 

程序主要采用指针操作的形式,首先看指针的定义及初始化:

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_code2的整数倍。

下面的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(memmovememcpy的区别可参考相关文档)memmove的函数原型为:

  void *memmove(void *dest, const void *src, size_t n);

*dest 为目标位置,

*src 为原位置,

为要拷贝的字符串字节数。

由一个图示来解释程序中的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端网络应用程序

前面博文介绍了PCCMD下输入telent命令的方式,这里应用Visual Studio 2010完成基于C#的客户端应用程序设计,便于设计符合自己要求的客户端应用程序。其界面如下:

 

 

 

注:本例程来自网络,后经调试使用。

 

五、总结

该篇博文分析了服务器端(FPGA端)网络任务处理过程,并完成服务器端与客户端(PC端)的网络通信。最后,可以根据自己的需要修改客户端和服务器端程序。

 

 

posted @ 2012-04-22 15:30  sunev  阅读(2970)  评论(1编辑  收藏  举报