TCP/UDP 的包到达机器之后,发现 MAC 地址匹配,之后会交给 IP 层的代码进行处理。IP 层的代码处理之后发现目标 IP 也匹配,接下去就要交给运输层进行处理了。
在 IP 头里面有个 8 位协议,表示是这个数据包是 UDP 的还是 TCP 的。运输层处理完之后,内核的事情就基本干完了,接下去就要交给应用程序自己去处理了。
而交给哪个应用程序是根据端口号来判断的,无论 TCP 还是 UDP,包里头都有端口号,根据端口号,将数据包交给相应的应用程序。
TCP
所谓的建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性。
TCP 主要关注以下几个问题:
- 连接维护;
- 顺序问题;
- 丢包问题;
- 流量控制;
- 拥塞控制;
TCP 报文格式
TCP 报文包含首部和数据两部分。
-
源端口号和目标端口号
-
序号 :用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。而进行编号是为了解决乱序问题,不编号的话,无法确定哪个包是先来的,哪个包是后到的。
-
确认号 :期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。
-
数据偏移 :指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。
-
确认 ACK :当 ACK=1 时确认号字段有效,否则无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。
-
同步 SYN :在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1,ACK=1。
-
终止 FIN :用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放连接。
-
重新连接 RST:重新连接的状态位。
这些带状态位的包的发送,会引起双方的状态变更。
-
窗口大小 :窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的,发送方要向接受方表示自己当前能够处理的能力。
TCP 三次握手的过程
**TCP 连接握手的本质其实是同步通信双方数据原点的序列号。**假设 A 为客户端,B 为服务器端。
-
首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。
-
A 向 B 发送连接请求报文,A 的 OS 会随机生成一个 32 位长的序列号,A 会向 B 发送含有这个序列号的数据包,就是告诉 B 自己的初始序列号。具体为:SYN=1,ACK=0,选择一个初始的序号 x。
为什么 A 的初始序号不能从 1 开始呢?因为这样往往会发生冲突。
假设 A 和 B 建立了连接,发送了 1、2、3 三个包。但是发送 3 的时候,包丢了或者是在链路上停留时间过长,于是 A 重新发送。之后,处理完之后,A 掉线了,重新连上 B 之后,序号又从 1 开始,然后只是发送 2,压根没想到发送 3。但是上次连接时候停留的 3 又回来了,发送给了 B,B 认为这是下一个包,于是发生了错误。
因而,每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的,可以看成一个 32 位的计数器,每 4 微秒加一,如果计算一下,如果到重复,需要 4 个多小时,那个停留的数据包早已不存在了,因为我们都知道 IP 包头里面有个 TTL,也即生存时间。
-
B 收到连接请求报文,如果同意建立连接,则向 A 发送连接ACK 确认报文和 B 自己初始序列号的数据包。具体为:SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。
-
A 收到 B 的连接确认报文后,还要向 B 发出回复一个 ACK 确认包,用于确认 A 和 B 就 B 的初始序列号已达成一致了。具体为:确认号为 y+1,序号为 x+1。
-
B 收到 A 的确认后,连接建立。
在连接建立的过程中,双方的状态变化时序图如下:
- 一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态。
- 然后客户端主动发起连接 SYN,之后处于 SYN-SENT 状态。
- 服务端收到发起的连接,返回 SYN,并且 ACK 客户端的 SYN,之后处于 SYN-RCVD 状态。
- 客户端收到服务端发送的 SYN 和 ACK 之后,发送 ACK 的 ACK,之后处于 ESTABLISHED 状态,因为它一发一收成功了。
- 服务端收到 ACK 的 ACK 之后,处于 ESTABLISHED 状态,因为它也一发一收了。
为什么需要三次
从可靠性来说
假设这个通信链路是非常不可靠的,数据包在这上面传输很有可能会丢包等。
这个时候 A 要发起一个连接,当发了请求之后,第一个请求包可能会丢,也可能会绕路,也可能 B 不想跟 A 建立连接。A 于是再发,当然 A 会在重试一段时间后放弃,连接建立失败。但是,这个时候 B 收到了 A 的请求包,并且也想跟 A 建立连接,这个时候就会发送应答包给 A。对于 B 发送的应答包来说,也可能会丢,会绕弯路,甚至 A 已经挂了都有可能。所以,这个时候假如是两次握手的话,也就是 B 发完应答包之后就建立连接肯定是不行的。
B 发送的应答包可能也会发送多次,但是这个时候 A 只要收到一个应答包就会建立连接。因为对 A 来讲,它发出去的消息有应答了就行。当 B 收到了应答包的应答之后,也会建立连接。
当然,在这个过程 A 发送的应答包的应答包也可能会丢,会绕弯路,甚至此时 B 由于一直没收到这个包就释放了连接也有可能。那么,按理来说,还应该有个应答包的应答包的应答包,也就是四次握手,同理为了确保这个包发送成功还要有第五次握手,那么这样下去就会没完没了,而且无论多少次都不能确保完整可靠。
因此,其实只要双方发出去的消息有个回复就基本可以了。并且,A 和 B 建立连接之后,A 一般会马上发送数据,一旦 A 发送了数据,那么很多问题就得到了解决。
- 比如 A 发送给 B 的应答丢了,但是这个时候 A 是已经处于连接状态了的,那么当 B 收到 A 发送的数据之后,B 可以认为这个连接已经建立。
- 或者 A 给 B 发送应答之后,B 断了,此时 A 是已经处于连接状态了的,那么这个时候 A 发送的数据会报错,说 B 不可达。
当然,可能会出现 A 不会第一时间发送数据包的情况,这个时候可以选择开启 keepalive 机制。这样,即使在没有真实的数据包的情况下,也会有探活包。另外,服务端 B 可以针对 A 这种长时间不发送数据包的连接进行关闭。
从同步 TCP 包的序号来说
除了上述可以可靠的建立连接之外,三次握手最主要的目的其实是为了同步通信双方的 TCP 包的起始序列号。
假如只有两次的话:
A 与 B 就 A 的初始序列号达成了一致,但是 B 不知道 A 是否接受了自己的数据包,如果数据包丢失了,那么 A 和 B 就 B 的初始序列号就无法达成一致。
假如四次的话
-
A 发送同步信号 SYN + A'sInitial sequence number
-
B 确认收到 A 的同步信号,并记录 A's ISN 到本地,命名 B's ACK sequence number
-
B 发送同步信号 SYN + B's Initial sequence number
-
A 确认收到 B 的同步信号,并记录 B's ISN 到本地,命名 A's ACK sequence number
那么中间的两次其实可以合并,从而提高连接的速度和效率。
三次握手过程可以携带数据吗
第三次握手的时候,是可以携带数据的。但是,第一次、第二次握手不可以携带数据。
假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。
TCP 四次挥手的过程
以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。
- A 发送连接释放报文,FIN=1。
- B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据。
- 当 B 不再需要连接时,发送连接释放报文,FIN=1。
- A 收到后发出确认,进入 TIME-WAIT 状态,等待 2 MSL(最大报文存活时间)后释放连接。
- B 收到 A 的确认后释放连接。
在连接断开的过程中,双方的状态变化时序图如下:
-
A 发送 FIN 连接释放报文之后,就进入 FIN_WAIT_1 的状态。
-
B 在收到这个报文之后,就会进入 CLOSE-WAIT 状态。同时发送一个应答包。
-
B 在收到这个应答包之后,就进入 FIN_WAIT_2 状态。如果,这个时候 B 出于某种原因断开了的话,那么 A 将永远处于这种状态。TCP 里头并没有针对这种状态的处理,但是 Linux 有,可以调整 tcp_fin_timeout 参数,从而设置一个超时时间。
-
之后 CLOSE-WAIT 这个状态是为了让服务器端发送还未传送完毕的数据。而这个时候 A 可以选择不再接收数据了,也可以选择最后再接收一段数据,等待 B 也主动关闭。当 B 的数据传送完毕之后,B 会发送 FIN 连接释放报文。之后进入 LAST—ACK 等待 A 的回复报文。
-
A 收到 B 发送的 FIN 连接释放报文之后。A 发送这个连接报文的 ACK,进入 TIME- WAIT 状态。
-
客户端接收到服务器端的 FIN 报文后进入 TIME-WAIT 状态,而不是直接进入 CLOSED 状态,主要是出于以下两点:
-
确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生。
这个时间要足够长,长到如果 B 没收到 ACK 的话,B 的 FIN 连接释放报文会重发,并且 A 会重新发一个 ACK 并且足够时间到达 B。
-
等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。因为假设 A 直接进入关闭状态,那么 A 的端口就直接空出来了,这个时候这个端口很有可能会被一个新的应用占用。而此时 B 原来发过的很多数据包都还在路上,那么这个时候新的应用会收到上个连接中 B 发送的数据包,虽然序列号是重新生成了的,但是以防万一,来个双保险从而防止产生混乱,因而也需要等待一段时间,让就连接的报文都从网络中消失。
TIME-WAIT 持续的时间一般设为 2MSL,MSL 是 Maximum Segment Lifetime,报文最大生存时间。它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。而 2 倍就是相当于有两个往返。协议规定 MSL 为 2 分钟,实际应用中常用的是 30 秒,1 分钟和 2 分钟等。
-
-
假设超过了 2MSL 的时间,A 进入了 CLOSE 状态,但是 B 还是没收到它发的 FIN 的 ACK,那么这个时候 B 还是会重发 FIN。假设 A 再收到这个包之后,A 会发送 RST 包,这样 B 就知道 A 早就断开了。
为什么要四次挥手
TCP 是全双工通信,在关闭连接时需要两端分别发送结束信号完全关闭连接。
假如,A 在收到 B 的断开连接请求之后,回复了个 ACK 之后就直接关闭的话,那么 B 还有要发送的数据是无法发送出去的。
总结
将连接建立和连接断开的两个时序状态图综合起来,就是这个著名的 TCP 的状态机。看的时候,建立将这个状态机和上面的时序状态机结合起来看。在这个图中:
- 加黑加粗的部分,是上面说到的主要流程;
- 阿拉伯数字的序号,是连接过程中的顺序;
- 大写中文数字的序号,是连接断开过程中的顺序;
- 加粗的实线是客户端 A 的状态变迁,加粗的虚线是服务端 B 的状态变迁。
TCP 可靠传输
窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。
发送窗口内的字节都允许被发送。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态。
对于接收端来说,接收窗口内的字节都允许被接收。接收窗口的滑动类似,接收窗口左部字节已经发送确认,就向右滑动接收窗口。为了保证不丢包,对于发送的包,接受端都要进行回答,并且由于收到的包不一定连续会出现空档,所以接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。这种方式是累计确认和累计应答(cumulative acknowledgment)。
顺序和丢包问题---确认与重发机制
如图所示,在发送端看来,1、2、3 已经发送并确认;4、5、6、7、8、9 都是发送了还没确认;10、11、12 是还没发出的;13、14、15 是接收方没有空间,不准备发的。
在接收端来看,1、2、3、4、5 是已经完成 ACK,但是没读取的;6、7 是等待接收的;8、9 是已经接收,但是无法发送 ACK 的。
综上来说,发送端和接受端的状态如下:
- 1、2、3 没有问题,双方达成了一致。
- 4、5 接收方发 ACK 了,但是发送方还没收到 ACK,有可能是 ACK 丢了,有可能在路上。
- 6、7、8、9 肯定都发了,但是 8、9 已经到了,但是 6、7 没到,出现了乱序,所以 8、9 缓存着但是没办法 ACK。
通过上述这个例子,我们看到顺序和丢包问题随时都可能发生,TCP 采用以下这几种方式来确保顺序和处理丢包问题。
超时重试
超时重试,也即对每一个发送了,但是没有 ACK 的包,都有设一个定时器,超过了一定的时间,就重新尝试。这个超时的时间不宜过短,时间必须大于往返时间 RTT,否则会引起不必要的重传。也不宜过长,这样超时时间变长,访问就变慢了。
估计往返时间,需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个值,而且这个值还是要不断变化的,因为网络状况不断地变化。除了采样 RTT,还要采样 RTT 的波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,我们称为自适应重传算法(Adaptive Retransmission Algorithm)。
如果过一段时间,4 的 ACK 发送方收到了,5、6、7 都超时了,就会重新发送。接收方发现 5 原来接收过,于是丢弃 5;6 收到了,7 不幸又丢了,发送 ACK,要求下一个是 7。这个时候 7 再次超时需要重传,那么 TCP 的策略是超时间隔加倍。每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
超时触发重传存在的问题是,超时周期可能相对较长。
快重传
有一个可以快速重传的机制,当接收方收到一个序号大于下一个所期望的报文段时,就会检测到数据流中的一个间隔,于是它就会发送冗余的 ACK,仍然 ACK 的是期望接收的报文段。而当客户端收到三个冗余的 ACK 后,就会在定时器过期之前,重传丢失的报文段。
例如,接收方发现 6 收到了,8 也收到了,但是 7 还没来,那肯定是丢了,于是发送 6 的 ACK,要求下一个是 7。接下来,收到后续的包,仍然发送 6 的 ACK,要求下一个是 7。当客户端收到 3 个重复 ACK,就会发现 7 的确丢了,不等超时,马上重发。
SACK
还有一种方式称为 Selective Acknowledgment (SACK)。这种方式需要在 TCP 头里加一个 SACK 的东西,可以将缓存的地图发送给发送方。例如可以发送 (ACK6、SACK8、SACK9),有了地图,发送方一下子就能看出来是 7 丢了。
流量控制
流量控制是作用于接收者的,它是控制发送者的发送速度从而使接收者来得及接收,防止分组丢失的(流量控制根本目的是防止分组丢失)。接收方返回的 ACK 中会包含自己的接收窗口的大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。
下面举个例子,先假设窗口不变的情况下,窗口始终为 9。4 的确认来的时候,会右移一个,这个时候第 13 个包也可以发送了,但是这个时候 10、11、12、13 都没有发送。
当 10、11、12、13 全部发送完毕,之后就停止发送了,未发送可发送部分为 0。
当对于包 5 的确认到达的时候,在发送端窗口会再向左滑动一格。这个时候,才可以有更多的包可以发送了,例如第 14 个包才可以发送。
如果这个时候,接受端处理的太慢,导致缓存中没有空间了,可以通过确认信息修改窗口的大小,甚至可以设置为 0,则发送方将暂时停止发送。这里假设一个极端情况,接收端的应用一直不读取缓存中的数据,当数据包 6 确认后,窗口大小就不能再是 9 了,就要缩小一个变为 8。
这个新的窗口 8 通过 6 的确认消息到达发送端后,发送端的窗口没有平行右移,而是仅仅左面的边右移了,发送端的窗口大小从 9 改成了 8。
如果,接受端还是一直不处理数据,随着确认的包越来越多,接受端窗口越来越小,直到为 0。
当这个窗口通过包 14 的确认到达发送端的时候,发送端的窗口也调整为 0,停止发送。
当窗口变为 0 之后,发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。这个时候当接受端接受比较慢的时候,为了防止低能窗口综合征,也就是为了防止空出一个字节就告诉对方,然后又瞬间填满的情况出现。这个时候,也就是当窗口还是太小的时候,可以先不更新窗口,直到到达了一定大小之后,比如缓冲区一半为空了之后,再更新窗口。
拥塞控制
如果我们设置发送窗口,当发送但未确认的包为通道的容量时,就能撑满整个管道。如图所示,假设往返时间为 8s,去 4s,回 4s,每秒发送一个包,每个包 1024byte。已经过去了 8s,则 8 个包都发出去了,其中前 4 个包已经到达接收端,但是 ACK 还没有返回,不能算发送成功。5-8 后四个包还在路上,还没被接收。这个时候,整个管道正好撑满。在发送端,已发送未确认的为 8 个包,正好等于通道容量,也即每秒发送 1 个包,乘以来回时间 8s。
通道的容量 = 带宽 × 往返延迟
那么如果这个时候如果在这个基础上再调大窗口的话,使得单位时间内更多的包可以发送。那么可能会出现包丢失和超时重传。比如,原来发送一个包,从一端到达另一端,假设一共经过四个设备,每个设备处理一个包时间耗费 1s,所以到达另一端需要耗费 4s,如果发送的更加快速,则单位时间内,会有更多的包到达这些中间设备,这些设备还是只能每秒处理一个包的话,多出来的包就会被丢弃。假如我们在这些设备上加缓存,处理不过来的在队列里面排着,这样包就不会丢失,但是缺点是会增加时延,这个缓存的包,4s 肯定到达不了接收端了,如果时延达到一定程度,就会超时重传。
所以 TCP 的拥塞控制主要来避免两种现象,包丢失和超时重传。它可以防止过多的数据注入到网络中,避免出现网络负载过大的情况。
常用用于拥塞控制的方法有:(1)慢开始;(2)快重传、快恢复。
-
慢开始
发送的最初执行慢开始,令 cwnd = 1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 ...
其实这个翻倍是这样的,cwnd=1 的时候,我收到了一个确认报文,所以 cwnd=cwnd+1,其实相当于翻倍了。这个时候,可以一次发 2 个,那么来一个确认的时候 cwnd+1,那么来两个之后 cwnd+2,所以其实也是翻倍
注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能性也就更高。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。
这个时候每个轮次+1可以这样理解,每来一个确认包,我 cwnd+1/cwnd,那么当本次发送的 cwnd 的包都到了之后,那么其实是 cwnd+1
如果出现了超时,则令 ssthresh = cwnd / 2,然后重新执行慢开始,也就是再将 cwnd 设置为 1。
-
快重传、快恢复
在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1 和 M2,此时收到 M4、M5、M6,都应该发送对 M2 的确认。
在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd / 2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。
慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。
存在的问题
TCP 的拥塞控制用来避免包丢失和超时重传,其实存在问题。
- 丢包并不代表着通道满了。例如公网上带宽不满也会丢包,这个时候就认为拥塞了,退缩了,其实是不对的。
- TCP 的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实 TCP 只要填满管道就可以了,不应该接着填,直到连缓存也填满。
BBR 拥塞算法
为了优化上述的两个问题,后来有了 TCP BBR 拥塞算法。它企图找到一个平衡点,就是通过不断地加快发送速度,将管道填满,但是不要填满中间设备的缓存。因为填满缓存之后,时延会增加。在这个平衡点可以很好地找到高带宽和低时延的平衡。
TCP 建立连接的过程
TCP编程的服务器端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt(); * 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();
4、开启监听,用函数listen();
5、接收客户端上来的连接,用函数accept();
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;
8、关闭监听;
TCP编程的客户端一般步骤是:
1、创建一个socket,用函数socket();
2、设置socket属性,用函数setsockopt();* 可选
3、绑定IP地址、端口等信息到socket上,用函数bind();* 可选
4、设置要连接的对方的IP地址和端口等属性;
5、连接服务器,用函数connect();
6、收发数据,用函数send()和recv(),或者read()和write();
7、关闭网络连接;
- 创建 Socket,在连接之前,客户端和服务端都要掉用函数 socket() 创建与结构体 socket 相关的资源,如创建文件描述符等资源。(这一步还可以调用相关函数设置 socket 属性,绑定 IP 地址等操作)
- 服务端调用函数 listen() 进入监听状态。在内核中主要是干两件事:指定全连接队列的大小。内核中维护着一个系统级的全连接队列的大小;将主动 socket 改为被动 socket,也就是将结构体 socket 的状态设置为 TCP_LISTEN。
- 服务端调用函数 accept() 等待与客户端连接。在内核中主要是干两件事:创建新的资源服务新的连接;如果全连接队列中存在已经建立连接的 socker,就将其出队,如果不存在则让进程进入阻塞状态等待客户端的连接。当 accept () 返回时,服务端与客户端完成了三次握手,建立了新的连接。
- 客户端调用 connect() 函数与服务端建立连接。这里包含了三次握手的全过程,三次连接的过程其实在内核层面看来其实就是改变内核数据结构的连接状态而已。
UDP
MAC 层定义了本地局域网的传输行为,IP 层定义了整个网络端到端的传输行为,这两层基本定义了这样的基因:网络传输是以包为单位的,二层叫帧,网络层叫包,传输层叫段。我们笼统地称为包。包单独传输,自行选路,在不同的设备封装解封装,不保证到达。UDP 就完全继承了这些特性。
UDP 数据报的格式
首部字段只有 8 个字节,包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。
UDP 的特点
- 不需要大量的数据结构、处理逻辑、包头字段等。
- 它不会建立连接,虽然有端口号,但是监听在这个地方,谁都可以传给他数据,他也可以传给任何人数据,甚至可以同时传给多个人数据。
- 不会根据网络的情况进行发包的拥塞控制,无论网络丢包丢成啥样了,它该怎么发还怎么发。
UDP 的使用场景
-
需要资源少,在网络情况比较好的内网,或者对于丢包不敏感的应用。
DHCP 就是基于 UDP 协议的。一般的获取 IP 地址都是内网请求,而且一次获取不到 IP 又没事,过一会儿还有机会。PXE 可以在启动的时候可以自动安装操作系统,操作系统镜像的下载使用的 TFTP 也是基于 UDP 协议的。在还没有操作系统的时候,客户端拥有的资源很少,不适合维护一个复杂的状态机,而且因为是内网,一般也没啥问题。
-
不需要一对一沟通,建立连接,而是可以广播的应用。
UDP 的不面向连接的功能,可以使得可以承载广播或者多播的协议。DHCP 就是一种广播的形式,就是基于 UDP 协议的。
对于多播,IP 地址分类中有一个 D 类地址,也即组播地址,使用这个地址,可以将包组播给一批机器。当一台机器上的某个进程想监听某个组播地址的时候,需要发送 IGMP 包,所在网络的路由器就能收到这个包,知道有个机器上有个进程在监听这个组播地址。当路由器收到这个组播地址的时候,会将包转发给这台机器。
在云中网络中有一个协议叫做 VXLAN,也是需要用到组播,也是基于 UDP 协议的。
-
需要处理速度快,时延低,可以容忍少数丢包,要求即便网络拥塞,也要继续不断发包的。
UDP 简单、处理速度快,不像 TCP 那样,各种重传啊,保证顺序到达(前面没到的,后面的就没法处理)。TCP 等这些事情做完之后,时延就上去了。而 TCP 在网络不好出现丢包的时候,拥塞控制策略会降低发送速度,这就相当于本来环境就差,还自断臂膀,用户本来就卡,这下更卡了。
当前很多应用都是要求低时延的,它们不想用 TCP 如此复杂的机制,而是想根据自己的场景,实现自己的可靠和连接保证。例如,如果应用自己控制,哪些包可以丢,哪些包丢了之后应用自己重传,而不依赖于 TCP。所以如果一个应用需要有自己的连接策略、可靠保证、时延要求的话,可以使用 UDP。
使用 UDP 的例子
-
网页或者 APP 的访问
原来访问网页和手机 APP 都是基于 HTTP 协议的。HTTP 协议是基于 TCP 的,建立连接都需要多次交互,对于目前主流的移动互联网来讲时延比较大,建立一次连接需要的时间会比较长。并且在移动中,TCP 可能还会断了重连,也很耗时。目前 HTTP 协议往往采取多个数据通道共享一个连接的情况,这本来是为了加快传输速度,但是由于 TCP 的严格顺序控制,使得在共享通道时,即使后一个和前一个没关系,只要中间的一个不来,就要等着,时延也会加大。
QUIC(全称 Quick UDP Internet Connections,快速 UDP 互联网连接)是 Google 提出的一种基于 UDP 改进的通信协议,其目的是降低网络通信的延迟,提供更好的用户互动体验。
QUIC 在应用层上,会自己实现快速连接建立、减少重传时延,自适应拥塞控制
-
流媒体的协议
直播协议多使用 RTMP,而 RTMP 协议也是基于 TCP 的。TCP 的严格顺序传输要保证前一个收到了,下一个才能确认,如果前一个收不到,下一个就算包已经收到了,在缓存里面,也需要等着。对于直播来讲,这显然是不合适的,因为老的视频帧丢了其实也就丢了,就算再传过来用户也不在意了,他们要看新的了,如果老是没来就等着,卡顿了,新的也看不了,那就会丢失客户,所以直播,实时性比较比较重要,宁可丢包,也不要卡顿的。
另外,对于丢包,其实对于视频播放来讲,有的包可以丢,有的包不能丢,因为视频的连续帧里面,有的帧重要,有的不重要,如果必须要丢包,隔几个帧丢一个,其实看视频的人不会感知,但是如果连续丢帧,就会感知了,因而在网络不好的情况下,应用希望选择性的丢帧。
还有就是当网络不好的时候,TCP 协议会主动降低发送速度,这对本来当时就卡的看视频来讲是要命的,应该应用层马上重传,而不是主动让步。因而,很多直播应用,都基于 UDP 实现了自己的视频传输协议。
-
实时游戏
游戏和流媒体类似,都要求实时性。因而,实时游戏中客户端和服务端要建立长连接,来保证实时传输。但是游戏玩家很多,服务器却不多。由于维护 TCP 连接需要在内核维护一些数据结构,因而一台机器能够支撑的 TCP 连接数目是有限的,然后 UDP 由于是没有连接的,在异步 IO 机制引入之前,常常是应对海量客户端连接的策略。
另外,对战的游戏,对网络的要求很简单,玩家通过客户端发送给服务器鼠标和键盘行走的位置,服务器会处理每个用户发送过来的所有场景,处理完再返回给客户端,客户端解析响应,渲染最新的场景展示给玩家。假如使用 TCP 的话,TCP 的强顺序使得如果出现一个数据包丢失,所有事情都需要停下来等待这个数据包重发。客户端会出现等待接收数据,然而玩家并不关心过期的数据,激战中卡 1 秒,等能动了都已经死了。
游戏对实时要求较为严格的情况下,采用自定义的可靠 UDP 协议,自定义重传策略,能够把丢包产生的延迟降到最低,尽量减少网络问题对游戏性造成的影响。
-
IoT 物联网
一方面,物联网领域终端资源少,很可能只是个内存非常小的嵌入式系统,而维护 TCP 协议代价太大;另一方面,物联网对实时性要求也很高,而 TCP 还是因为保证顺序问题会导致时延过大。
Google 旗下的 Nest 建立 Thread Group,推出了物联网通信协议 Thread,就是基于 UDP 协议的。
-
移动通信领域
在 4G 网络里,移动流量上网的数据面对的协议 GTP-U 是基于 UDP 的。因为移动网络协议比较复杂,而 GTP 协议本身就包含复杂的手机上线下线的通信协议。所以,这个时候在用复杂的 TCP 协议就显得多余了。
TCP 和 UDP 的区别
- TCP协议是一个面向连接、面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块)、可靠(通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达)、全双工的传输协议(全双工的意思是说,通信双方可以同时发送,接收数据),每一条 TCP 连接只能是点对点的。
- UDP是一个无连接、不可靠的(不保证不丢失,不保证按顺序到达)、尽最大可能交付、面向报文(面向报文是指应用层交给 UDP 多长的报文,UDP 就照样发送,不合并也不拆分)的协。支持一对一、一对多、多对一和多对多的交互通信。
- TCP要求系统资源较多,UDP较少;
- TCP 是可以有拥塞控制的,而 UDP 是没有的;
- TCP 其实是一个有状态服务,而 UDP 则是无状态服务;
UDP 常用于以下几个方面:
-
包总量较少的通信(DNS、SNMP等);
-
视频、音频等多媒体通信(即时通信);
-
限定于 LAN 等特定网络中的应用通信;
-
广播通信(广播、多播)。