• 《lwip学习10》-- TCP协议


    TCP服务简介

    UDP 运载的数据是以报文的形式,各个报文在网络中互不相干传输, UDP 每收到一个报文就递交给上层应用,因此如果对于大量数据来说,应用层的重装是非常麻烦的,因为UDP 报文在网络中到达目标主机的顺序是不一样的;
    而 TCP 采用数据流的形式传输, 先后发出的数据在网络中虽然也是互不相干的传输,但是这些数据本身携带的信息却是紧密联系的, TCP 协议会给每个传输的字节进行编号,当然啦,两个主机方向上的数据编号是彼此独立的, 在传输的过程中,发送方把数据的起始编号与长度放在 TCP 报文中,在接收方将所有数据按照编号组装起来,然后返回一个确认,当所有数据接收完成后才将数据递交到应用层中。

    TCP的特性

    连接机制

    TCP 是一个面向连接的协议,无论哪一方向另一方发送数据之前,都必须先在双方之间建立一个连接,否则将无法发送数据

    确认与重传

    一个完整的 TCP 传输必须有数据的交互,接收方在接收到数据之后必须正面进行确认,向发送方报告接收的结果,而发送方在发送数据之后必须等待接收方的确认,同时发送的时候会启动一个定时器,在指定超时时间内没收到确认,发送方就会认为发送失败,然后进行重发操作,这就是重传报文。

    缓冲机制

    在发送方想要发送数据的时候, 由于应用程序的数据大小、 类型都是不可预估的, 而TCP 协议提供了缓冲机制来处理这些数据, 如在数据量很小的时候, TCP 会将数据存储在一个缓冲空间中, 等到数据量足够大的时候在进行发送数据, 这样子能提供传输的效率并且减少网络中的通信量,而且在数据发送出去的时候并不会立即删除数据,还是让数据保存在缓冲区中,因为发送出去的数据不一定能被接收方正确接收,它需要等待到接收方的确认再将数据删除。同样的,在接收方也需要有同样的缓冲机制,因为在网络中传输的数据报到达的时间是不一样的,而且 TCP 协议还需要把这些数据报组装成完整的数据,然后再递交到应用层中。

    全双工通信

    在 TCP 连接建立后,那么两个主机就是对等的,任何一个主机都可以向另一个主机发送数据,数据是双向流通的,所以 TCP 协议是一个全双工的协议,这种机制为 TCP 协议传输数据带来很大的方便,一般来说, TCP 协议的确认是通过捎带的方式来实现,即接收方把确认信息放到反向传来的是数据报文中,不必单独为确认信息申请一个报文,捎带机制减少了网络中的通信流量。

    流量控制

    一条 TCP 连接每一侧的主机都设置了缓冲区域。当该接收方收到数据后,它就将数据放入接收缓冲区,当确认这段数据是正常的时候,就会向发送方返回一个确认。并且向相关的应用层递交该数据,但不一定是数据刚一到达就立即递交。事实上,接收方应用也许正忙于其他任务,甚至要过很长时间后才会去处理这些数据。 这样子如果接收方处理这些数据时相对缓慢,而发送方发送得太多、太快,就会很容易地使接收方的接收缓冲区发生溢出
    因此 TCP 提供了流量控制服务(flow-control service)以消除发送方使接收方缓冲区溢出的可能性。流量控制是一个速度匹配服务,即发送方的发送速率与接收方应用程序的读取速率相匹配, TCP 通过让发送方维护一个称为**接收窗口(receive window)**的变量来提供流量控制,它用于给发送方一个指示:接收方还能接收多少数据,接收方会将此窗口值放在 TCP 报文的首部中的窗口字段, 然后传递给发送方,这个窗口的大小是在发送数据的时候动态调整的。

    差错控制

    除了确认与重传之外, TCP 协议也会采用校验和的方式来检验数据的有效性,主机在接收数据的时候,会将重复的报文丢弃,将乱序的报文重组,发现某段报文丢失了会请求发送方进行重发,因此在 TCP 往上层协议递交的数据是顺序的、无差错的完整数据。

    拥塞控制

    如果一个主机还是以很大的流量给另一个主机发送数据, 但是其中间的路由器通道很小, 无法承受这样大的数据流量的时候, 就会导致拥塞的发生, 这样子就导致了接收方无法在超时时间内完成接收(接收方此时完全有能力处理大量数据), 而发送方又进行重传,这样子就导致了链路上的更加拥塞, 延迟发送方必须实现一直自适应的机制, 在网络中拥塞的情况下调整自身的发送速度,这种形式对发送方的控制被称为拥塞控制(congestioncontrol), 与前面我们说的流量控制是非常相似的,而且 TCP 协议采取的措施也非常相似,均是限制发送方的发送速度。

    端口号的概念

    TCP 协议的连接是包括上层应用间的连接,简单来说, TCP 连接是两个不同主机的应用连接,而传输层与上层协议是通过端口号进行识别的,如 IP 协议中以 IP 地址作为识别一样,端口号的取值范围是 0~65535,这些端口标识着上层应用的不同线程,一个主机内可能只有一个 IP 地址,但是可能有多个端口号,每个端口号表示不同的应用线程。
    常见的 TCP 协议端口号有 21、 53、 80 等等,更多端口描述具体见表格 13-1,其中 80端口号是我们日常生活中最常见的一个端口号,它也是 HTTP 服务器默认开放的端口。
    在这里插入图片描述

    TCP报文段的结构

    TCP报文段的封装

    如 ICMP 报文一样, TCP 报文段依赖 IP 协议进行发送,因此 TCP 报文段与 ICMP 报文一样,都是封装在 IP 数据报中, IP 数据报封装在以太网帧中,因此 TCP 报文段也是经过了两次的封装,然后发送出去, 其封装具体见图
    在这里插入图片描述

    TCP报文段格式

    TCP 报文段如 APR 报文、 IP 数据报一样,也是由首部+数据区域组成, TCP 报文段的首部我们称之为 TCP 首部,其首部内推很丰富,各个字段都有不一样的含义,如果不计算选项字段,一般来说 TCP 首部只有 20 个字节,具体见图
    在这里插入图片描述

    PACK_STRUCT_BEGIN
    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;
    PACK_STRUCT_END
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    每个 TCP 报文段都包含源主机和目标主机的端口号,用于寻找发送端和接收端应用线程,这两个值加上 I P 首部中的源 I P 地址和目标 I P 地址就能确定唯一一个 TCP 连接。
    序号字段用来标识从 TCP 发送端向 TCP 接收端发送的数据字节流,它的值表示在这个报文段中的第一个数据字节所处位置吗,根据接收到的数据区域长度, 就能计算出报文最后一个数据所处的序号, 因为 TCP 协议会对发送或者接收的数据进行编号(按字节的形式),那么使用序号对每个字节进行计数, 就能很轻易管理这些数据。序号是 32 bit 的无符号整数。
    当建立一个新的连接时, TCP 报文段首部的 SYN 标志变 1, 序号字段包含由这个主机随机选择的初始序号 ISN(Initial Sequence Number)。该主机要发送数据的第一个字节序号为 ISN+1,因为 SYN 标志会占用一个序号
    既然 TCP 协议给每个传输的字节都了编号, 那么确认序号就包含接收端所期望收到的下一个序号, 因此,确认序号应当是上次已成功收到数据的最后一个字节序号加 1。 当然,只有 ACK 标志为 1 时确认序号字段才有效, TCP 为应用层提供全双工服务,这意味数据能在两个方向上独立地进行传输,因此确认序号通常会与反向数据(即接收端传输给发送端的数据)封装在同一个报文中(即捎带) ,所以连接的每一端都必须保持每个方向上的传输数据序号准确性。
    首部长度字段占据 4bit 空间,它指出了 TCP 报文段首部长度,以字节为单位,最大能记录 15*4=60 字节的首部长度,因此, TCP 报文段首部最大长度为 60 字节。 在字段后接下来有 6bit 空间是保留未用的。
    此外还有 6bit 空间,是 TCP 报文段首部的标志字段,用于标志一些信息:
     URG:首部中的紧急指针字段标志,如果是 1 表示紧急指针字段有效。
     ACK:首部中的确认序号字段标志,如果是 1 表示确认序号字段有效。
     PSH: 该字段置一表示接收方应该尽快将这个报文段交给应用层。
     RST: 重新建立 TCP 连接。
     SYN: 用同步序号发起连接。
     FIN: 中止连接。
    TCP 的流量控制由连接的每一端通过声明的窗口大小来提供,窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端正期望接收的数据序号,发送方根据窗口大小调整发送数据,以实现流量控制。窗口大小是一个占据 16 bit 空间的字段,因而窗口最大为 65535 字节,当接收方告诉发送方一个大小为 0 的窗口时,将完全阻止发送方的数据发送。

    TCP连接

    三次握手

    第一步:客户端的 TCP 首先向服务器端的 TCP 发送一个特殊的 TCP 报文段。该报文段中不包含应用层数据, 但是在报文段的首部中的 SYN 标志位会被置为 1。因此,这个特殊报文段被称为 SYN 报文段(我们暂且称之为握手请求报文) 。另外,客户会随机地选择一个初始序号(ISN,假设为 A),并将此序号放置于该 SYN 报文段的序号字段中;但SYN 报文段中的 ACK 标志位 0,此时它的确认序号段是无效的。该报文段会被封装在一个IP 数据报中, 然后发送给目标服务器。

    第二步:一旦服务器收到了客户端发出的 SYN 报文段,知道客户端要请求握手了,服务器便会从 SYN 报文段中提取对应的信息,为该 TCP 连接分配 TCP 缓存和变量,并向该客户 TCP 发送允许连接的报文段(握手应答报文) 。 这个报文段同样也不包含任何应用层数据, 但是,在报文段的首部却包含 3 个重要的信息。

    1. SYN 与 ACK 标志都被置为 1。
    2. 将 TCP 报文段首部的确认序号字段设置为 A+1(这个 A(ISN)是从握手请求报
      文中得到) 。
    3. 服务器随机选择自己的初始序号(ISN,注意此 ISN 是服务器端的 ISN,假设为
      B) , 并将其放置到 TCP 报文段首部的序号字段中。

    第三步:当客户端收到服务器的握手应答报文后,会将 ACK 标志置位,此时客户端的 TCP 报文段的 ACK 标志被设置为 1,而对于 SYN 标志, 因为连接已经建立了,所以该标志会被置为 0, 同时客户端也要给该 TCP 连接分配缓存和变量,并且客户端还需要返回一个应答报文段,这个报文对服务器的应答报文段作出应答,将 TCP 报文段首部的确认序号字段设置为 B+1,同时也会告知服务器的窗口大小。

    在完成握手后,客户端与服务器就建立了连接, 同时双方都得到了彼此的窗口大小,序列号等信息, 在传输 TCP 报文段的时候,每个 TCP 报文段首部的 SYN 标志都会被置 0,因为它只用于发起连接,同步序号。
    在这里插入图片描述在这里插入图片描述

    四次挥手

    建立一个连接需要三次握手,而终止一个连接要经过 四次挥手(有一些书上也会称为“四次握手” ) , 这由 TCP 的特性造成的,因为 TCP 连接是全双工连接的服务,因此每个方向上的连接必须单独关闭。
    “四次挥手” 终止连接示意图具体见图,其具体过程如下:
    第一步:客户端发出一个 FIN 报文段主动进行关闭连接,此时报文段的 FIN 标志位为1,假设序号为 C,一般来说 ACK 标志也会被置一,但确认序号字段是无效的。
    第二步: 当服务器收到这个 FIN 报文段,它发回一个 ACK 报文段(此报文段是终止连接应答) ,确认序号为收到的序号加 1(C+1), 和 SYN 一样,一个 FIN 将占用一个序号,此时断开客户端->服务器的方向连接。
    第三步:服务器会向应用程序请求关闭与这个客户端的连接, 接着服务器就会发送一个 FIN 报文段(这个报文段是服务器向客户端发出,请求终止连接) ,此时假设序号为 D,ACK 标志虽然也为 1,但是确认序号字段是无效的。
    第四步: 客户端返回一个 ACK 报文段来确认终止连接的请求, ACK 标志置一, 并将确认序号设置为收到序号加 1(D+1) ,此时断开服务器->客户端的方向连接。
    在这里插入图片描述

    TCP状态

    Lwip中定义的TCP状态

    TCP 协议根据连接时接收到报文的不同类型,采取相应动作也不同,还要处理各个状态的关系,如当收到握手报文时候、超时的时候、用户主动关闭的时候等都需要不一样的状态去采取不一样的处理。 在 LwIP 中,为了实现 TCP 协议的稳定连接,采用数组的形式定义了 11 种连接时候的状态, 具体见代码清单

    static const char *const tcp_state_str[] = {
      "CLOSED",       //关闭状态(无连接)
      "LISTEN",       //监听状态
      "SYN_SENT",     //已发起请求连接(等待确认)
      "SYN_RCVD",     //已收到请求连接
      "ESTABLISHED",  //稳定连接状态
      "FIN_WAIT_1",   //单向请求终止连接状态
      "FIN_WAIT_2",   //对方已应答请求终止连接
      "CLOSE_WAIT",   //等待终止连接
      "CLOSING",      //两端同时关闭
      "LAST_ACK",     //服务器等待对方接受关闭
      "TIME_WAIT"     //关闭成功(2MSL 等待状态)
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    1. ESTABLISHED 状态: 这个状态是处于稳定连接状态,建立连接的 TCP 协议两端的主机都是处于这个状态,它们相互知道彼此的窗口大小、序列号、最大报文段等信息。
    2. FIN_WAIT_1 与 FIN_WAIT_2 状态:处于这个状态一般都是单向请求终止连接,然后主机等待对方的回应,而如果对方产生应答,则主机状态转移为FIN_WAIT_2,此时{主机->对方}方向上的 TCP 连接就断开,但是{对方->主机}方向上的连接还是存在的。此处有一个注意的地方: 如果主机处于 FIN_WAIT_2状态, 说明主机已经发出了 FIN 报文段,并且对方也已对它进行确认, 除非主机是在实行半关闭状态,否则将等待对方主机的应用层处理关闭连接,因为对方已经意识到它已收到 FIN 报文段, 它需要主机发一个 FIN 来关闭{对方->主机}方向上的连接。只有当另一端的进程完成这个关闭, 主机这端才会从 FIN_WAIT_2 状态进入 TIME_WAIT 状态。 否则这意味着主机这端可能永远保持这个FIN_WAIT_2 状态, 另一端的主机也将处于 CLOSE_WAIT 状态,并一直保持这个状态直到应用层决定进行关闭。
    3. TIME_WAIT 状态: TIME_WAIT 状态也称为 2MSL 等待状态。每个具体 TCP 连接的实现必须选择一个 TCP 报文段最大生存时间 MSL(Maximum Segment Lifetime) ,如 IP 数据报中的 TTL 字段,表示报文在网络中生存的时间, 它是任何报文段被丢弃前在网络内的最长时间, 这个时间是有限的, 为什么需要等待呢?我们知道 IP 数据报是不可靠的,而 TCP 报文段是封装在 IP 数据报中, TCP 协议必须保证发出的 ACK 报文段是正确被对方接收, 因此处于该状态的主机必须在这个状态停留最长时间为 2 倍的 MSL,以防最后这个 ACK 丢失,因为 TCP 协议必须保证数据能准确送达目的地。

    TCP状态转移

    在这里插入图片描述
     虚线:表示服务器的状态转移。
     实线:表示客户端的状态转移。
     图中所有“关闭”、“打开”都是应用程序主动处理。
     图中所有的“超时”都是内核超时处理。

    三次握手过程

    (7):服务器的应用程序主动使服务器进入监听状态,等待客户端的连接请求。
    图**(1):首先客户端的应用程序会主动发起连接,发送 SNY 报文段给服务器,在发送之后就进入 SYN_SENT 状态等待服务器的 SNY ACK 报文段进行确认,如果在指定超时时间内服务器不进行应答确认,那么客户端将关闭连接。
    (8):处于监听状态的服务器收到客户端的连接请求(SNY 报文段),那么服务器就返回一个 SNY ACK 报文段应答客户端的响应,并且服务器进入 SYN_RCVD 状态。
    (2):如果客户端收到了服务器的 SNY ACK 报文段,那么就进入ESTABLISHED 稳定连接状态,并向服务器发送一个 ACK 报文段。
    (9)**:服务器收到来自客户端的 ACK 报文段,表示连接成功,进入ESTABLISHED 稳定连接状态,这正是我们建立连接的三次握手过程。

    四次挥手过程

    图**(3)**:一般来说,都是客户端主动发送一个 FIN 报文段来终止连接,此时客户端从 ESTABLISHED 稳定连接状态转移为 FIN_WAIT_1 状态,并且等待来自服务器的应答确认。

    (10): 服务器收到 FIN 报文段,知道客户端请求终止连接,那么将返回一个ACK 报文段到客户端确认终止连接,并且服务器状态由稳定状态转移为 CLOSE_WAIT 等待终止连接状态。

    (4): 客户端收到确认报文段后,进入 FIN_WAIT_2 状态, 等待来自服务器的主动请求终止连接, 此时{客户端->服务器}方向上的连接已经断开。

    (11): 一般来说,当客户端终止了连接之后,服务器也会终止{服务器->客户端}方向上的连接,因此服务器的原因程序会主动关闭该方向上的连接,发送一个 FIN 报文段给客户端。

    (5): 处于 FIN_WAIT_2 的客户端收到 FIN 报文段后,发送一个 ACK 报文段给服务器。

    (12): 服务器收到 ACK 报文段,就直接关闭,此时{服务器->客户端}方向上的连接已经终止,进入 CLOSED 状态。

    (6): 客户端还会等待 2MSL,以防 ACK 报文段没被服务器收到,这就是四次挥手的全部过程。

    注意: 对于图 **(13)(14)(15)**的这些状态都是一些比较特殊的状态。

    TCP中的数据结构

    1 #define IP_PCB \
    2 /* 本地 ip 地址与远端 IP 地址 */ \
    3 ip_addr_t local_ip; \
    4 ip_addr_t remote_ip; \
    5 /* 绑定 netif 索引 */ \
    6 u8_t netif_idx; \
    7 /* 套接字选项 */ \
    8 u8_t so_options; \
    9 /* 服务类型 */ \
    10 u8_t tos; \
    11 /* 生存时间 */ \
    12 u8_t ttl \
    13 /* 链路层地址解析提示 */ \
    14 IP_PCB_NETIFHINT
    15
    16
    17 #define TCP_PCB_COMMON(type) \
    18 type *next; /* 指向链表中的下一个控制块 */ \
    19 void *callback_arg; \
    20 TCP_PCB_EXTARGS \
    21 enum tcp_state state; /* TCP 状态 */ \
    22 u8_t prio; \
    23 /* 本地主机端口号 */ \
    24 u16_t local_port
    25
    26
    27 /** TCP 协议控制块 */
    28 struct tcp_pcb
    29 {
    30 IP_PCB;
    31 /** 协议特定的 PCB 成员 */
    32 TCP_PCB_COMMON(struct tcp_pcb);
    33
    34 /* 远端端口号 */
    35 u16_t remote_port;
    36
    37 tcpflags_t flags;
    38 #define TF_ACK_DELAY 0x01U /* 延迟发送 ACK */
    39 #define TF_ACK_NOW 0x02U /* 立即发送 ACK. */
    40 #define TF_INFR 0x04U /* 在快速恢复。 */
    41 #define TF_CLOSEPEND 0x08U /* 关闭挂起 */
    42 #define TF_RXCLOSED 0x10U /* rx 由 tcp_shutdown 关闭 */
    43 #define TF_FIN 0x20U /* 连接在本地关闭 */
    44 #define TF_NODELAY 0x40U /* 禁用 Nagle 算法 */
    45 #define TF_NAGLEMEMERR 0x80U /* 本地缓冲区溢出 */
    46 #define TF_TIMESTAMP 0x0400U /* Timestamp option enabled */
    47 #endif
    48 #define TF_RTO 0x0800U /* RTO 计时器 */
    49
    50 u8_t polltmr, pollinterval;
    51 /* 控制块被最后一次处理的时间 */
    52 u8_t last_timer;
    53 u32_t tmr;
    54
    55 /* 接收窗口相关的字段 */
    56 u32_t rcv_nxt; /* 下一个期望收到的序号 */
    57 tcpwnd_size_t rcv_wnd; /* 接收窗口大小 */
    58 tcpwnd_size_t rcv_ann_wnd; /* 告诉对方窗口的大小 */
    59 u32_t rcv_ann_right_edge; /* 窗口的右边缘 */
    60
    61 /* 重传计时器。 */
    62 s16_t rtime;
    63
    64 u16_t mss; /* 最大报文段大小 */
    65
    66 /* RTT(往返时间)估计变量 */
    67 u32_t rttest; /* RTT 估计,以为 500 毫秒递增 */
    68 u32_t rtseq; /* 用于测试 RTT 的报文段序号 */
    69 s16_t sa, sv; /* RTT 估计得到的平均值与时间差 */
    70
    71 s16_t rto; /* 重传超时 */
    72 u8_t nrtx; /* 重传次数 */
    73
    74 /* 快速重传/恢复 */
    75 u8_t dupacks;
    76 u32_t lastack; /* 接收到的最大确认序号 */
    77
    78 /* 拥塞避免/控制变量 */
    79 tcpwnd_size_t cwnd; /* 连接当前的窗口大小 */
    80 tcpwnd_size_t ssthresh; /* 拥塞避免算法启动的阈值 */
    81
    82
    83 u32_t rto_end;
    84
    85 u32_t snd_nxt; /* 下一个要发送的序号 */
    86 u32_t snd_wl1, snd_wl2; /* 上一次收到的序号和确认号 */
    87 u32_t snd_lbb; /* 要缓冲的下一个字节的序列号 */
    88 tcpwnd_size_t snd_wnd; /* 发送窗口大小 */
    89 tcpwnd_size_t snd_wnd_max; /* 对方的最大发送方窗口 */
    90
    91 /* 可用的缓冲区空间(以字节为单位)。 */
    92 tcpwnd_size_t snd_buf;
    93
    94 tcpwnd_size_t bytes_acked;
    95
    96 struct tcp_seg *unsent; /* 未发送的报文段 */
    97 struct tcp_seg *unacked; /* 已发送但未收到确认的报文段 */
    98 struct tcp_seg *ooseq; /* 已收到的无序报文 */
    99 /* 以前收到但未被上层处理的数据 */
    100 struct pbuf *refused_data;
    101
    102 #if LWIP_CALLBACK_API || TCP_LISTEN_BACKLOG
    103 struct tcp_pcb_listen* listener;
    104 #endif
    105
    106 //TCP 协议相关的回调函数
    107 #if LWIP_CALLBACK_API
    108 /* 当数据发送成功后被调用 */
    109 tcp_sent_fn sent;
    110 /* 接收数据完成后被调用 */
    111 tcp_recv_fn recv;
    112 /* 建立连接后被调用 */
    113 tcp_connected_fn connected;
    114 /* 该函数被内核周期调用 */
    115 tcp_poll_fn poll;
    116 /* 发送错误时候被调用 */
    117 tcp_err_fn errf;
    118 #endif
    119
    120 /* 保持活性 */
    121 u32_t keep_idle;
    122 /* 坚持计时器计数器值 */
    123 u8_t persist_cnt;
    124 u8_t persist_backoff;
    125 u8_t persist_probe;
    126
    127 /* 保持活性报文发送次数 */
    128 u8_t keep_cnt_sent;
    129
    130 };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130

    LwIP 中除了定义了一个完整的 TCP 控制块之外,还定义了一个删减版的 TCP 控制块,叫 tcp_pcb_listen, 用于描述处于监听状态的连接, 因为分配完整的 TCP 控制块是比较消耗内存资源的, 而 TCP 协议在连接之前, 是无法进行数据传输的, 那么在监听的时候只需要把对方主机的相关信息得到, 然后无缝切换到完整的 TCP 控制块中, 这样子就能节省不少资源, 此外, LwIP 还定义了 4 个链表来维护 TCP 连接时的各种状态,具体见代码清单。

    1 /** 用于监听的 TCP 协议控制块 */
    2 struct tcp_pcb_listen
    3 {
    4 /** 所有 PCB 类型的通用成员 */
    5 IP_PCB;
    6 /** 协议特定的 PCB 成员 */
    7 TCP_PCB_COMMON(struct tcp_pcb_listen);
    8 };
    9
    10 /* The TCP PCB lists. */
    11
    12 /** 新绑定的端口 */
    13 struct tcp_pcb *tcp_bound_pcbs;
    14 /** 处于监听状态的 TCP 控制块 */
    15 union tcp_listen_pcbs_t tcp_listen_pcbs;
    16 /** 其他状态的 TCP 控制块*/
    17 struct tcp_pcb *tcp_active_pcbs;
    18 /** 处于 TIME_WAIT 状态的控制块 */
    19 struct tcp_pcb *tcp_tw_pcbs;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    tcp_bound_pcbs 链表上的 TCP 控制块可以看做是处于 CLOSED 状态,那些新绑定的端口初始的时候都是处于这个状态。 tcp_listen_pcbs 链表用于记录处于监听状态的 TCP 控制块, 一般就是记录的是 tcp_pcb_listen 控制块。 tcp_tw_pcbs 链表用于记录连接中处于TIME_WAIT 状态下的 TCP 控制块。 而 tcp_active_pcbs 链表用于记录所有其他状态的 TCP控制块, 这些端口是活跃的,可以不断进行状态转移。

    窗口的概念

    接收窗口

    TCP 控制块中关于接收窗口的成员变量有 rcv_nxt、 rcv_wnd、 rcv_ann_wnd、rcv_ann_right_edge,rcv_nxt 表示下次期望
    接收到的数据编号, rcv_wnd 表示接收窗口的大小, rcv_ann_wnd 用于告诉发送方窗口的大小, rcv_ann_right_edge 记录了窗口的右边界, 这 4 个成员变量都会在数据传输的过程中动态改变的,其示意图具体见图
    在这里插入图片描述
    比如在 7 字节之前的数据,都是已经接收确认的数据, 而 7 字节正是主机想要接收到的下一个字节数据编号,而窗口的大小是 7,它会告诉发送方“你可以发送 7 个数据过来”,窗口的右边界为 14,当主机下一次接收到 N(不一定是 7)字节数据的时候,窗口就会向右移动 N 个字节,但是 rcv_wnd、 rcv_ann_wnd、 rcv_ann_right_edge变量的值是不确定的,通过 LwIP 内部计算得出,而下一次想要接收的数据编号就为 7+N。

    发送窗口

    TCP 控制块中关于发送窗口的成员变量有 lastack、 snd_nxt、 snd_lbb、 snd_wnd,lastack 记录了已经确认的最大序号, snd_nxt 表示下次要发送的序号, snd_lbb 是表示下一个将被应用线程缓冲的序号,而 snd_wnd 表示发送窗口的大小, 是由接收已方提供的。这些值也是动态变化的,当发送的数据收到确认,就会更新 lastack, 并且随着数据的发送出去, 窗口会向右移动,即 snd_nxt 的值在增加,其示意图具体见图
    在这里插入图片描述
    每条 TCP 连接的每一端都必须设有两个窗口——一个发送窗口和一个接收窗口, TCP的可靠传输机制用字节的序号(编号)进行控制, TCP 所有的确认都是基于数据的序号而不是基于报文段,发送过的数据未收到确认之前必须保留,以便超时重传时使用,发送窗口在没收到确认序号之前是保持不动的,当收到确认序号就会向右移动,并且更新 lastack的值。

    发送缓冲区用来暂时存放应用程序发送给对方的数据,这是主机已发送出但未收到确认的数据。接收缓存用来暂时存放按序到达的、但尚未被接收应用程序读取的数据以及 不按序到达的数据。

    关于窗口的概念必须强调三点:

    1. 发送方的发送窗口并不总是和接收方接收窗口一样大,因为有一定的时间滞后。
    2. TCP 标准没有规定对不按序到达的数据应如何处理, 通常是先临时存放在接收窗口中,等到字节流中所缺少的字节收到后,再按序交付上层的应用进程。
    3. TCP 要求接收方必须有确认的功能,这样可以减小传输开销。

    TCP报文段处理

    报文段缓冲队列

    TCP 连接的每一端都有接收缓冲区与发送缓冲区(也可以称之为缓冲队列,下文均用缓冲队列) ,而 TCP 控制块只是维护缓冲区队列的指针,通过指针简单对这些缓冲区进行管理, LwIP 为了更好管理 TCP 报文段的缓冲队列数据, 特地定义了一个数据结
    构,命名为 tcp_seg, 使用它将所有的报文段连接起来

    /* This structure represents a TCP segment on the unsent, unacked and ooseq queues */
    struct tcp_seg {
      struct tcp_seg *next;    /* 指向下一个 tcp_seg */
      struct pbuf *p;          /* 指向报文段的 pbuf */
      u16_t len;               /* 报文段的数据长度 */
      u8_t  flags;             /* 报文段标志属性 */
    #define TF_SEG_OPTS_MSS         (u8_t)0x01U /* Include MSS option (only used in SYN segments) */
    #define TF_SEG_OPTS_TS          (u8_t)0x02U /* Include timestamp option. */
    #define TF_SEG_DATA_CHECKSUMMED (u8_t)0x04U /* ALL data (not the header) is
                                                   checksummed into 'chksum' */
    #define TF_SEG_OPTS_WND_SCALE   (u8_t)0x08U /* Include WND SCALE option (only used in SYN segments) */
    #define TF_SEG_OPTS_SACK_PERM   (u8_t)0x10U /* Include SACK Permitted option (only used in SYN segments) */
      struct tcp_hdr *tcphdr;  /* 指向报文段首部*/
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    每个已经连接的 TCP 控制块中维护了 3 个是指针,分别是 unsent、 unacked、 ooseq,unsent 指向未发送的报文段缓冲队列, unacked 指向已发送但未收到确认的报文段缓冲队列,ooseq 指向已经收到的无序报文段缓冲队列
    在这里插入图片描述
    其中,ooseq为可选功能,由 TCP_QUEUE_OOSEQ 定义,如果存储不够,定义为 0 ,及TCP不能对无序数据进行排队

    TCP报文段发送

    一般我们在应用层使用 NETCONN API 或者 Socket API 进行编程的时候,会将用户数据传递给传输层,那么本章关于应用层是如何传递数据到传输层的就暂时先不讲解,只需要知道数据到达传输层后是怎么输出的即可,如果我们使用的是 NETCONN API 对已经连接的 TCP 应用发送数据,那么经过内核的一系列处理,就会调用lwip_netconn_do_writemore()函数对发送数据,但是真正处理 TCP 报文段缓冲等操作是在tcp_write()函数中,在这个函数里, LwIP 会写入数据, 但是不会立即发送, 也就是存储在缓冲区里面,等待更多的数据进行高效的发送,这也是著名的 Nagle 算法,然后在调用tcp_output()函数进行发送出去,这样子一个应用层的数据就通过 TCP 协议传递给 IP 层了,tcp_output()函数具体见代码清单

    /**
     * @ingroup tcp_raw
     * Find out what we can send and send it
     *
     * @param pcb Protocol control block for the TCP connection to send data
     * @return ERR_OK if data has been sent or nothing to send
     *         another err_t on error
     */
    err_t
    tcp_output(struct tcp_pcb *pcb)
    {
      struct tcp_seg *seg, *useg;
      u32_t wnd, snd_nxt;
      err_t err;
      struct netif *netif;
    //如果控制块有数据在处理,直接返回
      if (tcp_input_pcb == pcb) {
        return ERR_OK;
      }
    //得到合适的发送窗口
      wnd = LWIP_MIN(pcb->snd_wnd, pcb->cwnd);
    //找到控制块中的未发送数据缓冲区链表
      seg = pcb->unsent;
    
      if (seg == NULL) {
    
        /* If the TF_ACK_NOW flag is set and the ->unsent queue is empty, construct
         * an empty ACK segment and send it. */
        if (pcb->flags & TF_ACK_NOW) {
          return tcp_send_empty_ack(pcb);
        }
        /* nothing to send: shortcut out of here */
        goto output_done;
      } else {
      }
    //根据控制块 IP 地址信息找到合适的网卡发送
      netif = tcp_route(pcb, &pcb->local_ip, &pcb->remote_ip);
      if (netif == NULL) {
        return ERR_RTE;
      }
    
      /* If we don't have a local IP address, we get one from netif */
      /* 如果没有本地 IP 地址,我们会从 netif 获得一个 */
      if (ip_addr_isany(&pcb->local_ip)) {
        const ip_addr_t *local_ip = ip_netif_get_local_ip(netif, &pcb->remote_ip);
        if (local_ip == NULL) {
          return ERR_RTE;
        }
        ip_addr_copy(pcb->local_ip, *local_ip);
      }
    
      /* Handle the current segment not fitting within the window */
      /* 处理当前不适合窗口的报文段 */
      if (lwip_ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len > wnd) {
        /* We need to start the persistent timer when the next unsent segment does not fit
         * within the remaining (could be 0) send window and RTO timer is not running (we
         * have no in-flight data). If window is still too small after persist timer fires,
         * then we split the segment. We don't consider the congestion window since a cwnd
         * smaller than 1 SMSS implies in-flight data
         */
         //开始持续定时器
        if (wnd == pcb->snd_wnd && pcb->unacked == NULL && pcb->persist_backoff == 0) {
          pcb->persist_cnt = 0;
          pcb->persist_backoff = 1;
          pcb->persist_probe = 0;
        }
        /* We need an ACK, but can't send data now, so send an empty ACK */
        /* 我们需要 ACK,但现在无法发送数据(无法捎带),所以发送一个 ACK 报文段 */
        if (pcb->flags & TF_ACK_NOW) {
          return tcp_send_empty_ack(pcb);
        }
        goto output_done;
      }
      /* Stop persist timer, above conditions are not active */
      /* 停止持续计时器 */
      pcb->persist_backoff = 0;
    
      /* useg should point to last segment on unacked queue */
      /* useg 指向未应答队列中的最后一个 tcp_seg 结构 */
      useg = pcb->unacked;
      if (useg != NULL) {
        for (; useg->next != NULL; useg = useg->next);
      }
      /* data available and window allows it to be sent? */
      /* 可用数据和窗口允许它发送报文段,直到把数据全部发送出去或者填满发送窗口 */
      while (seg != NULL &&
             lwip_ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len <= wnd) {
        LWIP_ASSERT("RST not expected here!",
                    (TCPH_FLAGS(seg->tcphdr) & TCP_RST) == 0);
        /* Stop sending if the nagle algorithm would prevent it
         * Don't stop:
         * - if tcp_write had a memory error before (prevent delayed ACK timeout) or
         * - if FIN was already enqueued for this PCB (SYN is always alone in a segment -
         *   either seg->next != NULL or pcb->unacked == NULL;
         *   RST is no sent using tcp_write/tcp_output.
         */
        if ((tcp_do_output_nagle(pcb) == 0) &&
            ((pcb->flags & (TF_NAGLEMEMERR | TF_FIN)) == 0)) {
          break;
        }
    
        if (pcb->state != SYN_SENT) {
          TCPH_SET_FLAG(seg->tcphdr, TCP_ACK);
        }
    //真正发送 TCP 报文的函数,此处发送 TCP 报文段
        err = tcp_output_segment(seg, pcb, netif);
        if (err != ERR_OK) {
          /* segment could not be sent, for whatever reason */
          tcp_set_flags(pcb, TF_NAGLEMEMERR);
          return err;
        }
    //得到下一个未发送的 tcp_seg
        pcb->unsent = seg->next;
        if (pcb->state != SYN_SENT) {
          tcp_clear_flags(pcb, TF_ACK_DELAY | TF_ACK_NOW);
        }
        //计算 snd_nxt 的值
        snd_nxt = lwip_ntohl(seg->tcphdr->seqno) + TCP_TCPLEN(seg);
        //更新下一个要发送的数据编号
        if (TCP_SEQ_LT(pcb->snd_nxt, snd_nxt)) {
          pcb->snd_nxt = snd_nxt;
        }
        /* put segment on unacknowledged list if length > 0 */
        /* 如果发送出去的数据长度>0,则将这些报文段放在未确认链表中 */
        if (TCP_TCPLEN(seg) > 0) {
          seg->next = NULL;
          /* unacked list is empty? */
          /* 未确认链表为空,插入即可 */
          if (pcb->unacked == NULL) {
            pcb->unacked = seg;
            useg = seg;
            /* unacked list is not empty? */
          } else {//如果不为空,按照顺序插入未确认链表中
            /* In the case of fast retransmit, the packet should not go to the tail
             * of the unacked queue, but rather somewhere before it. We need to check for
             * this case. -STJ Jul 27, 2004 */
            if (TCP_SEQ_LT(lwip_ntohl(seg->tcphdr->seqno), lwip_ntohl(useg->tcphdr->seqno))) {
              /* add segment to before tail of unacked list, keeping the list sorted */
              struct tcp_seg **cur_seg = &(pcb->unacked);
              while (*cur_seg &&
                     TCP_SEQ_LT(lwip_ntohl((*cur_seg)->tcphdr->seqno), lwip_ntohl(seg->tcphdr->seqno))) {
                cur_seg = &((*cur_seg)->next );
              }
              seg->next = (*cur_seg);
              (*cur_seg) = seg;
            } else {
              /* add segment to tail of unacked list */
              useg->next = seg;
              useg = useg->next;
            }
          }
          /* do not queue empty segments on the unacked list */
        } else {
          tcp_seg_free(seg);
        }
        seg = pcb->unsent;
      }
    
    output_done:
      tcp_clear_flags(pcb, TF_NAGLEMEMERR);
      return ERR_OK;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162

    如果控制块的 flags 字段被设置为TF_ACK_NOW, 但是此时还没有数据发送, 就只发送一个纯粹的 ACK 报文段,如果能发
    送数据,那就将 ACK 应答捎带过去,这样子就能减少网络中的流量,同时在发送的时候先找到未发送链表,然后调用 tcp_output_segment()->ip_output_if()函数进行发送,直到把未发送链表的数据完全发送出去或者直到填满发送窗口,并且更新发送窗口相关字段,同时将这些已发送但是未确认的数据存储在未确认链表中,以防丢失数据进行重发操作,放入未确认链表的时候是按序号升序进行排序的

    TCP报文段接收

    IP 数据报中如果是递交给 TCP 协议的数据,就会调用 tcp_input()函数往上层传递,而 TCP 协议收到数据就会对这些数据进行一系列的处理与验证。

    1 void
    2 tcp_input(struct pbuf *p, struct netif *inp)
    3 {
    4 struct tcp_pcb *pcb, *prev;
    5 struct tcp_pcb_listen *lpcb;
    6
    7 u8_t hdrlen_bytes;
    8 err_t err;
    9
    10 LWIP_UNUSED_ARG(inp);
    11
    12 PERF_START;
    13
    14 TCP_STATS_INC(tcp.recv);
    15 MIB2_STATS_INC(mib2.tcpinsegs);
    16
    17 tcphdr = (struct tcp_hdr *)p->payload;
    18
    19 /* 检查报文段是否有有效数据 */
    20 if (p->len < TCP_HLEN)
    21 {
    22 /* 如果没有就丢掉报文段 */
    23 TCP_STATS_INC(tcp.lenerr);
    24 goto dropped;
    25 }
    26
    27 /* 不处理传入的广播/多播报文段。 */
    28 if (ip_addr_isbroadcast(ip_current_dest_addr(),
    29 ip_current_netif()) ||
    30 ip_addr_ismulticast(ip_current_dest_addr()))
    31 {
    32 TCP_STATS_INC(tcp.proterr);
    33 goto dropped;
    34 }
    35
    36 /* 检查 TCP 报文段首部长度 */
    37 hdrlen_bytes = TCPH_HDRLEN_BYTES(tcphdr);
    38 if ((hdrlen_bytes < TCP_HLEN) || (hdrlen_bytes > p->tot_len))
    39 {
    40 TCP_STATS_INC(tcp.lenerr);
    41 goto dropped;
    42 }
    43
    44 /* 移动 pbuf 指针,指向 TCP 报文段数据区域 */
    45 tcphdr_optlen = (u16_t)(hdrlen_bytes - TCP_HLEN);
    46 tcphdr_opt2 = NULL;
    47 if (p->len >= hdrlen_bytes)
    48 {
    49 tcphdr_opt1len = tcphdr_optlen;
    50 pbuf_remove_header(p, hdrlen_bytes);
    51 }
    52
    53 /* 将 TCP 首部中的各字段内容提取出来。 */
    54 tcphdr->src = lwip_ntohs(tcphdr->src);
    55 tcphdr->dest = lwip_ntohs(tcphdr->dest);
    56 seqno = tcphdr->seqno = lwip_ntohl(tcphdr->seqno);
    57 ackno = tcphdr->ackno = lwip_ntohl(tcphdr->ackno);
    58 tcphdr->wnd = lwip_ntohs(tcphdr->wnd);
    59
    60 flags = TCPH_FLAGS(tcphdr);
    61 tcplen = p->tot_len;
    62
    63 if (flags & (TCP_FIN | TCP_SYN))
    64 {
    65 tcplen++;
    66 if (tcplen < p->tot_len)
    67 {
    68 /* u16_t 溢出,无法处理这个 */
    69 TCP_STATS_INC(tcp.lenerr);
    70 goto dropped;
    71 }
    72 }
    73
    74 prev = NULL;
    75
    76 //遍历 tcp_active_pcbs 链表寻找对应的 TCP 控制块
    77 for (pcb = tcp_active_pcbs; pcb != NULL; pcb = pcb->next)
    78 {
    79 /* 检查控制块是否与对应的网卡绑定 */
    80 if ((pcb->netif_idx != NETIF_NO_INDEX) &&
    81 (pcb->netif_idx !=
    82 netif_get_index(ip_data.current_input_netif)))
    83 {
    84 prev = pcb;
    85 continue;
    86 }
    87 /* ··· */
    88 /* 省略处理 */
    89 /* ··· */
    90
    91 if (pcb == NULL)
    92 {
    93 /* 如果 TCP 控制块没有处于连接状态,就去 tcp_tw_pcbs 链表中找 */
    94 for (pcb = tcp_tw_pcbs; pcb != NULL; pcb = pcb->next)
    95 {
    96 /* 检查控制块是否与对应的网卡绑定 */
    97 if ((pcb->netif_idx != NETIF_NO_INDEX) &&
    98 (pcb->netif_idx != netif_get_index
    99 (ip_data.current_input_netif)))
    100 {
    101 continue;
    102 }
    103
    104 if (pcb->remote_port == tcphdr->src &&
    105 pcb->local_port == tcphdr->dest &&
    106 ip_addr_cmp(&pcb->remote_ip, ip_current_src_addr()) &&
    107 ip_addr_cmp(&pcb->local_ip, ip_current_dest_addr()))
    108 {
    109 //找到了就处理它
    110 tcp_timewait_input(pcb);
    111
    112 pbuf_free(p);
    113 return;
    114 }
    115 }
    116
    117 /* 还是找不到就去 tcp_listen_pcbs 链表中找 */
    118 prev = NULL;
    119 for (lpcb = tcp_listen_pcbs.listen_pcbs;
    120 lpcb != NULL; lpcb = lpcb->next)
    121 {
    122 /* 检查控制块是否与对应的网卡绑定 */
    123 if ((lpcb->netif_idx != NETIF_NO_INDEX) &&
    124 (lpcb->netif_idx != netif_get_index(ip_data.current_input_netif)))
    125 {
    126 prev = (struct tcp_pcb *)lpcb;
    127 continue;
    128 }
    129 /* ··· */
    130 /* 省略处理 */
    131 /* ··· */
    132
    133 //找到了处于监听状态的 TCP 控制块
    134 if (lpcb != NULL)
    135 {
    136 if (prev != NULL)
    137 {
    138 ((struct tcp_pcb_listen *)prev)->next = lpcb->next;
    139 lpcb->next = tcp_listen_pcbs.listen_pcbs;
    140 tcp_listen_pcbs.listen_pcbs = lpcb;
    141 }
    142 else
    143 {
    144 TCP_STATS_INC(tcp.cachehit);
    145 }
    146 //处理报文段
    147 tcp_listen_input(lpcb);
    148 pbuf_free(p);
    149 return;
    150 }
    151 }
    152
    153 /* ··· */
    154 /* 省略处理 */
    155 /* ··· */
    156
    157 tcp_input_pcb = pcb;
    158 err = tcp_process(pcb);
    159
    160 /* ··· */
    161 /* 省略处理 */
    162 /* ··· */
    163
    164 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164

    tcp_input()函数会对传递进来的 IP 数据报进行处理,做一些校验数据报是否正确的操作,查看一下数据报中是否有数据, 如果没有就丢掉,看一下是不是多播、广播报文,如果是就不做处理,释放 pbuf。 将 TCP 首部中的各字段内容提取出来,首先在
    tcp_active_pcbs 链表中寻找对应的 TCP 控制块,找到了就调用 tcp_process()函数进行处理;如果找不到就去 tcp_tw_pcbs 链表中查找,找到了就调用 tcp_timewait_input()函数处理它;如果还是找不到就去 tcp_listen_pcbs 链表中找,如果找到就调用 tcp_listen_input()函数处理,如果找不到的话,就释放 pbuf。此外,还要补充,对于正常接收处理的数据,如果收到的报文段是复位报文或终止连接应答报文,那么就释放 pbuf,终止连接;如果 TCP 协议确认了报文段是新的数据,那么
    就调用带参宏 TCP_EVENT_SENT(其实是一个 sent 的回调函数) 去处理, 如果报文段中包含有效的数据, 就调用 TCP_EVENT_RECV 去处理 ,如果是收到 FIN 报文,则调用TCP_EVENT_CLOSED 去处理它。
    在这里插入图片描述

  • 相关阅读:
    LeetCode654. Maximum Binary Tree
    LDcad零件新增与导入
    口袋参谋:找关键词的三种方法!
    时序预测 | MATLAB实现NGO-LSTM北方苍鹰算法优化长短期记忆网络时间序列预测
    [论文笔记]GLM
    优秀代码赏读之第一篇
    云盒子联合深信服,为南京一中打造智慧双模教学资源分享平台
    R语言颜色细分
    C++新建单层文件目录和创建多层目录,mkdir返回 -1 问题
    PTA 7-199 水仙花求和
  • 原文地址:https://blog.csdn.net/qq_40831436/article/details/127548704