Alamofire(6)— 多表單上傳

??????Alamofire專題目錄,歡迎及時反饋交流 ??????


Alamofire 目錄直通車 --- 和諧學(xué)習(xí),不急不躁!


實際開發(fā)過程中,多表單上傳是非常重要的一種請求!服務(wù)端通常是根據(jù)請求頭(headers)中的 Content-Type 字段來獲知請求中的消息主體是用何種方式編碼,再對主體進(jìn)行解析。 所以說到 POST 提交數(shù)據(jù)方案,包含了 Content-Type 和消息主體編碼方式兩部分。 這個篇章我們來探索一下 多表單上傳文件 ~

一、多表單格式

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

  • --alamofire.boundary.4e076f46186e231d: 是分隔符,為了方便讀取數(shù)據(jù)
  • Content-Disposition: form-data; name="name": 其中 Content-dispositionMIME 協(xié)議的擴展,MIME 協(xié)議指示 MIME 用戶代理如何顯示附加的文件。Content-disposition 其實可以控制用戶請求所得的內(nèi)容存為一個文件的時候提供一個默認(rèn)的文件名,這里就是添加了一個 key = name
  • 接在后面就是 \r\n 換行符
  • 然后就是 key 對應(yīng)的 value = LGCooci
  • 最下面的亂碼是圖片data數(shù)據(jù)

Multipart 格式顯示整個數(shù)據(jù)就類似字典的 key-value

二、我們通過URLSeesion去請求多表單

1??:分隔符初始化

init() {
 self.boundary = NSUUID().uuidString
}
  • 利用 NSUUID().uuidString 設(shè)定為分隔符

2??:換行符號

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

3??: 數(shù)據(jù)格式處理&拼接

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??:數(shù)據(jù)處理完畢,然后設(shè)置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
    // 初始化 - 分隔符創(chuàng)建
    init() {
        self.boundary = NSUUID().uuidString
    }
    // 所有數(shù)據(jù)格式處理
    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)
    }
    // 數(shù)據(jù)格式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)
    }
    // 數(shù)據(jù)編碼
    fileprivate func encode(_ string: String) -> String {
        let characterSet = CharacterSet.MIMECharacterSet()
        return string.addingPercentEncoding(withAllowedCharacters: characterSet)!
    }
    // 轉(zhuǎn)成data 方便拼接 處理
    fileprivate func toData(_ string: String) -> Data {
        return string.data(using: .utf8)!
    }
    // 合并單個數(shù)據(jù)
    fileprivate func merge(_ chunks: [Data]) -> Data {
        let data = NSMutableData()
        for chunk in chunks {
            data.append(chunk)
        }
        return data.copy() as! Data
    }
}

// 整個數(shù)據(jù)的調(diào)用使用
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
}

小結(jié)

很顯然,如果每一次我們上傳文件,都這么處理那是非常惡心的!所以封裝對于開發(fā)來說是多么的重要!這里我們可以自定義封裝,根據(jù)自己公司需求包裝格式!但是有很多公司是不需要關(guān)系太多的,直接默認(rèn)操作就OK,只要字段匹配,那么 Alamofire 這個時候就很明顯感受到了舒服 ??????

Alamofire 表單數(shù)據(jù)上傳

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

// 1:上傳data格式
session.uploadTask(with: urlRequest, from: data)
// 2: 上傳文件地址
session.uploadTask(with: urlRequest, fromFile: url)
// 3:上傳stream流數(shù)據(jù)
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("上傳進(jìn)度:\(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 多表單源碼分析

?? 源碼前面分析的代碼就不貼出來,大家可以自行跟源碼 ??

1??:先創(chuàng)造容器

DispatchQueue.global(qos: .utility).async {
    let formData = MultipartFormData()
    multipartFormData(formData)
}
  • 在這個 MultipartFormData 類里面嵌套一個儲存結(jié)構(gòu)體 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)!
    }
}
  • 這里是把分隔符分成了三種
  • 第一種:最開始的分隔符(前面沒有拼接換行符)
  • 第二種:中間內(nèi)容直接的分隔符(前面拼接換行符+末尾拼接換行符)
  • 第三種:結(jié)束分隔符(前面拼接換行符+末尾拼接換行符)比第二種就是少了 “--” 字符串
  • 大家可以仔細(xì)對比一下,然后對照一下抓包數(shù)據(jù),你就明白為什么這么分情況了
  • multipartFormData(formData) 接下來調(diào)用外界閉包,準(zhǔn)備條件完成,開始填充數(shù)據(jù)

2??:填充數(shù)據(jù)

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

內(nèi)部調(diào)用就是獲取數(shù)據(jù)信息

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)
}
// 內(nèi)容頭格式拼接
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
}
  • 內(nèi)容頭固定格式處理,拼接 Content-Disposition 然后設(shè)置 fileName 完成之后整段設(shè)置 mimeType
  • 把我們的 value 也就是 LGCooci 的數(shù)據(jù)通過 Stream 包裝,節(jié)省內(nèi)存
  • 獲取數(shù)據(jù)長度 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)
}
  • 通過面向?qū)ο蟮脑O(shè)計原則,把凌亂的數(shù)據(jù)封裝 BodyPart 方面?zhèn)鬏?/li>
  • 通過 bodyParts 集合收集一個個 BodyPart

3??:數(shù)據(jù)整合

let data = try formData.encode()

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

// 遍歷bodyParts
for bodyPart in bodyParts {
    let encodedData = try encode(bodyPart)
    encoded.append(encodedData)
}
// 統(tǒng)一編碼
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)
    // 讀取數(shù)據(jù) Data
    let bodyStreamData = try encodeBodyStream(for: bodyPart)
    encoded.append(bodyStreamData)
    // 是否拼接結(jié)束分割符
    if bodyPart.hasFinalBoundary {
        encoded.append(finalBoundaryData())
    }

    return encoded
}
  • 判斷是否是第一行 data 確定分隔符
  • 拼接字段頭:encodeHeaders
  • 讀取數(shù)據(jù) Data
  • 是否拼接結(jié)束分割符
  • 最終所有的數(shù)據(jù)根據(jù)順序拼接到 data

4??:數(shù)據(jù)調(diào)用

let encodingResult = MultipartFormDataEncodingResult.success(
    request: self.upload(data, with: urlRequestWithContentType),
    streamingFromDisk: false,
    streamFileURL: nil
)
  • 傳進(jìn) uploadRequest 的請求器里面
  • 通過傳遞的數(shù)據(jù)類型確定調(diào)用 URLSession 的方法
  • 然后通過 SessionDelegate 接受上傳代理 - 最后下發(fā)給UploadTaskDelegate

總結(jié)

  • 數(shù)據(jù)就是通過,格式容器初始化
  • 然后用戶傳遞需要上傳的數(shù)據(jù),填充進(jìn)去
  • 包裝成一個個 bodyPart,通過一個結(jié)合容器收集bodyParts
  • 全部包裝完畢,遍歷 bodyParts 進(jìn)行詳細(xì)編碼
  • 首先拼接分隔符,拼接固定格式頭信息,然后通過 stream 讀取具體!值,
  • 通過data 傳進(jìn),調(diào)用 URLSession 響應(yīng)的方法,
  • 通過 SessionDelegate 接受上傳代理 - 最后下發(fā)給UploadTaskDelegate 最終返回上傳情況

到這里這個 多表單處理 篇章就寫完了!如有什么疑問,可以直接評論區(qū)交流討論!前段時間一直在忙公司周年慶的事情,博客落下了不少,不過這段時間我會一一補回來,謝謝,大家寄來的祝福!

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容