知识体系之TCP/IP详解

目录

1.计算机网络

1.1.分层

1.1.1.四层模型

1.1.2.七层模型

1.2.键入网页到网页显示,期间发生了什么?

1.3.Linux接收网络包的流程

1.4.Linux发送网络包的流程

2.TCP报头格式

2.1.为什么需要TCP协议?TCP协议在哪一层

2.2.什么是TCP

2.2.1.如何解决粘包?

2.3.什么是TCP连接

2.3.1.如何唯一的确定一个TCP链接?

2.3.2.UDP 和 TCP 有什么区别呢?分别的应用场景是?

2.4.TCP如何保证可靠性

2.4.1.校验和

2.4.2.确认应答与序列号

2.4.3.超时重传

2.4.4.快速重传

2.4.5.滑动窗口 + 流量控制

2.4.6.拥塞控制

2.5.用了TCP协议,数据一定不会丢失么?

2.5.1. 数据包的发送流程

2.5.2. 丢包场景

2.5.3. 发生丢包了怎么解决? 使用TCP协议

2.5.4. 用了TCP协议就一定不会丢包么?

2.5.5. 这类丢包问题怎么解决?

3.三次握手

3.1.三次握手过程

3.1.1.非阻塞套接字Connect

3.2.为什么是三次握手?不是两次、四次?

3.3.为什么每次建立TCP连接时,初始化的序列号都要求不一样呢?

3.3.1.初始序列号 ISN 是如何随机产生的?

3.4.数据包丢失,TCP是怎么处理的?

3.4.1.第一次握手丢失了,会发生什么?

3.4.2.第二次握手丢失了,会发生什么?

3.4.3.第三次握手丢失了,会发生什么?

3.5.syn请求队列 / 已连接队列

3.5.1. listen的第2个参数(两个队列:syn请求队列/已连接队列)

3.5.2. SYN泛洪攻击/DDOC攻击?如何避免?

3.6.​​​​​ API与三次握手

3.6.1.连接建立是在accept函数么?

3.6.2.没有 listen,能建立 TCP 连接吗?

3.7.如何优化TCP? 

3.7.1.TCP 三次握手的性能提升

3.7.2.TCP 四次握手的性能提升

3.7.3.TCP数据传输的性能提升

4.四次挥手

4.1.四次挥手过程

4.1.1.TIME-WAIT存在的原因?

4.1.2.TIME-WAIT过多的危害? 应该怎么解决?

4.1.3.服务器出现大量 TIME_WAIT 状态的原因有哪些?

4.1.4.服务器出现大量 CLOSE_WAIT 状态的原因有哪些?

4.2.断开连接

4.2.1.为什么是4次挥手?不是3次?

4.2.2.close/shutdown、引用计数

4.3.挥手数据包丢失了,TCP是怎么处理的?

4.3.1.第一次挥手丢失了,会发生什么?==> 重传FIN

4.3.2.第二次挥手丢失了,会发生什么?==> 重传FIN

4.3.3.第三次挥手丢失了,会发生什么?==> 重传FIN

4.3.4.第四次挥手丢失了,会发生什么?==> 重传FIN

5. TCP故障场景分析

5.1.TCP 连接,主机崩溃\进程崩溃有什么区别?

5.2.拔掉网线几秒,再插回去,原本的 TCP 连接还存在吗?

5.3.如果已经建立了连接,但是客户端突然出现故障了怎么办?

5.4.如果已经建立了连接,但是服务端的进程崩溃会发生什么?

6.TCP选项

6.1.keepalive

6.2.SO_LINGER

6.3.Nagle算法 & Delay ACK

6.3.1.Nagle算法

6.3.2.延迟确认Delay ACK

6.3.3.在Delay ACK开启时,一定要关闭Nagle算法

6.3.SO_REUSEPORT


1.计算机网络

1.1.分层

1.1.1.四层模型

应用层 应用层只需要专注于为用户提供应用功能,比如 HTTP、FTP、Telnet、DNS、SMTP
传输层 TCP、UDP
网络层 IP        
网络接口层

知识体系之TCP/IP详解_第1张图片

1.1.2.七层模型

  • 应用层,负责给应用程序提供统一的接口;
  • 表示层,负责把数据转换成兼容另一个系统能识别的格式;
  • 会话层,负责建立、管理和终止表示层实体之间的通信会话;
  • 传输层,负责端到端的数据传输;
  • 网络层,负责数据的路由、转发、分片;
  • 数据链路层,负责数据的封帧和差错检测,以及 MAC 寻址;
  • 物理层,负责在物理网络中传输数据帧;

1.2.键入网页到网页显示,期间发生了什么?

  1. 解析URL(生成HTTP请求信息)
  2. DNS:查询域名对应的IP地址(浏览器客户端缓存、操作系统缓存、hosts文件、DNS服务器)
  3. 协议栈:TCP/UDP、IP、ICMP、ARP

1.3.Linux接收网络包的流程

网卡是计算机里的一个硬件,专门负责接收、发送网络包

核心:网络接口层 ---[RingBuffer --- DMA --- 网卡 --- DMA --- RingBuffer ]--- 网络接口层

1. 网络接口层:数据发送和接收的最底层都会经过网络接口层

2. DMA:将数据从RingBuffer和网卡之间拷贝

当网卡接收到一个网络包后,会通过 DMA 技术,将网络包写入到指定的内存地址(也就是写入到 Ring Buffer ,这个是一个环形缓冲区),接着就会告诉操作系统这个网络包已经到达。

那应该怎么告诉操作系统这个网络包已经到达了呢?

  1. NAPI 机制,它是混合「中断和轮询」的方式来接收网络包:首先采用中断唤醒数据接收的服务程序,然后 poll 的方法来轮询数据
  2. 当有网络包到达时,会通过 DMA 技术,将网络包写入到指定的内存地址,接着网卡向 CPU 发起硬件中断,当 CPU 收到硬件中断请求后,根据中断表,调用已经注册的中断处理函数

硬件中断处理函数,会做如下的事情:

  • 需要先「暂时屏蔽中断」,表示已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知 CPU 了,这样可以提高效率,避免 CPU 不停的被中断。
  • 接着,发起「软中断」,然后恢复刚才屏蔽的中断。

至此,硬件中断处理函数的工作就已经完成(硬件中断处理函数做的事情很少,主要耗时的工作都交给软中断处理函数了)

软中断的处理:

内核中的 ksoftirqd 线程专门负责软中断的处理,当 ksoftirqd 内核线程收到软中断后,就会来轮询处理数据。

ksoftirqd 线程会从 Ring Buffer 中获取一个数据帧,用 sk_buff 表示,从而可以作为一个网络包交给网络协议栈进行逐层处理。

网络协议栈:

  1. 首先,会先进入到网络接口层,在这一层会检查报文的合法性,如果不合法则丢弃,合法则会找出该网络包的上层协议的类型,比如是 IPv4,还是 IPv6,接着再去掉帧头和帧尾,然后交给网络层。
  2. 到了网络层,则取出 IP 包,判断网络包下一步的走向,比如是交给上层处理还是转发出去。当确认这个网络包要发送给本机后,就会从 IP 头里看看上一层协议的类型是 TCP 还是 UDP,接着去掉 IP 头,然后交给传输层。
  3. 传输层取出 TCP 头或 UDP 头,根据四元组「源 IP、源端口、目的 IP、目的端口」 作为标识,找出对应的 Socket,并把数据放到 Socket 的接收缓冲区。
  4. 最后,应用层程序调用 Socket 接口,将内核的 Socket 接收缓冲区的数据「拷贝」到应用层的缓冲区,然后唤醒用户进程。

至此,一个网络包的接收过程就已经结束了,你也可以从下图左边部分看到网络包接收的流程,右边部分刚好反过来,它是网络包发送的流程。

知识体系之TCP/IP详解_第2张图片

1.4.Linux发送网络包的流程

如上图的右半部分,发送网络包的流程正好和接收流程相反。

  1. 首先,应用程序会调用 Socket 发送数据包的接口,由于这个是系统调用,所以会从用户态陷入到内核态中的 Socket 层,内核会申请一个内核态的 sk_buff 内存将用户待发送的数据拷贝到 sk_buff 内存,并将其加入到发送缓冲区
  2. 接下来,网络协议栈从 Socket 发送缓冲区中取出 sk_buff,并按照 TCP/IP 协议栈从上到下逐层处理。(如果使用的是 TCP 传输协议发送数据,那么先拷贝一个新的 sk_buff 副本 ,这是因为 sk_buff 后续在调用网络层,最后到达网卡发送完成的时候,这个 sk_buff 会被释放掉。而 TCP 协议是支持丢失重传的,在收到对方的 ACK 之前,这个 sk_buff 不能被删除。所以内核的做法就是每次调用网卡发送的时候,实际上传递出去的是 sk_buff 的一个拷贝,等收到 ACK 再真正删除)
  3. 接着,对 sk_buff 填充 TCP 头。这里提一下,sk_buff 可以表示各个层的数据包,在应用层数据包叫 data,在 TCP 层我们称为 segment,在 IP 层我们叫 packet,在数据链路层称为 frame。

这一些工作准备好后,会触发「软中断」告诉网卡驱动程序,这里有新的网络包需要发送,驱动程序会从发送队列中读取 sk_buff,将这个 sk_buff 挂到 RingBuffer 中,接着将 sk_buff 数据映射到网卡可访问的内存 DMA 区域,最后触发真实的发送。

当数据发送完成以后,其实工作并没有结束,因为内存还没有清理。当发送完成的时候,网卡设备会触发一个硬中断来释放内存,主要是释放 sk_buff 内存和清理 RingBuffer 内存。

最后,当收到这个 TCP 报文的 ACK 应答时,传输层就会释放原始的 sk_buff 。

2.TCP报头格式

知识体系之TCP/IP详解_第3张图片

源端口号、目的端口号

序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。

确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。

控制位:

  • SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
  • ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。
  • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段
  • RST:(连接重置)该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
  • URG:紧急指针有效
  • PSH:接收方应尽快将这个报文给应用层

校验和:发送主机根据数据计算出一个数值,接受主机接的数值与源主机要一致(证明数据的有效性)

窗口字段:窗口的字节容量, TCP的标准窗口最大为2^16 - 1 = 65535Byte

选项:可变长

数据

2.1.为什么需要TCP协议?TCP协议在哪一层

  • IP层是「不可靠」的,他不保证网络包的交付,不保证网络包的按序交付、也不保证网络包中数据的完整性
  • TCP(传输层)来保证:TCP 是一个工作在传输层可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的

2.2.什么是TCP

TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。

  • 面向连接一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;

  • 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;

  • 字节流:用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃

2.2.1.如何解决粘包?

一般有三种方式分包的方式:

  • 固定长度的消息;
  • 特殊字符作为边界;
  • 自定义消息结构

2.3.什么是TCP连接

        简单来说就是,用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。

        所以我们可以知道,建立一个 TCP 连接是需要客户端与服务端端达成上述三个信息的共识。

  • Socket:由 IP 地址和端口号组成
  • 序列号:用来解决乱序问题等
  • 窗口大小:用来做流量控制

2.3.1.如何唯一的确定一个TCP链接?

        TCP 四元组:源地址,源端口,目的地址,目的端口

有一个 IP 的服务端监听了一个端口,它的 TCP 的最大连接数是多少?

服务端通常固定在某个本地端口上监听,等待客户端的连接请求,是不变法。而,客户端 IP 和 端口是可变的,其理论值计算公式如下:

        最大TCP链接数 = 客户端IP个数 X 客户端端口数 = 2^32 X 2^16  = 2^48

当然,服务端最大并发 TCP 连接数远不能达到理论上限,会受以下因素影响:

  • 文件描述符限制,每个 TCP 连接都是一个文件,如果文件描述符被占满了,会发生 too many open files。Linux 对可打开的文件描述符的数量分别作了三个方面的限制:
    • 系统级:当前系统可打开的最大数量,通过 cat /proc/sys/fs/file-max 查看;
    • 用户级:指定用户可打开的最大数量,通过 cat /etc/security/limits.conf 查看;
    • 进程级:单个进程可打开的最大数量,通过 cat /proc/sys/fs/nr_open 查看;
  • 内存限制,每个 TCP 连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会发生 OOM

2.3.2.UDP 和 TCP 有什么区别呢?分别的应用场景是?

UDP 不提供复杂的控制机制,利用 IP 提供面向「无连接」的通信服务。

UDP 协议真的非常简单,头部只有 8 个字节( 64 位),UDP 的头部格式如下:

知识体系之TCP/IP详解_第4张图片

  • 目标和源端口:主要是告诉 UDP 协议应该把报文发给哪个进程。
  • 包长度:该字段保存了 UDP 首部的长度跟数据的长度之和。
  • 校验和:校验和是为了提供可靠的 UDP 首部和数据而设计,防止收到在网络传输中受损的 UDP包。

TCP 和 UDP 区别:

1. 连接

  • TCP 是面向连接的传输层协议,传输数据前先要建立连接。
  • UDP 是不需要连接,即刻传输数据。

2. 服务对象

  • TCP 是一对一的两点服务,即一条连接只有两个端点。
  • UDP 支持一对一、一对多、多对多的交互通信

3. 可靠性

  • TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。
  • UDP 是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议,具体可以参见这篇文章:如何基于 UDP 协议实现可靠传输?(opens new window)

4. 拥塞控制、流量控制

  • TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
  • UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。

5. 首部开销

  • TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。
  • UDP 首部只有 8 个字节,并且是固定不变的,开销较小。

6. 传输方式

  • TCP 是流式传输,没有边界,但保证顺序和可靠。
  • UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。

7. 分片不同

  • TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
  • UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。

TCP 和 UDP 应用场景:

由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:

  • FTP 文件传输;
  • HTTP / HTTPS;

由于 UDP 面向无连接,它可以随时发送数据,再加上 UDP 本身的处理既简单又高效,因此经常用于:

  • 包总量较少的通信,如 DNS 、SNMP 等;
  • 视频、音频等多媒体通信;
  • 广播通信;

Q:为什么 UDP 头部没有「首部长度」字段,而 TCP 头部有「首部长度」字段呢?

        原因是 TCP 有可变长的「选项」字段,而 UDP 头部长度则是不会变化的,无需多一个字段去记录 UDP 的首部长度。

2.4.TCP如何保证可靠性

  1. TCP的链接是基于三次握手,断开是基于四次挥手,确保链接和断开的可靠性。
  2. TCP的可靠性,还体现在“有状态”
  3. TCP通过校验和、序列号机制、ACK应答、超时重传,来保证数据包的按序到达,保证数据传输不出差错
  4. TCP通过流量控制(滑动窗口)和拥塞控制来控制发送方发送速率

2.4.1.校验和

  1. TCP校验和的计算和UDP一样,在计算时要加上12byte的伪首部,检验和总共计算3部分:TCP首部、TCP数据、TCP伪首部。
  2. 计算方法为:①在发送方将整个报文段分为多个16位的段,然后将所有段进行反码相加,将结果存在校验和字段中;②接收方用相同的方法进行计算,③将计算的结果,与检验字段存储的结果比较,相等则正确,不等则错误

2.4.2.确认应答与序列号

  1. 序列号的作用
    1. 保证可靠性(当接收到的数据总少了某个序号时,能马上知道)
    2. 保证数据的按序到达
    3. 提高效率,可实现多次发送,一次确认
    4. 去除重复数据

2.4.3.超时重传

TCP 实现可靠传输的方式之一,是通过序列号与确认应答。

在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息

        重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据,也就是我们常说的超时重传

TCP 会在以下两种情况发生超时重传:

  • 数据包丢失
  • 确认应答丢失

Q:重传时间的确定

  1. RTT(Round-Trip Time 往返时延) 指的是数据发送时刻到接收到确认的时刻的差值,也就是包的往返时间。
  2. 超时重传时间是以 RTO (Retransmission Timeout 超时重传时间)表示

假设在重传的情况下,超时时间 RTO 「较长或较短」时,会发生什么事情呢?

  • 当超时时间 RTO 较大时,重发就慢,丢了老半天才重发,没有效率,性能差;
  • 当超时时间 RTO 较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。

根据上述的两种情况,我们可以得知,超时重传时间 RTO 的值应该略大于报文往返 RTT 的值

        好像就是在发送端发包时记下 t0 ,然后接收端再把这个 ack 回来时再记一个 t1,于是 RTT = t1 – t0。没那么简单,这只是一个采样,不能代表普遍情况

        实际上「报文往返 RTT 的值」是经常变化的,因为我们的网络也是时常变化的。也就因为「报文往返 RTT 的值」 是经常波动变化的,所以「超时重传时间 RTO 的值」应该是一个动态变化的值

我们来看看 Linux 是如何计算 RTO 的呢?

估计往返时间,通常需要采样以下两个:

  • 需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个平滑 RTT 的值,而且这个值还是要不断变化的,因为网络状况不断地变化。
  • 除了采样 RTT,还要采样 RTT 的波动范围,这样就避免如果 RTT 有一个大的波动的话,很难被发现的情况。

2.4.4.快速重传

TCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传

知识体系之TCP/IP详解_第5张图片

发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2

Q:快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传一个,还是重传所有的问题。

A:SACK( Selective Acknowledgment)机制 —— 选择性确认

        这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,接收方可以将已收到的数据的信息发送给「发送方」,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据

如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK 信息发现只有 200~299 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。

知识体系之TCP/IP详解_第6张图片

2.4.5.滑动窗口 + 流量控制

① 引入窗口概念的原因:

  1. 我们都知道 TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。
  2. 这个模式就有点像我和你面对面聊天,你一句我一句。但这种方式的缺点是效率比较低的。
  3. 如果你说完一句话,我在处理其他事情,没有及时回复你,那你不是要干等着我做完其他事情后,我回复你,你才能说下一句话,很显然这不现实。

为解决这个问题,TCP 引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率

那么有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值

② 窗口大小由哪一方决定?

TCP 头里有一个字段叫 Window,也就是窗口大小。

  1. 这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
  2. 所以,通常窗口的大小是由接收方的窗口大小来决定的。

③原理:接收方每次接收到数据,向发送方返回ACK的时候,携带一个接收窗口大小(接收窗口的大小,是接收方根据自己的处理能力来确定的)。这就是滑动窗口机制

滑动窗口的实现方式

  1. 发送方每次向接收方发送一个窗口大小的数据,发送完成后等待ACK
  2. 发送端接收到ACK后,滑动窗口会一直向右滑动
  3. 在窗口滑动的过程中,如果窗口中的某个pos的数据没有收到ACK,可能会导致窗口不能向前滑动,当超时后,超时定时器会重发数据,直到接收到数据ACK位置
  4. 如此反复执行1~3,直到滑动窗口滑动到数据的尾部,数据全部发送完毕

零窗口

        当接收缓冲区满时,接收方在回复ACK的时候,会告诉发送方自己的接收窗口rwnd=0了,此时发送方接收到窗口大小为0后,就会停止发送数据。(此时会启动坚持定时器

Q1: 为什么引入坚持定时器?

A1: ① B发送给A窗口为0的确认包后,A之后就不会再给A发送数据。② 等待一段时间后,B缓冲区有数据,就会向A发送一个报文,通知A现在可以发送数据了。③ 但是该报文在传输的过程中一旦丢失了,就会形成下面的“双方死等现象”:A等待B通知它发送数据,B发送报文后等待A发来数据。

Q2: 零窗口探测报文机制

A2: 坚持计时器(在A接收到一个窗口为0的报文后,将每隔一段时间,发送给B探测报文,即探测接收窗口大小的报文,一旦接收窗口大小不为0,就可以继续发送数据)

糊涂窗口综合症

如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。

到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症。

就好像一个可以承载 50 人的大巴车,每次来了一两个人,就直接发车。除非家里有矿的大巴司机,才敢这样玩,不然迟早破产。要解决这个问题也不难,大巴司机等乘客数量超过了 25 个,才认定可以发车。

现举个糊涂窗口综合症的栗子,考虑以下场景:

        接收方的窗口大小是 360 字节,但接收方由于某些原因陷入困境,假设接收方的应用层读取的能力如下:

  • 接收方每接收 3 个字节,应用程序就只能从缓冲区中读取 1 个字节的数据;
  • 在下一个发送方的 TCP 段到达之前,应用程序还从缓冲区中读取了 40 个额外的字节;

2.4.6.拥塞控制

①为什么要有拥塞控制呀,不是有流量控制了吗?

        前面的流量控制是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。

        一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。

        在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大....

        所以,TCP 不能忽略网络上发生的事,它被设计成一个无私的协议,当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。

        于是,就有了拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。

总结:流量控制、拥塞控制完全是为了解决不同的问题产生的。① 流量控制主要是接收端为了防止发送方发送数据的速度,一直给发送方应答一个接收窗口。② 拥塞控制主要是通过拥塞窗口cwnd来防止过多的数据注入到网络中,导致网络拥堵

② 什么是拥塞窗口?和发送窗口有什么关系呢?

拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的

        我们在前面提到过 发送窗口 swnd 和 接收窗口 rwnd 是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。

③ 拥塞窗口 cwnd 变化的规则:

  • 只要网络中没有出现拥塞,cwnd 就会增大;
  • 但网络中出现了拥塞,cwnd 就减少;

④ 那么怎么知道当前网络是否出现了拥塞呢?

其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了拥塞。

⑤ 拥塞控制有哪些控制算法?

拥塞控制主要是四个算法:

  • 慢启动
    • 规则:当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1(指数增大)
    • 有一个叫慢启动门限 ssthresh (slow start threshold)状态变量
      • 当 cwnd < ssthresh 时,使用慢启动算法
      • 当 cwnd >= ssthresh 时,就会使用「拥塞避免算法」
  • 拥塞避免
    • 规则:每当收到一个 ACK 时,cwnd 增加 1/cwnd。(线性增长)
  • 拥塞发生
    • 当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种:超时重传、快速重传
    • 详细:cwnd是线性增长的(“加法增大”);直到发生【网络超时】,就会进行调整:门限值ssthresh设置为窗口的1/2,cwnd设置为1,继续执行慢开始算法
  • 快速恢复
    • 快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 RTO 超时那么强烈。
    • 详细:当发送方收到同一个包的连续3次重复确认时(说明存在网络丢包),此时,不用等待【网络超时】,直接重传数据数据给接收方。(即:接收方向发送方回复了1,2,4包的应答,且连续对包2进行了3次应答,说明包3丢失了)

2.5.用了TCP协议,数据一定不会丢失么?

TCP是可靠的,为什么通信会丢包? 怎么解决?

        例如服务器给客户端发大量数据,Send的频率很高,那么就有可能在Send时发生错误(原因可能是又多种,可能是程序处理逻辑问题,多线程同步问题,缓冲区溢出问题等等),如果没有对Send失败做处理重发数据,那么客户端收到的数据就会比理论应该收到的少,就会造成丢数据,丢包的现象。

        这种现象,其实本质上来说不是丢包,也不是丢数据,只是因为程序处理有错误,导致有些数据没有成功地被socket发送出去。

        常用的解决方法如下:拆包、加包头、发送,组合包,如果客户端、服务端掉线,常采用心跳测试。

2.5.1. 数据包的发送流程

为了发送数据包,两端首先会通过三次握手,建立TCP连接。

        一个数据包,从聊天框里发出,消息会从聊天软件所在的用户空间拷贝到内核空间发送缓冲区(send buffer),数据包就这样顺着传输层、网络层,进入到数据链路层,在这里数据包会经过流控,再通过RingBuffer发到物理层的网卡。数据就这样顺着网卡发到了纷繁复杂的网络世界里。这里头数据会经过n多个路由器交换机之间的跳转,最后到达目的机器的网卡处。

        此时,目的机器的网卡会通知DMA将数据包信息放到RingBuffer中,再触发一个硬中断CPUCPU触发软中断ksoftirqdRingBuffer收包,于是一个数据包就这样顺着物理层,数据链路层,网络层,传输层,最后从内核空间拷贝到用户空间里的聊天软件里。

2.5.2. 丢包场景

下面我们重点讲解下常见容易发生丢包的场景:

① 建立连接时丢包

  1. 在服务端,第一次握手之后,会先建立个半连接,然后再发出第二次握手。这时候需要有个地方可以暂存这些半连接。这个地方就叫半连接队列
  2. 如果之后第三次握手来了,半连接就会升级为全连接,然后暂存到另外一个叫全连接队列的地方,坐等程序执行accept()方法将其取走使用。

        是队列就有长度,有长度就有可能会满,如果它们满了,那新来的包就会被丢弃。(发生丢包,导致建立连接失败)

可以通过下面的方式查看是否存在这种丢包行为:

# 全连接队列溢出次数
# netstat -s | grep overflowed
    4343 times the listen queue of a socket overflowed

# 半连接队列溢出次数
# netstat -s | grep -i "SYNs to LISTEN sockets dropped"
    109 times the listen queue of a socket overflowed 

② 流量控制丢包

应用层能发网络数据包的软件有那么多,如果所有数据不加控制一股脑冲入到网卡,网卡会吃不消,那怎么办?让数据按一定的规则排个队依次处理,也就是所谓qdisc(排队规则),这也是我们常说的流量控制机制。

排队,得先有个队列,而队列有个长度txqueuelen。当发送数据过快,流控队列长度txqueuelen又不够大时,就容易出现丢包现象。

可以通过下面的ifconfig命令,查看TX下的dropped字段,当它大于0时,则有可能是发生了流控丢包。

# ifconfig eth0
eth0: flags=4163  mtu 1500
        inet 172.21.66.69  netmask 255.255.240.0  broadcast 172.21.79.255
        inet6 fe80::216:3eff:fe25:269f  prefixlen 64  scopeid 0x20
        ether 00:16:3e:25:26:9f  txqueuelen 1000  (Ethernet)
        RX packets 6962682  bytes 1119047079 (1.0 GiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 9688919  bytes 2072511384 (1.9 GiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

③ 网卡丢包

网卡和它的驱动导致丢包的场景也比较常见,原因很多,比如网线质量差,接触不良。除此之外,我们来聊几个常见的场景。

④ RingBuffer过小导致丢包

上面提到,在接收数据时,会将数据暂存到RingBuffer接收缓冲区中,然后等着内核触发软中断慢慢收走。如果这个缓冲区过小,而这时候发送的数据又过快,就有可能发生溢出,此时也会产生丢包

知识体系之TCP/IP详解_第7张图片

⑤ 网卡性能不足

网卡作为硬件,传输速度是有上限的。当网络传输速度过大,达到网卡上限时,就会发生丢包。这种情况一般常见于压测场景。

⑥ 接收缓冲区丢包

我们一般使用TCP socket进行网络编程的时候,内核都会分配一个发送缓冲区和一个接收缓冲区

当我们想要发一个数据包,会在代码里执行send(msg),这时候数据包并不是一把梭直接就走网卡飞出去的。而是将数据拷贝到内核发送缓冲区就完事返回了,至于什么时候发数据,发多少数据,这个后续由内核自己做决定。

接收缓冲区作用也类似,从外部网络收到的数据包就暂存在这个地方,然后坐等用户空间的应用程序将数据包取走。

那么问题来了,如果缓冲区设置过小会怎么样?

  1. 对于发送缓冲区,执行send的时候,如果是阻塞调用,那就会等,等到缓冲区有空位可以发数据。
  2. 如果是非阻塞调用,就会立刻返回一个 EAGAIN 错误信息,意思是 Try again。让应用程序下次再重试。这种情况下一般不会发生丢包
    1. 当接受缓冲区满了,事情就不一样了,它的TCP接收窗口会变为0,也就是所谓的零窗口,并且会通过数据包里的win=0,告诉发送端,"球球了,顶不住了,别发了"。
    2. 一般这种情况下,发送端就该停止发消息了,但如果这时候确实还有数据发来,就会发生丢包

2.5.3. 发生丢包了怎么解决? 使用TCP协议

说了这么多。只是想告诉大家,丢包是很常见的,几乎不可避免的一件事情

但问题来了,发生丢包了怎么办?

这个好办,用TCP协议去做传输。

2.5.4. 用了TCP协议就一定不会丢包么?

我们知道TCP位于传输层,在它的上面还有各种应用层协议,比如常见的HTTP或者各类RPC协议。

  1. TCP保证的可靠性,是传输层的可靠性。也就是说,TCP只保证数据从A机器的传输层可靠地发到B机器的传输层。
  2. 至于数据到了接收端的传输层之后,能不能保证到应用层,TCP并不管。

举例:

  1. 假设现在,我们输入一条消息,从聊天框发出,走到传输层TCP协议的发送缓冲区,不管中间有没有丢包,最后通过重传都保证发到了对方的传输层TCP接收缓冲区,此时接收端回复了一个ack,发送端收到这个ack后就会将自己发送缓冲区里的消息给扔掉。到这里TCP的任务就结束了。
  2. TCP任务是结束了,但聊天软件的任务没结束。
  3. 聊天软件还需要将数据从TCP的接收缓冲区里读出来,如果在读出来这一刻,手机由于内存不足或其他各种原因,导致软件崩溃闪退了。==> 发送端以为自己发的消息已经发给对方了,但接收端却并没有收到这条消息。于是乎,消息就丢了。

2.5.5. 这类丢包问题怎么解决?

大家有没有发现,有时候我们在手机里聊了一大堆内容,然后登录电脑版,它能将最近的聊天记录都同步到电脑版上。也就是说服务器可能记录了我们最近发过什么数据,假设每条消息都有个id,服务器和聊天软件每次都拿最新消息的id进行对比,就能知道两端消息是否一致,就像对账一样。

  1. 对于发送方,只要定时跟服务端的内容对账一下,就知道哪条消息没发送成功,直接重发就好了。
  2. 如果接收方的聊天软件崩溃了,重启后跟服务器稍微通信一下就知道少了哪条数据,同步上来就是了,所以也不存在上面提到的丢包情况。

可以看出,TCP只保证传输层的消息可靠性,并不保证应用层的消息可靠性。如果我们还想保证应用层的消息可靠性,就需要应用层自己去实现逻辑做保证。

  • 下面的server就是应用层实现的服务器,两个用户AB之间的通信,都经过server做中转

知识体系之TCP/IP详解_第8张图片

那么问题叒来了,两端通信的时候也能对账,为什么还要引入第三端服务器?

主要有三个原因。

  • 第一,如果是两端通信,你聊天软件里有1000个好友,你就得建立1000个连接。但如果引入服务端,你只需要跟服务器建立1个连接就够了,聊天软件消耗的资源越少,手机就越省电
  • 第二,就是安全问题,如果还是两端通信,随便一个人找你对账一下,你就把聊天记录给同步过去了,这并不合适吧。如果对方别有用心,信息就泄露了。引入第三方服务端就可以很方便的做各种鉴权校验。
  • 第三,是软件版本问题。软件装到用户手机之后,软件更不更新就是由用户说了算了。如果还是两端通信,且两端的软件版本跨度太大,很容易产生各种兼容性问题,但引入第三端服务器,就可以强制部分过低版本升级,否则不能使用软件。但对于大部分兼容性问题,给服务端加兼容逻辑就好了,不需要强制用户更新软件。

所以看到这里大家应该明白了,我把服务端去掉,并不单纯是为了简单

3.三次握手

3.1.三次握手过程

知识体系之TCP/IP详解_第9张图片

  • 一开始,客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口,处于 LISTEN 状态
  • 客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。
  • 服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1, 接着把 SYN 和 ACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。
  • 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态。
  • 服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态。

3.1.1.非阻塞套接字Connect

TCP的Connect

如果connect返回值为-1,如果错误码不是EINPROGRESS,说明出现错误,直接return;如果错误码是EINPROGRESS,意味着:表示此时TCP三次握手仍在进行。之后可以使用select检查连接是否建立成功

给select设置等待时间,并将打开的socket添加至select监控(即使用select函数等待正在后台连接的connect函数):

  1. 如果只可写,说明连接成功
  2. 如果描述符既可读又可写,分为两种情况
    1. socket连接错误(这是系统规定的,可读可写时可能是connect连接成功后远程主机断开了连接close(socket)
    2. connect连接成功,socket读缓冲区接收到了远程主机发送的数据,可以用getsockopt(socket, SOL_SOCKET, SO_ERROR, &error, sizeof(int))来得到error的值来判断,如果为0,则说明socket connect成功。否则,也是失败。

UDP的Connect

UDP中的connect与TCP中的connect有着本质上的区别:

  1. TCP的connect会引起三次握手,UDP不会
  2. 由于UDP是无连接的,在调用connect时其实没有向外发包,只是在协议栈中记录了该状态

【补充】采用connect的UDP发送接受报文可以调用send,write和recv,read操作,当然也可以调用sendto,recvfrom

  1. 异步ICMP错误不会返回给unconnect的UDP套接字,调用connect后,可以接收到异步ICMP错误
  2. 提高发送数据的效率
    1. 普通的UDP发送两个报文内核做了如下:#1:建立连结#2:发送报文#3:断开连结#4:建立连结#5:发送报文#6:断开连结
    2. 采用connect方式的UDP发送两个报文内核如下处理:#1:建立连结#2:发送报文#3:发送报文另外一点,  每次发送报文内核都由可能要做路由查询

Q1: UDP是否可以多次调用connect?

A1: UDP可以多次调用connect,而TCP只能调用一次connect

Q2: UDP多次调用connect的目的?

A2: 断开和之前的ip,port的连结,与新的ip,port连接

3.2.为什么是三次握手?不是两次、四次?

否则会带来 冗余链接 的问题

1. 当两次连接请求到来时,由于网络阻塞,若旧的连接请求先到达,会建立一次无效的连接,消耗资源

知识体系之TCP/IP详解_第10张图片

描述过程:旧的SYN请求包,比新的SYN请求包先到达,浪费一次服务端的连接

  1. 客户端:先后发起了2次SYN请求,分别是seq=100,seq=200(客户端记录了状态,它想收到ack=201的回包)
  2. 之后发生了网络阻塞,旧的seq=100先到达服务端,因为是两次握手,所以会建立连接成功。之后,服务端会回复ack=101
  3. 客户端收到了ack=101,不符合自己的预期,会回复rst断开链接。(此时,服务端之前建立的链接就是无效的
  4. 之后,新的SYN请求包syc=200到达了服务端,可以建立成功

2. 同步双方初始序列号

        TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:

  • 接收方可以去除重复的数据;
  • 接收方可以根据数据包的序列号按序接收;
  • 可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道);

        可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。

3. 客户端发送SYN,超时没有收到ACK,会重传SYN到服务端(服务端收到多次SYN后,会建立多个冗余的连接)

        如果只有「两次握手」,当客户端发生的 SYN 报文在网络中阻塞,客户端没有接收到 ACK 报文,超时就会重新发送 SYN ,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK 报文,所以服务端每收到一个 SYN 就只能先主动建立一个连接,这会造成:服务端在收到多个请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。

3.3.为什么每次建立TCP连接时,初始化的序列号都要求不一样呢?

1. 为了防止黑客伪造相同的序列号的TCP报文被对方接收

2. 为了防止在新的连接中,接收到旧的连接中的报文

 过程如下:

  • 客户端和服务端建立一个 TCP 连接
  • 客户端发送数据包A被网络阻塞了,然后超时重传了这个数据包
  • 而此时服务端设备断电重启了,之前与客户端建立的连接就消失了,于是在收到客户端的数据包的时候就会发送 RST 报文
  • 紧接着,客户端又与服务端建立了与上一个连接相同四元组的连接(此时,采用相同的syn=0来建立连接)
  • 在新连接建立完成后,上一个连接中被网络阻塞的数据包A正好抵达了服务端,刚好该数据包的序列号正好是在服务端的接收窗口内,所以该数据包会被服务端正常接收,就会造成数据错乱(即:在一个新的连接中,接收到了旧的连接的数据)

3.3.1.初始序列号 ISN 是如何随机产生的?

起始 ISN 是基于时钟的,每 4 微秒 + 1,转一圈要 4.55 个小时。

RFC793 提到初始化序列号 ISN 随机生成算法:ISN = M + F(localhost, localport, remotehost, remoteport)。

  • M 是一个计时器,这个计时器每隔 4 微秒加 1。
  • F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。

可以看到,随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。

3.4.数据包丢失,TCP是怎么处理的?

3.4.1.第一次握手丢失了,会发生什么?

描述:当客户端想和服务端建立 TCP 连接的时候,首先客户端会先发送 SYN 报文,然后进入到 SYN_SENT 状态。

结论:如果客户端迟迟收不到服务端的 SYN-ACK 报文,就会超时重传 SYN 报文(重传的 SYN 报文的序列号都是一样的)

SYN超时重传的次数/时间:

  • 客户端的 SYN 报文最大重传次数由 tcp_syn_retries 内核参数控制,这个参数是可以自定义的,默认值一般是 5
  • 指数退避:重传的时间,依次是1s,2s,4s,8s,16s
  • 客户端再次等待,超时断开等待:当第5次超时重传后,客户端不会再继续重传了,此时,仍然会继续等待32s,若服务端仍然没有回应ACK,客户端就断开等待

总耗时是 (1+2+4+8+16) +32=63 秒,大约 1 分钟左右

3.4.2.第二次握手丢失了,会发生什么?

描述:当服务端收到客户端的第一次握手后,就会回 SYN-ACK 报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD 状态。

第二次握手的 SYN-ACK 报文其实有两个目的 :

  • 第二次握手里的 ACK, 是对第一次握手SYN的确认报文
  • 第二次握手里的 SYN,是服务端发起建立 TCP 连接的报文

所以,如果第二次握手丢了,就会发生比较有意思的事情,具体会怎么样呢?

  • 客户端:超时,重传SYN请求包
    • 因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文
  • 服务端:超时,重传SYN+ACK
    • 因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了

SYN-ACK超时重传的次数/时间:

  • 客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由 tcp_syn_retries内核参数决定(同第一次握手SYN超时重传)
  • 服务端会重传 SYN-ACK 报文,也就是第二次握手,最大重传次数由 tcp_synack_retries 内核参数决定。(机制也与第一次握手SYNC超时重传相同)

3.4.3.第三次握手丢失了,会发生什么?

注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文

描述:客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH 状态。

结论:第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制

  • 服务端重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数

3.5.syn请求队列 已连接队列

3.5.1. listen的第2个参数(两个队列:syn请求队列/已连接队列)

在早期 Linux 内核 backlog 是 SYN 队列大小,也就是未完成的队列大小。

在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建立的队列长度,所以现在通常认为 backlog 是 accept 队列。

但是上限值是内核参数 somaxconn 的大小,也就说 accpet 队列长度 = min(backlog, somaxconn)。

        listen函数将主动套接字转换为被动监控套接字,其第二个参数backlog决定了内核的连接缓存队列长度。对于一个给定的监听套接字,内核维护两个队列:

半连接队列,也称 SYN 队列

全连接队列,也称 accept 队列
知识体系之TCP/IP详解_第11张图片

正常流程:

  • 当服务端接收到客户端的 SYN 报文时,会创建一个半连接的对象,然后将其加入到内核的「 SYN 队列」;
  • 接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文;
  • 服务端接收到 ACK 报文后,从「 SYN 队列」取出一个半连接对象,然后创建一个新的连接对象放入到「 Accept 队列」,此时,完成了三次握手
  • 应用通过调用 accpet() socket 接口,从「 Accept 队列」取出连接对象。(注意:accept之前就已经完成了三次握手,它只是把已经建好的连接从accept队列中取走)
        backlog决定了两个队列的长度之和(并不是说两个队列之和等于backlog,而是存在个转换,依赖于具体实现)。

        不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,默认情况都会丢弃报文。

3.5.2. SYN泛洪攻击/DDOC攻击?如何避免?

SYN泛洪攻击/DDOC攻击

        攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。

        SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。

如何避免?

避免 SYN 攻击方式,可以有以下四种方法:

  1. 调大 netdev_max_backlog:当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数,默认值是 1000
  2. 增大 TCP 半连接队列
    1. 增大 listen() 函数中的 backlog
    2. 增大 net.ipv4.tcp_max_syn_backlog
    3. 增大 net.core.somaxconn
  3. 开启 tcp_syncookies:在不使用 SYN 半连接队列的情况下成功建立连接,相当于绕过了 SYN 半连接来建立连接(下面详细描述原理)
  4. 减少 SYN+ACK 重传次数:让请求快速断开
    1. 当服务端受到 SYN 攻击时,服务端就会有大量处于 SYN_REVC 状态的 TCP 连接,处于这个状态的 TCP 会重传 SYN+ACK ,当重传超过次数达到上限后,就会断开连接。
    2. SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定(默认值是 5 次),将其减少

tcp_syncookies原理

核心:防止 半连接队列(SYN 队列) 被打满

知识体系之TCP/IP详解_第12张图片

具体过程:

  • 当 「 SYN 队列」满之后,后续服务端收到 SYN 包,不会丢弃(如果不开启tcp_syncookies,SYN请求包会直接丢弃),服务端会根据算法,计算出一个 cookie 值
  • 服务端将 cookie 值放到第二次握手报文的「序列号」里,回传给客户端
  • 服务端接收到客户端的应答报文时,服务端会检查这个 ACK 包的合法性。如果合法,将该连接对象放入到「 Accept 队列」
  • 最后应用程序通过调用 accpet() 接口,从「 Accept 队列」取出的连接

3.6.​​​​​ API与三次握手

3.6.1.连接建立是在accept函数么?

socket客户端连接上服务端是listen之后而非在accept之时

在刚刚接触网络编程时,很长一段时间都以为只有服务端调用accept后,客户端才会connect成功,但是实际上①只要服务端开启listen,客户端调用connect时,就会连接成功(即三次握手成功)。②accept,只是将fd从连接队列中移除

【结论】:此时,即使服务端在listen后调用了sleep后,没调用accept。客户端仍然可以调用send发送数据,也会发送成功。

3.6.2.没有 listen,能建立 TCP 连接吗?

答案:可以的

        客户端是可以自己连自己的形成连接(TCP自连接),也可以两个客户端同时向对方发出请求建立连接(TCP同时打开),这两个情况都有个共同点,就是没有服务端参与,也就是没有 listen,就能 TCP 建立连接。

更想了解这个问题,可以参考这篇文章:服务端没有 listen,客户端发起连接建立,会发生什么?

3.7.如何优化TCP? 

3.7.1.TCP 三次握手的性能提升

  1. 调整SYN报文重传次数、SYN+ACK报文重传次数
  2. 调整SYN半连接队列长度、Accept队列长度
  3. TCP Fast Open变成两次握手

3.7.2.TCP 四次握手的性能提升

  1. 调整FIN报文重传次数
  2. 调整FIN_WAIT2状态的时间
  3. 调整孤儿连接的上限个数
  4. 调整time_wait状态的上限个数
  5. 调整time_wait状态的链接

3.7.3.TCP数据传输的性能提升

  1. 扩大窗口大小
  2. 调整发送缓冲区范围、调整接收缓冲区范围
  3. 接收缓冲区动态调节
  4. 调整内存范围

4.四次挥手

4.1.四次挥手过程

每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手

知识体系之TCP/IP详解_第13张图片

  • 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSE_WAIT 状态。
  • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
  • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
  • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
  • 服务端收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
  • 客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。

注意:主动关闭连接的,才有 TIME_WAIT 状态

4.1.1.TIME-WAIT存在的原因?

MSL:Maximum Segment Lifetime报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃

TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间

主动发起关闭连接的一方,才会有 TIME-WAIT 状态。需要 TIME-WAIT 状态,主要是两个原因:

  1. 防止历史连接中的数据,被后面相同四元组的连接错误的接收(2MSL能保证两个方向上的数据包丢被丢弃)
  2. 保证「被动关闭连接」的一方,能被正确的关闭(TIME-WAIT 作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭)
    1. 如果客户端(主动关闭方)最后一次 ACK 报文(第四次挥手)在网络中丢失了,那么按照 TCP 可靠性原则,服务端(被动关闭方)会重发 FIN 报文
    2. 假设客户端没有 TIME_WAIT 状态,而是在发完最后一次回 ACK 报文就直接进入 CLOSE 状态,如果该 ACK 报文丢失了,服务端则重传的 FIN 报文,而这时客户端已经进入到关闭状态了,在收到服务端重传的 FIN 报文后,就会回 RST 报文

知识体系之TCP/IP详解_第14张图片

4.1.2.TIME-WAIT过多的危害? 应该怎么解决?

过多的 TIME-WAIT 状态主要的危害有2种:

  • 第一是占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等
  • 第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为 32768~61000,也可以通过 net.ipv4.ip_local_port_range参数指定范围

这里给出优化 TIME-WAIT 的几个方式,都是有利有弊:

  • 打开 Linux 内核参数选项 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps,可以复用处于TIME_WAIT的socket为新的连接所用
    • tcp_tw_reuse:只能用客户端(连接发起方),因为开启了该功能,在调用 connect() 函数时,内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用
    • 使用tcp_tw_reuse这个选项,还有一个前提,需要打开对 TCP 时间戳的支持(由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃)
  • net.ipv4.tcp_max_tw_buckets
    • 这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将后面的 TIME_WAIT 连接状态重置,这个方法比较暴力
  • 程序中使用 SO_LINGER ,来设置调用 close 关闭连接行为(应用强制使用 RST 关闭:如果l_onoff为非 0, 且l_linger值为 0,那么调用close后,会立该发送一个RST标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT状态,直接关闭)
  • 《UNIX网络编程》一书中却说道:TIME_WAIT 是我们的朋友,它是有助于我们的,不要试图避免这个状态,而是应该弄清楚它。
  • 如果服务端要避免过多的 TIME_WAIT 状态的连接,就永远不要主动断开连接,让客户端去断开,由分布在各处的客户端去承受 TIME_WAIT

4.1.3.服务器出现大量 TIME_WAIT 状态的原因有哪些?

首先要知道 TIME_WAIT 状态是主动关闭连接方才会出现的状态,所以如果服务器出现大量的 TIME_WAIT 状态的 TCP 连接,就是说明服务器主动断开了很多 TCP 连接

  • 在使用 HTTP/1.1长连接的时候,当发起 HTTP 请求的客户端数量超过服务器指定的最大长连接个数(比如 nginx 配置中的 keepalive_requests 参数),那么服务器会主动断开这个连接,此时服务器上就会出现大量的TIME_WAIT 状态
    • 解决方式:调大最大长连接个数。
  • 如果服务器是反向代理服务器,nginx(在服务器上跑)与后端进行大量的短连接请求,由于nginx 会主动挂断这个连接,在服务器上就会出现大量的 TIME_WAIT 状态
    • 解决方式:使用长连接
  • HTTP/1.1长连接的时候,客户端在超时时间内没有新的数据发送,那么服务器会主动挂断这个连接,在服务器上就会出现 TIME_WAIT 状态。
    • 这种情况一般不会出现大量的 TIME_WAIT 状态,因为很难出现同一时间出现大量客户端一直不发送新的数据。如果出现了,可以往网络问题的方向排查,比如是否是因为网络问题,导致客户端发送的数据一直没有被服务器接收到。
  • 服务器的进程挂掉了,内核会发起 FIN 报文,这时候相当于服务器会主动挂断这个连接,于是在服务器上就会出现大量的 TIME_WAIT 状态

4.1.4.服务器出现大量 CLOSE_WAIT 状态的原因有哪些?

CLOSE_WAIT 状态是「被动关闭方」才会有的状态,而且如果「被动关闭方」没有调用 close 函数关闭连接,那么就无法发出 FIN 报文,从而无法使得 CLOSE_WAIT 状态的连接转变为 LAST_ACK 状态。

所以,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接

当服务端出现大量 CLOSE_WAIT 状态的连接的时候,通常都是代码的问题,这时候我们需要针对具体的代码一步一步的进行排查和定位,主要分析的方向就是服务端为什么没有调用 close

4.2.断开连接

4.2.1.为什么是4次挥手?不是3次?

三次握手,是因为第二次的ack和seq是同时发送的,而四次挥手不行,因为

  1. 四次挥手要保证全双工的通道断开
  2. 第一对FIN和ACK,只是断开了发送通道,而此时接收方的缓冲区中可能还有数据未接收,因此要等到缓冲区中无数据后,才能发送第二队FIN和ACK,断开接收通道

4.2.2.close/shutdown、引用计数

close:会关闭套接字,但是如果调用close时,有其他进程共享着该套接字,那么该连接仍然是打开的,该连接仍然可以被其他打开的进程读写

shutdown:会切断进程共享的套接字的所有连接,不管引用计数是否为0,都会关闭。

关闭的方向由第2个参数决定SHUT_RD/SHUT_WR/SHUT_RDWR

4.3.挥手数据包丢失了,TCP是怎么处理的?

4.3.1.第一次挥手丢失了,会发生什么?==> 重传FIN

描述:当客户端(主动关闭方)调用 close 函数后,就会向服务端发送 FIN 报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1 状态。正常情况下,如果能及时收到服务端(被动关闭方)的 ACK,则会很快变为 FIN_WAIT2状态

结论:如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由 tcp_orphan_retries 参数控制。

        当客户端重传 FIN 报文的次数超过 tcp_orphan_retries 后,就不再发送 FIN 报文,则会在等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到第二次挥手,那么直接进入到 close 状态

4.3.2.第二次挥手丢失了,会发生什么?==> 重传FIN

描述:当服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入到 CLOSE_WAIT 状态。

        在前面我们也提了,ACK 报文是不会重传的,所以:如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。

4.3.3.第三次挥手丢失了,会发生什么?==> 重传FIN

描述:

当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT 状态,顾名思义,它表示等待应用进程调用 close 函数关闭连接。

此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。

服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。

如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。

具体过程:

  • 当服务端重传第三次挥手报文的次数达到了 3 次后,当达到了重传最大次数tcp_orphan_retries,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第四次挥手(ACK报文),那么服务端就会断开连接。
  • 客户端因为是通过 close 函数关闭连接的,处于 FIN_WAIT_2 状态是有时长限制的,如果 tcp_fin_timeout 时间内还是没能收到服务端的第三次挥手(FIN 报文),那么客户端就会断开连接

4.3.4.第四次挥手丢失了,会发生什么?==> 重传FIN

当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT 状态。

在 Linux 系统,TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。

然后,服务端(被动关闭方)没有收到 ACK 报文前,还是处于 LAST_ACK 状态。

如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。

5. TCP故障场景分析

TCP keepalive和HTTP keep-alive是一个东西么?

  • TCP 的 Keepalive,是由 TCP 层(内核态) 实现的,称为 TCP 保活机制
  • HTTP 的 Keep-Alive,是由应用层(用户态) 实现的,称为 HTTP 长连接
    • HTTP 长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态,是为了解决一次请求一个响应而提出的
    • Connection: Keep-Alive
    • keepalive_timeout: 用来指定 HTTP 长连接的超时时间

5.1.TCP 连接,主机崩溃\进程崩溃有什么区别?

主机崩溃\进程崩溃,二者的区别是:

1. 主机崩溃:操作系统也死了

2. 进程崩溃:进程死了,但是操作系统没死,操作系统还能帮忙完成TCP的挥手

鹅厂面试题:一个TCP链接,没有打开keepalive选项,没有数据交互,现在一端突然掉电和一端的进程crash了,请问这两种情况有什么区别?

case1:在开启了 TCP keepalive,且双方一直没有数据交互的情况下 ==> TCP探测报文

当两端的TCP连接一直没有数据交互,达到了触发TCP keepalive机制的条件(2h11min15sec没有数据交互),那么,内核里的TCP协议就会发送探测报文。

  • 如果对端程序是正常工作的:当TCP保活的探测报文发送给对端,对端会正常响应,这样TCP保活时间会被重置,等待下一个TCP保活时间的到来
  • 如果对端主机崩溃or对端由于其他原因导致报文不可达:当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡

case2:在没有开启 TCP keepalive,且双方一直没有数据交互的情况下

① 如果客户端/服务端的「主机崩溃」了,会发生什么?

  • 主机崩溃了,操作系统都停了,内核也无法帮TCP连接完成最终的挥手。

        客户端主机崩溃了,服务端是无法感知到的,在加上服务器没有开启TCP keepalive,又没有数据交互的情况下,服务端的 TCP 连接将会一直处于 ESTABLISHED 连接状态,直到服务端重启进程。

        所以,我们可以得知一个点,在没有使用 TCP 保活机制且双方不传输数据的情况下,一方的 TCP 连接处在 ESTABLISHED 状态,并不代表另一方的连接还一定正常

② 如果客户端/服务器的「进程崩溃」了,会发生什么?

  • 进程崩溃了,操作系统还在,只是某个进程死掉了而已,内核还在,它能帮TCP连接完成最终的挥手。

        TCP的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有TCP连接资源,于是内核会发送第一次挥手FIN报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成TCP四次挥手的过程

case3:在没有开启 TCP keepalive,且双方有数据交互的情况下

① 客户端主机宕机,又迅速重启,会发生什么?

结论:只要有一方重启完成后,收到之前 TCP 连接的报文,都会回复 RST 报文,以断开连接

  • 如果客户端主机上没有进程绑定该 TCP 报文的目标端口号,那么客户端内核就会回复 RST 报文,重置该 TCP 连接
  • 如果客户端主机上进程绑定该 TCP 报文的目标端口号,由于客户端主机重启后,之前的 TCP 连接的数据结构已经丢失了,客户端内核里协议栈会发现找不到该 TCP 连接的 socket 结构体,于是就会回复 RST 报文,重置该 TCP 连接

② 客户端主机宕机,一直没有重启,会发生什么?

        这种情况,服务端超时重传报文的次数达到一定阈值后,内核就会判定出该 TCP 有问题,然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了,于是服务端的 TCP 连接就会断开

5.2.拔掉网线几秒,再插回去,原本的 TCP 连接还存在吗?

        可能有的同学会说,网线都被拔掉了,那说明物理层被断开了,那在上层的传输层理应也会断开,所以原本的 TCP 连接就不会存在的了。就好像, 我们拨打有线电话的时候,如果某一方的电话线被拔了,那么本次通话就彻底断了。

真的是这样吗?

        上面这个逻辑就有问题。问题在于,错误的认为拔掉网线这个动作会影响传输层,事实上并不会影响。

        实际上,TCP 连接在 Linux 内核中是一个名为 struct socket 的结构体,该结构体的内容包含 TCP 连接的状态等信息。当拔掉网线的时候,操作系统并不会变更该结构体的任何内容,所以 TCP 连接的状态也不会发生改变。

接下来,要看拔掉网线后,根据是否有数据传输,查看双方做了什么动作

① 拔掉网线后,有数据传输

        在客户端拔掉网线后,服务端向客户端发送的数据报文会得不到任何的响应,在等待一定时长后,服务端就会触发超时重传机制,重传未得到响应的数据报文

  1. 如果在服务端重传报文的过程中,客户端刚好把网线插回去了,由于拔掉网线并不会改变客户端的 TCP 连接状态,并且还是处于 ESTABLISHED 状态,所以这时客户端是可以正常接收服务端发来的数据报文的,然后客户端就会回 ACK 响应报文。此时,客户端和服务端的 TCP 连接依然存在的,就感觉什么事情都没有发生。
  2. 但是,如果如果在服务端重传报文的过程中,客户端一直没有将网线插回去,服务端超时重传报文的次数达到一定阈值后,内核就会判定出该 TCP 有问题,然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了,于是服务端的 TCP 连接就会断开
  3. 而,等客户端插回网线后,如果客户端向服务端发送了数据,由于服务端已经没有与客户端相同四元祖的 TCP 连接了,因此,服务端内核就会回复 RST 报文,客户端收到后就会释放该 TCP 连接。

② 拔掉网线后,没有数据传输

        针对拔掉网线后,没有数据传输的场景,还得看是否开启了 TCP keepalive 机制 (TCP 保活机制)。

  1. 如果没有开启 TCP keepalive 机制,在客户端拔掉网线后,并且双方都没有进行数据传输,那么客户端和服务端的 TCP 连接将会一直保持存在
  2. 而如果开启了 TCP keepalive 机制,在客户端拔掉网线后,即使双方都没有进行数据传输,在持续一段时间后,TCP 就会发送探测报文:
    1. 如果对端是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来
    2. 如果对端主机崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡

5.3.如果已经建立了连接,但是客户端突然出现故障了怎么办?

        TCP 有一个机制是保活机制。这个机制的原理是这样的:定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。

        应用程序若想使用 TCP 保活机制需要通过 socket 接口设置 SO_KEEPALIVE 选项才能够生效,如果没有设置,那么就无法使用 TCP 保活机制。

        在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:

  • tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制
  • tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
  • tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。

        也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。

5.4.如果已经建立了连接,但是服务端的进程崩溃会发生什么?

        TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。

        我自己做了个实验,使用 kill -9 来模拟进程崩溃的情况,发现在 kill 掉进程后,服务端会发送 FIN 报文,与客户端进行四次挥手

6.TCP选项

6.1.keepalive

问题:TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗?

HTTP 的 Keep-Alive 也叫 HTTP 长连接,该功能是由「应用程序」实现的,可以使得用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,减少了 HTTP 短连接带来的多次 TCP 连接建立和释放的开销。

TCP 的 Keepalive 也叫 TCP 保活机制,该功能是由「内核」实现的,当客户端和服务端长达一定时间没有进行数据交互时,内核为了确保该连接是否还有效,就会发送探测报文,来检测对方是否还在线,然后来决定是否要关闭该连接

6.2.SO_LINGER

背景知识:设置close()关闭TCP连接时的行为。当socket发送缓冲区有数据残留时,一直等待数据发送给对方,数据缓冲区中没数据后,才close

SO_LINGER

l_onoff = 0,表示 立即关闭该连接。直接丢弃发送缓冲区中的数据,并向向对方发送RST标记,来关闭该连接。主动关闭的一方将跳过TIMEWAIT状态,直接进入CLOSED状态。

l_onoff = 1,l_linger =  给关闭连接设置一个超时时间

  1. 如果超时时间没到,且缓冲区中的数据被全部发送,内核将用FIN/ACK/FIN/ACK的方式关闭连接
  2. 如果超时时间到了,且缓冲区中依然有数据,则采用方式a的处理方式

【备注】网上很多人想用方式a避免TIMEWAIT,但是,这并不是一个好的注意,这种关闭方式的用途不在这里,实际用于在于服务器在应用层的需求!

6.3.Nagle算法 & Delay ACK

6.3.1.Nagle算法

setsockopt(client_fd, SOL_TCP, TCP_NODELAY,(int[]){1}, sizeof(int));

背景:在使用一些协议通讯的时候,比如Talnet,会有一个字节一个字节发送数据的场景,(但是,每次发送一个字节的有用数据,却需要产生20个字节的IP头/20个字节的TCP头),这就导致了发送一个字节的数据需要携带至少40个字节的协议头。当发送频率很高的时候,会有很多小包没得到确认,无疑会产生很大的浪费,造成网络上充斥着很多small packet时,会造成网络拥塞。

Nagle算法思想:发送方发送数据后,在没有收到确认ACK前,不能继续发送其他数据(它保证了TCP连接上最多只能有一个未被确认的发送数据)

Nagle算法的目的是:避免网络中出现大量的Small packet,但与此同时,因为只能一个小包得到ack后,才可以发送下一个小包,所以会造成网络传输速度下降(但是Nagle算法不会那么傻,它会将小包合并,一起发送)

6.3.2.延迟确认Delay ACK

原理

① A给B发送数据后,B不会直接给A发送应答(Delay ACK);当且仅当B有数据发送给A后,顺便将应答放在数据包中,一起发送给A。

② 如果等了一段时间(约40ms),没有数据从B发送给A,就回复一个“纯”确认ACK给A。

【总结】显然,上面过程,不是直接应答,中间发生了延迟确认。

6.3.3.在Delay ACK开启时,一定要关闭Nagle算法

分析:Nagle算法本身的立意是好的,避免网络充斥着过多的小包,提高网络传输的效率。与此同时,Delay ACK也是为了提高TCP的性能,不过二者遇到了,就比较悲剧了。

场景描述:如果同时开启了Nagle算法和Delay ACK,对于write(header)-wirte(body)-read(response)场景,会产生极大的副作用。

  1. 第一个header一定是能够发送出去的
  2. 由代码可知,接收方接收到TCP报文头header后,发现是不完全的,还会再次等待Body数据的到来,由于开启了Delay ACK,因此,不会立即回复ACK给发送方
  3. 而,发送方由于开启了Nagle算法,由于没有收到ACK,则不会再发送body给接收方。
  4. 直到接收方的Delay ACK超时时,才会向发送方应答确认ACK
  5. 接收方接收到应答ACK后,才能继续发送body数据。

显然,发送方每次都要等待Delay ACK超时的应答到来后,才可以继续发送body数据,这会产生巨大的延迟。

6.3.SO_REUSEPORT

SO_REUSEPORT支持多个进程或者线程绑定到同一端口,提高服务器程序的性能,解决的问题:

  • 允许多个套接字 bind()/listen() 同一个TCP/UDP端口
    • 每一个线程拥有自己的服务器套接字
    • 在服务器套接字上没有了锁的竞争
  • 内核层面实现负载均衡
  • 安全层面,监听同一个端口的套接字只能位于同一个用户下面

你可能感兴趣的:(知识体系,tcp/ip,服务器,linux)