(一).基础概念
TCP协议是有连接,面向字节流,可靠传输,全双工。对于有连接,前面在介绍TCP套接字的代码中可以看得出来,客户端和服务器先通过accept()方法建立连接。对于面向字节流,在套接字代码中可以看得出来,我们使用InputStream和OutputStream来进行读数据和写数据。对于可靠性,一会在下面介绍TCP协议的报文格式中就可以体现出来。对于全双工,客户端和服务器都可以进行发送和接收数据。
(二).TCP协议报文格式
1.基础概念

①.16位源端口号和16位目的端口号这个就不介绍了,这是传输层的核心内容。
②.32位序号和32位确认序号这个在下面进行介绍
③.4位首部长度,用来表示TCP整个报头的长度,4位表示4个bit位,范围为0~15。这个属性存在的原因是因为报头中"选项"的存在,"选项"也属于报头的一部分,这个属性可以有也可以没有,所以就会导致TCP的报头是可变的

"选项"上面的部分,已经占了20个字节了(一行是4个字节,5行),但是首部长度的范围最大就是到15,那么该如何表示?事实上,"4位首部长度" 的单位是以4字节为单位 的,如果首部长度里面的值为15,则表示整个报头的长度就是60字节
这就意味着报头的长度要是4的整数倍,即使后面再添加选项,每次添加一个选项,也要按照4的整数倍进行添加。
④.保留(6位)。这就体现出TCP和UDP的差距来了。UDP的主要问题就是长度不够也不能进行扩展。所以TCP报头中就会预留一些"保留位",表示"现在不用,但是先占个位子,将来可能会用到"
⑤.
这个表示的是TCP中最核心的6个标志位,这些标志位在下面会具体介绍到
⑥.16位窗口大小这个在下面介绍
⑦.16位校验和。用来校验数据是否出现错误的
⑧.16位紧急指针 也在下面介绍
⑨.选项也在下面介绍
2.TCP的核心机制
TCP的核心机制是依据"可靠性"来进行展开的。在进行网络通信的时候,整个过程是非常复杂的。这里说的"可靠性"并不是说 客户端给服务器发送一个数据包,服务器能100%收到,而是客户端给服务器发送一个数据包后,尽可能的让服务器收到
(1).确认应答
对于保证"可靠性",一个最主要的前提就是发送方发送的数据包是否被对方收到。这时就需要对方返回给发送方一个"应答报文"(acknowledge,简称"ack"),发送方收到应答报文了,就可以确定对方是收到了

但是,如果是客户端向服务器发送多条数据,那么就可能会出现问题。因为,网络上有一个现象叫做"后发先至"

这就是"后发先至"问题,客户端先发的数据包1,后发的数据包2,收到的ack可能是先收到数据包2的ack,再收到数据包1的ack
为了避免发生"后发先至"的问题出现,TCP采用的处理方案就是给传输的数据进行编号

同时这些序号就会存在TCP报头中的"32位序号"和"32位确认序号"中

注意:确认序号只会在"应答报文"中才能生效

上图就是TCP的载荷部分,TCP是面向字节流的,在编号的时候不是按照1条,2条这样的方式来编号的,而是按照"字节"来编号的,每个字节都分配一个编号,同时这个编号是连续递增的

对于一个TCP的载荷来说,是由多个字节构成的,那么在"32位序号"中,填写的是载荷部分的第一个字节的序号,同时序号是连续递增的;"32位确认序号"的填法是把收到的数据的最后一个字节的序号+1,然后填写到确认序号中
通过一个图来理解

确认序号的含义:①.<1001的数据都已经确认收到了,②.接下来的数据要从1001开始发送
当引入序号之后,接收方就可以根据序号来对数据进行排序了。当TCP处理掉后发先至的情况之后,就可以确保应用程序通过socket api 读到的数据的顺序是正确的了,以确保从代码里读到的数据(InputStream read) 和 发送方写的数据(OutputStream write)的顺序是一致的了
在接收方这里,TCP会安排**"接收缓冲区"** ,"接收缓冲区"可以理解为是一块"内存",是在操作系统内核中的。通过网卡读到的数据,也是先放到接收缓冲区中,后续代码里调用read方法,也是从接收缓冲区来读的。
数据会按照序号进行排序,序号小的在前面,序号大的在后面,以确保前面的数据已经到了,然后read()才能解除阻塞。如果后面的数据先到,那么read()就会阻塞等待,不会读取数据
(2).超时重传
在网络传输的过程中,"丢包"肯定是避免不了的。"超时重传"就是针对"丢包"现象来进行处理的。
出现丢包的原因:在进行网络传输的过程中,网络结构事实上是非常复杂的。当数据经过某个路由器的时候,该路由器已经非常繁忙了,导致当前需要转发的数据量超出了路由器的转发能力,此时数据会消耗很长的时间才能转发到达对方,更坏的情况是数据包太多太多了,路由器根本处理不过来,接收缓冲区都满了,所以只能丢弃了。此时,就需要进行重传,重传是有效的对抗丢包的手段。

当主机A给主机B发送一个数据包的时候,主机A长时间都没有收到主机B返回的ACK应答报文,此时主机A就认为在传输的过程中发生了"丢包"现象。此时有两种情况:①.B给A返回的ACK应答报文丢了②.A给B发送的数据包丢了
假设主机A给主机B发送数据的丢包的超时时间阈值为T,当主机A给主机B传输数据发生超时之后,就会延长这个时间阈值。随着进行不断的延长时间阈值,就会导致数据到达对方的概率越来越高,如果进行了重传,但是还没有成功,说明即使我们增加了概率,但还是没有成功,意味着当前丢包概率是一个非常大的数值,也就说明了网络上大概率已经出现了严重故障,此时继续重传的意义也就不大了,也就没有必要重传了,就会放弃这一次传输
两种情况:①.B给A返回的ACK应答报文丢了②.A给B发送的数据包丢了。对于这两种情况,主机A也区分不了是哪种情况,但是最终的做法都是进行重传

这个情况①还是比较好理解的

对应情况②,主机B已经有1~1000这个数据了。那么再重传之后,B又收到了一份一样的数据。此时TCP就会在内部进行**"去重"**操作,在"接收缓冲区"中,就可以根据序号,找一下这个数据包,如果有则直接丢弃,如果没有,则将该数据放入到"接收缓冲区"中
(3).连接管理
连接管理分为"建立连接"和"断开连接",这里的"连接"是抽象的连接,逻辑上的连接,通信双方各自保存了对端的信息。
Ⅰ.建立连接
TCP协议是通过**"三次握手"**的方式来完成建立连接的
①.客户端先向服务器发送一个 "syn"同步报文,携带自身初始序列号,发起连接
②.服务器收到之后,回复 "syn+ack"报文,既携带了自己的初始序列号,又确认收到了A的请求
③.客户端发送一个"ack"确认报文,告知服务器已经收到其序列号,双方连接成功建立

在传输的过程中,会将序号位的 SYN 设置为1

其实,上述握手应该是四次,只不过我将服务器发送 syn 和 ack 报文合并成了一次,因为服务器向客户端发送ack 和 syn都是由操作系统内核负责的和用户代码无关,所以可以保证是同一时机。这个合并操作,是可以有效的提高传输效率的,在网络传输的过程中,是要能够进行"封装和分用的"

上图是三次握手的详图,TCP的状态变化,就对应的socket api 的情况
这里主要看LISTEN状态和ESTABLISHED状态,因为SYN_SENT和SYN_RCVD状态正常情况下看不到
LISTEN表示服务器已经准备好了,随时都可以有客户端连上来
ESTABLISHED表示客户端和服务器已经建立好连接了,接下来就可以传输业务数据了。
"三次握手"的作用
①.先初步探一探网络的通信链路是否通畅
②.验证通信双方的发送和接收能力是否正常
③.可以协商一些关键的信息。例如,通信过程中,序号是从多少开始,注意:初始序号一般不会从0开始,并且两次连接的初始信号都是不同的,往往差距会比较大
④.建立可靠的面向连接服务
对于③,这里做出解释,具体看下图

那个丢失的数据包一开始迷路了,直到客户端和服务器断开连接也没有到达服务器。直到客户端和服务器再次建立连接后,这个丢失的数据包才到达服务器,那么这个服务器应该处理这个迷路的数据包吗?
答案肯定是不用的,因为第一次连接和第二次连接程序可能都不是同一个,那么这个迷路的数据包自然也就和第二次连接没有关系,所以不应该处理。为了避免这种情况,就会将两次传输的初始序号设置的差距很大。例如,第一次连接,协商好初始序号为1000000;第二次连接,协商好,初始序号为8000000。当之前迷路的数据和第二次连接的初始数据的序号相差很远,则就可以认为是之前的数据了
"三次握手"握两次行不行?握四次行不行?
对于"握两次"来说,肯定是不行的。
如果握两次,则对于发送方来说,传输和接收数据都正常了,因为通过两次握手,客户端已经明确了客户端自身和服务器两者是可以进行通信的。但是对于接收方来说是不够的,站在接收方的角度,如果发送方没有握第三次手,即没有给接收方发送ack数据包,则接收方不确定自己发的syn+ack报文发送方是否收到
对于"握四次"来说,没必要。"握四次"会多余消耗性能。"三次握手"的本质就是确认客户端和服务器各自发送和接收数据的能力是否正常,三次握手刚好可以完成双向信息确认,如果进行四次,则会增加握手时延,浪费网络开销和系统资源
注意:对于"三次握手"的发送方,一定是客户端
Ⅱ.断开连接
TCP协议是通过**"四次挥手"**的方式来完成断开连接的
①.发送方会向接收方发送一个"FIN结束报文",包含了发送方本次的报文段序列号
②.接收方返回发送方一个"ACK"确认报文,表示收到了发送方发来的FIN结束报文
③.接收方返回发送方一个"FIN"结束报文,包含了接收方本次的报文段序列号
④.发送方向接收方发送一个"ACK"确认报文,表示收到了接收方发来的FIN结束报文,发送方进入time_wait状态,接收方直接关闭

可能有的人会问,四次挥手可能将中间的两次合并吗?
答案是可以能,也可以不能。
对于不能,是因为,接收方返回"ACK"的时候,是接收方的内核负责返回的;接收方返回FIN的时候,是接收方的应用程序返回的,即代码中调用socket.close()方法。ack和fin这两次交互的时机是不同的,所以不能合并成一次。
对于能,是因为,在后面介绍到"延时应答"的时候,就可以合并成一次了。
注意:四次挥手,对于客户端和服务器来说,都是可以主动发起FIN请求的

上图,是TCP四次挥手的详图
谁是主动发起FIN的一方,谁就会进入到TIME_WAIT状态
谁是被动发起FIN的一方,谁就会进入到CLOSE_WAIT状态
对于COLSE_WAIT,是在等应用程序代码调用close()方法。例如之前写的套接字代码,一旦客户端发送了FIN,那么服务器的hasNext()就会结束阻塞,并返回false,从hasNext()返回false 到 close(),中间可以存在很多代码逻辑,就看咱们自己咋写
"四次挥手"丢包问题

对于丢包问题来说,一旦丢包,就需要进行重传。
对于四次握手来说,①②③三步,如果发生了丢包,直接重传就可以了。
但是,现在有一种情况,如果接收方给发送方返回一个FIN结束报文。同时发送方收到了接收方的结束报文,然后发送方向接收方发送ack确认报文,然后把连接就是放掉了。如果这个ack确认报文丢了,那么接收方就一直没有收到发送方发来的ack确认报文,此时接收方也不知道是给发送方发送的FIN丢了还是发送方给接受方发送的ack确认报文丢了,此时接收方就要重传FIN结束报文,但是现在发送方已经释放了连接,此时就无法来继续处理接收方重传的FIN结束报文了,那么不就出问题了嘛。。。
此时就体现出TIME_WAIT的作用来了。
当发送方收到接收方返回的FIN结束报文之后,并不会释放连接,而是进入TIME_WAIT状态。要等一下接收方是否可能重传FIN,此时会多等一会,确认接收方不会重传FIN了之后,再进行释放。等的时间大概是2*MSL,即网络上任何两个节点传输过程中消耗的最大时间(通常设置为60s)
"四次挥手"的异常情况
如果"四次挥手"全部挥完,则属于"正常现象",当然,也有"异常现象"。例如,如果接收方始终不调用close()方法,那么站在发送方的角度,发送方给接收方已经把FIN发了很久了,接收方也没有进行后续的挥手操作,此时发送方就会主动释放连接。接收方这里,由于代码有bug,这里的连接暂时还存在。
(4).滑动窗口
滑动窗口,主要是用来提高数据的传输效率的。TCP在保证数据传输"可靠性"的时候,就会造成效率低下问题。可以通过滑动窗口来提高传输效率。

上图是没有通过滑动窗口之前进行的传输

上图是通过滑动窗口进行的传输
"滑动窗口"的逻辑就是,前几个数据包都不需要等待,直接发送,发送到一定量之后在等待,这个"一定量"就是窗口的大小,不需要等待,能够连续发送大量的数据。当发送方收到一个接收方发来的ack之后,就发下一条。

上图中高亮的部分就是ack的窗口范围,宏观上看,这个窗口在快速的滑动。
窗口也不能无限大,窗口越大,说明批量转发的数据就越多,效率就越高,但是如果窗口太大,就会影响到可靠性。滑动窗口本质上是在可靠性的基础上,提高传输效率。
注意:如果2001比1001先到,那么窗口直接往后走两格。因为确认序号的含义是该序号之前的数据都确认收到了,2001能够涵盖1001的含义。只不过当从接收缓冲区读数据的时候,会阻塞等待,一直等待到前面的数据到了。
滑动窗口的丢包问题
①.接收方丢包

如果是接收方丢包,则丢的就是ack确认报文,此时不需要做任何的处理,因为后一个ack能够涵盖前一个ack的含义
②.发送方丢包

站在发送方的角度,当发送方将"1001~2000"的数据包丢了之后,即使发送方后面发"3001~4000","4001~5000",返回的ack都是"1001",当收到连续几个"1001"之后,发送方就会意识到了,"1001~2000"的数据怕是丢包了,所以发送方就会重新传输"1001~2000"
站在接收方的角度,当"1001~2000"的数据包,接收方没有收到,即使后面收到的是"2001~3000","3001~4000",但是由于"1001~2000"丢包了,接收方仍然在要"1001~2000"的数据
当发送方重传"1001~2000"之后,此时的确认需要直接就是"7001",这是因为之前的"2001~7000"的数据都收到了,就差"1001~2000"了。所以通过重传,直接就把缺失的数据给补上了,补上之后,继续从"7001"往后传输就可以了。这个现象叫做**"快速重传"**,快速重传只管谁丢了传谁,其他已经收到的数据无需重传,整个重传的过程速度非常快,相当于滑动窗口下的超市重传的变种
超时重传和快速重传的区别
超时重传,传输的数据量少,没有构成滑动窗口批量传输的形式
快速重传,传输的数据量多,形成滑动窗口
(5).流量控制
"流量控制"就是用来约束"滑动窗口"的。滑动窗口的窗口越大,效率就越高。但是不能无限大,太大就会影响可靠性,因为接收方的处理能力是有上限的。下面通过一个图来看

当"接收缓冲区"满了之后,此时发送方如果继续发送数据就会造成"丢包"现象
流量控制就是在给发送方踩刹车,让发送方发送数据的速度慢一点。流量控制可以让接收方,根据自身处理数据的速度,反馈给发送方,限制发送方的发送速度,在akc中,依赖一个特殊的属性,"窗口大小"

16位的窗口大小,意味着滑动窗口的大小最大数值就是64kb。其实在TCP中,并不是这样的。
在TCP报头中,有一个"选项",里面有一个特殊的属性,叫做"窗口扩展因子"。实际上的窗口大小=16位窗口大小 << 窗口扩展因子(指数增长),假设16位窗口大小为64kb,窗口扩展因子为2,则实际的窗口大小为 64*2*2 =256kb

上图是一个流量控制的详图
发送方接收到"3001~4000"的ack确认报文之后,发现,窗口大小为0了,此时就会暂停发送。然后过一会后,发送方会向接收方发送一个"窗口探测包",只是为了触发ACK,看触发的ACK的窗口大小是多少。如果发送方不发"窗口探测包"的话,就无法触发ack,也就相当于,即使接收方的接收缓冲区中有空余,A也不知道
(6).拥塞控制
拥塞控制和流量控制类似。
流量控制是依据接收方的处理能力进行限制的;拥塞控制是依据传输链路的转发能力,进行限制的

在传输链路中,也有的设备传输的快,有的设备传输的慢。即使发送方发送的再快,接收方接收的再快,只要中间有一个设备处理数据处理的很慢,那整个传输的速度也是慢的。所以类似于"木桶原理"
拥塞控制就是来解决该问题的。拥塞控制的策略:将整个通信链路视为一个"整体",通过"做实验"的方式,找到一个合适的窗口大小。"面多加水,水多加面"。先按照小的窗口进行发送数据,如果发现发送的很顺利,不丢包,则开始加大速度,如果出现丢包,则减小速度。如果发现又不丢包了,则继续增加速度,又丢包了,则减小速度。以此类推,维持一个动态平衡
注意:对于流量控制和拥塞控制,这两个值,哪个小,就按哪个传输,这样可以避免不出错

上图是拥塞控制的一个详图
一开始是指数增长,指数增长就会使窗口快速扩大,但是也不是无限的扩大,增加到阈值大小后,就开始线性增长,即使没有丢包,也不能增长的那么快了。线性增长的过程中,当出现丢包现象之后,会将阈值变成丢包时的值的一半。如果按照之前的策略的话,那么就会重新增长,即指数增长,线性增长 [已经被淘汰] 。如果按照现在的策略,就是从新的阈值开始,线性增长,后面以此类推......
(7).延时应答
在默认情况下,当接收方接收到发送方发来的数据包后的一瞬间,就会返回ack,但是可以通过演示返回ACK的方式来提高效率

注意:延时应答也不是100%能提高效率,还需要看应用程序处理数据的速度快不快
延时应答的策略:①.数量限制:每隔N个包就应答一次,不会因为ack少了而影响可靠性,确认序号后一个能够涵盖前一个
②.时间限制:超过最大延时时间就应答一次
(8).捎带应答
捎带应答是在延时应答的基础上引入的,当返回也数据的时候,顺便把上次的ACK给带回去

当引入延时应答后,ack可以往后演示一定时间,恰好这个时候要返回响应数据,此时就可以把ack也带入到响应数据中,一起返回。
在返回的时候,ack会将序列号"ack"设置为1,然后窗口大小设置为"接收缓冲区剩余量",确认序号等等都设置成合适的值。这些值都是在报头中设置的,不会影响响应数据。把两个包(ACK确认报文+响应数据包)合并成一个,合并成一个,从而提高效率
(9).面向字节流
TCP是通过字节流进行传输的,此时就会出现"粘包问题",这里"粘"的是应用层数据包。通过字节流传输,就很容易混淆包和包之间的边界,从而使接收方无法区分从哪里到哪里是一个完整的应用层数据包

当接收方read的时候,不知道要读几个字节,可能读"a" ,"aaa","aaab","aaabbbc"等等,如果读的包不完整,都可能使程序出现bug
这个问题在TCP层次上无法解决,需要在应用层上进行解决。需要我们定义好应用层协议,明确包和包之间的边界。例如,约定好包和包之间的分隔符,"\n";约定好包的长度,约定每个包开头4个字节,表示数据包一共多长

自定义应用层协议要做事情就是"解决粘包问题",比较成熟的方案就是 json,protobuf,都已经把粘包问题解决掉了
(10).异常情况的处理
Ⅰ.某个进程崩溃了
进程崩溃和主动退出没有本质区别。首先,进程释放,然后回收文件描述符表的每个资源,然后调用socket的close()方法,此时就会传输FIN结束报文,触发四次挥手。进程虽然没了,但是TCP的连接信息依然存在,此时四次挥手还是和正常进行的
Ⅱ.主机关机了
正常流程的关机,本质上还是杀死所有的用户进程。其实就和进程崩溃一样了。关机也是有一定的时间的,如果在一定时间内,完成了四次挥手,那么就和正常一样。
如果没有完成四次挥手,通过下图来看

如果接收方返回的FIN太迟了,发送方已经关机完成了,那么就意味着接收方的FIN不会有ACK。此时接收方就会重传FIN,经过几次之后,接收方就会认为发送方出现了问题,此时接收方就会放弃连接。
Ⅲ.主机掉电了
主机掉电分为接收方掉电 和发送方掉电
①.接收方掉电

接收方掉电,说明之后发送方发来的数据包都没有ACK应答报文了。此时发送方就会进行"超时重传",但是重传之后也没有解决问题。当超时重传到一定次数之后,发送方就会触发**"重置TCP连接"** ,发送方会发送一个**"复位报文(RST)"**

如果此处的RST同样没有ACK,则发送方就只能单方面释放连接了。
②.发送方掉电

当发送方突然掉电,接收方无法区分发送方挂了还是暂时休息一会。所以接收方只能等。当等待一定时间之后,就会给发送方传输一个特殊的报文**"心跳包"** ,不携带业务数据,只是为了触发ACK,如果发送方还有心跳,则继续等待,乳沟没有心跳,则只能通过RST尝试,如果还是不行,只能单方面释放连接。这被称为**"保活机制"**。
在分布式系统中,"心跳包"的思想非常广泛被使用。虽然TCP内置了心跳包,但是太慢了,在开发中,心跳包通常希望是"秒级",甚至"毫秒"级

此时,如果某个业务服务器挂了,就希望能够在 秒级 就发现问题,然后让入口服务器及时把浏览切换到其他机器上
Ⅳ.网线断开了
网线断开。站在发送方的视角,就和"接收方断电"的情况一样;站在接收方的视角,就和"发送方断电的情况一样"。最终都是能够释放资源的
3.介绍其他属性

再看TCP的报文格式,其实就给大家介绍的差不多了。剩下的继续介绍
(1).URG 和 16位紧急指针
URG表示"紧急指针位"。对于TCP来说,都是按照序号顺序进行发送和接收的。"紧急指针"相当于"插队"。URG和 "16位紧急指针"连用,跳过前面的数据,直接从某个指定的序号开始执行read()方法
(2).PSH
PSH表示"催促标志位",发送方给接收方发了数据中带有这个标志位,接收方就会尽快的将这个数据read到应用程序中。
(3).选项
大家可以去查看RFC标准文档,这里就不多介绍了。
(三).TCP和UDP的对比
1.TCP是可靠传输,效率低;UDP是不可靠传输,效率高。
2.在大部分场景下都会使用TCP,例如,HTTP请求等等
3.UDP对于性能要求高,可靠性要求不高的场景下使用,例如机房中。机房中,网络结构简单,带宽充裕,不填容易出现丢包的情况。
(四).KCP
UDP和TCP都太极端了。此时KCP介于两者之间,也是基于UDP实现的。
(五).如何基于UDP实现可靠传输?
①.实现"确认应答",当发送方给接收方发送一个数据的时候,当接收方收到数据,则返回一个应答报文
②.udp数据包本身是无序,重复,丢包的,可以给udp报文进行序号编号,排序,去重操作
③.实现"超时重传",当发送方给接收方发送一个数据的时候,发送方长时间没有收到接收方返回的应答报文,此时可以进行重新发送
④.实现"滑动窗口",批量发送多个报文,当接收方返回一个应答报文之后,发送方继续发送,从而提高传输吞吐效率
⑤.实现"流量控制",接收方告诉发送方自己的缓冲区的剩余容量,防止发送方发送的太快导致接收方溢出
⑥.实现"拥塞控制",感知网络整体拥堵状态,动态调整发送速率,避免因为网络阻塞而造成大面积丢包
