最全面的TCP知识,知道你想看点不一样的

756 阅读20分钟

前言

本文是我学习TCP中收集的资源,主要参考来源于网络并加以丰富,这里列举几个参考比较多的连接,如果知识复述有误,以原作者为准。

三元大佬的:(建议收藏)TCP协议灵魂之问,巩固你的网路底层基础 - 掘金 (juejin.cn)

潜行前行: 网络篇:朋友面试之TCP/IP,回去等通知吧 - 掘金 (juejin.cn)

TCP.png

TCP和UDP的区别

  • TCP是面向连接的,可靠的基于字节流可控制有状态的传输层协议
  • UDP是面向无连接的传输层协议

面向连接:

指的是客户端和服务端的连接,在通信前TCP需要三次握手建立连接,而UDP没有相应对的建立连接的过程。

可靠性:

TCP花了非常 多的功夫保证连接的可靠性,具体体现在有状态可控制

有状态:TCP会精准记录哪些数据发送了,哪些数据被对方接受了,哪些没有被接收到,而且保证数据的顺序性。

可控制:当意识到丢包了或者网络状态不佳,TCP会自我调节,可以控制自己的发送速度或者重发

相应的,UDP就是无状态不可控

面向字节流:

UDP的数据传输是基于数据报的,数据包是多少就发多少。这是因为仅仅只是继承了IP层的特性。TCP为了维护状态,就把一个个IP包变成了字节流

UDP的经典应用:DNS(域名解析),TFTP(文件传输)

网络层

掩码:用来和IP二进制的按位与运算,区分主机在哪个局域网的。

如果按位与运算的结果与局域网不同,则会选择下一跳进行再匹配。

如果相同,则会把包丢给下一跳,gateway

IP协议是不可靠协议,只负责传输,没有确认到达的机制,这一个机制是由上层协议TCP或者UDP来实现的

ARP及RARP协议

image-20211204110036331

ARP是根据IP地址获取MAC地址的一种协议

本来主机是完全不知道这个IP对应的是哪个主机的哪个接口,当主机要向一个主机发送一个IP包的时候,会先从ARP高速缓存中查询。

如果查不到,则会向网络发一个ARP协议广播包,这个广播包有主机自己的IP地址和待查询的IP地址。

收到这个广播包的主机们,就会查询自己的IP地址是否符合,如果符合,则准备好一个包含自己的MAC地址的ARP包发送给ARP广播的主机

ICMP协议

IP协议不是一个可靠协议,他不能保证数据能够准确无误地传达到接收端。

而ICMP(网络控制报文)就是负责这一部分的工作,ICMP是网络层的协议

当传送IP数据包发生错误,比如主机不可达,路由不可达等等。ICMP会将错误信息封包,然后传送给主机。给主机一个处理异常的机会。

ICMP中比较出名的ping应用指令。

链路层

网络层拿着下一跳地址去链路层找到下一跳的MAC网卡地址

ARP协议

ip在通信的过程中,是一直不会变得,代表的是端点

mac地址是会变的,代表的是下一跳,是一个节点的概念

负责的工作:

  • 封装成帧:把网络层的数据报文,追加头和尾,帧头包括源MAC地址和目的MAC地址

数据可靠性传

  • 校验和
  • 序列号
  • 三次握手四次分手
  • 确认应答机制(ACK)
  • 超时重传
  • 流量控制
  • 拥塞控制

TCP报文

image-20211204165641745

源端口、目标端口

如何表示一个唯一链接?

答案:是TCP连接的四元组,源IP + 端口 和 目标IP + 端口

为什么TCP报文没有IP的信息,因为在IP层已经处理了IP,TCP只需要记录端口号信息即可。

序列号

Sequence number,长度4个字节,也就是32位的无符号整数。如果到达最大值就循环到0。

序列号起的作用:

  • 在SYN报文中交换彼此的初始序列号
  • 保证数据包按正确的顺序组装
  • 用于ACK确认消息是否成功传递

时间戳Timestamp

Timestamp是TCP报文首部的一个可选项

TCP的时间戳主要解决两大问题

  • 计算往返时延RTT(Round-Trip-Time)
  • 防止序列号回绕问题

计算往返时延RTT

image-20211203154742362

在没有引入时间戳的时候,计算RTT,就会遇到上面两种问题

第一种情况就是,客户端传输第一次数据,但是服务端并没有响应,因此客户端进行重传,重传后,服务器并且也回应了。

如果以第一次发包时间为开始时间,则会出现RTT过长,应该采用第二次发包时间为基准

第二种情况就是,客户端传输第一次数据,服务端回应了,但是由于网络阻塞的原因,ACK迟迟没有到达客户端,客户端进行重传后,ACK到达了。如果以第二个发包时间为开始时间的话,就会导致RTT过短,应该采用第一个的。

因此我们得出结论,实际上无论以第一个发包时间还是第二个发包时间,都是不准确的。

因此引入了时间戳来解决这个问题。

场景:

现在a向b发送一个报文s1,b向a回复一个包含ACK的报文s2

  1. a向b发送的时候,timestamp中存放的内容就是a主机发送时刻的ta1

  2. b向a回复s2报文的时候,timestamp中存放的就是b主机时刻的tb,timestamp echo字段是从是

    报文中解析出来的ta1

  3. a收到b的s2报文之后,此时a主机的内核时刻是ta2,而在s2的报文中timestamp echo选项中可以得到ta1,ta2 - ta2 就是 RTT的值

防止序列号回绕问题

产生问题的前提背景是:

因为网络阻塞,导致某个序列号滞留,然后大量的消耗序列号之后,又轮到了那个滞留的序列号那里。

此时那个滞留的序列号的包回来了,怎么区分谁是谁的呢,这就产生了序列号回绕的问题,

有了timestamp之后,即使序列号相同,他们的时间戳也不可能相同,这样就能区分两个序列号

三次握手

数据传输,可以建立所谓的连接

image-20211203093412210

image-20211014101237490

接受queue 和 发送queue 监听的队列

三次握手完了之后,会开辟资源,内核交互

socket(套接字):四元组。能够 表示绝对唯一的连接

  • 源IP + port
  • 目的IP + port

两次握手可以吗?

不可以,原因是无法确定客户端的接受能力

如果是两次握手就建立连接,假设当前客户端发了一个SYN请求想要请求建立连接,但是由于网络阻塞的问题,导致迟迟没有到达服务端,这时候客户端选择重连,再发了一个SYN请求,两次握手建立好了连接。

完成了全部的传输后,客户端和服务端断开了连接。此时滞留的SYN请求到达了服务端,服务端只需要回应一个ACK,双方就可以再建立连接。因此导致浪费了资源。但是客户端是断开的。

四次握手可以吗?

当然可以,100次握手都可以,但是为了考虑资源的问题,3次握手就可以了。

三次握手过程中可以携带数据吗?

第三次握手的时候可以携带,前两次不可以携带。

第三次握手的时候吗,客户端已经处于ESTABLISHED的状态,并且已经确认过服务端的接受和发送能力,这个时候相对安全,可以携带数据。

如果前两次都可以携带数据,那么就会有的坏人对服务器进行攻击,在每个SYN请求,携带大量数据,那么服务端势必会消耗更多的时间和内存空间去处理这些数据。

四次挥手

分手 分的是连接(内存资源,释放资源)

image-20211203095140374

多了一次操作是因为服务端会把一些数据最后一次传给客户端。

等待2MSL的意义

1个MSL就是2分钟,2个MSL就是4分钟

  • 第一个MSL:确保四次挥手中,主动关闭的那一方的最后一个ACK最终能达到对端
  • 第二个MSL:确保对端的ACK重传的FIN请求可以到达
  • 确保被关闭的一方能接受到主动关闭一方的最后一个ACK
  • 避免上次TCP连接的数据包影响到下一次的TCP连接

让客户端等待2MSL,是为了保证客户端发出的ACK能够到达客户端。

假设在一个很坏很坏的网络中,最后一个ACK在1个MSL时间没有到达服务端就消失了,服务端会进行重传的操作,如果没有等待2MSL那么客户端直接就关闭了,无法收到服务端的重传消息,导致服务端会一重传。

如果客户端关闭了连接之后,和别的服务端在同一个端口号建立了新的连接,那么旧的请求将会干扰下一次的TCP连接。

为什么需要四次分手而不是三次?

当客户端发送了FIN请求给服务端后,服务端此时可能还剩余其他报文需要发给客户端,因此先发送ACK表示已经收到客户端的FIN,等全部的报文都发送完毕了之后,在发送FIN

三次握手的话,就是把FINACK的发送合并为一次挥手,这个长时间,会导致客户端以为丢包了,从而让客户端不断地重发FIN

半连接队列和SYN Flood攻击的关系

半连接队列:在三次握手前,服务端的状态从CLOSED变为LISTEN,同时在内部创建了两个队列,半连接队列全连接队列,即SYN队列ACCEPT队列

半连接队列

当客户端发送SYN请求给服务端,服务端收到以后回复ACKSYN,状态由LISTEN变为SYN_RCVD,此时这个连接就被推入了SYN队列(半连接队列

一来一回就是半连接队列

全连接队列

当客户端回复ACK,服务端接受之后,三次握手完成了,它会被推入另外一个TCP维护的队列,也就是Accpet队列(全连接队列

完成了三次握手,就是全连接队列

SYN Flood攻击原理

当服务端接受到SYN请求后,就会为该请求分配一个TCB,通常一个TCB至少需要280个字节,在某些操作系统中TCB甚至需要1300个字节。

因此SYN请求攻击的根本原理,就是让服务端创建TCB从而消耗资源

SYN Flood属于典型的Dos/DDos攻击,原理是,通过短时间伪造大量不存在的IP地址,并向服务端疯狂发送SYN

  • 首先这些请求会塞满半连接队列,导致无法处理正常的请求
  • 由于是不存在的IP,那么自然无法收到他们回应的ACK,服务端就会不断地重发,浪费资源。

预防手段

  1. 增加SYN连接,也就是增加半连接队列的容量
  2. 减少SYN+ACK重试次数,避免大量的超时重发
  3. 延缓TCB分配,具体技术:SYN Cookie的技术或者是SYN Cache

SYN Cookie

原理跟HTTP Cookies技术类似,服务端通过特定的办法把半开连接信息编码成Cookie,用作B给A的消息编号,随着SYN-ACK消息一同返回给连接发起方,这样在连接完全建立前不保存任何信息。

  • 如果A是正常用户,则会向B发送最后一个握手信息,B收到后,验证Cookie的内容并建立连接
  • 如果A是攻击者,则不会向B发送ACK消息,B也没损失,也就是说单纯的SYN攻击不会给B造成连接资源的消耗。

但是这种技术的缺点也是非常明显。B不保存连接的半开状态,意味着丧失了重发SYN-ACK消息的能力,一方面会降低正常用户的连接成功率。并且额外多出编码Cookie这一环节也会消耗一定的资源。

SYN Cache

这种即使在收到SYN数据报文的时候,不急着去分配TCB。

构造一个全局的Hash Table,用来缓存当前系统所有的半开连接信息,连接成功则从Cache中清除相关的信息。

Hash Table中每一个桶(buket)的容量大小也有限制,当桶满的时候,会把旧的数据清除。类似一个LRU机制

当服务端收到SYN请求后,会将半开连接信息加入到HashTable。最关键就是里面的key,这个key要包含SYN的消息中的部分信息,比如IP、port等。并且还会再做一步加密的操作。

无论是正常的SYN请求还是攻击者的SYN请求都会加入到这个HashTable中。

TCP快速打开的原理(TFO)

TCP Fast Open 就是 TFO,用的是SYN Cookie的技术

流程:

首轮三次握手

首先客户端发送SYN给服务端,服务端收到。

但这次并不是回复SYN + ACK,而是通过计算得到一个SYN Cookie,将这个Cookie放到TCP报文的Fast Open选项中。

客户端拿到这个Cookie的值缓存下来,用来后面的握手。

后面的三次握手:

客户端将之前缓存的CookieSYNHTTP请求(重点)发送给服务端,服务端验证了Cookie的合法性之后就返回正常的SYN + ACK

可以看出,采用了TFO之后,在未三次握手成功也可以发送HTTP请求,这个是最显著的改变。

优势:

服务端在拿到客户端的Cookie等信息之后,进行合法校验并通过后,可以直接返回HTTP响应,充分利用了1个RTT(Round-Trip-Time,往返时延)的时间提前进行数据传输,积累起来是一个比较大的优势。

TCP超时重传的时间如何计算

经典方法-----SRTT(Smoothed round trip time,平滑往返时间)

每产生一次新的RTT,就根据一定的算法对SRTT进行更新。

局限性:

在RTT稳定的地方表现还可以,而在RTT变化较大的地方,就不行。

标准方法也叫jacobson/karels算法

在SRTT基础上,引入了个新的变量RTTVAR,记录了最新的RTT与当前SRTT之间的差值,让系统能更敏锐的感知RTT的变化,更为简单地说就是提高了RTT的权重。

TCP的流量控制

对于发送端和接收端而言,TCP需要把发送的数据放到发送缓冲区,将接受的数据放到接受缓存区

而流量控制要做的事情,就是通过接收缓存区的大小,控制发送端的发送。如果对方的接收缓存区满了,就不能继续发送了。

TCP滑动窗口

TCP滑动窗口分为两种:发送窗口接收窗口

image-20211203161815784

发送窗口

image-20211203162416704

SND:Sent

WND:window.size

UNA:unackKnowledge

NXT:next

发送窗口 = 已发送但未确认 + 未发送且未确认

接收窗口

image-20211203163037187

流量控制过程

如果发送方发的速度很快,那么就会产生数据堆积,或者是接收方来不及接受。所谓流量控制就是让发送方的发送速率可以动态地控制,让接收方来得及接受。

接收方会通过发送ACK请求来告诉发送方,接收窗口rwnd的大小是多少,从而控制发送的速率。

当接收窗口大小为0的时候,发送方接收到此消息,就会停止发送数据

过一段时候后,发送方会发送一个TCP Keep Alive的报文,也就是窗口大小探测报文,去问问接收方的窗口大小,是否可以继续发送数据

滑动窗口面向流的可靠性

  1. 最基本的传输可靠性来源于确认重传
  2. 发送窗口只有收到对端对于本段发送窗口内字节的ACK确认,才会移动发送窗口的左边界
  3. 接收窗口只有在前面所有的段都确认的情况下,才会移动左边界

TCP的拥塞控制

流量控制是发送端和接收端之间,是端与端之间的关系,并没有考虑整个网络环境,如果网络特别差,特别容易丢包,那么发送端就要注意。拥塞控制就是负责解决此类问题。

  • 拥塞窗口(Congestion Window,cwnd
  • 慢启动阈值(Slow Start Threshold,ssthresh)

经典算法:慢启动拥塞避免快速重传和快速恢复

拥塞窗口

拥塞窗口(cwnd)是指目前自己还能传输的数据量大小

发送窗口的大小 = Min ( 拥塞窗口,接收窗口 )

拥塞控制,就是控制拥塞窗口(cwnd)的大小

慢启动

刚准备进入传输数据的时候,是不知道网路的堵塞情况,如果一下子传送的数据超出了负载,那么就会产生很严重的丢包从而发生雪崩式的网络灾难

所以拥塞控制采用一种保守的算法,用来慢慢适应整个网络,有一种预热的感觉。这就叫做慢启动

  1. 首先,三次握手,双方宣告自己的接收窗口(rwnd)大小
  2. 双方初始化自己的拥塞窗口(cwnd)
  3. 在开始传输的一段时间,发送端每接受到一个ACK,拥塞窗口大小加1,也就是说,每经过一个RTT,cwnd都会翻倍。(每发送一个报文,都会有一个ACK确认,因此就会翻倍)

当然他也不能无止境地翻倍下去,他有一个阈值,叫做慢启动阈值。当cwnd达到这个阈值之后,涨幅就会变得很慢。而这部分的工作就叫做拥塞避免

拥塞避免

原本是每接受到一个ACK,cwnd就会加1。

现在达到了阈值,cwnd只能加1 /cwnd

因此一个RTT下来后,cwnd只会增加1,并不会翻倍增加。

快速重传

image-20211203201636313

RTO(Retransmission TimeOut):超时重传间隔

在TCP传输的过程中,如果发生了丢包,即接收端发现数据段不是按序到达的时候,接收端的处理是重复发送之前的ACK

比如第5个包丢了,即使第6,7个包到达接收端,也是返回给发送端第四个包的ACK。

当发送端接收到3个重复的ACK,意识到丢包了,于是马上重传第5个包,不用等一个RTO的时间再重传。

这就是快速重传,解决是否需要重传的问题。那么TCP会重传第5个包还是第5个包之后的所有包呢?这就是选择性重传需要解决的

选择性重传

TCP只需要重传丢包的那一个包即可

在报文首部的可选项中,加上SACK这个属性,通过left edge 和right edge告诉发送端已经接收到了哪些区间的数据包。这个过程就叫做选择性重传,解决的是如何重传的问题

快速恢复

发送端收到接收端3个ack之后,发现丢包,察觉到现在网络并不是很通畅,自己就会进入快速恢复阶段

在这个阶段,发送端会做出以下改变:

  1. 拥塞阈值降低为cwnd的一半
  2. cwnd的大小变为拥塞阈值
  3. cwnd线性增加

Nagle算法

延迟发包

试想,发送端不断地发送很小很小的包。这种频繁的发送时存在问题的,不光时传输的时延消耗,发送和确认本身也是需要耗时的。

而避免小包的频繁发送,将多个小包打包成大包,就是Nagle算法要做的事情

Nagle算法的基本定义:

一个TCP连接中,最多只能拥有一个未被确认的小段。所谓小段,指的是小于MSS尺寸的数据块,

并且该数据块没有收到对方发送的ACK确认该数据已收到

Nagle规则:

  1. 如果包长度达到MSS,则允许直接发送
  2. 如果该包含有FIN,则允许直接发送
  3. 发生超时,也立即发送

MSS(Max Segment Size):最大段

延迟确认

延迟ACK

接收方在接受到发送方的数据后之后,在很短的时间里,又接收到了第二个包。

那么接收方不会一个一个去回复,而是回复最近的那个数据包。相当于一起回复了。

注意:

并不是所有情况都可以延迟确认

  • 出现乱序的包
  • 需要调整滑动窗口
  • 大于MSS的包

一般情况下,Nagle算法和延迟确认算法是不会一起使用的。

Nagle算法是延迟发

延迟确认算法是延迟收

这一来一回的,就会造成很大的延迟,本末倒置了,并不是我们想要的。他们是相悖的,相当于拔河的两端。

TCP粘包和拆包

image-20211204160636803

TCP中MSS是发送报文段最大的长度,但是程序需要发送的数据大小可能会比这个值小很多,也可能比这个值大很多。

因此为了解决小包的频繁传送,TCP会将若干个小包打包成一个大包进行传送。

也会将超出MSS的大包,拆分成多个小包进行传送

在IP协议层、链路层、物理层都存在拆包、粘包

TCP粘包和拆包的解决办法

  1. 发送端给每个数据包添加包首部,首部中应该告诉接受端数据包的长度,这样接收端在接受到数据包之后,通过读取该字段,即可知道数据包的实际长度(解决了拆包的问题)
  2. 固定每个数据包的长度。不足的补特殊符号,比如换行符或者是空格之类的。(解决了粘包的问题)
  3. 添加特殊字符进行分割。