文章目录
- TCP/IP 模型
- OSI 七层模型
-
- UDP
-
- TCP
-
- 总结
- TCP 三次握手和四次挥手
-
- TCP 三次握手建立连接
-
- 不是两次原因
- 不是四次原因
- 三次握手过程中可以携带数据吗?
- ISN(Initial Sequence Number)是固定的吗
- 三次握手是怎么过程,服务器怎么识别握手是同一个客户端的?
- TCP 四次挥手断开连接
-
- 等待2个MSL原因
- TCP 为什么是四次挥手,而不是三次?
- 半连接队列和全连接队列、SYN Flood 攻击
- TCP快速打开原理(TFO)
- TCP报文中时间戳的作用
- TCP 的超时重传时间(RTO)
- TCP 有哪些手段保证可靠交付
- TCP的流量控制
-
- TCP 的拥塞控制
-
- 拥塞窗口
- 慢启动
- 拥塞避免
- 快速重传
- 选择性重传
- 快速恢复
- 避免小包的频繁发送Nagle 算法和延迟确认
- TCP 的 keep-alive
- TCP传输确认机制是可靠的,但是TCP数据完整性的校验是不可靠的?
- TCP的定时器
- TCP 前向纠错
- 突破 TCP 同一域名下 6 个连接的问题
- 粘包和拆包
TCP/IP 模型
- 链路层:负责封装和解封装IP报文,发送和接受ARP/RARP报文等。
- 网络层:负责路由以及把分组报文发送给目标网络或主机。
- 传输层:负责对报文进行分组和重组,并以TCP或UDP协议格式封装报文。
- 应用层:负责向用户提供应用程序,比如HTTP、FTP、Telnet、DNS、SMTP等。
OSI 七层模型
数据经过七层模型的步骤
七层模型描述了数据从应用层到物理层的封装过程,以及从物理层到应用层的解封装过程
- 向下封装:当数据从发送端到接收端传输时,数据会从应用层开始,逐层向下传递。在每一层,都会添加该层特定的头部信息(也称为封装)。例如,在传输层,TCP或UDP协议会添加端口号和其他控制信息;在网络层,IP协议会添加源IP地址和目标IP地址等。这个过程就像在数据上一层层添加包装,所以称为封装。
- 向上解封:当数据到达接收端后,数据会从物理层开始,逐层向上传递。在每一层,都会去除该层添加的头部信息(也称为解封装),并根据这些信息进行相应的处理。例如,在传输层,TCP或UDP协议会根据端口号将数据传递给相应的应用程序;在网络层,IP协议会检查目标IP地址是否匹配等。这个过程就像在数据上一层层去掉包装,所以称为解封装。
UDP
UDP协议全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中,在第四层——传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。
- UDP数据包分 标头 (8个字节) 和 数据 (加标头不超过65535个字节), UDP数据包放在IP数据包的 数据 中,标头主要包括发出端口和接收端口
特点
-
面向无连接
首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。
具体来说就是:
在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作
-
有单播,多播,广播的功能
UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。
-
UDP是面向报文的
发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文
-
不可靠性
首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。
并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。
再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。
UDP只会把想发的数据报文一股脑的丢给对方,并不在意数据有无安全完整到达。
-
头部开销小,传输数据报文时是很高效的。
UDP 头部包含了以下几个数据:
两个十六位的端口号,分别为源端口(可选字段)和目标端口
整个数据报文的长度
整个数据报文的检验和(IPv4 可选 字段),该字段用于发现头部信息和数据中的错误
因此 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的
UDP 实现可靠传输
- UDP 协议本身是不可靠的,无连接的传输协议。所以需要在应用层实现 TCP 类似的策略来增加 UDP 传输的可靠性。
- 确认重传机制:当接收方收到数据包时,发送一个 ACK 确认报文给发送方。发送方在接收到 ACK 确认之前会认为数据包未送达。如果发送方在特定超时时间内未收到数据包的确认报文,假定数据包丢失并触发重传。发送方可以设置重传次数上限,超过次数则认为发送失败。
- 流量控制和拥塞控制
- 错误校验和纠正:通过校验和来检测数据包在传输过程中是否出现错误。比如前向纠错编码(FEC),还可以纠正数据包中的错误,提高可靠性。
- 顺序和重复报文处理:序列化发送报文,并在接收方重新对乱序的报文进行重组,确保按正确顺序处理数据包。
TCP
当一台计算机想要与另一台计算机通讯时,两台计算机之间的通信需要畅通且可靠,这样才能保证正确收发数据。例如,当你想查看网页或查看电子邮件时,希望完整且按顺序查看网页,而不丢失任何内容。当你下载文件时,希望获得的是完整的文件,而不仅仅是文件的一部分,因为如果数据丢失或乱序,都不是你希望得到的结果,于是就用到了TCP。
TCP协议全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的RFC 793定义。TCP 是面向连接的、可靠的流协议。流就是指不间断的数据结构,你可以把它想象成排水管中的水流
特点
-
面向连接
面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是“三次握手”,这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。
-
仅支持单播传输
每条TCP传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式。
-
面向字节流
TCP不像UDP一样那样一个个报文独立地传输,而是在不保留报文边界的情况下以字节流方式进行传输。
-
可靠传输
对于可靠传输,判断丢包,误码靠的是TCP的段编号以及确认号。TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。
TCP 会精准记录哪些数据发送了,哪些数据被对方接收了,哪些没有被接收到,而且保证数据包按序到达,不允许半点差错
-
提供拥塞控制和流量控制
当网络出现拥塞的时候,TCP能够减小向网络注入数据的速率和数量,缓解拥塞
-
TCP提供全双工通信
TCP允许通信双方的应用程序在任何时候都能发送数据,因为TCP连接的两端都设有缓存,用来临时存放双向通信的数据。当然,TCP可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于MSS)
缺点
队头阻塞
- TCP传输的单位是数据包,它的队头阻塞表示的是前一个报文没有收到便不会将下一个报文上传给HTTP
- 而HTTP队头阻塞是在 请求-响应 层面,前一个请求还没有处理完,后面的请求就被阻塞。
总结
TCP向上层提供面向连接的可靠服务 ,UDP向上层提供无连接不可靠服务。
虽然 UDP 并没有 TCP 传输来的准确,但是也能在很多实时性要求高的地方有所作为
对数据准确性要求高,速度可以相对较慢的,可以选用TCP
TCP 三次握手和四次挥手
- 32 比特的序号字段和确认号字段,TCP 字节流每一个字节都按顺序编号。确认号是接收方期望从对方收到的下一字节的序号。
- ACK 标志位,用于指示确认字段中的值是有效的 ACK=1 有效,ACK=0 无效。
- SYN 标志位,用于连接建立,SYN 为 1 时,表明这是一个请求建立连接报文。
- FIN 标志位,用于连接拆除,FIN 为 1 时,表明发送方数据已发送完毕,并要求释放连接
- 即 Reset,用来强制断开连接
- 即 Push, 告知对方这些数据包收到后应该马上交给上层的应用,不能缓存
TCP 三次握手建立连接
TCP 标准规定,ACK 报文段可以携带数据,但不携带数据就不用消耗序号
-
客户端向服务器发送一个 SYN(同步)报文,表示客户端请求建立连接。SYN 报文包含一个随机初始序列号(ISN,Initial Sequence Number)。假设客户端发送的 ISN 为 x。此时客户端处于 SYN_SEND 状态。(SYN 为 1 的时候,不能携带数据,但要消耗掉一个序号)
-
服务器收到 SYN 报文后,向客户端发送一个 SYN-ACK(同步-确认)报文。它包含两个重要的信息:服务器的随机初始序列号 y,以及一个确认序号 ack,等于客户端 ISN(x)加 1(即 x+1)。此时服务器处于 SYN_RCVD 的状态
- 服务器此时将客户端的 ISN 加 1,是为了确认收到了客户端的 SYN 报文
- 在确认报文段中 SYN=1,ACK=1,确认号 ack=x+1,初始序号 seq=y。
- 服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传。如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s,2s,4s,8s…
-
客户端收到 SYN-ACK 报文后,向服务器发送一个 ACK(确认)报文,这个报文包含一个确认序号 ack,等于服务器的 ISN(y)加 1(即 y+1)。此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。
- 确认报文段ACK=1,确认号ack=y+1,序号seq=x+1
通俗版:
- 第一次握手 客户端向服务端发送连接请求报文段,该报文段中包含自身的数据通讯初始序号,请求发送后,客户端便进入 SYN-SENT 状态
- 第二次握手 服务端收到连接请求报文段后,如果同意连接,则会发送一个应答,该应答中也会包含自身的数据通讯初始序号,发送完成后便进入 SYN-RECEIVED 状态
- 第三次握手 当客户端收到连接同意的应答后,还要向服务端发送一个确认报文,客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功
为什么是三次握手不是两次、四次?
不是两次原因
- 避免无效连接浪费资源
- 客户端发送第一次连接请求,但因为网络原因滞留了;于是客户端发送第二次连接请求,第二次正常建立了连接,数据传输完毕后,就释放了连接;如果之后第一次请求的报文抵达了服务端,只要服务端发出确认就建立新的连接了,导致服务端一直等待客户端发送数据,浪费资源。
- 通过三次握手,服务器在收到 ACK 后才会正式建立连接,如果发送报文已过期,客户端不会对 SYN-ACK 送回确认报文给服务器,从而避免无效连接的建立。
- 保证双方都具有发送和接收的能力
- 第一次握手:客户端发送网络包,服务端收到了。 这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
- 第二次握手:服务端发包,客户端收到了。 这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
- 第三次握手:客户端发包,服务端收到了。 这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。
- 防止 SYN Flood 攻击
- 就是用客户端在短时间内伪造大量不存在的 IP 地址,并向服务端疯狂发送SYN。如果两次握手足以建立连接,服务器可能会因此建立大量连接而耗尽资源
- 在实际情况中,攻击者不会发送第三次的确认报文。服务器在经过一定时间后,会自动断开并释放这些伪造的连接资源,三次握手略微降低了部分攻击的威力。
不是四次原因
- 三次握手的目的是确认双方发送和接收的能力,那四次握手当然可以,100 次都可以。但为了解决问题,三次就足够了,再多用处就不大了
三次握手过程中可以携带数据吗?
- 第三次握手的时候,可以携带。前两次握手不能携带数据。
- 如果前两次握手能够携带数据,那么一旦有人想攻击服务器,那么他只需要在第一次握手中的 SYN 报文中放大量数据,那么服务器势必会消耗更多的时间和内存空间去处理这些数据,增大了服务器被攻击的风险
- 第三次握手的时候,客户端处于ESTABLISHED状态,并且已经能够确认服务器的接收、发送能力正常,这个时候相对安全了,可以携带数据
ISN(Initial Sequence Number)是固定的吗
- 三次握手的其中一个重要功能是客户端和服务端交换 ISN(Initial Sequence Number),以便让对方知道接下来接收数据的时候如何按序列号组装数据。如果 ISN 是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。
三次握手是怎么过程,服务器怎么识别握手是同一个客户端的?
- 服务器通过携带的客户端 IP 地址、端口号、序列号和确认序号来确定握手请求来自同一个客户端。这些信息都是在 TCP 报文首部中包含的
TCP 四次挥手断开连接
FIN 报文段即使不携带数据,也要消耗序号。
- 客户端发送一个 FIN(完成)报文置为 1 的报文段。客户端变成了FIN-WAIT-1状态。注意, 这时候客户端同时也变成了half-close(半关闭)状态,即无法向服务端发送报文,只能接收
- 服务器回送一个ACK(确认)报文段,变成了CLOSED-WAIT状态。
- 客户端接收到了服务端的确认,变成了FIN-WAIT2状态
- 服务器继续发送未发送完的数据,并发送 FIN 置为 1 的报文段,进入LAST-ACK状态
- 客户端回送一个ACK(确认)报文段,自己变成了TIME-WAIT状态
- 同时客户端需要等待足够长的时间,具体来说,是 2 个 MSL(Maximum Segment Lifetime,报文最大生存时间), 在这段时间内如果客户端没有收到服务端的重发请求,那么表示 ACK 成功到达,挥手结束,否则客户端重发 ACK
等待2个MSL原因
- 第一个 MSL 确保四次挥手中主动关闭方最后(最后一次发送)的 ACK 报文最终能达到对端
- 第二个 MSL 确保对端没有收到 ACK 重传的 FIN 报文可以到达(即上一步)
- 防止“已失效的连接请求报文段”出现在本连接中
- 客户端在发送完最后一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。
TCP 为什么是四次挥手,而不是三次?
- 三次挥手:等于说服务端将ACK和FIN的发送合并为一次挥手
- 防止过早连接关闭:客户端或服务器向另一端发送的 ACK 和 FIN 报文可能组合在一个数据包中。这样的情况下,在一端还在发送数据时,另一端可能误以为已经处理完所有数据。
半连接队列和全连接队列、SYN Flood 攻击
- 半连接队列:当客户端发送SYN到服务端,服务端收到以后回复ACK和SYN,状态由LISTEN变为SYN_RCVD,此时这个连接就被推入了SYN队列,也就是半连接队列
- 全连接队列:当客户端返回ACK, 服务端接收后,三次握手完成。这个时候连接等待被具体的应用取走,在被取走之前,它会被推入另外一个 TCP 维护的队列,也就是全连接队列(Accept Queue)
- SYN Flood 攻击:SYN Flood 属于典型的 DoS/DDoS 攻击。其攻击的原理很简单,就是用客户端在短时间内伪造大量不存在的 IP 地址,并向服务端疯狂发送SYN。对于服务端而言,会产生两个危险的后果:
- 处理大量的SYN包并返回对应ACK, 势必有大量连接处于SYN_RCVD状态,从而占满整个半连接队列,无法处理正常的请
- 由于是不存在的 IP,服务端长时间收不到客户端的ACK,会导致服务端不断重发数据,直到耗尽服务端的资源
- 如何应对 SYN Flood 攻击
- 增加 SYN 连接,也就是增加半连接队列的容量
- 减少 SYN + ACK 重试次数,避免大量的超时重发
- 利用 SYN Cookie 技术,在服务端接收到SYN后不立即分配连接资源,而是根据这个SYN计算出一个Cookie,连同第二次握手回复给客户端,在客户端回复ACK的时候带上这个Cookie值,服务端验证 Cookie 合法之后才分配连接资源
TCP快速打开原理(TFO)
- 首先客户端发送SYN给服务端,服务端接收到
- 现在服务端不是立刻回复 SYN + ACK,而是通过计算得到一个SYN Cookie, 将这个Cookie放到 TCP 报文的 Fast Open选项中,然后才给客户端返回
- 客户端拿到这个 Cookie 的值缓存下来。回复ACK,后面正常完成三次握手。
- 在后面的握手中,客户端会将之前缓存的 Cookie、SYN 和HTTP请求发送给服务端,服务端验证了 Cookie 的合法性,如果不合法直接丢弃;如果是合法的,那么就正常返回SYN + ACK
- 现在服务端能向客户端发 HTTP 响应了,这是最显著的改变,三次握手还没建立,仅仅验证了 Cookie 的合法性,就可以返回 HTTP 响应了。
- 客户端最后握手的 ACK 不一定要等到服务端的 HTTP 响应到达才发送,两个过程没有任何关系
- 在拿到客户端的 Cookie 并验证通过以后,可以直接返回 HTTP 响应,充分利用了1 个RTT(Round-Trip Time,往返时延)的时间提前进行数据传输,积累起来还是一个比较大的优势。
TCP报文中时间戳的作用
报文格式:kind(1 字节) + length(1 字节) + info(8 个字节)
其中 kind = 8, length = 10, info 有两部分构成: timestamp和timestamp echo,各占 4 个字节
- 计算往返时延 RTT
- a 向 b 发送的时候,timestamp 中存放的内容就是 a 主机发送时的内核时刻 ta1
- b 向 a 回复 s2 报文的时候,timestamp 中存放的是 b 主机的时刻 tb, timestamp echo字段为从 a发送的s1 报文中解析出来的 ta1
- a 收到 b 的 s2 报文之后,此时 a 主机的内核时刻是 ta2, 而在 s2 报文中的 timestamp echo 选项中可以得到 ta1, 也就是 s2 对应的报文最初的发送时刻。然后直接采用 ta2 - ta1 就得到了 RTT 的值。
- 防止序列号的回绕
- 列号的范围其实是在0 ~ 2 ^ 32 - 1,假设范围是 0 ~ 4,那么到达 4 的时候会回到 0
- 在发送0-1的时候数据包滞留了,在3-4发送完后,序列号从0-1开始发送,之前还滞留在网路中的包回来了,那么就有两个序列号为0-1的数据包了,这个时候就产生了序列号回绕的问题,那么用 timestamp 就能很好地解决这个问题,因为每次发包的时候都是将发包机器当时的内核时间记录在报文中,那么两次发包序列号即使相同,时间戳也不可能相同,这样就能够区分开两个数据包了
TCP 的超时重传时间(RTO)
- 当发送方发送一个数据包后,等待接收方发送ACK(Acknowledgment,确认)数据包来确认收到数据包。当发送方在一定时间内没有收到ACK时,它将认为数据包可能已经丢失或损坏,然后重新发送该数据包。这种机制叫做TCP超时重传。
- 为了确定何时进行超时重传,TCP使用一个动态计算的重传超时(Retransmission Timeout,RTO)值。RTO值基于网络延迟的估计(包括往返时间RTT)和其变化(包括平均往返时间RTT以及RTT的偏差)。当经过了RTO这么长的时间之后,发送方仍然没有收到接收方的ACK,则认为该数据包已经丢失,触发超时重传。
- 当网络延迟变大时,计算出的重传时间相应地变长。这个机制有助于在网络延迟波动时,正确判断一个数据包是否丢失。
- 超时重传可能会导致发送方将已经存在于网络中的数据包再次发送。因为网络中的延迟会导致数据包和确认消息的传输时间大于发送方设置的超时时间。由于未能在超时时间内接收到确认消息,发送方会错误地触发超时重传机制并重新发送数据包。
经典方法
- 经典方法引入了一个新的概念——SRTT(Smoothed round trip time,即平滑往返时间),每产生一次新的 RTT,就根据一定的算法对 SRTT 进行更新
SRTT = (α * SRTT) + ((1 - α) * RTT)
α 是平滑因子,建议值是0.8,范围是0.8 ~ 0.9
RTO = min(ubound, max(lbound, β * SRTT))
β 是加权因子,一般为1.3 ~ 2.0, lbound 是下界,ubound 是上界
- 存在一定的局限,就是在 RTT 稳定的地方表现还可以,而在 RTT 变化较大的地方就不行了,因为平滑因子 α 的范围是0.8 ~ 0.9, RTT 对于 RTO 的影响太小
标准方法
- 为了解决经典方法对于 RTT 变化不敏感的问题,后面又引出了标准方法,也叫Jacobson / Karels 算法
- 计算SRTT,公式如下:
SRTT = (1 - α) * SRTT + α * RTT
α取值不一样了,建议值是1/8,也就是0.125
- 计算RTTVAR(round-trip time variation)这个中间变量
RTTVAR = (1 - β) * RTTVAR + β * (|RTT - SRTT|)
β 建议值为 0.25。这个值是这个算法中出彩的地方,也就是说,它记录了最新的 RTT 与当前 SRTT 之间的差值,给我们在后续感知到 RTT 的变化提供了抓手
RTO = µ * SRTT + ∂ * RTTVAR
建议值取1, ∂建议值取4
- 这个公式在 SRTT 的基础上加上了最新 RTT 与它的偏移,从而很好的感知了 RTT 的变化,这种算法下,RTO 与 RTT 变化的差值关系更加密切
TCP 有哪些手段保证可靠交付
- 序列号和确认号:由发送方在 TCP 报文段中定义的每个字节的序列号。接收方通过发送 ACK(确认)报文携带确认号来确认收到报文,确认号表示的是接收方期望收到的下一个字节的序列号。这个机制有助于检测和修复传输过程中的丢包现象。
- 超时重传机制:当发送方未在一个预定的时间内收到接收方发回的 ACK 报文时,它会假定报文丢失,并重新发送该报文。TCP 使用指数退避算法来动态调整重传超时时间,确保在不同网络环境中都能选择合适的超时时间。
- 乱序和重复检测:当接收器收到乱序或重复的报文时,TCP 能够检测并重新将其排序,填补丢失数据或丢弃重复报文。
- 错误校验和纠正:通过校验和来检测数据包在传输过程中是否出现错误。比如前向纠错编码(FEC),还可以纠正数据包中的错误,提高可靠性。
- 流量控制:TCP 使用滑动窗口机制实现流量控制。滑动窗口允许发送方在没有收到确认的情况下发送一定数量的报文。窗口的大小由接收方通告空余缓冲区大小决定。这有助于防止接收方被发送方发送的数据所淹没,以及避免网络拥塞
- 拥塞控制:TCP 使用多种拥塞控制策略(例如慢启动、拥塞避免、快速重传和快速恢复)来防止网络拥塞。这些策略通过调整发送者的发送速率,识别网络拥塞的发生并降低网络中数据传输的压力。
TCP的流量控制
对于发送端和接收端而言,TCP 需要把发送的数据放到发送缓存区, 将接收的数据放到接收缓存区。
流量控制要做的事情,就是在通过接收缓存区的大小,控制发送端的发送。如果对方的接收缓存区满了,就不能再继续发送了
滑动窗口
首先需要了解滑动窗口的概念
TCP 滑动窗口分为两种: 发送窗口和接收窗口。
发送窗口:
- 发送窗口用于限制发送方可以在确认之前发送的数据量。发送方的数据包按序号排列,当发送方接收到接收方的确认后,窗口向前滑动,将已确认的数据包从窗口中移除,然后将新的数据包添加到窗口中,等待发送和确认。
发送端的滑动窗口结构如下包含四大部分:
- 发送且已确认
- 已发送但未确认
- 未发送但可以发送
- 未发送也不可以发送
发送窗口就是图中被框住的范围
- 发送窗口就是图中被框住的范围。SND 即send, WND 即window, UNA 即unacknowledged, 表示未被确认,NXT 即next, 表示下一个发送的位置。
接收窗口:
- 接收窗口决定了发送方在等待接收方确认之前,可以发送多大的数据量。接收窗口的大小是动态调整的,当接收方缓冲区不足时,接收窗口会缩小,通知发送方降低发送窗口;当接收方缓冲区足够时,接收窗口会扩大,允许发送方加大发送窗口。
接收端的窗口结构如下
- REV 即 receive,NXT 表示下一个接收的位置,WND 表示接收窗口大小
流量控制过程
- 首先双方三次握手,初始化各自的窗口大小,均为 200 个字节。
假如当前发送端给接收端发送 100 个字节,那么此时对于发送端而言,SND.NXT 当然要右移 100 个字节,也就是说当前的可用窗口减少了 100 个字节,这很好理解。
- 现在这 100 个到达了接收端,被放到接收端的缓冲队列中。不过此时由于大量负载的原因,接收端处理不了这么多字节,只能处理 40 个字节,剩下的 60 个字节被留在了缓冲队列中。
- 注意了,此时接收端的情况是处理能力不够用啦,你发送端给我少发点,所以此时接收端的接收窗口应该缩小,具体来说,缩小 60 个字节,由 200 个字节变成了 140 字节,因为缓冲队列还有 60 个字节没被应用拿走。
- 因此,接收端会在 ACK 的报文首部带上缩小后的滑动窗口 140 字节,发送端对应地调整发送窗口的大小为 140 个字节。
- 此时对于发送端而言,已经发送且确认的部分增加 40 字节,也就是 SND.UNA 右移 40 个字节,同时发送窗口缩小为 140 个字节。
这也就是流量控制的过程。尽管回合再多,整个控制的过程和原理是一样的
TCP 的拥塞控制
流量控制发生在发送端跟接收端之间,并没有考虑到整个网络环境的影响,如果说当前网络特别差,特别容易丢包,那么发送端就应该注意一些了。而这,也正是拥塞控制需要处理的问题
对于拥塞控制来说,TCP 每条连接都需要维护两个核心状态:
- 拥塞窗口(Congestion Window,cwnd)
- 慢启动阈值(Slow Start Threshold,ssthresh)
涉及到的四种拥塞控制策略有:
拥塞窗口
拥塞窗口(Congestion Window,cwnd)是指目前自己还能传输的数据量大小。
和接收窗口的区别:
- 接收窗口(rwnd)是接收端给的限制
- 拥塞窗口(cwnd)是发送端的限制,限制的是发送窗口的大小
发送窗口大小 = min(rwnd, cwnd)
取两者的较小值。而拥塞控制,就是来控制cwnd的变化
慢启动
刚开始进入传输数据的时候,你是不知道现在的网路到底是稳定还是拥堵的,如果做的太激进,发包太急,那么疯狂丢包,造成雪崩式的网络灾难
因此,拥塞控制首先就是要采用一种保守的算法来慢慢地适应整个网路,这种算法叫慢启动。运作过程如下:
- 首先,三次握手,双方宣告自己的接收窗口大小
- 双方初始化自己的拥塞窗口(cwnd)大小
- 在开始传输的一段时间,发送端每收到一个 ACK,拥塞窗口大小加 1,也就是说,每经过一个 RTT,cwnd 翻倍。如果说初始窗口为 10,那么第一轮 10 个报文传完且发送端收到 ACK 后,cwnd 变为 20,第二轮变为 40,第三轮变为 80,依次类推
- 当 cwnd 到达慢启动阈值这个阈值之后,好比踩了下刹车,别涨了那么快了
- 在到达阈值后,通过拥塞避免来控制 cwnd 的大小
拥塞避免
拥塞控制机制通过限制发包速率来避免拥塞的发生或恶化
- 原来每收到一个 ACK,cwnd 加1,现在到达阈值了,cwnd 只能加这么一点: 1 / cwnd。一轮 RTT 下来,收到 cwnd 多个 ACK, 那最后拥塞窗口的大小 cwnd 总共才增加 1。
也就是说,以前一个 RTT 下来,cwnd翻倍,现在cwnd只是增加 1 而已
快速重传
在 TCP 传输的过程中,如果发生了丢包,即接收端发现数据段不是按序到达的时候,接收端的处理是重复发送之前的 ACK。
比如第 5 个包丢了,即使第 6、7 个包到达的接收端,接收端也一律返回第 4 个包的 ACK。当发送端收到 3 个重复的 ACK 时,意识到丢包了,于是马上进行重传,不用等到一个 RTO(超时重传) 的时间到了才重传
选择性重传
既然要重传,那么只重传第 5 个包还是第5、6、7 个包都重传呢?当然第 6、7 个都已经到达了,记录一下哪些包到了,哪些没到,针对性地重传
在收到发送端的报文后,接收端回复一个 ACK 报文,那么在这个报文首部的可选项中,就可以加上SACK这个属性,通过left edge和right edge告知发送端已经收到了哪些区间的数据报。因此,即使第 5 个包丢包了,当收到第 6、7 个包之后,接收端依然会告诉发送端,这两个包到了。剩下第 5 个包没到,就重传这个包。这个过程也叫做选择性重传(SACK,Selective Acknowledgment),它解决的是如何重传的问题。
快速恢复
发送端收到三次重复 ACK 之后,发现丢包,觉得现在的网络已经有些拥塞了,自己会进入快速恢复阶段
这个阶段,发送端如下改变:
- 拥塞阈值降低为 cwnd 的一半
- cwnd 的大小变为拥塞阈值
- cwnd 线性增加
避免小包的频繁发送Nagle 算法和延迟确认
试想一个场景,发送端不停地给接收端发很小的包,一次只发 1 个字节,那么发 1 千个字节需要发 1000 次。这种频繁的发送是存在问题的,不光是传输的时延消耗,发送和确认本身也是需要耗时的,频繁的发送接收带来了巨大的时延
而避免小包的频繁发送,这就是 Nagle 算法要做的事情。
具体来说,Nagle 算法的规则如下:
延迟确认:
想这样一个场景,当我收到了发送端的一个包,然后在极短的时间内又接收到了第二个包,那我是一个个地回复,还是稍微等一下,把两个包的 ACK 合并后一起回复呢?
延迟确认(delayed ack)所做的事情,就是后者,稍稍延迟,然后合并 ACK,最后才回复给发送端。TCP 要求这个延迟的时延必须小于500ms,一般操作系统实现都不会超过200ms
有一些场景是不能延迟确认的,收到了就要马上回复
- 接收到了大于一个 frame 的报文,且需要调整窗口大小
- TCP 处于 quickack 模式(通过tcp_in_quickack_mode设置)
- 发现了乱序包
TCP 的 keep-alive
当有一方因为网络故障或者宕机导致连接失效,由于 TCP 并不是一个轮询的协议,在下一个数据包到达之前,对端对连接失效的情况是一无所知的
这个时候就出现了 keep-alive, 它的作用就是探测对端的连接有没有失效
linux下查看
sudo sysctl -a | grep keepalive
net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_intvl = 75
站在应用的角度:
- 7200s 也就是两个小时检测一次,时间太长
- 时间再短一些,也难以体现其设计的初衷, 即检测长时间的死连接
TCP传输确认机制是可靠的,但是TCP数据完整性的校验是不可靠的?
- TCP的数据完整性校验采用的是校验和校验方法,就是把发送的字节做加法得到的和,附在TCP头部。接收方将收到的字节也做加法,得到的和与接收到校验和比较,如果相同,校验通过。只校验最终的和,所以不可靠。
- 应用程序发现TCP数据校验如此不堪,通过TLS(Transmit Layer Security),实现本应TCP完成的数据可靠传输、数据机密性要求。
TCP的定时器
- 重传定时器:当 TCP 发送一个数据包后,它会启动重传定时器。如果在预定时间内没有收到对应的 ACK 确认报文,TCP 会认为数据包丢失并触发重传。TCP 动态调整该定时器的实际取值,以适应网络条件的变化。
- 持续定时器:持续定时器用于解决“零窗口永久阻塞”问题。当接收端窗口为零时,发送方停止发送数据,并启动持续定时器。定时器到期时,发送方会向接收方发送一个探测报文,请接收方更新窗口大小。如果窗口大小大于零,则继续发送数据。
- TIME_WAIT 定时器:在 TCP 连接终止时(四次挥手),TIME_WAIT 定时器主要用于确保 TCP 收到来自对端的 ACK 确认和传输路径中可能延迟的数据包。定时器到期后,连接彻底关闭,占用的资源被释放。
- Keepalive 定时器:用于检测空闲 TCP 连接的存活状态。Keepalive 定时器会在连接空闲一段时间后启动。当定时器触发时,如果连接仍然空闲,会发送一个 Keepalive 探测报文(探测包)给远端。远端收到探测报文后回复一个 ACK 确认报文。收到 ACK 后,连接被视为存活,定时器将被重置。如果未收到,直至达到特定次数上限仍未收到任何 ACK,TCP 实现将会认为这个连接已失效
TCP 前向纠错
- 前向纠错(Forward Error Correction,简称 FEC)是一种通信系统中用于纠正数据在传输过程中出现的错误而无须重新传输数据的技术。
- FEC 机制分为两个主要步骤:
- 编码:在发送源数据之前,编码器在数据中添加冗余信息。这些冗余信息可以是奇偶校验位、校验和等。编码器使用特定的纠错算法(例如海明码、RS 码等)将源数据编码成更大的数据块。这些冗余信息使接收端能够解码时检测并纠正错误。
- 解码:接收端接收到加了冗余信息的数据交,然后使用相应的纠错算法解码。解码过程中,接收器尝试检测并纠正数据中的错误。如果错误未超过算法支持的纠错能力范围,接收器可以成功恢复源数据。否则,接收器将无法纠正所有错误。
- 通常情况下,FEC 可以纠正有限数量的错误。在噪声较小的通信环境中,FEC 可以有效地纠正错误。然而,当噪声过大,超过 FEC 算法的纠错能力范围时,无法有效纠正错误,此时可以考虑引入重传机制等措施来确保数据的可靠传输。
- 噪声:是指干扰原始信号传输和接收的随机信号。噪声可能导致数据丢失、错误传输和信号质量下降。以下是通信系统中主要的噪声来源
- 热噪声:由于电子设备的固有特性,其内部电子元件(如导线、电阻等)产生热噪声。热噪声与元件的物理温度有关,温度越高,热噪声越大。
- 电磁噪声:在电磁场中自然存在的电磁噪声、设备之间的电磁干扰(EMI)和射频干扰(RFI)等均会导致电磁噪声。电磁噪声通常由外部环境或相邻电子设备产生。
- 信道噪声:在通信信道(包括有线和无线通信)中,信号在传输过程中可能受到衰减、多径传播、相位扭曲等影响。这类现象会导致信号退化,成为信道噪声。
- 量化噪声:在模拟信号和数字信号之间转换过程中(如模数转换和数模转换),信号会被离散化和量化。在这个过程中,会产生量化误差和量化噪声。
- 码间干扰和其他干扰:在通信系统中,多信道、多用户或者同频段的信号可能产生相互干扰,导致噪声。
突破 TCP 同一域名下 6 个连接的问题
- 域名分片(Domain sharding):将 Web 资源分布在多个子域名下,使浏览器可以同时从多个子域名下载资源。由于每个子域名允许 6 个 TCP 连接,通过使用多个子域名可有效增加浏览器的并行下载能力。
- 使用 HTTP/2:HTTP/2 协议通过多路复用技术,允许在单个 TCP 连接上并发传输多个请求和响应。
- 合并和内联资源
- 合并多个 CSS 或 JavaScript 文件为单个文件,减少资源请求数量
- 图片使用雪碧图
- 将小型 CSS 和 JavaScript 代码块内联到 HTML 文件中,从而避免额外的请求
粘包和拆包
- 粘包和拆包问题发生在传输层,但是解决这个问题通常需要在应用层进行处理。
- 粘包(Sticky Packet):在接收方来说,粘包是指多个应用层数据包(即应用层发送的多条独立完整消息)被一起接收,合并为一个数据包,从而导致接收方难以区分这些应用层数据包的边界。这种现象称为"粘包"。
- 拆包(Split Packet):与粘包相反,拆包现象是指一个应用层数据包被分割成多个数据包发送。接收方需要对这些拆分的数据包进行重组,才能获取完整的应用层数据包。
- 产生原因
- TCP(Transmission Control Protocol)是面向流的协议,不像 UDP(User Datagram Protocol)那样保留数据包的边界。因此,在 TCP 传输过程中,实际传输的是一个字节流,没有提供应用层数据包边界信息。
- TCP 网络传输过程中可能涉及到拥塞控制、流量控制和优化传输。因此,发送端和接收端的缓冲区处理可能会导致合并数据包或分割数据包进行发送。
- 解决方式
- 添加前缀:在应用层数据包的头部添加长度字段,用于表示数据包的长度。接收方在接收数据时,解析出这个长度字段来判断数据包的边界。
- 固定长度:每个数据包的长度固定,不足的部分可以通过补空格或者其他特殊字符的方式来填充。这种方式简单,但可能会浪费一些带宽。
- 特殊分隔符:在每个数据包的末尾,定义一个不会出现在数据内容中的边界符作为数据包的分隔
- 使用 UDP