网络部署与优化| 豆包MarsCode AI 刷题

193 阅读12分钟

一、网络交互

1、网络接入 - 互联网
  • 互联网接入是指终端设备通过某种方式接入到全球范围的网络。常见的接入方式包括通过拨号、光纤、无线网络(如Wi-Fi、4G/5G)等。
  • 公网IP地址:通过互联网接入时,分配给每台终端设备的IP地址。公网IP是唯一的,可以在全球范围内识别设备。
2、网络接入 - 路由
  • 跨网段的路由配置方式(默认路由): 路由器根据目标IP地址判断数据包应走哪个路由。如果目标地址不在本地网段,数据包会通过默认路由转发。 默认路由是当路由器无法根据目标地址找到精确匹配的路由时,使用的备用路由。

    示例:

    • 路由器的路由表通常有两类信息:
      1. 静态路由:手动配置的路由规则。
      2. 动态路由:通过路由协议(如OSPF、BGP)自动学习到的路由信息。
  • 路由不一定是对称的: 在一些网络配置中,数据包的发送和接收路径可能不同,这种情况称为“非对称路由”。 例如,发送数据包时通过一条路由,接收数据包时可能经过另一条路由。通常,这在负载均衡和高可用性环境中较为常见。

3、网络接入 - ARP协议
  • ARP协议(地址解析协议)用于将IP地址映射为物理MAC地址。
  • 逻辑同网段才可以发送ARP: ARP只能用于同一网络(子网)内的设备之间通信。如果目标设备不在同一子网,数据包会通过路由器转发,ARP请求不会跨越路由器。
  • ARP请求广播,ARP应答单播
    • ARP请求是广播的方式,向局域网内的所有设备询问目标IP的MAC地址。
    • ARP应答是单播的方式,仅将MAC地址返回给请求的设备。
  • 免费ARP和ARP代理
    • 免费ARP:某些设备会主动发送ARP请求来“更新”其他设备的ARP缓存,以保持网络连通性。
    • ARP代理:当设备在不同子网间转发ARP请求时,可能需要ARP代理功能。这通常在NAT或VPN环境中出现,允许不同子网的设备之间互相通信。
4、网络接入 - IP协议
  • IP协议
    • IP地址是网络设备在IP协议下的唯一标识符。每个IP地址在全球范围内都是唯一的。
    • 生产路由时确定且不可更改:在配置网络时,IP地址在设备间的路由信息中已被确定,不能随意更改,特别是公网IP。
  • MAC地址不能代替IP地址
    • MAC地址是硬件层地址,用于在局域网内标识设备,无法在网络层(如互联网)中标识设备。MAC地址是固定的,而IP地址可以变化(动态分配或静态配置)。
  • IPv4不够用,一般如何解决: 由于IPv4地址池的有限性,IPv6逐渐被引入以解决地址不足的问题。IPv6提供了充足的地址空间,能够为地球上的每一粒沙子都分配独立的IP地址。
    • IPv4 地址耗尽的应对方案
      1. NAT:通过网络地址转换技术使多个设备共享一个公网IP地址。
      2. 私有地址和NAT:在内部网络使用私有IP地址(如192.168.x.x),通过NAT与外部互联网通信。
5、网络接入 - NAT协议
  • 家用路由器如何上网? 家用路由器通常通过NAT(网络地址转换)技术来实现多个设备共享一个公网IP地址。每个家用设备会使用私有IP地址,而路由器将其转换为公网IP进行通信,外部设备只能看到路由器的公网IP。

  • 多个内网客户端访问同一目标地址+端口,源端口一样,冲突了怎么办? 在NAT环境中,如果多个内网设备使用相同的源端口访问外部地址,会导致源端口冲突。为了解决这个问题,路由器会使用端口映射(Port Address Translation, PAT)来确保每个内网设备的连接都有唯一的标识。通过改变源端口号来避免冲突。

    例如:

    • 内网设备1使用源端口1234,内网设备2使用源端口1234,路由器会将它们映射到公网IP的不同端口,例如公网IP: 80.0.0.1:10001 和 80.0.0.1:10002。

二、网络传输

1、DNS解析
  • 基于UDP协议:DNS请求通常通过 UDP 发送,这种传输方式较快,但不可靠。

    • 如何使用UDP建立可靠协议(如QUIC)

      • 发包方式:QUIC通过流控制和重传机制增强可靠性,每个数据包都带有序列号,用于检查丢包情况。
      • 每次发多少:QUIC根据网络状况动态调整发送的数据量,以防止拥塞。
      • 如何检测丢包:通过序列号检查,若接收方未收到某个序列号的数据包,会通知发送方重传。
      • 权衡效率与质量:通过流量控制和拥塞控制,动态调节数据发送速率,确保传输质量。
  • 递归解析:DNS解析时,若本地DNS服务器无法解析目标域名,会递归地向其他DNS服务器查询,直到找到目标IP地址。

2、TCP连接
  • 三次握手

image-20241114201815565.png

  • 上图片中,可以看到一个完整的三次握手过程:

    1. 第一个数据包(SYN)从源端口 10371 向目标端口 443(通常是 HTTPS)发送,表示请求建立连接,此时Seq=0,Ack=0。
    2. 第二个数据包(SYN, ACK)从目标端口 443 返回,确认连接请求,Seq=0,Ack=1。
    3. 第三个数据包(ACK)从源端口 10371 返回,表示连接已建立,Seq=1,Ack=1。
    • Seq:表示数据包的序列号,用于确认数据包的顺序。
    • Ack:表示期望接收的下一个 Seq 值,即对方的确认号。
  • 拔掉网线并不会直接断开连接:TCP连接在没有收到对方的FIN或RST包之前不会立即断开,会等待超时后再关闭连接。

  • TIME_WAIT状态:主动关闭连接的一方进入TIME_WAIT状态,确保所有数据包都正确收到,防止旧连接的数据干扰新连接。

  • 丢包怎么办:TCP会自动重传丢失的数据包,通过确认号(ACK)机制来判断哪些数据包需要重发。

  • 滑动窗口:TCP滑动窗口用于流量控制,发送方根据接收方的窗口大小来控制发送速率。

  • 流量控制和拥塞控制:流量控制通过调整窗口大小来防止接收方被淹没;拥塞控制通过算法(如慢启动、拥塞避免)来防止网络过载。

3、加密算法
  • 对称加密(共享密钥加密):使用相同的密钥进行加密和解密,用于保护数据的传输内容。
  • 非对称加密(公开密钥加密):加密对称密钥和认证。
    • 公钥用于加密数据或验证签名。
    • 私钥用于解密数据或生成签名。
  • CA颁发数字证书:CA通过颁发数字证书保证公钥的合法性,防止中间人攻击。
4、HTTP1.1 / HTTPS
  • 长连接:HTTP1.1支持持久连接(Keep-Alive),在一个TCP连接中可以传输多个请求和响应,减少连接建立和关闭的开销。
  • HTTPS通过TLS/SSL握手:HTTPS在传输数据前进行TLS/SSL握手,确保数据加密和服务器身份验证,增强通信安全性。

三、网络提速

协议优化
  1. HTTP/2.0

    • 优点

      • 多路复用:在同一个连接中可以并行处理多个请求,无需等待单个请求完成后才能发起新的请求,减少了延迟。
      • 头部压缩:HTTP/2.0使用HPACK算法压缩头部信息,减少了带宽占用。
      • 服务器推送:服务器可以主动向客户端推送资源,减少了客户端的请求次数。
    • 缺点

      • 复杂度增加:HTTP/2.0的实现较复杂,增加了服务器和客户端的开发成本。
      • 不支持所有旧设备:某些旧的网络设备或防火墙不兼容HTTP/2.0。
  2. 多路复用 / Stream

    • 多路复用:允许在一个TCP连接中同时发送多个请求和响应,提高了带宽利用率。
    • Stream:HTTP/2.0中每个请求/响应对在一个独立的流中传输,流之间互不干扰。
  3. HTTP/3.0

    • 基于UDP:HTTP/3.0基于QUIC协议,而QUIC是建立在UDP之上的,具有更低的连接延迟。
    • 用户态:HTTP/3.0运行在用户空间,避免了内核的TCP栈开销。
    • 0 RTT:QUIC协议支持0-RTT连接建立,使得第一次请求就可以传输数据,减少延迟。
    • 弱网优势:在网络状况较差时,QUIC的快速恢复和拥塞控制机制能够更好地保持连接的稳定性。
网络路径优化
  1. 数据中心:将服务器部署在离用户更近的数据中心,可以减少传输延迟,提升访问速度。

  2. 同运营商访问:用户访问的服务器位于相同的运营商网络中,可以避免跨运营商的延迟问题。

  3. 静态资源路径优化(CDN)

    • 缓存:CDN节点缓存静态资源,用户可以从最近的节点获取资源,降低访问延迟。
    • 如果同级别缓存没有命中,会向下一级节点查找缓存。
  4. 动态API路径优化(DSA):通过动态选择最优路径,使API请求能够尽快到达服务器,减少传输延迟。

    网络稳定

  5. 容灾

    • 故障发生 -> 故障感知 -> 自动切换 -> 服务恢复
    • 案例一:外网容灾,在外网接入节点失效时,通过容灾策略切换到备用节点,保持服务连续性。
    • 案例二:调度容灾(容灾自动化)
      • 流量探测:实时监控网络流量,确保网络正常。
      • 故障感知:自动检测服务故障并启动容灾切换。
    • 案例三:主动降级/容灾,当系统负载过高时,主动降低某些非关键功能的服务质量,确保核心功能正常运转。
    • 案例四:前置兜底逻辑。当出现导致系统崩溃的Bug时,通过缓存数据等方式保证系统基本功能不受影响。
  6. 故障排查

    • 故障明确:识别和定位故障原因。

    • 故障止损:采取紧急措施,防止故障进一步扩大。

    • 分段排查

      • 客户端排查:检查用户端的设置和网络状态。
      • 服务端排查:检查服务器配置、日志等。
      • 中间链路排查:检查路由、网络设备。
    • 网络故障排查指令

      • dig:查询DNS解析。
      • ping/telnet/nmap:检查三层/四层的网络连通性。
      • traceroute:检测中间路由链路。
      • iptables:检查防火墙配置。
      • tcpdump:捕获网络流量,分析数据包内容。

课后作业

1、 UDP socket实现ack,感知丢包重传

// server.go
package main

import (
	"fmt"
	"net"
	"regexp"
	"strconv"
)

const (
	PORT = ":12345" // 定义监听的UDP端口
)

func main() {
	// 解析UDP地址,绑定端口
	addr, err := net.ResolveUDPAddr("udp", PORT)
	if err != nil {
		fmt.Println("解析地址出错:", err)
		return
	}

	// 监听UDP端口
	conn, err := net.ListenUDP("udp", addr)
	if err != nil {
		fmt.Println("启动服务器出错:", err)
		return
	}
	defer conn.Close() // 程序结束时关闭连接

	buffer := make([]byte, 1024)                // 创建用于接收数据的缓冲区
	seqRegex := regexp.MustCompile(`Seq (\d+)`) // 正则表达式,用于匹配消息中的序列号

	for {
		// 从客户端接收消息并存入缓冲区
		n, clientAddr, err := conn.ReadFromUDP(buffer)
		if err != nil {
			fmt.Println("接收数据出错:", err)
			continue // 发生错误,跳过本次循环,继续等待新的数据
		}

		msg := string(buffer[:n])                   // 将接收到的数据转换为字符串
		matches := seqRegex.FindStringSubmatch(msg) // 从消息中提取序列号

		if len(matches) < 2 { // 如果没有找到序列号,输出提示
			fmt.Println("消息中未找到序列号:", msg)
			continue
		}

		// 将提取到的序列号字符串转换为整数
		seq, err := strconv.Atoi(matches[1])
		if err != nil {
			fmt.Println("无效的序列号:", matches[1])
			continue
		}
		fmt.Printf("接收到来自 %s 的消息,序列号为 %d\n", clientAddr, seq)

		// 发送ACK(确认)消息,包含相同的序列号
		ackMsg := strconv.Itoa(seq) // 将序列号转换为字符串
		_, err = conn.WriteToUDP([]byte(ackMsg), clientAddr)
		if err != nil {
			fmt.Println("发送ACK出错:", err)
			continue
		}
	}
}

// client.go
package main

import (
	"fmt"
	"net"
	"strconv"
	"time"
)

const (
	SERVER_ADDR = "127.0.0.1:12345" // 定义服务器的地址和端口
)

func main() {
	// 解析服务器地址
	serverAddr, err := net.ResolveUDPAddr("udp", SERVER_ADDR)
	if err != nil {
		fmt.Println("解析服务器地址出错:", err)
		return
	}

	// 连接到服务器
	conn, err := net.DialUDP("udp", nil, serverAddr)
	if err != nil {
		fmt.Println("连接服务器出错:", err)
		return
	}
	defer conn.Close() // 程序结束时关闭连接

	buffer := make([]byte, 1024) // 创建用于接收数据的缓冲区
	seq := 1                     // 序列号初始值设为1

	for {
		// 构造包含序列号的消息
		message := fmt.Sprintf("Hello, Server - Seq %d", seq)

		// 向服务器发送消息
		_, err := conn.Write([]byte(message))
		if err != nil {
			fmt.Println("发送数据出错:", err)
			return
		}
		fmt.Printf("发送消息,序列号为 %d\n", seq)

		// 设置读取超时时间,等待ACK
		conn.SetReadDeadline(time.Now().Add(2 * time.Second))
		n, _, err := conn.ReadFromUDP(buffer) // 读取服务器发送的ACK
		if err != nil {
			fmt.Printf("未收到序列号为 %d 的ACK,重试发送...\n", seq)
			continue // 如果没有收到ACK,继续发送当前消息
		}

		// 解析ACK消息中的序列号
		ackMsg := string(buffer[:n])
		ackSeq, err := strconv.Atoi(ackMsg)
		if err != nil {
			fmt.Println("接收到无效的ACK:", ackMsg)
			continue
		}

		// 检查ACK序列号是否与当前消息的序列号匹配
		if ackSeq == seq {
			fmt.Printf("收到序列号为 %d 的ACK\n", seq)
			seq++ // 收到正确的ACK后,增加序列号,发送下一条消息
		} else {
			fmt.Printf("收到意外的ACK序列号 %d,期望值为 %d,请重试...\n", ackSeq, seq)
		}
	}
}