前言
网络层只把分组发送到目的主机,但是真正通信的并不是主机而是主机中的进程。传输层提供了进程间的逻辑通信,传输层向高层用户屏蔽了下面网络层的核心细节,使应用程序看起来像是在两个传输层实体之间有一条端到端的逻辑通信信道。
1、UDP 和 TCP 的特点与区别
用户数据报协议 UDP(User Datagram Protocol)的特点:
- 无连接
- 尽最大可能交付
- 没有拥塞控制
- 面向报文:对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部
- 支持一对一、一对多、多对一和多对多的交互通信
传输控制协议 TCP(Transmission Control Protocol)的特点:
- 面向连接
- 提供可靠交付,有流量控制和拥塞控制
- 提供全双工通信
- 面向字节流:把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块
- 每一条 TCP 连接只能是点对点的(一对一)
2、UDP、TCP 首部格式
UDP 首部只有 8 个字节,共 4 个字段,每个字段 2 个字节:
| 字段 | 长度 | 说明 |
|---|---|---|
| 源端口 | 16 bit | 发送方端口号 |
| 目的端口 | 16 bit | 接收方端口号 |
| 长度 | 16 bit | 首部加数据的总长度(单位为字节) |
| 检验和 | 16 bit | 用于检验首部和数据在传输过程中是否出错 |
12 字节的伪首部是为了计算检验和临时添加的,只参与计算,并不会被真正发送出去。
TCP 首部格式比 UDP 复杂,固定部分 20 个字节,主要字段如下:
| 字段 | 长度 | 作用 |
|---|---|---|
| 序号(Sequence Number) | 32 bit | 用于对字节流进行编号。例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401 |
| 确认号(Acknowledgment Number) | 32 bit | 期望收到的下一个字节的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701 |
| 数据偏移(Data Offset) | 4 bit | 指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度 |
| 控制位(Control Flags) | 8 bit | 从左到右分别是 CWR、ECE、URG、ACK、PSH、RST、SYN、FIN |
| 窗口(Window) | 16 bit | 窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的 |
控制位逐个说明:
- CWR:CWR 标志与后面的 ECE 标志都用于 IP 首部的 ECN 字段,CWR 为 1 时,通知对方已将拥塞窗口缩小
- ECE:若其值为 1 则会通知对方,从对方到这边的网络有阻塞。在收到数据包的 IP 首部中 ECN 为 1 时,将 TCP 首部中的 ECE 设为 1
- URG:该位设为 1,表示包中有需要紧急处理的数据,对于需要紧急处理的数据,与后面的紧急指针有关
- ACK:该位设为 1,确认应答的字段有效。TCP 规定除了最初建立连接时的 SYN 包之外,该位必须设为 1
- PSH:该位设为 1,表示需要将收到的数据立刻传给上层应用协议;若设为 0,则先将数据进行缓存
- RST:该位设为 1,表示 TCP 连接出现异常,必须强制断开连接
- SYN:用于建立连接,该位设为 1,表示希望建立连接,并在其序列号的字段进行序列号初值设定
- FIN:该位设为 1,表示今后不再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位置为 1 的 TCP 段
每个主机在对对方的 FIN 包进行确认应答之后就可以断开连接。不过,主机收到 FIN 设置为 1 的 TCP 段之后不必马上回复一个 FIN 包,而是可以等到缓冲区中的所有数据都因为已成功发送而被自动删除之后再发 FIN 包。
3、什么是 TCP 的三次握手和四次挥手?
TCP 是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务器的内存里保存的一份关于对方的信息,如 IP 地址、端口号等。
TCP 可以看成是一种字节流,它会处理 IP 层及以下各层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数,这些参数可以放在 TCP 头部。
TCP 提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接,采用四次挥手来关闭一个连接(详细过程可以参见本站的《三次握手与四次挥手》)。
一个 TCP 连接由一个 4 元组构成,分别是两个 IP 地址和两个端口号。一个 TCP 连接通常分为三个阶段:启动、数据传输、退出(关闭)。
当 TCP 接收到另一端的数据时,它会发送一个确认,但这个确认不会立即发送,一般会延迟一会(见下文第 12 节「提高网络利用率」)。
ACK 是累积的:一个确认字节号 N 的 ACK 表示所有直到 N 的字节(不包括 N)已经成功被接收了。这样的好处是,如果一个 ACK 丢失,很可能后续的 ACK 就足以确认前面的报文段了。
一个完整的 TCP 连接是双向和对称的,数据可以在两个方向上平等地流动,给上层应用程序提供一种双工服务。一旦建立了一个连接,这个连接的一个方向上的每个 TCP 报文段都包含了相反方向上的报文段的一个 ACK。
序列号的作用是使得一个 TCP 接收端可丢弃重复的报文段,记录以杂乱次序到达的报文段。因为 TCP 使用 IP 来传输报文段,而 IP 不提供重复消除或者保证次序正确的功能。
另一方面,TCP 是一个字节流协议,绝不会以杂乱的次序给上层程序发送数据。因此 TCP 接收端会被迫先保持大序列号的数据不交给应用程序,直到缺失的小序列号的报文段被填满。
4、TCP 的三次握手(为什么三次?)
三次握手的过程如下,假设 A 为客户端,B 为服务器端,握手开始前 B 处于 LISTEN(监听)状态,等待客户的连接请求:
- 第一次握手:A 向 B 发送连接请求报文段(SYN=1,初始序号 seq=x),A 进入 SYN-SENT(同步已发送)状态
- 第二次握手:B 收到 A 的连接请求后,同意建立连接,向 A 发回确认报文段(SYN=1,ACK=1,ack=x+1,seq=y),B 进入 SYN-RCVD(同步收到)状态
- 第三次握手:A 收到 B 的确认后,再向 B 发出确认报文段(ACK=1,ack=y+1,seq=x+1),A 进入 ESTABLISHED(连接已建立)状态
最后,B 收到 A 的确认后,也进入 ESTABLISHED 状态,连接建立。
为什么三次?
第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。
换个易于理解的视角来看为什么要 3 次握手:客户端和服务端通信前要进行连接,“3 次握手”的作用就是双方都能明确自己和对方的收、发能力是正常的(本站的《深入理解握手分手》对此有更深入的讨论)。
第一次握手:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。从客户端的视角来看,我接到了服务端发送过来的响应数据包,说明服务端接收到了我在第一次握手时发送的网络包,并且成功发送了响应数据包,这就说明,服务端的接收、发送能力正常。而另一方面,我收到了服务端的响应数据包,说明我第一次发送的网络包成功到达服务端,这样,我自己的发送和接收能力也是正常的。
第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力,服务端的发送、接收能力是正常的。第一、二次握手后,服务端并不知道客户端的接收能力以及自己的发送能力是否正常。
而在第三次握手时,服务端收到了客户端对第二次握手作的回应。从服务端的角度,我在第二次握手时的响应数据发送出去了,客户端接收到了。所以,我的发送能力是正常的,而客户端的接收能力也是正常的。
经历了上面的三次握手过程,客户端和服务端都确认了自己的接收、发送能力是正常的,之后就可以正常通信了。
每次都是接收到数据包的一方可以得到一些结论,发送的一方其实没有任何头绪:我虽然有发包的动作,但是我怎么知道我有没有发出去,而对方有没有接收到呢?
而从上面的过程可以看到,最少是需要三次握手过程的,两次达不到让双方都得出自己、对方的接收、发送能力都正常的结论。
其实每次收到网络包的一方至少是可以得到:对方的发送、我方的接收是正常的。而每一步都是有关联的,下一次的“响应”是由于第一次的“请求”触发,因此每次握手其实是可以得到额外的结论的。
比如第三次握手时,服务端收到数据包,表面看服务端只能得到客户端的发送能力、服务端的接收能力是正常的,但是结合第二次,说明服务端在第二次发送的响应包,客户端接收到了,并且作出了响应,从而得到额外的结论:客户端的接收、服务端的发送是正常的。
5、TCP 的四次挥手(为什么四次?)
为什么建立连接是三次握手,而关闭连接却是四次挥手呢?
TCP 连接是双向传输的对等的模式,就是说双方都可以同时向对方发送或接收数据。当有一方要关闭连接时,会发送指令告知对方,我要关闭连接了。
这时对方会回一个 ACK,此时一个方向的连接关闭。但是另一个方向仍然可以继续传输数据。也就是说,服务端收到客户端的 FIN 标志,知道客户端想要断开这次连接了,但是,我服务端,我还想发数据呢?我等到发送完了所有的数据后,会发送一个 FIN 段来关闭此方向上的连接,接收方发送 ACK 确认关闭连接。
注意,接收到 FIN 报文的一方只能回复一个 ACK,它是无法马上返回对方一个 FIN 报文段的。因为结束数据传输的“指令”是上层应用层给出的,我只是一个“搬运工”,我无法了解“上层的意志”。
客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。
因为服务端在 LISTEN 状态下,收到建立连接请求的 SYN 报文后,把 ACK 和 SYN 放在一个报文里发送给客户端。而关闭连接时,当收到对方的 FIN 报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方是否现在关闭发送数据通道,需要上层应用来决定,因此,己方 ACK 和 FIN 一般都会分开发送。
客户端接收到服务器端的 FIN 报文后,进入 TIME_WAIT 状态。此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由:
- 理由一:保证客户端发送的最后一个 ACK 报文段能够到达服务器。这个 ACK 报文段可能丢失,服务器收不到确认就会超时重传 FIN+ACK 报文段,客户端就能在 2MSL 时间内收到这个重传的报文段,接着重传一次确认,重新启动 2MSL 计时器,服务器也就能正常收到确认并进入 CLOSED 状态
- 理由二:防止已失效的连接请求报文段出现在本连接中。客户端在发送完最后一个 ACK 报文段后,再经过时间 2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,这样就可以使下一个新的连接中不会出现旧连接的报文段
关于 TIME_WAIT 状态,本站还有两篇展开:《TIME_WAIT 状态详解》和《为什么需要 TIME_WAIT》。
6、TCP 短连接和长连接的区别
短连接:Client 向 Server 发送消息,Server 回应 Client,然后一次读写就完成了,这时候双方任何一个都可以发起 close 操作,不过一般都是 Client 先发起 close 操作。短连接一般只会在 Client/Server 间传递一次读写操作。
短连接的优点:管理起来比较简单,建立存在的连接都是有用的连接,不需要额外的控制手段。
长连接:Client 与 Server 完成一次读写之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。连接长时间空闲时,还可以依靠 TCP 自带的 Keepalive 机制来探测对端是否存活(详见本站的《TCPKeepalive》)。
在长连接的应用场景下,Client 端一般不会主动关闭它们之间的连接。Client 与 Server 之间的连接如果一直不关闭的话,随着客户端连接越来越多,Server 压力也越来越大,这时候 Server 端需要采取一些策略,如关闭一些长时间没有读写事件发生的连接,这样可以避免一些恶意连接导致 Server 端服务受损;如果条件再允许,可以以客户端为粒度,限制每个客户端的最大长连接数,从而避免某个客户端连累后端的服务。
长连接和短连接的产生在于 Client 和 Server 采取的关闭策略,具体的应用场景采用具体的策略。
7、TCP 粘包、拆包及解决办法
为什么常说 TCP 有粘包和拆包的问题,而不说 UDP?
由第 1、2 节可知,UDP 是基于报文发送的,UDP 首部采用了 16 bit 来指示 UDP 数据报文的长度,因此在应用层能很好地将不同的数据报文区分开,从而避免粘包和拆包的问题。
而 TCP 是基于字节流的,虽然应用层和 TCP 传输层之间的数据交互是大小不等的数据块,但是 TCP 并没有把这些数据块区分边界,仅仅是一连串没有结构的字节流;另外从第 2 节的 TCP 首部字段表也可以看出,TCP 的首部没有表示数据长度的字段。基于上面两点,在使用 TCP 传输数据时,才有粘包或者拆包现象发生的可能。
什么是粘包、拆包?
假设 Client 向 Server 连续发送了两个数据包,用 packet1 和 packet2 来表示,那么服务端收到的数据可以分为三种情况:
- 第一种情况:接收端正常收到两个数据包,即没有发生拆包和粘包的现象
- 第二种情况:接收端只收到一个数据包,但是这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理
- 第三种情况:接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的
为什么会发生 TCP 粘包、拆包?
常见原因如下:
- 应用程序写入的数据大于套接字发送缓冲区的大小,会发生拆包
- 待发送的数据大于 TCP 报文段的最大长度(MSS),会发生拆包
- 待发送数据的长度大于路径上的 MTU,需要在 IP 层分片,也会发生拆包
- 应用程序写入的数据小于套接字发送缓冲区的大小,TCP 会将多次写入的数据一次性发送出去,会发生粘包
- 接收端没有及时读取套接字接收缓冲区中的数据,新数据到来时会与缓冲区中残留的数据粘在一起,会发生粘包
粘包、拆包解决办法
由于 TCP 本身是面向字节流的,无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决。根据业界主流协议的解决方案,归纳如下:
- 消息定长:发送方给每个报文固定长度,不足则补位,接收方按固定长度切分
- 包尾加分隔符:以特定字符作为消息边界,例如以
\r\n结尾(FTP 协议就是这么做的) - 消息头 + 消息体:消息头中写明消息体的长度,接收方先读消息头拿到长度,再按长度读取消息体,这是自定义应用层协议最常见的做法
8、TCP 可靠传输
TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。
一个报文段从发送到接收到确认所经过的时间称为往返时间 RTT,加权平均往返时间 RTTs 每收到一个 RTT 样本更新一次,计算方式如下:
RTTs = (1 - a) × 旧 RTTs + a × 新 RTT 样本
其中,0 ≤ a < 1,RTTs 随着 a 的增加更容易受到 RTT 的影响(RFC 6298 推荐 a 取 1/8)。超时时间 RTO 应该略大于 RTTs,TCP 使用的超时时间计算方式如下:
RTO = RTTs + 4 × RTTd
其中 RTTd 为 RTT 偏差的加权平均值。
9、TCP 滑动窗口
窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。
发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。
接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收了。
关于滑动窗口与重传、拥塞控制的配合,本站的《重传滑动窗口与拥塞控制》有更详细的图解。
10、TCP 流量控制
流量控制是为了控制发送方发送速率,保证接收方来得及接收。
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。
这里有一个隐患:如果接收方发出的“窗口恢复为非 0”的通知在传输中丢失了,发送方会一直等待新的窗口通知,接收方又会一直等待发送方的数据,双方互相等待,形成死锁。实际上,为了避免此问题的产生,发送端主机会时不时地发送一个叫做窗口探测的数据段,此数据段仅包含一个字节,用来获取最新的窗口大小信息。
11、TCP 拥塞控制
如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同:流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。
TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。
发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量。注意拥塞窗口与发送窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送窗口,而发送窗口取拥塞窗口和接收方窗口中较小的一个。
为了便于讨论,做如下假设:
- 数据是单方向传送的,而对方的确认报文段可以及时返回
- 接收方总是有足够大的缓存空间,因而发送窗口的大小由网络的拥塞程度决定
慢开始与拥塞避免
发送的最初执行慢开始,令 cwnd = 1,发送方只能发送 1 个报文段;每经过一个传输轮次(把当前 cwnd 里的报文段全部发送出去,并收到了对它们的确认),cwnd 就加倍,因此之后发送方能够发送的报文段数量为:2、4、8 …
注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送速度的增长过快,网络拥塞的可能性也就更高。因此设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。
如果出现了超时,则令 ssthresh = cwnd / 2,然后重新执行慢开始。
快重传与快恢复
在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1 和 M2,此时收到 M4,应当发送对 M2 的确认。
在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M2 的重复确认,则说明 M3 丢失,立即重传 M3。
在这种情况下,只是丢失个别报文段,而不是网络拥塞,因此执行快恢复,令 ssthresh = cwnd / 2,cwnd = ssthresh,并直接进入拥塞避免。
慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率:慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。
12、提高网络利用率
Nagle 算法
发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送的一种处理机制。具体来说,就是仅在下列任意一种条件下才能发送数据,如果两个条件都不满足,那么暂时等待一段时间以后再进行数据发送:
- 已发送的数据都已经收到了确认应答
- 可以发送的数据长度达到了最大段长度(MSS)
延迟确认应答
接收方收到数据之后可以不立即返回确认应答,而是延迟一段时间的机制。这样,如果接收方恰好也有数据要发送,就可以把确认捎带在数据包上一起返回(也就是下一节的捎带应答);即使没有数据可发,也可以等一小段时间,看能不能用一个确认覆盖更多已收到的报文段。
捎带应答
在一个 TCP 包中既发送数据又发送确认应答的一种机制。由此,网络利用率会提高,计算机的负荷也会减轻,但是这种应答必须等到应用处理完数据并将作为回执的数据返回为止。
Nagle 算法作用于发送端,延迟确认应答和捎带应答作用于接收端,三者配合,共同减少了网络中小包的数量,也就提高了网络利用率。
今天的知识点掌握了吗?不要忘了学而时习之,不亦可乎。
欢迎留言和我交流~
大家如果有补充的,也是可以留言区补充一波哦。