首页 > 其他分享 > 【lwip】15-NETCONN接口

【lwip】15-NETCONN接口

时间:2023-05-30 12:56:41浏览次数:61  
标签:lwip NETCONN tcp netconn LWIP msg 15 conn

前言

终于到接口层了。

原文:李柱明博客:https://www.cnblogs.com/lizhuming/p/17442931.html

框架描述

前面我们已经学完了,都知道raw接口了,其实也可以直接用,就是麻烦点。

这里NETCONN就是封装了raw接口,让用户使用更加简单。

socket接口是封装NETconn接口的,让用户使用更加标准,方便应用程序移植。

NETCONN的接口框架:

解耦:编写回调函数xxx_tcp()、xxx_udp(),注册到协议栈里面。协议栈通过回调函数告知接口层,当前PCB的状态信息。接口层根据当前PCB的状态信息做相应处理即可。

文件:

  • api_msg.c​:构建api msg,被netconn调用,发送到内核锁或tcpip内核线程执行指定的回调函数。
  • api_lib.c​:netconn API。
  • sockets.c​:socket套接字接口层,封装netconn接口。供用户使用。

NETCONN重要组成

内核回调接口

在raw/callback API编程时,用户编程的方法就是向内核注册各种自定义的回调函数,回调函数是与内核实现交互的唯一方式

协议栈API NETCONN是基于raw/callback API实现的。

内核通过调用注册到TCP/UDP内核的回调函数,把接收到的数据或可发送数据的事件发送回netconn对应的邮箱中,上层检查这些邮箱即可和内核协议栈交互。

netbuf:数据缓冲

netbuf是应用层描述待发送数据和已接收数据的基本结构。当然,最基本的粒度数据结构还是pbuf。

应用层接收数据:

  • 当协议栈收到数据包后,会将数据封装在一个netbuf​中,并递交给应用层。

应用层发送数据:

  • TCP:用户只需要提供待发数据的起始地址和长度,内核会根据实际情况封装在合适大小的数据包中。
  • UDP:需要用户自行将数据封装在netbuf​​结构中。

netbuf数据结构:

/** "Network buffer" - contains data and addressing info */
struct netbuf {
  struct pbuf *p, *ptr; /* 包缓冲区。p:pbuf链。ptr:pbuf链中当前pbuf游标。 */
  ip_addr_t addr; /* 发送方IP */
  u16_t port; /* 发送方端口 */
#if LWIP_NETBUF_RECVINFO || LWIP_CHECKSUM_ON_COPY
  u8_t flags; /* 标志位 */
  u16_t toport_chksum; /* 目的端口号。用于checksum */
#if LWIP_NETBUF_RECVINFO
  ip_addr_t toaddr; /* 目的地址 */
#endif /* LWIP_NETBUF_RECVINFO */
#endif /* LWIP_NETBUF_RECVINFO || LWIP_CHECKSUM_ON_COPY */
};

netconn:接口数据结构

代表一个连接,TCP或UDP等。

相关文件:api.h

分析完UDP和TCP协议实现后,会分析他们的原生接口udp_xxx()和tcp_xxx()都是互相独立的。

而连接结构netconn就是为了统一这些接口。

netconn控制块:

/** A netconn descriptor */
struct netconn {
  /** type of the netconn (TCP, UDP or RAW) */
  enum netconn_type type; /* 连接类型 */
  enum netconn_state state; /* netconn当前状态。即是当前netconn被哪些netconn API占用 */
  union {
    struct ip_pcb  *ip;   /* IP控制块 */
    struct tcp_pcb *tcp;  /* TCP控制块 */
    struct udp_pcb *udp;  /* UDP控制块 */
    struct raw_pcb *raw;  /* TCP控制块 */
  } pcb;  /* 内核中与连接相关的控制块指针 */
  /* 此netconn的最新未报告的异步错误 */
  err_t pending_err;
#if !LWIP_NETCONN_SEM_PER_THREAD /* 只能每个netconn数据结构占用一个信号量 */

  /* 信号量。是对一个API完成两部分线程的同步。如用户调用API,API调用内核API,并等待内核API完成后通过该信号量通知当前API。 */
  sys_sem_t op_completed;
#endif

  /* 接收数据的邮箱。数据缓冲队列。 */
  sys_mbox_t recvmbox;
#if LWIP_TCP

  /* 用于TCP服务器端。连接请求的缓冲队列。 */
  sys_mbox_t acceptmbox;
#endif /* LWIP_TCP */
#if LWIP_NETCONN_FULLDUPLEX /* 全功率 */

  /* mbox的读阻塞线程数。当线程在waiting时closing,需要解除所有线程的阻塞。 */
  int mbox_threads_waiting;
#endif
  union {
    int socket; /* socket */
    void *ptr;  /* 指针 */
  } callback_arg; /* 回调参数 */
#if LWIP_SO_SNDTIMEO /* 发送超时 */

  /* 等待发送数据超时值,单位ms。 */
  s32_t send_timeout;
#endif /* LWIP_SO_RCVTIMEO */
#if LWIP_SO_RCVTIMEO /* 接收超时 */

  /* 等待接收新数据的超时时间,单位ms。 */
  u32_t recv_timeout;
#endif /* LWIP_SO_RCVTIMEO */
#if LWIP_SO_RCVBUF /* 接收缓冲区 */

  /* 应用层的接收缓冲区size。限制recvmbox上所有数据的size。 */
  int recv_bufsize;

  /* recvmbox 当前接收到的数据size,用于FIONREAD。 */
  int recv_avail;
#endif /* LWIP_SO_RCVBUF */
#if LWIP_SO_LINGER /* SO_LINGER选项 */

  /*  < 0: 关闭该功能。
      = 0: 立即关闭。发送缓冲区残留有数据时,RST给对端。
      > 0: 超时值。单位:秒。超时前尽量把发送缓冲区中的数据发送出去。 */
  s16_t linger;
#endif /* LWIP_SO_LINGER */

  /* 包含更多的netconn-internal状态。参考NETCONN_FLAG_x宏 */
  u8_t flags;
#if LWIP_TCP

  /* 当调用netconn_write()函数发送的数据不适合发送到缓冲区时,
      数据会暂时存储在current_msg中,等待数据合适的时候进行发送。 */
  struct api_msg *current_msg;
#endif /* LWIP_TCP */
  /* netconn相关的回调函数。socket API时使用。 */
  netconn_callback callback;
};

部分变量描述:

  • type​:连接类型:TCP、UDP、RAW。
  • state​:当前连接状态。
  • pcb​:协议栈内核连接控制块:TCP控制块、UDP控制块等等。
  • err​:记录当前连接上函数调用的执行结果。
  • op_completed​:是上下两部分API实现同步的重要字段,netconn_xxx()​函数在投递完消息后,便会阻塞在连接的这个信号量上,当内核的do_xxx()​执行完成后便释放信号量。
  • recvmbox​:该连接的数据接收邮箱,也是缓冲队列。内核接收到属于该连接的数据包(封装在netbuf中)投递到该邮箱。应用程序调用数据接收函数,就是从该队列中读取数据。
  • recv_avail​:记录当前接收邮箱中已经缓冲好的数据总长度。
  • acceptmbox​:该连接作为TCP服务器时使用到,内核会把所有新建立好的连接结构netconn​投递到该邮箱,服务器程序调用netconn_accept()​函数便会得到一个新的连接结构。
  • mbox_threads_waiting​:表示读阻塞在当前连接的应用程序数量,在关闭连接时,需要往recvmbox​邮箱发送mbox_threads_waiting​个邮件来解除这些应用层的阻塞。

NETCONN支持的协议类型

/** @ingroup netconn_common
 * 协议族和netconn类型。
 *
 */
enum netconn_type {
  NETCONN_INVALID     = 0, /* 无效类型 */
  /** TCP IPv4 */
  NETCONN_TCP         = 0x10,
#if LWIP_IPV6
  /** TCP IPv6 */
  NETCONN_TCP_IPV6    = NETCONN_TCP | NETCONN_TYPE_IPV6 /* 0x18 */,
#endif /* LWIP_IPV6 */
  /** UDP IPv4 */
  NETCONN_UDP         = 0x20,
  /** UDP IPv4 lite */
  NETCONN_UDPLITE     = 0x21,
  /** UDP IPv4 no checksum */
  NETCONN_UDPNOCHKSUM = 0x22,

#if LWIP_IPV6
  /** UDP IPv6 (dual-stack by default, unless you call @ref netconn_set_ipv6only) */
  NETCONN_UDP_IPV6         = NETCONN_UDP | NETCONN_TYPE_IPV6 /* 0x28 */,
  /** UDP IPv6 lite (dual-stack by default, unless you call @ref netconn_set_ipv6only) */
  NETCONN_UDPLITE_IPV6     = NETCONN_UDPLITE | NETCONN_TYPE_IPV6 /* 0x29 */,
  /** UDP IPv6 no checksum (dual-stack by default, unless you call @ref netconn_set_ipv6only) */
  NETCONN_UDPNOCHKSUM_IPV6 = NETCONN_UDPNOCHKSUM | NETCONN_TYPE_IPV6 /* 0x2a */,
#endif /* LWIP_IPV6 */

  /** Raw connection IPv4 */
  /* RAW ipv4 连接 */
  NETCONN_RAW         = 0x40
#if LWIP_IPV6
  /** Raw connection IPv6 (dual-stack by default, unless you call @ref netconn_set_ipv6only) */
  , NETCONN_RAW_IPV6    = NETCONN_RAW | NETCONN_TYPE_IPV6 /* 0x48 */
#endif /* LWIP_IPV6 */
};

NETCONN状态

/* 当前netconn接口数据结构所处的状态。
    如当前netconn被netconn_write()接口调用,就处于WRITE状态。
    也可以理解为当前netconn被哪些netconn API占用 */
enum netconn_state {
  NETCONN_NONE,     /* 空闲状态 */
  NETCONN_WRITE,    /* 正在发送数据 */
  NETCONN_LISTEN,   /* 侦听状态 */
  NETCONN_CONNECT,  /* 连接状态 */
  NETCONN_CLOSE     /* 关闭状态 */
};

NETCONN北向事件

上层收:按次数统计。NETCONN中有多少个可被上层接收。

上层发:按是否有无统计。NETCONN中是否可写。

/*
 * netconn_x()API 通知更上层(如socket)的事件。
 *
 * 事件说明:
 * 在netconn实现中,有三种方法来阻塞客户端:
 * - accept mbox:netconn_accept()函数中的sys_arch_mbox_fetch()
 * - receive mbox:netconn_recv_data()函数中的sys_arch_mbox_fetch()
 * - send queue if full:lwip_netconn_do_write()函数中的sys_arch_sem_wait()
 *
 * 这些事件都是给这些mboxes/semaphores标记状态的事件。
 * 对于非阻塞式的连接,我们可以通过这些事件,提前知道调用netconn API是否会阻塞。
 *
 * NETCONN_EVT_RCVPLUS: 加。mboxes/semaphores 对象,可安全调用相关netconn API不会被阻塞的次数+1。
 * 如在sockets中是按次计数:如accept mbox连续收到三个NETCONN_EVT_RCVPLUS事件,
 * 则可以连续三次调用netconn_accept()不会被阻塞。receive mbox也一样。
 *
 * NETCONN_EVT_RCVMINUS: 减。mboxes/semaphores 对象,可安全调用相关netconn API不会被阻塞的次数-1。
 * 一般在调用对应函数成功后,统计一次。
 *
 * 而对于TX,没有次数统计,只是一个标志。
 *
 * NETCONN_EVT_SENDPLUS: 表示调用netconn_send()发送数据不会阻塞。
 * 一般发生在发送缓冲区中的数据被ACK了,缓冲区空闲空间增加时会回调该事件到上层。
 *
 * NETCONN_EVT_SENDMINUS: 表示调用netconn_send()会阻塞。
 * 一般发生在协议栈内部PCB不可发送数据时会通过该事件通知上层,此时调用netconn_send()会阻塞,如发送缓冲区不足,内存不足等等。
 * 触发该事件后,内部PCB会在pcb->poll()函数会检查PCB是否可发送数据,如果可发,就会触发NETCONN_EVT_SENDPLUS事件通知上层。
 *
 */
enum netconn_evt {
  NETCONN_EVT_RCVPLUS,  /* 收到数据。可安全调用API不会被阻塞次数+1 */
  NETCONN_EVT_RCVMINUS, /* 可安全调用API不会被阻塞次数-1 */
  NETCONN_EVT_SENDPLUS, /* PCB可发送数据事件 */
  NETCONN_EVT_SENDMINUS,/* PCB不可发送事件 */
  NETCONN_EVT_ERROR     /* 错误事件 */
};

NETBUF相关接口

前提实现可以自行看源码。

netbuf​操作接口:

/* Network buffer functions: */
struct netbuf *   netbuf_new      (void);
void              netbuf_delete   (struct netbuf *buf);
void *            netbuf_alloc    (struct netbuf *buf, u16_t size);
void              netbuf_free     (struct netbuf *buf);
err_t             netbuf_ref      (struct netbuf *buf,
                                   const void *dataptr, u16_t size);
void              netbuf_chain    (struct netbuf *head, struct netbuf *tail);

err_t             netbuf_data     (struct netbuf *buf,
                                   void **dataptr, u16_t *len);
s8_t              netbuf_next     (struct netbuf *buf);
void              netbuf_first    (struct netbuf *buf);

内核回调接口

在学完TCP、UDP内核实现后,就知道我们需要往这些内核里注册回调函数,用于内核和上层交互。如tcp的tcp_recv()​就是往内核注册接收回调函数。

所以在实现NETCONN接口时,需要编写这些回调函数,并注册到内核中。

注册NETCONN相关回调到内核

TCP:setup_tcp()​:

/**
 * 注册netconn tcp基础接口相关的回调到TCP层
 *
 */
static void
setup_tcp(struct netconn *conn)
{
  struct tcp_pcb *pcb;

  pcb = conn->pcb.tcp;
  tcp_arg(pcb, conn);    // PCB绑定NETCONN接口控制块
  tcp_recv(pcb, recv_tcp);    // 注册接收回调
  tcp_sent(pcb, sent_tcp);    // 注册发送回调
  tcp_poll(pcb, poll_tcp, NETCONN_TCP_POLL_INTERVAL);    // 注册poll
  tcp_err(pcb, err_tcp);    // 注册异常回调
}

recv_tcp:TCP接收回调

recv_tcp()​是TCP netconn注册到tcp的tcp_pcb->recv()​接收回调函数。

TCP内核收到数据后会通过当前回调函数发送数据包到conn->recvmbox​,如果投递失败,则不能删除这些pbuf​,因为tcp_fasttmr()​会在后面再次通知我们上层接收。

这里发送失败但是不能删除这些pbuf​的原因:我们TCP已经ACK了这些数据,对端不会再发这些数据了的,所以我们不能完全删除,只能晚点上交给应用层。

recv_udp()​略有区别,这里不封装netbuf​,在调用上层调用netconn_recv()​函数中再把pbuf​装成netbuf​。

这样做的目的是因为TCP数据包的封装、处理设计其它很多额外的操作,而当前函数却是一个回调函数,不适合多做业务及长时间占有。

static err_t
recv_tcp(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err)
{
  struct netconn *conn;
  u16_t len;
  void *msg;

  LWIP_UNUSED_ARG(pcb);
  LWIP_ASSERT("recv_tcp must have a pcb argument", pcb != NULL);
  LWIP_ASSERT("recv_tcp must have an argument", arg != NULL);
  LWIP_ASSERT("err != ERR_OK unhandled", err == ERR_OK);
  LWIP_UNUSED_ARG(err); /* for LWIP_NOASSERT */
  conn = (struct netconn *)arg;

  if (conn == NULL) {
    return ERR_VAL;
  }
  LWIP_ASSERT("recv_tcp: recv for wrong pcb!", conn->pcb.tcp == pcb);

  if (!NETCONN_MBOX_VALID(conn, &conn->recvmbox)) {
    /* recvmbox已经被删除了。如shutdown RX */
    if (p != NULL) {
      tcp_recved(pcb, p->tot_len); /* 把这些数据从TCP接收缓冲区中全部读走,并更新接收窗口 */
      pbuf_free(p); /* 然后释放这些pbuf */
    }
    return ERR_OK; /* 算是接收成功 */
  }

  /* 与UDP或RAW pcb不同,不要使用recv_avail检查可用空间,因为这可能会破坏连接。
    (这些数据都是已经被我们ACK了的) */

  if (p != NULL) { /* 有数据 */
    msg = p;
    len = p->tot_len;
  } else { /* 没数据也触发当前回调,说明TCP协议栈底层是想表示连接已断开 */
    msg = LWIP_CONST_CAST(void *, &netconn_closed);
    len = 0;
  }

  if (sys_mbox_trypost(&conn->recvmbox, msg) != ERR_OK) {
    /* 不要释放p:它稍后会从tcp_fasttmr再次给我们! */
    return ERR_MEM;
  } else {
#if LWIP_SO_RCVBUF
    SYS_ARCH_INC(conn->recv_avail, len);
#endif /* LWIP_SO_RCVBUF */
    /* 通知上层,有数据可读 */
    API_EVENT(conn, NETCONN_EVT_RCVPLUS, len);
  }

  return ERR_OK;
}

sent_tcp:TCP发送回调

sent_tcp()​是TCP netconn注册到tcp的tcp_pcb->sent()​发送回调函数。

当TCP收到更多ACK,发送缓冲区可用空间增大了,就会调用当前回调函数。

主要是唤醒阻塞等待连接关闭或数据发送的应用程序线程。

检查和通知接口层(netconn、socket),有更多缓冲空间了,如果有数据,可以发过来。

static err_t
sent_tcp(void *arg, struct tcp_pcb *pcb, u16_t len)
{
  struct netconn *conn = (struct netconn *)arg;

  LWIP_UNUSED_ARG(pcb);
  LWIP_ASSERT("conn != NULL", (conn != NULL));

  if (conn) { /* 接口连接还存在 */
    if (conn->state == NETCONN_WRITE) { /* 接口层需要发送数据 */
      lwip_netconn_do_writemore(conn  WRITE_DELAYED); /* 把数据写入TCP发送缓冲区 */
    } else if (conn->state == NETCONN_CLOSE) {  /* 接口层已经关闭了当前连接 */
      lwip_netconn_do_close_internal(conn  WRITE_DELAYED);  /* TCP内部资源也关闭 */
    }

    /* 检查水位线:TCP发送缓冲区 可用空间size在水位线上 && 当前pbuf数量在水位线下
        即可通知上层,可往TCP发送缓冲区写入数据。 */
    if ((conn->pcb.tcp != NULL) && (tcp_sndbuf(conn->pcb.tcp) > TCP_SNDLOWAT) &&
        (tcp_sndqueuelen(conn->pcb.tcp) < TCP_SNDQUEUELOWAT)) {
      netconn_clear_flags(conn, NETCONN_FLAG_CHECK_WRITESPACE); /* 清除 检查缓冲区可写 标志 */
      API_EVENT(conn, NETCONN_EVT_SENDPLUS, len); /* 通知接口层,当前TCP发送缓冲区可写 */
    }
  }

  return ERR_OK;
}

poll_tcp:TCP poll函数

poll_tcp()​是TCP netconn注册到tcp的tcp_pcb->poll()​周期函数。

tcp_pcb->poll()​被TCP慢时钟tcp_slowtmr()​时钟调用。

NETCONN_TCP_POLL_INTERVAL==2​,表示每秒会轮询一次该函数。

主要是唤醒阻塞等待连接关闭或数据发送的应用程序线程。

解除应用程序线程阻塞的方式:发送信号量conn->sem。

如果关闭失败,netconn_close()等待conn->sem。

static err_t
poll_tcp(void *arg, struct tcp_pcb *pcb)
{
  struct netconn *conn = (struct netconn *)arg;

  LWIP_UNUSED_ARG(pcb);
  LWIP_ASSERT("conn != NULL", (conn != NULL));

  if (conn->state == NETCONN_WRITE) { /* 如果netconn处于正在发送数据状态,那tcp层就继续从netconn取数据发出去 */
    lwip_netconn_do_writemore(conn  WRITE_DELAYED);
  } else if (conn->state == NETCONN_CLOSE) { /* netconn已经close当前连接了,内部tcp层也要close */
#if !LWIP_SO_SNDTIMEO && !LWIP_SO_LINGER /* 没开socket发送超时 && 没开close()后残留数据超时 */
    if (conn->current_msg && conn->current_msg->msg.sd.polls_left) {
      conn->current_msg->msg.sd.polls_left--;
    }
#endif /* !LWIP_SO_SNDTIMEO && !LWIP_SO_LINGER */
    lwip_netconn_do_close_internal(conn  WRITE_DELAYED);
  }

  /* 之前是否有非阻塞的写操作失败?有就检查写缓冲区是否有可用空间 */
  if (conn->flags & NETCONN_FLAG_CHECK_WRITESPACE) { /* 之前存在非阻塞写入失败 */
    /* 检查发送缓冲区:缓冲区可用size是否足够 和 pbuf数量是否超限 */
    if ((conn->pcb.tcp != NULL) && (tcp_sndbuf(conn->pcb.tcp) > TCP_SNDLOWAT) &&
        (tcp_sndqueuelen(conn->pcb.tcp) < TCP_SNDQUEUELOWAT)) {
      netconn_clear_flags(conn, NETCONN_FLAG_CHECK_WRITESPACE); /* tcp层有更多的发送缓冲区空间可用,则清除该标记 */
      API_EVENT(conn, NETCONN_EVT_SENDPLUS, 0); /* 触发一个可写事件到netconn的上层(如socket层) */
    }
  }

  return ERR_OK;
}

err_tcp:TCP 异常回调函数

poll_tcp()​是TCP netconn注册到tcp的tcp_pcb->errf()​异常回调函数。

TCP PCB出现错误时,会调用当前函数回调到接口层处理:

  • 向netconn数据结构中的回调函数发送ERROR​、RCVPLUS​、SENDPLUS​事件;
  • 向netconn数据结构中的所有邮箱recv_mboxes​、accept_mboxes​发送异常事件;

这种做法的目的就是唤醒因各种情况而阻塞的应用程序,告知当前连接发生错误,需要处理。

static void
err_tcp(void *arg, err_t err)
{
  struct netconn *conn;
  enum netconn_state old_state;
  void *mbox_msg;
  SYS_ARCH_DECL_PROTECT(lev);

  conn = (struct netconn *)arg;
  LWIP_ASSERT("conn != NULL", (conn != NULL));

  SYS_ARCH_PROTECT(lev); /* 系统保护:进入临界 */

  /* 发生错误,PCB就会被释放,所以可在接口层解除绑定 */
  conn->pcb.tcp = NULL;
  /* 保存错误码 */
  conn->pending_err = err;
  /* 防止应用程序线程在'recvmbox'/'acceptmbox'上阻塞 */
  conn->flags |= NETCONN_FLAG_MBOXCLOSED;

  /* 在唤醒其它线程前,重置当前状态 */
  old_state = conn->state;
  conn->state = NETCONN_NONE;

  SYS_ARCH_UNPROTECT(lev); /* 退出临界 */

  /* 通知socket层,当前连接异常。 */
  API_EVENT(conn, NETCONN_EVT_ERROR, 0);
  /* 给socket层一个可读、可写事件,可让应用层不会阻塞于读、写。 */
  API_EVENT(conn, NETCONN_EVT_RCVPLUS, 0);
  API_EVENT(conn, NETCONN_EVT_SENDPLUS, 0);

  mbox_msg = lwip_netconn_err_to_msg(err); /* err翻译成msg */
  /* 通过error message到recvmbox来唤醒阻塞于recv的应用层线程 */
  if (NETCONN_MBOX_VALID(conn, &conn->recvmbox)) {
    /* use trypost to prevent deadlock */
    /* 使用trypost,可以防止死锁 */
    sys_mbox_trypost(&conn->recvmbox, mbox_msg);
  }
  /* 通过error message到acceptmbox来唤醒阻塞于accept的应用层线程 */
  if (NETCONN_MBOX_VALID(conn, &conn->acceptmbox)) {
    /* 使用trypost,可以防止死锁 */
    sys_mbox_trypost(&conn->acceptmbox, mbox_msg);
  }

  if ((old_state == NETCONN_WRITE) || (old_state == NETCONN_CLOSE) ||
      (old_state == NETCONN_CONNECT)) { /* 处于非监听的所有有效态 */
    /* PCB已经被干掉了,所以没必要调用lwip_netconn_do_writemore()、lwip_netconn_do_close_internal()这些函数了 */
    int was_nonblocking_connect = IN_NONBLOCKING_CONNECT(conn); /* 获取当前netconn是否处于非阻塞连接 */
    SET_NONBLOCKING_CONNECT(conn, 0); /* 清除netconn中该标记 */

    if (!was_nonblocking_connect) { /* 不处于非阻塞连接状态 */
      sys_sem_t *op_completed_sem;
      /* set error return code */
      LWIP_ASSERT("conn->current_msg != NULL", conn->current_msg != NULL);
      if (old_state == NETCONN_CLOSE) {
        /* netconn处于close状态,则返回OK,表示close成功 */
        conn->current_msg->err = ERR_OK;
      } else {
        /* 如果处于写或连接状态,则返回对应ERR,表示当前连接异常。 */
        conn->current_msg->err = err;
      }
      /* 获取当前netconn的同步信号量 */
      op_completed_sem = LWIP_API_MSG_SEM(conn->current_msg);
      LWIP_ASSERT("invalid op_completed_sem", sys_sem_valid(op_completed_sem));
      conn->current_msg = NULL; /* 解绑netconn中的当前的同步信号量 */
      /* 唤醒阻塞与写或连接的应用程序线程 */
      sys_sem_signal(op_completed_sem);
    } else { /* 应用程序线程是非阻塞连接 */
      /* @todo: 测试非阻塞连接的错误情况 */
    }
  } else {
    /* netconn处于监听态或空闲态 */
    LWIP_ASSERT("conn->current_msg == NULL", conn->current_msg == NULL);
  }
}

accept_function:TCP accept回调函数

accept_function()​是TCP netconn注册到tcp的lpcb->accept()​accept回调函数。

  • 使用tcp_accept()​API注册。
  • lpcb->accept()​,用于TCP服务器,监听类型的pcb。
  • TCP层收到客户端连接,分配PCB,并握手成功后会调用当前回调。(tcp申请新的客户端PCB失败时也会回调)

其他需要注册到协议栈内核的回调函数

recv_udp()​:

  • udp的接收回调函数,该函数会被udp_recv()函数注册到UDP PCB中。在UDP收到数据时被调用,用于回调数据到上层。
  • 是UDP收到的数据包pbuf,组装成一个上层数据包格式netbuf。
  • 然后把这个netbuf投递到连接netconn->recvmbox接收邮箱中。
  • 应用层可以通过调用API函数netconn_recv()从该邮箱中获取netbuf格式的数据包,然后提取出pbuf,再提取出UDP数据即可。

NETCONN接口线程安全实现(重要)

NETCONN接口线程安全原理

通过lwip内核实现的学习,我们知道,lwip内核实现是需要线程安全的。

目前有两种方式:

  1. 开启LWIP_TCPIP_CORE_LOCKING​内核安全锁功能,使用该锁来实现lwip内核的线程安全。
  2. 如果没有开启内核安全锁,则可以把需要执行的lwip内核API通过API消息发送到LWIP内核线程去执行。

netconn用户接口形式:netconn_xxx()

netconn内核接口形式:lwip_netconn_xxx()

用户调用netconn用户接口时,用户接口的目的就是把netconn的内核接口通过安全锁或api消息发送到wlip内核线程执行。

NETCONN接口线程安全具体实现

netconn用户接口使用netconn_apimsg()​-->tcpip_send_msg_wait_sem()​来共同实现。

netconn_apimsg()​:

  • tcpip_callback_fn fn​:需要线程安全的netconn内核API。
  • struct api_msg *apimsg​:API的指针形参(既然形参是指针,说明是双向参数)
static err_t
netconn_apimsg(tcpip_callback_fn fn, struct api_msg *apimsg)
{
  err_t err;

#ifdef LWIP_DEBUG
  /* 捕获不设置错误的函数 */
  apimsg->err = ERR_VAL;
#endif /* LWIP_DEBUG */

#if LWIP_NETCONN_SEM_PER_THREAD
  apimsg->op_completed_sem = LWIP_NETCONN_THREAD_SEM_GET(); /* 获取同步信号量 */
#endif /* LWIP_NETCONN_SEM_PER_THREAD */

  /* 把fn()搞到tcpip内核锁内执行 */
  err = tcpip_send_msg_wait_sem(fn, apimsg, LWIP_API_MSG_SEM(apimsg));
  if (err == ERR_OK) {
    return apimsg->err;
  }
  return err;
}

tcpip_send_msg_wait_sem()​:

  • tcpip_callback_fn fn​:需要线程安全的netconn内核API。
  • void *apimsg​:API的指针形参。
  • sys_sem_t *sem​:同步信号量。用于阻塞。
  • 发送一个回调函数到TCPIP线程执行的步骤:构建tcpip_msg​,发送到tcpip_mbox​,由TCPIP内核线程监测、执行。
  • 调用者线程阻塞在自己指定的同步信号量上,函数执行完毕后,由回调函数fn​释放该同步信号量来解除调用者线程阻塞。
  • 建议:使用LWIP_TCPIP_CORE_LOCKING​内核安全锁,因为这是运行时开销最小的方法。
err_t
tcpip_send_msg_wait_sem(tcpip_callback_fn fn, void *apimsg, sys_sem_t *sem)
{
#if LWIP_TCPIP_CORE_LOCKING /* 开启了内核锁,直接在当前线程调用即可 */
  LWIP_UNUSED_ARG(sem);
  LOCK_TCPIP_CORE();  /* 内核锁上锁 */
  fn(apimsg); /* 执行回调 */
  UNLOCK_TCPIP_CORE();  /* 释放内核锁 */
  return ERR_OK;

#else /* LWIP_TCPIP_CORE_LOCKING */ /* 没有开启内核锁,需要把回调函数外包到TCPIP内核线程 */

  TCPIP_MSG_VAR_DECLARE(msg); /* 定义一个tcpip_msg */

  LWIP_ASSERT("semaphore not initialized", sys_sem_valid(sem));
  LWIP_ASSERT("Invalid mbox", sys_mbox_valid_val(tcpip_mbox));

  TCPIP_MSG_VAR_ALLOC(msg); /* 开了MPU,这个就为NULL了 */
  TCPIP_MSG_VAR_REF(msg).type = TCPIP_MSG_API;  /* 无回传的API消息类型 */
  TCPIP_MSG_VAR_REF(msg).msg.api_msg.function = fn; /* API */
  TCPIP_MSG_VAR_REF(msg).msg.api_msg.msg = apimsg;  /* apimsg */
  sys_mbox_post(&tcpip_mbox, &TCPIP_MSG_VAR_REF(msg));  /* 往tcpip_mbox发送一个tcpip_msg */
  sys_arch_sem_wait(sem, 0);  /* 等待同步信号量被回调函数fn()释放 */
  TCPIP_MSG_VAR_FREE(msg);  /* 释放tcpip_msg */
  return ERR_OK;
#endif /* LWIP_TCPIP_CORE_LOCKING */
}

这个同步信号量,就是用于阻塞的,具体是:netconn接口控制块中的op_completed​信号量。

#define LWIP_API_MSG_SEM(msg)          (&(msg)->conn->op_completed)

接口实现例子

通过一个API例子来实例化线程安全的使用。

用户调用netconn用户接口netconn_new()​,其实就是netconn_new_with_proto_and_callback()​:

#define netconn_new(t)                  netconn_new_with_proto_and_callback(t, 0, NULL)

netconn_new_with_proto_and_callback()​源码实现如下:

  • 主要内容就是申请api_msg​资源,把需要内核执行的netconn内核接口和该接口需要的数据打包到api_msg​。然后将该msg发送到lwip内核(或上锁)执行。如果是发送到lwip内核,则当前线程会等待同步信号量conn->op_completed​,如果内核执行了netconn内核接口,这个接口会释放该信号量,表示内核已经执行了对应API。
struct netconn *
netconn_new_with_proto_and_callback(enum netconn_type t, u8_t proto, netconn_callback callback)
{
  struct netconn *conn;
  API_MSG_VAR_DECLARE(msg); // 定义一个api_msg数据结构
  API_MSG_VAR_ALLOC_RETURN_NULL(msg); // 申请api_msg数据结构资源,指定错误时返回NULL

  conn = netconn_alloc(t, callback); // 申请netconn控制块资源
  if (conn != NULL) {
    err_t err;

    API_MSG_VAR_REF(msg).msg.n.proto = proto; // 把用户连接协议记录到api_msg中
    API_MSG_VAR_REF(msg).conn = conn; // 把netconn控制块记录到api_msg中
    // 把这个api_msg资源和lwip_netconn_do_newconn()函数封装好,在线程安全下跑(上锁或发送到lwip内核)
    err = netconn_apimsg(lwip_netconn_do_newconn, &API_MSG_VAR_REF(msg));
    if (err != ERR_OK) {
      LWIP_ASSERT("freeing conn without freeing pcb", conn->pcb.tcp == NULL);
      LWIP_ASSERT("conn has no recvmbox", sys_mbox_valid(&conn->recvmbox));
#if LWIP_TCP
      LWIP_ASSERT("conn->acceptmbox shouldn't exist", !sys_mbox_valid(&conn->acceptmbox));
#endif /* LWIP_TCP */
#if !LWIP_NETCONN_SEM_PER_THREAD
      LWIP_ASSERT("conn has no op_completed", sys_sem_valid(&conn->op_completed));
      sys_sem_free(&conn->op_completed);
#endif /* !LWIP_NETCONN_SEM_PER_THREAD */
      sys_mbox_free(&conn->recvmbox);
      memp_free(MEMP_NETCONN, conn);
      API_MSG_VAR_FREE(msg);
      return NULL;
    }
  }
  API_MSG_VAR_FREE(msg); // 释放api_msg资源
  return conn;
}

lwip_netconn_do_newconn()​:

  • 创建一个新的特定类型的PCB。
  • 执行完毕,释放信号量,解除调用者线程阻塞。
void
lwip_netconn_do_newconn(void *m)
{
  struct api_msg *msg = (struct api_msg *)m;

  msg->err = ERR_OK;
  if (msg->conn->pcb.tcp == NULL) {
    pcb_new(msg); // 创建一个新的特定类型的PCB。
  }

  /* 释放信号量 */
  TCPIP_APIMSG_ACK(msg);
}

NETCONN内核接口

参考./src/include/lwip/priv/api_msg.h

netconn内核接口是在LWIP线程安全的下运行的,要么上lwip内核资源锁,要么发送到lwip内核线程去执行,这些操作俊友netconn用户接口去实现。

netconn内核接口主要封装各个协议栈的RAW接口实现,如果线程安全是发送到lwip内核实现,则需要在业务执行完毕后调用TCPIP_APIMSG_ACK(msg);​来解除调用者线程的阻塞。

部分接口列表:

void lwip_netconn_do_newconn         (void *m);
void lwip_netconn_do_delconn         (void *m);
void lwip_netconn_do_bind            (void *m);
void lwip_netconn_do_bind_if         (void *m);
void lwip_netconn_do_connect         (void *m);
void lwip_netconn_do_disconnect      (void *m);
void lwip_netconn_do_listen          (void *m);
void lwip_netconn_do_send            (void *m);
void lwip_netconn_do_recv            (void *m);
#if TCP_LISTEN_BACKLOG
void lwip_netconn_do_accepted        (void *m);
#endif /* TCP_LISTEN_BACKLOG */
void lwip_netconn_do_write           (void *m);
void lwip_netconn_do_getaddr         (void *m);
void lwip_netconn_do_close           (void *m);
void lwip_netconn_do_shutdown        (void *m);
#if LWIP_IGMP || (LWIP_IPV6 && LWIP_IPV6_MLD)
void lwip_netconn_do_join_leave_group(void *m);
void lwip_netconn_do_join_leave_group_netif(void *m);
#endif /* LWIP_IGMP || (LWIP_IPV6 && LWIP_IPV6_MLD) */

#if LWIP_DNS
void lwip_netconn_do_gethostbyname(void *arg);
#endif /* LWIP_DNS */

NETCONN用户接口

netconn接口就不分析源码了,直接描述功能。

这些接口在./src/include/lwip/api.h​路径中。

下面只列出部分API,这些API都有兄嘚API,可以查看上述路径。

这些接口都是把netconn内核接口封装到api_msg​中,然后将其转发到lwip内核线程(或上锁)执行。

netconn_new():新建一个netconn接口控制块

是一个宏,实际是netconn_new_with_proto_and_callback()​:

#define netconn_new(t)                  netconn_new_with_proto_and_callback(t, 0, NULL)

struct netconn *
netconn_new_with_proto_and_callback(enum netconn_type t, u8_t proto, netconn_callback callback)
{}

创建一个新的netconn,指定协议,指定回调函数。

协议类型查看netconn_type​。

回调函数查看netconn_callback:

/* 通知netconn事件的回调原型 */
typedef void (* netconn_callback)(struct netconn *, enum netconn_evt, u16_t len);

netconn_delete():删除一个netconn接口控制块

netconn_delete()​函数关闭一个netconn“连接”并释放它的资源。

UDP和RAW连接是完全关闭的,TCP pcb可能仍然在等待状态后返回。

netconn_getaddr():获取地址信息

获取netconn的local或remote的IP地址和端口号。

对于RAW类型的netconn,返回的不是端口号,而是协议。

netconn_bind():绑定本地IP&PORT

netconn绑定指定的local IP地址和端口号。

一个netconn连续两次绑定同一个IP(注意任意IP)和端口号,第二次会响应绑定失败。

netconn_connect():连接远端

netconn连接到指定的remote IP和端口号。

netconn_disconnect():断开连接

netconn断开当前连接(仅对UDP netconn有效)

netconn_listen():监听

设置一个TCP netconn进入监听模式,设置backlog数量上限。

#define netconn_listen(conn) netconn_listen_with_backlog(conn, TCP_DEFAULT_LISTEN_BACKLOG)

TCP_DEFAULT_LISTEN_BACKLOG​默认0xff。

netconn_accept():接受连接

accept()一个新的TCP客户端连接。

服务器调用该函数可以从conn->acceptmbox​邮箱中获取一个新的连接,如果邮箱为空,该函数会一直阻塞,直至有新的连接到来。

在调用次函数之前,先调用netconn_listen()​让服务器加入监听状态。

netconn_recv():接收数据

该函数是从conn->recvmbox​邮箱中等待数据消息:这些消息就是数据缓存队列:

  • 对于UDP连接:回调函数recv_udp()​会先将接收到的UDP数据封装在netbuf​结构中,然后将数据消息投递到邮箱中。
  • 对于TCP连接:投递到该邮箱中的数据依然是pbuf​封装的,在接收到数据包后,函数netconn_recv()​才将这些pbuf​封装成netbuf​。然后发送一个API消息lwip_netconn_do_recv()​到内核,告知TCP内核,本次已经从TCP缓存中接收了多少数据,让内核调用tcp_recved()​函数滑动接收窗口。

netconn_sent():UDP/RAW发送数据

通过过UDP或RAW网络(已经连接)发送数据。

大概内容就是从API消息中获取目的地址信息和数据报pbuf,然后调用udp_send()​或udp_sendto()​将数据放出去。

netconn_write():TCP发送数据

用于在稳定的TCP连接中发送数据。

netconn_close():关闭连接

标签:lwip,NETCONN,tcp,netconn,LWIP,msg,15,conn
From: https://www.cnblogs.com/lizhuming/p/17442931.html

相关文章

  • 代码随想录算法训练营第15天 | ● 层序遍历 10 ● 226.翻转二叉树 ● 101.对称二叉
     第六章二叉树 part02 今日内容:  ●  层序遍历  10 ●  226.翻转二叉树 ●  101.对称二叉树 2    详细布置   层序遍历  看完本篇可以一口气刷十道题,试一试, 层序遍历并不难,大家可以很快刷了十道题。 题目链接/文章讲解/视频讲解:htt......
  • hdu 1506(dp || 单调栈)
    题意:这题是要找最大的矩形面积。解题思路:这题的关键是要找每个条形能够往左和往右能够到达的最大长度。我最开始的思路是单调栈去维护,只要入栈的元素比栈顶元素小,栈顶就要出栈,并且知道其最右能够到达的最远距离。当要入栈的元素已经找到了位置,那么它左边的元素所在的位置就是其能到......
  • hdu 1516(编辑距离+记录路径)
    最开始把问题搞错了,以为是两个串都可以做修改,无论我怎么想都不通。回到这个题目上,这道题和最长公共子序列很相似,思路可以说是一样的,包括记录路径。其实也就是根据递推数组的结果来判断。#include<iostream>#include<cstdio>#include<cstring>usingnamespacestd;constintma......
  • hdu 1510
    WhiteRectanglesTimeLimit:2000/1000MS(Java/Others)    MemoryLimit:65536/32768K(Java/Others)ProblemDescriptionYouaregivenachessboardmadeupofNsquaresbyNsquareswithequalsize.Someofthesquaresarecoloredblack,andthe......
  • hdu 1534(差分约束)
    题意:安排计划,有4种约束方式,给出你这些时间的n个约束..如果计划是可行的,求出每一件事发生的最早时间..否则输出“impossible”..①.FAFaba要在b完成后完成..②.FASaba要在b开始前完成..③.SASaba要在b开始前开始..④.SAFaba要在b结束前开......
  • hdu 1532(最大流)
    解题思路:这是一道典型的模板题,直接套用EK算法即可。。。我感觉最大流的本质就是能否找到一个从源点到汇点的增广路径,并将其最小的边作为增加值,沿着增广路上的边进行更新。AC:#include<iostream>#include<cstdio>#include<cstring>#include<queue>usingnamespacestd;consti......
  • [刷题笔记55 动态规划15]
    @目录动态规划392.判断子序列115.不同的子序列动态规划●392.判断子序列●115.不同的子序列392.判断子序列392.判断子序列法1:动态规划boolisSubsequence(strings,stringt){//动态规划vector<vector<int>>dp(s.size()+1,vector<int>(t.size(......
  • CF1585F. Non-equal Neighbours
    三倍经验:CF1591F.Non-equalNeighbours,ARC115E-LEQandNEQ。提供一种力大砖飞的数据结构\(O(n\logn)\)做法,非常好写/好调,去掉数据结构部分只有1k。定义\(f_{i,j}\)表示前\(i\)个数,最后一个为\(j\)的方案数。显然第1维可以压掉,写成\(f_j\)的形式。然后这个东......
  • hdu 1593(数学)
    往相反的方面跑,但是,最理想的初始位置并不是圆点和圆上的某一点,应该还有更理想的初始逃跑状态.这里有一点需要注意,就是逃跑者极力想达到理想逃跑初态,而追赶者极力阻止逃跑者达到这一状态,所以,理想初态应该是无论追赶者如何阻止,逃跑者仍然可以达到的理想状态.最理想的......
  • poj 2415(BFS)
    题意: 有一种游戏,共有三个piece(不妨称为棋子),棋盘是由N个点构成的完全图,边有颜色。每次可以移动一个棋子,移动时必须满足棋子走过的边的颜色和其他两个棋子之间的连边的颜色一致。求把三个棋子移到同一个顶点的最少次数。这道题是一个很简单的BFS,但为何一直TLE。。。。#include<ios......