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

CISay發表於2019-01-21

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

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

- (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 內容可能如下

--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--
複製程式碼

它的特點是

  • 每一部分都可以包含 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 定義的如下方法

- (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;
複製程式碼

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

  • 通過 requestSerializer 的 multipartFormRequestWithMethod 方法構建 NSMutableURLRequest 物件
  • 設定頭部
  • 通過 AFURLSessionManager 建立 NSURLSessionUploadTask 物件

從中可以看出,請求序列化主要發生在 multipartFormRequestWithMethod 方法中,而 AFHttpSessionManager 預設的 requestSerializer 是 AFHTTPAFHTTPRequestSerializer。

3. 請求序列化

AFHTTPAFHTTPRequestSerializer 對於 form-data 提供瞭如下方法進行序列化

- (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 用統一的程式碼模型處理,對外暴露的方法簡潔一致,因而便於使用和理解。

相關文章