目录
前言
TCP协议格式
确认应答机制(ACK)
理解可靠性
确认应答的机制
16位窗口大小
缓冲区
流量控制
6个标志位
16位紧急指针
★三次握手,四次挥手
如何理解连接
如何理解三次握手
如何理解四次挥手
TCP可靠性机制
确认应答机制(补充)
编辑 超时重传机制
流量控制机制(补充)
滑动窗口
滑动窗口工作流程
常见滑动窗口问题
快重传
拥塞控制
延迟应答
捎带应答
TCP小结
理解面向字节流
粘包问题
前言
上一章我们讲解了UDP协议,了解了UDP协议的格式及作用。本文章将详细讲解TCP协议的相关内容,TCP全称为 "传输控制协议,人如其名, 要对数据的传输进行一个详细的控制。TCP协议不仅是计算机网络的重要内容,更是面试频率非常高的一类内容,尤其是其中的三次握手,四次挥手然后衍生出来的各种问题。本文将详细介绍TCP协议格式,如何建立TCP连接,以及是如何保证可靠性和性能的各种策略等等,内容很多,耐心阅读完收获一定很大。
TCP协议格式
在进行任何操作之前,都需要先了解TCP协议的报头格式。图片如下:
- 源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去;
- 32位序号/32位确认号: 后面详细讲;
- 4位TCP报头长度: 表示该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部最大长度是15 * 4 = 60。(上面讲的)
- 6位标志位:
- URG: 紧急指针是否有效
- ACK: 确认号是否有效
- PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
- RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
- SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
- FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段
16位窗口大小: 后面再说
16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分.
16位紧急指针: 标识哪部分数据是紧急数据;
40字节头部选项: 暂时忽略
这就必须得回答两个必须的问题:
a.如何交付给上一层 b.如何解包
a.这个简单,因为TCP报头中含有16位目的端口号,可以根据端口号找到上一层的进程然后交付。
☆b.TCP的报头是变长报头,因为有选项的加入。每次我们先提取前20个字节,然后我们根据提到的报头中的 4位首部长度来得到整个报头(包括选项)的大小。
但是4位首部长度能表示的范围就是0000-1111,即0-15个字节的长度,连自身那20个字节都 放不下,怎么放选项呢?
其实,规定4位首部长度的单位是4字节,而不是一字节。即0001代表的是4字节,而不是1字节了这样4位首部长度 能表示的范围是0-60个字节了。而由于报头最少有20字节,所以4位首部长度实际代表的范围是[20-60].
然后用4位首部长度代表的字节数-20字节就是选项的大小,然后解析选项,剩下的就是数据了。
所以总结就是:
- 提取前20个字节
- 根据标准报头,提取4位首部长度 * 4 - 20,设为res
- a.若res == 0,说明没有选项,此时就已经读取完成了 b.res > 0,说明有选项,此时再读取选项即可。
- 读完了报头,剩下的就都是 有效载荷了。
确认应答机制(ACK)
理解可靠性
举个例子,两个人A和B在通话,当A给B说:“你吃了吗?”,B如果此时回答说“我吃了”,然后A听到B说的后,就知道B一定收到了我说的话,因为他给了我一个确认的应答.
但是B并不知道自己说的“我吃了”,A有没有成功收到,如果此时A再说“那你吃的什么呢”,此时B便可以确定A一定收到了自己的说的话,因为收到了应答。
但此时A并不知道自己说的“那你吃的什么呢”被对方收到了。
所以无论是A和B,都无法保证最新发送数据的一方,发送的数据一定被对方收到。
但是在局部上,我们能做到100%可靠。
因为所发出的所有消息,只要有匹配的应答,我就能保证刚发送出的消息一定被对方收到了。
所以TCP协议的确认应答机制:只要一个报文收到了对应的应答,就能保证我发送的数据对方一定收到了。
确认应答的机制
确认应答机制(ACK)是计算机网络中一种通信机制, 在TCP通信中,当发送方向接收方发送数据时,发送方会等待接收方返回一个确认应答(ACK)来确认已成功接收到数据。
实现的方式:序号和确认序号保证了 响应应答针对的是哪一条消息的应答.
序号和确认序号:
序号字段指示TCP数据报中数据部分的第一个字节的序号,用于对数据进行编号和排序
确认序号字段表示发送方期望下一个收到的字节的序号
实现TCP的可靠传输和流量控制:
1.保证请求和应答进行一一对应.
2.确认序号:确认序号之前的数据已经全部收到。(比如我发送了1000,2000,3000,然后接收方发送了确认序号1001,2001,3001,如果最后发送方只收到了3001,那么代表3001之前数据已经全部收到了,这是确认序号的含义所确定的)
3.允许部分确认丢失,或者不给应答。
确认部分丢失:例如发送方发送了1000,2000,3000,然后接收方接收到了并应答给发送方,发送方最后只收到了3001,但此时也没有问题 ,因为这代表3001之前的数据已经全部收到了
不给应答:当发送方发送了1000,2000,3000给接收方,但接受方只收到了1000,3000,此时只能给发送方1001应答,不能给3001应答,因为没有收到2000,
4.为什么要有两个字段(序号和确认序号两个字段)?
我们发送的时候用的是序号,接收的时候是确认序号。为什么要用两个呢,一个不就行了吗,将发送的这个序号作为确认序号不也可以吗?
我们要知道TCP是全双工的,任何一方,即可以收,也可以发!如果不加以区分,会使发送的数据和接收的数据混在一起。比如发送方可能把接收方发送的数据当做一个应答,或把对方的应答当做一个发送的数据。
5.如何保证顺序,比如发送方发送123,收方可能收到231?任何一方都会收到报文,报文中会携带序号,收到后会进行排序重组保证数据的顺序。确保数据按正确的顺序交付给应用层。
16位窗口大小
16位窗口大小表示接收方在接收数据时的可用缓冲区大小。该字段指示了发送方在不需要等待确认应答的情况下,可以连续发送给接收方的数据的最大量。
缓冲区
首先TCP也是全双工的,即发送数据的同时也可以接收数据,
TCP发送端和接收端都有发送缓冲区和接收缓冲区,而UDP是没有发送缓冲区的。
流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应.造成效率的下降。
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
每次发送数据,接收缓冲区都会被填充,接收缓冲区剩余的空间大小便是16位窗口大小。
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "16位窗口大小" 字段, 通过ACK端通知发送端;
- 窗口大小字段越大, 说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端,发送端接受到这个窗口之后, 就会减慢自己的发送速度;
- 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.
6个标志位
在TCP协议中,TCP报文头中包含了一些标志位(Flags),用于在通信过程中传递特定的控制信息和状态。
一共有6个标志位,分别是URG,ACK,PSH,RST,SYN,FIN. 先来说几个常见的标识:
- ACK:确认应答标志位。凡是该报文具有应命苦教育难题互不干扰v反而多生娃钱啊答特征,该标志位都会被设为1。大部分网络报文该标志位都被设置为1的,除了第一个链接时的请求。
- SYN:建立连接请求。当SYN标志位被设置时,它指示发起连接的一方(通常是客户端)希望建立连接,并初始化序号。
- FIN:断开连接请求。当FIN标志位被设置时,它指示发送方希望关闭连接,不再发送数据。
- URG:表示紧急指针字段是否生效。当URG标志位被设置时,它指示报文段中的一部分数据被标记为紧急数据,需要尽快处理。
- PSH:表示接收方应该立即将数据交给上层应用程序而不需要等待缓存填满。PSH标志位的设置告知接收方应该立即处理接收到的数据,而不进行缓存。
- RST:表示重置连接。当RST标志位被设置时,它指示中断和重置当前的连接。通常在检测到丢失的数据或其他错误情况下使用。
这些字段具体的作用我们在后面的机制中会逐渐用到。
16位紧急指针
16位紧急指针和URG标志位联系在一起。
当URG标志位被设置时,代表紧急指针有效,表示该报文数据段中有部分数据被标记位紧急数据,需要尽快优先处理。
紧急指针: 紧急指针是TCP报文头中的一个字段,占用16位,用于指示紧急数据在数据段中的偏移量位置。它表示从报文的起始位置开始,紧急数据的偏移量.
当发送方发送设置URG标志位的数据段时,紧急指针指示在数据段中紧急数据的位置。接收方在接收到含有URG标志位的TCP数据段时,可以通过检查紧急指针来确定从哪里开始处理紧急数据。
例如正文有100个字节的数据,紧急指针偏移量为50,那么接收方读取紧急数据时会从第50个字节的位置开始读取。
★三次握手,四次挥手
如何理解连接
因为有大量的client将来可能连接server,所以server一定会有大量的连接,操作系统OS要管理这些连接,按之前说的: 先管理,再组织。把链接抽象为数据结构,然后以某种数据结构管理起来。
所谓的连接:本质就是内核的一种数据结构,建立连接成功的时候,就是在内存中创建对应的连接对象。然后对这些对象进行某种数据结构比如链表式的组织。
所以维护这些连接都是有成本的(内存+CPU)。
如何理解三次握手
三次握手:客户端先向服务端发送标志位为SYN的请求,服务端收到后,向客户端发送SYN(连接)+ACK(确认)请求,此时客户端已经认为建立连接完成(认为服务端已经可以正常接收和发送数据了),然后向服务端再次发送ACK(确认服务端发来的SYN连接请求)应答。
流程图如下:
这里有个问题,为什么一定要三次握手,一次,两次不可以吗?五次呢?
这里有两点:1.为了验证双方的全双工,确保双方能够正常的发送和接收数据.
在第一次握手中,客户端向服务器发送SYN,如果服务器能够接收到该SYN并回复SYN+ACK,便可以确定客户端的发送和接收能力正常(因为客户端发送后SYN后,收到了服务端的ACK确认,但不可以确定服务端是否正常,因为发送SYN后,还没有收到客户端的ACK应答); 当客户端给服务端ACK请求并被服务端收到后(服务端收到了ACK确认),说明服务器的发送和接收能力正常。
2.为了服务器的安全
如果只有一次握手,那么可能会服务器可能会遭受攻击。比如一台主机同时大量的向服务端发送SYN请求,而我们一开始也说了,这些连接 是需要被维护的,而维护需要占用资源的。所以这样会大量的占用服务器资源,造成瘫痪,所以不可取。这便是SYN洪水攻击。
如果只有两次握手,第一次握手服务器连接建立好后,第二次向客户端发送请求,而客户端则可以直接把连接丢弃,这样就造成了服务器还在维护着连接,而客户端没有连接,依然可以大量的发送SYN连接请求,也不太可行.
只有三次握手时,当客户端也确认连接后,才正常进行连接。这样客户端便承担与服务端等价的资源消耗。每当客户端发送一个连接请求,必须客户端自己也与服务端成功连接后,服务端再进行维护。这样虽然服务端也维护这些连接,但自己主机上也在维护相同数量的连接。所以这样便可以有效的防止攻击。
这里有一个问题,当第二次握手完成后,客户端便会认为自己已经建立完毕,开始发送数据了。与此同时,客户端向服务端发送ACK报文,但如果此时由于一些原因,服务端没有收到这个ACK,服务端便不能建立连接。服务端没有建立连接,而客户端认为自己连接已经建立完毕了,开始发送数据,这肯定会造成问题了。
所以此时服务端会向客户端发送带有RST(重新连接)的标志位的报头,表示重新与客户端建立连接,此时客户端与服务端便重新开始三次握手。
还有一种情况,假设接收端的接收缓冲区已经满了,无法再接收数据了。此时便会将16位窗口大小设置为0,告知发送方不要发送消息了。
然后每隔一段时间,发送方每次发送一个空数据的报文,来检测接收方的 是否可以接受数据,如果接收方接收缓冲区总是满的,不能接收消息。这个时候,便向接收方发送一个带有PSH标志位的,告诉对方尽快将当前数据交付给上层。
如何理解四次挥手
四次挥手是指终止一个TCP连接的过程。它是TCP协议中用于正常关闭连接的机制。下面是四次挥手的具体流程:
-
客户端发送连接断开报文: 客户端首先向服务器发送一个连接连接报文,标志位中设置 FIN(Finish)标志,表示客户端不再发送数据。然后进入 FIN_WAIT_1 状态,等待服务器的确认。
-
服务器发送确认报文: 服务器收到客户端的连接断开报文后,会发送一个确认报文作为响应。服务器的确认报文中的标志位包括 ACK。ACK表示确认收到了客户端的连接断开报文,服务器进入 CLOSE_WAIT 状态。
-
服务器发送连接断开报文: 服务器确认终止与客户端的连接后,会发送一个连接断开报文,标志位中包含 FIN 标志。这表示服务器不再发送数据。服务器进入 LAST_ACK 状态,等待客户端的确认。
-
客户端发送确认报文: 客户端收到服务器的连接断开报文后,会发送一个确认报文作为确认。ACK 标志表示客户端确认收到了服务器的连接释放报文。这样,客户端和服务器都知道对方已确认关闭连接。
同时, 一旦客户端发送确认报文,就进入 TIME_WAIT 状态,等待可能出现的延迟报文的到达。在等待一段时间后,若没有收到报文,则客户端关闭连接,完成整个四次挥手过程。
为什么需要TIME_WAIT?
我们知道当主动断开连接的一方,当完成三次挥手时,会进入TIME_WAIT状态,等待一段时间后再关闭,这是为什么呢?
我们要知道,虽然四次握手完成时,但是主动断开连接的一方要维持一段时间的TIME_WAIT状态,在该状态(TIME_WAIT)下,连接已经释放,但是地址信息ip,port依旧是被占用的。
TIME_WAIT状态在TCP协议中的作用是确保可靠地关闭TCP连接。
当TCP连接的一方(通常是主动发起关闭的一方)发送最后一个ACK确认报文后,它会进入TIME_WAIT状态,并等待一段时间(通常是2倍的最大报文段生存时间(MSL))。在这个时间段内,该连接仍然可以接收到延迟的报文。
MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2MSL的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到
来自上一个进程的迟到的数据, 但是这种数据很可能是错误的,如下第2点)TIME_WAIT状态的存在有以下几个主要原因:
确保对端收到最后的确认: 在TCP的四次挥手过程中,最后一次ACK确认报文可能会在网络上延迟或丢失。如果发送方立即关闭连接,这个确认可能不会到达对端,导致对端误认为连接没有正常关闭。通过进入TIME_WAIT状态,发送方可以等待一段时间,以确保对端收到最后的确认。
处理网络中滞留的报文: 在网络中,报文可能会因为网络延迟、路由器缓存或重传等原因而滞留一段时间。如果关闭连接后立即重新创建相同的IP地址和端口号的连接,就有可能接收到之前滞留的报文,这些报文可能会被错误地解释为属于新连接的数据。通过进入TIME_WAIT状态,可以排除滞留报文对新连接的干扰。
确保连接的唯一性: TIME_WAIT状态还可以确保在此状态下的连接的唯一性。在TIME_WAIT状态期间,对应的TCP连接四元组(源IP、源端口、目的IP、目的端口)会被保留一段时间。这样可以防止新建立的连接与该连接的四元组相同,避免可能的混淆和冲突。
虽然TIME_WAIT状态会占用一些资源,并延迟释放连接所使用的端口,但它是TCP协议中的重要机制,有助于维护连接的可靠性、完整性和唯一性。
还有一个问题比如服务器突然挂掉了,也就是服务器是主动断开连接的一方,这个时候服务器会进入CLOSE_WAIT状态,必须等待一段时间后,才能进行重新连接,因为在该状态下此时ip和端口是被占用的!会导致bind失败,服务器也不能重新连接上。如果此时正值双十一等消费量很高的节日,这点时间造成的损失可是很惨重的。所以为了可以在TIME_WAIT状态下立即重启,而不要等待时间结束,需要使用setsockopt().
setsockopt()
设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个
socket描述符.
也就是可以利用setsockopt()暂时救急,服务器可以利用原来的ip地址和端口迅速重启,因为原来的ip地址和端口处于TIME_WAIT状态,是不可用的!
TCP可靠性机制
确认应答机制(补充)
这个内容文章上面大部分已经介绍了,这里再对内容做一些解释:
数据段:是由TCP协议头部和数据部分组成.
我们前面说了,TCP通信会将序号发送给对方,然后对方再返回给自己确认序号。那这个序号到底是什么呢?
我们每次向对方发送消息,实则是把数据先拷贝到自己的发送缓冲区,然后再发送给对方的接收缓冲区。 这个缓冲区可以看做是一个char类型的数组,会对数据中的每个字节进行编号。
32位序号是用来标识TCP数据流中的每一个字节的序号。
比如我想发送100字节的数据,那么会分别对每个数据进行编号,直到100。接收方收到100后,会向发送方发送101的确认序号,表示期望发送方下一次发送的序号位置从101开始。
超时重传机制
超时重传机制是在TCP协议中用于处理数据包丢失的一种机制。当发送方发送一个数据段后,会启动一个计时器(称为重传计时器),等待接收方发送对应的确认序号。如果在计时器超时之前未收到确认,发送方会认为数据包丢失,并重新发送该数据包。
数据包丢失会有两种情况:一是发送方发送的数据丢失,导致接收方没有收到数据无法应答;二是接收方发送数据丢失,导致发送方收不到ACK,确认序号等信息。
对于第一种情况,若规定时间内没有收到数据,发送方重新发送数据即可。
对于第二种情况,接收方已经收到了数据,但是ACK确认丢失了,此时发送方又会重新发送相同的数据,会导致服务端收到重复的数据,服务端需要进行去重,这么该如何解决呢?
此时我们上面说的序列号便发挥了作用,服务端会根据收到的序列号进行接收。若是已经拥有的序列号,则直接丢弃这部分数据,便完成了去重。
那么, 如果超时的时间大小是多少合适呢?
最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回".但是这个时间的长短, 随着网络环境的不同, 是有差异的.
如果超时时间设的太长, 会影响整体的重传效率;
如果超时时间设的太短, 有可能会频繁发送重复的包.
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间,例如500ms.
如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
累计到一定的重传次数,若依然没有应答, TC则P认为网络或者对端主机出现异常, 强制关闭连接.
流量控制机制(补充)
这个我们在上面也基本上全部说明了,可以直接向上翻查找。这里再回答一个问题:
在进行流量控制的时候,第一次力我们是如何得知对方的接收能的(接收缓冲区剩余空间大小)?
假设第一次发送数据时,还不知道对方的接收能力,直接发送超过对方接收能力的数据怎么办?
其实,我们要知道第一次发送数据不等于第一次发送报文!在通信前 三次握手的时候,会交换双方的报文,其中就会有对方的接收缓冲区大小,即16位窗口大小。因此在TCP三次握手时,它们就已经得知了双方的接收能力。
当对方接收缓冲区 满时,已经不能再接受数据后,会用两种方式来检测通知。
1.每隔一段时间,发送方会发送一个窗口探测的包,用来检测对方接收缓冲区是否已经可以接收数据。
2.当接收方接收缓冲区已经可以接受数据后,会向发送方发送一个窗口更新通知,告知对方已经可以发送数据。
★滑动窗口
滑动窗口工作流程
上面我们讨论了确认应答机制, 对每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段.这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候.
既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了).
那么如何控制发送的数据呢? 这里就用到了滑动窗口。
滑动窗口是TCP协议中的一种机制,用于控制发送方和接收方之间的数据传输。它是一个动态的数据缓冲区,用于管理已经发送但未确认的数据序列号.
窗口大小指的是 已经发送但未确认的数据的 最大值.(目前认知,后面拥塞控制纠正)
也就是滑动窗口想要:1.即要给对方多发送数据;2.又要保证对方来得及接收。
发送的数据太多,对方来不及接收。需要控制发送尽量多的数据但在对方的接收能力范围之内。
所以滑动窗口的本质:发送方,可以一次向对方推送数据的上限。
这个上限是由对方的接收能力决定的。
过程是:
发送四个数据段(不一定是四个数据段,看具体情况)的时候, 不需要等待任何ACK, 直接发送;
收到ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只有确认应答(ACK)过的数据, 才能从缓冲区删掉,即滑动窗口在自己的发送缓冲区中,属于自己的发送缓冲区一部分。
窗口越大, 则网络的吞吐率就越高,因为可发送的数据越多。
常见滑动窗口问题
上面说了滑动窗口本质上是发送方一次向对方发送数据的上限,这个上限大小即窗口大小实则是由两个指针维护的,win_start指向滑动窗口起始位置,win_end指向滑动窗口结束位置,其中win_end=win_start + 对面的接收能力。
每次更新时,只需要使win_start = 收到的应答报文中确认序号;win_end=win_start+收到的应答报文中窗口大小.
回答几个问题:
1.每次滑动窗口必须向右移动吗?
答案是不一定。假设对方接收缓冲区一直不向上层交付,导致win_end一直不变,不会向右移动。
2.滑动窗口可以为0吗?
答案是可以,当win_start = win_end时,滑动窗口即为0.通常说明此时对方接收缓冲区是处于一种满的状态。
3.如果没有收到开始的应答报文,而是收到中间的,这可能吗,影不影响?
答案是可能,但不影响。首先中间确认报文中的确认序号保证了在此序号之前的所有报文都已经被成功接收。如果没有收到之前所有的报文是不会发送该确认报文的。
例如滑动窗口现在是1000-5000,我收到了一个确认序号是3001,但没收到确认序号2001.但不影响,这说明3001之前的数据都已经收到了,只不过确认报文丢失了。
(注意,图中的例子和刚举的例子数据不一样,不要混淆了)
4.超时重传,背后的含义:在没有收到应答时,数据都必须被暂时保存起来。
假设真的丢失了1000-2000,但是2001-3000,3001-4000,4001-5000数据都已经收到了,但最终接收方还是只能给你返回1001,因为这一段报文并没有收到,即使后面的全部收到了,也只能返回1001。这个时候如果过了超时时间,便会利用暂时保存的数据进行重发。
快重传
- 当某一段报文段(例如1-1000)丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 "我想要的是 1001"一样;
- 如果发送端主机连续三次收到了同样一个 "1001" 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;
- 这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中;
这种机制被称为 "高速重发控制"(也叫 "快重传").即收到3个同样的确认应答报文时立刻进行重发。
那么既然有了快重传,为什么还要有超时重传呢,快重传不更快呢,为什么还要等?
第一,快重传是在连续收到三个相同应答报文后才触发的机制,如果我只发了两个数据段,那么就没办法进行重传了。
第二,我们的应答报文也有可能会丢失,造成无法及时重传。
拥塞控制
虽然TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 仍然可能引发问题.
我们之前一直解决的是两端的问题,如客户端和服务端的发送和接收问题,但是还没有说它们传输过程中的网络相关问题。
网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据,是很有可能引起雪上加霜的.
TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据
这里我们引用拥塞窗口的概念,拥塞窗口:单台主机一次向网络中发送大量数据时,可能会引发网络拥塞的上限值了。
所以此时滑动窗口值的大小不仅仅再是依靠对方的接收能力,还有拥塞窗口的大小。
滑动窗口的值是取 拥塞窗口 和 对方窗口大小(接收能力)的最小值。
发送开始时,定义的拥塞窗口大小为1
每次收到一个ACK应答,将拥塞窗口+1,然后取拥塞窗口和接收端的窗口大小的最小值作为实际发送的窗口值。
按以上的拥塞窗口增长速度,是指数级别的“慢启动”,初始时满,但是增长速度特别快.
为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.
此处引入一个叫做慢启动的阈值
当拥塞窗口 超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
- 当TCP开始启动的时候, 慢启动阈值每次等于窗口最大值;
- 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1.
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;
当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
拥塞控制, 归根结底是TCP协议想
1.尽可能快的把数据传输给对方 2.但是又要避免给网络造成太大压力的折中方案.
延迟应答
如果接收数据的主机 收到 数据后立刻返回ACK应答, 这时候返回的窗口可能比较小.因为此时上层没来得及接收。返回给对方的接收能力(16位窗口大小)会小。
我们要知道,网络传输中,单次IO的数据越多,吞吐量越高,效率就越高。
- 假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
- 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
- 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
- 如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率.
那么所有的包都可以延迟应答么?
答案肯定不是,有两点:
1.数量限制:每隔N个包就应答一次。假设第一次来了一个包,我先不应答,来第二个包时,我再返回一个应答。在等待第二个包的同时给了将数据交付给上层的时间。
2.时间限制:超过最大延迟时间就应答一次。假设第一次来了一个包,然后过了一段时间,当然不能超过超时重传的时间,再给对方发送一个应答,在这段时间也足以让把数据交付给上层。
具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间(不是超时重传的)取200ms
捎带应答
我们发现客户端和服务器基本上是一发一收。对于每一次发送和接收,都需要给对方发送一个ACK应答,但是每次如果只是单纯的发送ACK应答后,再发送数据,会造成效率低下。所以既然是发消息,我们直接把ACK捎带上即可。即把含有数据的报文中的ACK标志位设置为1.这样就只需要发送一次报文即可(我们前面基本一直是默认这样的)
TCP小结
为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能.
保证可靠性的机制:
- 校验和
- 序列号(按序到达)
- 确认应答
- 超时重发
- 连接管理
- 流量控制
- 拥塞控制
提高性能的机制:
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答
理解面向字节流
我们先来理解下面的过程。
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区.
- 调用write时, 数据会先写入发送缓冲区中;
- 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
- 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
- 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
- 然后应用程序可以调用read从接收缓冲区拿数据;
由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:
- 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;或者因为TCP一些策略中途就发送等等。
- 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read两个字节, 重复50次;
这个和UDP的面向数据报就有区别了,UDP是写几次发几次,就和送快递一样,你发了几件,我这里就收几件,你只要收一件就说明你肯定发了一件。
TCP好比于用水桶接水,不论是用水管,或者用碗 一碗碗的倒,最后只需要的是水桶里的这些水,而不管你怎么完成的。这便是面向字节流。
TCP只管把数据交给对方,至于如何解析这些数据,是上层应用层协议所做的事情,TCP只需要把数据安全可靠的发送给对方就好了。所以有了应用层的http协议等等。
粘包问题
我们上面刚说了。TCP是面向字节流的,接收方收到的是发送方发送的一连串的字节数据,那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包.
那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界
- 对于定长的包, 保证每次都按固定大小读取即可; 比如UDP,报头中有16位UDP长度。可以根据这个来提取数据。
- 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
- 对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序员自己来定的, 只要保证分隔符不和正文冲突即可);
那么UDP存在粘包问题吗?
答案是不存在的,正如刚才所说,UDP报头有16位UDP长度,可以明确数据的总长度,可以清楚的明确数据的边界。使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况。
到这里,TCP协议的相关内容就讲解完了,从TCP协议报头的认识与作用,到后面TCP实现可靠性的各种机制,都进行了非常深入的详解。耐心阅读完本篇一定会收获很大!