TCP协议

发布于:2025-03-04 ⋅ 阅读:(13) ⋅ 点赞:(0)

TCP的特点

  • 有连接:TCP想要进行通信需要先和对方建立连接(互相保存对方信息),做完后在可以通信
  • 可靠传输:这里的可靠不是确保数据一定能正确传输给对方,而是可以确保对方正确收到
  • 面向字节流:TCP也是和文件传输一样以字节为单位进行传输
  • 全双工:一个通道,双向通信

TCP报文格式 

 源端口号/目的端口号:表示发送方端口号,和接收方端口号,用来描述数据从哪个进程来到哪个进程去。

4位首部长度:表示TCP报头的长度,这里的单位是32bit(4字节),因为该部分长度是4bit,所以TCP报头长度最大为15*4=60字节

注意:TCP报头长度是可变的,前20个字节是固定不变的,在下面有一个选项,后面的选项是可变的,最大是40个字节

6位保留位:方便之后扩展,目前没有明确的定义

6位标志位

  • URG紧急指针是否有效
  • ACK确认号是否有效
  • PSH:提示接收端应用程序立刻从缓冲区把数据读走
  • RST:对方要求重新建立连接,我们把携带RST标识的称为复位报文段
  • SYN:请求建立连接,我们把携带SYN标识的称为同步报文段
  • FIN:通知对方,本端要关闭了。我们称携带FIN标识的为结束报文段

具体用法会在之后介绍

 16位校验和:接收端校验不通过,就会认为数据有问题,可以看博主的上一篇UDP协议里有具体介绍

16位紧急指针:标识那部分数据是紧急数据

 TCP原理

TCP最核心的特性是--可靠传输,但这个可靠传输并不是指发送方把数据能够100%的传输给接收方,而是发送方发出去数据之后,能够知道接收方是否收到数据,一旦发现对方没收到,就可以通过一系列的手段来"补救"

确认应答(安全机制)

发送方,把数据发给接收方之后,接收方收到数据就会给发送方返回一个应答报文ack(acknowledge)。发送方,如果收到这个应答报文了,就知道自己的数据是否发送成功了。

//如果一条报文为应答报文则ACK就会为1

一段正常的通信应该如下:

但是可能因为一些其他因素导致"后发先至"

这种情况肯定是不符合逻辑的,所以针对这种问题我们可以对请求和应答报文进行编号,请求1就对应,应答1,请求2对应,应答2.

通过序号和确认序号保证可靠传输:

一个TCP数据包里一共有1000个字节的载荷数据,其中第一个字节的序号就是1,就在TCP报头的序号字段中写“1”,由于一共是1000个字节,此时最后一个字节的序号自然就是1000了,但是1000这样的数据并没有在TCP报头中记录,TCP报头中记录的序号,是这一次传输的载荷数据中的第一个字节的序号。在接收到这个数据段之后,就会在应答报文中的确认序号字段中填写1001,因为收到的数据是1~1000,所以1001之前的数据,都已经被B给收到了,或者可以理解为,确认序号是B接下来向A索要1001序号开始的数据。

通过特殊的ack数据包,里面携带的“确认序号”就可以告诉发送方,那些数据已经确认收到了,此时发送方就知道那些数据发送成功,这样就可以保证一定的可靠传输

TCP如何保证可靠传输:通过确认应答为核心,借助其他机制辅助,最终完成可靠传输 

超时重传(安全机制)

如果发送方给接收方发送了一条数据,过了一定时间还没有收到对方返回的响应(ACK),就会认为该条数据丢包了,便会出发超时重传。

出发超时重传的场景有两种

  • 传输时数据丢了
  • 接收方返回ack时,ack丢了

对应发送方是无法区分这两种情况的,无所无论出现那种情况都会触发超时重传。

 那么等待多久算超时呢?

初始的等待时间是可以配置的,和不同的系统上可能都不一样,也可以通过修改一些内核参数来改变这个时间,等待的时间也会发生动态变化,每经历一次超时,等待的时间都会变长比如:

A给B发送一条数据,第一次A等待的时间假如是50ms,此时如果达到50ms还没有收到ack,A就会触发超时重传,当A第二次发送后,还没有收到ack,第二次等待时间就会比第一次等待的时间长。不过时间变长也不是无限的,当重传若干次后,时间已经拉长到一定程度,就会认为数据已经传不过去了,就会放弃tcp连接(会触发tcp的重置连接)。

接收端接收到两条相同的数据

假如是因为ack丢包而出发的超时重传,其实数据已经正确的传输到对端了,只是发送方不知道,那么此时在发送一条相同的数据会不会造成bug?理论上是会的,但是TCP已经贴心的把这个问题给我们解决了。TCP有一个接收缓冲区(一个内存空间),会保存当前已经收到的数据已经数据的序号,接收方如果发现,当前发送来的数据,已经在缓冲区中存在,接收方就会把这个后来的数据丢掉,确保应用程序进行read时,读到的只有一条数据。

//接受缓冲区,不仅仅可以进行去重,还能进行重新排序,确保发送的顺序和应用程序读取的顺序是一致的

 三次握手(安全机制)

三次握手是TCP协议用来建立一个稳定连接的机制,通信双方通过三次通信确保已经建立起可靠的连接,并且同步双方的初始序列号。

如果A想要和B建立连接,A就会主动发起握手操作,实际开发中,主动发起握手的一方,就是所谓的“客户端”,被接受的一方就是“服务器”具体流程如下:

//同步报文段是一个特殊的TCP数据包,是没有载荷的(不带业务逻辑)

//不过三次握手为什么有四次数据的交互?其实中间两次是可以合在一起的,也就是说,B向A发送的ack和syn是可以一起发送的

三次握手的步骤:

  • 第一次握手(syn):发送方给接收方发送一个同步报文段syn数据包(6位标志位中的syn设置为1就说明该报文是一条同步报文段),相当于给对方说我要和你建立连接了,然后发送方会随机选择一个初始序列号(序列号并不是从1开始),并将它放在syn数据包中发送给接收方
  • 第二次握手(ack+syn):接收方收到syn后,如果同意和对方建立连接,就会向发送方发送一个(syn+ack)的数据包,表示接受连接,接收方也会随机选择一个序列号把他放在ack+syn包中发送给对方。
  • 第三次握手(ack):发送方收到ack+syn数据包后就知道对方同意建立连接,并且通信没有问题,然后会向接收方发送一个ack数据包,表示确认收到,并完成连接的建立。(在收到ack+syn之后发送方知道接收方通信没有问题,但是接收方没有收到发送方的回应,还不知道发送方是否可以接收到自己的数据,所以发送方此时要发送一个ack表示,我可以接受到你的数据)

 三次握手的意义:

  •  投石问路,确认当前网络是否通畅
  • 要让发送方和接收方都能确认自己的发送能力和接受能力正常
  • 让通信双方,在握手的过程中,正对一些重要的参数进行协商

 握手要协商的信息其实有好几个,比如tcp在通信时的序号从几开始就是协商出来的,因为有时网络不太好客户端和服务器可能会断开连接后又重新连接,重新连接时就有可能在新的连接建立好了之后,旧的数据姗姗来迟,这种迟到的数据,是应该丢掉的,不能形象到现在的数据,通过这样的设定序号的规则就可以有效避免重连之后的数据受到断开之前的数据的影响。

 三次握手时的状态:

四次挥手(安全机制)

四次挥手是TCP协议用于中止已经建立的连接的机制,通信的双方通过四次通信确保正确中止连接。

建立连接一般是由客户端主动发起的,而断开连接客户端和服务器都可以主动发起,断开连接就相当于A和B都把对方的信息删除了

四次挥手的步骤:

 第一次挥手(FIN):发送方向接收方发送一个FIN(结束报文段)数据包(6位标志位中的FIN设置为1),表示自己请求断开连接,不在发送数据,但是还可以接收数据

第二次挥手(ACK):接收方收到FIN数据包后,向发送方发送一个ACK数据包表示自己收到断开请求,表示告诉对方自己已经请求。

第三次挥手(FIN):接收方在发送ACK后还会发送一个FIN数据包,表示接收方也请求断开连接,不在发送数据,但是可以接收数据

第四次挥手(ACK):发送方收到FIN包后,还会在向接收方发送一个ACK数据包,表示告诉对方自己已经收到请求。

 四次握手为什么不将中间两次通信合并?

因为接收方ACK和FIN的触发时机是不同的,ACK是内核响应,接收方收到发送方的FIN就会立即返回ACK,而接收方的FIN是应用程序的代码出发,接收方这边调用close方法才会出发FIN,从接受方收到FIN(同时发送ACK),再到执行到close发起FIN,这中间要经历多少时间,是不确定的(看具体代码怎么写)。之前的三次握手。ACK和第二个SYN都是内核触发,可以合并。

四次挥手的状态 

 //当哪一方主动断开连接,哪一方就会进入TIME_WAIT(等待)状态,这个状态存在的意义是,防止A的最后一个ACK丢失,留一个后手。如果最后一个ACK丢了,在B的角度看,没有收到ACK就会触发超时重传,再发起一次FIN,如果没有TIME_WAIT状态,就意味着A此时已经断开连接释放了,B发送的FIN就永远也不会有回应了。

 A这边使用这个TIME_WAIT状态进行等待,就是为了处理这种情况,重传的FIN有回应,重传才有意义,假如网络上连个节点通信消耗的最大时间位m,此时TIME_WAIT等待的时间就是2m

滑动窗口(效率机制)

我们刚刚所讨论的确认应答机制,当客户端发送一个数据包之后服务器返回一个ack,这样的传输效率太低,如果一个ack传输出现问题,通信双方都要进行等待,既然一条一条的发的效率低,那么我们就多条一起发,这样可以把多条数据包或ack丢包等待重传的时间,叠加在一起(也就是,等一个是等,等两个也是等,所以与其等一个,不如一次性多发几个一起等了)。

窗口大小就是,每次无需确认应答就可以发送的数据的最大值,上面就是4000个字节

最开始的四个字段,是不需要返回ack的,在收到第一个ack后滑动窗口向后移动,继续发送第五个数据,以此类推

操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有那些数据没有应答,只有确认应答过的数据,才能从缓冲区删掉

流量控制(安全机制)

如果发送端发送的数据太多太快,而接收端处理数据的能力跟不上,那么缓存区就会被占满,导致后面的数据发生丢包。

不过TCP有一个机制可以解决这个问题,就是流量控制。就是根据接收端的处理数据的能力,来决定发送方的发送数据的速度。

流量控制的实现过程(假设接收缓冲区的总空间是4000)

  1. 接收方接受到1000大小的数据之后,接收缓冲区还有3000的空间,接收端将自己可以接受的缓冲区大小放入TCP首部的‘窗口大小’字段中,通过ack一起返回给发送方
  2. 发送方收到ACK包之后就会根据,接收方的缓冲区大小确定此次发送数据的窗口大小
  3. 接着发送方进行新一轮数据的发送(假设接收方此时还没来得及处理数据),此时接收方的数据缓冲区满了,就会给发送方反馈0,这就意味着,告诉发送方自己的缓冲区满了,停止发送数据
  4. 发送方收到接收方的请求后,就会暂停发送,但是也不是什么事情都不干,发送方会时不时给接收方发送一个,窗口探测数据段,这个数据段不带有任何业务逻辑,相当于问接收方你当前缓冲区的内存有多少,我可不可以发送数据?当接收到返回的ACK数据包后,发送方就知道是继续暂停还是接着发送数据。

// 接收方返回的ACK的‘窗口大小’字段越大说明,网络的吞吐量越大

不过我们回想一下,TCP首部报头里的‘窗口大小’只有16位数字,难道TCP窗口大小最大就是65535字节吗?实际上,TCP首部40字节的选项字段中,包含一个窗口扩大因子M,实际窗口大小就是窗口字段的值左移M位

阻塞控制(安全机制)

流量控制,是考虑接收方的处理能力,但是我们在数据传输的过程中往往不是从一端直接发送到另一端的,中间一般还会有很多通信结点,那么问题就来了。如果是一对一的传输接收方的处理能力就很好去量化,但是中间再加上几个传输的结点,结构更为复杂,就很难进行量化了。虽然有滑动窗口,但是如果刚开始就发送大量数据,仍然可能出现问题。

TCP引入阻塞控制(慢启动),由于中间节点结构复杂更难以量化,所以就可以使用‘实验’的方式来找到一个合适的窗口大小,让A先按照比较低的速度先以一个小的窗口发送数据,如果数据传输的非常顺利,没有丢包,再尝试使用更大的窗口,然后就这样一点点的变化,流程大概如下:

  • 发送开始时的窗口大小为1
  • 发送端收到接收端返回的ack之后,就将窗口增大
  • 每次发送数据包的时候,将阻塞窗口和接收端主机反馈的窗口大小作比较,取较小值作为实际发送窗口。

不过随着窗口大小不停的增大,达到一定程度,可能中间节点就出现问题了,发送方就要调小窗口大小,如果依然丢包就继续调小窗口直到不丢包,就再次尝试阔大窗口,在这个过程中,发送方不断调整窗口大小,逐渐达成‘动态平衡’(这种做法就相当于把中间节点都视为整体,通过实验的方式,来找到中间节点的瓶颈在哪里,然后确定最后的窗口大小,大概就类似于木桶原理,水面与取决于最低的那块木板)

 不过我们刚刚所谓的初始状态‘1’只是初始比较低,但是之后的增长是呈指数级别的

不过为了让窗口不增长那么快,就会引入一个慢启动的阈值,当窗口大小达到这个阈值后就会由指数增长变为线性增长

拥塞窗口:拥塞控制下,发送方应该按照多块的速度(多大的窗口)来进行传输

网络拥塞:丢包。一旦触发丢包,就把窗口缩小,从新进行前面的,慢开始->指数增长->线性增长,并且会根据当前丢包的窗口大小,重新指定线性增长阈值(避免指数增长一下就达到丢包极限)(如果只是少量的丢包,就只是触发超时重传,如果是大量丢包才会缩小窗口)

//不过以上版本是一个比较经典的拥塞控制的图,实际上,当触发网络阻塞时不会直接回到原来慢开始的值而是任有一定初始速度,和初始值的增长。

流量控制和拥塞控制,都是在限制发送方的发送窗口的大小,最终的窗口大小,是取流量控制和拥塞控制中的较小值。

延时应答(效率机制)

正常情况下,当发送方给接收方发送数据,接收方会立即返回一个ACK,不过可能有时会有一个问题。

假如接收端的缓冲区是1000,第一次发送方给接收方发送500的数据量,此时接收方返回的ACK报头的’窗口大小‘字段就应该是500,表示我还可以接受500的数据量,但是假如接收方的处理数据的速度很快在下次数据还没发来时,就已经把这500的数据处理完了,那么此时其实接收方可以接受1000的数据量,但是发送方的窗口大小却只有500,并没有达到接收方的极限。

不过如果此时接收方延迟一会返回ACK就可以处理更多数据之后,返回一个更大的'窗口大小',就可以让发送速率更大一些(窗口越大,网络吞吐量就越大,传输效率越高)

捎带应答(效率机制)

在网络通信中往往是'一问一答',的形式,即客户端向服务器发送一个请求(request)之后,服务器会返回一个响应(response),而且服务器在收到发送过来数据时还会返回ACK,所以这时我们就可以乡镇将ACK和响应一起返回,这样就进一步提高了效率。

不过ACK是内核立即返回响应,而response则是应用层代码来返回,这两者时机不对,但是由于tcp引入了延时应答机制,收到数据后ACK不一定立即返回,可能会等一会,这时服务器就可以计算好,response返回给客户端的同时将ACK也带上

//其实这里和四次挥手有点类似,之所以要四次挥手就是因为ACK和FIN返回的时机不一样,但是利用延时应答,就可能实现三次挥手

//上图的request和response都是业务上的数据

面向字节流

粘包问题

在最开始就有提到TCP的特点之一就是面向字节流,这样的特点可以让TCP一字节为单位进行数据的传输。但是也会引起粘包问题(此处的包是应用层数据包)

在TCP的报头中没有如同UDP的“报文长度”字段,只是每个报文都有一个序号,假如此时客户端发送了三条数据包111,222,333服务器在读数据时,可以一次读一个字节,也可以一次读100个字节,但最终的目标是为了得到完整的应用层数据报。但是这三条数据包不会直接被服务器接受而是现在服务器的接收缓冲区中按照拍好的序号,以字节的形式紧紧挨在一起。但是站在应用层的角度,这三个数据包只是一串连续的字节数据,不知道从哪里到哪里是一个完整的数据包。

//这就是粘包问题,其实这个问题不只是TCP所独有的,实际上面向字节流的传输方式都会存在这样一个问题

要想解决粘包问题,只需要做一件事,明确两个包之间的边界,比如:

  • 如果发送的都是定长的包(每个包长度一样),就可以让接收方保证,每次按一个包的长度固定读取数据的就行
  • 如果是变长的包,就可以在每个包的头部记下这个包有多长,然后接收方就按这个长度读取就可以读到一个完整的包
  • 对于变长的包还可以用一种简单的方法,在每个包中间加入分割符(不能和正文冲突),比如“/”或者“#”,就以刚刚的例子来讲如果发送的三个包是111/222/333,这样的话服务器就很容易分清三个包了。

异常处理的情况

进程崩溃:进程没了,异常终止,文件描述符表也就释放了,之后就进行正常的四次挥手,相当于调用close(),TCP的连接可以独立于进程存在,进程没了,TCP连接不一定立即没

主机关机:在关机的时候,会先触发强制终止操作(正常进行四次挥手),在进行四次挥手时如果系统已经关闭,对端还没收到ACK和FIN,无法进行后续的ACK返回,此时对端就会触发超时重传,如果还是没有响应,自然就自动放弃连接了(删除对端信息)

主机掉电:这个事情就是一瞬间的事情了,此时进程是来不及反应进行正常四次挥手断开的,但是站在对端角度,是不知道这个事情的。

  • 如果是接收方掉电,发送方就会等待ACK,接着触发超时重传,依然没有效果之后,触发TCP的”重置连接“功能,发起复位报文段RST,再没响应后就会自动断开连接
  • 如果是发送方掉电,接收方长时间没有等到消息,此时就无法区分对方是没发消息还是挂了,此时TCP提供了一个“心跳机制”,接收方也会周期性的发送一个特殊的,不带有任何业务逻辑的数据包,并且需要发送方返回一个ACK,如果重复多次后对方迟迟没有回答,就会认为对方挂了,自己就断开链接了

网线断开:这时和刚刚的主机掉电很类似,但是此时接收方和发送方都可以正常工作,所以发送方会触发超时重传->连接重置->单方面断开连接,接收方则是触发心跳机制->发现对端无响应->单方面断开连接 

TCP和UDP的对比

TCP的优势是可靠传输,应用于大部分场景,UDP的优势是,更高效率,更适合一些可靠性要求不高,但是性能要求较高的场景,比如局域网内部的主机之间进行通信。

如果传输的数据报较大则是TCP更优先,因为UDP有64kb的限制,但是如果要进行“广播传输”,者优先考虑UDP,因为UDP天然支持广播传输。

//广播传输:把数据发送给局域网内的所有机器,比如手机的投屏功能就是向这个局域网内的所有机器询问,谁可以投屏,此时投屏设备就会回应,并把自己的IP地址和端口号告诉手机

以上就是博主对TCP知识的分享,在之后的博客中会陆续分享有关TCP的其他知识,如果有不懂的或者有其他见解的欢迎在下方评论或者私信博主,也希望可以多多支持博主!!🥰🥰


网站公告

今日签到

点亮在社区的每一天
去签到