目录
一、TCP协议段格式介绍
下图为TCP协议段格式图:
1.1 源端口和目的端口
源端口就是表示发送方进程所占用的端口,目的端口就是接收方进程所占用的端口。他们俩就表示数据从哪里来,到哪里去。
1.2 4位TCP报文长度
4位首部长度就表示TCP头部有多少个32位bit,但是由于4位首部长度的单位是4字节,因此TCP头部最大长度为60字节。
1.3 6位标志位
1. URG:紧急指针是否有效。
2. ACK:确认序号是否有效。
3. PSH:提示接收端li应用程序立刻从TCP缓冲区把数据取走。
4. RST:对方要求重新建立连接;我们把携带RST标识的称为复位报文段。
5. SYN:请求建立连接;我们把携带SYN标识的称为同步报文段。
6. FIN: 通知对方,本端要关闭了,我们将携带FIN标识的称为结束报文段。
1.4 16位校验和
校验和,用来验证传输的数据是否正确的。如果接收方和发送方计算的校验和不一样, 那么就说明传输的数据有错。如果一样,则说明数据正确。
1.5 选项
选项,说明是可有可无的。这里的选项是对TCP报文的一些属性进行解释性说明的。
由于首部长度描述了TCP报头具体多长,并且选项之前的部分的长度是固定的(20字节)。因此可以用 首部长度-20字节,就可以计算出选项长度。
1.6 保留
保留位的意义就是:如果后续TCP想要引入一些新的功能,那么就可以使用这些保留字段。这样扩展的成本就会降低很多。
剩下的有些没有介绍的,在后面我会一一介绍。
二、TCP内部的工作机制
TCP协议非常复杂,其中的工作机制非常多,因此这里只介绍10个比较核心的机制。
2.1 确认应答
我们知道TCP是可靠传输,其中可靠就是发送方知道接收方是否接受到了消息。那么TCP是怎样实现可靠传输的呢?其中,确认应答机制就是实现确认应答的一个比较重要的机制。
确认应答机制就是:当发送方给接收方发送一条信息之后,接收方受到之后会给发送方返回一个“应答报文”,此时,发送方就知道了接收方真的已经接收到了这条信息。
就像我给我的兄弟发一条出来玩的短信:
其中,我给兄弟发的“别学习了,出来玩”就是一条消息。而回复的“好嘞”就是一条应答报文,当我受到兄弟发的“好嘞”,我就知道兄弟已经收到了我的消息。
其中我们要注意:应答报文的6位标志位中 ACK 位为1。
那当我们的聊天更复杂的时候是什么情况呢?
在这个情况下,我发送了两条消息,一条为“别学习了,出去玩”,另一条为“我们在房子里打游戏吧”。我的兄弟也恢回复了两句,为“好嘞”和“算了吧”。在这个情况下,我们知道,我的兄弟是想出去玩,他并不想在房子里打游戏。但是我们知道,在网络传输中,两个主机之间的路线存在多条,数据报1和数据报2走的路线可能也是不一样的。而有的路线速度快,有的路线速度慢。因此,数据到达的顺序就有了变数。如果在网络传输中发生了“后发先至”的情况。那么到时候就可能出现下列情况:
如果出现“后发先至”,那么从我这个视角来看。我的兄弟的意思就是想在房子里打游戏,而不想出去玩。
那么如何规避“先发后至”呢?我们可以将传输的报文和应答报文都编上序号,这样就可以避免这种情况。
这样,就算发生后发先至,我们也知道应答报文所对应的是哪条数据。
此时,此处应答报文中的序号就是TCP报头中的“32位确认序号”。 并且,确认序号只有应答报文有。
注意:应答报文的序号不是我们图中这样“针对1”,“针对2”这样的。而是,如果报文1一共是1000字节,假设是从1开始编号,第二个字节序号为2,以此类推,最后一个字节的序号就为1000。那么报文1的序号为1,其应答报文的序号就是1001。并且,TCP报文的序号是依次累加的。
下图是确认序号的一个演示图:
其中,确认序号为1001的含义为:
1.1000以前的数据都已经收到了。2001的含义为:2000以前的数据都收到了。
2.发送方接下来应该从1001这个序号开始继续发送。
2.2 超时重传
上述在确认应答过程中,我们只讨论了一种最理想的情况,那就是没有丢包的情况。但是在现实的网络环境中,丢包是随时可能发生的。那么如果发送方发送的数据丢包了,亦或者应答报文丢包了,这两种情况下发送方并不知道是自己的数据丢罢了还是应答报文丢包了。面对这种情况,TCP该如何处理呢?此时TCP就会触发超时重传机制,尽自己最大的努力将数据传过去。
TCP在超时重传中,引入了一个“时间阈值”,如果发送方发送了一个数据之后,就会等待ACK,在此刻就会开始计时。如果在时间阈值之内,没有收到ACK。无论是ACK还在路上,或者是ACK真的丢包了。那么就直接认为数据丢包了,发送方就会重传一次数据。
但是如果重传的数据又丢包了,此时怎么办。这时TCP还是会不厌其烦的重传数据。但是TCP会不会无限的重传呢?当然不会,当重传了多次之后,数据还没有发送过去,这个时候你的网络就可能已经出现了重大故障,重传几次都没用!
并且随着你的重传次数的增多,超时时间就会随之增大,重传的频率也会降低。因为当你重传的次数过多时,你成功的概率也就越来越小了。此时重传的太快也都是白白浪费系统资源。
接下里是超时重传的示例图:
当数据丢包的时候:
当应答报文丢包的时候:
从这张图我们也可以看出,当应答报文丢包的时候,接收方是会收到两条数据的。这个操作是非常恐怖的,假如你发送的数据是一个付款数据呢?因此,TCP协议对于数据重复是有特殊处理的。
接收方收到发送方的数据时,其实接收方是将数据从网卡中读出来,接着放进对应进程的socket缓冲区中去(后续应用程序使用getInputStream,进一步使用read操作,都是在针对数据缓冲区进行行读操作的)。在数据缓冲区中,因为每个数据都有相应的序号,因此TCP就可以很容易的识别出重复的数据,并进行去重。并且,TCP的数据缓冲区相当于是一个有序队列,它不仅可以根据序号进行去重,还可以根据序号对数据进行排序,保证数据的顺序(后发先至就被解决了)!
总结:1.由于去重和重新排序的机制的存在。发送方只要发现ACK没有按时到达,就会重传数据。即使数据重复了或者顺序乱了,数据缓冲区都会进行相应的处理。
2.可靠传输是TCP最核心的部分,TCP的可靠传输就是通过确认应答+超时传说来进行体现的。确认应答是传输顺利的情况,超时重传是传输失败时候的情况。两个机制相互配合,支撑起了TCP的可靠性。
2.3 连接管理
连接管理主要是包括TCP的建立过程(三次握手)和断开连接(四次挥手)的过程。
2.3.1 建立连接(三次握手)
我们知道,通信时,通信双方需要记录对方的信息,彼此之间要相互认同。而建立认同这个过程就是三次握手。就像一对新人走进婚姻的殿堂的时候,他们都会有一下的对话:
这就是一次建立认同(连接)的过程。他们互相对对方许诺,想要成为对方的唯一,并且对方都统同意了。这样连接就建立起来了!
在TCP中,“亲爱的,你愿意做我的新娘吗?”其实是一个SYN报文,称为同步报文。 而“我愿意!”这句话就是一个应答报文。
因此就是下面这个图:
看到这里,就会有人说:这不是四次握手吗?哪里是三次握手。其实,这里就是三次握手,因为第二次和第三次是可以合并成一次交互的!因为第二次和第三次报文发送的时机几乎是同时的,并且,中间这两次合并之后,成本势必会比不合并的时候低。因此,此图就变成下面这样:
那就有人说,中间两次不合并可以吗?我的回答时,不行。因为合并了之后,成本会降低,就例如你在淘宝某个店里买一些小物件,你分两次下单物品。这时,商家肯定也是给你一个包裹就发过来两个小物品,而不是分两个包裹给你发过来。
如果四次握手不行,那么两次握手可以吗?我的回答是,也不可以!
因为,当是两次握手的时候,说明主机A就不发送最后一个ACK,如果不发送最后一个ACK,那么主机B怎么知道是它的数据丢了,还是是主机A的ACK丢了呢?这样的话,连接该如何建立?就如一对新人,当新郎问新娘:你愿意做我的新娘吗。新娘回答:我愿意!但当新娘问新郎:你愿意做我的新娘吗?新郎这个时候沉默不语,那么新娘该怎么想?就是这个道理。因此,两次握手不可以。
并且,三次握手还有一个重要的作用:验证通信双各自的发送能力和接收能力是否正常。如果是两次握手,那么主机A就不知道主机B的发送能力和接收能力是否正常。因此,三次握手也一定程度上保证了TCP传输的可靠性。
总结三次握手的意义:
1. 让通信双方各自建立对对方的认同。
2. 验证通信双方的发送和接收能力是否正常。
3. 在握手的时候,协商一些重要参数。
2.3.2 断开连接(四次挥手)
四次挥手和三次握手类似。四次挥手是通过通信双方给对方发送一个断开连接的请求再各自给对方一个回应。这里的“挥手”和三次握手里面的“握手”,其实都是服务器和客户端之间的数据交互。
四次挥手可以用个例子形象的说明一下:
这就跟四次挥手一样,我给我的兄弟说:“我要回家了”,其实就是断开连接的操作。然后兄弟回了句:“好,路上慢点”,就是说明我的兄弟已经知道我要回家了。接着我的兄弟给我说:“那我也回家了”(我也要断开连接了),就是给我通知,他也要回家了。我也回一个:“好的,路上慢点”,表明我知道我兄弟也要回家了。此时我们的连接就彻底断开了,各找各妈了。
下面是四次挥手的示意图:
上图中,FIN是结束报文段。那有人就会问:这里的第二次和第三次交互不能合并吗?我的回答是:一般不能!
在三次握手中,第二次和第三次能合并是因为SYN和ACK都是在内核中完成的。当系统内核收到SYN的时候,会立即返回一个ACK,也会立即返回一个SYN,这两个报文的间隔时间极短,因此可以合并。但是在四次挥手中,FIN的发起不是由内核控制的,而是由应用程序。当应用程序调用socket的close方法,这时才会触发FIN。但是我们并不知道,当主机A调用了close方法之后,主机B返回ACK之后,多久才能执行到FIN。这主要是看程序员的代码如何实现。因此ACK和FIN之间就隔了一个时间,因此不能被合并。但是当两者之间的间隔时间极短,也是有可能会被合并的。
2.4 滑动窗口
上面讲到的确认应答、超时重传、连接管理其实都是给TCP的可靠性提供支持的。但是我们要知道,效率和可靠是不可兼得的。此时,滑动窗口就出现了,它是在保证可靠性的基础上,来尽量提高传输的效率(亡羊补牢)。但是我们要知道,虽然提高了效率, 但是还是没有UDP那种毫不关心可靠性的效率高。
我们知道,每次发送方发一个数据,都需要等待接收方回一个ACK,这就大大折损了效率。但是滑动窗口就是:不等待的批量发送一批数据,使用一份时间来等待这一组数据的ACK。
示例图如下:
这就是滑动窗口的示例图。其中,把不需要等待,直接就能发送的数据的最大的量,称为“窗口大小”。 在此图中,窗口的大小就是4000。
当批量发送了 窗口大小 的这些数据之后,发送方就需要等待ACK的到达了。那么,发送方什么时候可以继续发送数据了呢?注意:并不是等待所以的ACK到达之后才能继续发送数据。而是到达一个ACK,就继续往下发一条数据。这样,发送方要等待的ACK就一直是“窗口大小”这么多的ACK了。
但是上面我们讨论的都是数据没有丢包的情况下,如果数据丢包了,怎么办呢?其中,丢包分为两种情况:
1.ACK丢了
如果ACK丢了,示例图如下:
如上图所示,当我们的数据全部到达,但是ACK却有几个没有到达,这时该怎么办呢?此时,不用担心,这种情况对数据传输没有影响。因为,我们要知道ACK序号的含义是该序号之前的数据全部都已经介绍到了。因此,一个ACK丢了并没有关系,接收方仍可以通过下一个ACK来确认上一条数据是否到达。
就假如我的1001这个ACK丢失了,但是ACK2001到了,那么就代表2001之前的所有数据都已经收到了。
2.数据丢了
上图就是数据丢了的情况。假设1001-2000的数据丢了,那么接下来的ACK都会索要1001这个数据。此时虽然主机A已经发送了许多数据,但是主机B一直在索要1001,说明1001这个数据主机B是没有收到的,因此就对1001进行重传。当 1001这个数据到达了之后,接下来主机B索要的数据就是4001了。
上面丢包重传的方式,起了个名字叫做“快速重传”(重传只重传了丢失的数据)。
如果当前传输的数据较多时,按照滑动窗口的方式来传输,此时就按照快速重传的方式来处理丢包。而当传输数据较少的时候,就不按照滑动窗口的方式来发送数据了,此时就还是按照超时重传的规则来处理丢包。
2.5 流量控制
流量控制是一种干预窗口大小的机制。上面所讲的“滑动窗口”机制,不是说滑动窗口越大越好,虽然滑动窗口的窗口越大,传输效率就越高。但是,窗口也不能无限的大。因为:
1.当窗口太大的时候,可靠性就画上了问号,因为完全不等待ACK,ACK就无法保障了。
2.如果发送方发的太快,接收方来不及接收,发了也是白发。
3.窗口太大,消耗的系统资源也就越多。
因此,就需要限制一下窗口大小,发送方的发送速度,不能超过接收方的接收处理能力。流量控制的工作就是:根据接收方的处理能力,协调发送方的发送速率。
但是,如何衡量接收方的接收能力呢?大佬们就通过 接收方的接收缓冲区的剩余大小来衡量接收方的处理能力。当此时接收缓冲区的剩余空间较大时,就使发送速率增大;当接收缓冲区的剩余空间较少时,就使发送速率降低。因此窗口大小是一直在动态调整的。
我们可以把接收缓冲区想象成一个蓄水池。接收方从下面的放水,发送方从上方倒水。用蓄水池的剩余量来决定接下来的发送速率是多少。
注意:接下来的窗口大小是多少是通过ACK报文来传送的,其中,报头中有“16位窗口大小”这样一个数据,这里就是用来存储接下来的窗口大小为多少的,当发送方收到ACK后,就会根据“16位窗口大小”的值来调整窗口大小。
并且TCP为了使“16位窗口大小”表示的数字更大,在选项部分就引入了“窗口扩展因子”。
例如:窗口大小为64kb,扩展因子为2,此时就意味着让64kb<<2位,表示的就是256kb。
2.6 拥塞控制
上面讲了流量控制,其实,决定窗口大小的机制还有一个是“拥塞控制”。流量控制考虑的是接收方的能力,而拥塞控制描述的是传输过程中,中间节点的控制能力。
我们知道,两台主机之间的路线是错综复杂的,要经过许多交换机和路由器的。在流量控制中,只考虑了接收方的处理能力,没有考虑中间节点的传输速度。而网络传输是一个木桶效应,如果传输过程中,其他节点都很快,只有一个节点速度很慢,那么整体的速度也就是慢的。
那么,中间节点的处理能力该怎么衡量呢?设计TCP的大佬们就用了一个绝妙的方法来测试出合适的值。大佬们利用“实验”,来测试出合适的值。
下图就是用三个控制的示意图:
我们看到,在第0轮的时候,拥塞窗口大小为1,第1轮为2,第2轮为4,第三轮为8,因为此时窗口大小较小,因此此时为指数规律增长。直到拥塞窗口大小为16的时候(当增长速率达到阈值时),增长策略就从指数规律变成加法增长,此时拥塞窗口大小也就随着传输轮次慢慢增长,当增长到一定程度的时候,出现了网络堵塞/丢包。此时拥塞窗口大小就会突然降的很低,继续重复刚才的操作。
因此拥塞窗口不是固定数值,而是不断变化的。拥塞窗口和流量控制的窗口大小一起决定了滑动窗口大小(两者较小值)。
2.7 延时应答
延时应答也是一种提升效率的机制。它是在滑动窗口的基础设计的。滑动窗口主要是让窗口大一点,传输效率就快。因此延时应答要做的就是,尽量让窗口大小放大一点。
延迟应答就是收到数据之后,不立即返回ACK,而是等待一会再返回ACK。在等待的时间里面,接收方就能将接收缓冲区的数据消耗一波,这样剩余的空间就大了。
下面是其示意图:
上图中ACK就不是每一条数据都返回,而是隔一条返回一个。
2.8 捎带应答
捎带应答是基于延迟应答的。也是提高效率的一种方式。
我们知道,服务器和客户端最典型的模型就是“一问一答”(业务上的请求和响应)。此时,当ACK返回的时候,可以捎带这把返回的数据一块给发送过去。
就例如下面例子:
其中,第二条就是一条ACK,是从系统内核中立即返回的。而第三条是业务上的响应,是主机B从程序中发出的。两者并不是同一时机,但由于延时应答机制,就可能导致ACK的过程中,B也要给A发送数据了,此时这个ACK就可以将业务数据捎上,一起发过去就行了。此时第二条和第三条就合并了,效率也就高了。
但是这个和连接管理中的三次握手里的合并不一样。三次握手中第二次和第三次可以合并是因为两次和第三次报文发送的时机是几乎一致的,因此可以合并。而捎带应答里的合并是ACK“故意”等一下数据,因而合并的。因此两个合并的原理是不一样的。
2.9 粘包问题
首先说明,这里的“包”,指的是“数据包”。我们知道,TCP的协议头里面,没有报文长度这一个字段,但是TCP数据报是一个个发送过来的。由于TCP是字节流的,一次读一个字节或者N个字节都是可以的。因此,就可能导致一次读到的数据可能是半个应用层数据报,也可能是一个半应用层数据报。这个就叫做粘包问题。
解决这个方法的方法也很简单:
1. 约定好分隔符。
我们可以提前约定好分隔符,假如我约定“/n”为分隔符,这样,当我读到 /n 的时候,就完成读操作。然后再读一下个即可。
2. 约定好包的长度。
我们也可以约定好一个包的大小,假设所有的包都是1000字节,那么我们只需要从头读到1000字节,这就是一个包,然后再读1000字节,就是第二个包。这样就分隔开了。
2.10 TCP异常终止
1.进程崩溃和主机关机
进程崩溃和主机关机实际上是同一种问题,都是进程没了。因此进程对应的PCB就没有了,相应的文件描述符表喝酒释放了。相当于是进行了socket.close()操作,因此此时系统内核中依旧会完成四次挥手的操作,此时其实是一个正常断开的过程。
2.主机断电和网线断开
主机断电和网线断开显然是无征兆的。
假设是接收方主机断电了,在断电的一瞬间,主机之间关闭了,并没有走关机时杀进程这一操作。因此是来不及挥手的。此时发送方还依旧在向接收方发送消息,但是发送方并不知道接收方已经收不到了。当发送方不断地发送数据,而迟迟等不到ack时,就认为是自己的数据丢包了,因此就进行超时重传,但是重传多次之后,依旧没有收到ack。就会尝试重置TCP连接(复位报文段 RST),显然重置操作也会失败。因此发送方就单方面放弃连接了。
如果是发送方突然断电呢?当接收方收不到数据的时候,接收方不知道是发送方断电了还是对方要组织语言还是发送方数据报丢包了。因此就先等待一段时间。但是接收方会周期性的给发送方发送一个“心跳包”,确认一下对方是否还在正常工作。如果对方没有反应,就说明对方已经没有正常工作了。
以上就是TCP协议的内容,本人知识有限,难免有误,请多多指教!