详解TCP和UDP

727 阅读15分钟

TCP

TCP协议位于传输层,全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。

TCP 协议的特点

面向连接

面向连接,是指发送数据之前必须在两端建立连接,建立连接的方法就是 三次握手,这样能建立可靠的连接,为数据的可靠传输打下了基础。

仅支持单播传输

每条 TCP 传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式。

面向字节流

TCP 不像 UDP 一样那样一个个报文独立传输,而是在不保留报文边界的情况下以字节流方式进行传输。

全双工通信

TCP提供全双工通信。全双工指的是连接双方可以同时收发数据。在收发两端都有发送缓存和接收缓存,发送缓存就是一个准备发送的队列,接收缓存是一个准备接收的队列。

可靠传输

  • 校验。伪首部是为了增加TCP校验和的检错能力:通过伪首部的目的IP地址来检查TCP报文是否收错了、通过伪首部的传输层协议号来检查传输层协议是否选对了。需要注意的是,伪首部实际上是不存在的,只是用来验证TCP报文是否出错。
  • 序号。之前我们提到TCP是面向字节流的,比如第一个字节就是序号1,第二个字节就是序号2。 而在TCP报文格式介绍的时候,有一个序号字段,这个指的是一个报文段第一个字节的序号。报文段就是你每个数据包。有了序号,就能保证数据是有序的传入应用层。
  • 确认。发送方在收到接收方的确认包之后,才继续发送剩下的数据。
  • 重传。TCP的发送方在规定的时间内没有收到确认就要重传已发送的报文段(超时重传)。重传时间是动态改变的,依据的是RTTS(加权平均往返时间)。
  • TCP提供流量控制和拥塞控制机制进行数据报的传输。

首部开销大

TCP首部复杂,最小有20个字节最大可达到60个字节,所以传输速率相较于UDP低。

TCP三次握手

为什么是三次握手?

为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

第一次握手:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。

第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。

第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

因此,需要三次握手才能确认双方的接收与发送能力是否正常。

试想如果是用两次握手,则会出现下面这种情况:

当客户端发出第一个连接请求报文段时并没有丢失,而是在某个网络节点出现了长时间的滞留,以至于延误了连接请求在某个时间之后才到达服务器。这应该是一个早已失效的报文段。但是服务器在收到此失效的连接请求报文段后,以为是客户端的一个新请求,于是就向客户端发出了确认报文段,同意建立连接,然而客户端状态早已不是 SYNC-SENT 状态,所以并不会传输数据。假设不采用三次握手,那么只要服务器发出确认后,新的连接就可以建立了。但是由于客户端没有发出建立连接的请求,因此不会管服务器的确认,也不会向服务器发送数据,但服务器却以为新的运输连接已经建立,一直在等待,所以,服务器的资源就白白浪费掉了。

三次握手过程中可以携带数据吗?

其实第三次握手的时候,是可以携带数据的。但是,第一次、第二次握手不可以携带数据

为什么这样呢?大家可以想一个问题,假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。

也就是说,第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也没啥毛病。

TCP四次挥手

为什么是四次挥手?

因为服务端第三次挥手的时候没有立即进入 CLOSE 状态,而是进入了 CLOSE_WAIT 状态。直到等到客户端第四次挥手消息到达才会进入 CLOSE 状态。

为何客户端最后还等待 2MSL

为了保证最后发送的 ack 包能被服务端接收到,所以客户端要等待两个最长报文段寿命的时间,以便于服务端没有收到请求之后重新发送请求。客户端等待 2MSL 后依然没有收到回复,则证明服务端已正常关闭,那好,客户端也可以关闭连接了。

TCP 的流量控制

流量控制主要用到了滑动窗口的概念。简单理解就是通过调节窗口的大小来实现流量的控制。

发送端的滑动窗口结构如下:

17072401c4d59dcb_tplv-t2oaga2asx-image.png

其中包含四大部分:

  • 已发送且已确认
  • 已发送但未确认
  • 未发送但可以发送
  • 未发送也不可以发送

接收端的滑动窗口结构如下:

17072406c803d2c7_tplv-t2oaga2asx-image.png

流量控制整个过程:

以一个最简单的来回来模拟一下流量控制的过程,方便大家理解。 首先双方三次握手,初始化各自的窗口大小,均为 200 个字节。 假如当前发送端给接收端发送 100 个字节,那么此时对于发送端而言,SND.NXT 当然要右移 100 个字节,也就是说当前的可用窗口减少了 100 个字节,这很好理解。 现在这 100 个到达了接收端,被放到接收端的缓冲队列中。不过此时由于大量负载的原因,接收端处理不了这么多字节,只能处理 40 个字节,剩下的 60 个字节被留在了缓冲队列中。 注意了,此时接收端的情况是处理能力不够用啦,你发送端给我少发点,所以此时接收端的接收窗口应该缩小,具体来说,缩小 60 个字节,由 200 个字节变成了 140 字节,因为缓冲队列还有 60 个字节没被应用拿走。 因此,接收端会在 ACK 的报文首部带上缩小后的滑动窗口 140 字节,发送端对应地调整发送窗口的大小为 140 个字节。 此时对于发送端而言,已经发送且确认的部分增加 40 字节,也就是 SND.UNA 右移 40 个字节,同时发送窗口缩小为 140 个字节。 这也就是流量控制的过程。尽管回合再多,整个控制的过程和原理是一样的。

TCP 的拥塞控制

拥塞窗口(cwnd)是对发送端的限制,发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。

拥塞控制涉及到的算法有以下四个:

  1. 慢启动
  2. 拥塞避免
  3. 快速重传
  4. 快速恢复

慢启动

刚开始进入传输数据的时候,你是不知道现在的网路到底是稳定还是拥堵的,如果做的太激进,发包太急,那么疯狂丢包,造成雪崩式的网络灾难。 因此,拥塞控制首先就是要采用一种保守的算法来慢慢地适应整个网路,这种算法叫慢启动。运作过程如下:

  1. 首先,三次握手,双方宣告自己的接收窗口大小
  2. 双方初始化自己的拥塞窗口(cwnd)大小
  3. 在开始传输的一段时间,发送端每收到一个 ACK,拥塞窗口大小加 1,也就是说,每经过一个 RTT,cwnd 翻倍。如果说初始窗口为 10,那么第一轮 10 个报文传完且发送端收到 ACK 后,cwnd 变为 20,第二轮变为 40,第三轮变为 80,依次类推。

难道就这么无止境地翻倍下去?当然不可能。它的阈值叫做慢启动阈值(ssthresh),当 cwnd 大于这个阈值之后,好比踩了下刹车,别涨了那么快了,老铁,先 hold 住!在到达阈值后,如何来控制 cwnd 的大小呢?这就是拥塞避免做的事情了。

拥塞避免

原来每收到一个 ACK,cwnd 加 1,现在到达阈值了,cwnd 只能加这么一点: 1 / cwnd。那你仔细算算,一轮 RTT 下来,收到 cwnd 个 ACK, 那最后拥塞窗口的大小 cwnd 总共才增加 1。 也就是说,以前一个 RTT 下来,cwnd 翻倍,现在 cwnd 只是增加 1 而已。当然,慢启动和拥塞避免是一起作用的,是一体的。

快速重传

重传分为快速重传和选择性重传,快速重传是有一个包丢失重传所有,而选择性重传是丢了什么包重传什么包。

快速恢复

  • 在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1 和 M2,此时收到 M4,应当发送对 M2 的确认。

  • 在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快速重传,立即重传下一个报文段。例如收到三个 M2,则 M3 丢失,立即重传 M3。

  • 在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快速恢复,令 ssthresh = cwnd / 2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。

  • 慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快速恢复 cwnd 设定为 ssthresh。

UDP

UDP位于传输层,全称是用户数据报协议,是一种无连接、不可靠的协议。

UDP协议的特点

面向无连接

  • UDP 想发数据就可以开始发送了,不需要连接,它只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作
  • 在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了
  • 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作

支持单播、多播、广播传输

UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。

面向报文

  • 发送方的 UDP 对应用程序交下来的报文,在添加首部后就向下交付到网络层,UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界,因此,应用程序必须选择合适大小的报文。

不可靠传输

  • 不可靠性首先体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。
  • 收到什么数据就传什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。
  • 网络环境时好时坏,但 UDP 没有拥塞控制和流量控制,一直会以恒定的速度发送数据,即使网络条件不好,也不会对发送速率进行调整,这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景 (比如视频会议、直播) 就需要使用 UDP 而不是 TCP。

首部开销小

UDP首部没有TCP首付复杂,只有8个字节,所以在这方面优于TCP,传输速率更快。

TCP和UDP对比

90ea5411b2834e19825740de1de0b4fb_tplv-k3u1fbpfcp-watermark.png

扩展

什么是半连接队列,全连接队列?

服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。

当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。

这里在补充一点关于 SYN-ACK 重传次数的问题:

服务器发送完 SYN-ACK 包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传。如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。

注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s,2s,4s,8s…

ISN(Initial Sequence Number)是固定的吗?

当一端为建立连接而发送它的 SYN 时,它为连接选择一个初始序号(ISN)。ISN 随时间而变化,因此每个连接都将具有不同的 ISN。ISN 可以看作是一个 32 比特的计数器,每 4ms 加 1 。这样选择序号的目的在于防止在网络中被延迟的分组在以后又被传送,而导致某个连接的一方对它做错误的解释。

三次握手的其中一个重要功能是客户端和服务端交换 ISN(Initial Sequence Number),以便让对方知道接下来接收数据的时候如何按序列号组装数据。如果 ISN 是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。

SYN 攻击是什么?

服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到 SYN 洪泛攻击。SYN 攻击就是 Client 在短时间内伪造大量不存在的 IP 地址,并向 Server 不断地发送 SYN 包,Server 则回复确认包,并等待 Client 确认,由于源地址不存在,因此 Server 需要不断重发直至超时,这些伪造的 SYN 包将长时间占用半连接队列,导致正常的 SYN 请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。SYN 攻击是一种典型的 DoS/DDoS 攻击。

检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源 IP 地址是随机的,基本上可以断定这是一次 SYN 攻击。在 Linux/Unix 上可以使用系统自带的 netstats 命令来检测 SYN 攻击。

常见的防御 SYN 攻击的方法有如下几种

  1. 缩短超时(SYN Timeout)时间和重试次数
  2. 增加最大半连接数
  3. 利用 SYN Cookie 技术,在服务端接收到 SYN 后不立即分配连接资源,而是根据这个 SYN 计算出一个 Cookie,连同第二次握手回复给客户端,在客户端回复 ACK 的时候带上这个 Cookie 值,服务端验证 Cookie 合法之后才分配连接资源。

参考文章

TCP协议灵魂之问,巩固你的网路底层基础

后记

本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个赞~~