目录

  1. 传输层协议
  2. UDP
    1. 报文格式
    2. 基于 UDP 的应用
  3. TCP
    1. 报文格式
    2. 可靠性控制
      1. 发送端可靠算法
      2. 接收端确认产生算法
      3. 计时器间隔设置
    3. 流量控制
    4. 连接管理
      1. 三次握手(连接建立)
        1. 三次握手原因一
        2. 三次握手原因二
        3. 三次握手原因三
        4. 三次握手总结
        5. SYN 攻击
      2. 四次挥手(连接关闭)
        1. 为什么是四次挥手
        2. 为什么需要 TIME_WAIT ?
        3. 为什么 TIME_WAIT 时间是 2MSL
        4. TIME_WAIT 过多如何?
    5. 拥塞控制
      1. 时延
      2. 丢包
  4. 附录

传输层协议

传输层对应用层提供进程到进程的通信支持,为了能够做到进程间通信,传输层使用了端口。能够将发送端某个进程的报文封装成,传送到接收端对应进程中。

✨传输层特点

  • 此层只有两个协议,即 UDPTCP
  • 除了提供段传送功能外,还提供了复用/解复用差错检测功能

❓ 复用和解复用是什么意思?

  • 复用的含义是:应用层不同进程的报文在传输层封装成不同的段传送到网络,信息在传输层不会混合在一起
  • 解复用的含义是:到达接收端传输层的段,按照目的端口号分别送给各自的进程,不会送错

📖 TCP 和 UDP 协议只运行在端系统上,提供全双工的通信,当路由器的角色是端系统时,它也有传输层和应用层,也要运行这两个协议

UDP

UDP(User Datagram Protocol)是用户数据报协议,这是一个非常简单的协议,和下层的 IP 相比,就多了一个端口号承载功能。所以,它和 IP 协议一样,服务模型都是尽力而为

✨UDP 协议特点

  • UDP 提供的是无连接、不可靠的服务,直接传送段,不需要先建立连接
  • 协议简单报文段头小比 TCP 快

📖UDP 发送的前后段之间没有依赖关系,UDP 的段可能丢失,也可能乱序(后发的段先到达接收端进程),当然也可能出现差错。出现这些问题时,UDP 并没有提供问题恢复的功能

报文格式

UDP 的段头只有 8 个字节,分为四个字段,每个字段都是两个字节长:源端口号目的端口号长度(单位是字节)校验和(checksum)

🎶校验和字段是多余的,对于每个段的传送,源端和目的端都会进行校验和计算和验证,这会增加端到端的传送时延

🤔为什么校验和是多余的 ?

  1. UDP 通信允许丢包和出现差错,即便出现差错也不会去处理
  2. 如果传输过程中出现差错,数据链路层就会把包含差错的帧丢弃,不会再向上层传送
  3. 凡是送达传输层的段,都是没有差错的

基于 UDP 的应用

局域网典型的 UDP 应用
远程文件系统(NFS)
网络管理(SNMP)
动态主机配置(DHCP)
简单文件传输(TFTP, trivial FTP)

🎶这些应用都是面向局域网的,对实时性和可靠性均有较高的要求。局域网通信一般不太会丢包,使用 UDP 可以大体上保证实时性

Internet 典型的 UDP 应用
流媒体(音视频点播)
可视电话
域名服务(DNS)
路由信息协议(RIP)
路由跟踪(Tracert 网络命令)
多播应用
基于 RTP 的实时应用

TCP

TCP 是传输控制协议(Transport Control Protocol),仅支持点对点通信,提供面向连接的可靠的字节有序的信息传输服务。

TCP 包含四个主要技术:可靠性控制流量控制拥塞控制连接管理

报文格式

TCP 的段头是 20 个字节长,缺省的 TCP 最大段长固定为 536 字节, 这意味着一个 TCP 段的负载最多是 516 字节

报文含义
Head length段头长度,占 4 位,可区别 16 个字
6 位是保留位没有使用
U 是 Urgent 的字头紧急位,表示这个段需要立即向上层传送实际当中没有使用
A 是确认段标记A=1,表示这个段是确认段
P 是 Push 的字头含义和 U 类似,表示立即向下传送实际当中没有使用
R、 S、 F 三个位跟连接管理有关R=1 表示重新建立连接;
S=1 表示这个段是同步段,S=0 表示连接建立结束
F=1 表示连接关闭
Rcvr window size接收窗口,即接收端的空闲缓存。发送端基于这个值来控制发送速率,以免造成接收端因接收缓存溢出而丢包
checksum 字段存放 16 位的校验和,接收端据此来判断所接收的段里是否有差错。
实践当中没有使用
ptr urgent data16 位,存放紧急数据的指针,实践当中没有使用
可变长的选项字段,存放需要协商的最大段长、时间戳、窗口的变化因子
实际当中没有使用

可靠性控制

TCP 可靠性控制的基本思想是:丢包重传,即发送端发现哪个段丢失了,就重传哪个段

✨TCP 实现可靠传送的要求:

  1. 发送端必须开辟缓存,缓存发送出去段,保证段丢失了可以重发这些段
  2. 发送端和接收端需要相互协作(要有确认机制,告诉发送端它收到了那些段;发送端要有计时机制,一旦确认段丢失,也能发现有段丢失)
  3. 使用段序号对所发送的段进行编号,以便发送端能确定是哪些段丢了

发送端可靠算法

发送端开辟的缓存称之为发送窗口 ,需要几个变量来维护这个窗口。

变量含义
send_base基指针,指向发送窗口的最左侧的段
收到一个确认段时, 基指针就右移到确认段中确认号指向的段、这意味着接收端确认段之前所有的段都已经收到了
nextseqnum可以使用的下一个段序号,发送端每封装一个段,nextseqnum 就右移一位
N窗口的大小,当发送窗口满时,停止发送

接收端确认产生算法

✨TCP 累积确认机制

  • TCP 接收端返回确认段的确认号始终是它期待那个段的段序号

📚累积确认,将告诉发送端两件事

  1. 当前段之前所有的段都接受到了
  2. 期待当前段序号的段

接收端是有接收窗口的,用以存放所收到的失序的段

变量含义
rcv_base基指针,始终指向期待接收的段。
如果基指针指向的段到了,基指针就会右移到所期待的段
N接收窗口的大小,当接收窗口满时,新到的段就被丢弃

TCP ACK 产生机制
事件接收端动作
按序到到达一个段,没有间隙
前面所有的段都已确认
延迟确认,做最多 500 ms 的延时,等待下一个段。如果下一个段未到,延时结束时发送 ACK
按序到达一个段,没有间隙
前面一个正在做延时
立即发送 ACK,累积确认
乱序到达一个段,段序号比期待的段序号高,检测到间隙重发 ACK,确认号还是期待段的段序号
到达一个段,位于接受窗口的间隙中立即给出确认
如果位于最左侧的间隙,右移接收窗口的基指针
否则,标记该段接受
TCP 可靠重传算法

🤔如何保证 TCP 的全双工可靠通信?

  • TCP 发送端的可靠性控制算法在接收端部署一份
  • 接收端的确认产生算法在发送端也部署一份

计时器间隔设置

TCP 发送端每发送一个段,就会启动一个逻辑计时器, 对这个段进行计时, 在发送端确定是否需要重传

🤔这个计时器的触发间隔应该是多长呢?

  • 太长,对丢包的响应很慢,影响通信效率
  • 太短,会出现假丢包现象,造成大量不必要的重传浪费网络带宽资源
  • 理想情况,应能反映最近未来的网络状况,应该比接下来的那个 RTT 略大一点,同时又具有较好的平滑性,避免频繁地设置计时器

如果我们把未来这个 RTT 值定义为 EstimatedRTT, TCP 预测 EstimatedRTT 使用的是“加权运动平均”模型, 这个模型使用了很多次历史测量的 RTT 值,而不是只使用刚刚测量的那个 RTT 值。

$EstimatedRTT_n = (1-x)EstimatedRTT_{n-1} + xSampleRTT$

变量含义
$SampleRTT$刚刚测量的 RTT 值
$EstimatedRTT_{n-1}$上一次的预测值
$x$经验值,通常为$0.125$

当网络状态波动大时,偏差值 Deviation 也大;

当网络状态波动小时, Deviation 也小。

因此,实际设定的间隔值 Timeout 具有自适应性,能自适应网络的忙闲状况。

流量控制

🤔为什么需要流量控制 ?

  • TCP 的接收端的段处理速度有可能比较慢,这就容易造成接收缓存溢出导致接收端的传输层丢包

🤔如何达成流量控制 ?

  • 🎶发送端的发送速率和目的端的处理速率匹配起来,对发送端的发送速率进行控制,使得既不会导致接收端丢包,也不会导致发送端发送速率过慢影响通信效率

👴本质上,TCP 流量控制就是——速率匹配机制

TCP 的流控机制使用了一个重要参数: $RcvWindow$,表示空闲的接收缓存大小

$$
\text{RcvWindow}= \text{RcvBuffer}-(\text{LastByteRcvd}-\text{LastByteRead})
$$

变量含义
$\text{RcvBuffer}$接收缓存
$\text{LastByteRcvd}$所接收的、最后的字节数
$\text{LastByteRead}$传送给应用层的最后字节数
$\text{LastByteRcvd}-\text{LastByteRead}$被占据的接收缓存空间

$\text{LastByteRcvd}-\text{LastByteRead}$参数是动态变化的,伴随着确认段的返回,通过段头不断地反馈给源端。源端据此来控制自己的发送速率:所发送的、尚未被确认的数据量不超过 $\text{RcvWindow}$

  1. $\text{Initially}, \text{RcvWindow}=\text{RcvBuffer}$
  2. $\text{LastByteSent}-\text{LastByteAcked}<=\text{RcvWindow}$
  3. When $\text{RcvWindow}=0$ , one byte data is still sent to the receiver;

🤔为什么接收区缓存满了之后,还需要发送一个比特的数据尼?

  • 接收缓存满时,如果停止发送数据,很容易导致通信停止(如果一直接收不到确认信
    息,通信就终止了,即便接收缓存变空,源端也感知不到)
  • 所以,即便接收缓存满了仍然发送一个字节的数据(段大小是 21 个字节)
  • 当这个段到达接收端时,即便接收缓存腾出一点空间,这个段也能存放进去,接收端就会返回一个确认段,确认段中包含有最新的 $RcvWindow$ 值,通信就不会终止
  • 如果这个很小的段到达接收端,接收缓存仍然是满的,这个段被丢弃掉,损失也不大

这个算法还可以进一步优化。

在目前 TCP 的控制算法中,源端的数据发送量是根据半个 RTT 之前接收端的空闲接收缓存大小决定的,而真正想匹配的却是半个 RTT 之后接收端接收缓存的大小。这实际上是存在一个 RTT 的时差的,在这一个 RTT 内,空闲的接收缓存大小很可能发生变化了。 如果源端能根据接收端反馈的 RcvWindow 值, 通过预测模型(如加权运动平均模型) 来预测半个 RTT 之后的 RcvWindow 大小,就能提高匹配的精度,提高
TCP 的通信效率。

连接管理

连接管理主要包括连接建立连接关闭

TCP 在通信之前必须要先建立连接(这个连接是逻辑上的,只存在于两个端系统之上)

🎶建立连接的目的是

  1. 两个端系统开辟缓存(发送缓存和接收缓存)
  2. 设置控制所用的变量
  3. 交换初始的段序号

三次握手(连接建立)

  1. 客户端向服务器端发送同步段及进行连接请求,段头里包含有初始段序号,同步位S=1
  2. 服务器端给出连接确认返回同意连接 的同步段,段头里包含有服务器端随机选取的初始段序号,同步位 S=1、确认号=客户端发送的初始段序号+1,之后服务器开辟缓存,设置相关变量
  3. 客户端返回一个确认段其中S=0,段序号=初始段序号+1,确认号=服务器端发送的初始段序号+1,随后客户端开辟缓存,设置相关变量。

🎶三次握手的过程中,所交换段的负载域均为空

🤔为什么是三次握手?而不是四次或者两次?

  • The Priciple reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion. ——RFC 793

    首要原因就是防止旧的重复连接初始化造成混乱

  • 同步双方初始序列号

  • 避免资源浪费

三次握手原因一

客户端连续发送多次 SYN 建立连接的报文,在网络拥堵情况下:

  • 一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端;
  • 那么此时服务端就会回一个 SYN + ACK 报文给客户端;
  • 客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送 RST 报文给服务端,表示中止这一次连接。

如果是两次握手连接,就不能判断当前连接是否是历史连接,三次握手则可以在客户端(发送方)准备发送第三次报文时,客户端因有足够的上下文来判断当前连接是否是历史连接:

  • 如果是历史连接(序列号过期或超时),则第三次握手发送的报文是 RST 报文,以此中止历史连接;
  • 如果不是历史连接,则第三次发送的报文是 ACK 报文,通信双方就会成功建立连接;

所以,TCP 使用三次握手建立连接的最主要原因是防止历史连接初始化了连接。

三次握手原因二

TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:

  • 接收方可以去除重复的数据
  • 接收方可以根据数据包的序列号按序接收
  • 可以标识发送出去的数据包中, 哪些是已经被对方收到的

当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。

✨四次握手、三次握手、二次握手

  • 四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」
  • 两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收
三次握手原因三

如果只有「两次握手」,当客户端的 SYN 请求连接在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的 ACK 确认信号,所以每收到一个 SYN 就只能先主动建立一个连接

如果客户端的 SYN 阻塞了,重复发送多次 SYN 报文,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费

🎶两次握手会造成资源浪费,服务器重复接收无用的连接请求,而造成重复分配资源

三次握手总结

不使用「两次握手」和「四次握手」的原因:

  • 「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列
  • 「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数
SYN 攻击

如果只有前两次握手,所建立的连接称为半连接

在半连接中,只有服务器端开辟了缓存,客户端并没有开辟缓存如果一个客户端连续地和服务器端建立半连接,很可能导致服务器端因资源耗尽而瘫痪。这种半连接攻击方法是一种 DoS(Denial of Service)攻击。如果多个客户端同时半连接攻击一台服务器或路由器,就能很快地导致服务器或路由器瘫痪,这种攻击方法称为 DDoS(Distributed Denial of Service)

避免 SYN 攻击方式一

修改 Linux 内核参数,控制队列大小和当队列满时应做的处理

  • 当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数:
1
net.core.netdev_max_backlog
  • SYN_RCVD 状态连接的最大个数:
1
net.ipv4.tcp_max_syn_backlog
  • 超出处理能时,对新的 SYN 直接回报 RST,丢弃连接:
1
net.ipv4.tcp_abort_on_overflow
避免 SYN 攻击方式二

Linux 内核的 SYN(未完成连接建立)队列与 Accpet(已完成连接建立)队列的工作方式

  1. 当服务端接收到客户端的 SYN 报文时,会将其加入到内核的「 SYN 队列」
  2. 接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文
  3. 服务端接收到 ACK 报文后,从「 SYN 队列」移除放入到「 Accept 队列」
  4. 应用通过调用 accpet() socket 接口,从「 Accept 队列」取出连接

tcp_syncookies 的方式可以应对 SYN 攻击的方法:

1
net.ipv4.tcp_syncookies = 1

tcp_syncookies 应对 SYN 攻击

  1. 当 「 SYN 队列」满之后,后续服务器收到 SYN 包,不进入「 SYN 队列」
  2. 计算出一个 cookie 值,再以 SYN + ACK 中的「序列号」返回客户端
  3. 服务端接收到客户端的应答报文时,服务器会检查这个 ACK 包的合法性。如果合法,直接放入到「 Accept 队列」
  4. 最后应用通过调用 accpet() socket 接口,从「 Accept 队列」取出的连接

四次挥手(连接关闭)

双方都可以主动断开连接,断开连接后主机中的资源将被释放

由于 TCP 连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个 FIN 来终止这一方向的连接,收到一个 FIN 只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个 TCP 连接上仍然能够发送数据,直到这一方向也发送了 FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭,上图描述的即是如此。

  • 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSED_WAIT 状态。
  • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
  • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
  • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
  • 服务器收到了 ACK 应答报文后,就进入了 CLOSED 状态,至此服务端已经完成连接的关闭。
  • 客户端在经过 2MSL 一段时间后,自动进入 CLOSED 状态,至此客户端也完成连接的关闭。

🎶只有发起关闭方,才会有TIME_WAIT状态

为什么是四次挥手
  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据
  • 服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接

服务端通常需要等待完成数据的发送和处理,所以服务端的 ACKFIN 一般都会分开发送,所以需要多发送一次

为什么需要 TIME_WAIT ?

主动发起关闭连接的一方,才会有 TIME-WAIT 状态。

需要 TIME-WAIT 状态,主要是两个原因:

  • 防止具有相同「四元组」的「旧」数据包被收到
  • 保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭
防止旧连接的数据包

假设 TIME-WAIT 没有等待时间或时间过短,被延迟的数据包抵达后会造成接收到历史数据的异常

  • 如上图黄色框框服务端在关闭连接之前发送的 SEQ = 301 报文,被网络延迟了。
  • 这时有相同端口的 TCP 连接被复用后,被延迟的 SEQ = 301 抵达了客户端,那么客户端是有可能正常接收这个过期的报文,这就会产生数据错乱等严重的问题

🎶所以,TCP 就设计出了这么一个机制,经过 2MSL 这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的

保证连接正确关闭

TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP received the acknowledgment of its connection termination request. —— RFC 793

TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭

如果 TIME-WAIT 没有等待时间或时间过短,断开连接会造成如下问题:

没有确保正常断开的异常

  • 如上图红色框框客户端四次挥手的最后一个 ACK 报文如果在网络中被丢失了,此时如果客户端 TIME-WAIT 过短或没有,则就直接进入了 CLOSED 状态了,那么服务端则会一直处在 LASE_ACK 状态。
  • 当客户端发起建立连接的 SYN 请求报文后,服务端会发送 RST 报文给客户端,连接建立的过程就会被终止。

如果 TIME-WAIT 等待足够长的情况就会遇到两种情况:

  • 服务端正常收到四次挥手的最后一个 ACK 报文,则服务端正常关闭连接。
  • 服务端没有收到四次挥手的最后一个 ACK 报文时,则会重发 FIN 关闭连接报文并等待新的 ACK 报文。

所以客户端在 TIME-WAIT 状态等待 2MSL 时间后,就可以保证双方的连接都可以正常的关闭

为什么 TIME_WAIT 时间是 2MSL

MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,所以 MSL 时间就是 TTL 为零所花费的时间

🤔为什么 TIME_WAIT 等待的时间是 2MSL?

  • 网络中可能存在来自发送方的数据包,当这些数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间

比如如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 Fin 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。

2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时

TIME_WAIT 过多如何?

过多的 TIME-WAIT 状态主要的危害有两种:

  • 第一是内存资源占用
  • 第二是对端口资源的占用,一个 TCP 连接至少消耗一个本地端口

如果发起连接一方的 TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。

客户端受端口资源限制:

  • 客户端 TIME_WAIT 过多,就会导致端口资源被占用,因为端口就 65536 个,被占满就会导致无法创建新的连接

服务端受系统资源限制:

  • 由于一个四元组表示 TCP 连接,理论上服务端可以建立很多连接,服务端确实只监听一个端口,但是会把连接扔给处理线程,所以理论上监听的端口可以继续监听。但是线程池处理不了那么多一直不断的连接了。所以当服务端出现大量 TIME_WAIT 时,系统资源被占满时,会导致处理不过来新的连接

拥塞控制

TCP 流控的目的是尽量避免接收端丢包,而 TCP 拥塞控制的目的则尽量避免路由器丢包

🎶流量控制和拥塞控制均是 TCP 为了不丢包额外增加的功能

TCP 的拥塞控制属于端到端的拥塞控制方法,要靠端系统自己去感知网络是否拥塞

反映网络是否拥塞的主要参数有两个: 网络丢包时延

时延

其中时延的大小不但跟网络忙闲状况有关,还跟两个端系统之间的网络距离有关系,据此来判断网络是否拥塞比较困难

丢包

TCP 采用的是基于丢包来判定网络是否拥塞。其基本思想是:

  • 网络不拥塞时,发送端增大发送窗口
  • 网络拥塞时,发送端就减小发送窗口,从而会降低路由器的数据输入量,让路由器恢复到非拥塞状态

拥塞控制算法(Additive Increase, Multiplicative Decrease ,AIMD)分为两个部分:慢启动拥塞避免

网络情况拥塞情况
计时器超时导致了丢包网络进入重度拥塞状态
三个相同的确认段导致的丢包网络进入轻度拥塞状态

✨AIMD 算法具有四个特性

  1. 有效性(Efficiency)
  2. 收敛性
  3. 公正性(对同样的 TCP 公正)
  4. 友好型

附录

35 张图解:被问千百遍的 TCP 三次握手和四次挥手面试题