从AWS EC2下载到iOS应用程序

时间:2017-05-28 05:17:04

标签: swift amazon-ec2 https ios9 kitura

我有一个使用Kitura(http://www.kitura.io)在Swift中编写的自定义服务器,在AWS EC2服务器上运行(在Ubuntu 16.04下)。我使用CA签名的SSL证书(https://letsencrypt.org)保护它,因此我可以使用https从客户端连接到服务器。客户端在iOS(9.3)下本地运行。我在iOS上使用URLSession来连接服务器。

当我对iOS客户端进行多次大量下载时,我遇到了客户端超时问题。超时看起来像:

  

错误域= NSURLErrorDomain代码= -1001"请求超时。"   的UserInfo = {NSErrorFailingURLStringKey = https://开头,   _kCFStreamErrorCodeKey = -2102,NSErrorFailingURLKey = https://,NSLocalizedDescription =请求超时。,   _kCFStreamErrorDomainKey = 4,NSUnderlyingError = 0x7f9f23d0 {错误域= kCFErrorDomainCFNetwork代码= -1001"(null)"   UserInfo = {_ kCFStreamErrorDomainKey = 4,_kCFStreamErrorCodeKey = -2102}}}

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

失败的客户端测试是:https://github.com/crspybits/SyncServerII/blob/master/iOS/Example/Tests/Performance.swift#L53

我没有从Amazon S3下载。数据是在服务器上从另一个Web源获取的,然后通过https从EC2上运行的服务器下载到我的客户端。

例如,它需要3-4秒才能下载1.2 MB的数据,当我尝试其中10个这样的1.2 MB数据库背靠背时,其中三个会超时。使用HTTPS GET请求进行下载。

有趣的是,首先执行这些下载的测试会上传相同数据大小的内容。即,它每次以1.2 MB上传10次。我发现这些上传没有超时失败。

我的大多数请求有效,所以这似乎不是一个问题,例如,一个未正确安装的SSL证书(我已用https://www.sslshopper.com检查过)。 iOS端的不正确的https设置似乎也不是问题,我使用亚马逊的建议(https://aws.amazon.com/blogs/mobile/preparing-your-apps-for-ios-9/)在我的应用.plist中设置了NSAppTransportSecurity

思想?

UPDATE1: 我只是尝试在我的服务器上运行本地Ubuntu 16.04系统,并使用自签名SSL证书 - 其他因素保持不变。我得到了同样的问题。因此,似乎很明显,与AWS具体相关。

UPDATE2: 服务器在本地Ubuntu 16.04系统上运行,并且不使用SSL(服务器代码只需更改一行,而客户端使用http而不是https),问题是当下。下载成功发生。因此,很明显此问题确实与SSL相关。

UPDATE3: 当服务器在本地Ubuntu 16.04系统上运行,并再次使用自签名SSL证书时,我使用了一个简单的curl客户端。为了模拟我尽可能密切使用的测试,我打断了现有的iOS客户端测试,就像它开始下载一样,并使用我的curl客户端重启了在服务器上下载端点,下载相同的1.2MB文件20次。该错误复制。我的结论是问题源于iOS客户端和SSL之间的交互。

UPDATE4: 我现在有一个更简单的iOS客户端版本来重现这个问题。我将在下面复制它,但总的来说,它使用URLSession并且我看到相同的超时问题(服务器使用自签名SSL证书在我的本地Ubuntu系统上运行)。当我禁用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!))
    }
}

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

这是我的新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)")
    }
}

Update6: 我现在看到如何处理HTTP头部情况。我可以使用URLRequest的allHTTPHeaderFields属性。情况应该基本解决!

Update7: 我可能已经弄明白为什么背景技术有效:

  

后台会话创建的任何上传或下载任务都是   如果原始请求因超时而失败,则自动重试。

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

1 个答案:

答案 0 :(得分:1)

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

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

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