iOS网络协议栈原理(七) -- 不同种类的 URLProtocol 与自定义的 URLProtocol
系统内置的FTPURLProtocol
URLSession中除了支持URL scheme
是http/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
}
}
}
比较关键的三个部分就是:
override class func canInit(with request: URLRequest) -> Bool
方法需要判断url.scheme
是否为ftp
.- 另外实现, response header/response 的核心回调即可.
系统内置的DataURLProtocol
另外, 系统也实现了一个scheme
为data:
的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
时候可以借鉴:
- 如果请求(数据处理)成功, 按照以下回调方法调用:
urlClient.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
urlClient.urlProtocol(self, didLoad: decodedData)
urlClient.urlProtocolDidFinishLoading(self)
- 如果请求(数据处理)失败, 按照以下方法回调:
delegate.urlSession(session, task: task, didCompleteWithError: error)
自定义的URLProtocol
URLSession支持我们自定义一个URLProtocol
, 加入到Apple 的URL Loading System
中. 这里不展开, 可以参考Apple 官方文档和Demo, 另外有很多第三方库会使用URLProtocol做网络监控, 或者进行测试环境的Mock, 例如Flex
,CocoaDebug
, DoraemonKit
等
https://developer.apple.com/documentation/foundation/nsurlprotocol
https://developer.apple.com/library/archive/samplecode/CustomHTTPProtocol/Introduction/Intro.html
https://github.com/FLEXTool/FLEX
https://github.com/didi/DoKit/tree/master/iOS/DoraemonKit
也可以看一下之前我做的自定义URLProtocol的总结: