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

标签 swift amazon-ec2 https ios9 kitura

我有一个用Swift编写的自定义服务器,使用Kitura(http://www.kitura.io),运行在AWS EC2服务器上(在Ubuntu 16.04下)。我使用CA签名的SSL证书(https://letsencrypt.org)来保护它,因此我可以使用https从客户端连接到服务器。客户端在iOS(9.3)下本机运行。我使用iOS上的URLSession连接到服务器。
当我背对背地向iOS客户端进行多个大型下载时,我遇到了客户端超时问题。超时看起来像:
Error Domain=NSURLErrorDomain Code=-1001“请求超时。”
UserInfo={NSErrorFailingURLStringKey=https://,
_kCFStreamErrorCodeKey=-2102,NSErrorFailingURLKey=https://,NSLocalizedDescription=请求超时。,
_kCFStreamErrorDomainKey=4,NSUnderlyingError=0x7f9f23d0{错误域=kcferrordomaincfn网络代码=-1001“(空)”
用户信息={u kCFStreamErrorDomainKey=4,{u kCFStreamErrorCodeKey=-2102}}
在服务器上,超时总是发生在代码中的同一个位置——它们会导致特定的服务器请求线程阻塞并永远无法恢复。当服务器线程调用KituraRouterResponseend方法时发生超时。也就是说,服务器线程在调用此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
我不是从亚马逊S3下载的。数据在服务器上从另一个web源获取,然后通过https从EC2上运行的服务器下载到我的客户机。
举个例子,下载1.2 MB的数据需要3-4秒,当我尝试背靠背下载其中10个1.2 MB时,其中3个超时。下载使用HTTPS GET请求进行。
有趣的是,首先进行这些下载的测试会上传相同大小的数据。也就是说,它以1.2 MB的速度进行10次上传。我看到上传没有超时失败。
我的大多数请求都能正常工作,所以这看起来并不是简单的问题,比如说,不正确安装的SSL证书(我已经用https://www.sslshopper.com检查过了)。iOS端的https设置不正确似乎也不是问题,我的app.plist中使用了Amazon的推荐(https://aws.amazon.com/blogs/mobile/preparing-your-apps-for-ios-9/)设置了NSAppTransportSecurity
思想?
更新1:
我只是在本地Ubuntu16.04系统上运行服务器,并使用自签名的SSL证书——其他因素保持不变。我也遇到同样的问题。因此,很明显这与AWS没有特别的关系。
更新2:
由于服务器运行在本地Ubuntu 16.04系统上,并且没有使用SSL(只是服务器代码中的一行更改,并且在客户端使用http而不是https),因此不存在此问题。下载成功。所以,这个问题显然与SSL有关。
更新3:
服务器运行在本地Ubuntu 16.04系统上,并再次使用自签名SSL证书,我使用了一个简单的curl客户端。为了模拟我一直尽可能地使用的测试,我中断了现有的iOS客户端测试,就像它开始下载一样,并使用我的curl客户端重新启动,它使用服务器上的下载端点下载相同的1.2MB文件20次。错误没有复制。我的结论是,问题源于iOS客户端和SSL之间的交互。
更新4:
我现在有一个更简单的版本的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!))
    }
}

更新5:
呼!我正在朝着正确的方向前进!我现在有了一个使用SSL/https的更简单的iOS客户端,并且没有引起这个问题。这个改变是由@Ankit Thakur建议的:我现在使用的是URLSessionConfiguration.background而不是URLSessionConfiguration.default,这似乎是使这个工作的原因。但我不知道为什么。这是否表示URLSessionConfiguration.default中存在错误?例如,在我的测试过程中,我的应用程序没有明确地进入后台。另外:我不确定如何或者是否能够在我的客户端应用程序中使用这种代码模式——似乎这种URLSession的用法不允许在创建URLSession之后更改httpAdditionalHeaders。似乎URLSessionConfiguration.background的意图是URLSession应该在应用程序的生命周期内存在。这对我来说是个问题,因为我的HTTP头可以在应用程序的一次启动期间更改。
这是我的新下载.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头的情况。我可以使用URLRequest的allHTTPHeaderFields属性。情况应该基本解决!
更新7:
我可能已经弄明白了背景技术的作用:
后台会话创建的任何上载或下载任务都是
如果原始请求因超时而失败,则自动重试。
https://developer.apple.com/reference/foundation/nsurlsessionconfiguration/1408259-timeoutintervalforrequest

最佳答案

代码适合客户端。你会试着用SessionConfigurationbackground而不是default吗。let sessionConfiguration = URLSessionConfiguration.default
有很多场景,我发现.background.default工作得更好。
例如超时、GCD支持、后台下载。
我总是喜欢使用.background会话配置。

关于swift - 从AWS EC2下载到iOS应用程序时出现超时问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44224048/

相关文章:

nginx - nginx 前端后面的 Artifactory pro 服务器

ios - 无法控制表单中的动画?

ios - 无法在 Xcode 中看到 Cocos2d 类的自动完成

amazon-ec2 - 我可以创建包含多个 ebs 卷(即 sda 和 sdb)的 AMI

facebook - 正在开发应用程序,不会在 iOS 原生 FB 应用程序中运行

ssl - 通过 HTTPS,为什么浏览器相信它正在与它真正想要的服务器通信?

swift - NSCoder 无法解码 Float

ios - RestKit JSON 映射

java - 如何创建安全组以允许java代码中的所有端口

linux - 使用 scp 上传到附加/安装的 EBS 卷?