Alamofire(6)— 多表單上傳

😊😊😊Alamofire專題目錄,歡迎及時反饋交流 😊😊😊安全


Alamofire 目錄直通車 --- 和諧學習,不急不躁!網絡


實際開發過程當中,多表單上傳是很是重要的一種請求!服務端一般是根據請求頭(headers)中的 Content-Type 字段來獲知請求中的消息主體是用何種方式編碼,再對主體進行解析。 因此說到 POST 提交數據方案,包含了 Content-Type 和消息主體編碼方式兩部分。 這個篇章咱們來探索一下 多表單上傳文件 ~session

1、多表單格式

下面我經過 Charles 抓包上傳圖片的接口閉包

  • --alamofire.boundary.4e076f46186e231d: 是分隔符,爲了方便讀取數據
  • Content-Disposition: form-data; name="name": 其中 Content-dispositionMIME 協議的擴展,MIME 協議指示 MIME 用戶代理如何顯示附加的文件。Content-disposition 其實能夠控制用戶請求所得的內容存爲一個文件的時候提供一個默認的文件名,這裏就是添加了一個 key = name
  • 接在後面就是 \r\n 換行符
  • 而後就是 key 對應的 value = LGCooci
  • 最下面的亂碼是圖片data數據

Multipart 格式顯示整個數據就相似字典的 key-valueapp

2、咱們經過URLSeesion去請求多表單

1️⃣:分隔符初始化

init() {
 self.boundary = NSUUID().uuidString
}
複製代碼
  • 利用 NSUUID().uuidString 設定爲分隔符

2️⃣:換行符號

extension CharacterSet {
    static func MIMECharacterSet() -> CharacterSet {
        let characterSet = CharacterSet(charactersIn: "\"\n\r")
        return characterSet.inverted
    }
}
複製代碼

3️⃣: 數據格式處理&拼接

public func appendFormData(_ name: String, content: Data, fileName: String, contentType: String) {
    
    let contentDisposition = "Content-Disposition: form-data; name=\"\(self.encode(name))\"; filename=\"\(self.encode(fileName))\""
    let contentTypeHeader = "Content-Type: \(contentType)"
    let data = self.merge([
        self.toData(contentDisposition),
        MutlipartFormCRLFData,
        self.toData(contentTypeHeader),
        MutlipartFormCRLFData,
        MutlipartFormCRLFData,
        content,
        MutlipartFormCRLFData
        ])
    self.fields.append(data)
}
複製代碼

4️⃣:數據處理完畢,而後設置httpBody

public extension URLRequest {
    mutating func setMultipartBody(_ data: Data, boundary: String) {
        self.httpMethod = "POST"
        self.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        self.httpBody = data
        self.setValue(String( data.count ), forHTTPHeaderField: "Content-Length")
        self.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
    }
}
複製代碼

5️⃣:多表單格式封裝,以及使用

public extension URLRequest {
    mutating func setMultipartBody(_ data: Data, boundary: String) {
        self.httpMethod = "POST"
        self.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        self.httpBody = data
        self.setValue(String( data.count ), forHTTPHeaderField: "Content-Length")
        self.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
    }
}

// 換行符處理
extension CharacterSet {
    static func MIMECharacterSet() -> CharacterSet {
        let characterSet = CharacterSet(charactersIn: "\"\n\r")
        return characterSet.inverted
    }
}
// 多表單工廠器
struct LGMultipartDataBuilder{
    var fields: [Data] = []
    public let boundary: String
    // 初始化 - 分隔符建立
    init() {
        self.boundary = NSUUID().uuidString
    }
    // 全部數據格式處理
    func build() -> Data? {
        let data = NSMutableData()
        
        for field in self.fields {
            data.append(self.toData("--\(self.boundary)"))
            data.append(MutlipartFormCRLFData)
            data.append(field)
        }
        data.append(self.toData("--\(self.boundary)--"))
        data.append(MutlipartFormCRLFData)
        
        return (data.copy() as! Data)
    }
    // 數據格式key value拼接
    mutating public func appendFormData(_ key: String, value: String) {
        let content = "Content-Disposition: form-data; name=\"\(encode(key))\""
        let data = self.merge([
            self.toData(content),
            MutlipartFormCRLFData,
            MutlipartFormCRLFData,
            self.toData(value),
            MutlipartFormCRLFData
            ])
        self.fields.append(data)
    }

     // 格式拼接
    mutating public func appendFormData(_ name: String, content: Data, fileName: String, contentType: String) {
        
        let contentDisposition = "Content-Disposition: form-data; name=\"\(self.encode(name))\"; filename=\"\(self.encode(fileName))\""
        let contentTypeHeader = "Content-Type: \(contentType)"
        let data = self.merge([
            self.toData(contentDisposition),
            MutlipartFormCRLFData,
            self.toData(contentTypeHeader),
            MutlipartFormCRLFData,
            MutlipartFormCRLFData,
            content,
            MutlipartFormCRLFData
            ])
        self.fields.append(data)
    }
    // 數據編碼
    fileprivate func encode(_ string: String) -> String {
        let characterSet = CharacterSet.MIMECharacterSet()
        return string.addingPercentEncoding(withAllowedCharacters: characterSet)!
    }
    // 轉成data 方便拼接 處理
    fileprivate func toData(_ string: String) -> Data {
        return string.data(using: .utf8)!
    }
    // 合併單個數據
    fileprivate func merge(_ chunks: [Data]) -> Data {
        let data = NSMutableData()
        for chunk in chunks {
            data.append(chunk)
        }
        return data.copy() as! Data
    }
}

// 整個數據的調用使用
fileprivate func dealwithRequest(urlStr:String) -> URLRequest{
    var request = URLRequest(url: URL(string: urlStr)!)
    var builder = LGMultipartDataBuilder()
    let data = self.readLocalData(fileNameStr: "Cooci", type: "jpg")
    builder.appendFormData("filedata",content:data as! Data , fileName: "fileName", contentType: "image/jpeg")
    request.setMultipartBody(builder.build()!, boundary: builder.boundary)
    return request
}
複製代碼

小結

很顯然,若是每一次咱們上傳文件,都這麼處理那是很是噁心的!因此封裝對於開發來講是多麼的重要!這裏咱們能夠自定義封裝,根據本身公司需求包裝格式!可是有不少公司是不須要關係太多的,直接默認操做就OK,只要字段匹配,那麼 Alamofire 這個時候就很明顯感覺到了舒服 👍👍👍dom

Alamofire 表單數據上傳

Alamofire 處理多表單的方式有三種,根據 URLSession 的三個方法封裝而來async

// 1:上傳data格式
session.uploadTask(with: urlRequest, from: data)
// 2: 上傳文件地址
session.uploadTask(with: urlRequest, fromFile: url)
// 3:上傳stream流數據
session.uploadTask(withStreamedRequest: urlRequest)
複製代碼

🌰 具體使用以下:🌰源碼分析

//MARK: - alamofire上傳文件 - 其餘方法
func alamofireUploadFileOtherMethod(){
    // 1: 文件上傳
    // file 的路徑
    let path = Bundle.main.path(forResource: "Cooci", ofType: "jpg");
    let url = URL(fileURLWithPath: path!)
    
    SessionManager.default.upload(url, to: jianshuUrl).uploadProgress(closure: { (progress) in
        print("上傳進度:\(progress)")
    }).response { (response) in
        print(response)
    }
    
    // 2: data上傳
    let data = self.readLocalData(fileNameStr: "Cooci", type: "jpg")
    
    SessionManager.default.upload(data as! Data, to: jianshuUrl, method: .post, headers: ["":""]).validate().responseJSON { (DataResponse) in
        if DataResponse.result.isSuccess {
            print(String.init(data: DataResponse.data!, encoding: String.Encoding.utf8)!)
        }
        if DataResponse.result.isFailure {
            print("上傳失敗!!!")
        }
    }
    
    // 3: stream上傳
    let inputStream = InputStream(data: data as! Data)
    SessionManager.default.upload(inputStream, to: jianshuUrl, method: .post, headers: ["":""]).response(queue: DispatchQueue.main) { (DDataRespose) in
        if let acceptData = DDataRespose.data {
            print(String.init(data: acceptData, encoding: String.Encoding.utf8)!)
        }
        if DDataRespose.error != nil {
            print("上傳失敗!!!")
        }
    }
    // 4: 多表單上傳
    SessionManager.default
        .upload(multipartFormData: { (mutilPartData) in
            mutilPartData.append("cooci".data(using: .utf8)!, withName: "name")
            mutilPartData.append("LGCooci".data(using: .utf8)!, withName: "username")
            mutilPartData.append("123456".data(using: .utf8)!, withName: "PASSWORD")
            
            mutilPartData.append(data as! Data, withName: "fileName")
        }, to: urlString) { (result) in
            print(result)
            switch result {
            case .failure(let error):
                print(error)
            case .success(let upload,_,_):
                upload.response(completionHandler: { (response) in
                    print("****:\(response) ****")
                })
            }
    }
}
複製代碼
  • 若是你只是想使用,但這裏就OK!
  • 接下來咱們開始展開分析 Alamofire 源碼,方便咱們更加深刻了解 Alamofire!

Alamofire 多表單源碼分析

⚠️ 源碼前面分析的代碼就不貼出來,你們能夠自行跟源碼 ⚠️post

1️⃣:先創造容器

DispatchQueue.global(qos: .utility).async {
    let formData = MultipartFormData()
    multipartFormData(formData)
}
複製代碼
  • 在這個 MultipartFormData 類裏面嵌套一個儲存結構體 EncodingCharacters 保存換行符 \r\n
  • BoundaryGenerator 分隔符處理 = String(format: "alamofire.boundary.%08x%08x", arc4random(), arc4random() 是一個固定字段拼接隨機字段
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)!
    }
}
複製代碼
  • 這裏是把分隔符分紅了三種
  • 第一種:最開始的分隔符(前面沒有拼接換行符)
  • 第二種:中間內容直接的分隔符(前面拼接換行符+末尾拼接換行符)
  • 第三種:結束分隔符(前面拼接換行符+末尾拼接換行符)比第二種就是少了 「--」 字符串
  • 你們能夠仔細對比一下,而後對照一下抓包數據,你就明白爲何這麼分狀況了
  • multipartFormData(formData) 接下來調用外界閉包,準備條件完成,開始填充數據

2️⃣:填充數據

mutilPartData.append("LGCooci".data(using: .utf8)!, withName: "username")學習

內部調用就是獲取數據信息

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)
}
// 內容頭格式拼接
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
}
複製代碼
  • 內容頭固定格式處理,拼接 Content-Disposition 而後設置 fileName 完成以後整段設置 mimeType
  • 把咱們的 value 也就是 LGCooci 的數據經過 Stream 包裝,節省內存
  • 獲取數據長度 UInt64(data.count)
public func append(_ stream: InputStream, withLength length: UInt64, headers: HTTPHeaders) {
    let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length)
    bodyParts.append(bodyPart)
}
複製代碼
  • 經過面向對象的設計原則,把凌亂的數據封裝 BodyPart 方面傳輸
  • 經過 bodyParts 集合收集一個個 BodyPart

3️⃣:數據整合

let data = try formData.encode()

接下來經過遍歷 bodyParts 封裝成合適的格式返回出 data 賦值給 httpBody

// 遍歷bodyParts
for bodyPart in bodyParts {
    let encodedData = try encode(bodyPart)
    encoded.append(encodedData)
}
// 統一編碼
private func encode(_ bodyPart: BodyPart) throws -> Data {
    var encoded = Data()
    // 判斷是不是第一行data肯定分隔符
    let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
    encoded.append(initialData)
    // 拼接字段頭:encodeHeaders
    let headerData = encodeHeaders(for: bodyPart)
    encoded.append(headerData)
    // 讀取數據 Data
    let bodyStreamData = try encodeBodyStream(for: bodyPart)
    encoded.append(bodyStreamData)
    // 是否拼接結束分割符
    if bodyPart.hasFinalBoundary {
        encoded.append(finalBoundaryData())
    }

    return encoded
}
複製代碼
  • 判斷是不是第一行 data 肯定分隔符
  • 拼接字段頭:encodeHeaders
  • 讀取數據 Data
  • 是否拼接結束分割符
  • 最終全部的數據根據順序拼接到 data

4️⃣:數據調用

let encodingResult = MultipartFormDataEncodingResult.success(
    request: self.upload(data, with: urlRequestWithContentType),
    streamingFromDisk: false,
    streamFileURL: nil
)
複製代碼
  • 傳進 uploadRequest 的請求器裏面
  • 經過傳遞的數據類型肯定調用 URLSession 的方法
  • 而後經過 SessionDelegate 接受上傳代理 - 最後下發給UploadTaskDelegate

總結

  • 數據就是經過,格式容器初始化
  • 而後用戶傳遞須要上傳的數據,填充進去
  • 包裝成一個個 bodyPart,經過一個結合容器收集bodyParts
  • 所有包裝完畢,遍歷 bodyParts 進行詳細編碼
  • 首先拼接分隔符,拼接固定格式頭信息,而後經過 stream 讀取具體!值,
  • 經過data 傳進,調用 URLSession 響應的方法,
  • 經過 SessionDelegate 接受上傳代理 - 最後下發給UploadTaskDelegate 最終返回上傳狀況

到這裏這個 多表單處理 篇章就寫完了!若有什麼疑問,能夠直接評論區交流討論!前段時間一直在忙公司週年慶的事情,博客落下了很多,不過這段時間我會一一補回來,謝謝,你們寄來的祝福!

就問此時此刻還有誰?45度仰望天空,該死!我這無處安放的魅力!

相關文章
相關標籤/搜索