目录

今日良言:但尽全力,且让心安

     一、TCP协议特点

     二、TCP协议段格式

三、TCP的10个核心机制

四、三次握手和四次挥手

今日良言:但尽全力,且让心安

一、TCP协议特点

TCP :Transmission Control Protocol,传输控制协议.

在之前已经介绍过了TCP协议的特点,简单回顾一下:

有连接 可靠传输 面向字节流 全双工

有连接:TCP类似于打电话,需要建立连接,才可以发送消息.

可靠传输:发送方发送的数据,并不是百分百发送给接收方,而是尽力而为,尽可能的把数据传输过去,同时,如果还是传输不过去,至少能知道.

面向字节流:数据传输与文件读写类似,是"流式"的(一次可以读一个字节或者十个字节或者一百个字节)

全双工:一个通信通道,可以双向传输.(既可以发送,也可以接收)

二、TCP协议段格式

首先来看一下TCP协议段格式

和UDP一样,都是表示源端口和目的端口

 

任何一条数据都是有序号的,但是确认序号只有应答报文有. (序号和确认序号已经应答报文后面会详细介绍)

 4位首部长度:

一个TCP报头长度是可变的,并不是像UDP的报头一样固定8个字节.

因此,首部长度就描述了TCP报头具体多长,另外,选项部分之前的固定是20个字节.

首部长度-20字节 = 选项长度

注:首部长度这里的单位不是字节,而是4字节.

     如果首部长度的值是5,此时表示整个TCP报头的长度是 4*5 = 20字节(此时相当于没有选项)

    如果首部长度的值是15,此时表示整个TCP报头的长度是4*15 = 60字节(此时相当于选项是40个字节)

保留(6位)

在进行程序开发的时候,其中一个重点考虑的事情就是可扩展性(有些功能现在不需要,以后可能用得到).此处,TCP的保留6位,也是为了以后的扩展来考虑的.

为什么要有保留位呢?

这是因为,对于网络协议来说,扩展升级是一件成本极高的事情.以UDP为例,UDP报文长度是2个字节,因此一个包最大是64kb,现在想要升级一下UDP协议,让它支持更大的长度,比如使用4个字节表示报文长度,理论上是可行的,但是实际操作成本极高.

比如:全世界上百亿能上网的计算机/路由器等等..此时这个设备的操作系统里,就是支持2个字节的UDP,要想进行升级,就需要让这些设备的操作系统都升级为能够支持4个字节的UDP.显然,这样做的成本极高.

如果引入了"保留位",此时升级操作成本就会降低不少,如果后续TCP引入了一些新的功能,就可以使用这些保留位字段,此时,对于原来TCP的报头结构的影响是最小的,老的设备即使不升级也更容易兼容.

6位标志位:

URG

:紧急指针是否有效

ACK

:确认序号是否有效,标志位为1表示是应答报文,为0则不是应答报文

PSH

:提示接收端应用程序立刻从

TCP

缓冲区把数据读走

RST

:对方要求重新建立连接;我们把携带

RST

标识的称为

复位报文段

SYN

:请求建立连接;我们把携带

SYN

标识的称为

同步报文段

FIN

:通知对方,本端要关闭了,我们称携带

FIN

标识的为

结束报文段

16位窗口大小:

不需要等待就能直接发送的数据的最大的量称为"窗口大小".(后面会详细介绍)

16位校验和:

和UDP的校验和一样,作用都是验证传输的数据是否正确

16位紧急指针:

标识哪部分数据是紧急数据;

选项:

 相当于是对这个TCP报文的一些属性进行解释说明.

三、TCP的10个核心机制

接下来,学习一下TCP内部的工作机制.

TCP是一个复杂的协议,里面有很多东西很多机制,这里介绍TCP提供的10个比较核心的机制.

1.确认应答

2.超时重传

3.连接管理

4.滑动窗口

5.流量控制

6.拥塞控制

7.延时应答

8.捎带应答

9.面向字节流

10.异常情况

1.确认应答

以小马给女神在qq发消息为例,解释一下确认应答,以及相关知识点.

故事背景:小马和女神已经认识了很久,互生情愫,只差捅破窗户纸.

有一天,小马打开了和女神的聊天框,拨通了qq电话,女神接通以后,对女神说:女神,你今天的照片口红画歪了.然后女神回了:6

此时,女神回复的"6",就称为"应答报文",也叫做"ack"(acknowledge缩写).

当小马收到"6"这条消息的时候,就可以知道,他发送的消息已经被女神收到了(并没有丢包),

如果隔了一天,女神还没有回消息,那就说明发出的消息大概率丢包了.

TCP进行可靠传输,最主要的就是靠这个确认应答机制:

A给B发送了个消息,B收到之后就会返回一个应答报文(ACK),此时,A收到应答报文之后,就可以知道刚才发的数据成功到达B了.

 接下来,考虑更复杂一点的情况:

小马蓄谋已久,酝酿了半天,点开和女神的聊天框,发送了第一条消息:女神,今晚和我吃饭好不好.然后又发送了第二条消息:女神,做我女朋友好吗.   然后女神在看到消息之后,先发送了一条:好啊.然后发送了:做梦呢? 但是,由于网络原因,在小马收到的两条消息中,先接收到了"做梦呢?" 然后又收到了"好啊",此时,小马的女神梦主观意识上已经成了,但是实际上出现了问题.

 这种情况称之为"后发先至",在网络上,可能存在"先发后至",两个主机之间,路线存在多条,数据报1和数据报2走的都是不同路线,数据报1转发路径上的路由器/交换机和数据报2转发路径上的路由器/交换机也不一样,此时,这俩数据报到达顺序会有变数.

网络上后发先至这个现象是客观存在的,无法避免的,因此,应答报文到达的顺序也是有变数的,此时就需要考虑如何避免这种情况的出现.

那么,如何解决上述"后发先至"的问题呢?

给所有数据都进行编号(传输数据和应答报文)

此时,再来看刚刚的情况:

此时,小马的女神梦主观意识上就泡汤了.

当引入了序号以后,此时就不担心顺序乱了,即使顺序乱了,也可以通过序号来区分当前应答报文是针对哪个数据进行的了.

任何一条数据(传输的数据和应答报文)都是有序号的,但是,确认序号,只有应答报文有(普通报文里确认序号字段里的值无意义).

一条报文是不是应答报文,取决于ACK这个标志位,如果标志位1,就是应答报文,否则不是.

实际上,TCP的序号并不是按照一条两条这样的方式来编号的,TCP是面向字节流的,TCP的序号也是按照字节来进行编号的.

假设A给B发送了一条数据,共100个字节.

 假设字节的序号是从1开始编号,第一个字节编号1,第二个字节2....最后一个字节编号是1000,但是,由于这1000个字节都是属于同一个TCP报文,TCP报头里就只记录当前的第一个字节的序号,也就是说,此处的报头的序号是1.

接下来,如果A要继续给B发送数据,此时第二条TCP数据报的第一个字节序号就是1001,假设长度还是1000字节,此时最后一个字节的序号就是2001,第二条TCP报头里的序号就是1001.

TCP的字节的序号是依次累加起来的,这个依次累加,是基于上一条TCP数据报的最后一个字节的序号,起始字节序号就是上一条TCP数据报的最后一个字节序号+1.

每个TCP数据报报头填写的序号只需要写TCP数据的头一个字节序号即可.

观察上图:应答报文中填写的序号是1001和1002,就是在1000和2000的基础上+1,

针对确认序号是1001的应答报文来说,表示的含义有两个:

1.小于1000的数据都已经确认收到了. 

2.主机A接下来应该从1001这个序号开始继续发送.

确认序号的取值,是收到的数据的最后一个字节序号+1.

 小结:

TCP可靠传输能力,最主要的就是靠确认应答机制来保证的.

通过应答报文,就可以让发送方清楚的知道数据是否传输成功.

进一步的引入了序号和确认序号,针对多组数据进行详细的区分. 

2.超时重传 

超过一定时间,还没响应,就重新传输.

确认应答考虑的是数据成功传输的情况,如果数据传输过程中丢包了呢?这就是超时重传机制要解决的问题.

丢包涉及到两种情况:

1).发的数据丢了

2).返回的ack丢了

对于发送方而言,就是没有收到ack,区分不了是哪种情况.此时,就都会认为是丢包了.

丢包是一个概率性问题(通常情况下,丢包的概率是比较小的),因此,如果重新发一下这个数据报,还是有很大的概率传输成功的.

因此,TCP就引入了重传机制,在丢包的时候,就要重新发送一次同样的数据.

TCP直接引入了一个时间阈值,发送方发送了一个数据之后,就要等待ACK,此时开始即使,如果在时间阈值之内,ack都没有到达,说明此时丢包了.

由于重传机制的引入,这又会带来一个新问题:这会导致接收方重复的消息,可能收到多次.

如下图:

如果这是一个支付请求,造成的影响是非常大的,值得庆幸的是TCP针对这种重复数据的传输,会特殊处理:去重.

 TCP存在一个"接收缓冲区"这样的存储空间(接收方操作系统内核里的一部分),每个TCP对应的socket对象都有一个接收缓冲区和一个发送缓冲区.

主机B收到主机A的数据,实际上是主机B的网卡读到了数据,然后将这个数据放到主机B对应的socket对象的接收缓冲区中,这个接收缓冲区可以想象成一个阻塞队列,根据数据的序号,TCP很容易识别出当前接收缓冲区里的两条数据是否重复,如果重复,就将后来的这份数据就直接丢弃.保证了应用系统调用read读取到的数据,一定是不重复的.

TCP使用这个接收缓冲区,对收到的数据进行重新排序,使应用程序read到的数据是保证有序的(和发送顺序一致)

由于去重和重新排序机制的存在,发送方只要发现ack没有按时到达,就会重传数据,即使重复了,顺序乱了,接收方也能很好的处理(去重和排序都依赖TCP报头的序号)

重传的数据是否有可能又丢包了呢?

答案是有可能的.因此超时重传可能会重传多次,重传的次数也并非很多次,这是因为,当重传几次都没传过去,再重传也就意义不大了.

假设一次丢包的概率是10%,传输成功的概率是90%,如果第一次丢包,第二次重传也丢包的概率是1%,第三次重传又丢包的概率是0.1%.连续重传都丢包的概率是非常低的.

因此,重传达到一定的次数,就不会重传了,会认为网络出现故障.接下来,TCP会尝试重置连接(相当于断开重连一样),如果重置还是失败,就彻底断开连接了.

小结:

TCP的可靠传输就是靠确认应答和超时重传来进行体现的.

其中确认应答描述的是传输顺利的情况,超时重传描述的是传输出现问题的情况.

这两者相互配合,共同支撑整体的TCP可靠性.

注:

TCP的可靠性是通过三次握手来保证的,这句话是错误的!!!!!!!

三次握手与TCP的可靠性有关,但是起到的效果是与超时重传和确认应答相比是微乎其微的.

3.连接管理

先来解释一下连接管理:

TCP建立连接

 当这两部分信息都被维护好以后,此时连接就有了,此时,也把保存这部分信息的这个空间(数据结构)称为连接

断开连接:A和B将自己存储的连接信息(数据结构)删了,此时连接就断开了.

管理:描述了连接如何建立,如何断开 

TCP建立连接的过程是:三次握手

TCP断开连接的过程是:四次挥手

由于三次握手和四次挥手比较重要,博主放到第四部分重点介绍.

 4.滑动窗口

确认应答 超时重传 连接管理 都是给TCP的可靠性提供支持.

引入了可靠性,其实付出了代价,会降低传输效率.

TCP会在保证可靠的基础上,尽可能的提高传输效率(尽量降低效率的折损)

对于基本确认应答的情况来说,每次发一个数据,都需要等,等ack到了再发下一个.

滑动窗口的本质就是不等待的批量发送一批数据,然后用一份时间来等待这一组数据的多个ack.

把不需要等待就能直接发送的数据的最大的量称为"窗口大小".

图中的窗口大小就是4000.

 

 当批量发送了窗口大小的数据之后,发送方就要等待ack了,并不是等待所有的ack都到达才继续往下发数据,而是收到一条ack,然后发送一条数据,这样就保证了此处等待的ack始终都是固定的值.

比如上图,当发送了窗口大小4000的数据,然后开始等待,收到一条ack以后,继续往下发数据,但是等待的ack始终是4条.

观察上图来理解"滑动":

主机A 本来等待的是ack是1001 - 5000  ,接下来收到了2001这个ack,这说明之前的数据( 1001 -2000) 的数据已经确认收到了,此时就立刻发送5001 - 6000的数,此时意味着等待ack的范围是2001-6000.窗口大小始终是4000,但是等待的ack一直在向后滑动.

设想一下,如果上述发生丢包的情况下,该如何处理?

1.如果ack丢了

如果丢失一部分ack,不需要做任何处理,丢了就丢了,不会有任何影响.

 理解这里的确认序号的含义:

确认序号表示:该序号往前的所有数据都已经确认到达了.

比如上图中:1001这个ack丢了,但是2001这个ack没丢,2001表示:1-1000 和 1001-2000的数据都到达了.2001这个ack其实涵盖了上一个1001这个ack.

实际上,这里的ack并不是老老实实的全部都发的,可能会故意少发一部分(并不影响可靠性,同时节省系统资源),但是不至于全部丢完ack.

2.如果发的数据丢了.

 由于1001-2000丢包了,接下来2001-3000到达主机B后,B给A返回的ACK确认序号仍然是1001,意思就是在索要1001开头的数据.接下来的几个数据,B返回的ACK确认序号都是1001,此时,A收到3个同样的确认序号,于是对1001-2000的数据开始重传.接下来,由于2000-7000的数据B都已经收到了,B会开始索要7001开头的数据.

上述丢包重传的方式,叫做"快速重传".

可以视为是"超时重传"机制在滑动窗口下的变形.

如果当前传输数据密集,按照滑动窗口的方式来传输,此时按照快速重传来处理丢包.

如果当前传输数据稀疏,不再按照滑动窗口方式了,此时按照之前的超时重传来处理丢包.

5.流量控制

这是一种干预发送的窗口大小的机制.

滑动窗口,窗口越大,传输效率就越高(一份时间等的ack就多了),但是,窗口的大小不能无限大.

这是因为:

1).完全不等ack,可靠性的保障就会出现问题.

2).窗口太大也会消耗太多的系统资源,

3).发送方发送的太快,接收方可能处理不过来,此时发了也白发.

流量控制要做的工作就是第三点,根据接收方的处理能力,协调发送方的发送速率.

如何衡量接收方的处理能力呢?

直接看接收方的接收缓冲区的剩余大小!!.

每次主机A给主机B发送个数据,B就需要计算一下接受缓冲区的剩余大小,然后把这个值,通过ack报文返回给A,A就根据这个返回的值决定接下来的发送速率是多少,也就是窗口大小是多少.这个窗口大小也就是之前TCP报文结构报头中的窗口大小(报文是ack的时候,才有效)

 16位窗口大小,并不是意味着,窗口的大小最大是64Kb.TCP为了让窗口更大,在选项部分引入了窗口扩展因子.

比如窗口的大小已经是64kb,窗口扩展因子中写了个2,意思就是让 64kb << 2 = 256kb

 发送方的窗口大小并不是固定的,而是随着传输过程的进行,动态调整的.

当窗口大小为0的时候,发送方就要暂停发送数据,开始等待ack,在等待ack的过程中,会给B发送窗口探测报文,这个报文不具备任何业务逻辑,只是为了触发ack查询窗口大小.

6.拥塞控制

 流量控制和拥塞控制共同决定了发送方的窗口大小是多少.

 流量控制:考虑的是接收方的处理能力.

拥塞控制:描述的是传输过程中中间节点的发送能力.

 在数据传输过程中,有很多个节点,如上图.在介绍流量控制的时候,只考虑了接收方和发送方,但是没有考虑中间节点.

如果要考虑数据传输的效率,就不能忽略中间节点的发送能力.

类似于木桶效应,一个木桶的储水能力,取决于最短的木块,而非最长的木块.

接收方的处理能力,好量化衡量,但是,中间节点却不好衡量.

于是,设计TCP的大佬们就通过"实验"的方式,来测试出合适的值.

简而言之,就是在危险的边缘跃跃欲试(丢包的边缘)

初始阶段,由于初始窗口比较小,每一轮不丢包都会使窗口扩大一倍(指数增长),当增长速率达到阈值之后,此时指数增长就成了线性增长,增长的前提是不丢包.

接下来,传输过程丢包了,说明此时的发送速率已经接近网络极限了,此时就把窗口大小缩小成很小的值,然后重复上面指数增长和线性增长的过程.

拥塞窗口不是一个固定的值,而是一直动态变化的,随着时间的推移,逐渐达到一个动态平衡的过程. 

拥塞窗口和流量控制的窗口,共同决定了发送方实际的发送窗口(拥塞窗口和流量控制窗口的较小值)

 7.延时应答

延时应答也是提高效率的机制,也是基于滑动窗口做一些处理.

延时应答是接收方在收到数据以后,不是立即返回ACK了,而是等待一段时间再返回.

在等待的这段时间里,接收方的应用程序就能够把接收缓冲区的数据给消费一些,这样接收缓冲区剩余空间就更大了.

实际上,延时应答采用的方式,就是在滑动窗口的方式下,ack不再每条数据都返回了,比如上图:隔一条返回一个ack.

实际上 剩余空间大小变化是一个复杂的过程,既取决于发送方的发送,又取决于接收方的处理.

8.捎带应答

捎带应答也是提高效率的方式,是在延时应答的基础之上引入的.

服务器客户端程序,最典型的模型就是"一问一答".

以业务上的请求和响应为例:

A给时间服务器B发送了一个请求"现在几点了".当服务器B收到了这个请求之后,立即发送一个ack"我看看".

然后一段时候后回复"11:30",这也是个业务上的响应,是在B的应用程序里发送的,于是A收到之后,就会立刻给B返回一个ack"我知道了".

在上图中,红色部分内容都是ack,ack是由系统内核立刻返回的.

 这个业务上的响应"11:30" 和 "我看看" 本身是不同的时机,但是由于TCP存在延时应答,就导致等待ACK("我看看")的过程中,B就已经要给A发送业务响应("11:30")了,于是就可以让业务数据捎带上这个ACK("我看看")一起发送过去了.

9.面向字节流

由于TCP是面向字节流的,这引入了一个问题:粘包问题.

比如客户端A和服务器B进行了多次交互.

接收缓冲区将刚刚收到的多个数据报都放到一起了

应用程序read读取的时候,读到哪里才算一个完整的应用层数据报呢?

由于TCP是面向字节流的,一次读一个字节/N个字节都是可以的,这就导致一次读到的数据可能是半个应用层数据报,也有可能是一个应用层数据报,还有可能是多个应用层数据报,把这个问题称之为"粘包问题".

 假设接收到上述三条数据,应用层调用read的时候:

如果read的是6个字节,此时就正好读出了 aaaaaa,这是一个完整的应用层数据报.

如果read的是7个字节,此时就读出了aaaaaab,这是"一个半"应用层数据报.

如果read的是5个字节,此时就读出了aaaaa,这是"半个"应用层数据报.

在TCP层次,没有socket   api告诉我们应该读几个字节,具体怎么读,取决于程序猿自己.

在传输层可以看到上述是三个不同的数据报,但是应用层看到的是完整的字节流 abc都是连在一起的,所以说,需要在应用层做出区分.

那么,该如何读取到一个完整的应用层数据报呢?

解决方案其实很简单,在应用层进行处理,约定好应用层协议,尤其是明确应用层数据报和应用层数据报之间的边界就好了.方法主要有两个:

1)约定好分隔符

2)约定每个包的长度

比如:在每个应用层数据最前面加上长度.

10.异常情况 

传输过程中出现了不可抗力,导致出现下面几种情况.

进程崩溃了主机关机(按照正常流程关机)主机掉电网线断开

1)和2) 是一类(正常情况):

1):

进程没了,对应的PCB就没了,对应的文件描述符表就释放了,相当于socket.close()

此时内核会继续完成四次挥手,此时其实仍然是一个正常断开的流程.

2):

主机关机要先杀进程,然后才正式关机(杀死进程的过程中,也是和上面一样触发四次挥手)

3)和4)是一类(非正常情况):

显然来不及挥手.

假设是接收方掉电了,发送方仍然在发送数据,发完数据要等待ACK,ack等不到就超时重传,N次重传也收不到ACK.

重传几次,还没有应答,就尝试重置TCP连接(复位报文段..RST).显然这个重置也会是失败,放弃连接(单方面放弃了)

如果是发送方掉电.

接收方发现没数据了,没数据是发送方挂了还是发送方要等会再发,接收方不知道,就先等待.接收方就要周期性的给发送方发送一个消息,确认下对方是否还工作正常.

接收方发送的这个消息就是心跳包.

心跳包也是一个形象的比喻:1.心跳是周期性的 2.如果心跳无了也就挂了.

心跳包来确认通信双方处在正常的工作状态中.

四、三次握手和四次挥手

1.TCP建立连接:三次握手

通信双方要各自记录对方的信息.彼此之间要认同.

还是以之间小马和女神聊天为例来解释三次握手.

首先,小马对女神表白,女神接受了小马的表白,此时,小马和女神建立了一个认同:女神是小马的枕边人.

但是,要想称为男女朋友,就需要彼此都认同.

此时,女神也对小马表白,小马也接受了,此时,小马和女神又建立了一个认同:小马是女神的枕边人.

这个时候,小马和女神就是男女朋友关系了,相当于连接建立完成.

 此处,就把上述过程中的每次通信,都称为是一次"握手".

看到这里,提出一个疑惑:上述过程不是四次交互吗?为什么是三次握手?

这是因为:上述过程中确实是进行了四次交互,但是中间两次交互是可以合并成一次的.

 

三次握手,本质上是四次交互.

通信双方要各自向对方发送一个"建立连接"的请求,也要各自向对方回应一个ack.这里其实一共有四次信息交互,但是,中间的两次交互是可以合并成一次的,因此就构成了"三次握手".

为什么要把中间的两次交互合并呢?不合并行不行?

不行!!必须合并,这里关系到封装分用,封装分用两次一定比封装分用一次的成本更高.

如果是两次握手,能否完成建立连接的过程?

不行!!!  还是以上述小马和女神为例.

 如果少了最后一次握手,此时,小马已经知道了:女神是小马的枕边人   小马也是女神的枕边人

但是,女神只知道了:女神是小马的枕边人, 不知道小马是不是答应做她的枕边人,此时,在女神看来,小马可能就是个渣男了.

三次握手另一个重要的作用:

验证通信双方各自的接收能力和发送能力是否正常.

三次握手一定程度上保证了TCP的可靠性,但是起到的作用仅仅只是辅助作用,并不是关键作用.

三次握手的意义:

1).让通信双方各自建立对对方的"认同".(保存对方的信息)

2).验证通信双方各自的接收能力和发送能力是否正常.

3).在三次握手过程中,双方协商一些重要参数.

TCP通信过程中,有些数据需要通信双方相互同步,此时就需要这样的交互过程,恰好可以利用三次握手的机会,来完成数据的同步.

接下来,观察一下TCP建立连接的详细流程图:

 客户端是主动的一方,服务器是被动的一方.一定是客户端先向服务器发起"建立连接请求".

客户端和服务器这个身份概念,只是按照主不主动进行区分的,同一个程序,在不同的场景下可能是作为服务器也可能作为客户端.

客户端主动给服务器发送的建立连接请求,称为"SYN"(同步报文段)

也就是之前在TCP报头结构中看到的6个标志位中的一个.

 TCP也是有状态的,TCP的状态是很复杂的,要远远超过之前介绍的线程的状态.

下面介绍建立连接阶段的两个重要状态:

1).LISTEN  服务器的状态

表示服务器已经准备就绪,随时可以有客户端来建立连接了.

2).ESTABLISHED    客户端和服务器都有

表示连接建立完成,可以进行通信了.

小结:三次握手的基本流程图

2.断开连接:四次挥手

"挥手"和"握手"一样,都是形象的叫法,都是客户端和服务器之间的数据交互.

四次挥手和三次握手类似,通信双方各自向对方发送一个断开连接的请求,然后再给一个回应.

还是以小马和女神为例,解释四次挥手:

书接上回,小马和女神在一起以后,突然发现,女神和心中白月光的形象有很大差距,于是,再经过深思熟虑之后,小马决定对女神提出分手,于是对女神说:女神,你不再是我的枕边人了,女神回复:嗯好的,女神对小马失望至极,于是经过一段时间的冷静,也对小马说:你也不再是我的枕边人了,小马回复:好的.

于是呢,小马和女神就从情侣变成了路上,世间好物不坚牢,彩云易碎琉璃脆,小马和女神的恋爱史到此已经结束了.

此时,连接就彻底断开了.

通过观察,会发现,断开连接也是四次信息交互,中间两次也能合并吗?

通常情况下不能(特殊情况下可以)

两个数据发送的时机相同才能合并,如果是不同时机,就不能合并.

三次握手的中间两次能够合并,是因为这两次信息交互是同一时机.

具体来说,三次握手这三次信息交互过程,是纯内核完成的(应用程序感知不到,也干预不了)

服务器的系统内核收到syn之后,会立即发送一个ack也会立即发送一个syn

上述是四次挥手的流程图:

FIN是结束文段 ,在TCP报头结构的6个标志位中也有

FIN的发起,不是由内核控制的,而是由应用程序调用socket的close方法(或者进程退出) ,才会触发FIN

ack是由系统内核控制的,当服务器收到FIN之后,会立刻返回一个ack.

服务器发送的FIN,只有服务器的应用程序执行到对应的close方法,才会触发FIN,和上面立刻发送的ACK是有一个时间差的,时机不同,通常情况下不能合并.

接下来,通过观察之前写过的基于TCP的简单客户端服务器网络通信程序的代码来加深理解.

(10条消息) 简单客户端服务器网络通信程序_程序猿小马的博客-CSDN博客

观察TCP版本的回显服务器的代码

 这里的break决定循环退出,break的原因是因为上面hasNext的结果是false,当流对象读到了EOF(文件结束标记)时,hasNext的结果就是false.

为什么会读到EOF呢?

是因为内核接收到了对方发来的FIN数据报,这里客户端的代码并没有显式的写close,但是当客户端进程退出以后,就会触发socket.close,也就触发FIN.

当循环结束以后,就会执行服务器这里的close操作,此时,服务器就会触发FIN.

在上述代码中,当前循环一结束,就会立即执行close操作触发FIN,此时的ack和FIN之间时间间隔就比较短,此时系统可能就会把这两个合并成一个了. 

但是,如果之间间隔比较长,在close之前执行别的工作,比如:睡眠1000ms,此时就无法合并了.

接下来,观察一下断开连接阶段(四次挥手)的详细流程图:

 四次挥手也涉及两个TCP的重要状态:

1.CLOSE_WAIT

出现在被动断开连接的一方.

等待关闭(等待socket调用close方法)

建立连接请求一定是客户端先发起的,但是断开连接请求可能是客户端主动发起,也有可能是服务器主动发起.

2.TIME_WAIT

出现在主动断开连接的一方.

TIME_WAIT要保持当前的TCP连接状态不要立即断开.

为什么不立即释放?

这是因为:此时最后一个ack刚发送出去,可能还未到达,万一这个ack丢包了,就没法处理了.于是

TIME_WAIT会等,如果等了一段时间后,还没有收到重传的FIN,此时就认为这个ack已经到达了,然后就彻底释放连接了.

(但是,有没有可能这个重传的FIN恰好也丢包了?有可能!但是这种情况概率比较低,如果真的发生了,就只能自认倒霉了.)

在三次握手和四次挥手的过程中,同样是存在超时重传的.

如果是最后一个ack丢包了,在服务器的角度来看,服务器不知道是因为自己发的FIN丢了,还是ack丢了,所以统一视为是FIN丢了,进行重传操作.

既然服务器可能要重传FIN,那么客户端就要对这个重传的FIN进行ACK响应.很明显,如果直接将TCP连接释放了,这样的ACK就无法进行了,因此,使用TIME_WAIT会保留一段时间,就是为了能够处理最后一个ACK丢包的情况,能够在收到重传的FIN之后,进行ACK响应.

TIME_WAIT保留2MSL,之后就真正释放了.

这个MSL是一个约定时间.指的是互联网上,两个节点之间,数据传输消耗的最大时间,通常情况下MSL的值是60s.

小结:四次挥手的流程图:

文章来源

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: