Skip to content

TCP

TCP是面向连接的、可靠的、基于字节流的传输层通信协议

头部格式

  • 源端口和目标端口:分别是指发送方应用程序的端口和目标应用程序的端口号,IP地址+端口号就能确定进程地址
  • 序列号:TCP字节流中每一个字节都会按顺序标号,序列号表示本次报文发送数据的第一个字节的序号
  • 确认号:期望收到对方下一个报文段中数据的第一个字节的序号
  • 数据偏移:TCP报文段的数据起始处距离TCP报文段的起始处有多远,这个距离就是TCP报文段的首部长度
  • 保留:供以后使用
  • 控制位
    • ACK:置1表示确认号字段有效,置0表示确认号字段无效,TCP规定连接建立后传送的所有报文都必须把ACK置1
    • SYN:置1表示请求建立连接
    • FIN:置1表示数据已经发送完毕,要求释放连接
    • RST:置1表示TCP连接中出现严重错误,必须释放连接然后重新建立连接
    • URG:置1表示报文段中有紧急数据,应尽快发送,不用在缓存中排队,需要配合紧急指针使用(紧急指针指出该报文段中紧急数据的字节数)
    • PSH:置1表示立即创建一个报文段发送,接收方会尽快交付,而不是等缓存满了再交付,用于两个应用程序交互式通信时,一端希望立即收到另一端的响应。

三次握手

三次握手

  • 第一次握手

    客户端向服务器发送请求建立连接报文,此时报文中没有数据,会把SYN置为1表示发起一个连接并且随机产生一个序列号seq=x,之后客户端进入SYN_SENT状态

  • 第二次握手:

    服务器收到报文后,会向客户端发送一个确认应答并请求连接报文,会把SYN置为1表示发起一个连接,还会把ACK置为1表示收到了客户端的报文,会产生一个随机的序列号seq=y,还会产生确认号ack=x+1,之后服务器进入SYN_RCVD状态

  • 第三次握手:

    客户端收到报文后,会检查确认号ack是否为x+1,ACK是否为1,如果都正确则会向服务器发送一个确认应答报文,会把SYN置为0表示不用请求建立连接了,要正式发送数据了,会将ACK置为1表示收到了服务器的报文,会产生确认号ack=y+1,会产生序列号seq=x+1,之后客户端进入ESTABLISHED状态。服务器收到报文后会检查确认号ack是否为y+1,ACK是否为1,如果都正确则服务器进入ESTABLISHED状态,三次握手完成。

四次挥手

  • 第一次挥手

    客户端发送一个请求断开连接报文,会把FIN置为1表示发送数据的任务已经完成,要释放连接了,会产生一个序列号seq=x(x = 传输数据的最后一个字节的序号 + 1),客户端进入FIN-WAIT1状态,此时客户端能接收报文,但不能发送带有数据的报文,只能发送确认应答报文

  • 第二次挥手

    服务器收到报文后,会向客户端发送一个确认应答报文,会将ACK置为1表示收到了客户端的报文,会产生一个确认应答号ack=x+1,会产生序列号seq=y,服务器进入CLOSE_WAIT状态,此时服务器还能发送和接收报文,客户端收到服务器的报文后,进入FIN-WAIT2状态

  • 第三次挥手

    当服务器没有可发送的信息后,会给客户端发送请求断开连接报文,会把FIN置为1表示发送数据的任务已经完成,要释放连接了,会把ACK置为1表示收到了客户端的报文,产生确认号ack=x+1,产生序列号z(不是y,因为服务器发送请求断开连接报文前可能还给客户端发送了报文,字节流序号可能会变),服务器进入LAST_ACK状态

  • 第四次挥手

    客户端收到报文后,会检查确认ack是否为x+1,ACK是否为1,如果都正确向服务器发送确认应答报文,将ACK置为1表示收到了服务器的报文,产生确认应答号ack = z + 1,产生序列号seq = x + 1,报文发送完毕后客户端进入TIME_WAIT状态,等待2MSL后,进入CLOSED状态。服务器收到报文后进入CLOSED状态

为什么是三次握手?不是两次、四次?

TCP需要序列号seq来做可靠重传或接收,为了避免连接复用时无法分辨出序列号seq是延迟或者是旧连接的seq,因此需要三次握手来约定确定双方的 ISN(初始seq序列号)。

假设客户端发出的第一个连接请求报文段并没有丢失,而是因为网络问题导致在连接释放以后的某个时间才到达 服务器。

两次握手的情况下:本来这是一个早已失效的报文段,但服务器收到此失效的连接请求报文段后,就误认为是客户端再次发出的一个新的连接请求。于是就向客户端发出确认报文段,同意建立连接。由于现在客户端并没有发出建立连接的请求,因此不会理睬服务器的确认报文,也不会向服务器发送数据。但服务器却以为新的连接已经建立,并一直等待客户端发来数据。这样,服务器的很多资源就白白浪费掉了。

三次握手的情况下:服务器向客户端发出确认报文段,客户端收到服务器的报文段后会发现确认号ack对应不上,不会向服务器发送确认报文段。服务器由于收不到确认报文段,就知道客户端并没有要求建立连接。

四次握手的情况下:也能避免上述问题,但四次握手可以简化成三次握手。

其他解释:

第一次握手:

​ 客户端:什么都不能确认

​ 服务器:自身接收正常,客户端发送正常

第二次握手

​ 客户端:自身发送、接收正常;服务器发送、接收正常

​ 服务器:自身接收正常;客户端发送正常

第三次握手

​ 客户端:自身发送、接收正常;服务器发送、接收正常

​ 服务器:自身发送、接收正常;客户端发送、接收正常

为什么是四次挥手?

因为TCP是全双工通信的,客户端发送FIN报文后,服务器可能还有要发送给客户端的数据,所以服务器的ACK和FIN一般都会分开发,所以是四次挥手。

为什么需要等待2MSL

假如客户端发送的最后一个ACK报文因为网络问题丢失,如果服务端发出FIN报文后没有收到ACK报文,就会重发FIN报文,此时客户端就会重发ACK报文。客户端不能无限久的等待这个可能会重发的FIN报文,因为如果服务端正常接收到了ACK报文后是不会再重发FIN报文的。MSL指一个报文在网络中最大的存活时间,服务器需要1MSL确认有没有收到ACK报文,则客户端需要1MSL确认有没有收到重发的FIN报文(如果有重发的FIN报文)。如果经过2MSL时间后,客户端还没有收到重发的FIN报文,那么说明ACK报文已被被服务器成功收到,客户端进入CLOSED状态。

其他解释:

防止旧连接的报文段出现在新的连接中。客户端在发送完最后一个ACK报文后,再经过2MSL,就可以确认由于网络问题产生的滞留报文段失效,新的连接中就不会出现旧的报文段。

TCP和UDP的区别

  • TCP面向连接,发送数据前要经历三次握手建立连接;UDP无连接,发送数据前不用建立连接
  • TCP提供可靠传输,基于TCP连接传输的数据不丢失、不重复、安全可靠;而UDP尽最大努力交付,不保证可靠传输
  • TCP只能一对一通信;UDP可以一对一、一对多、多对一、多对多通信
  • TCP面向字节流传输、UDP面向报文传输
  • TCP首部开销大,最小20字节,最大60字节;UDP首部开销小,仅8字节
  • TCP使用可靠传输应用,如文件传输;UDP使用实时应用,如视频会议、直播等。

半连接队列(SYN队列)和全连接队列(Accept队列)

服务器收到客户端的SYN报文后,会把该连接存储到半连接队列,并向客户端发送SYN+ACK报文,之后客户端向服务器发送ACK报文,服务器收到后,会把连接从半连接队列移除,创建新的完全的连接并添加到Accept队列,等待进程调用accept函数时把连接取出来。当半连接队列或全连接队列溢出时,会被丢弃或返回RST报文

SYN Flood攻击会在短时间内伪造大量不存在的 IP 地址,向服务器发送大量的SYN报文,当服务器发送SYN+ACK报文后,不向服务器发送ACK报文,导致服务器超时后重传SYN+ACK,需要维护大量的半连接,导致半连接队列溢出,无法处理正常的请求。

防御措施:

  • 增大半连接队列

  • 开启tcp_syncookies

    服务器收到SYN报文不分配资源保存客户端信息,根据SYN报文计算出一个cookie值作为返回的SYN+ACK报文的序列号,当客户端返回ACK报文时序列号为cookie+1,服务收到ACK报文后通过序列号-1拿到当初发送的SYN+ACK报文的序列号,对这个序列号进行检查,如果检查通过才分配资源,建立连接。

  • 减少 SYN+ACK 重传次数,默认是5次,被攻击时调小这个值,加快连接取消的速度

TCP如何保证可靠传输

  • 校验和,发送端计算待发送TCP报文段的校验和,接收端对接收到的TCP报文段验证校验和从而判断TCP首部和数据是否发生了变动,如果校验和有差错,该TCP报文段会被直接丢弃

  • 通过序列号和确认号确保不重复发送和接收。

  • 超时重传

  • 滑动窗口

  • 流量控制

  • 拥塞控制

TCP重传机制

  • 超时重传

    在发送数据报文时,设定一个定时器,超过指定时间后没有收到对方的ACK报文,就会重发该报文,当超时重发的报文再次重传时,会把定时器时间加倍

    超时重传缺点:

    • 当一个报文段丢失时,会等待一定的超时周期然后才重传分组,增加了时延。
  • 快速重传

    当收到三次冗余的ACK时,就会触发重传,解决超时周期可能过长的问题

    发送端发送了六份报文:

    • 第一份报文,传输的字节流为1-100,成功送达,ACK报文确认号为101
    • 第二份报文,传输的字节流为101-200,由于网络原因,未送达
    • 第三份报文,传输的字节流为201-300,成功送达,ACK报文确认号为101
    • 第四份报文,传输的字节流为301-400,成功送达,ACK报文确认号为101
    • 第五份报文,传输的字节流为401-500,成功送达,ACK报文确认号为101
    • 第六份报文,传输的字节流为501-600,成功送达,ACK报文确认号为101

    发送端收到了4个确认号为101的ACK报文,除第一个外另外三个是重复冗余的,触发快速重传,于是在定时器过期之前重传第二份报文,接收端收到后,由于第三、四、五、六份报文也收到了,于是CK报文确认号为601

    快速重传缺点:

    • 发送端不能确定除了最大的有序报文段之外其他报文段是否丢失,以上面例子举例,假设第三份报文也丢失,第一次重传只会重传第二份报文,因为这时还不知道第三份报文丢失。
  • SACK(选择性确认重传)

    解决快速重传的问题,在快速重传的基础上,接收端返回最近收到的报文段的序列号范围,这样发送端就知道接收端哪些数据包没收到,清楚该重传哪些数据包,SACK标记是加在TCP头部选项字段里面的。

    发送端发送了6份报文:

    • 第一份报文,传输的字节流为1-100,成功送达,ACK报文确认号为101,其中SACK为[0, 100]
    • 第二份报文,传输的字节流为101-200,由于网络原因,未送达
    • 第三份报文,传输的字节流为201-300,由于网络原因,未送达
    • 第四份报文,传输的字节流为301-400,成功送达,ACK报文确认号为101,其中SACK为[0, 100], [301, 400]
    • 第五份报文,传输的字节流为401-500,成功送达,ACK报文确认号为101,其中SACK为[0, 100], [301, 400], [401, 500]
    • 第六份报文,传输的字节流为501-600,成功送达,ACK报文确认号为101,其中SACK为[0, 100], [301, 400], [401, 500], [501, 600]

    客户端通过SACK信息发现缺失[101, 300]这段数据,于是重发

滑动窗口

窗口大小就是指无需等待确认应答,可以继续发送数据的最大值, 窗口的实现是通过操作系统开辟的一个缓冲区,发送方在接收方的确认应答报文到达之前,必须在缓冲区中保留已发送的数据,如果在规定时间内收到确认应答报文,就可以将数据从缓冲区中清除。

  • 累积确认

    发送端发送了3份报文:

    • 第一份报文,传输的字节流为1-100,成功送达,ACK报文确认号为101
    • 第二份报文,传输的字节流为101-200,成功送达,ACK报文确认号为201,但ACK报文丢失
    • 第三份报文,传输的字节流为201-300,成功送达,ACK报文确认号为301

    即使确认号为201的ACK报文丢失,但发送方收到了确认号为301的ACK报文,意味着301之前的数据都接收到了,发送端就不会因为没有收到确认号为201的ACK报文而认为第二份报文丢失,不会对第二份报文进行重传,这种模式就叫累积确认或累积应答

  • 发送窗口结构

    • 已发送并已确认的数据,假设为1-10字节
    • 已发送但未确认的数据,假设为11-20字节
    • 即将发送的数据,假设为21-30字节
    • 直到窗口移动前都不能发送的数据,假设为31-40字节

    滑动窗口 = 已发送但未确认的数据 + 即将发送的数据

    发生方把即将发送的数据都发送出去后,即将发送的数据为0字节,在没有收到ACK报文之前无法继续发送数据,假设收到了编号11-20的ACK报文,则滑动窗口往右移动10字节,接下来31-40字节就变成了即将发送的数据

  • 接收窗口结构

    • 已接收并确认的数据
    • 接收后会保存的数据
    • 不能接收的数据

    接收窗口 = 接收后会保存的数据

    接收端接收的数据的序列号在接收窗口 =内,则接收,否则丢弃,接收之后等到收到ACK报文后接收窗口右移,

流量控制

让发送方根据接收方实际的接收能力控制发送的数据量,如果发送的速度太快,接收端的缓存区就会溢出,此时发送端继续发送数据,接收端无法处理,会丢弃数据,就导致触发重传机制,降低了传输效率。

接收方会在发送ACK报文时,将自己的接收窗口填入,发送方根据ACK报文中接收窗口的大小指改变发送速度,如果值为0,就停止发送数据并通过坚持定时器每隔一段时间向接收端发送窗口探测报文,打听是否可用继续发送了,如何可以的话接收端就会告诉发送方此时接收窗口的大小。

拥塞控制

拥塞指用户对网络资源的需求超过了固有的处理能力和容量导致网络性能下降。流量控制是为了让发送方根据接收方实际的接收能力控制发送的数据量,而拥塞控制是为了降低整个网络的拥塞程度,通过拥塞窗口(cwnd)来控制,拥塞窗口是发送方维护的一个状态变量,它会根据网络的拥塞程度动态变化,只要网络中出现了拥塞,cwnd 就会减少;若网络中没有出现拥塞,cwnd 就会增大,在引入拥塞窗口之前,发送窗口大小和接收窗口大小基本是相等的关系。引入拥塞窗口后,发送窗口的大小就等于拥塞窗口和接收窗口的最小值。

  • 慢启动

    TCP 在刚建立完连接后,如果立即把大量数据注入到网络,那么很有可能引起网络阻塞。最好先探测一下,由小到大逐渐增大拥塞窗口大小。cwnd 初始值为 1,每经过一个传播轮次,cwnd 指数增长,会设置一个慢启动门限 ssthreshold

    • cwnd < ssthreshold 时,继续使用慢启动算法,
    • cwnd >= ssthreshold 时,开始使用拥塞避免算法

    image-20220311174157096

  • 拥塞避免

    拥塞避免算法就是让拥塞窗口每经过一个传播轮次加 1,当发生超时重传时,慢启动门限 sshreshold 和 拥塞窗口大小 cwnd 就会发生变化,

    • ssthresh 设为 cwnd / 2
    • ``cwnd重置为1`
    • 重新开始执行慢启动算法。

    image-20220311174340433

  • 快重传

    触发快速重传机制时,执行快重传算法

    • cwnd = cwnd / 2
    • ssthresh = cwnd
    • 重新进入拥塞避免阶段
  • 快恢复

    快恢复算法是快重传算法最后进入的不是拥塞避免阶段,而是快恢复阶段。

    快恢复的思想是“数据包守恒”原则,即同一个时刻在网络中的数据包数量是恒定的,只有当“老”数据包离开了网络后,才能向网络中发送一 个“新”的数据包,如果发送方收到一个重复的 ACK,就表明有一个数据包离开了网络,于是 cwnd 加 1。

    快速恢复的主要步骤是:

    • cwnd =ssthresh + 3,然后重传丢失的报文段,加 3 的原因是因为收到 3 个重复的 ACK,表明有 3 个“老”的数据包离开了网络。
    • 再收到重复的 ACK 时,拥塞窗口 cwnd 增加 1
    • 当收到新的数据包的 ACK 时,把 cwnd 设置为第一步中的 ssthresh 的值。原因是因为该 ACK 确认了新的数据,说明从重复 ACK 时的数据都已收到,恢复过程已经结束,可以回到恢复之前的状态,即再次进入拥塞避免状态。

    image-20220311174619791