swift 中的串行网络调用序列

Sequence of serial network calls in swift

有时我需要进行一系列网络调用,其中每个调用都依赖于之前的调用,因此它们必须连续进行。通常,网络调用将完成处理程序作为参数。可以通过嵌套的完成处理程序完成一系列调用,但这很难阅读。

作为替代方案,我一直在将进程分派到全局队列并使用 Grand Central Dispatch DispatchSemaphore 来停止直到网络查询 returns。但它似乎不是很优雅。这是一些示例代码:

let networkSemaphore = DispatchSemaphore(value: 0)

var body: some View {
    Text("Hello, world!")
        .padding()
        .onAppear { doChainOfEvents() }
}

func networkFetch(item: String, callback: @escaping (Int) -> Void) {
    print("Loading \(item)...")
    DispatchQueue.global().asyncAfter(deadline: .now() + .seconds(2)) {
        let numLoaded = Int.random(in: 1...100)
        callback(numLoaded)
        networkSemaphore.signal()
    }
    networkSemaphore.wait()
}

func doChainOfEvents() {
    DispatchQueue(label: "com.test.queue.serial").async {
        networkFetch(item: "accounts") { num in
            print("\(num) accounts loaded.")
        }
        networkFetch(item: "history") { num in
            print("\(num) messages loaded.")
        }
        networkFetch(item: "requests") { num in
            print("\(num) requests loaded")
        }
        print("Network requests complete. ✓")
    }
    print("Control flow continues while network calls operate.")
}

doChainOfEvents()的打印结果:

Control flow continues while network calls operate.
Loading accounts...   // (2 second delay)
79 accounts loaded.
Loading history...    // (2 second delay)
87 messages loaded.
Loading requests...   // (2 second delay)
54 requests loaded
Network requests complete. ✓

你能想出更优雅的方法来实现这个吗?在我看来应该有一个使用 Grand Central Dispatch 的人。我可以使用 DispatchGroup 代替信号量,但我认为它不会给我带来任何好处,并且一次只有一个项目的一组看起来很愚蠢。

使用 Combine,您可以链接一系列请求,例如,创建一个 属性 来保存 AnyCancellable 对象:

var cancellable: AnyCancellable?

然后,假设您需要“帐户”来获取“历史记录”,并且需要“历史记录”来获取“请求”,您可以这样做:

func startRequests() {
    cancellable = accountsPublisher()
        .flatMap { accounts in self.historyPublisher(for: accounts) }
        .flatMap { history in self.requestsPublisher(for: history) }
        .subscribe(on: DispatchQueue.main)
        .sink { completion in
            print(completion)
        } receiveValue: { requests in
            print(requests)
        }
}

这将 运行 发布商按顺序将值从一个传递到下一个。


这与手头的问题无关,但这是我的模型:

struct Account: Codable {
    let accountNumber: String
}

struct History: Codable {
    let history: String
}

struct Request: Codable {
    let identifier: String
}

struct ResponseObject<T: Decodable>: Decodable {
    let json: T
    let origin: String
}

func accountsPublisher() -> AnyPublisher<[Account], Error> {
    URLSession.shared
        .dataTaskPublisher(for: accountRequest)
        .map(\.data)
        .decode(type: ResponseObject<[Account]>.self, decoder: decoder)
        .map(\.json)
        .eraseToAnyPublisher()
}

func historyPublisher(for accounts: [Account]) -> AnyPublisher<History, Error> {
    URLSession.shared
        .dataTaskPublisher(for: historyRequest)
        .map(\.data)
        .decode(type: ResponseObject<History>.self, decoder: decoder)
        .map(\.json)
        .eraseToAnyPublisher()
}

func requestsPublisher(for history: History) -> AnyPublisher<[Request], Error> {
    URLSession.shared
        .dataTaskPublisher(for: requestsRequest)
        .map(\.data)
        .decode(type: ResponseObject<[Request]>.self, decoder: decoder)
        .map(\.json)
        .eraseToAnyPublisher()
}

var accountRequest: URLRequest = {
    let url = URL(string: "http://httpbin.org/post")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try! JSONEncoder().encode([Account(accountNumber: "123"), Account(accountNumber: "789")])
    return request
}()

var requestsRequest: URLRequest = {
    let url = URL(string: "http://httpbin.org/post")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try! JSONEncoder().encode([Request(identifier: "bar"), Request(identifier: "baz")])
    return request
}()

var historyRequest: URLRequest = {
    let url = URL(string: "http://httpbin.org/post")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try! JSONEncoder().encode(History(history: "foo"))
    return request
}()

现在,在上面的示例中,我使用 httpbin.org/postjson 键下向我鹦鹉学舌返回数据。显然,这不是一个现实的场景,但它说明了将各种发布者链接在一起的模式(并使用 Combine 摆脱编写命令式代码的杂草)。

所以不要迷失在杂乱无章的模型中,而是要关注此时间轴中请求的顺序性质:


或查看 示例,了解如何使用 Combine 并发执行请求,但将并发程度限制在合理范围内。只要有可能,运行 并发请求,否则,网络延迟效应会加剧并使整个过程变慢。