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

套接字任务主函数。

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

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

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

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 的首地址,如下图所示。

 

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

 

第 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 语句,如下图所示。

 

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

 

左右侧分别为运行内存管理语句前后存储区状态。其中,

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#的客户端应用程序设计,便于设计符合自己要求的客户端应用程序。其界面如下:

 

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

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

五、总结

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

扫码关注尚为网微信公众号

尚为网微信公众号
每天学习电路设计嵌入式系统的专业知识,关注一波,没准就用上了。

原创文章,作者:sunev,如若转载,请注明出处:https://www.sunev.cn/embedded/32.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2012年3月16日
下一篇 2012年5月17日

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注