Alamofire(6)— 多表单上传

作者 : 开心源码 本文共9618个字,预计阅读时间需要25分钟 发布时间: 2022-05-13 共210人阅读

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

  • Alamofire (1)—— URLSession必备技能
  • Alamofire (2)—— 后端下载
  • Alamofire (3)—— Request
  • Alamofire (4)—— 你需要知道的细节
  • Alamofire (5)—— Response
  • Alamofire(6)— 多表单上传
  • Alamofire(7)— 安全认证
  • Alamofire(8)— 终章(网络监控&通知&下载器封装)

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

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

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » Alamofire(6)— 多表单上传

发表回复