我有一个用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}}
在服务器上,超时总是发生在代码中的同一个位置——它们会导致特定的服务器请求线程阻塞并永远无法恢复。当服务器线程调用KituraRouterResponse
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
我不是从亚马逊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
最佳答案
代码适合客户端。你会试着用SessionConfiguration
来background
而不是default
吗。let sessionConfiguration = URLSessionConfiguration.default
。
有很多场景,我发现.background
比.default
工作得更好。
例如超时、GCD支持、后台下载。
我总是喜欢使用.background
会话配置。
关于swift - 从AWS EC2下载到iOS应用程序时出现超时问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44224048/