【ONE·Linux || 网络基础(四)】

总言

  主要内容:传输层UDP、TCP协议基本介绍。UDP报文格式、TCP报文格式、三次握手四次挥手、TCP可靠性策略说明。


  
  
  

8、UDP协议:用户数据报协议(传输层·一)

8.1、传输层预备知识

8.1.1、再谈端口号

  1)、什么是端口号
  说明: 端口号(Port)是大小为2字节(16比特位)的整数,标识了一个主机上进行通信的不同的应用程序。(一些概念理解见网络基础(一):认识端口号
  作用: 一台计算机上同时可以运行多个程序。传输层协议正是利用这些端口号识别本机中正在进行通信的应用程序,并准确地将数据传输。

在这里插入图片描述

  说明:TCP/IP 或UDP/IP通信中通常采用5个信息来识别一个通信。它们是 “源IP地址”、“目标IP地址”、“协议号”、“源端口号”、“目标端口号”只要其中某一项不同,则被认为是不同的通信。(可以通过netstat -n查看:显示所有已建立的有效连接。)

在这里插入图片描述

  
  
  
  2)、端口号范围划分(标准既定的端口号)
  0 - 1023知名端口号(Well-Know Port Number)。 HTTP、FTP、SSH等这些广为使用的应用层协议, 他们所使用的端口号都是固定的

  1024 - 65535操作系统动态分配的端口号。 客户端程序的端口号, 就是由操作系统从这个范围分配的。
  
  说明:为了使用方便,人们约定一些常用的服务器都是用以下这些固定的端口号。在/etc/services文件中可查看。

ssh服务器, 使用22端口
ftp服务器, 使用21端口
telnet服务器, 使用23端口
http服务器, 使用80端口
https服务器, 使用443

在这里插入图片描述

  
  
  
  3)、问题说明
  问题一:一个进程是否可以绑定(bind)多个端口号?
  回答:是的,一个进程可以绑定(bind)多个端口号。在套接字(socket)编程中,当创建一个套接字时,系统会为该套接字分配一个文件描述符(file descriptor,通常表示为sockfd),这个描述符在操作系统中用于唯一标识这个套接字。由于进程可以拥有并操作多个文件描述符(包括套接字描述符),因此,一个进程可以通过创建多个套接字并分别绑定到不同的端口号,来实现同时绑定多个端口号的功能。每个套接字都可以独立地绑定到一个不同的端口,从而实现进程在多个端口上的监听和服务功能。
  
  


  问题二:一个端口号是否可以被多个进程绑定(bind)?
  回答:通常情况下,一个端口号在同一时间只能被一个进程绑定(bind),这是因为端口号在本地主机上是唯一的,用于标识不同的网络服务或应用程序。当一个进程成功绑定到一个端口后,它会独占该端口,其他进程无法再绑定到相同的端口,除非第一个进程关闭该端口或发生异常终止。
  
  ①特别说明一:一个进程先绑定一个端口号,然后通过fork创建一个子进程,这样看起来似乎是“多个进程”绑定了同一个端口。但实际上,在fork之后,子进程会复制父进程的套接字描述符,因此子进程和父进程共享同一个套接字和端口绑定。这并不意味着两个独立的进程可以绑定到同一个端口,而是说它们共享了相同的套接字和端口绑定。

  特别说明二:服务器挂掉不能立即重启(TIME_WAIT状态)也侧面证明了端口号的独占性。这是因为TCP协议中的TIME_WAIT状态是为了确保客户端发送的最后一个ACK报文段能够到达服务器。即使服务器进程已经终止,操作系统仍然会保持该套接字在TIME_WAIT状态一段时间,以确保所有已发送的报文段都得到确认。在这个时间段内,其他进程无法绑定到相同的端口,因为该端口仍然处于某个连接的状态(在这里是TIME_WAIT状态) ,所以无法再次被绑定。
  
  


  问题三:为什么不直接使用进程PID来代替端口号识别唯一的进程?
  回答:①不是所有的进程都要对外提供网络服务,如果直接使用进程pid,OS还需要筛选到底是哪些进程参与提供网络服务(增添筛选成本)。②为了让系统功能和网络功能解偶联。
  
  


  
  
  
  
  
  

8.1.2、一些指令(netstat、pidof、xargs)

  1)、netstat
  netstat是一个用来查看网络状态的重要工具。Netstat会显示与IP、TCP、UDP和ICMP协议相关的统计数据,一般用于检验本机各端口的网络连接情况。
  语法:netstat [选项]
  功能:查看网络状态
  常用选项:

-n 拒绝显示别名,能显示数字的全部转化成数字
-l 仅列出有在 Listen (监听) 的服务状态
-p 显示建立相关链接的程序名
-t (tcp)仅显示tcp相关选项
-u (udp)仅显示udp相关选项
-a (all)显示所有选项,默认不显示LISTEN相关

在这里插入图片描述

  常用:-nltp-antp.
  
  
  
  
  2)、pidof
  语法: pidof [进程名]
  功能: 通过进程名, 查看进程id
  
  以下为简单演示,通过这种方法,可以很快的查看自己写的进程。
在这里插入图片描述

  
  
  
  3)、xargs
  xargs 是一个在 Unix 和 Linux 系统中常用的命令行工具,用于从标准输入(stdin)读取参数,并将它们作为命令行参数传递给其他命令。
  相关指令介绍见常见指令入门(二)

在这里插入图片描述

  
  一个小演示:文件日期更新。

[wj@VM-4-3-centos http_demo]$ ls -al #原先文件日期
total 36
drwxrwxr-x 3 wj wj 4096 Nov 12 21:59 .
drwxrwxr-x 4 wj wj 4096 Nov 11 20:11 ..
-rw-rw-r-- 1 wj wj 2894 Nov 12 21:40 HttpServer.cc
-rw-rw-r-- 1 wj wj 1667 Nov 11 11:02 HttpServer.hpp
-rw-rw-r-- 1 wj wj 1107 Nov 11 11:02 Log.hpp
-rw-rw-r-- 1 wj wj   98 Nov 11 11:02 Makefile
-rw-rw-r-- 1 wj wj 4043 Nov 11 11:02 Sock.hpp
-rw-rw-r-- 1 wj wj 1334 Nov 11 19:41 Util.hpp
drwxrwxr-x 5 wj wj 4096 Nov 11 18:47 wwwroot
[wj@VM-4-3-centos http_demo]$ ls | xargs touch
[wj@VM-4-3-centos http_demo]$ ls -al  #更新后的文件日期
total 36
drwxrwxr-x 3 wj wj 4096 Nov 12 21:59 .
drwxrwxr-x 4 wj wj 4096 Nov 11 20:11 ..
-rw-rw-r-- 1 wj wj 2894 Nov 19 09:37 HttpServer.cc
-rw-rw-r-- 1 wj wj 1667 Nov 19 09:37 HttpServer.hpp
-rw-rw-r-- 1 wj wj 1107 Nov 19 09:37 Log.hpp
-rw-rw-r-- 1 wj wj   98 Nov 19 09:37 Makefile
-rw-rw-r-- 1 wj wj 4043 Nov 19 09:37 Sock.hpp
-rw-rw-r-- 1 wj wj 1334 Nov 19 09:37 Util.hpp
drwxrwxr-x 5 wj wj 4096 Nov 19 09:37 wwwroot
[wj@VM-4-3-centos http_demo]$ 

  
  
  
  

  
  
  
  

8.2、UDP报文

在这里插入图片描述
  
  

8.2.1、基本认识:协议的封装(解封)和交付

  1)、引入:协议的封装(解封)和交付
  问题描述: 根据之前学习,我们了解到在数据通信过程中,数据从发送方到接收方的传输涉及到了数据包的封装和解封装过程。具体来说,当数据(从应用层到物理层)自上而下交付时,需要层层封装,添加报头信息。相反地,当数据(从物理层到应用层)自下而上递交时,需要层层解包,去掉报头信息。

  在实际的通信协议中,几乎任何协议都需要解决以下两个关键问题:
  ①如何进行报文的封装(Segmentation)或拆封(Decapsulation)? 这涉及到如何将原始数据(或来自上层的数据)与当前层的报头信息组合(封装)成适合当前层传输的数据单元,以及如何在数据到达目标层时移除报头信息(拆封)。
  ②如何实现数据的交付(Delivery)? 这涉及到如何确保数据能够在网络中的各个节点之间准确无误地传输,并最终到达目的地址。
  

  
  
  2)、UDP协议端格式
  这里介绍UDP协议和TCP协议,同理也需要解决上述1)中的两个问题。这里我们先介绍UDP协议:

1、UDP协议是如何分离报头(or 封装报头)的?
2、又是如何将报文交付到某一具体主机的具体进程上?

  要回答这两个问题,首先先来了解一下UDP协议的基本格式。
在这里插入图片描述
  
  根据上述UDP报头可知:

  问题一:UDP协议是如何处理报头的封装与解封装过程的?
  回答:UDP协议采用固定长度的报头(共8字节),该报头包含了UDP协议所需的必要信息。 在数据发送时,UDP会将其报头与有效载荷(即应用层数据)封装成一个完整的UDP数据报。在接收端,网络层会根据UDP数据报的格式,准确地识别并分离出UDP报头和有效载荷,进而将有效载荷递交给应用层进行处理。这个过程被称为UDP报文的解封装。

  扩展·再次理解“协议”: 那么HTTP/HTTPS协议又是如何分离报头和有效载荷的?根据先前的学习,其报头部分包含了请求或响应的元信息,并以回车换行符(\r\n)结束每一行。当报头部分结束后,会再添加一个空行(仅包含回车换行符),以明确标识报头与有效载荷之间的分隔。
  这里举例是想说明:无论是HTTP/HTTPS还是UDP/TCP等采用何种方式,这种必须为通讯双方所遵守一组规则和约定,这就是所谓的协议。

  
  问题二:UDP协议又是如何将报文准确地交付到某一具体主机的具体进程上的?
  回答:每层协议层报头中存储着当前层的首部属性信息。UDP协议通过报头中的16位目的端口号来确保报文能够准确地交付到目标主机的特定进程上。
  详细:在UDP报头的首部,有一个16位的字段用于存储目的端口号。当报文到达目标主机时,传输层会根据这个目的端口号来识别并确定接收该报文的进程。(PS:根据之前我们学习的网络套接字编程,无论客服端还是服务端,进程使用bind函数时,绑定了端口号,以便接收来自网络的数据。因此,当UDP报文到达时,操作系统会根据报头中的目的端口号,将报文递交给相应的进程进行处理。这样,UDP协议就实现了将报文准确地交付到某一具体主机的具体进程上的功能。)
  
  
  
  3)、有了上述认识,这里我们补充理解两个小问题
  问题1:为什么在之前使用socket编写代码时,我们总是将端口号的类型设定为uint16_t
  回答: 操作系统和计算机网络协议标准定义了端口号应为一个16位的无符号整数。 因此,在编程时,为了符合这一标准并确保端口号的正确表示,我们通常会使用uint16_t这一数据类型来表示端口号。 这样做既符合协议规范,也便于程序在不同平台和系统之间的移植。

  
  
  问题2:UDP是如何正确提取到整个完整报文的?
  回答:UDP报头中有记录报文长度的属性,16位UDP长度
  详细: UDP协议通过其报头中的长度字段来指示整个UDP数据报的长度。这个长度字段是一个16位的值,表示了包括UDP报头和有效载荷在内的总字节数。因此,当UDP数据报到达接收端时,系统会根据这个长度字段来提取整个完整的报文

  PS:需要注意的是,UDP是一个无连接的协议,它并不保证数据的可靠传输,所以 在不考虑丢包的情况下,UDP确实具有将报文一个一个正确接收的能力,即它是面向数据报的 。(重点在于理解这里的一个一个报文,注意与TCP的数据流区分) 然而,在实际应用中,由于网络环境的复杂性和不可预测性,UDP报文可能会出现丢失、乱序或重复的情况,因此在使用UDP时需要考虑到这些因素并采取相应的措施来处理。
  
  
  
  
  

8.2.2、细节理解:如何理解报文本身?

  问题描述:虽然上述给我们展示了报文的结构,但这也只是在逻辑角度让我们知道了报文中有哪些内容,但这个报文究竟是什么?这是我们尚不清楚的。

  回答:实际上,报头本质是一种结构化的字段。例如:位段结构,使用几个比特位来表示不同的状态或属性。以下为一个简易版的理解草图(实际应用于OS中会很复杂,比如要处理位段的可移植性等各种问题)。
在这里插入图片描述
  
  
  
  

8.2.3、UDP数据长度补充说明(大数据传输的处理)

  1)、概述
  在之前我们介绍过,UDP协议的首部包含一个16位的长度字段,该字段表示整个UDP数据报(包括UDP首部和UDP数据)的最大长度。因此,一个UDP数据报的最大长度理论上是 2 16 2^{16} 216字节,即65535字节(或64K字节)。

在这里插入图片描述

  一个问题说明:UDP协议首部中有一个16位的最大长度,也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部)。而64K在当今的互联网环境下是一个非常小的数字,如果我们需要传输的数据超过64K,就需要在应用层手动的分包、多次发送,并在接收端手动拼装。
  
  
  2)、展开介绍
  实际的数据长度限制: 虽然理论上UDP数据报的最大长度是64K字节,但实际上由于UDP首部长度固定(通常为8字节),所以实际的数据部分最大长度为64K减去首部长度,即65535 - 8 = 65527字节。但考虑到某些网络设备和操作系统的限制,实际使用中可能会更小。
  
  互联网环境下的限制: 在当前的互联网环境下,由于标准MTU(最大传输单元)的限制(如常见的576字节),为了避免数据在传输过程中被分片(fragmentation),实际应用中建议将UDP的数据长度控制在比MTU稍小的范围内。例如,对于MTU为576字节的网络,建议将UDP数据长度控制在576 - 8(UDP首部长度) - 20(IP首部长度)= 548字节以内。
  
  大数据传输的处理: 当需要传输的数据超过UDP的最大长度限制时,需要在应用层进行手动分包(fragmentation)。 这通常意味着将数据划分为多个较小的数据包,并分别通过UDP发送。在接收端,需要手动将这些数据包重新组装(reassembly)成原始数据。这种分包和重组的过程需要应用程序自行处理,因为UDP协议本身并不提供这样的服务。

  
  
  
  
  
  
  

8.3、UDP传输的特点

8.3.1、基本介绍

  1)、UDP的三个传输特点

  无连接:知道对端的IP和端口号就直接进行传输,不需要建立连接。

  • 这种无连接的特点使得UDP具有较低的延迟,因为不需要像TCP那样进行三次握手建立连接和四次挥手断开连接。但同时,也因为没有连接状态的管理,UDP不提供流量控制和拥塞控制,这可能导致数据的丢失或乱序。

  不可靠(非贬义,中性特点): 没有确认机制(即发送方不会等待接收方的确认就认为数据已经发送成功),没有重传机制(即当数据在传输过程中丢失时,UDP不会尝试重新发送。)。如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息。

  • 这种不可靠性并不意味着UDP是一种不好的协议,而是它的设计初衷就是用于那些不需要可靠传输的场景,如实时通信、流媒体传输等。在这些场景下,即使丢失少量的数据也不会对整体应用产生太大的影响。

  
  面向数据报:不能够灵活的控制读写数据的次数和数量。

  • UDP将应用程序传下来的报文分成若干个长度不超过64K(包括64K)的数据报,并附上UDP头部信息,然后将这些数据报发往网络层。每个数据报都是一个完整的、独立的传输单元,拥有自己的报头和数据部分。每个数据报在传输过程中都是独立的,互不影响。

  
  
  
  2)、对面向数据报的理解

  说明: 应用层交给UDP多长的报文,UDP就原样发送该长度的报文。在这个过程中,UDP不会关心数据报的大小,它只是简单地将数据报作为一个整体进行传输。既不会对其进行拆分,也不会将其与其他数据单元合并。

  举例: 应用层使用UDP传输100个字节的数据。
  ①如果发送端调用一次sendto函数来发送这100个字节,那么接收端在接收这些数据时,也必须调用对应的一次recvfrom函数,并且指定接收缓冲区的大小为100个字节,以确保能够接收到完整的数据报。
  ②如果接收端循环调用10次recvfrom函数,每次接收10个字节,那么它将无法正确地接收并处理这个数据报,因为UDP已经将数据作为一个整体发送出去了。

  
  
  
  
  

8.3.2、UDP缓冲区

  1)、如何理解sent \ recvfrom \ write \ read \ recv \ send 等等 IO类接口?

  说明: 在计算机网络通信中,我们在调用send/write或者recv/read等函数接口时,这些函数并非直接负责将数据发送到网络或对方主机,而是实现了一个关键的数据传输过程:数据的拷贝

  具体来说:
  ①当调用send或write函数时,应用程序实际上是将数据从用户空间拷贝到内核空间的发送缓冲区中。
  ②同样地,当调用recv、read或recvfrom函数时,数据则是从内核空间的接收缓冲区拷贝到用户空间。
  ③这样的设计确保了用户进程与内核之间的清晰边界,并提高了系统的安全性和稳定性。
在这里插入图片描述

  这就意味着,IO函数本质只是在做用户进程与内核之间的拷贝工作。 至于数据什么时候发送?一次发送多少?出错了怎么办?等等此类问题,这些不是应用层来考虑的事,而是由传输层的传输控制协议(TCP、UDP)决定的。(体现了传输层协议传输控制的特性
  
  
  
  
  
  2)、如何理解UDP缓冲区?

  1、UDP协议提供了一种无连接的、不可靠的、基于数据报文的传输方式。在UDP中,虽然存在接收缓冲区,但它并不保证接收到的UDP数据包的顺序与发送时完全一致。 也就是说,即使数据包是按照特定顺序发送的,但在接收端,它们可能会以不同的顺序到达。此外,当UDP接收缓冲区已满时,后续到达的UDP数据包将被直接丢弃,而不会像TCP那样进行缓存或重传。
  2、与TCP不同,UDP没有真正意义上的发送缓冲区。当应用程序调用sendto函数发送UDP数据包时,这些数据将直接交给操作系统内核进行处理。内核会将这些数据传递给网络层协议,并由网络层协议负责后续的传输动作。由于UDP是无连接的,因此内核不会为每个UDP连接维护一个独立的发送缓冲区。

  3、UDP的socket(套接字)支持全双工通信,即可以同时进行读写操作。 这意味着在一个UDP连接中,数据可以双向流动,既可以从一端发送到另一端,也可以从另一端接收数据。这种全双工的特性使得UDP非常适合于需要同时处理发送和接收数据的场景。

全双工:同一时刻,能供同时读和写
半双工:同一时刻,读写操作只能进行一项

  
  
  
  

8.3.3、扩展:全双工、半双工简单介绍

  1)、全双工(Full-Duplex)

  全双工通信允许数据同时在两个方向上传输,即通信的双方可以同时发送和接收数据。在全双工模式下,存在两条独立的通信通道,一条用于发送数据,另一条用于接收数据。这两个通道可以同时处于活动状态,互不干扰。因此,全双工通信能够充分利用传输介质的带宽,提高通信效率。
  
  在计算机网络中,全双工通信的典型例子是TCP/IP协议栈中的TCP和UDP协议。这些协议支持双向数据传输,使得客户端和服务器之间可以同时进行读写操作。在硬件层面,全双工通信常见于串行通信接口,如RS-232、RS-422和RS-485等,它们使用两对线路分别进行发送和接收。

  
  
  2)、半双工(Half-Duplex)

  半双工通信允许数据在两个方向上传输,但同一时刻只能进行一个方向上的数据传输。也就是说,在某一时刻,通信的双方只能发送数据或接收数据,不能同时进行。半双工通信通过切换通信通道来实现双向数据传输,但每次只能有一个通道处于活动状态。
  
  在计算机网络中,半双工通信较少使用,因为它限制了通信的效率和灵活性。然而,在某些特殊情况下,如某些简单的串行通信接口或某些受限的通信环境中,半双工通信可能仍然是一种可行的解决方案。在硬件层面,半双工通信可以通过单个线路或一对线路实现,通过控制信号的切换来实现双向数据传输。

  
  
  
  

  
  
  
  
  
  
  

9、TCP协议:传输控制协议(传输层·二)

  TCP全称为 “传输控制协议(Transmission Control Protocol”)。 “人如其名”,TCP协议会对数据的传输进行一个详细的控制。
  

9.1、初步理解

9.1.1、首要解决的两个问题(16位目的端口号、四位首部长度)

  1)、tcp报文格式
  说明:TCP首部内容很丰富,如果不计算选项字段,一般来说TCP首部有20字节 (由此也说明TCP的报头是变长的,不像UDP那样固定长度)。简图如下:

在这里插入图片描述
  
  在上述UDP协议中,我们提到过,几乎任何协议都需要解决两个关键问题:①如何进行报文的封装(Segmentation)或拆封(Decapsulation)?②如何实现数据的交付(Delivery)?

  这里,学习tcp报头,我们也要关注这两个问题。
  
  
  
  2)、如何交付报文
  说明:根据上述TCP协议段格式,TCP报头属性中含有16位源/目的端口号,可以告知数据是从哪个进程来,到哪个进程去。

  
  

  3)、如何解包 or 封装
  问题引入: 要进行交付报文,则要能将TCP数据报的报头和有效载荷拆分开,那么如何拆分?
  

在这里插入图片描述前提知识补充:要理解该问题,首先要理解4位首部长度

  首部长度,也称数据偏移,该字段长4位,单位为4字节(32位)。它指出了TCP报文段的数据起始处距离TCP报文段的起始处有多远,即TCP报文的首部长度。
  应注意,“数据偏移”以4字节为计算单位,由于4位二进制数表示的最大十进制数字是15,因此整个TCP首部长度的最大值是60字(即选项的长度最大为40字节)。
在这里插入图片描述

  
  

在这里插入图片描述正题:如何获取报头和有效载荷?

  基本步骤如下:
  1、提取前20字节(标准报头)。
  2、根据标准报头,提取4位首部长度L(二进制表示,需要转换为十进制)。检测L× 4 ,若为20,代表报头中没有选项属性,报头提取完毕,剩下的即为有效载荷。否则,说明存在选项属性,读取[L× 4 - 20]字节数据,即选项的长度。
  3、将20字节的属性和选项都读完,剩下的就都是有效载荷了。
  
  细节理解:观察TCP报文格式,我们不难发现,与UDP报文格式相比,TCP协议并没有直接提供一个字段来记录整个报文(或称为数据段)的大小(即有效载荷的大小)。UDP报文则具有一个明确的16位UDP长度字段,用以标识UDP数据报文的长度。这是为什么?

  回答:TCP的这种设计差异源于其面向字节流的本质。在TCP的传输过程中,数据被视为一连串无边界的字节流,这意味着TCP本身并不区分单个报文的边界。相反,这种区分工作通常由应用层协议来完成,它们通过特定的应用层协议标记或数据格式来识别数据的起始和结束。
  
  
  
  

9.1.2、初步理解可靠性

  内容说明:这里我们先挑一个重要的属性(可靠性),作为讲解TCP协议的切入点。
  为了通过IP数据报实现可靠性传输,需要考虑很多事情,例如数据的破坏、丟包、重复以及分片顺序混乱等问题。如不能解决这些问题,也就无从谈起可靠传输。通常,TCP通过检验和、序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现可靠性传输。
  
  

  1)、问题引入:要理解可靠性,首要要理解为什么不可靠
  问题一:是什么原因造成的数据传输不可靠?
  回答:单纯就是数据传输的距离变长了。这也是为什么在操作系统单机内部,我们一般不谈TCP/IP协议,而一旦涉及到网络,就开始牵扯到TCP/IP协议的原因。
  
  
  问题二:存不存在100%可靠的协议呢?
  回答:从绝对意义上讲,不存在100%可靠的通信协议。通信双方都无法保证自己作为 “最新发送数据的一方”,发送出去的 “最新数据 ” 能被对方收到。但是在局部上,对于历史发送的数据,可以做到100%可靠(正是因为接收到对方发送的数据,才有了最新数据的发送,作为对接收到的消息的回应,也就是所谓的确认应答机制)。
在这里插入图片描述

  
  
  
  2)、如何做到可靠性?(局部)
  TCP协议的确认应答机制:只要发送方发出的一个报文收到了接收方传回的对应应答,就能保证我发出的数据对方收到了。(后续会详细介绍到)
在这里插入图片描述

  
  
  

9.1.3、32位序号和32位确认序号

  1)、问题引入

  问题说明:数据包乱序,也是造成数据传输不可靠的因素之一。 例如:

  ①客户端一次可能向服务端发送多个报文,就有一个问题,发送的顺序不一定是接受顺序。
  ②同理,服务端可以一次响应多个报文,那么客户端就需要确认哪个应答对应哪个请求。
在这里插入图片描述

  在数据传输的各个环节中,一旦数据包乱序现象发生,就可能导致接收端无法正确解析信息,从而影响通信的准确性和效率。 为了解决上述问题,我们引入了序号和确认序号。
  
  
  
  
  2)、序号和确认序号

在这里插入图片描述基本介绍:

  32位序号和32位确认序号是TCP协议中用于确保数据传输可靠性和顺序性的重要机制。


  序号(Sequence Number):在TCP通信中,序号字段占据4个字节的空间,其作用是确保数据的有序传输。由于TCP是面向字节流的协议,它在传输过程中会将每一个字节都进行编号。这种编号是连续的,从TCP连接建立之初就设定了整个要传送字节流的起始序号。在TCP报文段的首部中,序号字段的值特指当前报文段所携带的数据部分第一个字节的序号,从而确保接收端能够按照正确的顺序重组数据

  例如,一个报文段的序号是1001,而数据共1000字节,这就表明:本报文段的数据的第一个字节的序号是1001,最后一个字节的序号是2000。显然,下一个报文段(如果还有的话)的数据序号应当从2001开始,即下一个报文段的序号字段值应为2001。
在这里插入图片描述
  扩展: 序列号不会从0或1开始,而是在建立连接时由计算机生成的随机数作为其初始值,通过SYN包传给接收端主机。然后再将每转发过去的字节数累加到初始值上表示数据的位置。此外,在建立连接和断开连接时发送的SYN包和FIN包虽然并不携带数据,但是也会作为一个字节增加对应的序列号。
  


  确认号(Acknowledgment Number):确认号字段同样占据4个字节,在TCP通信中,它的值是收到对方下一个报文段的第一个数据字节的序号。通过发送这个确认号,接收端能够告知发送端哪些数据已经被成功接收,哪些数据还待接收,从而确保数据的可靠传输和流量控制。

  例如,B正确收到了A发送过来的一个报文段,其序号字段值是501,而数据长度是200字节(序号501~700),这表明B正确收到了A发送的到序号700为止的数据。因此,B期望收到A的下一个数据序号是701,于是B在发送给A的确认报文段中把确认号置为701。
  
在这里插入图片描述


  
  
  
  

在这里插入图片描述作用说明:

  根据上述描述,可知:
  1、 唯一对应关系: 序号和确认序号确保了发送的请求与接收到的应答之间建立了唯一的对应关系。这使得通信双方能够清晰地识别每一个请求所对应的响应,从而准确地进行数据处理。

  2、数据确认与提示: 确认序号不仅仅表示接收方已经成功接收了某一序号之前的数据,更重要的是它告诉发送方,下次发送数据时,应从该确认序号所指示的下一个序号开始。这种机制有效地减少了数据的重复发送,提高了传输效率。

  3、容忍部分丢失,或者不给应答: 在数据传输过程中,确认序号的存在允许了部分确认丢失的情况。例如,当发送方连续发送了序号为1000、2000、3000的数据包,而接收方仅返回了确认序号为3001的确认包时,即使代表1001、2001的确认序号丢失或未收到,发送方也能确定前两次的请求已经被成功接收。这种容错性极大地增强了通信的可靠性。

  4、 排序与同步: 序号和确认序号在数据传输中起到了关键的排序和同步作用。由于网络传输的复杂性,数据包的到达顺序可能与发送顺序不一致。然而,通过检查报文中携带的序号,接收方能够确保数据的正确顺序,并据此进行响应操作。这种排序机制有效地避免了数据接收顺序不匹配所导致的问题,保证了通信的准确性和一致性。
  
  
  

在这里插入图片描述为什么要分别使用序号、确认序号两个字段?(单独只使用序号字段不行吗?)

  回答:TCP是全双工的通信协议,意味着在任何时刻,通信的双方都可以同时发送和接收数据。
  如果server即想给对方确认应答,又想同时给对方进行发送它的消息。这时候就体现了两个字段的用途。相当于四个字段,两个为一组,分别服务通讯的一方。由此保障了数据的收发。
在这里插入图片描述

  (PS:往往给对方发送消息,本身即为应答。)

  
  
  
  
  
  

9.2、其它TCP报头属性

在这里插入图片描述
  
  
  

9.2.1、16位窗口大小与流量控制(引入篇)

  1)、TCP发送缓冲区和接受缓冲区
  数据发送存在的问题举例: ①丢包、②乱序、③发送太快或太慢。

  对于问题③:与UDP不同,TCP协议既有发送缓冲区,又有接收缓冲区,但需要明确的是,缓冲区的大小是有限的,这意味着发送端和接收端处理数据的速度都受到一定的限制
  如果发送端发的太快,导致接收端的缓冲区被打满,此时若发送端继续发送数据,就会造成丢包,继而引起丢包重传等等一系列连锁反应。同理,如果数据发送太慢,会严重影响通信的整体效率。
在这里插入图片描述
  TCP就是用来解决如何发送/接受的问题,因此称其为传输控制协议。
  
  
  
  2)、初步认识流量控制
  为了解决上述数据传输太快或太慢的问题,TCP协议设计了一种机制,使得传输双方需要给对方同步自己的接收能力。这种接收能力通常由接收缓冲区中剩余空间的大小来衡量。TCP正是基于接收端的处理能力,来动态调整发送端的发送速度,这一机制被称为流量控制(Flow Control)

  细节理解:鉴于TCP协议具备全双工的特性,通讯双方皆享有同时发送与接收数据的能力。这种机制决定了流量控制必然是一个双向互动的过程,是两个方向上的流量控制。
  
  
  
  3)、16位窗口大小的作用
  TCP报文格式中,有一个叫做16位窗口大小的报头字段,其就是用于表示接收端当前可用的缓冲区大小(即剩余空间大小)。这个字段的值是动态变化的,根据接收端缓冲区的实际使用情况而调整。
  
  16位窗口大小与流量控制的关系:后续也会讲到,这里简单了解)
  接收端将自己的缓冲区大小(或剩余空间大小)放入TCP报文段首部的16位窗口大小字段中,并通过ACK报文段通知发送端。
  发送端在收到ACK报文段后,会根据其中的窗口大小字段值来调整自己的发送窗口大小(即允许发送的数据量大小)。
  如果接收端缓冲区快满了,它会将窗口大小设置成一个更小的值通知给发送端;如果接收端缓冲区满了,它会将窗口大小置为0,此时发送端会暂停发送数据,直到收到新的非零窗口大小的ACK报文段。
  
  
  
  
  
  
  
  
  

9.2.2、6位标志位

  基本说明:不同版本可能存在情况不同,这里讲述标准的6位标记位的情况
  
  1)、是什么和为什么
  问题一:为什么需要多个标记位?
  回答:服务端会收到大量的不同的报文,例如,常规报文、建立链接的报文、断开链接的报文、确认报文、等等其他类型的报文。不同报文,其所需要做的处理工作各有千秋,为了方便区别管理以及进行后续处理,使用标记位来标记报文的类型。
  
  
  问题二:各个标记位都是什么含义?
  回答:标记位实际上是宏。
  
在这里插入图片描述

  
  
  2)、相关介绍


  SYN(Synchronize):同步位,用于请求建立连接。我们把携带SYN标识的称为同步报文段,占1位。
  例如:将SYN=1时,服务器收到报文解析后,就能知道当前客户端是想要进行连接,那么后续的处理操作就顺理成章了。
  
  FIN(Finish):终止位,用于结束连接,通知对方本端即将关闭。我们称携带FIN标识的为结束报文段,占1位。
  例如:当FIN=1时,表明此报文段的数据已发送完毕,并要求释放连接。
  
  ACK(Acknowledgment):确认位(确认应答标志位),用于确认收到数据。凡是该报文具有应答特征,该标志位都会被设置为1。
  说明:大部分网络报文的ACK都是被设置为1的。但是有些报文的ACK不会被置为1,比如第一个链接请求报文(原因:既然是首个请求报文,说明历史上客户端和服务器从来没有法发过数据)
  


  
  URG(Urgent):紧急位(紧急标志位),配合16位紧急指针使用,用于指示紧急数据。占1位。
  说明:当URG=1时,表明紧急指针字段有效。它告诉系统此报文段中有紧急数据,应尽快发送(相当于高优先级的数据),而不要按原来的排队顺序来传送。要与首部中紧急指针字段配合使用。
  
  PSH(Push):推送位,用于立即传送数据给接收端。提示接收端应用程序立刻从TCP缓冲区把数据读走。占1位。
  说明:当两个应用进程进行交互式通信时,有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应。在这种情况下,TCP就可以使用 推送(push) 的操作。这时,发送方TCP把PSH置为1,并立即创建一个报文段发送出去,接收方TCP收到PSH=1的报文段,就尽快地交付接收应用进程,而不用再等到整个缓存都填满了后再向上交付。
  
  RST(Reset):复位位,用于强制关闭连接。 我们把携带RST标识的称为复位报文段,占1位。
  说明:当RST=1时,表明TCP连接中出现了严重错误(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立传输连接。RST置为1还用来拒绝一个非法的报文段或拒绝打开一个连接。


  
  
  
  

9.2.3、其余报头简述

  1)、保留
  保留(Reserved):在TCP固定头部结构中,这个字段并不直接作为一个独立的字段出现,而是包含在TCP头部的其他字段中。例如,在TCP头部长度字段(通常占4位)之后,可能会有一段保留位(Reserved Bits),这些位在当前TCP协议版本中可能没有被使用,但为未来扩展预留了空间

  在当前版本的TCP协议中,这些保留位通常被设置为0,表示它们当前没有被使用。然而,不同的TCP实现可能会对这些保留位有不同的处理方式,因此在编写与TCP协议相关的代码时,需要注意这些差异。
  随着网络技术的不断发展,TCP协议可能需要进行相应的更新和扩展,以适应新的应用场景和需求。这些保留位可以为未来的扩展提供灵活性。
  
  
  

  2)、校验和
  TCP校验和( Checksum): 主要目的是确保数据在传输过程中没有被修改或损坏。发送方在发送数据之前计算数据的校验和,并将其附加在TCP报文段上。接收方在接收到数据后,会重新计算数据的校验和,并与发送方发送的校验和进行比较,以验证数据的完整性。
  
  
  

  3)、紧急指针
  紧急指针(Urgent Pointer): 用于处理TCP连接中的紧急数据。该字段长为16位,只有在URG控制位为1时有效
  紧急指针的值是一个相对于当前序列号的偏移量,用于指示紧急数据在整个数据流中的位置。这个偏移量表示从当前序列号开始的字节数,指向紧急数据的最后一个字节的下一个字节。

  接收端在收到紧急指针后,会中断正在处理的数据流,以便立即处理紧急数据。一旦处理完紧急数据,接收端会恢复正常的数据流处理。

  需要注意的是,紧急指针的使用并不常见,而且需要双方协商和支持。在实际应用中,它通常用于传递一些重要的控制信息或紧急指令,而不是用于大量的数据传输。例如在Web浏览器中点击停止按钮,或者使用TELNET输人Ctrl + C时都会有URG为1的包。此外,紧急指针也用作表示数据流分段的标志。
  
  
  
  
  
  
  
  

9.3、TCP建立和断开连接(三次握手和四次挥手)

在这里插入图片描述

9.3.1、如何理解连接

  因为有大量的客户端将来可能连接服务器,所以服务端一定会存在大量的连接。对于这些连接,操作系统也需要对它们进行管理,其管理的纲领无非为【先描述,在组织】。
在这里插入图片描述

  因此,所谓的连接,本质是内核的一种数据结构类型建立连接成功的时候,就是在内存中创建对应的连接对象,再对多个连接对象进行某种数据结构的组织(对连接的管理,即对这些连接对象的增删查改操作)。
  
  需要注意的是,维护连接是有成本的(内存 + cpu资源)
  
  
  
  
  

9.3.2、如何理解三次握手

9.3.2.1、基本描述

  1)、总览
  相关文章:深入浅出TCP三次握手

  
  目的说明: TCP三次握手是浏览器和服务器建立连接的方式,目的是为了使二者能够建立可靠的连接,确保双方具有数据通信的能力,并同步双方的初始序列号,为后续的数据传输做准备。

  第一次握手:客户端发送带有 SYN 标志的连接请求数据包给服务端;
  第二次握手:服务端发送带有 SYN+ACK 标志的连接请求和应答数据包给客户端;
  第三次握手:客户端发送带有 ACK 标志的应答数据包给服务端(可以携带数据)。
  
  
  
  
  2)、绘图说明
在这里插入图片描述

  准备说明: 所谓的三次握手,指的是TCP连接的建立。在最初时,两端的TCP进程都处于关闭状态CLOSED),之后,为了建立连接,这两个进程将分别创建传输控制块(TCB),这是TCP协议栈中用于管理TCP连接的重要数据结构
  对于客户端, TCP连接建立是由客户端主动发起的,因此称为主动打开连接
  对于服务端, 它属于被动打开连接的一方,服务端会进入监听状态(LISTEN),等待来自TCP客户进程的连接请求。

  
  
  后续:
在这里插入图片描述
  第一次握手(SYN=1, seq=x):客户端发送TCP连接请求。
  1、客户端发送一个SYN报文段到服务器,请求建立连接。之后,客户端进入同步已发送状态(SYN_SEND),等待服务器的确认。
  2、这里,SYN = 1表示这是一个连接请求,seq = x是客户端所选择的初始序列号。
  
  
  第二次握手(SYN=1, ACK=1, ack=x+1, seq=y):服务端发送针对TCP连接请求进行确认的报文。
  1、服务器收到客户端的 SYN 报文段后,根据序列号和反序列号,设置 SYN=1ACK=1,表示这是一个 SYN 握手和 ACK 确认应答报文。在将该报文(SYN+ACK)发给客户端后,服务端进入同步已接收状态(SYN_RCVD,received的缩写)。
  2、这里,SYN表示这是一个连接请求,ACK表示确认收到了客户端的SYN报文段。ack = x+1是对TCP客户端所选择的初始序号seq = x的确认(确认序号 = 序号+1)。seq = y是服务器所选择的初始序列号。
  
  
  第三次握手(ACK=1, ack=y+1, seq=x+1):客户端发送确认的确认。
  1、客户端收到服务器的SYN+ACK报文段后,会向服务器发送一个ACK报文段作为应答,表示连接已经建立(这次报文可以携带数据),之后客户端处于连接已建立状态(ESTABLISHED)。服务器收到客户端的应答报文后,也进入连接已建立状态(ESTABLISHED),此后可以开始数据交换。
  2、这里,ACK=1表示确认收到了服务器的SYN+ACK报文段,确认号字段ack = y+1,这是对TCP服务端所选择的初始序号seq=y的确认(即服务器初始序列号+1)。序号字段seq = x+1,这是因为TCP客户端发送的第一个TCP报文段的序号为x,并且不携带数据,此时的第二个报文段的序号为x +1。

  特别说明: 三次握手不一定要保证连接成功,客户端只要将ACK发出,就会变为ESTABLISHED状态,但是只有服务端收到了真正的ACK才会建立真正的连接。
  
  

  3)、注意事项
  1、为什么绘制的图解中是斜线的箭头传输?
  回答:在三次握手期间存在时间成本问题。
  
  2、发送的只是SYN、ACK这样的报文字段吗?
  回答:始终要记住,双方传输的是完整的报文,即包含完整的报头和正文数据(可无)。
  
  
  
  
  
  

9.3.2.2、细节理解

  1、是不是三次握手都一定要保证成功?
  回答:不一定。tcp的可靠性是在建立连接之后。在三次握手期间,存在丢包概率,前两次由于有应答还能获取反馈,最后一次无法保证。
  
  
  

  2、三次握手是对客户端服务端都要起效。 这是保障客户端和服务端状态改变的判断依据。
在这里插入图片描述

  前两次丢包:因为有ACK应答,可以知晓,会根据情况做出后续处理。
  对于第三次丢包:客户端只要发出报文就会改变状态为ESTABLISHED,但它也有可能在中途丢包,导致服务端没有收到第三次报文。
  
  
  
  
  
  3、为什么要三次握手?

在这里插入图片描述问题一:一次握手行不行?

  如下图描述,这种场景下客服端可以写一个死循环,不断请求连接服务器,导致服务器资源充满,最终挂掉。

在这里插入图片描述
  
  

在这里插入图片描述问题二:两次握手行不行?

在这里插入图片描述

  
  如果只进行两次握手,那么服务器只能确认客户端的请求。由于存在丢包等问题,客户端无法保障每次都能收到应答,同理,服务器也无法得知当前响应在客户端的接收情况。
  这种情况下,服务端在返回应答报文后就改变状态进入ESTABLISHED ,假如此处客户端因资源阻塞、恶意攻击等原因不断重复发送 SYN 报文请求,那么服务器在收到请求后就会建立多个冗余的无效链接,浪费TCP服务器的资源,还可能引发所谓的“SYN洪水”问题。
  
  
  

在这里插入图片描述问题三:为什么三次握手就行?

在这里插入图片描述
  此外,三次握手还保证了站在双方立场上,都能够保障全双工(能收&&能发)。(一次握手、两次握手达不到此目的。)
在这里插入图片描述

  
  
  

在这里插入图片描述问题四:四次握手、五次握手……行不行?

在这里插入图片描述

  
  
  
  
  

9.3.3、如何理解四次挥手

9.3.3.1、基本描述

  1)、总览
  相关文章:深入浅出TCP四次挥手

  简述:TCP的四次挥手(也称为连接终止协议)指的是断开一个TCP连接时,客户端和服务器之间需要发送四个数据包以确认连接的断开。由于TCP连接是全双工的,每个方向的数据传输都需要单独关闭。四次挥手的目的就是确保在关闭连接时,每个方向的数据都能得到完整传输,并且连接能被正常终止

  第一次挥手:客户端通过发送FIN报文段来发起关闭连接的请求。
  第二次挥手:服务器通过发送ACK报文段来确认收到客户端的关闭请求。
  第三次挥手:服务器通过发送FIN报文段来发起自己的关闭请求。
  第四次挥手:客户端通过发送ACK报文段来确认收到服务器的关闭请求,并等待一段时间后关闭连接。这样,双方就完成了四次挥手的过程,TCP连接被完全关闭。
  
  
  
  2)、绘图说明
  
在这里插入图片描述

  第一次挥手(FIN=1,seq=u):
  客户端发出断开连接请求FIN 报文(其中 FIN 置为 1 并且随机生成序号 u 并填充进序号字段),告诉服务器客户端已经没有更多数据要发送了,希望关闭连接。随后,客户端进入 FIN-WAIT1 状态(此时客户端已经无法发送应用层数据给客户端,但可以发送响应报文)

  

  第二次挥手(ACK=1,ack=u+1,seq=v):
  ①服务端接收到客户端的FIN请求报文段后,发送一个ACK应答报文段给客户端(该ACK报文段是对客户端FIN报文段的确认),此时服务端进入CLOSE_WAIT状态,表示服务器已经同意关闭连接,但是还在等待是否有遗留数据需要发送给客户端。②客户端接受到来自服务端的ACK应答报文后,进入FIN_WAIT_2状态。

  

  第三次挥手(FIN=1,seq=w):
  服务端处理完数据后,向客户端发送一个断开连接的FIN请求报文段,此后服务端进入LAST_ACK状态,等待客户端的确认。

  

  第四次挥手:(ACK=1,ack=w+1,seq=u+1)
  ①客户端收到服务器的FIN报文段后,会发送一个ACK报文段(给服务器,表示对服务器FIN报文段的确认。此后客户端进入TIME-WAIT状态,等待一段时间后(通常是2MSL,MSL是最大段生存期)进入CLOSED状态,表示连接已经完全关闭。②服务器收到客户端的ACK报文段后,也进入CLOSED状态,表示连接已经完全关闭。
  
  
  
  
  
  
  

9.3.3.2、细节理解

在这里插入图片描述CLOSE_WAIT状态介绍

  CLOSE_WAIT对方主动关闭连接或者网络异常导致连接中断,这时我方的状态会变成CLOSE_WAIT,此时在编写网络应用程序时我方要调用close()来使得连接正确关闭。(属于被动关闭)

  1、如果我们发现服务器具有大量的CLOSE_WAIT状态的连接的时候,原因是什么?
  主要原因:应用层服务器写得有bug,例如忘了关闭对应的连接sockfd。 (客服端和服务端只有完成四次挥手后连接才算真正断开,此时双方才会释放对应的连接资源。如果服务器接收到两次挥手后不进行调用close()函数,那么服务器端就会存在大量处于CLOSE_WAIT状态的连接。)
  
  演示代码如下:(借助了先前的代码快速演示)

#include "Sock.hpp"

int main()
{
    Sock sock;
    int listensock = sock.Socket(); // 创建套接字
    sock.Bind(listensock, 8080);    // 绑定端口号
    sock.Listen(listensock);        // 监听
    while (true)
    {
        uint16_t clientport;
        std::string clientip;
        int sockfd = sock.Accept(listensock, &clientip, &clientport);// 连接
        if(sockfd > 0)
        {
            std::cout << "[" << clientip << ":" << clientport << "]# " << sockfd << std::endl;
        }
       //这里我们没有关闭套接字 
    }

    return 0;
}

  
  演示结果如下:
在这里插入图片描述

  
  说明:close_wait的危害在于,在一个进程上打开的文件描述符超过一定数量时,新来的socket连接就无法建立了,因为每个socket连接也算是一个文件描述符。(报错:Too many open files。)
  
  
  
  

在这里插入图片描述TIME_WAIT状态介绍

  1、TIME_WAIT:我方主动调用close()断开连接,收到对方确认后状态变为TIME_WAIT。TCP协议规定TIME_WAIT状态会一直持续2MSL(即两倍的分段最大生存期),以此来确保旧的连接状态不会对新连接产生影响。
  
  相关演示:

#include "Sock.hpp"

int main()
{
    Sock sock;
    int listensock = sock.Socket(); // 创建套接字
    sock.Bind(listensock, 9090);    // 绑定端口号
    sock.Listen(listensock);        // 监听
    while (true)
    {
        uint16_t clientport;
        std::string clientip;
        int sockfd = sock.Accept(listensock, &clientip, &clientport);// 连接
        if(sockfd > 0)
        {
            std::cout << "[" << clientip << ":" << clientport << "]# " << sockfd << std::endl;
        }
       //这里我们没有关闭套接字 
       std::cout << "即将关闭套接字" <<std::endl;
       sleep(10);

       close(sockfd);

       std::cout << "套接字已经关闭:" << sockfd <<std::endl;
       
    }

    return 0;
}

  演示结果:

在这里插入图片描述
  
  

  2、细节说明主动断开连接的一方要维持一段时间的TIME_WAIT状态。在该状态下,连接其实已经释放,但是地址信息ip, port依旧是被占用的(即处于TIME_WAIT状态的连接占用的资源不会被内核立马释放)。所以,当服务器断开后立即重新启动时,经常会看到提示绑定失败。

在这里插入图片描述
  
  问题描述: 在上述,服务器的TCP连接没有完全断开之前不允许重新监听,这在某些情况下可能是不合理的。

  • 服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短,但是每秒都有很大数量的客户端来请求),这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃,就需要被服务器端主动清理掉),就会产生大量TIME_WAIT连接。
  • 由于我们的请求量很大,就可能导致TIME_WAIT的连接数很多。而每个连接都会占用一个通信五元组(源ip、源端口、目的ip、目的端口、协议),其中服务器的ip、端口号、协议是固定的,如果新来的客户端连接的ip、端口号和TIME_WAIT状态占用的连接重复了,就会出现问题。

  解决TIME_WAIT状态引起的bind失败的方法 :使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同、但IP地址不同的多个socket描述符。

        int opt = 1;
        setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

  
  man setsockopt:该函数相关使用介绍,setsockopt 函数功能及参数详解

NAME
       getsockopt, setsockopt - get and set options on sockets

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int getsockopt(int sockfd, int level, int optname,
                      void *optval, socklen_t *optlen);
       int setsockopt(int sockfd, int level, int optname,
                      const void *optval, socklen_t optlen);

  
  在创建套接字时设置。

    // 创建套接字:int socket(int domain, int type, int protocol);
    int Socket()
    {
        int listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (listensock < 0)
        {
            logMessage(FATAL, "socket:创建套接字失败。%d:%s", errno, strerror(errno));
            exit(2); // 退出
        }
        //设置setsockopt套接字选项
        int opt = 1;
        setsockopt(listensock,SOL_SOCKET,SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
        logMessage(NORMAL, "socket:创建套接字成功, listensock:%d", listensock);
        return listensock; // 将套接字返回给TcpServer中的成员函数_listensock
    }

  
  
  
  3、回答为什么: 为什么要有TIME_WAIT这一段等待时间?

  • 可靠地终止 TCP 连接:网络是不可靠的,一个TCP报文段在传输过程中可能会丢失或者延迟到达,因此需要等待一段时间,确保双方都已经收到对方的ACK报文段,防止出现重复的连接或者错误的连接。
  • 保证让迟来的TCP报文段有足够的时间被识别并丢弃。数据报文可能在发送途中延迟但最终会到达,因此要等延时的报文段或尚未处理的报文在通讯双方断开连接前来得及接收并做出处理,避免造成数据错乱。
    在这里插入图片描述

  
  扩展:为什么是2MLS?
在这里插入图片描述
  
  
  
  
  
  
  
  

9.4、TCP可靠性的其他策略

9.4.1、确认应答(ACK)机制

  1)、理解确认应答机制
  概念介绍:在 TCP 协议中,发送方发送数据后,接收方需要对数据进行确认应答(ACK,acknowledge的缩写),以确保数据已经被正确接收。
在这里插入图片描述

   确认应答机制的基本原理:

  • 发送方将数据分割成称为TCP段(TCP segment) 的较小单元,并为每个段分配一个唯一的序列号。
  • 发送方将这些TCP段发送给接收方,并启动一个定时器来跟踪每个已发送段的确认
  • 接收方收到TCP段后,将按序将它们重新组装成完整的数据流,并发送一个确认(ACK)给发送方。确认中包含接收到的最高序列号,表示该序列号之前的所有数据都已正确接收。
  • 发送方在接收到确认后,会停止相应定时器,并继续发送下一个序列号的TCP段。
  • 如果发送方在定时器超时之前未收到确认,它将重新发送未确认的TCP段

  
  
  

  2)、理解序号、确认序号的来由
  序列号:发送方发送的每个TCP段都包含一些字节的数据,TCP为每个数据字节分配一个唯一的编号以进行标识,此唯一编号称为TCP序列号。
  反序列号:每一个ACK都带有对应的确认序列号,意思是告诉发送者,目前已经收到了哪些数据,下一次该从哪里开始发。
  
  PS:可以将其想象成字节流式的数组,序号代表位置即数组下标位置。
在这里插入图片描述
  
  发送缓冲区与序号的关系: 发送缓冲区中的每个字节都会被赋予一个序号。当发送端从发送缓冲区中取出数据并封装成TCP报文段进行发送时,该报文段的序号即为发送缓冲区中该字节的序号。这样,接收端就可以根据接收到的报文段的序号来判断数据包的顺序,并据此进行数据处理。
在这里插入图片描述

  对上述的详细解释:如何将发送缓冲区中的字节编号以获取报文序号?
  初始化: 在TCP连接建立时,发送端和接收端会协商一个初始序号。这个初始序号通常是一个随机值,用于防止序号冲突。
  发送数据: 当发送端有数据需要发送时,它会将数据放入发送缓冲区中,并为每个字节分配一个序号。这个序号等于初始序号加上该字节在发送缓冲区中的相对位置(即偏移量)。
  封装报文段: 当发送端从发送缓冲区中取出一段数据(即一个报文段)进行发送时,它会将该报文段的第一个字节的序号作为整个报文段的序号。这个序号将被封装在TCP报文段的头部,并随数据包一起发送到接收端。
  接收数据: 接收端在接收到数据包后,会根据TCP报文段头部的序号来判断数据包的顺序。如果数据包的序号与接收端期望的下一个数据包的序号一致,则接收端会将该数据包中的数据放入接收缓冲区中,并更新期望的下一个数据包的序号。如果数据包的序号与期望的序号不一致(即发生了乱序或丢包),则接收端会采取相应的处理措施(如重传请求、丢弃数据包等)。
  
  
  

  3)、为什么引入需要序列号和反序列号?
  回答:
  1、TCP传输中存在 ①发送端发送的数据丢包、②接收端发送的确认应答ACK丢包或延迟等等各种行为,导致发送方和接收方数据在相应时间段内对接不上,出现数据重发的现象(即下述要介绍的超时重传)。
  2、虽然源主机可以通过重发数据来提供可靠的传输,但对于目标主机而言,反复收到相同的数据既浪费网络资源,还要耗资源对它处理。所以,我们需要一种机制来识别是否已经接收到了这个数据包、又能够判断数据包是否需要接收
  3、因此,TCP协议引入序列号,按顺序给发送数据的每一个字节(8位字节)都标上号码的编号(序列号的初始值并非为0。而是在建立连接以后由随机数生成。而后面的计算则是对每一字节加一) 。接收端查询接收数据TCP首部中的序列号和数据的长度,将自己下一步应该接收的序号作为确认应答返送回去,以此保证数据的顺序和完整性。
在这里插入图片描述

  相关文章:确认应答机制与超时重发机制
  
  
  
  
  
  
  
  

9.4.2、超时重传机制

  1)、什么是超时重传
  基本说明: 重传超时是指在重发数据之前,等代等待确认应答到来的那个特定时间间隔。如果超过了这个时间仍未收到确认应答,发送端将进行数据重发
  

  详述:
  1、TCP协议为了确保数据的可靠传输,引入了超时重传机制。在TCP发送数据后,会启动一个计时器,这个计时器设定的时间间隔被称为重传超时(RTO,Retransmission Timeout)。如果在计时器超时之前,发送端没有收到接收端对于发送数据的确认信息(ACK),则认为该数据包可能在网络中丢失或损坏,此时发送端会重传该数据包。
  2、在重传超时发生后,发送端会重新发送之前未确认的数据包,并重新启动计时器等待确认。如果重传后仍然未收到确认信息,发送端可能会按照某种策略(如指数退避算法)继续增加重传超时的值,并再次重传数据包,直到数据包被成功接收或达到最大重传次数为止。(下述有图示介绍)

  
  

  2)、两种导致超时重传的情况介绍

情况一:数据真实丢包
情况二:ACK应答丢包,导致接收方收到重复数据

在这里插入图片描述
  
  

  3)、问题:如何设置超时重传的时间?
  1、最理想的情况下,找到一个最小的时间保证“确认应答一定能在这个时间内返回”
  2、但是这个时间的长短随着网络环境的不同是有差异的。如果超时时间设的太长,会影响整体的重传效率;如果超时时间设的太短,有可能会频繁发送重复的包。(PS:由此我们可知超时时间不能固定,在网络好的时候,超时时间应该短一些,网络不好的时候,超时时间应该长一些)
  3、TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间

  
在这里插入图片描述
  补充说明:
  1、Linux中(BSDUnix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。
  2、如果重发一次之后仍然得不到应答,等待 2 × 500 m s 2×500ms 2×500ms 后再进行重传。如果仍然得不到应答,等待 4 × 500 m s 4×500ms 4×500ms 进行重传。依次类推, 以指数形式递增。累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。

  
  
  
  
  
  

9.4.3、流量控制

  1)、为什么需要流量控制?
  说明:接收端处理数据的速度是有限的,如果发送端发的太快导致接收端的缓冲区被打满,这个时候如果发送端继续发送就会造成丢包,继而引起丢包重传等等一系列连锁反应。因此,TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制(Flow Control)
  相关视频讲解:流量控制
在这里插入图片描述

  1、接收端自己当前可用的接收缓冲区大小放入 TCP 首部中的 “16位窗口大小” 字段,通过ACK数据段的形式通知发送端。这个窗口大小字段的数值越大,说明网络的吞吐量越高。
  2、接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值,并通过ACK段将此信息通知给发送端。发送端接收到这个信息后(更新后的窗口大小),就会相应地减慢自己的发送速度,以确保不会因发送过快而导致接收端缓冲区溢出。
  3、如果接收端缓冲区满了,就会将窗口大小设置为0,并通过ACK段告知发送端。在这种情况下,发送端会暂停数据的发送,但为了保持连接的活跃状态并探测接收端窗口何时重新开放,发送端会定期发送一个窗口探测数据段(也称为零窗口探测报文),让接收端把窗口大小告诉发送端。(当然,在某些情况下,接收端也可以主动发送一个窗口更新通知数据段,以提前告知发送端可以开始发送新数据了。)
  
  
  
  

  2)、相关问题说明

在这里插入图片描述1、一个基本认识

  在之前介绍16位端口号时我们也讲过,TCP是全双工的,通讯双方皆享有同时发送与接收数据的能力,这就决定了流量控制必然是一个双向互动的过程,是两个方向上的流量控制。
  具体而言,当主机A向主机B发送数据时,发送的数据量受限于主机B接收缓冲区的剩余空间大小;相应地,当主机B向主机A发送数据时,其发送的数据量则受限于主机A接收缓冲区的剩余空间大小
  
  
  
  

在这里插入图片描述2、流量控制中,第一次发送数据时,通讯双方如何知道对方的接收能力?

  回答: 第一次发送数据 ≠ ≠ = 第一次交换报文。第一次报文交换是在三次握手期间,这期间通讯双方是不进行数据交换的,但可以在对应报文中附带我方当前的接收能力信息(TCP首部中有一个16位窗口字段,存放了窗口大小信息)。
  
  扩展: 16位数字最大表示65535,是否意味着TCP窗口最大就是65535字节?
  回答:实际上TCP首部40字节选项中,还包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移 M 位;
  
  
  
  

在这里插入图片描述3、当接收方缓冲区满时,后续发送方如何知道什么时候可以再次发送数据?

  回答:通过窗口探测和窗口更新通知。
  
  扩展:为什么需要窗口更新通知? 假设窗口探测间隔为1s,但是窗口立马更新好了,此时要等待1s后再进行通信,无疑是降低效率的行为。因此,设置窗口更新通知,可以让接收方在更新接收缓冲区的接收能力后,立马通知对方。(双重保障的灵活使用)
  
  
  
  
  
  
  
  

9.4.4、滑动窗口 && 快重传

  1)、滑动窗口是什么?(基本介绍)
  问题引入: 对每一个发送的数据段,都要给一个ACK确认应答,收到ACK后再发送下一个数据段。这样做有一个比较大的缺点就是性能较差,尤其是数据往返的时间较长的时候。
  
  为解决这个问题,TCP引入了窗口这个概念。即使在往返时间较长的情况下,它也能控制网络性能的下降。确认应答不再是以每个分段,而是以更大的单位进行确认时,转发时间将会被大幅度的缩短。也就是说,发送端主机在发送了一个段以后不必要一直等待确认应答,而是继续发送
在这里插入图片描述

  
  相关机制说明:
在这里插入图片描述

  注意事项:
  1、数据发出后,如果收到确认应答,就可以从缓冲区清除这部分数据。如果没有收到确认应答,必须在缓冲区保留这部分数据。
  2、尽管滑动窗口一次能向对方发送多条数据段,但这里一次发送的“多条数据”也是有上限的,而这个上限取决于对方当前的接收能力(由上一次ACK报文中携带首部信息)。换句话说,滑动窗口的大小是动态变化的(有上限),其与之前讲的流量控制二者并不冲突

  
  
  
  2)、滑动窗口的完善理解(建立一个认知滑动窗口的模型)

  问题一:滑动窗口的本质是什么?
  回答:实际上,滑动窗口在自己的发送缓冲区中,是属于自己的发送缓冲区中的一部分。而根据之前所学,缓冲区是一个类似于数组的结构,因此滑动窗口本质就是数组中的一段范围区域,以指针或下标表示范围
在这里插入图片描述
  需要注意,这里滑动窗口的大小拥塞窗口大小与对方接收缓冲区的接收能力大小的比较值。(关于拥塞窗口,后续会介绍的)
  
  
  问题二:滑动窗口是否总是向右移动?滑动窗口的大小能否为零?
  回答:滑动窗口的移动方向并不总是固定右滑的。在某些情况下,滑动窗口可能不会向右移动。 例如,当接收方在一定时间内未能及时读取接收缓冲区中的数据,而发送方持续发送数据时,由于接收缓冲区的有限大小,发送方在接收到ACK(确认)报文后,可能会遇到这样的情况:下一次的滑动窗口左边界(即左指针或左下标)向右偏移,而右边界(即右指针或右下标)保持不变。这是因为发送方需要等待接收方读取并确认更多数据以释放缓冲区空间。
  至于滑动窗口的大小,它确实有可能为零 当接收缓冲区已满,并且接收方尚未确认并处理足够的数据以释放空间时,滑动窗口的大小将减小到零。这表示发送方需要暂停发送数据,直到接收方释放足够的缓冲区空间来接收新的数据。
在这里插入图片描述

  
  
  
  问题三:若滑动窗口一直向右滑动,是否存在越界问题?
  回答:不存在。TCP的缓冲区实则是环状结构的(线性结构模拟环状结构),会根据实际大小做取模处理。
  
  
  问题四:发送方一次发送多个数据段,①若接收方最终都接收到了报文数据,但返回时某一个ACK应答报文丢包了,是否存在影响?②若是发送方发送的报文丢包了呢?
  回答:
  对情况①,数据包已经抵达,ACK丢了。在使用窗口控制时,是不需要进行重发的。
  对情况②,数据包丢失。当某段报文丢失的时候,当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 "我想要的是 1001"一样;如果发送端主机连续三次收到了同样一个 “1001” 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中。这种机制也被称为 “高速重发控制”

在这里插入图片描述
  
  
  
  
  
  
  
  

9.4.5、高速重发机制:快重传

  说明:快速重传(Fast Retransmission)是一种提高TCP性能的重传策略。当接收方连续收到三个相同序号的确认报文(Duplicate ACKs)时,发送方会认为对应的数据包发生了丢失。为了尽快补发丢失的数据包,发送方会立即进行重传,而不再等待重传计时器超时。这种方法可以减小因数据包丢失导致的延迟。
  
  
  思考问题:既然有快重传,为什么还需要超时重传?(快重传与超时重传的区别)
  回答: 快重传机制在特定情况下能够高效地重传丢失的数据段,但也有其局限性。此时就需要超时重传,因此,二者实则为相辅相成的作用(并不冲突),共同保障了数据的可靠传输。以下是两种可能需要依赖超时重传的情况:
  ①、不满足快重传触发条件: 在某些情况下,由于网络状况、接收方的处理能力或其他因素,发送方可能连续发送的报文段数量少于三个。在这种情况下,即使数据段丢失,快重传机制也不会被触发,因为不满足其要求的三个及以上重复确认的条件。
  ②、确认应答报文丢失: 另一个可能的情况是,接收方已经发送了三个或更多重复的确认报文来指示某个数据段的丢失,但这些确认应答报文在网络传输过程中可能因各种原因(如网络拥塞、错误或丢失)而未能到达发送方。在这种情况下,快重传机制同样无法被触发,因为发送方没有收到足够的重复确认来识别数据段的丢失。此时,就需要依赖超时重传机制来确保数据的可靠传输。
  
  
  
  
  
  
  
  

9.4.6、慢启动机制 && 拥塞窗口

  1)、网络状态带来的问题说明
  问题引入: 上述介绍到的几种机制,能为我们提升TCP的可靠性,然而,这些机制主要聚焦于端到端之间的通信可靠性。要知道计算机网络都处在一个共享的环境,因此,网络的健康状态也会对TCP数据传输造成影响。 若当前的网络状态出现拥堵,在不清楚当前网络状态下贸然发送大量的数据,很有可能让当前的网络状态雪上加霜。
在这里插入图片描述

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

  
  
  问题1:如何知道网络拥塞? (少量丢包 VS 大量丢包)

少量丢包时:可能是当前通讯双方的主机数据传送的问题。
大量丢包时:可能是网络出现问题,如网络拥塞。

类比课程挂科:
挂科率极低,说明本次考试试卷处于正常水平,极大概率下是学生主观因素造成的;
若几乎全班都挂了,说明本次考试试卷严重超纲,属于教学事故,任课老师和评阅老师都要承担责任。

  
  
  问题2:网络拥塞是否还能够重传?为什么?
  回答:网络拥塞时若再重传数据,虽然一个主机传送的数据大小看起来无足轻重,但要知道网络中不止一个主机,拥塞是对当前区域内所有主机而言的,假若这些主机都不分情况一律向拥塞的网络中传送数据,就好比交通堵塞的道路不断涌入车辆,不利于疏通
  
  
  
  问题3:如何解决网络拥塞问题?
  慢启动机制和拥塞窗口。
  
  
  
  2)、解决方案说明

  拥塞窗口:拥塞窗口是用于在发送端精细控制数据传输量的一个重要概念。具体来说,拥塞窗口(cwnd)是发送方维护的一个状态变量,它设定了单台主机一次向网络中发送大量数据时,可能会引发网络拥塞的上限值,该值会根据网路拥堵情况动态调节

  • ①当发送开始时,拥塞窗口的大小被初始化为1。之后,每当发送方收到一个ACK应答(表示数据已被成功接收),拥塞窗口的大小就会增加1,以此逐步扩大数据传输的速率。
  • ②每次发送数据包的时候,发送方会将拥塞窗口大小和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口,即滑动窗口大小 = min(拥塞窗口大小,16位窗口大小[对方接收能力] );
      
      拥塞窗口变化规则如下: ①只要网络中没有出现阻塞,窗口就会增大;②网络中出现阻塞,窗口就会减小。
      
      

  慢启动机制:为了预防网络拥塞问题的发生,TCP协议在通信的初始阶段会采用“慢启动”算法 得出数值。该算法通过逐步增加发送数据的数量,来精细控制数据的传输速率。
在这里插入图片描述

举例:
1、一开始初始化 cwnd = 1,表示可以传一个 MSS 大小的数据。
2、当收到一个 ACK 确认应答后,cwnd 增加 1,于是一次能够发送 2 个
3、当收到 2 个的 ACK 确认应答后, cwnd 增加 2,于是就可以比之前多发2 个,所以这一次能够发送 4 个
4、当这 4 个的 ACK 确认到来的时候,每个确认 cwnd 增加 1, 4 个确认 cwnd 增加 4,于是就可以比之前多发 4 个,所以这一次能够发送 8 个。
5、……如此下去,即一个指数级别的增长。

  细节理解:同上述,始终要有一个宏观角度,网络中主机不止一台,一个主机的慢启动机制所带来的效果微乎其微,但所有主机遵守相同的协议机制,网络拥塞时因慢启动机制和拥塞窗口的存在,会减少数据量的发送,于是就可达到“星星之火,可以燎原”的效果。
  
  


  拥塞窗口(cwnd)与慢启动的关系: 拥塞窗口的大小直接受到慢启动机制的影响。在慢启动阶段,拥塞窗口的大小会随着每次收到的ACK信号而指数级增长。为了不能让拥塞窗口单纯倍增式增长,引入慢启动的阈值(ssthresh),当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长。 此外,当网络出现拥塞时,TCP协议会通过降低慢启动阈值和将拥塞窗口重置为1来响应,并重新开始慢启动过程。
在这里插入图片描述

当 cwnd < ssthresh 时,使用慢开始算法。
当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。
当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞避免算法。

  
  
  细节理解: 仔细理解上述慢启动机制,我们说过它是一种指数级别的增长方式,具有前期增长慢,后期增长快的特点。那么问题来了,面对网络拥塞的情境,为何会采用这种初期增长缓慢,而后增速迅猛的策略呢?
  回答: 一旦发生拥塞,①前期,给予网络足够的缓冲时间,主机发送的数据量应当被严格控制,既慢且少,以避免进一步加剧拥塞;②中后期随着网络状况的逐渐改善,为了恢复并维持通信的效率,发送的数据量需要快速增加。此时,指数增长的方式便显得尤为适宜。
在这里插入图片描述
  
  


  拥塞避免算法:就是将原本慢启动算法的指数增长变成线性增长,本质还是增长阶段,只是增长速度缓慢了一些。若这样一直增长下去,网络就会慢慢进入拥塞状况,于是会出现丢包现象,这时就需要对丢失的数据包进行重传。当触发了重传机制,也就进入了「拥塞发生算法」(慢启动阈值会变成原来的一半,同时拥塞窗口置回1)。

  需要注意的是,丢包事件也可以被 3 个连续的 ACK 触发,但是这种丢包行为不能代表网络及其拥塞,而是还能够传输报文段。因此对于此情况 TCP 的反应不会那么剧烈,首先将 cwnd 减半,将 ssthresh 的值记录为 cwnd 的一半,进入快速恢复的阶段。
在这里插入图片描述

  需要指出,“拥塞避免”并非指完全能够避免了拥塞,利用以上的措施要完全避免网络拥塞还是不可能的。“拥塞避免”是说在拥塞避免阶段把拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞。
  
  
  一个具体举例:相关文章链接
在这里插入图片描述

  当 TCP 连接进行初始化时,将拥塞窗口置为 1,图中的窗口单位不使用字节而使用报文段。慢开始门限的初始值设置为 16 个报文段,即 ssthresh = 16,发送端的发送窗口不能超过拥塞窗口 cwnd 和接收端窗口 rwnd 中的最小值。我们假定接收端窗口足够大,因此现在发送窗口的数值等于拥塞窗口的数值。
  在执行慢开始算法时,拥塞窗口 cwnd=1,发送第一个报文段。发送方每收到一个对新报文段的确认 ACK,就把拥塞窗口值加 1,然后开始下一轮的传输(请注意,横坐标是传输轮次,不是时间)。因此拥塞窗口 cwnd 随着传输轮次按指数规律增长。
  当拥塞窗口 cwnd 增长到慢开始门限值 ssthresh 时(图中的点 2,此时拥塞窗口cwnd = 16),就改为执行拥塞避免算法,拥塞窗口按线性规律增长。当拥塞窗口 cwnd = 24 时,网络出现了超时,发送方判断为网络拥塞。于是调整门限值 ssthresh = cwnd / 2 = 12,同时设置拥塞窗口 cwnd = 1,进入慢开始阶段。按照慢开始算法,发送方每收到一个对新报文段的确认 ACK,就把拥塞窗口值加 1。当拥塞窗口 cwnd = ssthresh = 12 时(图中的点 3,此时拥塞窗口 cwnd = 16),改为执行拥塞避免算法,拥塞窗口按线性规律增大。
  当拥塞窗口 cwnd = 16时,出现了一个新的情况,就是发送方一连收到 3 个对同一个报文段的重复确认(图中记为3-ACK)。发送方改为执行快重传和快恢复算法。因此在图的点 4,发送方知道现在只是丢失了个别的报文段,于是不启动慢开始,而是执行快恢复算法。这时,发送方调整门限值 ssthresh = cwnd / 2 = 8,同时设置拥塞窗口cwnd = ssthresh = 8(见图中的点5),并开始执行拥塞避免算法。
  
  
  
  
  
  
  
  
  

9.4.7、延迟应答

  1)、为什么需要有延迟应答
在这里插入图片描述
  
  
  2)、介绍延迟应答
  如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小:

  假设接收端的缓冲区容量为1MB,当它一次性接收到500KB的数据时,如果立即发送应答,返回的窗口大小将是剩余的500KB。但在实际应用中,如果接受端处理数据的速度非常迅速,够在极短的时间内(如10毫秒)就将这500KB的数据从缓冲区中读取走,那么接收端的处理能力尚未达到其极限。在这种情境下,即使窗口大小放大一些,接收端也能有效处理。
  如果接收端选择稍微等待一会再发送应答。例如,如果选择等待200毫秒再发送应答,此段时间足以让上层将接收缓冲区中的遗留数据读走,那么此时ACK返回的窗口大小就是完整的1MB。而窗口越大,网络吞吐量就越大,传输效率就越高,我们的目标是在保证网络不拥塞的情况下,尽量提高传输效率。

  

  问题:那么所有的数据包都可以延迟应答么?
  回答:并非如此,延迟应答也有其规则。
  ①首先,存在数量限制:即每隔一定数量的数据包(N个)后,接收端会发送一次应答。
  ②其次,还有时间限制:即如果超过了预设的最大延迟时间,即便未达到数量限制,接收端也会发送应答。
  ③最后,这些具体的数量和超时时间设置会因操作系统的不同而有所差异。一般来说,N通常被设定为2,而超时时间则常设为200ms(毫秒)。这些设置旨在平衡网络效率和响应速度,确保网络传输的顺畅进行。
在这里插入图片描述

  
  延迟应答的另一个便捷之处:比如服务端收到客户端的数据后,不是立刻回ACK给客户端,而是等一段时间(一般最大200ms),这样如果服务端要是有数据需要发给客户端,那么这个ACK就和服务端的数据一起发给客户端了,这样比立即回给客户端一个ACK节省了一个数据包。
  像上述这样的机制,即捎带应答(下述具体介绍)。
  
  
  
  
  
  
  
  

9.4.8、捎带应答

  基本介绍:很多情况下,客户端服务器在应用层是 “一发一收” 的,意味着客户端给服务器发送了一个数据报文,服务器也会给客户端回复相应数据,若此时ACK搭上顺风车和服务器回应一起回给客户端。类似这样TCP的确认应答和回执数据通过一个包发送的方式,叫做捎带应答。
  
在这里插入图片描述

  PS:如果接受数据立刻返回确认应答,就无法实现捎带应答,而是将所接受的数据传送应用处理生成返回数据以后在进行发送请求为止。所以捎带应答需要延迟应答一起配合实现提高网络效率,通过这种机制,可以使收发的数据量减少。
  
  由于捎带应答的存在,四次挥手,可以合并成三次挥手
在这里插入图片描述

  • 当被动关闭方在 TCP 挥手过程中,「没有数据要发送」 并且 「开启了 TCP 延迟确认机制」 ,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。此时主动关闭的一方会直接从 FIN_WAIT_1 状态转入 TIME_WAIT 状态。
    在这里插入图片描述
      
      
      
      
      
      

9.4.9、一个小结

  TCP协议在保证可靠性的同时,也通过一系列机制来提高性能。
  可靠性:

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

  提高性能:

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

  其他:

定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)

  
  
  
  
  
  
  
  

9.5、其它问题说明

9.5.1、面向字节流

  1)、UDP、TCP协议说明

  UDP协议面向数据报:当用户消息通过 UDP 协议传输时,操作系统不会对消息进行拆分,在组装好 UDP 头部后就交给网络层来处理,所以发出去的 UDP 报文中的数据部分就是完整的用户消息。也就是每个 UDP 报文就是一个用户消息的边界,这样接收方在接收到 UDP 报文后,读一个 UDP 报文就能读取到完整的用户消息。
  
  TCP是一种流协议(stream protocol):这就意味着数据是以字节流的形式传递给接收者的,没有固有的”报文”或”报文边界”的概念。
  当用户消息通过 TCP 协议传输时,消息可能会被操作系统分组成多个的 TCP 报文,也就是一个完整的用户消息被拆分成多个 TCP 报文进行传输。这时,接收方的程序如果不知道发送方发送的消息的长度,也就是不知道消息的边界时,是无法读出一个有效的用户消息的(用户消息被拆分成多个 TCP 报文后,并不能像 UDP 那样,一个 UDP 报文就能代表一个完整的用户消息)。

在这里插入图片描述
  
  
  2)、进一步介绍

  对应TCP协议,创建一个TCP的socket,OS会在内核中同时创建一个 发送缓冲区 和一个 接收缓冲区

  • 调用write等写入函数时,数据会先写入发送缓冲区中。如果发送的字节数太长,会被拆分成多个TCP的数据包发出;如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区此时装入的数据长度差不多,或其他合适的时机时,再将数据一次性发送出去;
  • 接收数据的时候,数据也是从网卡驱动程序先到达内核的接收缓冲区,然后应用程序可以调用read等函数从接收缓冲区拿数据。

  此外,TCP的一个连接既有发送缓冲区,也有接收缓冲区。那么对于这样一个连接,既可以读数据,也可以写数据, 这个概念叫做 全双工。由于缓冲区的存在,TCP程序的读和写不需要一一匹配。

  • 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
  • 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;
      
      
      
      
      
      
      
      

9.5.2、数据报粘包问题

  1)、问题引入
  首先要明确, 粘包问题中的 “包” , 是指的应用层的数据包.
  由于TCP面向字节流的属性,协议头中没有如UDP一样的 “报文长度” 这样的字段,但是有一个序号这样的字段。站在传输层的角度,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中。站在应用层的角度,应用程序看到的只是一串连续的字节数据,并不知道这一串连续字节是否为一个完整的应用层数据包。
在这里插入图片描述

  
  
  2)、解决方法
  如何避免粘包问题?
  归根结底就是明确两个包之间的边界

  1、对于定长的包,保证每次都按固定大小读取即可。由于数据报是固定大小 S I Z E SIZE SIZE, 那么就从缓冲区从头开始按照固定长度 S I Z E SIZE SIZE依次读取即可。
  2、对于变长的包,可以在包头的位置约定一个包总长度的字段,从而就知道了包的结束位置。
  3、对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议, 是程序员自己来定的,只要保证分隔符不和正文冲突即可);
  
  
  
  思考:对于UDP协议来说, 是否也存在 “粘包问题” ?
  回答,不存在。对于UDP而言,一旦数据被封装成UDP报文并发送,其报文长度在传输过程中是保持不变的。UDP是基于数据报文的协议,它会一个接一个地将数据报文完整地交付给应用层。因此,从应用层的角度来看,使用UDP时,要么收到完整的UDP报文,要么完全不收,不会出现只收到“半个”报文的情况。这种明确的数据边界特性使得UDP在处理数据传输时具有清晰、独立的数据单元。
  
  
  
  

9.5.3、TCP异常

  进程终止TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃/进程终止后,内核需要回收该进程的所有资源( 进程控制块中包含文件描述符,文件描述符表中有Socket网卡文件,因此TCP的连接资源也被释放)。内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP四次挥手的过程
  

  正常关机:和进程终止的情况相同,关机时会先强制终止进程,回收进程资源(此时会释放文件描述符),内核发送FIN报文,进行四次挥手。
  PS:由于触发四次挥手断开网络连接时,我方主机正在关机,所以可能四次挥手挥完了以后才关机,也有可能没有挥完就关机了。 但是四次挥手有没有挥完是没有问题的,我方关机后不再应答,对方就会触发超时重传,多次重传都没有得到ACK应答,对方就会单方面的断开连接(释放存储网络连接相关信息的内存)。
  

  机器掉电/网线断开:被拔掉电源的一方是没有机会向对方发送四次挥手的,也就是先有拔网线的行为,再有识别检测到TCP连接异常。
  对于我方:识别到网络发生变化,自动关闭连接。
  对于对方:由于没有进行四次挥手,会认为连接还在。①此时若将网络立马恢复,发送端发送数据时,接受端会识别到连接异常,并重新reset连接(触发复位报文(RST),尝试重置连接。对客户端拔掉电立马恢复和服务端掉电立马恢复都一样)。②即使没有写入操作,TCP自己也内置了一个保活定时器(TCP心跳包),会定期询问对方是否还在,如果对方不在,也会把连接释放。
在这里插入图片描述
  
  
  PS: 应用层的某些协议也有一些这样的检测机制。例如HTTP长连接中,也会定期检测对方的状态。
  举例简述:我们日常使用的微信、QQ,并非长期与服务器保持连接,在我们不打开APP应用或只是占线但长时间不访问资源时,应用层会对这样的长连接进行一定的管理工作,如APP保持登录状态,但将与服务器的连接处于断开状态(典型的表现:QQ头像呈现灰色、离线),用户后续使用时后会自动重建连接关系。
  
  
  
  
  
  
  
  
  

9.5.4、 listen 的第二个参数

  1)、问题回顾

  对服务端,其TCP套接字一系列历程如下:

初始化服务器:socket创建套接字、bind绑定、listen监听
启动服务器:accept获取连接、开始进行通讯服务

  1、accept获取连接,要不要参与三次握手过程?
  回答:不需要,accept的作用是直接从底层获取已经建立好的链接。也就意味着是连接先建立好,然后accept才能获取对应的连接。即,即使不使用accept,底层也将连接建立好了。
  
  2、如此,就有一个问题,如果上层来不及调用accept,并且对端还来了大量的连接,难道所有的连接都应该先建立好吗?
  回答:并非如此,实际这与listen的第二参数有关。

  
  3、在之前学习TCP套接字时,将listen的第二参数设置如下状态,为什么要这样设置?如何理解listen的第二参数?

const static int gbacklog = 20; //不能太大、也不能太小
NAME
       listen - listen for connections on a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int listen(int sockfd, int backlog);

  
  
  
  
  2)、正试介绍

在这里插入图片描述是什么?

  文档解释如下,根据文档可知,
  在linux 2.2 内核之前,backlog是指半连接队列(syns_queue)的长度。
  在linux2.2及之后,backlog是指已经完全建立连接,但是还没有被应用层accetp之前,socket所处全连接队列(accetp_queue)的长度

在这里插入图片描述
  
  

  具体说明:
  Linux内核协议栈为一个tcp连接管理使用两个队列:
  1. 半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
  2. 全连接队列(accpetd队列)(用来保存那些处于established状态而应用层没有调用accept取走的请求)

  全连接队列的长度会受到 listen 第二个参数backlog的影响。 全连接队列满了的时候,就无法继续让当前连接的状态进入 established 状态了。
  
  
  
  
  
  

在这里插入图片描述使用举例

  相关演示:
在这里插入图片描述

  演示结果:
在这里插入图片描述

  
  
  其它说明:为什么该连接队列不能太长,也不能太短?
  连接队列太长:会导致请求处理时间过长,甚至超时失败。
  连接队列太短:服务端工作不饱和,系统性能没有完全发挥(最大程度利用)。
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  

Fin、共勉。

在这里插入图片描述

  • 24
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值