TCP/IP面试要点浅析
一.TCP基本认识
1.TCP头部格式
- 序列号
- 在建立连接时由计算机生成的随机数作为初始值,通过SYN包传输给接收端
- 每发送一次数据,就
累加
一次该数据字节数的大小 - 用来解决
网络包乱序
的问题
- 确认应答号
- 指下次
期望
收到数据的序列号 - 发送端收到这个确认应答号以后,可以认为在这个号以前的数据都已经被正常接收
- 用来解决
丢包
问题
- 指下次
- 控制位
- ACK
- 该位为1时,
确认应答
的字段变为有效 - TCP 规定除了最初建立连接时的
SYN
包之外该位必须设置为1
- 该位为1时,
- RST
- 该位为1时,表示TCP链接中出现异常,必须强制断开连接
- SYN
- 该位为1时,表示希望建立连接,并舍得序列号字段初始值
- FIN
- 该位为1时,表示今后不会再有数据发送,希望断开连接
- 当通信希望断开连接时,通信双方就可以相互交换FIN位为1的TCP段
- ACK
2.什么是TCP?什么是TCP连接?如何确定一个TCP连接?
什么是TCP?
TCP是面向连接的
,可靠的
,基于字节流
的传输层通信协议
- 面向连接:一对一的连接,不能向UDP那样一对多
- 可靠的:无论网络怎么变化,TCP都保证一个报文一定能到达接收端
- 基于字节流:消息是
无边界的
,无论消息多大都可以传输,并且消息是有序的
,当前一个消息没有收到的时候,即使先收到了后面的字节,那么也不能扔给应用层去处理,同时对重复的报文会自动丢弃。
什么是TCP连接?
TCP连接:用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合叫做连接(包括socket,序列号,窗口大小)
如何确定一个TCP连接?
四元组
- 源地址
- 源端口
- 目标地址
- 目标端口
源地址和目标地址是在IP头部中,作用是通过IP协议发送报文给对方主机
源端口和目标端口是在TCP头部中,作用是告诉TCP协议应该把报文发送给哪个进程
3.为什么需要TCP?TCP工作在哪一层?
TCP工作在传输层
因为网络层是不可靠的,如果需要保证网络数据包的可靠性,那么就需要有传输层的TCP来保证
因为TCP是一个工作在传输层的可靠数据传输服务,他能确保接收端接收的网络包是无损坏,无间隔,非冗余和按序的
4.TCP最大连接数如何计算?实际受哪些因素影响?
理论上,TCP最大连接数=客户端IP数(2的32次方)*客户端端口数(2的16次方)
实际上TCP的最大连接数远不能达到理论上限,受以下因素影响:
- 文件描述符fd数量限制:每个tcp连接都是一个文件,Linux对文件描述符做了三个方面的限制
- 系统级:当前系统可打开的fd最大数量
- 用户级:当前用户可打开的fd最大数量
- 进程级:当前进程可打开的fd最大数量
- 内存大小限制:每个tcp都要占用一定内存,内存有限,被占满后会OOM
5.UDP和TCP有什么区别?分别对应什么应用场景?
区别如下:
1.连接
- TCP传输数据前要先建立连接
- UDP不需要建立连接,即刻传输数据
2.服务对象的数量
- TCP仅支持一对一
- UDP支持一对一,一对多,多对多
3.可靠性
- TCP是可靠的
- UDP是不可靠的,尽最大努力交付
4.控制机制
- TCP有拥塞控制和流量控制
- UDP没有,即使网络很堵,UPDDR发送速率也不会变
5.首部大小
- TCP首部较长,大小会变
- UDP首部仅8个字节,固定不变,开销小
6.传输方式
- TCP是流式传输,没有边界,但保证顺序和可靠
- UDP是一个一个包发送,有边界,会丢包和乱序
应用场景:
TCP
-
HTTP协议:超文本传输协议,用于普通浏览
-
HTTPS协议:安全超文本传输协议,身披SSL外衣的HTTP协议
-
FTP协议:文件传输协议,用于文件传输
-
POP3协议:邮局协议,收邮件使用
-
SMTP协议:简单邮件传输协议,用来发送电子邮件
-
Telent协议:远程登陆协议,通过一个终端登陆到网络
-
SSH协议:安全外壳协议,用于加密安全登陆,替代安全性差的Telent协议
UDP
-
包总量少的通信,比如DNS
-
音频视频等多媒体通信
-
广播通信
-
DHCP协议:动态主机配置协议,动态配置IP地址
-
NTP协议:网络时间协议,用于网络时间同步
-
BOOTP协议:引导程序协议,DHCP协议的前身,用于无盘工作站从中心服务器上获取IP地址
6.TCP协议的11种状态分别代表什么意思?
-
CLOSED状态:初始状态,表示TCP连接是“关闭的”或者“未打开的”
-
LISTEN状态:表示服务端的某个端口正处于监听状态,正在等待客户端连接的到来
-
SYN_SENT状态:当客户端发送SYN请求建立连接之后,客户端处于SYN_SENT状态,等待服务器发送SYN+ACK
-
SYN_RCVD状态:当服务器收到来自客户端的连接请求SYN之后,服务器处于SYN_RCVD状态,在接收到SYN请求之后会向客户端回复一个SYN+ACK的确认报文
-
ESTABLISED状态:当客户端回复服务器一个ACK和服务器收到该ACK(TCP最后一次握手)之后,服务器和客户端都处于该状态,表示TCP连接已经成功建立
-
FIN_WAIT_1状态:当数据传输期间当客户端想断开连接,向服务器发送了一个FIN之后,客户端处于该状态
-
FIN_WAIT_2状态:当客户端收到服务器发送的连接断开确认ACK之后,客户端处于该状态
-
CLOSE_WAIT状态:当服务器发送连接断开确认ACK之后但是还没有发送自己的FIN之前的这段时间,服务器处于该状态
-
TIME_WAIT状态:当客户端收到了服务器发送的FIN并且发送了自己的ACK之后,客户端处于该状态,关于TIME_WAIT状态在整个TCP体系中的作用,请参考:https://www.cnblogs.com/yinbiao/p/10945836.html
-
LAST_ACK状态:表示被动关闭的一方(比如服务器)在发送FIN之后,等待对方的ACK报文时,就处于该状态
-
CLOSING状态:连接断开期间,一般是客户端发送一个FIN,然后服务器回复一个ACK,然后服务器发送完数据后再回复一个FIN,当客户端和服务器同时接受到FIN时,客户端和服务器处于CLOSING状态,也就是此时双方都正在关闭同一个连接
二.TCP连接建立
1.TCP三次握手的过程和状态变迁
服务端监听端口,客户端主动发起TCP请求
整个过程:
第一次握手:客户端发SYN报文
第二次握手:服务端发SYN+ACK报文
第三次握手:客户端发ACK报文
下面分别阐述一下每次握手的报文是如何构造的
- 第一次:SYN报文
- 客户端随机初始化
序列号字段
,SYN标志位为1,表示此报文是SYN报文 - 把SYN报文发送给服务端,表示向服务端发起连接,然后客户端处于
SYN-SENT
状态 - 此报文不包含应用层数据
- 客户端随机初始化
- 第二次:SYN+ACK报文
- 服务端收到客户端的SYN报文后,也构建一个报文发过去,然后服务端处于
SYN-RCVD
状态随机初始化序列号
,确认应答号是客户端SYN报文的序列号+1
- SYN和ACK标志位置为1
- 此报文也不包含应用层数据
- 服务端收到客户端的SYN报文后,也构建一个报文发过去,然后服务端处于
- 第三次:ACK报文
- 客户端收到服务端报文后,还要向服务端回应最后一个应答ACK报文,按如下要求构建报文
- ACK标志位置为1
- 确认应答号是收到的服务端报文的序列号+1
- 将构建好的报文发送给服务端,之后客户端处于
ESTABLISHED
状态,服务器收到报文后,也处于此状态 - 此处报文可以携带应用层数据
- 客户端收到服务端报文后,还要向服务端回应最后一个应答ACK报文,按如下要求构建报文
第三次握手可以携带应用数据,前两次不可以
一旦完成三次握手,双方都处于ESTABLISHED
状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了
2.为什么握手需要三次?2次不行吗?
2次不行
第一个原因:三次握手可以阻止历史连接的初始化
客户端连续多次发送请求建立连接的SYN报文,在网络堵塞情况下:
- 一个旧的SYN报文比最新的SYN报文提早到达了服务端
- 那么此时服务端会回一个SYN+ACK报文
- 客户端收到服务端的SYN+ACK报文后,可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送RST报文给服务端,表示终止这一次连接
而如果只有两次握手,则无法处理历史连接的情况
原因2:可靠的同步序列号
序列号在TCP中十分重要:
- 接收方可以去重重复数据
- 接收方可以根据序列号按序接收
- 可以标识发出去的数据包,哪些是已经收到的
如果只有两次握手,那么其只保证了一方的序列号已经被另一方接收,没办法保证双方的初始序列号都能被确认接收
原因三:避免资源浪费
如果只有两次握手,那么一发SYN报文就建立连接,如果重复发了多次SYN报文,那么就会建立多个冗余的无效连接,造成不必要的资源浪费
3.为什么每次建立TCP连接时,初始化的序列号都要求不一样呢?
-
防止历史报文被被下一个相同的四元组接收
如果序列号每次都一样:
- 客户端和服务端建立一个tpc连接,客户端发送时网络阻塞了,服务端进程重启了,服务端会发送RST报文来断开连接
- 紧接着,客户端又和服务端建立了与上一个连接相同四元组的连接
- 在新连接建立完成后,上一个连接中被网络阻塞的数据包正好抵达了服务端,刚好该数据包的序列号正好在服务端的接收窗口内,所以该数据包会被服务端正常接收,就会造成数据混乱
-
为了安全性,防止黑客伪造相同序列号的TCP报文被对方正常接收
4.第一次握手丢失了,会发生什么?
- 客户端触发超时重传机制,重传第一次握手的SYN报文
第一次握手:客户端给服务端发SYN报文,然后客户端处于SYN—SENT
状态。
当此报文丢失后,服务端没有收到此报文,就不会发SYN-ACK报文,客户端就会触发超时重传机制,重传SYN报文
Linux中,超时重传次数由内核控制,默认是五次
通常,第一次超时重传是在一秒后,第二次超时重传是在2秒后,第三次超时重传是在4秒后,第四次超时重传是在8秒后,第五次超时重传是在16秒后,当第五次超时重传后,会继续等待32秒,如果服务端仍然没有响应ACK,那么就断开连接,不再重传
所以,总耗时是 1+2+4+8+16+32=63 秒,大约 1 分钟左右。
5.第二次握手丢失了,会发生什么?
- 客户端和服务端都触发超时重传机制,客户端重传第一次握手SYN的报文,服务端重传第二次握手的SYN+ACK报文。
第二次握手指的是,服务端发送的SYN+ACK报文
当此报文丢失后
客户端迟迟没有收到第二次握手报文,会以为自己的第一次报文丢失了,会重传第一次握手报文
服务端没有收到第三次握手报文,会认为自己的第二次报文丢失了,会重传第二次握手报文
6.第三次握手丢失了,会发生什么?
- 服务端重传第二次握手的SYN—ACK报文
第三次握手指的是,客户端收到服务端的SYN-ACK报文后,会响应一个ACK报文
当此报文丢失后,服务端迟迟收不到第三次握手的ACK报文,会超时重传第二次握手的SYN-ACK报文,直到收到第三次握手报文或者到达最大重传次数
7.什么是SYN攻击?如何避免?
SYN攻击:攻击者短时间伪造不同IP地址的SYN报文(第一次握手),服务端每接收到一个SYN报文,就处于SYN_RECV
状态,但服务端发送出去的SYN+ACK报文,无法得到未知IP的ACK响应,久而久之就会占满服务端的半连接队列
,使得服务器不能服务正常用户
如何避免SYN攻击?
-
方法1:修改 Linux 内核参数,调整半连接队列大小和当队列满时应做什么处理
比如队列满时,对新SYN报文直接响应RST,丢弃连接
-
方法2:SYN Cookie
服务器在收到SYN包时并不马上分配存储连接的数据区,而是根据这个syn包计算出一个cookie,填入到响应的SYN+ACK报文的序列号中,等对方回应ack包时,再检查ack包序列号是否为预期,如果为预期则再分配数据区
此方法的缺点就是cookie的计算仍需要消耗cpu资源,因为cookie的计算较为复杂,不能被黑客猜到
三.TCP连接断开
1.TCP四次挥手的过程
整体流程如下:
整体概述:
- 第一次挥手:客户端发送FIN
- 第二次挥手:服务端发ACK,表示收到了客户端的FIN
- 第三次挥手:服务端发FIN
- 第四次挥手:客户端发ACK,表示收到了服务端的FIN
可以看到,每个方向都要一个ACK和一个FIN,总共是四次
具体过程:
- 客户端打算关闭连接,此时会发送一个 TCP 首部
FIN
标志位被置为1
的报文,也即FIN
报文,之后客户端进入FIN_WAIT_1
状态。 - 服务端收到该报文后,就向客户端发送
ACK
应答报文,接着服务端进入CLOSED_WAIT
状态。 - 客户端收到服务端的
ACK
应答报文后,之后进入FIN_WAIT_2
状态。 - 等待服务端处理完数据后,也向客户端发送
FIN
报文,之后服务端进入LAST_ACK
状态。 - 客户端收到服务端的
FIN
报文后,回一个ACK
应答报文,之后进入TIME_WAIT
状态 - 服务器收到了
ACK
应答报文后,就进入了CLOSED
状态,至此服务端已经完成连接的关闭。 - 客户端在经过
2MSL
一段时间后,自动进入CLOSED
状态,至此客户端也完成连接的关闭。
2.为什么挥手需要四次?
之所以多了一次,是因为服务端的ACK和FIN分开发送了,为什么需要分开发送呢?
是为了处理服务端数据还没有全部发送完的情况,服务端先发ACK响应客户端,等服务端数据全部发送完了再发FIN,表示同意关闭连接
3.第一次挥手丢失了,会发生什么?
- 会触发客户端的超时重传机制,重传客户端的FIN报文
第一次挥手,即客户端发送的FIN报文
丢失了则服务端不会响应ACK,客户端则会一直等待,直到触发超时重传,当重传次数超过某个阈值时,不再进行重传,客户端直接进入close状态
4.第二次挥手丢失了,会发生什么?
第二次挥手:即服务端响应的ACK报文丢失
服务端是不会重传ACK报文的,客户端一直没有收到服务端的ACK,会重传第一次挥手的FIN,直到收到第二次挥手报文或者重传达到最大次数
5.第三次挥手丢失了,会发生什么?
第三次挥手:服务端响应的FIN
正常情况下,服务端响应FIN,客户端会进行第四次挥手响应ACK,当服务端因为丢失了第三次挥手报文,而导致收不到第四次挥手的ACK时,会重传第三次挥手的FIN
6.第四次挥手丢失了,会发生什么?
第四次挥手:客户端响应的ACK
当第四次挥手的ACK丢失了,服务端会认为自己的第三次挥手的FIN可能丢失了,会重传第三次挥手的FIN
7.什么是TIME_WAIT状态?为什么需要它?
主动关闭的一方在收到第三次挥手的FIN后,会响应一个ACK,此时主动关闭方进入TIME_WAIT状态
为什么需要TIME_WAIT状态?
- 因为tcp无法根据序列号来判断不同连接的新老数据,为了防止历史连接中的数据,被后面相同的四元组连接错误的接收,因此TCP设计了TIME_WAIT状态,状态会持续2MSL,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中自然的消失,再出现的数据包一定都是新建立的连接产生的。
- TIME_WAIT等待足够的时间后,可以
确保最后一次挥手的ACK能让被动关闭方接收,从而帮助其正常关闭
- 假如客户端没有TIME_WAIT状态,而是在发完最后一次挥手ACK时就直接进入
CLOSE
状态,如果最后一次握手的报文丢失了,服务端重传FIN,此时客户端若直接进入关闭状态,会直接响应RST报文,导致服务端不能正常的关闭。 - 所以,为了防止这种情况的出现,客户端必须等待足够长的时间来确保服务端可以收到最后一次挥手ACK,从而帮助服务端进行正常关闭
- 假如客户端没有TIME_WAIT状态,而是在发完最后一次挥手ACK时就直接进入
8.为什么TIME_WAIT的等待时间是2 MSL?
MSL: 报文的最大生存时间
TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
比如,如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 FIN
报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。
2MSL
的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
在 Linux 系统里 2MSL
默认是 60
秒,那么一个 MSL
也就是 30
秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。
如果要修改 TIME_WAIT 的时间长度,只能修改 Linux 内核代码里 TCP_TIMEWAIT_LEN 的值,并重新编译 Linux 内核。
9.如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP 有一个机制是保活机制。这个机制的原理是这样的:
定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:
- tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制
- tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
- tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。
也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。
如果开启了 TCP 保活,需要考虑以下几种情况:
- 第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
- 第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。
- 第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。
TCP 保活的这个机制检测的时间是有点长,我们可以自己在应用层实现一个心跳机制。
比如,web 服务软件一般都会提供 keepalive_timeout
参数,用来指定 HTTP 长连接的超时时间。如果设置了 HTTP 长连接的超时时间是 60 秒,web 服务软件就会启动一个定时器,如果客户端在完后一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,定时器的时间一到,就会触发回调函数来释放该连接。
四.TCP数据传输
TCP是如何保证可靠性传输的呢?主要可以概括为以下四个方面
- 重传机制
- 滑动窗口
- 流量控制
- 拥塞控制
1.重传机制
重传机制,用于解决数据包丢失的情况,主要有以下四种重传机制:
- 超时重传
- 快速重传
- SACK
- D-SACK
1.超时重传
什么叫超时重传?
超时重传就是在发送数据时,设定一个计时器,当超过指定时间后,没有收到对方ack的应答报文,就重发该数据
「超时重传时间 RTO 的值」是一个动态变化的值。
如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。
也就是每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
Linux中,超时重传次数由内核控制,默认是五次
通常,第一次超时重传是在一秒后,第二次超时重传是在2秒后,第三次超时重传是在4秒后,第四次超时重传是在8秒后,第五次超时重传是在16秒后,当第五次超时重传后,会继续等待32秒,如果服务端仍然没有响应ACK,那么就断开连接,不再重传
超时触发重传存在的问题是,超时周期可能相对较长
。那是不是可以有更快的方式呢?
于是就可以用「快速重传」机制来解决超时重发的时间等待。
2.快速重传
快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。
具体样例如下:
在上图,发送方发出了 1,2,3,4,5 份数据:
- 第一份 Seq1 先送到了,于是就 Ack 回 2;
- 结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2;
- 后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到;
- 发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。
- 最后,收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。
快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传之前的一个,还是重传所有的问题。
比如对于上面的例子,是重传 Seq2 呢?还是重传 Seq2、Seq3、Seq4、Seq5 呢?因为发送端并不清楚这连续的三个 Ack 2 是谁传回来的。
根据 TCP 不同的实现,以上两种情况都是有可能的。可见,这是一把双刃剑。
为了解决不知道该重传哪些 TCP 报文,于是就有 SACK 选择性确认
方法
3.SACK 选择性确认 重传
这种方式需要在 TCP 头部「选项」字段里加一个 SACK
的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
样例如下:
如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK
信息发现只有 200~299
这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复
如果要支持 SACK
,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack
参数打开这个功能(Linux 2.4 后默认打开)。
4.Duplicate SACK
Duplicate SACK 又称 D-SACK
,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。
D-SACK
有这么几个好处:
- 可以让「发送方」知道,是发出去的包丢了,还是接收方回应的 ACK 包丢了;
- 可以知道是不是「发送方」的数据包被网络延迟了;
- 可以知道网络中是不是把「发送方」的数据包给复制了;
在 Linux 下可以通过 net.ipv4.tcp_dsack
参数开启/关闭这个功能(Linux 2.4 后默认打开)。
样例1:ACK丢包
- 「接收方」发给「发送方」的两个 ACK 确认应答都丢失了,所以发送方超时后,重传第一个数据包(3000 ~ 3499)
- 于是「接收方」发现数据是重复收到的,于是回了一个 SACK = 3000~3500,告诉「发送方」 3000~3500 的数据早已被接收了,因为 ACK 都到了 4000 了,已经意味着 4000 之前的所有数据都已收到,所以这个 SACK 就代表着
D-SACK
。 - 这样「发送方」就知道了,数据没有丢,是「接收方」的 ACK 确认报文丢了。
样例2:网络延时
- 数据包(1000~1499) 被网络延迟了,导致「发送方」没有收到 Ack 1500 的确认报文。
- 而后面报文到达的三个相同的 ACK 确认报文,就触发了快速重传机制,但是在重传后,被延迟的数据包(1000~1499)又到了「接收方」;
- 所以「接收方」回了一个 SACK=1000~1500,因为 ACK 已经到了 3000,所以这个 SACK 是 D-SACK,表示收到了重复的包。
- 这样发送方就知道快速重传触发的原因不是发出去的包丢了,也不是因为回应的 ACK 包丢了,而是因为网络延迟了。
2.滑动窗口机制
可以先看两个协议
-
停止等待协议:每发送完一个分组,就等待对方确认,收到确认之后才再发送一个分组(简单,但是信道的利用率低)
-
连续ARQ协议(滑动窗口协议):接收方不必对收到的分组逐个发送确认,而是在收到几个分组后,对按序到达的最后一个分组发送确认,这就表示:到这个分组为止的所有分组都已经正确收到了(不对每个分组返回确认,只对收到的连续几个分组返回确认,这样返回确认的次数就变少了)
滑动窗口机制正是连续ARQ协议的实现
样例如下:
图中的 ACK 600 确认应答报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答。
窗口大小是指无需等待确认应答,而可以继续发送数据的最大值。
TCP 头里有一个字段叫 Window
,也就是窗口大小。
这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
所以,通常窗口的大小是由接收方的窗口大小来决定的。
发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。
发送方的滑动窗口
当收到之前发送的数据 32~36
字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 52~56
字节又变成了可用窗口,那么后续也就可以发送 52~56
这 5 个字节的数据了。
接收方的滑动窗口
接收窗口和发送窗口的大小是相等的吗?
并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。
因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。
3.流量控制
利用滑动窗口实现流量控制:在合适的时候调节滑动窗口的大小
当发送方可用窗口变为 0 时,发送方实际上会定时发送窗口探测报文,以便知道接收方的窗口是否发生了改变
当服务端系统资源非常紧张的时候,操心系统可能会直接减少了接收缓冲区大小,这时应用程序又无法及时读取缓存数据,那么这时候就有严重的事情发生了,会出现数据包丢失的现象。
为了防止这种情况发生,TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间再减少缓存,这样就可以避免了丢包情况。
如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。
窗口关闭潜在的危险
接收方向发送方通告窗口大小时,是通过 ACK
报文来通告的。
那么,当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,那麻烦就大了。
这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不采取措施,这种相互等待的过程,会造成了死锁的现象。
TCP 是如何解决窗口关闭时,潜在的死锁现象呢?
为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。
如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。
什么叫糊涂窗口综合征?
如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。
到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,
要知道,我们的 TCP + IP
头有 40
个字节,为了传输那几个字节的数据,要达上这么大的开销,这太不经济了。这就是糊涂窗口综合症。
如何解决糊涂窗口综合征?
接收方通常的策略如下:
当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0
,也就阻止了发送方再发数据过来。
等到接收方处理了一些数据后,窗口大小 >= MSS,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。
发送方通常的策略:
使用 Nagle 算法,该算法的思路是延时处理,它满足以下两个条件中的一条才可以发送数据:
- 要等到窗口大小 >=
MSS
或是 数据大小 >=MSS
- 收到之前发送数据的
ack
回包
只要没满足上面条件中的一条,发送方一直在囤积数据,直到满足上面的发送条件。
另外,Nagle 算法默认是打开的,如果对于一些需要小数据包交互的场景的程序,比如,telnet 或 ssh 这样的交互性比较强的程序,则需要关闭 Nagle 算法。
可以在 Socket 设置 TCP_NODELAY
选项来关闭这个算法(关闭 Nagle 算法没有全局参数,需要根据每个应用自己的特点来关闭)
4.拥塞控制
即网络堵塞时,牺牲自己,少发点数据到网络上,避免网络堵塞恶性循环
为了在「发送方」调节所要发送数据的量,定义了一个叫做「拥塞窗口」的概念。
什么是拥塞窗口?和发送窗口有什么关系呢?
拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。
发送窗口的值 = min(拥塞窗口,接收窗口)
拥塞窗口 cwnd
变化的规则:
- 只要网络中没有出现拥塞,
cwnd
就会增大; - 但网络中出现了拥塞,
cwnd
就减少;
那么怎么知道当前网络是否出现了拥塞呢?
其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了用拥塞。
拥塞控制有哪些控制算法?
- 慢启动
- 拥塞避免
- 拥塞发生
- 快速恢复
1.慢启动算法
慢启动的意思就是一点一点的提高发送数据包的数量,如果一上来就发大量的数据,这不是给网络添堵吗?
慢启动的算法记住一个规则就行:当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。
可以看出慢启动算法,发包的个数是指数性的增长。
有一个叫慢启动门限 ssthresh
(slow start threshold)状态变量。
- 当
cwnd
<ssthresh
时,使用慢启动算法。 - 当
cwnd
>=ssthresh
时,就会使用「拥塞避免算法」。
2.拥塞避免算法
拥塞避免算法就是将原本慢启动算法的指数增长变成了线性增长,还是增长阶段,但是增长速度缓慢了一些。
拥塞窗口 cwnd
「超过」慢启动门限 ssthresh
就会进入拥塞避免算法。
一般来说 ssthresh
的大小是 65535
字节。
那么进入拥塞避免算法后,它的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。
就这么一直增长着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。
当触发了重传机制,也就进入了「拥塞发生算法」。
3.快重传算法
发送端只要一连收到三个重复的ACK即可断定有分组丢失了,应理解重传丢失的报文段而不必继续等待为该报文段设置的重传计时器的超时(快重传并非取消重传计时器,而是在某些情况下更早的重传丢失的报文段)
4.快恢复算法
根据收到的重复ACK的多少调节慢开始门限ssthresh
五.TCP相关实践
1.如何在 Linux 系统中查看 TCP 状态?
TCP 的连接状态查看,在 Linux 可以通过 netstat -napt
命令查看。