linux 内核五大模块:网络通信

网络通信

网络通信是一种把不同计算机或网络设备连接到一起的技术,本质上是跨系统的进程间通信,必须要通过网络(硬件)才能进行。随着高并发、分布式、云计算、微服务等技术的普及,网络的性能也变得越来越重要。

一、网络模型

1.1 OSI模型

为了解决网络互联中异构设备的兼容性问题,并解耦复杂的网络包处理流程,OSI 模型把网络互联的框架分为应用层、表示层、会话层、传输层、网络层、数据链路层以及物理层等七层,每个层负责不同的功能。其中,

  1. 应用层,负责为应用程序提供统一的接口。
  2. 表示层,负责把数据转换成兼容接收系统的格式。
  3. 会话层,负责维护计算机之间的通信连接。
  4. 传输层,负责为数据加上传输表头,形成数据包。
  5. 网络层,负责数据的路由和转发。
  6. 数据链路层,负责 MAC 寻址、错误侦测和改错。
  7. 物理层,负责在物理网络中传输数据帧。

1.2 TCP/IP 模型

TCP/IP 模型把网络互联的框架分为应用层、传输层、网络层、网络接口层等四层,其中,

  1. 应用层,负责向用户提供一组应用程序,比如 HTTP、FTP、DNS 等。
  2. 传输层,负责端到端的通信,比如 TCP、UDP 等。
  3. 网络层,负责网络包的封装、寻址和路由,比如 IP、ICMP 等。
  4. 网络接口层,负责网络包在物理网络中的传输,比如 MAC 寻址、错误侦测以及通过网卡传输网络帧等。

虽说 Linux 实际按照 TCP/IP 模型,实现了网络协议栈,但在平时的学习交流中,我们习惯上还是用 OSI 七层模型来描述。比如,说到七层和四层负载均衡,对应的分别是 OSI 模型中的应用层和传输层(而它们对应到 TCP/IP 模型中,实际上是四层和三层)。

二、linux 网络栈

2.1 基础知识

TCP/IP 模型中,需要进行网络传输时,数据包就会按照协议栈,对上一层发来的数据进行逐层处理;然后封装上该层的协议头,再发送给下一层。
当然,网络包在每一层的处理逻辑,都取决于各层采用的网络协议。比如在应用层,一个提供 REST API 的应用,可以使用 HTTP 协议,把它需要传输的 JSON 数据封装到 HTTP 协议中,然后向下传递给 TCP 层。
而封装做的事情就很简单了,只是在原来的负载前后,增加固定格式的元数据,原始的负载数据并不会被修改。
比如,以通过 TCP 协议通信的网络包为例,通过下面这张图,我们可以看到,应用程序数据在每个层的封装格式。

其中:

  1. 传输层在应用程序数据前面增加了 TCP 头;
  2. 网络层在 TCP 数据包前增加了 IP 头;
  3. 而网络接口层,又在 IP 数据包前后分别增加了帧头和帧尾。
    这些新增的头部和尾部,都按照特定的协议格式填充,想了解具体格式,你可以查看协议的文档。

这些新增的头部和尾部,增加了网络包的大小,但我们都知道,物理链路中并不能传输任意大小的数据包。网络接口配置的最大传输单元(MTU),就规定了最大的 IP 包大小。在我们最常用的以太网中,MTU 默认值是 1500(这也是 Linux 的默认值)。

一旦网络包超过 MTU 的大小,就会在网络层分片,以保证分片后的 IP 包不大于 MTU 值。显然,MTU 越大,需要的分包也就越少,自然,网络吞吐能力就越好。

理解了 TCP/IP 网络模型和网络包的封装原理后,你很容易能想到,Linux 内核中的网络栈,其实也类似于 TCP/IP 的四层结构。如下图所示,就是 Linux 通用 IP 网络栈的示意图:

我们从上到下来看这个网络栈,你可以发现:

  1. 最上层的应用程序,需要通过系统调用,来跟套接字接口进行交互;
  2. 套接字的下面,就是我们前面提到的传输层、网络层和网络接口层;
  3. 最底层,则是网卡驱动程序以及物理网卡设备。
    这里我简单说一下网卡。网卡是发送和接收网络包的基本设备。在系统启动过程中,网卡通过内核中的网卡驱动程序注册到系统中。而在网络收发过程中,内核通过中断跟网卡进行交互。
    再结合前面提到的 Linux 网络栈,可以看出,网络包的处理非常复杂。所以,网卡硬中断只处理最核心的网卡数据读取或发送,而协议栈中的大部分逻辑,都会放到软中断中处理。

套接字(socket) = IP + 端口号。IP是网络层协议报头包含的字段,标识着网络传输时应该将数据传输给哪个主机。端口号是传输层协议报头包含的字段,对应着传输层报文中的数据应该交付给主机上哪个进程。然后该进程收到传输层的数据后,根据应用层协议将应用层报文中的提取数据。

以TCP/IP为例,如下为用户态、内核态、网卡之间的关系:

2.2 UDP

2.2.1 UDP报文格式


(1) 源端口号,目的端口号标明了此UDP报文是哪个进程发出的,发送给哪个进程。
(2) 如何解包(分离):UDP采用固定长度报头,接收方将报文前8字节提取出,剩下的就是有效载荷。
(3) 如何向上交付:接收方的OS的传输层收到UDP报文之后,16位目的端口号标明了对应进程。(该进程bind了端口号,在内核中,存储诸如port : PCB指针这样的KV类型,就可以通过端口号找到对应的进程)
(4) 承接第三点,这也是为什么在应用层编写UDP代码时,定义端口号时,喜欢定义为uint16_t,正是因为传输层协议使用的端口号为16位的。
(5) UDP如何提取到整个完整报文:16位UDP长度字段

2.2.2 UDP的特点

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

2.2.3 UDP的缓冲区

UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核, 由内核将数据传给网络层协议进行后续的传输动作;
UDP具有接收缓冲区,但是因为UDP不可靠,没有任何传输控制行为。故这个接收缓冲区无法保证接收到的UDP报的顺序和发送UDP报的顺序一致。如果缓冲区满了,再到达的UDP数据就会被丢弃。(提醒一下,接收缓冲区中存储的是UDP报文中去掉报头之后的数据)

2.2.4 UDP是全双工的

UDP没有发送缓冲区,有接收缓冲区,数据在网络中的发送和接收互不影响,可以同时进行,因此为全双工的。UDP的socket既能读,也能写。

2.2.5 UDP注意事项

我们注意到, UDP协议首部中有一个16位的最大长度. 也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部8字节). 然而64K在当今的互联网环境下, 是一个非常小的数字. 如果我们需要传输的数据超过64K, 就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装;

2.3 TCP

TCP是面向字节流,可靠的协议。

2.3.1 TCP报文格式


(1)16位源/目的端口号:表示数据从哪个进程来,到哪个进程去。
(2)TCP报文如何进行解包(分离):报头中,有一个4位首部长度字段,表征着该TCP报头有多少个32bit(4字节),TCP报头不是定长的,而是变长的,因为选项内容不定。所以,TCP头部最大为154=60字节。即20~60字节。
所以,解包的大致过程为:提取20字节,获取4位首部长度(在20字节中的位置固定),x
4-20为选项的长度。提取出x*4-20字节后,剩下的就是有效载荷(也就是应该交给应用层的数据,需存入TCP的接收缓冲区中)
(3)16位校验和:
(4)6位标志位:TCP报文有多种类型(属性),这6个标志位是用于标记报文类型的,比如ACK标志位若为1,则代表这个报文有ACK属性,即表示确认序号有效(见下方确认应答(ACK)机制)(多个标志位可叠加,一个报文可有多种属性类型)

  • URG: 紧急指针是否有效(TCP因为有序号,故数据是按序到达的,URG配合16位紧急指针可以实现将有效载荷中的某紧急数据提前向上交付)
  • ACK: 确认序号是否有效(凡是该报文具有应答特征,该标志位都会被设置为1。大部分网络报文ACK都是被设置为1的,因为TCP有捎带应答机制。但是第一个TCP连接请求报文的ACK标志位不为1)
  • PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走(联系流量控制和滑动窗口机制)
  • RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段(比如一端网线断了,则两端对于连接建立认知不一致)
  • SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
  • FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段

2.3.2 确认应答机制(ACK)

TCP和UDP一个最大的不同就是,TCP具有可靠性,而可靠性就需要TCP采取一些机制和策略措施来实现,其中最重要的就是确认应答(ACK)机制。
为什么网络传输存在不可靠性呢?(这里的不可靠,比如数据在传输过程中丢失了,丢包了。)其实,单纯只是因为传输距离变长了,比如在操作系统内部,也就是一个机器中,也需要近距离通过电路传输,因为这里距离近,所以不存在协议。而一旦进行网络传输,也就是主机与主机之间,距离变长了,就需要一些协议,比如TCP/IP协议。
不存在100%可靠的协议,比如A主机向B主机发送一个数据,没有哪个协议能够保证这个数据一定送达。但是TCP的确认应答(ACK)机制,能够保证在局部上数据传输的100%可靠性。
序号和确认序号的作用:

  1. 将请求和应答一一对应。(接受方知道哪个ACK对应哪个之前发送过的数据报文)
  2. 允许部分ACK丢失或者不给应答。因为确认序号的含义是确认序号之前的所有数据都收到了。
  3. 接收方可以根据序号,将接收到的报文进行排序,解决报文乱序问题。
  4. 一个报文中设置序号和确认序号两个字段是因为,TCP是全双工的,任何一方在发送ACK确认的时候,也可能同时想向对方发送数据消息。既可以收,也可以发。

2.3.3 流量控制(针对接收端)

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

  1. 接收端将自己可以接收的剩余接收缓冲区空间大小放入TCP首部中的 "窗口大小" 字段, 通过ACK报文通知发送端;(因为接收端需要给发送端发送的报文进行ACK)
  2. 窗口大小字段越大, 说明网络的吞吐量越高;
  3. 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端; 发送端接受到这个更小的窗口大小值之后, 就会减慢自己的发送速度;
  4. 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段(可以理解为不携带有效数据的报文), 使接收端把窗口大小告诉发送端。
  5. TCP首部中的16位窗口大小字段,就是存放了窗口大小信息。16位最大表示65535字节。实际上,TCP首部40字节选项(最大40字节)中还包含了一个窗口扩大因子M, 实际窗口大小是窗口字段的值左移 M 位;
  6. 当接收端的ACK报文窗口大小总是很小或总是为0时,发送端发送的报文中可以将PSH标志位置为1,表示督促对方尽快将接收缓冲区中的数据向上交付。
  7. 流量控制是TCP连接双方的,因为TCP协议的每一方都有一个发送缓冲区和一个接收缓冲区。
  8. 在TCP连接双方第一次进行携带有有效载荷的报文进行通信时,如何得知对方的窗口带下?其实,第一次数据通信 不等于 第一次交换报文。在TCP三次握手时,需要传输报文,这时就可以填写窗口大小字段,交换双方的接收缓冲区剩余空间大小。

2.3.4 三次握手

对于一个server来说,可能会有很多client连接server,所以server端一定会存在大量的连接。OS需要管理这些连接!则需要先描述,再组织。所以所谓的连接,本质就是操作系统内核中的一种数据结构类型。当建立连接成功时,就是在内存中创建对应的连接对象。再对多个连接对象进行某种数据结构的组织,方便管理。

  1. TCP三次握手的过程中,并非是传输SYN,ACK...而是传输TCP报文,这个报文的SYN,ACK标志位为1
  2. 可以将上图的纵轴理解为时间线,TCP三次握手的过程需要时间消耗,每次报文的传输也需要时间消耗。
  3. 三次握手并不是一定成功,只是较大概率成功,TCP的可靠性是在三次握手建立连接完成之后才能保证的。主要原因是第三次握手时,客户端发来的ACK报文可能丢包。
  4. 客户端和服务端在三次握手的进行过程中,会有状态变化。比较值得关注的是,客户端在发送完第三次握手的ACK之后,进入ESTABLISED状态,而服务端接收到ACK才会认为连接建立完成。具有滞后性。

为什么TCP连接过程设置为三次握手,而不是一次,两次,四次呢?
TCP三次握手的作用,意义,好处:

  1. 三次握手,推广至奇数次握手,则最后一次握手一定是由客户端发起的(大多),则在最后一次ACK发起后,客户端认为连接建立成功,而服务端收到ACK后,才会认为连接建立成功,则服务端建立连接在客户端之后,这样,可以嫁接同等的成本给客户端,防止单机客户端耗费低成本就消耗服务端的大量资源。(一般服务端配置都比客户端高)同时,若第三次握手ACK丢失,则此次连接失败的后果由客户端承担。
  2. 三次握手,对于客户端和服务端来说,每一方都收到了对方的一次ACK,证明此方到彼方的TCP网络传输信道是畅通的。也就是在TCP建立连接的过程中,验证了全双工!而一次握手和两次握手都无法验证全双工。
    TCP一次握手建立连接:一次握手会使得服务端很难确定连接的请求是否是合法的,因为一次握手无法提供足够的信息来判断请求的来源和目的。攻击者可以通过伪造请求来欺骗服务端,从而实现未授权访问或恶意攻击。同时,恶意客户端可能采用同一时间发送大量SYN请求报文的方式来攻击服务端。OS管理这些连接需要成本(内核数据结构),故,对于服务端来说很危险。且无法验证全双工。
    TCP两次握手建立连接:两次握手,对于服务端来说依旧有安全问题,比如客户端发送大量的SYN,服务端向客户端发送SYN+ACK,这样服务端就认为TCP连接建立成功的话,客户端如果把服务端的第二次握手报文丢弃,则服务端建立大量恶意连接,消耗系统资源,则客户端承担的成本比服务端小的多,这样单机恶意攻击服务端的情况就难以防御。类似于一次握手。同时无法验证全双工,只能验证客户端到服务端的传输信道。
    TCP四次握手建立连接:依旧有安全问题,客户端可能丢弃第四次报文,此时server比client先建立连接。同时,偶数次握手,TCP建立连接失败的后果由服务端承担,不安全。
    总之,TCP的三次握手,其实本身就无法保证连接一定建立成功。是一种较小成本,较为安全的一种握手方式。1. 若客户端恶意攻击,则可以嫁接同等成本给客户端,防止单机以低成本恶意攻击服务端。2. 验证全双工。而对于5678更多次的握手,就是浪费网络资源,同时也无法提高安全性。

没有accept能进行三次握手吗?
首先accept与三次握手没关系。在listen时,内核已经把半连接队列和全连接队列建好了。等到客户端调用connect后,两边就完成三次握手。相关的连接信息会放入全连接队列中。
如果此时服务端有调用accept函数,则内核会取出此连接,并返回一个socket文件描述符给应用程序。供应用程序使用此连接。
如果此时服务端没有调用accept函数,此连接不会被取出。而且客户端如果发消息过来,服务端是可以正常回复ACK的。如果之后再调用accept函数,之前的消息也能被收到(这是合乎逻辑的,因为服务端也不知道客户端什么时候connect).

没调用accept会导致应用程序无法使用TCP连接,即不能接收信息,不能关闭连接。

2.3.5 四次挥手

  1. 主动断开连接的一方先发送FIN报文:FIN,ACK,FIN,ACK
  2. 被动断开连接的一方可能将第二次挥手的ACK和第三次挥手的FIN合并为一个报文。四次挥手可能为三次挥手。
  3. 四次挥手不一定顺利完成,比如第二次ACK丢失,最后一次ACK丢失。但是因为TCP有超时重传机制,所以整体来说不影响。
  4. 为什么是四次挥手?因为断开连接是建立TCP连接双方的事情,需要双方都关闭此方到彼方的传输信道,故每一方都需要发送一次FIN报文。(因为TCP全双工,两个通信信道呀,所以需要两次FIN报文发送,同时需要两次ACK确认应答。

TCP四次挥手的状态变化
服务端状态转化:
[CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接;
[LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端 发送SYN确认报文.
[SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状态, 可以进行 读写数据了.
[ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器返回确认报文段并进入CLOSE_WAIT;
[CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待最后一个 ACK到来(这个ACK是客户端确认收到了FIN)
[LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接.

客户端状态转化:
[CLOSED -> SYN_SENT] 客户端调用connect, 发送同步报文段;
[SYN_SENT -> ESTABLISHED] connect调用成功, 则进入ESTABLISHED状态, 开始读写数据; [ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入 FIN_WAIT_1;
[FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服务器的结束报文段;
[FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK;
[TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入CLOSED状态.

其中比较关键的两个状态是:

  1. 被动关闭方收到对方的FIN,并发出ACK之后,在发出第三次挥手的FIN之前,会进入CLOSE_WAIT状态。
  2. 主动关闭方在收到对方第三次挥手的FIN,并发出ACK之后,会进入一段TIME_WAIT状态,而不是直接进入CLOSED状态。
    四次挥手的CLOSE_WAIT状态
    大多情况下,TCP连接的断开,都是由客户端发起的。则若服务端在收到客户端的FIN并发出ACK之后,不发出FIN,则会一直进入CLOSE_WAIT状态。而发出第三次挥手的FIN,就需要服务端主动close关闭文件描述符。
    故,服务端需要注意,当TCP通信结束后,需要close对应的文件描述符。否则,这个TCP连接将不会释放,时间长了,越来越多的TCP连接占用内存资源,会引发问题。同时,服务进程的文件描述符也会越来越少。
    所以,若发现服务器有大量的close_wait状态的连接存在时,原因是什么呢?即应用层服务器写的有bug,忘记关闭对应的连接sockfd,导致四次挥手没有正确完成。(注:下方多版本TCPserver+client,服务端都进行了close文件描述符,避免了CLOSE_WAIT连接大量存在。)

四次挥手的TIME_WAIT状态
主动断开连接的一方,收到对方第三次挥手的FIN,且发出ACK之后,会进入TIME_WAIT状态,而不是直接进入CLOSED状态。经验证,确实是!
在此状态下,虽然应用程序终止了,但TCP协议层的TCP连接并没有完全断开,地址信息ip,port依旧是被占用的。
故,有一个现象:当服务端直接ctrl+c之后,因为文件描述符表的生命周期随进程!故,进程终止,相当于所有文件描述符被关闭,相当于服务端主动进行四次挥手断开连接。故TCP协议层的连接此时会进入TIME_WAIT状态,故此时ip,port是被占用的,故此时若立刻再次启动服务进程,会显示失败。

  1. TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime) 的时间后才能进入到CLOSED状态;
  2. 我们使用Ctrl-C终止了server, 所以server是主动关闭连接的一方, 在TIME_WAIT期间仍然不能再次监听同样的server端口;
  3. MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s;

为什么要有TIME_WAIT状态?(且还是2MSL)

  1. MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2MSL的话, 就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的);
  2. 保证最后一个ACK成功传输给对端。(若传输失败,则对端可能会进行超时重传,重新传来一个FIN,这时虽然客户端不存在了,但是TCP连接还在,仍然可以重发LAST_ACK;

解决TIME_WAIT状态引起的bind失败的方法
在server的TCP连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的。
int opt = 1; setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); 进行setsockopt即可让端口号和ip在TIME_WAIT期间,依旧被服务器再次绑定。

2.3.6 超时重传机制

超时重传机制是TCP协议保证可靠性的一个关键因素。个人认为,TCP协议可靠性最关键的就是两个,一个是确认应答ACK机制,另一个就是超时重传机制。有了这两个机制,几乎就可以保证数据可靠地传输给对方。

超时重传机制:在确认应答机制的前提下,当A向B发送报文,收到对应的ACK后,可以确保报文传达给了B。而当A在一定时间内没有收到B的ACK时,则判定为出问题了,则A重新给B发送报文。

可能的情况:

  1. 主机A发送数据报给B之后, 可能因为网络拥堵等原因, 数据报无法到达主机B,丢包了。
  2. 报文没有丢包,B发送的ACK丢失了。
    则不论情况一还是情况二,A都需要在一定时间没有收到ACK之后,重新发送报文。但如果是情况二,则主机B会收到很多重复数据(报文),则TCP协议就需要能够识别出哪些包是重复的包,并将重复的丢弃掉。接收方可以根据TCP协议报头中的序列号,很容易进行去重。

超时的时间如何确定?

  1. 这个时间长短,随着网络环境的不同,是有差异的。网络环境好,则时间应相对短一些,网络环境差,则时间应相对长一些。
  2. 如果时间设得太长,会影响整体的重传效率。如果时间设的太短,有可能会频繁发送重复的包。也会影响整体传输效率。
  3. TCP为了保证无论在任何环境下都能比较高性能地通信, 因此会动态计算这个最大超时时间. Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍. 如果重发一次之后, 仍然得不到应答, 等待 2500ms 后再进行重传. 如果仍然得不到应答, 等待 4500ms 进行重传. 依次类推, 以指数形式递增. 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接.

2.3.7 拥塞控制(针对网络)

避免网络拥塞,以及网络拥塞之后的应对策略等,称为拥塞控制。

  1. 拥塞窗口:单机主机一次向网络中发送大量的数据时,可能会引发网络拥塞的上限值。
  2. 滑动窗口大小 = min(拥塞窗口大小,对端的窗口大小(接收能力))(其实就是考虑对方接收能力,还要考虑网络拥塞情况)
  3. 在TCP连接刚建立好时,如果在刚开始阶段就发送大量的数据, 仍然可能引发问题,因为此时网络状况并不清楚,因此TCP引入 慢启动 机制,先发少量数据,探探路,摸清当先的网络拥堵状况,再逐渐增大数据传输速度。慢启动机制除了在TCP刚建立好时使用,还有每一次发生网络拥塞之后。
  4. 慢启动:拥塞窗口为1(刚建立好 && 网络拥塞之后),后面若正常收到ACK应答,则拥塞窗口先以指数方式增长。到了一定阈值之后,再线性增长。
  5. "慢启动" 只是指初始时慢, 但是增长速度非常快(因为是指数增长).
  6. 不能一直指数增长, 故引入一个拥塞窗口的阈值,超过阈值之后线性增长....
  7. 为什么网络拥塞之后,前期是指数增长?指数:前期较慢,后期增长较快。a. 前期给网络一个缓冲的机会 - 慢 b. 中后期,网络恢复之后,需要尽快恢复通信的效率 - 快。 (而因为不能一味的指数增长,所以有了后面的阈值)
    拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案

2.3.8 滑动窗口/流量控制

因为有了确认应答机制,于是对于每一个发出的数据报文,都需要一个ACK确认应答。收到ACK之后再发送下一个数据段。但这样做会导致TCP网络通信性能较差,尤其是数据往返的时间较长时。
而滑动窗口策略就是用来提高TCP传输效率的。
一收一发的策略下,互换一次数据报的时间是X(捎带应答机制下),而滑动窗口使得可以一次发送多条报文(其实本质不是一次发送多条,而是发送滑动窗口内数据报文后,不需要等待ACK就可以发送窗口内的下一条),这样整体效率就提高了。(多个数据段的等待时间重叠了)


结合上面的两个图(并不对应)

  1. 滑动窗口的窗口大小指的是无需等待确认应答就可以继续发送数据的最大值.上图中的窗口大小即为4000字节。(TCP面向字节流,这4000字节被分为几个报文进行发送不确定,上图中为4个)

  2. 发送前四个段时,不需要等待任何ACK,可以直接发送。

  3. 收到第一个ACK后,滑动窗口向右滑动。继续发送第五个段的数据;以此类推。

  4. 窗口越大,表示网络的吞吐率越高(传输效率越高)

  5. 如图一所示,发送缓冲区中的数据可以大致分为三个部分,已发送并收到ACK确认的数据,已发送但未收到ACK确认的数据,未发送的数据。其中,窗口包括第二部分,可能包括未发送数据的某一部分,也就是,已发送但未收到ACK的数据,收到ACK之后,窗口会右移(后移),此时窗口可能包括未发送的数据的一部分,也就是不需要等待前方报文的ACK就可以立即发送了,但尚未发送。

  6. 滑动窗口的发送策略,其实并不是完全按照一批一批发送的,也就是,图二中,若收到了1001的ACK,则窗口会右移,此时,4001-5000的数据报文就可以立即发送了。

  7. 滑动窗口在发送缓冲区内,属于发送方的发送缓冲区的一部分(如第五点所示),滑动窗口的本质:发送方,可以一次性向对方发送数据的最大值(滑动窗口,提高效率的一个策略)。滑动窗口的大小 = 接收方窗口大小(接收缓冲区剩余空间) 和 拥塞窗口(见下文)的较小值。

  8. 滑动窗口模型理解:可以将发送缓冲区理解为一个字节数组,每一个字节都有下标。滑动窗口有两个下标:win_start, win_end。win_end = win_start + min(窗口大小(不是滑动窗口大小),拥塞窗口大小),若收到ACK,则win_start = ACK的确认序号,win_end = win_start + min(窗口大小,拥塞窗口大小)。故,滑动窗口的本质就是指针或者下标。

  9. 滑动窗口不一定必须向右移动,比如收到ACK且min(x, y)减小,则可能不会右移。滑动窗口可能为0,比如对方窗口大小 == 0 或者 拥塞窗口大小 == 0。中间发送的某些报文的ACK丢失,可能,但是不影响。因为ACK的确认序号的含义是该序号前的数据都收到了。滑动窗口一直右移,可能越界吗?不可能,因为发送缓冲区的物理上是线性的,逻辑上是环形的。

2.3.9 粘包问题

  1. 首先要明确, 粘包问题中的 "包" , 是指的应用层的数据包.
  2. 在TCP的协议头中, 没有如同UDP一样的 "报文长度" 这样的字段, 但是有一个序号这样的字段.
  3. 站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在接收缓冲区中.
  4. 站在应用层的角度, 看到的只是一串连续的字节数据.
  5. 那么应用层看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包.(因此,需要制定应用层协议)(粘包其实就是应用层读取时,读到一个半,或两个应用层数据包。其实半个也一样,也需要解决)

那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界!其实就是制定应用层协议,使得应用层读取到一段字节数据之后,可以判断到哪里是一个完整的应用层数据报。应用层读取到一个完整的报文之后,再根据应用层协议,将其中的有效载荷提取出来,这一部分就是对方主机真正想向己方发送的数据。

  1. 在信息中加入特殊的标志作为分隔符
  2. 加入信息的长度
  3. 添加包首部

资料:
https://zhuanlan.zhihu.com/p/634994085
https://www.cnblogs.com/linguoguo/p/16248620.html
https://blog.csdn.net/i777777777777777/article/details/130086733
https://baijiahao.baidu.com/s?id=1744728859176967544&wfr=spider&for=pc
https://blog.csdn.net/ZBraveHeart/article/details/123820768

posted @ 2023-10-09 12:46  小海哥哥de  阅读(611)  评论(0编辑  收藏  举报