文章目录
前言
TCP协议具有有连接、可靠传输、面向字节流、全双工等特点,可靠传输是其中最核心的部分。
一、TCP报头结构
图1
如上图就是TCP报头的结构。
(1)源端口和目的端口无需过多介绍,就是用于标明发送端以及接收端的程序。
(2)序号和确认序号是用于后续介绍的TCP中的确认应答机制。
(3)数据偏移部分的4位实际上是用来表示这里的TCP报头长度的,因为4位所以表示的最大数就是15,但因为单位为4字节,所以TCP报头的最大长度为60字节。
(4)我们都知道对于UDP数据包最大长度就是64kb,这是很局限的,所以为了避免这种情况,TCP报头中提供了6位的保留位,用于之后如果扩展TCP的功能,就可以使用保留位来表示了。
(5)6个字节的英文标识与后面介绍的TCP的机制有关,这里先不做介绍。
(6)检验和和UDP中的校验和作用一样也是用来检验数据在传输的过程中有没有发生改变。
(7)窗口会在后续介绍机制的时候谈到。
(8)紧急指针后续再介绍。
(9)选项中就是一些可选/可不选的选项。
二、TCP的十个核心机制
2.1 确认应答
TCP协议要解决一个很重要的问题——可靠传输。所谓的可靠传输不是说发送方能够百分百把数据发送给接收方,但是会尽可能让发送方知道接收方是否知道。
图2
如图2,每当你发送一条消息给女神,女神就回一条消息给你,这就是应答,TCP中也是这样的,客户端发送数据包,服务器就会返回一个响应数据包,此时响应数据包在图1中的标志位ACK就会置为1。
图3
如图3当我们给女神发送多条信息,因为女神响应消息的速度不同,所以很容易让我们搞混,我们会以为女神答应做我的女朋友,实际上女神说的是滚,这是网络中客观存在的后发先至的问题。显然发送多个数据包到客户端,客户端响应多个应答数据包我们也要去区分哪个响应对应哪个发送的数据包,这个问题可以使用图1中的序号以及确认序号来解决。
每个发送过去的数据包都有一个序号,但是没有确认序号,或者说确认序号字段是无效的,响应它的数据包ACK为1此时确认序号字段是有效的,发送的数据包序号和响应的数据包确认序号是对应的,这样就能分清谁是谁的响应,就能解决上述的网络中的后发先至的问题。
实际的TCP数据包的序号是按照字节来进行编号的,每个字节都会被分配一个序号,TCP报头中的序号的数值就是载荷中第一个字节的编号,比如说载荷部分的第一个字节编号为1,并且载荷长度为1000个字节,那么该数据包的响应数据包包头中的确认序号为1001,这是因为数据包对应的响应数据包的确认序号被设定为载荷的最后一个字节的编号加1,其实这也比较好理解,返回1001就代表发送过来的1000个字节的载荷已经收到了,并且请求1001之后的数据,如图4。
图4
后续发送数据包,序号是递增的,但是需要注意一点就是虽然是递增的但是序号并非从0或者1开始。
可靠传输之所以能够达成,主要就是依靠“确认应答”机制。它可以通过应答报文告诉发送方是否一切顺利,是否出现丢包,如果出现丢包(数据传输过程中被丢弃了,无法到达对端,客观存在的随机事件)那该咋办?
2.2 超时重传
超时重传就是用来应对网络中的丢包问题的。
这里主要分为两种情况:
(1)传输的数据包丢失
此时B没有接收到A的数据包就不会发送响应数据包,当A等待的事件超过一定的阈值后,就会认定出现了丢包问题,于是重新传输数据包。
(2)响应的数据包丢失
当A没有收到响应数据包,还是一样重传一个数据包过去,但是此时B已经收到一个数据包,加上重传的数据包就收到两个一样的数据包了,这是很不科学的,尤其是对于转账这种问题,就会出现重复转账的问题。
对于上述问题,TCP接收方这边就会针对收到的数据按照序号进行去重。TCP层面对于重复问题无所谓,关键是不能够使得应用层的应用程序读到重复的数据,无论重传多少次,要保证应用层读到的数据就只有一份。
接收方操作系统内核中,存在一个数据结构——接受缓冲区,这个数据结构与优先级阻塞队列类似,当B收到数据包经过层层分用到传输层就会有一个阻塞队列把数据放入其中,队列会根据数据包的序号判断数据是否存在过,如果存在过(存在过就说明曾接收到过一样的数据包并且已经被应用层读取过一次了)就会直接丢弃。接收缓冲区还有一点就是可以解决网络中后发先至的问题,对于发送过来的数据会根据序号进行排序,然后按顺序被应用层的程序给消费掉。
如上图,到达接收缓冲区的数据都会根据序号进行排序,这样不仅解决了后发先至的问题,也解决了接收到重复数据包的问题,此时假如接收到重传的数据包序号为500就会直接丢弃,因为接收缓冲区的最小序号为1000说明500早就已经被应用程序读取过了。
丢包本身是一个小概率事件,当丢包次数变多网络的问题就很大了。随着重传次数的增多,重传的时间间隔在不断变长,因为重传次数越多说明这个网络已经出问题了,频繁重传反而会消耗资源。当重传次数到达一个阈值时就会发送一个复位报文RST标志位为1,清除两端的中间状态,如果重置报文都没有被响应,那么就会删除两端的连接。
超时重传是对于确认应答的补充。
2.3 连接管理
2.3.1 建立连接:三次握手
图5
建立连接就是通信双方各自保存对端的信息,具体完成需要进行三次网络交互如图5。
三次握手的第一次一定是客户端发起的,谁发起三次握手谁就是客户端。具体过程如图5,首先客户端发送SYN数据包,也就是包头的标志位SYN置为1,之后服务器返回响应数据包,响应数据包的ACK和SYN标志位都置为1,因为ACK和SYN标志位的数据包都是操作系统内核来发送的是同一时刻的,所以可以放在一起发送可以提高性能。最后客户端发送响应数据包到服务器,三次握手完成。这个过程中传输的数据包是不包含业务数据的。
三次握手的意义:
(1)投石问路
确认通信链路是否畅通
(2)协商一些重要的参数
比如说传输数据包的序号
(3)确认双方的接受能力以及发送能力
为什么要三次握手?四次握手、两次握手行不行?
四次握手虽然不会影响正常功能但是会降低性能,两次握手不能够完全确认服务器的接收能力以及发送能力。
另外这里涉及到两个比较重要的状态,第一个就是listen状态代表此时服务器已经绑定好端口,就等着客户端发来SYN数据包,另一个就是established,很好理解就是三次握手完成建立了连接之后的状态。
2.3.2 断开连接:四次挥手.
不同于三次握手,先发起的只能是客户端,四次挥手的过程服务器和客户端都可以主动发起。
图6
四次挥手的具体过程如图6,当客户端代码中调用socket.close()方法或者等到进程结束就会给服务器发送FIN结束报文,服务器会立即发送ACK报文回去,但是要等到服务器代码中也调用socket.close()这样的代码后才能发送FIN结束报文给客户端,之后客户端再发送ACK报文给服务器,四次挥手完成。
这里的中间的ACK和FIN是不能合在一起的,因为ACK是由系统内核发送的,所以在服务器接收到FIN报文时就会立即发送,但是FIN报文是要等到服务器端的代码执行socket.close()后才能发送,两者时间上是有差距的。但是想让两者合起来也是有办法,在特殊情况下可以让ACK延迟发送,这样就能够和FIN一起发送了。
另外在四次挥手过程中涉及到两个状态,第一个就是close_wait状态,这个状态就是在接收方接收到发送方的FIN报文后处于的状态,另一个状态就是time_wait,这个状态就是当发送方接收到接收方发来的FIN之后发送ACK给接收方之后需要等待的一个状态,不能立即断开连接,因为要防止最后发送方给接收方发送的ACK丢包,接收方重传FIN,要保证发送方还能接收到这个FIN,这个状态的时间一般是2MSI(MSI:两端数据传输的最大时间),一般是2分钟。
如果发现接收方出现大量的close_wait说明忘记调用close()方法,如果发现服务器出现大量的time_wait说明服务器触发了大量的主动断开TCP的操作。
2.4 滑动窗口
TCP需要保证可靠的传输,但同时也想要去尽可能高效地去完成数据传输,滑动窗口就是一种提高效率的机制。这实际上也是一种亡羊补牢的方法,因为为了保证可靠性,TCP牺牲了很多性能,无论你再怎么滑动窗口传输数据的速度都不可能比UDP快。
图7
如图7所示,这就是数据传输的过程,但是这样发送一个数据包收到一个响应数据包的过程还是比较慢的。
图8
如图8,这是引入滑动窗口机制之后的,一次不是发送一个数据包而是多个数据包,这样返回的响应数据包的等待时间就重叠在一起了。不等待ACK,批量发送的数据多少就是窗口大小。
图9
滑动窗口的过程如图9,假设窗口大小为4组,当一组接收到ACK之后,就会有新的数据发送补上,这就相当于一个滑动的过程。如果发送方接收到3001的ACK,那么说明1001到3001的数据都接收到了,所以窗口可以向右移动两格。
滑动窗口中如果出现丢包怎么办,分为两种情况:
(1)发送的数据包丢失
如果某一组的发送的数据报丢失了,此时虽然你批量发送很多组数据到接收方,但是收到的ACK的确认序号还是丢失的那一组的,直至发送方重传了该数据报。就比如说上图1001~2000的数据报丢失了,即使后面传输了多组数据最终返回的都是1001的ACK直至发送方重传并且接收方收到,才响应7001的ACK。
(2)响应的ACK丢失
如果是响应的ACK丢失了,那么就直接不用管,因为直接等到其它组数据的ACK返回即可,比如说上图中的1001的ACK丢了没关系,下一个2001的ACK发送方接收到了就说明前面的数据已经接收到了,如果后面数据的ACK丢失了也是一个道理。
上述对于丢包的处理还是很高效的,数据包丢了把缺口补上也就是数据重传即可,ACK丢了直接不用管。这样的操作被称为快速重传。
超时重传和快速重传属于在不同环境下采取的不同策略,如果你TCP传输的数据少、不频繁就会触发超时重传,如果你TCP短时间内需要传输大量数据才会触发滑动窗口以及快速重传,快速重传相当于超时重传在滑动窗口下的变种。
2.5 流量控制
前面说到滑动窗口,窗口大小是可变的,可以通过改变窗口的大小来控制发送方的发送速度,窗口越大单位时间发送的数据越多,效率就越高,窗口越小单位时间发送的数据越少,效率就越低。通常情况下当然是希望效率越高越好,但是高效的前提是要保证可靠性,如果说发送方的发送速度太快,接收方处理不过来,那么就可能引起丢包,更合理的做法就是接收方来告诉发送方发送的速度太快了,这就是“流量控制”。
如上图,前面已经说过了内核中有一个数据结构接收缓冲区,接收方会把接收缓冲区的空闲空间的大小作为窗口大小来返回。前面的TCP包头有一个16位的窗口大小字段就是用ACK来保存并返回这个信息的,窗口大小这个字段只会在ACK中生效。
如上图,ACK会返回窗口的大小从而达到流量控制的目的,当窗口大小返回为0时,发送方回周期性发送不包含业务数据的探测报文来触发ACK从而知道缓冲区的情况,有没有空闲空间。
2.6 拥塞控制
拥塞控制和流量控制很类似,都是和滑动窗口搭配的机制。
如上图网络中的链路是很复杂的,链路上的任何一个节点都有可能制约发送方的速度。拥塞控制的思想就是任你中间结构有多复杂,都把它看成一个整体,然后通过实验的方式来找到一个最合适的窗口大小。
如上图就是一个拥塞控制的过程,先给一个比较小的窗口大小(慢启动)来试试看,因为不知道网络的拥堵情况,之后窗口大小以指数增长,到达某个阈值时开始线性增长,再增长到一定程度就出现丢包,此时立即将窗口变小,变小有两种方式:
(1)直接缩到底,回到刚开始慢启动的时候,之后再重复之前的过程(已经被废弃)
(2)缩一半,之后线性增长(实际使用的方式)
拥塞控制就是使用实验的方式去找到一个合适的窗口大小,丢包丢的多就减小窗口大小,不丢包就增加窗口大小。
2.7 延时应答
延时应答顾名思义就是稍等一会再返回ACK,这其实也涉及到窗口大小的问题,因为延迟返回ACK接收方就有更多的时间去消费掉接收缓冲区的数据从而空闲缓冲区大小增大,ACK返回的窗口大小就增大,发送方就能批量发送更多的数据。
有两种延时应答的方式:
(1)按照一定的时间来指定延时
(2)按照收到的数据量
上述两种策略是结合使用的。
2.8 捎带应答
捎带应答其实之前就已经出现过来了,用来提高传输效率的一种机制。就是三次握手中ACK和SYN使用同一个数据包返回的情况。还有就是类似四次挥手里面的那种情况,因为ACK和FIN不同时间发送所以不能进行捎带应答,但是有了延时应答ACK不用那么快就发给发送方,就可以把它和FIN的两次传输通过捎带应答来合成为一次传输。
2.9 面向字节流
面向字节流是TCP的机制,在这里需要注意一个问题就是粘包问题,这种问题就是分不清不同的应用层数据包之间的界限导致的,由于字节流的特点,服务器一次可以读取多个字节也可以读取一个字节,就很容易造成这种问题。
对于上述的粘包问题有两种解决方案:
(1)使用分隔符
使用任何符号都可以只要在请求数据包中不存在
(2)约定数据包的长度
不过java程序员大部分情况下不会直接使用TCP,更多使用的是http这种现成的协议,或者是基于protobuffer或者基于dubbo等工具来实现网络通信,这些方法已经在内部把粘包问题解决掉了。
2.10 异常情况
(1)通信两端,一端的进程崩溃了。
操作系统完成四次挥手,并且挥手PCB。
(2)某个主机被关机(正常流程)
第一种可能就是操作系统完成四次挥手,第二种可能就是接收方发送FIN之后没有ACK响应,重传多次后单方面删除连接,至于发送方也就是关机的那一方,既然都关机了存储的信息(内存)自然也没了。
(3)某个主机电源掉电
当掉电的主机是服务器时,此时客户端发送的数据包没有ACK就会重传,多次重传没有结果就会删除连接。
当掉电的主机是客户端时,此时服务器很久没有接收数据包就会周期性地发送一种没有载荷地心跳包,只是为了去触发ACK,如果客户端正常就会返回ACK反之就收不到任何回应,连续发送多次客户端都没有回应就可以认为客户端挂了删除连接信息。
另外TCP虽然实现了心跳包但是周期较长,指望通过这种心跳包来发现客户端挂了往往需要分钟级别的时间。在实际的开发中会实现应用层的心跳包,更高频更短周期(秒级/毫秒级),发送心跳包,A->B发一个ping,B->A回复一个pong,一旦某个设备挂了可以快速的发现问题。
(4)网线断开
本质上就是第三种情况,对于发送方没有接收到ACk就会超时重传,然后发送RST,之后单方面删除连接,对于接收方没收到数据包就会发送心跳包,没接收到ACK就单方面删除信息。
2.11 补充
TCP包头结构还有两个标志位没有说到就是PSH以及URG,PSH是催促对方尽快返回响应。URG和TCP包头的紧急指针字段相关联搭配在一起使用,用来控制TCP带外数据传输。
带外数据传输就是指除了业务数据之外还有一些特殊的用来控制TCP自身工作机制特殊的数据包。