文章目录
TCP
全称为"传输控制协议"(Transmission Control Protocol)。正如其名字一样,要对数据的传输进行一个详细的控制。
一、TCP 协议段格式
下图即为 TCP 的协议段格式(大概知道就行)。
- 源 | 目的端口号:表示数据是从哪个进程来,到哪个进程去。
- 序号 | 确认序号:用来保证数据的顺序。在确认应答中,会详细介绍。
- 首部长度:表示该
TCP
头部有多少个 32 位 bit (有多少个 4 字节)。4 位就是 4 个 bit 表示的数据范围 0~15,由于单位是 4 字节,所以最大长度为 60 字节。除去选项和数据,根据上图可知,TCP
的最短长度为 20 字节(16 位为 2 个字节)。TCP
报头自身不表示TCP
长度,TCP
载荷的长度需要结合IP
协议报头来计算。- 保留(6 位):未被使用的 bit 位。属于防范于未然,后续如果要扩展一些新功能,就可以使用保留位来表示。
UDP
就是吃了扩展性差的亏。- 6 位标志位:TCP 的灵魂。 URG:紧急指针是否有效。 ACK:确认序号是否有效。 PSH:提示接收端应用程序立刻从 TCP 缓冲区把数据读走。 RST:要求重新建立连接。我们把携带 RST 标识的称为复位报文段。 SYN:请求建立连接。我们把携带 SYN 标识的称为同步报文段。 FIN:通知对方,本端要关闭了。我们称携带 FIN 标识的为结束报文段。
- 窗口大小:表示滑动窗口的剩余大小。在下面滑动窗口会涉及到。
- 校验和:发送端填充,CRC 校验。接收端校验不通过,则认为数据有问题。此处的检验和不光包含 TCP 首部,也包含 TCP 数据部分。
- 紧急指针:标识哪部分数据是紧急数据。
- 选项:翻译成“可选项”,可以选择加或者不加一些参数。TCP 头部最长长度和最短长度的差,即为选项的最大字节数为 40 字节。
- 数据:载荷部分。TCP 数据长度没有限制。
二、TCP 协议的核心机制
接下来要介绍
TCP
10 个比较核心的机制。注意:TCP 不是只有 10 个机制!这里是根据面试和实际工作,挑了 10 个有代表性的来介绍。
10 个核心机制如下:
- 确认应答
- 超时重传
- 连接管理
- 滑动窗口
- 流量控制
- 拥塞控制
- 延时应答
- 捎带应答
- 面向字节流
- 异常情况
2.1 确认应答:
确认应答是
TCP
保证可靠传输的核心机制。发送方向接收方发送数据后,接收方会向发送方回复收到(这样用于“应答”的报文称为“应答报文”,应答对应英文单词
acknowledge
缩写即为
ack
,就是 6 个标志位的其中一个,如果 ack 标志位为 1 说明这是一个应答报文)。
这样的做法还存在一个问题:如果在网络传输的过程中,发生后发先至的情况该怎么办,本来是要应答问题 2 的,给应答到问题 1 上了。
解决方案:后发先至是网络通信中客观存在的,改变不了。我们可以对传输的数据进行编号,并且让应答报文的编号和发送的数据编号能够对应起来。这样即使出现后发先至,也不会出现“牛头不对马嘴”的情况。
**
TCP
将每个字节的数据都进行了编号,即为序号。**
TCP
报头通过存储载荷数据第一个字节的序号,来推算出后面字节的序号(一个
TCP
报文载荷字节序号是连续递增的)。
注意:虽然 TCP 序号是连续递增的,但是不一定是从 0 或者 1 开始的。具体如何开始,为啥这么设定,下面“连接管理”再详细解释。
确认序号的设定也非常有特点:取值是要应答数据的最后一个字节的序号再 + 1。
这是一个非常好的设计,含义有两个:以上图为例,B 发送确认应答报文(确认序号为:1001)。含义一:对于 B 来说,小于 1001 的数据都已经确认收到。含义二:B 再向 A 索要从 1001 开始的数据。
2.2 超时重传:
超时重传用来解决丢包问题。
- 主机 A 发送数据给 B 之后,可能因为网络拥堵等原因,数据无法到达主机 B 。
- 如果主机 A 在一个特定时间间隔内没有收到 B 发来的确认应答,就会进行重发。
但是,主机 A 未收到 B 发来的确认应答,也可能是因为
ACK
丢失了。
因此主机 B 会收到很多重复数据,那么
TCP
协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉。
这时候我们可以利用前面提到的序号,就可以很容易做到去重的效果。
- 超时时间的设定:
最理想的情况下,找到一个最小的时间,保证 "确认应答"一定能在这个时间内返回。但是这个时间的长短,随着网络环境的不同,是有差异的。如果超时时间设的太长,会影响整体的重传效率。如果超时时间设的太短,有可能会频繁发送重复的包。
TCP
为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间。假设第一次传输数据等待 50 ms,触发重传。重传之后等待 100 ms,如果还没收到,再传输数据…超时时间间隔会越来越长,不一定是线性增长,具体增长法,取决于系统的具体实现。
重传到一定的次数,还是不成功,就会尝试“重置连接”。触发“复位报文(标志位
RST
为1)”尝试重置连接。如果网络出现严重故障,重置也失败了,就只能断开连接了。
2.3 连接管理:
在正常情况下,
TCP
要经过三次握手建立连接,四次挥手断开连接。在这个过程中传输的报文是不懈怠任何业务上的数据。
- 建立连接:三次握手
建立连接,就是通信双方,各自保存对端的信息。需要经过三次网络交互。
注意:三次握手的第一次,一定是客户端先发起的。
三次握手的流程如下图:第二次握手 SYN 和 ACK 因为发送的时机都是收到第一次 SYN 马上发送,所以可以合并在同一个报文中。
SYN是 6 大标志位中的一位,是 synchronize(同步)的前三个字母。
三次握手相当于双方各自让对方保存自己的信息。
三次握手的意义:
- 投石问路: 确认当前通信路径是否畅通。
- 验证能力: 验证通信双方的发送能力和接受能力是否正常。
- 协商参数: 通信双方共同确认一些通信中的必备参数数值。TCP 通信时使用的序号,就是协商出来的,TCP 为了避免出现重连后序号发送混合,所以第一次连接和第二次连接,协商出来的起始序号差异很大。
经典面试题:TCP 为啥必须要三次握手,两次行不行,四次行不行?
答:不行,两次握手服务器对于通信双方的发送能力和接受能力的验证还没有完成。四次握手行但是没有必要,虽然不影响 TCP 的正常功能,但是会有性能损失。
TCP 三次握手的状态转化:
这里主要掌握两个状态即可:(1)listen(2)established
- listen:是
服务器
出现的状态,当服务器绑定端口成功后,就会进入到 listen 状态,表示服务器可以连接客户端。 - established:表示通信建立连接成功,可以进行通信。
- 断开连接:四次挥手
断开连接,就是通信双方,删除对端的信息。需要经过四次网络交互。
注意:三次握手一定是客户端先发起第一次请求。四次挥手则不一定,客户端和服务器都可以主动发起。
四次挥手的流程如下图:第二、三次挥手,ACK(应答报文)是接受到 FIN 马上发送。FIN(结束报文)要在应用程序代码中调用 close 的时候才会发送,二者相差的时间间隔较久,所以一般情况下,不能进行合并。特殊情况下可以通过延迟 ACK 发送的时机实现合并(在下面延时应答和捎带应答机制中会涉及到)。
FIN 是 6 大标志位中的一位,是 finish(结束)的缩写。
四次挥手相当于双方各自让对方删除自己的信息。
TCP 四次挥手的状态转化:
主要掌握两个状态:(1)CLOSE_WAIT(2)TIME_WAIT
- CLOSE_WAIT:被动一方进入的状态,等待代码调用 close。 如果程序出现问题,close 没有及时调用或者根本忘记调用就可能使机器上出现大量的 CLOSE_WAIT。
- TIME_WAIT:存在的意义是为了应对最后一个 ACK 出现丢包的情况。 在一定时间内(一般是 2MSL),如果没有收到重传的 fin,潜台词就是,最后一个 ack,已经被对方收到了,就不会重传 fin,此时 TIME_WAIT 就可以释放了。
TIME_WAIT的时间是 2MSL 的原因是:MSL是 TCP 报文的最大存活时间,因此 TIME_WAIT 持续存在 2MSL 的话,就能保证在两个传输端上的尚未被接收或迟到的报文段都已经消失(否则服务器立即重启,可能会收到来自上一个进程的迟到的数据,但是这种数据很可能是错误的),同时也是在理论上保证最后一个报文可靠到达(假设最后一个 ACK 丢失,那么服务器会再重发⼀个 FIN。这时虽然客户端的进程不在了,但是TCP 连接还在,仍然可以重发 LAST_ACK)。
注意:CLOSE_WAIT 不一定是服务器处于的状态(被动接受一方)。TIME_WAIT 也不一定是客户端处于的状态(主动发起一方)。
面试题:如果发现服务器端出现大量的 TIME_WAIT,说明了什么问题?
答:服务器出现大量的 TIME_WAIT 说明服务器这边触发了大量的主动断开 TCP 连接的操作。这个操作对于服务器来说不是很科学,一般都是客户端主动断开连接。
下图是 TCP 状态转换的一个汇总(简单了解一下即可,需要的时候过来查一下):
- 较粗的虚线表示服务端的状态变化情况。
- 较粗的实线表示客户端的状态变化情况。
- CLOSED 是一个假想的起始点,不是真实状态。
2.4 滑动窗口:
刚才我们讨论了确认应答策略,对每一个发送的数据段,都要给一个 ACK 确认应答,收到 ACK
后再发送下一个数据段。这样做有一个比较大的缺点,就是性能较差。尤其是数据往返的时间较长的时候。
既然这样一发一收的方式性能较低,那么我们一次发送多条数据,就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)。
- 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。上图的窗口大小就是 4000 个字节 (四个段)。
- 发送前四个段的时候,不需要等待任何 ACK,直接发送。
- 收到第一个 ACK 后,滑动窗口向后移动,继续发送第五个段的数据,依次类推。
- 操作系统内核为了维护这个滑动窗口,需要开辟
发送缓冲区
来记录当前还有哪些数据没有应答,只有确认应答过的数据,才能从缓冲区删掉。- 窗口越大,则网络的吞吐率就越高。
其滑动过程如下:
- 在滑动窗口的过程中,出现丢包的处理方式?
仍然要保证可靠性,这里分两种情况讨论:
- 情况一:数据包抵达,ACK丢了。
这种情况下,部分 ACK 丢了并不要紧,因为可以通过后续的ACK进行确认(例如收到下一个是 3001 的 ACK 说明 3000 之前的数据都已收到,如果还有疑惑看完下面的情况二,相信就能理解)。
- 情况二:数据包丢了。
- 当某一段报文段丢失之后,发送端会一直收到 1001 这样的 ACK,就像是在提醒发送端"我想要的是 1001"⼀样。
- 如果发送端主机连续三次收到了同样一个"1001"这样的应答,就会将对应的数据 1001~2000 重新 发送。
- 这个时候接收端收到了 1001 之后,再次返回的 ACK 就是 7001 了(因为 2001~7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的
接收缓冲区
中。
这种机制被称为"高速重发控制"(也叫"快重传")。
问题:超时重传和快重传是否冲突?
答:当然不冲突!在不同情况下,采取对应的重传策略,快重传相当于超时重传在滑动窗口下的特殊变种。TCP 传输的数据少,不频繁,不会触发滑动窗口,这时采用超时重传策略。TCP 传输的数据多,频繁,触发滑动窗口,采用快重传,按照 ack 反馈的次数来解决丢包问题。
- 重要结论: 滑动窗口,说是提升效率的机制,更准确的说是“亡羊补牢”的机制。TCP 为了保证可靠性,牺牲了很多效率,引入滑动窗口,让效率牺牲的少一些,但是仍然是存在牺牲的,再如何滑动窗口,速度不可能比 UDP 这种没有可靠机制的协议更快。
2.5 流量控制:
接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应。
因此 TCP 支持根据
接收端
的处理能力,来决定
发送端
的发送速度。这个机制就叫做
流量控制
(Flow Control)。
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的
窗口大小
字段,通过 ACK 端通知发送端。- 窗口大小字段越大,说明网络的吞吐量越高。
- 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端。
- 发送端接受到这个窗口之后,就会减慢自己的发送速度。
- 如果接收端缓冲区满了,就会将窗口置为0,这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
回忆我们的 TCP 首部中,有一个 16 位窗口字段,就是存放了窗口大小信息。那么问题来了,16 位数字最大表示 65535,那么 TCP 窗口最大就是 65535 字节么?
答:实际上,TCP 首部 40 字节选项中还包含了一个窗口扩展因子 M,实际窗口大小是窗口字段的值
左移 M 位
。
2.6 拥塞控制:
和流量控制类似,都是和滑动窗口搭配的机制。流量控制是站在接收方的角度,控制发送方发送速度,拥塞控制是站在发送方和接收方通信路上节点的角度,控制发送方发送速度,考虑到中间节点,就很复杂了,中间有多少个设备,每次走的路径都可能不一样,每个设备处理能力和繁忙程度也不一样…
解决办法:不论中间结构有多复杂,TCP 都把它们视为一个整体,通过
实验
的方式,找到一个合适的窗口大小。
TCP 引入
慢启动
机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。
- 此处引入一个概念为
拥塞窗口
- 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口大小。
像上面这样的拥塞窗口增长速度,是指数级别的。
慢启动
只是指初始时慢,但是增长速度非常快。
- 指数增长过程中,达到某个阈值,就要变成线性增长。
- 增长到一定程度,就会出现丢包,此时,发送方大概就知道当前网络通信能力的水平,将窗口立即变小。
- 窗口缩小有两种方式:(1)直接缩到底(慢启动)(2)缩小到出现丢包时窗口大小的一半,接下来线性增长。(1)是之前 TCP 的方式,现在已经废弃了,(2)是现在采用的方式。
- 每次超时重传(快重传)的时候,慢启动阈值会变成触发快速重传的拥塞窗口的一半。 该图片出自: 知乎小林
2.7 延时应答:
如果接收数据的主机立刻返回 ACK 应答,这时候返回的窗口可能比较小。
假设接收端缓冲区为1M,一次收到了 500K 的数据,如果立刻应答,返回的窗口就是 500K,但实际上可能接受端处理的速度很快,10ms 之内就把 500K 数据从缓冲区消费掉了。在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来。如果接收端稍微等一会再应答,比如等待 20ms 再应答,那么这个时候返回的窗口大小就是1M。
窗口越大,网络吞吐量就越大,传输效率就越高。我们的目标是在保证网络不拥塞的情况下尽量提高传输效率。延时应答的核心目的就是提升传输效率。
延时有两种方式:(1)按照一定时间来指定延时。(2)按照收到的数据量。
这两种策略是结合使用的。
2.8 捎带应答:
和延时应答的目的一样,也是为了提升效率。
在延时应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是"⼀发⼀收"的。意味着客户端给服务器说了"How are you",服务器也会给客户端回一个 “Fine,thank you”。那么这个时候延时的 ACK 就可以搭顺风车,和服务器回应的"Fine,thank you"一起回给客户端。
同理通过将 ACK 延时到与 FIN 同时发送的时候就可以进行合并,实现三次挥手。由于这是特殊情况,所以我们对于关闭连接都是称为 4 次挥手。
2.9 面向字节流:
这里主要注意一个面向字节流的普遍问题:粘包问题。
首先要明确,粘包问题中的"包",是指的应用层的数据包,在 TCP 的协议头中,没有如同 UDP ⼀样的"报文长度"这样的字段,但是有一个序号这样的字段,站在传输层的角度,TCP是⼀个⼀个报文过来的,按照序号排好序放在缓冲区中。站在应用层的角度,看到的只是一串连续的字节数据。那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。
那么如何避免粘包问题呢?归根结底就是一句话,明确两个包之间的边界。常见有三种方式:
- 对于定长的包,保证每次都按固定大小读取即可。例如上面的 Request 结构,是固定大小的,那么就从缓冲区从头开始按 sizeof(Request)依次读取即可。
- 对于变长的包,可以在包头的位置。约定一个包总长度的字段,从而就知道了包的结束位置。
- 对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来定的,只要保证分隔符不和正文冲突即可)。
对于 UDP 协议是否会存在“粘包问题”?
答:不会。对于 UDP,存在报文长度,同时 UDP 是一个一个把数据交付给应用层,有很明确的数据边界。站在应用层角度,使用 UDP 的时候不会存在‘半个“的情况。
2.10 异常情况:
主要有4种情况:
- 进程终止:进程终止会释放文件描述符,仍然可以发送 FIN 和正常关闭没有什么区别。
- 机器重启(正常流程关机):和进程终止的情况相同。对于正常流程的关机,操作系统会先尝试强制结束所有用户进程,然后再进入关机流程。
- 机器掉电:A 和 B 通信,A 突然掉电了。A 无法做出任何反应就没了,B 还在等 A。对于这种要分成两种情况来进行讨论: (1)B 是发送方:触发超时重传,触发 RST,RST 重连不成功,单方面删除信息。 (2)B 是接收方:由于接收方无法知道对方啥时候给自己发送数据,所以 B 在一定时间之内没有收到 A 的数据之后,会触发
心跳包
。连续发了若干次,A 都没有回应,这时 B 认为 A 挂了,于是单方面释放连接。 - 网线断开:和第三种的情况一样。
终于把 TCP 核心的 10 个机制讲完了🎉🎉🎉
三、TCP 小结
为什么
TCP
这么复杂?因为要保证可靠性,同时又尽可能的提高性能。
- 可靠性:
- 校验和
- 序列号(按序到达)
- 确认应答
- 超时重发
- 连接管理
- 流量控制
- 拥塞控制
- 提高性能:
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答
- TCP / UDP 对比:
我们说了 TCP 是可靠连接,那么是不是 TCP 一定就优于 UDP 呢?TCP 和 UDP 之间的优点和缺点,不能简单,绝对的进行比较,TCP 用于可靠传输的情况,应用于文件传输,重要状态更新等场景。UDP 用于对高速传输和实时性要求较高的通信领域,例如,早期的QQ,视频传输等。另外UDP可以用于广播。归根结底,TCP 和 UDP 都是程序员的工具,什么时机用,具体怎么用,还是要根据具体的需求场景去判定。
- 经典面试题:用 UDP 实现可靠传输。 答:通过应用层代码实现可靠传输的过程。(1)引入确认应答机制(ACK)。(2)引入序号 + 确认序号。(3)引入超时重传。
结语:
其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。
版权归原作者 gobeyye 所有, 如有侵权,请联系我们删除。