前言

此处是本人基于小林 coding 等网站进行的笔记,主要目的是在记笔记的过程中帮自己记忆。

TCP/IP 网络模型

应用层 (应用层协议 http,websocket 等) 传输层 (TCP/UDP):负责端到端通信 网络层 (IP 层):负责网络包的封装、分片、路由、转发 网络接口层:负责网络包在物理网络里的传输

Linux 网络是如何收发网络包的(待完善)

此部分博主要复习一下之前阅读过的《深入理解 linux 网络》,现在的版本是直接从小林 coding 上总结的,缺乏细节。

Linux 是如何收到网络包的

  1. 当网卡收到一个网络包,会通过 DMA 将网络包写入 ring buffer。然后触发硬中断,硬中断会暂时屏蔽终端,表示已经知道内存中有数据了,网卡在此期间再收到数据包直接写入内存就可以了。然后硬中断还会发起软中断,然后停止屏蔽终端。
  2. 内核中的 ksoftirqd 线程专门负责软中断的处理,会轮询处理数据。ksoftirq 线程会从 ring buffer 中获取一个数据帧,用 sk_buff 表示,交给网络协议栈逐层处理。
  3. 网络包首先会进入网络接口层,在这一层检查报文的合法性,然后找出网络包的协议(IPV4、IPV6),再去掉帧头帧尾交给网络层。
  4. 到了网络层,取出 IP 包,判断下一步是要转发出去还是交给上层。当确认是要交给本机上层后,就会从 IP 头里看看上一层协议的类型是 TCP 还是 UDP,接着去掉 IP 头,然后交给传输层。
  5. 传输层取出 TCP 或 UDP 头,根据四元组「源 IP、源端口、目的 IP、目的端口」作为标识,找出对应的 Socket。并把数据放到 Socket 的接收缓冲区。
  6. 应用层调用 Socket 接口,将内核的 Socket 接收缓冲区的数据拷贝到应用层缓冲区,然后唤醒用户进程。

Linux 是如何发送网络包的

  1. 应用程序会调用 Socket 发送数据包的接口。应用程序会从用户态陷入到内核态的 Socket 层,然后内核会申请一个内核态的 sk_buff 内存,将用户数据拷贝到 sk_buff,并将其加入到发送缓冲区
  2. 网络协议栈从 Socket 发送缓冲区取下 sk_buff,并按照 TCP/IP 协议栈从上到下逐层处理。
  3. 如果是 TCP 协议,需要先 拷贝一个新的 sk_buff 副本,因为 sk_buff 后续在调用网络层,最后到达网卡发送完成的时候,这个 sk_buff 会释放掉。而 TCP 协议是支持丢失重传的,在收到对方的 ACK 之前,这个 sk_buff 不能被删除。
  4. 接着对 sk_buff 填充 TCP 头。sk_buff 头部预留了协议栈的空间,通过前移 data 指针来逐层填充协议头。
  5. 然后数据包交给网络层,网络层会选取路由(确认下一跳的 ip)、填充 IP 头、netfilter 过滤,对超过 MTU 大小的数据包进行分片。接着交给网络接口层处理。
  6. 网络接口层通过 ARP 协议获得下一跳的 MAC 地址,然后对 sk_buff 填充帧头和帧尾,然后将 sk_buff 放到网卡的发送队列中。
  7. 然后会触发软中断告诉网卡区驱动,有新数据包要发送。驱动程序会从发送队列中读取 sk_buff,将这个 sk_buff 挂到 RingBuffer 中,然后将 sk_buff 映射到网卡可访问的内存 DMA 区域,最后触发真实的发送。
  8. 当发送完成的时候,网卡会触发硬中断释放 sk_buff 内存和 ring buffer 的内存。最后,当收到这个报文的 ACK 后,传输层就会释放原始的 sk_buff。

发送网络数据涉及几次内存拷贝?

  1. 调用发送数据的系统调用时,用户数据会拷贝到 sk_buff 内存
  2. 使用 TCP 协议时,从传输层进入网络层的时候,会将 sk_buff 拷贝一个副本用于发送
  3. 当 IP 层发送发现 sk_buff 大于 MTU 时会再申请额外的 sk_buff,将原来的 sk_buff 拷贝为多个小的 sk_buff。

键入网址后发生的事情

  1. 浏览器解析 URL,生成发送给 web 服务器的请求信息
  2. 查询 DNS,获取域名的 IP 地址(DNS 有缓存)
  3. 获取 IP 后要建立 TCP 连接,经过三次握手后,就建立好连接,然后将要发送的请求信息封装为 TCP 包,TCP 负责将数据可靠交付给服务端。如果信息过长,需要进行分包。TCP 头部需要填写服务端和客户端的端口号。封装好后的包会交给下一层 IP 层。(在实际发送数据包前,如果是 https,还需要进行 tls 四次握手,在 tcp 基础上建立 tls 连接)
  4. IP 层将拿到的 TCP 包再封装为 IP 包,需要填写源地址 IP 和目的地址 IP,目的地址 IP 通过 DNS 解析得到。源 IP 在本机有多块网卡的情况下,使用路由表规则判断用哪一个网卡作为源地址 IP。
  5. 有了 IP 头部后,网络包还需要在 IP 头前面加上 MAC 头部。发送方的 MAC 头部直接从网卡的 ROM 中读取,接收方的 MAC 地址通过查路由表得到下一跳 IP,然后通过 ARP 协议在局域网内广播,获取下一跳 IP 对应的 MAC 地址(ARP 也有缓存)
  6. 封装好 MAC 包后交给网卡,网卡负责将数字信息实际转换为电信号发出去。
  7. 发送的包途径到交换机,交换机工作在二层,没有自己的 MAC 地址,它根据自己内部的 MAC 表决定发送到哪个网口,如果没有记录,则将该包广播到除接收到 MAC 帧端口的所有端口。
  8. 途径路由器的话,路由器是三层设备,会拆开 MAC 包,根据 IP 头部的目的 IP 查询路由表,判断转发目标,然后重新封装新的 MAC 包发送。
  9. 最终到达服务器后,开始按找打包顺序的逆顺序,一层一层解包:MAC 包->IP 包->TCP 包->HTTP 数据,拿到数据后,根据请求生成对应的回复信息,然后重新经历封包过程发送给客户端。
  10. 当客户端要离开时,向服务器发起 TCP 的四次挥手,断开连接。

HTTP 协议

基本概念

  1. HTTP(HyperText Transfer Protocol)超文本传输协议。

  2. GET 和 POST 的区别:

    • GET 语义是从服务器获取指定的资源,参数一般写在 URL 中,参数格式为 ASSCI 码字符,浏览器对 URL 长度有限制。从语义上来讲 GET 是幂等的,因为它是请求资源的操作,是只读的,可以对 GET 请求做缓存。
    • POST 语义是根据请求负荷对指定资源做出处理,携带数据位置一般在报文 body 中,数据格式可以是任意的,只要和服务端协商好即可,数据大小一般没有限制。从语义上讲,POST 是新增或修改数据,所以不是幂等的。
  3. HTTP 缓存技术

    • 强制缓存 强制缓存指的是只要浏览器没有判断缓存过期,则直接使用浏览器本地缓存,决定是否使用缓存的主动性在浏览器。

      实现方式是Cache-ControlExpires两个字段。Cache-Control字段优先级高于Expires。 流程:

      • 浏览器第一次请求服务器时,服务器返回资源和Cache-Control的过期时间大小;
      • 浏览器在此请求时,计算Cache-Control是否过期,若没有则使用缓存,否则请求服务器。
      • 服务器再次收到请求后,会再次更新 Respnse 头部的 Cache-Control。
    • 协商缓存 协商缓存指的是客户端与服务端协商之后,通过协商结果来判断是否使用本地缓存。 实现方式:

      • 基于请求头中的If-Modified-Since字段与响应头中的Last-Modified字段实现。If-Modified-Since字段带上上次 Last-Modified 的时间,服务端接收到请求后,将字段值与被请求资源的Last-Modified字段对比,如果资源被改过了返回最新资源,HTTP 200 OK。否则响应 HTTP 304 走缓存。
      • 基于请求头中的If-None-Match字段与响应头部中的ETag字段。ETag用于唯一标识响应资源。当资源过期时,浏览器将If-None-Match值设置为上次返回的Etag值,服务器收到进行对比,如果资源没变化返回 304,变化了返回 200. 如果两种方式都用了,则ETag优先级更高,因为ETag唯一标识资源。而Last-Modified基于时间。

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

HTTP 特性(http1.1)

  1. 优点
    1. 简单 报文格式:Header+Body,头部信息是 kv 形式,简单。
    2. 灵活和易于扩展 Http 协议里的 URI/URL、状态码、头字段等每个组成都可以自定义和扩充。且 HTTP 是应用层协议,下层可以随意变化。
    3. 应用广泛和跨平台
  2. 缺点
    1. 无状态(双刃剑) 服务器不需要记忆 HTTP 状态,能减轻服务器负担。但由于没有记忆,完成有关联性的操作时会很麻烦。比较简单的解决方式是 Cookie 技术。
    2. 明文 & 不安全 由于明文带来的不安全问题,内容可能会被窃听。不验证通信方的身份,有可能遭遇伪装。无法证明报文的完整性,报文可能已遭篡改。引入 SSL/TLS 层后,HTTPS 没有明文和不安全的问题了。
  3. 性能特点
    1. 长连接 HTTP1.1 提出了长连接的通信方式,在一定时间内,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。
    2. 管道网络传输 在同一个 TCP 连接里客户端可以发起多个请求,不必等一个请求的回复就可以发出第二个请求,可以减少整体的响应时间。但是服务端必须按照接收请求的顺序发送响应。(所以说 HTTP1.1 解决了请求的对头阻塞,但没有解决响应的对头阻塞)(HTTP1.1 默认不开启管道传输,并且浏览器基本都不支持)
    3. 队头阻塞(不开启管道传输) 请求必须接收到响应才能发起第二个请求,因此会导致队头阻塞。

HTTP 协议演变

  1. HTTP/1.0 -> HTTP/1.1: 改进:

    • 长连接
    • 支持管道网络传输

    仍然存在缺陷:

    • 请求未经压缩就发送
    • 发送冗长的首部
    • 请求是顺序响应的,如果前一个连接响应慢,会阻塞后续所有连接(对头阻塞)
    • 没有请求优先级控制
    • 请求只能从客户端开始,服务端只能被动响应
  2. HTTP/1.1 -> HTTP/2.0 改进:

    • 头部压缩 HTTP/2 会压缩头,如果同时发出多个请求,它们的头时一样或相似的,协议会帮忙消除重复的部分(HPACK 算法)。HPACK 算法会在客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号。
    • 二进制格式 HTTP/2 采用了 2 进制格式,头信息和数据体都是二进制,并且统称为帧:头信息帧和数据帧。计算机收到报文后无需再将明文的报文转换为二进制,可以直接解析,提高了数据传输的效率。
    • 并发传输 HTTP/2.0 引入了 steam 的概念,多个 stream 复用一条 TCP 连接。不同 steam 有 steam id 来区分,不同 stream 间的信息可以乱序发送,并行处理。
    • 服务器主动推送资源 HTTP/2.0 服务器不再是被动响应,可以主动向客户端发送消息。客户端和服务器都可以建立 stream,客户端的 stream id 必须是奇数,服务器建立的必须是偶数。例子:客户端请求 HTML 文件,HTML 可能还依赖 CSS 来渲染画面,此时如果有服务器主动推送功能,客户端请求 HTML 后,服务端就可以主动推送 CSS 文件,减少消息传递次数。

    缺陷: HTTP/2.0 是基于 TCP 的,由于 TCP 的重传机制,导致如果在 TCP 连接上丢失一个包,后续所有的 HTTP 请求包都必须等待这个丢了的包重传了才能继续传递。

  3. HTTP/2.0 -> HTTP/3.0 HTTP/3.0 把下层的协议改成了 UDP,基于 UDP 的 QUIC 协议实现可靠传输。 改进:

    • 无队头阻塞 QUIC 也有 stream 的概念。当某个流发生丢包时,只会阻塞这个流,其他流不会受到影响,因此不存在队头阻塞的问题。
    • 更快的连接建立 在 HTTP/1 和 HTTP/2 协议里,TCP 和 TLS 是分层的,分别属于内核的传输层和 openssl 库实现的表示层,需要先进行 TCP 握手再 TLS 握手。 而 HTTP/3 的 QUIC 协议握手只需要 1RTT,确认双方的连接 ID。并且 QUIC 内部包含了 TLS,它在自己的帧会携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS/1.3,因此仅需 1RTT 就可以同时完成建立连接与密钥协商。而且在第二次连接时,握手信息和数据包可以一起发送,达到 0-RTT 的效果。
    • 连接迁移 TCP 基于「源地址 IP、源端口、目的地址 IP、目的地址端口」四元组确定一条 TCP 连接。因此当移动设备的网络从 4G 切换到 WI-FI 时,IP 地址变化会导致 TCP 连接重新建立,用户会感觉网络突然卡顿。 QUIC 协议是通过连接 ID 来标记通信的两个端点,在移动设备发生 IP 地址变化时,仍可以通过保存的连接信息(连接 ID、TLS 密钥)无缝复用原连接,达到连接迁移的功能。 缺点: 普及缓慢,QUIC 基于 UDP 实现,有的网络设备会丢弃 UDP 包,如果网络设备无法识别 QUIC,就会把它当作 UDP 包丢弃。

HTTPS

HTTPS 常用的密钥交换算法有 RSA 和 ECDHE 算法。RSA 不具备前向安全性,因此现在广泛使用的是 ECDHE 算法。

  • ECDHE 握手过程:

    1. TLS 第一次握手,客户端首先发送一个「client hello」消息,包含客户端使用的 TLS 版本号、支持的密码套件列表,以及生成的随机数。
    2. TLS 第二次握手,服务端受到客户端的 hello,会返回 server hello,包含服务器确认选择的 TLS 版本号以及一个服务端生成的随机数和合适的密码套件。然后服务端把自己的证书也发给客户端,证明自己的身份。证书发送后会发送「server key exchange」消息,确定椭圆曲线和椭圆曲线公钥并公开给客户端,同时生成随机数作为椭圆曲线私钥保留在本地。然后发送「server hello done」消息。
    3. TLS 第三次握手,客户端对服务器证书进行校验,确保合法;然后生成一个椭圆曲线私钥保留再本地,然后生成客户端的椭圆曲线公钥用「client key exchange」消息发给服务端。至此,双方都拥有对方的椭圆曲线公钥,自己的椭圆曲线私钥、椭圆曲线的基点 G。然后双方基于此信息计算出点(x,y),x 坐标值双方都一样,最终的回话密钥是「客户端随机数+服务端随机数+x」三个材料生成的。算好回话密钥后,客户端发送「change cipher spec」告知服务器改用对称加密通信;接着客户端会发送之前数据的摘要,再用对称密钥加密一下,给服务端用于验证。
    4. TLS 第四次握手,服务端同样发送「change cipher spec」和摘要验证包(「encrypted handshake message」,如果双方验证加密和揭秘没问题,那么握手正式完成。
  • TLS1.3: TLS1.3 在发送「client hello」的同时会发送自己支持的椭圆曲线和对应的公钥,服务端收到后选定一个椭圆曲线,并带上服务端的椭圆曲线公钥,经过 1 个 RTT,双方就有生成会话密钥的材料了,于是客户端计算出会话密钥,就可以进行加密传输了。

    此外,TLS1.3 还取消了很多古老且不安全的密码套件,避免中间人利用降级攻击破解密文。

  • 会话重用(待补充)