前言
大家好,我是柒八九
。今天我们继续来讲述一下,针对网络通信方面的东西。
从上图可知,不管是 OSI七层模型
还是 TCP/IP 四层模型
或者是 TCP/IP 五层模型
,其中传输层和网络层都是很重要的一部分。
而 TCP/IP
也常被称为因特网协议套件(Internet Protocol Suite)。
- TCP,即 Transmission Control Protocol(传输控制协议)
负责在不可靠的传输信道之上提供可靠的抽象层 - IP:即 Internet Protocol(因特网协议)
负责联网主机之间的路由选择和寻址
在前几篇文章
我们基本上从宏观角度描述了,应用层是如何构建通信消息、查询服务端IP地址的,利用Socket委托协议栈将包封装好。今天,我们着重讲讲,在客户端准备好通信消息后,在传输层都干了啥。因为,网络环境中大部分的数据都是通过 TCP 协议发送,所以,先来讨论 TCP协议的东西,后续会有 UDP 的介绍。
时间不早了,干点正事哇。
一图胜千言
简明扼要
- TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议
- Sequence Number 分包序号主要用来 解决网络报乱序的问题
- Acknowledge Number 回应序号用来解决不丢包的问题
- 客户端可以在发送 ACK 分组之后立即发送数据,而服务器必须等接收到 ACK 分组之后才能发送数据
- 通过分包序号+数据长度,就可以知道发送的数据是从第几个字节开始,长度是多少
- 所谓滑动窗口,就是在发送一个包之后,不等待 ACK 号返回,而是直接发送后续的一系列包
- 每个 ACK 分组都会携带相应的最新
rwnd
值,以便两端动态调整数据流速 - 客户端与服务器之间最大可以传输(未经 ACK 确认的)数据量取 rwnd 和 cwnd 变量中的最小值
- 接收窗口会随每次 ACK 一起发送,而拥塞窗口则由发送端根据拥塞控制和预防算法动态调整
- 无论带宽多大,每个 TCP 连接都必须经过慢启动阶段
- 把初始拥塞窗口大小增加到一个合理值,可以减少客户端与服务器之间的往返时间
- 带宽并不影响 TCP 连接的启动阶段,延迟和拥塞窗口大小才是限制因素
- TCP 队首阻塞造成的延迟,也是影响应用程序性能的一个主要因素
面试加油站:
0. TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议
1. Sequence Number(分包序号主要用来 解决网络报乱序的问题
2. Acknowledge Number(回应序号用来解决不丢包的问题
3. 无论带宽多大,每个 TCP 连接都必须经过慢启动阶段
4. 把初始拥塞窗口大小增加到一个合理值,可以减少客户端与服务器之间的往返时间
5. 带宽并不影响 TCP 连接的启动阶段,延迟和拥塞窗口大小才是限制因素
6. TCP 队首阻塞造成的延迟,也是影响应用程序性能的一个主要因素
文章概要
- 三次握手
- 序号和 ACK 号的用法
- 滑动窗口
- 队首阻塞
- 四次挥手
注意,我们全篇都是从传输层考虑,一些涉及到上下层的信息处理,我们暂时先不考虑。
1. 三次握手
在网络通信开始之前,我们进行双端的信息确认,也就是我们需要在双端建立通信连接。而 所有 TCP 连接一开始都要经过三次握手。
在很早的时候,我们有一篇关于 TCP 的基础介绍文章,所以,有些比较基础的信息,我们就一带而过了。如果想详细了解,可以先去了解一下。不要忘记回来就行。有些必要的信息,我就直接拿来主义了。
我们来简单介绍一下,重要字段的作用。
- Source Port(源端口号)/ Destination Port (目标端口号):
用于区别主机中的不同进程,而IP地址是用来区分不同的主机的,源端口号和目的端口号配合上IP首部中的源IP地址和目的IP地址就能唯一的确定一个TCP连接; - Sequence Number(分包序号:
应用程序数据如果 大于MSS
(Maximum Segment Size,最大分段大小) 就得要进行分段。(详细可参考网络拾遗之Socket-数据分包) 这个 Sequence Number 就是记录每个封包的序号,可以让 server 重新将 TCP 的数据组合起来。
主要用来解决网络报乱序的问题 - Acknowledge Number(回应序号):
为了确认 发送端 确实有收到我们 接收端 所送出的封包数据。 当 接收端 收到这个确认码时,就能够确定之前传递的封包已经被正确的收下了。
回应序号应当是上次已成功收到分包序号加1。
用来解决不丢包的问题 - Code(Control Flag, 控制标识码)这个字段共有 6 个 bits ,分别代表 6 个句柄,若为 1 则为启动 (只介绍常用的)
- ACK(Acknowledge):若为 1 代表这个封包为响应封包, 则与上面提到的 Acknowledge Number 有关
- SYN(Synchronous):若为 1,表示client 希望双方建立同步处理, 也就是要求建立联机
- FIN(Finish):若为 1 ,表示传送结束,所以通知对方数据传毕, 是否同意断线,只是发送者还在等待对方的响应而已
Sequence Number(分包序号主要用来 解决网络报乱序的问题
Acknowledge Number(回应序号用来解决不丢包的问题
- SYN
客户端选择一个随机序列号 x,并发送一个SYN
分组,其中可能还包括其他 TCP标志和选项。(这里后面有介绍) - SYN + ACK
服务器给 x 加 1,并选择自己的一个随机序列号 y,追加自己的标志和选项,然后返回响应 - ACK
客户端给 x 和 y 加 1 并发送握手期间的最后一个 ACK 分组
Note:上图上 SYN/ACK就是控制标识码,它们的值都是1。
在握手阶段,因为没有通信数据,所以在双端传递的包都是只有控制信息的网络包。
三次握手完成后,客户端与服务器之间就可以通信了。客户端可以在发送 ACK 分组之后立即发送数据,而服务器必须等接收到 ACK 分组之后才能发送数据。
2. 序号和 ACK 号的用法
首先,TCP 模块在拆分数据时,会先算好每一块数据相当于从头开始的第几个字节,接下来在发送这一块数据时,将算好的字节数写在 TCP 头部中,Sequence Number 分包序号就是派在这个用场上的。
然后,发送数据的长度也需要告知接收方,用整个网络包的长度减去头部的长度就可以得到数据的长度,所以接收方可以用这种方法来进行计算。有了上面两个数值(分包序号+数据长度),就可以知道发送的数据是从第几个字节开始,长度是多少了。
发送方说的是“现在发送的是从第 ×× 字节开始的部分,一共有 ×× 字节!”而接收方则回复说,“到第 ×× 字节之前的数据我已经都收到了!”这个返回 ACK 号的操作(已经收到的字节长度+1)被称为确认响应,通过这样的方式,发送方就能够确认对方到底收到了多少数据。
这里再做一个简单的补充,在实际的通信中,序号并不是从 1 开始的,而是需要用随机数计算出一个初始值。这也就是为什么,在握手阶段,双端都会进行一次 random
的操作。
通过握手阶段和通信交互阶段,我们发现,其实TCP的通信通过“序号”和“ACK 号”可以确认接收方是否收到了网络包
3. 滑动窗口
上面我们介绍了一个最简单的通信方式,每发送一个包就等待一个 ACK 号(a),但在等待 ACK 号的这段时间中,如果什么都不做那就会造成网络延迟。我们在Web性能优化之延迟与带宽中介绍过,影响网络性能主要的因素就是延迟。为了能够利用这段时间,TCP 采用(b)这样的滑动窗口方式来管理数据发送和 ACK 号的操作。
所谓滑动窗口,就是在发送一个包之后,不等待 ACK 号返回,而是直接发送后续的一系列包
但是,使用滑动窗口有一个问题,就是果不等返回 ACK 号就连续发送包,就有可能会出现发送包的频率超过接收方处理能力的情况。
当接收方的 TCP 收到包后,会先将数据存放到接收缓冲区中。然后,接收方需要计算 ACK 号,将数据块组装起来还原成原本的数据并传递给应用程序。
但是,如果数据到达的速率比处理这些数据并传递给应用程序的速率还要快,那么接收缓冲区中的数据就会越堆越多,最后就会溢出。然后,数据就会丢失。
所以,我们需要通过一些方式来避免上述情况发生。首先,接收方需要告诉发送方自己最多能接收多少数据,然后发送方根据这个值对数据发送操作进行控制,这就是滑动窗口方式的基本思路。
通过这种方式,就可以实现同一时间发送多个包,减少网络延迟。
其实,通过窗口,TCP 可以控制双向发送数据的速度。
流量控制
流量控制是一种预防发送端过多向接收端发送数据的机制。为实现流量控制,TCP 连接的每一方都要通告自己的接收窗口(rwnd),其中包含能够保存数据的缓冲区空间大小信息。
第一次建立连接时,两端都会使用自身系统的默认设置来发送 rwnd
。在后面的数据交换过程中,每个 ACK 分组都会携带相应的最新 rwnd
值,以便两端动态调整数据流速,使之适应发送端和接收端的容量及处理能力。
慢启动
流量控制确实可以防止发送端向接收端过多发送数据,但却没有机制预防任何一端向潜在网络过多发送数据。换句话说,发送端和接收端在连接建立之初,谁也不知道可用带宽是多少。因此需要一个估算机制,然后还要根据网络中不断变化的条件而动态改变速度。
通过三次握手,而且在此期间双方各自通过 ACK 分组通告自己的接收窗口(rwnd)大小。在发送完最后一次 ACK 分组后,就可以交换应用数据了。
此时,根据交换数据来估算客户端与服务器之间的可用带宽是唯一的方法,而且这也是慢启动算法的设计思路。首先,服务器通过 TCP 连接初始化一个新的拥塞窗口(cwnd
)变量,将其值设置为一个系统设定的保守值。
拥塞窗口大小(cwnd):发送端对从客户端接收确认(ACK)之前可以发送数据量的限制。发送端不会通告 cwnd 变量,即发送端和接收端不会交换这个值。
可以通过算法来确定每个连接的窗口大小。解决方案就是慢启动,即在分组被确认后增大窗口大小,慢慢地启动。
此时又有一条新规则:
客户端与服务器之间最大可以传输(未经 ACK 确认的)数据量取 rwnd 和 cwnd 变量中的最小值
服务器和客户端怎么确定拥塞窗口大小的最优值呢:解决方案就是慢启动:即在分组被确认后增大窗口大小,慢慢地启动。
新 TCP 连接传输的最大数据量取 rwnd
和 cwnd
中的最小值,而服务器实际上可以向客户端发送 4 个 TCP 段,然后就必须停下来等待确认。此后,每收到一个 ACK,慢启动算法就会告诉服务器可以将它的 cwnd 窗口增加 1 个 TCP 段。每次收到 ACK后,都可以多发送两个新的分组。TCP 连接的这个阶段通常被称为指数增长阶段,客户端和服务器都在向两者之间网络路径的有效带宽迅速靠拢。
接收窗口会随每次 ACK 一起发送,而拥塞窗口则由发送端根据拥塞控制和预防算法动态调整
无论带宽多大,每个 TCP 连接都必须经过慢启动阶段
换句话说,应用不可能一上来就完全利用连接的最大带宽
把初始拥塞窗口大小增加到一个合理值,可以减少客户端与服务器之间的往返时间
对于很多 HTTP 连接,特别是一些短暂、突发的连接而言,常常会出现还没有达到最大窗口请求就被终止的情况。慢启动限制了可用的吞吐量,而这对于小文件传输非常不利。
通过一个例子,我们来演示三次握手和慢启动对简单 HTTP 传输的影响。连接的 参数如下
- 往返时间:56 ms。
- 客户端到服务器的带宽:5 Mbit/s。
- 客户端和服务器接收窗口(rwnd):65 535 字节。
- 初始的拥塞窗口:4 段(4×1460 字节 ≈ 5.7 KB)。
- 服务器生成响应的处理时间:40 ms。
- 没有分组丢失、每个分组都要确认、GET 请求只占 1 段。
- 0 ms:客户端发送
SYN
分组开始 TCP 握手。 - 28 ms:服务器响应
SYN-ACK
并指定其rwnd
大小。 - 56 ms:客户端确认 SYN-ACK,指定其
rwnd
大小,并立即发送 HTTP GET 请求。 - 84 ms:服务器收到 HTTP 请求。
- 124 ms:服务器生成 20 KB 的响应,并发送 4 个 TCP 段(初始 cwnd 大小为 4),然后等待 ACK。
- 152 ms:客户端收到 4 个段,并分别发送 ACK 确认。
- 180 ms:服务器针对每个 ACK 递增 cwnd,然后发送 8 个 TCP 段。
- 208 ms:客户端接收 8 个段,并分别发送 ACK 确认。
- 236 ms:服务器针对每个 ACK 递增 cwnd,然后发送剩余的 TCP 段。
- 264 ms:客户端收到剩余的 TCP 段,并分别发送 ACK 确认。
通过新 TCP 连接在往返时间为 56 ms 的客户端与服务器间传输一个 20 KB 的文件需要 264 ms。
作为对比,现在假设客户端可以重用同一个 TCP 连接,再发送一次相同的请求。
- 0 ms:客户端发送 HTTP 请求。
- 28 ms:服务器收到 HTTP 请求。
- 68 ms:服务器生成 20 KB 响应,但 cwnd 已经大于发送文件所需的 15 段了,因此一次性发送所有数据段。
- 96 ms:客户端收到所有 15 个段,分别发送 ACK 确认
同一个连接、同样的请求,但没有三次握手和慢启动,只花了 96 ms,性能提升幅 度达 275%。
以上两种情况下,服务器和客户端之间的 5 Mbit/s 带宽并不影响 TCP 连接的启动阶 段。此时,延迟和拥塞窗口大小才是限制因素。
4. 队首阻塞
TCP 在不可靠的信道上实现了可靠的网络传输
每个 TCP 分组都会带着一个唯一的序列号被发出,而所有分组必须按顺序传送到接收端。如果中途有一个分组没能到达接收端,那么后续分组必须保存在接收端的 TCP 缓冲区,等待丢失的分组重发并到达接收端。这一切都发生在 TCP 层,应用程序对 TCP 重发和缓冲区中排队的分组一无所知,必须等待分组全部到达才能访问数据。在此之前,应用程序只能在通过套接字读数据时感觉到延迟交付。这种效应称为 TCP 的队首(HOL,Head of Line)阻塞
队首阻塞造成的延迟可以让我们的应用程序不用关心分组重排和重组,分组到达时间会存在无法预知的延迟变化。这个时间变化通常被称为抖动,也是影响应用程序性能的一个主要因素。
TCP 队首阻塞造成的延迟,也是影响应用程序性能的一个主要因素
5. 四次挥手
- 相比三次握手,四次挥手,在 server 发起的时候,是将控制标志码由
SYN
换成FIN
, 而通过我们讲述 Header 字段的时候,就着重描述了他们各自的含义和用处。 - 可以看到,在第二次挥手和第三次挥手中间,有很多未发送完成的数据,其实也好理解,在 client 接收到 server 传入的
FIN
包时候,此时可能正处于某些大包数据的发送阶段,如果此时直接回复 发送端的断开操作。并且,如果 server FIN 包早于其他正常数据包到达 client。那这些本应该被 client 收录的数据,就会平白无故的丢失。
为什么要四次挥手
TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。
TCP是全双工模式,这就意味着
- 当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据
- 当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的
- 当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接
后记
分享是一种态度,这篇文章,主要的篇幅来自于《网络是如何连接的》/《Web性能权威指南》,算是一个自我学习过程中的一种记录和总结。主要是把自己认为重要的点,都罗列出来。同时,也是为大家节省一下排雷和踩坑的时间。当然,可能由于自己认知能力所限,有些点,没能表达的很好。如果大家想看原文,“墙裂推荐”看原文。
参考资料:
- 网络是如何连接的
- Web性能权威指南