
??????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-disposition是MIME協(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度仰望天空,該死!我這無處安放的魅力!