【lwip】13-TCP协议分析之源码篇

前言

上一年就写好了,一直没时间整理出来,现在不整理了,直接放出来。
链接:https://www.cnblogs.com/lizhuming/p/17438682.html

TCP RAW接口分析

先分析北向接口,这些接口可供用户使用。

相关文件:

  • lwip/src/core/tcp.c
  • lwip/src/include/lwip/tcp.h

LWIP接口层级:RAW --> NETCONN --> SOCKET。

RAW接口使用

建立连接

用于建立连接的函数类似于连续API和BSD套接字API的函数。

使用tcp_new()函数创建一个新的TCP连接标识符(即协议控制块PCB)。

然后可以将这个PCB设置为监听新的传入连接,或者显式地连接到另一个主机。

参考使用:

tcp_new(); /* 新建一个TCP */
tcp_bind(); /* 绑定本地服务 */
tcp_listen(); /* or */ tcp_listen_with_backlog(); /* 监听(用于服务端) */
tcp_accept(); /* 接受连接(用于服务端) */
tcp_connect(); /* 建立一个连接(用于客户端) */

发送数据

通过调用tcp_write()对数据进行排队,并通过调用tcp_output()触发发送TCP数据。

当数据成功传输到远程主机时,将通过tcp_sent()指定回调函数回调通知到应用程序。

tcp_write(); /* 该函数用于把数据插入TCP发送缓冲区 */
tcp_output(); /* 该函数用于触发TCP缓冲区发送数据 */
tcp_sent(); /* 注册发送回调函数 */

接收数据

TCP数据接收是基于回调函数实现的。

当新数据到达时调用应用程序之前tcp_recv()注册的回调函数。

当应用程序获得数据后,它必须调用tcp_recved()函数来指示TCP可以通告增加接收窗口。

tcp_recv(); /* 注册接收回调函数 */
tcp_recved(); /* 应用层成功接收到数据通知回TCP的函数 */

应用轮询(守护)

逻辑功能:就是注册一个poll()函数到TCP内核,这个函数会被TCP内核周期调用。

当连接空闲时(即,既没有传输数据也没有接收数据),lwip将通过调用指定的回调函数来反复轮询应用程序。

这既可以用作看门狗定时器来终止空闲时间过长的连接,也可以用作一种等待内存可用的方法。

例如,如果由于内存不可用而导致tcp_write()发送数据失败,则应用程序可能在连接空闲一段时间后使用轮询功能再次调用tcp_write()

tcp_poll(); /* 注册周期回调函数,被TCP内核周期调用 */

关闭连接

关闭和中止连接。

tcp_close()是通过四次挥手(FIN)正常关闭连接。

tcp_abort()是通过RST强制终止连接。

tcp_err()是注册异常回调函数。当TCP异常时,会通过该函数注册的回调函数通知应用层。

  • 注意:当调用这个回调时,相应的pcb已经被释放了!
tcp_close(); /* 正常关闭连接,释放PCB资源 */
tcp_abort(); /* RST方式终止连接 */
tcp_err(); /* 注册异常回调函数 */

新建控制块:tcp_new()

tcp_new()接口调用tcp_alloc()接口。

一个TCP连接需要TCP PCB(TCP 控制块)来管理本连接的相关数据。

在本函数中,能了解到LWIP申请TCP PCB的内存管理逻辑,也能找到TCP性能的默认值(这个对TCP网络分析的同学挺有用的)。

struct tcp_pcb *tcp_alloc(u8_t prio):申请&初始化TCP PCB。

  • u8_t prio:新建的TCP PCB优先级。
  • 如果MEMP_TCP_PCB内存池还有空间,则直接从该内存池申请。
  • 如果MEMP_TCP_PCB内存池空间不足,则按照以下顺序进行强制占用:最老的:TIME-WAIT > LAST_ACK > CLOSING > 优先级更低的已激活的连接。
/**
 * 申请tcp pcb内存。
 * 如果内存不足,按以下顺序释放pcb:最老的:TIME-WAIT > LAST_ACK > CLOSING > 优先级更低的已激活的连接。
 * tcp pcb内存资源申请成功后,初始化部分字段。
 *
 */
struct tcp_pcb *
tcp_alloc(u8_t prio)
{
  struct tcp_pcb *pcb;

  LWIP_ASSERT_CORE_LOCKED();

  pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
  if (pcb == NULL) {
    /* 先处理那些处于TF_CLOSEPEND状态的pcb。主动触发他们再次发起FIN。(之前发送FIN失败的pcb,这些pcb都是我们想关闭的pcb了) */
    tcp_handle_closepend();

    LWIP_DEBUGF(TCP_DEBUG, ("tcp_alloc: killing off oldest TIME-WAIT connection\n"));
    /* 内存不足,干掉最老的TIME_WAIT连接 */
    tcp_kill_timewait();
    pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
    if (pcb == NULL) {
      LWIP_DEBUGF(TCP_DEBUG, ("tcp_alloc: killing off oldest LAST-ACK connection\n"));
      /* 还是内存不足,就干掉最老的LAST_ACK连接 */
      tcp_kill_state(LAST_ACK);
      pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
      if (pcb == NULL) {
        LWIP_DEBUGF(TCP_DEBUG, ("tcp_alloc: killing off oldest CLOSING connection\n"));
        /* 还是内存不足,干掉最老的CLOSING连接 */
        tcp_kill_state(CLOSING);
        pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
        if (pcb == NULL) {
          LWIP_DEBUGF(TCP_DEBUG, ("tcp_alloc: killing oldest connection with prio lower than %d\n", prio));
          /* 还是内存不足,那就干掉优先级更低的最老的连接 */
          tcp_kill_prio(prio);
          pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
          if (pcb != NULL) {
            /* 还是内存不足,没办法了 */
            MEMP_STATS_DEC(err, MEMP_TCP_PCB);
          }
        }
        if (pcb != NULL) {
          /* adjust err stats: memp_malloc failed multiple times before */
          MEMP_STATS_DEC(err, MEMP_TCP_PCB);
        }
      }
      if (pcb != NULL) {
        /* adjust err stats: memp_malloc failed multiple times before */
        MEMP_STATS_DEC(err, MEMP_TCP_PCB);
      }
    }
    if (pcb != NULL) {
      /* adjust err stats: memp_malloc failed above */
      MEMP_STATS_DEC(err, MEMP_TCP_PCB);
    }
  }
  if (pcb != NULL) {
    /* 申请成功 */
    memset(pcb, 0, sizeof(struct tcp_pcb)); /* 清空所有字段 */
    pcb->prio = prio; /* 设置控制块优先级 */
    pcb->snd_buf = TCP_SND_BUF; /* 设置发送缓冲区大小 */
    pcb->rcv_wnd = pcb->rcv_ann_wnd = TCPWND_MIN16(TCP_WND); /* 初始化接收窗口和窗口通告值 */
    pcb->ttl = TCP_TTL; /* TTL */
    pcb->mss = INITIAL_MSS; /* 初始化MSS,在SYN时,会在选项字段发送到对端。 */
    pcb->rto = LWIP_TCP_RTO_TIME / TCP_SLOW_INTERVAL; /* 初始RTO时间为LWIP_TCP_RTO_TIME,默认3000ms */
    pcb->sv = LWIP_TCP_RTO_TIME / TCP_SLOW_INTERVAL; /* 初始RTT时间差为RTO的初始值 */
    pcb->rtime = -1; /* 初始为停止重传计时值计时 */
    pcb->cwnd = 1; /* 初始拥塞窗口值 */
    pcb->tmr = tcp_ticks; /* 保存当前TCP节拍值为当前PCB的TCP节拍初始值 */
    pcb->last_timer = tcp_timer_ctr; /* 初始化PCB最后一次活动的时间 */

    /* RFC 5618建议设置ssthresh值尽可能高,比如设置为最大可能的窗口通告值大小(可以理解为最大可能的发送窗口大小 )。 */
    /* 这里先设置为本地发送缓冲区大小,即是最大飞行数据量。后面进行窗口缩放和自动调优时自动调整。 */
    pcb->ssthresh = TCP_SND_BUF;

#if LWIP_CALLBACK_API
    /* 默认接收回调 */
    pcb->recv = tcp_recv_null;
#endif /* LWIP_CALLBACK_API */

    /* 保活计时器超时值:默认7200秒,即是两小时。 */
    pcb->keep_idle  = TCP_KEEPIDLE_DEFAULT;

#if LWIP_TCP_KEEPALIVE
    /* 保活时间间隔:默认75秒 */
    pcb->keep_intvl = TCP_KEEPINTVL_DEFAULT;
    /* 保活探测数:默认9次。 */
    pcb->keep_cnt   = TCP_KEEPCNT_DEFAULT;
#endif /* LWIP_TCP_KEEPALIVE */
  }
  return pcb;
}

绑定本地服务:tcp_bind()

TCP PCB新建后,需要绑定本地的IP和端口号,这样就能表示一个接入到应用层的连接了。

err_t tcp_bind(struct tcp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port)

  • struct tcp_pcb *pcb:TCP PCB。

  • const ip_addr_t *ipaddr:需要绑定的本地IP地址。如果本地IP填了NULLIP_ANY_TYPE,则表示任意IP,绑定本地所有IP的意思。

  • u16_t port:需要绑定的绑定端口号。如果本地端口号填了0,则会调用tcp_new_port()申请一个随机端口号。如果指定了端口号,需要检查是否有复用。

  • SO_REUSE:如果设置了SO_REUSEADDR选项,且绑定的IP和PORT已经被使用且处于TIME_WAIT状态,也可以被重复使用。如果没有设置,则不能释放处于TIME_WAIT状态的PCB。

  • IP&PORT复用检查:遍历所有pcb链表tcp_pcb_lists[],如果当前IP和端口号已经被使用了,且任意一个PCB没有开启端口复用选项SO_REUSEADDR,本地绑定都视为绑定失败。

    • 需要注意的是:任意IP(全0)是万能的。
  • 绑定成功后,把当前PCB迁移到tcp_bound_pcbs链表。

/**
 * @ingroup tcp_raw
 * Binds the connection to a local port number and IP address. 
 * If the IP address is not given (i.e., ipaddr == IP_ANY_TYPE), the connection is bound to all local IP addresses.
 * If another connection is bound to the same port, the function will return ERR_USE, otherwise ERR_OK is returned.
 * @see MEMP_NUM_TCP_PCB_LISTEN and MEMP_NUM_TCP_PCB
 *
 * @param pcb the tcp_pcb to bind (no check is done whether this pcb is already bound!)
 * @param ipaddr the local ip address to bind to (use IPx_ADDR_ANY to bind to any local address
 * @param port the local port to bind to
 * @return ERR_USE if the port is already in use
 *         ERR_VAL if bind failed because the PCB is not in a valid state
 *         ERR_OK if bound
 */
err_t
tcp_bind(struct tcp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port)
{
  int i;
  int max_pcb_list = NUM_TCP_PCB_LISTS;
  struct tcp_pcb *cpcb;
#if LWIP_IPV6 && LWIP_IPV6_SCOPES
  ip_addr_t zoned_ipaddr;
#endif /* LWIP_IPV6 && LWIP_IPV6_SCOPES */

  LWIP_ASSERT_CORE_LOCKED();

#if LWIP_IPV4
  /* Don't propagate NULL pointer (IPv4 ANY) to subsequent functions */
  if (ipaddr == NULL) {
    ipaddr = IP4_ADDR_ANY;
  }
#else /* LWIP_IPV4 */
  LWIP_ERROR("tcp_bind: invalid ipaddr", ipaddr != NULL, return ERR_ARG);
#endif /* LWIP_IPV4 */

  LWIP_ERROR("tcp_bind: invalid pcb", pcb != NULL, return ERR_ARG);

  LWIP_ERROR("tcp_bind: can only bind in state CLOSED", pcb->state == CLOSED, return ERR_VAL);

#if SO_REUSE /* 选项:SO_REUSEADDR */
  /* 如果设置了SO_REUSEADDR选项,且绑定的IP和PORT已经被使用且处于TIME_WAIT状态,也可以被重复使用。
      如果没有设置,则不能释放处于TIME_WAIT状态的PCB。 */
  if (ip_get_option(pcb, SOF_REUSEADDR)) {
    /* 不用遍历处于TIME_WAIT状态的TCP PCB是否被复用,因为SO_REUSEADDR选项运行其复用行为 */
    max_pcb_list = NUM_TCP_PCB_LISTS_NO_TIME_WAIT;
  }
#endif /* SO_REUSE */

#if LWIP_IPV6 && LWIP_IPV6_SCOPES
  /* If the given IP address should have a zone but doesn't, assign one now.
   * This is legacy support: scope-aware callers should always provide properly
   * zoned source addresses. Do the zone selection before the address-in-use
   * check below; as such we have to make a temporary copy of the address. */
  if (IP_IS_V6(ipaddr) && ip6_addr_lacks_zone(ip_2_ip6(ipaddr), IP6_UNICAST)) {
    ip_addr_copy(zoned_ipaddr, *ipaddr);
    ip6_addr_select_zone(ip_2_ip6(&zoned_ipaddr), ip_2_ip6(&zoned_ipaddr));
    ipaddr = &zoned_ipaddr;
  }
#endif /* LWIP_IPV6 && LWIP_IPV6_SCOPES */

  if (port == 0) {
    /* 自动生成端口号 */
    port = tcp_new_port();
    if (port == 0) {
      /* 端口号申请失败,绑定失败 */
      return ERR_BUF;
    }
  } else {
    /* 指定端口号。遍历TCP PCB链表,IP和PORT是否被占用。 */
    /* Check if the address already is in use (on all lists) */
    for (i = 0; i < max_pcb_list; i++) {
      for (cpcb = *tcp_pcb_lists[i]; cpcb != NULL; cpcb = cpcb->next) {
        if (cpcb->local_port == port) {
#if SO_REUSE
          /* 如果两个TCP PCB都设置了SO_REUSEADDR选项,则可以复用同一个IP和端口号 */
          if (!ip_get_option(pcb, SOF_REUSEADDR) ||
              !ip_get_option(cpcb, SOF_REUSEADDR))
#endif /* SO_REUSE */
          {
            /* @todo: check accept_any_ip_version */
            /* 注意:任意IP即是万能IP */
            if ((IP_IS_V6(ipaddr) == IP_IS_V6_VAL(cpcb->local_ip)) &&
                (ip_addr_isany(&cpcb->local_ip) ||
                 ip_addr_isany(ipaddr) ||
                 ip_addr_eq(&cpcb->local_ip, ipaddr))) {
              /* 如果IP和PORT已经被占用了,则返回ERR_USE */
              return ERR_USE;
            }
          }
        }
      }
    }
  }

  if (!ip_addr_isany(ipaddr) /* 绑定的IP不是任意IP */
#if LWIP_IPV4 && LWIP_IPV6
      /* 绑定的IP类型和原有IP类型不一致,也要更新 */
      || (IP_GET_TYPE(ipaddr) != IP_GET_TYPE(&pcb->local_ip))
#endif /* LWIP_IPV4 && LWIP_IPV6 */
     ) {
    /* 绑定IP,更新TCP PCB本地IP字段 */
    ip_addr_set(&pcb->local_ip, ipaddr);
  }
  /* 本地PORT */
  pcb->local_port = port;
  TCP_REG(&tcp_bound_pcbs, pcb);
  LWIP_DEBUGF(TCP_DEBUG, ("tcp_bind: bind to port %"U16_F"\n", port));
  return ERR_OK;
}

监听:tcp_listen()

用于服务端。

tcp_listen()调用tcp_listen_with_backlog()调用tcp_listen_with_backlog_and_err()

#define          tcp_listen(pcb) tcp_listen_with_backlog(pcb, TCP_DEFAULT_LISTEN_BACKLOG)
struct tcp_pcb *
tcp_listen_with_backlog(struct tcp_pcb *pcb, u8_t backlog)
{
  LWIP_ASSERT_CORE_LOCKED();
  return tcp_listen_with_backlog_and_err(pcb, backlog, NULL);
}

struct tcp_pcb *tcp_listen_with_backlog_and_err(struct tcp_pcb *pcb, u8_t backlog, err_t *err)

  • struct tcp_pcb *pcb:PCB。

  • u8_t backlog:等待accept()连接的上限值。

  • err_t *err:当返回NULL时,该回传参数包含错误原因。

  • 当前函数就是设置PCB进入LISTEN状态。如果已经是LISTEN状态,则不需要处理。

  • SO_REUSE:如果设置了SOF_REUSEADDR则需要检查是否有IP&PORT服务已经处于LISTEN状态,如果有,则本次进入LISTEN失败(因为不支持同时存在两个及以上的正常服务)。

  • 重置PCB的数据结构为tcp_pcb_listen,降低内存浪费。并初始化新的数据结构,当然包括lpcb->state = LISTEN;

    • 具体看本函数源码。
  • 把当前PCB插入tcp_listen_pcbs.pcbs链表中。

/**
 * @ingroup tcp_raw
 * 把当前PCB设为LISTEN状态(不可逆),表示可以处理连接进来的TCP客户端。
 * TCP PCB重新分配为监听专用的PCB,降低内存占用。
 *
 * @param pcb the original tcp_pcb
 * @param backlog the incoming connections queue limit
 * @param err when NULL is returned, this contains the error reason
 * @return tcp_pcb used for listening, consumes less memory.
 *
 * @note The original tcp_pcb is freed. This function therefore has to be
 *       called like this:
 *             tpcb = tcp_listen_with_backlog_and_err(tpcb, backlog, &err);
 */
struct tcp_pcb *
tcp_listen_with_backlog_and_err(struct tcp_pcb *pcb, u8_t backlog, err_t *err)
{
  struct tcp_pcb_listen *lpcb = NULL;
  err_t res;

  LWIP_UNUSED_ARG(backlog);

  LWIP_ASSERT_CORE_LOCKED();

  LWIP_ERROR("tcp_listen_with_backlog_and_err: invalid pcb", pcb != NULL, res = ERR_ARG; goto done);
  LWIP_ERROR("tcp_listen_with_backlog_and_err: pcb already connected", pcb->state == CLOSED, res = ERR_CLSD; goto done);

  if (pcb->state == LISTEN) {
    /* 已经是监听状态了,不需要重复处理 */
    lpcb = (struct tcp_pcb_listen *)pcb;
    res = ERR_ALREADY;
    goto done;
  }
#if SO_REUSE
  if (ip_get_option(pcb, SOF_REUSEADDR)) {
    /* Since SOF_REUSEADDR allows reusing a local address before the pcb's usage
       is declared (listen-/connection-pcb), we have to make sure now that
       this port is only used once for every local IP. */
    /* 不能有相同IP和PORT的TCP服务器 */
    for (lpcb = tcp_listen_pcbs.listen_pcbs; lpcb != NULL; lpcb = lpcb->next) {
      if ((lpcb->local_port == pcb->local_port) &&
          ip_addr_eq(&lpcb->local_ip, &pcb->local_ip)) {
        /* this address/port is already used */
        lpcb = NULL;
        res = ERR_USE;
        goto done;
      }
    }
  }
#endif /* SO_REUSE */
  /* 由于当前服务器原有的TCP PCB为tcp_pcb,对于TCP服务器的监听TCP来说,里面的很多字段都没用到,
      所以LWIP使用tcp_pcb_listen作为监听TCP的PCB,这样占用内存更小。 */
  /* 申请TCP LISTEN PCB资源 */
  lpcb = (struct tcp_pcb_listen *)memp_malloc(MEMP_TCP_PCB_LISTEN);
  if (lpcb == NULL) {
    res = ERR_MEM;
    goto done;
  }
  /* 申请成功,填写相关字段 */
  lpcb->callback_arg = pcb->callback_arg;
  lpcb->local_port = pcb->local_port;
  lpcb->state = LISTEN; /* 标记为监听状态 */
  lpcb->prio = pcb->prio;
  lpcb->so_options = pcb->so_options;
  lpcb->netif_idx = pcb->netif_idx;
  lpcb->ttl = pcb->ttl;
  lpcb->tos = pcb->tos;
#if LWIP_VLAN_PCP
  lpcb->netif_hints.tci = pcb->netif_hints.tci;
#endif /* LWIP_VLAN_PCP */
#if LWIP_IPV4 && LWIP_IPV6
  IP_SET_TYPE_VAL(lpcb->remote_ip, pcb->local_ip.type);
#endif /* LWIP_IPV4 && LWIP_IPV6 */
  ip_addr_copy(lpcb->local_ip, pcb->local_ip);
  if (pcb->local_port != 0) {
    /* 先把原生监听TCP PCB从tcp_bound_pcbs链表中移除 */
    TCP_RMV(&tcp_bound_pcbs, pcb);
  }
#if LWIP_TCP_PCB_NUM_EXT_ARGS
  /* copy over ext_args to listening pcb  */
  memcpy(&lpcb->ext_args, &pcb->ext_args, sizeof(pcb->ext_args));
#endif
  /* 释放原生监听TCP PCB */
  tcp_free(pcb);
#if LWIP_CALLBACK_API
  /* 配置默认accept() */
  lpcb->accept = tcp_accept_null;
#endif /* LWIP_CALLBACK_API */
#if TCP_LISTEN_BACKLOG
  /* 目前没有阻塞需要接入当前服务器的客户端连接 */
  lpcb->accepts_pending = 0;
  tcp_backlog_set(lpcb, backlog);
#endif /* TCP_LISTEN_BACKLOG */
  /* 修改点:https://github.com/yarrick/lwip/commit/6fb248c9e0a540112d0b4616b89f0130e4d57270 */
  /*        http://savannah.nongnu.org/task/?func=detailitem&item_id=10088#options */
  /* 把新的简版监听TCP PCB插回对应状态链表中 */
  TCP_REG(&tcp_listen_pcbs.pcbs, (struct tcp_pcb *)lpcb);
  res = ERR_OK;
done:
  if (err != NULL) {
    *err = res;
  }
  return (struct tcp_pcb *)lpcb;
}

接受连接:tcp_accept()

用于服务端。

注册一个accept()回调函数到TCP内核中,当TCP内核监听到TCP客户端并握手成功后会调用该回调函数通知应用层。

void tcp_accept(struct tcp_pcb *pcb, tcp_accept_fn accept)

  • struct tcp_pcb *pcb:PCB。
  • tcp_accept_fn accept:需要注册的回调函数。
/**
 * @ingroup tcp_raw
 * 用于指定当侦听连接已连接到另一个主机时应调用的函数。
 * @see MEMP_NUM_TCP_PCB_LISTEN and MEMP_NUM_TCP_PCB
 *
 * @param pcb tcp_pcb to set the accept callback
 * @param accept callback function to call for this pcb when LISTENing
 *        connection has been connected to another host
 *
 * 注册accept()函数,TCP服务器接受一条客户端连接时被调用。
 */
void
tcp_accept(struct tcp_pcb *pcb, tcp_accept_fn accept)
{
  LWIP_ASSERT_CORE_LOCKED();
  if ((pcb != NULL) && (pcb->state == LISTEN)) {
    struct tcp_pcb_listen *lpcb = (struct tcp_pcb_listen *)pcb;
    lpcb->accept = accept;
  }
}

连接远端:tcp_connect()

用于客户端。

会触发三次握手的接口。

对于服务端来说,绑定成功后还需要对该IP&PORT进行监听,监听到了就进行ACCETP处理即可,表示已经连接完成。

而对于客户端来说,绑定成功后,就可以调用当前函数连接服务端了。

err_t tcp_connect(struct tcp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port, tcp_connected_fn connected)

  • struct tcp_pcb *pcb:PCB。

  • const ip_addr_t *ipaddr:需要连接的远端IP地址。

  • u16_t port:需要连接的远端端口号。

  • tcp_connected_fn connected:连接情况回调函数。

  • 本函数用于连接到远端TCP主机。

  • 当前函数是非阻塞的,如果不能连接(如内存不足,参数错误等等),会立即返回。如果SYN报文能正常入队,则会立即返回ERR_OK

    • 当连接成功后,注册进去的connected()回调函数会被调用。
    • 当连接失败会调用之前注册的err()回调函数返回结果。(如对端主机拒绝连接、没收到对端响应等握手失败的可能)
  • 如果当前PCB的端口号为0,在当前连接函数中,也会随机分配一个空闲端口号。

  • SO_REUSE:如果设置了SOF_REUSEADDR选项值,则需要判断五元组唯一才能连接:本地IP、本地PORT、远端IP、远端PORT和TCP PCB状态。

    • 说明:复用IP和端口号,是不能复用连接的,所以复用的IP和端口号中,只能由一个能建立正常连接。
  • 初始化报文相关字段,如ISS(起始SEQ)、接收窗口、发送窗口、拥塞窗口、注册connected()回调。

  • SYN报文插入发送队列。

  • 调用tcp_output()触发处理发送队列的报文段。

/**
 * @ingroup tcp_raw
 * Connects to another host. 
 * The function given as the "connected" argument will be called when the connection has been established.
 * Sets up the pcb to connect to the remote host and sends the initial SYN segment which opens the connection.
 *
 * The tcp_connect() function returns immediately; it does not wait for the connection to be properly setup. 
 * Instead, it will call the function specified as the fourth argument (the "connected" argument) when the connection is established.
 * If the connection could not be properly established, either because the other host refused the connection or because the other host didn't answer, the "err" callback function of this pcb (registered with tcp_err, see below) will be called.
 *
 * The tcp_connect() function can return ERR_MEM if no memory is available for enqueueing the SYN segment.
 * If the SYN indeed was enqueued successfully, the tcp_connect() function returns ERR_OK.
 *
 * @param pcb the tcp_pcb used to establish the connection
 * @param ipaddr the remote ip address to connect to
 * @param port the remote tcp port to connect to
 * @param connected callback function to call when connected (on error,
                    the err callback will be called)
 * @return ERR_VAL if invalid arguments are given
 *         ERR_OK if connect request has been sent
 *         other err_t values if connect request couldn't be sent
 */
err_t
tcp_connect(struct tcp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port,
            tcp_connected_fn connected)
{
  struct netif *netif = NULL;
  err_t ret;
  u32_t iss;
  u16_t old_local_port;

  LWIP_ASSERT_CORE_LOCKED();

  LWIP_ERROR("tcp_connect: invalid pcb", pcb != NULL, return ERR_ARG);
  LWIP_ERROR("tcp_connect: invalid ipaddr", ipaddr != NULL, return ERR_ARG);

  LWIP_ERROR("tcp_connect: can only connect from state CLOSED", pcb->state == CLOSED, return ERR_ISCONN);

  LWIP_DEBUGF(TCP_DEBUG, ("tcp_connect to port %"U16_F"\n", port));
  ip_addr_set(&pcb->remote_ip, ipaddr);
  pcb->remote_port = port;

  if (pcb->netif_idx != NETIF_NO_INDEX) {
    netif = netif_get_by_index(pcb->netif_idx);
  } else {
    /* check if we have a route to the remote host */
    netif = ip_route(&pcb->local_ip, &pcb->remote_ip);
  }
  if (netif == NULL) {
    /* Don't even try to send a SYN packet if we have no route since that will fail. */
    return ERR_RTE;
  }

  /* check if local IP has been assigned to pcb, if not, get one */
  if (ip_addr_isany(&pcb->local_ip)) {
    const ip_addr_t *local_ip = ip_netif_get_local_ip(netif, ipaddr);
    if (local_ip == NULL) {
      return ERR_RTE;
    }
    ip_addr_copy(pcb->local_ip, *local_ip);
  }

#if LWIP_IPV6 && LWIP_IPV6_SCOPES
  /* If the given IP address should have a zone but doesn't, assign one now.
   * Given that we already have the target netif, this is easy and cheap. */
  if (IP_IS_V6(&pcb->remote_ip) &&
      ip6_addr_lacks_zone(ip_2_ip6(&pcb->remote_ip), IP6_UNICAST)) {
    ip6_addr_assign_zone(ip_2_ip6(&pcb->remote_ip), IP6_UNICAST, netif);
  }
#endif /* LWIP_IPV6 && LWIP_IPV6_SCOPES */

  old_local_port = pcb->local_port;
  if (pcb->local_port == 0) {
    pcb->local_port = tcp_new_port();
    if (pcb->local_port == 0) {
      return ERR_BUF;
    }
  } else {
#if SO_REUSE
    if (ip_get_option(pcb, SOF_REUSEADDR)) {
      /* 如果设置了SOF_REUSEADDR选项值,
          则需要判断五元组唯一才能连接:本地IP、本地PORT、远端IP、远端PORT和TCP PCB状态 */
      struct tcp_pcb *cpcb;
      int i;
      /* TCP PCB状态链表只遍历稳定态和TIME_WAIT状态的,不遍历绑定态和监听态的,因为设置了SOF_REUSEADDR,是允许客户端复用服务器的。 */
      for (i = 2; i < NUM_TCP_PCB_LISTS; i++) {
        for (cpcb = *tcp_pcb_lists[i]; cpcb != NULL; cpcb = cpcb->next) {
          if ((cpcb->local_port == pcb->local_port) &&
              (cpcb->remote_port == port) &&
              ip_addr_eq(&cpcb->local_ip, &pcb->local_ip) &&
              ip_addr_eq(&cpcb->remote_ip, ipaddr)) {
            /* linux returns EISCONN here, but ERR_USE should be OK for us */
            return ERR_USE;
          }
        }
      }
    }
#endif /* SO_REUSE */
  }

  iss = tcp_next_iss(pcb); /* 获取第一个要发送的seq号值 */
  pcb->rcv_nxt = 0;
  pcb->snd_nxt = iss;
  pcb->lastack = iss - 1;
  pcb->snd_wl2 = iss - 1;
  pcb->snd_lbb = iss - 1;
  /* 初始化接收窗口、窗口通告值、窗口通告值右边沿值 */
  pcb->rcv_wnd = pcb->rcv_ann_wnd = TCPWND_MIN16(TCP_WND);
  pcb->rcv_ann_right_edge = pcb->rcv_nxt;
  /* 初始化发送窗口 */
  pcb->snd_wnd = TCP_WND;
  /* 初始化MSS,LWIP限制在536 */
  pcb->mss = INITIAL_MSS;
#if TCP_CALCULATE_EFF_SEND_MSS
  /* 根据netif和远端IP来设置MSS */
  pcb->mss = tcp_eff_send_mss_netif(pcb->mss, netif, &pcb->remote_ip);
#endif /* TCP_CALCULATE_EFF_SEND_MSS */
  /* 拥塞窗口初始值 */
  pcb->cwnd = 1;
#if LWIP_CALLBACK_API
  /* 回调函数connected() */
  pcb->connected = connected;
#else /* LWIP_CALLBACK_API */
  LWIP_UNUSED_ARG(connected);
#endif /* LWIP_CALLBACK_API */

  /* 构造一个连接请求报文到TCP PCB中:SYN + MSS option */
  ret = tcp_enqueue_flags(pcb, TCP_SYN);
  if (ret == ERR_OK) {
    /* 更新为SYN_SENT状态 */
    pcb->state = SYN_SENT;
    if (old_local_port != 0) {
      /* 旧TCP PCB端口不为0,则将TCP PCB先从tcp_bound_pcbs状态链表移除 */
      TCP_RMV(&tcp_bound_pcbs, pcb);
    }
    /* 再把当前TCP PCB插入到稳定态tcp_active_pcbs链表 */
    TCP_REG_ACTIVE(pcb);
    MIB2_STATS_INC(mib2.tcpactiveopens);

    /* 将TCP PCB上的报文发送出去 */
    tcp_output(pcb);
  }
  return ret;
}

应用层通知TCP内核成功接收数据:tcp_recved()

tcp_recved()函数是被应用层调用,用于通知TCP内核:应用层已经从接收到的数据size,你可以释放这部分数据的内存了。

void tcp_recved(struct tcp_pcb *pcb, u16_t len)

  • struct tcp_pcb *pcb:pcb。

  • u16_t len:成功接收的长度。

  • 窗口滑动:

    • 当前接收窗口恢复。
    • 在糊涂窗口算法下,通告接收窗口。
/**
 * @ingroup tcp_raw
 * @param pcb the tcp_pcb for which data is read
 * @param len the amount of bytes that have been read by the application
 *
 * 应用程序从PCB缓冲区中提取走数据后,应该调用当前函数来更新当前PCB的接收窗口。
 *
 */
void
tcp_recved(struct tcp_pcb *pcb, u16_t len)
{
  u32_t wnd_inflation;
  tcpwnd_size_t rcv_wnd;

  LWIP_ASSERT_CORE_LOCKED();

  LWIP_ERROR("tcp_recved: invalid pcb", pcb != NULL, return);

  /* pcb->state LISTEN not allowed here */
  LWIP_ASSERT("don't call tcp_recved for listen-pcbs",
              pcb->state != LISTEN);

  /* 接收窗口扩大len */
  rcv_wnd = (tcpwnd_size_t)(pcb->rcv_wnd + len);
  /* 更新接收窗口值 */
  if ((rcv_wnd > TCP_WND_MAX(pcb)) || (rcv_wnd < pcb->rcv_wnd)) {
    /* window got too big or tcpwnd_size_t overflow */
    LWIP_DEBUGF(TCP_DEBUG, ("tcp_recved: window got too big or tcpwnd_size_t overflow\n"));
    pcb->rcv_wnd = TCP_WND_MAX(pcb);
  } else  {
    pcb->rcv_wnd = rcv_wnd;
  }

  /* 更新滑动窗口。支持糊涂窗口避免算法。 */
  wnd_inflation = tcp_update_rcv_ann_wnd(pcb);

  /* 如果接收窗口右边界滑动了 (1/4接收缓冲) || (4个MSS) 都可以立即发送窗口通告值到对端; */
  /* 如果接收窗口右边界滑动达不到阈值,就等正常发送数据时才附带窗口通告值。 */
  if (wnd_inflation >= TCP_WND_UPDATE_THRESHOLD) {
    tcp_ack_now(pcb);
    tcp_output(pcb);
  }

  LWIP_DEBUGF(TCP_DEBUG, ("tcp_recved: received %"U16_F" bytes, wnd %"TCPWNDSIZE_F" (%"TCPWNDSIZE_F").\n",
                          len, pcb->rcv_wnd, (u16_t)(TCP_WND_MAX(pcb) - pcb->rcv_wnd)));
}

关闭连接:tcp_close()

LISTEN状态、未连接的PCB直接被释放,不能再被引用。

如果PCB建立了连接(包括收到了SYN或处于closing状态),就关闭连接,并按照状态机转换进入对应的状态。其PCB会在tcp_slowtmr()慢时钟中被释放。

注意,当前函数也是一个协议不安全函数,存在必要时会发送RST来关闭连接导致数据丢失:(ESTABLISHED || CLOSE_WAIT) && (应用层还没读取完接收缓冲区的数据)

返回:

  • ERR_OK:关闭成功。

  • another err_t:关闭失败或PCB没有被释放。

    • ERR_MEM,在关闭连接时,可能需要发送FIN报文,这就需要申请报文段资源,如果申请失败,就表示FIN发送不了,返回ERR_MEM通知回来。

所以,既然tcp_close()这个接口会因为内存不足而导致关闭失败,返回ERR_MEM,那么我们就需要检查返回值操作,如遇到内部内存不足导致关闭失败就需要继续调用tcp_close(),而不是忽略返回值导致更多的内存泄漏。

err_t
tcp_close(struct tcp_pcb *pcb)
{
  LWIP_ASSERT_CORE_LOCKED();

  LWIP_ERROR("tcp_close: invalid pcb", pcb != NULL, return ERR_ARG);
  LWIP_DEBUGF(TCP_DEBUG, ("tcp_close: closing in "));

  tcp_debug_print_state(pcb->state);

  if (pcb->state != LISTEN) {
    /* Set a flag not to receive any more data... */
    tcp_set_flags(pcb, TF_RXCLOSED);
  }
  /* ... and close */
  return tcp_close_shutdown(pcb, 1);
}

热门相关:骑士归来   无限杀路      梦回大明春   梦回大明春