mcpField
A
axiom-networking-diag

axiom-networking-diag

github.com/charleswiltgen开发1.2k180 次安装今天更新
代码仓库

关于 axiom-networking-diag

面向 iOS 开发者的 Network.framework 网络诊断技能,用决策树和分步模式定位连接、TLS、数据传输及性能问题。它强调先收集状态与日志,再逐项验证原因和修复。

主要功能

  1. 01按连接状态引导诊断
  2. 02覆盖 DNS 与 TLS 故障
  3. 03排查消息分帧问题
  4. 04诊断拥塞和缓冲问题
  5. 05提供验证与测试步骤

适用场景

  1. 01排查连接卡在准备或等待状态
  2. 02分析 TLS 证书验证失败
  3. 03诊断发送成功但接收不到数据
  4. 04调查网络切换后的连接或性能问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-networking-diag

安装后在 Claude Code 中输入/axiom-networking-diag确认已加载。

详细介绍

Network.framework 诊断

概述

核心原则: 85% 的网络问题源于误解连接状态、未处理网络切换或错误处理不当,而非 Network.framework 缺陷。

Network.framework 经受了实战考验,存在于每个 iOS 应用中(为 URLSession 提供底层支持),每天处理数万亿个请求,并通过 Happy Eyeballs、代理评估和 WiFi Assist 提供智能连接建立能力。如果连接失败、超时或行为异常,问题几乎总是出在框架的使用方式上,而非框架本身。

本技能可帮助你通过系统化诊断,在几分钟而非几小时内找出根本原因。

危险信号——怀疑存在网络问题

如果出现以下任一情况,应怀疑网络配置错误,而非框架故障:

  • 连接超时 60 秒,且没有明确错误

  • 某些网络上 TLS 握手因“证书无效”而失败

  • 数据已发送,但接收方始终收不到

  • 从 WiFi 切换到蜂窝网络时连接断开

  • 在 WiFi 上运行正常,但在蜂窝网络上 100% 失败

  • 在模拟器中运行正常,但在真机上失败

  • 在你的网络上连接成功,但用户连接失败

  • ❌ 禁止“Network.framework 坏了,我们应该改用套接字重写”

    • Network.framework 为 URLSession 提供底层支持,而 URLSession 用于每个 iOS 应用
    • 它能处理你使用套接字时可能要花数月才能发现的边缘情况
    • Apple 工程师在生产环境中积累的 10 多年调试经验都融入了这个框架
    • 切换到套接字会让你暴露于 100 多种边缘情况

关键区别: 模拟器使用 macOS 网络堆栈(而非 iOS),会掩盖蜂窝网络特有的问题(仅 IPv6 网络),也不会模拟网络切换。必须:在真实设备和真实网络条件下测试。

必须执行的第一步

务必先运行以下命令(然后再修改代码):

// 1. Enable Network.framework logging
// Add to Xcode scheme: Product → Scheme → Edit Scheme → Arguments
// -NWLoggingEnabled 1
// -NWConnectionLoggingEnabled 1

// 2. Check connection state history
connection.stateUpdateHandler = { state in
    print("\(Date()): Connection state: \(state)")
    // Log every state transition with timestamp
}

// 3. Check TLS configuration
// If using custom TLS parameters:
print("TLS version: \(tlsParameters.minimumTLSProtocolVersion)")
print("Cipher suites: \(tlsParameters.tlsCipherSuites ?? [])")

// 4. Test with packet capture (Charles Proxy or Wireshark)
// On device: Settings → WiFi → (i) → Configure Proxy → Manual
// Charles: Help → SSL Proxying → Install Charles Root Certificate on iOS

// 5. Test on different networks
// - WiFi
// - Cellular (disable WiFi)
// - Airplane Mode → WiFi (test waiting state)
// - VPN active
// - IPv6-only (some cellular carriers)
这些信息说明什么
现象诊断下一步
卡在 .preparing 状态超过 5 秒DNS 解析失败或网络已断开模式 1a
立即转为 .waiting 状态无网络连接(飞行模式、无信号)模式 1b
.failed 且 POSIX 错误为 61连接被拒绝(服务器未在监听)模式 1c
.failed 且 POSIX 错误为 50网络已断开(接口已禁用)模式 1d
.ready 后立即变为 .failedTLS 握手失败模式 2b
.ready,发送成功,但没有数据到达帧格式问题或接收方未处理数据模式 3a
WiFi 可用,蜂窝网络失败仅 IPv6 网络(硬编码 IPv4)模式 5a
不使用 VPN 时正常,使用 VPN 时失败代理干扰或 DNS 覆盖模式 5b
必须进行的判断

在修改任何代码之前,先确定以下一种情况:

  1. 如果卡在 .preparing 状态且网络可用 → DNS 解析失败(检查 nslookup)
  2. 如果立即进入 .waiting 状态且飞行模式已关闭 → 接口特定问题(蜂窝网络被阻止)
  3. 如果 .failed 且 POSIX 错误为 61 → 服务器问题(检查服务器日志)
  4. 如果 .failed 且出现 TLS 错误 -9806 → 证书验证问题(使用 openssl 检查)
  5. 如果 .ready 但数据未到达 → 帧格式或接收方问题(启用数据包捕获)
如果诊断结果互相矛盾或不明确
  • 停止。暂时不要继续处理各种模式
  • 为每次发送和接收调用添加时间戳日志
  • 启用数据包捕获(Charles/Wireshark)
  • 在另一台设备上测试,以区分硬件问题和软件问题

决策树

使用此决策树,在 2 分钟内找到正确的诊断模式:

Network problem?
├─ Connection never reaches .ready?
│  ├─ Stuck in .preparing for >5 seconds?
│  │  ├─ DNS lookup timing out? → Pattern 1a (DNS Failure)
│  │  ├─ Network available but can't reach host? → Pattern 1c (Connection Refused)
│  │  └─ First connection slow, subsequent fast? → Pattern 1e (DNS Caching)
│  │
│  ├─ Moves to .waiting immediately?
│  │  ├─ Airplane Mode or no signal? → Pattern 1b (No Connectivity)
│  │  ├─ Cellular blocked by parameters? → Pattern 1b (Interface Restrictions)
│  │  └─ VPN connecting? → Wait and retry
│  │
│  ├─ .failed with POSIX error 61?
│  │  └─ → Pattern 1c (Connection Refused)
│  │
│  └─ .failed with POSIX error 50?
│     └─ → Pattern 1d (Network Down)
│
├─ Connection reaches .ready, then fails?
│  ├─ Fails immediately after .ready?
│  │  ├─ TLS error -9806? → Pattern 2b (Certificate Validation)
│  │  ├─ TLS error -9801? → Pattern 2b (Protocol Version)
│  │  └─ POSIX error 54? → Pattern 2d (Connection Reset)
│  │
│  ├─ Fails after network change (WiFi → cellular)?
│  │  ├─ No viabilityUpdateHandler? → Pattern 2a (Viability Not Handled)
│  │  ├─ Didn't detect better path? → Pattern 2a (Better Path)
│  │  └─ IPv6 → IPv4 transition? → Pattern 5a (Dual Stack)
│  │
│  ├─ Fails after timeout?
│  │  └─ → Pattern 2c (Receiver Not Responding)
│  │
│  └─ Random disconnects?
│     └─ → Pattern 2d (Network Instability)
│
├─ Data not arriving?
│  ├─ Send succeeds, receive never returns?
│  │  ├─ No message framing? → Pattern 3a (Framing Problem)
│  │  ├─ Wrong byte count? → Pattern 3b (Min/Max Bytes)
│  │  └─ Receiver not calling receive()? → Check receiver code
│  │
│  ├─ Partial data arrives?
│  │  ├─ receive(exactly:) too large? → Pattern 3b (Chunking)
│  │  ├─ Sender closing too early? → Check sender lifecycle
│  │  └─ Buffer overflow? → Pattern 3b (Buffer Management)
│  │
│  ├─ Data corrupted?
│  │  ├─ TLS disabled? → Pattern 3c (No Encryption)
│  │  ├─ Binary vs text encoding? → Check ContentType
│  │  └─ Byte order (endianness)? → Use network byte order
│  │
│  └─ Works sometimes, fails intermittently?
│     └─ → Pattern 3d (Race Condition)
│
├─ Performance degrading?
│  ├─ Latency increasing over time?
│  │  ├─ TCP congestion? → Pattern 4a (Congestion Control)
│  │  ├─ No contentProcessed pacing? → Pattern 4a (Buffering)
│  │  └─ Server overloaded? → Check server metrics
│  │
│  ├─ Throughput decreasing?
│  │  ├─ Network transition WiFi → cellular? → Pattern 4b (Bandwidth Change)
│  │  ├─ Packet loss increasing? → Pattern 4b (Network Quality)
│  │  └─ Multiple streams competing? → Pattern 4b (Prioritization)
│  │
│  ├─ High CPU usage?
│  │  ├─ Not using batch for UDP? → Pattern 4c (Batching)
│  │  ├─ Too many small sends? → Pattern 4c (Coalescing)
│  │  └─ Using sockets instead of Network.framework? → Migrate (30% CPU savings)
│  │
│  └─ Memory growing?
│     ├─ Not releasing connections? → Pattern 4d (Connection Leaks)
│     ├─ Not cancelling on deinit? → Pattern 4d (Lifecycle)
│     └─ Missing [weak self]? → Pattern 4d (Retain Cycles)
│
└─ Works on WiFi, fails on cellular/VPN?
   ├─ IPv6-only cellular network?
   │  ├─ Hardcoded IPv4 address? → Pattern 5a (IPv4 Literal)
   │  ├─ getaddrinfo with AF_INET only? → Pattern 5a (Address Family)
   │  └─ Works on some carriers, not others? → Pattern 5a (Regional IPv6)
   │
   ├─ Corporate VPN active?
   │  ├─ Proxy configuration failing? → Pattern 5b (PAC)
   │  ├─ DNS override blocking hostname? → Pattern 5b (DNS)
   │  └─ Certificate pinning failing? → Pattern 5b (TLS in VPN)
   │
   ├─ Port blocked by firewall?
   │  ├─ Non-standard port? → Pattern 5c (Firewall)
   │  ├─ Outbound only? → Pattern 5c (NATing)
   │  └─ Works on port 443, not 8080? → Pattern 5c (Port Scanning)
   │
   ├─ Peer-to-peer connection failing?
   │  ├─ NAT traversal issue? → Pattern 5d (STUN/TURN)
   │  ├─ Symmetric NAT? → Pattern 5d (NAT Type)
   │  └─ Local network only? → Pattern 5d (Bonjour/mDNS)
   │
   └─ URLSession fails but NWConnection works?
      ├─ HTTP URL blocked? → Pattern 6a (ATS HTTP Block)
      ├─ "SSL error" on HTTPS? → Pattern 6b (ATS TLS Version)
      └─ Works on older iOS? → Pattern 6a/6b (ATS enforcement)

模式选择规则(强制)

开始使用某个模式前:

  1. 连接始终无法达到 .ready → 从模式 1 开始(DNS、连接问题、连接被拒绝)
  2. TLS 错误代码 → 直接跳到模式 2b(证书验证)
  3. 未收到数据 → 先启用数据包捕获,然后再使用模式 3
  4. 特定网络问题(WiFi 上正常,蜂窝网络上失败) → 在该确切网络上测试,使用模式 5
  5. 性能下降 → 使用 Instruments Network 模板进行性能分析,使用模式 4
一次只应用一种模式
  • 应用一种模式中的修复方案
  • 彻底测试
  • 只有问题仍然存在时,才尝试下一种模式
  • 不要同时应用多种模式(无法确定原因)
禁止事项
  • 未经诊断就猜测解决方案
  • 一次更改多个事项
  • 认定“只是需要更长的超时时间”
  • “暂时”禁用 TLS
  • 为了“规避框架问题”而切换到套接字

诊断模式

模式 1a:DNS 解析失败

耗时 10-15 分钟

症状
  • 连接卡在 .preparing 状态超过 5 秒
  • 最终失败或超时
  • 使用 IP 地址可以正常工作,但使用主机名不行
  • 在一个网络上正常工作,在另一个网络上失败
诊断
// Enable DNS logging
// -NWLoggingEnabled 1

// Check DNS resolution manually
// Terminal: nslookup example.com
// Terminal: dig example.com

// Logs show:
// "DNS lookup timed out"
// "getaddrinfo failed: 8 (nodename nor servname provided)"
常见原因
  1. 无法访问 DNS 服务器(企业网络会屏蔽外部 DNS)
  2. 主机名拼写错误或不存在
  3. DNS 缓存中的条目已过期(不常见,但会发生)
  4. VPN 阻止 DNS 解析
修复
// ❌ WRONG — Adding timeout doesn't fix DNS
/*
let parameters = NWParameters.tls
parameters.expiredDNSBehavior = .allow // Doesn't help if DNS never resolves
*/

// ✅ CORRECT — Verify hostname, test DNS manually
// 1. Test DNS manually:
// $ nslookup your-hostname.com
// If this fails, DNS is the problem (not your code)

// 2. If DNS works manually but not in app:
// Check if VPN or enterprise config blocking app DNS

// 3. If hostname doesn't exist:
let connection = NWConnection(
    host: NWEndpoint.Host("correct-hostname.com"), // Fix typo
    port: 443,
    using: .tls
)

// 4. If DNS caching issue (rare):
// Restart device to clear DNS cache
// Or use IP address temporarily while investigating DNS server issue
验证
  • 运行 nslookup your-hostname.com — 应在不到 1 秒内返回 IP
  • 在蜂窝网络上测试(使用不同的 DNS 服务器)— 应能正常工作
  • 检查企业网络的 DNS 配置
预防
  • 使用知名主机名(不要依赖内部 DNS)
  • 开发期间在多个网络上测试
  • 不要硬编码 IP(如果 DNS 失败,应修复 DNS,而不是绕过它)

模式 2b:TLS 证书验证失败

耗时 15-20 分钟

症状
  • 连接短暂达到 .ready 状态,然后立即变为 .failed
  • 错误:-9806(kSSLPeerCertInvalid)
  • 错误:-9807(kSSLPeerCertExpired)
  • 错误:-9801(kSSLProtocol)
  • 在某些服务上正常工作,在其他服务上失败
诊断
# Test TLS manually with openssl
openssl s_client -connect example.com:443 -showcerts

# Check certificate details
openssl s_client -connect example.com:443 | openssl x509 -noout -dates
# notBefore: Jan  1 00:00:00 2024 GMT
# notAfter: Dec 31 23:59:59 2024 GMT ← Check if expired

# Check certificate chain
openssl s_client -connect example.com:443 -showcerts | grep "CN="
# Should show: Subject CN=example.com, Issuer CN=Trusted CA
常见原因
  1. 自签名证书(开发/预发布服务)
  2. 证书已过期
  3. 证书主机名不匹配(证书适用于“example.com”,但连接的是“www.example.com”)
  4. 缺少中间 CA 证书
  5. TLS 1.0/1.1(iOS 13+ 要求 TLS 1.2+)
修复
适用于证书无效的生产服务
// ❌ WRONG — Never disable certificate validation in production
/*
let tlsOptions = NWProtocolTLS.Options()
sec_protocol_options_set_verify_block(tlsOptions.securityProtocolOptions, { ... }, .main)
// This disables validation → security vulnerability
*/

// ✅ CORRECT — Fix the certificate on server
// 1. Renew expired certificate (Let's Encrypt, DigiCert, etc.)
// 2. Ensure hostname matches (CN=example.com or SAN includes example.com)
// 3. Include intermediate CA certificates on server
// 4. Test with: openssl s_client -connect example.com:443
适用于开发服务(临时)
// ⚠️ ONLY for development/staging
#if DEBUG
let tlsOptions = NWProtocolTLS.Options()

sec_protocol_options_set_verify_block(
    tlsOptions.securityProtocolOptions,
    { (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in
        // Trust any certificate (DEV ONLY)
        sec_protocol_verify_complete(true)
    },
    .main
)

let parameters = NWParameters(tls: tlsOptions)
let connection = NWConnection(host: "dev-server.example.com", port: 443, using: parameters)
#endif
适用于证书固定
// Production-grade certificate pinning
let tlsOptions = NWProtocolTLS.Options()

sec_protocol_options_set_verify_block(
    tlsOptions.securityProtocolOptions,
    { (metadata, trust, complete) in
        let trust = sec_protocol_metadata_copy_peer_public_key(metadata)
        // Compare trust with pinned certificate
        let pinnedCertificateData = Data(/* your cert */)
        let serverCertificateData = SecCertificateCopyData(trust) as Data

        if serverCertificateData == pinnedCertificateData {
            complete(true)
        } else {
            complete(false) // Reject non-pinned certificates
        }
    },
    .main
)
验证
  • openssl s_client -connect example.com:443 显示 Verify return code: 0 (ok)
  • 证书将在 30 天以后才过期
  • 证书 CN 与主机名匹配
  • 在真实 iOS 设备上测试(不只是模拟器)

模式 3a:消息分帧问题

耗时 20-30 分钟

症状
  • connection.send() 成功且没有错误
  • connection.receive() 始终不返回数据
  • 或 receive() 返回部分数据
  • 数据包捕获显示线路上有字节,但应用未处理这些字节
诊断
// Enable detailed logging
connection.send(content: data, completion: .contentProcessed { error in
    if let error = error {
        print("Send error: \(error)")
    } else {
        print("✅ Sent \(data.count) bytes at \(Date())")
    }
})

connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, context, isComplete, error in
    if let error = error {
        print("Receive error: \(error)")
    } else if let data = data {
        print("✅ Received \(data.count) bytes at \(Date())")
    }
}

// Use Charles Proxy or Wireshark to verify bytes on wire

常见原因 流协议(TCP/TLS)不会保留消息边界。

示例
// Sender sends 3 messages:
send("Hello") // 5 bytes
send("World") // 5 bytes
send("!") // 1 byte

// Receiver might get:
receive() → "HelloWorld!" // All 11 bytes at once
// Or:
receive() → "Hel" // 3 bytes
receive() → "loWorld!" // 8 bytes

// Message boundaries lost!
修复
方案 1:使用 TLV 分帧(iOS 26+)
// NetworkConnection with TLV
let connection = NetworkConnection(
    to: .hostPort(host: "example.com", port: 1029)
) {
    TLV {
        TLS()
    }
}

// Send typed messages
enum MessageType: Int {
    case chat = 1
    case ping = 2
}

let chatData = Data("Hello".utf8)
try await connection.send(chatData, type: MessageType.chat.rawValue)

// Receive typed messages
let (data, metadata) = try await connection.receive()
if metadata.type == MessageType.chat.rawValue {
    print("Chat message: \(String(data: data, encoding: .utf8)!)")
}
方案 2:手动添加长度前缀(iOS 12-25)
// Sender: Prefix message with UInt32 length
func sendMessage(_ message: Data) {
    var length = UInt32(message.count).bigEndian
    let lengthData = Data(bytes: &length, count: 4)

    connection.send(content: lengthData, completion: .contentProcessed { _ in
        connection.send(content: message, completion: .contentProcessed { _ in
            print("Sent message with length prefix")
        })
    })
}

// Receiver: Read length, then read message
func receiveMessage() {
    // 1. Read 4-byte length
    connection.receive(minimumIncompleteLength: 4, maximumLength: 4) { lengthData, _, _, error in
        guard let lengthData = lengthData else { return }

        let length = lengthData.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }

        // 2. Read message of exact length
        connection.receive(minimumIncompleteLength: Int(length), maximumLength: Int(length)) { messageData, _, _, error in
            guard let messageData = messageData else { return }
            print("Received complete message: \(messageData.count) bytes")
        }
    }
}
验证
  • 发送 10 条消息,确认接收方恰好收到 10 条消息
  • 发送不同大小的消息(1 字节、1000 字节、64KB)
  • 使用丢包模拟进行测试(Network Link Conditioner)

模式 4a:TCP 拥塞和缓冲

耗时 15-25 分钟

症状
  • 开始几次发送速度很快,之后逐渐变慢
  • 延迟随时间从 50ms 增至 500ms,再增至 2000ms
  • 内存用量增加(未发送数据被缓冲)
  • 用户反映应用在运行 5 分钟后“感觉迟钝”
诊断
// Monitor send completion time
let sendStart = Date()
connection.send(content: data, completion: .contentProcessed { error in
    let elapsed = Date().timeIntervalSince(sendStart)
    print("Send completed in \(elapsed)s") // Should be < 0.1s normally
    // If > 1s, TCP congestion or receiver not draining fast enough
})

// Profile with Instruments
// Xcode → Product → Profile → Network template
// Check "Bytes Sent" vs "Time" graph
// Should be smooth line, not stepped/stalled
常见原因
  1. 发送方的发送速度超过接收方的处理速度(背压)
  2. 网络拥塞(丢包、重传)
  3. 未使用 contentProcessed 回调进行速率控制
  4. 在已失去可用性的连接上发送数据
修复方法
// ❌ WRONG — Sending without pacing
/*
for frame in videoFrames {
    connection.send(content: frame, completion: .contentProcessed { _ in })
    // Buffers all frames immediately → memory spike → congestion
}
*/

// ✅ CORRECT — Pace with contentProcessed callback
func sendFrameWithPacing() {
    guard let nextFrame = getNextFrame() else { return }

    connection.send(content: nextFrame, completion: .contentProcessed { [weak self] error in
        if let error = error {
            print("Send error: \(error)")
            return
        }

        // contentProcessed = network stack consumed frame
        // NOW send next frame (pacing)
        self?.sendFrameWithPacing()
    })
}

// Start pacing
sendFrameWithPacing()
替代方案:Async/await(iOS 26+)
// NetworkConnection with natural back pressure
func sendFrames() async throws {
    for frame in videoFrames {
        try await connection.send(frame)
        // Suspends automatically if network can't keep up
        // Built-in back pressure, no manual pacing needed
    }
}
验证
  • 发送 1000 条消息,监控内存使用情况(应保持稳定)
  • 监控发送完成时间(应低于 100ms)
  • 使用 Network Link Conditioner 进行测试(100ms 延迟,3% 丢包)

模式 5a:仅支持 IPv6 的蜂窝网络(硬编码 IPv4)

耗时 10-15 分钟

症状
  • 在 WiFi(双栈 IPv4/IPv6)上运行正常
  • 在蜂窝网络(仅 IPv6)上始终失败
  • 在部分运营商网络上正常(T-Mobile),在其他运营商网络上失败(Verizon)
  • 日志显示“Host unreachable”或 POSIX 错误 65(EHOSTUNREACH)
诊断
# Check if hostname has IPv6
dig AAAA example.com

# Check if device is on IPv6-only network
# Settings → WiFi/Cellular → (i) → IP Address
# If starts with "2001:" or "fe80:" → IPv6
# If "192.168" or "10." → IPv4

# Test with IPv6-only simulator
# Xcode → Devices → (device) → Use as Development Target
# Settings → Developer → Networking → DNS64/NAT64
常见原因
  1. 硬编码 IPv4 地址(“192.168.1.1”)
  2. getaddrinfo 仅使用 AF_INET(过滤掉 IPv6)
  3. 服务没有 IPv6 地址(AAAA 记录)
  4. 未使用 Connect by Name(手动 DNS)
修复方法
// ❌ WRONG — Hardcoded IPv4
/*
let host = "192.168.1.100" // Fails on IPv6-only cellular
*/

// ❌ WRONG — Forcing IPv4
/*
let parameters = NWParameters.tcp
parameters.requiredInterfaceType = .wifi
parameters.ipOptions.version = .v4 // Fails on IPv6-only
*/

// ✅ CORRECT — Use hostname, let framework handle IPv4/IPv6
let connection = NWConnection(
    host: NWEndpoint.Host("example.com"), // Hostname, not IP
    port: 443,
    using: .tls
)
// Framework automatically:
// 1. Resolves both A (IPv4) and AAAA (IPv6) records
// 2. Tries IPv6 first (if available)
// 3. Falls back to IPv4 (Happy Eyeballs)
// 4. Works on any network (IPv4, IPv6, dual-stack)
验证
  • 在真实设备的蜂窝网络上测试(关闭 WiFi)
  • 使用多个运营商进行测试(Verizon、AT&T、T-Mobile)
  • 在开发者设置中启用 DNS64/NAT64
  • 运行 dig AAAA your-hostname.com,确认 IPv6 记录存在

生产环境危机情景

背景:iOS 更新导致 15% 的连接失败
情况
  • 公司于周一上午发布 iOS 应用更新(v4.2)
  • 到中午,客户支持部门报告“应用无法使用”工单激增
  • 分析数据显示,15% 的用户遇到连接失败(超过 10,000 名用户)
  • CEO 发送 Slack 消息:“出什么事了?我们多快能修好?”
  • 工程经理询问预计完成时间
  • 你是负责网络的工程师
压力信号
  • 🚨 生产环境故障 超过 10K 名用户受影响,营收受损,App Store 负面评价不断涌入
  • ⏰ 时间紧迫 “需要尽快修复,Twitter 上都在热议”
  • 👔 高管关注 CEO 亲自询问进展
  • 📊 公众形象 App Store 评分在 3 小时内从 4.8 降至 4.1
  • 💸 财务影响 这是一个电商应用,每分钟损失 5K 美元的销售额
合理化陷阱(切勿落入)
  1. “直接回滚到 v4.1”

    • 看似诱人,但应用审核需要 1-2 小时,用户更新还要再等 24 小时
    • 无法找到根本原因(问题可能再次发生)
    • 会失去你花费数周开发的 v4.2 功能
  2. “暂时禁用 TLS,缩小排查范围”

    • 这会造成安全漏洞,并导致 App Store 审核被拒
    • 无法解决实际问题(只是掩盖症状)
    • 何时重新启用?(剧透:永远不会,因为“之后再修”往往永远不会发生)
  3. “我的设备上能用,一定是用户操作有误”

    • 这是傲慢,不是诊断
    • 1 万名用户都遇到同一个“错误”?这不可能是用户操作有误。
  4. “我们加上重试逻辑和更长的超时时间吧”

    • 无法解决根本原因
    • 只会让问题恶化(重试越多,故障路径承受的负载就越大)
强制诊断流程

你有 1 小时向 CEO 提供以下信息:

  1. 根本原因
  2. 修复时间线
  3. 缓解计划
步骤 1:确定基线(5 分钟)
// Check what changed in v4.2
git diff v4.1 v4.2 -- NetworkClient.swift

// Most likely culprits:
// - TLS configuration changed
// - Added certificate pinning
// - Changed connection parameters
// - Updated hostname
步骤 2:在生产环境中复现(10 分钟)
// Check failure pattern:
// - Random 15%? Or specific user segment?
// - Specific iOS version? (check analytics)
// - Specific network? (WiFi vs cellular)

// Enable logging on production builds (emergency flag):
#if PRODUCTION
if UserDefaults.standard.bool(forKey: "EnableNetworkLogging") {
    // -NWLoggingEnabled 1
}
#endif

// Ask Customer Support to enable for affected users
// Check logs for specific error code
步骤 3:检查近期代码变更(5 分钟)
// Found in git diff:
// v4.1:
let parameters = NWParameters.tls

// v4.2:
let tlsOptions = NWProtocolTLS.Options()
tlsOptions.minimumTLSProtocolVersion = .TLSv13 // ← SMOKING GUN
let parameters = NWParameters(tls: tlsOptions)

已查明根本原因 部分用户的后端基础设施(负载均衡器、代理服务器)不支持 TLS 1.3。v4.1 协商使用 TLS 1.2,v4.2 要求使用 TLS 1.3 → 连接失败。

步骤 4:实施有针对性的修复(15 分钟)
// Fix: Support both TLS 1.2 and TLS 1.3
let tlsOptions = NWProtocolTLS.Options()
tlsOptions.minimumTLSProtocolVersion = .TLSv12 // ✅ Support older infrastructure
// TLS 1.3 will still be used where supported (automatic negotiation)
let parameters = NWParameters(tls: tlsOptions)
步骤 5:部署热修复(20 分钟)
# Build hotfix v4.2.1
# Test on affected user's network (critical!)
# Submit to App Store with expedited review request
# Explain: "Production outage affecting 15% of users"
专业沟通模板
发给 CEO(危机开始 15 分钟后)
Found root cause: v4.2 requires TLS 1.3, but 15% of users on older infrastructure
(enterprise proxies, older load balancers) that only support TLS 1.2.

Fix: Change minimum TLS version to 1.2 (backward compatible, 1.3 still used when available).

ETA: Hotfix v4.2.1 in App Store in 1 hour (expedited review).
Full rollout to users: 24 hours.

Mitigation now: Telling affected users to update immediately when available.
发给工程经理
Root cause: TLS version requirement changed in v4.2 (TLS 1.3 only).
15% of users behind infrastructure that doesn't support TLS 1.3.

Technical fix: Set tlsOptions.minimumTLSProtocolVersion = .TLSv12
This allows backward compatibility while still using TLS 1.3 where supported.

Testing: Verified fix on user's network (enterprise VPN with old proxy).
Deployment: Hotfix build in progress, ETA 30 minutes to submit.

Prevention: Add TLS compatibility testing to pre-release checklist.
发给客户支持部门
Update: We've identified the issue and have a fix deploying within 1 hour.

Affected users: Those on enterprise networks or older ISP infrastructure.
Workaround: None (network level issue).

Expected resolution: v4.2.1 will be available in App Store in 1 hour.
Ask users to update immediately.

Updates: I'll notify you every 30 minutes.
节省的时间
方法解决所需时间用户影响
❌ 恐慌式回滚应用审核 1-2 小时 + 用户更新 24 小时 = 26 小时10K 用户中断 26 小时
❌“增加重试次数”未知(无法解决根本原因)永久维持 15% 的失败率
❌“我这里能用”花几天时间排查错误的问题用户感到沮丧,留下差评
✅ 系统化诊断诊断 30 分钟 + 修复 20 分钟 + 审核 1 小时 = 2 小时10K 用户中断 2 小时
经验教训
  1. 在不同网络上测试 不要只在 WiFi 上测试。还要在蜂窝网络、VPN 和企业网络上测试。
  2. 监控 TLS 兼容性 如果更改 TLS 配置,请确认后端支持该配置。
  3. 逐步发布 使用分阶段发布(10% → 50% → 100%),尽早发现问题。
  4. 紧急日志记录 准备好在生产环境中启用详细日志记录的方法,以便诊断问题。
  5. 沟通频率 每 30 分钟向利益相关方更新进展,即使只是说“仍在调查”。

快速参考表

症状可能原因首要检查项模式修复时间
卡在 .preparingDNS 故障nslookup hostname1a10-15 分钟
立即进入 .waiting无连接是否开启了飞行模式?1b5 分钟
.failed POSIX 61连接被拒绝服务器正在监听吗?1c5-10 分钟
.failed POSIX 50网络断开检查接口1d5 分钟
TLS 错误 -9806证书无效openssl s_client2b15-20 分钟
未收到数据帧格式问题数据包捕获3a20-30 分钟
数据不完整最小/最大字节数错误检查 receive() 参数3b10 分钟
延迟不断增加TCP 拥塞contentProcessed 节流4a15-25 分钟
CPU 使用率高未进行批处理使用 connection.batch4c10 分钟
内存不断增长连接泄漏检查 [weak self]4d10-15 分钟
WiFi 可用,蜂窝网络失败IPv6-only 网络dig AAAA hostname5a10-15 分钟
未使用 VPN 时可用,使用 VPN 时失败代理干扰测试 PAC 文件5b20-30 分钟
端口被阻止防火墙尝试 443,而不是 80805c10 分钟
HTTP URL 被静默阻止ATS 强制执行检查 Info.plist6a5-10 分钟
“发生了 SSL 错误”ATS TLS 要求检查服务器 TLS 版本6b10-15 分钟

模式 6:应用传输安全(ATS)故障

耗时 5-15 分钟

默认情况下,ATS 会强制所有连接使用 HTTPS(iOS 9+)。ATS 故障不会明确提示——连接会失败并返回通用错误,控制台中不会显示 ATS 专属消息。

模式 6a:HTTP 被 ATS 阻止
症状
  • URLSession 请求失败,错误为 NSURLErrorSecureConnectionFailed (-1200) 或 NSURLErrorAppTransportSecurityRequiresSecureConnection (-1022)
  • Network.framework 连接可用,但 URLSession 不可用
  • 在较旧的 iOS 版本上可用,在较新的版本上失败
  • 没有明确的错误消息——只显示“连接失败”
诊断
# Check if ATS is blocking the connection
nscurl --ats-diagnostics https://yourserver.com
# Shows exactly which ATS policy the server fails
// In console, look for:
// "App Transport Security has blocked a cleartext HTTP (http://) resource load"
// This only appears if OS-level logging is enabled
修复——允许特定 HTTP 域名(首选)

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>api.legacy-server.com</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
    </dict>
</dict>

不要使用 NSAllowsArbitraryLoads——这会完全禁用 ATS。App Store 审核会对此标记,应用可能因此被拒。请使用针对特定域名的例外。

模式 6b:ATS TLS 版本要求
症状
  • HTTPS 连接失败并显示“SSL 错误”,尽管证书有效
  • 服务器使用 TLS 1.0 或 1.1(ATS 要求 TLS 1.2+)
  • nscurl --ats-diagnostics 显示 TLS 版本失败
诊断
# Check server's TLS version
openssl s_client -connect yourserver.com:443 -tls1_2
# If this fails but -tls1 succeeds → server doesn't support TLS 1.2
修复——升级服务器(首选)或添加例外

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>legacy-api.example.com</key>
        <dict>
            <key>NSExceptionMinimumTLSVersion</key>
            <string>TLSv1.0</string>
        </dict>
    </dict>
</dict>

更好的修复方法:将服务器升级到 TLS 1.2+。针对 TLS 降级设置的 ATS 例外会受到 App Store 审核的严格审查。

ATS 与 Network.framework 的区别

ATS 适用于 URLSession 和 WKWebView 连接。Network.framework(NWConnection/NetworkConnection)不受 ATS 限制——它通过 tlsOptions 直接处理 TLS 配置。如果 URLSession 连接失败,但 NWConnection 连接到同一服务器时成功,几乎可以确定原因是 ATS。


常见错误

错误 1:调试前未启用日志记录

问题 在看不到框架内部状态的情况下尝试调试网络问题。

为什么会失败 你只能猜测发生了什么。日志会显示精确的状态转换、错误代码和时间信息。

修复方法
// Add to Xcode scheme BEFORE debugging:
// -NWLoggingEnabled 1
// -NWConnectionLoggingEnabled 1

// Or programmatically:
#if DEBUG
ProcessInfo.processInfo.environment["NW_LOGGING_ENABLED"] = "1"
#endif
错误 2:只在 WiFi 下测试

问题 WiFi 和蜂窝网络的特性不同(仅支持 IPv6、代理配置、丢包)。

为什么会失败 40% 的连接失败是由网络环境导致的。如果你只测试 WiFi,就会漏掉蜂窝网络的问题。

修复方法
  • 在关闭 WiFi 的真实设备上测试
  • 在多个运营商的网络上测试(Verizon、AT&T、T-Mobile 的配置各不相同)
  • 在启用 VPN 的情况下测试(企业用户)
  • 使用 Network Link Conditioner(Xcode → Devices)
错误 3:忽略 POSIX 错误代码

问题 看到 .failed(let error),却只向用户显示笼统的“连接失败”。

为什么会失败 不同的错误代码需要不同的修复方法。POSIX 61 = 服务端问题,POSIX 50 = 客户端网络问题。

修复方法
if case .failed(let error) = state {
    let posixError = (error as NSError).code
    switch posixError {
    case 61: // ECONNREFUSED
        print("Server not listening, check server logs")
    case 50: // ENETDOWN
        print("Network interface down, check WiFi/cellular")
    case 60: // ETIMEDOUT
        print("Connection timeout, check firewall/DNS")
    default:
        print("Connection failed: \(error)")
    }
}
错误 4:未测试状态转换

问题 只测试正常流程(.preparing → .ready),不测试 .waiting、网络变化和失败情况。

为什么会失败 真实用户会遇到网络切换(WiFi → 蜂窝网络)、飞行模式和信号弱等情况。

修复方法
// Test with Network Link Conditioner:
// 1. 100% Loss — verify .waiting state shows "Waiting for network"
// 2. WiFi → None → WiFi — verify automatic reconnection
// 3. 3% packet loss — verify performance graceful degradation
错误 5:假设模拟器等同于设备

问题 只在模拟器中测试。模拟器使用 macOS 网络(与 iOS 不同),也没有蜂窝网络。

为什么会失败 模拟器无法暴露仅支持 IPv6 的问题,也不会模拟网络切换,而且 DNS 行为也不同。

修复方法
  • 发布前务必在真实设备上测试
  • 切换飞行模式进行测试(模拟网络切换)
  • 仅使用蜂窝网络进行测试(关闭 WiFi)

交叉引用

预防性模式

networking skill — 用于规范行为、避免反模式:

  • 危险信号:SCNetworkReachability、阻塞式套接字、硬编码 IP
  • 模式 1a:使用 TLS 的 NetworkConnection(正确实现)
  • 模式 2a:正确处理状态的 NWConnection
  • 压力场景:如何在截止日期压力下避免走捷径
API 参考

network-framework-ref skill — 完整的 API 文档:

  • NetworkConnection(iOS 26+):全部 12 个 WWDC 2025 示例
  • NWConnection(iOS 12-25):完整 API 和示例
  • TLV framing、Coder protocol、NetworkListener、NetworkBrowser
  • 从套接字、URLSession、NWConnection 迁移的策略
相关问题

swift-concurrency skill — 使用 async/await 时:

  • 模式 3:Task 闭包中的 weak self(类似的内存泄漏预防方法)
  • 在连接状态更新中使用 @MainActor
  • 连接失败时取消 Task

最后更新 2025-12-02 状态 基于 WWDC 2018/2025 的生产就绪诊断方案 已测试 诊断模式已通过真实生产问题验证

评论

支持 Markdown