网络原理-UDP和TCP

1 UDP的协议报文格式

header有四个字段,每个字段固定两个字节,所以一共8个字节,64个bit位。端口号是2个字节,16bit位,那么一个端口号的取值范围就是 0->65535。但是在实际开发中,一般把1024以下的端口保留,所以写代码时用1024->65535这个范围。服务器的端口是程序员指定的,而客户端的窗口是系统自动分配的空闲端口

HTTP的报头是文本格式的,UDP/TCP/IP 报头是二进制的

值得注意的是,长度是整个数据报的长度(报头+载荷),长度属性也是2个字节,表示范围是0->65535,也就是64kb。64kb,对于现在来说,是非常小的数字,随便一个图片就是几个MB

那如何传输一个大的数据呢?

这里有两个方案:

1)应用层代码做拆包操作:

一个大的应用层数据包,拆成多个小的包,使用多个UDP数据报传输。但是这种做法的工作量是比较大的,需要写大量的逻辑来实现此处的分包组包功能,并且需要进行复杂的验证

2)使用TCP协议,它没有数据包长度的限制(选这个)

接下来,再来谈谈校验和

校验和是验证数据是否发生修改的手段。我们知道,HTTP的数字签名是为了防止黑客篡改(防人)。对于UDP来说,它的校验和不是为了防人,和安全性无关,而是防止出现传输过程中的"比特翻转"。

流程:发送之前会先计算一个校验和,会把整个数据包的数据都代入。然后,把数据 和 校验和 一起发送给对端。接收方收到之后会重新计算一下校验和,和收到的校验和对比,如果发现不一致,就会直接丢弃。

UDP的校验和使用了CRC方式来进行校验(循环冗余校验),会把每个字节(除了校验和位置的部分),都当做整数进行累加,溢出也没关系,会继续加,最终得到结果,crc校验和

传输到对端,如果数据出现错误了,对端再次计算的校验和,就会和第一个校验和不一样了

我们会认为:两个原始数据相同,还使用相同的校验和算法,得到的校验和也是相同的。反之,如果两个校验和相同,原始数据不一定相同。

2 TCP协议

我们知道 TCP是 有连接,可靠传输,面向字节流,全双工的

2.1 简单了解一下TCP协议报文格式

这里只是把熟悉的部分介绍一下,剩下的会随着内容逐渐展开

16位源端口号和16位目的端口号:是传输层的核心内容,已经很熟悉了,就不解释了

4位首都长度:表⽰该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部最⼤⻓度是15 *
4 = 60。选项的存在,导致TCP的报头长度是可变的,最多是40
保留(6位):对于UDP来说,它的长度不够,不能进行扩展。TCP就考虑了这样的问题,在TCP报头中就预留了一些"保留位"(现在先不用,只是占个位置)
16位校验和:用来检验数据是否出现错误
:TCP最核心的6个标志位。后面会介绍

2.2 TCP的核心机制

2.2.1 核心机制1:确认应答

我们知道TCP有可靠性这样的特性。这里的可靠性不是说 A给B发个消息,B 100% 能收到,而是A 给B发了消息之后,尽可能的让B收到。

那A怎么能够知道B收到了呢?

那就是 确认应答

保证可靠性的一个关键前提:是发送方知道自己的数据是否被接收方收到。需要给对方返回一个应答报文(acknowledge,ack),发送方知道应答报文,就可以确认对方收到了

举个生活中的例子:

我收到 "好啊好啊"(应答报文),就知道对方收到了。

但是,有个明显的缺陷。如果我连续发多条,可能会出现问题

比如:

此时,我还是能正确理解的。但是,在网络上存在一个很神奇的操作,那就是"后发先至"

比如:

这样就出现问题了。

针对后发先至,TCP给出的处理方案:给传输的数据,进行编号

比如:

这就涉及到了TCP报文格式中的确认序号只在应答报文中 ,以及ack为1,表示这个是应答报文

值得注意的是,此处所涉及到的应答报文和业务是无关的,不管回答啥,都表示收到了数据

比如:

接下来看看TCP具体是怎样做的

TCP是面向字节流的,所以在编号的时候是按照 字节 来编号的。每个字节都分配一个编号,编号是连续递增的。

一个TCP的载荷是由多个字节构成的,就意味着多个编号,此处的序号是写哪个序号呢?

序号字段 填写载荷部分的第一个字节的序号,序号是连续递增的

序号是保证应用程序read数据的先后顺序,不是数据到达对方的顺序

那确认序号呢?

确认序号的填法是 把收到的数据,载荷的最后一个字节序号 +1,填写到确认序号中

序号和确认序号都是针对载荷的

比如:

确认序号的含义是:

1)<1001的数据都已经确认收到了

2)接下来你要从1001开始给我发送

引入序号之后,接收方就可以根据序号对数据进行排序

TCP需要处理后发先至的情况,确保应用程序通过socket api读到的数据顺序(即使出现后发先至的情况,tcp也给我处理掉,确保代码里读到的数据和发送方写入的数据顺序一致)是正确的,这就需要接收方的数据排序了。

TCP在接收方这里会安排"接收缓冲区"(内存,操作系统内核里),通过网卡读到的数据,先放到接收缓冲区中,后续代码调用read,也是从接收缓冲区来读的(生产者消费者模型)。根据序号来排序,序号小的在前面,大的在后面,确保前面的数据已经到了,然后read才能阻塞。如果是后面的数据先到,read继续阻塞,不会读取到数据

2.2.2 核心机制2:超时重传

TCP最核心的一点是保证可靠传输,核心机制1完成了一部分的小目标,对方收到数据了就可靠,没收到就要靠超时重传了。

所以说超时重传是针对丢包的情况做出处理

那为什么会丢包呢?

网络结构是非常复杂的。数据报经过某个路由器/交换机转发的时候,该路由器/交换机已经非常繁忙了,导致当前需要转发的数据量超过路由器/交换机的转发能力的上限。数据报就会消耗更多的时间,才能到达对方。更糟糕的情况是数据报太多太多了,路由器/交换机根本处理不过来,接收缓冲区都满了,那就只能丢弃了(丢弃最新的数据,网络上的数据报都是有时效性的)

所以说,丢包是不能避免的客观现象。重传就是有效的对抗丢包的手段。假设当前网络丢包的概率是10%,90%的概率能到达对方,重传,连续发两个包都丢的概率是 1%,至少有一个报到达对方的概率就是 99%。重传次数的增加,数据报到达对方的概率就会增加

丢包的两种情况:

达到等待时间的上限,还没有收到ack,A就认为传输中发生丢包了

1)A -> B 发的数据丢了

2)B -> A 返回的ack丢了

那怎么来判定是否丢包了呢?

引入超时时间,TCP中,判定的超时的时间阈值,不是固定的,而是动态改变的。

假设当前 A->B发送数据,丢包的超时时间阈值为T,当A给B传输发生超时之后,就会延长这个时间阈值,但是不是无休止的,超时次数达到一定程度/等待时间达到一定程度,就会放弃这一次传输。随着重传,导致数据到达对方的概率越来越高。如果重传还不成功,意味着当前丢包的概率是一个非常大的数值,网络上大概率已经出现严重故障了,此时,就算继续重传,意义也不大了。

对于发送方A来说,它区分不了当前是上面两种情况中的哪一种,就会进行重传

这个可以

B已经有了1-1000这个数据了,再重传B就收到了两份同样的数据,如果TCP不处理,可能会使应用层读到两次一样的数据,这个肯定是不行的。比如 扣款,扣完一次,还想再扣一次???

tcp会在内部进行去重操作,可以根据序号再缓冲区中找一下,如果存在,就直接丢弃,如果不存在,才放进去

以上说的两种核心机制:确认应答+超时重传,保证了TCP能够进行可靠传输

2.2.3 核心机制3:连接管理

连接管理分为两个部分:一个是建立连接,另一个是断开连接

这里的连接指的是 抽象的,逻辑上的连接,通信双方各自保存对端的信息

我们先来谈谈建立连接

1)建立连接

TCP是通过"三次握手 "的方式来完成的。握手操作,没有实际的业务,只是"打个招呼"。我发送一个不携带业务的数据,通过这个数据和对方"打个招呼"

比如:

A会给B先发送一个数据包

记住一定是客户端主动发起syn

synchronized 同步。在TCP中,指的是 数据上的同步。A告诉B,接下来我要和你建立连接,就需要你把我的关键信息保存好,同时你也把你的信息同步发给我。

,syn为1表示同步报文

看到这,可能会有一个疑问:三次握手,上面是四次呀???

其实,其中有两次,是可以合并的

为什么合并呢?

网络传输过程是要能够进行封装和分用的,合并操作是能够提高传输的效率的。syn和ack本身就可以合并,本身就没有载荷数据,同时只需要报头的两个标志位(ack和syn)表示,所以把两个标志位设为1,就是合并的效果了.

还有一个更全的:

红框的部分就是"三次握手",这张图里面比上面的图多了TCP的状态变化以及对应的socket api(图上的是系统原生api,这里就不讨论了)情况。

接下来聊聊TCP的状态:

CLOSED:不存在的状态,tcp连接还没有呢

LISTEN:启动服务器,new ServerSocket的时候,就会进入(服务器准备好了,随时有客户端可以上来了)

ESTABLISHED:客户端和服务器已经建立了连接了,接下来就可以传输业务数据了

SYN_SENT和SYN_RCVD:正常情况下,肉眼看不到这俩状态

已经知道了三次握手的基本流程。,那为啥要三次握手呢?它有什么用呢?解决了什么问题呢?

1)三次握手,相当于"投石问路"

先初步的探一探网络的通信链路是否通畅(网络通畅是可靠传输的前提条件)

2)验证通信双方的发送能力和接受能力是否也正常

3)三次握手过程中,可以协商一些关键信息

TCP要协商的一个非常关键的信息:通信过程中,序号从几开始

初始序号,一般不是从0开始的,并且两次连接的初始序号都是不同的,往往差别很大

那为什么两次连接的初始序号是不同的呢?

比如:

第一次连接建立好之后,A给B发送数据包,但是其中有一个数据包,转发的时候迷路了,迟迟没有到达B。当第二次连接建立好之后,已经通信了好一段时间之后,刚才第一次连接迷路的数据包,才到达B。此时B不应该处理这个数据包,因为当前第二次连接,程序可能不是同一个。

接收方就需要把这个包处理掉,就可以通过序号来区分。比如,第一次连接,协商好,初始序号是1000000,第二次连接,协商好,初始序号是 8000000。收到的数据和当前连接的数据的序号相差甚远就可以认为这个数据包是第一次连接了的

建立连接聊完了,接下来聊聊断开连接吧

2)断开连接

通过"四次挥手 "来完成,和三次握手差不多,也是发送不携带载荷的数据

比如:

,FIN为1,表示结束报文

有一个问题:三次握手的中间两次能合并,四次挥手能合并吗???

有时候能(延时应答,这个之后会聊到),有时候不能(B的ACK是B的内核返回的,是立刻马上的,而FIN是B的应用程序(写的代码)里调用socket.close方法触发的,不是同一个交互时机)

三次握手:B的syn和ack都是内核负责的,和用户代码无关,可以保证是同一时机的

上面谈过,三次握手一定是客户端主动发起syn(第一次,一定是客户端)。在这里,四次挥手,客户端和服务器都可以主动发起FIN(看谁先调用close)。但是,从实践角度看,还是客户端断开连接的可能性更大。

更详细的图:

还是看看状态

TIME_WAIT:谁是主动发起 FIN 的一方,就会进入到TIME_WAIT

CLOSE_WAIT:谁是被动发起 FIN 的一方,就会进入CLOSE_WAIT,等待应用程序调用close方法。CLOSE_WAIT在正常开发中,应该是看不到的。原则上来说,感知到对方断开连接后,就应该尽快的执行close。如果你发现服务器这边存在大量的 CLOSE_WAIT而且持续的还很久,此时意味着你代码大概率有bug(检查是否执行到close了)。

网络传输中,随时会丢包。三次握手和四次挥手也毫无意外

比如:

如果是第一个FIN丢了,A就无法在规定时间内拿到ACK,于是A重新发送FIN。如果是第一个ACK丢了,同上。如果是第二个FIN丢了,B也再次重传。在第二个FIN到达A之前,A这里的连接肯定是存在的,就能够及时处理ACK。

假设出现这样的情况,如果B给A发FIN,A收到了,A收到之后,返回ACK,把连接直接释放了

A收到FIN之后,其实不能立即释放连接,而是要等一下对方是否可能重传FIN,最后一个ACK是可能会丢包的

那要等多久合适呢?

2*MSL(网络上任何两个节点传输过程中消耗的最大时间)(通常这个时间会配置成60s)。此处这个TIME_WAIT等待2min这个数值不要背,不同系统可能不一样,是可以修改的。超时重传的时间阈值是ms级别的。

三次握手只是在最开始建立连接的时候进行的。一旦连接建立好了,后续进行业务数据的通信就和三次握手没关系了

2.2.4 核心机制4:滑动窗口

能够提高效率

每传输一个数据,都需要等待ACK的时间

太慢了,怎么解决呢?

批量传输

前几个数据包,都是不等待,直接往后发。发到一定的(窗口大小:不需要等待,能够连续发送的最大数据量)之后,再去等待。用一份时间等多组ACK(把多组等待ACK时间重叠成一份了)。

那能否一直发呢?

肯定不行,就相当于没有可靠传输了

那下一组是怎么发的呢?

1)等这一组的所有ACK都回来,再发第二组

这么做,花的时间会更长 ❌️

2)收到一个ACK,就发下一条

✅️

滑动是一个形象的比喻,收到一个ack,就发下一个,收一个,发一个......

图解:

那如果是 2001 ack 比 1001 先到呢?

窗口直接往后走两个格子就可以了。确认序号的含义:该序号之前的数据,都确认收到了,也就是说,2001 ack能够涵盖 1001的含义。

我们知道窗口越大,批量发的数据越多,效率就越高。但是窗口不能无限大,太大会影响可靠性。

滑动窗口是在可靠传输的基础上 ,来提高效率(只是亡羊补牢,引入可靠性,会使效率产生折损,引入滑动窗口,是要让折损更小,效率不可能比UDP这种还高)

同样的,滑动窗口过程中,也会丢包,两种情况:

1)数据包已经抵达,ACK丢了

此时,不用做任何额外处理。后一个ACK会涵盖前一个ACK的含义

2)数据包直接丢了

1001-2000丢了

当A收到连续多个1001之后,就会意识到 1001-2001丢包了,A就会重传 1001-2000

1001-2000重传成功之后,此时的确认序号就是7001,因为刚才2001-7000这些数据都收到了,就差1001-2000了,通过重传,就把缺失的部分给补上了,补上之后,继续从7001往后传输就可以了

图:

以上操作,称为"快速重传",只是谁丢了,重传谁,其他已经收到的数据无需重传,整个重传的过程是很快的(滑动窗口下的,超时重传的变种操作)

那超时重传和快速重传这两个机制是矛盾的吗?

当然不是,是针对不同情况下的重传机制

超时重传:传输的数量少,没有构成滑动窗口批量传输的形式

快速重传:传输的数量多,形成滑动窗口

2.2.5 核心机制5:流量控制

刚才知道了,滑动窗口越大,效率越高。但是不能无限大,太大了会影响到可靠性

那为什么太大会影响到可靠性呢?

关键在于接收方的处理能力是有上限的。而这个是和你写的代码有关系的。应用程序需要从接收缓冲区中读取数据,读了一个字节,这个字节就可以从接收缓存区中删除了。如果发送方的发送速度大于应用程序的读取速度(看你的代码咋写的),逐渐变满,满了就会丢包。

值得注意的是,每个Socket对象,都会对应到一组接收缓冲区(和发送缓冲区)

流量控制就是给发送方踩刹车,让它发的慢点。它可以让接收方根据自身处理数据的速度,反馈给发送方来限制发送方发送的速度。在ack中,依赖一个特殊的属性,"窗口大小"

,接收方接收缓冲区的剩余空间大小填入到这个属性中,发送方就会按照这个数字来重新设定发送的窗口大小(滑动窗口的大小,是动态变化的)

64kb,是否滑动窗口大小的最大数值就是64kb呢?

当然不是,TCP选项中还包含了⼀个窗⼝扩⼤因⼦M, 实际窗⼝⼤⼩是 窗⼝字段的值左移 (指数增长)M 位。

图解:

2.2.6 核心机制6:拥塞控制

流量控制是依据接收方处理能力(根据接收缓冲区空余空间,来定量衡量),进行限制的,而拥塞控制是依据传输链路的转发能力来进行限制的。

如果不好具体衡量到某个设备,就可以把整个通信链路视为"整体",通过"做实验的方式"(通俗来讲:面多加水吗,水多加面)找到一个合适的窗口大小。

大体思想:先按照小的窗口(小的速度)先发着。如果发的时候,很顺利,不丢包,那就加大速度。如果出现丢包了,减小速度,又不丢包了,加大速度,又丢包了,继续减小速度......动态平衡。

流量控制和拥塞控制都能限制发送方的窗口大小,这两个值,哪个小,哪个说的算

具体策略:

这个图就表示了拥塞控制的工作流程:慢启动,指数增长,线性增长,丢包,窗口变成较小值......

2.2.7 核心机制7:延时应答

默认情况下,接收方都是在收到数据报的第一瞬间,就返回ack,但是可以通过延时返回ack的方式来提高效率

为什么延时能提高效率呢?

和流量控制是密切相关的

举个例子:

流量控制是通过接收缓冲区的剩余空间大小来去衡量发送窗口的大小

目的是返回一个更大的窗口。到底是多大得看接收方的应用程序的情况,应用程序会在能够处理的限度下,尽可能的增加窗口大小。

值得注意的是,延时返回也不是100%能提高效率,这主要是看应用程序消费的速度快不快。还有可能会变小,比如 延时时期内,接收方有收到了其他数据。

延时应答的具体方式:

在实践中,这两种方式是综合的。传输的数据密集,按第一个方式,传输的数据少,按第二个方式

2.2.8 核心机制8:捎带应答

TCP已经有了延时应答,基于延时应答,引入了"捎带应答",就是返回业务数据的时候,顺便把上次的ack给带回去

比如:

如果没有延时应答,返回ack的时机和返回响应的时机(ack是内核,响应是代码),就是不同的时机。上面这个图是不考虑延时应答的情况

引入了延时应答,ack就可以往后延时一定时间,恰好这个时候要返回响应数据,此时就可以把ack代入到响应中,来一起返回。对于ack来说,它需要做这些事:ack设为1,窗口大小设为接收缓冲区的剩余值,确认序号设为合适的值(都是在报头里设置的),设置这些内容不影响响应数据。把两个包合到一起,就能起到提高效率的作用

2.2.9 核心机制9:面向字节流

这涉及到粘包问题,粘的是应用层数据包。因为通过字节流方式传输,很容易混淆包和包之间的边界,从而接收方无法区分从哪里到哪里是一个完整的应用层数据包

比如:

这就导致接收方read的时候,就蒙了,不知道读多少个字节了:可以读出a,可以读出aa,可以读出aaa,可以读出aaab......

如果包读的不完整或者读多了,都可能使程序出现bug,主要是因为字节流传输太灵活了

上述问题,就称为粘包问题

这样的问题,在tcp的层次上是无解的,而需要在应用层解决:

定义好应用层协议,来明确包与包之间的边界 ,有两种方法:

1 约定包和包之间的分隔符(包的结束标记)

echonserver 采用的办法,约定\n作为结束标记

这回直接往后read,读到\n,就认为读完了一个数据包

2 约定包的长度

比如约定每个包开头几个字节,来表示数据包一共多长

先读4个字节,得到长度,再根据长度的值,决定接下来读多少

先读3,然后读aaa,这是一个完整的数据包。再读4,然后读bbbb,这也是一个完整的数据包......

如果是43aaa,后面应该就是43个a。长度写了43,实际a不足以43,相当于应用层代码有bug

那如果3是数据而不是标识呢?

数据包:

读的时候,先根据固定4个字节读到4,再根据4的值,再往后读4个字节

在HTTP中,两种方案都有体现:

1 GET请求,没有body,使用空行,作为结束标记

2 POST请求,有body的时候,通过Content_Length决定body多长

解决粘包问题,就是在自定义应用层协议的时候要考虑的问题

2.2.10 核心机制10:异常情况的处理

TCP在通信过程中存在特殊情况(四次挥手没挥完),如下:

1 某个进程满了

进程崩溃和主动退出没有本质区别,都会使进程释放 = > 回收文件描述符表的每个资源 => 调用socket的close(会触发FIN,触发四次挥手,进程虽然没了,但是TCP的连接信息还存在,此时四次挥手还是可以正常进行的)

TCP连接的释放时机,更晚

2 主机关机了

本质上会杀死所有的用户进程( 调用socket的close,和上面一样)。关机需要一定的时间,如果一定的时间内,四次挥手挥完了,就和正常一样了。如果没有挥完呢?

假设B的FIN来的太迟了,而A已经关机完成了,就意味着B的FIN不会有ACK。B就会重传FIN,重传几次之后,也没有ACK,就认为对端出现严重问题了。此时B就会主动放弃连接(B把保存的A的信息就删掉了)

3 主机掉电了

比如拔电源

1)接收方掉电

B后续发来的数据都没有ACK了。B就会触发超时重传,但是重传不能解决问题。重传达到一定次数,就会触发"重置TCP连接",B主动发送一个复位报文(从头开始,既往不咎)

,此处RST,仍没有ACK,B就只能单方面释放连接了

2)发送方掉电

B突然发现,A没有声音了。此时B区分不了 A是挂了,还是暂时休息一会。B就只能继续等(不是无限等),B等待一定时间之后,就会给A传输一个**"心跳包"(不携带业务数据(载荷),只是为了触发ACK)** ,它有两个特性,一个是周期性,另外一个就是没有心跳的话,就认为对方挂了。如果对方有心跳,就继续等待,如果没心跳,就只能通过rst尝试,还是不行的话,就只能单方面释放连接了。这就为"保活机制"。

4 网线断开了

站在A的视角,就和接收方断电一样

站在B的视角,就和发送方断电一样

最终都是能够释放连接的

通过上面的讨论,把TCP 的主要内容说的差不多了,

6个标志位中,还差2个,这2个在开发中一般不会使用,了解即可

URG表示紧急指针位和 是有关系的。TCP正常来说是按照序号顺序来发送和接收的,紧急指针就相当于"插队",会跳过前面的数据,直接从某个指定的序号来开始read.

PSH表示催促标志位,发送方给接收方发的数据中带有这个标志位,接收方就会尽快的把这个数据read到应用程序中

到这,就把UDP和TCP的内容说的差不多了,接下来,就来对比一下吧

TCP是可靠传输的,在大部分场景下,都会优先使用TCP。比如:HTTP,浏览器/app访问服务器

UDP是在性能要求高,可靠性要求不高的场景下使用的

那如何基于UDP实现可靠传输???(重点)

很简单,就把UDP往TCP上套,就好了

OK啦,到此结束!!

相关推荐
muyeminyu2 天前
SDUT计算机网络实验二:交换机的配置
计算机网络·大学生·计网·计网实验·sdut
河北之花2 天前
计算机三级:交换机VLAN配置
计算机网络
2501_916008892 天前
全平台抓包工具,Windows、iPhone、Linux三个平台抓包测试
网络协议·计算机网络·网络安全·ios·adb·https·udp
爱学习的程序媛2 天前
以太网协议详解
网络·网络协议·计算机网络·以太网·ethernet·通信协议
河北之花2 天前
计算机三级:VLAN虚拟局域网
计算机网络
代码中介商3 天前
DNS 完全指南:从域名解析到实战排查
计算机网络·dns
疯狂打码的少年4 天前
【计算机网络】IPv6基础(特点、表示法、与IPv4对比)
网络·笔记·计算机网络
网安老伯4 天前
都2026年了,还在问网络安全怎么入门?看完这一篇你就懂了
运维·计算机网络·web安全·网络安全·wireshark·密码学·网络攻击模型
河北之花4 天前
计算机三级:BGQ
网络·tcp/ip·计算机网络·智能路由器