【网络原理】TCP/IP协议01

TCP/IP协议分为五层:应用层、传输层、网络层、数据链路层、物理层。现在学习每个层中关键的协议。

1.应用层

应用层是与应用程序直接相关的,写的代码,只要是涉及到网络通信的,都可以视为应用层的一部分。应用层这里的东西是与程序员直接相关的,应用层中涉及到的网络通信协议,很多也是程序员自定义的(基于其他的协议)。

1.1如何自定义协议?

分成两个阶段:

  1. 根据需求,明确传输哪些信息
  2. 约定好信息组织的格式

示例:一个外卖应用程序,首页会显示商家列表,这其中关于 客户端-服务器 之间传递的信息,这个信息是根据需求来的,用户想要点外卖,就需要用户自己账号信息,根据用户想要点的外卖,即请求,服务器返回符合用户请求的所有响应:

  • 请求:用户的位置信息(经纬度)、用户的id
  • 响应:商家的id、商家的名字、商家图片、评分、配送费、种类 【很多条】

上述就是根据需求,明确传输的信息的阶段,接下来,就是约定好这些信息组织的格式:

1)行文本的方式约定请求和响应

  • 例如,请求的信息中每个列之间使用 " 、" 分隔,使用 \n 作为结束标志
  • 一个响应是由多行构成的,每一行是一个商家,每个商家中包含的一些列信息,组织的格式和请求一样

自定义协议,只要客户端和服务器都按照这同一套规则来进行构造/解析数据就可以使用。

2)通过 xml 格式约定请求和响应

xml 与 html 一样,都是成对的标签构成的键值对结构,html 的标签内容是固定的,约定好的,不能乱写,也不能创建新的标签(html 5 允许自定义标签了),而 xml 标签内容是自定义的。

按照 xml 约定格式:

  • 请求:

    XML 复制代码
    <request>
        <userId>2100</userId>
        <position>45E45N</position>
    </request>
  • 响应:

    XML 复制代码
    <response>
        <shop>
            <id>1</id>
            <name>塔斯汀</name>
            <image>图片1.jpg</image>
            <rank>4.8</rank>
            <sendprice>12</sendprice>
        </shop>
    </resopnse>

注意

  • XML 的设计目标是描述数据、传输和存储数据(用来网络传输的)。它不关心数据长什么样,只约定信息的结构和含义,非常适合在网络上进行数据交换(例如 API 接口返回 XML 格式的数据)。
    • xml 可以用于很多的场景,主要用于 组织一段格式化数据,用来网络传输,作为配置文件等。
  • HTML 的设计目标是显示数据(约定浏览器怎么显示)。它定义了一套固定的标签,浏览器能理解这些标签(如 <h1>、<p>),并按照默认的显示规则(以及配合 CSS)把内容渲染出来。

xml 的优点是可读性好,而缺点是:冗余信息多,网络传输中,会消耗更多的带宽。

3)通过 json 格式约定请求和响应

json 的可读性也好,消耗的带宽也比 xml 更节省。

按照 json 约定格式:

  • 请求:

    XML 复制代码
    {
        "request":{
            "userId":2100,
            "position":45E45N
        }
    }  
  • 响应:

    XML 复制代码
    {
      "response": {
        "shop": {
          "id": 1,
          "name": "塔斯汀",
          "image": "图片1.jpg",
          "rank": 4.8,
          "sendprice": 12
        }
      }
    }

    但是,这种格式还是存在冗余信息。

4)通过 protobuf 格式约定请求和响应

protobuf 是基于二进制的格式对数据进行压缩,不涉及到 json/xml 的冗余信息,带宽消耗更少,但是可读性就变差了。

protobuf 中,约定一段二进制数据中哪几个字节表示信息,就是单纯的用户发送的请求或者响应的信息,没有其他的冗余信息。如果性能要求高的场景,就使用 protobuf,如果性能要求不高,还是更建议使用 json。

5)总结

  1. 行文本(最原始)
  2. xml(比较原始,可读性好,冗余较多)
  3. json(主流的方式,可读性好,冗余一般)
  4. protobuf(高性能场景下使用,可读性差,冗余最小)

1.2 HTTP/HTTPS 协议

应用层这里,除了自定义协议之外,也有一些定好的协议,如 FTP(文件传输)、SSH 远程操作主机、telnet 网络调试工具、HTTP协议(HTTPS是在HTTP基础上+安全层-S表示安全层SSL)............

这里重点说一下 HTTP/HTTPS协议。

请看上一篇文章(详细讲解了HTTP/HTTPS协议):【网络原理】HTTP/HTTPS协议

2. 传输层

应用层的很多操作,都是要调用传输层提供的接口的。

在传输层中,最主要的协议是 TCP 和 UDP 协议。

2.1 端口号

端口号(Port)标识了⼀个主机上进行通信的不同的应用程序。

在TCP/IP协议中, 用 "源IP","源端口号", "目的IP", "目的端口号", "协议号" 这样⼀个五元组来标识⼀个通信。

端口号占2个字节,16个bit位 ,即一个端口号的取值范围是0~65535

而实际上,一般把1024以下的端口保留,自己写代码的时候都是使用 1024~65535 这个范围的端口号,原因:

  • 0- 1023: 知名端口号, HTTP(80), HTTPS(443) ,FTP(21), SSH(22)等这些⼴为使用的应用层协议, 他们的端口号都是固定的
  • 1024 - 65535: 操作系统动态分配的端口号.,客户端程序的端口号, 就是由操作系统从这个范围分配的

2.2 UDP协议

UDP 的全称是 User Datagram Protocol,即为用户数据报协议

UDP 特点:无连接,不可靠传输,面向数据报,全双工。

①UDP报文段格式

header中的长度,表示整个数据报(UDP报头+UDP数据报)的最大长度,而这个长度属性,也是2个字节,表示的范围是 0~65535,即64kb。

这是一个非常小的数字,如何传输一个大的数据呢?

有两个方案:

  1. 应用层代码做拆包操作,一个大的应用层数据包,拆成多个小的包,使用多个UDP数据报传输。但是工作量比较大,写大量的逻辑,实现此处的分包组包功能,并且需要进行复杂的验证。
  2. 使用 TCP协议,没有数据包长度限制。

检验和:验证数据是否发生修改,HTTPS的数字签名,是为了防止黑客篡改,而UDP的校验和,是为了防止出现传输过程中的 "比特翻转" (0和1 是通过光信号/电信号/电磁波传播的,收到干扰,可能会使高低电平发生改变)。

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

UDP的校验和使用了 CRC(循环冗余校验) 方式来进行校验,把每个字节(除了校验和位置的部分除外),都当作整数,进行累加,溢出也没有关系,继续加,最终得到的最终结果,就是 CRC校验和,传输到对端,数据出现错误了,对端再次计算的校验和,就会和第一个校验和不一样了。

  • 两个原始数据相同,使用相同的校验和算法,得到的校验和也是相同的
  • 反之,如果两个校验和相同,原始数据不一定是相同的,例如前一个字节出现了 bit 翻转,后一个字节也刚好出现了 bit 翻转,最终加在一起,校验和是一样的。

②UDP的特点

UDP传输的过程类似于寄信:

  • 无连接: 知道对端的IP和端⼝号就直接进行传输,不需要建立连接
  • 不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息
  • 面向数据报: 不能够灵活的控制读写数据的次数和数量
    • 应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并,如果用UDP传输100个字节的数据,发送端发送这100字节的数据,那么接收端也必须一次接收这100字节的数据,而不能分批发送 , 每次接收10个字节

③基于UDP的应用层协议

  • NFS: 网络文件系统
  • TFTP: 简单文件传输协议
  • DHCP: 动态主机配置协议
  • BOOTP: 启动协议(用于无盘设备启动)
  • DNS: 域名解析协议

也包括自己写UDP程序时自定义的应用层协议。

2.3 TCP协议

TCP全称为 "传输控制协议(Transmission Control Protocol").,即要对数据的传输进行⼀个详细的控制。

TCP 特点:有连接,面向字节流,可靠传输,全双工。

①TCP报文段格式

1️⃣源/目的端口号:表示数据是从哪个进程来,到哪个进程去

2️⃣选项:用于扩展TCP功能,可有可无,选项的存在,那么TCP的报头长度是可变的

3️⃣4位首部长度:表示TCP报头的总长度(以4字节为单位)

  • 由于首部长度字段 在TCP报头中占 4 bit位,因此它的取值是 0 到 15
  • 该字段的 单位是 4 字节(32 位),那么数值 N(0-15范围内) 表示TCP 头部总长度为 N × 4 字节,即TCP报头总长度是 4的倍数。
  • 最小值 5:因为必须包含至少 20 字节的固定头部,即TCP报头最小总长度5 × 4 = 20 字节
    • TCP固定头部(即不包含选项时的20字节)由这些字段组成:源端口-2字节,目的端口-2字节,序号-4字节,确认序号-4字节,首部长度(4bit)+保留(6bit)+标志位(6bit)-2字节,窗口大小-2字节,校验和-2字节,紧急指针-2字节。
  • 最大值 15:15 × 4 = 60 字节,即TCP报头最大总长度60 字节,其中 40 字节用于选项字段

4️⃣校验和:发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含 TCP首部, 也包含TCP数据部分。

5️⃣6位标志位:

  • URG:紧急标志。为1时表示紧急指针字段有效,本报文段中含有紧急数据,需优先处理。
  • ACK:确认标志。为1时确认序号字段有效。除初始SYN报文外,所有报文都应置1。
  • PSH:推送标志。为1时接收方应尽快将数据交付给应用层,而不是等待缓冲区填满。
  • RST:复位标志。为1时表示连接出现严重错误(如端口未打开、连接超时),必须立即释放连接并重建。
  • SYN:同步标志。用于建立连接:连接请求:SYN=1, ACK=0。连接响应:SYN=1, ACK=1
  • FIN:结束标志。为1时表示发送方数据已全部发出,请求释放连接(四次挥手)。

6️⃣序号:本报文段第一个数据字节的序号(从随机初始序号开始)。用于:

  • 数据重传时的定位
  • 接收端对乱序数据进行排序
  • 检测重复数据

7️⃣确认序号:仅当ACK标志位为1时有效。表示期望收到的下一个序号,同时隐含确认该序号之前的所有数据已正确接收。是可靠传输和滑动窗口的基础。

8️⃣窗口大小:发送方当前可接收的字节数(即接收窗口)。用于流量控制,防止发送方发送过快导致缓冲区溢出。窗口大小可动态变化。

9️⃣紧急指针:仅当URG=1时有效。指出本报文段中紧急数据的最后一个字节相对于序列号的偏移量。用于发送端中断当前传输,优先发送关键数据。

🔟数据:上层协议(如HTTP、FTP等)的实际有效载荷。如果报文段只用于控制(如SYN、FIN、ACK且不带数据),则没有数据字段。

②TCP的核心机制

1)确认应答

TCP的特点之一可靠性,不是说A给B发一个消息,B就100%收到,而是A给B发了消息之后,尽可能让B收到。

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

示例:如以下,是发短信的过程

发送方发了两条消息给接受方,接收方按照先后顺序进行应答,此时发送方能正确理解接收方的意思,但是,网络上存在一个现象,就是先发后至

此时就无法正确理解意思。

针对这个问题,TCP 的处理方案是:给要传输的数据,进行编号,也就是报头中的这两个字段:

  • 32位序号
  • 32位确认序号,这个确认序号只有在应答报文中才有效,即 ACK = 1。

TCP是面向字节流的,其实在编号的时候,不是按照 1条,2条这样的方式来编号的,而是按照 "字节" 来编号的,每个字节都分配一个编号,连续递增。

即TCP将每个字节的数据(载荷)都进行的编号,即为 序号:

而一个TCP的载荷是多个字节构成的,意味有许多个编号,那么此处的 序号 字段,应该记录的是载荷部分的第一个字节的序号。例如上述图中,要发送1-1000编号的字节数据,序号字段记录的是 1 序号。

每⼀个ACK (应答报文)都带有对应的确认序号 , 意思是告诉发送者, 我已经收到了哪些数据; 下⼀次你从哪里开始发,即 确认序号 字段,记录的是收到的数据载荷的最后一个字节 序号 +1

通过上图,可以知道确认应答的含义:

  • 1-1000 序号的数据,都已经确认收到了(此时确认序号字段记录 1001序号)
  • 接下来你要从 1001 序号 开始给我发送数据

TCP 通过 序号 和 确认序号 配合接收方的重排序缓存(接收缓冲区),完美处理"先发后至"(即乱序到达)的问题:

  • 每个报文段携带序号,接收方不按到达顺序而按序号将数据插入缓冲区的正确位置,使后到达但序号靠前的数据与先到达但序号靠后的数据都能被暂存;确认序号始终指向缓冲区中连续已收字节的下一个位置(即期望收到的下一个序号),即使后发的报文先到达,接收方也不会跳跃确认,而是继续等待缺失字节,从而触发发送方重传缺失部分,保证数据最终有序交付。

2)超时重传

是可靠性的另一个关键前提,是针对丢包问题进行处理的机制。

为何会出现丢包?

  • 数据报经过某个路由器,交换机转发的时候,该路由器/交换机非常繁忙,导致当前需要转发的数据量超出路由器/交换机的转发能力上限,也就是 "堵车",那么数据报就要等待更多的时间,才能到达对方,更坏的情况,数据报太多了,路由器/交换机处理不过来,缓存区都满了,于是就只能 丢弃了,也就是丢包,说明网络上的数据包都是有时效性的。

示例:如以下的图:当 A 达到等待时间的上限,还没有收到 ACK,A就认为传输中发生了丢包

丢包有两种可能情况:

  1. A 发送给 B 的数据丢了
  2. B 返回给 A 的 ACK 丢了

无论是上述的哪一种情况,重传,都是对抗丢包的有效手段,即 A 再次重新发数据给B(丢包的两种可能情况,发送方A是区分不了当前是属于哪个情况的,做法都是进行重传)。

  • 假设当前网络丢包的概率是 10%,那么有90%的概率能够到达对方,而连续发送两个包,都丢的概率是 1%,那么至少有一个到达对方的概率是99%。
  • 重传的次数增加,数据报到达对方的概率就增加。

A 在⼀个特定时间间隔内没有收到B发来的确认应答, 就会进行重发:

不过,A未收到B发来的确认应答, 也可能是因为ACK丢失了,那么此时A也会进行重发:

依据这种情况,B 就会收到重复的数据,那么TCP协议需要能够识别出哪些包是重复的包, 并且把重复的丢弃掉(去重)。

这时候可以利用前面提到的序列号, 就能够很容易做到去重的效果:

  • 在 B 收到数据之前,根据数据的序号,在接收缓冲区中找一下,如果存在,就直接丢弃,然后再次返回一个一样的 ACK 给A,如果不存在,则放进去,再返回 ACK。

总结:确认应答和超时重传,是TCP协议中最核心的两个机制,保证了TCP能够进行可靠传输。

3)连接管理

连接是抽象的,逻辑上的连接,能够让通信双方各自保存对端信息。

连接 有建立连接和断开连接。

在正常情况下, TCP要经过三次握手建立连接, 四次挥手断开连接。

🅰️建立连接 - 三次握手

TCP 通过 "三次握手" 的方式来完成建立连接。

握手(handshake),握手操作,没有实际的业务,只是 "打个招呼",即发送一个不携带业务的数据(不包含上层应用载荷的报文),通过这个数据与对方打个招呼,协商初始序号、确认连接能力,而SYN标志位正是用来标识这种握手报文的:

  • SYN,即 synchronized 同步,在加锁中,同步理解为互斥,但是在 TCP中的同步,指的是 "数据上的同步",A 告诉 B 我要和你建立连接,需要你把我的关键信息保存好,同时你也把你的信息同步发给我。
  • 当TCP报文头中的SYN位被置为1时,表示同步报文(一个连接请求或连接应答报文),用于告诉对方"我想和你建立连接"并同步双方的序号。
  • 当然,双方也是需要确认自己的信息是否被对方保存,也就是说双方各自发送了 SYN(同步报文)后,会返回给对方一个 ACK(应答报文),告知收到了。

以上的说明,如下图:

问题:不是说 "三次握手" 吗,怎么上述的图是 四次?

------ 其中有两次,是能够合并成一次的,即 B 在确认应答的同时,可以发送同步报文:

  • SYN+ACK 可以合并是因为TCP标志位设计允许同时置位(同时置1),合并后能减少一次交互,提高握手效率

(客户端和服务器在同一个程序的不同场景下,可以扮演不同的角色,主动发起 SYN 的,一定是客户端)

连接管理详细图:看 三次握手 的部分:

  • CLOSED:表示不存在的状态,即TCP还没有连接
  • USTEN:启动服务器,即 new ServerSocket 的时候,就会进入 LISTEN 的状态,表示服务器准备好了,随时可以有客户端连上来
  • SYN_SENT:客户端主动发起连接,发送一个 SYN 标志位为 1 的报文(不含业务数据,仅"打招呼")后,进入 SYN_SENT 状态,等待服务器回复 SYN+ACK。
  • SYN_RCVD:服务器收到客户端的 SYN 报文后,回复 SYN+ACK 报文,并进入 SYN_RCVD 状态,等待客户端发送最终的 ACK 确认报文。
  • ESTABUSHED:客户端和服务器已经建立了连接

在TCP建立连接时,两次握手不够,四次握手可以但没必要,原因如下:

  • 两次握手(客户端发SYN,服务器回SYN+ACK):
    • 【1】无法确认双方的接收能力,第三次握手(客户端发送ACK)的核心作用是:告诉服务器"我确实收到了你的SYN+ACK,并且我的接收正常",
    • 如果没有第三次ACK,服务器只知道"我发出了SYN+ACK",但不知道客户端是否收到了(可能服务器发的ACK丢失了,或者客户端已崩溃)
    • 这样服务器就会盲目进入ESTABLISHED状态,可能导致数据发送到已经失效的连接。
    • 【2】无法防止"历史连接"干扰,假设客户端发送一个旧的、延迟到达的SYN报文(比如之前连接失败残留的请求),
    • 服务器收到后,返回SYN+ACK,并认为连接已建立,开始分配资源等待数据,但客户端实际上并没有发起这个连接请求,收到SYN+ACK后会发现序号不匹配,直接丢弃该报文
    • 结果:服务器白白等待,浪费资源,且无法知道客户端是否真的准备好。
  • 四次握手 ------ 客户端发SYN,服务器回ACK(确认收到SYN),服务器再发SYN(自己的序号),客户端回ACK(确认服务器SYN) :
    • 可以,但属于冗余设计,浪费一次报文交互,增加延迟,降低效率
    • SYN和ACK本身互不冲突,完全可以合并发送,既然一次能做的事不分两次做

三次握手的作用:

  1. 三次握手,相当于 "投石问路",先初步探一探网络的通信链路是否通畅(网络通畅是可靠传输的前提条件)
  2. 验证通信双方的发送能力和接受能力是否正常
  3. 三次握手的过程中,可以协商一些关键信息
    1. TCP 要协商的一个非常关键的信息,就是通信过程中,序号 从几开始,初始的序号,一般不是从0开始的,并且两次连接的初始序号都是不同的,往往差别会很大。
    2. 也就是关于 "历史连接" 的问题,A和B建立连接后,A发送了一个数据包,转发的时候迷路了,迟迟没有到达 B,A没等到B的回复,断开了连接,当第二次连接建立好后,之前第一次连接中迷路的数据包才到达B ,但是在这一次连接中 A 根本就没有发送这个数据包,且第一次连接和第二次连接的程序可能都不是同一个,
    3. 那么此时 接收方A 就要把这个 "历史数据" 剔除,就可以通过 序号 来区分,比如,第一次连接协商好初始序号为1000000,第二次连接协商好初始序号为8000000,当收到数据后,发现和当前连接的数据的序号相差甚远,就可以认为这个数据是 "历史数据"。
    4. 这也进一步解释了 两次握手 不行的原因。

注意:TCP 的可靠传输,是靠 确认应答+超时重传 实现的,而三次握手,对于可靠传输有一定的帮助,但是一旦建立连接好后,后续进行业务数据的通信,靠的是确认应答+超时重传,就和三次握手无关了。

🅱️断开连接 - 四次挥手

TCP 通过 "四次挥手" 的方式来完成断开连接。

FIN标志位正是用来标识这种挥手报文的:

  • FIN,A 告诉 B 我要和你断开连接,需要你把我的关键信息删除,同时你也把我的信息删除
  • 当TCP报文头中的FIN位被置为1时,表示结束报文,用于告诉对方"我想和你断开连接"
  • 当然,双方也是需要确认自己的信息是否被对方保存,也就是说双方各自发送了 FIN(结束报文)后,会返回给对方一个 ACK(应答报文),告知收到了。

以上的说明,如下图:

在 三次握手 中,中间两次能够合并,那 四次挥手 能不能合并呢?

答案是不能的,因为 ACK 和 FIN 这两次交互的时机是不同的。

  • 四次挥手中 ACK 和 FIN 通常不能合并,根本原因是被动关闭方需要等待自己的数据发送完毕,而三次握手中服务器没有这种等待需求,服务器收到客户端的 SYN 后,不需要等待任何额外的条件,它可以立即同时确认客户端的 SYN 并发送自己的 SYN。这两件事互不冲突,且合并能减少一次报文交互。

  • 被动关闭方可能还有数据要发送:

    当主动关闭方发送 FIN 表示自己已无数据要发时,被动关闭方可能尚未发完自己的数据(例如服务器还在向客户端传输文件)。因此,它只能先回复一个 ACK 确认收到 FIN,但暂时不能发送 FIN(因为连接仍处于半关闭状态,它需要继续传输剩余数据)。等到数据全部发完后,被动关闭方再单独发送 FIN,然后主动关闭方返回一个 ACK 后,正式断开连接

    如果被动关闭方强制将 ACK 和 FIN 合并,就意味着它一收到 FIN 就立即关闭自己的发送通道,这会导致尚未发送的数据丢失,也违背了 TCP 允许"一方关闭发送,另一方继续发送"的半关闭语义。

连接管理详细图:看 四次挥手 的部分:

  • 谁是主动发起 FIN 的一方,就会进入到 TIME_WAIT 状态
  • 谁是被动发起 FIN 的一方,就会进入到 CLOSE_WAIT 状态

TIME_WAIT ,其实在给最后一个 ACK 丢包行为,做一个托底

在网络传输中,随时会丢包,三次握手,四次挥手时,也一样会丢包,而丢包就会触发超时重传:

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

假设出现这种情况:如果 B 给 A 发送 FIN,A 收到之后返回 ACK,然后 A 不等 B 是否收到 ACK 就直接把连接释放了,而此时 ACK 又恰好丢了,那么 B 没有收到 A 的回复,B 就只能再次重传 FIN ,但是此时 A 已经释放了连接,此时无人来处理重传的 FIN。

所以,要求 A 这边在收到 FIN 之后,不能立即释放连接,而是需要等一下,等一下对方是否可能重传 FIN(最后一个 ACK 可能丢包了),确认对方不会重传 FIN 之后再释放。

4)滑动窗口

TCP 在保证可靠性的时候,付出的代价,就是效率。

确认应答策略,对每⼀个发送的数据段,都要给⼀个ACK确认应答. 收到ACK后再发送下 ⼀个数据段. 这样做有⼀个比较大的缺点,就是性能较差. 尤其是数据往返的时间较长的时候。

既然这样⼀发⼀收的方式性能较低,那么我们⼀次发送多条数据, 就可以大大的提高性能,即前几个数据包都不等待ACK,直接往后发,发到一定的量之后,再等待ACK(用一份时间等待多组ACK,换一句话说,把多组等待ACK的时间重叠成一份)

这个 "发到一定的量" 就是一个窗口,而窗口大小是动态变化的:

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

问题:一组数据包发送完成后,下一组怎么发?有两种发法:

  1. 等这一组所有ACK都回来,再发第二组
  2. 收到一个ACK,就发下一条

答案当然是选择第二种,第一组的话会花更长的时间。

有一种情况:如果是 3001ACK应答 比 2001ACK 先到达,那么窗口应该如何走呢?

------ 窗口直接往后走两个格子就可以了,因为 2001 ACK其实接收方已经收到了

  • 确认序号的含义,就是该序号之前的所有数据,都确认收到了,
  • ACK 的含义是告诉发送方我收到了。
  • 而 3001ACK 能够涵盖 2001 的含义,也就是说,如果是 3001 先到了,那么 2001 到不到都无所谓了,因为 3001 到了,等于 2001 也已经收到了 (这里还是不太理解往下看)

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


在滑动窗口的过程中,也会出现丢包,那么如何进行重传?分两种情况:

1️⃣数据包已经抵达,但是ACK丢包了

这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进行确认,即后续的ACK能够涵盖前面的含义。

ACK 只是告诉发送方我收到了,如果它丢了,虽然发送方就无法确认接收方是否收到了刚才发的数据包,但是如果发送方继续发送后续数据包,而接收方能够正常返回后续数据包的ACK,则说明接收方是收到了的,所以说,这种情况下,不要紧。

2️⃣数据包丢包了

  • 当某一段数据包丢了,虽然发送端能够正常发送后续的数据包,但是发送端会一直收到接收端返回的 1001 这样的 ACK ,就像是在提醒发送端 "我想要的是 1001" ⼀样
  • 如果发送端主机连续三次收到了同样⼀个 "1001" 这样的应答,就会将对应的数据 1001 - 2000 重新发送/重传;
  • 这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中。就像拼图,其他的 2001~7000 的拼图都完整了,就差 1001~2000 这一块拼图,通过重传,把这个缺失的拼图补上,补上之后,继续从 7001 往后传输即可。

上述的这种机制,被称为 "高速重发控制",也叫快速重传。谁丢了,就重传谁,其他已经收到的数据无需重传,整个重传的速度非常快。快速重传,是滑动窗口下,超时重传的变种操作。

  • 超时重传:传输的数据量少,没有构成滑动窗口批量传输的形式
  • 快速重传:传输的数据量大,形成了滑动窗口
  • 这两种机制是针对不同情况下的重传机制,不矛盾。

5)流量控制

前面说过,滑动窗口,窗口越大,效率就越高,但是不能无限大,太大了会影响到可靠性,

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

发送端发送的速度,就像水池蓄水的速度,而接收端读取的速度,就像水池放水的速度,而水池,就相当于接收缓冲区:

因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度,即让接收端根据自身处理数据的速度,反馈给发送端,限制发送端的发送速度,这个机制就叫做流量控制

  • 接收端将自己可以接收的缓冲区大小放入 TCP 头部中的 "窗口大小" 字段, 通过 ACK 中依赖的一个特殊属性 "窗口大小" 来通知发送端,TCP 报头中的 "窗口大小" 字段,应该填入的是接收缓冲区的剩余空间大小的值,之后发送端就会根据这个值来重新设定发送的滑动窗口的大小。
  • 而报头中这个 "窗口大小" 字段,是16bit位,即64kb,那么是否滑动窗口大小,最大数值就是64KB?答案当然不是,与UDP不一样,不要忘记,TCP报头中,有一个选项 字段,而这个字段中,就有一个特殊的属性 ------ 窗口扩展因子 ,如果指定了一个窗口扩展因子 为2,且64kb 不够用了,那么 窗口大小 << 窗口扩展因子,即 64 << 2 = 256kb,窗口大小最大数值就变大了。
  • 窗口大小字段越大, 说明网络的吞吐量越高
  • 接收端⼀旦发现自己的缓冲区快满了,就会将窗口大小设置成⼀个更小的值通知给发送端,发送端接受到这个窗口之后,就会减慢自己的发送速度
  • 如果接收端缓冲区满了,且接收端并没有从缓冲区中读取数据,就会将窗口置为0,防止丢包 。这时发送端不再发送数据,但是需要定期发送⼀个窗口探测数据段,因为发送端暂停发送数据包,就无法触发ACK,也就是说如果接收缓冲区中有空余了,发送端就无法知道,因此,窗口探测包就能够让 接收端把实时的窗口大小告诉发送端。

6)拥塞控制

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

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

TCP引入 慢启动 机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据,即先按照较小的窗口(小的速度)先发送数据,如果发的时候很顺利,没有丢包,就加大速度,出现丢包,就减小速度,依次类推,又不丢包了,加大速度......

  • 此处引入⼀个概念程为拥塞窗口
  • 发送开始的时候, 定义拥塞窗口大小为1
  • 每次收到⼀个ACK应答, 拥塞窗口加1
  • 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口,即拥塞控制和流量控制都能限制发送方的窗口大小,这两个值,哪个小就使用哪个。

像上面这样的拥塞窗口增长速度, 是指数级别的。"慢启动" 只是指初使时慢, 但是增长速度非常快

  • 为了不增长的那么快,因此不能使拥塞窗口单纯的加倍
  • 此处引入⼀个叫做慢启动的阈值
  • 当拥塞窗口超过这个阈值的时候, ++不再按照指数方式增长,而是按照线性方式增长++
  • 当TCP开始启动的时候, 慢启动阈值等于窗口最大值
  • 在每次超时重发的时候, 慢启动阈值会变成原来的⼀半, 同时拥塞窗口置回1

少量的丢包, 仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;

当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降。

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

上述的 传输轮次-拥塞窗口 图,表示了拥塞控制的工作工程:

  1. 慢启动
  2. 指数增长
  3. 线性增长
  4. 丢包,窗口变小

7)延时应答

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

如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小,但是如果接收端稍等一会再应答,等的过程中,应用程序就会消耗掉缓冲区中一部分数据,这时候返回的窗口可能比较大。

那么所有的包都可以延迟应答么? 肯定也不是

  • 1.数量限制: 每隔N个包就应答⼀次(不会因为ACK少而影响可靠性,后一个ACK涵盖前面的)
  • 2.时间限制: 超过最大延迟时间就应答⼀次

具体的数量和超时时间, 依操作系统不同也有差异; ⼀般N取2, 超时时间取200ms

综合的,传输的数据密集,就按照第一个方式,数据稀疏时,就按照第二个方式。

8)捎带应答

基于延时应答的基础上,引入 捎带应答,返回业务数据的时候顺便把上一次的 ACK 给稍带回去。

它们的关系可以概括为:延时应答是手段,捎带应答是目的

即接收方在向发送方回传正常数据时,将原本需要单独发送的ACK确认信息,顺便"附带"在这个数据包里一起发送,这样减少网络中纯ACK报文的数量。

  • 延时应答:接收方收到数据后,不立即发送ACK,而是等待一小段时间(通常40-200ms)。在这段延迟期内,接收方会观察两点:① 对方是否还会继续发送数据;② 自己是否有数据需要回传给对方。
  • 捎带应答:如果接收方在延迟期内恰好有数据要发送给对方,就把ACK信息"附着"在这个数据报文中一起发送,从而用一个包完成"确认+发送数据"两件事。

它们如何协作(典型流程):

  1. 接收方收到一个数据包。
  2. 接收方启动延迟确认计时器(如40ms)。
  3. 在计时器超时前:
  4. 情况A:接收方恰好有数据要发送。 → 发送一个数据包,其中TCP头部ACK标志位置1,确认序号字段填写期望的下一个序号。 → 成功实现捎带应答,取消计时器。
  5. 情况B:接收方没有数据要发,但对方又传来了新数据。 → 延迟确认继续,可能最终用一个ACK确认多个数据段。
  6. 情况C:计时器超时,依然没有数据要发。 → 发送一个纯ACK包(无业务数据),结束本次延迟确认。

那么,通过上述的这个机制,四次挥手,就可以实现为 三次挥手

9)面向字节流 - 粘包问题

首先要明确,粘包问题中的 "包" , 是指的应用层的数据包。TCP 通过字节流的方式传输,很容易混淆包和包之间的边界,从而接收方无法区分从哪里到哪里是一个完整的应用层数据包。

  • 在TCP的协议头中, 没有如同UDP⼀样的 "报文长度" 这样的字段, 但是有⼀个序号这样的字段
  • 站在传输层的角度,TCP是⼀个⼀个报文过来的,按照序号排好序放在缓冲区中
  • 站在应用层的角度,看到的只是⼀串连续的字节数据
  • 那么应用程序看到了这么⼀连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是⼀个完整的应用层数据包

(UDP就不会出现这种情况,因为UDP就是以一个数据包为单位读写的,而不是字节,边界清晰)

如何避免粘包问题呢?

------ 就是⼀句话, 明确两个包之间的边界

而上述的这个问题,在 TCP 的层次上是无解的,需要站在应用层的角度来解决,即定义好应用层协议,明确包与包之间的边界。

  1. 约定好包与包之间的分隔符(包的结束标志)。例如约定 \n 为结束标志
  2. 约定包的长度。
    1. 对于定长的包,保证每次都按照固定的大小从缓冲区中从头开始读取sizeof大小即可
    2. 对于变长的包,可以在每个包包头的位置,约定⼀个包总长度的字段,从而就知道了包的结束位置。又或者像上面的那样在包和包之间使用明确的分隔符。

在 HTTP 中,上述的两种方法都有体现

  1. GET 请求,没有 body,使用空行作为结束标志
  2. POST 请求,有 body,通过 Content-Length 决定 body 的长度

解决粘包问题,是我们自定义应用层协议要做的事情,像 json,protobuf 这些,已经把粘包问题解决了。

10)异常情况

TCP在通信过程中,存在特殊情况,就需要去处理

1️⃣某个进程崩溃

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

2️⃣主机关机了

  • 按照正常流程的关机,本质上还是会先杀死所用的用户进程,和上面的情况一样

3️⃣主机掉电了

示例:

  • 情况1:接收方掉电:
    • A突然掉电了,B后续发来的数据,都没有ACK,对于B触发超时重传,也不能解决问题
    • 重传达到一定程度,就会触发"重置TCP连接"
    • B 主动发送一个 复位报文(从头开始,既往不咎),即 TCP报头标志位中的 RST复位标志,释放连接并重建连接
    • 但是由于A掉电了,此处的 RST 也没有ACK,B 就只能单方面释放连接
  • 情况2:发送方掉电:
    • B 突然发现 A 没有声音了,此时 B 分不清楚 A是挂了,还是暂时休息一会,
    • B 只能继续等,等到一定时间之后,就会给 A传输一个特殊的报文,"心跳包",不携带业务数据(载荷),只是为了触发 ACK。
    • 心跳包,是周期性的,如果对方有心跳,继续正常等待,如果没有心跳,就只能 RST 尝试,还是不行,就只能单方面释放连接。

4️⃣网线断了

  • 站在 A 的角度,就是和 "接收方掉电" 的情况一样
  • 站在 B 的角度,就是和 "发送方掉电" 的情况一样
  • 最终都能够释连接放资源

到这里,TCP的学习基本结束,这里介绍一下,6位标志位中的其他没有讲到的2位:

  1. URG:紧急指针位,TCP 正常来说,按照序号顺序发送和接收,而紧急指针相当于插队,跳过前面的数据,直接从某个指定的序号开始读取
  2. PSH:催促标志位,发送方给接收方发了数据中带有 PSH 的标志位,接收方就会尽快的给这个数据读取到应用程序中

2.4 TCP 总结

TCP 即要保证可靠性, 同时又尽可能的提高性能。

可靠性

  • 校验和
  • 序列号(按序达到)
  • 确认应答
  • 超时重传
  • 连接管理
  • 流量控制
  • 拥塞控制

提高性能

  • 滑动窗口
  • 快速重传
  • 延时应答
  • 捎带应答

2.5 基于TCP的应用层协议

  • HTTP
  • HTTPS
  • SSH
  • FTP
  • Telnet
  • SMTP

也包括自己写UDP程序时自定义的应用层协议。

2.6 TCP/UDP 对比

我们说了TCP是可靠连接, 那么是不是TCP⼀定就优于UDP呢?

TCP和UDP之间的优点和缺点, 不能简单, 绝对的进行比较

  • TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景
  • UDP用于对高速传输和实时性要求较高的通信领域

如何基于 UDP 实现可靠传输?

------ 这个问题,其实就是在问,TCP 如何实现可靠传输

  • 引入序列号,保证数据顺序
  • 引入确认应答,确保对端收到了数据
  • 引入超时重传,如果隔⼀段时间没有应答,就重发数据
  • ............
相关推荐
dxxt_yy2 小时前
鼎讯信通 TY-30H 光纤熔接机:铁路通信施工设备科普
网络
星恒讯工业路由器3 小时前
4G自组网与VPDN专网技术解析
网络·物联网·信息与通信·4g自组网·vpdn专网
淼淼爱喝水3 小时前
DVWA靶场命令注入漏洞检测实验
网络·安全·靶场·dvwa
KaMeidebaby3 小时前
卡梅德生物技术快报|免疫共沉淀 - Co-IP 实验在转录因子 ATF3/Smad4 蛋白互作研究中的应用实例解析
网络·人工智能·网络协议·tcp/ip·其他·算法·新浪微博
林熙蕾LXL4 小时前
IPC使用套接字进程通讯
网络
宋浮檀s4 小时前
应急响应——Web高危漏洞应急(SQL注入+XSS跨站+文件上传)
前端·网络·安全·web安全·xss
阿部多瑞 ABU4 小时前
一次针对大语言模型的“虚构历史前提注入”红队测试实录:当AI相信了不存在的对话历史
网络·人工智能·安全
ylscode13 小时前
PureLogs 信息窃取恶意软件惊现高危变种:借道 MsBuild.exe 进程空心化实施无痕攻击
网络·安全·安全威胁分析
IPHWT 零软网络13 小时前
MX60E-A信创级智能语音网关技术实现与架构分析
网络·网络安全·国产自研·技术实现·智能语音网关·政企通信·信创技术