TCP协议
# 传输层协议
# UDP 用户数据报协议
UDP 协议在传输数据之前不需要建立连接,远端主机收到报文后也无需进行确认,因此 UDP 不提供可靠交付。
# 特点:
- 无连接
- 尽最大努力交付
- 面向报文:UDP 对报文既不合并也不拆分,添加首部之后就向上或向下传输,一次交付一个完整的报文,不管报文有多长
- 无拥塞控制:如果网络出现拥塞,UDP 的传输速率不会改变
- 支持一对一,一对多,多对多通信
- 首部占用空间小,仅 8 个字节
# 首部格式
UDP 报文包含两个部分,一个是报文首部字段,一个是数据字段。首部共 8 个字节,有 4 个字段组成,每个字段占 2 个字节:
- 源端口
- 目的端口
- 长度:UDP 用户数据报的长度,最小值就是 8,表示只有首部字段,没有数据字段
- 校验和:用于校验报文在传输中是否有差错,有差错就丢弃
# TCP 传输控制协议
TCP 是一个面向连接的、可靠的、基于字节流、全双工的传输层协议。
# 特点:
- 面向连接:数据传输前必须先建立连接,传输结束后还需要关闭连接
- 提供可靠交付:无差错、不丢失、不重复、有序到达
- 面向字节流:虽然上层应用和 TCP 交互是一次一个数据块(大小不定),但 TCP 把这些数据块看成是一连串无结构的字节流
- 提供全双工通信:TCP 连接的两端都设有发送缓存和接收缓存,用来存放双向通信的数据
# TCP 报文格式
跟 UDP 类似,TCP 报文也分为首部字段以及数据字段两个部分
# 固定长度部分
其中,首部字段的前 20 个字节是固定的,分为 5 个部分,每个部分各占 4 个字节,也就是 32 位。后面还有长度可变的选项字段和填充字段。所以 TCP 首部字段最少为 20 个字节长。
前 20 个字节的固定长度主要分为下面 5 个部分:
- 源端口以及目的端口,各占 2 个字节
- 序号(Seq):4 个字节,序号范围[0, 2^32 - 1],大概可对 4GB 数据进行编号,当编号达到最大值就会再次从 0 开始编号。TCP 是面向字节流的,因此传输的每一个字节都会按顺序编号。而首部中的这个序号字段,是代表本次所发送的报文段中的第一个字节的序号。
- 确认号(Ack):4 个字节,代表期待收到对方下一个报文段的第一个数据字节的序号(Seq)。若确认号 = N,则代表,截止到序号为 N-1 的所有数据都已经正确收到。
- 第四段分为多个部分,共 32 位。
- 数据偏移:4 位,它其实指出的是 TCP 报文中数据字段与首部字段的距离(因为选项字段是可变的),所以换句话说,它其实标记的是整个 TCP 首部的长度。数据偏移以 4 个字节为一个单位,所以 4 位长度的数据偏移字段最大可以表示
(2^4 - 1) * 4 = 60
字节长度的首部,又因为首部字段的前 20 个字节的长度是固定的,所以长度可变的选项字段最长为 40 字节 - 保留:6 位,今后使用
- 这一部分是 6 个控制位,每个控制位占 1 位长度:
- URG(紧急指针是否有效)。URG=1 时,代表有效,该报文段拥有更高的优先级,会被优先处理。
- ACK(表示确认号是否有效)只有 ACK 标志为 1 时,确认序号才有效。TCP 规定,在连接建立后所有传输的报文段都需要将 ACK 置为 1
- PSH(缓冲区尚未填满)当 PSH=1 时,是在告知对方这些数据包收到后应该马上交给上层的应用,不用等到缓存区满了之后再提交给上层应用
- RST(表示要求对方重新建立连接)当 RST=1 时,表示出现连接错误,必须释放连接,然后再重建传输连接。复位比特还用来拒绝一个不法的报文段或拒绝打开一个连接
- SYN(建立连接消息标志)SYN=1, ACK=0 时表示请求建立一个连接,对方若同意连接,则会响应一个 SYN=1,ACK=1 的报文段
- FIN(用来释放一个连接)。当 FIN=1 时,表示此报文段的发送方的数据已经发送完毕,并要求释放连接
- 窗口大小:16 位,是 TCP 流量控制的一个手段,用来指出现在允许对方发送的最大数据量,它是经常变化的。这里说的窗口,指的是发送本报文段一方的接收窗口大小。它的作用是告诉对方:从本报文段的确认号算起,接收方所允许对方发送的数据大小,这样对方就可以控制发送数据的速度
- 校验和与紧急指针,分别占 2 个字节长度。
- 校验和字段检验的范围包括首部和数据两个部分,
- 紧急指针是和 URG 标志位配合使用的,它指出了紧急数据末尾在报文段中的位置。
# 可变长度部分
TCP 报文首部字段的最后一个部分就是,长度可变的选项部分,最长可达 40 个字节。
TCP 最初只规定了一种选项,MSS。
- MSS 最大报文长度
MSS 是 TCP 报文段中数据字段的最大长度。
TCP 首部长度 + MSS = TCP 报文段长度
它的意义如下:
如果主机不指定,则默认为 536 个字节长。
随着因特网的发展,又增加了其他几个选项:
- 窗口扩大选项
占 3 个字节,目的是为了扩大窗口,因为 TCP 首部字段中,窗口长度为 16 位,所以最大的窗口大小为 2^16 = 64kb 大小,这对一些需要高吞吐率的场景产生了限制。
时间戳 占 10 个字节,作用如下
- 用于计算 RTT 时间
- 用于处理 TCP 序号字段大于 2^32 的情况,这又称为防止序号绕回(PAWS)。
选择确认(SACK)
# TCP 如何保证可靠传输
由于 TCP 下层的 IP 协议只能提供最大努力交付,因此 TCP 需要才去适当的措施,才能保证两个运输层之间的数据传输变得可靠。
主要是利用:重传和确认机制,就可以实现在不可靠的网络上实现可靠通信
# 超时重传机制
原理是在发送某一个数据以后就开启一个计时器,在一定时间内如果没有得到发送的数据报的 ACK 报文,那么就重新发送数据,直到发送成功为止。原理很简单,但是重点在于**超时重传时间(RTO)**的选择
报文段往返时间(RTT): 记录了一个报文发出的时间与收到该报文相应的确认时间之间的差值。
# 选择确认
如果收到的报文段无差错,只是未按序号,中间缺少一些序号的数据,选择确认机制会只传送缺少部分的数据,而不是重传所有已经到达接收方的数据。
# 流量控制
# 滑动窗口
TCP 滑动窗口是以字节为单位的。发送方的窗口,不能超出接收方窗口的数值。
# 拥塞控制
拥塞控制是作用于网络的,防止过多的数据包注入到网络中,避免出现网络负载过大的情况。它的目标主要是最大化利用网络上瓶颈链路的带宽。
发送方维护一个拥塞窗口 cwnd(congestion window)
的变量
# 慢启动
它表示 TCP 建立连接完成后,一开始不要发送大量的数据,而是先探测一下网络的拥塞程度。由小到大逐渐增加拥塞窗口的大小,如果没有出现丢包,每收到一个 ACK,就将拥塞窗口 cwnd 大小就加 1(单位是 MSS)。每轮次发送窗口增加一倍,呈指数增长,如果出现丢包,拥塞窗口就减半,进入拥塞避免阶段。
- TCP 连接完成,初始化 cwnd = 1,表明可以传一个 MSS 单位大小的数据。
- 每当收到一个 ACK,cwnd 就加一;
- 每当过了一个 RTT,cwnd 就增加一倍; 呈指数让升
# 拥塞避免
一般来说,慢启动阀值 ssthresh 是 65535 字节,cwnd 到达慢启动阀值后
- 每收到一个 ACK 时,cwnd = cwnd + 1/cwnd
- 当每过一个 RTT 时,cwnd = cwnd + 1
显然这是一个线性上升的算法,避免过快导致网络拥塞问题。
# 快重传
在 TCP 传输的过程中,如果发生了丢包,即接收端发现数据段不是按序到达的时候,接收端的处理是重复发送之前的 ACK。比如第 5 个包丢了,即使第 6、7 个包到达的接收端,接收端也一律返回第 4 个包的 ACK。当发送端收到 3 个重复的 ACK 时,意识到丢包了,于是马上进行重传,不用等到一个 RTO 的时间到了才重传。这就是快速重传,它解决的是是否需要重传的问题
# 快恢复
当然,发送端收到三次重复 ACK 之后,发现丢包,觉得现在的网络已经有些拥塞了,自己会进入快速恢复阶段。在这个阶段,发送端如下改变:
- 拥塞阈值降低为 cwnd 的一半
- cwnd 的大小变为拥塞阈值
- cwnd 线性增加
# 三次握手
TCP 的三次握手,也是需要确认双方的两样能力: 发送的能力和接收的能力。于是便会有下面的三次握手的过程:
- SYN:同步序列号,是用来建立连接的握手信号。
- ack:确认序号,当 ACK 为 1 时,ack 有效,当 ACK 为 0 时,ack 无效。
- seq:数据序号。
- ACK:确认序号有效。
- FIN: 结束标志,用来表示断开连接。
# 握手过程
最开始的时候,客户端和服务端都是 Closed(关闭)状态,准备发送连接请求前,Server 会进入 LISTEN(监听)状态。
- 第一次握手: 客户端(Client)会给服务端(Server)发送请求报文段,并指定同步序列号 SYN = 1,ACK = 0, 初始序号为 seq = x,(seq 里面就是字节的序号),TCP 规定,SYN = 1 的报文段不能携带数据,而且需要消耗掉一个序号,同时 TCP 的客户端进程进入 **SYN-SENT(同步已发送)**状态。
- 第二次握手: 服务端收到客户端发送的请求报文 SYN 后,会向客户端发送一个 SYN 报文作为应答,表示同意建立连接,同时指定了自己的 SYN = 1(不能携带数据,而且需要消耗掉一个序号), ACK = 1,还会向客户端发送 seq = y,来表示自己的一个初始序号,同时也会告诉客户端下一次应该从哪开始发送的确认序号(ack),由于客户端发送过来的初始序号 seq = x, 所以确认序号 ack = x + 1,这时,TCP 的服务端进入 **SYN-RCVD(同步收到)**状态。
- 第三次握手: 客户端收到服务端的确认报文之后,会再次向服务端发送确认信息,表示已经收到。所以 ACK = 1,TCP 规定,ACK 可以携带数据,如果不携带则不消耗序号, 在不携带数据的情况下,seq = x + 1, ack = y + 1。TCP 建立连接,客户端和服务器进入 **ESTAB-LISTEND(已建立连接)**状态。
# 为什么握三次手,握两次或者四次行不行?
- 第一次握手
客户端发送建立连接的请求报文段,服务端收到了,这样服务端能够得出客户端的发送和自己的接收是没有问题的。
- 第二次握手
服务端也发送一个请求报文段,表示自己收到了来自客户端的建立连接请求报文段,同时客户端也收到服务器响应的这个报文段,这样客户端能够得出服务端的发送和自己的接收是没有问题的,但是服务端并不知道客户端的接收有没有问题(只是客户端自己知道)所以还需要第三次握手来告知服务端。
- 第三次握手
客户端接着发送请求报文段,表示自己收到了服务端的确认信息,然后服务端也正常收到了。自此,握手结束,可以知道双方的发送和接收是正常的。
假如只握手了前两次,会造成什么样的后果呢?
客户端第一次向服务端发送了建立连接的请求报文段,有可能因为网络的原因滞留了,客户端就会认为这个请求失效了,会重新向服务端发送一个连接请求,然后服务器正常响应连接。在某个时间段,第一次发送的连接请求才到达服务端,如果没有第三次握手确认,那么此时服务端会误认为客户端又发了一个新的连接请求,会再一次响应客户端,客户端收到响应请求,发现这个请求刚刚已经发过了,而且也收到了服务端的响应,就忽略这个请求,但此时的服务端却还一直等待客户端的响应,这样就会造成资源的浪费。
# 半连接队列 和 SYN Flood 攻击
三次握手前,服务端的状态从 CLOSED 变为 LISTEN, 同时在内部创建了两个队列:半连接队列和全连接队列,即 SYN 队列和 ACCEPT 队列
- 半连接队列
当客户端发送 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 合法之后才分配连接资源。
# 握手优化 - 快打开(TFO)
首轮三次握手
首先客户端发送 SYN 给服务端,服务端接收到。但是现在服务端不立刻回复 SYN + ACK,而是通过计算得到一个 SYN Cookie, 将这个 Cookie 放到 TCP 报文的 Fast Open 选项中,然后才给客户端返回。客户端拿到这个 Cookie 的值缓存下来。后面正常完成三次握手。
后面的握手过程
在后面的握手中,客户端会将之前缓存的 Cookie、SYN 和 HTTP 请求发送给服务端,服务端验证了 Cookie 的合法性,如果不合法直接丢弃;如果是合法的,那么就正常返回 SYN + ACK。 重点来了,现在服务端能向客户端发 HTTP 响应了!这是最显著的改变,三次握手还没建立,仅仅验证了 Cookie 的合法性,就可以返回 HTTP 响应了。客户端的 ACK 还得正常传过来,只不过客户端最后握手的 ACK 不一定要等到服务端的 HTTP 响应到达才发送,两个过程没有任何关系
优势
TFO 的优势并不在与首轮三次握手,而在于后面的握手,在拿到客户端的 Cookie 并验证通过以后,可以直接返回 HTTP 响应,充分利用了 1 个 RTT(Round-Trip Time,往返时延)的时间提前进行数据传输,积累起来还是一个比较大的优势。
# 四次挥手
# 挥手过程
第一次挥手: 客户端主动断开连接,向服务端发送 FIN 报文段,即连接释放报文段(FIN=1,序号 seq=u),并且状态变为 FIN-WAIT-1(终止等待 1)。TCP 规定,FIN 报文段不能携带数据,而且也消耗一个序号,所以下一次服务端响应的 ack = u+1。
第二次挥手: 服务端收到客户端的 FIN 报文段,响应请求,也向客户端发送一个 ACK = 1,seq = v, ack = u + 1 的报文段,来表示自己已经收到了客户端的断开连接的请求报文,同时将自己的状态变为 CLOSE-WAIT(关闭等待)。客户端收到服务端发送的确认报文之后,会将自身的状态变为 FIN-WAIT-2(终止等待 2),此时客户端已经断开了对服务端的连接,TCP 处于半关闭状态,即客户端没有数据要发送了,但服务端如果要发送数据,客户端仍然要接收
第三次挥手: 等待一段时间,当服务端将剩余数据发送完后,也会向客户端发送一个 FIN = 1 的报文段,其中 seq = w(因为又发送了数据,所以序号会变), ack = u + 1(因为 FIN 消耗一个序号), ACK = 1,同时,自身进入 **LAST-ACK(最后确认)**状态,来等待客户端的 ACK。
第四次挥手: 客户端收到服务端的断开释放报文段 FIN,同样对它进行响应,向服务段发送 ACK = 1 ,seq = u + 1, ack = w + 1 的报文段,并且自身进入 TIME-WAIT 状态,服务端收到客户端的响应报文,将自身状态变为 Closed。客户端现在 TCP 还没有释放掉,必须经过 时间等待计时器 设定的时间 2MSL 后,才变为 CLOSED 状态。
至此,四次挥手完毕,客户端和服务端断开连接。
MSL: 最长报文段寿命
# 等待 2MSL 的作用
- 为了保证客户端发送的最后一个 ACK 报文段能够到达服务端,这样才能保证客户端和服务端都能正常进入 CLOSED 状态。
假如,客户端在第四次挥手的过程中,收到来自服务端的 FIN 报文段并发送最后一次 ACK 报文之后立即释放连接,那么这个 ACK 报文是可能会丢失的,丢失之后,还处在 LAST_ACK 状态下的服务端,会重发 FIN+ACK 报文段,由于客户端已经关闭了连接,所以这个报文客户端永远也收不到,这就导致服务端无法进入 CLOSED 状态。
- 为了防止新的连接接收到旧连接发送的报文段。
加入当客户端发送最后一个 ACK 之后立刻断开连接,而后又有新的应用使用相同的端口,那新的应用是有可能会接收到来自服务端发送的旧的连接的数据的。
# TCP 连接中的计时器
# 重传计时器
大家都知道 TCP 是保证数据可靠传输的。怎么保证呢?带确认的重传机制。在滑动窗口协议中,接受窗口会在连续收到的包序列中的最后一个包向接收端发送一个 ACK,当网络拥堵的时候,发送端的数据包和接收端的 ACK 包都有可能丢失。TCP 为了保证数据可靠传输,就规定在重传的“时间片”到了以后,如果还没有收到对方的 ACK,就重发此包,以避免陷入无限等待中。
当 TCP 发送报文段时,就创建该特定报文的重传计时器。可能发生两种情况:
- 若在计时器截止时间到之前收到了对此特定报文段的确认,则撤销此计时器。
- 若在收到了对此特定报文段的确认之前计时器截止时间到,则重传此报文段,并将计时器复位。
# 坚持计时器
专门对付零窗口通知而设立的,
先来考虑一下情景:发送端向接收端发送数据包知道接受窗口填满了,然后接受窗口告诉发送方接受窗口填满了停止发送数据。此时的状态称为“零窗口”状态,发送端和接收端窗口大小均为 0.直到接受 TCP 发送确认并宣布一个非零的窗口大小。但这个确认会丢失。我们知道 TCP 中,对确认是不需要发送确认的。若确认丢失了,接受 TCP 并不知道,而是会认为他已经完成了任务,并等待着发送 TCP 接着会发送更多的报文段。但发送 TCP 由于没有收到确认,就等待对方发送确认来通知窗口大小。双方的 TCP 都在永远的等待着对方。
要打开这种死锁,TCP 为每一个链接使用一个持久计时器。当发送 TCP 收到窗口大小为 0 的确认时,就坚持启动计时器。当坚持计时器期限到时,发送 TCP 就发送一个特殊的报文段,叫做探测报文。这个报文段只有一个字节的数据。他有一个序号,但他的序号永远不需要确认;甚至在计算机对其他部分的数据的确认时该序号也被忽略。探测报文段提醒接受 TCP:确认已丢失,必须重传。
坚持计时器的值设置为重传时间的数值。但是,若没有收到从接收端来的响应,则需发送另一个探测报文段,并将坚持计时器的值加倍和复位。发送端继续发送探测报文段,将坚持计时器设定的值加倍和复位,直到这个值增大到门限值(通常是 60 秒)为止。在这以后,发送端每个 60 秒就发送一个探测报文,直到窗口重新打开。
# 保活计时器
保活计时器使用在某些实现中,用来防止在两个 TCP 之间的连接出现长时间的空闲。假定客户打开了到服务器的连接,传送了一些数据,然后就保持静默了。也许这个客户出故障了。在这种情况下,这个连接将永远的处理打开状态。
要解决这种问题,在大多数的实现中都是使服务器设置保活计时器。每当服务器收到客户的信息,就将计时器复位。通常设置为两小时。若服务器过了两小时还没有收到客户的信息,他就发送探测报文段。若发送了 10 个探测报文段(每一个像个 75 秒)还没有响应,就假定客户除了故障,因而就终止了该连接。
这种连接的断开当然不会使用四次握手,而是直接硬性的中断和客户端的 TCP 连接。
# 时间等待计时器
时间等待计时器是在四次握手的时候使用的。
四次握手的简单过程是这样的:假设客户端准备中断连接,首先向服务器端发送一个 FIN 的请求关闭包(FIN=final),然后由 established 过渡到 FIN-WAIT1 状态。服务器收到 FIN 包以后会发送一个 ACK,然后自己有 established 进入 CLOSE-WAIT.
此时通信进入半双工状态,即留给服务器一个机会将剩余数据传递给客户端,传递完后服务器发送一个 FIN+ACK 的包,表示我已经发送完数据可以断开连接了,就这便进入 LAST_ACK 阶段。
客户端收到以后,发送一个 ACK 表示收到并同意请求,接着由 FIN-WAIT2 进入 TIME-WAIT2 阶段。服务器收到 ACK,结束连接。此时(即客户端发送完 ACK 包之后),客户端还要等待 时间等待计时器设置的 2MSL(MSL=maxinum segment lifetime 最长报文生存时间,2MSL 就是两倍的 MSL)后才能真正的关闭连接。