本篇講解跟上傳數據相關的多表單html
我相信應該有很多的開發者不明白多表單是怎麼一回事,然而事實上,多表單確實很簡單。試想一下,若是有多個不一樣類型的文件(png/txt/mp3/pdf等等)須要上傳給服務器,你打算怎麼辦?若是你一個一個的上傳,那我無話可說,可是若是你想一次性上傳,那麼就要考慮服務端如何識別這些不一樣類型的數據呢?編程
服務端對不一樣類型數據的識別解決方案就是多表單。客戶端與服務端共同制定一套規範,彼此使用該規則交換數據就徹底ok了,swift
在本篇中我會帶來多表單的格式說明和實現多表單的過程的說明,我會在整個解讀過程當中,先給出設計思想,而後再講解源碼。數組
咱們先看一個多變單的格式例子:安全
POST / HTTP/1.1 [[ Less interesting headers ... ]] Content-Type: multipart/form-data; boundary=735323031399963166993862150 Content-Length: 834 --735323031399963166993862150 Content-Disposition: form-data; name="text1" text default 735323031399963166993862150 Content-Disposition: form-data; name="text2" aωb 735323031399963166993862150 Content-Disposition: form-data; name="file1"; filename="a.txt" Content-Type: text/plain Content of a.txt. 735323031399963166993862150 Content-Disposition: form-data; name="file2"; filename="a.html" Content-Type: text/html <!DOCTYPE html><title>Content of a.html.</title> 735323031399963166993862150 Content-Disposition: form-data; name="file3"; filename="binary" Content-Type: application/octet-stream aωb 735323031399963166993862150--
經過上邊的內容,咱們能夠分析出來下邊的幾點知識:服務器
Content-Type: multipart/form-data; boundary=735323031399963166993862150
經過Content-Type來講明當前數據的類型爲multipart/form-data,這樣服務器就知道客戶端將要發送的數據是多表單了。多表單說白了就是把各類數據拼接起來,要想區分數據,必須添加一個界限標識符。所以經過boundary設置邊界。這些設置不能省略Content-Length: 834
告訴服務端數據的總長度,你們留意一下這個字段,在後邊的代碼中會有一個屬性來提供這個數據,咱們最終上傳的數據都是二進制流,所以知道獲取到Data就能計算大小--735323031399963166993862150
735323031399963166993862150咱們已經知道它表示的是邊界。若是在前邊添加了--
就表示是多表單的開始邊界,與之對應的是735323031399963166993862150--
Content-Disposition: form-data; name="file1"; filename="a.txt"
對內容的進一步說明Content-Type: text/html
表示對錶單內該數據的類型的說明735323031399963166993862150--
結束邊界上邊的例子只是演示了一個比較簡單的表單樣式,表單中嵌套表單也有可能。在實際開發處理中,須要根據不一樣的組成部分獲取Data,最後拼接成一個總體的Data。網絡
整體上咱們須要拼接出像上邊示例中的結構的數據,所以咱們把這些步驟進行拆分:閉包
關於邊界,經過上邊的分析,咱們知道有3中類型的邊界:app
所以設計一個枚舉來封裝邊界類型:dom
enum BoundaryType { case initial, encapsulated, final }
除了邊界的類型以外,咱們要生成邊界字符串,一般該字符串採用隨機生成的方式:
static func randomBoundary() -> String { return String(format: "alamofire.boundary.%08x%08x", arc4random(), arc4random()) }
上邊的代碼有一個小的知識點,%08x爲整型以16進制方式輸出的格式字符串,會把後續對應參數的整型數字,以16進制輸出。08的含義爲,輸出的16進制值佔8位,不足部分左側補0。因而,若是執行printf("0x%08x", 0x1234);會輸出0x00001234。
由於最終上傳的數據是Data類型,所以須要一個轉換函數,把邊界轉換成Data類型:
static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data { let boundaryText: String switch boundaryType { case .initial: boundaryText = "--\(boundary)\(EncodingCharacters.crlf)" case .encapsulated: boundaryText = "\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)" case .final: boundaryText = "\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)" } return boundaryText.data(using: String.Encoding.utf8, allowLossyConversion: false)! }
在Alamofire中,上邊的代碼組成了BoundaryGenerator,表示邊界生產者。上邊代碼中用到了EncodingCharacters.crlf,其實它是對"\r\n"
的一個封裝,表示換行回車的意思。
針對多表單中的內一個表單也須要作一個封裝成一個對象,其內部須要做出下邊這些說明:
headers: HTTPHeaders
這個是對數據的描述bodyStream: InputStream
數據來源,Alamofire中使用InputStream統一進行處理bodyContentLength: UInt64
該數據的大小hasInitialBoundary = false
是否包含初始邊界hasFinalBoundary = false
是否包含結束邊界所以設計的代碼以下:
/// 對每個body部分的描述,這個類只能在MultipartFormData內部訪問,外部沒法訪問 class BodyPart { let headers: HTTPHeaders let bodyStream: InputStream let bodyContentLength: UInt64 var hasInitialBoundary = false var hasFinalBoundary = false init(headers: HTTPHeaders, bodyStream: InputStream, bodyContentLength: UInt64) { self.headers = headers self.bodyStream = bodyStream self.bodyContentLength = bodyContentLength } }
MultipartFormData被設計爲一個對象,在SessionManager.swift那一篇文章中咱們會介紹MultipartFormData的具體用法。總之,MultipartFormData必須給咱們提供一下的幾個功能:
接下來,咱們就跟着上邊這些設計思想來一步一步的分析核心代碼的來源。
公開或者私有的屬性有下邊幾個:
open var contentType: String { return "multipart/form-data; boundary=\(boundary)" } /// The content length of all body parts used to generate the `multipart/form-data` not including the boundaries. /// 這裏的0表示初始值,$0表示計算結果類型,$1表示數組元素類型 public var contentLength: UInt64 { return bodyParts.reduce(0) { $0 + $1.bodyContentLength } } /// The boundary used to separate the body parts in the encoded form data. public let boundary: String private var bodyParts: [BodyPart] private var bodyPartError: AFError? private let streamBufferSize: Int
咱們對他們作一些簡單的說明:
contentType: String
咱們在上邊已經詳細講過這個屬性了contentLength: UInt64
獲取數據的大小,該屬性是一個計算屬性boundary: String
表示邊界,在初始化中會使用BoundaryGenerator來生成一個邊界字符串bodyParts: [BodyPart]
是一個集合,包含了每個數據的封裝對象BodyPartbodyPartError: AFError?
streamBufferSize: Int
設置stream傳輸的buffer大小初始化方法就一個:
/// Creates a multipart form data object. /// /// - returns: The multipart form data object. public init() { self.boundary = BoundaryGenerator.randomBoundary() self.bodyParts = [] /// /// The optimal read/write buffer size in bytes for input and output streams is 1024 (1KB). For more /// information, please refer to the following article: /// - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Streams/Articles/ReadingInputStreams.html /// self.streamBufferSize = 1024 }
咱們想象一下,若是有不少種不一樣類型的文件要拼接到一個對象中,該怎麼辦?咱們分析一下:
首先應該考慮輸入源的問題,由於在開發中可能使用的輸入源有3種
明確了數據的輸入源以後,咱們還要考慮提供哪些參數來描述這些數據,這頗有必要,好比只傳遞一個Data,服務端根本不知道應該如何解析它。根據不一樣的需求,須要提供一下參數:
name
與數據相關的名字mimeType
表示數據的類型fileName
表示數據的文件名稱,length
表示數據大小stream
表示輸入流headers
數據的headers根據第二步中的參數設計函數,函數的目的就是把每一條數據封裝成BodyPart對象,而後拼接到bodyParts數組中
經過上邊的分析呢,咱們接下來的任務就是設計各類包含不一樣參數的函數。結合上邊第一步和第二步的內容,咱們分析後的結果以下:
由BodyPart的初始化方法init(headers: HTTPHeaders, bodyStream: InputStream, bodyContentLength: UInt64)
,咱們知道,給出headers
,stream
和length
咱們就能生成BodyPart對象,而後把它拼接到數組中就好了,所以該函數已經設計ok
public func append(_ stream: InputStream, withLength length: UInt64, headers: HTTPHeaders) { let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length) bodyParts.append(bodyPart) }
其實,到此爲止,咱們正處在一個編程中很是經典的概念中。你們能夠本身去了解尾調函數的概念。那麼如今要設計一個包含最多參數的函數,這個函數會成爲其餘函數的內部實現基礎。咱們把headers
這個參數去掉,這個參數能夠根據name
,mimeType
,fileName
計算出來,所以有了下邊的函數:
public func append( _ stream: InputStream, withLength length: UInt64, name: String, fileName: String, mimeType: String) { let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType) append(stream, withLength: length, headers: headers) }
這裏邊出現了一個陌生的函數contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
,它的功能是根據name
,mimeType
,fileName
計算出headers
,其內部實現以下:
private func contentHeaders(withName name: String, fileName: String? = nil, mimeType: String? = nil) -> [String: String] { var disposition = "form-data; name=\"\(name)\"" if let fileName = fileName { disposition += "; filename=\"\(fileName)\"" } var headers = ["Content-Disposition": disposition] if let mimeType = mimeType { headers["Content-Type"] = mimeType } return headers }
上邊的函數仍是太麻煩,那麼咱們開始考慮若是我傳入的數據是個Data類型呢?能對Data進行描述的有3個參數:name
,mimeType
,fileName
,所以咱們會設計3個函數,首先是設計參數最多的函數,做爲其餘兩個函數的內部實現基礎:
public func append(_ data: Data, withName name: String, fileName: String, mimeType: String) { let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType) let stream = InputStream(data: data) let length = UInt64(data.count) append(stream, withLength: length, headers: headers) }
在上邊咱們已經介紹過了contentHeaders函數的做用,上邊的代碼中,根據data生成InputStream和length是關鍵。接下來咱們把參數減小一個,先減小fileName,由於fileName是一個可選的參數:
public func append(_ data: Data, withName name: String, mimeType: String) { let headers = contentHeaders(withName: name, mimeType: mimeType) let stream = InputStream(data: data) let length = UInt64(data.count) append(stream, withLength: length, headers: headers) }
咱們在去掉一個參數:mimeType,mimeType也是一個可選的參數:
public func append(_ data: Data, withName name: String) { let headers = contentHeaders(withName: name) let stream = InputStream(data: data) let length = UInt64(data.count) append(stream, withLength: length, headers: headers) }
對於處理Data類型的數據的函數已經寫完了,接下來咱們繼續設計fileURL類型數據的處理函數。首先就是包含name
,mimeType
,fileName
和fileURL
。
public func append(_ fileURL: URL, withName name: String, fileName: String, mimeType: String) { let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType) //============================================================ // Check 1 - is file URL? //============================================================ guard fileURL.isFileURL else { setBodyPartError(withReason: .bodyPartURLInvalid(url: fileURL)) return } //============================================================ // Check 2 - is file URL reachable? //============================================================ do { let isReachable = try fileURL.checkPromisedItemIsReachable() guard isReachable else { setBodyPartError(withReason: .bodyPartFileNotReachable(at: fileURL)) return } } catch { setBodyPartError(withReason: .bodyPartFileNotReachableWithError(atURL: fileURL, error: error)) return } //============================================================ // Check 3 - is file URL a directory? //============================================================ var isDirectory: ObjCBool = false let path = fileURL.path guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) && !isDirectory.boolValue else { setBodyPartError(withReason: .bodyPartFileIsDirectory(at: fileURL)) return } //============================================================ // Check 4 - can the file size be extracted? //============================================================ let bodyContentLength: UInt64 do { guard let fileSize = try FileManager.default.attributesOfItem(atPath: path)[.size] as? NSNumber else { setBodyPartError(withReason: .bodyPartFileSizeNotAvailable(at: fileURL)) return } bodyContentLength = fileSize.uint64Value } catch { setBodyPartError(withReason: .bodyPartFileSizeQueryFailedWithError(forURL: fileURL, error: error)) return } //============================================================ // Check 5 - can a stream be created from file URL? //============================================================ guard let stream = InputStream(url: fileURL) else { setBodyPartError(withReason: .bodyPartInputStreamCreationFailed(for: fileURL)) return } append(stream, withLength: bodyContentLength, headers: headers) }
上邊的函數很長,可是思想很簡單,根據fileURL生成InputStream,但其中對可能出現的錯誤的處理,值得咱們學習,我用黑色粗色的字體來記錄。
fileURL.isFileURL
判斷fileURL是否是一個file的URLfileURL.checkPromisedItemIsReachable()
判斷該fileURL是否是可達的FileManager.default.attributesOfItem(atPath: path)[.size] as? NSNumber
判斷fileURL指定的文件能不能被讀取InputStream(url: fileURL)
判斷能不能經過fileURL建立InputStream綜上所述,當須要把文件寫入fileURL中,或者從fileURL中讀取數據時,必定要像上邊那樣對全部可能出錯的狀況作出處理。
經過上一小節的append方法,咱們已經可以把數據拼接到bodyParts數組中了,接下來考慮的是怎麼數組中的模型拼接成一個完整的Data。
這裏有一個編碼的小技巧,必須先檢測有沒有錯誤發生,若是有錯誤發生,那麼就不必繼續encode了。
public func encode() throws -> Data { if let bodyPartError = bodyPartError { throw bodyPartError } var encoded = Data() bodyParts.first?.hasInitialBoundary = true bodyParts.last?.hasFinalBoundary = true for bodyPart in bodyParts { let encodedData = try encode(bodyPart) encoded.append(encodedData) } return encoded }
上邊的代碼作了3件事:
上邊的函數出現了一個新的函數;encode(_ bodyPart: BodyPart) throws -> Data
private func encode(_ bodyPart: BodyPart) throws -> Data { var encoded = Data() let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData() encoded.append(initialData) let headerData = encodeHeaders(for: bodyPart) encoded.append(headerData) let bodyStreamData = try encodeBodyStream(for: bodyPart) encoded.append(bodyStreamData) if bodyPart.hasFinalBoundary { encoded.append(finalBoundaryData()) } return encoded }
上邊的代碼作了四件事:
在上邊的函數中出現了5個輔助函數:
initialBoundaryData()
生成開始邊界Data
private func initialBoundaryData() -> Data { return BoundaryGenerator.boundaryData(forBoundaryType: .initial, boundary: boundary) }
encapsulatedBoundaryData()
生成內容中間的邊界Data
private func encapsulatedBoundaryData() -> Data { return BoundaryGenerator.boundaryData(forBoundaryType: .encapsulated, boundary: boundary) }
finalBoundaryData()
生成結束邊界Data
private func finalBoundaryData() -> Data { return BoundaryGenerator.boundaryData(forBoundaryType: .final, boundary: boundary) }
encodeHeaders(for bodyPart: BodyPart) -> Data
生成headerData
private func encodeHeaders(for bodyPart: BodyPart) -> Data { var headerText = "" for (key, value) in bodyPart.headers { headerText += "\(key): \(value)\(EncodingCharacters.crlf)" } headerText += EncodingCharacters.crlf return headerText.data(using: String.Encoding.utf8, allowLossyConversion: false)! }
encodeBodyStream(for bodyPart: BodyPart) throws -> Data
生成數據Data
private func encodeBodyStream(for bodyPart: BodyPart) throws -> Data { let inputStream = bodyPart.bodyStream inputStream.open() defer { inputStream.close() } var encoded = Data() while inputStream.hasBytesAvailable { var buffer = [UInt8](repeating: 0, count: streamBufferSize) let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize) if let error = inputStream.streamError { throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: error)) } if bytesRead > 0 { encoded.append(buffer, count: bytesRead) } else { break } } return encoded }
上邊的代碼中有兩點須要注意,defer { inputStream.close() }
能夠定義代碼塊結束後執行的語句,經過while讀取stream中數據的典型代碼。
在Alamofire中,若是編碼後的數據超過了某個值,就會把該數據寫入到fileURL中,在發送請求的時候,在fileURL中讀取數據上傳。
public func writeEncodedData(to fileURL: URL) throws { if let bodyPartError = bodyPartError { throw bodyPartError } if FileManager.default.fileExists(atPath: fileURL.path) { throw AFError.multipartEncodingFailed(reason: .outputStreamFileAlreadyExists(at: fileURL)) } else if !fileURL.isFileURL { throw AFError.multipartEncodingFailed(reason: .outputStreamURLInvalid(url: fileURL)) } guard let outputStream = OutputStream(url: fileURL, append: false) else { throw AFError.multipartEncodingFailed(reason: .outputStreamCreationFailed(for: fileURL)) } outputStream.open() /// 新的 defer 關鍵字爲此提供了安全又簡單的處理方式:聲明一個 block,當前代碼執行的閉包退出時會執行該 block。 defer { outputStream.close() } self.bodyParts.first?.hasInitialBoundary = true self.bodyParts.last?.hasFinalBoundary = true for bodyPart in self.bodyParts { try write(bodyPart, to: outputStream) } }
上邊的代碼在檢查完錯誤後,建立了一個outputStream,經過這個outputStream來把數據寫到fileURL中。
注意,經過上邊的函數能夠看出,Alamofire並無使用上邊的encode函數來生成一個Data,而後再寫入fileURL。這是由於大文件每每咱們是經過append(fileURL)方式拼接進來的,並無把數據加載到內存。
上邊的代碼中出現了一個輔助函數write(bodyPart, to: outputStream)
private func write(_ bodyPart: BodyPart, to outputStream: OutputStream) throws { try writeInitialBoundaryData(for: bodyPart, to: outputStream) try writeHeaderData(for: bodyPart, to: outputStream) try writeBodyStream(for: bodyPart, to: outputStream) try writeFinalBoundaryData(for: bodyPart, to: outputStream) }
該函數出現了4個輔助函數:
private func writeInitialBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws { let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData() return try write(initialData, to: outputStream) } private func writeHeaderData(for bodyPart: BodyPart, to outputStream: OutputStream) throws { let headerData = encodeHeaders(for: bodyPart) return try write(headerData, to: outputStream) } private func writeBodyStream(for bodyPart: BodyPart, to outputStream: OutputStream) throws { let inputStream = bodyPart.bodyStream inputStream.open() defer { inputStream.close() } while inputStream.hasBytesAvailable { var buffer = [UInt8](repeating: 0, count: streamBufferSize) let bytesRead = inputStream.read(&buffer, maxLength: streamBufferSize) if let streamError = inputStream.streamError { throw AFError.multipartEncodingFailed(reason: .inputStreamReadFailed(error: streamError)) } if bytesRead > 0 { if buffer.count != bytesRead { buffer = Array(buffer[0..<bytesRead]) } try write(&buffer, to: outputStream) } else { break } } } private func writeFinalBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws { if bodyPart.hasFinalBoundary { return try write(finalBoundaryData(), to: outputStream) } }
因爲上邊函數的思想咱們在文章中都講過了,這裏就不提了。除了上邊的函數,還有兩個寫數據的輔助函數:
private func write(_ data: Data, to outputStream: OutputStream) throws { var buffer = [UInt8](repeating: 0, count: data.count) data.copyBytes(to: &buffer, count: data.count) return try write(&buffer, to: outputStream) } private func write(_ buffer: inout [UInt8], to outputStream: OutputStream) throws { var bytesToWrite = buffer.count while bytesToWrite > 0, outputStream.hasSpaceAvailable { let bytesWritten = outputStream.write(buffer, maxLength: bytesToWrite) if let error = outputStream.streamError { throw AFError.multipartEncodingFailed(reason: .outputStreamWriteFailed(error: error)) } bytesToWrite -= bytesWritten if bytesToWrite > 0 { buffer = Array(buffer[bytesWritten..<buffer.count]) } } }
對於上邊的函數,你們瞭解下就好了。那麼到這裏爲止,MultipartFormData咱們就已經分析完成了。
上邊漏掉了下邊這一個函數:
private func mimeType(forPathExtension pathExtension: String) -> String { if let id = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue(), let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?.takeRetainedValue() { return contentType as String } /// 若是是一個二進制文件,一般遇到這種類型,軟件丟回提示使用其餘程序打開 return "application/octet-stream" }
當Content-Type使用了application/octet-stream時,每每客戶端就會給出使用其餘程序打開的提示。你們平時有沒有見過這種狀況呢?
因爲知識水平有限,若有錯誤,還望指出
Alamofire源碼解讀系列(一)之概述和使用 簡書-----博客園
Alamofire源碼解讀系列(二)之錯誤處理(AFError) 簡書-----博客園
Alamofire源碼解讀系列(三)之通知處理(Notification) 簡書-----博客園
Alamofire源碼解讀系列(四)之參數編碼(ParameterEncoding) 簡書-----博客園
Alamofire源碼解讀系列(五)之結果封裝(Result) 簡書-----博客園
Alamofire源碼解讀系列(六)之Task代理(TaskDelegate) 簡書-----博客園
Alamofire源碼解讀系列(七)之網絡監控(NetworkReachabilityManager) 簡書-----博客園
Alamofire源碼解讀系列(八)之安全策略(ServerTrustPolicy) 簡書-----博客園