前言
我们在网络基础中,谈论过,网络的四层模型是为了解决网络通信的问题而创建的,每一层都会解决一个网络通信中的问题,而协议是解决问题的手段,本次文章讨论的主要——传输层的TCP协议,它作用就是保证数据可靠传输!
注:本文章,量大管饱,满满干货,一键带你认识TCP协议!
TCP协议
什么是TCP协议?
- TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。
三个关键点
- 面向连接
- 可靠的
- 基于字节流
我们接下来将对这三个点进行一一阐述,对这三个点了解,你将熟悉TCP协议!
但首先,先来看看TCP协议的表现形式吧
TCP基本形式
TCP协议位于传输层,上一层是应用层
向下封装的过程中,传输层协议会将应用层传下来的报文当做有效载荷
所以,TCP报文的基本形式,我们只看报头即可
- 源/目的端口号:表示数据是从哪个进程来,到哪个进程去
- 32序号/32确认序号:用来标识TCP报文的唯一性
- 头部长度:表示该TCP报头长度多少字节,单位 :4字节
- 16位窗口大小:略,后面细讲
- 16校验和:
- 16位紧急指针:标识哪部分数据是紧急数据
- 选项:最小0字节,最大40字节,我们暂时不解释它,只知道大小范围就可以
每一个报文都需要解决两个问题
- 如何将有效载荷和报头分离?
- 如何将有效载荷向上交付?
如何将有效载荷和报头分离?
- 看头部长度,TCP报头长度分为固定长度和可变长度,其中固定长度20字节,可变长度为20~40字节,报头中的头部长度的值*4字节,就是整个报头的大小,知道报头大小,读取报头大小后,就到有效载荷了!
如何将有效载荷向上交付?
- 看目的端口号,传输层的上一层是应用层,向上交付就是交付给应用层,实际上是交付给应用层中的某一个进程,而TCP协议中的目的端口号,所代表的就是交付有效载荷的目的进程!
标记位
TCP协议相较于UDP协议来说,更可靠,但更慢,原因在于TCP协议拥有更复杂的机制,这些机制保证了TCP协议的可靠,但也降低了TCP协议的传输速度。
更复杂的机制,其实我们从标记位上就可以看出来
TCP协议一共有六个标记位
每一个标志位都有其特殊的含义与触发的机制
我们暂时先认识一下有哪些标记位
URG、ACK、RSH、SYN、FIN、RST
我们在后续的讲解中再对他们一一细讲,目前我们可以先认识一下URG
- URG:紧急指针是否有效
当URG标志为1的时候,表示当前的TCP报文中携带紧急数据
TCP协议就会查看报头中的紧急指针,迅速找到其在有效载荷的位置
交付有效载荷后,提醒应用层尽快读取紧急数据
目前,认识一下URG就可以了。
TCP协议下的缓冲区
TCP协议掌管数据的发送和接收
它自己会在操作系统内核中专门维护两个缓冲区来方便数据传输和接收
两个缓冲区
- 发送缓冲区(OutBuffer)
- 接收缓冲区(InBuffer)
所以,实际上数据传输和接收过程
- 传输数据:数据先到发送缓冲区中,再将发送缓冲区中的数据发送
- 接收数据:数据先到接收缓冲区中,再从接收缓冲区中取走数据
例子:主机A向主机B发送数据
主机A应用层的数据会先进入TCP协议的发送缓冲区
发送
主机B拿到,先存入TCP协议的接收缓冲区中,再被应用层取走
注意:
- 发送缓冲区满了,发送就会停止发送数据
- 当接受缓冲区空了,接收方就会停止取走数据
TCP协议的可靠性
TCP协议被广泛运用的原因之一,就是它——太可靠啦!
TCP协议的可靠体现在对数据传输的两个方面
- 效率
- 安全
而如何保证数据传输效率,由如何保证数据传输安全?
- 用机制!TCP协议复杂,就在其创建了大量复杂的机制,在保证数据安全的大前提下,不断提高数据传输效率!
接下来,来看看这些机制!
确认应答机制
确认应答机制是什么?
- 通过“如有发送,必有应答”的形式,来知晓通信过程中的数据,是否已经到达目的地的机制
过程:“如有发送,必有应答”
- 发送方发送一个报文给接收方,接收方收到报文后,会发送一个应答报文给接收方
- 发送方收到应答报文,就知道自己的报文已经被接收方收到
- 发送方没有收到应答报文,就知道自己的报文没有被接收方收到
概述过程很简单直接,但实际情况并不是这样的。
网络通信需要考虑诸多因素,如网络环境
网络环境的好坏,会直接影响报文在网络中的传输速度,甚至决定报文能否到达目的地
假设一下:
你短时间内发送两个报文
第一个报文发送时的网络环境很差,后来变好了
第二个报文发送时的网络环境很好,后来变差了
一会后,你只收到了一个应答报文
这个应答报文是第一个报文的,还是第二个报文的?
怎么区分?
- 使用确认序号区分
TCP协议的报头中,有两个序号
- 序号代表正常报文使用的序号
- 确认序号代表应答报文使用的序号
正常TCP报文会携带有效载荷,这些报文报头中的序号会被填充数字,标识唯一性。
应答报文,又称ACK报文,不携带有效载荷,当一个报文为ACK报文的时候,它的六位标志位上的ACK标志会被置为1,代表这个报文是一个应答报文,同时,不会给它填充序号,而是填充确认序号!
讲究就在填充确认序号当中,应答报文是根据自己要答复的报文而产生的,它的确认序号是在自己答复的报文序号基础上加一。
比如
一个正常报文序号为1000
它对应的确认报文的确认就为序号为1001!
如此,发送方就能知晓自己发送的报文,是否被接收方成功接收
如果被成功接收,则正常进行后续通信
如果没有被成功接收,则用其他机制补救。
确认应答机制的作用
- 判断报文是否安全到达,让发送方及时知晓是否发送成功,方便后续应对
序号顺序机制
发送的数据,会先进入发送缓冲区中,再按照先入先出的顺序依次发送
所以发送方应用层的报文发送顺序,就是发送缓冲区的报文发送顺序
也是接收方应用层从接收缓冲区拿取报文的顺序
但并不是网络中的报文到达接收方中的接收缓冲区顺序
为什么?
TCP协议不仅需要报文安全到达,也要报文依次到达,以满足应用层需求。
但报文到达接收区的顺序并不是按照发送顺序到达的,一般是混乱的
- 先发送的,可能后到达
- 后发送的,可能先到达
因此,当接收方从接收缓冲区拿取报文的时候,必须理顺报文的顺序,然后依次拿去
怎么理顺?
依靠报文的序号理顺,报文的序号,是根据报文的发送顺序依次增大的。
如,每个报文大小为1000字节,则第一个报文序号为1000,第二个报文的序号就是2000
所以,在TCP协议控制数据传输中,序号的作用
- 代表报文的发送顺序,帮助接收方按序处理报文
- 为应答报文的确认序号提供参考,帮助发送方知晓报文是否安全
捎带应答机制
在我们目前的认知中认为TCP协议的通信模式是“一问一答”
发送方发一次,接收方必须回答一次
但真的只有这样吗?
现实生活中,我们遇到熟人,会这样打招呼
你问对方:“吃了吗?”
对方会回答:“吃了,你呢?”
“吃了”是“吃了吗”的应答报文,而“你呢?”是对方发送的正常报文
这样一句话中,既回答了对方,又向对方询问,一举两得
而TCP协议中,也有这种发挥类似作用的机制
捎带应答机制
- 即一个报文既可以携带有效载荷,又允许其携带应答
我们之前会很纳闷,为什么报头中,会有一个确认序号和一个序号
当你是正常报文的时候,只会用到序号
当你是应答报文的时候,只会用到确认序号
而当你作为正常报文,又携带应答的时候,就会又用到序号,又会用到确认序号了
这也就是为什么,报头中会有两个作用不同的序号的原因!
捎带应答的作用
- 接收方在接收到数据报文的同时,也收到了ACK报文的确认,从而实现了数据的可靠传输和效率的提升。
流量控制机制
我们知道,当发送缓冲区满的时候,发送方就无法发送数据
而什么时候发送缓冲区会满?
- 接收方的接收缓冲区爆满,导致发送方无法发送报文,报文滞留在发送缓冲区中,一直累积,直到发送缓冲区满!
由此可见,接收方的接收缓冲区会对发送方的发送缓冲区造成影响
一旦接收方的接收缓冲区饱满了,就不会再让发送方的报文到达了
但想一下,有没有这种报文?
历经了重重网络节点,经过多次转发,跨越了千里,最后终于到达接收方的接收缓冲区
结果,报文被告知,这里已经满了,你回去吧!
那我这一路走来的艰辛算什么?
这种报文会存在,而且不少,且我们要明白,每一个网络传输的报文,都是需要消耗网络资源的,而这种报文,消耗了资源,但,竹篮子打水——一场空!
因此,为了避免这种情况发生,TCP协议推出——流量控制机制
定义
- 发送方会根据接收方的接收缓冲区实时大小,来动态调整自己的发送能力,从而保证发送的报文每一个报文都能按要求到达对方的接收缓冲区,不会被拒绝
流量控制机制的关键——动态调整发送能力
而调整的前提,在于知晓接收方的接收缓冲区的大小
如何知晓?
依靠应答报文报头的窗口大小字段!
每次发送一个报文,发送方都会收到一个应答报文
应答报文除了帮助发送方确认报文是否安全抵达,还会告诉发送方——接收方的接收缓冲区的剩余大小。
所以,TCP协议报头中的窗口大小,实际上就是接收方接收缓冲区的大小!
每次发送方收到应答报文的时候,会有两个动作
- 查看确认序号,知晓历史报文是否安全送达
- 查看窗口大小,知晓接收方的接收缓冲区大小,从而动态调整发送能力
延迟应答机制
学习了确认应答机制,我们明白,TCP协议下的通信模式是"一发一答"。
发送方每发送一个报文,接收方收到报文后,都会返回一个应答报文
但应答报文发送的具体时间呢?
- 是报文到达接收缓冲区的那一刻?
- 是接收方从接收缓冲区拿取报文的那一刻?
都不是!
应答报文的发送时间,由TCP协议的应答延迟机制决定!
应答延迟机制概念
- 发送方报文到达接收方缓冲区,一段时间后,再发送应答报文
这个一段时间,我们称之为“延迟时间”
延迟时间,由机制自己决定,一般是根据收到的报文个数来确定,如收到两个报文,就会返回一个应答报文。
为什么要延迟应答,马上应答不好吗?
延迟应答可以提高网络吞吐量,从而提高效率
- 接收端在接收报文的同时,也在处理接收缓冲区内的数据。延迟应答可以让接收端有更多的时间来处理数据,从而增大ACK报文中窗口大小字段的值。窗口越大,网络吞吐量越大,传输效率越高!
超时重传机制
当发送方发送一个报文
- 如果收到接收方的应答报文,代表报文安全送达
- 如果没有收到应答报文,则用其他机制进行补救
补救机制:超时重传机制
- 对可能出现丢失的报文,在一段时间后,让发送方重新发送该报文
超时重传机制工作流程:
- 发送方发送报文,重传机制开始计时。
- 接收方收到报文后,返回确认报文。
- 在重传机制的标准时间内,如果发送方收到应答报文,则继续发送下一个报文。
- 在重传机制的标准时间内,如果发送方仍未收到确认报文,则发送方重传该报文。
重传超时时间(RTO)
重传机制的标准时间,我们称之为:重传超时时间(RTO)
RTO的设定对于机制的效率至关重要。如果RTO设置得过大,发送方需要等待较长时间才能发现报文丢失,从而降低数据传输的吞吐量;如果RTO设置得过小,发送方可能会过早地认为报文丢失并重传,从而造成不必要的网络负载。
TCP协议会采用很复杂的算法不断对RTO进行调整,来适应网络变化,进行合理时间地重传,这里就不多赘述其中的具体细节了......
滑动窗口机制
首先,学习前面的机制,会提出两个问题
- 发送方知晓接收方的接收缓冲区大小,要调整发送能力,具体怎么调整?
- 超时重传,会发送一样的报文,而原本的报文已经发出去了,怎么发一样的?
这两个问题,都可以由滑动窗口来解决!
滑动窗口
TCP协议会为通信双方各自维护发送缓冲区和接收缓冲区
实际上,我们可以将这两个缓冲区,当成线性数组来看待,只不过数组元素是报文
而我们的滑动窗口,实际上就是发送缓冲区数组上的一块区域
这块区域,用两个下标来作为开始和结束
- 我们将滑动窗口的起始下标称为:win_start
- 我们将滑动窗口的终点下标称为:win_end
- 滑动窗口当中的报文,是即将被发送,或者已经发送,但没有被确认的报文
- 滑动窗口左边的报文,则是已经发送过,且被确认的报文
- 滑动窗口右边的报文,则是没有被发送过,将来会被发送的报文
解决问题一:调整发送能力
滑动窗口中的都是要发送,或者已经发送但未确认的报文
也就是说,滑动窗口中的报文都必须到达接收方的接收缓冲区中!
滑动窗口中的每一个报文,都会对接收方的接收缓冲区大小产生影响
因此,调整发送能力,就是调整滑动窗口的大小
- 当接收缓冲区大了,发送能力就应该强,滑动窗口就会变大!
- 当接受缓冲区小了,发送能力就应该弱,滑动窗口就会变小!
解决问题二:重传报文哪找
滑动窗口如何更新?
滑动窗口中的报文,是马上要被发送,或者已经发送,但没有收到确认的报文
当滑动窗口中的一个报文被发送,且收到应答报文
滑动窗口就会更新,向右滑动,将收到应答的报文隔离在滑动窗口的左边
滑动窗口的更新方式像滑动一样,这也是滑动窗口命名的由来
而没有收到应答的报文,则不会引起滑动窗口更新,会一直存在于滑动窗口当中!
一旦超过重传时间,TCP协议就会在滑动窗口中,找到该报文,再次发送!
滑动窗口允许应答丢失
滑动窗口能够提高效率!
如何提高?
滑动窗口中的报文,不是一个一个发送
在接收方缓冲区充裕的情况下,滑动窗口中的报文不是简简单单的一个,而是连续的好几个,这些报文,将在极其短的时间内,一次性发送给接收方,且也会在极端的时间内一起到达
应答报文,也不是一个一个发送,一个一个确认!
接收方收到这些报文后,只会发送最后一个序号的报文的应答报文!
而这最后一个序号报文的应答报文,就代表一次性发送的所有报文,都安全到达!
为什么?
滑动窗口,允许应答丢失!
一个报文,对应一个应答
十个报文,对应十个应答
但实际上,用一个应答报文也能保证十个报文全部安全送达
怎么说?
看图
分析:
- 主机A向主机B一共发送十个报文
- 每收到一个主机A的报文,主机B都会发送一个应答报文
- 每收到一个主机B的应答报文,主机A都知道自己的报文发送成功!
但实际上,最后一个确认序号为10001的报文,就可以告诉主机A,历史报文,均发送成功!
确认序号的两个意义
- 表示序号=确认序号-1的报文,已经收到了
- 表示发送方接下来应该发送确认序号后面的报文,前面的报文已经收到了!
所以,当主机A收到确认序号为10001的报文,就知道序号为10000的报文已经发送成功,接下来只要发送10001后面后面的报文就可以了!前面的不用管,肯定到了!
可以说,当你拿到最后一个序号的确认报文,代表前面的报文应该全部已经安全到达!
如果中间的报文发生丢失呢?
当中间的报文发生丢失,因为滑动窗口会在短时间一次性发送连续的几个报文,一旦接收方发现这些报文发生序号顺序断层,则知道有报文发生丢失
这时候,就会将丢失序号报文后所有的应答报文,一个一个发送给发送方,且这些应答报文的序号的确认序号=丢失序号+1
当收到连续三个确认序号相同的应答报文,发送方就会明白滑动窗口中序号=确认序号-1的报文发生丢失,开始重传!
只有接收方的接收缓冲区中的报文序号连续,不断层后,才会发送正常的应答报文!
拥塞控制机制
目前的网络环境,其实已经相当好了,很少会发生报文丢包的现象。
而即使发生丢包,也会有超时重传进行补救,所以基本不用担心丢包。
那么,网络异常的时候呢?
网络如果异常,会导致报文大面积丢包,这时候,发生方就会检测到自己发送的报文出现大面积丢失,判断网络当前状态为拥塞状态,从而启用拥塞控制措施
拥塞控制措施——慢启动
当发生大面积丢包的时候,发送方判定当前网络拥塞,此时不会盲目地根据接收方缓冲区大小发送报文,而是小心翼翼地,有策略地发送
- 第一次,发送一个报文
- 第二次,发送两个报文
- 第三次,发送四个报文
以此类推,循序渐进地发送报文,如果后续没有发生报文大面积丢失,就继续发,试探网络的底线
我们将慢启动时,每次将要发送的报文数量,称之为拥塞窗口
滑动窗口的大小=min(拥塞窗口大小,接收缓冲区大小)
拥塞窗口的大小呈指数增长
- 为了不让拥塞窗口增长那么快,因此不能使拥塞窗口大小单纯地加倍
- 此处引入一个叫慢启动的阈值
- 当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长
阈值的更新
- 当再次发生大面积丢包,取当前拥塞窗口的大小的值除以2,得到一个新值,将其作为新的阈值,再次慢启动
面向连接
TCP协议规定
- 只有先建立连接,才能进行通信!
什么是连接?
只有先建立连接次,才能通信
但连接是什么?
- 两个端点(通常是两个进程)之间建立的一种虚拟链路,用于可靠地传输数据
特点:
- 全双工的,意味着数据可以在两个方向上同时传输
一个客户端会和一个服务端建立连接
但一个服务器会和大量的客户端建立连接
服务端的操作系统需不需要管理这些庞大数量的连接呢?
要!如何管理?
一个原则:先描述,在组织
- 先描述:连接抽象为内核中的数据结构
- 再组织:将这些数据结构对象用链表或者其他数据结构连接起来
所以,连接的本质,就是内核中的一种数据结构,建立一个连接的时候,就是在内核中创建一个数据结构对象,一旦连接多了,就将这些数据结构对象用某种数据结构,如链表,进行连接。
连接的本质是内核中的一个数据结构,在内核中创建和管理这个数据结构,是要消耗资源的。
即,连接的建立和维护是需要成本的!
三次握手
预备知识:
- SYN报文:主动建立连接的一方,发送的特殊报文,标志位SYN为1,无有效载荷,旨在告诉接收方:“我要建立连接,你知道吗?”
TCP协议为数据传输保驾护航的前提——连接建立
连接建立的前提是需要确认双方的接收能力和发送能力是否正常
- 即, 连接建立的就绪条件:双方的接收能力和发送能力正常!
那么,如何满足前提,建立连接呢?
三次握手!
- 建立连接的通信双方,进行三次报文发送(三次握手),来确认双方的接收能力和发生能力是否正常,依次来建立可靠的连接
过程:客户端主动向服务端建立连接
第一次握手:
客户端发送SYN报文给服务端,服务端收到报文
告知服务端:“我要和你建立连接!”
客户端视角:
- 不知自己的发送能力和接收能力,
- 不知服务端的接受能力和发送能力
服务端视角:
- 知道自己的接收能力正常,但不知发送能力
- 知道客户端的发送能力正常,但不知接收能力
状态变化:
- 客户端:发送报文的那一刻,客户端进入SYN_SENT状态
- 服务端:收到报文后,进入SYN_RCVD状态
第二次握手:
服务端发送发送一个SYN+ACK报文给客户端,客户端收到报文
服务端告知客户端:“我知道了你的建立连接的请求,并且我也要和你建立连接!”
客户端视角:
- 知道自己的发送能力,接收能力正常
- 知道服务端的发送能力,接收能力正常
服务端视角:
- 知道自己的接收能力正常,但不知发送能力
- 知道客户端的发送能力正常,但不知接收能力
状态变化:
- 服务端:无变化
- 客户端:收到报文后,客户端进入ESTABUSHED状态
此时,客户端视角,双方的接收能力和发生能力都正常,客户端的连接就绪条件满足,先建立连接
第三次握手:
客户端发送一个ACK报文,服务端收到报文
告知服务端:“我也知道你的建立连接的请求了!”
客户端视角:
- 自己的接收能力,发送能力正常
- 服务端的接收能力,发送能力正常
服务端视角:
- 自己的接收能力,发送能力正常
- 客户端的接收能力,发生能力正常
状态变化:
- 客户端:无变化
- 服务端:收到报文后,进入ESTABUSHED状态
此时,服务端视角,双方的接收能力和发生能力都正常,服务端的连接就绪条件也满足,建立连接。
这就是三次握手的完整过程,以及状态变化 ,三次握手的目的,在于让双方知道自己和对方的接受和发送能力是否正常,只有在信任自己,信任对方的条件下,才能建立可靠的连接!
三次握手的本质
为什么要三次握手,一次两次不行吗?
三次握手是以最小成本验证全双工的方式,一次握手和两次握手连全双工都无法验证!
以两次握手为例
第一握手:客户端向服务端发送SYN报文,服务端收到
第二次握手:服务端向客户端发送ACK报文,客户端收到
两次握手结束后
客户端视角下,双方的发送和接收能力均正常,客户端可以建立连接
服务端视角下,无法得知自己的发送能力,客户端的接收能力,就无法建立连接
因此,两次握手,只能让主动建立连接的一方建立连接!
如果需要让双方建立连接,则又需要两次握手!
所以,四次握手,才能让双方完整建立连接
但为什么,三次握手也可以?
因为三次握手将被建立连接一方的ACK报文和SYN报文融合在一起,以SYN+ACK的报文形式,发送给请求建立连接的一方!
所以,三次握手本质上是四次握手+一次捎带应答
从另一方面,我们也可以得出:三次握手,建立的不是一个连接,而是两个连接
- 客户端到服务端的连接
- 服务端到客户端的连接
- 连接是全双工的
半连接队列
只有第一次握手:当服务端第一次收到客户端的SYN报文后,并发送应答报文后,就会处于SYN_RCVD状态,此时双方还没有完全建立连接,但服务器会将此种状态下的连接,放入一个队列里,我们将这种队列,称之为半连接队列
全连接队列
第三次握手后:当服务端收到客户端的应答报文,进入ESTABUSHED状态,代表连接建立完毕,此种状态下的连接,会被服务端放入一个队列里,我们将这个队列,称之为全连接队列
SYN洪攻击
客户端伪造大量IP地址,向服务端发出大量SYN报文,服务端就会有许多处于SYN_RCVD状态下的连接,这种连接会被放入半连接队列,而这些IP地址是伪造的,服务端发送应答报文后就没有后续了,因此半连接,队列会越来越多,直到堆满。
后果:半连接队列堆满后,无法再处理新的SYN报文,导致无法与真正的客户端建立连接
四次挥手
三次握手是建立双方连接的过程,而四次挥手则是销毁连接的过程!
预备知识:
- FIN报文:要断开连接的一方,发送的特殊报文,标志位FIN为1,无有效载荷,旨在告诉对方,“我的数据已经发送完了,我要和你断开连接,你知道吗?”
- 半连接状态:只能接收报文,不能发送带有数据(有效载荷)的报文
四次挥手
- 建立连接的双方,通过四次报文发送(四次挥手),来销毁彼此建立的连接
流程:客户端主动关闭连接
第一次挥手:客户端发送FIN报文给服务端,服务端收到报文,告知服务端:“我的数据已经发送完毕,将要断开连接,你知道吗?”
状态变化:
- 客户端发送FIN报文后,进入FIN_WAIT_1状态
- 服务端收到FIN报文后,进入CLOSE_WAIT状态
第二次挥手:服务端发送ACK报文给客户端,客户端收到报文,告知客户端:“你的关闭连接的请求,我已经知道并同意了!”
状态变化:
- 服务端无变化
- 客户端收到报文后,进入FIN_WAIT_2状态
此时,客户端连接的发送通道已经关闭,进入半关闭状态,只能接受数据和发送ACK报文
第三次挥手:服务端发送FIN报文给客户端,客户端收到报文,告知客户端:“我的数据已经发送完毕,我要关闭连接,你知道吗?”
状态变化:
- 服务端发送FIN报文后,进入LAST_ACK状态
- 客户端收到FIN报文后,进入TIME_WAIT状态
第四次挥手:客户端发送ACK报文给服务端,服务端收到报文,表示客户端已经知道服务端的关闭连接请求了
状态变化:
- 服务端收到ACK报文后,立即进入CLOSED状态
- 客户端发送完ACK报文后,一段时间后,进入CLOSE状态
此时,服务端到客户端的连接关闭;客户端到服务端的连接关闭!
注意:客户端比服务端更晚进入CLOSED状态
三次挥手?四次挥手!
我们知道,三次握手的本质是四次挥手+一次捎带应答
那么同样,四次挥手,也可以用捎带应答,缩减成三次挥手吗?
大多数场景下,不可行。
三次握手的捎带应答是因为双方都有共同的强烈意愿去建立连接
在建立连接这一件事情上,双方都达成了同一意见
就像求婚,两个人只有在双方的感情足够坚固,并且有结婚的打算的情况下
另一方才会求婚,并且被求婚的一方会立马答应
而四次挥手是断开连接,停止数据发送的过程
可能你的数据发完了,但我没有
你想断开你的连接,可以,但我的数据没有发完,我不想断开我的连接
就像离婚,你可能不爱我了,但我已经离不开你
有特殊情况,但很少
当通信双方的数据均发送完毕,都想要断开连接的时候,四次挥手可以变为三次挥手
TIME_WAIT状态
在四次挥手的流程中,最后一次分手,主动断开连接的客户端会进入TIME_WAIT状态,进入这个状态后,客户端会在一段时间过后,再进入CLOSED状态。
TIME_WAIT状态相当于一个延迟关闭状态,就是确定已经要关闭了,但是主动延迟关闭的时间,不立即关闭!
为什么,既然要关闭,为什么要延迟,快刀斩乱麻不好吗?
两个原因
- 解决连接留下的历史报文不干净问题
- 预防ACK报文丢失问题
历史报文不干净
虽然此时,被关闭连接的一方已经明确了数据发送完毕,没有要发送的数据了,但不排除历史报文因为网络环境原因,仍然有一些重复报文残留在网络当中,而延迟关闭时间,就是为了让接收方等待这些报文,一旦报文到达,立即丢弃!防止危害后续新连接!
ACK报文丢失
最后主动关闭连接一方发送的ACK报文,有丢失的风险!一旦丢失,必须重传,否则被关闭连接一方就无法正常关闭连接,导致该连接一直占用资源!
所以,延迟关闭,是为了即使ACK报文丢失,也能及时检测到,并重传ACK报文,保证对方正常进入CLOSED状态!
所以,延迟关闭的目的
- 解决历史遗留报文,保证网络环境干净
- ACK报文补救,提高关闭连接的可靠性
延迟关闭时间
延迟关闭是有具体时间的,我们一般将延迟关闭的时间,设置为2MSL
什么是MSL?
- MSL是TCP报文的最大生存时间,在网络中存在的最大时间
2MSL有什么用?
解决历史报文残留:
- 通过等待MSL的时间,可以确保所有可能在网络中滞留的报文都被丢弃,从而避免因为历史报文残留而影响后续新连接的通信
为ACK报文检测和重传留足时间:
- 当最后一个ACK报文发生丢包的时候,2MSL的时间能够让发送方有时间反应ACK已经丢失,也有时间重传ACK报文,并让其到达接收方
补充:三次握手的作用
不卖关子,三次握手的作用
除了确保双方接收和发送能力正常外,还有两个作用
- 交换初始化序号,确认序号的初始值!
- 初始化滑动窗口大小!
重谈滑动窗口
在滑动窗口机制中,我们明白滑动窗口是发送缓冲区上一段特定的区域,明白滑动窗口决定发送报文的数量,数据的多少,也知道滑动窗口需要应答报文才能更新。
但滑动窗口的初始大小是多少呢?
滑动窗口的更新,与应答报文息息相关
所以,滑动窗口的大小离不开应答报文
滑动窗口不仅需要应答报文的确认序号,来进行窗口滑动
还需要应答报文的窗口大小字段的值,来对自身大小进行调整!
即滑动窗口的大小,取决于接收方的接收缓冲区的剩余大小!
所以,滑动窗口的初始大小,就是接收方缓冲区没有数据时的大小!
在三次握手过程中
- 被建立连接的一方会发送ACK报文
- 主动建立连接的一方会发送ACK报文
在发送ACK报文的时候,双方的接收缓冲区中均无数据,实际上,这就是在告知对方“我的接收缓冲区这么大,所以你的滑动窗口初始大小应该这么大!”
所以,三次握手的另一个作用
- 通信双方互相告知接收缓冲区的大小,初始化各自的滑动窗口!
重谈序号
序号:序号是一个32位的字段,用于标识发送端发送的字节流中的每一个字节的顺序编号,具有唯一性。
报文的序号具体怎么定呢?
我们将缓冲区看成一个线性数组,元素是报文,而报文是由许多字节构成的。
这些字节放在也可以看成一个线性结构,这是缓冲区的第几第几个字节!
报文的序号就是该报文起始地址相对于缓冲区起始地址的偏移量。
真的是这样吗?
如果真的是这样,那么序号将无法真正标识报文的唯一性.
发送缓冲区,不是你一个有,连接的双方都会有发送缓冲区
而互联网中,连接千千万万,序号即偏移量,那么同一时间,会有上万个相同序号的报文!
即使有IP地址和MAC地址的筛选,最后到达同一终点主机的相同序号报文,也会有很多!
所以,真实情况是序号=偏移量+初始化序号
- 偏移量:标识此连接双方通信中报文的唯一性
- 初始化序号:标识此连接的唯一性!
初始化序号是什么?
- 是基于时钟生成的一个随机数,以确保每个连接都有不同的序列号
初始化序号有什么用?
标识连接的唯一性,防止历史连接中的数据被后面相同连接错误接收
- 如新旧连接的源IP地址、源端口号、目的地址、目的端口号相同,旧连接残留在网络中的报文就可能被新链接接收
初始化过程:
- 客户端生产一个初始化序号,将其填充进SYN报文中,发送给服务端
- 服务端拿到SYN报文,得到客户端的初始化序号,相当于后续就认识了来自客户端连接的报文;服务端自己生成一个初始化序号,将其填充进SYN+ACK报文中,发送给客户端
- 客户端收到SYN+ACK报文,得到服务端的初始化序号,相当于后续就认识了来自服务端连接的报文
第二次握手后,双方就相当于完成了初始化序号的交换,认识了彼此的连接
在后续的数据通信中,双方的报文序号=各自的初始化序号+偏移量
所以,三次握手的另一作用
- 交换初始化序列号,让建立连接的双方认识彼此连接!
面向字节流
UDP协议面向数据包,而TCP协议面向字节流
我认为面向字节流最显著的一个特点:你知道自己发送了多少数据,却永远不知道会读多少!
怎么说?
TCP协议将所有应用层的数据均当成无结构的连续的字节序列对待。
流程:
- 当应用层的数据要发送,先进入发送缓冲区,再发送缓冲区中,TCP协议统一将其当成字节序列看待,直接拼接,如果有新的数据下来,则继续拼接。
- 当发送缓冲区的数据要被发送,TCP会将缓冲区的字节序列进行分段,称为数据段,每一段打上序号,再封装成一个独立的报文,最后发送。
- 报文到达接收方,先拆包,再进入接收缓冲区当中,TCP根据序号,将这些分散无序的数据段排列组合,重新拼接为一个字节序列。
例子:客户端发送三个数据 :“你好”,“我是”,“大学生”。
- 应用层:“你好”,“我是”,“大学生”
- 在发送缓冲区:“你好我是大学生”。
- 发送时:“你好我”,“是”,“大学生”。
- 在接收方缓冲区:“你好我是大学生”
TCP将应用层的所有数据,全当成无结构的字节序列来处理,虽然通过机制来保证数据传输的可靠性和效率,但没有考虑应用层如何得到数据。
TCP将发送方应用层传下来的有结构、有类型的数据当成无结构的字节序列处理,当接收方从接收缓冲区拿取的时候,在接收方的视角当中,这些数据也是一串连续的无结构的字节序列。
没有结构,没有类型,面对一串字节序列,接收方是不知道怎么取,取多少的,所以,就会出现我们说的,“读取时不知道会读多少”的问题,甚至会出现粘包问题!
粘包问题
粘包问题:接收端盲目读取数据,导致不同的数据之间出现混杂,使数据丧失原本的意义,甚至变成另一种意思。
举个例子:“床前明月光,低头思故乡”
客户端发送的是:
“窗前明月光”“低头思故乡”
服务端读取的是:
“窗前明月”“光低头思故乡”
这就是很典型的例子,诗句原本的意境之间没了,变得很随意,如果是一些结构体,一些精确的数字,复杂的类型信息呢?那后果就非常严重了,因此必须解决粘包问题!
解决方法:明确两个包之间的边界
- 对于定长的数据,每次按照固定大小读取即可
- 对于变长的数据,可以在数据头的位置,阅读一个数据总长度的字段,从而知道了数据的结束位置
- 对于变长的数据,还可以在数据和数据之间使用明确的分隔符(如http协议使用“\r\n”)
总结
标签:报文,TCP,发送,&&,Linux,序号,连接,客户端 From: https://blog.csdn.net/2402_85267481/article/details/143465827全是干货,因为TCP协议真的很复杂,很多东西,希望看完本文章后,对大家了解TCP协议有一定帮助
如果有问题,欢迎在评论区讨论
如果有帮助,点个赞点个收藏点个关注