LWIP之TCP协议

IP协议提供了在各个主机之间传送数据报的功能,但是数据的最终目的地是主机上的特定应用程序。传输层协议就承担了这样的责任,典型的传输层协议有UDP和TCP两种。

UDP只为应用程序提供了一种无连接的、不可靠的传输服务。

TCP适用于可靠性要求很高的场合。TCP将所有数据看作数据流按照编号的顺序组织起来,采用正面确认以及重传等机制,保证数据流能全部正确到达,才把数据递交给应用层。许多著名的上层协议都是基于TCP实现的,如DNS、HTTP、FTP、SMTP、TELNET等。

报文格式

源端口号和目的端口号:用于标识发送端和接收端应用进程

序号:从发送端到接收端的数据的第一个字节编号。新连接建立时(SYN为1),发送方随机一个初始序号ISN

确认序号:ACK为1时有效,表示上次已成功收到数据字节序号加1

首部长度:TCP首部长度,以4字节为长度。如果没有任何选项字段,首部长度应该为5(20字节)

6个标志比特:

窗口大小:通知发送方接收缓冲区大小,用于实现流量控制

检验和:保证数据正确性

紧急指针:URG为1时有效,表示报文中包含紧急数据。紧急数据始终放在数据区的开始,紧急指针定义了紧急数据结束处

数据流编号

在IP协议中,会对每个数据报进行编号,而在TCP中,没有报文编号的概念,因为它的目标是数据流传输,数据流由连续的字节流组成,尽管在上层可能用各种各样的数据结构和格式来描述数据,但在TCP看来,数据都是字节流。TCP把一个连接中的所有数据字节都进行编号,当然两个方向上的编号是彼此独立的,编号的初始值由发送数据的一方随机选取,编号的取值范围在0~2^32-1。

紧急数据

TCP是一个面向数据流的协议,应用程序需要发送的所有数据将被组织成字节流,每个字节的数据都在流中占用一个位置,并且依次发送。但是在某些特殊情况下,发送方应用程序需要发送一些紧急数据,它并不期望这些紧急数据和普通数据一样被放在流中,而是期望接收方能优先读取这些数据。

使用URG位置1的报文段,这种类型的报文段将告诉接收方:这里面的数据是紧急的,可以直接读取,不必把它们放在接收缓冲里面。在包含紧急数据的报文段中,紧急数据总是放在数据区域的起始处,且报文首部中的紧急指针表明了紧急数据的最后一个字节。

强迫数据交互

TCP采用了缓冲机制来保证协议的高效性,在数据发送时,软件将延迟小分组数据的交付,它将等待足够长的时间,以期待接收更多的应用数据,最后再一起发送;在接收数据时,TCP首先是将数据放在接收缓冲中,只有在应用程序准备就绪或者TCP协议认为时机恰当的时候,数据才会交付给应用程序。

缓冲机制是出于对网络性能提升的考虑,但是这种机制也降低了应用程序的实时性。为了解决这个问题,发送方应用程序向TCP传递数据时,请求推送操作,这时TCP协议不会等待发送缓冲区被填满,而是直接将报文发送出去。同时,被推送出去的报文首部中推送位(PSH)将被置1,接收端在收到这类的报文时。会尽快将数据递交给应用程序,而不必缓冲更多的数据再递交。

LWIP对于TCP首部的定义如下

TCP首部结构体
struct tcp_hdr {
  PACK_STRUCT_FIELD(u16_t src);                    //源端口号
  PACK_STRUCT_FIELD(u16_t dest);                   //目的端口号
  PACK_STRUCT_FIELD(u32_t seqno);                  //序号
  PACK_STRUCT_FIELD(u32_t ackno);                  //确认序号
  PACK_STRUCT_FIELD(u16_t _hdrlen_rsvd_flags);     //首部长度+保留位+标志位
  PACK_STRUCT_FIELD(u16_t wnd);                    //窗口大小
  PACK_STRUCT_FIELD(u16_t chksum);                 //校验和
  PACK_STRUCT_FIELD(u16_t urgp);                   //紧急指针
} PACK_STRUCT_STRUCT;

/* TCP头部标志位 */
#define TCP_FIN 0x01U        //终止连接
#define TCP_SYN 0x02U        //发起连接,同步序号
#define TCP_RST 0x04U        //复位连接
#define TCP_PSH 0x08U        //推送数据
#define TCP_ACK 0x10U        //确认序号有效
#define TCP_URG 0x20U        //紧急指针有效
#define TCP_ECE 0x40U        //ECN相关字段,lwip不支持ECN
#define TCP_CWR 0x80U        //ECN相关字段,lwip不支持ECN
#define TCP_FLAGS 0x3fU      //有效TCP首部标志

控制块

LwIP中定义了两种类型的TCP控制块,一种专门用于描述处于LISTEN状态的连接,另一种用于描述处于其他状态的连接。

#define TCP_PCB_COMMON(type) \
  type *next;                \        //用于将控制块组成链表
  enum tcp_state state;      \        //连接的状态
  u8_t prio;                 \        //优先级,可用于回收低于优先级控制块
  void *callback_arg;        \        //指向用户自定义数据,在函数回调时使用
  u16_t local_port;          \        //连接绑定的本地端口
  err_t (*accept)(void *arg, struct tcp_pcb *newpcb, err_t err)    //接受连接回调函数
struct tcp_pcb_listen {
  IP_PCB;                                    //包含IP相关数据
  TCP_PCB_COMMON(struct tcp_pcb_listen);     //TCP控制块共有数据
};
struct tcp_pcb {
  IP_PCB;                                //IP相关数据
  TCP_PCB_COMMON(struct tcp_pcb);        //TCP控制块共有数据

  u16_t remote_port;                     //远端端口号
  u8_t flags;                            //控制块状态、标志字段

#define TF_ACK_DELAY   ((u8_t)0x01U)     //延迟发送ACK(推迟确认)
#define TF_ACK_NOW     ((u8_t)0x02U)     //立即发送ACK
#define TF_INFR        ((u8_t)0x04U)     //连接处于快重传状态
#define TF_TIMESTAMP   ((u8_t)0x08U)     //连接的时间戳使能
#define TF_FIN         ((u8_t)0x20U)     //应用程序已关闭该连接
#define TF_NODELAY     ((u8_t)0x40U)     //进制Nagle算法
#define TF_NAGLEMEMERR ((u8_t)0x80U)     //本地缓冲区溢出

  /* 接收窗口相关字段 */
  u32_t rcv_nxt;
  u16_t rcv_wnd;
  u16_t rcv_ann_wnd;
  u32_t rcv_ann_right_edge;

  /* 定时器相关字段 */
  u32_t tmr;
  u8_t polltmr, pollinterval;
  s16_t rtime;
  
  /* 最大报文段大小 */
  u16_t mss;                             
  
  /* RTT相关字段 */
  u32_t rttest;
  u32_t rtseq;
  s16_t sa, sv;

  /* rto相关字段 */
  s16_t rto;
  u8_t nrtx;

  /* 快速重传与恢复相关字段 */
  u32_t lastack;
  u8_t dupacks;
  
  /* 拥塞控制 */
  u16_t cwnd;  
  u16_t ssthresh;

  /* 发送窗口相关字段 */
  u32_t snd_nxt;
  u16_t snd_wnd;
  u32_t snd_wl1, snd_wl2;
  u32_t snd_lbb;

  /* 发送缓冲区相关字段 */
  u16_t acked;
  u16_t snd_buf;
#define TCP_SNDQUEUELEN_OVERFLOW (0xffff-3)
  u16_t snd_queuelen;
  struct tcp_seg *unsent;
  struct tcp_seg *unacked;

  /* 接收缓冲区相关字段 */
  struct tcp_seg *ooseq;
  struct pbuf *refused_data;

  /* 回调函数 */
  err_t (*sent)(void *arg, struct tcp_pcb *pcb, u16_t space);
  err_t (*recv)(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err);
  err_t (*connected)(void *arg, struct tcp_pcb *pcb, err_t err);
  err_t (*poll)(void *arg, struct tcp_pcb *pcb);
  void (*errf)(void *arg, err_t err);

  /* 保活定时器相关字段 */
  u32_t keep_idle;
  u32_t keep_intvl;
  u32_t keep_cnt;
  u8_t keep_cnt_sent;

  /* 坚持定时器相关字段 */
  u32_t persist_cnt;
  u8_t persist_backoff;
};

为了组织和描述系统内的所有TCP控制块,内核定义了四条链表来连接处于不同状态下的控制块。

struct tcp_pcb *tcp_bound_pcbs;            //绑定本地端口的控制块
union tcp_listen_pcbs_t tcp_listen_pcbs;   //侦听状态下的控制块
struct tcp_pcb *tcp_active_pcbs;           //处于其他状态的控制块
struct tcp_pcb *tcp_tw_pcbs;               //TIME-WAIT状态下的控制块

连接机制

TCP协议为通信双方提供了完善的连接建立机制和连接断开机制。

客户端和服务器建立连接的过程称为三次握手过程

  1. 客户端发送一个SYN标志置1的TCP报文段,报文段首部指明自己的端口号及期望连接的服务器端口号,通常服务器端口号为一个熟知端口号,客户端选择的端口号通常为一个短暂端口号,可以由一TCP软件自动分配。同时在报文段中,客户端需要通告自己的初始序列号ISN。除此之外,这个报文中还可以携带一些选项字段,例如前面所说的最大报文段大小、窗口扩大因子,选项将客户端的一些连接属性通告给服务器。
  2. 当服务器接收到该报文并解析后,返回一个SYN和ACK标志置1的报文段作为应答。ACK为1表示该报文包含了有效的确认号,这个值应该是客户端初始序列号加1(即A+1)。另一方面,SYN标志表明服务器响应连接,并在回应报文中包含服务器自身选定的初始序号ISN(这里假设为B)。服务器可以在这个报文段中加上选项字段,告诉客户端自己的连接属性,同时报文首部中的窗口大小也有效,它向客户端指明自己的接收窗口大小。
  3. 当客户端接收到服务器的SYN应答报文后,会再次产生ACK置位的报文段,该报文段包含了对服务器SYN报文段的有效确认号,该值为服务器发送的ISN加1(即B+1)。同时,该报文段中还包含了有效的窗口大小,用来向服务器指明客户端的接收窗口大小。

                                                                    

客户端和服务器断开连接的过程称为四次握手过程

  1. 当客户端应用程序主动执行关闭操作时,客户端会向服务器发送一个FIN标志置1的报文段,用来关闭从客户端到服务器的数据传送,假如该报文段序号字段值为C。
  2. 当服务器收到这个FIN报文段后,它返回一个ACK报文,确认序号为收到的序号加1(即C+1)。和SYN一样,一个FIN将占用一个序号。当客户端接收到这个ACK后,从客户端到服务器方向的连接就断开了。
  3. 服务器TCP向其上层应用程序通告客户端的断开操作,这会导致服务器程序关闭它的连接,同样,此时一个FIN标志置1的报文段将被发送到客户端,假设序号为D。
  4. 客户端也会返回一个ACK报文段(D+1)作为响应,以完成该方向连接的断开操作。

                                                         

客户端和服务器复位连接

在正常情况下,通信双方通过使用关闭连接的握手过程来结束一个连接,但是在连接出现异常的情况下(例如TCP软件在检测到一个SYN报文段请求连接的端口在本地并不存在时),TCP可以直接中断这个连接,这就是连接的复位。复位连接时,发送方将发送一个RST标志被置1的报文段,接收方对复位报文段的处理是直接终止该连接上的数据传送,释放发送、接收缓冲,并向应用程序通知连接复位。

TCP为每个连接定义了11中状态,在LwIP中,这11中状态的定义如下

enum tcp_state {
  CLOSED      = 0,    //没有连接
  LISTEN      = 1,    //服务器进入侦听状态,等待客户端的连接请求
  SYN_SENT    = 2,    //连接请求已发送,等待确认
  SYN_RCVD    = 3,    //已接收到对方连接请求
  ESTABLISHED = 4,    //连接已建立
  FIN_WAIT_1  = 5,    //程序已关闭该连接
  FIN_WAIT_2  = 6,    //另一端已接收关闭连接
  CLOSE_WAIT  = 7,    //等待程序关闭连接
  CLOSING     = 8,    //两端同时收到对方的关闭请求
  LAST_ACK    = 9,    //服务器等待对方接受关闭操作
  TIME_WAIT   = 10    //关闭成功,等待网络中可能出现的剩余数据
};

新建TCP控制块

struct tcp_pcb *tcp_new(void)
{
  return tcp_alloc(TCP_PRIO_NORMAL);
}

struct tcp_pcb *tcp_alloc(u8_t prio)
{
  struct tcp_pcb *pcb;
  u32_t iss;
  
  /* 为控制块申请内存 */
  pcb = memp_malloc(MEMP_TCP_PCB);
  if (pcb == NULL) {
    tcp_kill_timewait();
    pcb = memp_malloc(MEMP_TCP_PCB);
    if (pcb == NULL) {
      tcp_kill_prio(prio);
      pcb = memp_malloc(MEMP_TCP_PCB);
      if (pcb != NULL) {
      }
    }
    if (pcb != NULL) {
    }
  }

  /* 申请成功,初始化字段 */
  if (pcb != NULL) {
    memset(pcb, 0, sizeof(struct tcp_pcb));
    pcb->prio = TCP_PRIO_NORMAL;
    pcb->snd_buf = TCP_SND_BUF;
    pcb->snd_queuelen = 0;
    pcb->rcv_wnd = TCP_WND;
    pcb->rcv_ann_wnd = TCP_WND;
    pcb->tos = 0;
    pcb->ttl = TCP_TTL;
    pcb->mss = (TCP_MSS > 536) ? 536 : TCP_MSS;
    pcb->rto = 3000 / TCP_SLOW_INTERVAL;
    pcb->sa = 0;
    pcb->sv = 3000 / TCP_SLOW_INTERVAL;
    pcb->rtime = -1;
    pcb->cwnd = 1;
    iss = tcp_next_iss();
    pcb->snd_wl2 = iss;
    pcb->snd_nxt = iss;
    pcb->lastack = iss;
    pcb->snd_lbb = iss;   
    pcb->tmr = tcp_ticks;
    pcb->polltmr = 0;
    pcb->recv = tcp_recv_null;
    pcb->keep_idle  = TCP_KEEPIDLE_DEFAULT;
    pcb->keep_intvl = TCP_KEEPINTVL_DEFAULT;
    pcb->keep_cnt   = TCP_KEEPCNT_DEFAULT;
    pcb->keep_cnt_sent = 0;
  }

  return pcb;
}

控制块绑定IP和端口号

err_t tcp_bind(struct tcp_pcb *pcb, struct ip_addr *ipaddr, u16_t port)
{
  struct tcp_pcb *cpcb;

  if (port == 0) {
    port = tcp_new_port();
  }

  /* 检查端口号和IP地址是否已经使用 */
  for(cpcb = (struct tcp_pcb *)tcp_listen_pcbs.pcbs; cpcb != NULL; cpcb = cpcb->next) {
    if (cpcb->local_port == port) {
      if (ip_addr_isany(&(cpcb->local_ip)) ||
          ip_addr_isany(ipaddr) ||
          ip_addr_cmp(&(cpcb->local_ip), ipaddr)) {
        return ERR_USE;
      }
    }
  }

  for(cpcb = tcp_active_pcbs; cpcb != NULL; cpcb = cpcb->next) {
    if (cpcb->local_port == port) {
      if (ip_addr_isany(&(cpcb->local_ip)) ||
          ip_addr_isany(ipaddr) ||
          ip_addr_cmp(&(cpcb->local_ip), ipaddr)) {
        return ERR_USE;
      }
    }
  }

  for(cpcb = tcp_bound_pcbs; cpcb != NULL; cpcb = cpcb->next) {
    if (cpcb->local_port == port) {
      if (ip_addr_isany(&(cpcb->local_ip)) ||
          ip_addr_isany(ipaddr) ||
          ip_addr_cmp(&(cpcb->local_ip), ipaddr)) {
        return ERR_USE;
      }
    }
  }

  for(cpcb = tcp_tw_pcbs; cpcb != NULL; cpcb = cpcb->next) {
    if (cpcb->local_port == port) {
      if (ip_addr_cmp(&(cpcb->local_ip), ipaddr)) {
        return ERR_USE;
      }
    }
  }

  /* 设置IP和端口号 */
  if (!ip_addr_isany(ipaddr)) {
    pcb->local_ip = *ipaddr;
  }
  pcb->local_port = port;

  /* 将控制块挂接到链表 */
  TCP_REG(&tcp_bound_pcbs, pcb);

  return ERR_OK;
}

服务器侦听

#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)
{
  struct tcp_pcb_listen *lpcb;

  if (pcb->state == LISTEN) {
    return pcb;
  }

  /* 为监听控制块分配新的内存空间 */
  lpcb = memp_malloc(MEMP_TCP_PCB_LISTEN);
  if (lpcb == NULL) {
    return NULL;
  }

  /* 将相关字段拷贝到监听控制块 */
  lpcb->callback_arg = pcb->callback_arg;
  lpcb->local_port = pcb->local_port;
  lpcb->state = LISTEN;
  lpcb->so_options = pcb->so_options;
  lpcb->so_options |= SOF_ACCEPTCONN;
  lpcb->ttl = pcb->ttl;
  lpcb->tos = pcb->tos;
  ip_addr_set(&lpcb->local_ip, &pcb->local_ip);

  /* 将原控制块从绑定链表中移除 */
  TCP_RMV(&tcp_bound_pcbs, pcb);

  /* 释放原控制块内存 */
  memp_free(MEMP_TCP_PCB, pcb);

  /* 接收客户端连接的默认回调函数 */
  lpcb->accept = tcp_accept_null;

  /* 控制块接入侦听链表 */
  TCP_REG(&tcp_listen_pcbs.listen_pcbs, lpcb);

  return (struct tcp_pcb *)lpcb;
}

连接服务器

err_t tcp_connect(struct tcp_pcb *pcb, struct ip_addr *ipaddr, u16_t port, err_t (* connected)(void *arg, struct tcp_pcb *tpcb, err_t err))
{
  err_t ret;
  u32_t iss;

  /* 设置远程IP和端口号 */
  if (ipaddr != NULL) {
    pcb->remote_ip = *ipaddr;
  } else {
    return ERR_VAL;
  }
  pcb->remote_port = port;
  if (pcb->local_port == 0) {
    pcb->local_port = tcp_new_port();
  }

  /* 初始化序号 */
  iss = tcp_next_iss();

  /* 初始化控制块各个字段 */
  pcb->rcv_nxt = 0;
  pcb->snd_nxt = iss;
  pcb->lastack = iss - 1;
  pcb->snd_lbb = iss - 1;
  pcb->rcv_wnd = TCP_WND;
  pcb->rcv_ann_wnd = TCP_WND;
  pcb->rcv_ann_right_edge = pcb->rcv_nxt;
  pcb->snd_wnd = TCP_WND;
  pcb->mss = (TCP_MSS > 536) ? 536 : TCP_MSS;
  pcb->cwnd = 1;
  pcb->ssthresh = pcb->mss * 10;
  pcb->state = SYN_SENT;
  pcb->connected = connected;

  /* 将控制块从绑定链表移除,插入active链表 */
  TCP_RMV(&tcp_bound_pcbs, pcb);
  TCP_REG(&tcp_active_pcbs, pcb);

  /* 组建SYN报文并发送 */
  ret = tcp_enqueue(pcb, NULL, 0, TCP_SYN, 0, TF_SEG_OPTS_MSS);
  if (ret == ERR_OK) { 
    tcp_output(pcb);
  }

  return ret;
} 

关闭连接

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

  /* 将控制块从相应的链表移除并释放/发送FIN报文关闭连接 */
  switch (pcb->state) {
  case CLOSED:
    err = ERR_OK;
    TCP_RMV(&tcp_bound_pcbs, pcb);
    memp_free(MEMP_TCP_PCB, pcb);
    pcb = NULL;
    break;

  case LISTEN:
    err = ERR_OK;
    tcp_pcb_remove((struct tcp_pcb **)&tcp_listen_pcbs.pcbs, pcb);
    memp_free(MEMP_TCP_PCB_LISTEN, pcb);
    pcb = NULL;
    break;

  case SYN_SENT:
    err = ERR_OK;
    tcp_pcb_remove(&tcp_active_pcbs, pcb);
    memp_free(MEMP_TCP_PCB, pcb);
    pcb = NULL;
    snmp_inc_tcpattemptfails();
    break;

  case SYN_RCVD:
    err = tcp_send_ctrl(pcb, TCP_FIN);
    if (err == ERR_OK) {
      snmp_inc_tcpattemptfails();
      pcb->state = FIN_WAIT_1;
    }
    break;

  case ESTABLISHED:
    err = tcp_send_ctrl(pcb, TCP_FIN);
    if (err == ERR_OK) {
      snmp_inc_tcpestabresets();
      pcb->state = FIN_WAIT_1;
    }
    break;

  case CLOSE_WAIT:
    err = tcp_send_ctrl(pcb, TCP_FIN);
    if (err == ERR_OK) {
      snmp_inc_tcpestabresets();
      pcb->state = LAST_ACK;
    }
    break;

  default:
    err = ERR_OK;
    pcb = NULL;
    break;
  }

  if (pcb != NULL && err == ERR_OK) {
    tcp_output(pcb);
  }

  return err;
}

输出处理

err_t tcp_write(struct tcp_pcb *pcb, const void *data, u16_t len, u8_t apiflags)
{
  /* 连接建立、等待应用程序关闭、连接请求已发送、已收到连接请求 */
  if (pcb->state == ESTABLISHED || pcb->state == CLOSE_WAIT ||
      pcb->state == SYN_SENT || pcb->state == SYN_RCVD) {
    /* 组装并发送数据 */
    if (len > 0) {
      return tcp_enqueue(pcb, (void *)data, len, 0, apiflags, 0);
    }
    return ERR_OK;
  } else {
    return ERR_CONN;
  }
}

err_t tcp_enqueue(struct tcp_pcb *pcb, void *arg, u16_t len, u8_t flags, u8_t apiflags, u8_t optflags)
{
  struct pbuf *p;
  struct tcp_seg *seg, *useg, *queue;
  u32_t seqno;
  u16_t left, seglen;
  void *ptr;
  u16_t queuelen;
  u8_t optlen;

  /* 发送缓冲区不够 */
  if (len > pcb->snd_buf) {
    pcb->flags |= TF_NAGLEMEMERR;
    return ERR_MEM;
  }
  left = len;
  ptr = arg;

  optlen = LWIP_TCP_OPT_LENGTH(optflags);

  seqno = pcb->snd_lbb;

  /* 超过pbuf上限值 */
  queuelen = pcb->snd_queuelen;
  if ((queuelen >= TCP_SND_QUEUELEN) || (queuelen > TCP_SNDQUEUELEN_OVERFLOW)) {
    pcb->flags |= TF_NAGLEMEMERR;
    return ERR_MEM;
  }

  /* 将数据组装成TCP报文段 */
  useg = queue = seg = NULL;
  seglen = 0;
  while (queue == NULL || left > 0) {
    seglen = left > (pcb->mss - optlen) ? (pcb->mss - optlen) : left;

    seg = memp_malloc(MEMP_TCP_SEG);
    if (seg == NULL) {
      goto memerr;
    }
    seg->next = NULL;
    seg->p = NULL;

    if (queue == NULL) {
      queue = seg;
    }
    else {
      useg->next = seg;
    }
    useg = seg;

    if (apiflags & TCP_WRITE_FLAG_COPY) {
      if ((seg->p = pbuf_alloc(PBUF_TRANSPORT, seglen + optlen, PBUF_RAM)) == NULL) {
        goto memerr;
      }
      queuelen += pbuf_clen(seg->p);
      if (arg != NULL) {
        MEMCPY((char *)seg->p->payload + optlen, ptr, seglen);
      }
      seg->dataptr = seg->p->payload;
    }
    else {
      if ((seg->p = pbuf_alloc(PBUF_TRANSPORT, optlen, PBUF_RAM)) == NULL) {
        goto memerr;
      }
      queuelen += pbuf_clen(seg->p);

      if (left > 0) {
        if ((p = pbuf_alloc(PBUF_RAW, seglen, PBUF_ROM)) == NULL) {
          pbuf_free(seg->p);
          seg->p = NULL;
          goto memerr;
        }
        ++queuelen;
        p->payload = ptr;
        seg->dataptr = ptr;

        pbuf_cat(seg->p, p);
        p = NULL;
      }
    }

    if ((queuelen > TCP_SND_QUEUELEN) || (queuelen > TCP_SNDQUEUELEN_OVERFLOW)) {
      goto memerr;
    }

    seg->len = seglen;

    if (pbuf_header(seg->p, TCP_HLEN)) {
      goto memerr;
    }
    seg->tcphdr = seg->p->payload;
    seg->tcphdr->src = htons(pcb->local_port);
    seg->tcphdr->dest = htons(pcb->remote_port);
    seg->tcphdr->seqno = htonl(seqno);
    seg->tcphdr->urgp = 0;
    TCPH_FLAGS_SET(seg->tcphdr, flags);

    seg->flags = optflags;

    TCPH_HDRLEN_SET(seg->tcphdr, (5 + optlen / 4));

    left -= seglen;
    seqno += seglen;
    ptr = (void *)((u8_t *)ptr + seglen);
  }

  /* 将报文段挂接到未发送队列 */
  if (pcb->unsent == NULL) {
    useg = NULL;
  }
  else {
    for (useg = pcb->unsent; useg->next != NULL; useg = useg->next);
  }

  if (useg != NULL &&
    TCP_TCPLEN(useg) != 0 &&
    !(TCPH_FLAGS(useg->tcphdr) & (TCP_SYN | TCP_FIN)) &&
    (!(flags & (TCP_SYN | TCP_FIN)) || (flags == TCP_FIN)) &&
    (useg->len + queue->len <= pcb->mss) &&
    (useg->flags == queue->flags) &&
    (ntohl(useg->tcphdr->seqno) + useg->len == ntohl(queue->tcphdr->seqno)) ) {
    if(pbuf_header(queue->p, -(TCP_HLEN + optlen))) {
      goto memerr;
    }
    if (queue->p->len == 0) {
      struct pbuf *old_q = queue->p;
      queue->p = queue->p->next;
      old_q->next = NULL;
      queuelen--;
      pbuf_free(old_q);
    }
    if (flags & TCP_FIN) {
      TCPH_SET_FLAG(useg->tcphdr, TCP_FIN);
    } else {
      pbuf_cat(useg->p, queue->p);
      useg->len += queue->len;
      useg->next = queue->next;
    }

    if (seg == queue) {
      seg = useg;
      seglen = useg->len;
    }
    memp_free(MEMP_TCP_SEG, queue);
  }
  else {
    if (useg == NULL) {
      pcb->unsent = queue;
    }
    else {
      useg->next = queue;
    }
  }

  /* 调整相关字段 */
  if ((flags & TCP_SYN) || (flags & TCP_FIN)) {
    ++len;
  }
  if (flags & TCP_FIN) {
    pcb->flags |= TF_FIN;
  }
  pcb->snd_lbb += len;

  pcb->snd_buf -= len;

  pcb->snd_queuelen = queuelen;

  if (seg != NULL && seglen > 0 && seg->tcphdr != NULL && ((apiflags & TCP_WRITE_FLAG_MORE)==0)) {
    TCPH_SET_FLAG(seg->tcphdr, TCP_PSH);
  }

  return ERR_OK;
memerr:
  pcb->flags |= TF_NAGLEMEMERR;

  if (queue != NULL) {
    tcp_segs_free(queue);
  }

  return ERR_MEM;
}

差错控制(确认与重传)

确认与重传,接收方通过确认的机制来告诉发送方数据的接收状况,这是通过向发送方返回一个确认号来完成的,确认号标识出自己期望接收到的下一个字节的编号。如果接收方只收到报文段1和3,那么返回的确认号仍然是320。

                                  

发送方为每个报文启动一个定时器,在指定的时间内没有收到确认,会认为报文发送失败,并重传报文。超时重传,相关的字段包括rtime、rttest、rtseq、sa、sv、rto和nrtx。rtime表示重传定时器,它的值每500ms被内核加1,当该值超过rto时,报文段重传将发生;rttest用于对某个报文段计时,测算该报文段在两台主机之间的往返时间,不同的时间点、不同的网络状况下,往返时间的估计都会各有差异,TCP会根据往返时间的估计值动态设置各个报文段的超时间隔rto;rtseq表示了当前正在进行往返时间估计的报文段序号;sa、sv是与超时时间rto设置密切相关的两个字段;rto表示超时时间间隔;nrtx表示报文段被重传的次数,也是与rto的设置密切相关。

怎样决定超时间隔(RTO)是提高TCP性能的关键,而这些都与往返时间(RTT)估计密切相关。发送方可能在某段时间内连续发送多个报文段,但只能选择一个报文段作为基准,启动一个定时器以测量其RTT值。TCP/IP协议利用一些优化算法平滑RTT的值,使得这些报文段测量出来的RTT值更具有效性,而在往返时间变化起伏很大时,基于均值和方差来计算RTO能提供更好的效果。

              

上面各式中,M表示某次测量的RTT值;A表示已测得的RTT平均值;D值为RTT估计的方差;g和h都为常数,一般g取1/8,h取1/4。在算法初始时,RTO取值为6,即3s;A值为0,D值为6。

注:sa和sv是计算中间量,其中sa = 8A、sv = 4D。

当重发后仍然收不到确认,很可能是网络不通或者网络阻塞。如果还使用原来的RTO重发数据包,势必使问题越来越严重。参照TCP/IP标准中的算法,LwIP的做法是:如果重发的报文超时,则接下来的重发必须将RTO的值设置为前一次的两倍,且当重发次数超过上限后,停止重发。

注:重发报文期间不进行RTT估计。

缓冲机制

在发送方,上层应用程序可能会将各种各样、大小各异的数据流交给TCP发送,TCP提供了完整的缓冲机制来提高传送效率并减少网络中的通信量,在少量数据发送时,协议通常会短暂延迟数据的发送时间,以缓冲到更多的用户数据,以组成一个最佳大小的报文段发送出去;对于每个发送出去的报文,TCP不会马上删除它们,而是将它们保存在内部缓冲中,以便需要的时候重传,只有等到对应的确认到达后,报文才会在缓冲区中被删除;同理,接收方也必须维护良好的缓冲机制,因为底层的报文可能是无序到达的,这里需要把各个无序报文组织为有序数据流,重复的报文会被删除。

流量控制(滑动窗口)

发送方发送报文速度很快,而接收方处理报文的速度很慢,这时在接收方会发生数据丢失的情况,丢失最终导致发送方的重传,这使得整个连接的通信效率降低;另一方面,如果发生每次只发送很少的数据,然后等待确认的返回,而接收方在处理完数据并返回确认后,又继续等待接收后续的数据,这样不管是接收方还是发送方在很多时候都会处于等待状态,连接的效率也无法得到提升。TCP中引进了滑动窗口的概念来进行流量的控制,接收数据的一方可以向发送方通告自己接收窗口的大小,告诉发送方自己的接收能力,而发送方可以根据这个窗口的大小来发送尽可能多的数据,同时又保证了接收方能够处理这些数据。

接收窗口,相关的字段包括rcv_nxt、rcv_wnd、rcv_ann_wnd和rcv_ann_right_edge。rcv_nxt是自己期望接收到的下一个数据字编号;rcv_wnd表示接收窗口的大小,收到数据会减小,上层取走数据则增大;rcv_ann_wnd表示将向对方通告的窗口大小值,当rcv_wnd改变时,内核会计算出一个合理的通告窗口值rcv_ann_wnd;rcv_ann_right_edge记录了上一次窗口通告时窗口右边界取值。

                                               

发送窗口,相关的字段包括lastack、snd_nxt、snd_wnd、snd_lbb、snd_wl1和snd_wl2。lastack记录了被接收方确认的最高序列号;snd_nxt表示自己将要发送的下一个数据的起始编号;snd_wnd记录了当前的发送窗口大小,它常被设置为接收方通告的接收窗口值;snd_lbb记录了下一个将被应用程序缓存的数据的起始编号;snd_wl1记录上一次窗口更新时收到的数据序号;snd_wl2记录上一次窗口更新时收到的确认序号。

                                              

拥塞控制(慢启动与拥塞避免)

拥塞控制考虑的是网络传输状况。在路由器发送拥塞时,会丢弃不能处理的数据报,这将导致发送方因接收不到确认而重传,重传的数据同样不会成功,且重传会使得路由器中拥塞更为严重。拥塞发生时报文被丢弃,但是发送方不会得到任何报文丢失的信息,因此,发送方必须实现一种自适应机制,及时检测网络中的拥塞状况,自动调节数据的发送速度,这样才能提高数据发送的成功率。

拥塞控制需要考虑两种情况:

1.在连接刚建立时,无法得知合理的报文发送速率。

2.在产生拥塞之后,必须减小报文发送速率。

慢启动算法,为发送方增加了阻塞窗口,相关字段为cwnd。阻塞窗口被初始化为1个报文段大小。当发送方检测到拥塞时(超时或收到重复确认),ssthresh被设置为有效发送窗口的一半,cwnd被设置为1个报文段大小。发送方取阻塞窗口与通告窗口两者中的最小值(有效发送窗口大小)作为发送上限。每收到一个ACK,阻塞窗口增加一个报文段大小。一旦发现报文丢失,发送方就把阻塞窗口减半,直至减少至最小的窗口(至少一个报文段)。

拥塞避免算法,相关字段为ssthresh,表示拥塞避免启动门限。在LwIP中,客户端初始值被设置为10个报文段大小,服务器初始值被设置为对方通告的接收窗口大小。当发送方检测到拥塞时(超时或收到重复确认),ssthresh被设置为有效发送窗口的一半,cwnd被设置为1个报文段大小。每次收到一个确认时将cwnd增加1/cwnd,当整个窗口内的数据被确认,cwnd值只增加了1。

如果cwnd小于或等于ssthresh,则处于慢启动阶段,否则处于拥塞避免阶段。

快速重传与快速恢复

两种情况可能使发送方得到重复的ACK(能够不断收到重复ACK,说明网络不存在拥塞)

  1. 序号靠前的报文段丢失。
  2. 各个报文在网络中独立路由,序号靠前的报文段较其他报文段晚到达接收端。

如果网络环境不稳定导致报文段丢失,需要重传报文。但是TCP存在超时重传机制,这带来了一些新的问题

  1. 当一个报文段丢失时,会等待一定的超时周期然后才重传分组,增加了端到端的时延。
  2. 当一个报文段丢失时,在等待超时的过程中,其后的报文段已经被接收端接收但却迟迟得不到确认,发送端会认为也丢失了,从而引起不必要的重传,既浪费资源也浪费时间。

快速重传算法,当发送机接收到三个重复确认时,TCP假定网络环境不稳定导致报文段丢失。此时,发送方无需等待超时定时器溢出就进行重传,TCP进入快速重传模式。ssthresh设置为有效发送窗口的一半,cwnd设置为ssthresh + 3个报文段大小,此后每收到一个重复确认cwnd增加一个报文段大小。

快速恢复算法,若收到了新报文段的确认,将cwnd设置为ssthresh,即后续直接执行拥塞避免算法。

糊涂窗口与避免

糊涂窗口综合征(SWS):可能是由发送方引起的,也可能是由接收方引起的,最终导致网络上产生很多的小报文段。小报文段中IP首部和TCP首部这些字段占了大部分空间,而真正的TCP数据却很少,小报文段的传输浪费了网络的大量带宽。

1、发送方引起的糊涂窗口综合征

发送端产生数据很慢,一次将一个字节的数据写入发送端的TCP缓存。如果发送端的TCP没有特定的指令,它就产生一个字节数据的报文段,结果有很多41字节的IP数据报就在互连网中传来传去。

解决方法是:强迫发送端的TCP收集数据,然后用一个更大的数据块来发送。如果发送端的TCP等待时间过长,就会降低实时性;等待时间不够长,就可能发送较小的报文段。为了解决这个问题,Nagle发明了Nagle算法。

Nagle算法(LWIP的实现方式):

(1)如果不存在已发送未确认的报文段,则允许发送

(2)如果该包含有FIN,则允许发送;

(3)设置了TCP_NODELAY选项,则允许发送;

(4)未发送报文段大于等于两个,则允许发送;

(5)第一个未发送的报文段长度达到MSS,则允许发送;

2、接收方引起的糊涂窗口综合征

接收端消耗数据很慢,缓存满了,一次消耗一个字节。接收端的TCP通告其窗口大小为1字节,发送端就发送一个字节数据的报文段,结果有很多41字节的IP数据报就在互连网中传来传去。

解决办法有两种:

(1)只要有数据到达就发送确认,但宣布的窗口大小为零,直到或者缓存空间已能放入具有最大长度的报文段,或者缓存空间的一半已经空了。

(2)当数据到达时并不立即确认,直到入缓存有足够的空间。迟延的确认还有另一个优点,它减少了通信量。但是如果延时过长,就会导致重传;如果延时不够长,就可能发送较小的报文段。需要用协议来平衡,如确认延迟不超过500毫秒。

零窗口探查

如果发送方接收到的通告窗口大小为0,则会停止报文段发送,直到接收方通告非0的窗口。但是通常这个非0窗口通告在一个不含任何数据的ACK报文中发送,并且接收方不会对ACK报文段进行确认。如果这个非0窗口通告丢失了,则双方可能陷入互相等待。为了防止这种死锁情况的发生,发送方使用一个坚持定时器来周期性地向接收方查询,以便发现窗口是否已经增大。

坚持定时器,相关的字段包括persist_cnt和persist_backoff。persist_cnt用于坚持定时器计数,当计时超过某个上限时,则发送窗口探测报文;persist_backoff是计时上限数组的索引。

当发送窗口太小以至于不能发送下一个报文时,启动窗口于探测。计时上限数组为1.5S、3S、6S、12S、24S、48S、60S,当persist_cnt大于数组元素个数时,时间间隔变为60S。

若检测到一个非0窗口,则停止窗口探查。

保活机制

当TCP连接已处于稳定状态,而双方没有数据需要发送,则在这个连接之间不会再有任何信息交互。如果其中一方崩溃或重启,那么原来的有效连接将变得无效。因此TCP提供一种保活机制,来进行检测。

1.如果双方没有任何数据交互,服务器默认将每两个小时检测一次。当然,一旦服务器收到任何数据,将重新进行计时。

2.如果客户端没有响应,服务器还会默认发送9个探查报文进行检测,每个报文默认间隔75s。

3.如果服务器一个响应都没有收到,就会任何客户端已经关闭。

注:如果客户端崩溃并重启,则客户端收到探查报文后会发送复位报文,服务器收到后结束该连接。

保活定时器,相关的字段包括keep_idle、keep_intvl、keep_cnt、keep_cnt_sent。keep_idle记录了多久之后进行保活探测,默认2小时;keep_cnt_sent表示已经发送保活探查报文个数;keep_intvl表示保活时间间隔,默认75s;keep_cnt表示保活最大报文数,默认9次。

发布了208 篇原创文章 · 获赞 90 · 访问量 25万+

猜你喜欢

转载自blog.csdn.net/lushoumin/article/details/104126131
今日推荐