目录
TCP全称为 "传输控制协议(Transmission Control Protocol"). ⼈如其名, 要对数据的传输进⾏⼀个详细的控制; 所以TCP是传输层的协议
TCP的基本特点:有链接、可靠传输、面向字节流、全双工
有链接可以通过代码层面的accept()方法体会到,面向字节流 和 全双工,也可以通过代码层面的getInputStream()、getOutputStream()来体会。但是可靠传输,我们无法通过代码层面体会到,那么就需要来了解TCP的核心机制,看它是如何保证数据的可靠性的。
虽然是可靠传输,但是也不能保证"100%"送达,只能尽可能的使数据能到达对方。
一、TCP的协议和数据报格式
TCP协议段格式:![]()
TCP中的报文格式比较复杂,有的需要结合TCP的机制讲解,这里做一个简单的介绍:1. 源/⽬的端⼝号: 表⽰数据是从哪个进程来, 到哪个进程去
2. 32位序号/32位确认号: 后⾯详细讲
3. 4位首部长度:指的是TCP报头的长度,TCP的报头是可变长的。用4个bit来表示,4个比特位最大就是表示十进制的15,但是15个字节貌似并不能装下报头,所以这里设定了单位,是4个字节。所以TCP头部最⼤⻓度是15 * 4 = 60
4. 保留位:这是吸取UDP的经验,虽然目前不用,但是先让它存在,防止未来需要扩展,或者某些位置需要扩容。考虑了未来的可扩展性
5.
6位标志位:
- URG: 紧急指针是否有效
- ACK: 确认号是否有效
- PSH: 提⽰接收端应⽤程序⽴刻从TCP缓冲区把数据读⾛
- RST: 对⽅要求重新建⽴连接; 我们把携带RST标识的称为复位报⽂段
- SYN: 请求建⽴连接; 我们把携带SYN标识的称为同步报⽂段
- FIN: 通知对⽅, 本端要关闭了, 我们称携带FIN标识的为结束报⽂段
6. 16位窗⼝⼤⼩: 后⾯再说
7. 16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP⾸部, 也包含TCP数据部分
8. 16位紧急指针: 标识哪部分数据是紧急数据
9. 40字节头部选项: 暂时忽略
二、TCP常见的核心机制
网上有蛮多人都说,TCP的可靠传输是因为:“三次握手,四次挥手”,但其实并不是这样,“三次握手,四次挥手”只是为可靠传输提供了一些帮助或者说铺垫吧。往后面看到“三次握手,四次挥手”就可以理解我为什么这么说了。
1. 确认应答
确认应答其实就是每收到一条数据,就告诉对方我收到数据了
举个例子来帮助大家理解:
假如有个 计算机小白(客户端) 和 博主(服务器):
ack的缩写是 acknowledge(理会) 的缩写,做为一个应答报文:
做为一个应答报文,那么这里的ack就会置为1
大多数情况下,应答报文(ack)不会携带数据,只是起到一个通知的作用,不过后面有特殊情况,会进行讲解
但是如果发多条数据:
网络通信中存在后发先至的情况,因为:
网络通信中可以通过各种路线到达接收方,那么就意味着,有可能 数据1 绕了一大圈在到接收方,但是在它之后发送的 数据2 、 数据3…… ,早就到了接收方,此时顺序也就乱了。
为了处理这样的情况,给每一个发送出去的数据都进行了编号:
(其实起始位置的编号并不是1,而且每一次起始的编号变化都很大,为了防止数据混淆(前朝的剑斩今朝的管),这里会举例起始编号为1,为了方便大家理解)
由于TCP是面向字节流的,假设我们每一次发送1000个字节的数据,那么第一个字节编号就是1、第二个就是2 ,一直到编号为1000的字节。
最终接收方都收到了,就会返回1001,这代表前面1000个字节的数据都接收到了,你下一次要发送过来头数据编号是 1001.
那么就需要一个能 存放序号 和 确认序号 的空间:
32位序号:
32位确认序号:
TCP就可以针对接收方的数据,进行重新排序,确保应用程序,read到的数据一定是和发送方一致的:
假设我要发3000个字节的数据,每一次发1000个字节:
那么这样就可以做到 接收 和 发送 数据顺序相同了
但是实际上网络中的通信并不会一帆风顺,而是可能会出现 “丢包” 这样的情况。
丢包的原因有很多,例如:遇到了磁场,发生了bit翻转、某个结点的负载太高了,达到了负载的上限,就会抛弃新接收的数据等等
那么就引入了这样的机制->
2. 超时重传
TCP会通过 应答报文(ack)来区分:
- 如何收到了应答报文,就说明数据包没有丢
- 如果没有收到应答报文,就说明数据包丢了
但是没收到有可能是网络延时,过一会就收到了。为了区分是网络延时,还是丢包了,发送方发送数据之后,会给出一个 “时间限制(超时时间)” 如果在这个时间限制之内,ack没有收到,就视为丢包了 。
如果是数据包丢了:
如果是 “应答报文(ack)” 丢了:
如果对方发生了重大的网络故障或者其他原因导致无法进行网络传输:
发送方就会进行多次重传:
发送方第一次重传,超时时间是t1,如果重传之后,仍然没有ack。就会重传第二次,超时时间是t2。 t2的时间设置是大于t1的 (t2 > t1)
经过多次重传,对方都没有响应,那么TCP就会单方面的放弃连接。
那么TCP的可靠传输,就是靠着 “确认应答” 和 “超时重传” ,相互补充 构建了 TCP “可靠传输的机制”。
并不是因为三次握手和四次挥手,马上就会讲到连接管理,大家看完也就知道原因了。
3. 连接管理
连接管理就是大家常说的:“三次握手” 和 “四次挥手”
三次握手(建立连接)
不过这里不一定是,客户端做为发起方,也有可能是服务器,虽然大部分来说是客户端做为主动发起的一方。
建立连接是一个“双向的操作”:
客户端 给 服务器说,我想和你建立连接(客户端 想保存 服务器的信息)
服务器 给 客户端说, 我也想和你建立连接 (服务器 想保存 客户端的信息)
通过上述描述知道了,三次握手的流程,但我们还不知道为什么要这么设置,每次发送信息的背后,都是什么意思:
1. 投石问路,初步验证通信的链路是否正常(这是传输的前提条件)
2. 确认通信双方的接收和发送能力
3.在通信之前,对通信过程中需要用到的一些关键参数,进行协商
例如TCP通信的起始序号
四次挥手(断开连接)
发起结束报文 fin -> (英文意思finish结束),此时“6位标志位”中的fin置为1
问题来了,为什么建立连接只需要3次信息发送,而断开连接就需要发4次:
对于三次握手,中间的两次ack+syn,都是在内核中,由操作系统进行的,时间都是在收到syn之后,此时同一时机,就可以合并了。
但是对于四次挥手来说,ack是内核控制的,但是fin的触发,则是通过应用程序,(调用close/进程退出)来触发的,在close之前,我们大概率是需要做一些收尾工作的,这个时候就导致了,发出的时间不一致,从而需要发开发两次。
常见的状态和整体的传输流程
这里我们只关系部分常用的一些状态:
三次握手:
listen(听):服务器会进入的状态,服务器端口绑定完成,准备迎接客户端
established(建立):服务器客户端都会进入的状态,建立连接完成(保存了对方的信息),可以开始正常业务数据的传输了
四次挥手:
close_wait: 被动断开连接的一方(先收到fin的一方),等待执行close方法
time_wait: 主动断开连接的一方,等待一段时间后释放。
为什么time_wait要等待一段时间,为了防止发出的ack丢包,如果丢了还可以重发:
看了上述的内容,相信你以及明白了,TCP可靠性的核心是 “确认应答 和 超时重传” ,连接管理只是确认了,当前网络具备传输的条件,保证网络传输的头 和 尾
4. 滑动窗口
上述的各种机制都是在保证或间接保证数据能够可靠传输,为了可靠传输损失了不少效率,滑动窗口就可以挽回一些效率。
没有使用滑动窗口:
使用滑动窗口:
滑动窗口大小:
批量发送数据,不需要等待的,数据量称为“滑动窗口”,批量发送的是字节数,不是“条”,例如上述发送的窗口大小就是4000,每一次发1000字节。
后面会讲到窗口大小的设置
体会滑动窗口的过程:
上述都是一切正常的情况,没有发生丢包,那么如果发生丢包呢?
回应的ack发生了丢包:
此时就需要打破之前对ack的认识,现在就不仅仅是上一条数据接收到了,而是:
下一个数据是5001开头的,并且5001之前的数据都接收到了
数据包丢了:
接收到3次相同的ack,就定位丢失的数据立马重发。这样的过程就是快速重传
那么窗口的大小又是怎么设置的呢?下面讲的流量控制就会涉及到
5. 流量控制
为了防止接收端的数据处理不过来,接收缓冲区都满了,这个时候再发数据过去就会丢包,我们就引入了流量控制
TCP就用 “接收缓冲区的剩余空间” 去设置窗口大小,来控制发送端,发送数据的数量,窗口就要做为一个传输的值了,存放在:
只有在ack为 1 的时候, 16位窗口大小才有效
具体发送接收流程:
超出时间就会发出一个 “窗口弹窗包”,窗口探测包是不携带业务数据的
6. 拥塞控制
除了要感知接收端是否能处理得过来,还要考虑到一些中间设备是否能够接收得过来
这样的情况就非常难衡量:
- 中间结点非常多
- 每次传输的数据,走的路线还不一样
- 中间哪个结点遇到瓶颈了
- 中间结点传输的数据还不止你一个
那么聪明的程序员就用做工程的方式,找到了解决办法:
- 先按照一个比较小的速度发数据
- 数据非常畅通,没有丢包,说明网络上传输数据整体是比较通畅的,就可以加快传输速度
- 增大到一定的速度之后,发现出现丢包了,说明网络上可能存在拥堵了,就减慢数据的传输速度
- 减速之后,发现又不丢包了,继续加速
- 加速之后,又开始丢包了,继续减速
……
拥塞控制也是通过,限制发送窗口大小,来控制速度的。
那么拥塞控制和流量控制都会控制窗口大小,听哪个的呢?
这两个机制都会起作用,最终的发送窗口大小,取决于两个机制的窗口大小最小值
拥塞控制 控制 滑动窗口大小的过程:
通过颜色区分文字讲解的哪部分内容
很明显经典版本并不太合适,现在都用快恢复的方式
7. 延迟应答
延迟应答也是一种提升效率的机制,让窗口大小尽可能的大一点
如果此时接收到新增数据,立即返回ack,窗口大小就是4kb。但其实接收方大概率是在消费数据的,如果我们晚一点返回ack,可能就消费了1kb的数据了。那么此时再返回窗口大小就是5kb了,从而提升了传输速度。
8.捎带应答
捎带应答是在延迟应答的基础上,再次提升效率的机制
ack是内核返回的,收到请求后,立即就返回ack
但是由于有延迟应答的存在,ack就有可能和响应(业务数据)同时返回,只要它两的时间刚好碰上了,就会触发捎带应答:
ack 和 载荷 的区别
ack: ack只是告知对方我接收到了,主要携带的是 ack这置为1、设置窗口大小、设置确认序号
应答:主要携带的是业务层面的一些数据
9.面向字节流造成的 “粘包问题”
由于TCP是面向字节流的,一次发多少个字节的数据都可以,但是在由于数据在接收缓冲区中,连成一片,如果不对正确读取的长度做限制,就可能造成粘包问题,
例如:正确读取到的应该是aaa bbb ccc
如果不再区分就有可能是 aaab bbcc c、aa abb bcc c……
方案一:如果是文本数据就可以用一种符号做为结束符号例如 “\n”:
方案二:如果是二进制的数据,就需要指定数据的长度。这是一个很重要的方式
10、异常处理
1.进程崩溃
现象:运行 TCP 进程的程序突然终止(如崩溃、被强制杀死)
正常情况:若进程通过操作系统正常释放资源,TCP 会发送 FIN 包(结束连接请求),触发四次挥手流程,双方逐步关闭连接
异常情况:若进程未及时通知操作系统(如瞬间崩溃),TCP 层可能无法发送 FIN 包,导致对端无法感知连接已失效。对端等待数据时,因收不到响应且未收到 FIN,会触发 超时重传。若重传多次失败(超过阈值),对端会判定连接超时,进入异常关闭状态。
2.主机关机
现象:主机执行正常关机流程
操作系统会先终止所有进程并关闭网络连接,TCP 层会发送 FIN 包,触发四次挥手,双方完成连接关闭。
3.主机掉电 (没电了)和 网线断开
现象:主机突然断电(如电源故障、强制关机),无法进行任何网络操作。
接收方掉电:
发送方掉电: