一、网络交互
1、网络接入 - 互联网
- 互联网接入是指终端设备通过某种方式接入到全球范围的网络。常见的接入方式包括通过拨号、光纤、无线网络(如Wi-Fi、4G/5G)等。
- 公网IP地址:通过互联网接入时,分配给每台终端设备的IP地址。公网IP是唯一的,可以在全球范围内识别设备。
2、网络接入 - 路由
-
跨网段的路由配置方式(默认路由): 路由器根据目标IP地址判断数据包应走哪个路由。如果目标地址不在本地网段,数据包会通过默认路由转发。 默认路由是当路由器无法根据目标地址找到精确匹配的路由时,使用的备用路由。
示例:
- 路由器的路由表通常有两类信息:
- 静态路由:手动配置的路由规则。
- 动态路由:通过路由协议(如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 地址耗尽的应对方案
- NAT:通过网络地址转换技术使多个设备共享一个公网IP地址。
- 私有地址和NAT:在内部网络使用私有IP地址(如192.168.x.x),通过NAT与外部互联网通信。
- IPv4 地址耗尽的应对方案
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连接
- 三次握手:
-
上图片中,可以看到一个完整的三次握手过程:
- 第一个数据包(SYN)从源端口 10371 向目标端口 443(通常是 HTTPS)发送,表示请求建立连接,此时Seq=0,Ack=0。
- 第二个数据包(SYN, ACK)从目标端口 443 返回,确认连接请求,Seq=0,Ack=1。
- 第三个数据包(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握手,确保数据加密和服务器身份验证,增强通信安全性。
三、网络提速
协议优化
-
HTTP/2.0
-
优点
:
- 多路复用:在同一个连接中可以并行处理多个请求,无需等待单个请求完成后才能发起新的请求,减少了延迟。
- 头部压缩:HTTP/2.0使用HPACK算法压缩头部信息,减少了带宽占用。
- 服务器推送:服务器可以主动向客户端推送资源,减少了客户端的请求次数。
-
缺点
:
- 复杂度增加:HTTP/2.0的实现较复杂,增加了服务器和客户端的开发成本。
- 不支持所有旧设备:某些旧的网络设备或防火墙不兼容HTTP/2.0。
-
-
多路复用 / Stream
- 多路复用:允许在一个TCP连接中同时发送多个请求和响应,提高了带宽利用率。
- Stream:HTTP/2.0中每个请求/响应对在一个独立的流中传输,流之间互不干扰。
-
HTTP/3.0
- 基于UDP:HTTP/3.0基于QUIC协议,而QUIC是建立在UDP之上的,具有更低的连接延迟。
- 用户态:HTTP/3.0运行在用户空间,避免了内核的TCP栈开销。
- 0 RTT:QUIC协议支持0-RTT连接建立,使得第一次请求就可以传输数据,减少延迟。
- 弱网优势:在网络状况较差时,QUIC的快速恢复和拥塞控制机制能够更好地保持连接的稳定性。
网络路径优化
-
数据中心:将服务器部署在离用户更近的数据中心,可以减少传输延迟,提升访问速度。
-
同运营商访问:用户访问的服务器位于相同的运营商网络中,可以避免跨运营商的延迟问题。
-
静态资源路径优化(CDN):
- 缓存:CDN节点缓存静态资源,用户可以从最近的节点获取资源,降低访问延迟。
- 如果同级别缓存没有命中,会向下一级节点查找缓存。
-
动态API路径优化(DSA):通过动态选择最优路径,使API请求能够尽快到达服务器,减少传输延迟。
网络稳定
-
容灾
- 故障发生 -> 故障感知 -> 自动切换 -> 服务恢复
- 案例一:外网容灾,在外网接入节点失效时,通过容灾策略切换到备用节点,保持服务连续性。
- 案例二:调度容灾(容灾自动化)
- 流量探测:实时监控网络流量,确保网络正常。
- 故障感知:自动检测服务故障并启动容灾切换。
- 案例三:主动降级/容灾,当系统负载过高时,主动降低某些非关键功能的服务质量,确保核心功能正常运转。
- 案例四:前置兜底逻辑。当出现导致系统崩溃的Bug时,通过缓存数据等方式保证系统基本功能不受影响。
-
故障排查
-
故障明确:识别和定位故障原因。
-
故障止损:采取紧急措施,防止故障进一步扩大。
-
分段排查
:
- 客户端排查:检查用户端的设置和网络状态。
- 服务端排查:检查服务器配置、日志等。
- 中间链路排查:检查路由、网络设备。
-
网络故障排查指令
:
- 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)
}
}
}