TCP协议也是传输层的一个重要的网络协议,也是我们面试的常考点。
TCP协议的特点是有链接,面向字节流,可靠传输、全双工的特点
1.TCP协议段格式
下图是TCP协议的格式内容,下图的内容应该都是一行的,为了方便,所以画成下面那样
1.源端口号和目的端口号是传输层协议的核心内容,表述数据从哪个进程来,到哪个进程去
2.32位序号/32为确认序号:32位序号表示该某一段字节流发送的起点,32位确认序号表示下一段字节流发送的起点
3.4位首段长度:表示TCP报头的长度,这里的一位是以15bite为单位,所以TCP报头的最大长度可达到15*4=60个bite的大小
4.选项:选项就是TCP长度可变的原因,固定长度是20字节,选项最多增加至40字节
5保留位:UDP长度不够时,不能扩展,而保留位就是为了解决这种问题的
- 6位标志位:
2.TCP十大核心机制
2.1 确认应答
确认应答机制是为了保证让数据发送方知道数据接收方成功接收到数据,同时也是保证可靠性的机制之一
确认应答是如何让数据发送方知道数据接收方成功接收到数据的呢?
此时就涉及到了一个名为"应答报文"东西,此时假设A是数据发送方,B是数据接收方,此时A向B发送一段数据,当B成功接收到数据之后,就会给A返回一个应答报文(acknowledge,ack),此时A接收到B返回的ack后,A就知道B成功接收到发出的数据了
如下图

但是确认应答机制有一个问题,由于网络传输是一个错综复杂的过程,就有可能出现发出的消息出现一种 "后发先置" 的问题
比如,你向你的好朋友发送了两条信息,第一条信息是约你的好朋友去学习,第二条消息是约你的好朋友去打游戏,此时好朋友接收到这两条消息后,好朋友针对第一条消息返回一个不好,针对第二个消息返回一个好的,此时好朋友的意思是就是想去打游戏啊,不想去学习,但是这两条数据在返回给你时,首先你先接收到了好朋友返回的第二条数据,后接到了好朋友返回的一条数据,这就让你误认为你的好朋友想去学习,不想去打游戏
TCP针对"先发后置"这个问题的解决方案是给传输的数据进行编号。
此时32为序号和32位确认序号就起到作用了。
由于TCP是面向字节流来传输数据的,且TCP数据报的载荷是由多个字节组成了,此时TCP就会给在载荷中的每一个字节分配一个编号。
此时由于有多个编号,此时序号字段填写的是载荷部分的第一个字节的编号,确认序号填写了的是 把接收到的数据载荷的最后一个字节序号+1

每一个ACK都带有对应的确认序号,意思是告诉发送者,我已经接收到哪些数据,下一次你从哪里开始发
举个例子
假设A向B发出了一条消息,由于TCP协议是面向字节流的,这条消息可能会被拆分成多段进行传输,每段在传输时都会带有一个序号,表示这段从哪个字节开始读取,接收方每次接收到一段数据,就会返回一个ack,该ack中就填写了确认序号的值,告诉数据发送方下一次的数据发送从哪个字节开始发送,如下图

有了序号和确认序号,接收方就可以根据序号对接收到的数据进行排序。
此时为了TCP在处理"后发先至"的情况的时候,还要确保数据接收方按照正确的顺序去读取接收到的数据
此时是如何保证数据接收方按照正确的顺序读取数据呢?
TCP在数据接收方这里会安排一个"接收缓冲区(在操作系统内核里)",此时将接收到的数据先放到接收缓冲区里面,此时接收缓冲区中的数据会根据序号的大小来排序,让序号小的数据排在前面,让序号大的数据排在前面,此时后续代码中调用read方法来从接收缓冲区中读取数据时,读取某一段数据时,确保前面的数据已经到达才会解除read的阻塞状态,如果是后面的数据先到,此时read会持续阻塞,知道前面的数据到达接收缓冲区
2.2超时重传
超时重传也是保证网络传输可靠性的机制之一
超时重传是TCP协议专门针对丢包的一个机制
超时重传就是数据发送方在发出数据后,数据发送方迟迟接收不到接收方返回的ack,一直处于等待状态,当等待一定的时间之后且还没接收到ack,就会认为这次网络传输发生了丢包现象,并重新发送这条数据
为什么会发生丢包呢?
由于网络传输一个非常复杂的事情,当数据包在传输的过程中,遇到某个交换机或者路由器转发的时候,此时如果交换机或者路由器已经非常繁忙了,此时就会将新接收到转发的数据就会导致当前需要转发的数量超出路由器或者交换机的转发能力上限,此时数据报就会消耗更多的时间才会达到对方,更糟糕的情况是,交换机或者路由器处理的数据报实在是太多了,根本处理不过来,接收缓冲区都满,此时就只能丢弃一些数据报了
此时还有一个问题,就是数据接收方是如何判断发生丢包现象的呢?
答案就是引入超时时间来判断是否发生丢包,这里的超时时间是指数据发送方等待数据接收方返回的ack的时间
当数据发送方等待ack的时间大于超时时间之后,此时就会认为发生了丢包,就会进行重传
在TCP中,判断超时的时间阈值,不是固定的,而是动态改变的
假设当前A向B发送数据,此时判断丢包的时间阈值为T,当A给B传输发生超时之后,就会延长这个超时时间,然后在进行重传,随着不断进行重传,此时就会让数据达到接收方的概率会越来越高,此时如果还发生超时,就会认为此时丢包的概率非常大,意味着当前网络出现了严重的状况,就会降低重传的概率
当然也不会无休止的延长这个超时时间,当超时次数达到一定程度之后,就会放弃这次网络传输
此时的丢包会有两种情况:
第一种情况是数据发送方发送的数据发生了丢包现象
第二种情况就是数据接收方返回的ack发生了丢包现象
由于数据发送方分不清到底是哪一种情况,此时上面的两种情况的解决方案都是数据发送方进行重传
如下图,第一种情况:

第二种情况:

此时第二中情况就有一个问题,那就是进行重传后,主机B就接收到了两份同样的数据,如果tcp不处理,可能会使引用层读到两份一样的数据,这是非常不好了,比如扣款数据,你明明只买了一个,却扣了两个的钱,这是一个非常严重的问题
此时为了解决这个问题,tcp就会在内部进行去重操作,由于接收到的数据会先存储到接收缓冲区中,此时就可以根据序号去接受缓冲区中找一下,如果接受缓冲区中存在这样一份数据,就会将新接收到的同样的数据丢弃掉,如果不存在这样一份数据,才放进接收缓存区里面去
确认应答和超时重传是TCP协议最核心的两个机制,保证了TCP能够进行可靠传输
2.3连接管理
连接管理也是TCP的重要机制之一,在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接
2.3.1三次握手
TCP协议是要经过三次握手来建立连接的,这里的连接是一种逻辑上的连接,是通信双反各自保存对端的信息
三次握手的过程是怎么样的呢?
假设此时客户端和一个服务器,此时客户端要与服务器建立连接,首先客户端会想服务器方发送一个syn(同步报文段),此时服务器接收到客户端发送的syn之后,服务器也会给客户端返回一个ack和一个syn,当客户端接收到服务器返回的ack和syn之后,客户端就会处于一个ESTABLISHED状态,此时也会返回一个ack给服务器,当服务器接收到客户端返回的ack之后,此时服务器也会处于一个ESTABLISHED状态,当客户端和服务器都处于Established状态,说明连接建立成功,也就是完成了三次握手,如下图

此时有些同学看了上面这张图,此时就很疑惑了,这不是4次握手了吗?
但是服务器在发送syn和返回ack的时,两个操作是和操作系统的内核相关的,与代码无关,此时就可以将这两步封装成一步,要知道数据在网络上传输是要经过数据的封装与分用的,将返回syn和ack封装成一步,此时也是可以节省网络资源和提高传输的效率的,如下图

为什么TCP建立连接偏偏是三次握手,不是两次握手,不是四次握手呢?
首先,因为TCP协议是要保证可靠传输的,而网络通畅是可靠传输的前提,三次握手相当于先初步试探一下当前网络的通信链路是否通畅。
其次就是三次握手就是要保证通信双方都要知道对方能正常发送消息和正常接收消息,此时如果只进行两次握手的话,此时的状态就是,消息发送方知道的发送消息的能力正常和接收消息能力正常,也能知道消息接收方正常的发送消息和正常的接收消息。但是此时消息接受方只知道自己的消息接受能力正常和对方的发送消息能量正常,此时不能保证自己的消息发送能力是否正常,也无法知道对方的消息接受能力是否正常,所以此时就需要第三次握手,让消息接受方知道自己的消息发送和对方的消息接受能力都处于正常状态
如下图

最后就是三次握手的过程中可以协商一些关键信息,TCP要协商的一个关键信息就是:初始序列号,初始序列号代表了发送方传输数据的起始序号,也是消息接受方判断该数据是否有效的判断依据。
什么意思呢?
假设客户端和服务器建立了一次链接,客户端发送了一个数据报,但是此时数据包在传输的过程中迷路了,此时客户端和服务器之间断开连接,过了一段时间,客户端和服务器再次建立连接,如果没有协商好初始序列号,此时服务器还是根据上次连接时的初始序列号读取数据,此时服务器就可能接收到了上次连接时的迷路的数据包,显然这个迷路的数据包不是一个有效的数据包,此时服务器就没必要去处理这个数据包而是要将这个数据包剔除掉,此时就可以根据建立连接过程中协商好的初始序列号来解决这个问题

2.3.2四次挥手
有建立连接的过程,就有断开连接的过程,TCP的断开连接是通过四次挥手来实现的
四次挥手的过程:
首先是客户端或者服务端发起第一个fin,一般都是有客户端发起第一个fin,下面就以客户端发起fin来讲解
首先,客户端调用socket.close()方法,向服务端发送一个fin,告诉服务端我要关闭数据传输通道了,此时服务端接收到客户端发来的fin之后,服务端也会向客户端返回一个ack和fin,因为ack的发送是有内核控制的,和我们的项目程序代码是无关的,所以此时服务端的内核接受到fin后,会第一时间返回一个ack给客户端,在这之后,服务端就会处于CLOSE_WAIT状态,这个状态是为了保证服务端将还没有传输完的数据全部传输完毕,当服务端将全部数据传输完毕之后,此时服务端就会调用socket.close方法,向客户端也返回一个fin,当客户端接收到服务端返回的fin之后,此时客户端也会返回一个ack给服务端,此时客户端就会处于一个TIME_WAIT状态,此时只有当服务端接收到客户端返回的ack之后,客户端和服务端才会释放各自的连接。
客户端处于TIME_WAIT状态的作用
因为四次挥手的过程中也是会出现丢包的情况的,如果此时客户端返回ack之后不处于一个TIME_WAIT的状态,而是立刻断开连接的话,如果客户端返回的ack出现了丢包的情况,由于服务端迟迟接受不到客户端返回的ack,此时服务端这边就会触发超时重传,重新返回一个fin给客户端,但是由于此前客户端已经释放连接了,所以此时就没有谁来处理这个fin,此时服务端就也不会接受到ack,这就会导致服务端这边长时间内无法断开连接,一直重传到超时为止,这就会出现一个浪费网路资源的问题,所以,这个TIME_WAIT状态是为了对抗最后一个ack丢包的情况
2.4滑动窗口
滑动窗口是为了解决网络传输效率的问题,如果没有滑动窗口,数据发送方每发送一个数据之后,必须等待对应的ack返回后才能发送下一条数据,如果要传输的数据很多和网络延迟较大的情况下,这种传输数据方式的效率肯定是很慢的,如下图

而有了滑动窗口机制后,允许发送方在未收到ack时,连续发送窗口大小内的多个数据包,如下图

滑动窗口机制解析:
- 窗口大小是指无需等待确认应答而可以继续发送数据的最大值,上图的滑动窗口大小就是4000个字节
- 发送前4个段时,无需等待任何ack,直接发送
- 收到第一个ack时,滑动窗口向后移动,继续发送第5个段的数据,然后以此类推下去
- 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前发送出去的数据还有哪些没有应答;只有确认应答过的数据才能从缓冲区中删掉
- 窗口越大,则网络的吞吐率就越高

如果此时出现了丢包现象,是如何进行重传的呢?此时会分为两种情况
第一种情况:数据包已抵达,但是返回的ack丢了
在这种情况下,部分ack丢了并不要紧,因为可以通过后续的ack来确定,举个例子,假设第3001~4000字节的数据发送后,该段数据返回的ack丢包了,此时后面发送的第4001~5000的数据成功返回了ack的话,就是就也可以保证5000字节前的数据数据接收方已经成功接收到数据了,此时数据发送方就可以知道地3001~4000字节的数据已经成功接收,不需要重传

第二中情况是数据包直接丢了
- 如下图,当第1001~2000字节这一段的报文段发生丢包后,数据发送方就会一直接收到数据接收方放回的"下一个是1001"这样的ack,就像是提醒发送"我迟迟没有接收到第1001~2000字节"的数据
- 如果数据发送方连续收到了同样的"下一个是1001"的这样的ack,此时数据发送方就会将第1001~2000的数据重新发送
- 此时如果数据接收方成功接收到了第1001~2000字节的数据,此时再次返回的ack就是7001了,因为第2001~7000字节的数据接收方之前就已经收到了,被放到了接收端操作系统内核的系统缓冲区中

2.5流量控制
有了滑动窗口之后呢,虽然提高了数据传输的效率,但是不能只追求速度,因为数据接收方处理数据的是有限的,此时如果接收方的接收缓冲区已经满了,此时发送方还是继续发送数据的话,就会造成丢包,从而引起丢包重传等等一些列的连锁反应
因此TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就是流量控制
流量控制就是允许接收方将自己可以接收的数据缓冲区的大小放入TCP首部中的"窗口大小"字段(窗口大小字段越大,说明网络的吞吐量越高),并通过返回ack的方式来告知发送方,当接收方发现自己的接收缓冲区快满了,就会将窗口大小的值设置成更小的值通知给发送方,当发送方接收到这个窗口值之后,就会减缓自己数据的发送速度。如果此时接收方的接收缓冲区已经满了的话,此时就会将窗口值设置为0,此时发送方接收到这个值为0的窗口值之后,就会停止数据的发送。但是为了能够及时得察觉到接收方的缓冲区有了空闲空间,发送方会时不时得向接收方发送一个窗口探测包,这个窗口探测包主要目的的触发ack机制,也是通过ack来得知接收方的接收缓冲区的大小
注意:发送方是时不时得发送窗口探测包,因为如果只是发送一次窗口探测包的话,而窗口探测包返回的ack可能会出现丢包情况,此时就会导致发送方迟迟察觉不到接收方的接收缓冲区的大小变化情况
但是TCP首部的窗口字段只有16位,最大只能表示65535字节。在现代的高速网络中,这个值太小了,因此引入了选项字段中的"窗口缩放因子",实际窗口大小=首部窗口值*2^(缩放因子)

2.6拥塞控制
虽然TCP有了滑动窗口这个机制,能够高效可靠的发送大量的数据,但是如果在刚开始阶段就发送大量的数据,还是可能会引发一些问题。因为网络上是有很多计算机,可能当前的网络状态就已经很拥堵了,此时如果贸然发送大量的数据,是很有可能导致大量数据传输失败或者其他后果的。
此时TCP的拥塞控制机制就发挥作用了,拥塞控制是依据传输链路的转发能力进行限制的,因为一次网络数据传输会涉及到很多设备,所以如果我们想具体衡量到某一个设备,这几乎是不可能的,此时就可以将整个通信链路视为一个整体,通过"做实验的方式"来找到一个合适的窗口大小
做实验就类似于水多加面,面多加水的过程,先按照小的窗口发着,如果此时发得很顺利,没有丢包现象,此时就加大速度,如果出现丢包现象,就减小速度,一直持续达到一种动态平衡的效果
如下图:

- 慢启动:初始窗口的大小
- ssthresh:预设的窗口大小
- 过程解析:慢启动->指数增长->线性增长->丢包,窗口变回较小值
拥塞控制归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案
2.7延时应答
默认的情况下,接收方都是在收到数据报的第一瞬间就返回ack,但是可以通过延迟返回ack的方式来提高效率的。
举个例子:
假设此时数据接收方的接收缓冲区的大小是1M,此时数据发送方向接收方发送了500M的数据,此时如果立刻返回ack的话,此时数据发送方就得知接收方的接收缓冲区大小就只剩下500M了,此时发送方就会减小窗口大小来发送数据,但是这发送过来的500M数据对于接收方来说,接收方实际处理数据的速度很快,可能在10ms内就将这500M的数据全部处理完了,所以在这种情况下,接收方还远远没有达到自己的极限,即使在窗口在放大一些,也能处理过来,如果此时接收方稍微等待一会再返回ack,比如等待个200ms,那么此时返回的窗口大小就1M了。
所以通过延迟应答就可以尽可能的让发送方在接收方的实力处理数据的能力范围内,尽可能得以较大的窗口大小来发送数据
但是也不是所有的包都可以延迟应答的,有两个限制:
- 数量限制:每隔N个包就应答一次
- 时间限制:超过最大延迟时间就应答一次

2.8捎带应答
TCP有了延时应答之后,接收方接收到数据之后就不会立即返回ack,此时,就基于延时应答,引入了"捎带应答",就是在返回业务数据时顺便将上次的ack的带回去,此时就是将两次数据的返回封装为一次了
由于每一次网络数据的传输是涉及到数据的封装与分用的,通过捎带应答,就可以将多次数据的传输封装为一次,此时就可以提高数据的传输效率。

2.9面向字节流
TCP的面向字节流简单来说就是把数据看做是一串连续的字节序列,没有一个特殊的字段来明确数据包与数据包之间的边界。
由于没有明确边界,此时就会导致一个粘包问题,粘包问题中的包是指应用层数据包,因为没有明确边界,站在传输层的角度来说,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中,但是站在应用层的角度,看到的只是一串连续的字节数据,那么当应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到那个部分结束是一个应用层数据包。
举个例子,假设发送发第一次发送方发了一个a,第二次发了一个b,此时接收方就可能一次性读到了ab
粘包问题的解决方案:
- 站在TCP的角度来说,无解,只能在应用层来解决,需要定义好应用层协议,明确包之间的边界
- 方法1:约定包与包之间的分隔符(确定一个数据包的结束标记)
- 方法2:约定包的长度,比如约定每个包开头的4个字节来表示数据包一共多长
2.10异常情况的处理
TCP在通信过程会可能遇到一些特殊情况:
1.某个进程崩溃了,这种情况下会回收文件描述符,任然可以发送fin,还是可以出发四次挥手,进程是没了,但是TCP的连接信息还在,此时四次挥手还是可以继续进行的
2.主机关机了,关机的本质还是杀死所有进程,因为关机是需要时间的,如果在这段时间内完成了四次挥手,此时还是可以正常释放连接的,但是在这段时间内没有完成四次挥手呢?
假设发送方向接收方发送了一个fin,此时发送方接收到接收方返回的ack和fin之后,发送方这里就关机了,此时意味着这接收端的fin没有ack,当接收方长时间没有接收到ack后,就会出发超市重传,但是如果进过几次重传之后,还是没有接收到ack,此时就会认为对端发生了严重问题,此时接收端就主动放弃连接(也就是接收端把保存的发送方信息删掉)
3.主机掉电或者网线断开了
此时分为两种情况:
接收方掉电:
当接收方掉电了,意味着意味着发送端后续发来的数据都没有ack了,此时发送端这边就会出发超时重传,当重传到一定次数还是不能解决问题的话,就会出发"TCP重置连接",发送方主动发送一个复位报文---RST,此时如果RST还是没有ack的话,此时发送方就只能单方面释放连接了
发送方掉电了:
此时的发送方一般是指客户端,接收方一般是指服务端,此时服务端突然发现客户端没有声音了,此时服务端无法区分客户端是挂了还是暂时休息一会,服务端就只能等待了,但也不会一直等待,此时服务端等待了一定时间后,就会给客户端传输一个特殊的报文---心跳包,该心跳包只是为了出发ack,如果对方有心跳的话,则正常等待,如果没心跳,就只能通过rst尝试重置TCP连接,还是不行的话,此时也只能单方面释放连接了
3.TCP和UDP对比
- TCP是可靠连接,但是TCP不一定由于UDP,还是要根据实际因为来决定使用TCP或者UDP
- 比如TCP用于可靠传输的情况,应用与文件传输,重要状态更新等场景
- UDP用于高速传输和实时性要求较高的通信领域。