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

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

AFURLRequestSerialization 是一個協(xié)議,它定義了一個方法用于序列化參數(shù)到 NSURLRequest 中,AFHTTPRequestSerializer 實現(xiàn)了這個協(xié)議,并實現(xiàn)了相應的方法。它不僅提供了普通的參數(shù)編碼方法,也提供了 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方法中傳遞多種格式和含義的數(shù)據(jù),在 body 中引入 boundary 的概念,用分割線將多部分數(shù)據(jù)融合到一個 body 中發(fā)送給服務端。那么對于一個簡單的 form-data,它發(fā)送的 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 中,要發(fā)送 form-data,可以通過如下方式發(fā)送

    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;

它的內部實現(xiàn),主要做了這幾件事

  • 通過 requestSerializer 的 multipartFormRequestWithMethod 方法構建 NSMutableURLRequest 對象
  • 設置頭部
  • 通過 AFURLSessionManager 創(chuàng)建 NSURLSessionUploadTask 對象

從中可以看出,請求序列化主要發(fā)生在 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

在方法實現(xiàn)里主要做了以下事情

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

那么 AFStreamingMultipartFormData 是一個什么類呢。

4. 構造 form-data 數(shù)據(jù)

AFNetworking 定義的 AFStreamingMultipartFormData 類用于表征一個 form-data 格式 body 的數(shù)據(jù),它遵循 AFMultipartFormData 協(xié)議,能管理 boundary 字符串、用于向 request 傳輸數(shù)據(jù)的 NSInputStream 對象。

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

  • 這個 part 的頭部 header
  • 分割字符串 boundary
  • 內容區(qū)長度
  • id 類型的 body
  • 數(shù)據(jù)流 inputStream

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

具體來說,AFMultipartFormData 協(xié)議(也就是 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 為例看下具體實現(xiàn)

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

    NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
    // 每一塊數(shù)據(jù),默認帶上 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];
}

可以看到,就是根據(jù)數(shù)據(jù)構造一個 AFHTTPBodyPart 對象添加到 bodyStream 屬性中;至于文件和 inputStream,則是直接將文件 url 和 inputStream 對象賦值給 id 類型的 body。

這樣將所有數(shù)據(jù)都 append 到了 AFStreamingMultipartFormData 中以后,再調用 AFStreamingMultipartFormData 的 requestByFinalizingMultipartFormData 方法就可以構造一個 NSMutableURLRequest 對象了,而在 requestByFinalizingMultipartFormData 方法中,主要做了如下工作

  • 將構造出來的 NSMutableURLRequest 的 HTTPBodyStream 屬性設置為 AFStreamingMultipartFormData 的 bodyStream 對象,也就是 AFMultipartBodyStream 作為 NSMutableURLRequest 的 body 數(shù)據(jù)源
  • 設置 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 讀取數(shù)據(jù)

AFMultipartBodyStream 直接繼承自 NSInputStream,它維護一個 包含全部 AFHTTPBodyPart 的數(shù)組,當通過 request 發(fā)起一個 NSURLSessionUploadTask 以后,由于設置了 request 的 HTTPBodyStream,則系統(tǒng)會嘗試從 AFMultipartBodyStream 讀取 body 數(shù)據(jù),這里就涉及到了 AFMultipartBodyStream 的 read: maxLength: 方法,它從流中讀取數(shù)據(jù)到 buffer 中,并返回實際讀取的數(shù)據(jù)長度(該長度最大為 len)。而實際上 AFMultipartBodyStream 的 numberOfBytesInPacket 屬性就可以限制讀取數(shù)據(jù)的最大長度。

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

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

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

    return totalNumberOfBytesRead;
}

這里通過一個 currentHTTPBodyPart 對象對 AFMultipartBodyStream 維護的 AFHTTPBodyPart 數(shù)組進行遍歷,讀取其中每一個 AFHTTPBodyPart 對象的數(shù)據(jù)到 buffer 中。AFHTTPBodyPart 類也實現(xiàn)了同名的 read 方法,在這個方法里,按照如下順序,讀取相應部分的數(shù)據(jù)

  • AFEncapsulationBoundaryPhase 頂部邊界
  • AFHeaderPhase 頭部數(shù)據(jù)
  • AFBodyPhase 實體
  • AFFinalBoundaryPhase 底部邊界

例如讀取頂部邊界數(shù)據(jù)如下

        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 在這里統(tǒng)一將 body 的讀取歸一化為 inputStream 流方式讀取,按照如下規(guī)則構建 inputStream

- (NSInputStream *)inputStream {
    // inputStream 根據(jù) body 的類別返回不同的數(shù)據(jù)源
    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

// 這里是根據(jù)當前 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 用統(tǒng)一的代碼模型處理,對外暴露的方法簡潔一致,因而便于使用和理解。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容