浅谈 TCP & UDP

什么是三握四挥,TCP如何保证可靠性,粘包分包

Posted by SWZ on August 7, 2019

前言

TCP和UDP是TCP/IP协议簇里位于传输层的两个重要协议。它们就如同快递公司,有的奉行低价优先,有的奉行服务至上,但它们的目的都是将数据发送给接收方,TCP慢却稳,UDP快但可能丢包。至于孰优孰劣,得看具体应用场景。


TCP(Transmission Control Protocol,传输控制协议)

TCP是一种面向连接可靠的、基于字节流的传输层通信协议。每一条TCP连接只能有两个端点(endpoint),点对点(一对一)。TCP还提供了全双工通信服务,即可同时收和发,作用就是:A和B打电话,A一直在讲话,如果B不回点什么的话A可能觉得线路断了,所以B也得时不时“嗯”一下保持反馈。

如何保证传输可靠性

  1. 首先,采用三次握手来建立TCP连接,四次握手来释放TCP连接,从而保证建立的传输信道是可靠的。

  2. 其次,确认和重传机制,接收方收到报文就会确认,发送方发送一段时间后没有收到确认就重传。TCP采用了连续ARQ协议(Automatic Repeat-reQuest)自动重传请求,来保证数据传输的正确性,使用滑动窗口协议来保证接方能够及时处理所接收到的数据,当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失,进行流量控制。

  3. 最后,TCP使用慢开始、拥塞避免、快重传和快恢复来进行拥塞控制,避免网络拥塞。

三次握手

  1. 其中TCP客户端主动发起TCP连接建立,握手需要在TCP客户端与服务器之间交换三个TCP报文段。首先TCP客户端向TCP服务器发送TCP连接报文段,并进入同步已发送状态(SYN-SENT)。TCP连接请求报文段首部中的同步位SYN被设置为1,表明这是一个TCP连接请求报文段。序号字段seq被设置了一个初始值x,作为TCP客户端所选择的初始序号(注意:TCP规定SYN被设置为1的报文段不能携带数据,但要消耗掉一个序号)。

  2. TCP服务器收到TCP请求报文段后,如果同意建立连接则向客户端发送TCP连接请求确认报文段,并进入同步已接收状态(SYN-RCVD)。该报文段首部中的同步位SYN确认位ACK都设置为1,表明这是一个TCP连接请求确认报文段。序号字段seq被设置了一个初始值y,作为TCP服务器所选择的初始序号,确认号字段ack被设置成了x+1,这是对TCP客户端所选择的初始序号的确认(注意:这个报文段也不能携带数据,因为它是SYN被设置为1的报文段,但同样需要消耗掉一个序号)。

  3. TCP客户端在收到TCP连接请求确认报文段后,还要向TCP服务器发送一个普通的TCP确认报文段,并进入连接已建立状态(ESTABLISHED)。该报文段首部中的确认位ACK被设置为1,表明这是一个普通的TCP确认报文段。序号字段seq被设置为x+1,这是因为TCP客户端发送的第一个TCP报文段的序号为x,并且不携带数据,因此第二个报文段的序号为x+1(注意:TCP规定,普通的TCP确认报文段可以携带数据,如果不携带数据,则不消耗序号)。确认号字段ack被设置为y+1,这是对TCP服务器所选择的初始序号的确认。

  4. TCP服务器收到该确认报文段后进入连接已建立状态(ESTABLISHED)

序号:用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。

确认号:期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。

数据偏移:指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。

控制位:八位从左到右分别是 CWR,ECE,URG,ACK,PSH,RST,SYN,FIN。

CWR:CWR 标志与后面的 ECE 标志都用于 IP 首部的 ECN 字段,ECE 标志为 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 包。

窗口:窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。

问题:两次握手可以么,三次是否多余? 三握不多余,这是为了防止已经失效的连接请求报文段突然又传到了TCP服务器,因而导致错误。

问题:为什么不四次握手? 完全可靠的通信协议是根本不存在的,我们任何的通信协议都是在接受这样的现实情况之上进行的。 三次握手后,A 和 B 至少可以确认之前的通信情况,但无法确认之后的情况。在这个道理上说,无论是四次还是五次或是更多次都是徒劳的。

问题:三次握手真正的原因是什么? 为了确认双方都知道 “对方愿意建立连接并且对方知道自己愿意建立连接“的这个事实。

四次挥手

  1. TCP客户端会发送TCP连接释放报文段并进入终止等待1状态(FIN-WAIT-1),该报文段中的终止位FIN确认位ACK的值都被设为1,表明这是一个TCP释放报文段,序号seq字段的值设置为u,它等于客户端已传送数据的最后一个字节的序号加1(注意TCP规定终止位FIN为1的报文段即使不携带数据也要消耗掉一个序号)。确认号ack字段的值设置为v,它等于客户端已收到数据的最后一个字节的序号加1。

  2. TCP服务端在收到TCP连接释放报文段后会发送一个普通的TCP确认报文段并进入关闭等待状态(CLOSE-WAIT)。该报文段首部中的确认位ACK的值被设置为1,表明这是一个普通的TCP确认报文段,序号seq字段的值设置为v,它等于TCP服务器已传送过的数据的最后一个字节的序号加1,这也与之前收到的TCP连接释放报文段中的确认号所匹配。确认号ack字段的值设置为u+1,这是对TCP连接释放报文段的确认。此时,从TCP客户端到TCP服务端这个方向的连接就释放了,这时的TCP连接处于半关闭状态,但TCP服务端如果还有数据要发送TCP客户端仍要接收,也就是说从TCP服务端到TCP客户端这个方向的连接并未关闭。

  3. TCP客户端收到TCP确认报文段后就进入终止等待2状态(FIN-WAIT-2),等待TCP服务器发出的连接释放报文段。

  4. 若TCP服务器已经没有数据要发送了,则向TCP客户端发送TCP连接释放报文段并进入最后确认状态(LAST-ACK)。该报文段首部中的终止位FIN确认位ACK的值都设置为1,表明这是一个TCP连接释放报文段,序号seq字段为w,确认号ack字段的值为u+1,这是对 之前收到的TCP连接释放报文段的重复确认。

  5. TCP客户端收到TCP连接释放报文段后,必须针对该报文段发送普通的TCP确认报文段,之后进入时间等待状态(TIME-WAIT),在经过2MSL(240s)才会进入关闭状态(CLOSED),该报文段首部的确认位ACK的值被设为1,表明这是一个普通的TCP确认报文段,序号seq字段的值被设置为u+1,这是因为TCP客户端之前发送的TCP连接释放报文段虽然不携带数据,但也要消耗掉一个序号。确认号ack字段的值设置为w+1,这是对所收到的TCP连接释放报文段的确认。

  6. TCP服务器收到该报文段后就进入关闭状态(CLOSED)

处于TIME-WAIT状态的客户端必须等待2MSL时间后,才会进入CLOSED状态。MSL(Maximum Segment Lifetime)最长报文段寿命,RFC 793 建议设为两分钟,对于现在的网络,MSL= 2分钟可能太长了一些,我们可根据具体情况使用更小的MSL值。

问题:为什么要经过2MSL后才进入关闭状态? 首先,可以确保TCP服务器收到最后一个TCP确认报文段而进入关闭状态(假定是客户端先发起的连接释放请求);再者,可以使本次连接持续时间内所产生的所有报文段都从网络中消失,这样就可以使下一个新的TCP连接中不会出现旧连接中的报文段。

详细说明:

TCP服务端在发送连接释放报文段后进入最后确认状态,TCP客户端收到该报文段后发送普通的TCP确认报文段,并进入关闭状态。但TCP确认报文段丢失了,这必然会造成TCP服务器对之前发送的TCP连接释放报文段的超时重传,并仍处于最后确认状态。由于TCP服务器向TCP客户端方向的通信已经关闭,因此不会接收该报文段。这就会造成TCP服务器反复重传TCP连接释放报文段,并一直处于最后确认状态而无法进入关闭状态。

滑动窗口

发送端希望在收到确认前,继续发送其它报文段。比如说在收到0号报文的确认前还发出了1-3号的报文,这样提高了信道的利用率。但可以想想,0-4发出去后可能要重传,所以需要一个缓冲区维护这些报文,所以就有了窗口。

窗口是缓存的一部分,用来暂时存放字节流。发送方(swnd)和接收方(rwnd)各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。

发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。

接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。

流量控制

流量控制是为了控制发送方发送速率,保证接收方来得及接收。

接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。

实际上,为了避免此问题的产生,发送端主机会时不时的发送一个叫做窗口探测的数据段,此数据段仅包含一个字节来获取最新的窗口大小信息。

拥塞控制

如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。

TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。

发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口(swnd)的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口 swnd=min(rwnd,cwnd)。为了便于讨论,做如下假设:1. 接收方有足够大的接收缓存,因此不会发生流量控制;2. 虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。

慢开始与拥塞避免

发送的最初执行慢开始,令 cwnd = 1,发送方只能发送 1 个报文段;当收到确认后,将 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。

粘包 & 拆包

什么是粘包和拆包

正常情况会接收到两个完整的报文。但也有可能出现,接收到的是一个报文,它是由发送的两个报文组成的,这样对于应用程序来说就很难处理了(粘包)。还有可能出现,虽然收到了两个包,但是里面的内容却是互相包含,对于应用来说依然无法解析(拆包)。

为什么会发生粘包和拆包

  • 要发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包。
  • 待发送数据大于 MSS(Maximum Segment Size)最大报文长度,TCP 在传输前将进行拆包。
  • 要发送的数据小于 TCP 发送缓冲区的大小,TCP 将多次写入缓冲区的数据一次发送出去,将会发生粘包。
  • 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。

解决方案

由于 TCP 本身是面向字节流的,无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的。只能由我们自己在业务逻辑中去处理。以下给出了常用的三种方法:

  • 消息定长:发送端将每个数据包封装为固定长度(不够的可以通过补 0 填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
  • 长度信息法:在每个数据包前面加上长度信息。每次接收到数据后,先读取表示长度的字节,如果缓冲区的数据长度大于要取的字节数,则取出相应的字节,否则等待下一次数据接收。
  • 结束符号法:规定一个结束符号,作为消息间的分隔符。

UDP(User Datagram Protocol,用户数据报协议)

UDP是一种无连接的、不可靠的、但传输效率较高的通信协议。UDP 不止支持一对一的传输方式,有单播,多播,广播的功能。

面向无连接

首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。

具体来说就是:

  • 在发送端,应用层将数据传递给传输层的 UDP 协议,UDP 只会给数据增加一个 UDP 头标识下是 UDP 协议,然后就传递给网络层了。
  • 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作。

面向报文

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

UDP 头部

  • 两个十六位的端口号,分别为源端口(可选字段)和目标端口。
  • 整个数据报文的长度。
  • 整个数据报文的检验和(IPv4 可选 字段),该字段用于发现头部信息和数据中的错误。

因此 UDP 的头部开销小,只有八字节,相比 TCP 的至少二十字节要少得多,在传输数据报文时是很高效的。


总结

  UDP TCP
是否连接 无连接 面向连接
是否可靠 不可靠传输,不使用流量控制和拥塞控制,不保证数据顺序 可靠传输,使用流量控制和拥塞控制,保证数据顺序
连接对象个数 支持一对一,一对多,多对一和多对多交互通信 只能是一对一通信
传输方式 面向报文 面向字节流
首部开销 首部开销小,仅8字节 首部最小20字节,最大60字节
适用场景 适用于实时应用(IP电话、视频会议、直播等) 适用于要求可靠传输的应用,例如文件传输