文章目录
- 传输控制协议
- 🚩TCP协议段格式
- ⭐确认应答(ACK)机制
- ⭐超时重传机制
- 🚩3次握手4次挥手
- 🚩连接状态转换
- ⭐流量控制机制
- ⭐滑动窗口
- ⭐延迟应答
- ⭐拥塞控制
- ⭐面向字节流
- ⭐粘包问题
- TCP异常情况
- 🚩TCP小结
传输控制协议
TCP全名叫做传输控制协议(Transmission Control Protocol),要对传输进行控制

HTTP协议在应用层,我们已经讲解了,TCP协议发生在传输层,
应用层有用户级缓冲区,🚩传输层有发送缓冲区和接收缓冲区
🚩当我们调用系统函数send,write,read,本质上是将数据从用户级缓冲区拷贝到发送缓冲区,发送缓冲区通过TCP和IP协议发送至目标主机的接收缓冲区,,再从接收缓冲区拷贝到用户级缓冲区
🚩TCP协议段格式

- 标准报头20个字节,选项40个字节,如何分离报头和有效载荷(交付上层)? 固定长度+自描述字段(有效载荷长度类型)
- 16位源端口号,和16位目的端口号表示从哪个进程来到哪个进程去,故之前学的端口号是16位
- 4bit位首部长度,表示报头大小,注意这里基本单位是4字节,2^4*4=60字节,20字节是标准报头长度,剩下40字节是选项,暂时忽略
- 16位窗口大小,表示当前接收缓冲区的大小,
- 16位检验和,发送端填充,接收端检验,若检验不通过则认为数据有问题
TCP是有状态的,6个标志位,URG,ACK,PSH,RST,SYN,FIN,接收端接收不同信号做出不同相应
- SYN申请建立链接,把带有SYN标识叫做同步报文段
- ACK确认信号有效
- FIN告诉对方,本端要关闭了
- PSH通知接收端,快点把TCP接收缓冲区拿到上层
- RST申请建立链接
- URG设置紧急指针有效
紧急指针应用场景,一个软件出现故障了,接收缓冲区阻塞了,我们TCP为了保证有序,发过去的检查报文肯定要排序,但是阻塞轮不到我们的检查包,这时URG设置1,紧急指针设置偏移量,接收端优先处理紧急指针,检查软件状态
⭐确认应答(ACK)机制
为了保证TCP的可靠性,我们必须保证数据传输的顺序性,序号因此被设计,每个数据包都有自己的序号
比如说我们发送了序号为100的数据包,长度100字节,接收端会返回ACK201确认信号,表示201之前的数据包我都收到了
发送了序号为100的数据包,长度100字节,又发送了序号201的数据包,长度100,接收端如果先收到序号201的包,会暂存,等待序号100发送,收到后返回301,表示301之前的数据我都收到了
确认序号的含义:表示确认信号之前的报文我们都收到了,保证了滑动窗口线性滑动(避免没收到的报文我们直接跳过了),
⭐超时重传机制
客户端发送100-200数据包,还没发过去就丢了怎么办?

超过一定的时间间隔,发送端会再次发送,
如果接收端接收到了,返回的ACK的应答机制丢失了怎么办?

因为发送端没收到应答,超时后会再次发送数据包,那接收端岂不是接收两个相同的数据包?
别担心,序号自带去重机制
过多少时间算超时?
这个时间间隔随网络状况动态变化,
太长了会耽误效率,太短了数据包还在网络中没传过过去,频繁发重复的包==
TCP为了保持高性能通信,动态计算这个值,LINUX和Windows超时都是以500ms单位控制,超时都是500ms整数倍,若500ms未收到应答,重传,再未收到应答,过500*2ms重传,还没收到,过500*4ms,依次倍增,若累计多次无应答,发送方认为网络或对方主机出现异常,强制关闭连接
🚩3次握手4次挥手
3次握手
首先建立链接:
客户端向服务端发送SYN报文,服务端收到之后返回对此应答,再发送SYN报文给客户端,客户端收到后返回应答,双向链接建立成功,为了提高效率,服务端通常把应答和SYN合并,叫作捎带应答,所以4次握手也是3次握手
理解链接:维护链接是有成本的,服务端肯定会与多个客户端建立链接,将这些链接先描述再组织管理起来
另外,世界上不存在100%成功的通信,因为我们通信的时候最后一句永远没有应答
所以当我们客户端发起最后一次应答时,操作系统默认认为成功,为客户端分配通信资源
所以为什么要三次握手而不是1次或2次?
1次握手会导致SYN洪水,客户端发送一个SYN请求,服务端就要开空间存储,但是如果客户端也不知道链接是否建立,会多次发送SYN,浪费服务端资源,况且如果想恶意攻击服务器,只需要多个客户端多次请求链接即可(肉机),

2次握手为什么不行?
客户端请求,服务端返回应答同时必须要为此次链接开辟空间,客户端收到了应答开辟空间,虽然看着可行,但是代价还是让服务器承受了,如果链接出问题了,浪费的是服务端资源,而服务端又要和多个客户端链接,基数大易出错,
所以3次链接,客户端先创建资源,🚩把风险规避到客户端一人身上并且验证全双工,不然服务器崩了所有链接都失败
4次挥手
若客户端先关闭链接,先向服务端发送FIN报文,服务端收到后返回ACK应答,服务端发送FIN报文,客户端收到返回ACK应答,4次挥手,链接关闭
为什么4次挥手?
发送报文双方都要发送,断开链接双方都要断开
客户端发送FIN后,服务端如果没有想给客户端发送的报文了,恰好也想中断链接,也可以将FIN和应答一起发送,也就是3次挥手,但可能还有没发送的报文,在此发送
🚩连接状态转换

建立连接:
服务端调用listen函数进入监听状态LISTEN,调用accept阻塞等待客户端连接
客户端调用connect函数(之后阻塞等待),发送SYN报头,
服务端接收后进入状态SYN_RCVD,捎带应答,
客户端接收后进入状态ESTABLISHED,connect返回,客户端开辟资源建立连接,
给服务端应答,服务端接收应答后进入ESTABLISHED状态,返回accept
write实际是写入发送缓冲区,read从接收缓冲区读取数据
断开连接:
客户端调用close,进入FIN_WAIT_1状态,发送FIN,
服务端接收后进入CLOSE_WAIT状态,返回应答
客户端进入TIME_WAIT_2状态,
服务端调用close,进入LAST_ACK状态,发送FIN
客户端进入TIME_WAIT状态,等待一段时间进入CLOSED,返回应答
服务端接收进入CLOSED,连接关闭
🚩细节
1,3次握手与上层accept没关系,是操作系统自动完成的
2,listen第二个参数是全连接队列最大节点数量,
既然accept与握手没关系,服务端建立好的连接肯定是要组织的,用队列组织起来,accept从队列中拿节点,全连接队列从半连接队列拿节点。这也是个消费生成者模型,生产的节点放入队列,accept消费,
为什么listen第二个参数不能设置太长?
accept消费能力有限,太长它也消费不过来,维护队列还要浪费资源
为什么不能太短?
比如餐厅外面放很多板凳,生意火爆挤不进去,但一但有座位直接进去,节点数就是凳子,设置太短的话,如果空出桌子来我们无法立即补充,白白损失性能
3,服务器不会长时间保持SYN_RCVD状态,处于此状态节点加入半连接队列,不会长时间维护半连接,处于半连接队列完成的节点完成3次握手后加入全连接队列
4,主动断开连接的一方,在4次挥手之后进入TIME_WAIT状态,2MSL之后进入CLOSED,数据自动释放
MSL:TCP报文在网络传输最大存活时间,通常设定2分钟
TIME_WAIT作用:
- 通信双方数据自动消散
- 断开连接,提高4次挥手容错性(万一最后一次ACK丢失了)
为什么服务器主动断开连接不能迅速重启?
因为进入TIME_WAIT状态要等待数据消散,并且端口号可能被其他线程使用,

解决方案,处于TIME_WAIT状态的服务器可以立即重启

为什么客户端看起来不用等待,可以直接重连?
因为客户端端口号随机分配的
⭐流量控制机制
接收端处理数据是有限的,如果发送端发送数据过快导致接收缓冲区被打满,再发送数据就会丢包,所以我们需要控制发送速度,
接收端可以把自己接收缓冲区大小放入TCP报文"窗口大小"字段中通过ACK给发送端,字段越大说明吞吐量越大,如果收到窗口小,就要减缓发送速度,为0,发送端不再发数据,但是定时发一个报头询问对方缓冲区大小
流量控制通过滑动窗口实现
⭐滑动窗口
如果我们每一个发送数据都要等待应答,那样性能也太差了,尤其是往返时间长的时候

所以我们多条发送,多条应答(将等待时间叠在一起)

前4个数据发送不用等待应答,收到第一条应答后,窗口向后滑动,发送第5条数据,以此类推
窗口越大吞吐量越高,
万一有发送过去没收到应答的数据呢?所以我们需要开辟发送缓冲区维护窗口,

双指针,左边是已发送的已被确认的,中间是已发送但未收到应答,右边是待发送
- 如果丢包该如何重传?
情况一:数据包已传过去,但应答丢了

我们收到6001,默认已收到前面应答,但实际上滑动窗口滑不过去,卡在1-1000,会重新补发
情况二:数据包在传过去路上就丢了

1001-2000在路上丢了,会不断返回1001,就像在我想要1001
发送方收到3个1001,会重新补发,收到1001的应答之后,再次收到就是7001了,因为前面发的都已经乱序缓存了
这种机制叫做高速重发控制,也叫快重传
已经有了快重传为什么还要超时等待?
快重传提高效率,超时等待兜底
- 滑动窗口只能右移,窗口大小动态变化的
左不动右移,窗口变大,左动右不动,窗口变小,也可以不变
窗口怎么确定?
int start:确认序号设置
int end:确认序号+win大小
win=min(win,有效数据,拥塞窗口)
起始序号是双方协商定的比如定在1234,传数据时数组下标+1234即可,因为之前通信有残留数据,如果我们断开快速重连,可能收到旧数据,
所以起始序号随机值,再与之前通信数据重叠的概率就很低了
我们下一次窗口定了5000大小,但我们只剩2000大小的数据了怎么办?
当然是缩小窗口,只传有效值
- 滑动窗口会越界吗?
不会,TCP缓冲区采用了环形算法,其实还有第4块窗口(可以归并到已确认的窗口),

⭐延迟应答
比如说:接收端缓冲区大小1M,我们向接收端缓冲区发送500KB数据,这时立即返回窗口就是500KB,但是有可能10ms后,上层就处理完数据了,所以如果10ms再后返回的窗口就是1M,所以我们延迟一会应答吞吐量会更大,
延迟应答也有限制
- 设置隔几个包就应答一次
- 超过最大时间应答一次
不同操作系统有差异,一般是隔两个包,最大时间设置200ms
最好的方法就是,缓冲区有数据,立即通过read,recv从内核拿上来,返回的窗口就更大
记住,窗口越大,吞吐量更大,性能越佳

⭐拥塞控制
TCP发送报文时,不一定是对方主机出现问题,也有可能是网络问题,少量丢包可以理解,但是大量丢包有可能是网络阻塞了,这时我们不敢贸然发送大量数据了,否则只会加重阻塞,同时,网络不止我们一个用户,多个用户共用一个网络

每台主机识别到网络阻塞时都要做慢启动
滑动窗口大小=min(窗口大小,有效数值,拥塞窗口大小)
慢启动,就是一开始发送数据很小,但之后呈指数增加
后面越来越快,不能让它成倍增加,我们设置一个阈值,超过这个阈值之后,线性增加

线性增加到一定值又阻塞后,我们将阈值减为该值的一半,即指数增长到该值后,再次线性增长
⭐面向字节流
TCP是面向字节流的,怎么理解?如同水龙头,想开多大就开多大,字节就像水
站在用户层来说
对于写100个字节,我们可以写1次100字节,也可以写100次1字节
读取字节也一样,
面向字节流,用户不认识报文,只认识一串字节

站在传输层看,TCP是按照序号一个个排在缓冲区的
站在用户层,看到的只是一串字节,有时多拿有时少拿,所以该怎么明确报文与报文边界?
⭐粘包问题
包是就是应用层数据包
解决粘包,就是明确两个包边界
四种方案:
- 固定报文,报文大小固定,应用层固定大小去拿
- 特殊字符,特殊字符分割两个包
- 自描述字段+特殊字符,如http报文,\n分行,content确认正文大小
- 自描述字段+定长报头,如UDP,先读取20字节报头,再读取content大小
UDP会出现粘包问题吗?
不会,UDP一个个交给应用层,具有明显边界,要么不传,要么传1个,不会出现传半个情况
TCP异常情况
- 关闭程序,在关闭的时候其实就是正常挥手的过程
- 电脑重启,重启之前就是在关闭所有进程,TCPsocket类似文件描述符由进程持有,,和关闭程序一致,正常挥手断开连接
- 突然断电,服务端也会有保活机制,过一段时间向客户端发送消息,久久不回答会断开连接
🚩TCP小结
与UDP相比保证可靠性方法:
- 序列号(按序到达)
- 连接管理
- 确认应答
- 超时重传
- 流量控制
- 拥塞控制
提高性能方法
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答
TCPUDP相比
- TCP用于可靠性传输,如文件传输,重要状态更新
- UDP用于高速传输和追求实时更新,如早期qq,视频传输,也用于广播