Alamofire(6)— 多表单上传

2,641 阅读7分钟

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