TCP/IP应用层协议实现 - 数据收发send/recv(lwip)

Posted arm7star

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TCP/IP应用层协议实现 - 数据收发send/recv(lwip)相关的知识,希望对你有一定的参考价值。

1、接收数据(recv)

lwip的recv最终调用lwip_recvfrom接收数据。https://blog.csdn.net/arm7star/article/details/117187409 《TCP/IP传输层协议实现 - TCP报文接收/发送(lwip)》有介绍,传输层将数据发送到应用层的邮箱里面,lwip_recvfrom主要负责从邮箱读取数据。

检查上次接收的邮箱是否有数据待接收。(应用层接收数据时会指定长度,一次从邮箱接收到的数据可能大于应用层指定接收的数据,因此sock->lastdata记录上一次尚未接收完的数据)

  do 
    LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_recvfrom: top while sock->lastdata=%p\\n", (void*)sock->lastdata));
    /* Check if there is data left from the last recv operation. */
    if (sock->lastdata) 
      buf = sock->lastdata; // 继续接收上一次从邮箱接收到的数据
     else 

如果lastdata为空,需要继续从邮箱读取数据。(接收数据,设置错误码用于判断recv返回原因)

     else 
      /* If this is non-blocking call, then check first */
      if (((flags & MSG_DONTWAIT) || (sock->flags & O_NONBLOCK)) &&  // 非阻塞模式
          (sock->rcvevent <= 0))  // rcvevent一定程度代表邮箱里面有多少邮件(多少数据块待接收,传输层每次接收到数据都将数据发送到邮箱里面,一个邮件就是一次接收到的数据)
        if (off > 0)  // 如果有数据(应用层接收数据是循环读取邮箱,一次接收的数据比较小的时候,继续读取邮箱的下一个数据,off > 0表示前面的邮箱有读取到数据)
          /* already received data, return that */
          sock_set_errno(sock, 0); // 设置errno为0
          return off; // 非阻塞模式返回当前已经读取到的数据
        
        LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_recvfrom(%d): returning EWOULDBLOCK\\n", s));
        sock_set_errno(sock, EWOULDBLOCK); // 非阻塞模式下,没有数据,设置errno为EWOULDBLOCK(应用层根据errno判断返回原因;代码这里有个问题,如果收到FIN之后,再次调用recv,那么这个错误码应该设置为连接关闭,否则非阻塞模式下,应用层无知道收到了FIN!!! 收到FIN之后,传输层不会再发送数据到应用层;这里应该根据阻塞模式收到NULL时的代码一样,设置errno;阻塞模式会调用netconn_recv,netconn_recv会设置错误返回NULL,因此阻塞模式不存在问题)
        return -1; // 没有数据返回-1
      

      /* No data was left from the previous operation, so we try to get
      some from the network. */
      sock->lastdata = buf = netconn_recv(sock->conn); // 阻塞模式,从邮箱读取/等待数据(rcvevent > 0时,邮箱有数据,读取数据,rcvevent <= 0时,邮箱没有数据,阻塞等待邮箱有数据)
      LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_recvfrom: netconn_recv netbuf=%p\\n", (void*)buf));

      if (!buf)  // buf为NULL(之前文章有介绍,传输层收到FIN报文时,会发送一个NULL的消息数据标记连接结束(对端调用了close),此次即为FIN的NULL数据;netconn_recv收到FIN的NULL消息后,netconn_recv会设置netconn状态为ERR_CLSD,再次调用netconn_recv也会返回NULL(接下来的off > 0的情况下,recv仍会接收到数据,errno为0,即使buf为NULL,应用层也判断不出连接已关闭,因此下次调用netconn_recv没有收到任何数据时,即可判断连接已关闭))
        if (off > 0)  // 前面邮箱已经收到数据
          /* already received data, return that */
          sock_set_errno(sock, 0); // 设置errno为0
          return off; // 返回收到的数据长度
        
        /* We should really do some error checking here. */
        LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_recvfrom(%d): buf == NULL!\\n", s));
        sock_set_errno(sock, (((sock->conn->pcb.ip != NULL) && (sock->conn->err == ERR_OK))
          ? ETIMEDOUT : err_to_errno(sock->conn->err))); // 设置errno(连接已经关闭,没有收到数据,设置errno)
        return 0; // 没有数据返回0
      
    

PUSH标志检查。(recv读取到PUSH标志后,即使没有收到指定长度的数据也会返回)

    if (netconn_type(sock->conn) == NETCONN_TCP) 
      LWIP_ASSERT("invalid copylen, len would underflow", len >= copylen);
      len -= copylen;
      if ( (len <= 0) || 
           (buf->p->flags & PBUF_FLAG_PUSH) || // 收到的数据带有PUSH
           (sock->rcvevent <= 0) || 
           ((flags & MSG_PEEK)!=0)) 
        done = 1; // 设置done为1,recv返回
      
     else 

 

2、发送数据(send)

send比较简单。前面文章介绍过,发送的时候是将数据缓存到发送队列,发送队列及发送缓存有限,因此send存在阻塞。

发送的代码主要在tcp/ip线程执行,发送操作与tcp传输层的数据结构关系比较紧密,因此在一个线程里面串行处理,避免竞争。

tcp/ip线程调用do_write、do_writemore发送数据到发送队列。

tcp数据发送。

    if ((msg->conn->pcb.tcp != NULL) && (msg->conn->type == NETCONN_TCP)) 
#if LWIP_TCP
      msg->conn->state = NETCONN_WRITE; // 设置netconn的状态为写状态(传输层有数据被确认接收后,如果还有数据待写入发送队列,会根据这个状态来发送更多数据到发送队列里面)
      /* set all the variables used by do_writemore */
      LWIP_ASSERT("already writing", msg->conn->write_msg == NULL &&
        msg->conn->write_offset == 0);
      msg->conn->write_msg = msg;
      msg->conn->write_offset = 0; // 写数据的偏移(第一次写入发送队列,从应用层数据的0偏移开始写)
#if LWIP_TCPIP_CORE_LOCKING
      msg->conn->write_delayed = 0;
      if (do_writemore(msg->conn) != ERR_OK) 
        LWIP_ASSERT("state!", msg->conn->state == NETCONN_WRITE);
        UNLOCK_TCPIP_CORE();
        sys_arch_sem_wait(msg->conn->op_completed, 0);
        LOCK_TCPIP_CORE();
        LWIP_ASSERT("state!", msg->conn->state == NETCONN_NONE);
      
#else
      do_writemore(msg->conn); // 调用do_writemore写发送队列(数据指针、偏移等都在msg->conn里面)
#endif
      /* for both cases: if do_writemore was called, don't ACK the APIMSG! */
      return; // 返回(这是在tcp/ip线程里面执行返回,不会导致应用层的send返回!!!)
#endif /* LWIP_TCP */
#if (LWIP_UDP || LWIP_RAW)
     else 

写发送队列。(do_writemore如果一次写不完,则写当前能写的数据,剩余的数据由传输层的sent事件触发调用do_writemore写发送队列)

static err_t
do_writemore(struct netconn *conn)

  err_t err;
  void *dataptr;
  u16_t len, available;
  u8_t write_finished = 0;
  size_t diff;

  LWIP_ASSERT("conn->state == NETCONN_WRITE", (conn->state == NETCONN_WRITE));

  dataptr = (u8_t*)conn->write_msg->msg.w.dataptr + conn->write_offset; // 本次从dataptr开始写数据到发送队列
  diff = conn->write_msg->msg.w.len - conn->write_offset; // 全部待写数据长度
  if (diff > 0xffffUL)  /* max_u16_t */ // 待写数据太多
    len = 0xffff; // 一次最多发送0xffff到发送队列
#if LWIP_TCPIP_CORE_LOCKING
    conn->write_delayed = 1;
#endif
   else 
    len = (u16_t)diff; // 本次可写发送队列数据的长度
  
  available = tcp_sndbuf(conn->pcb.tcp); // 可用发送缓存大小
  if (available < len)  // 可用发送缓存小于需要发送的数据
    /* don't try to write more than sendbuf */
    len = available; // 本次发送数据设置为可用发送缓存的大小
#if LWIP_TCPIP_CORE_LOCKING
    conn->write_delayed = 1;
#endif
  

  err = tcp_write(conn->pcb.tcp, dataptr, len, conn->write_msg->msg.w.apiflags);
  LWIP_ASSERT("do_writemore: invalid length!", ((conn->write_offset + len) <= conn->write_msg->msg.w.len)); // 将数据添加到发送队列
  if (err == ERR_OK) 
    conn->write_offset += len; // 下次调用do_writemore时的数据偏移
    if (conn->write_offset == conn->write_msg->msg.w.len)  // 下次发送数据的偏移等于数据的长度(所有数据已经发送到了发送队列里面)
      /* everything was written */
      write_finished = 1; // 写完成(send已经将所有数据发送到发送队列)
      conn->write_msg = NULL; // 待写数据设置为空
      conn->write_offset = 0;
      /* API_EVENT might call tcp_tmr, so reset conn->state now */
      conn->state = NETCONN_NONE; // netconn状态设置为NETCONN_NONE(传输层检测到NETCONN_NONE状态就不用判断应用层是否有更多数据需要写入发送队列,不用调用do_writemore)
    
    err = tcp_output_nagle(conn->pcb.tcp); // 调用tcp_output_nagle尝试发送报文(如果没有数据在发送或者发送窗口没有满,则可能需要将本次发送队列的数据发送出去)
    conn->err = err;
    if ((err == ERR_OK) && (tcp_sndbuf(conn->pcb.tcp) <= TCP_SNDLOWAT)) 
      API_EVENT(conn, NETCONN_EVT_SENDMINUS, len);
    
   else if (err == ERR_MEM) 
    /* If ERR_MEM, we wait for sent_tcp or poll_tcp to be called
       we do NOT return to the application thread, since ERR_MEM is
       only a temporary error! */

    /* tcp_enqueue returned ERR_MEM, try tcp_output anyway */
    err = tcp_output(conn->pcb.tcp); // 没有更多内存,调用tcp_output发送报文(尽快让对端接收数据,尽快释放发送缓存)

#if LWIP_TCPIP_CORE_LOCKING
    conn->write_delayed = 1;
#endif
   else 
    /* On errors != ERR_MEM, we don't try writing any more but return
       the error to the application thread. */
    conn->err = err;
    write_finished = 1;
  

  if (write_finished) 
    /* everything was written: set back connection state
       and back to application task */
    conn->state = NETCONN_NONE;
#if LWIP_TCPIP_CORE_LOCKING
    if (conn->write_delayed != 0)
#endif
    
      sys_sem_signal(conn->op_completed); // 所有数据发送到发送队列后,设置op_completed信号,应用层send等待op_completed,send调用返回
    
  
#if LWIP_TCPIP_CORE_LOCKING
  else
    return ERR_MEM;
#endif
  return ERR_OK;

数据被接收,发送缓存被释放,发送更多数据到发送队列。

static err_t
sent_tcp(void *arg, struct tcp_pcb *pcb, u16_t len)

  struct netconn *conn = arg;

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

  if (conn->state == NETCONN_WRITE)  // 有待写入发送队列的数据
    LWIP_ASSERT("conn->pcb.tcp != NULL", conn->pcb.tcp != NULL);
    do_writemore(conn); // 调用do_writemore将conn里面的数据写入发送队列(一次写不完,下次调用sent_tcp再次写发送队列),最后一次do_writemore写完所有数据到发送队列后,设置信号唤醒send线程
   else if (conn->state == NETCONN_CLOSE) 
    do_close_internal(conn);
  

  if (conn) 
    if ((conn->pcb.tcp != NULL) && (tcp_sndbuf(conn->pcb.tcp) > TCP_SNDLOWAT)) 
      API_EVENT(conn, NETCONN_EVT_SENDPLUS, len);
    
  
  
  return ERR_OK;

 

 

以上是关于TCP/IP应用层协议实现 - 数据收发send/recv(lwip)的主要内容,如果未能解决你的问题,请参考以下文章

Linux Kernel TCP/IP Stack — 协议栈发包处理流程

TCP/IP传输层协议实现 - TCP报文接收/发送(lwip)

深入理解TCP协议及其源代码-send和recv背后数据的收发过程

linux内核源码分析之网络数据收发流程

OSI协议和TCP/IP协议笔记

socket,tcp,http三者之间的区别和原理