LwIP代码收发报流程分析(2)数据包的接收

2 篇文章 0 订阅

LwIP代码收发报流程分析(2)数据包的接收

上一篇博客,LwIP代码收发报流程分析(1)数据包的发送 我们一起分析了 LwIP 是如何将 ICMP echo 也就是 Ping 报文发送出去的过程,在这篇博客中我们将一起分析 LwIP 是如何接收网络数据包的,为后面我们将其移植到 CH32 MCU 上做做预习。

这次还是选 LwIP 官方代码中的 ping 例程作为代码分析的对象,代码位置在 LWIP\contrib-2.1.0\apps\ping\ping.c 文件中。

由于本博客主要是分析 LwIP 的接收数据包流程的,相关的代码也只选取围绕这一主旨的,其他的一些方面的细节,只要在流程清晰的情况下再找到代码自己看看一般就能明白。或者大家也可以论可以留言~ 咱们一起讨论、学习。

网络数据包的接收

上一篇博客中说到发包使用 ping_send 函数,那么与之对应的就是 ping_recv 函数了。

static void
ping_recv(int s)
{
  char buf[64];
  int len;
  struct sockaddr_storage from;
  int fromlen = sizeof(from);
	/* 可以看出是使用 lwip_recvfrom 来进行报文的接收 */
  while((len = lwip_recvfrom(s, buf, sizeof(buf), 0, (struct sockaddr*)&from, (socklen_t*)&fromlen)) > 0) {
    ...
  }
...
}

在下面的 lwip_recvfrom 函数中可以看出里面实际上是数据结构的转换,数据是调用了另外一个函数来获得的。

ssize_t
lwip_recvfrom(int s, void *mem, size_t len, int flags,
              struct sockaddr *from, socklen_t *fromlen)
{
...
  sock = get_socket(s);

/* 组合为 msghdr 结构,用于传递给接收函数当参数*/
  u16_t datagram_len = 0;
  struct iovec vec;
  struct msghdr msg;
  err_t err;
  vec.iov_base = mem;
  vec.iov_len = len;
  msg.msg_control = NULL;
  msg.msg_controllen = 0;
  msg.msg_flags = 0;
  msg.msg_iov = &vec;
  msg.msg_iovlen = 1;
  msg.msg_name = from;
  msg.msg_namelen = (fromlen ? *fromlen : 0);
  /* 网络数据包的来源实际上是通过这个函数 */
  err = lwip_recvfrom_udp_raw(sock, flags, &msg, &datagram_len, s);

    ...
  }
}

可以看出实际上数据包的接收是 lwip_recvfrom_udp_raw 函数通过 msghdr 结构进行接收的。
这个函数从名字就可以看出来是用来接收像 udp 和 raw 这样的无连接协议的数据包使用的。

static err_t
lwip_recvfrom_udp_raw(struct lwip_sock *sock, int flags, struct msghdr *msg, u16_t *datagram_len, int dbg_s)
{
	...
	/*这里可以看出 flag 参数的目的就是对 socket 是否阻塞进行设置用的 */
  if (flags & MSG_DONTWAIT) {
    apiflags = NETCONN_DONTBLOCK;
  } else {
    apiflags = 0;
  }

  /* 这里看到只有把上一次的接收到的数据全部取走才会开启新的一次数据包的接收,
  *  这也就是为什么要在接收的时候使用循环接收的原因。*/
  buf = sock->lastdata.netbuf;
  if (buf == NULL) {
    err = netconn_recv_udp_raw_netbuf_flags(sock->conn, &buf, apiflags);
    sock->lastdata.netbuf = buf;
  }
...
  /* 将接收到的 buf 转换为 iovec 结构  */
  for (i = 0; (i < msg->msg_iovlen) && (copied < buflen); i++) {
    u16_t len_left = (u16_t)(buflen - copied);
    if (msg->msg_iov[i].iov_len > len_left) {
      copylen = len_left;
    } else {
      copylen = (u16_t)msg->msg_iov[i].iov_len;
    }

    pbuf_copy_partial(buf->p, (u8_t *)msg->msg_iov[i].iov_base, copylen, copied);
    copied = (u16_t)(copied + copylen);
  }
...
}

netconn_recv_udp_raw_netbuf_flags 这个函数可以理解为,什么都没有做,就直接调用了 netconn_recv_data 函数。

err_t
netconn_recv_udp_raw_netbuf_flags(struct netconn *conn, struct netbuf **new_buf, u8_t apiflags)
{
  LWIP_ERROR("netconn_recv_udp_raw_netbuf: invalid conn", (conn != NULL) &&
             NETCONNTYPE_GROUP(netconn_type(conn)) != NETCONN_TCP, return ERR_ARG;);

  return netconn_recv_data(conn, (void **)new_buf, apiflags);
}

netconn_recv_data 该函数也处理 TCP ,但是目前分析的点不在 如何实现的 TCP 连接,而是网络数据包的接收流程,所以我在这里将此部分代码进行忽略。

static err_t
netconn_recv_data(struct netconn *conn, void **new_buf, u8_t apiflags)
{
...

/*这里判断 socket 是不是非阻塞且无连接*/
  if (netconn_is_nonblocking(conn) || (apiflags & NETCONN_DONTBLOCK) ||
      (conn->flags & NETCONN_FLAG_MBOXCLOSED) || (conn->pending_err != ERR_OK)) {
		sys_arch_mbox_tryfetch(&conn->recvmbox, &buf) == SYS_MBOX_EMPTY) 
    }

/* 这里是处理 udp 和 raw 协议的位置 */
#if (LWIP_UDP || LWIP_RAW)
  {
    len = netbuf_len((struct netbuf *)buf);
  }

/* 此处会执行回调函数用于通知接收事件以及其数据长度 */
API_EVENT(conn, NETCONN_EVT_RCVMINUS, len);

  *new_buf = buf;
  return ERR_OK;
}

接下来分析 sys_arch_mbox_tryfetch 函数,该函数用于获取到实际的网络数据包。而且该函数还是一个根据不同 OS 而使用不同设计的一个函数。 这里以 freeRTOS 举例:

u32_t
sys_arch_mbox_tryfetch(sys_mbox_t *mbox, void **msg)
{
  void *msg_dummy;

  if (!msg) {
    msg = &msg_dummy;
  }

  ret = xQueueReceive(mbox->mbx, &(*msg), 0);
  ...
}

OK~ 我们看到了 xQueueReceive 函数,该函数在 FreeRTOS 中的作用是一种线程间通讯的一种方式,即 消息队列。
看到这里我们也明白了,一定是在 LwIP 的设计中有一个线程是专门负责接收网络数据,然后上层再通过不同的消息队列拿到那个专门负责接收线程的数据。

既然有消息队列的收端,就一定有对应消息队列的发端。 在与 sys_arch_mbox_tryfetch 函数同一个文件下还有一个 sys_mbox_trypost 函数。

err_t
sys_mbox_trypost(sys_mbox_t *mbox, void *msg)
{
  BaseType_t ret;

  ret = xQueueSendToBack(mbox->mbx, &msg, 0);
    ...
}

现在就可以从 sys_mbox_trypost 函数倒推出函数调用逻辑。也就是说,往下的倒推过程就是将数据通过消息队列发送的过程。
通过搜索就很容能够找到 recv_raw 函数调用了 sys_mbox_trypost 函数。

static u8_t
recv_raw(void *arg, struct raw_pcb *pcb, struct pbuf *p,
         const ip_addr_t *addr)
{
    ...
    conn = (struct netconn *)arg;

    /* 把整个包进行拷贝到新的位置 */
    q = pbuf_clone(PBUF_RAW, PBUF_RAM, p);

    u16_t len;
    buf = (struct netbuf *)memp_malloc(MEMP_NETBUF);

    buf->p = q;
    buf->ptr = q;
    ip_addr_copy(buf->addr, *ip_current_src_addr());
    buf->port = pcb->protocol;

    len = q->tot_len;
    /* 将数据包放入消息队列中 */
    sys_mbox_trypost(&conn->recvmbox, buf);

  ...
}

到了 recv_raw 函数这里,其参数 *p 已经是接收到的数据包了,所以还要往上一路找过去。

pcb_new 函数中有这样一句话 raw_recv(msg->conn->pcb.raw, recv_raw, msg->conn);。 这里 recv_raw 就是作为回调函数传入的。

void
raw_recv(struct raw_pcb *pcb, raw_recv_fn recv, void *recv_arg)
{
    /* */
  pcb->recv = recv;
  pcb->recv_arg = recv_arg;
}

上面的函数为 pcb 的 recv 成员指定了 recv_raw 这个成员函数。接下来就看一下哪里调用了这个 recv 函数。

raw_input_state_t
raw_input(struct pbuf *p, struct netif *inp)
{
    ...

    proto = IPH_PROTO((struct ip_hdr *)p->payload);

  while (pcb != NULL) {
    if ((pcb->protocol == proto) && raw_input_local_match(pcb, broadcast) &&
        (((pcb->flags & RAW_FLAGS_CONNECTED) == 0) ||
         ip_addr_eq(&pcb->remote_ip, ip_current_src_addr()))) {

        ret = RAW_INPUT_DELIVERED;
        /* 你看这里不就调用了吗~  */
        eaten = pcb->recv(pcb->recv_arg, pcb, p, ip_current_src_addr());
    ...
    prev = pcb;
    pcb = pcb->next;
  }
  return ret;
}

后面的函数内容很多,先 focous 在调用关系上。 下面也就是 ip_input 或者 ethernet_input函数调用了 ip4_input 函数。

ip4_input(struct pbuf *p, struct netif *inp)
ip_input & ethernet_input 函数。

而这个函数的区别就是,是不是在 flags 上置位了 NETIF_FLAG_ETHARPNETIF_FLAG_ETHERNET 这两个宏,里面的逻辑可能会有所区别,但是这并不是当前我们感兴趣的内容。

err_t
tcpip_input(struct pbuf *p, struct netif *inp)
{
#if LWIP_ETHERNET
  if (inp->flags & (NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET)) {
    return tcpip_inpkt(p, inp, ethernet_input);
  } else
#endif /* LWIP_ETHERNET */
    return tcpip_inpkt(p, inp, ip_input);
}

tcpip_inpkt 函数只是设置了 ethernet_input & ip_input 作为回调函数,等待调用。

err_t
tcpip_inpkt(struct pbuf *p, struct netif *inp, netif_input_fn input_fn)
{

   /*分配 msg 结构并填充其内容*/
  msg = (struct tcpip_msg *)memp_malloc(MEMP_TCPIP_MSG_INPKT);

  msg->type = TCPIP_MSG_INPKT;
  msg->msg.inp.p = p;
  msg->msg.inp.netif = inp;
  /* 设置接收数据包的函数 */
  msg->msg.inp.input_fn = input_fn;
  if (sys_mbox_trypost(&tcpip_mbox, msg) != ERR_OK) {
    memp_free(MEMP_TCPIP_MSG_INPKT, msg);
    return ERR_MEM;
  }
  return ERR_OK;
#endif /* LWIP_TCPIP_CORE_LOCKING_INPUT */
}

继续看下去 tcpip_input 又是在 netif_init 函数中通过下面的代码进行调用的。
netif_add(&loop_netif, LOOPIF_ADDRINIT NULL, netif_loopif_init, tcpip_input);

而在 netif_add 函数中设置了这个用于处理将数据包处理并送入消息队列中的函数。

    /*netif_add 函数很长,这里就留下这三行*/
  netif->state = state;
  netif->num = netif_num;
  netif->input = input;

继续看一下, netif_init 函数又是在 lwip_init 处进行调用的。

lwip_init 函数就是在最初的 LwIP 的初始化中进行的。

好的,到这里我们已经知道了在初始化的时候就会注册各个结构中用于处理接收数据的函数,并将数据送入消息队列等待处理。
tcpip_init 函数的注释中可以发现,LwIP 初始化应根据是否使用了 OS 的情况下,对应不同的初始化函数。在使用 OS 的情况下 LwIP 的初始化应该使用 tcpip_init 函数,lwip_init 函数也在这个函数中得到了调用。

/**
 * @ingroup lwip_os
 * Initialize this module:
 * - initialize all sub modules
 * - start the tcpip_thread
 *
 * @param initfunc a function to call when tcpip_thread is running and finished initializing
 * @param arg argument to pass to initfunc
 */
void
tcpip_init(tcpip_init_done_fn initfunc, void *arg)
{
  lwip_init();
  /* 这里面的这个函数以及其参数,会在 tcpip_thread 线程中执行,也就是在初始化完成之后进行。 */
  tcpip_init_done = initfunc;
  tcpip_init_done_arg = arg;

  /* 这里创建的就是要创建消息队列用于传输网络数据包 */
  sys_mbox_new(&tcpip_mbox, TCPIP_MBOX_SIZE);

  sys_thread_new(TCPIP_THREAD_NAME, tcpip_thread, NULL, TCPIP_THREAD_STACKSIZE, TCPIP_THREAD_PRIO);
}

tcpip_thread 函数

static void
tcpip_thread(void *arg)
{
   /* 这里就能出来,将 tcpip_init 函数传递进来的函数以及参数进行执行了。 */
  if (tcpip_init_done != NULL) {
    tcpip_init_done(tcpip_init_done_arg);
  }

  while (1) {                      
    LWIP_TCPIP_THREAD_ALIVE();
    /* 这里面适用于接收消息队列中的数据包并且可以判断超时,后面再分析。*/
    TCPIP_MBOX_FETCH(&tcpip_mbox, (void **)&msg);

    tcpip_thread_handle_msg(msg);
  }
}

tcpip_thread_handle_msg 函数会根据入 tcpip_msg 数据格式的类型判断如何进行处理,其中 TCPIP_MSG_INPKT 类型 会调用 msg->msg.inp.input_fn 函数进行处理。这个函数就是在前面介绍过的 tcpip_inpkt 函数中指定的接收函数。


static void
tcpip_thread_handle_msg(struct tcpip_msg *msg)
{
  switch (msg->type) {
...
    case TCPIP_MSG_INPKT:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: PACKET %p\n", (void *)msg));
      if (msg->msg.inp.input_fn(msg->msg.inp.p, msg->msg.inp.netif) != ERR_OK) {
        pbuf_free(msg->msg.inp.p);
      }
      memp_free(MEMP_TCPIP_MSG_INPKT, msg);
      break;

    case TCPIP_MSG_CALLBACK:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: CALLBACK %p\n", (void *)msg));
      msg->msg.cb.function(msg->msg.cb.ctx);
      memp_free(MEMP_TCPIP_MSG_API, msg);
      break;

    case TCPIP_MSG_CALLBACK_STATIC:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: CALLBACK_STATIC %p\n", (void *)msg));
      msg->msg.cb.function(msg->msg.cb.ctx);
      break;

    default:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: invalid message: %d\n", msg->type));
      LWIP_ASSERT("tcpip_thread: invalid message", 0);
      break;
  }
  ...
}

OK~ 目前调用的流程是没问题了,但是大家是否注意到在 tcpip_thread_handle_msg 函数调用的时候,实际上 msg 结构中已经存在了数据包。
这个数据包正是上面正确调用了接收相关的函数才正确放入消息队列中的。

那么下一步要解决的就是网卡如何将数据包传递到 msg 结构中的。

上一篇博客也介绍了,在 LwIP 例子中包含了一个网卡接口的程序 contrib\examples\ethernetif\ethernetif.c

里面的 ethernetif_init 函数用于完成网卡硬件和其他一些的初始化工作,而且上一篇博客也介绍了ethernetif.c 文件中的 low_level_output 函数用于实际网卡发送网络数据包,而 low_level_input 函数则就是用来接收网络数据包的网卡驱动部分。 该函数在 ethernetif_input 函数中被调用,并且注释中也对该函数进行了比较详细的说明。
下面的代码中我保留了LwIP代码中原始的注释。

/**
 * This function should be called when a packet is ready to be read
 * from the interface. It uses the function low_level_input() that
 * should handle the actual reception of bytes from the network
 * interface. Then the type of the received packet is determined and
 * the appropriate input function is called.
 *
 * @param netif the lwip network interface structure for this ethernetif
 */
static void
ethernetif_input(struct netif *netif)
{
  struct ethernetif *ethernetif;
  struct eth_hdr *ethhdr;
  struct pbuf *p;

  ethernetif = netif->state;

  /* move received packet into a new pbuf */
  p = low_level_input(netif);

  /* if no packet could be read, silently ignore this */
  if (p != NULL) {
    /* pass all packets to ethernet_input, which decides what packets it supports */
    if (netif->input(p, netif) != ERR_OK) {
      LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: IP input error\n"));
      pbuf_free(p);
      p = NULL;
    }
  }
}

上面的代码实际是调用内部的 low_level_input 函数完成了实际的网络数据包的接收工作,然后调用 netif->input 函数对网络数据包进行接收并处理。

这里的 netif->input 函数就是在前面介绍过的 netif_add 函数中进行指定的。下面再把设置 netif->input 函数这段代码拿过来看看。

#if NO_SYS
  netif_add(&loop_netif, LOOPIF_ADDRINIT NULL, netif_loopif_init, ip_input);
#else  /* NO_SYS */
  netif_add(&loop_netif, LOOPIF_ADDRINIT NULL, netif_loopif_init, tcpip_input);

到这里对 LwIP 的接收流程是不是已经有了一个整体的概念呢? 我再用图更清晰的表示一下。

在这里插入图片描述


请添加图片描述

  • 21
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值