web-dev-qa-db-fra.com

NSURLSession demandes simultanées avec Alamofire

Je rencontre un comportement étrange avec mon application de test. J'ai environ 50 demandes GET simultanées que j'envoie au même serveur. Le serveur est un serveur embarqué sur un petit matériel avec des ressources très limitées. Afin d'optimiser les performances de chaque requête, je configure une instance de Alamofire.Manager Comme suit:

let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
configuration.HTTPMaximumConnectionsPerHost = 2
configuration.timeoutIntervalForRequest = 30
let manager = Alamofire.Manager(configuration: configuration)

Lorsque j'envoie les requêtes avec manager.request(...) elles sont envoyées par paires de 2 (comme prévu, vérifiées avec Charles HTTP Proxy). La chose étrange cependant, c'est que toutes les demandes qui ne se sont pas terminées dans les 30 secondes suivant la première demande, sont annulées en raison du délai en même temps (même si elles n'ont pas encore été envoyées). Voici une illustration présentant le comportement:

concurrent request illustration

S'agit-il d'un comportement attendu et comment puis-je m'assurer que les demandes n'obtiendront pas le délai avant même d'être envoyées?

Merci beaucoup!

68
Hannes

Oui, c'est un comportement attendu. Une solution consiste à encapsuler vos demandes dans une sous-classe NSOperation asynchrone personnalisée, puis à utiliser le maxConcurrentOperationCount de la file d'attente des opérations pour contrôler le nombre de demandes simultanées plutôt que le paramètre HTTPMaximumConnectionsPerHost .

L'AFNetworking d'origine a fait un travail merveilleux en enveloppant les demandes dans les opérations, ce qui a rendu cela trivial. Mais l'implémentation de NSURLSession d'AFNetworking ne l'a jamais fait, pas plus qu'Alamofire.


Vous pouvez facilement envelopper le Request dans une sous-classe NSOperation. Par exemple:

class NetworkOperation: AsynchronousOperation {

    // define properties to hold everything that you'll supply when you instantiate
    // this object and will be used when the request finally starts
    //
    // in this example, I'll keep track of (a) URL; and (b) closure to call when request is done

    private let urlString: String
    private var networkOperationCompletionHandler: ((_ responseObject: Any?, _ error: Error?) -> Void)?

    // we'll also keep track of the resulting request operation in case we need to cancel it later

    weak var request: Alamofire.Request?

    // define init method that captures all of the properties to be used when issuing the request

    init(urlString: String, networkOperationCompletionHandler: ((_ responseObject: Any?, _ error: Error?) -> Void)? = nil) {
        self.urlString = urlString
        self.networkOperationCompletionHandler = networkOperationCompletionHandler
        super.init()
    }

    // when the operation actually starts, this is the method that will be called

    override func main() {
        request = Alamofire.request(urlString, method: .get, parameters: ["foo" : "bar"])
            .responseJSON { response in
                // do whatever you want here; personally, I'll just all the completion handler that was passed to me in `init`

                self.networkOperationCompletionHandler?(response.result.value, response.result.error)
                self.networkOperationCompletionHandler = nil

                // now that I'm done, complete this operation

                self.completeOperation()
        }
    }

    // we'll also support canceling the request, in case we need it

    override func cancel() {
        request?.cancel()
        super.cancel()
    }
}

Ensuite, lorsque je veux lancer mes 50 demandes, je fais quelque chose comme ceci:

let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2

for i in 0 ..< 50 {
    let operation = NetworkOperation(urlString: "http://example.com/request.php?value=\(i)") { responseObject, error in
        guard let responseObject = responseObject else {
            // handle error here

            print("failed: \(error?.localizedDescription ?? "Unknown error")")
            return
        }

        // update UI to reflect the `responseObject` finished successfully

        print("responseObject=\(responseObject)")
    }
    queue.addOperation(operation)
}

De cette façon, ces demandes seront limitées par le maxConcurrentOperationCount, et nous n'avons pas à nous soucier de l'expiration des demandes.

Voici un exemple de classe de base AsynchronousOperation, qui prend en charge le KVN associé à la sous-classe asynchrone/simultanée NSOperation:

//
//  AsynchronousOperation.Swift
//
//  Created by Robert Ryan on 9/20/14.
//  Copyright (c) 2014 Robert Ryan. All rights reserved.
//

import Foundation

/// Asynchronous Operation base class
///
/// This class performs all of the necessary KVN of `isFinished` and
/// `isExecuting` for a concurrent `NSOperation` subclass. So, to developer
/// a concurrent NSOperation subclass, you instead subclass this class which:
///
/// - must override `main()` with the tasks that initiate the asynchronous task;
///
/// - must call `completeOperation()` function when the asynchronous task is done;
///
/// - optionally, periodically check `self.cancelled` status, performing any clean-up
///   necessary and then ensuring that `completeOperation()` is called; or
///   override `cancel` method, calling `super.cancel()` and then cleaning-up
///   and ensuring `completeOperation()` is called.

public class AsynchronousOperation : Operation {

    private let stateLock = NSLock()

    private var _executing: Bool = false
    override private(set) public var isExecuting: Bool {
        get {
            return stateLock.withCriticalScope { _executing }
        }
        set {
            willChangeValue(forKey: "isExecuting")
            stateLock.withCriticalScope { _executing = newValue }
            didChangeValue(forKey: "isExecuting")
        }
    }

    private var _finished: Bool = false
    override private(set) public var isFinished: Bool {
        get {
            return stateLock.withCriticalScope { _finished }
        }
        set {
            willChangeValue(forKey: "isFinished")
            stateLock.withCriticalScope { _finished = newValue }
            didChangeValue(forKey: "isFinished")
        }
    }

    /// Complete the operation
    ///
    /// This will result in the appropriate KVN of isFinished and isExecuting

    public func completeOperation() {
        if isExecuting {
            isExecuting = false
        }

        if !isFinished {
            isFinished = true
        }
    }

    override public func start() {
        if isCancelled {
            isFinished = true
            return
        }

        isExecuting = true

        main()
    }

    override public func main() {
        fatalError("subclasses must override `main`")
    }
}

/*
 Copyright (C) 2015 Apple Inc. All Rights Reserved.
 See LICENSE.txt for this sample’s licensing information

 Abstract:
 An extension to `NSLock` to simplify executing critical code.

 From Advanced NSOperations sample code in WWDC 2015 https://developer.Apple.com/videos/play/wwdc2015/226/
 From https://developer.Apple.com/sample-code/wwdc/2015/downloads/Advanced-NSOperations.Zip
 */

import Foundation

extension NSLock {

    /// Perform closure within lock.
    ///
    /// An extension to `NSLock` to simplify executing critical code.
    ///
    /// - parameter block: The closure to be performed.

    func withCriticalScope<T>( block: (Void) -> T) -> T {
        lock()
        let value = block()
        unlock()
        return value
    }
}

Il existe d'autres variantes possibles de ce modèle, mais assurez-vous simplement que vous (a) retournez true pour asynchronous; et (b) vous publiez les isFinished et isExecuting KVN nécessaires comme indiqué dans la section Configuration des opérations pour l'exécution simultanée de le Guide de programmation des accès concurrents: files d'attente d'opérations .

122
Rob