Alamofire(6)— 多表单上传

😊😊😊Alamofire专题目录,欢迎及时反馈交流 😊😊😊


Alamofire 目录直通车 --- 和谐学习,不急不躁!


实际开发过程中,多表单上传是非常重要的一种请求!服务端通常是根据请求头(headers)中的 Content-Type 字段来获知请求中的消息主体是用何种方式编码,再对主体进行解析。 所以说到 POST 提交数据方案,包含了 Content-Type 和消息主体编码方式两部分。 这个篇章我们来探索一下 多表单上传文件 ~

一、多表单格式

下面我通过 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-value

二、我们通过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 这个时候就很明显感受到了舒服 👍👍👍

Alamofire 表单数据上传

Alamofire 处理多表单的方式有三种,根据 URLSession 的三个方法封装而来

// 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 多表单源码分析

⚠️ 源码前面分析的代码就不贴出来,大家可以自行跟源码 ⚠️

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度仰望天空,该死!我这无处安放的魅力!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352