- 正文
- TCP协议面试10连问
- 001、 能不能说一说 TCP 和 UDP 的区别?
- 002: 说说 TCP 三次握手的过程?为什么是三次而不是两次、四次?
- 003: 说说 TCP 四次挥手的过程
- 004: 说说半连接队列和 SYN Flood 攻击的关系
- 005: 介绍一下 TCP 报文头部的字段
- 006: 说说 TCP 快速打开的原理(TFO)
- 007: 能不能说说TCP报文中时间戳的作用?
- 008: TCP 的超时重传时间是如何计算的?
- 009: 能不能说一说 TCP 的流量控制?
- 010: 能不能说说 TCP 的拥塞控制?
- 011: 能不能说说 Nagle 算法和延迟确认?
- 012. 如何理解 TCP 的 keep-alive?
- 24张图 图解 TCP
- 40 张图带你搞懂 TCP 和 UDP
- 视频
- 参考资料
正文
你是一台电脑,你的名字叫 A
经过《图解 | 原来这就是网络》这篇文章中的一番折腾,只要你知道另一位伙伴 B 的 IP 地址,且你们之间的网络是通的,无论多远, 你都可以将一个数据包发送给你的伙伴 B
这就是物理层、数据链路层、网络层这三层所做的事情。站在第四层的你,就可以不要脸地利用下三层所做的铺垫, 随心所欲地发送数据,而不必担心找不到对方了。
虽然你此时还什么都没干,但你还是给自己这一层起了个响亮的名字,叫做传输层。你本以为自己所在的第四层万事大吉, 啥事没有,但很快问题就接踵而至。
问题来了
前三层协议只能把数据包从一个主机搬到另外一台主机,但是,到了目的地以后,数据包具体交给哪个程序(进程)呢?
所以,你需要把通信的进程区分开来,于是就给每个进程分配一个数字编号,你给它起了一个响亮的名字:端口号。
然后你在要发送的数据包上,增加了传输层的头部,源端口号与目标端口号。
OK,这样你将原本主机到主机的通信,升级为了进程和进程之间的通信。你没有意识到,你不知不觉实现了 UDP 协议! (当然 UDP 协议中不光有源端口和目标端口,还有数据包长度和校验值,我们暂且略过)就这样, 你用 UDP 协议无忧无虑地同 B 进行着通信,一直没发生什么问题。
但很快,你发现事情变得非常复杂……
丢包问题
由于网络的不可靠,数据包可能在半路丢失,而 A 和 B 却无法察觉。
对于丢包问题,只要解决两个事就好了。第一个,A 怎么知道包丢了?答案:让 B 告诉 A。第二个,丢了的包怎么办? 答案:重传,于是你设计了如下方案,A 每发一个包,都必须收到来自 B 的确认(ACK),再发下一个,否则在一定时间内没有收到确认,就重传这个包。
你管它叫停止等待协议。只要按照这个协议来,虽然 A 无法保证 B 一定能收到包,但 A 能够确认 B 是否收到了包, 收不到就重试,尽最大努力让这个通信过程变得可靠,于是你们现在的通信过程又有了一个新的特征,可靠交付。
效率问题
停止等待虽然能解决问题,但是效率太低了,A 原本可以在发完第一个数据包之后立刻开始发第二个数据包, 但由于停止等待协议,A 必须等数据包到达了 B ,且 B 的 ACK 包又回到了 A,才可以继续发第二个数据包, 这效率慢得可不是一点两点。于是你对这个过程进行了改进,采用流水线的方式,不再傻傻地等。
顺序问题
但是网路是复杂的、不可靠的。有的时候 A 发出去的数据包,分别走了不同的路由到达 B,可能无法保证和发送数据包时一样的顺序。
在流水线中有多个数据包和ACK包在乱序流动,他们之间对应关系就乱掉了。 难道还回到停止等待协议?A 每收到一个包的确认(ACK)再发下一个包,那就根本不存在顺序问题。 应该有更好的办法!A 在发送的数据包中增加一个序号(seq),同时 B 要在 ACK 包上增加一个确认号(ack), 这样不但解决了停止等待协议的效率问题,也通过这样标序号的方式解决了顺序问题。
而 B 这个确认号意味深长:比如 B 发了一个确认号为 ack = 3,它不仅仅表示 A 发送的序号为 2 的包收到了, 还表示 2 之前的数据包都收到了。这种方式叫累计确认或累计应答。
注意,实际上 ack 的号是收到的最后一个数据包的序号 seq + 1,也就是告诉对方下一个应该发的序号是多少。 但图中为了便于理解,ack 就表示收到的那个序号,不必纠结。
流量问题
有的时候,A 发送数据包的速度太快,而 B 的接收能力不够,但 B 却没有告知 A 这个情况。
怎么解决呢?很简单,B 告诉 A 自己的接收能力,A 根据 B 的接收能力,相应控制自己的发送速率,就好了。 B 怎么告诉 A 呢?B 跟 A 说”我很强”这三个字么?那肯定不行,得有一个严谨的规范。 于是 B 决定,每次发送数据包给 A 时,顺带传过来一个值,叫窗口大小(win),这个值就表示 B 的接收能力。 同理,每次 A 给 B 发包时也带上自己的窗口大小,表示 A 的接收能力。
B 告诉了 A 自己的窗口大小值,A 怎么利用它去做 A 这边发包的流量控制呢? 很简单,假如 B 给 A 传过来的窗口大小 win = 5,那 A 根据这个值,把自己要发送的数据分成这么几类。
图片过于清晰,就不再文字解释了。当 A 不断发送数据包时,已发送的最后一个序号就往右移动,直到碰到了窗口的上边界, 此时 A 就无法继续发包,达到了流量控制。
但是当 A 不断发包的同时,A 也会收到来自 B 的确认包,此时整个窗口会往右移动,因此上边界也往右移动,A 就能发更多的数据包了。
以上都是在窗口大小不变的情况下,而 B 在发给 A 的 ACK 包中,每一个都可以重新设置一个新的窗口大小, 如果 A 收到了一个新的窗口大小值,A 会随之调整。如果 A 收到了比原窗口值更大的窗口大小, 比如 win = 6,则 A 会直接将窗口上边界向右移动 1 个单位。
如果 A 收到了比原窗口值小的窗口大小,比如 win = 4,则 A 暂时不会改变窗口大小,更不会将窗口上边界向左移动, 而是等着 ACK 的到来,不断将左边界向右移动,直到窗口大小值收缩到新大小为止。
OK,终于将流量控制问题解决得差不多了,你看着上面一个个小动图,给这个窗口起了一个更生动的名字,滑动窗口。
拥塞问题
但有的时候,不是 B 的接受能力不够,而是网络不太好,造成了网络拥塞。
拥塞控制与流量控制有些像,但流量控制是受 B 的接收能力影响,而拥塞控制是受网络环境的影响。 拥塞控制的解决办法依然是通过设置一定的窗口大小,只不过,流量控制的窗口大小是 B 直接告诉 A 的, 而拥塞控制的窗口大小按理说就应该是网络环境主动告诉 A。但网络环境怎么可能主动告诉 A 呢? 只能 A 单方面通过试探,不断感知网络环境的好坏,进而确定自己的拥塞窗口的大小。
拥塞窗口大小的计算有很多复杂的算法,就不在本文中展开了,假如拥塞窗口的大小为 cwnd, 上一部分流量控制的滑动窗口的大小为 rwnd,那么窗口的右边界受这两个值共同的影响,需要取它俩的最小值。 窗口大小 = min(cwnd, rwnd)含义很容易理解,当 B 的接受能力比较差时,即使网络非常通畅, A 也需要根据 B 的接收能力限制自己的发送窗口。当网络环境比较差时,即使 B 有很强的接收能力, A 也要根据网络的拥塞情况来限制自己的发送窗口。正所谓受其短板的影响嘛~
连接问题
有的时候,B 主机的相应进程还没有准备好或是挂掉了,A 就开始发送数据包,导致了浪费。
这个问题在于,A 在跟 B 通信之前,没有事先确认 B 是否已经准备好,就开始发了一连串的信息。就好比你和另一个人打电话, 你还没有”喂”一下确认对方有没有在听,你就巴拉巴拉说了一堆。这个问题该怎么解决呢?地球人都知道,三次握手嘛!
A:我准备好了(SYN)
B:我知道了(ACK),我也准备好了(SYN)
A:我知道了(ACK)
A 与 B 各自在内存中维护着自己的状态变量,三次握手之后,双方的状态都变成了连接已建立(ESTABLISHED)。虽然就只是发了三次数据包,并且在各自的内存中维护了状态变量,但这么说总觉得太 low,你看这个过程相当于双方建立连接的过程,于是你灵机一动,就叫它面向连接吧。注意:这个连接是虚拟的,是由 A 和 B 这两个终端共同维护的,在网络中的设备根本就不知道连接这回事儿!但凡事有始就有终,有了建立连接的过程,就要考虑释放连接的过程,又是地球人都知道,四次挥手嘛!
A:再见,我要关闭了(FIN)
B:我知道了(ACK)
给 B 一段时间把自己的事情处理完…
B:再见,我要关闭了(FIN)
A:我知道了(ACK)
总结
以上讲述的,就是 TCP 协议的核心思想,上面过程中需要传输的信息,就体现在 TCP 协议的头部,这里放上最常见的 TCP 协议头解读的图。
不知道你现在再看下面这句话,是否能理解:
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
面向连接、可靠,这两个词通过上面的讲述很容易理解,那什么叫做基于字节流呢?很简单,TCP 在建立连接时,需要告诉对方 MSS(最大报文段大小)。 也就是说,如果要发送的数据很大,在 TCP 层是需要按照 MSS 来切割成一个个的 TCP 报文段 的。切割的时候我才不管你原来的数据表示什么意思, 需要在哪里断句啥的,我就把它当成一串毫无意义的字节,在我想要切割的地方咔嚓就来一刀,标上序号, 只要接收方再根据这个序号拼成最终想要的完整数据就行了。在我 TCP 传输这里,我就把它当做一个个的字节,也就是基于字节流的含义了。
最后留给大家一个作业,模拟 A 与 B 建立一个 TCP 连接。
第一题:A 给 B 发送 “aaa” ,然后 B 给 A 回复一个简单的字符串 “success”,并将此过程抓包。
第二题:A 给 B 发送 “aaaaaa … a” 超过最大报文段大小,然后 B 给 A 回复一个简单的字符串 “success”,并将此过程抓包。
下面是我抓的包(第二题)
三次握手阶段
A -> B [SYN] Seq=0 Win=64240 Len=0
MSS=1460 WS=256
B - >A [SYN, ACK] Seq=0 Ack=1 Win=29200 Len=0
MSS=1424 WS=512
A -> B [ACK] Seq=1 Ack=1 Win=132352 Len=0
数据发送阶段
A -> B [ACK] Seq=1 Ack=1 Win=132352 Len=1424
A -> B [ACK] Seq=1425 Ack=1 Win=132352 Len=1424
A -> B [PSH, ACK] Seq=2849 Ack=1 Win=132352 Len=1247
B -> A [ACK] Seq=1 Ack=1425 Win=32256 Len=0
B -> A [ACK] Seq=1 Ack=2849 Win=35328 Len=0
B -> A [ACK] Seq=1 Ack=4096 Win=37888 Len=0
B -> A [PSH, ACK] Seq=1 Ack=4096 Win=37888 Len=7
四次挥手阶段
B -> A [FIN, ACK] Seq=8 Ack=4096 Win=37888 Len=0
A -> B [ACK] Seq=4096 Ack=9 Win=132352 Len=0
A -> B [FIN, ACK] Seq=4096 Ack=9 Win=132352 Len=0
A -> B [ACK]
详细的抓包数据与分析整理,就不在文章里展示了。
一提到 TCP,可能很多人都想起被三次握手和四次挥手所支配的恐惧。但其实你跟着文中的思路你就会发现, 三次握手与四次挥手只占 TCP 所解决的核心问题中很小的一部分,只是因为它在面试中很适合作为知识点进行考察, 所以在很多人的印象中就好像 TCP 的核心就是握手和挥手似的。 本文希望你能从问题出发,真正理解 TCP 所想要解决的问题,你会发现很多原理就好像生活常识一样顺其自然,并不复杂,希望你有收获~
TCP协议面试10连问
001、 能不能说一说 TCP 和 UDP 的区别?
首先概括一下基本的区别:
TCP是一个面向连接的、可靠的、基于字节流的传输层协议。
而UDP是一个面向无连接的传输层协议。(就这么简单,其它TCP的特性也就没有了)。
具体来分析,和 UDP 相比,TCP 有三大核心特性:
1、 面向连接。所谓的连接,指的是客户端和服务器的连接,在双方互相通信之前,TCP 需要三次握手建立连接,而 UDP 没有相应建立连接的过程。
2、 可靠性。TCP 花了非常多的功夫保证连接的可靠,这个可靠性体现在哪些方面呢?一个是有状态,另一个是可控制。
TCP 会精准记录哪些数据发送了,哪些数据被对方接收了,哪些没有被接收到,而且保证数据包按序到达,不允许半点差错。这是有状态。
当意识到丢包了或者网络环境不佳,TCP 会根据具体情况调整自己的行为,控制自己的发送速度或者重发。这是可控制。
相应的,UDP 就是无状态, 不可控的。
3、 面向字节流。UDP 的数据传输是基于数据报的,这是因为仅仅只是继承了 IP 层的特性,而 TCP 为了维护状态,将一个个 IP 包变成了字节流。
002: 说说 TCP 三次握手的过程?为什么是三次而不是两次、四次?
恋爱模拟
以谈恋爱为例,两个人能够在一起最重要的事情是首先确认各自爱和被爱的能力。接下来我们以此来模拟三次握手的过程。
第一次:
男: 我爱你。
女方收到。
由此证明男方拥有爱的能力。
第二次:
女: 我收到了你的爱,我也爱你。
男方收到。
OK,现在的情况说明,女方拥有爱和被爱的能力。
第三次:
男: 我收到了你的爱。
女方收到。
现在能够保证男方具备被爱的能力。
由此完整地确认了双方爱和被爱的能力,两人开始一段甜蜜的爱情。
真实握手
当然刚刚那段属于扯淡,不代表本人价值观,目的是让大家理解整个握手过程的意义,因为两个过程非常相似。 对应到 TCP 的三次握手,也是需要确认双方的两样能力: 发送的能力和接收的能力。于是便会有下面的三次握手的过程:
从最开始双方都处于CLOSED状态。然后服务端开始监听某个端口,进入了LISTEN状态。
然后客户端主动发起连接,发送 SYN , 自己变成了SYN-SENT状态。
服务端接收到,返回SYN和ACK(对应客户端发来的SYN),自己变成了SYN-REVD。
之后客户端再发送ACK给服务端,自己变成了ESTABLISHED状态;服务端收到ACK之后,也变成了ESTABLISHED状态。
另外需要提醒你注意的是,从图中可以看出,SYN 是需要消耗一个序列号的,下次发送对应的 ACK 序列号要加1,为什么呢?只需要记住一个规则:
- 凡是需要对端确认的,一定消耗TCP报文的序列号。
SYN 需要对端的确认, 而 ACK 并不需要,因此 SYN 消耗一个序列号而 ACK 不需要。
为什么不是两次?
根本原因: 无法确认客户端的接收能力。
分析如下:
如果是两次,你现在发了 SYN 报文想握手,但是这个包滞留在了当前的网络中迟迟没有到达,TCP 以为这是丢了包,于是重传,两次握手建立好了连接。
看似没有问题,但是连接关闭后,如果这个滞留在网路中的包到达了服务端呢?这时候由于是两次握手, 服务端只要接收到然后发送相应的数据包,就默认建立连接,但是现在客户端已经断开了。
看到问题的吧,这就带来了连接资源的浪费。
为什么不是四次?
三次握手的目的是确认双方发送和接收的能力,那四次握手可以嘛?
当然可以,100 次都可以。但为了解决问题,三次就足够了,再多用处就不大了。 三次握手过程中可以携带数据么?
第三次握手的时候,可以携带。前两次握手不能携带数据。
如果前两次握手能够携带数据,那么一旦有人想攻击服务器,那么他只需要在第一次握手中的 SYN 报文中放大量数据, 那么服务器势必会消耗更多的时间和内存空间去处理这些数据,增大了服务器被攻击的风险。
第三次握手的时候,客户端已经处于ESTABLISHED状态,并且已经能够确认服务器的接收、发送能力正常,这个时候相对安全了,可以携带数据。
同时打开会怎样?
如果双方同时发 SYN报文,状态变化会是怎样的呢?
这是一个可能会发生的情况。
状态变迁如下:
在发送方给接收方发SYN报文的同时,接收方也给发送方发SYN报文,两个人刚上了!
发完SYN,两者的状态都变为SYN-SENT。
在各自收到对方的SYN后,两者状态都变为SYN-REVD。
接着会回复对应的ACK + SYN,这个报文在对方接收之后,两者状态一起变为ESTABLISHED。
这就是同时打开情况下的状态变迁。
003: 说说 TCP 四次挥手的过程
过程拆解
刚开始双方处于ESTABLISHED状态。
客户端要断开了,向服务器发送 FIN 报文,在 TCP 报文中的位置如下图:
发送后客户端变成了FIN-WAIT-1状态。注意, 这时候客户端同时也变成了half-close(半关闭)状态,即无法向服务端发送报文,只能接收。
服务端接收后向客户端确认,变成了CLOSED-WAIT状态。
客户端接收到了服务端的确认,变成了FIN-WAIT2状态。
随后,服务端向客户端发送FIN,自己进入LAST-ACK状态,
客户端收到服务端发来的FIN后,自己变成了TIME-WAIT状态,然后发送 ACK 给服务端。
注意了,这个时候,客户端需要等待足够长的时间,具体来说,是 2 个 MSL(Maximum Segment Lifetime,报文最大生存时间), 在这段时间内如果客户端没有收到服务端的重发请求,那么表示 ACK 成功到达,挥手结束,否则客户端重发 ACK。
等待2MSL的意义
如果不等待会怎样?
如果不等待,客户端直接跑路,当服务端还有很多数据包要给客户端发,且还在路上的时候,若客户端的端口此时刚好被新的应用占用, 那么就接收到了无用数据包,造成数据包混乱。所以,最保险的做法是等服务器发来的数据包都死翘翘再启动新的应用。
那,照这样说一个 MSL 不就不够了吗,为什么要等待 2 MSL?
- 1 个 MSL 确保四次挥手中主动关闭方最后的 ACK 报文最终能达到对端
- 1 个 MSL 确保对端没有收到 ACK 重传的 FIN 报文可以到达
这就是等待 2MSL 的意义。
为什么是四次挥手而不是三次?
因为服务端在接收到FIN, 往往不会立即返回FIN, 必须等到服务端所有的报文都发送完毕了,才能发FIN。 因此先发一个ACK表示已经收到客户端的FIN,延迟一段时间才发FIN。这就造成了四次挥手。
如果是三次挥手会有什么问题?
等于说服务端将ACK和FIN的发送合并为一次挥手,这个时候长时间的延迟可能会导致客户端误以为FIN没有到达客户端,从而让客户端不断的重发FIN。
同时关闭会怎样?
如果客户端和服务端同时发送 FIN ,状态会如何变化?如图所示:
004: 说说半连接队列和 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 合法之后才分配连接资源。
005: 介绍一下 TCP 报文头部的字段
报文头部结构如下(单位为字节):
请大家牢记这张图!
源端口、目标端口
如何唯一标识一个连接?答案是 TCP 连接的四元组——源 IP、源端口、目标 IP 和目标端口。
那 TCP 报文怎么没有源 IP 和目标 IP 呢?这是因为在 IP 层就已经处理了 IP 。TCP 只需要记录两者的端口即可。
序列号
即Sequence number, 指的是本报文段第一个字节的序列号。
从图中可以看出,序列号是一个长为 4 个字节,也就是 32 位的无符号整数,表示范围为 0 ~ 2^32 - 1。如果到达最大值了后就循环到0。
序列号在 TCP 通信的过程中有两个作用:
1、在 SYN 报文中交换彼此的初始序列号。
2、保证数据包按正确的顺序组装。
ISN
即Initial Sequence Number(初始序列号),在三次握手的过程当中,双方会用SYN报文来交换彼此的 ISN。
ISN 并不是一个固定的值,而是每 4 ms 加一,溢出则回到 0,这个算法使得猜测 ISN 变得很困难。那为什么要这么做?
如果 ISN 被攻击者预测到,要知道源 IP 和源端口号都是很容易伪造的,当攻击者猜测 ISN 之后,直接伪造一个 RST 后,就可以强制连接关闭的,这是非常危险的。
而动态增长的 ISN 大大提高了猜测 ISN 的难度。
确认号
即ACK(Acknowledgment number)。用来告知对方下一个期望接收的序列号,小于ACK的所有字节已经全部收到。
标记位
常见的标记位有SYN,ACK,FIN,RST,PSH。
SYN 和 ACK 已经在上文说过,后三个解释如下: FIN:即 Finish,表示发送方准备断开连接。
RST:即 Reset,用来强制断开连接。
PSH:即 Push, 告知对方这些数据包收到后应该马上交给上层的应用,不能缓存。
窗口大小
占用两个字节,也就是 16 位,但实际上是不够用的。因此 TCP 引入了窗口缩放的选项,作为窗口缩放的比例因子, 这个比例因子的范围在 0 ~ 14,比例因子可以将窗口的值扩大为原来的 2 ^ n 次方。
校验和
占用两个字节,防止传输过程中数据包有损坏,如果遇到校验和有差错的报文,TCP 直接丢弃之,等待重传。
可选项
可选项的格式如下:
常用的可选项有以下几个:
TimeStamp: TCP 时间戳,后面详细介绍。
MSS: 指的是 TCP 允许的从对方接收的最大报文段。
SACK: 选择确认选项。
Window Scale:窗口缩放选项。
006: 说说 TCP 快速打开的原理(TFO)
第一节讲了 TCP 三次握手,可能有人会说,每次都三次握手好麻烦呀!能不能优化一点?
可以啊。今天来说说这个优化后的 TCP 握手流程,也就是 TCP 快速打开(TCP Fast Open, 即TFO)的原理。
优化的过程是这样的,还记得我们说 SYN Flood 攻击时提到的 SYN Cookie 吗?这个 Cookie 可不是浏览器的Cookie, 用它同样可以实现 TFO。
TFO 流程
首轮三次握手
首先客户端发送SYN给服务端,服务端接收到。
注意哦!现在服务端不是立刻回复 SYN + ACK,而是通过计算得到一个SYN Cookie, 将这个Cookie放到 TCP 报文的 Fast Open选项中,然后才给客户端返回。
客户端拿到这个 Cookie 的值缓存下来。后面正常完成三次握手。
首轮三次握手就是这样的流程。而后面的三次握手就不一样啦!
后面的三次握手
在后面的三次握手中,客户端会将之前缓存的 Cookie、SYN 和HTTP请求(是的,你没看错)发送给服务端,服务端验证了 Cookie 的合法性, 如果不合法直接丢弃;如果是合法的,那么就正常返回SYN + ACK。
重点来了,现在服务端能向客户端发 HTTP 响应了!这是最显著的改变,三次握手还没建立,仅仅验证了 Cookie 的合法性,就可以返回 HTTP 响应了。
当然,客户端的ACK还得正常传过来,不然怎么叫三次握手嘛。
流程如下:
注意: 客户端最后握手的 ACK 不一定要等到服务端的 HTTP 响应到达才发送,两个过程没有任何关系。
TFO 的优势
TFO 的优势并不在与首轮三次握手,而在于后面的握手,在拿到客户端的 Cookie 并验证通过以后,可以直接返回 HTTP 响应, 充分利用了1 个RTT(Round-Trip Time,往返时延)的时间提前进行数据传输,积累起来还是一个比较大的优势。
007: 能不能说说TCP报文中时间戳的作用?
timestamp是 TCP 报文首部的一个可选项,一共占 10 个字节,格式如下:
kind(1 字节) + length(1 字节) + info(8 个字节)
其中 kind = 8, length = 10, info 有两部分构成: timestamp和timestamp echo,各占 4 个字节。
那么这些字段都是干嘛的呢?它们用来解决那些问题?
接下来我们就来一一梳理,TCP 的时间戳主要解决两大问题:
计算往返时延 RTT(Round-Trip Time)
防止序列号的回绕问题
计算往返时延 RTT
在没有时间戳的时候,计算 RTT 会遇到的问题如下图所示:
如果以第一次发包为开始时间的话,就会出现左图的问题,RTT 明显偏大,开始时间应该采用第二次的;
如果以第二次发包为开始时间的话,就会导致右图的问题,RTT 明显偏小,开始时间应该采用第一次发包的。
实际上无论开始时间以第一次发包还是第二次发包为准,都是不准确的。
那这个时候引入时间戳就很好的解决了这个问题。
比如现在 a 向 b 发送一个报文 s1,b 向 a 回复一个含 ACK 的报文 s2 那么:
step 1: a 向 b 发送的时候,timestamp 中存放的内容就是 a 主机发送时的内核时刻 ta1。
step 2: b 向 a 回复 s2 报文的时候,timestamp 中存放的是 b 主机的时刻 tb, timestamp echo字段为从 s1 报文中解析出来的 ta1。
step 3: a 收到 b 的 s2 报文之后,此时 a 主机的内核时刻是 ta2, 而在 s2 报文中的 timestamp echo 选项中可以得到 ta1, 也就是 s2 对应的报文最初的发送时刻。然后直接采用 ta2 - ta1 就得到了 RTT 的值。
防止序列号回绕问题
现在我们来模拟一下这个问题。
序列号的范围其实是在0 ~ 2 ^ 32 - 1, 为了方便演示,我们缩小一下这个区间,假设范围是 0 ~ 4,那么到达 4 的时候会回到 0。
第几次发包 | 发送字节 | 对应序列号 | 状态 |
---|---|---|---|
1 | 0 ~ 1 | 0 ~ 1 | 成功接收 |
2 | 1 ~ 2 | 1 ~ 2 | 滞留在网络中 |
3 | 2 ~ 3 | 2 ~ 3 | 成功接收 |
4 | 3 ~ 4 | 3 ~ 4 | 成功接收 |
5 | 4 ~ 5 | 0 ~ 1 | 成功接收,序列号从0开始 |
6 | 5 ~ 6 | 1 ~ 2 | ??? |
假设在第 6 次的时候,之前还滞留在网路中的包回来了,那么就有两个序列号为1 ~ 2的数据包了,怎么区分谁是谁呢?这个时候就产生了序列号回绕的问题。
那么用 timestamp 就能很好地解决这个问题,因为每次发包的时候都是将发包机器当时的内核时间记录在报文中, 那么两次发包序列号即使相同,时间戳也不可能相同,这样就能够区分开两个数据包了。
008: TCP 的超时重传时间是如何计算的?
TCP 具有超时重传机制,即间隔一段时间没有等到数据包的回复时,重传这个数据包。
那么这个重传间隔是如何来计算的呢?
今天我们就来讨论一下这个问题。
这个重传间隔也叫做超时重传时间(Retransmission TimeOut, 简称RTO),它的计算跟上一节提到的 RTT 密切相关。 这里我们将介绍两种主要的方法,一个是经典方法,一个是标准方法。
经典方法
经典方法引入了一个新的概念——SRTT(Smoothed round trip time,即平滑往返时间),每产生一次新的 RTT. 就根据一定的算法对 SRTT 进行更新, 具体而言,计算方式如下(SRTT 初始值为0):
SRTT = (α * SRTT) + ((1 - α) * RTT)
其中,α 是平滑因子,建议值是0.8,范围是0.8 ~ 0.9。
拿到 SRTT,我们就可以计算 RTO 的值了:
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:
RTO = µ * SRTT + ∂ * RTTVAR
µ建议值取1, ∂建议值取4。
这个公式在 SRTT 的基础上加上了最新 RTT 与它的偏移,从而很好的感知了 RTT 的变化,这种算法下,RTO 与 RTT 变化的差值关系更加密切。
009: 能不能说一说 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 个字节。
这也就是流量控制的过程。尽管回合再多,整个控制的过程和原理是一样的。
010: 能不能说说 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 到达这个阈值之后,好比踩了下刹车,别涨了那么快了,老铁,先 hold 住!
在到达阈值后,如何来控制 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 个都已经到达了,TCP 的设计者也不傻,已经传过去干嘛还要传?干脆记录一下哪些包到了,哪些没到,针对性地重传。
在收到发送端的报文后,接收端回复一个 ACK 报文,那么在这个报文首部的可选项中,就可以加上SACK这个属性, 通过left edge和right edge告知发送端已经收到了哪些区间的数据报。因此,即使第 5 个包丢包了,当收到第 6、7 个包之后, 接收端依然会告诉发送端,这两个包到了。剩下第 5 个包没到,就重传这个包。这个过程也叫做选择性重传(SACK,Selective Acknowledgment), 它解决的是如何重传的问题。
快速恢复
当然,发送端收到三次重复 ACK 之后,发现丢包,觉得现在的网络已经有些拥塞了,自己会进入快速恢复阶段。
在这个阶段,发送端如下改变:
- 拥塞阈值降低为 cwnd 的一半
- cwnd 的大小变为拥塞阈值
- cwnd 线性增加
以上就是 TCP 拥塞控制的经典算法: 慢启动、拥塞避免、快速重传和快速恢复。
011: 能不能说说 Nagle 算法和延迟确认?
Nagle 算法
试想一个场景,发送端不停地给接收端发很小的包,一次只发 1 个字节,那么发 1 千个字节需要发 1000 次。 这种频繁的发送是存在问题的,不光是传输的时延消耗,发送和确认本身也是需要耗时的,频繁的发送接收带来了巨大的时延。
而避免小包的频繁发送,这就是 Nagle 算法要做的事情。
具体来说,Nagle 算法的规则如下:
- 当第一次发送数据时不用等待,就算是 1byte 的小包也立即发送
- 后面发送满足下面条件之一就可以发了:
- 数据包大小达到最大段大小(Max Segment Size, 即 MSS)
- 之前所有包的 ACK 都已接收到
延迟确认
试想这样一个场景,当我收到了发送端的一个包,然后在极短的时间内又接收到了第二个包,那我是一个个地回复, 还是稍微等一下,把两个包的 ACK 合并后一起回复呢?
延迟确认(delayed ack)所做的事情,就是后者,稍稍延迟,然后合并 ACK,最后才回复给发送端。 TCP 要求这个延迟的时延必须小于500ms,一般操作系统实现都不会超过200ms。
不过需要主要的是,有一些场景是不能延迟确认的,收到了就要马上回复:
- 接收到了大于一个 frame 的报文,且需要调整窗口大小
- TCP 处于 quickack 模式(通过tcp_in_quickack_mode设置)
- 发现了乱序包
两者一起使用会怎样?
前者意味着延迟发,后者意味着延迟接收,会造成更大的延迟,产生性能问题。
012. 如何理解 TCP 的 keep-alive?
大家都听说过 http 的keep-alive, 不过 TCP 层面也是有keep-alive机制,而且跟应用层不太一样。
试想一个场景,当有一方因为网络故障或者宕机导致连接失效,由于 TCP 并不是一个轮询的协议,在下一个数据包到达之前,对端对连接失效的情况是一无所知的。
这个时候就出现了 keep-alive, 它的作用就是探测对端的连接有没有失效。
在 Linux 下,可以这样查看相关的配置:
sudo sysctl -a | grep keepalive
返回:
[root@localhost ~]# sysctl -a | grep keepalive
net.ipv4.tcp_keepalive_intvl = 75 # 每个包的间隔重传间隔 75 s
sysctl: reading key "net.ipv6.conf.all.stable_secret"
net.ipv4.tcp_keepalive_probes = 9 # 一次最多重传 9 个包
net.ipv4.tcp_keepalive_time = 7200 # 每隔 7200 s 检测一次
sysctl: reading key "net.ipv6.conf.default.stable_secret"
sysctl: reading key "net.ipv6.conf.enp0s3.stable_secret"
sysctl: reading key "net.ipv6.conf.enp0s8.stable_secret"
sysctl: reading key "net.ipv6.conf.lo.stable_secret"
[root@localhost ~]#
不过,现状是大部分的应用并没有默认开启 TCP 的keep-alive选项,为什么?
站在应用的角度:
- 7200s 也就是两个小时检测一次,时间太长
- 时间再短一些,也难以体现其设计的初衷, 即检测长时间的死连接
因此是一个比较尴尬的设计。
24张图 图解 TCP
参阅 https://www.cnblogs.com/cxuanBlog/p/14911326.html
TCP 是一种面向连接的单播协议,在 TCP 中,并不存在多播、广播的这种行为,因为 TCP 报文段中能明确发送方和接受方的 IP 地址。
在发送数据前,相互通信的双方(即发送方和接受方)需要建立一条连接,在发送数据后,通信双方需要断开连接,这就是 TCP 连接的建立和终止。
TCP 连接的建立和终止
TCP 半开启
TCP 连接处于半开启的这种状态是因为连接的一方关闭或者终止了这个 TCP 连接却没有通知另一方, 也就是说两个人正在微信聊天,cxuan 你下线了你不告诉我,我还在跟你侃八卦呢。此时就认为这条连接处于半开启状态。 这种情况发生在通信中的一方处于主机崩溃的情况下,你 xxx 的,我电脑死机了我咋告诉你? 只要处于半连接状态的一方不传输数据的话,那么是无法检测出来对方主机已经下线的。
另外一种处于半开启状态的原因是通信的一方关闭了主机电源 而不是正常关机。这种情况下会导致服务器上有很多半开启的 TCP 连接。
TCP 半关闭
既然 TCP 支持半开启操作,那么我们可以设想 TCP 也支持半关闭操作。同样的,TCP 半关闭也并不常见。 TCP 的半关闭操作是指仅仅关闭数据流的一个传输方向。两个半关闭操作合在一起就能够关闭整个连接。 在一般情况下,通信双方会通过应用程序互相发送 FIN 报文段来结束连接,但是在 TCP 半关闭的情况下, 应用程序会表明自己的想法:”我已经完成了数据的发送发送,并发送了一个 FIN 报文段给对方, 但是我依然希望接收来自对方的数据直到它发送一个 FIN 报文段给我”。
解释一下这个过程: 首先客户端主机和服务器主机一直在进行数据传输,一段时间后,客户端发起了 FIN 报文,要求主动断开连接, 服务器收到 FIN 后,回应 ACK ,由于此时发起半关闭的一方也就是客户端仍然希望服务器发送数据, 所以服务器会继续发送数据,一段时间后服务器发送另外一条 FIN 报文,在客户端收到 FIN 报文回应 ACK 给服务器后,断开连接。
TCP 的半关闭操作中,连接的一个方向被关闭,而另一个方向仍在传输数据直到它被关闭为止。只不过很少有应用程序使用这一特性。
TCP 状态转换
TCP 超时和重传
TCP 的重传有两种方式,一种是基于时间,一种是基于确认信息,一般通过确认信息要比通过时间更加高效。
TCP 在发送数据时会设置一个定时器,如果在定时器指定的时间内未收到确认信息, 那么就会触发相应的超时或者基于计时器的重传操作,计时器超时通常被称为重传超时(RTO)。
快速重传
如上图所示,报文段 1 成功接收并被确认为 ACK 2,接收端的期待序号为 2,当报文段 2 丢失后,报文段 3。 失序到达,但是与接收端的期望不匹配,所以接收端会重复发送冗余 ACK 2。
这样,在超时重传定时器到期之前,接收收到连续三个相同的 ACK 后,发送端就知道哪个报文段丢失了, 于是发送方会重发这个丢失的报文段,这样就不用等待重传定时器的到期,大大提高了效率。
SACK
在标准的 TCP 确认机制中,如果发送方发送了 0 - 10000 序号之间的数据,但是接收方只接收到了 0 -1000, 3000 - 10000 之间的数据, 而 1000 - 3000 之间的数据没有到达接收端,此时发送方会重传 1000 - 10000 之间的数据,实际上这是没有必要的, 因为 3000 后面的数据已经被接收了。但是发送方无法感知这种情况的存在。
如何避免或者说解决这种问题呢?
为了优化这种情况,我们有必要让客户端知道更多的消息,在 TCP 报文段中,有一个 SACK 选项字段, 这个字段是一种选择性确认(selective acknowledgment)机制,这个机制能告诉 TCP 客户端,用我们的俗语来解释就是: “我这里最多允许接收 1000 之后的报文段,但是我却收到了 3000 - 10000 的报文段,请给我 1000 - 3000 之间的报文段”。
但是,这个选择性确认机制的是否开启还受一个字段的影响,这个字段就是 SACK 允许选项字段, 通信双方在 SYN 段或者 SYN + ACK 段中添加 SACK 允许选项字段来通知对端主机是否支持 SACK,如果双方都支持的话, 后续在 SYN 段中就可以使用 SACK 选项了。
这里需要注意下:SACK 选项字段只能出现在 SYN 段中。
伪超时和伪重传
在某些情况下,即使没有出现报文段的丢失也可能会引发报文重传。这种重传行为被称为 伪重传(spurious retransmission) , 这种重传是没有必要的,造成这种情况的因素可能是由于 伪超时(spurious timeout), 伪超时的意思就是过早的判定超时发生。造成伪超时的因素有很多,比如 报文段失序到达、报文段重复、ACK 丢失 等情况。
检测和处理伪超时的方法有很多,这些方法统称为检测算法和响应算法。 检测算法用于判断是否出现了超时现象或出现了计时器的重传现象。 一旦出现了超时或者重传的情况,就会执行响应算法撤销或者减轻超时带来的影响, 下面是几种算法,此篇文章暂不深入这些实现细节
- 重复 SACK 扩展- DSACK
- Eifel 检测算法
- 前移 RTO 恢复 - F-RTO
- Eifel 响应算法
包失序和包重复
TCP 数据流和窗口管理
客户端和服务器可以相互提供数据流信息的交换,数据流的相关信息主要包括报文段序列号、ACK 号和窗口大小。
拥塞控制
拥塞控制主要有两种方法:
- 端到端的拥塞控制: 因为网络层没有为运输层拥塞控制提供显示支持。所以即使网络中存在拥塞情况, 端系统也要通过对网络行为的观察来推断。TCP 就是使用了端到端的拥塞控制方式。IP 层不会向端系统提供有关网络拥塞的反馈信息。 那么 TCP 如何推断网络拥塞呢?如果超时或者三次冗余确认就被认为是网络拥塞,TCP 会减小窗口的大小,或者增加往返时延来避免。
- 网络辅助的拥塞控制: 在网络辅助的拥塞控制中,路由器会向发送方提供关于网络中拥塞状态的反馈。 这种反馈信息就是一个比特信息,它指示链路中的拥塞情况。
TCP 拥塞控制
慢启动
拥塞避免
快速恢复
40 张图带你搞懂 TCP 和 UDP
运输层概述
端系统
逻辑通信
运输层协议是在端系统中实现的,而不是在路由器中实现的。路由只是做识别地址并转发的功能。
在发送端,运输层将从发送应用程序进程接收到的报文转化成运输层分组,分组在计算机网络中也称为 报文段(segment)。 运输层一般会将报文段进行分割,分割成为较小的块,为每一块加上运输层首部并将其向目的地发送。
TCP 和 UDP 前置知识
TCP 叫做传输控制协议(TCP,Transmission Control Protocol)。
UDP 叫做 用户数据报协议(UDP,User Datagram Protocol)。
在计算机网络中,在不同层之间会有不同的描述。我们上面提到会将运输层的分组称为报文段, 除此之外,还会将 TCP 中的分组也称为报文段,然而将 UDP 的分组称为数据报,同时也将网络层的分组称为数据报。 但是为了统一,一般在计算机网络中我们统一称 TCP 和 UDP 的报文为 报文段。
套接字
在 TCP 或者 UDP 发送具体的报文信息前,需要先经过一扇门,这个门就是套接字(socket),套接字向上连接着应用层,向下连接着网络层。 在操作系统中,操作系统分别为应用和硬件提供了接口(Application Programming Interface)。 而在计算机网络中,套接字同样是一种接口,它也是有接口 API 的。
Socket 和 TCP/IP 没有必然联系,Socket 的出现只是方便了 TCP/IP 的使用,如何方便使用呢?你可以直接使用下面 Socket API 的这些方法。
套接字类型
套接字的主要类型有三种,下面我们分别介绍一下:
-
数据报套接字(Datagram sockets):数据报套接字提供一种无连接的服务,而且并不能保证数据传输的可靠性。 数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。 数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性, 对于有可能出现的数据丢失情况,需要在程序中做相应的处理。
-
流套接字(Stream sockets):流套接字用于提供面向连接、可靠的数据传输服务。能够保证数据的可靠性、顺序性。 流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即 TCP(The Transmission Control Protocol)协议。
-
原始套接字(Raw sockets):原始套接字允许直接发送和接收 IP 数据包,而无需任何特定于协议的传输层格式, 原始套接字可以读写内核没有处理过的 IP 数据包。
套接字处理过程
聊聊 IP
IP 是Internet Protocol(网际互连协议)的缩写,是 TCP/IP 体系中的网络层协议。设计 IP 的初衷主要想解决两类问题:
- 提高网络扩展性:实现大规模网络互联
- 对应用层和链路层进行解藕,让二者独立发展。
IP 是整个 TCP/IP 协议族的核心,也是构成互联网的基础。为了实现大规模网络的互通互联, IP 更加注重适应性、简洁性和可操作性,并在可靠性做了一定的牺牲。IP 不保证分组的交付时限和可靠性, 所传送分组有可能出现丢失、重复、延迟或乱序等问题。
我们知道,TCP 协议的下一层就是 IP 协议层,既然 IP 不可靠,那么如何保证数据能够准确无误地到达呢?
这就涉及到 TCP 传输机制的问题了。
端口号
套接字都有唯一标识符——文件描述符 fd。
端口号是 16 位的非负整数,它的范围是 0 - 65535 之间,这个范围会分为三种不同的端口号段,由 Internet 号码分配机构 IANA 进行分配:
- 周知/标准端口号,它的范围是 0 - 1023
- 注册端口号,范围是 1024 - 49151
- 私有端口号,范围是 49152 - 65535
举个例子反驳一下 我,假如到达服务器的两条数据都是到 80 端口的,你该如何区分呢? 或者说到达服务器的两条数据端口一样,协议不同,该如何区分呢?
所以仅凭端口号来确定某一条报文显然是不够的。
互联网上一般使用 源 IP 地址、目标 IP 地址、源端口号、目标端口号 来进行区分。 如果其中的某一项不同,就被认为是不同的报文段。这些也是多路分解和多路复用 的基础。
确定端口号
在实际通信之前,需要先确定一下端口号,确定端口号的方法分为两种:
- 标准既定的端口号
标准既定的端口号是静态分配的,每个程序都会有自己的端口号,每个端口号都有不同的用途。 端口号是一个 16 比特的数,其大小在 0 - 65535 之间,0 - 1023 范围内的端口号都是动态分配的既定端口号, 例如 HTTP 使用 80 端口来标识,FTP 使用 21 端口来标识,SSH 使用 22 来标识。这类端口号有一个特殊的名字, 叫做 周知端口号(Well-Known Port Number)。
- 时序分配的端口号
第二种分配端口号的方式是一种动态分配法,在这种方法下,客户端应用程序可以完全不用自己设置端口号, 凭借操作系统进行分配,操作系统可以为每个应用程序分配互不冲突的端口号。 这种动态分配端口号的机制即使是同一个客户端发起的 TCP 连接,也能识别不同的连接。
多路复用和多路分解
在主机上的每个套接字都会分配一个端口号,当报文段到达主机时,运输层会检查报文段中的目的端口号, 并将其定向到相应的套接字,然后报文段中的数据通过套接字进入其所连接的进程。 下面我们来聊一下什么是多路复用和多路分解的概念。
多路复用和多路分解分为两种,即无连接的多路复用(多路分解)和面向连接的多路复用(多路分解)。
无连接的多路复用和多路分解,运输层采用的是 UDP 协议。 UDP 套接字包含 IP 地址和端口号。既然是无连接,为什么要有源端口号呢?是因为 目的服务 需要知道把信息 发送 到哪个 源服务。
面向连接的多路复用与多路分解,运输层采用的是 TCP 协议。
UDP
UDP 协议一般作为流媒体应用、语音交流、视频会议所使用的传输层协议。DNS 协议底层也使用了 UDP 协议。
UDP 特点:
- 速度快(TCP 有拥塞控制的功能)
- 无须建立连接
- 无连接状态
- 分组首部开销小(每个 TCP 报文段都有 20 字节的首部开销,而 UDP 仅仅只有 8 字节的开销。)
UDP 报文结构
源端口号(Source Port):字段长为 16 位;
目标端口号(Destination Port): 字段长为 16 位;
长度(Length): 该字段占据 16 位,表示 UDP 数据报长度,包含 UDP 报文头和 UDP 数据长度。 因为 UDP 报文头长度是 8 个字节,所以这个值最小为 8,最大长度为 65535 字节。
校验和(Checksum):差错检测,字段长为 16 位。
TCP
TCP 连接是全双工服务(full-duplex service)的,全双工是什么意思?全双工指的是主机 A 与另外一个主机 B 存在一条 TCP 连接, 那么应用程数据就可以从主机 B 流向主机 A 的同时,也从主机 A 流向主机 B。
TCP 只能进行 点对点(point-to-point) 连接,不能实现多播。
TCP 会将数据临时存储到连接的发送缓存(send buffer)中,这个 send buffer 是三次握手之间设置的缓存之一, 然后 TCP 在合适的时间将发送缓存中的数据发送到目标主机的接收缓存中,实际上,每一端都会有发送缓存和接收缓存。
TCP 会将要传输的数据流分为多个块(chunk),然后向每个 chunk 中添加 TCP 标头,这样就形成了一个 TCP 段也就是报文段(segment)。 每一个报文段可以传输的长度是有限的,不能超过最大数据长度(Maximum Segment Size),俗称 MSS。 在报文段向下传输的过程中,会经过链路层,链路层有一个Maximum Transmission Unit,最大传输单元 MTU, 即数据链路层上所能通过最大数据包的大小,最大传输单元通常与通信接口有关。
那么 MSS 和 MTU 有啥关系呢?
因为计算机网络是分层考虑的,这个很重要,不同层的称呼不一样,对于传输层来说,称为报文段,而对网络层来说就叫做 IP 数据包, 所以,MTU 可以认为是网络层能够传输的最大 IP 数据包, 而 MSS(Maximum segment size)可以认为是传输层的概念,也就是 TCP 数据包每次能够传输的最大量。
TCP 报文段结构
序号、确认号实现传输可靠性
几种通信模型:
- 单工通信:单工数据传输只支持数据在一个方向上传输;在同一时间只有一方能接受或发送信息,不能实现双向通信,比如广播、电视等。
- 双工通信是一种点对点系统,由两个或者多个在两个方向上相互通信的连接方或者设备组成。
双工通信模型有两种:全双工(FDX)和半双工(HDX)
- 全双工:在全双工系统中,连接双方可以相互通信,一个最常见的例子就是电话通信。 全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。
- 半双工:在半双工系统中,连接双方可以彼此通信,但不能同时通信, 比如对讲机,只有把按钮按住的人才能够讲话,只有一个人讲完话后另外一个人才能讲话。
TCP 是一种全双工的通信协议,因此主机 A 在向主机 B 发送消息的过程中,也在接受来自主机 B 的数据。 主机 A 填充进报文段的确认号是期望从主机 B 收到的下一字节的序号。
累积确认
传输控制
利用窗口控制提高速度
原来是一问一答,引入滑动窗口(已确认区,待确认区,待发送区)后,可以批量发送,然后等待应答。
在这个窗口机制中,大量使用了 缓冲区 ,通过对多个段同时进行确认应答的功能。
窗口控制和重发
如下图所示,报文段 0 - 999 丢失后,但是主机 A 并不会等待,主机 A 会继续发送余下的报文段, 主机 B 发送的确认应答却一直是 1000,同一个确认号的应答报文会被持续不断的返回, 如果发送端主机在连续 3 次收到同一个确认应答后,就会将其所对应的数据重发,这种机制要比之前提到的超时重发更加高效, 这种机制也被称为高速重发控制。这种重发的确认应答也被称为 冗余 ACK(响应)。
流量控制
在每个 TCP 连接的一侧主机都会有一个 socket 缓冲区,缓冲区会为每个连接设置接收缓存和发送缓存, 当 TCP 建立连接后,从应用程序产生的数据就会到达接收方的接收缓冲区中,接收方的应用程序并不一定会马上读取缓冲区的数据, 它需要等待操作系统分配时间片。如果此时发送方的应用程序产生数据过快,而接收方读取接受缓冲区的数据相对较慢的话, 那么接收方中缓冲区的数据将会溢出。
TCP 有 流量控制服务(flow-control service) 用于消除缓冲区溢出的情况。 流量控制是一个速度匹配服务,即发送方的发送速率与接受方应用程序的读取速率相匹配。
TCP 通过使用一个 接收窗口(receive window) 的变量来提供流量控制。接受窗口会给发送方一个指示到底还有多少可用的缓存空间。 发送端会根据接收端的实际接受能力来控制发送的数据量。
TCP 的首部有一个接收窗口,这个字段用于流量控制,它用于指示接收方能够/愿意接受的字节数量。
发送端主机会定期发送一个窗口探测包,这个包用于探测接收端主机是否还能够接受数据, 当接收端的缓冲区一旦面临数据溢出的风险时,窗口大小的值也随之被设置为一个更小的值通知发送端,从而控制数据发送量。
如上图所示,当主机 B 收到报文段 2000 - 2999 之后缓冲区已满,不得不暂时停止接收数据。 然后主机 A 发送窗口探测包,窗口探测包非常小仅仅一个字节。 然后主机 B 更新缓冲区接收窗口大小并发送窗口更新通知给主机 A,然后主机 A 再继续发送报文段。
在上面的发送过程中,窗口更新通知可能会丢失,一旦丢失发送端就不会发送数据,所以窗口探测包会随机发送,以避免这种情况发生。
连接管理
这四种状态是 TCP 三次握手所涉及的:
- LISTEN: 表示等待任何来自远程 TCP 和端口的连接请求。
- SYN-SEND: 表示发送连接请求后等待匹配的连接请求。
- SYN-RECEIVED: 表示已接收并发送连接请求后等待连接确认,也就是 TCP 三次握手中第二步后服务端的状态
- ESTABLISHED: 表示已经连接已经建立,可以将应用数据发送给其他主机
这七种种状态是 TCP 四次挥手,也就是断开链接所设计的:
- FIN-WAIT-1: 表示等待来自远程 TCP 的连接终止请求,或者等待先前发送的连接终止请求的确认。
- FIN-WAIT-2: 表示等待来自远程 TCP 的连接终止请求。
- CLOSE-WAIT: 表示等待本地用户的连接终止请求。
- CLOSING: 表示等待来自远程 TCP 的连接终止请求确认。
- LAST-ACK: 表示等待先前发送给远程 TCP 的连接终止请求的确认(包括对它的连接终止请求的确认)。
- TIME-WAIT: 表示等待足够的时间以确保远程 TCP 收到其连接终止请求的确认。
- CLOSED: 表示连接已经关闭,无连接状态。
什么是 TIME-WAIT:MSL 是 TCP 报文段可以存活或者驻留在网络中的最长时间。RFC 793 定义了 MSL 的时间是两分钟, 但是具体的实现还要根据程序员来指定,一些实现采用了 30 秒的这个最大存活时间。
当主机收到 TCP 报文段后,其 IP 和端口号不匹配的情况,服务器就会发出一个 RST 特殊报文段给客户端, 告诉客户端没有匹配的套接字连接,请不要再继续发送了。
SYN 洪泛攻击,建立大量半连接,这种攻击也是属于 Dos 攻击的一种,抵御这种攻击的方式是使用 SYN cookie。
拥塞控制
拥塞控制机制会在面临网络拥塞时遏制发送方的数据发送。
拥塞控制主要有两种方法:
- 端到端的拥塞控制。IP 层不会向端系统提供有关网络拥塞的反馈信息。 那么 TCP 如何推断网络拥塞呢?如果超时或者三次冗余确认就被认为是网络拥塞,TCP 会减小窗口的大小,或者增加往返时延来避免。
- 网络辅助的拥塞控制。路由器会向发送方提供关于网络中拥塞状态的反馈。
TCP 拥塞控制
TCP 是由接收缓存、发送缓存和变量(LastByteRead, rwnd,等)组成。发送方的 TCP 拥塞控制机制会跟踪一个变量, 即 拥塞窗口(congestion window) 的变量,拥塞窗口表示为 cwnd,用于限制 TCP 在接收到 ACK 之前可以发送到网络的数据量。 而接收窗口(rwnd) 是一个用于告诉接收方能够接受的数据量。
一般来说,发送方未确认的数据量不得超过 cwnd 和 rwnd 的最小值,也就是
LastByteSent - LastByteAcked <= min(cwnd,rwnd)
TCP 的拥塞控制算法(TCP congestion control algorithm)主要包含三个部分:慢启动、拥塞避免、快速恢复。
ssthresh(慢启动阈值)
视频
如果有时间,可以看视频理解下他人的见解。
https://www.bilibili.com/video/BV1Pe4y1S7mo/
参考资料
图解 原来这就是TCP https://cnblogs.com/flashsun/p/14322660.html
TCP协议面试10连问,网友直呼太强! https://mp.weixin.qq.com/s/BRVJWcXZvQjKBCs_dQzSjw