前言
此处是本人基于小林 coding 等网站进行的笔记,主要目的是在记笔记的过程中帮自己记忆。
TCP/IP 网络模型
应用层 (应用层协议 http,websocket 等) 传输层 (TCP/UDP):负责端到端通信 网络层 (IP 层):负责网络包的封装、分片、路由、转发 网络接口层:负责网络包在物理网络里的传输
Linux 网络是如何收发网络包的(待完善)
此部分博主要复习一下之前阅读过的《深入理解 linux 网络》,现在的版本是直接从小林 coding 上总结的,缺乏细节。
Linux 是如何收到网络包的
- 当网卡收到一个网络包,会通过 DMA 将网络包写入 ring buffer。然后触发硬中断,硬中断会暂时屏蔽终端,表示已经知道内存中有数据了,网卡在此期间再收到数据包直接写入内存就可以了。然后硬中断还会发起软中断,然后停止屏蔽终端。
- 内核中的 ksoftirqd 线程专门负责软中断的处理,会轮询处理数据。ksoftirq 线程会从 ring buffer 中获取一个数据帧,用 sk_buff 表示,交给网络协议栈逐层处理。
- 网络包首先会进入网络接口层,在这一层检查报文的合法性,然后找出网络包的协议(IPV4、IPV6),再去掉帧头帧尾交给网络层。
- 到了网络层,取出 IP 包,判断下一步是要转发出去还是交给上层。当确认是要交给本机上层后,就会从 IP 头里看看上一层协议的类型是 TCP 还是 UDP,接着去掉 IP 头,然后交给传输层。
- 传输层取出 TCP 或 UDP 头,根据四元组「源 IP、源端口、目的 IP、目的端口」作为标识,找出对应的 Socket。并把数据放到 Socket 的接收缓冲区。
- 应用层调用 Socket 接口,将内核的 Socket 接收缓冲区的数据拷贝到应用层缓冲区,然后唤醒用户进程。
Linux 是如何发送网络包的
- 应用程序会调用 Socket 发送数据包的接口。应用程序会从用户态陷入到内核态的 Socket 层,然后内核会申请一个内核态的 sk_buff 内存,将用户数据拷贝到 sk_buff,并将其加入到发送缓冲区。
- 网络协议栈从 Socket 发送缓冲区取下 sk_buff,并按照 TCP/IP 协议栈从上到下逐层处理。
- 如果是 TCP 协议,需要先 拷贝一个新的 sk_buff 副本,因为 sk_buff 后续在调用网络层,最后到达网卡发送完成的时候,这个 sk_buff 会释放掉。而 TCP 协议是支持丢失重传的,在收到对方的 ACK 之前,这个 sk_buff 不能被删除。
- 接着对 sk_buff 填充 TCP 头。sk_buff 头部预留了协议栈的空间,通过前移 data 指针来逐层填充协议头。
- 然后数据包交给网络层,网络层会选取路由(确认下一跳的 ip)、填充 IP 头、netfilter 过滤,对超过 MTU 大小的数据包进行分片。接着交给网络接口层处理。
- 网络接口层通过 ARP 协议获得下一跳的 MAC 地址,然后对 sk_buff 填充帧头和帧尾,然后将 sk_buff 放到网卡的发送队列中。
- 然后会触发软中断告诉网卡区驱动,有新数据包要发送。驱动程序会从发送队列中读取 sk_buff,将这个 sk_buff 挂到 RingBuffer 中,然后将 sk_buff 映射到网卡可访问的内存 DMA 区域,最后触发真实的发送。
- 当发送完成的时候,网卡会触发硬中断释放 sk_buff 内存和 ring buffer 的内存。最后,当收到这个报文的 ACK 后,传输层就会释放原始的 sk_buff。
发送网络数据涉及几次内存拷贝?
- 调用发送数据的系统调用时,用户数据会拷贝到 sk_buff 内存
- 使用 TCP 协议时,从传输层进入网络层的时候,会将 sk_buff 拷贝一个副本用于发送
- 当 IP 层发送发现 sk_buff 大于 MTU 时会再申请额外的 sk_buff,将原来的 sk_buff 拷贝为多个小的 sk_buff。
键入网址后发生的事情
- 浏览器解析 URL,生成发送给 web 服务器的请求信息
- 查询 DNS,获取域名的 IP 地址(DNS 有缓存)
- 获取 IP 后要建立 TCP 连接,经过三次握手后,就建立好连接,然后将要发送的请求信息封装为 TCP 包,TCP 负责将数据可靠交付给服务端。如果信息过长,需要进行分包。TCP 头部需要填写服务端和客户端的端口号。封装好后的包会交给下一层 IP 层。(在实际发送数据包前,如果是 https,还需要进行 tls 四次握手,在 tcp 基础上建立 tls 连接)
- IP 层将拿到的 TCP 包再封装为 IP 包,需要填写源地址 IP 和目的地址 IP,目的地址 IP 通过 DNS 解析得到。源 IP 在本机有多块网卡的情况下,使用路由表规则判断用哪一个网卡作为源地址 IP。
- 有了 IP 头部后,网络包还需要在 IP 头前面加上 MAC 头部。发送方的 MAC 头部直接从网卡的 ROM 中读取,接收方的 MAC 地址通过查路由表得到下一跳 IP,然后通过 ARP 协议在局域网内广播,获取下一跳 IP 对应的 MAC 地址(ARP 也有缓存)
- 封装好 MAC 包后交给网卡,网卡负责将数字信息实际转换为电信号发出去。
- 发送的包途径到交换机,交换机工作在二层,没有自己的 MAC 地址,它根据自己内部的 MAC 表决定发送到哪个网口,如果没有记录,则将该包广播到除接收到 MAC 帧端口的所有端口。
- 途径路由器的话,路由器是三层设备,会拆开 MAC 包,根据 IP 头部的目的 IP 查询路由表,判断转发目标,然后重新封装新的 MAC 包发送。
- 最终到达服务器后,开始按找打包顺序的逆顺序,一层一层解包:MAC 包->IP 包->TCP 包->HTTP 数据,拿到数据后,根据请求生成对应的回复信息,然后重新经历封包过程发送给客户端。
- 当客户端要离开时,向服务器发起 TCP 的四次挥手,断开连接。
HTTP 协议
基本概念
HTTP(HyperText Transfer Protocol)超文本传输协议。
GET 和 POST 的区别:
- GET 语义是从服务器获取指定的资源,参数一般写在 URL 中,参数格式为 ASSCI 码字符,浏览器对 URL 长度有限制。从语义上来讲 GET 是幂等的,因为它是请求资源的操作,是只读的,可以对 GET 请求做缓存。
- POST 语义是根据请求负荷对指定资源做出处理,携带数据位置一般在报文 body 中,数据格式可以是任意的,只要和服务端协商好即可,数据大小一般没有限制。从语义上讲,POST 是新增或修改数据,所以不是幂等的。
HTTP 缓存技术
强制缓存 强制缓存指的是只要浏览器没有判断缓存过期,则直接使用浏览器本地缓存,决定是否使用缓存的主动性在浏览器。
实现方式是
Cache-Control
和Expires
两个字段。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)
- 优点
- 简单 报文格式:Header+Body,头部信息是 kv 形式,简单。
- 灵活和易于扩展 Http 协议里的 URI/URL、状态码、头字段等每个组成都可以自定义和扩充。且 HTTP 是应用层协议,下层可以随意变化。
- 应用广泛和跨平台
- 缺点
- 无状态(双刃剑) 服务器不需要记忆 HTTP 状态,能减轻服务器负担。但由于没有记忆,完成有关联性的操作时会很麻烦。比较简单的解决方式是 Cookie 技术。
- 明文 & 不安全 由于明文带来的不安全问题,内容可能会被窃听。不验证通信方的身份,有可能遭遇伪装。无法证明报文的完整性,报文可能已遭篡改。引入 SSL/TLS 层后,HTTPS 没有明文和不安全的问题了。
- 性能特点
- 长连接 HTTP1.1 提出了长连接的通信方式,在一定时间内,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。
- 管道网络传输 在同一个 TCP 连接里客户端可以发起多个请求,不必等一个请求的回复就可以发出第二个请求,可以减少整体的响应时间。但是服务端必须按照接收请求的顺序发送响应。(所以说 HTTP1.1 解决了请求的对头阻塞,但没有解决响应的对头阻塞)(HTTP1.1 默认不开启管道传输,并且浏览器基本都不支持)
- 队头阻塞(不开启管道传输) 请求必须接收到响应才能发起第二个请求,因此会导致队头阻塞。
HTTP 协议演变
HTTP/1.0 -> HTTP/1.1: 改进:
- 长连接
- 支持管道网络传输
仍然存在缺陷:
- 请求未经压缩就发送
- 发送冗长的首部
- 请求是顺序响应的,如果前一个连接响应慢,会阻塞后续所有连接(对头阻塞)
- 没有请求优先级控制
- 请求只能从客户端开始,服务端只能被动响应
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 请求包都必须等待这个丢了的包重传了才能继续传递。
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 握手过程:
- TLS 第一次握手,客户端首先发送一个「client hello」消息,包含客户端使用的 TLS 版本号、支持的密码套件列表,以及生成的随机数。
- TLS 第二次握手,服务端受到客户端的 hello,会返回 server hello,包含服务器确认选择的 TLS 版本号以及一个服务端生成的随机数和合适的密码套件。然后服务端把自己的证书也发给客户端,证明自己的身份。证书发送后会发送「server key exchange」消息,确定椭圆曲线和椭圆曲线公钥并公开给客户端,同时生成随机数作为椭圆曲线私钥保留在本地。然后发送「server hello done」消息。
- TLS 第三次握手,客户端对服务器证书进行校验,确保合法;然后生成一个椭圆曲线私钥保留再本地,然后生成客户端的椭圆曲线公钥用「client key exchange」消息发给服务端。至此,双方都拥有对方的椭圆曲线公钥,自己的椭圆曲线私钥、椭圆曲线的基点 G。然后双方基于此信息计算出点(x,y),x 坐标值双方都一样,最终的回话密钥是「客户端随机数+服务端随机数+x」三个材料生成的。算好回话密钥后,客户端发送「change cipher spec」告知服务器改用对称加密通信;接着客户端会发送之前数据的摘要,再用对称密钥加密一下,给服务端用于验证。
- TLS 第四次握手,服务端同样发送「change cipher spec」和摘要验证包(「encrypted handshake message」,如果双方验证加密和揭秘没问题,那么握手正式完成。
TLS1.3: TLS1.3 在发送「client hello」的同时会发送自己支持的椭圆曲线和对应的公钥,服务端收到后选定一个椭圆曲线,并带上服务端的椭圆曲线公钥,经过 1 个 RTT,双方就有生成会话密钥的材料了,于是客户端计算出会话密钥,就可以进行加密传输了。
此外,TLS1.3 还取消了很多古老且不安全的密码套件,避免中间人利用降级攻击破解密文。
会话重用(待补充)