AFNetworking(二)AFNetworking對form-data請求體的處理

AFNetworking 發送 GET、POST 等請求時能夠直接將參數按照字典結構傳入,最終編碼到 url 中或者是 body 實體中,同時也支持按照 multipart/form-data 格式,將多種不一樣的數據合入到 body 中進行發送,而這些就涉及到 AFNetworking 的請求序列化類,也就是 AFURLRequestSerialization。javascript

AFURLRequestSerialization 是一個協議,它定義了一個方法用於序列化參數到 NSURLRequest 中,AFHTTPRequestSerializer 實現了這個協議,並實現了相應的方法。它不只提供了普通的參數編碼方法,也提供了 form-data 格式的 request 構建方法,也就是下面的方法html

- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
                                              URLString:(NSString *)URLString
                                             parameters:(NSDictionary *)parameters
                              constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
                                                  error:(NSError *__autoreleasing *)error
複製代碼

1. form-data

首先簡單介紹一下 form-data,multipart/form-data 主要用於 POST方法中傳遞多種格式和含義的數據,在 body 中引入 boundary 的概念,用分割線將多部分數據融合到一個 body 中發送給服務端。那麼對於一個簡單的 form-data,它發送的 body 內容可能以下java

--Boundary+FD2E180F039993ED
Content-Disposition: form-data; name="myArray[]"

v1
--Boundary+FD2E180F039993ED
Content-Disposition: form-data; name="myArray[]"

v2
--Boundary+FD2E180F039993ED
Content-Disposition: form-data; name="myArray[]"

v3
--Boundary+FD2E180F039993ED
Content-Disposition: form-data; name="mydic[key1]"

value1
--Boundary+FD2E180F039993ED
Content-Disposition: form-data; name="mydic[key2]"

value2
--Boundary+FD2E180F039993ED
header: headerkey

BodyData
--Boundary+FD2E180F039993ED--
複製代碼

它的特色是json

  • 每一部分均可以包含 header,通常默認必須包含的標識 header 是 Content-Disposition
  • 頭部和每一部分須要以 --Boundary+{XXX} 格式分割
  • 末尾以 --Boundary+{XXX}-- 結束
  • 請求頭中,要設置 Content-Type: multipart/form-data; boundary=Boundary+{XXX}
  • 請求頭要設置 Content-Length 爲 body 總長度

2. 一個 form-data 類型的 POST 請求

在 AFNetworking 中,要發送 form-data,能夠經過以下方式發送數組

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    manager.requestSerializer.timeoutInterval = 100;
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/plain", @"text/html",@"application/json", @"text/json" ,@"text/javascript", nil];;
    [manager POST:@"https://www.baidu.com" parameters:@{@"mydic":@{@"key1":@"value1",@"key2":@"value2"},
                                                          @"myArray":@[@"v1", @"v2", @"v3"]
                                                          } headers:nil constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
                                                              [formData appendPartWithFileData:[@"Data" dataUsingEncoding:NSUTF8StringEncoding]
                                                                                          name:@"DataName"
                                                                                      fileName:@"DataFileName"
                                                                                      mimeType:@"data"];
                                                          } progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {

                                                          } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

                                                          }];
複製代碼

主要用到 AFHTTPSessionManager 定義的以下方法bash

- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
                             parameters:(nullable id)parameters
                                headers:(nullable NSDictionary <NSString *, NSString *> *)headers
              constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
                               progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
                                success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
                                failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;
複製代碼

它的內部實現,主要作了這幾件事app

  • 經過 requestSerializer 的 multipartFormRequestWithMethod 方法構建 NSMutableURLRequest 對象
  • 設置頭部
  • 經過 AFURLSessionManager 建立 NSURLSessionUploadTask 對象

從中能夠看出,請求序列化主要發生在 multipartFormRequestWithMethod 方法中,而 AFHttpSessionManager 默認的 requestSerializer 是 AFHTTPAFHTTPRequestSerializer。oop

3. 請求序列化

AFHTTPAFHTTPRequestSerializer 對於 form-data 提供了以下方法進行序列化ui

- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
                                              URLString:(NSString *)URLString
                                             parameters:(NSDictionary *)parameters
                              constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
                                                  error:(NSError *__autoreleasing *)error
複製代碼

在方法實現裏主要作了如下事情編碼

  • 與普通的 urlencode 請求相似,先設置 request 相關參數,仍然是經過 KVO 記錄須要設置的參數,其餘都走默認邏輯
  • 構造 AFStreamingMultipartFormData 對象,將傳入的參數深度遍歷後一一經過 appendPartWithFormData: name: 方法添加到 AFStreamingMultipartFormData 中
  • 提供外部 block,對 AFStreamingMultipartFormData 對象進一步添加數據
  • 經過 AFStreamingMultipartFormData 的 requestByFinalizingMultipartFormData 方法構建 request

那麼 AFStreamingMultipartFormData 是一個什麼類呢。

4. 構造 form-data 數據

AFNetworking 定義的 AFStreamingMultipartFormData 類用於表徵一個 form-data 格式 body 的數據,它遵循 AFMultipartFormData 協議,能管理 boundary 字符串、用於向 request 傳輸數據的 NSInputStream 對象。

其中對於 form-data 的每個 part,AFNetworking 定義了一個 AFHTTPBodyPart 類,其中包含以下信息

  • 這個 part 的頭部 header
  • 分割字符串 boundary
  • 內容區長度
  • id 類型的 body
  • 數據流 inputStream

AFStreamingMultipartFormData 所包含的 NSInputStream 類,實質上是繼承自 NSInputStream 的子類 AFMultipartBodyStream,AFMultipartBodyStream 有一個 HTTPBodyParts 屬性,是一個 AFHTTPBodyPart 類型的數組,全部 append 到 AFStreamingMultipartFormData 的 part,最後都轉化爲一個 AFHTTPBodyPart 對象加入到了 AFMultipartBodyStream 的 HTTPBodyParts 中。

具體來講,AFMultipartFormData 協議(也就是 AFStreamingMultipartFormData 類)定義了以下一些 append 方法

  • appendPartWithFileURL: name: error: 添加文件路徑內的文件內容到 form-data
  • appendPartWithFileURL: name: fileName: mimeType: error: 添加文件路徑內的文件內容到 form-data,指定文件名和 mimeType
  • appendPartWithInputStream: name: fileName: length: mimeType: 添加 inputStream 到 form-data
  • appendPartWithFileData: name: fileName: mimeType: 添加 NSData 到 form-data
  • appendPartWithFormData: name: 添加 NSData 到 form-data
  • appendPartWithHeaders: body: 添加自定義 header 和 body 到 form-data

下面以 appendPartWithFormData 爲例看下具體實現

- (void)appendPartWithFormData:(NSData *)data
                          name:(NSString *)name
{
    NSParameterAssert(name);

    NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
    // 每一塊數據,默認帶上 Content-Disposition 做爲頭部
    [mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"", name] forKey:@"Content-Disposition"];

    [self appendPartWithHeaders:mutableHeaders body:data];
}

- (void)appendPartWithHeaders:(NSDictionary *)headers
                         body:(NSData *)body
{
    NSParameterAssert(body);

    AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init];
    bodyPart.stringEncoding = self.stringEncoding;
    bodyPart.headers = headers;
    // 複用一個 boundary
    bodyPart.boundary = self.boundary;
    // body 長度
    bodyPart.bodyContentLength = [body length];
    bodyPart.body = body;
    // 添加到 stream 中
    [self.bodyStream appendHTTPBodyPart:bodyPart];
}
複製代碼

能夠看到,就是根據數據構造一個 AFHTTPBodyPart 對象添加到 bodyStream 屬性中;至於文件和 inputStream,則是直接將文件 url 和 inputStream 對象賦值給 id 類型的 body。

這樣將全部數據都 append 到了 AFStreamingMultipartFormData 中之後,再調用 AFStreamingMultipartFormData 的 requestByFinalizingMultipartFormData 方法就能夠構造一個 NSMutableURLRequest 對象了,而在 requestByFinalizingMultipartFormData 方法中,主要作了以下工做

  • 將構造出來的 NSMutableURLRequest 的 HTTPBodyStream 屬性設置爲 AFStreamingMultipartFormData 的 bodyStream 對象,也就是 AFMultipartBodyStream 做爲 NSMutableURLRequest 的 body 數據源
  • 設置 Content-Type
[self.request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", self.boundary] forHTTPHeaderField:@"Content-Type"];
複製代碼
  • 設置 Content-Length
[self.request setValue:[NSString stringWithFormat:@"%llu", [self.bodyStream contentLength]] forHTTPHeaderField:@"Content-Length"];
複製代碼

5. 從 bodyStream 讀取數據

AFMultipartBodyStream 直接繼承自 NSInputStream,它維護一個 包含所有 AFHTTPBodyPart 的數組,當經過 request 發起一個 NSURLSessionUploadTask 之後,因爲設置了 request 的 HTTPBodyStream,則系統會嘗試從 AFMultipartBodyStream 讀取 body 數據,這裏就涉及到了 AFMultipartBodyStream 的 read: maxLength: 方法,它從流中讀取數據到 buffer 中,並返回實際讀取的數據長度(該長度最大爲 len)。而實際上 AFMultipartBodyStream 的 numberOfBytesInPacket 屬性就能夠限制讀取數據的最大長度。

{
    if ([self streamStatus] == NSStreamStatusClosed) {
        // 流已關閉,返回長度 0
        return 0;
    }

    NSInteger totalNumberOfBytesRead = 0;
    // 一直從 HTTPBodyParts 讀取到字節數達到 length 爲止
    while ((NSUInteger)totalNumberOfBytesRead < MIN(length, self.numberOfBytesInPacket)) {
        // 若是還未開始讀取,或者當前 part 已經讀取結束,則進入下一個
        if (!self.currentHTTPBodyPart || ![self.currentHTTPBodyPart hasBytesAvailable]) {
            if (!(self.currentHTTPBodyPart = [self.HTTPBodyPartEnumerator nextObject])) {
                break;
            }
        } else {
            NSUInteger maxLength = MIN(length, self.numberOfBytesInPacket) - (NSUInteger)totalNumberOfBytesRead;
            // 從 part 中讀取數據
            NSInteger numberOfBytesRead = [self.currentHTTPBodyPart read:&buffer[totalNumberOfBytesRead] maxLength:maxLength];
            if (numberOfBytesRead == -1) {
                // 讀取出錯
                self.streamError = self.currentHTTPBodyPart.inputStream.streamError;
                break;
            } else {
                // 更新總讀取字節數
                totalNumberOfBytesRead += numberOfBytesRead;

                if (self.delay > 0.0f) {
                    [NSThread sleepForTimeInterval:self.delay];
                }
            }
        }
    }

    return totalNumberOfBytesRead;
}
複製代碼

這裏經過一個 currentHTTPBodyPart 對象對 AFMultipartBodyStream 維護的 AFHTTPBodyPart 數組進行遍歷,讀取其中每個 AFHTTPBodyPart 對象的數據到 buffer 中。AFHTTPBodyPart 類也實現了同名的 read 方法,在這個方法裏,按照以下順序,讀取相應部分的數據

  • AFEncapsulationBoundaryPhase 頂部邊界
  • AFHeaderPhase 頭部數據
  • AFBodyPhase 實體
  • AFFinalBoundaryPhase 底部邊界

例如讀取頂部邊界數據以下

NSData *encapsulationBoundaryData = [([self hasInitialBoundary] ? AFMultipartFormInitialBoundary(self.boundary) : AFMultipartFormEncapsulationBoundary(self.boundary)) dataUsingEncoding:self.stringEncoding];
        totalNumberOfBytesRead += [self readData:encapsulationBoundaryData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
複製代碼

可是當讀取到 body 部分時要注意,因爲 body 是一個 id 類型,外界主要設置的可能值有 NSData、NSURL、NSInputStream 等,AFNetworking 在這裏統一將 body 的讀取歸一化爲 inputStream 流方式讀取,按照以下規則構建 inputStream

- (NSInputStream *)inputStream {
    // inputStream 根據 body 的類別返回不一樣的數據源
    if (!_inputStream) {
        if ([self.body isKindOfClass:[NSData class]]) {
            _inputStream = [NSInputStream inputStreamWithData:self.body];
        } else if ([self.body isKindOfClass:[NSURL class]]) {
            _inputStream = [NSInputStream inputStreamWithURL:self.body];
        } else if ([self.body isKindOfClass:[NSInputStream class]]) {
            _inputStream = self.body;
        } else {
            _inputStream = [NSInputStream inputStreamWithData:[NSData data]];
        }
    }

    return _inputStream;
}
複製代碼

讀取到 body 部分時則啓動 stream,讀取完 body 之後關閉 stream

// 這裏是根據當前 phase 切換到下一端 phase 的邏輯
        case AFHeaderPhase:
            // header -> body
            [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
            [self.inputStream open];
            _phase = AFBodyPhase;
            break;
        case AFBodyPhase:
            // body -> 底部邊界
            [self.inputStream close];
            _phase = AFFinalBoundaryPhase;
            break;
複製代碼

以上就是 AFNetworking 對於 form-data 請求的完整處理,基於 inputStream,將多種不一樣類型的 form-data 用統一的代碼模型處理,對外暴露的方法簡潔一致,於是便於使用和理解。

相關文章
相關標籤/搜索