5.流量控制
接收端处理数据的速度是有限的。如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送就会造成丢包,继而引起丢包重传等等一系列连锁反应。
因此TCP 支持根据接收端的接收数据的能力来决定发送端发送数据的速度,这个机制叫做流量控制(Flow Control)。
-
在进行流量控制时,发送方是如何在第一次发送数据的时候,得知对方的接收能力的呢?
我们在建立连接的过程中三次握手的时候不就互相交换报文了吗?报文内的窗口大小就是告诉对方我的缓冲区还能接收多少字节,也就是我的接收能力(接收缓冲区中剩余空间的大小)
也就是说三次握手期间,已经协商交换过了双方的接收能力
-
接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段,通过 ACK 端通知发送端。
-
窗口大小字段越大,说明网络的吞吐量越高。
-
接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值来通知给发送端。
-
发送端接受到这个窗口之后,就会减慢自己的发送速度。
-
如果接收端缓冲区满了就会将窗口置为 0。这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
当发送端得知接收端接收数据的能力为 0 时,通常意味着接收端的缓冲区已满,不能再接收更多的数据。在这种情况下,发送端需要等待接收端的通知,才能继续发送数据。发送端可以通过以下两种方式来得知何时可以继续发送数据: 1. TCP 延迟确认(Delayed Acknowledgments): 接收端在接收到数据后,可能会延迟发送确认(ACK)包,以减少网络上的 ACK 流量。当接收端的缓冲区有空间时,它会在延迟确认的时间内发送一个 ACK 包,这个 ACK 包会包含当前接收端可以接收的数据量(即窗口大小)。 如果接收端因为缓冲区满而发送了一个窗口大小为 0 的 ACK,那么一旦接收端处理了一些数据,释放了缓冲区空间,它会发送一个非零窗口大小的 ACK,告诉发送端可以开始发送数据了。 2. TCP 持续计时器(Persistence Timer): 当发送端的窗口大小变为 0 时,它会启动一个持续计时器。这个计时器是为了确保在接收端缓冲区再次可用时,发送端能够及时得知这一信息。 持续计时器到期后,发送端会发送一个小的探测段(probe segment),通常是携带一个字节数据的段。这个探测段会促使接收端回应一个窗口更新,告知发送端当前的窗口大小。 如果接收端仍然不能接收数据,它会再次发送一个窗口大小为 0 的 ACK,发送端则会重置持续计时器并等待。 通过这两种机制,TCP 协议能够有效地管理网络拥塞和控制数据传输的流量,确保数据传输的可靠性和网络的稳定性。
-
如果接收端反馈一直接受缓冲区为0怎么办呢?
发送端就会发送一个报文,设置标志位PSH
,也就是PUSH的意思,催促接收端赶紧处理空间;如果我们想让对方尽快处理数据,都可以设置PSH
平常我们在命令行中输入指令的时候,都默认带了PSH标志,催促OS赶紧处理,并返回结果
16 位数字最大表示 65535,那么 TCP 窗口最大就是 65535 字节吗?
是的,在 TCP 头部中,窗口大小(Window Size)字段是一个 16 位的字段,这意味着它可以表示的最大值是 2^16 - 1,即 65535。因此,在理论上,TCP 窗口的最大值是 65535 字节。
然而,实际上 TCP 窗口大小可以超过这个值。这是通过使用所谓的 TCP 窗口缩放(Window Scaling)选项来实现的,该选项是在 TCP 连接建立期间通过 TCP 三次握手过程协商的。
窗口缩放选项允许发送方和接收方协商一个缩放因子,该因子可以用于放大窗口大小的实际值。缩放因子是一个 0 到 14 之间的值,可以使得窗口大小最大扩展到 (2^16 - 1) * (2^缩放因子) 字节。例如,如果缩放因子是 7,那么最大窗口大小可以是: 65535 * (2^7) = 65535 * 128 = 8,388,608 字节
因此,使用窗口缩放,TCP 窗口可以远远超过 65535 字节的限制,这有助于提高在高延迟网络上的吞吐量。需要注意的是,为了使用窗口缩放,两端都必须支持并在连接建立时启用这个选项。
TCP的六个标志位前面已经见过5个了,这里把最后一个URG讲一下
URG如果不设置为1,那么16位紧急指针是没有用处的;只有URG设为1,紧急指针才有用处
URG:紧急指针是否有效
16位紧急指针:标识哪部分数据是紧急数据
6.滑动窗口
刚才我们讨论了确认应答策略,对每一个发送的数据段,都要给一个ACK确认应答。收到ACK后再发送下一个数据段,这样做有一个比较大的缺点,就是性能较差,尤其是数据往返的时间较长的时候
既然这样一发一收的方式性能较低,那么我们一次发送多条数据,就可以大大的提高性能(其实就是将多个段的等待时间重叠在一起了)
两个问题: 1. 流量控制:发送方如何根据对方的接受能力,发送数据? 1. 超时重传:超时时间以内,已经发送的报文不能被丢弃,而是要保存起来!保存在哪里?
于是发送方就规定一个概念--->滑动窗口,在滑动窗口以内的数据,可以直接发送,暂时不用收到应答;
在发送缓冲区内,发送缓冲区被分为三个部分,1️⃣ 已发送已确认数据,2️⃣暂时不用应答,可以直接发送的数据,3️⃣待发送的数据
-
窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。上图的窗口大小就是 4000 个字节(四个段)。
-
发送前四个段的时候不需要等待任何 ACK,直接发送。
-
收到第一个 ACK 后,滑动窗口向后移动,继续发送第五个段的数据,依次类推。
-
操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答。只有确认应答过的数据,才能从缓冲区删掉。
-
窗口越大,则网络的吞吐率就越高。
如何理解滑动窗口
把缓冲区看成一个数组,那么滑动窗口的移动就是下标的更新
滑动窗口的大小就是接收缓冲区的大小;
滑动窗口的大小 = 对方同步给我的窗口大小,即对方的接收能力(暂时)
-
滑动窗口一定会整体右移吗?
不一定,可能会向右滑动,也有可能保持不变;因为有可能接收缓冲区的上方不读数据,导致接收缓冲区数据越来越多,只会导致左侧的win_start向右,而win_end不变;
-
滑动窗口的大小可以为0吗?
当然可以,只要接收缓冲区上层一直不读数据,那么接收缓冲区满了之后,win_start == win_end了,也就是接收缓冲区大小为0;
-
滑动窗口如何滑动更新?
当发送端收到对方的应答后,应答报文中有确认序号,那么把win_start的下标改为应答序号就可以了,而win_start + 对方接收缓冲区大小,就是win_end的值了
丢包两种情况讨论
那么如果出现了丢包,如何进行重传?这里分两种情况讨论
【情况一】:数据包已经抵达,ACK被丢了
这种情况下,部分ACK丢了不要紧,因为可以通过后续的ACK进行确认;因为确认应答序号代表的是该序号前面的报文我已经全部收到了
【情况二】:数据包直接丢了
我们也都知道我们可以发送缓冲区看作是数组,此时就要分情况
a.最左侧报文丢失
最左侧报文丢失,即使后面的报文接收端全部收到了,也只能发给发送端的确认序号为1001,而如果接收端连续收到三个同样的确认序号,就会触发重传机制,这种机制被称为“高速重发机制”,也被称为“快重传”
快重传是能够快速进行数据的重发,当发送端连续收到三次相同的应答时就会触发快重传,而不像超时重传一样需要通过设置重传定时器,在固定的时间后才会进行重传。
-
当某一段报文段丢失之后,发送端会一直收到 1001 这样的 ACK,就像是在提醒发送端 “我想要的是 1001” 一样。
-
如果发送端主机连续三次收到了同样一个 "1001" 这样的应答,就会将对应的数据 1001 - 2000 重新发送;
-
这个时候接收端收到了 1001 之后,再次返回的 ACK 就是 7001 了(因为 2001 - 7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中。
总结一下也就是:最左侧报文丢失
a.确认序号规定的约束,滑动窗口左侧不动
b.快重传&&超时重传,对最左侧报文进行补发
b-c.中间报文丢失,最右侧报文丢失
那就会变成最左侧报文丢失,重新执行上面的操作
既然有了快重传,为什么还要有超时重传?
因为快重传是有条件的,必须收到连续三个以上的同样的 ACK。快重传和超时重传不是对立的,而是协作的。
7.拥塞控制
1000个报文中丢掉一两个很正常,重新补发即可,但1000个报文中有999个都丢了,那么我们还需要选择重传吗?
【举例】:一个班25个人考试,只有一个人挂科了,那么就是这个人的问题;而如果只有一个人通过了,剩下24个人都挂科了,那么还是学生的问题吗?显然不是,是试卷出了问题
虽然TCP 有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据.但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题.
因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵.在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的.
对于网络拥塞的学习一定要有全局的认识,一定不能只考虑两端的主机,一定要考虑其他主机之间也有可能使用网络通信
TCP引入慢启动机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据;
拥塞窗口
拥塞窗口(Congestion Window,简称 cwnd)是网络传输中用于控制数据流量的一个重要参数,特别是在传输控制协议(TCP)中。拥塞窗口的主要作用是防止网络拥塞,通过限制可以发送的数据量来动态调整网络负载。
我们说客户端一次性并行发送给客户端数据的大小是对方接收缓冲区大小是暂时定义的;现在我们也要考虑网络的情况,网络一次性能够发送的数据大小,我们称为拥塞窗口
那么我们需要重新定义一下滑动窗口,滑动窗口的大小应该是对方接收缓冲区大小与网络能够一次性发送数据的大小的较低值
发送开始的时候,定义拥塞窗口大小为1
每次收到一个ACK应答,拥塞窗口加1
每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口
滑动窗口大小 = min(拥塞窗口,对方窗口大小[接收能力])
像上面这样的拥塞窗口增长速度,是指数级别的,“慢启动”只是指初始时慢,但是增长速度非常快
因为网络的状况是浮动的,所以拥塞窗口的大小,也必然是浮动的,主机应该怎么样才能得知,拥塞窗口的接近大小应该是多大??必然经过多轮尝试,才能知道
为了不增长的那么快,因此不能使拥塞窗口单纯的加倍
此处引入一个叫做慢启动的阈值(拥塞阈值(ssthresh))
当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长
前期慢,可以慢慢减少网络发送,让网络恢复
网络恢复,我们的通信过程也要恢复起来,中后期增长快
-
当 TCP 开始启动的时候,慢启动阈值等于对方窗口大小的最大值。
-
在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回 1,如此循环下去
-
少量的丢包仅仅是触发超时重传,大量的丢包就认为网络拥塞。
当 TCP 通信开始后,网络吞吐量会逐渐上升,随着网络发生拥堵,吞吐量会立刻下降。拥塞控制归根结底是 TCP 协议想尽可能快的把数据传输给对方,但又要避免给网络造成太大压力的折中方案
8.延迟应答
延迟应答是一种策略,允许接收方在收到数据包后,延迟发送确认(ACK)消息。这种延迟通常是在接收到多个数据包后,合并这些ACK消息,从而减少网络中小包的数量和拥塞。
还记得我们的接受缓冲区吗?当我们一次收到很多个数据的时候,接收端并不立刻作出回答,而是等一会,这样上层就能够多读一段时间,这样接收端发送应答的时候有可能给对方通告一个更大的接受窗口;所以延迟应答能够提高传输效率
如果接收数据的主机立刻返回 ACK 应答,这时候返回的窗口可能比较小。假设接收端缓冲区为 1M,一次收到了 500K 的数据。如果立刻应答,返回的窗口就是 500K。但实际上可能处理端处理的速度很快,10ms 之内就把 500K 数据从缓冲区消费掉了。在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些也能处理过来。如果接收端稍微等一会再应答,比如等待 200ms 再应答,那么这个时候返回的窗口大小就是 1M。
一定要记得。窗口越大,网络吞吐量就越大,传输效率就越高,我们的目标是在保证网络不拥塞的情况下尽量提高传输效率
那么所有的包都可以延迟应答吗?肯定也不是
数量限制:每隔N个包就应答一次
时间限制:超过最大延迟时间就应答一次
具体的数量和超时时间,依操作系统不同也有差异;一般N取2,超时时间取200ms
9.捎带应答
在延迟应答的基础上,我们发现很多情况下,客户端服务器在应用层也是“一发一收”的。意味着客户端给服务器说了“How are you”,服务器也会给客户端回一个“Fine,thank you”
那么这个时候ACK就可以搭顺风车,和服务器回应的“Fine thank you”一起回给客户端
其实我们在三次握手的时候就见到了,服务端把ACK和SYN一起发送给客户端
10.面向字节流
TCP中的“面向字节流”(Stream-oriented)特性是TCP协议的一个核心概念。它决定了TCP协议如何传输数据,确保数据流在传输过程中保持顺序一致且完整。
创建一个TCP的socket,同时在内核中创建一个发送缓冲区和一个接收缓冲区
调用 write 时,数据会先写入发送缓冲区中。
如果发送的字节数太长,会被拆分成多个 TCP的数据包发出。
如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去。
接收数据时,数据也是从网卡驱动程序到达内核的接收缓冲区。
然后应用程序可以调用read 从接收缓冲区拿数据。
另一方面,TCP的一个连接既有发送缓冲区,也有接收缓冲区。那么对于这一个连接既可以读数据,也可以写数据,这个概念叫做全双工。
由于缓冲区的存在,TCP 程序的读和写不需要一一匹配,例如:
写 100 个字节数据时, 可以调用一次 write 写 100 个字节,也可以调用 100 次 write,每次写一个字节。
读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次 read 一个字节,重复 100 次
两个概念:
字节流传输: TCP将应用层的数据视为连续的字节流,而不是一个个离散的数据包。也就是说,TCP并不会关心应用层发送的数据是文本、文件、图片还是其他数据,而是将其当作一串字节流来处理。
无消息边界: 不同于面向消息的协议(如UDP),TCP并不在数据传输时划分消息边界。应用层传输的数据无论大小如何,TCP都将其视为一个字节序列。
工作原理
分段处理: 虽然TCP将数据视为一个连续的字节流,但实际上传输时会将字节流拆分为多个TCP段(Segment),每个段都有一个序列号,用于确保字节的顺序。
序列号(Sequence Number): 每个TCP段的头部包含一个序列号,标识该段在字节流中的位置。这使得接收方能够根据序列号将数据重新排列成原始顺序,即使这些段在传输中出现乱序。
流量控制和确认机制: TCP通过流量控制和确认机制(ACK)来管理字节流的传输。接收方会确认每个字节序列,使发送方知道哪些数据已经成功接收,哪些数据需要重传。
滑动窗口: TCP使用滑动窗口机制来管理数据流的发送与接收。窗口的大小可以动态调整,以适应网络的当前状况,从而在保证数据流连续性的同时,避免网络拥塞。
优点:
可靠性: 面向字节流的传输方式保证了数据的顺序和完整性。即使数据在网络中出现丢失或乱序,TCP能够通过重传和重新排序机制确保数据正确到达接收方。
顺序一致: TCP确保接收方以应用层发出的顺序接收到数据,避免了数据错乱问题。
适用于长时间传输的连接: 面向字节流的特性使TCP非常适合用于需要长时间稳定连接的应用,如文件传输、网页浏览等。
缺点:
没有消息边界: 由于TCP是面向字节流的,没有明确的消息边界,因此应用层需要自己处理数据边界。这意味着如果应用层发送的是一条条消息或数据块,接收方需要能够识别这些数据块的边界,这通常通过在数据中添加分隔符或消息长度来实现。
相对较大的开销: 为了保证可靠的字节流传输,TCP需要额外的确认、重传和流量控制机制,这增加了传输的复杂性和开销。
应用场景
文件传输: 如FTP(文件传输协议),TCP确保文件的完整性和顺序,适合于需要精确、完整数据传输的场景。
网页浏览: HTTP协议通常使用TCP传输网页内容,确保页面元素按顺序加载并显示给用户。
电子邮件传输: SMTP(简单邮件传输协议)通常使用TCP连接来传输邮件内容,确保邮件的完整性。
11.粘包问题
粘包问题是TCP协议中常见的问题,通常发生在接收方读取数据时。由于TCP是面向字节流的协议,数据在传输过程中没有明确的消息边界,导致多个消息可能在接收端被粘连在一起,难以区分。:
1. 粘包问题的成因
粘包问题主要由以下原因引起:
-
TCP的面向字节流特性:TCP将数据视为连续的字节流,而非独立的消息块,因此在接收时无法自动分辨消息的边界。
-
Nagle算法:TCP中的Nagle算法会在网络负载较高或数据量较小时,将小的数据包合并为一个大的数据包发送,进一步导致粘包问题。
-
发送速度过快:如果发送方在短时间内发送了多条消息,接收方在处理不及时的情况下,可能会将多条消息粘在一起。
2. 粘包和拆包的定义
-
粘包:多个小消息被合并为一个数据包发送,导致接收方在读取时发现接收的数据包含了多条消息。
-
拆包:一个大消息被拆分成多个数据包发送,导致接收方收到的数据是一个不完整的消息,需要将多次接收到的数据拼接起来。
3. 粘包问题的影响
粘包问题会导致接收方无法正确地解析消息内容,从而引发数据错乱。例如,假设发送方发送了两条消息“Hello”和“World”,接收方可能收到“HelloWorld”这一条粘连的信息,如果没有适当的处理,将无法正确识别消息边界。
4. 粘包问题的解决方法
针对粘包问题,常见的解决方案如下:
方法一:使用分隔符
在每条消息的末尾添加一个特殊的分隔符(如换行符、特殊字符),接收方根据分隔符来区分消息边界。这种方法适合于消息结构简单、长度不固定的应用。
-
优点:实现简单,只需定义一个不会出现在数据中的分隔符。
-
缺点:需要确保数据内容中不会包含分隔符,以免误判消息边界。
方法二:定长消息
在发送消息时,确保每条消息的长度是固定的,接收方可以按固定长度读取数据,无需额外的边界标记。这种方法适合于消息长度恒定的情况。
-
优点:实现简单,接收方读取定长数据即可。
-
缺点:不适用于长度不固定的数据,浪费带宽。
方法三:消息头包含长度信息
在每条消息的头部添加一个固定长度的消息头,用来指示该消息的总长度。接收方根据消息头读取指定长度的数据,确保能够准确接收完整消息。这种方法适合于长度可变的消息传输。
-
优点:适用于长度不固定的消息,灵活且可靠。
-
缺点:需要在每条消息中加入长度信息,稍微增加了消息的开销。
方法四:应用层协议自定义
通过设计应用层协议,制定数据传输的规则,以确保数据能够准确传输。例如,在协议中定义具体的字段来标识消息的起始和结束,确保接收方能够正确解析。
5. 应用场景示例
-
文件传输:文件内容一般较大,且大小不固定,因此会采用带长度信息的消息头或应用层协议,确保文件数据的正确性。
-
消息传输:在即时通讯系统中,消息内容长度不一,通常会采用分隔符或长度标识的方法,避免信息混淆。
思考
对于UDP协议来说,是否也存在“粘包问题”?
对于UDP,如果还没有上层交付数据,UDP报文长度仍然在,同时,UDP是一个一个把数据交付给应用层,就有很明确的数据边界
站在应用层角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会出现“半个”的情况
12.TCP异常情况
在TCP传输过程中,可能会遇到多种异常情况。这些异常会影响数据的正常传输,甚至导致连接中断。
在TCP连接中,如果一个已经建立连接的进程突然挂掉(比如因为软件崩溃、硬件故障或被强制终止),TCP协议提供了一些机制来处理这种情况:
-
被动关闭:挂掉的进程无法主动发送TCP报文来关闭连接。在这种情况下,如果另一个进程继续发送数据,最终会因为无法得到确认(ACK)而触发超时重传机制。在多次尝试失败后,通常TCP实现会认为连接已失效,并开始终止连接的过程。
-
TCP保活(TCP Keepalive):许多TCP实现包括一个保活机制,这允许系统定期探查空闲的连接以确定对端是否仍然可用。如果一个保活探查没有得到响应,那么连接最终会被认为已经中断,并予以关闭。
-
对端检测到连接中断:如果挂掉的进程是服务器,而客户端具有检测到这种情况的逻辑(例如通过心跳包或某种形式的轮询),客户端可以在检测到服务器无响应后主动关闭连接。
以下是TCP连接可能发生的情况:
-
RST(Reset)报文:如果挂掉的进程是在监听端(比如服务器),其操作系统可能会发送一个RST报文给客户端,立即终止连接。客户端在接收到RST后,会意识到连接已经中断。
-
超时:如果没有任何RST报文发送,另一端的进程可能会在尝试发送数据并等待确认时超时。在经过一定数量的重传尝试后,TCP栈通常会放弃,并通知应用层连接已断开。
-
半开连接:在某些情况下,可能会出现半开连接,即一端已经挂掉,而另一端仍然认为连接是开放的。这种状态不会持续很长时间,因为如前所述,最终另一端会通过超时或其他机制检测到连接问题。
另外,应用层某些协议,也有一些这样的检测机制,例如HTTP长连接中,也会定期检测对方的状态;例如QQ,在QQ断线之后,也会定期尝试重新连接
13.TCP小结
为什么TCP这么复杂?因为要保证可靠性,同时又尽可能的提高性能
可靠性 | 提高性能 |
---|---|
校验和 | 滑动窗口 |
序列号(按序到达) | 快速重传 |
确认应答 | 延迟应答 |
超时重发 | 捎带应答 |
连接管理 | |
流量控制 | |
拥塞控制 |
还有定时器(超时重传定时器,保活定时器,TIME_WAIT 定时器等)
基于TCP应用层协议
-
HTTP
-
HTTPS
-
SSH
-
Telnet
-
FTP
-
SMTP
当然,也包括自己写的TCP程序时自定义的应用层协议
TCP / UDP 对比
TCP 和 UDP 之间的优点和缺点, 不能简单, 绝对的进行比较。
-
TCP 用于可靠传输的情况,应用于文件传输,重要状态更新等场景。
-
UDP 用于对高速传输和实时性要求较高的通信领域。例如,早期的 QQ,视频传输等,另外UDP可以用于广 播。
归根结底,TCP 和 UDP 都是程序员的工具,什么时机用,具体怎么用,还是要根据具体的需求场景去判定。