2017-08-28 114 views
0

我正在练习制作URLProtocol的子类。我正在使用URLSessionStreamTask来读取&。在试用这个子类的时候,我得到了一个超时。我以为我搞乱了阅读过程,但添加日志记录显示我没有通过最初的写作!为什么使用URLSessionStreamTask写入超时?

这里是我的子类的缩短版:

import Foundation 
import LoggerAPI 

class GopherUrlProtocol: URLProtocol { 

    enum Constants { 
     static let schemeName = "gopher" 
     static let defaultPort = 70 
    } 

    var innerSession: URLSession? 
    var innerTask: URLSessionStreamTask? 

    override class func canInit(with request: URLRequest) -> Bool { /*...*/ } 
    override class func canonicalRequest(for request: URLRequest) -> URLRequest { /*...*/ } 

    override func startLoading() { 
     Log.entry("Starting up a download") 
     defer { Log.exit("Started a download") } 

     precondition(innerSession == nil) 
     precondition(innerTask == nil) 

     innerSession = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: .current) 
     innerTask = innerSession?.streamTask(withHostName: (request.url?.host)!, port: request.url?.port ?? Constants.defaultPort) 
     innerTask!.resume() 
     downloadGopher() 
    } 

    override func stopLoading() { 
     Log.entry("Stopping a download") 
     defer { Log.exit("Stopped a download") } 

     innerTask?.cancel() 
     innerTask = nil 
     innerSession = nil 
    } 

} 

extension GopherUrlProtocol { 

    func downloadGopher() { 
     Log.entry("Doing a gopher download") 
     defer { Log.exit("Did a gopher download") } 

     guard let task = innerTask, let url = request.url, let path = URLComponents(url: url, resolvingAgainstBaseURL: false)?.path else { return } 

     let downloadAsText = determineTextDownload(path) 
     task.write(determineRetrievalKey(path).data(using: .isoLatin1)!, timeout: innerSession?.configuration.timeoutIntervalForRequest ?? 60) { 
      Log.entry("Responding to a write with error '\(String(describing: $0))'") 
      defer { Log.exit("Responded to a write") } 
      Log.info("Hi") 

      if let error = $0 { 
       self.client?.urlProtocol(self, didFailWithError: error) 
       return 
      } 

      var hasSentClientData = false 
      var endReadLoop = false 
      let aMinute: TimeInterval = 60 
      let bufferSize = 1024 
      let noData = Data() 
      var result = noData 
      while !endReadLoop { 
       task.readData(ofMinLength: 1, maxLength: bufferSize, timeout: self.innerSession?.configuration.timeoutIntervalForRequest ?? aMinute) { data, atEOF, error in 
        Log.entry("Responding to a read with data '\(String(describing: data))', at-EOF '\(atEOF)', and error '\(String(describing: error))'") 
        defer { Log.exit("Responded to a read") } 
        Log.info("Hello") 

        if let error = error { 
         self.client?.urlProtocol(self, didFailWithError: error) 
         endReadLoop = true 
         return 
        } 
        endReadLoop = atEOF 
        result.append(data ?? noData) 
        hasSentClientData = hasSentClientData || data != nil 
       } 
      } 
      if hasSentClientData { 
       self.client?.urlProtocol(self, didReceive: URLResponse(url: url, mimeType: downloadAsText ? "text/plain" : "application/octet-stream", expectedContentLength: result.count, textEncodingName: nil), cacheStoragePolicy: .notAllowed) // To-do: Update cache policy 
       self.client?.urlProtocol(self, didLoad: result) 
      } 
     } 
    } 

} 

extension GopherUrlProtocol: URLSessionStreamDelegate {} 

和日志:

[2017-08-28T00:52:33.861-04:00] [ENTRY] [GopherUrlProtocol.swift:39 canInit(with:)] GopherUrlProtocol checks if it can 'init' gopher://gopher.floodgap.com/ 
[2017-08-28T00:52:33.863-04:00] [EXIT] [GopherUrlProtocol.swift:41 canInit(with:)] Returning true 
[2017-08-28T00:52:33.863-04:00] [ENTRY] [GopherUrlProtocol.swift:39 canInit(with:)] GopherUrlProtocol checks if it can 'init' gopher://gopher.floodgap.com/ 
[2017-08-28T00:52:33.863-04:00] [EXIT] [GopherUrlProtocol.swift:41 canInit(with:)] Returning true 
[2017-08-28T00:52:33.864-04:00] [ENTRY] [GopherUrlProtocol.swift:54 canonicalRequest(for:)] GopherUrlProtocol canonizes gopher://gopher.floodgap.com/ 
[2017-08-28T00:52:33.864-04:00] [EXIT] [GopherUrlProtocol.swift:56 canonicalRequest(for:)] Returning gopher://gopher.floodgap.com 
[2017-08-28T00:52:33.867-04:00] [ENTRY] [GopherUrlProtocol.swift:82 startLoading()] Starting up a download 
[2017-08-28T00:52:33.868-04:00] [ENTRY] [GopherUrlProtocol.swift:112 downloadGopher()] Doing a gopher download 
[2017-08-28T00:52:33.868-04:00] [EXIT] [GopherUrlProtocol.swift:113 downloadGopher()] Did a gopher download 
[2017-08-28T00:52:33.868-04:00] [EXIT] [GopherUrlProtocol.swift:83 startLoading()] Started a download 
[2017-08-28T00:53:33.871-04:00] [ENTRY] [GopherUrlProtocol.swift:132 downloadGopher()] Responding to a write with error 'Optional(Error Domain=NSPOSIXErrorDomain Code=60 "Operation timed out" UserInfo={_kCFStreamErrorCodeKey=60, _kCFStreamErrorDomainKey=1})' 
[2017-08-28T00:53:33.871-04:00] [INFO] [GopherUrlProtocol.swift:134 downloadGopher()] Hi 
[2017-08-28T00:53:33.872-04:00] [EXIT] [GopherUrlProtocol.swift:133 downloadGopher()] Responded to a write 
[2017-08-28T00:53:33.876-04:00] [ENTRY] [GopherUrlProtocol.swift:95 stopLoading()] Stopping a download 
[2017-08-28T00:53:33.876-04:00] [ERROR] [main.swift:42 cget] Retrieval Error: Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={NSUnderlyingError=0x100e01470 {Error Domain=kCFErrorDomainCFNetwork Code=-1001 "(null)" UserInfo={_kCFStreamErrorCodeKey=-2102, _kCFStreamErrorDomainKey=4}}, NSErrorFailingURLStringKey=gopher://gopher.floodgap.com, NSErrorFailingURLKey=gopher://gopher.floodgap.com, _kCFStreamErrorDomainKey=4, _kCFStreamErrorCodeKey=-2102, NSLocalizedDescription=The request timed out.} 
[2017-08-28T00:53:33.876-04:00] [EXIT] [GopherUrlProtocol.swift:96 stopLoading()] Stopped a download 
Program ended with exit code: 11 

奇怪的是,对于书写关闭记录只出现有时。也许这是某种线程/时间问题。 (在这里,我跑了程序两次。)

我使用URLSessionStreamTask错了吗?还是URLProtocol错?或者,虽然它不是HTTP,我是否触发了ATS?

回答

1

它看起来像你排队读回调的疯狂数量在while循环和阻塞线程哪里该回调实际上将不会从while循环返回。

这个读取调用是异步的,这意味着回调里面会稍后运行 - 可能是后面。因此,你的“while not EOF”事件会阻塞调用线程,确保它永远不会返回到运行循环的顶部以允许回调运行,从而确保回调将永远无法将eof标志设置为终止while循环。基本上,你锁定了线程。

你几乎不应该在任何类型的循环中调用异步方法。相反:

  • 创建的唯一目的就是要返回块/关闭(也许getReadHandlerBlock)的方法。
  • 调用read方法,将调用返回的块传递给getReadHandlerBlock

然后,读取处理程序块的底部:看

  • 检查,如果您需要了解更多。
  • 如果是这样,请致电getReadHandlerBlockself的弱引用以获取对读取处理程序块的引用。
  • 调用读取方法。

希望有帮助。(请注意,我并不是说这是代码的唯一问题,我没有详细看过它,这只是我注意到的第一件事。)

+0

在我上床之前,我在智能手机上看到了有关此答案的通知,并显示了第一句话。几小时后我醒了过来,突然意识到这可能就是你所说的。由于呼叫是异步的,while循环的运行没有在循环中完成。我有一个具有相同问题的示例项目,并将其更改为删除循环,并将读取回调调用为下一次迭代(如果需要),而不是工作。谢谢。 – CTMacUser

+0

我已经从100%的CPU利用率和100 MB的内存,以及两个攀升到2%和12 MB。 – CTMacUser

0

我正要在不同的论坛上提出这个问题。在我的步骤中,我提到我发出请求行...

Line?用...结尾?

[查一查RFC 1436,第2章]

啊,我计算并从该URL发送的检索字符串,但忘了帽子摘下与CR-LF的请求。这通过了请求。

但现在我得到的回读超时....

+0

不久后,我意识到这一点“解决方案”实际上并不能解决最初的问题。我不知道为什么我的原始发送被破坏,也不知道为什么它现在可以工作。见@ dgatwood的答案。 – CTMacUser