TCP/IP协议栈之LwIP(九)---Sequetia API编程

本文深入剖析了LwIP协议栈中的Sequetia API,探讨了其设计理念和实现机制,包括用户进程与内核进程间的通信、消息机制、数据包管理以及Sequetia API的编程示例。Sequetia API避免了数据拷贝,提高了效率,并通过内核回调函数简化了网络编程,使得在嵌入式系统中进行TCP/IP编程更为便捷。文中给出了UDP回显服务器、Web服务器和并发服务器的编程实例,展示了Sequetia API在实际应用中的效能。
摘要由CSDN通过智能技术生成

一、Sequetia API实现原理

前面介绍了RAW/Callback API编程,同时也谈到了RAW API编程的缺点:用户应用程序和协议栈内核处于同一进程中,两者间会出现相互制约的关系,影响协议栈接收、处理新数据包的效率,甚至出现丢包错误。本文将介绍协议栈在有操作系统模拟层下的编程方式,这时协议栈内核和用户程序就可以处在两个相互独立的进程中运行,两者间也就不存在相互制约的问题了。

在Linux中有BSD Socket API进行网络编程,它遵循了一套简单的编程步骤和规则,可以提高应用程序的开发效率。但由于BSD Socket函数实现上具有很高的抽象特性,所以并不适合在小型嵌入式TCP/IP协议栈中使用,更重要的是BSD Socket编程时,在应用程序进程和协议栈内核进程之间会出现数据拷贝的情况,拷贝消耗的时间和空间资源对嵌入式系统来说是非常宝贵的资源。

LwIP协议栈针对上述特点提供了Sequetia API,它的出发点是上层已经预知了协议栈内核的部分结构,API可以使用这种预知来避免数据拷贝的出现,Sequetia API与BSD Socket具有很大的相似性,但工作在更低的层次(Sequetia API操作的是一个网络连接,BSD Socket像操作普通文件那样来操作一个网络连接,充分贯彻linux一切皆文件的理念),用户进程可以直接操作内核进程中的数据包数据。

Sequetia API的实现由两部分组成:一部分作为用户编程接口函数提供给用户,这些函数在用户进程中执行;另一部分驻留在协议栈内核进程中,这两部分通过进程通信机制(IPC:Inter-Process Communication)实现通信和同步,共同为应用程序提供服务。被用到的进程通信机制包括以下三种:

  • 消息邮箱,用于两部分API的通信;
  • 信号量,用于两部分API的同步;
  • 共享内存,用于保存用户进程与内核进程的消息结构。

Sequetia API设计的核心在于让用户进程负责尽可能多的工作,例如数据的计算、拷贝等;而协议栈进程只负责简单的通信工作,这点很关键,因为系统可能存在多个应用程序,它们都使用协议栈进程提供的通信服务,保证内核进程的高效性和实时性是提高系统性能的重要保障。Sequetia API的实现中,两部分API间的关系如下图示:
LwIP协议栈内核API
Sequetia API实现时,也为用户提供了数据包管理函数,可以完成数据包内存申请、释放、数据拷贝等任务,应用程序使用netbuf结构来描述、组装数据包,该结构只是对内核pbuf的简单封装,通过共享一个netbuf结构,两部分API就能实现对数据包的共同处理,避免了数据的拷贝。同时,由于RAW API针对RAW、UDP、TCP各有一套API,使用不够方便,协议栈内核对RAW API进行了更高抽象的封装,将RAW、UDP、TCP统一为一套接口(像上图中do_xxx格式的函数,本文称该接口函数为内核进程API),根据类型字段自动选择合适的调用对象,实现原理下文再介绍。

上图中用户进程API与内核进程API之间进行交互信息时,通常是由用户进程API函数调用时发出消息,用来告诉内核进程执行哪一个内核API函数(通过函数指针和参数指针的传递),各种类型内核消息的具体消息内容都可以直接包含在内核消息结构tcpip_msg中,两部分API互相配合共同完成一个完整的API功能。举个例子,如果用户程序中调用函数netconn_bind绑定了一个连接,该函数实现时是通过向内核进程发送一个TCPIP_MSG_API类型的消息,告诉内核进程执行do_bind函数;在消息发出后,函数阻塞在信号量上,等待内核处理该消息;内核在处理消息时,会根据消息内容调用do_bind,而do_bind会根据连接的类型调用内核函数udp_bind、tcp_bind或raw_bind;当do_bind执行完成后,它会释放信号量,这使被阻塞的netconn_bind得以继续执行,整个过程如下图所示:
Sequetia API函数调用关系

二、协议栈消息机制

2.1 内核进程消息结构

内核进程tcpip_thread在前篇介绍协议栈内核定时事件时简单介绍过,该进程阻塞在邮箱上接收消息,系统消息是通过结构tcpip_msg来描述的,内核进程接收到消息后根据识别出的消息类型,调用相应的内核进程API函数处理这些消息。消息tcpip_msg的数据结构及内核进程tcpip_thread的实现代码如下:

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\tcpip.h

enum tcpip_msg_type {
   
  TCPIP_MSG_API,
  TCPIP_MSG_INPKT,
  TCPIP_MSG_NETIFAPI,
  TCPIP_MSG_TIMEOUT,
  TCPIP_MSG_UNTIMEOUT,
  TCPIP_MSG_CALLBACK,
  TCPIP_MSG_CALLBACK_STATIC
};

struct tcpip_msg {
   
  enum tcpip_msg_type type;
  sys_sem_t *sem;
  union {
   
    struct api_msg *apimsg;
    struct netifapi_msg *netifapimsg;
    struct {
   
      struct pbuf *p;
      struct netif *netif;
    } inp;
    struct {
   
      tcpip_callback_fn function;
      void *ctx;
    } cb;
    struct {
   
      u32_t msecs;
      sys_timeout_handler h;
      void *arg;
    } tmo;
  } msg;
};
// rt-thread\components\net\lwip-1.4.1\src\api\tcpip.c

/* global variables */
static tcpip_init_done_fn tcpip_init_done;
static void *tcpip_init_done_arg;
static sys_mbox_t mbox;
/** The global semaphore to lock the stack. */
sys_mutex_t lock_tcpip_core;

/**
 * The main lwIP thread. This thread has exclusive access to lwIP core functions
 * (unless access to them is not locked). Other threads communicate with this
 * thread using message boxes.
 * It also starts all the timers to make sure they are running in the right
 * thread context.
 * @param arg unused argument
 */
static void tcpip_thread(void *arg)
{
   
  struct tcpip_msg *msg;

  if (tcpip_init_done != NULL) {
   
    tcpip_init_done(tcpip_init_done_arg);
  }

  LOCK_TCPIP_CORE();
  while (1) {
                             /* MAIN Loop */
    UNLOCK_TCPIP_CORE();
    LWIP_TCPIP_THREAD_ALIVE();
    /* wait for a message, timeouts are processed while waiting */
    sys_timeouts_mbox_fetch(&mbox, (void **)&msg);
    LOCK_TCPIP_CORE();
    switch (msg->type) {
   
    case TCPIP_MSG_API:
      msg->msg.apimsg->function(&(msg->msg.apimsg->msg));
      break;
    case TCPIP_MSG_INPKT:
      if (msg->msg.inp.netif->flags & (NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET)) {
   
        ethernet_input(msg->msg.inp.p, msg->msg.inp.netif);
      } else {
   
        ip_input(msg->msg.inp.p, msg->msg.inp.netif);
      }
      memp_free(MEMP_TCPIP_MSG_INPKT, msg);
      break;
    case TCPIP_MSG_NETIFAPI:
      msg->msg.netifapimsg->function(&(msg->msg.netifapimsg->msg));
      break;
    case TCPIP_MSG_TIMEOUT:
      sys_timeout(msg->msg.tmo.msecs, msg->msg.tmo.h, msg->msg.tmo.arg);
      memp_free(MEMP_TCPIP_MSG_API, msg);
      break;
    case TCPIP_MSG_UNTIMEOUT:
      sys_untimeout(msg->msg.tmo.h, msg->msg.tmo.arg);
      memp_free(MEMP_TCPIP_MSG_API, msg);
      break;
    case TCPIP_MSG_CALLBACK:
      msg->msg.cb.function(msg->msg.cb.ctx);
      memp_free(MEMP_TCPIP_MSG_API, msg);
      break;
    case TCPIP_MSG_CALLBACK_STATIC:
      msg->msg.cb.function(msg->msg.cb.ctx);
      break;

    default:
      LWIP_ASSERT("tcpip_thread: invalid message", 0);
      break;
    }
  }
}

枚举类型tcpip_msg_type定义了系统中可能出现的消息类型,消息结构的msg字段是一个共用体union,共用体中定义了各类型消息的具体内容,每种类型的消息对应了共用体中的一个字段,其中注册定时事件和注销定时事件消息共用一个tmo结构、回调事件与静态回调事件消息也共用一个cb结构;API调用与NETIFAPI调用相关的消息具体内容比较多,不宜直接放在tcpip_msg中,系统用了专门的结构体api_msg和netifapimsg来描述对应消息的具体内容,tcpip_msg中只保存了一个指向api_msg和netifapimsg的指针。

2.2 用户进程消息结构

内核进程tcpip_thread负责接收被投递到邮箱中的tcpip_msg消息,那么这个消息是被哪个函数投递的呢?我们以上图为例,查看netconn_bind的代码可知,向邮箱中投递TCPIP_MSG_API类型消息的函数是tcpip_apimsg,函数代码如下:

// rt-thread\components\net\lwip-1.4.1\src\api\api_lib.c
/**
 * Bind a netconn to a specific local IP address and port.
 * Binding one netconn twice might not always be checked correctly!
 * @param conn the netconn to bind
 * @param addr the local IP address to bind the netconn to (use IP_ADDR_ANY
 *             to bind to all addresses)
 * @param port the local port to bind the netconn to (not used for RAW)
 * @return ERR_OK if bound, any other err_t on failure
 */
err_t netconn_bind(struct netconn *conn, ip_addr_t *addr, u16_t port)
{
   
  struct api_msg msg;
  err_t err;

  msg.function = do_bind;
  msg.msg.conn = conn;
  msg.msg.msg.bc.ipaddr = addr;
  msg.msg.msg.bc.port = port;
  err = TCPIP_APIMSG(&msg);

  return err;tcpip
}
// rt-thread\components\net\lwip-1.4.1\src\include\lwip\tcpip.h
#define TCPIP_APIMSG(m)       tcpip_apimsg(m)

// rt-thread\components\net\lwip-1.4.1\src\api\tcpip.c
/**
 * Call the lower part of a netconn_* function
 * This function is then running in the thread context
 * of tcpip_thread and has exclusive access to lwIP core code.
 * @param apimsg a struct containing the function to call and its parameters
 * @return ERR_OK if the function was called, another err_t if not
 */
err_t tcpip_apimsg(struct api_msg *apimsg)
{
   
  struct tcpip_msg msg;
  
  if (sys_mbox_valid(&mbox)) {
   
    msg.type = TCPIP_MSG_API;
    msg.msg.apimsg = apimsg;
    sys_mbox_post(&mbox, &msg);
    sys_arch_sem_wait(&apimsg->msg.conn->op_completed, 0);
    return apimsg->msg.err;
  }
  return ERR_VAL;
}

在tcpip_apimsg函数中完成将api_msg结构的消息封装为tcpip_msg结构的消息,并向邮箱中投递该消息,并阻塞等待信号量,直到内核进程tcpip_thread处理完该消息并释放信号量后,tcpip_apimsg函数获得信号量继续执行。

用户进程调用的API(比如netconn_bind)实际操作的是api_msg结构的消息,在内核进程tcpip_thread中处理TCPIP_MSG_API类型的消息时,也是调用api_msg结构消息中的函数指针,api_msg数据结构的描述如下:

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\api_msg.h

/** This struct includes everything that is necessary to execute a function
    for a netconn in another thread context (mainly used to process netconns
    in the tcpip_thread context to be thread safe). 
    */
struct api_msg_msg {
   
  /** The netconn which to process - always needed: it includes the semaphore
      which is used to block the application thread until the function finished. */
  struct netconn *conn;
  /** The return value of the function executed in tcpip_thread. */
  err_t err;
  /** Depending on the executed function, one of these union members is used */
  union {
   
    /** used for do_send */
    struct netbuf *b;
    /** used for do_newconn */
    struct {
   
      u8_t proto;
    } n;
    /** used for do_bind and do_connect */
    struct {
   
      ip_addr_t *ipaddr;
      u16_t port;
    } bc;
    /** used for do_getaddr */
    struct {
   
      ip_addr_t *ipaddr;
      u16_t *port;
      u8_t local;
    } ad;
    /** used for do_write */
    struct {
   
      const void *dataptr;
      size_t len;
      u8_t apiflags;
    } w;
    /** used for do_recv */
    struct {
   
      u32_t len;
    } r;
    /** used for do_close (/shutdown) */
    struct {
   
      u8_t shut;
    } sd;
#if LWIP_IGMP
    /** used for do_join_leave_group */
    struct {
   
      ip_addr_t *multiaddr;
      ip_addr_t *netif_addr;
      enum netconn_igmp join_or_leave;
    } jl;
#endif /* LWIP_IGMP */
  } msg;
};

/** This struct contains a function to execute in another thread context and
    a struct api_msg_msg that serves as an argument for this function.
    This is passed to tcpip_apimsg to execute functions in tcpip_thread context. 
    */   
struct api_msg {
   
  /** function to execute in tcpip_thread context */
  void (* function)(struct api_msg_msg *msg);
  /** arguments for this function */
  struct api_msg_msg msg;
};

api_msg结构主要由两部分组成:一是希望内核进程执行的API函数do_xxx;另一个是执行相应函数时需要的参数api_msg_msg。api_msg_msg结构包含了三个字段:一是描述连接信息的conn,它包含了与该连接相关的信号量、邮箱等信息,do_xxx执行时要用这些信息来完成与应用进程间的通信和同步;二是内核回调执行结果err,记录内核do_xxx函数的执行结果;三是共用体msg,各个成员记录了各个函数执行时需要的详细参数。

2.3 内核进程API

内核进程tcpip_thread从邮箱中获取到tcpip_msg消息后,看到netconn_bind投递过来的消息类型是TCPIP_MSG_API,根据类型执行回调函数msg->msg.apimsg->function(&(msg->msg.apimsg->msg)),也即api_msg结构定义的函数指针和参数指针,这里的函数指针是do_bind,参数指针除了连接结构conn和执行结果err外还包括本地IP和本地port,实际内核进程执行的是do_bind(msg)。那么,do_bind最终又是如何调用RAW API的呢?或者说do_xxx形式的内核API是如何统一IP RAW、UDP RAW、TCP RAW API的呢?下面下看看do_bind的函数实现代码:

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\api.h

/* Helpers to process several netconn_types by the same code */
#define NETCONNTYPE_GROUP(t)    (t&0xF0)

/** Protocol family and type of the netconn */
enum netconn_type {
   
  NETCONN_INVALID    = 0,
  /* NETCONN_TCP Group */
  NETCONN_TCP        = 0x10,
  /* NETCONN_UDP Group */
  NETCONN_UDP        = 0x20,
  NETCONN_UDPLITE    = 0x21,
  NETCONN_UDPNOCHKSUM= 0x22,
  /* NETCONN_RAW Group */
  NETCONN_RAW        = 0x40
};
// rt-thread\components\net\lwip-1.4.1\src\api\api_msg.c
/**
 * Bind a pcb contained in a netconn
 * Called from netconn_bind.
 * @param msg the api_msg_msg pointing to the connection and containing
 *            the IP address and port to bind to
 */
void do_bind(struct api_msg_msg *msg)
{
   
  if (ERR_IS_FATAL(msg->conn->last_err)) {
   
    msg->err = msg->conn->last_err;
  } else {
   
    msg->err = ERR_VAL;
    if (msg->conn->pcb.tcp != NULL) {
   
      switch (NETCONNTYPE_GROUP(msg->conn->type)) {
   
      case NETCONN_RAW:
        msg->err = raw_bind(msg->conn->pcb.raw, msg->msg.bc.ipaddr);
        break;
      case NETCONN_UDP:
        msg->err = udp_bind(msg->conn->pcb.udp, msg->msg.bc.ipaddr, msg->msg.bc.port);
        break;
      case NETCONN_TCP:
        msg->err = tcp_bind(msg->conn->pcb.tcp, msg->msg.bc.ipaddr, msg->msg.bc.port);
        break;
      default:
        break;
      }
    }
  }
  TCPIP_APIMSG_ACK(msg);
}

在参数指针msg中有一个表示连接的字段conn,其中包含了类型字段conn_type,do_bind函数根据连接类型调用相应的RAW API,实现将多种RAW API抽象为统一的内核进程API(do_xxx的形式)的目的。下面列出内核进程API的其余接口函数:

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\api_msg.h

void do_newconn         ( struct api_msg_msg *msg);		// * Create a new pcb of a specific type inside a netconn, Called from netconn_new_with_proto_and_callback.
void do_delconn         ( struct api_msg_msg *msg);		// * Delete the pcb inside a netconn, Called from netconn_delete.
void do_bind            ( struct api_msg_msg *msg);		// * Bind a pcb contained in a netconn, Called from netconn_bind.
void do_connect         ( struct api_msg_msg *msg);		// * Connect a pcb contained inside a netconn, Called from netconn_connect.
void do_disconnect      ( struct api_msg_msg *msg);		// * Disconnect a pcb contained inside a netconn, Only used for UDP netconns, Called from netconn_disconnect.
void do_listen          ( struct api_msg_msg *msg);		// * Set a TCP pcb contained in a netconn into listen mode, Called from netconn_listen.
void do_send            ( struct api_msg_msg *msg);		// * Send some data on a RAW or UDP pcb contained in a netconn, Called from netconn_send.
void do_recv            ( struct api_msg_msg *msg);		// * Indicate data has been received from a TCP pcb contained in a netconn, Called from netconn_recv.
void do_write           ( struct api_msg_msg *msg);		// * Send some data on a TCP pcb contained in a netconn, Called from netconn_write.
void do_getaddr         ( struct api_msg_msg *msg);		// * Return a connection's local or remote address, Called from netconn_getaddr.
void do_close           ( struct api_msg_msg *msg);		// * Close a TCP pcb contained in a netconn, Called from netconn_close.
void do_shutdown        ( struct api_msg_msg *msg);
#if LWIP_IGMP
void do_join_leave_group( struct api_msg_msg *msg);		// * Join multicast groups for UDP netconns, Called from netconn_join_leave_group.
#endif /* LWIP_IGMP */

#if LWIP_DNS
void do_gethostbyname(void *arg);						// * Execute a DNS query, Called from netconn_gethostbyname.
#endif /* LWIP_DNS */

2.4 内核回调接口

使用Raw/Callback API编程时,用户编程的方法就是向内核注册各种自定义的回调函数,回调是与内核实现交互的唯一方式。协议栈Sequetia API是基于Raw/Callback API来实现的,它与内核交互的方式也只能通过回调,但协议栈Sequetia API的一个重要目的就是简化网络编程(Raw/Callback API编写并注册回调函数的方式没有明确规定用户编程的步骤和规则,使用这种API需要对协议栈内核有透彻的理解),因此协议栈实现了几个默认的回调函数,当为新连接创建内核控制块时,这些回调函数会被默认的注册到控制块中相关字段,这就为内核和协议栈Sequetia API的交互提供了保证。

这些默认的回调函数如下图所示,其中用于RAW的有1个函数(图中省略了),用于UDP的有1个函数,用于TCP的有6个函数:
内核回调接口函数
这些默认的回调函数是在为新连接创建内核控制块时,被默认注册到控制块中相关字段的,默认回调函数注册过程的相关代码如下:

// rt-thread\components\net\lwip-1.4.1\src\api\api_msg.c
/**
 * Create a new pcb of a specific type inside a netconn.
 * Called from netconn_new_with_proto_and_callback.
 * @param msg the api_msg_msg describing the connection type
 */
void do_newconn(struct api_msg_msg *msg)
{
   
  msg->err = ERR_OK;
  if(msg->conn->pcb.tcp == NULL) {
   
    pcb_new(msg);
  }
  TCPIP_APIMSG_ACK(msg);
}

/**
 * Create a new pcb of a specific type.
 * Called from do_newconn().
 * @param msg the api_msg_msg describing the connection type
 * @return msg->conn->err, but the return value is currently ignored
 */
static void pcb_new(struct api_msg_msg *msg)
{
   
  LWIP_ASSERT("pcb_new: pcb already allocated", msg->conn->pcb.tcp == NULL);

  /* Allocate a PCB for this connection */
  switch(NETCONNTYPE_GROUP(msg->conn->type)) {
   
  case NETCONN_RAW:
    msg->conn->pcb.raw = raw_new(msg->msg.n.proto);
    if(msg->conn->pcb.raw == NULL) {
   
      msg->err = ERR_MEM;
      break;
    }
    raw_recv(msg->conn->pcb.raw, recv_raw, msg->conn);
    break;
    
  case NETCONN_UDP:
    msg->conn->pcb.udp = udp_new();
    if(msg->conn->pcb.udp == NULL) {
   
      msg->err = ERR_MEM;
      break;
    }
    
  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流云IoT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值