网络基础之UDP和TCP

857 阅读19分钟

1 UDP

    UDPUser DataPram Protocol的简称,中文名是用户数据报协议,是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。

1.1 面向无连接

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

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

    首先不可靠性体现在无连接上,通信都不需要连接,想发就发,并且收到什么数据就传递什么数据,也不会备份数据,发送数据方也不关心接收方是否已经正确接收到数据了。     再者网络环境不稳定,但是UDP因为没有阻塞控制,一直会以一定的数据发送数据,即使网络不好,也不会对发送速率进行调整。这样在网络情况不好的时候,就会导致丢包。但是优势也是明显的,对于一些实时性要求较高的场景(网络游戏,直播视频,电话会议)就需要使用UDP而不是TCP

1.3 高效性

    虽然UDP协议不是那么的可靠,但是正是因为它不那么可靠,所以也没有TCP那么复杂,也不用保证数据准确且有序到达。     因为UDP的头部开销比较小,只有8个字节,相对TCP的至少二十字节要小得多,在传输数据报文的是很高效的。

  • Source Port:16位数据源端口,在需要对方回信时选用,不需要时可用全0
  • Destination Port: 16位数据接收方端口号,在终点交付报文时必须用到。
  • Length:数据报和首部的长度,最小值为8Byte(数据报为空)
  • Check Sum:整个数据报文的检验和(IPv4 可选 字段),检测UDP数据报在传输中是否有错,有错就丢弃。该字段时可选的,当源主机不想计算校验和,则直接令该字段为全0。当传输层IP层收到UDP数据报时,就根据首部中的目的端口,把UDP数据报通过相应的端口,上交给进程。如果接收方UDP发现收到的报文中目的端口号不正确(即不存在对应端口号的应用进程),就丢弃该报文,并由ICMP发送“端口不可达”差错报文交给发送方。
/*UDP头定义,共8个字节*/
typedef struct _UDP_HEADER 
{
 unsigned short m_usSourPort; // 源端口号16bit
 unsigned short m_usDestPort; // 目的端口号16bit
 unsigned short m_usLength;   // 数据包长度16bit
 unsigned short m_usCheckSum; // 校验和16bit
}__attribute__((packed))UDP_HEADER, *PUDP_HEADER;
1.4 传输方式

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

1.5 适合使用的场景

    UDP 虽然对比 TCP 有很多缺点,但是正是因为这些缺点造就了它高效的特性,在很多实时性要求高的地方都可以看到 UDP 的身影。比如说直播,游戏,网络会议。

2 TCP

    TCPTransmission Control Protocol传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通讯协议。

2.1 头部

    从这个图上我们就可以发现 TCP 头部比 UDP 头部复杂的多。

对于TCP头部来说,以下几个字段是很重要的

  • Source Port:源端口,16位
  • Destination Port:目的端口,16位
  • Sequence number:是发送数据报的第一个字节的序列号,32位,这个序列号保证了TCP传输的数据是有序的,对端可以通过序列号顺序拼接数据。
  • Acknowledgement number:是确认序列号,32位,这个序号标识数据接收端期望接收的下一个字节的编号是多少,同时也表示上一个序号的数据已经收到。
  • Window Size:标识接收缓存区的空闲空间,16位,用来告诉TCP连接对端自己能够接收的最大数据长度。
  • Checksum:校验和,16位,对整个TCP数据段,TCP头部和TCP数据进行校验和计算,并由目标端进行验证。
  • Urgent Pointer:是紧急指针,16位,只有URG标志位被设置时该字段才有意义,表示紧急数据相对序列号(Sequence Number字段的值)的偏移。
  • 标识符
    • URG=1:该字段为1表示本数据报的数据部分包含紧急信息,是一个高级优先的数据报文,此时紧急指针(Urgent pointer)有效。紧急数据一定位于当前数据报数据部分的最前面,紧急指针表明了紧急数据的尾部。
    • ACK=1:该字段为1表示确认号(ACknowledgement number)字段有效,此外,TCP还规定在连接后传送的所有报文段都需要把ACK设为1
    • PSH=1:该字段为1表示接收端应该立即将数据push给应用,而不是等到缓存区满后再提交。
    • RST=1:该字段为1表示当前TCP连接出现严重问题,可能需要重新建立连接,也可以用于拒绝非法报文段和拒绝连接请求。
    • SYN=1:当SYN=1,ACK=0时表示当前报文段是一个连接请求报文。当SYN=1,ACK=1时,表示当前报文段是一个同意建立连接的应答报文。
    • FIN=1:该字段为1表示此段报文段是一个释放连接的请求报文。
2.2 状态机

    TCP 的状态机是很复杂的,并且与建立断开连接时的握手息息相关,接下来就来详细描述下两种握手

2.2.1 建立连接三次握手

    TCP是一个全双工协议,当连接建立完成后接收端和发送端都可以发送和接收数据。起初,两端都是CLOSED状态。在开始通讯前都会创建TCB。创建完成接收端处于LISTEN状态,此时等待发送端发送数据。

  • 第一次握手:接收端向发送端发送连接请求报文段(SYN=1,seq=x)。请求发送后发送端进入SYN-SENT状态。
  • 第二次握手:发送端收到连接请求报文段后,若同意连接,会发送一个应答(SYN=1,ACK=1,seq=y,ack=x+1),发送完成后进入SYN-RECEIVED
  • 第三次握手:当接收端接收到同意连接的应答后,还要向发送端发送一个确认报文(ACK=1,seq=x+1,ack=y+1)。发完后进入ESTABLISHED状态,发送端收到应答后也进入ESTABLISHED状态,此时连接成功,双向可以发送数据。

PS:第三次握手可以包含数据,通过快速打开(TFO)技术可以实现

PS:为什么 TCP 建立连接需要三次握手,明明两次就可以建立起连接?
因为为了防止出现失效的连接请求报文段被服务端接收的情况,从而发生错误。
可以想象一下,如果发送端发送了一个连接请求A,但是因为网络原因造成了超时,这时TCP会启动超时重传机制再次发送一个连接请求B,此时请求顺利到达接收端,接收端应答完就建立了请求,然后发送完数据后断开连接。最后请求A在两端关闭完了到达了接收端,那么此时的接收端会认为又要建立连接,从而应答了请求并进入ESTABLISHED状态。但是客户端其实是CLOSED状态,那么就会导致服务端一直等待,造成资源浪费。

PS:在建立连接中,任意一段掉线,TCP都会重新发SYN包,一般会重试五次,在建立中可能会遇到SYN Flood。遇到这种情况可以选择调低重试次数或干脆在不能处理的情况拒绝请求。

PS:为什么不是四次?
三次握手的目的是确认双方发送和接收的能力,100 次都可以。但为了解决问题,三次就足够了,再多用处就不大了。

2.2.2 断开连接四次握手

因为TCP是全双工的,在断开连接的时候接收端和发送端都需要发送FINACK

  • 第一次握手:若接收端认为数据发送完成,则向发送端发送释放连接(FIN=1,seq=u)的请求,并进入FIN-WAIT-1
  • 第二次握手:发送端收到连接释放请求后,会告诉应用层要释放TCP连接。然后会发送ACK包,并进入CLOSE-WAIT状态,此时表明接收端到发送端连接已经释放了,不再接收接收端发的数据了。但是因为TCP是双向连接的,所以发送端仍然可以发送数据给接收端。
  • 第三次握手:发送端若此时还没有发送完数据则会继续发送数据,完毕后会向接收端发送连接释放请求(FIN=1,ACK=1,seq=w,ack=u+1),然后发送端进入LAST-ACK。通过延迟确认的技术,可以将第二次和第三次的握手合并,延迟ACK包的发送(通常有时间限制,否则双方会误认为需要重传)。
  • 第四次握手:接收端收到释放请求,向发送端确认应答(ACK=1,seq=u+1,ack=w+1),此时接收端进入TIME-WAIT状态。该状态会持续2*MSLMaximum Segment Lifetime,最大生存期,指报文在网络中生存的时间,超时会被抛弃。TCP允许不同的实现可以设置不同的MSL值)时间,若该时间内发送端没有重发请求的话,就进入CLOSE状态。当发送端接收到请求后,便也进入到CLOSE状态。

PS:为什么接收端要进入TIME-WAIT等待2MSL时间CLOSE
保证接收端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失或超时,发送端没有收到回应,于是服务器会重新发送一次,而接收端就能在2MSL时间内接收到重传的报文,接着给出回应报文,并且会重启2MSL计时器,防止‘已失效的连接请求出现在本报文中’。若接收端发完确认应答后直接进入 CLOSED 状态,如果确认应答因为网络问题一直没有到达,那么会造成 发送端 不能正常关闭

PS:为什么建立连接是三次握手,关闭连接确是四次挥手呢?
建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。

2.3 确认应答机制(ACK机制)

TCP将每个字节的数据都进行了编号, 即为序列号。 每一个ack都带有对应的确认序列号,意思是告诉发送者,我已经收到哪些数据;下次你要从哪里开始发。

比如, 客户端向服务器发送了1005字节的数据,服务器返回给客户端的确认序号是1003, 那么说明服务器只收到了1-1002的数据。1003,1004,1005都没收到.此时客户端就会从1003开始重发。

2.4 超时重传机制(ARQ)

通过确认和超时机制保证了数据的正确送达,ARQ 协议包含停止等待 ARQ 和连续 ARQ 两种协议。

2.4.1 停止等待ARQ
2.4.1.1 正常的传输过程

只要 A 向 B 发送一段报文,都要停止停止发送并启动一个定时器,等待对端回应,在定时器时间内收到对端应答就取消定时器发送下一段报文。

2.4.1.2 报文丢失或出错

在报文传输过程可能出现丢包的情况,这时超过定时器的时间就会再次发送丢失的数据到对端响应,所以需要每次都要备份发送的数据。即使保温正常的被传输到对端,也可能出现在传输过程中报文出错的问题。这时候会对端抛弃该报文并等待 A 重传。

PS:、、、、、、、、、

2.4.1.3 ACK 超时或丢失

对端传输的应答也可能出现丢失或超时的情况。那么超过定时器时间 A 端照样会重传报文。这时候 B 端收到相同序号的报文会丢弃该报文并重传应答,直到 A 端发送下一个序号的报文。 在超时的情况下也可能出现应答很迟到达,这时 A 端会判断该序号是否已经接收过,如果接收过只需要丢弃应答即可。

从上面的描述中大家肯定可以发现这肯定不是一个高效的方式。假设在良好的网络环境中,每次发送数据都需要等待片刻肯定是不能接受的。那么既然我们不能接受这个不那么高效的协议,就来继续学习相对高效的协议吧。

2.4.2 连续ARQ

在连续ARQ中,发送端拥有一个发送窗口,可以在没有收到应答的情况下持续发送窗口内的数据,这样相比停止等待ARQ来说减少了等待时间,提高了效率。

连续ARQ中,接收端会持续不断受到报文。如果和停止等待ARQ中接收到一个报文就发送一个应答一样,就太浪费资源了。通过累计确认,可以在收到多个报文后统一回复一个应答报文。在报文中的ACK标志位可以用来告诉发送端这个序号之前的数据已经全部接收到了,下次请求发送这个序号后的数据。

弊端是,在连续接收报文时,可能会遇到后发的数据比先发的数据优先收到(如接收到序号7报文后,并未接收到序号8报文,而是接收到序号9报文)。这种情况,ACK只能回复8,这样就会造成发送端重复发送数据的情况。

2.5 滑动窗口

连续ARQ中讲到了发送窗口。在 TCP 中,两端其实都维护着窗口:分别为发送端窗口和接收端窗口

发送端窗口包含已发送但未收到应答的数据和可以发送但是未发送的数据。

发送端窗口是由接收窗口剩余大小决定的。接收方会把当前接收窗口的剩余大小写入应答报文,发送端收到应答后根据该值和当前网络拥塞情况设置发送窗口的大小,所以发送窗口的大小是不断变化的。 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答,只有ACK确认应答过的数据,才能从缓冲区删掉。当发送端接收到应答报文后,会随之将窗口进行滑动。

滑动窗口是一个很重要的概念,它帮助 TCP 实现了流量控制的功能。接收方通过报文告知发送方还可以发送多少数据,从而保证接收方能够来得及接收数据,防止出现接收方带宽已满,但是发送方还一直发送数据的情况。

2.5.1 Zero 窗口

在发送报文的过程中,可能会遇到对端出现零窗口的情况。在该情况下,发送端会停止发送数据,需要定期发送一个窗口探测数据段,让对端告知窗口大小。在重试次数超过一定次数后,可能会中断 TCP 链接。

2.6 流量控制

接收端处理数据的速度是有限的。如果发送端发的太快,导致接收端的缓冲区被填满,这个时候如果发送端继续发送,就会造成丢包,进而引起丢包重传等一系列连锁反应。因此TCP支持根据接收端的处理能力,来决定发送端的发送速度。这个机制就叫做 流量控制Flow Control)。

接收端将自己可以接收的缓冲区大小放入到TCP首部中window size字段,通过ACK通知发送端;窗口大小越大,说明网络的吞吐量越高;接收端一旦发现自己的缓存区快满了,就会将窗口大小设置成一个更小的值通知给发送端;发送端接收到这个窗口大小的通知后,就会减小发送窗口,减慢自己的发送速度;如果缓存区满了,就会设置窗口置为0,发送方将不再发送数据,而是定期发送一个窗口探测数据段,让接收端把窗口大小告诉自己再发送数据;

PS:那么接收端如何把窗口大小告诉发送端呢?
首先在TCP首部有一个16位窗口大小字段存放了窗口大小信息(最大值2 ** 16 === 65536),实际窗口大小是该窗口大小字段的值左移窗口扩大因子M位(TCP首部40字节选项中的窗口扩大因子M)

2.7 拥塞控制

虽然TCP拥有了滑动窗口这个大杀器,能够高效可靠地发送大量数据,但是如果一开始就发送大量数据,仍然可能引发一些问题,因为网络上有很多计算机可能的网络状态比较拥堵,在不清楚网络状态的情况下,贸然发送大量的数据,很可能雪上加霜。因此TCP引入了慢启动的机制,先发送少量的数据,探探路,摸清楚了当前网络拥堵状态后,再决定按多大的速度传输数据。

  • 在发送开始的时候,定义拥塞窗口大小为 1
  • 每次收到一个ACK应答,拥塞窗口 > +1(左移加一);
  • 每次发送数据包的时候,将拥塞窗口和发送端返回的TCP首部的窗口大小进行比较取最小值作为实际发送的窗口。

向上面这样的拥塞窗口增长速度是指数级,为了不增长的那么快,此处引入一个慢启动的阈值,当拥塞窗口大小超过这个阈值的时候,就会按线性的方式增长

  • 当TCP开始启动的时候, 慢启动阈值等于窗口最大值
  • 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1

少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为是网络拥塞; 当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降.

拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。

2.8 延迟应答

如果接收数据的主机立即返回ACK应答,这时候返回的窗口可能比较小。假设接收端缓冲区为1M,一次收到了500K的数据;如果立刻应答,返回的窗口大小就是500K,但实际上可能处理端处理的速度很快,10ms之内就把500K数据从缓冲区消费掉了,在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来,如果接收端稍微等一会儿再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M。窗口越大, 网络吞吐量就越大, 传输效率就越高,TCP的目标是在保证网络不拥堵的情况下尽量提高传输效率。

有一些场景是不能延迟确认的,收到了就要马上回复

  • 接收到了大于一个 frame 的报文,且需要调整窗口大小
  • TCP 处于 quickack 模式(通过tcp_in_quickack_mode设置)
  • 发现了乱序包
  • 数量限制: 每隔N个包就应答一次
  • 时间限制: 超过最大延迟时间(一般200ms)就应答一次

参考文章

IP头、TCP头、UDP头详解以及定义

TCP 详解