简体   繁体   English

iOS Swift URLSession POST 请求因慢速 API 调用而重复

[英]iOS Swift URLSession POST request getting duplicated for slow API calls

I have a download task that work by first calling a REST API for which the server needs to generate a fairly large file which takes it several minutes to generate, as it is CPU and disk IO intensive.我有一个下载任务,首先调用 REST API 来工作,服务器需要为此生成一个相当大的文件,这需要几分钟才能生成,因为它是 CPU 和磁盘 ZCF3882F1C43AB22BFF0BD9D82D83251。 The client waits for the server to give a JSON response with the URL of the file it generated.客户端等待服务器使用它生成的文件的 URL 给出 JSON 响应。 The file download then starts after it gets the first result.文件下载在获得第一个结果后开始。

For the calls that generate a particularly large file, which causes the server to be very slow to respond, I am seeing duplicate requests that my code is not initiating.对于生成特别大文件的调用,这会导致服务器响应速度非常慢,我看到我的代码没有启动的重复请求。

Initially the someone who works on the server side told me about the duplicate requests.最初在服务器端工作的人告诉我重复的请求。 Then I set up a way to inspect network traffic.然后我设置了一种检查网络流量的方法。 This was done by setting up a Mac connected to a wired network and enabling network sharing and using Proxyman to inspect the traffic from the iPhone to the API server.这是通过设置连接到有线网络的 Mac 并启用网络共享并使用Proxyman检查从 iPhone 到 API 服务器的流量来完成的。 I see multiple instances of the same API request on the network layer but my code was never notified.我在网络层看到相同 API 请求的多个实例,但我的代码从未收到通知。

Code looks like this代码看起来像这样

@objc class OfflineMapDownloadManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {
@objc func download(){     
    
    let config = URLSessionConfiguration.background(withIdentifier: "OfflineMapDownloadSession")
    config.timeoutIntervalForRequest = 500
    config.shouldUseExtendedBackgroundIdleMode = true
    config.sessionSendsLaunchEvents = true
  
    urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)    
    getMapUrlsFromServer(bounds)
}


func getMapUrlsFromServer(){
    
    var urlString = "http://www.fake.com/DoMakeMap.php" 
    if let url = URL(string: urlString) {
        let request = NSMutableURLRequest(url: url)
        //...Real code sets up a JSON body in to params...
        request.httpBody = params.data(using: .utf8 )
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpMethod = "POST"
        request.timeoutInterval = 500
        urlSession?.configuration.timeoutIntervalForRequest = 500
        urlSession?.configuration.timeoutIntervalForResource = 500
        request.httpShouldUsePipelining = true
        let backgroundTask = urlSession?.downloadTask(with: request as URLRequest)
        backgroundTask?.countOfBytesClientExpectsToSend = Int64(params.lengthOfBytes(using: .utf8))
        backgroundTask?.countOfBytesClientExpectsToReceive = 1000
        backgroundTask?.taskDescription = "Map Url Download"
        backgroundTask?.resume()
    }
}

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {    
    if (downloadTask.taskDescription == "CTM1 Url Download") {
        do {
            let data = try Data(contentsOf: location, options: .mappedIfSafe)
            let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves)
            if let jsonResult = jsonResult as? Dictionary<String, AnyObject> {
                if let ctm1Url = jsonResult["CTM1Url"] as? String {
                    if let filesize = jsonResult["filesize"] as? Int {
                        currentDownload?.ctm1Url = URL(string: ctm1Url)
                        currentDownload?.ctm1FileSize = Int32(filesize)
                        if (Int32(filesize) == 0) {
                            postDownloadFailed()
                        } else {
                            startCtm1FileDownload(ctm1Url,filesize)
                        }
                    }
                }
            }
        } catch {
            postDownloadFailed()
        }
    }
}    

There is more to this download class as it will download the actual file once the first api call is done.此下载 class 还有更多内容,因为一旦完成第一个 api 调用,它将下载实际文件。 Since the problem happens before that code would be executed, I did not include it in the sample code.由于问题发生在该代码执行之前,因此我没有将其包含在示例代码中。

The log from Proxyman shows that the API call went out at (minutes:seconds) 46:06, 47:13, 48:21, 49:30, 50:44, 52:06, 53:45来自 Proxyman 的日志显示 API 调用在(分:秒)46:06、47:13、48:21、49:30、50:44、52:06、53:45 发出在此处输入图像描述 It looks like the request gets repeated with intervals that are just over 1 minute.看起来请求以刚刚超过 1 分钟的间隔重复。

There is an API field where I can put any value and it will be echoed back to me by the server.有一个 API 字段,我可以在其中输入任何值,服务器会将其回显给我。 I put a timestamp there generated with CACurrentMediaTime() and log in Proxyman shows that indeed its the same API call so there is no way my code is getting called multiple times.我在那里放置了一个用 CACurrentMediaTime() 生成的时间戳,并登录 Proxyman 显示它确实是相同的 API 调用,所以我的代码不可能被多次调用。 It seems as though the iOS networking layer is re-issuing the http request because the server is taking a very long time to respond.似乎 iOS 网络层正在重新发出 http 请求,因为服务器需要很长时间才能响应。 This ends up causing problems on the server and the API fails.这最终导致服务器出现问题,并且 API 失败。

Any help would be greatly appreciated.任何帮助将不胜感激。

I think the problem is in using URLSessionConfiguration.background(withIdentifier:) for this api call.我认为问题在于对这个 api 调用使用URLSessionConfiguration.background(withIdentifier:)

Use this method to initialize a configuration object suitable for transferring data files while the app runs in the background.使用此方法初始化配置 object 适合在应用程序在后台运行时传输数据文件。 A session configured with this object hands control of the transfers over to the system, which handles the transfers in a separate process.使用此 object 配置的 session 将传输控制权交给系统,该系统在单独的进程中处理传输。 In iOS, this configuration makes it possible for transfers to continue even when the app itself is suspended or terminated.在 iOS 中,即使应用程序本身暂停或终止,此配置也可以继续传输。

So the problem is that the system is retrying your request unnecessarily because of this wrong API usage.所以问题是系统正在不必要地重试您的请求,因为这个错误的 API 用法。

Here's what I recommend -这是我推荐的-

  1. Use default session configuration (NOT background).使用默认 session 配置(非背景)。
  2. Do this api call that initiates this long job, do NOT have client wait on this job, from server side return a job_id back to client as soon as this job is initiated.执行此 api 调用以启动此长时间作业,不要让客户端等待此作业,一旦启动此作业,服务器端就会将 job_id 返回给客户端。
  3. Client can now poll server every X seconds using that job_id value to know about the status of the job, even can show progress on client side if needed.客户端现在可以使用该 job_id 值每 X 秒轮询一次服务器以了解作业的状态,甚至可以在需要时在客户端显示进度。
  4. When job is completed, and client polls next time, it gets the download URL for this big file.当作业完成,客户端下次轮询时,它会为这个大文件下载 URL。
  5. Download the file (using default / background session configuration as you prefer).下载文件(根据您的喜好使用默认/后台 session 配置)。

This sounds a lot like TCP retransmission.这听起来很像 TCP 重传。 If the client sends a TCP segment, and the server does not acknowledge receipt within a short span of time, the client assumes the segment didn't make it to the destination, and it sends the segment again.如果客户端发送一个 TCP 段,并且服务器在短时间内没有确认接收,客户端认为该段没有到达目的地,并再次发送该段。 This is a significantly lower-level mechanism than URLSession.这是一个比 URLSession 低得多的机制。

It's possible the HTTP server application this API is using (think Apache, IIS, LigHTTPd, nginx, etc.) is configured to acknowledge with the response data to save packeting and framing overhead. It's possible the HTTP server application this API is using (think Apache, IIS, LigHTTPd, nginx, etc.) is configured to acknowledge with the response data to save packeting and framing overhead. If so, and if the response data takes longer than the client's TCP retransmission timeout, you will get this behavior.如果是这样,并且如果响应数据花费的时间比客户端的 TCP 重传超时时间长,您将得到此行为。

Do you have a packet capture of the connection?你有连接的数据包捕获吗? If not, try collecting one with tcpdump and reviewing it in Wireshark.如果没有,请尝试使用 tcpdump 收集一份并在 Wireshark 中查看。 If I'm right, you will see multiple requests, and they will all have the same sequence number.如果我是对的,您将看到多个请求,并且它们都将具有相同的序列号。

As for how to fix it if that's the problem, I'm not sure.至于如果这是问题如何解决它,我不确定。 The server should acknowledge requests as soon as they are received.服务器在收到请求后立即确认请求。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM