一、网络层架构设计原则

一个健壮的iOS网络层需要解决以下核心问题:统一错误处理、统一超时机制、证书校验、重试策略、Mock测试、流量统计。网络层的架构设计决定了整个App的数据通信质量。

1.1 分层架构设计

// 三层网络架构
//
// Layer 1: URLSession 封装层
// 职责: URLSession 配置、SessionDelegate、基础请求发送
//
// Layer 2: 协议层(Protocol-Oriented Network)
// 职责: 请求/响应模型定义、参数编码、结果解析
//
// Layer 3: 业务层
// 职责: 具体API调用、错误映射、业务逻辑处理

// Layer 1: 核心网络引擎
class NetworkEngine {
    static let shared = NetworkEngine()

    private let session: URLSession

    private init() {
        let config = URLSessionConfiguration.default
        config.timeoutIntervalForRequest = 30
        config.timeoutIntervalForResource = 60
        config.waitsForConnectivity = true  // 网络不可用时等待
        config.requestCachePolicy = .reloadIgnoringLocalCacheData
        self.session = URLSession(configuration: config)
    }

    func request(_ endpoint: Endpoint,
                 completion: @escaping (Result) -> Void) {
        guard let request = endpoint.urlRequest else {
            completion(.failure(.invalidURL))
            return
        }

        let task = session.dataTask(with: request) { data, response, error in
            if let error = error {
                completion(.failure(.networkError(error)))
                return
            }

            guard let httpResponse = response as? HTTPURLResponse else {
                completion(.failure(.invalidResponse))
                return
            }

            guard (200...299).contains(httpResponse.statusCode) else {
                completion(.failure(.httpError(httpResponse.statusCode)))
                return
            }

            guard let data = data else {
                completion(.failure(.noData))
                return
            }

            completion(.success(data))
        }
        task.resume()
    }
}

二、协议化网络层(Protocol-Oriented)

2.1 Endpoint 协议定义

每个API作为一个 Endpoint 对象,职责单一且可测试:

protocol Endpoint {
    var baseURL: String { get }
    var path: String { get }
    var method: HTTPMethod { get }
    var headers: [String: String]? { get }
    var parameters: Parameters? { get }
    var parameterEncoding: ParameterEncoding { get }
    var timeout: TimeInterval { get }
}

extension Endpoint {
    var baseURL: String { "https://api.example.com" }

    var headers: [String: String]? {
        ["Content-Type": "application/json",
         "Accept": "application/json"]
    }

    var timeout: TimeInterval { 30 }

    var urlRequest: URLRequest? {
        guard let url = URL(string: baseURL + path) else { return nil }
        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue
        request.allHTTPHeaderFields = headers
        request.timeoutInterval = timeout

        if let params = parameters {
            do {
                request.httpBody = try parameterEncoding.encode(params, into: &request)
            } catch {
                return nil
            }
        }
        return request
    }
}

// 具体API实现
enum UserAPI: Endpoint {
    case login(username: String, password: String)
    case fetchUser(id: String)
    case updateProfile(User)

    var path: String {
        switch self {
        case .login: return "/auth/login"
        case .fetchUser(let id): return "/users/\(id)"
        case .updateProfile: return "/users/profile"
        }
    }

    var method: HTTPMethod {
        switch self {
        case .login: return .post
        case .fetchUser: return .get
        case .updateProfile: return .put
        }
    }

    var parameters: Parameters? {
        switch self {
        case .login(let username, let password):
            return ["username": username, "password": password]
        case .fetchUser: return nil
        case .updateProfile(let user):
            return user.toDictionary()
        }
    }
}

三、重试与幂等策略

3.1 指数退避重试

网络抖动时立即重试会导致拥塞。采用指数退避(Exponential Backoff)策略:

class RetryPolicy {
    private let maxRetries: Int
    private let baseDelay: TimeInterval
    private let maxDelay: TimeInterval

    init(maxRetries: Int = 3, baseDelay: TimeInterval = 1.0, maxDelay: TimeInterval = 30) {
        self.maxRetries = maxRetries
        self.baseDelay = baseDelay
        self.maxDelay = maxDelay
    }

    func execute(_ operation: @escaping () async throws -> Void) async throws {
        var attempt = 0
        var lastError: Error?

        while attempt < maxRetries {
            do {
                try await operation()
                return
            } catch {
                lastError = error
                attempt += 1
                if attempt >= maxRetries { break }

                // 指数退避:1s → 2s → 4s → 8s...(加随机抖动防止惊群)
                let delay = min(baseDelay * pow(2.0, Double(attempt - 1)), maxDelay)
                let jitter = Double.random(in: 0...0.3) * delay
                try await Task.sleep(nanoseconds: UInt64((delay + jitter) * 1_000_000_000))
            }
        }
        throw lastError ?? NetworkError.unknown
    }
}

// 判断错误是否可重试
enum RetryableError {
    static func shouldRetry(_ error: Error, attempt: Int) -> Bool {
        guard attempt < 3 else { return false }

        if let networkError = error as? NetworkError {
            switch networkError {
            case .timeout, .noConnection, .networkError:
                return true  // 网络错误可重试
            case .httpError(let code):
                return [408, 429, 500, 502, 503, 504].contains(code)
                // 408超时、429限流、5xx服务端错误 → 可重试
            default: return false
            }
        }
        return false
    }
}

3.2 幂等性保障

重试的前提是接口幂等(同一请求执行多次产生相同结果)。需要后端配合,但iOS端可以通过Idempotency Key防止非幂等操作被重复执行:

// 幂等Key:同一操作的唯一标识
class IdempotentRequestManager {
    private var inFlight: [String: Task] = [:]
    private let lock = NSLock()

    func execute(
        _ endpoint: Endpoint,
        idempotencyKey: String
    ) async throws -> T {
        lock.lock()
        defer { lock.unlock() }

        // 防止同一操作并发发送(页面快速抖动)
        if let existing = inFlight[idempotencyKey] {
            let data = try await existing.value
            return try JSONDecoder().decode(T.self, from: data)
        }

        let task = Task {
            var request = endpoint.urlRequest
            request?.setValue(idempotencyKey, forHTTPHeaderField: "Idempotency-Key")
            // 等待请求完成
            return try await withCheckedThrowingContinuation { continuation in
                NetworkEngine.shared.request(endpoint) { result in
                    continuation.resume(with: result)
                }
            }
        }
        inFlight[idempotencyKey] = task

        do {
            let data = try await task.value
            inFlight.removeValue(forKey: idempotencyKey)
            return try JSONDecoder().decode(T.self, from: data)
        } catch {
            inFlight.removeValue(forKey: idempotencyKey)
            throw error
        }
    }
}

四、证书校验与安全

4.1 SSL Pinning 实现

防止中间人攻击(MITM)的标准做法是SSL Pinning。推荐使用 TrustKit 的自动刷本方案:

// 方案1:证书公钥Pinning(最稳定,证书续期不受影响)
class PinnedURLSessionDelegate: NSObject, URLSessionDelegate {
    private let pinnedPublicKeyHashes: Set

    init(hashes: [String]) {
        self.pinnedPublicKeyHashes = Set(hashes)
        super.init()
    }

    func urlSession(_ session: URLSession,
                    didReceive challenge: URLAuthenticationChallenge,
                    completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

        guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
              let serverTrust = challenge.protectionSpace.serverTrust else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

        // 获取服务器证书链中的所有公钥
        let serverCertificateChain = SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate] ?? []
        var publicKeyHashesFound: Set = []

        for certificate in serverCertificateChain {
            if let publicKey = SecCertificateCopyKey(certificate) {
                let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil) as Data?
                let hash = publicKeyData?.sha256.base64EncodedString()
                if let hash = hash {
                    publicKeyHashesFound.insert(hash)
                }
            }
        }

        // 判断是否有任何受信任的公钥
        if pinnedPublicKeyHashes.isDisjoint(with: publicKeyHashesFound) {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }

        completionHandler(.useCredential, URLCredential(trust: serverTrust))
    }
}

// 方案2:TrustKit(推荐,开源且维护活跃)
// TrustKit自动处理证书续期、后台刷新、报告服务器异常

4.2 HSTS 与 HTTPS 强制

在 info.plist 中强制使用TLS,防止降级攻击:

// info.plist
NSAppTransportSecurity

    NSAllowsArbitraryLoads
    
    NSExceptionDomains
    
        example.com
        
            NSExceptionRequiresForwardSecrecy
            
            NSExceptionMinimumTLSVersion
            TLSv1.2
        
    

五、网络状态监控

5.1 NWPathMonitor 实时监控

// 使用 Network.framework 的 NWPathMonitor(iOS 12+)
class NetworkMonitor: ObservableObject {
    static let shared = NetworkMonitor()

    @Published private(set) var isConnected = true
    @Published private(set) var connectionType: ConnectionType = .unknown

    enum ConnectionType {
        case wifi, cellular, ethernet, unknown
    }

    private let monitor = NWPathMonitor()
    private let queue = DispatchQueue(label: "NetworkMonitor")

    private init() {
        monitor.pathUpdateHandler = { [weak self] path in
            DispatchQueue.main.async {
                self?.isConnected = path.status == .satisfied
                self?.connectionType = self?.getConnectionType(path) ?? .unknown
            }
        }
        monitor.start(queue: queue)
    }

    private func getConnectionType(_ path: NWPath) -> ConnectionType {
        if path.usesInterfaceType(.wifi) { return .wifi }
        if path.usesInterfaceType(.cellular) { return .cellular }
        if path.usesInterfaceType(.wiredEthernet) { return .ethernet }
        return .unknown
    }
}

网络层设计检查清单

  • 统一错误枚举(NetworkError),不要让NSError四处散落
  • 幂等操作用 GET/PUT/DELETE,非幂等操作用 POST 并配 Idempotency Key
  • 网络监控器(NWPathMonitor)驱动 App 的离线状态展示
  • HTTPS Pinning 必须在 Release 配置中启用
  • 使用URLCache统一管理响应缓存,设置合理的过期策略