【EE初阶 - 网络原理】传输层协议

文章目录

传输层协议

(一)UDP协议

格式

HTTP的报头是文本格式的

UDP/TCP/IP报头是二进制的

传输层协议,只考虑到端口号这一层


如果要使用UDP来传输超过64KB的数据,就需要在应用层手动的分包,多次发送,并在接收端手动拼接

校验和

检验数据是否发生修改

HTTPS的数据签名,为了防止黑客篡改(是为了防人)

UDP的校验和,不是为了防人,和安全性无关,而是防止出现传输过程中的"比特翻转"

1 -> 0

0 -> 1

光信号,电信号,电磁波受到外界干扰,可能会使高低电平/高低频光信号发生改变~

过程:发送之前,先计算一个校验和,把这个数据报的数据都代入

把数据和校验和一起发送给对端

接收方收到之后,重新计算一下校验和,和收到的校验和进行对比(UDP发现校验和不一致,就是直接丢弃)

UDP的校验和使用CRC方式进行校验(循环冗余校验)

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

(二)TCP协议

格式

  • 16位源端口号 和 16位目的端口号 => 传输层的核心内容

  • 4位首部长度: 4个bit位,使用4个字节为单位 =>4 * (2^4-1) =>60

  • 保留(6位):UDP的问题,长度不够,又不能扩展,TCP设计者在TCP报头中预留了一些"保留位"(先不用,但是占个位子)

  • 6位标志位:

    • URG: 紧急指针是否有效
    • ACK: 确认号是否有效
    • PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
    • RST: 对方要求重新建立连接;我们把携带RST标识的称为复位报文段
    • SYN: 请求建立连接;我们把携带SYN标识的称为同步报文段
    • FIN: 通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段
  • 16位校验和:用啦检验数据是否出错

TCP的核心机制

可靠性:网络通信是非常复杂的,此处的可靠性,并不是说A给B发一个消息,B 100%能收到,而是A给B发了消息之后,尽可能的让B收到~~

A能够知道B是否收到了~

1.确认应答

保证可靠性的一个关键前提,是发送方知道自己的数据是否被对方收到,需要对方给返回一个"应答报文",发送方知道应答报文,就可以确认对方是收到了
ACK = 1 ,表示这个是应答报文

发多条

对于可能出现的后发先至现象,TCP的处理方案 => 给传输的数据,进行编号,即序列号

引入序号之后,接收方就可以根据序号对数据进行排序,TCP处理后发先至的情况,确保应用程序通过 socket api 读到的数据顺序是正确的~~

即使出现后发先至,tcp也会处理掉,确保代码读到的数据(InputStream,read)与发送方写入的数据顺序一致(OutputStream,write)

TCP 在接收方这里会安排 "接收缓冲区"(内存,操作系统内核里)

通过网卡读到的数据,先放到接收缓冲区中,后续代码里调用 read,

也是从接受缓冲区来读的~~

根据序号来排序,序号小的在前面,大的在后面

确保前面的数据已经到了,然后 read 才能接触阻塞.

如果是后面的数据先到,read 继续阻塞,不会读取到数据~~

这也是生产者消费者模型的一种体现

2.超时重传

超时重传是针对丢包情况作出处理

为啥会丢包?

数据经过某个路由器/交换机转发的时候,该路由器/交换机,已经非常繁忙了,导致当前需要转发的数据量超出路由器.交换机的转发能力上限

直到接受缓冲区都满了~~,只能丢弃最新的数据(拒绝策略)

  • 主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B
  • 如果主机A在一个特定的时间间隔内没有收到B发来的确认应答,就会进行重发

但是,主机A未收到B发来的确认应答,也可能是因为ACK丢了

达到等待时间的上限,还没收到ACK A就认为传输中发生了丢包
1. A -> B 发的数据丢了 2. B -> A 返回的 ACK丢了

引入超时时间,来判定是否丢包~

TCP中,判定超时的时间阈值,不是固定数值,是动态改变的

假设发生丢包,超时阈值为T

传输发生超时之后,会延长T

不是无休止的。超时次数达到一定程度 / 等待时间达到一定程度就认为网络出现严重故障,放弃这一次传输~~

随着进行重传~~导致数据到达对方的概率越来越高的~~重传还不成,说明即使我们增加了概率,还是不能成功意味着当前丢包概率是一个非常大的数值意味着网络上大概率已经出现严重故障了~~此时,就算继续重传,意义也不大了~~也就没有必要重传频率那么高 (破罐子破摔了)

连续发两个包都丢的概率 1%
至少有一个到达对方的概率 99%
重传次数的增加, 数据报到达对方的概率增加~~ =>超时重传 可以有效应对丢包问题

出现丢包,B会受到很多重复数据,如果TCP不处理,可能会使应用层读到两次一样的数据(扣款数据),TCP利用前面提到的序号,很容易做到去重效果 => 根据序号,在接收缓冲区找一下,若存在,直接丢弃;若不存在,才放进去~

这里要明确一个概念:保证TCP可靠传输的关键机制是确认应答+超时重传,网络上有一些资料 会写"三次握手","三次握手"只是对可靠性有一定的帮助,但是最关键的还是 确认应答+超时重传

3.连接管理

连接管理:

1.建立连接(三次握手)

2.断开连接(四次挥手)

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

我们先来讨论建立连接,TCP通过"三次握手"方式来完成的~~

生活中的握手,就是打招呼,握手操作没有实际业务,只是发送了一个不携带业务的数据

比如,我们前面提到的HTTPS获取证书,验证证书,加密对称密钥,传输对称密钥,确认对称密钥收到...就是SSL协议的"握手流程"

不是三次握手吗,这怎么是四次?

合并可以提高户传输效率,网络传输过程中,是要能够进行"封装和分用的" 合并=>减少"封装分用"

synchronized 同步
在加锁里,理解为"互斥"
在TCP中同步,指的是"数据上的同步" syn = 1 表示同步报文
A告诉B,接下来我要和你建立连接,就需要你把我的关键信息保存好,同时你把你的信息同步给我
关键信息:IP和端口号(并不是载荷数据) => IP在IP报头里,端口号在TCP报头里
syn与ack都是不携带载荷的~~

为啥TCP要建立连接(三次握手)的意义是什么?

1.三次握手,相当于"投石问路",先初步探一探网络的通信链路是否通畅(这是网络可靠传输的前提)

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

3.三次握手过程中,可以协商一些关键信息,协商参数,确认双方一些通信中的必备参数数值

对于第二点举个栗子🌰,开黑打游戏

通过上述讨论,建立连接的操作,如果只握手两次,是不够的,因为有一个设备并不能知道对方的接受能力和自己的发送能力是否正常

那握手四次呢?? 可以但是没有必要,直接合并还能提高效率呢!!

对于第三点举个栗子🌰,前朝的剑斩本朝的官=>斩断余孽

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

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

OK,以上为连接管理中的建立连接,接下来我们讨论断开连接(四次挥手)~~

虽然时机不同,但是能否像三次握手一样合并? 三次握手那里能合并主要是因为 ACK SYN都是内核进行操作的相同时机~~

如能!!(有时候能,有时候不能)
不能是因为: ACK 和 FIN这两次交互的时机不同~~
能是因为:延时应答 可能就会使得 两次交互在同一时机

延时应答也是TCP的核心机制 我们后面聊~~

CLOSE_WAIT

CLOSE_WAIT 正常开发中,应该是看不到的,原则上来说,感知到对方断开后,就应该尽快执行close

TIME_WAIT

TIME_WAIT 有点像 TIMED_WAITING 线程状态

网络传输中,随时会丢包,三次握手,四次挥手时也一样会丢包~~

丢包就要触达超时重传~~

假设 A发完ACK之后,就释放连接,那么ACK丢后,B会再给A发送FIN,就没有人处理FIN了
所以A收到FIN之后需要等一下,等对方是否可能重传FIN(最后一个ACK可能丢包 )
但是等多久呢?

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

四次挥手全都挥完,属于"正常情况",也有"异常情况",比如服务武器始终不调用close,A已经发了FIN好久了,B一直没有后续挥手操作,A就会主动释放连接(也就是把B的核心消息删了)

B这边代码是有bug的,连接暂时存在(还会保存对方的信息,此时也没办法进行正常的数据通信了)

4.滑动窗口

TCP保证可靠性的同时,会牺牲效率
确认应答策略,对每一个发送的数据段,都要给一个ACK确认应答,收到ACK之后再发送下一个数据段,这样就会性能差,尤其是数据往返的时间较长的时候

一发一收性能较低,那我们一次发送多条(多个段的等待时间重叠在一起),就可以大大提高效率

  • 窗口大小 指的是无需等待确认应答而可以继续发送数据的最大值.上图的窗口大小就是4000个字节(四个段).
  • 发送前四个段的时候,不需要等待任何ACK,直接发送;=>用一份时间等多组ACK
  • 收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据;依次类推;
  • 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答;只有确认应答过的数据,才能从缓冲区删掉;
  • 窗口越大,则网络的吞吐率就越高;

如果2001 ack比 1001 先到,窗口直接往后走两个格子就可以了

确认序号的含义:该序号之前的数据,都确认收到了

2001 ack 能涵盖1001 的含义~~

窗口越大,批量发的数据越多,效率就越高.但是窗口也不能无限大,太大也会影响到可靠性.滑动窗口是在可靠传输的基础上,提高效率 .

只是亡羊补牢.

引入可靠性,会使效率产生折损

引入滑动窗口,是要让折损更小,

不可能效率比UDP这种还高~~

如果出现丢包,如何进行重传,这里分两种情况讨论

情况一:数据包已经到达,ACK被丢了

这已经达到了丢包 50%,已经是相当严重的网络问题了

此时不用做任何额外的处理,没事~~

后一个ACK到了,就能够涵盖前一个ACK的含义了

情况二:数据包直接丢了

  • 当某一段报文段丢失之后,发送端会一直收到1001这样的ACK,就像是在提醒发送端"我想要的是1001"一样;
  • 如果发送端主机连续三次收到了同样一个"1001"这样的应答,就会将对应的数据1001 - 2000重新发送;
  • 这个时候接收端收到了1001之后,再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中;
    这种机制被称为"高速重发控制"(也叫"快重传")。

超时重传:传输的数量少,没有构成滑动窗口批量传输的形式
快速重传:传输的数量多,形成滑动窗口
两种机制并不矛盾 ,只是针对不同的情况的重传机制~~

5.流量控制

接收端处理数据的速度是有限的.如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应.

因此TCP支持根据接收端的处理能力,来决定发送端的发送速度.这个机制就叫做流量控制

  • 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段,通过ACK端通知发送端;
  • 窗口大小字段越大,说明网络的吞吐量越高;
  • 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端;
  • 发送端接受到这个窗口之后,就会减慢自己的发送速度;
  • 如果接收端缓冲区满了,就会将窗口置为0 ;这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
6.拥塞控制

因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵.在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的.

TCP引入慢启动机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据;

流量控制:根据接收方处理能力,进行限制
拥塞控制:根据传输链路的转发能力,进行限制

  • 此处引入一个概念称为拥塞窗口
  • 发送开始的时候,定义拥塞窗口大小为1;
  • 每次收到一个ACK应答,拥塞窗口加1;
  • 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口;
    像上面这样的拥塞窗口增长速度,是指数级别的."慢启动"只是指初始时慢,但是增长速度非常快.
  • 为了不增长的那么快,因此不能使拥塞窗口单纯的加倍.
  • 此处引入一个叫做慢启动的阈值
  • 当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长
  • 当TCP开始启动的时候,慢启动阈值等于窗口最大值;
  • 在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1;
    少量的丢包,我们仅仅是触发超时重传;大量的丢包,我们就认为网络拥塞;
    当TCP通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降;

总结一下上面就是:
通过做实验的方式,找到合适的窗口大小
面对加水 ,水多加面
先按照小的窗口(小的速度),先发着,如果发的时候很顺利,不丢包,就加大速度,出现丢包,就减小速度,又不丢了,再加大速度 => 动态平衡
如果不好具体衡量到某个设备,就把整个通信连路视为"整体"

流量控制 和 拥塞控制 都能限制发送方的窗口大小 ,两个值谁小,听谁的

7.延时应答

如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小.

  • 假设接收端缓冲区为1M.一次收到了500K的数据;如果立刻应答,返回的窗口就是500K;
  • 但实际上可能处理端处理的速度很快,10ms之内就把500K数据从缓冲区消费掉了;
  • 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来;
  • 如果接收端稍微等一会再应答,比如等待200ms再应答,那么这个时候返回的窗口大小就是1M;
    一定要记得,窗口越大,网络吞吐量就越大,传输效率就越高.我们的目标是在保证网络不拥塞的情况尽量提高传输效率;
    那么所有的包都可以延迟应答么?肯定也不是;
  • 数量限制:每隔N个包就应答一次;
  • 时间限制:超过最大延迟时间就应答一次;
    具体的数量和超时时间,依操作系统不同也有差异;一般N取2,超时时间取200ms;

具体的参数,隔多少个包,隔多长时间,这些参数都是可以调整的

默认情况下,接收方都是在收到数据报的第一时间,就返回ACK,但是可以通过延时应答,延时返回ACK的方式提高效率 => 所以有些时候"四次挥手"可能为"三次挥手",就是ACK延时后可能会与FIN同一时机返回

举个栗子🌰

老师来催作业了,我有10次作业没交

1.我立刻给老师回消息,仍然是10次作业没交,就会比较虚~

2.不立即回复,而是赶紧去补交作业,一次性交完7个,再回消息,这样就会理直气壮一些~~

利用延时时间,赶紧消费队列中的数据

8.捎带应答

在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是"一发一收"的.意味着客户端给服务器说了"How are you",服务器也会给客户端回一个"Fine, thank you";

那么这个时候ACK就可以搭顺风车,和服务器回应的"Fine, thank you"一起回给客户端

如果没有延时应答,返回ACK的时机和返回响应的时机,就是不同时机

9.面向字节流

创建一个TCP的socket,同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;

  • 调用write时,数据会先写入发送缓冲区中;
  • 如果发送的字节数太长,会被拆分成多个TCP的数据包发出;
  • 如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去;
  • 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区;
  • 然后应用程序可以调用read从接收缓冲区拿数据;
  • 另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据.这个概念叫做 全双工
    由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:
  • 写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节;
  • 读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read 100个字节,也可以一次read一个字节,重复100次;

关键就是咋拆包,从哪里到哪里是一个完整的应用层数据包 =>粘包问题(粘的就是应用层的数据包)

粘包问题

  • 首先要明确,粘包问题中的 "包",是指的应用层的数据包.
  • 在TCP的协议头中,没有如同UDP一样的 "报文长度" 这样的字段,但是有一个序号这样的字段.
  • 站在传输层的角度, TCP是一个一个报文过来的.按照序号排好序放在缓冲区中.
  • 站在应用层的角度,看到的只是一串连续的字节数据.
  • 那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包.
    那么如何避免粘包问题呢?归根结底就是一句话,明确两个包之间的边界.
  • 对于定长的包,保证每次都按固定大小读取即可;例如上面的Request结构,是固定大小的,那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
  • 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置;
  • 对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来定的,只要保证分隔符不和正文冲突即可);

上述问题,TCP层次无法解决,只能在应用层解决
1.约定好包和包之间的分隔符(包的结束标记)
比如我们的TcpEchoServer里面使用的 \n 作为结束标记
2.约定包的长度
比如约定每个包开头的4个字节,表示数据包一共多长

HTTP中,这两种方案都有体现
1.GET请求,没有body,使用空行,作为结束标记
2.POST请求,有body的时候,通过Content-Length决定body多长~~

10.异常处理
  • 进程终止:进程终止会释放文件描述符,仍然可以发送FIN.和正常关闭没有什么区别.
  • 机器重启:和进程终止的情况相同.
  • 机器掉电/网线断开 :接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset.即使没有写入操作,TCP自己也内置了一个保活定时器 ,会定期询问对方是否还在.如果对方不在,也会把连接释放.
    另外,应用层的某些协议,也有一些这样的检测机制 .例如HTTP长连接中,也会定期检测对方的状态.例如QQ,在QQ断线之后,也会定期尝试重新连接.

1.某个进程崩溃了

进程崩溃和主动退出没有本质区别 =>回收文件描述符表的资源 =>调用 socket的close =>TCP连接还存在 四次挥手正常进行

2.主机关机了,本质上还是会先杀死所有的用户进程

关机需要一定的时间 =>时间之内,四次挥手,挥完了,就和正常情况一样

如果没有挥完呢? -->最终可以把连接释放掉

3.主机掉电了(接收方掉电 和 发送方掉电)

4.网线断开了

站在 A 的视角,就是和刚才上面 "接收方掉电" 是一样的情况

站在 B 的视角,就和刚才上面 "发送方掉电" 是一样的情况~~

最终都是能够释放连接资源的~

保活机制

分布式系统中,心跳包的思想方法,非常广泛使用的~ (TCP 的心跳,周期太长了,分钟级别的)

虽然 TCP 内置了心跳包,实际开发中,通常还是会在应用层重新实现心跳包效果

现在通常希望,秒级,甚至毫秒级,就能发现对端是否正常存活,从而触发一些后续的操作~~

TCP 小结

为什么TCP这么复杂?因为要保证可靠性,同时又尽可能的提高性能.

可靠性:

  • 校验和
  • 序列号(按序到达)
  • 确认应答
  • 超时重发
  • 连接管理
  • 流量控制
  • 拥塞控制
    提高性能:
  • 滑动窗口
  • 快速重传
  • 延迟应答
  • 捎带应答
    其他:
  • 定时器(超时重传定时器,保活定时器,TIME_WAIT定时器等)

TCP 可靠传输(大部分场景下,都会优先使用TCP)

UDP 效率更高(对于性能要求高,可靠性要求不高的场景下)

HTTP,用于浏览器/APP 访问服务器

相关推荐
润 下3 小时前
C语言——回调函数的典型示例(分析详解)
c语言·开发语言·人工智能·经验分享·笔记·程序人生
oak隔壁找我3 小时前
Java 使用技巧与最佳实践
java·后端
oak隔壁找我3 小时前
SpringMVC 使用技巧与最佳实践
java·后端
oak隔壁找我3 小时前
Spring 框架使用技巧与最佳实践
java·后端
koo3643 小时前
李宏毅机器学习笔记27
人工智能·笔记·机器学习
前端架构师-老李3 小时前
Java开发—JDK的安装和版本管理(macOS)
java·开发语言·macos
DoveLx3 小时前
Spring Boot 事务管理:从基础到高级
java·后端
oak隔壁找我3 小时前
Spring Boot 使用技巧与最佳实践
java·后端·面试
虎子_layor3 小时前
Java线程池快速入门
java·后端