从 AWS EC2 下载到 iOS 应用程序时出现超时问题

Timeout Issue When Downloading from AWS EC2 to iOS app

我有一个使用 Kitura (http://www.kitura.io), running on an AWS EC2 server (under Ubuntu 16.04). I am securing it using a CA signed SSL certificate (https://letsencrypt.org) 用 Swift 编写的自定义服务器,因此我可以使用 https 从客户端连接到服务器。客户端在 iOS (9.3) 下本地运行。我在 iOS 上使用 URLSession 连接到服务器。

当我背靠背地向 iOS 客户端进行多次大型下载时,我遇到了客户端超时问题。超时看起来像:

Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={NSErrorFailingURLStringKey=https://, _kCFStreamErrorCodeKey=-2102, NSErrorFailingURLKey=https://, NSLocalizedDescription=The request timed out., _kCFStreamErrorDomainKey=4, NSUnderlyingError=0x7f9f23d0 {Error Domain=kCFErrorDomainCFNetwork Code=-1001 "(null)" UserInfo={_kCFStreamErrorDomainKey=4, _kCFStreamErrorCodeKey=-2102}}}

在服务器上,超时总是发生在代码中的同一位置——它们会导致特定的服务器请求线程阻塞并且永远不会恢复。超时发生在服务器线程调用 Kitura RouterResponse end 方法时。即,服务器线程在调用此 end 方法时阻塞。鉴于此,客户端应用程序超时也就不足为奇了。此代码是 open-source,因此我将 link 到服务器阻止的位置:https://github.com/crspybits/SyncServerII/blob/master/Server/Sources/Server/ServerSetup.swift#L146

失败的client-side测试是:https://github.com/crspybits/SyncServerII/blob/master/iOS/Example/Tests/Performance.swift#L53

我不是从 Amazon S3 之类的网站下载的。数据是从另一个网络源在服务器上获取的,然后通过 https 从 EC2 上的服务器 运行 下载到我的客户端。

例如,下载 1.2 MB 的数据需要 3-4 秒,当我连续尝试下载 10 次 1.2 MB 的数据时,其中 3 次超时。使用 HTTPS GET 请求进行下载。

一件有趣的事情是,首先进行这些下载的测试会进行相同数据大小的上传。即,它以每次 1.2 MB 的速度上传 10 次。我没有看到这些上传超时失败。

我的大部分请求 有效,所以这似乎不仅仅是 SSL 证书安装不当的问题(我已经用 https://www.sslshopper.com). Nor does it seem to be a problem with improper https setup on the iOS side, where I've got NSAppTransportSecurity setup in my app .plist using Amazon's recommendation (https://aws.amazon.com/blogs/mobile/preparing-your-apps-for-ios-9/).

想法?

更新1: 我刚刚在本地 Ubuntu 16.04 系统上使用我的服务器 运行 进行了尝试,并使用了 self-signed SSL 证书——其他因素保持不变。我遇到了同样的问题。因此,这似乎很明显 与 AWS 具体相关。

更新2: 使用本地 Ubuntu 16.04 系统上的服务器 运行,并且不使用 SSL(服务器代码中只有一行更改以及在客户端中使用 http 而不是 https),问题是存在。下载成功。因此,这个问题 似乎与 SSL 相关。

更新3: 在本地 Ubuntu 16.04 系统上使用服务器 运行,并再次使用 self-signed SSL 证书,我使用了一个简单的 curl 客户端。为了尽可能地模拟我一直在使用的测试,我在现有的 iOS 客户端测试开始开始下载时中断了它,并使用我的 curl 客户端重新启动——使用服务器上的下载端点下载相同的 1.2MB 文件 20 次。该错误 复制。我的结论是问题源于 iOS 客户端和 SSL 之间的交互。

更新4: 我现在有一个更简单版本的 iOS 客户端重现了这个问题。我会在下面复制它,但总而言之,它使用 URLSession,我看到了同样的超时问题(服务器是 运行 在我的本地 Ubuntu 系统上使用 self-signed SSL证书)。当我禁用 SSL 使用时(http 并且服务器上没有使用 SSL 证书),我没有 得到这个问题。

这是更简单的客户端:

class ViewController: UIViewController {        
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        download(10)
    }

    func download(_ count:Int) {
        if count > 0 {
            let masterVersion = 16
            let fileUUID = "31BFA360-A09A-4FAA-8B5D-1B2F4BFA5F0A"

            let url = URL(string: "http://127.0.0.1:8181/DownloadFile/?fileUUID=\(fileUUID)&fileVersion=0&masterVersion=\(masterVersion)")!
            Download.session.downloadFrom(url) {
                self.download(count - 1)
            }
        }
    }
}

// 在名为 "Download.swift":

的文件中
import Foundation

class Download : NSObject {
    static let session = Download()

    var authHeaders:[String:String]!

    override init() {
        super.init()
        authHeaders = [
            <snip: HTTP headers specific to my server>
        ]
    }

    func downloadFrom(_ serverURL: URL, completion:@escaping ()->()) {

        let sessionConfiguration = URLSessionConfiguration.default
        sessionConfiguration.httpAdditionalHeaders = authHeaders

        let session = URLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: nil)

        var request = URLRequest(url: serverURL)
        request.httpMethod = "GET"

        print("downloadFrom: serverURL: \(serverURL)")

        var downloadTask:URLSessionDownloadTask!

        downloadTask = session.downloadTask(with: request) { (url, urlResponse, error) in

            print("downloadFrom completed: url: \(String(describing: url)); error:  \(String(describing: error)); status: \(String(describing: (urlResponse as? HTTPURLResponse)?.statusCode))")
            completion()
        }

        downloadTask.resume()
    }
}

extension Download : URLSessionDelegate, URLSessionTaskDelegate /*, URLSessionDownloadDelegate */ {
    public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {
        completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
    }
}

更新5: 哇!我现在正朝着正确的方向前进!我现在有一个更简单的 iOS 客户端使用 SSL/https 并且不会导致这个问题。 @Ankit Thakur 建议进行更改:我现在使用的是 URLSessionConfiguration.background 而不是 URLSessionConfiguration.default,这似乎是它起作用的原因。我不确定为什么。这是否代表 URLSessionConfiguration.default 中的错误?例如,我的应用程序在我的测试期间没有明确进入后台。另外:我不确定如何或是否能够在我的客户端应用程序中使用这种代码模式——似乎 URLSession 的这种用法不允许您更改 httpAdditionalHeaders 创建 URLSession 之后。 URLSessionConfiguration.background 的意图似乎是 URLSession 应该在应用程序的生命周期内存在。这对我来说是个问题,因为我的 HTTP headers 可以在应用程序的单次启动期间更改。

这是我的新 Download.swift 代码。我的简单示例中的其他代码保持不变:

import Foundation

class Download : NSObject {
    static let session = Download()

    var sessionConfiguration:URLSessionConfiguration!
    var session:URLSession!
    var authHeaders:[String:String]!
    var downloadCompletion:(()->())!
    var downloadTask:URLSessionDownloadTask!
    var numberDownloads = 0

    override init() {
        super.init()
        // https://developer.apple.com/reference/foundation/urlsessionconfiguration/1407496-background
        sessionConfiguration = URLSessionConfiguration.background(withIdentifier: "MyIdentifier")

        authHeaders = [
            <snip: my headers>
        ]

        sessionConfiguration.httpAdditionalHeaders = authHeaders

        session = URLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: OperationQueue.main)
    }

    func downloadFrom(_ serverURL: URL, completion:@escaping ()->()) {
        downloadCompletion = completion

        var request = URLRequest(url: serverURL)
        request.httpMethod = "GET"

        print("downloadFrom: serverURL: \(serverURL)")

        downloadTask = session.downloadTask(with: request)

        downloadTask.resume()
    }
}

extension Download : URLSessionDelegate, URLSessionTaskDelegate, URLSessionDownloadDelegate {
    public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {
        completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
    }

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        print("download completed: location: \(location);  status: \(String(describing: (downloadTask.response as? HTTPURLResponse)?.statusCode))")
        let completion = downloadCompletion
        downloadCompletion = nil
        numberDownloads += 1
        print("numberDownloads: \(numberDownloads)")
        completion?()
    }

    // This gets called even when there was no error
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        print("didCompleteWithError: \(String(describing: error)); status: \(String(describing: (task.response as? HTTPURLResponse)?.statusCode))")
        print("numberDownloads: \(numberDownloads)")
    }
}

更新6: 我现在知道如何处理 HTTP header 情况了。我可以只使用 URLRequest 的 allHTTPHeaderFields 属性。情况应该基本解决了!

更新7: 我可能已经弄明白为什么背景技术有效了:

Any upload or download tasks created by a background session are automatically retried if the original request fails due to a timeout.

https://developer.apple.com/reference/foundation/nsurlsessionconfiguration/1408259-timeoutintervalforrequest

代码看起来很适合客户端。你会尝试 SessionConfigurationbackground 而不是 defaultlet sessionConfiguration = URLSessionConfiguration.default.

有很多场景,我发现 .background.default 工作得更好。 例如超时,GCD 支持,后台下载。

我总是喜欢使用 .background 会话配置。