iOS网络协议栈原理(七) -- 不同种类的 URLProtocol 与自定义的 URLProtocol

139 阅读3分钟

iOS网络协议栈原理(七) -- 不同种类的 URLProtocol 与自定义的 URLProtocol

系统内置的FTPURLProtocol

URLSession中除了支持URL schemehttp/https的协议, 也支持 ftp 协议, 得益于底层的curl支持ftp.

其中针对ftp协议的URLProtocol实现, 简单多了:

internal class _FTPURLProtocol: _NativeProtocol {

    public required init(task: URLSessionTask, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
        super.init(task: task, cachedResponse: cachedResponse, client: client)
    }

    public required init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
        super.init(request: request, cachedResponse: cachedResponse, client: client)
    }

    override class func canInit(with request: URLRequest) -> Bool {
        // TODO: Implement sftp and ftps
        guard request.url?.scheme == "ftp"
            else { return false }
        return true
    }

    override func didReceive(headerData data: Data, contentLength: Int64) -> _EasyHandle._Action {
        guard case .transferInProgress(let ts) = internalState else { fatalError("Received body data, but no transfer in progress.") }
        guard let task = task else { fatalError("Received header data but no task available.") }
        task.countOfBytesExpectedToReceive = contentLength > 0 ? contentLength : NSURLSessionTransferSizeUnknown
        do {
            let newTS = try ts.byAppendingFTP(headerLine: data, expectedContentLength: contentLength)
            internalState = .transferInProgress(newTS)
            let didCompleteHeader = !ts.isHeaderComplete && newTS.isHeaderComplete
            if didCompleteHeader {
                // The header is now complete, but wasn't before.
                didReceiveResponse()
            }
            return .proceed
        } catch {
            return .abort
        }
    }

    /// Response processing
    /// Whenever we receive a response (i.e. a complete header) from libcurl,
    /// this method gets called.
    func didReceiveResponse() {
        guard let _ = task as? URLSessionDataTask else { return }
        guard case .transferInProgress(let ts) = self.internalState else { fatalError("Transfer not in progress.") }
        guard let response = ts.response else { fatalError("Header complete, but not URL response.") }
        guard let session = task?.session as? URLSession else { fatalError() }
        switch session.behaviour(for: self.task!) {
        case .noDelegate:
            break
        case .taskDelegate:
            self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
        case .dataCompletionHandler:
            break
        case .downloadCompletionHandler:
            break
        }
    }
}

比较关键的三个部分就是:

  1. override class func canInit(with request: URLRequest) -> Bool 方法需要判断url.scheme 是否为ftp.
  2. 另外实现, response header/response 的核心回调即可.

系统内置的DataURLProtocol

另外, 系统也实现了一个schemedata:的URL数据源的解析, 称为DataURLProtocol.所谓data类型的url, 是在RFC2397中提出的, 目的是对于一些小的数据, 可以在网页中直接载入, 而不是从外部文件载入.

例如, 在html中, 如果要使用引入图像资源, 可以用以下方式:

.striped_box  
  {  
  width: 100px;  
  height: 100px;  
  background-image: url("data:image/gif;base64,R0lGODlhAwADAIAAAP///8zMzCH5BAAAAAAALAAAAAADAAMAAAIEBHIJBQA7");  
  border: 1px solid gray;  
  padding: 10px;  
  }  

针对这里的url("data:image/gif;base64,R0lGODlhAwAD...") 就能用下面的_DataURLProtocol 进行数据解析.

internal class _DataURLProtocol: URLProtocol {

    override class func canInit(with request: URLRequest) -> Bool {
        return request.url?.scheme == "data"
    }

    override class func canInit(with task: URLSessionTask) -> Bool {
        return task.currentRequest?.url?.scheme == "data"
    }

    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    override func startLoading() {
        guard let urlClient = self.client else { fatalError("No URLProtocol client set") }

        if let (response, decodedData) = decodeURI() {
            urlClient.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
            urlClient.urlProtocol(self, didLoad: decodedData)

            // 代理模式!!! - 数据加载完成, 直接调用 urlClient - didFinishLoading !!! 告知它任务结束了
            urlClient.urlProtocolDidFinishLoading(self)
        } else {
            let error = NSError(domain: NSURLErrorDomain, code: NSURLErrorBadURL)
            if let session = self.task?.session as? URLSession, let delegate = session.delegate as? URLSessionTaskDelegate,
                let task = self.task {
                delegate.urlSession(session, task: task, didCompleteWithError: error)
            }
        }
    }

    // 核心通过 URL 获取data:// 中的内容
    private func decodeURI() -> (URLResponse, Data)? {
        guard let url = self.request.url else {
            return nil
        }
        // dataBody 的后面是通过 absoluteString 获取的
        let dataBody = url.absoluteString
        guard dataBody.hasPrefix("data:") else {
            return nil
        }

        let startIdx = dataBody.index(dataBody.startIndex, offsetBy: 5)
        var iterator = _PercentDecoder(subString: dataBody[startIdx...])

        var mimeType: String?
        var charSet: String?
        var base64 = false

        // Simple validation that the mime type has only one '/' and its not at the start or end.
        func validate(mimeType: String) -> Bool {
            ...
        }

        // Determine optional mime type, optional charset and whether ;base64 flag is just before a comma.
        func decodeHeader() -> Bool {
            ...
        }

        // Convert any percent encoding to bytes then pass the whole String to be Base64 decoded.
        // Let the Base64 decoder take care of input validation.
        func decodeBase64Body() -> Data? {
            ...
        }

        // Convert any percent encoding to bytes and append to a `Data` instance. The bytes may
        // be valid in the specified charset in the header and not necessarily UTF-8.
        func decodeStringBody() -> Data? {
            ...
        }

        guard decodeHeader() else { return nil }
        guard let decodedData = base64 ? decodeBase64Body() : decodeStringBody() else {
            return nil
        }

        let response = URLResponse(url: url, mimeType: mimeType, expectedContentLength: decodedData.count, textEncodingName: charSet)
        return (response, decodedData)
    }

    // Nothing to do here.
    override func stopLoading() {
    }
}

以上源码中的核心代码是func decodeURI() -> (URLResponse, Data)?, 具体的解析过程, 这里就不深入解释了.

另外, 这里的实现方式也让我们在自定义 URLProtocol 时候可以借鉴:

  1. 如果请求(数据处理)成功, 按照以下回调方法调用:
    1. urlClient.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
    2. urlClient.urlProtocol(self, didLoad: decodedData)
    3. urlClient.urlProtocolDidFinishLoading(self)
  2. 如果请求(数据处理)失败, 按照以下方法回调:
    1. delegate.urlSession(session, task: task, didCompleteWithError: error)

自定义的URLProtocol

URLSession支持我们自定义一个URLProtocol, 加入到Apple 的URL Loading System中. 这里不展开, 可以参考Apple 官方文档和Demo, 另外有很多第三方库会使用URLProtocol做网络监控, 或者进行测试环境的Mock, 例如Flex,CocoaDebug, DoraemonKit

  1. https://developer.apple.com/documentation/foundation/nsurlprotocol
  2. https://developer.apple.com/library/archive/samplecode/CustomHTTPProtocol/Introduction/Intro.html
  3. https://github.com/FLEXTool/FLEX
  4. https://github.com/didi/DoKit/tree/master/iOS/DoraemonKit

也可以看一下之前我做的自定义URLProtocol的总结:

  1. iOS中NSURLProtocol 的简单研究