传输层协议

传输层协议主要包括TCP/UDP两个协议,TCP是面向连接,可靠传输的;UDP是面向无连接,不可靠传输的。当然,现在也有一些协议是建立在UDP上面向可靠传输的,也可以把他们归纳到传输层协议中,比如KCP(可以使用UDP作为底层),QUIC等等。但是,传输层协议主要有TCP和UDP两种。

| TCP | UDP |
| :——————: | :——————–: |
| 有连接 | 无连接 |
| 可靠 | 不可靠 |
| 字节流 | 数据报文 |
| 点对点 | 一对一,一对多,多对多 |
| 首部20字节 | 首部8字节 |
| 有接收发送缓冲区 | 只有接收缓冲区 |
| 有拥塞控制和流量控制 | 无拥塞和流量控制 |

UDP协议

UDP协议相对简单,体现在机制简单、报头简单等等

报头

0      7 8     15 16    23 24    31
+--------+--------+--------+--------+
|     Source      |   Destination   |
|      Port       |      Port       |
+--------+--------+--------+--------+
|                 |                 |
|     Length      |    Checksum     |
+--------+--------+--------+--------+
|
|                数据 ...
+---------------- ...

ps: 出自 RFC 768, https://datatracker.ietf.org/doc/html/rfc768
  • Source Port:源端口
  • Destination Port:目的端口
  • Length: UDP报文总长度,以8字节为单位
  • 校验和:根据IP报头、UDP报头和数据部分构成的伪报头来检验,过程为反码求和再取16位反码,如果奇数长度需填充8比特0为偶数长度(16位为一个单位)

伪报头如下:

0      7 8     15 16    23 24    31
+--------+--------+--------+--------+
|          source address           |
+--------+--------+--------+--------+
|        destination address        |
+--------+--------+--------+--------+
|  zero  |protocol|   UDP length    |
+--------+--------+--------+--------+
  • source address:源地址
  • destination address:目的地址
  • zero:零
  • protocol: IP数据包的协议,为17(UDP协议就是17)
  • UDP length: UDP报头里的Length

校验和计算

在设置好伪报头就可以计算校验和了,需要计算伪报头-UDP数据报,其中校验和先置为0,计算方法如下

uint16_t *UDP; // 存储着伪报头-UDP数据报,并且已经实现填充,设长度为 N
uint32_t sum = 0; // 使用32位来存储临时结果,因为可能会溢出
for(int i = 0; i < N; ++i){
    sum += UDP[i];
    if((sum & 0xFFFF0000) != 0){  // 17位上有进位,需要重新加到最低位
        sum &= 0xFFFF0000;  // 消除大于16位的所有位
        sum += 0x1; // 将17位进1
    }
}
uint16_t check_sum = (sum & 0x0000FFFF);
check_sum = ~check_sum; // 取反码,填充到校验和位置即可
UDP[9] = check_sum;

如何检验呢?

可以直接重新计算检验和进行检验,也可以直接计算,最后观察结果是否为0xFFFF即可。

TCP协议

基础

最大报文长度(MSS)作用:双方协商报文最大的数据长度,在TCP上面首先分片,分片之后保证一片TCP数据尽量能存在于单个IP数据报中。由于IP层不会自动重传,如果某个IP报文丢失了,且这个IP报文中载荷了TCP报文的一部分,那么整个TCP报文都会重传。

超时重传时间(Retransmission Time Out, RTO)指的是重传时间,一般略大于往返时间(Round Trip Time, RTT)。

下面是TCP通信的所有状态,可以看出两部分,一部分是连接建立,一部分是连接释放

                              +---------+ ---------\      active OPEN
                              |  CLOSED |            \    -----------
                              +---------+<---------\   \   create TCB
                                |     ^              \   \  snd SYN
                   passive OPEN |     |   CLOSE        \   \
                   ------------ |     | ----------       \   \
                    create TCB  |     | delete TCB         \   \
                                V     |                      \   \
                              +---------+            CLOSE    |    \
                              |  LISTEN |          ---------- |     |
                              +---------+          delete TCB |     |
                   rcv SYN      |     |     SEND              |     |
                  -----------   |     |    -------            |     V
 +---------+      snd SYN,ACK  /       \   snd SYN          +---------+
 |         |<-----------------           ------------------>|         |
 |   SYN   |                    rcv SYN                     |   SYN   |
 |   RCVD  |<-----------------------------------------------|   SENT  |
 |         |                    snd ACK                     |         |
 |         |------------------           -------------------|         |
 +---------+   rcv ACK of SYN  \       /  rcv SYN,ACK       +---------+
   |           --------------   |     |   -----------
   |                  x         |     |     snd ACK
   |                            V     V
   |  CLOSE                   +---------+
   | -------                  |  ESTAB  |
   | snd FIN                  +---------+
   |                   CLOSE    |     |    rcv FIN
   V                  -------   |     |    -------
 +---------+          snd FIN  /       \   snd ACK          +---------+
 |  FIN    |<-----------------           ------------------>|  CLOSE  |
 | WAIT-1  |------------------                              |   WAIT  |
 +---------+          rcv FIN  \                            +---------+
   | rcv ACK of FIN   -------   |                            CLOSE  |
   | --------------   snd ACK   |                           ------- |
   V        x                   V                           snd FIN V
 +---------+                  +---------+                   +---------+
 |FINWAIT-2|                  | CLOSING |                   | LAST-ACK|
 +---------+                  +---------+                   +---------+
   |                rcv ACK of FIN |                 rcv ACK of FIN |
   |  rcv FIN       -------------- |    Timeout=2MSL -------------- |
   |  -------              x       V    ------------        x       V
    \ snd ACK                 +---------+delete TCB         +---------+
     ------------------------>|TIME WAIT|------------------>| CLOSED  |
                              +---------+                   +---------+
节选自 RFC 793: https://datatracker.ietf.org/doc/html/rfc793

头部

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |       Destination Port        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Sequence Number                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Acknowledgment Number                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Data |           |U|A|P|R|S|F|                               |
| Offset| Reserved  |R|C|S|S|Y|I|            Window             |
|       |           |G|K|H|T|N|N|                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Checksum            |         Urgent Pointer        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options                    |    Padding    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             data                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • Source Port:源端口
  • Destination Port:目的端口
  • Sequence Number: Seq,序列号,起始序号通常是随机生成的,称作(Initial Sequence Number, ISN)
  • Acknowledgment Number: Ack,确认号,也可以表示期望号,即期望收到 num 的数据
  • Data Offset: 数据偏移量,以字节为单位,标志数据段的偏移量,也可以看作是首部长度,因此最大首部长度是4B*16=60B
  • Reserved: 保留字段,置为0
  • URG: 1表示高优先级,尽快发送出去
  • ACK:指示Acknowledgment Number有效,需要处理
  • PSH:1表示尽快交付给应用层,由于上传给用户层的数据可能需要等待一定长度再上传,节省内核态用户态的切换
  • RST: 1表示断开连接,有可能会发生网络错误,接收方接收到无效数据报,但是并没有跟对方建立连接,则会返回一个RST,告知对方可以断开这个连接了
  • SYN: 1表示连接的第一次握手和第二次握手,第一次握手,SYN=1, ACK=0;第二次握手,SYN=1, ACK=1。
  • FIN: 1表示释放TCP连接,用于释放的四次握手的第一次和第三次握手
  • Window: 告知对方自身的接收窗口大小,用于流量控制
  • Checksum:检验和,TCP首部+数据,方法同UDP
  • Urgent Pointer:紧急指针,指向紧急数据的末尾,数据会分为紧急数据和普通数据,紧急数据会放在普通数据之前,也可以表示普通数据的起始位置。
  • Options: 长度可变,存放一些选项信息,长度从0-40B都可
  • Padding: 由于长度是以4B为单位的,由于 Options 长度不一定为4B的整数倍,需要这个来填充

连接建立-三次握手

TCP 三次握手

PS:图片来源小林coding

三次握手的目的在于:

  • 防止历史连接初始化连接
  • 双方同步初始序列号
  • 避免资源浪费

初始化序列号随机:

  • 防止历史报文被下个相同四元组连接接收
  • 防止黑客伪造攻击

ISN的产生:$\text{ISN}=M+F(\text{localhost, localport, remotehost, remoteport})$

  • $M$是一个计时器,每隔$4\mu s$就自增

  • $F$是一个哈希算法,如MD5

  • 第一次握手丢失:

    服务端是LISTEN状态,客户端是SYS_SENT状态

    那么客户端会一直重传,重传时间为RTO, 2RTO, 4RTO, 8RTO,…,次数取决于tcp_syn_retries,可在/proc/sys/net/ipv4/tcp_syn_retries查看

  • 第二次握手丢失:

    服务端是SYN_RCVD状态,客户端是SYS_SENT状态

    客户端仍然会重传第一次握手数据,但是服务端仍然有个第二次握手最大重传次数,/proc/sys/net/ipv4/tcp_synack_retries,对于客户端来说,相当于第一次握手丢失效果

  • 第三次握手丢失:

    服务端是SYN_RCVD状态,客户端是ESTABLISHED状态

    服务端会一直重传二次握手,知道重传次数超过tcp_synack_retries,关闭连接

  • SYN攻击:攻击者只发送第一次握手,并不回答第二次握手数据,导致服务器一直处于SYS_RCVD状态

结合Linux来看TCP三次握手:

  • 全连接,半连接队列:内核存在SYN队列(半连接)和Accept队列(全连接)

    接收到的SYN报文,会把它们放入SYN队列中,内核会依次从队头取出连接,并发送二次握手数据,如果成功了,就放入Accept队列中

    当用户调用accept时,会从Accept队列中取出连接返回给用户进程

  • accept函数调用发生在什么时候:最早会发生在LISTEN之后,但是会阻塞,直到三次连接建立,才会返回。如果调用的时候就是三次连接建立好了,那么就会立刻返回。

连接释放-四次握手

客户端主动关闭连接 —— TCP 四次挥手

PS:图片来源小林coding

四次挥手的意义在于:每次调用发送FIN只是代表自己不再发送数据了,但是还可以接收数据。因此需要双方都发送FIN,并且都需要对FIN回复ACK

如果服务端收到FIN且刚好他也没有数据发送了,那么也可以跟ACK一起发送FIN,这样就变成三次握手了

下面简要介绍几种连接释放失败的情况,以客户端发起关闭为例

  1. 第一次挥手丢失:

    服务端是ESTABLISH状态,客户端是FIN_WAIT_1状态

    客户端会一直重传,每次重传时间翻倍RTO,最大重传次数为/proc/sys/net/ipv4/tcp_orphan_retries,然后直接关闭连接

  2. 第二次挥手丢失:

    服务端是CLOSED_WAIT状态,客户端是FIN_WAIT_1状态

    客户端没收到,也会一直重传,跟第一次挥手丢失一样,达到最大重传次数,客户端关闭。

    至于服务端,应该主动发送第三次握手数据才会关闭,这是因为FIN数据都应该是主动的

  3. 第三次挥手丢失:假设第二次和第三次没有聚合在一起

    服务端是LAST_ACK状态,客户端是FIN_WAIT_2状态

    服务端会主动发送FIN告知客户端断开,服务端会自己切换状态,客户端由于收到第二次握手,也会切换状态

    客户端如果调用close函数关闭连接,那么处于FIN_WAIT_2的状态只有60秒(具体时间是/proc/sys/net/ipv4/tcp_fin_timeout)。

    客户端如果调用shutdown函数关闭连接,那么FIN_WAIT_2的状态就会一直持续,这是非常浪费资源的一种行为。

    服务端会自动重传FIN报文,其行为可参考第一次挥手丢失,本质是一样的

  4. 第四次挥手丢失:

    服务端是LAST_ACK状态,客户端是TIME_WAIT状态

    服务端由于没收到ACK所以会一直重传,参考第三次挥手丢失

    客户端收到了对方的FIN报文,所以会启动2MSL定时器,一旦定时器归零,那么就关闭连接

    如果服务端一直重传的FIN报文被客户端重复接收,那么接收一次就会重置2MSL定时器,直到接收不到对方的FIN报文且2MSL定时器归零(对方最大重传次数过了)就关闭

2MSL:最大报文生存时间(Maximum Segment Lifetime, MSL)指任何一个报文可以在网络中最大的存在时间,超过时间这个报文就会被丢弃。和以前IP数据报的TTL字段,不过现在TTL字段表示最大跳数。MSL应该略大于TTL消耗时间,TTL一般是64,Linux中MSL为30秒。设置为2MSL的原因在于,一来一回两个数据报最多存在2MSL。Linux中体现在内核代码#define TCP_TIMEWAIT_LEN (60*HZ)

TIME_WAIT状态可以防止接收到历史连接,这是因为TIME_WAIT之后,可以认为网络中不存在此次连接的数据报了,下次连接就是干净的。

窗口

不妨想象发送方的TCP是一串字节流,接收方也会接收一串字节流。

发送方有一个发送窗口,接收方有一个接收窗口,接收窗口检验完接收到的数据就会后移,发送窗口后移只能等待接收方返回的确认号来后移。

发送窗口=min(流量窗口,拥塞窗口)

下面是一个简单示意图

发送窗口和接收窗口

PS:图片来源于King’s Blog

可靠机制

TCP可靠传输实现是通过确认机制与重传机制来实现的。

重传机制主要有超时重传和快速重传:

  • 超时重传:在发送窗口中没有数据可发送了,对于已经发送但是未接收到的数据来说,超过特定时间间隔没有接收到确认帧,那么就会再次重传,重传间隔是RTO(略大于RTT)。
  • 快速重传:在接收到连续三次相同的确认帧后,快速把根据确认帧的确认号重传对应的序列。

同样,确认的机制也有:

  • 选择确认SACK(Selective Acknowledgment),收到数据后发送期望下一次接收到的序号
  • 重复选择确认(Duplicate SACK, D-SACK),不仅发送期望下一次收到的序号,也会发送重复接收的序号,这样可以防止因为ACK报文丢失导致超时重传

流量控制

流量控制是接收端为了限制发送端的速度,缓解接收端的负载的。

接收端虽然发送了确认帧,但是由于接收端处理慢,导致成功接收的数据还没从内核中移出导致内核缓冲不够,导致流量浪费,所以会限制发送窗口,在TCP头部中Window中设定还能接收的窗口大小。接收端会对齐窗口,从后往前对齐,因为前面的不一定接收到了确认。

  • 窗口关闭:当窗口为0的时候,称之为窗口关闭,这样发送方就不会再发送数据了,所以必须由接收方打开,但是接收方由于没收到发送方的数据,所以也不会发送ACK,这样会陷入死锁了。因此,发送方如果收到了窗口关闭信息,会定时发送窗口探测报文,探测次数一般是3次,约30-60秒,如果还为0,那就发送RST中断连接。

  • 糊涂窗口症:如果接收方繁忙,导致空窗口一会儿空出几个字节就发送几个字节的空窗口,发送方如果收到空窗口就会立马发送数据,这样会导致一个TCP报文只发送几个数据,造成大量的首部浪费现象。

    解决方法:

    1. 当接收方的窗口小于一个阈值(MSS,提交给IP层的最长TCP数据),那就通告窗口为0,这样就不会有比这个长度小的数据被单独发送了
    2. 发送方等待一个阈值(MSS)长度的可发送数据,或者收到了之前的ACK包,才发送数据(Nagle算法)

拥塞控制

拥塞控制是发送端为了缓解整个网络的流量负载的

拥塞控制主要是控制拥塞窗口大小,进而控制发送窗口大小完成的。

拥塞控制分为,慢启动、拥塞避免、拥塞发生、快速恢复阶段。核心规则如下

  • 慢启动:当发送方收到一个ACK,拥塞窗口大小+1
  • 拥塞避免:当发送方收到cwnd个ACK,拥塞窗口大小+1(cwnd表示拥塞窗口,可通过ss -nli | grep cwnd查看)

具体规则:

  1. 初始拥塞窗口为1,发送1个数据并接收

  2. 拥塞窗口增加1,变为2,发送2个数据并成功接收

  3. 拥塞窗口增加2,变为4,发送4个数据并成功接收

  4. 拥塞窗口增加4,变为8,发送8个数据并成功接收

  5. 重复上述过程,直到拥塞窗口大于慢启动门限值(ssthresh,假如为8)

  6. 拥塞窗口增加1,变为9,发送9个数据并成功接收

  7. 拥塞窗口增加1,变为10,发送10个数据并成功接收

  8. 直到拥塞发生,即重传机制(超时重传或快速重传)

    • 超时重传:

      认为这种情况是网络严重堵塞了,因此设置

      ssthresh = cwnd/2

      cwnd = 1

    • 快速重传:

      不一定是网络出现问题,可能是某个数据丢失了而已,因此设置

      ssthresh = cwnd / 2

      cwnd = ssthresh

      所以快速重传之后仍然进行拥塞避免阶段,而不是慢启动阶段。

      初次之外,还可以结合快速恢复来设置窗口大小

      快速恢复:

      由于接收到了3个ACK,说明网络情况还是良好的,因此可以稍微把窗口设置大一点,即

      ssthresh = cwnd / 2

      cwnd = ssthresh + 3,3表示三个重复ACK