当前位置: 首页 > news >正文

【图解网络】学习记录

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • TCP/IP 网络模型有哪几层?
  • 键入网址到网页显示,期间发生了什么?
  • Linux 系统是如何收发网络包的?
  • NAPI
  • HTTP 是什么?
  • HTTP 常见的状态码有哪些?
  • HTTP 常见字段有哪些?
  • GET 和 POST 有什么区别?
  • HTTP 缓存有哪些实现方式?
  • 为什么 ETag 的优先级更高?
  • HTTP/1.1 的优点有哪些?
  • HTTP/1.1 的缺点有哪些?
  • HTTP/1.1 的性能如何?
  • HTTP 与 HTTPS 有哪些区别?
  • HTTPS 解决了 HTTP 的哪些问题?
  • HTTPS 是如何建立连接的?其间交互了什么?
  • 客户端校验数字证书的流程是怎样的?
  • HTTPS 的应用数据是如何保证完整性的?
  • HTTPS 一定安全可靠吗?
  • 为什么抓包工具能截取 HTTPS 数据?
  • 如何避免被中间人抓取数据?
  • HTTP/1.1 相比 HTTP/1.0 提高了什么性能?
  • HTTP/2 做了什么优化?
  • HTTP/2 有什么缺陷?
  • HTTP/3 做了哪些优化?
  • https 和 http 相比,就是传输的内容多了对称加密,可以这么理解吗?
  • 为啥 SSL 的握手是 4 次?
  • 你知道 HTTP/1.1 该如何优化吗?
  • TLS 握手过程
  • RSA 握手过程
    • TLS RSA 握手过程简单描述如下:
    • RSA 算法的缺陷
  • 密钥交换算法优化
  • TLS 升级
  • 证书优化
  • HTTPS优化
  • HTTP/1.1 协议的性能问题
  • 兼容 HTTP/1.1
  • 头部压缩
  • 静态表编码
    • 动态表编码
  • 二进制帧
  • 并发传输
  • 服务器主动推送资源
  • 关于 HTTP/2 是如何提升性能的几个方向
  • 美中不足的 HTTP/2
  • QUIC 协议的特点
  • HTTP/3 协议
  • 既然有 HTTP 协议,为什么还要有 RPC?
  • 既然有 HTTP 协议,为什么还要有 WebSocket?
  • TCP 头格式有哪些?
  • 为什么需要 TCP 协议? TCP 工作在哪一层?
  • 什么是 TCP ?
  • 什么是 TCP 连接?
  • 如何唯一确定一个 TCP 连接呢?
  • UDP 和 TCP 有什么区别呢?分别的应用场景是?
  • TCP 和 UDP 可以使用同一个端口吗?
  • TCP 三次握手过程是怎样的?
  • 不使用「两次握手」和「四次握手」的原因:
  • 为什么每次建立 TCP 连接时,初始化的序列号都要求不一样呢?
  • 初始序列号 ISN 是如何随机产生的?
  • 既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?
  • 第一次握手丢失了,会发生什么?
  • 第二次握手丢失了,会发生什么?
  • 第三次握手丢失了,会发生什么?
  • 什么是 SYN 攻击?如何避免 SYN 攻击?
  • TCP 四次挥手过程是怎样的?
  • 为什么挥手需要四次?
  • 第一次挥手丢失了,会发生什么?
  • 第二次挥手丢失了,会发生什么?
  • 第三次挥手丢失了,会发生什么?
  • 第四次挥手丢失了,会发生什么?
  • 为什么 TIME_WAIT 等待的时间是 2MSL?
  • 为什么需要 TIME_WAIT 状态?
  • TIME_WAIT 过多有什么危害?
  • 服务器出现大量 TIME_WAIT 状态的原因有哪些?
  • 客户端禁用了 HTTP Keep-Alive,服务端开启 HTTP Keep-Alive,谁是主动关闭方?
  • 客户端开启了 HTTP Keep-Alive,服务端禁用了 HTTP Keep-Alive,谁是主动关闭方?
  • 服务器出现大量 CLOSE_WAIT 状态的原因有哪些?
  • 如果已经建立了连接,但是客户端突然出现故障了怎么办?
  • 如果已经建立了连接,但是服务端的进程崩溃会发生什么?
  • 拔掉网线后, 原本的 TCP 连接还存在吗?
  • 针对 TCP 应该如何 Socket 编程?
  • listen 时候参数 backlog 的意义?
  • accept 发生在三次握手的哪一步?
  • 客户端调用 close 了,连接是断开的流程是什么?
  • 没有 accept,能建立 TCP 连接吗?
  • 介绍 TCP 的重传机制
  • 滑动窗口
  • 窗口大小由哪一方决定?
  • 发送方的滑动窗口
  • 程序是如何表示发送方的四个部分的呢?
  • 接收方的滑动窗口
  • 接收窗口和发送窗口的大小是相等的吗?
  • 流量控制
  • 操作系统缓冲区与滑动窗口的关系
  • 窗口关闭
  • TCP 是如何解决窗口关闭时,潜在的死锁现象呢?
  • 糊涂窗口综合症
  • 为什么要有拥塞控制呀,不是有流量控制了吗?
  • 怎么知道当前网络是否出现了拥塞呢?
  • 什么是拥塞窗口?和发送窗口有什么关系呢?
  • 慢启动
  • 那慢启动涨到什么时候是个头呢?
  • 拥塞避免算法
  • 拥塞发生


TCP/IP 网络模型有哪几层?

TCP/IP 网络模型共有4层:

  1. 网络接口层:负责硬件接口和底层网络传输。在 IP 头部的前面加上 MAC 头部,并封装成数据帧(Data frame)发送到网络上。

  2. 网络层(IP层):负责数据包的寻址和路由,主要协议是IP协议。IP 协议的寻址作用是告诉我们去往下一个目的地该朝哪个方向走,路由则是根据「下一个目的地」选择路径。寻址更像在导航,路由更像在操作方向盘。

  3. 传输层(TCP/UDP层):负责端到端的可靠数据传输,主要协议是TCP和UDP。TCP 相比 UDP 多了很多特性,比如流量控制、超时重传、拥塞控制等,这些都是为了保证数据包能可靠地传输给对方。

  4. 应用层:为应用程序提供网络服务,常见协议有HTTP、FTP、SMTP等。应用层是不用去关心数据是如何传输的,就类似于,我们寄快递的时候,只需要把包裹交给快递员,由他负责运输快递,我们不需要关心快递是如何被运输的。

这四层协同工作,构成了整个TCP/IP网络模型。通过这种分层的设计,网络系统的复杂性得到了很好的管理和控制,各层之间可以相对独立地进行开发和优化。
在这里插入图片描述

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

  1. 浏览器解析URL:浏览器首先会解析用户输入的URL,提取出主机名、端口号、路径等关键信息。

  2. 进行DNS查询:浏览器根据URL中的主机名,向DNS服务器发送查询请求,获取对应的IP地址。

  3. 建立TCP连接:有了IP地址后,浏览器会与目标Web服务器建立TCP连接。

  4. 发送HTTP请求:通过TCP连接,浏览器向Web服务器发送HTTP请求报文。

  5. 服务器响应请求:Web服务器接收到请求后,开始处理并准备响应数据。

  6. 传输网页数据:Web服务器将响应数据通过TCP连接传输回浏览器。

  7. 浏览器解析渲染:浏览器接收到数据后,会解析HTML、CSS、JavaScript等,并渲染出最终的网页。

  8. 展示网页:浏览器将渲染好的网页内容显示在屏幕上,用户就可以看到网页了。

Linux 系统是如何收发网络包的?

Linux 系统中网络包的收发过程:

  1. 网络包的接收:

    • 当 NIC 设备接收到网络数据包时,会产生一个中断信号通知操作系统。
    • 操作系统的中断处理程序会将数据从 NIC 缓存区复制到内核的网络栈缓冲区中。
    • 在这个过程中,NAPI 机制会发挥作用:
      • NAPI 会将收到的多个网络包临时缓存在设备驱动程序中,而不是逐个处理。
      • 当缓存积累到一定数量时,NAPI 会统一上交给上层网络协议栈处理,这就是"批量处理"。
      • NAPI 还会动态地在中断模式和轮询模式之间切换,以降低中断处理开销。
    • 数据包经过网络层(IP)、传输层(TCP/UDP)的处理后,最终传递到相应的应用程序。
  2. 网络包的发送:

    • 当应用程序要发送数据时,会将数据交给操作系统的网络栈。
    • 网络栈会将数据进行分段、封装 TCP/IP 头部等处理。
    • 数据包被传递到网络驱动程序,驱动程序将数据包写入 NIC 的发送缓冲区。
    • NIC 检测到有数据要发送,会开始通过网络电缆传输数据。
  3. 其他机制:

    • Linux 使用套接字(socket)作为应用程序与网络栈的接口。
    • 网络栈使用多个队列和锁机制来提高并发性能。
    • 内核还提供了各种网络管理工具,如 iptables 防火墙、tc 流量控制等。

总的来说,Linux 网络的收发过程涉及硬件驱动、内核网络栈、应用程序等多个层面的协作。操作系统负责将网络数据在不同层次间高效传递,NAPI 机制在其中扮演着重要的优化角色,提高了网络包处理的性能和效率。

NAPI

NAPI(New API)是 Linux 内核用于优化网络包处理性能的一种机制。它主要有以下特点:

  1. 中断处理优化:

    • 传统模式下,每收到一个网络包都产生一次中断,频繁中断会降低性能。
    • NAPI 采用"轮询"方式,在单次中断中一次性处理多个网络包。
  2. 批量处理:

    • NAPI 会将收到的网络包临时缓存在设备驱动程序中。
    • 当缓存积累到一定数量时,再统一上交给上层网络协议栈处理。
  3. 动态切换机制:

    • NAPI 可以在中断和轮询两种处理模式之间动态切换。
    • 当负载较低时使用中断模式,负载升高时切换为轮询模式。
  4. 多队列支持:

    • NAPI 支持网卡硬件多队列特性,可以并行处理不同队列的网络包。
    • 这进一步提高了网络包处理的并发性能。

综上所述, NAPI 机制通过优化中断处理、采用批量处理、动态切换模式等手段,大幅提升了 Linux 内核的网络性能。它是 Linux 网络子系统的一个重要优化机制。

HTTP 是什么?

HTTP 全称是 Hypertext Transfer Protocol,即超文本传输协议。它是Web应用程序中最常用的一种网络协议,定义了浏览器和Web服务器之间如何通信的标准。HTTP 是一个在计算机世界里专门在「两点」之间「传输」文字、图片、音频、视频等「超文本」数据的「约定和规范」。

HTTP 常见的状态码有哪些?

在这里插入图片描述

HTTP 常见字段有哪些?

HTTP 协议中常见的请求头和响应头字段有以下几种:

  1. 请求头字段:

    • Host: 请求的目标主机名
    • User-Agent: 发送请求的客户端信息
    • Accept: 客户端能够接受的资源类型
    • Accept-Encoding: 客户端能够接受的压缩编码方式
    • Accept-Language: 客户端偏好的语言
    • Cookie: 客户端随请求发送的Cookie信息
    • Content-Type: 请求消息体的MIME类型
    • Content-Length: 请求消息体的长度
    • Connection: 用于客户端要求服务器使用「HTTP 长连接」机制,以便其他请求复用。
  2. 响应头字段:

    • Content-Type: 响应消息体的MIME类型
    • Content-Length: 响应消息体的长度
    • Content-Encoding: 响应内容的压缩编码方式
    • Last-Modified: 资源最后修改时间
    • Expires: 资源过期时间
    • Cache-Control: 缓存控制指令
    • Set-Cookie: 服务器发送的Cookie信息
    • Location: 重定向目标URL

这些头字段在请求和响应中起到了重要的作用,用于传递各种元数据信息,如资源类型、编码方式、缓存控制等。

HTTP头字段是可扩展的,除了标准字段外,还可以自定义头字段来传递特殊信息。了解常见的HTTP头字段有助于我们更好地理解和使用HTTP协议。

GET 和 POST 有什么区别?

GET 的语义是请求获取指定的资源。GET 方法是安全、幂等、可被缓存的。

POST 的语义是根据请求负荷(报文主体)对指定的资源做出处理,具体的处理方式视资源类型而不同。POST 不安全,不幂等,(大部分实现)不可缓存。

GET 和 POST的区别在于:

  1. 数据传输方式不同:

    • GET 请求将参数附加在 URL 后面,以"?"分隔,数据包含在 URL 中。
    • POST 请求将参数放在请求体中进行传输,数据不会包含在 URL 中。
  2. 数据长度限制不同:

    • GET 请求的数据长度受 URL 长度限制,通常不能超过 2KB。
    • POST 请求理论上没有大小限制,可以传输大量数据。
  3. 安全性不同:

    • GET 请求将参数暴露在 URL 中,不够安全,尤其是包含敏感信息时。
    • POST 请求将参数放在请求体中,相对更加安全。
  4. 浏览器历史记录和缓存不同:

    • GET 请求的 URL 会被浏览器记录在历史记录中,POST 不会。
    • GET 请求的响应可以被浏览器缓存,POST 不会。
  5. 幂等性不同:

    • GET 请求是幂等的,即多次执行结果相同。因为它是「只读」操作,无论操作多少次,服务器上的数据都是安全的,且每次的结果都是相同的。所以,可以对 GET 请求的数据做缓存,这个缓存可以做到浏览器本身上(彻底避免浏览器发请求),也可以做到代理上(如nginx),而且在浏览器中 GET 请求可以保存为书签。
    • POST 请求通常是非幂等的,每次执行结果可能不同。POST 因为是「新增或提交数据」的操作,会修改服务器上的资源,所以是不安全的,且多次提交数据就会创建多个资源,所以不是幂等的。所以,浏览器一般不会缓存 POST 请求,也不能把 POST 请求保存为书签。

根据 RFC 规范,GET 的语义是从服务器获取指定的资源,这个资源可以是静态的文本、页面、图片视频等。GET 请求的参数位置一般是写在 URL 中,URL 规定只能支持 ASCII,所以 GET 请求的参数只允许 ASCII 字符 ,而且浏览器会对 URL 的长度有限制(HTTP协议本身对 URL长度并没有做任何规定)。POST 的语义是根据请求负荷(报文body)对指定的资源做出处理,具体的处理方式视资源类型而不同。POST 请求携带数据的位置一般是写在报文 body 中,body 中的数据可以是任意格式的数据,只要客户端与服务端协商好即可,而且浏览器不会对 body 大小做限制。
在这里插入图片描述

HTTP 缓存有哪些实现方式?

HTTP 缓存的实现主要包括两种方式:强制缓存和协商缓存。

  1. 强制缓存:

    • 基于响应头中的 Expires 和 Cache-Control 字段实现。
    • Expires 指定资源过期时间,到期前浏览器可直接使用缓存。
    • Cache-Control 提供更细粒度的缓存控制,如 max-age、public、private 等。
    • 命中强制缓存时,浏览器无需再次请求服务器。
  2. 协商缓存:

    • 基于响应头中的 ETag 和 Last-Modified 字段实现。
    • ETag 是资源的唯一标识,Last-Modified 是资源最后修改时间。
    • 浏览器再次请求时,携带 If-None-Match 或 If-Modified-Since 头询问服务器。
    • 服务器比对后发现资源未变,则返回 304 Not Modified,浏览器使用本地缓存。

这两种缓存机制相互配合使用,可以最大化利用缓存,提高 HTTP 性能。强制缓存优先级高于协商缓存,只有当强制缓存失效时,才会使用协商缓存。合理配置这两种缓存策略,可以大幅改善 Web 应用的响应速度和用户体验。

为什么 ETag 的优先级更高?

这是因为 ETag 主要能解决 Last-Modified 几个比较难以解决的问题:

  • 在没有修改文件内容情况下文件的最后修改时间可能也会改变,这会导致客户端认为这文件被改动了,从而重新请求;
  • 可能有些文件是在秒级以内修改的,If-Modified-Since 能检查到的粒度是秒级的,使用 Etag就能够保证这种需求下客户端在 1 秒内能刷新多次;
  • 有些服务器不能精确获取文件的最后修改时间。

注意,协商缓存这两个字段都需要配合强制缓存中 Cache-Control 字段来使用,只有在未能命中强制缓存的时候,才能发起带有协商缓存字段的请求。

当使用 ETag 字段实现的协商缓存的过程:

  • 当浏览器第一次请求访问服务器资源时,服务器会在返回这个资源的同时,在 Response 头部加上 ETag 唯一标识,这个唯一标识的值是根据当前请求的资源生成的;
  • 当浏览器再次请求访问服务器中的该资源时,首先会先检查强制缓存是否过期:
  • 如果没有过期,则直接使用本地缓存;
  • 如果缓存过期了,会在 Request 头部加上 If-None-Match 字段,该字段的值就是 ETag 唯一标识;
  • 服务器再次收到请求后,会根据请求中的 If-None-Match 值与当前请求的资源生成的唯一标识进行比较:
  • 如果值相等,则返回 304 Not Modified,不会返回资源;
  • 如果不相等,则返回 200 状态码和返回资源,并在 Response 头部加上新的 ETag 唯一标识;
  • 如果浏览器收到 304 的请求响应状态码,则会从本地缓存中加载资源,否则更新资源。

HTTP/1.1 的优点有哪些?

HTTP 最突出的优点是「简单、灵活和易于扩展、应用广泛和跨平台」。

  1. 简单

HTTP 基本的报文格式就是 header + body,头部信息也是 key-value 简单文本的形式,易于理解,降低了学习和使用的门槛。

  1. 灵活和易于扩展

HTTP 协议里的各类请求方法、URI/URL、状态码、头字段等每个组成要求都没有被固定死,都允许开发人员自定义和扩充。

同时 HTTP 由于是工作在应用层( OSI 第七层),则它下层可以随意变化,比如:

  • HTTPS 就是在 HTTP 与 TCP 层之间增加了 SSL/TLS 安全传输层;
  • HTTP/1.1 和 HTTP/2.0 传输协议使用的是 TCP 协议,而到了 HTTP/3.0 传输协议改用了 UDP 协议。
  1. 应用广泛和跨平台

互联网发展至今,HTTP 的应用范围非常的广泛,从台式机的浏览器到手机上的各种 APP,从看新闻、刷贴吧到购物、理财、吃鸡,HTTP 的应用遍地开花,同时天然具有跨平台的优越性。

HTTP/1.1 的缺点有哪些?

HTTP 协议里有优缺点一体的双刃剑,分别是「无状态、明文传输」,同时还有一大缺点「不安全」。

  1. 无状态双刃剑

无状态的好处,因为服务器不会去记忆 HTTP 的状态,所以不需要额外的资源来记录状态信息,这能减轻服务器的负担,能够把更多的 CPU 和内存用来对外提供服务。

无状态的坏处,既然服务器没有记忆能力,它在完成有关联性的操作时会非常麻烦。

例如登录->添加购物车->下单->结算->支付,这系列操作都要知道用户的身份才行。但服务器不知道这些请求是有关联的,每次都要问一遍身份信息。

这样每操作一次,都要验证信息,这样的购物体验还能愉快吗?别问,问就是酸爽!

对于无状态的问题,解法方案有很多种,其中比较简单的方式用 Cookie 技术。

Cookie 通过在请求和响应报文中写入 Cookie 信息来控制客户端的状态。

相当于,在客户端第一次请求后,服务器会下发一个装有客户信息的「小贴纸」,后续客户端请求服务器的时候,带上「小贴纸」,服务器就能认得了了,

  1. 明文传输双刃剑

明文意味着在传输过程中的信息,是可方便阅读的,比如 Wireshark 抓包都可以直接肉眼查看,为我们调试工作带了极大的便利性。

但是这正是这样,HTTP 的所有信息都暴露在了光天化日下,相当于信息裸奔。在传输的漫长的过程中,信息的内容都毫无隐私可言,很容易就能被窃取,如果里面有你的账号密码信息,那你号没了。

  1. 不安全

HTTP 比较严重的缺点就是不安全:

  • 通信使用明文(不加密),内容可能会被窃听。比如,账号信息容易泄漏,那你号没了。
  • 不验证通信方的身份,因此有可能遭遇伪装。比如,访问假的淘宝、拼多多,那你钱没了。
  • 无法证明报文的完整性,所以有可能已遭篡改。比如,网页上植入垃圾广告,视觉污染,眼没了。
  • HTTP 的安全问题,可以用 HTTPS 的方式解决,也就是通过引入 SSL/TLS 层,使得在安全上达到了极致。

HTTP/1.1 的性能如何?

HTTP 协议是基于 TCP/IP,并且使用了「请求 - 应答」的通信模式,所以性能的关键就在这两点里。

  1. 长连接

早期 HTTP/1.0 性能上的一个很大的问题,那就是每发起一个请求,都要新建一次 TCP 连接(三次握手),而且是串行请求,做了无谓的 TCP 连接建立和断开,增加了通信开销。

为了解决上述 TCP 连接问题,HTTP/1.1 提出了长连接的通信方式,也叫持久连接。这种方式的好处在于减少了 TCP 连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。

持久连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。

当然,如果某个 HTTP 长连接超过一定时间没有任何数据交互,服务端就会主动断开这个连接。

  1. 管道网络传输

HTTP/1.1 采用了长连接的方式,这使得管道(pipeline)网络传输成为了可能。

即可在同一个 TCP 连接里面,客户端可以发起多个请求,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。

举例来说,客户端需要请求两个资源。以前的做法是,在同一个 TCP 连接里面,先发送 A 请求,然后等待服务器做出回应,收到后再发出 B 请求。那么,管道机制则是允许浏览器同时发出 A 请求和 B 请求,如下图:
在这里插入图片描述
但是服务器必须按照接收请求的顺序发送对这些管道化请求的响应。

如果服务端在处理 A 请求时耗时比较长,那么后续的请求的处理都会被阻塞住,这称为「队头堵塞」。

所以,HTTP/1.1 管道解决了请求的队头阻塞,但是没有解决响应的队头阻塞。

  1. 队头阻塞

「请求 - 应答」的模式会造成 HTTP 的性能问题。为什么呢?

因为当顺序发送的请求序列中的一个请求因为某种原因被阻塞时,在后面排队的所有请求也一同被阻塞了,会招致客户端一直请求不到数据,这也就是「队头阻塞」,好比上班的路上塞车。在这里插入图片描述

HTTP 与 HTTPS 有哪些区别?

HTTP 和 HTTPS 的主要区别如下:

  1. 安全性

    • HTTP 是明文传输,容易被窃听和篡改。
    • HTTPS 采用加密传输,使用 SSL/TLS 协议对数据进行加密,提高了安全性。
  2. 默认端口

    • HTTP 默认使用 80 端口。
    • HTTPS 默认使用 443 端口。
  3. 证书验证

    • HTTPS 需要部署数字证书,用于身份验证和加密密钥的生成。
    • 客户端会验证服务器证书的合法性,确保连接的安全性。
  4. 性能开销

    • HTTPS 需要进行加解密等额外计算,会带来一定的性能开销。
    • 但随着硬件性能的提升和算法的优化,HTTPS 的性能影响已大幅降低。
  5. 搜索引擎优化(SEO)

    • Google 等搜索引擎会给予 HTTPS 网站更高的排名权重。
    • 使用 HTTPS 有利于网站的搜索引擎优化。
  6. 兼容性

    • 所有现代浏览器都支持 HTTPS,但 HTTP 仍然被广泛使用。
    • 对于不支持 HTTPS 的老旧设备或浏览器,只能使用 HTTP。

总之,HTTPS 相比 HTTP 提供了更好的安全性和可信度,但也带来了一定的性能开销和部署复杂度。对于涉及用户隐私或敏感交易的 Web 应用,使用 HTTPS 协议是强烈推荐的。

HTTPS 解决了 HTTP 的哪些问题?

HTTP 由于是明文传输,所以安全上存在以下三个风险:

  • 窃听风险,比如通信链路上可以获取通信内容,用户号容易没。
  • 篡改风险,比如强制植入垃圾广告,视觉污染,用户眼容易瞎。
  • 冒充风险,比如冒充淘宝网站,用户钱容易没。

HTTPS 在 HTTP 与 TCP 层之间加入了 SSL/TLS 协议,可以很好的解决了上述的风险:

  • 信息加密:交互信息无法被窃取,但你的号会因为「自身忘记」账号而没。
  • 校验机制:无法篡改通信内容,篡改了就不能正常显示,但百度「竞价排名」依然可以搜索垃圾广告。
  • 身份证书:证明淘宝是真的淘宝网,但你的钱还是会因为「剁手」而没。
    可见,只要自身不做「恶」,SSL/TLS 协议是能保证通信是安全的。

HTTPS 是如何解决上面的三个风险的?

  • 混合加密的方式实现信息的机密性,解决了窃听的风险。
  • 摘要算法的方式来实现完整性,它能够为数据生成独一无二的「指纹」,指纹用于校验数据的完整性,解决了篡改的风险。
  • 将服务器公钥放入到数字证书中,解决了冒充的风险。
    在这里插入图片描述

HTTPS 是如何建立连接的?其间交互了什么?

HTTPS 建立连接的过程可以用一个简单的流程来描述:

  1. 客户端(浏览器)发起 HTTPS 连接请求,向服务器发送 “Client Hello” 消息。这个消息包含了客户端支持的加密算法、压缩算法等信息。

  2. 服务器收到请求后,发送 “Server Hello” 消息,选择双方都支持的加密算法等参数,并附上服务器的数字证书。

  3. 客户端验证服务器的数字证书,如果通过验证,就生成一个随机的对称密钥,并用服务器的公钥加密发送给服务器。

  4. 服务器收到加密的对称密钥后,使用自己的私钥解密,获得对称密钥。之后双方就可以使用这个对称密钥进行加密通信了。

  5. 在加密通信过程中,双方还可能交换其他消息以确认连接安全性。

这个流程就是 HTTPS 建立安全连接的基本过程,从客户端发起请求到最终双方使用对称密钥进行加密通信。整个过程确保了传输数据的机密性和完整性。在这里插入图片描述

客户端校验数字证书的流程是怎样的?

客户端校验服务器数字证书的具体流程如下:

  1. 客户端首先获取服务器提供的数字证书。数字证书通常包含以下信息:

    • 证书所有者的身份信息(如域名、公司名等)
    • 证书所有者的公钥
    • 证书颁发机构(CA)的身份信息
    • 证书的有效期
    • 数字签名(由CA使用私钥签名的)
  2. 客户端首先验证证书的数字签名是否有效。它使用CA的公钥解密数字签名,检查签名是否与证书中的信息匹配。

  3. 客户端检查证书的有效期,确保证书未过期。

  4. 客户端检查证书所有者的身份信息是否与它想要连接的服务器域名相匹配。

  5. 客户端检查证书是否在可信任的CA颁发列表中。操作系统和浏览器会预置一些知名CA的公钥,用于验证证书。

  6. 如果以上所有检查都通过,客户端就可以信任这个证书,并使用证书中的公钥进行后续的加密通信。

整个过程确保了服务器身份的真实性和通信的安全性。只有当证书通过全面验证后,客户端才会建立起与服务器的安全连接。
在这里插入图片描述

HTTPS 的应用数据是如何保证完整性的?

HTTPS 使用数字签名来保证应用数据的完整性。具体流程如下:

  1. 当客户端向服务器发送数据时,客户端会先计算数据的哈希值(摘要)。

  2. 客户端使用自己的私钥对这个哈希值进行数字签名,生成数字签名。

  3. 客户端将原始数据和数字签名一起发送给服务器。

  4. 服务器收到数据后,首先使用客户端的公钥验证数字签名。

  5. 服务器再次计算收到的数据的哈希值,并与数字签名中的哈希值进行比对。

  6. 如果两个哈希值一致,说明数据在传输过程中未被篡改,完整性得到保证。

这个过程利用了非对称加密的原理。客户端使用私钥签名,服务器使用公钥验签。只有拥有私钥的客户端才能产生有效的数字签名。

即使数据在传输过程中被篡改,由于哈希值不匹配,服务器也能够发现并拒绝接收。

因此,HTTPS 不仅确保了数据的机密性,也有效保证了数据的完整性。这是 HTTPS 相比HTTP更安全的关键所在。

HTTPS 一定安全可靠吗?

HTTPS 协议本身到目前为止还是没有任何漏洞的,即使你成功进行中间人攻击,本质上是利用了客户端的漏洞(用户点击继续访问或者被恶意导入伪造的根证书),并不是 HTTPS 不够安全。

为什么抓包工具能截取 HTTPS 数据?

很多抓包工具 之所以可以明文看到 HTTPS 数据,工作原理与中间人一致的。

对于 HTTPS 连接来说,中间人要满足以下两点,才能实现真正的明文代理:

  • 中间人,作为客户端与真实服务端建立连接这一步不会有问题,因为服务端不会校验客户端的身份;
  • 中间人,作为服务端与真实客户端建立连接,这里会有客户端信任服务端的问题,也就是服务端必须有对应域名的私钥;

中间人要拿到私钥只能通过如下方式:

  • 去网站服务端拿到私钥;
  • 去CA处拿域名签发私钥;
  • 自己签发证书,切要被浏览器信任;
    不用解释,抓包工具只能使用第三种方式取得中间人的身份。

使用抓包工具进行 HTTPS 抓包的时候,需要在客户端安装 Fiddler 的根证书,这里实际上起认证中心(CA)的作用。

抓包工具能够抓包的关键是客户端会往系统受信任的根证书列表中导入抓包工具生成的证书,而这个证书会被浏览器信任,也就是抓包工具给自己创建了一个认证中心 CA,客户端拿着中间人签发的证书去中间人自己的 CA 去认证,当然认为这个证书是有效的。

如何避免被中间人抓取数据?

我们要保证自己电脑的安全,不要被病毒乘虚而入,而且也不要点击任何证书非法的网站,这样 HTTPS 数据就不会被中间人截取到了。

当然,我们还可以通过 HTTPS 双向认证来避免这种问题。

一般我们的 HTTPS 是单向认证,客户端只会验证了服务端的身份,但是服务端并不会验证客户端的身份。在这里插入图片描述
如果用了双向认证方式,不仅客户端会验证服务端的身份,而且服务端也会验证客户端的身份。服务端一旦验证到请求自己的客户端为不可信任的,服务端就拒绝继续通信,客户端如果发现服务端为不可信任的,那么也中止通信。

HTTP/1.1 相比 HTTP/1.0 提高了什么性能?

HTTP/1.1 相比 HTTP/1.0 性能上的改进:

  • 使用长连接的方式改善了 HTTP/1.0 短连接造成的性能开销。
  • 支持管道(pipeline)网络传输,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。

但 HTTP/1.1 还是有性能瓶颈:

  • 请求 / 响应头部(Header)未经压缩就发送,首部信息越多延迟越大。只能压缩 Body 的部分;
  • 发送冗长的首部。每次互相发送相同的首部造成的浪费较多;
  • 服务器是按请求的顺序响应的,如果服务器响应慢,会招致客户端一直请求不到数据,也就是队头阻塞;
  • 没有请求优先级控制;
  • 请求只能从客户端开始,服务器只能被动响应。

HTTP/2 做了什么优化?

HTTP/2 在以下几个方面进行了优化,进一步提升了 Web 应用的性能:

  1. 二进制协议

    • HTTP/2 采用了二进制格式,替代了 HTTP/1.x 中的纯文本协议。头信息和数据体都是二进制,并且统称为帧(frame):头信息帧(Headers Frame)和数据帧(Data Frame)。
    • 二进制协议更加紧凑、解析更高效,减少了网络传输开销。
  2. 多路复用(Multiplexing)

    • HTTP/2 允许在单一的 TCP 连接上并发发送多个请求/响应数据流。针对不同的 HTTP 请求用独一无二的 Stream ID 来区分,接收端可以通过 Stream ID 有序组装成 HTTP 消息,不同 Stream 的帧是可以乱序发送的,因此可以并发不同的 Stream ,也就是 HTTP/2 可以并行交错地发送请求和响应。在这里插入图片描述
    • 这避免了 HTTP/1.1 中串行处理请求导致的队头阻塞问题。
  3. 头部压缩(Header Compression)

    • HTTP/2 使用 HPACK 算法对头部信息进行压缩传输。这就是所谓的 HPACK 算法:在客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。
    • 大幅减少了头部数据的传输开销。
  4. 服务器推送(Server Push)

    • HTTP/2 允许服务器主动向客户端推送资源,而无需客户端请求。
    • 这可以提前加载页面所需的资源,降低页面加载时间。
  5. 优先级和依赖性

    • HTTP/2 允许客户端指定请求的优先级和资源依赖关系。
    • 服务器可以据此调整资源的传输顺序,优先处理重要请求。
  6. 流量控制

    • HTTP/2 在连接、流以及帧级别提供了完善的流量控制机制。
    • 可以防止服务器被大量请求淹没,提高抗压能力。
      在这里插入图片描述

HTTP/2 有什么缺陷?

HTTP/2 通过 Stream 的并发能力,解决了 HTTP/1 队头阻塞的问题,看似很完美了,但是 HTTP/2 还是存在“队头阻塞”的问题,只不过问题不是在 HTTP 这一层面,而是在 TCP 这一层。

HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当「前 1 个字节数据」没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。
在这里插入图片描述

HTTP/3 做了哪些优化?

HTTP/3 在解决 HTTP/2 的队头阻塞问题上做出了重大改进:
在这里插入图片描述

  1. 基于 QUIC 传输协议

    • HTTP/3 摒弃了基于 TCP 的传输,转而采用基于 UDP 的 QUIC 协议。
    • QUIC 提供了可靠的数据流传输,同时具备更好的抗网络延迟和丢包能力。
  2. 多路复用 (Multiplexing)

    • 和 HTTP/2 一样,HTTP/3 依然保留了多路复用的能力,允许在单一 QUIC 连接上并发传输多个数据流。
    • 但由于摆脱了 TCP 的队头阻塞问题,HTTP/3 的多路复用能力更加高效和可靠。
  3. 快速连接建立

    • QUIC 协议支持 0-RTT 快速建立连接,避免了 TCP 的三次握手开销。在这里插入图片描述
    • 这进一步降低了连接建立时延,提高了整体响应速度。
  4. 流量控制和可靠性

    • QUIC 提供了更细粒度的流量控制和可靠传输机制。
    • 即使某个数据流出现问题,也不会影响其他数据流的传输。
  5. 更好的加密支持

    • QUIC 将加密机制深度集成到协议中,提供了更强大的端到端加密能力。
    • 这也有助于提高通信的安全性。

总之,通过采用 QUIC 传输协议,HTTP/3 彻底解决了 HTTP/2 中 TCP 队头阻塞的问题。加上其他诸如快速连接建立、细粒度流量控制等优化,HTTP/3 在性能和安全性方面都有了很大提升,是Web性能优化的重要里程碑。

但QUIC 是新协议,对于很多网络设备,根本不知道什么是 QUIC,只会当做 UDP,这样会出现新的问题,因为有的网络设备是会丢掉 UDP 包的,而 QUIC 是基于 UDP 实现的,那么如果网络设备无法识别这个是 QUIC 包,那么就会当作 UDP包,然后被丢弃。

HTTP/3 现在普及的进度非常的缓慢,不知道未来 UDP 是否能够逆袭 TCP。

https 和 http 相比,就是传输的内容多了对称加密,可以这么理解吗?

  1. 建立连接时候:https 比 http多了 TLS 的握手过程;

  2. 传输内容的时候:https 会把数据进行加密,通常是对称加密数据;

为啥 SSL 的握手是 4 次?

SSL/TLS 握手过程之所以要 4 次交互,主要是为了实现以下几个目标:

  1. 服务器身份认证和密钥协商

    • 客户端首先发送 “ClientHello” 消息,包含了客户端支持的加密套件等信息。
    • 服务器发送 “ServerHello” 消息,选择加密套件,并附上服务器的数字证书。
    • 客户端验证服务器证书,然后生成一个随机的预主密钥,用服务器公钥加密发送给服务器。
  2. 建立会话密钥

    • 服务器使用自己的私钥解密获得预主密钥,并根据协商的算法生成主密钥。
    • 服务器发送 “ChangeCipherSpec” 消息,通知客户端之后的通信都将使用新协商的密钥。
  3. 验证通信的完整性

    • 客户端也发送 “ChangeCipherSpec” 消息,表示已切换到新的加密环境。
    • 双方交换 “Finished” 消息,携带加密的校验值,验证通信过程的完整性。

整个过程共 4 次交互,目的是确保在建立安全连接之前,双方的身份和密钥都得到了可靠的验证和协商。

这种 4 次握手机制虽然增加了一些延迟,但确保了 SSL/TLS 连接的安全性,防止了中间人攻击等威胁。这也是 SSL/TLS 广泛应用的重要原因之一。

你知道 HTTP/1.1 该如何优化吗?

对于 HTTP/1.1,有以下一些优化方式可以考虑:

  1. 使用持久连接(Persistent Connections)

    • 充分利用 HTTP/1.1 支持的持久连接特性,降低连接建立和断开的开销。
    • 可以通过设置 Connection: keep-alive 头部来实现。
  2. 启用 HTTP 管道(HTTP Pipelining)

    • 使用管道化请求,客户端可以发送多个请求而无需等待上一个响应。
    • 这可以提高吞吐量,但需要服务器端也支持管道化处理。
  3. 使用 HTTP 缓存

    • 充分利用 HTTP 缓存机制,如 Expires、Cache-Control 等头部。
    • 合理设置缓存策略,可以减少不必要的网络请求。通过缓存技术来避免发送 HTTP 请求。客户端收到第一个请求的响应后,可以将其缓存在本地磁盘,下次请求的时候,如果缓存没过期,就直接读取本地缓存的响应数据。如果缓存过期,客户端发送请求的时候带上响应数据的摘要,服务器比对后发现资源没有变化,就发出不带包体的 304 响应,告诉客户端缓存的响应仍然有效。
  4. 优化资源的请求顺序

    • 分析页面结构,确定关键资源的加载优先级。
    • 调整资源的请求顺序,提升关键资源的加载速度。
  5. 压缩传输内容

    • 对 HTTP 响应内容启用 Gzip/Brotli 压缩,减小传输数据量。
    • 这需要服务器和客户端都支持相应的压缩算法。
  6. 使用 CDN 加速

    • 将静态资源部署到 CDN 节点,就近提供服务,降低网络延迟。
    • CDN 通常也支持缓存和压缩等优化手段。
  7. 优化 HTTP 头部

    • 精简 HTTP 头部,减小请求/响应的开销。
    • 合理使用头部如 If-Modified-Since、Etag 等进行缓存验证。

总的来说,对于 HTTP/1.1 来说,合理利用其支持的各种优化机制,结合 CDN、内容压缩等手段,都可以有效提升 Web 应用的性能表现。

TLS 握手过程

HTTP 由于是明文传输,所谓的明文,就是说客户端与服务端通信的信息都是肉眼可见的,随意使用一个抓包工具都可以截获通信的内容。

所以安全上存在以下三个风险:

  1. 窃听风险,比如通信链路上可以获取通信内容,用户号容易没。
  2. 篡改风险,比如强制植入垃圾广告,视觉污染,用户眼容易瞎。
  3. 冒充风险,比如冒充淘宝网站,用户钱容易没。

HTTPS 在 HTTP 与 TCP 层之间加入了 TLS 协议,来解决上述的风险。
在这里插入图片描述
TLS 协议是如何解决 HTTP 的风险的呢?

  1. 信息加密: HTTP 交互信息是被加密的,第三方就无法被窃取;
  2. 校验机制:校验信息传输过程中是否有被第三方篡改过,如果被篡改过,则会有警告提示;
  3. 身份证书:证明淘宝是真的淘宝网;

可见,有了 TLS 协议,能保证 HTTP 通信是安全的了,那么在进行 HTTP 通信前,需要先进行 TLS 握手。TLS 的握手过程,如下图:在这里插入图片描述
上图简要概述了 TLS 的握手过程,其中每一个「框」都是一个记录(record),记录是 TLS 收发数据的基本单位,类似于 TCP 里的 segment。多个记录可以组合成一个 TCP 包发送,所以通常经过「四个消息」就可以完成 TLS 握手,也就是需要 2个 RTT 的时延,然后就可以在安全的通信环境里发送 HTTP 报文,实现 HTTPS 协议。

所以可以发现,HTTPS 是应用层协议,需要先完成 TCP 连接建立,然后走 TLS 握手过程后,才能建立通信安全的连接。

RSA 握手过程

传统的 TLS 握手基本都是使用 RSA 算法来实现密钥交换的,在将 TLS 证书部署服务端时,证书文件其实就是服务端的公钥,会在 TLS 握手阶段传递给客户端,而服务端的私钥则一直留在服务端,一定要确保私钥不能被窃取。

在 RSA 密钥协商算法中,客户端会生成随机密钥,并使用服务端的公钥加密后再传给服务端。根据非对称加密算法,公钥加密的消息仅能通过私钥解密,这样服务端解密后,双方就得到了相同的密钥,再用它加密应用消息。
在这里插入图片描述

TLS RSA 握手过程简单描述如下:

  1. 客户端发送 ClientHello,包含支持的加密套件等信息。消息里面有客户端使用的 TLS 版本号、支持的密码套件列表,以及生成的随机数(Client Random),这个随机数会被服务端保留,它是生成对称加密密钥的材料之一。
  2. 服务器响应 ServerHello,选择加密套件,并发送自己的数字证书。
  3. 客户端验证服务器证书,然后生成随机的预主密钥,用服务器公钥加密发送给服务器。
  4. 服务器使用自己的私钥解密获得预主密钥,并根据协商的算法生成主密钥。
  5. 服务器发送 ChangeCipherSpec 消息,通知客户端之后的通信都将使用新密钥。
  6. 客户端也发送 ChangeCipherSpec 消息,表示已切换到新加密环境。
  7. 双方交换 Finished 消息,携带加密的校验值,验证通信完整性。

至此,TLS RSA 安全连接建立完成,后续通信将基于协商的主密钥进行加密。整个过程共 7 个步骤。

RSA 算法的缺陷

使用 RSA 密钥协商算法的最大问题是不支持前向保密。

因为客户端传递随机数(用于生成对称加密密钥的条件之一)给服务端时使用的是公钥加密的,服务端收到后,会用私钥解密得到随机数。所以一旦服务端的私钥泄漏了,过去被第三方截获的所有 TLS 通讯密文都会被破解。

密钥交换算法优化

TLS 1.2 版本如果使用的是 RSA 密钥交换算法,那么需要 4 次握手,也就是要花费 2 RTT,才可以进行应用数据的传输,而且 RSA 密钥交换算法不具备前向安全性。

总之使用 RSA 密钥交换算法的 TLS 握手过程,不仅慢,而且安全性也不高。

因此如果可以,尽量选用 ECDHE 密钥交换算法替换 RSA 算法,因为该算法由于支持「False Start」,它是“抢跑”的意思,客户端可以在 TLS 协议的第 3 次握手后,第 4 次握手前,发送加密的应用数据,以此将 TLS 握手的消息往返由 2 RTT 减少到 1 RTT,而且安全性也高,具备前向安全性。

ECDHE 算法是基于椭圆曲线实现的,不同的椭圆曲线性能也不同,应该尽量选择 x25519 曲线,该曲线是目前最快的椭圆曲线。

比如在 Nginx 上,可以使用 ssl_ecdh_curve 指令配置想使用的椭圆曲线,把优先使用的放在前面:在这里插入图片描述
对于对称加密算法方面,如果对安全性不是特别高的要求,可以选用 AES_128_GCM,它比 AES_256_GCM 快一些,因为密钥的长度短一些。

比如在 Nginx 上,可以使用 ssl_ciphers 指令配置想使用的非对称加密算法和对称加密算法,也就是密钥套件,而且把性能最快最安全的算法放在最前面:在这里插入图片描述

TLS 升级

当然,如果可以,直接把 TLS 1.2 升级成 TLS 1.3,TLS 1.3 大幅度简化了握手的步骤,完成 TLS 握手只要 1 RTT,而且安全性更高。

在 TLS 1.2 的握手中,一般是需要 4 次握手,先要通过 Client Hello (第 1 次握手)和 Server Hello(第 2 次握手) 消息协商出后续使用的加密算法,再互相交换公钥(第 3 和 第 4 次握手),然后计算出最终的会话密钥,下图的左边部分就是 TLS 1.2 的握手过程:在这里插入图片描述
上图的右边部分就是 TLS 1.3 的握手过程,可以发现 TLS 1.3 把 Hello 和公钥交换这两个消息合并成了一个消息,于是这样就减少到只需 1 RTT 就能完成 TLS 握手。

怎么合并的呢?具体的做法是,客户端在 Client Hello 消息里带上了支持的椭圆曲线,以及这些椭圆曲线对应的公钥。

服务端收到后,选定一个椭圆曲线等参数,然后返回消息时,带上服务端这边的公钥。经过这 1 个 RTT,双方手上已经有生成会话密钥的材料了,于是客户端计算出会话密钥,就可以进行应用数据的加密传输了。

而且,TLS1.3 对密码套件进行“减肥”了, 对于密钥交换算法,废除了不支持前向安全性的 RSA 和 DH 算法,只支持 ECDHE 算法。

对于对称加密和签名算法,只支持目前最安全的几个密码套件,比如 openssl 中仅支持下面 5 种密码套件:

  • TLS_AES_256_GCM_SHA384
  • TLS_CHACHA20_POLY1305_SHA256
  • TLS_AES_128_GCM_SHA256
  • TLS_AES_128_CCM_8_SHA256
  • TLS_AES_128_CCM_SHA256

之所以 TLS1.3 仅支持这么少的密码套件,是因为 TLS1.2 由于支持各种古老且不安全的密码套件,中间人可以利用降级攻击,伪造客户端的 Client Hello 消息,替换客户端支持的密码套件为一些不安全的密码套件,使得服务器被迫使用这个密码套件进行 HTTPS 连接,从而破解密文。

证书优化

  1. 证书传输优化
    要让证书更便于传输,那必然是减少证书的大小,这样可以节约带宽,也能减少客户端的运算量。所以,对于服务器的证书应该选择椭圆曲线(ECDSA)证书,而不是 RSA 证书,因为在相同安全强度下, ECC 密钥长度比 RSA 短的多。

  2. 证书验证优化
    客户端在验证证书时,是个复杂的过程,会走证书链逐级验证,验证的过程不仅需要「用 CA 公钥解密证书」以及「用签名算法验证证书的完整性」,而且为了知道证书是否被 CA 吊销,客户端有时还会再去访问 CA, 下载 CRL 或者 OCSP 数据,以此确认证书的有效性。

在线证书状态协议(Online Certificate Status Protocol)来查询证书的有效性,它的工作方式是向 CA 发送查询请求,让 CA 返回证书的有效状态。OCSP 需要向 CA 查询,因此也是要发生网络请求,而且还得看 CA 服务器的“脸色”,如果网络状态不好,或者 CA 服务器繁忙,也会导致客户端在校验证书这一环节的延时变大。在这里插入图片描述
OCSP Stapling
于是为了解决这一个网络开销,就出现了 OCSP Stapling,其原理是:服务器向 CA 周期性地查询证书状态,获得一个带有时间戳和签名的响应结果并缓存它。在这里插入图片描述
当有客户端发起连接请求时,服务器会把这个「响应结果」在 TLS 握手过程中发给客户端。由于有签名的存在,服务器无法篡改,因此客户端就能得知证书是否已被吊销了,这样客户端就不需要再去查询。

HTTPS优化

  1. 对于硬件优化的方向,因为 HTTPS 是属于计算密集型,应该选择计算力更强的 CPU,而且最好选择支持 AES-NI 特性的 CPU,这个特性可以在硬件级别优化 AES 对称加密算法,加快应用数据的加解密。

  2. 对于软件优化的方向,如果可以,把软件升级成较新的版本,比如将 Linux 内核 2.X 升级成 4.X,将 openssl 1.0.1 升级到 1.1.1,因为新版本的软件不仅会提供新的特性,而且还会修复老版本的问题。

2.1 对于协议优化的方向:

  • 密钥交换算法应该选择 ECDHE 算法,而不用 RSA 算法,因为 ECDHE 算法具备前向安全性,而且客户端可以在第三次握手之后,就发送加密应用数据,节省了 1 RTT。

  • 将 TLS1.2 升级 TLS1.3,因为 TLS1.3 的握手过程只需要 1 RTT,而且安全性更强。
    2.2 对于证书优化的方向:

  • 服务器应该选用 ECDSA 证书,而非 RSA 证书,因为在相同安全级别下,ECC 的密钥长度比 RSA 短很多,这样可以提高证书传输的效率;

  • 服务器应该开启 OCSP Stapling 功能,由服务器预先获得 OCSP 的响应,并把响应结果缓存起来,这样 TLS 握手的时候就不用再访问 CA 服务器,减少了网络通信的开销,提高了证书验证的效率;

  • 对于重连 HTTPS 时,我们可以使用一些技术让客户端和服务端使用上一次 HTTPS 连接使用的会话密钥,直接恢复会话,而不用再重新走完整的 TLS 握手过程。

常见的会话重用技术有 Session ID 和 Session Ticket,用了会话重用技术,当再次重连 HTTPS 时,只需要 1 RTT 就可以恢复会话。对于 TLS1.3 使用 Pre-shared Key 会话重用技术,只需要 0 RTT 就可以恢复会话。

这些会话重用技术虽然好用,但是存在一定的安全风险,它们不仅不具备前向安全,而且有重放攻击的风险,所以应当对会话密钥设定一个合理的过期时间。

HTTP/1.1 协议的性能问题

  1. HTTP/1.1协议存在的性能问题:

    • 消息大小、页面资源变多、内容形式变多样、实时性要求提高
    • 导致了高延迟、并发连接有限、队头阻塞、头部重复等问题
  2. HTTP/1.1的优化手段:

    • 合并小图片、Base64嵌入、打包JS、域名分散并发
    • 但这些都只是"外部"优化,无法解决协议自身的问题
  3. 为了根本解决这些性能瓶颈,于是有了HTTP/2的重新设计:

    • 引入二进制协议、多路复用、头部压缩、服务器推送等优化
    • 彻底解决了HTTP/1.1中的队头阻塞、延迟等问题

兼容 HTTP/1.1

HTTP/2 在协议升级方面确实做得很出色:

  1. 兼容性:

    • 没有引入新的协议名,保留了"http://“和"https://”
    • 让浏览器和服务器自动升级协议版本,实现平滑升级
  2. 向后兼容:

    • 只对"语法"层面做改变,而"语义"层保持不变
    • 请求方法、状态码、头字段等规则均与HTTP/1.1一致

这种设计确实大大促进了HTTP/2的普及和推广。传统的应用无需重写,只需在底层自动切换到HTTP/2协议即可享受性能提升。

与此同时,通过重新设计"语法"层,HTTP/2引入了诸如二进制格式、多路复用、头部压缩等优化措施,有效解决了HTTP/1.1的性能问题。这种在保持向后兼容的前提下进行协议改进的做法,可以说是非常成功的设计。

头部压缩

HTTP 协议的报文是由「Header + Body」构成的,对于 Body 部分,HTTP/1.1 协议可以使用头字段 「Content-Encoding」指定 Body 的压缩方式,比如用 gzip 压缩,这样可以节约带宽,但报文中的另外一部分 Header,是没有针对它的优化手段。

HTTP/1.1 报文中 Header 部分存在的问题:

含很多固定的字段,比如 Cookie、User Agent、Accept 等,这些字段加起来也高达几百字节甚至上千字节,所以有必要压缩;

  • 大量的请求和响应的报文里有很多字段值都是重复的,这样会使得大量带宽被这些冗余的数据占用了,所以有必须要避免重复性;
  • 字段是 ASCII 编码的,虽然易于人类观察,但效率低,所以有必要改成二进制编码;

HTTP/2 对 Header 部分做了大改造,把以上的问题都解决了。

HTTP/2 没使用常见的 gzip 压缩方式来压缩头部,而是开发了 HPACK 算法,HPACK 算法主要包含三个组成部分:

  • 静态字典;
  • 动态字典;
  • Huffman 编码(压缩算法);

客户端和服务器两端都会建立和维护「字典」,用长度较小的索引号表示重复的字符串,再用 Huffman 编码压缩数据,可达到 50%~90% 的高压缩率。

静态表编码

HTTP/2 为高频出现在头部的字符串和字段建立了一张静态表,它是写入到 HTTP/2 框架里的,不会变化的,静态表里共有 61 组,如下图:
在这里插入图片描述
表中的 Index 表示索引(Key),Header Value 表示索引对应的 Value,Header Name 表示字段的名字,比如 Index 为 2 代表 GET,Index 为 8 代表状态码 200。

你可能注意到,表中有的 Index 没有对应的 Header Value,这是因为这些 Value 并不是固定的而是变化的,这些 Value 都会经过 Huffman 编码后,才会发送出去。在这里插入图片描述

动态表编码

静态表只包含了 61 种高频出现在头部的字符串,不在静态表范围内的头部字符串就要自行构建动态表,它的 Index 从 62 起步,会在编码解码的时候随时更新。

比如,第一次发送时头部中的「User-Agent 」字段数据有上百个字节,经过 Huffman 编码发送出去后,客户端和服务器双方都会更新自己的动态表,添加一个新的 Index 号 62。那么在下一次发送的时候,就不用重复发这个字段的数据了,只用发 1 个字节的 Index 号就好了,因为双方都可以根据自己的动态表获取到字段的数据。

所以,使得动态表生效有一个前提:必须同一个连接上,重复传输完全相同的 HTTP 头部。如果消息字段在 1 个连接上只发送了 1 次,或者重复传输时,字段总是略有变化,动态表就无法被充分利用了。

因此,随着在同一 HTTP/2 连接上发送的报文越来越多,客户端和服务器双方的「字典」积累的越来越多,理论上最终每个头部字段都会变成 1 个字节的 Index,这样便避免了大量的冗余数据的传输,大大节约了带宽。

理想很美好,现实很骨感。动态表越大,占用的内存也就越大,如果占用了太多内存,是会影响服务器性能的,因此 Web 服务器都会提供类似 http2_max_requests 的配置,用于限制一个连接上能够传输的请求数量,避免动态表无限增大,请求数量到达上限后,就会关闭 HTTP/2 连接来释放内存。

综上,HTTP/2 头部的编码通过「静态表、动态表、Huffman 编码」共同完成的。

在这里插入图片描述

二进制帧

HTTP/2 厉害的地方在于将 HTTP/1 的文本格式改成二进制格式传输数据,极大提高了 HTTP 传输效率,而且二进制数据使用位运算能高效解析。
在这里插入图片描述
HTTP/2 把响应报文划分成了两类帧(Frame),图中的 HEADERS(首部)和 DATA(消息负载) 是帧的类型,也就是说一条 HTTP 响应,划分成了两类帧来传输,并且采用二进制来编码。

在这里插入图片描述
帧头(Frame Header)很小,只有 9 个字节,帧开头的前 3 个字节表示帧数据(Frame Playload)的长度。

帧长度后面的一个字节是表示帧的类型,HTTP/2 总共定义了 10 种类型的帧,一般分为数据帧和控制帧两类,如下表格:在这里插入图片描述
帧类型后面的一个字节是标志位,可以保存 8 个标志位,用于携带简单的控制信息,比如:

END_HEADERS 表示头数据结束标志,相当于 HTTP/1 里头后的空行(“\r\n”);
END_Stream 表示单方向数据发送结束,后续不会再有数据帧。
PRIORITY 表示流的优先级;
帧头的最后 4 个字节是流标识符(Stream ID),但最高位被保留不用,只有 31 位可以使用,因此流标识符的最大值是 2^31,大约是 21 亿,它的作用是用来标识该 Frame 属于哪个 Stream,接收方可以根据这个信息从乱序的帧里找到相同 Stream ID 的帧,从而有序组装信息。

最后面就是帧数据了,它存放的是通过 HPACK 算法压缩过的 HTTP 头部和包体。

并发传输

HTTP/1.1 的实现是基于请求-响应模型的。同一个连接中,HTTP 完成一个事务(请求与响应),才能处理下一个事务,也就是说在发出请求等待响应的过程中,是没办法做其他事情的,如果响应迟迟不来,那么后续的请求是无法发送的,也造成了队头阻塞的问题。

而 HTTP/2 就很牛逼了,通过 Stream 这个设计,多个 Stream 复用一条 TCP 连接,达到并发的效果,解决了 HTTP/1.1 队头阻塞的问题,提高了 HTTP 传输的吞吐量。
在这里插入图片描述
你可以从上图中看到:

  • 1 个 TCP 连接包含一个或者多个 Stream,Stream 是 HTTP/2 并发的关键技术;
  • Stream 里可以包含 1 个或多个 Message,Message 对应 HTTP/1 中的请求或响应,由 HTTP 头部和包体构成;
  • Message 里包含一条或者多个 Frame,Frame 是 HTTP/2 最小单位,以二进制压缩格式存放 HTTP/1 中的内容(头部和包体);

因此,我们可以得出个结论:多个 Stream 跑在一条 TCP 连接,同一个 HTTP 请求与响应是跑在同一个 Stream 中,HTTP 消息可以由多个 Frame 构成, 一个 Frame 可以由多个 TCP 报文构成。
在这里插入图片描述
在 HTTP/2 连接上,不同 Stream 的帧是可以乱序发送的(因此可以并发不同的 Stream ),因为每个帧的头部会携带 Stream ID 信息,所以接收端可以通过 Stream ID 有序组装成 HTTP 消息,而同一 Stream 内部的帧必须是严格有序的。

比如下图,服务端并行交错地发送了两个响应: Stream 1 和 Stream 3,这两个 Stream 都是跑在一个 TCP 连接上,客户端收到后,会根据相同的 Stream ID 有序组装成 HTTP 消息。在这里插入图片描述
客户端和服务器双方都可以建立 Stream,因为服务端可以主动推送资源给客户端, 客户端建立的 Stream 必须是奇数号,而服务器建立的 Stream 必须是偶数号。

比如下图,Stream 1 是客户端向服务端请求的资源,属于客户端建立的 Stream,所以该 Stream 的 ID 是奇数(数字 1);Stream 2 和 4 都是服务端主动向客户端推送的资源,属于服务端建立的 Stream,所以这两个 Stream 的 ID 是偶数(数字 2 和 4)。在这里插入图片描述
同一个连接中的 Stream ID 是不能复用的,只能顺序递增,所以当 Stream ID 耗尽时,需要发一个控制帧 GOAWAY,用来关闭 TCP 连接。

在 Nginx 中,可以通过 http2_max_concurrent_Streams 配置来设置 Stream 的上限,默认是 128 个。

HTTP/2 通过 Stream 实现的并发,比 HTTP/1.1 通过 TCP 连接实现并发要牛逼的多,因为当 HTTP/2 实现 100 个并发 Stream 时,只需要建立一次 TCP 连接,而 HTTP/1.1 需要建立 100 个 TCP 连接,每个 TCP 连接都要经过 TCP 握手、慢启动以及 TLS 握手过程,这些都是很耗时的。

HTTP/2 还可以对每个 Stream 设置不同优先级,帧头中的「标志位」可以设置优先级,比如客户端访问 HTML/CSS 和图片资源时,希望服务器先传递 HTML/CSS,再传图片,那么就可以通过设置 Stream 的优先级来实现,以此提高用户体验。

服务器主动推送资源

在 HTTP/2 中,客户端在访问 HTML 时,服务器可以直接主动推送 CSS 文件,减少了消息传递的次数。在这里插入图片描述
HTTP/2 的推送是怎么实现的?

客户端发起的请求,必须使用的是奇数号 Stream,服务器主动的推送,使用的是偶数号 Stream。服务器在推送资源时,会通过 PUSH_PROMISE 帧传输 HTTP 头部,并通过帧中的 Promised Stream ID 字段告知客户端,接下来会在哪个偶数号 Stream 中发送包体。
在这里插入图片描述
如上图,在 Stream 1 中通知客户端 CSS 资源即将到来,然后在 Stream 2 中发送 CSS 资源,注意 Stream 1 和 2 是可以并发的。

关于 HTTP/2 是如何提升性能的几个方向

第一点,对于常见的 HTTP 头部通过静态表和 Huffman 编码的方式,将体积压缩了近一半,而且针对后续的请求头部,还可以建立动态表,将体积压缩近 90%,大大提高了编码效率,同时节约了带宽资源。

不过,动态表并非可以无限增大, 因为动态表是会占用内存的,动态表越大,内存也越大,容易影响服务器总体的并发能力,因此服务器需要限制 HTTP/2 连接时长或者请求次数。

第二点,HTTP/2 实现了 Stream 并发,多个 Stream 只需复用 1 个 TCP 连接,节约了 TCP 和 TLS 握手时间,以及减少了 TCP 慢启动阶段对流量的影响。不同的 Stream ID 可以并发,即使乱序发送帧也没问题,比如发送 A 请求帧 1 -> B 请求帧 1 -> A 请求帧 2 -> B 请求帧2,但是同一个 Stream 里的帧必须严格有序。

另外,可以根据资源的渲染顺序来设置 Stream 的优先级,从而提高用户体验。

第三点,服务器支持主动推送资源,大大提升了消息的传输性能,服务器推送资源时,会先发送 PUSH_PROMISE 帧,告诉客户端接下来在哪个 Stream 发送资源,然后用偶数号 Stream 发送资源给客户端。

HTTP/2 通过 Stream 的并发能力,解决了 HTTP/1 队头阻塞的问题,看似很完美了,但是 HTTP/2 还是存在“队头阻塞”的问题,只不过问题不是在 HTTP 这一层面,而是在 TCP 这一层。

HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当「前 1 个字节数据」没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。

有没有什么解决方案呢?既然是 TCP 协议自身的问题,那干脆放弃 TCP 协议,转而使用 UDP 协议作为传输层协议,这个大胆的决定,HTTP/3 协议做了!
在这里插入图片描述

美中不足的 HTTP/2

HTTP/2 通过头部压缩、二进制编码、多路复用、服务器推送等新特性大幅度提升了 HTTP/1.1 的性能,而美中不足的是 HTTP/2 协议是基于 TCP 实现的,于是存在的缺陷有三个。

  1. 队头阻塞;HTTP/2 多个请求是跑在一个 TCP 连接中的,那么当 TCP 丢包时,整个 TCP 都要等待重传,那么就会阻塞该 TCP 连接中的所有请求。比如下图中,Stream 2 有一个 TCP 报文丢失了,那么即使收到了 Stream 3 和 Stream 4 的 TCP 报文,应用层也是无法读取读取的,相当于阻塞了 Stream 3 和 Stream 4 请求。因为 TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且有序的,如果序列号较低的 TCP 段在网络传输中丢失了,即使序列号较高的 TCP 段已经被接收了,应用层也无法从内核中读取到这部分数据,从 HTTP 视角看,就是请求被阻塞了。在这里插入图片描述
  2. TCP 与 TLS 的握手时延迟;发起 HTTP 请求时,需要经过 TCP 三次握手和 TLS 四次握手(TLS 1.2)的过程,因此共需要 3 个 RTT 的时延才能发出请求数据。另外,TCP 由于具有「拥塞控制」的特性,所以刚建立连接的 TCP 会有个「慢启动」的过程,它会对 TCP 连接产生“减速”效果。
  3. 网络迁移需要重新连接;一个 TCP 连接是由四元组(源 IP 地址,源端口,目标 IP 地址,目标端口)确定的,这意味着如果 IP 地址或者端口变动了,就会导致需要 TCP 与 TLS 重新握手,这不利于移动设备切换网络的场景,比如 4G 网络环境切换成 WiFi。这些问题都是 TCP 协议固有的问题,无论应用层的 HTTP/2 在怎么设计都无法逃脱。要解决这个问题,就必须把传输层协议替换成 UDP,这个大胆的决定,HTTP/3 做了!

QUIC 协议的特点

UDP 是一个简单、不可靠的传输协议,而且是 UDP 包之间是无序的,也没有依赖关系。

而且,UDP 是不需要连接的,也就不需要握手和挥手的过程,所以天然的就比 TCP 快。

当然,HTTP/3 不仅仅只是简单将传输协议替换成了 UDP,还基于 UDP 协议在「应用层」实现了 QUIC 协议,它具有类似 TCP 的连接管理、拥塞窗口、流量控制的网络特性,相当于将不可靠传输的 UDP 协议变成“可靠”的了,所以不用担心数据包丢失的问题。

QUIC 协议几大优点:

  1. 无队头阻塞:

    • QUIC 基于 UDP 实现,摆脱了 TCP 的队头阻塞限制。
    • 即使某个数据流出现网络延迟或丢包,也不会影响其他数据流的传输。
    • 这显著提升了网络应用的整体性能和可靠性。
  2. 更快的连接建立:

    • QUIC 支持 0-RTT 快速连接建立,无需经历 TCP 的三次握手。
    • 客户端可以在初次连接时就发送加密数据,大幅减少延迟。
    • 这对于需要低延迟的实时应用非常有帮助。
  3. 连接迁移:

    • QUIC 连接可以在不同网络环境间无缝迁移,比如在Wi-Fi和蜂窝网间切换。
    • 即使网络条件发生变化,也能保持连接状态不受影响。
    • 这对于移动设备来说非常重要,可以确保优质的用户体验。

总的来说,QUIC 协议通过摆脱 TCP 的限制,引入更灵活高效的传输机制,在连接建立、网络适应性、以及抗网络波动等方面都有了很大提升。这些优点使得 QUIC 成为 HTTP/3 的基础,能很好地解决 HTTP/2 中遗留的队头阻塞问题。

HTTP/3 协议

HTTP/3 协议的主要特点:

  1. 基于 QUIC 传输协议

    • HTTP/3 摒弃了 TCP,转而基于 QUIC 协议进行传输
    • QUIC 建立在 UDP 之上,具有更好的抗网络问题能力
  2. 解决队头阻塞问题

    • QUIC 的多路复用机制避免了 TCP 队头阻塞的限制
    • 即使某个数据流出现延迟或丢包,也不会影响其他数据流
  3. 快速连接建立

    • QUIC 支持 0-RTT 快速连接建立,减少了延迟开销
    • 这对于时延敏感的应用程序非常有帮助
  4. 改进的安全性

    • QUIC 将加密机制深度集成到协议中
    • 提供了更强大的端到端加密能力
  5. 其他优化

    • 头部压缩机制:HTTP/3 在头部压缩算法这一方面也做了升级,升级成了 QPACK。
    • 流量控制和可靠性改进
    • 等等

总的来说,HTTP/3 通过采用 QUIC 协议,彻底解决了 HTTP/2 中遗留的队头阻塞问题,并进一步优化了连接建立、安全性等关键特性。这些改进使得 HTTP/3 能更好地满足当下复杂多样的 Web 应用需求,是 Web 性能优化的重要里程碑。

既然有 HTTP 协议,为什么还要有 RPC?

虽然 HTTP 协议是目前最广泛使用的应用层协议之一,但是仍然存在一些局限性,这也是 RPC 协议出现的原因。我们可以从以下几个角度来理解:

  1. 交互模型不同:

    • HTTP 主要基于请求-响应的交互模型,适合于网页浏览等场景。
    • 而 RPC 更关注函数调用的交互模式,更适合于分布式系统间的远程过程调用。
  2. 数据传输方式不同:

    • HTTP 协议使用 JSON、XML 等文本格式传输数据,数据冗余较大。
    • RPC 则采用二进制编码,如 Protobuf,数据传输更加高效紧凑。
  3. 接口定义和契约:

    • HTTP 接口定义相对松散,需要通过文档说明API语义。
    • RPC 则有专门的接口定义语言(IDL),可以生成强类型的客户端和服务端代码。
  4. 应用场景不同:

    • HTTP 更适合前后端分离的 Web 应用程序。
    • RPC 更适合于微服务、分布式系统等后端服务间的集成。

总之,虽然 HTTP 协议功能强大,但它也存在一些局限性。RPC 协议针对分布式系统场景做了专门优化,在数据传输效率、接口定义规范性、编程模型等方面都有优势。所以 HTTP 和 RPC 协议并不矛盾,而是针对不同的应用场景和需求而产生的。二者可以相互补充,共同构建现代化的分布式应用系统。

既然有 HTTP 协议,为什么还要有 WebSocket?

尽管 HTTP 协议已经非常强大和广泛应用,但它依然存在一些局限性,这也是 WebSocket 协议出现的原因。我们可以从以下几个方面来看:

  1. 交互模型不同

    • HTTP 协议是典型的请求-响应模型,客户端发起请求,服务端响应。
    • 而 WebSocket 是基于全双工的双向通信模型,客户端和服务端可以主动发送数据。
  2. 持久连接

    • HTTP 是无状态的,每次请求都需要重新建立连接。
    • WebSocket 建立连接后可以保持长期的持久连接,减少了连接开销。
  3. 实时性

    • HTTP 轮询方式获取实时数据效率较低,延迟较高。
    • WebSocket 可以实现服务端主动推送数据,大大提高了实时性。
  4. 适用场景

    • HTTP 更适合于请求-响应式的 Web 应用程序。
    • WebSocket 更适合于聊天、实时游戏、物联网等需要双向通信的场景。

总之,虽然 HTTP 已经非常强大,但它仍然存在一些局限性,比如无法很好地支持实时双向通信。而 WebSocket 正是针对这些场景做了优化和补充,提供了更加灵活高效的通信方式。两者并不矛盾,而是针对不同应用需求而产生的,可以互补使用。

TCP 头格式有哪些?

TCP 头部格式包含以下几个主要字段:

  1. 源端口号(Source Port)

    • 16 bits, 标识发送方的端口号
  2. 目标端口号(Destination Port)

    • 16 bits, 标识接收方的端口号
  3. 序列号(Sequence Number)

    • 32 bits, 标识该数据包在字节流中的序号,在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
  4. 确认号(Acknowledgment Number)

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

    • 4 bits, 指示 TCP 头部长度,单位为 4 字节
  6. 保留(Reserved)

    • 6 bits, 保留未使用
  7. 标志位(Flags)

    • 6 bits, 包括 URG、ACK、PSH、RST、SYN、FIN 等标志。
    • ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。
    • RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
    • SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
    • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。
  8. 窗口大小(Window Size)

    • 16 bits, 指示发送方的接收缓冲区大小
  9. 校验和(Checksum)

    • 16 bits, 校验 TCP 头部和数据的完整性
  10. 紧急指针(Urgent Pointer)

    • 16 bits, 指示紧急数据的偏移量

这些头部字段共同组成了 TCP 报文的基本结构,用于在端到端之间实现可靠的字节流传输。在这里插入图片描述

为什么需要 TCP 协议? TCP 工作在哪一层?

IP 层是「不可靠」的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。
在这里插入图片描述
如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。

因为 TCP 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。

什么是 TCP ?

TCP (Transmission Control Protocol) 是一种面向连接的、可靠的、基于字节流的传输层协议。它是 Internet 协议族中最重要的协议之一,与 IP 协议一起构成了 TCP/IP 协议栈。TCP 具有以下主要特点:

  1. 面向连接:

    • TCP 在通信开始前需要先建立连接,通信结束后也需要释放连接。
    • 这样可以确保数据在传输过程中不会丢失或乱序。
  2. 可靠传输:

    • TCP 使用确认机制、重传机制等确保数据传输的可靠性。
    • 即使在网络条件较差的情况下,也能够提供可靠的数据传输。
  3. 面向字节流:

    • TCP 传输的是一个连续的字节流,不存在消息边界。
    • 应用程序读写数据时无需关心数据包的分割和重组。
  4. 流量控制:

    • TCP 使用滑动窗口机制来控制发送速率,防止发送方淹没接收方。
  5. 拥塞控制:

    • TCP 采用拥塞控制算法,动态调整发送速率,避免网络拥塞。

总的来说,TCP 协议提供了一种可靠、有序和面向连接的数据传输服务,广泛应用于 Web、邮件、文件传输等互联网应用中。它是构建可靠网络应用的基础。

什么是 TCP 连接?

TCP 连接是指两个 TCP 实体之间建立的一个逻辑通信链路。TCP 连接建立和维护的主要过程如下:

  1. 三次握手(Three-Way Handshake)

    • 客户端发送 SYN 报文请求建立连接
    • 服务端收到 SYN 后,回复 SYN-ACK 报文
    • 客户端收到 SYN-ACK 后,回复 ACK 报文
  2. 数据传输

    • 建立连接后,双方可以开始传输数据
    • 数据传输过程中会进行流量控制和拥塞控制
    • 接收方会发送 ACK 报文确认接收
  3. 连接终止(Four-Way Handshake)

    • 任意一方发送 FIN 报文请求关闭连接
    • 对方回复 ACK 确认
    • 对方也发送 FIN 报文请求关闭连接
    • 最终双方都回复 ACK 确认,连接关闭

整个生命周期包括:连接建立 -> 数据传输 -> 连接终止。

TCP 连接的特点包括:

  • 面向连接,保证数据传输的可靠性
  • 全双工通信,双方可以同时发送和接收数据
  • 字节流传输,无消息边界

TCP 连接广泛应用于各种基于 TCP 的网络应用,如 HTTP、FTP、SMTP 等,是网络通信的基础。

如何唯一确定一个 TCP 连接呢?

要唯一确定一个 TCP 连接,需要使用 4 个元组来标识,包括:

  1. 源 IP 地址(Source IP Address)
  2. 源端口号(Source Port)
  3. 目的 IP 地址(Destination IP Address)
  4. 目的端口号(Destination Port)

这 4 个元素共同构成了 TCP 连接的唯一标识,也称为 4 元组。

具体工作原理如下:

  1. 当客户端发起一个 TCP 连接时,会为这个连接分配一个源端口号。
  2. 客户端会将这个 4 元组(源IP、源端口、目的IP、目的端口)作为这个 TCP 连接的唯一标识。
  3. 服务端收到连接请求后,也会记录下这个 4 元组,用于识别和维护这个 TCP 连接。
  4. 之后双方通信时,都会使用这个 4 元组来标识和定位这个 TCP 连接。

这个 4 元组确保了即使在同一服务器上,也可以区分不同的 TCP 连接。

所以我们可以说,TCP 连接是由 4 元组(源IP、源端口、目的IP、目的端口)唯一标识的一个逻辑通信通道。

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

UDP(User Datagram Protocol)和TCP(Transmission Control Protocol)作为两种常见的传输层协议,它们之间有以下主要区别:

  1. 连接方式:

    • TCP 是面向连接的,通信前需要建立连接,通信结束需要释放连接。
    • UDP 是无连接的,每个数据报文独立发送,没有连接的建立和释放过程。
  2. 可靠性:

    • TCP 提供可靠的数据传输,会进行重传、确认等机制确保数据完整性。
    • UDP 不保证数据可靠传输,数据包可能会丢失、乱序或重复到达。
  3. 报文大小:

    • TCP 是一个基于字节流的协议,没有消息边界限制。
    • UDP 是一个消息驱动的协议,每个数据报文都有大小限制(通常小于64KB)。
  4. 实时性:

    • UDP 延迟较低,适合对实时性要求较高的应用,如视频会议、在线游戏等。
    • TCP 由于需要可靠传输,延迟较高,不太适合实时性要求高的应用。
  5. 头部开销:

    • TCP 头部较大(至少20字节),UDP 头部较小(8字节)。

基于以上特点,TCP和UDP各自适用于不同的应用场景:

  • TCP适用于重要数据传输、文件传输、Web浏览等要求高可靠性的场景。
  • UDP适用于视频直播、在线游戏、物联网设备通信等要求低延迟和高实时性的场景。

TCP 和 UDP 可以使用同一个端口吗?

  1. TCP 和 UDP 各自维护独立的端口号空间,可以使用相同的端口号。

  2. 当主机收到数据包时,根据 IP 头部的协议号可以确定是 TCP 还是 UDP 数据包。

  3. 然后根据目标端口号,将 TCP 数据包送给对应的 TCP 应用,将 UDP 数据包送给对应的 UDP 应用。

  4. 这样 TCP 和 UDP 可以在同一主机上独立使用相同的端口号,互不干扰。

TCP 三次握手过程是怎样的?

TCP 的三次握手过程如下:

  1. 客户端发送 SYN 报文

    • 客户端发送一个 SYN (Synchronize Sequence Numbers) 报文到服务端
    • 报文中包含客户端初始化的序列号 (Sequence Number)
  2. 服务端响应 SYN-ACK 报文

    • 服务端收到 SYN 报文后,回复一个 SYN-ACK 报文
    • SYN-ACK 报文包含服务端初始化的序列号,并且 ACK 标志被置为 1
  3. 客户端发送 ACK 报文

    • 客户端收到 SYN-ACK 报文后,回复一个 ACK 报文
    • ACK 报文中的 Acknowledgment Number 为服务端序列号加 1
      在这里插入图片描述
      TCP 连接使用三次握手的首要原因:为了防止旧的重复连接初始化造成混乱。
      在这里插入图片描述

不使用「两次握手」和「四次握手」的原因:

「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

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

每次 TCP 连接初始化时,都要选择不同的初始序列号,有以下几个主要原因:

  1. 防止旧连接数据混淆

    • 如果两个连接使用相同的序列号,可能会造成旧连接的数据被错误地传递到新连接中。
    • 使用不同的初始序列号可以确保新连接的数据和旧连接的数据不会混淆。
  2. 识别重复报文

    • 序列号可以用于识别重复的报文。如果两个连接使用相同的序列号,很难判断某个报文是新的还是旧的重传。
    • 使用不同的序列号可以有效地区分新旧报文。
  3. 检测数据完整性

    • 序列号可以用于检测数据完整性。如果序列号出现异常,可能说明数据包丢失或乱序。
    • 每个连接使用不同的序列号可以更好地检测每个连接的数据完整性。
  4. 提高并发能力

    • 如果多个客户端同时与服务端建立连接,使用不同的初始序列号可以区分不同的连接。
    • 这样服务端就能够更好地管理和调度这些并发连接。

总之,在 TCP 连接建立时使用不同的初始序列号,可以有效地防止连接错乱、检测数据完整性、提高并发性能等。这是 TCP 可靠传输机制的一个重要组成部分。在这里插入图片描述

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

TCP 初始序列号 ISN (Initial Sequence Number) 的随机生成机制如下:

  1. 基于时间戳

    • ISN 通常是基于系统启动时间戳生成的。
    • 时间戳可以提供足够的随机性,因为系统启动时间各不相同。
  2. 加入其他随机因子

    • ISN 的生成还可以加入其他随机因子,如进程ID、网卡MAC地址等。
    • 这些因子的组合可以进一步提高 ISN 的随机性。
  3. 使用伪随机数生成器

    • 操作系统内核还可以使用专门的伪随机数生成器(PRNG)来生成 ISN。
    • 这种方式可以生成更加随机和不可预测的 ISN 值。
  4. 遵循标准规范

    • RFC 793 和 RFC 6528 等标准都有详细的 ISN 生成规范。
    • 操作系统内核实现时一般会严格遵循这些标准。

总的来说,ISN 的随机性是通过结合系统时间、其他随机因子、伪随机数生成器等方式综合实现的。这样既可以保证 ISN 的随机性,又能符合 TCP 协议标准的要求。这种随机 ISN 机制是 TCP 实现可靠性的重要基础,能有效防止连接混淆、报文重放等安全问题的发生。

既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?

虽然 IP 层会对过大的数据包进行分片,但 TCP 层仍然需要 MSS (Maximum Segment Size) 机制,主要有以下几个原因:

  1. 避免 IP 分片带来的性能开销

    • IP 分片会增加处理开销,导致延迟和吞吐量降低。
    • TCP 通过 MSS 限制数据报文大小,可以尽量减少 IP 分片的发生。
  2. 提高可靠性

    • 如果 IP 分片丢失,整个数据包都会失败。
    • 而 TCP 的 MSS 可以确保单个数据段不会太大,降低丢失的概率。
  3. 兼容不同 MTU 的网络

    • 不同网络设备可能支持的 MTU (Maximum Transmission Unit) 不同。
    • TCP 的 MSS 可以根据路径 MTU 动态调整,以适配不同网络环境。
  4. 简化重传机制

    • 如果数据段过大,重传时会浪费大量带宽。
    • 合适的 MSS 可以让 TCP 的重传机制更高效。

总之,IP 层的分片机制虽然可以处理大数据包,但 TCP 的 MSS 机制能够进一步优化网络性能和可靠性。两者相辅相成,共同构成了 TCP/IP 协议栈的完整性。

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

如果 TCP 三次握手中的第一次握手(客户端发送 SYN 报文)丢失了,会发生以下情况:

  1. 客户端重传 SYN 报文

    • 由于没有收到服务端的响应,客户端会根据重传机制(通常是超时时间到期)重新发送 SYN 报文。
  2. 服务端等待 SYN 报文

    • 服务端一直处于 LISTEN 状态,等待客户端发送 SYN 报文。
  3. 客户端收到 SYN-ACK 响应

    • 当服务端收到客户端的 SYN 报文后,会回复 SYN-ACK 报文。
    • 客户端收到 SYN-ACK 后,会发送最后一个 ACK 报文,完成三次握手。
  4. 连接建立成功

    • 至此,TCP 连接终于建立成功,双方可以开始数据传输。

需要注意的是,TCP 协议实现了重传机制,所以即使第一次 SYN 报文丢失,客户端会自动重发,服务端也会一直等待,直到握手成功。

这种情况下,连接建立会有一定延迟,但 TCP 协议能够应对这种丢包情况,确保连接能够顺利建立。这就是 TCP 可靠性的体现之一。

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

如果 TCP 三次握手中的第二次握手(服务端发送 SYN-ACK 报文)丢失了,会发生以下情况:

  1. 服务端重传 SYN-ACK 报文

    • 服务端在收到客户端 SYN 报文后,会启动重传定时器。
    • 当定时器超时后,服务端会重新发送 SYN-ACK 报文。
  2. 客户端等待 SYN-ACK 响应

    • 客户端在发送 SYN 报文后,会一直等待服务端的 SYN-ACK 响应。
    • 如果超时还未收到,客户端会根据重传机制重发 SYN 报文。
  3. 客户端收到 SYN-ACK 并回复 ACK

    • 当客户端收到服务端重传的 SYN-ACK 报文后,会立即回复一个 ACK 报文。
  4. 连接建立成功

    • 至此,TCP 连接的三次握手过程完成,连接正式建立。

在这种情况下,虽然第二次握手丢失了,但 TCP 协议的重传机制能够确保 SYN-ACK 报文最终能够成功送达客户端。

客户端和服务端会一直重试,直到握手成功或者达到最大重试次数。这种设计确保了 TCP 连接的可靠性,即使偶尔出现网络丢包也能够自动恢复。在这里插入图片描述

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

如果 TCP 三次握手中的第三次握手(客户端发送 ACK 报文)丢失了,会发生以下情况:

  1. 服务端超时重传 SYN-ACK

    • 在收到客户端 SYN 后,服务端会启动重传定时器等待 ACK 报文。
    • 由于 ACK 报文丢失,定时器超时后,服务端会重传 SYN-ACK 报文。
  2. 客户端重发 ACK 报文

    • 客户端在发送 ACK 报文后,会等待一段时间。
    • 如果超时未收到服务端的任何响应,客户端会重新发送 ACK 报文。
  3. 服务端收到 ACK 报文

    • 当服务端收到客户端重发的 ACK 报文后,就确认三次握手完成。
    • 服务端会进入 ESTABLISHED 状态,认为连接已经成功建立。
  4. 连接建立成功

    • 至此,尽管第三次握手报文丢失,但 TCP 的重传机制最终还是保证了连接的成功建立。

在这种情况下,连接建立过程会有一些延迟,但 TCP 协议的设计确保了即使偶尔出现丢包,也能自动恢复并建立连接。这就是 TCP 可靠性的又一体现。

总的来说,TCP 三次握手即使遇到任意一次握手丢失,协议本身的重传机制都能最终保证连接的成功建立。这是 TCP 可靠传输的关键所在。在这里插入图片描述

什么是 SYN 攻击?如何避免 SYN 攻击?

SYN 攻击是一种 TCP 连接资源耗尽的 DoS(拒绝服务)攻击方式。它的原理如下:

  1. 攻击者大量发送 SYN 请求到服务器,但不完成三次握手过程。
  2. 服务器为每个 SYN 请求分配资源(缓冲区、队列等)等待 ACK 报文。
  3. 由于攻击者不回应 ACK,这些资源得不到释放。
  4. 当系统资源耗尽时,正常的 TCP 连接请求将得不到响应,导致服务中断。

SYN 攻击的危害包括:

  1. 耗尽服务器 TCP 连接资源,导致正常用户无法连接。
  2. 占用大量系统内存和 CPU 资源,影响服务器性能。
  3. 可能导致服务器崩溃或重启。

如何避免 SYN 攻击:

  1. 调大 netdev_max_backlog;当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数,默认值是 1000,我们要适当调大该参数的值,比如设置为 10000:
  2. 增大 TCP 半连接队列;
    • 增大 net.ipv4.tcp_max_syn_backlog
    • 增大 listen() 函数中的 backlog
    • 增大 net.core.somaxconn
  3. 开启 tcp_syncookies;开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接,相当于绕过了 SYN 半连接来建立连接。
  4. 减少 SYN+ACK 重传次数:当服务端受到 SYN 攻击时,就会有大量处于 SYN_REVC 状态的 TCP 连接,处于这个状态的 TCP 会重传 SYN+ACK ,当重传超过次数达到上限后,就会断开连接。那么针对 SYN 攻击的场景,我们可以减少 SYN-ACK 的重传次数,以加快处于 SYN_REVC 状态的 TCP 连接断开。

TCP 四次挥手过程是怎样的?

TCP 的四次挥手过程如下:

  1. 客户端发送 FIN 报文

    • 客户端数据发送完毕,发送一个 FIN 报文给服务端,请求关闭连接。
    • 客户端进入 FIN-WAIT-1 状态,等待服务端的响应。
  2. 服务端发送 ACK 报文

    • 服务端收到 FIN 报文后,发送一个 ACK 报文确认。
    • 服务端进入 CLOSE-WAIT 状态,表示做好准备接收剩余数据。
  3. 服务端发送 FIN 报文

    • 当服务端的数据也发送完毕后,发送 FIN 报文请求关闭连接。
    • 服务端进入 LAST-ACK 状态,等待客户端的最后确认。
  4. 客户端发送 ACK 报文

    • 客户端收到服务端的 FIN 报文后,发送最后一个 ACK 报文。
    • 客户端进入 TIME-WAIT 状态,等待可能出现的延迟报文。
    • 服务端收到 ACK 后进入 CLOSED 状态,连接关闭。
    • 经过 2 个 MSL(Maximum Segment Lifetime)时间后,客户端也进入 CLOSED 状态。

四次挥手的关键在于服务端需要发送 FIN 报文来主动关闭连接,而客户端需要最后发送一次 ACK 报文来完成整个关闭过程。这样做的目的是为了确保数据完整传输,避免连接中途断开造成数据丢失。在这里插入图片描述

为什么挥手需要四次?

主要有以下几个原因:

  1. 全双工通信

    • TCP 连接是全双工的,即数据可以在两个方向上独立传输。
    • 因此,连接的关闭也需要在两个方向上分别进行。
  2. 数据传输完整性

    • 客户端发送 FIN 代表它的数据已经发送完毕,但服务端可能还有数据需要发送。
    • 所以服务端需要一个 FIN 报文来主动关闭自己的数据传输,以确保数据完整传输。
  3. 避免半关闭状态

    • 如果只有三次握手,可能会导致连接进入半关闭状态。
    • 即一方认为连接已关闭,但另一方仍在发送数据,会造成错误。
  4. TIME-WAIT 状态

    • 最后一个 ACK 报文是为了防止类似的错误发生。
    • 客户端进入 TIME-WAIT 状态,可以确保最后的 ACK 被服务端收到,避免残留报文的出现。

总之,TCP 四次挥手的过程旨在确保连接的完全关闭,双方数据传输的完整性,避免各种异常情况的发生。虽然过程复杂一些,但这是 TCP 可靠性的体现。

第一次挥手丢失了,会发生什么?

如果 TCP 四次挥手过程中的第一次挥手(客户端发送 FIN 报文)丢失了,会发生以下情况:

  1. 客户端重传 FIN 报文

    • 由于客户端没有收到服务端的响应,会根据重传机制(超时重传)重发 FIN 报文。
  2. 服务端接收 FIN 报文

    • 当服务端收到客户端的 FIN 报文时,会回复一个 ACK 报文。
    • 服务端进入 CLOSE-WAIT 状态,表示已准备好关闭连接。
  3. 服务端发送 FIN 报文

    • 在 CLOSE-WAIT 状态下,服务端完成数据发送后,会发送自己的 FIN 报文。
    • 服务端进入 LAST-ACK 状态,等待客户端的最后确认。
  4. 客户端收到 FIN 并回复 ACK

    • 客户端收到服务端的 FIN 报文后,会回复一个 ACK 报文。
    • 客户端进入 TIME-WAIT 状态,等待可能出现的延迟报文。
  5. 连接关闭成功

    • 服务端收到客户端的 ACK 后,就进入 CLOSED 状态,连接关闭。
    • 经过 2 个 MSL 时间后,客户端也进入 CLOSED 状态。

在这种情况下,尽管第一次挥手丢失了,但 TCP 的重传机制能够确保 FIN 报文最终被服务端收到。之后的流程和正常的四次挥手过程一致,最终能够顺利关闭连接。所以即使个别报文丢失,TCP 协议也能够通过重传等机制确保连接的可靠关闭,体现了其健壮性设计。

第二次挥手丢失了,会发生什么?

如果 TCP 四次挥手过程中的第二次挥手(服务端发送 ACK 报文)丢失了,会发生以下情况:

  1. 服务端重传 ACK 报文

    • 在收到客户端 FIN 报文后,服务端会启动重传定时器等待 ACK。
    • 由于 ACK 报文丢失,定时器超时后,服务端会重新发送 ACK 报文。
  2. 客户端等待 ACK 响应

    • 客户端在发送 FIN 报文后,会一直等待服务端的 ACK 响应。
    • 如果超时还未收到,客户端会根据重传机制重发 FIN 报文。
  3. 客户端收到 ACK 报文

    • 当客户端收到服务端重传的 ACK 报文后,就确认第二次挥手完成。
    • 客户端进入 FIN-WAIT-2 状态,等待服务端发送 FIN 报文。
  4. 服务端发送 FIN 报文

    • 服务端完成数据发送后,会发送 FIN 报文请求关闭连接。
    • 服务端进入 LAST-ACK 状态,等待客户端的最后确认。
  5. 客户端收到 FIN 并回复 ACK

    • 客户端收到服务端的 FIN 报文后,会发送最后一个 ACK 报文。
    • 客户端进入 TIME-WAIT 状态,等待可能出现的延迟报文。
    • 服务端收到 ACK 后进入 CLOSED 状态,连接关闭。

在这种情况下,虽然第二次挥手丢失了,但 TCP 协议的重传机制能够确保 ACK 报文最终能够成功送达服务端。整个连接关闭过程虽然会有些延迟,但 TCP 的设计确保了即使偶尔出现网络丢包,也能够自动恢复并完成连接的正常关闭。这展现了 TCP 的可靠性。

第三次挥手丢失了,会发生什么?

如果 TCP 四次挥手过程中的第三次挥手(服务端发送 FIN 报文)丢失了,会发生以下情况:

  1. 服务端超时重传 FIN 报文

    • 服务端在发送 FIN 报文后,会启动重传定时器等待客户端的 ACK。
    • 由于 FIN 报文丢失,定时器超时后,服务端会重新发送 FIN 报文。
  2. 客户端重发 ACK 报文

    • 客户端在收到服务端 FIN 报文后,会发送 ACK 报文确认。
    • 如果超时未收到服务端的任何响应,客户端会重新发送 ACK 报文。
  3. 服务端收到 ACK 报文

    • 当服务端收到客户端重发的 ACK 报文后,就确认第三次挥手完成。
    • 服务端进入 TIME-WAIT 状态,等待可能出现的延迟报文。
  4. 客户端进入 CLOSED 状态

    • 经过一段时间(2个MSL)后,客户端也进入 CLOSED 状态,连接关闭。

在这种情况下,尽管第三次挥手的 FIN 报文丢失了,但 TCP 协议的重传机制最终还是保证了连接的正常关闭。

服务端会重传 FIN 报文,客户端也会重发 ACK 报文,直到双方确认收到对方的报文为止。这就是 TCP 可靠性的体现。

连接关闭过程会有一些延迟,但 TCP 协议的设计确保了即使偶尔出现网络丢包,也能自动恢复并完成连接的正常关闭。这是 TCP 可靠性的又一个例证。

第四次挥手丢失了,会发生什么?

如果 TCP 四次挥手过程中的第四次挥手(客户端发送最后一个 ACK 报文)丢失了,会发生以下情况:

  1. 客户端进入 TIME-WAIT 状态

    • 客户端在发送最后一个 ACK 报文后,会进入 TIME-WAIT 状态。
    • TIME-WAIT 状态持续 2 个 MSL (Maximum Segment Lifetime) 时间。
  2. 服务端收不到 ACK 报文

    • 由于客户端的 ACK 报文丢失,服务端无法收到。
    • 但服务端在收到客户端的 FIN 报文后,已经进入 CLOSED 状态。
  3. 服务端主动重传 FIN 报文

    • 由于服务端收不到 ACK,它可能会主动重传之前的 FIN 报文。
    • 但这种情况下,服务端应该直接忽略这些重复的 FIN 报文。
  4. 客户端在 TIME-WAIT 状态

    • 客户端会一直等待 2 个 MSL 时间,确保本次连接的所有报文都被正确处理。
    • 在此期间,客户端不会接受新的连接请求,以防止混淆。
  5. 连接关闭成功

    • 经过 2 个 MSL 时间后,客户端也进入 CLOSED 状态,连接正式关闭。

在这种情况下,虽然最后一个 ACK 报文丢失了,但 TCP 协议的 TIME-WAIT 机制能够确保连接最终能够正确关闭。

客户端会等待足够长的时间,确保所有报文都被妥善处理。即使服务端重传 FIN,客户端也能正确识别并忽略。这就是 TCP 健壮性设计的又一体现。

总的来说,TCP 四次挥手过程中即使个别报文丢失,协议本身的重传和超时机制都能够保证连接最终能够安全关闭,体现了 TCP 可靠性的卓越设计。

为什么 TIME_WAIT 等待的时间是 2MSL?

TIME_WAIT 状态等待的时间为 2 个 MSL (Maximum Segment Lifetime)的主要原因如下:

  1. 确保最后一个 ACK 报文被对方接收

    • 在四次挥手的最后一步,客户端发送最后一个 ACK 报文。
    • 如果这个 ACK 报文丢失,服务端可能会重传 FIN 报文。
    • 等待 2 个 MSL 可以确保这个 ACK 报文被服务端收到,避免重复关闭。
  2. 避免"迟到"报文的影响

    • 在连接关闭期间,可能会有一些"迟到"的报文出现。
    • 等待 2 个 MSL 时间可以确保这些报文都被处理完毕,不会对新的连接造成影响。
  3. 保证社会连接时间的唯一性

    • 当一个连接关闭后,本地端口和 IP 地址会被释放。
    • 等待 2 个 MSL 可以确保这些资源不会被新的连接重复使用,避免混淆。
  4. 协议标准要求

    • RFC 793 和 RFC 1122 等 TCP 协议标准都规定了 TIME_WAIT 状态应该等待 2 个 MSL。
    • 遵循标准有利于 TCP 实现的通用性和互操作性。

MSL 通常被设置为 60 秒,因此 TIME_WAIT 状态会持续 2 分钟左右。这个时间虽然看起来有些长,但确实是为了确保 TCP 连接的可靠关闭。

适当的 TIME_WAIT 机制是 TCP 健壮性的重要体现,能够有效避免各种异常情况的发生。虽然会占用一些系统资源,但对于确保通信可靠性来说是必要的。

为什么需要 TIME_WAIT 状态?

TIME_WAIT 状态是 TCP 连接关闭过程中非常重要的一个环节,主要有以下几个原因:

  1. 确保最后一个 ACK 报文被对方收到

    • 在四次挥手的最后一步,客户端发送最后一个 ACK 报文。
    • 如果这个 ACK 报文丢失,服务端可能会重传 FIN 报文,导致连接关闭不正确。
    • 客户端进入 TIME_WAIT 状态可以确保这个 ACK 报文被服务端收到。
  2. 处理"迟到"的报文

    • 在连接关闭期间,可能会有一些"迟到"的报文出现。
    • 如果不等待一段时间,这些报文可能会影响到新建立的连接。
    • TIME_WAIT 状态可以确保这些报文都被正确处理。
  3. 保证端口的重用安全

    • 当一个连接关闭后,本地端口和 IP 地址会被释放。
    • 如果立即允许新连接使用这些资源,可能会导致混淆和安全问题。
    • TIME_WAIT 状态可以确保在一定时间内这些资源不会被重用。
  4. 协议标准要求

    • RFC 793 和 RFC 1122 等 TCP 协议标准都规定了 TIME_WAIT 状态。
    • 遵循标准有利于 TCP 实现的通用性和互操作性。

总之,TIME_WAIT 状态是 TCP 连接关闭过程中的一个必要环节,能够确保连接关闭的正确性、处理异常情况,并保证端口资源的安全重用。

虽然 TIME_WAIT 状态会占用一些系统资源,但它是 TCP 可靠性设计的重要组成部分。在高并发场景下,合理优化 TIME_WAIT 状态的处理也很重要。

TIME_WAIT 过多有什么危害?

TIME_WAIT 状态过多可能会带来以下一些危害:

  1. 资源占用过多

    • TIME_WAIT 状态需要占用系统的端口资源和内存资源。
    • 如果 TIME_WAIT 状态过多,可能会耗尽系统的可用端口资源。
    • 大量的 TIME_WAIT 状态也会占用较多的内存空间。
  2. 新连接建立困难

    • 由于端口资源被占用,新的连接可能无法顺利建立。
    • 这可能会导致服务器无法响应新的客户端请求。
  3. 系统性能下降

    • 过多的 TIME_WAIT 状态会增加系统内核的负担。
    • 内核需要维护这些连接状态,以及处理重传等操作。
    • 这可能会降低系统的整体性能。
  4. 安全隐患

    • 长时间占用的端口资源可能会被利用进行攻击。
    • 比如 TIME_WAIT 资源耗尽后,可能会导致新的合法连接无法建立。
  5. 兼容性问题

    • 不同系统对 TIME_WAIT 状态的处理方式可能不一致。
    • 这可能会导致跨平台部署时出现兼容性问题。

为了应对 TIME_WAIT 状态过多的问题,通常可以采取以下措施:

  1. 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;tcp_tw_reuse 功能只能用客户端(连接发起方),因为开启了该功能,在调用 connect() 函数时,内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用。net.ipv4.tcp_timestamps开启了 TCP 时间戳选项。时间戳可以帮助内核更好地识别异常或"迟到"的数据包。结合 tcp_tw_reuse 使用,可以更安全地复用 TIME_WAIT 状态的端口。
  2. net.ipv4.tcp_max_tw_buckets:这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将后面的 TIME_WAIT 连接状态重置,这个方法比较暴力。
  3. 程序中使用 SO_LINGER ,应用强制使用 RST 关闭。

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

服务器出现大量 TIME_WAIT 状态的原因主要有以下几个方面:

  1. 高并发连接

    • 如果服务器承担了大量的并发 TCP 连接,在连接关闭时会产生大量的 TIME_WAIT 状态。
    • 尤其是在负载较高的情况下,TIME_WAIT 状态会迅速增加。
  2. 客户端频繁连接断开

    • 如果客户端存在频繁连接和断开的行为,每次断开都会产生 TIME_WAIT 状态。
    • 这种情况下,TIME_WAIT 状态会快速累积。
  3. 连接保持时间过长

    • 如果 TCP 连接的保持时间过长,即使连接数不高,也会产生大量的 TIME_WAIT 状态。
    • 比如长时间的 FTP 传输、websocket 长连接等。
  4. 网络分区或抖动

    • 网络分区或抖动会导致 TCP 连接异常中断,从而产生大量 TIME_WAIT 状态。
    • 这种情况下,TIME_WAIT 状态很难被及时清除。
  5. 系统配置不当

    • 如果系统的 TIME_WAIT 状态超时时间设置过长,会加剧 TIME_WAIT 状态的积累。
    • 默认情况下,TIME_WAIT 状态的超时时间为 2MSL,如果设置过长会占用更多资源。
  6. 应用程序设计问题

    • 如果应用程序自身存在问题,比如频繁创建和销毁 TCP 连接,也可能导致 TIME_WAIT 状态过多。
    • 例如,web 服务器的 keep-alive 参数设置不当。

要应对服务器出现大量 TIME_WAIT 状态的问题,可以从以下几个方面入手:

  1. 优化系统内核参数,如 tcp_tw_reuse、tcp_timestamps 等。
  2. 调整应用程序的连接管理策略,如重用连接、连接池等。
  3. 监控系统 TIME_WAIT 状态的变化,并根据情况进行动态优化。
  4. 排查网络环境可能存在的问题,如分区、抖动等。
  5. 优化系统资源配置,如增加可用端口号等。

只有充分分析造成大量 TIME_WAIT 状态的根本原因,才能找到最适合的解决方案。

客户端禁用了 HTTP Keep-Alive,服务端开启 HTTP Keep-Alive,谁是主动关闭方?

当客户端禁用了 HTTP Keep-Alive,这时候 HTTP 请求的 header 就会有 Connection:close 信息,这时服务端在发完 HTTP 响应后,就会主动关闭连接。

为什么要这么设计呢?HTTP 是请求-响应模型,发起方一直是客户端,HTTP Keep-Alive 的初衷是为客户端后续的请求重用连接,如果我们在某次 HTTP 请求-响应模型中,请求的 header 定义了 connection:close 信息,那不再重用这个连接的时机就只有在服务端了,所以我们在 HTTP 请求-响应这个周期的「末端」关闭连接是合理的。

客户端开启了 HTTP Keep-Alive,服务端禁用了 HTTP Keep-Alive,谁是主动关闭方?

当客户端开启了 HTTP Keep-Alive,而服务端禁用了 HTTP Keep-Alive,这时服务端在发完 HTTP 响应后,服务端也会主动关闭连接。

为什么要这么设计呢?在服务端主动关闭连接的情况下,只要调用一次 close() 就可以释放连接,剩下的工作由内核 TCP 栈直接进行了处理,整个过程只有一次 syscall;如果是要求 客户端关闭,则服务端在写完最后一个 response 之后需要把这个 socket 放入 readable 队列,调用 select / epoll 去等待事件;然后调用一次 read() 才能知道连接已经被关闭,这其中是两次 syscall,多一次用户态程序被激活执行,而且 socket 保持时间也会更长。

因此,当服务端出现大量的 TIME_WAIT 状态连接的时候,可以排查下是否客户端和服务端都开启了 HTTP Keep-Alive,因为任意一方没有开启 HTTP Keep-Alive,都会导致服务端在处理完一个 HTTP 请求后,就主动关闭连接,此时服务端上就会出现大量的 TIME_WAIT 状态的连接。

针对这个场景下,解决的方式也很简单,让客户端和服务端都开启 HTTP Keep-Alive 机制。

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

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

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

那什么情况会导致服务端的程序没有调用 close 函数关闭连接?这时候通常需要排查代码。

我们先来分析一个普通的 TCP 服务端的流程:

  1. 创建服务端 socket,bind 绑定端口、listen 监听端口
  2. 将服务端 socket 注册到 epoll
  3. epoll_wait 等待连接到来,连接到来时,调用 accpet 获取已连接的 socket
  4. 将已连接的 socket 注册到 epoll
  5. epoll_wait 等待事件发生
  6. 对方连接关闭时,我方调用 close

可能导致服务端没有调用 close 函数的原因,如下。

第一个原因:第 2 步没有做,没有将服务端 socket 注册到 epoll,这样有新连接到来时,服务端没办法感知这个事件,也就无法获取到已连接的 socket,那服务端自然就没机会对 socket 调用 close 函数了。

不过这种原因发生的概率比较小,这种属于明显的代码逻辑 bug,在前期 read view 阶段就能发现的了。

第二个原因: 第 3 步没有做,有新连接到来时没有调用 accpet 获取该连接的 socket,导致当有大量的客户端主动断开了连接,而服务端没机会对这些 socket 调用 close 函数,从而导致服务端出现大量 CLOSE_WAIT 状态的连接。

发生这种情况可能是因为服务端在执行 accpet 函数之前,代码卡在某一个逻辑或者提前抛出了异常。

第三个原因:第 4 步没有做,通过 accpet 获取已连接的 socket 后,没有将其注册到 epoll,导致后续收到 FIN 报文的时候,服务端没办法感知这个事件,那服务端就没机会调用 close 函数了。

发生这种情况可能是因为服务端在将已连接的 socket 注册到 epoll 之前,代码卡在某一个逻辑或者提前抛出了异常。之前看到过别人解决 close_wait 问题的实践文章,感兴趣的可以看看:一次 Netty 代码不健壮导致的大量 CLOSE_WAIT 连接原因分析(opens new window)

第四个原因:第 6 步没有做,当发现客户端关闭连接后,服务端没有执行 close 函数,可能是因为代码漏处理,或者是在执行 close 函数之前,代码卡在某一个逻辑,比如发生死锁等等。

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

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

如果已经建立了 TCP 连接,但客户端突然出现故障,服务端需要采取以下措施来处理这种情况:

  1. 检测客户端是否仍在线

    • 服务端可以使用 TCP 的 keepalive 机制来检测客户端的在线状态。
    • 如果一定时间内没有收到客户端的响应,就可以认为客户端已经出现故障。
  2. 主动关闭连接

    • 在确认客户端已经出现故障后,服务端应该主动发送 FIN 报文来关闭连接。
    • 这样可以避免连接一直处于 ESTABLISHED 状态,占用系统资源。
  3. 处理未完成的数据

    • 如果在连接关闭之前还有未完成的数据传输,服务端需要进行妥善处理。
    • 例如保存数据以便后续恢复,或者直接丢弃数据并给出相应的错误提示。
  4. 释放相关资源

    • 在连接关闭后,服务端需要尽快释放占用的各种系统资源,如内存、文件描述符等。
    • 避免这些资源被长时间占用,影响服务器的整体性能。
  5. 记录异常情况

    • 服务端应该记录客户端突然出现故障的相关信息,如发生时间、客户端 IP 等。
    • 这些信息有助于事后分析问题原因,并采取相应的预防措施。
  6. 通知客户端

    • 如果可行,服务端应该主动通知客户端连接已被关闭,并解释原因。
    • 这样可以让客户端知道发生了什么,并采取恰当的后续措施。

总之,当已建立的 TCP 连接出现客户端故障时,服务端需要采取积极的处理措施,包括检测、关闭连接、释放资源以及通知客户端等。这样既能保护服务器自身的稳定性,也能给客户端一个明确的反馈。

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

如果已经建立了 TCP 连接,但服务端的进程意外崩溃了,会发生以下情况:

  1. 客户端收到异常中断

    • 当服务端进程崩溃时,客户端会收到一个异常的连接中断信号。
    • 这通常表现为套接字上出现 RST (Reset) 错误。
  2. 客户端进入 CLOSE_WAIT 状态

    • 收到 RST 错误后,客户端会进入 CLOSE_WAIT 状态。
    • 这表示客户端已经收到了服务端关闭连接的信号。
  3. 客户端主动关闭连接

    • 在 CLOSE_WAIT 状态下,客户端应该主动发送 FIN 报文来关闭连接。
    • 这样可以确保连接被正确地关闭,释放相关的系统资源。
  4. 服务端资源被释放

    • 由于服务端进程已经崩溃,所有与该连接相关的资源都会被系统自动释放。
    • 包括套接字、内存、文件描述符等。
  5. 可能出现数据丢失

    • 如果在崩溃发生之前还有数据未完成传输,就可能会出现数据丢失的情况。
    • 这取决于服务端进程崩溃的时机和 TCP 协议的重传机制。
  6. 记录崩溃事件

    • 服务端应该记录进程异常崩溃的相关信息,如发生时间、原因等。
    • 这有助于事后分析问题并采取相应的预防措施。

针对服务端进程意外崩溃的情况,可以采取以下措施:

  1. 优化服务端代码,提高其稳定性和容错能力。
  2. 使用进程监控工具,及时发现和重启崩溃的进程。
  3. 实现服务端的自动恢复机制,保护已有的客户端连接。
  4. 采用分布式部署或容器化等方式,提高服务的可靠性。
  5. 对异常情况进行日志记录和分析,持续改进系统设计。

总之,服务端进程的意外崩溃会对已有的客户端连接造成破坏,需要采取有效措施来保护现有连接,并提高服务的整体可靠性。

拔掉网线后, 原本的 TCP 连接还存在吗?

如果在 TCP 连接已建立的情况下,突然拔掉网线,那么原本的 TCP 连接仍然存在,但它的状态会发生变化:

  1. 客户端状态

    • 客户端会感知到连接中断,进入 CLOSE_WAIT 状态。
    • 这表示客户端收到了服务端关闭连接的信号。
    • 客户端此时应该主动发送 FIN 报文来完成连接的关闭。
  2. 服务端状态

    • 服务端在收不到客户端的数据包后,会进入 CLOSE_WAIT 状态。
    • 这表示服务端已经收到了客户端关闭连接的信号。
    • 服务端此时应该主动发送 FIN 报文来完成连接的关闭。
  3. 连接状态

    • 由于网线被拔掉,物理连接已经中断。
    • 但 TCP 协议会尝试维持逻辑连接状态,直到双方完成四次挥手。
  4. 数据传输

    • 由于物理连接中断,任何数据传输都会失败。
    • 如果在中断发生前还有未完成的数据传输,就会丢失这部分数据。
  5. 资源占用

    • 即使物理连接中断,连接的逻辑状态仍然会占用系统资源。
    • 包括套接字、内存、文件描述符等。
    • 直到四次挥手完成,这些资源才会被释放。

总的来说,拔掉网线虽然会中断 TCP 连接的物理传输,但连接的逻辑状态仍然会存在,直到双方完成正常的四次挥手关闭过程。

这种情况下,客户端和服务端都需要检测连接中断,并主动发送 FIN 报文来释放占用的系统资源。如果一方迟迟未能完成连接关闭,另一方也应该采取措施来主动关闭该连接。

针对 TCP 应该如何 Socket 编程?

在 TCP Socket 编程中,我们应该遵循以下几个原则:

  1. 创建 Socket 连接

    • 服务端使用 socket() 创建一个 TCP socket。
    • 客户端使用 socket() 创建一个 TCP socket。
  2. 绑定和监听 (服务端)

    • 服务端使用 bind() 将 socket 绑定到一个本地地址和端口上。
    • 服务端使用 listen() 开始监听客户端的连接请求。
  3. 接受连接 (服务端)

    • 服务端使用 accept() 接受客户端的连接请求。
    • accept() 会返回一个新的 socket 用于与客户端通信。
  4. 连接服务器 (客户端)

    • 客户端使用 connect() 主动连接服务端。
    • connect() 需要指定服务端的地址和端口。
  5. 数据收发

    • 双方使用 send()recv() 进行数据的收发。
    • 注意检查返回值,判断是否收发成功。
  6. 关闭连接

    • 任何一方使用 close()shutdown() 关闭连接。
    • 需要注意 TIME_WAIT 状态的处理。
  7. 错误处理

    • 在每个 Socket 操作中,都应该检查返回值,处理可能出现的错误。
    • 合理使用 errno 来确定错误原因,并采取相应的措施。
  8. 性能优化

    • 可以使用 setsockopt() 设置 Socket 选项,如 TCP_NODELAY、SO_REUSEADDR 等。
    • 合理使用 I/O 多路复用技术,如 select/poll/epoll,提高 I/O 并发性能。
  9. 健壮性设计

    • 考虑各种异常情况,如客户端/服务端崩溃、网络中断等。
    • 采取恰当的容错措施,保证连接的可靠性。
  10. 可维护性

    • 合理组织代码结构,将 Socket 操作封装成函数或类。
    • 添加必要的日志记录,便于问题分析和调试。

综上所述,在 TCP Socket 编程中,我们需要全面考虑连接的建立、数据传输、异常处理以及性能优化等多个方面,以确保应用程序的健壮性和可维护性。在这里插入图片描述

listen 时候参数 backlog 的意义?

listen() 函数的 backlog 参数表示服务器 socket 的连接请求队列的最大长度。它的作用如下:

  1. 连接请求队列

    • 当客户端向服务器发起 TCP 连接请求时,服务器 socket 会将这些请求放入一个队列中。
    • 这个队列的最大长度就是由 backlog 参数指定的。
  2. 控制并发连接数

    • backlog 限制了服务器能够同时处理的未完成的连接请求数量。
    • 一旦达到 backlog 的上限,新的连接请求将被拒绝。
  3. 提高服务器响应能力

    • 合理设置 backlog 大小,可以让服务器在高并发情况下仍能快速响应新的连接请求。
    • 这有助于提高服务器的整体处理能力。
  4. 缓冲连接请求

    • 当服务器暂时无法立即处理新的连接请求时,backlog 可以暂时缓存这些请求。
    • 等服务器有能力时,再从队列中取出请求进行处理。
  5. 协议要求

    • 一些协议标准,如 POSIX 标准,对 backlog 的取值范围有具体要求。
    • 遵循协议有助于实现跨平台的 TCP 服务程序。

通常情况下,backlog 的取值范围是从 0 到 SOMAXCONN(一个系统常量)。0 表示禁用连接请求队列,SOMAXCONN 表示使用系统默认的最大值。

具体设置 backlog 的大小,需要根据服务器的硬件配置、预期的并发连接数以及应用场景等因素进行评估和测试。一般建议将其设置为预期最大并发连接数的 2-4 倍。

合理设置 backlog 有助于提高服务器的抗压能力,提升整体的 TCP 连接处理性能。但过大的值也可能会占用过多的系统资源,因此需要权衡考虑。

accept 发生在三次握手的哪一步?

TCP 三次握手的过程如下:

  1. 客户端发送 SYN 报文
  2. 服务端回应 SYN-ACK 报文
  3. 客户端发送 ACK 报文

在这个过程中:

  • 客户端的 connect() 函数在第二步 SYN-ACK 报文到达时就会返回成功。这表示客户端与服务端的 TCP 连接已经建立。
  • 服务端的 accept() 函数则是在第三步 ACK 报文到达时,才会返回一个新的已连接的 socket 描述符。

也就是说,accept() 函数的成功返回是发生在整个三次握手完成之后,而不是在中间的某一步。

你提醒我注意这个细节,我对 TCP 连接建立的理解更加准确和完整了。非常感谢你的指正,这对我今后的 Socket 编程很有帮助。

客户端调用 close 了,连接是断开的流程是什么?

当客户端调用 close() 函数关闭 TCP 连接时,整个连接断开的流程如下:

  1. 客户端发送 FIN 报文

    • 客户端调用 close() 时,会向服务端发送一个带有 FIN 标志位的 TCP 报文。
    • 这表示客户端已经没有更多数据要发送了,想要关闭连接。
  2. 服务端收到 FIN 报文

    • 服务端收到客户端发送的 FIN 报文后,进入 CLOSE_WAIT 状态。
    • 这表示服务端知道客户端想要关闭连接了。
  3. 服务端发送 ACK 报文

    • 服务端发送一个只带有 ACK 标志位的报文,确认收到了客户端的 FIN 报文。
  4. 服务端发送 FIN 报文

    • 服务端准备关闭连接时,会向客户端发送一个带有 FIN 标志位的报文。
  5. 客户端收到 FIN 报文

    • 客户端收到服务端的 FIN 报文后,进入 TIME_WAIT 状态。
    • 这是为了确保服务端能够收到客户端最后一次的 ACK 报文。
  6. 客户端发送 ACK 报文

    • 客户端发送一个只带有 ACK 标志位的报文,确认收到了服务端的 FIN 报文。
  7. 连接关闭

    • 经过上述 4 次挥手过程后,TCP 连接就被完全关闭了。
    • 客户端和服务端都可以释放掉连接占用的系统资源。

需要注意的是,在 TIME_WAIT 状态下,客户端还会等待一段时间(通常为 2MSL,即2倍的最大报文生存时间)才会真正关闭连接。这是为了确保最后一个 ACK 报文能够顺利送达服务端。

整个连接关闭的过程遵循 TCP 四次挥手机制,确保连接能够被安全地关闭。这种设计能够最大限度地避免数据丢失和其他问题的发生。

没有 accept,能建立 TCP 连接吗?

可以的。

accpet 系统调用并不参与 TCP 三次握手过程,它只是负责从 TCP 全连接队列取出一个已经建立连接的 socket,用户层通过 accpet 系统调用拿到了已经建立连接的 socket,就可以对该 socket 进行读写操作了。在这里插入图片描述

介绍 TCP 的重传机制

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

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

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

超时时间应该设置为多少呢?
每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。

  1. 快速重传:接收方连续收到3个相同序号的数据包时,会立即发送 ACK 确认。
  2. SACK( Selective Acknowledgment), 选择性确认。在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将已收到的数据的信息发送给「发送方」,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。

如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK 信息发现只有 200~299 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。在这里插入图片描述
4. Duplicate SACK:使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。
非常感谢你进一步补充了 Duplicate SACK 的概念,这让我对 TCP 的 SACK 机制有了更加深入的理解。

Duplicate SACK 是 SACK 机制的一个延伸功能:

  1. 目的

    • 当接收方收到重复的数据块时,可以使用 Duplicate SACK 通知发送方。
    • 这样发送方就能知道哪些数据被重复接收了,避免不必要的重传。
  2. 工作原理

    • 接收方在 ACK 报文中,除了确认正常接收的数据块外,
    • 还会额外包含重复接收的数据块信息,形成 Duplicate SACK。
    • 发送方收到 Duplicate SACK 后,就知道这些数据块已经被重复接收。
  3. 优势

    • Duplicate SACK 可以进一步提高 TCP 传输的效率。
    • 避免发送方盲目地重传已经被接收的数据,减少网络开销。
    • 与普通 SACK 相比,Duplicate SACK 提供了更细致的反馈信息。

滑动窗口

TCP 的滑动窗口机制是一种非常重要的流量控制机制,它可以动态调整发送方的发送速率,以匹配接收方的接收能力。

  1. 滑动窗口概念

    • 滑动窗口是发送方维护的一个缓冲区,用于存储已发送但尚未收到确认的数据。
    • 窗口大小决定了发送方在任何时刻可以发送的最大数据量。
  2. 窗口大小的协商

    • 连接建立时,双方协商并确定初始窗口大小。
    • 之后接收方可以动态调整窗口大小,并在 ACK 报文中通知发送方。
  3. 窗口更新机制

    • 每收到一个 ACK,发送方就会将窗口向前滑动,腾出空间发送新的数据。
    • 如果发送方发现窗口大小变小了,就会相应地降低发送速率。
  4. 优势

    • 滑动窗口可以动态适应网络状况,避免发送方过度拥塞网络。
    • 它配合重传机制使用,可以确保数据可靠传输,同时提高传输效率。
  5. 实现细节

    • TCP 报头中包含发送窗口大小的字段,用于通知对端。
    • 接收方根据自身缓存情况动态调整窗口大小。
    • 发送方根据窗口大小来控制发送速率。

总的来说,TCP 的滑动窗口机制是一种精妙的流量控制手段,能够动态平衡发送方的发送能力和接收方的接收能力,确保 TCP 连接的高效稳定运行。

窗口大小由哪一方决定?

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

这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。

所以,通常窗口的大小是由接收方的窗口大小来决定的。

发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。

发送方的滑动窗口

在这里插入图片描述
在这里插入图片描述
在下图,当收到之前发送的数据 32-36 字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 52-56 字节又变成了可用窗口,那么后续也就可以发送 52~56 这 5 个字节的数据了。
在这里插入图片描述

程序是如何表示发送方的四个部分的呢?

TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。
在这里插入图片描述

接收方的滑动窗口

在这里插入图片描述

接收窗口和发送窗口的大小是相等的吗?

并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。

因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。

流量控制

发送方不能无脑的发数据给接收方,要考虑接收方处理能力。

如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端的浪费。

为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。

操作系统缓冲区与滑动窗口的关系

操作系统的缓冲区和 TCP 的滑动窗口机制之间确实存在着密切的关系。让我来详细阐述一下:

  1. 接收缓冲区与接收窗口

    • 接收方操作系统会为 TCP 连接维护一个接收缓冲区。
    • 接收窗口的大小就是根据这个接收缓冲区的剩余空间动态计算出来的。
    • 当缓冲区剩余空间变小时,接收窗口也会相应减小,通知发送方降低发送速率。
  2. 发送缓冲区与发送窗口

    • 发送方操作系统也会为 TCP 连接维护一个发送缓冲区。
    • 发送窗口大小由接收方通知,但实际上也受发送缓冲区剩余空间的限制。
    • 发送方不会将数据发送超出自身发送缓冲区的容量。
  3. 缓冲区大小的影响

    • 操作系统分配给 TCP 连接的缓冲区越大,滑动窗口就越大。
    • 较大的缓冲区可以提高 TCP 吞吐量,但也会占用更多系统资源。
    • 需要在性能和资源消耗之间权衡,合理配置缓冲区大小。
  4. 缓冲区管理策略

    • 操作系统会根据系统资源情况动态调整 TCP 连接的缓冲区大小。
    • 通过合理的缓冲区管理策略,可以提高 TCP 传输的整体效率。
  5. 协议层面的优化

    • TCP 的一些扩展机制,如 SACK、时间戳等,也会影响缓冲区的使用。
    • 这些机制能进一步优化 TCP 的传输性能,减少缓冲区的使用。

总之,操作系统的缓冲区管理和 TCP 的滑动窗口机制是密切相关的。合理配置缓冲区大小,并采用优化的缓冲区管理策略,可以显著提高 TCP 传输的效率和性能。这是 TCP 协议设计的一个重要方面。

窗口关闭

在前面我们都看到了,TCP 通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制。

如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。

窗口关闭潜在的危险:

接收方向发送方通告窗口大小时,是通过 ACK 报文来通告的。

那么,当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,那麻烦就大了。
在这里插入图片描述
这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不采取措施,这种相互等待的过程,会造成了死锁的现象。

TCP 是如何解决窗口关闭时,潜在的死锁现象呢?

TCP 在窗口关闭时确实会面临潜在的死锁问题,TCP 协议中采用了一些机制来避免这种情况发生。主要包括以下几个方面:

  1. 零窗口探测

    • 当接收方的接收窗口大小变为 0 时,接收方会在 ACK 报文中通知发送方。
    • 此时发送方会启动零窗口探测定时器,周期性地向接收方探测窗口大小。
  2. 零窗口探测重传

    • 如果在探测过程中,接收方的窗口大小一直为 0,发送方会重复发送探测报文。
    • 这样可以防止发送方一直处于等待状态,从而避免死锁的发生。
  3. 零窗口更新

    • 当接收方的缓冲区有空间时,就会在 ACK 报文中更新窗口大小。
    • 发送方收到这个窗口更新后,就可以立即恢复数据传输,避免了死锁。
  4. 最大报文段大小

    • 发送方会根据接收方的窗口大小,调整发送的最大报文段大小。
    • 这样可以降低因零窗口而导致的数据传输暂停时间。
  5. 被动关闭连接

    • 如果一方长时间收不到对方的窗口更新,可以主动关闭连接。
    • 这样可以及时避免死锁的发生,释放系统资源。

通过这些机制的结合使用,TCP 协议能够很好地应对窗口关闭时可能出现的死锁问题,确保数据传输的持续进行。这是 TCP 设计中值得注意的一个重要方面。

糊涂窗口综合症

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

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

要知道,我们的 TCP + IP 头有 40 个字节,为了传输那几个字节的数据,要搭上这么大的开销,这太不经济了。

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

解决措施

  1. 接收方:不要立即通知小窗口大小,而是等到窗口足够大时再通知。当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。等到接收方处理了一些数据后,窗口大小 >= MSS,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。
  2. 发送方:当接收到小窗口大小时,不要立即发送小数据块,而是等待直到窗口足够大。使用 Nagle 算法,该算法的思路是延时处理,只有满足下面两个条件中的任意一个条件,才可以发送数据:
  • 条件一:要等到窗口大小 >= MSS 并且 数据大小 >= MSS;
  • 条件二:收到之前发送数据的 ack 回包;

具体机制

  • 接收方:实现 Nagle 算法,缓存小数据直到窗口足够大时再发送。
  • 发送方:实现 SWS avoidance 算法,限制发送小数据块的条件。

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

即使 TCP 已经有了流量控制机制,为什么还需要额外的拥塞控制呢?这是因为流量控制和拥塞控制解决的是不同层面的问题。

  1. 流量控制的作用

    • 流量控制是为了匹配发送方的发送能力和接收方的接收能力。
    • 通过滑动窗口机制,发送方可以根据接收方的反馈动态调整发送速率。
    • 这可以确保接收方不会因为缓冲区溢出而丢失数据。
  2. 拥塞控制的必要性

    • 流量控制只能解决端到端的传输问题,但无法解决网络拥塞问题。
    • 即使接收方的接收能力足够,网络中也可能出现拥塞,导致大量数据丢失。
    • 如果没有拥塞控制,发送方会盲目地增加发送速率,进一步加剧网络拥塞。
  3. 拥塞控制的作用

    • 拥塞控制的目标是避免向网络中注入过多的数据,防止网络拥塞。
    • 它可以根据网络拥塞程度,动态调整发送方的发送速率。
    • 这样可以在网络状况恶化时主动降低发送速率,避免更严重的数据丢失。
  4. 拥塞控制的实现

    • 通过慢启动、拥塞避免、快速重传和快速恢复等算法实现。
    • 发送方可以根据丢包情况和 RTT 变化来判断网络拥塞程度。

总之,流量控制和拥塞控制是 TCP 可靠传输的两个关键机制。流量控制解决的是端到端的传输问题,而拥塞控制则解决的是网络层面的拥塞问题。两者相互配合,才能确保 TCP 在各种网络环境下都能保持高效稳定的传输性能。

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

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

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

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

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

拥塞窗口 cwnd 变化的规则:

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

慢启动

TCP 在刚建立连接完成后,首先是有个慢启动的过程,这个慢启动的意思就是一点一点的提高发送数据包的数量,如果一上来就发大量的数据,这不是给网络添堵吗?

慢启动的算法记住一个规则就行:当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。
在这里插入图片描述

那慢启动涨到什么时候是个头呢?

有一个叫慢启动门限 ssthresh (slow start threshold)状态变量。

  • 当 cwnd < ssthresh 时,使用慢启动算法。
  • 当 cwnd >= ssthresh 时,就会使用「拥塞避免算法」。

拥塞避免算法

工作原理:

  1. 目的

    • 拥塞避免算法的目的是防止网络拥塞的发生。
    • 它通过动态调整发送窗口的大小,控制数据发送的速率。
  2. 算法流程

    • 在连接建立初期,采用慢启动算法快速增大发送窗口。
    • 当窗口增大到一定阈值后,进入拥塞避免阶段。
    • 拥塞避免阶段,每收到一个 ACK,就将窗口大小增加 1/cwnd。
    • 这样可以线性地增加发送速率,避免突然增加导致网络拥塞。
  3. 拥塞阈值

    • 拥塞阈值是一个动态变化的参数,表示网络可以承受的最大发送窗口大小。
    • 当发送窗口大小超过拥塞阈值时,就会进入慢启动阶段。
    • 拥塞阈值的设置可以根据丢包情况和 RTT 变化来动态调整。
  4. 拥塞信号

    • 拥塞信号通常通过检测到数据包丢失来判断。
    • 接收方可以通过3个重复 ACK 或者超时重传来检测到丢包。
    • 一旦检测到丢包,就会触发拥塞控制机制。
  5. 优化方案

    • TCP 还有一些优化策略,如快速重传和快速恢复算法。
    • 这些算法可以更快地检测和恢复网络拥塞,提高传输效率。

总之,拥塞避免算法是 TCP 实现可靠传输的一个关键机制。它可以根据网络状况动态调整发送速率,避免网络拥塞的发生,确保 TCP 连接的稳定高效运行。在这里插入图片描述

拥塞发生

当网络出现拥塞时,就会导致数据包丢失,此时 TCP 需要通过重传机制来弥补数据的丢失。主要有以下两种重传机制:

  1. 超时重传(Retransmission Timeout, RTO)

    • 发送方在发送数据后会启动重传定时器。
    • 如果在定时器超时前没有收到对应的确认,就会触发超时重传。
    • 超时时间的设置是一个动态值,会根据丢包情况和 RTT 变化而自适应调整。
  2. 快速重传(Fast Retransmit)

    • 接收方收到乱序的数据包时,会重复发送之前的 ACK 确认。
    • 发送方连续收到 3 个相同的 ACK 确认,就会触发快速重传。
    • 此时不需要等待超时,可以立即重传丢失的数据包。

这两种重传机制相互配合使用,可以提高 TCP 的传输效率:

  • 超时重传可以应对网络拥塞导致的大量数据包丢失。
  • 快速重传则可以更快地检测到个别数据包的丢失。

通过这种方式,TCP 可以更好地应对各种网络环境下的数据包丢失问题,确保数据能够可靠地传输到目的地。这也是 TCP 成为事实上的标准传输层协议的一个重要原因。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 每日OJ_牛客CM26 二进制插入
  • 【模型】LightGBM
  • OCC BRepOffsetAPI_ThruSections使用
  • UVM_ERROR: SEQREQZMB解决方案
  • <设计模式> 工厂模式
  • 【排序算法(二)】——冒泡排序、快速排序和归并排序—>深层解析
  • Photos框架 - 自定义媒体选择器(UI列表)
  • 您需要了解的有关 5G 的一切。
  • 大数据之Oracle同步Doris数据不一致问题
  • C#用Aspose.Cells导出Excel,.NET导出Excel
  • Dockerfile自定义镜像
  • 搜索引擎项目(四)
  • ubuntu20.04.6 安装Skywalking 10.0.1
  • Kylin系列(一):入门与深入解析(大数据分析)
  • RVC-AI声音克隆-你的声音不再是唯一
  • bootstrap创建登录注册页面
  • Java程序员幽默爆笑锦集
  • js写一个简单的选项卡
  • python3 使用 asyncio 代替线程
  • springMvc学习笔记(2)
  • Stream流与Lambda表达式(三) 静态工厂类Collectors
  • Vue.js 移动端适配之 vw 解决方案
  • 检测对象或数组
  • 将回调地狱按在地上摩擦的Promise
  • 看图轻松理解数据结构与算法系列(基于数组的栈)
  • 前端临床手札——文件上传
  • 前端面试总结(at, md)
  • -- 数据结构 顺序表 --Java
  • 无服务器化是企业 IT 架构的未来吗?
  • ​​​​​​​​​​​​​​Γ函数
  • ​批处理文件中的errorlevel用法
  • ‌分布式计算技术与复杂算法优化:‌现代数据处理的基石
  • #!/usr/bin/python与#!/usr/bin/env python的区别
  • #define、const、typedef的差别
  • #宝哥教你#查看jquery绑定的事件函数
  • #我与Java虚拟机的故事#连载09:面试大厂逃不过的JVM
  • (2)STM32单片机上位机
  • (8)Linux使用C语言读取proc/stat等cpu使用数据
  • (arch)linux 转换文件编码格式
  • (html5)在移动端input输入搜索项后 输入法下面为什么不想百度那样出现前往? 而我的出现的是换行...
  • (区间dp) (经典例题) 石子合并
  • (转) Android中ViewStub组件使用
  • (转)创业的注意事项
  • (转)拼包函数及网络封包的异常处理(含代码)
  • .NET Compact Framework 3.5 支持 WCF 的子集
  • .net core docker部署教程和细节问题
  • .NET 中小心嵌套等待的 Task,它可能会耗尽你线程池的现有资源,出现类似死锁的情况
  • .NET/C# 中你可以在代码中写多个 Main 函数,然后按需要随时切换
  • .NET/MSBuild 中的发布路径在哪里呢?如何在扩展编译的时候修改发布路径中的文件呢?
  • ::前边啥也没有
  • @property括号内属性讲解
  • [001-03-007].第07节:Redis中的事务
  • [100天算法】-二叉树剪枝(day 48)
  • [4]CUDA中的向量计算与并行通信模式
  • [AIGC] 解题神器:Python中常用的高级数据结构