fix network

This commit is contained in:
JinJiangHuang
2026-03-10 21:37:51 +08:00
parent 76ed32ccf3
commit fac70d7496
7 changed files with 383 additions and 37 deletions
+153 -18
View File
@@ -5,6 +5,13 @@ class NetworkManager {
///
static let shared = NetworkManager()
///
static let defaultMaxRetries = 2
/// = base * 2^attempt + jitter
private static let retryBaseDelay: TimeInterval = 0.5
///
private static let retryMaxDelay: TimeInterval = 8.0
///
private static let fallbackCharsetNames: [String] = [
"utf-8",
@@ -43,11 +50,16 @@ class NetworkManager {
config.timeoutIntervalForRequest = 15
config.timeoutIntervalForResource = 30
config.httpMaximumConnectionsPerHost = 5
config.waitsForConnectivity = true
self.session = URLSession(configuration: config)
}
/// GET
func getString(from urlString: String, headers: [String: String]? = nil) async throws -> String {
/// GET
func getString(
from urlString: String,
headers: [String: String]? = nil,
maxRetries: Int = NetworkManager.defaultMaxRetries
) async throws -> String {
guard let url = URL(string: urlString.trimmingCharacters(in: .whitespacesAndNewlines)) else {
throw NetworkError.invalidURL(urlString)
}
@@ -56,17 +68,8 @@ class NetworkManager {
request.httpMethod = "GET"
headers?.forEach { request.setValue($1, forHTTPHeaderField: $0) }
let (data, response) = try await session.data(for: request)
let (data, httpResponse) = try await performRequest(request, maxRetries: maxRetries)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
throw NetworkError.httpError(httpResponse.statusCode)
}
// +
guard let str = Self.decodeString(data: data, response: httpResponse) else {
throw NetworkError.decodingError("文本解码失败")
}
@@ -74,24 +77,127 @@ class NetworkManager {
return str
}
/// GET JSON
func getJSON<T: Decodable>(from urlString: String, type: T.Type, headers: [String: String]? = nil) async throws -> T {
let str = try await getString(from: urlString, headers: headers)
/// GET JSON
func getJSON<T: Decodable>(
from urlString: String,
type: T.Type,
headers: [String: String]? = nil,
maxRetries: Int = NetworkManager.defaultMaxRetries
) async throws -> T {
let str = try await getString(from: urlString, headers: headers, maxRetries: maxRetries)
guard let data = str.data(using: .utf8) else {
throw NetworkError.decodingError("字符串转 Data 失败")
}
return try decoder.decode(T.self, from: data)
}
/// GET Data
func getData(from urlString: String) async throws -> Data {
/// GET Data HTTP
func getData(
from urlString: String,
maxRetries: Int = NetworkManager.defaultMaxRetries
) async throws -> Data {
guard let url = URL(string: urlString.trimmingCharacters(in: .whitespacesAndNewlines)) else {
throw NetworkError.invalidURL(urlString)
}
let (data, _) = try await session.data(from: url)
let request = URLRequest(url: url)
let (data, _) = try await performRequest(request, maxRetries: maxRetries)
return data
}
// MARK: -
/// HTTP 退
private func performRequest(
_ request: URLRequest,
maxRetries: Int
) async throws -> (Data, HTTPURLResponse) {
var lastError: Error = NetworkError.invalidResponse
let totalAttempts = max(1, maxRetries + 1)
for attempt in 0..<totalAttempts {
do {
try Task.checkCancellation()
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard (200...299).contains(httpResponse.statusCode) else {
let error = NetworkError.httpError(httpResponse.statusCode)
if Self.isRetryableHTTPStatus(httpResponse.statusCode) && attempt < totalAttempts - 1 {
lastError = error
try await retryDelay(attempt: attempt)
continue
}
throw error
}
return (data, httpResponse)
} catch is CancellationError {
throw CancellationError()
} catch {
lastError = error
if Self.isRetryableError(error) && attempt < totalAttempts - 1 {
try await retryDelay(attempt: attempt)
continue
}
throw error
}
}
throw lastError
}
/// 退 +
private func retryDelay(attempt: Int) async throws {
let base = Self.retryBaseDelay * pow(2.0, Double(attempt))
let jitter = Double.random(in: 0...0.5)
let delay = min(base + jitter, Self.retryMaxDelay)
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
}
/// HTTP
private static func isRetryableHTTPStatus(_ statusCode: Int) -> Bool {
switch statusCode {
case 408, 429, 500, 502, 503, 504: return true
default: return false
}
}
///
private static func isRetryableError(_ error: Error) -> Bool {
if error is CancellationError { return false }
if let networkError = error as? NetworkError {
if case .httpError(let code) = networkError {
return isRetryableHTTPStatus(code)
}
if case .invalidURL = networkError { return false }
if case .decodingError = networkError { return false }
}
let nsError = error as NSError
if nsError.domain == NSURLErrorDomain {
switch nsError.code {
case NSURLErrorTimedOut,
NSURLErrorCannotFindHost,
NSURLErrorCannotConnectToHost,
NSURLErrorNetworkConnectionLost,
NSURLErrorDNSLookupFailed,
NSURLErrorNotConnectedToInternet,
NSURLErrorSecureConnectionFailed,
NSURLErrorDataNotAllowed:
return true
default:
return false
}
}
return false
}
///
/// 1)
/// 2)
@@ -152,4 +258,33 @@ enum NetworkError: LocalizedError {
case .decodingError(let msg): return "解码错误: \(msg)"
}
}
/// UI
var isNetworkUnavailable: Bool {
if case .httpError = self { return false }
return true
}
}
extension Error {
/// DNS
var isNetworkConnectionError: Bool {
if self is CancellationError { return false }
let nsError = self as NSError
if nsError.domain == NSURLErrorDomain {
switch nsError.code {
case NSURLErrorTimedOut,
NSURLErrorCannotFindHost,
NSURLErrorCannotConnectToHost,
NSURLErrorNetworkConnectionLost,
NSURLErrorDNSLookupFailed,
NSURLErrorNotConnectedToInternet,
NSURLErrorDataNotAllowed:
return true
default:
return false
}
}
return false
}
}