天天看點

AFNetworking解析(三)Multipart協定如何添加一個AFHTTPBodyPart呢?self.boundary的構造

本文回詳細的介紹一下Multipart協定在AFN中的封裝

Multipart協定

Multipart協定是基于post方法的組合實作,和post協定的主要差別在于請求頭和請求體的不同

multipart/form-data的請求頭必須包含一個特殊的頭資訊:Content-Type,且其值也必須規定為multipart/form-data,同時還需要規定一個内容分割符用于分割請求體中的多個post的内容,如檔案内容和文本内容自然需要分割開來,不然接收方就無法正常解析和還原這個檔案了

multipart/form-data的請求體也是一個字元串,不過和post的請求體不同的是它的構造方式,post是簡單的name=value值連接配接,而multipart/form-data則是添加了分隔符等内容的構造體

下面我們列舉一個具有代表性的例子

--${fengefu}//表示檔案名(分隔符可以自定義)
Content-Disposition: form-data; name="Filename"

Test.txt
--${fengefu}//檔案内容
Content-Disposition: form-data; name="file000"; filename="Test測試.txt"
Content-Type: application/octet-stream

%PDF-
file content
%%EOF

--${fengefu}//組合後的字元串
Content-Disposition: form-data; name="Upload"

Submit Query
--${fengefu}--//結束标志
           
現在我們想對上面的例子使用AFNetworking進行調用:
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager POST:@"postURLString" parameters:@{@"Filename":@"Test.txt"} constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
    [formData appendPartWithFileData:[txt檔案具體内容(NSData *)]
                                name:@"file000"
                            fileName:@"Test測試.txt"
                            mimeType:@"application/octet-stream"];
    [formData appendPartWithFormData:[@"Submit Query" dataUsingEncoding:NSUTF8StringEncoding]
                                name:@"Upload"];
} progress:nil success:nil failure:nil];
           

此處帶constructingBodyWithBlock的POST方法與- [AFHTTPSessionManager POST:parameters:progress:success:failure:]明顯的差別在于建構request的時候,使用的是multipartFormRequestWithMethod:以及建構NSURLSessionDataTask的時候使用的是uploadTaskWithStreamedRequest:。

multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:error:除了需要使用普通的request構造函數requestWithMethod:URLString:parameters:error:來構造request,還需要根據multipart獨有的屬性來修飾這個request,其中最關鍵的就是要構造http body(請求體)部分。

因為multipart是基于POST請求的,是以應該首先排除GET和HEAD請求方法

- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
                                              URLString:(NSString *)URLString
                                             parameters:(NSDictionary *)parameters
                              constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
                                                  error:(NSError *__autoreleasing *)error
{
    NSParameterAssert(method);
    NSParameterAssert(![method isEqualToString:@"GET"] && ![method isEqualToString:@"HEAD"]);

    NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error];
//初始化AFStreamingMultipartFormData 建構bodyStream
    __block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];

    if (parameters) {

//        建構一個AFQueryStringPair,其中field為"Filename",value為"檔案名"

        for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
            NSData *data = nil;
            if ([pair.value isKindOfClass:[NSData class]]) {
                data = pair.value;
            } else if ([pair.value isEqual:[NSNull null]]) {
                data = [NSData data];
            } else {

//            根據對應value的類型,建構出一個NSData變量    把string類型轉換為NSData類型資料

                data = [[pair.value description] dataUsingEncoding:self.stringEncoding];
            }

            if (data) {

//                 根據data和name建構Request的header和body

                [formData appendPartWithFormData:data name:[pair.field description]];
            }
        }
    }

    if (block) {
//        往formData中添加資料
        block(formData);
    }
//         設定一下MultipartRequest的bodyStream或者其特有的content-type
    return [formData requestByFinalizingMultipartFormData];
}
下面我們通過詳細分析請求的三個部分來看一下AFNetworking在multipart中是如何實作的。
每一個request都分為三個部分:請求行、請求頭和請求體
multipart請求頭
- (NSMutableURLRequest *)requestByFinalizingMultipartFormData {
    if ([self.bodyStream isEmpty]) {//self.bodyStream 為空時,即和普通的post請求一樣
        return self.request;
    }

    // Reset the initial and final boundaries to ensure correct Content-Length
    [self.bodyStream setInitialAndFinalBoundaries];
    [self.request setHTTPBodyStream:self.bodyStream];

    [self.request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", self.boundary] forHTTPHeaderField:@"Content-Type"];
    [self.request setValue:[NSString stringWithFormat:@"%llu", [self.bodyStream contentLength]] forHTTPHeaderField:@"Content-Length"];

    return self.request;
}
           

其中主要的資訊内容有self.bodyStream和self.boundary

self.bodyStream的構造

self.bodyStream是區分普通post請求方法和multipart請求的關鍵要素

是以我們首先介紹bodyStream

事實上對于bodyStream的建構就是對AFStreamingMultipartFormData對象的處理,比如函數- [AFHTTPRequestSerializer multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:error:]的那個formData就是一個AFStreamingMultipartFormData對象

AFStreamingMultipartFormData類中的appendPart函數最終目的就是給bodyStream中HTTPBodyParts添加一個AFHTTPBodyPart對象

如何添加一個AFHTTPBodyPart呢?

(BOOL) - appendPartWithFileURL:name:error: 根據檔案位置構造資料源,使用檔案類型名作為mimeType

(BOOL) - appendPartWithFileURL:name:fileName:mimeType:error: 根據檔案位置構造資料源,需要提供mimeType

(void) - appendPartWithInputStream:name:fileName:length:mimeType: 直接使用NSInputStream作為資料源

(void) - appendPartWithFileData:name:fileName:mimeType: 使用NSData作為資料源

(void) - appendPartWithFormData:name: 使用NSData作為資料源,NSData并不是一個檔案,可能隻是一個字元串

建立一個AFHTTPBodyPart對象bodyPart,然後給bodyPart設定各種參數,其中比較重要的參數是headers和body這兩個。最後使用appendHTTPBodyPart:方法,将bodyPart添加到bodyStream的HTTPBodyParts上。

如:

- (void)appendPartWithInputStream:(NSInputStream *)inputStream
                             name:(NSString *)name
                         fileName:(NSString *)fileName
                           length:(int64_t)length
                         mimeType:(NSString *)mimeType
{
    NSParameterAssert(name);
    NSParameterAssert(fileName);
    NSParameterAssert(mimeType);

    NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
    [mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"];
    [mutableHeaders setValue:mimeType forKey:@"Content-Type"];

    AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init];
    bodyPart.stringEncoding = self.stringEncoding;
    bodyPart.headers = mutableHeaders;
    bodyPart.boundary = self.boundary;
    bodyPart.body = inputStream;

    bodyPart.bodyContentLength = (unsigned long long)length;

    [self.bodyStream appendHTTPBodyPart:bodyPart];
}
           

appendPartWithFileURL:函數會首先檢查fileURL是否可用,使用[fileURL isFileURL]檢查檔案位置格式是否正确。使用[fileURL checkResourceIsReachableAndReturnError:error]來檢查該檔案是否存在,是否能擷取到。最後使用NSFileManager擷取到檔案attributes,并判斷attributes是否存在。另外注意到此處直接使用的是fileURL作為AFHTTPBodyPart對象的body屬性。

appendPartWithFileData:和appendPartWithFormData:兩個函數實作中,最後使用的是appendPartWithHeaders:建構AFHTTPBodyPart對象

self.boundary的構造

boundary是用來分割不同資料内容的,其實就是上面舉的那個例子中的${fengefu}。我們注意到boundary需要處理以下幾個情況:

建立boundary字元串

此處AFNetworking自定義了個函數建立boundary字元串。

static NSString * AFCreateMultipartFormBoundary() {
    // 使用兩個十六進制随機數拼接在Boundary後面來表示分隔符
    return [NSString stringWithFormat:@"Boundary+%08X%08X", arc4random(), arc4random()];
}
           

如果是開頭分隔符的,那麼隻需在分隔符結尾加一個換行符

static inline NSString * AFMultipartFormInitialBoundary(NSString *boundary) {
    return [NSString stringWithFormat:@"--%@%@", boundary, kAFMultipartFormCRLF];
}
           

如果是中間部分分隔符,那麼需要分隔符前面和結尾都加換行符

static inline NSString * AFMultipartFormEncapsulationBoundary(NSString *boundary) {
    return [NSString stringWithFormat:@"%@--%@%@", kAFMultipartFormCRLF, boundary, kAFMultipartFormCRLF];
}
           

如果是末尾,還得使用–分隔符–作為請求體的結束标志

static inline NSString * AFMultipartFormFinalBoundary(NSString *boundary) {
    return [NSString stringWithFormat:@"%@--%@--%@", kAFMultipartFormCRLF, boundary, kAFMultipartFormCRLF];
           

boundary的用處

除了設定Content-Type外,在設定Content-Length時使用的[self.bodyStream contentLength]中會使用到boundary的這些相關函數:

// AFMultipartBodyStream函數

// 計算上面那個bodyStream的總長度作為Content-Length
- (unsigned long long)contentLength {
    unsigned long long length = ;
    // 注意bodyStream是由多個AFHTTPBodyPart對象組成的,比如上面那個例子就是有三個對象組成
    for (AFHTTPBodyPart *bodyPart in self.HTTPBodyParts) {
        length += [bodyPart contentLength];
    }
    return length;
}
// 計算上面每個AFHTTPBodyPart對象的長度
// 使用AFHTTPBodyPart中hasInitialBoundary和hasFinalBoundary屬性表示開頭bodyPart和結尾bodyPart
- (unsigned long long)contentLength {
    unsigned long long length = ;
    // 需要拼接上分割符
    NSData *encapsulationBoundaryData = [([self hasInitialBoundary] ? AFMultipartFormInitialBoundary(self.boundary) : AFMultipartFormEncapsulationBoundary(self.boundary)) dataUsingEncoding:self.stringEncoding];
    length += [encapsulationBoundaryData length];
    // 每個AFHTTPBodyPart對象中還有Content-Disposition等header-使用stringForHeader擷取
    NSData *headersData = [[self stringForHeaders] dataUsingEncoding:self.stringEncoding];
    length += [headersData length];
    // 加上每個AFHTTPBodyPart對象具體的資料(比如檔案内容)長度
    length += _bodyContentLength;
    // 如果是最後一個AFHTTPBodyPart,還需要加上“--分隔符--”的長度
    NSData *closingBoundaryData = ([self hasFinalBoundary] ? [AFMultipartFormFinalBoundary(self.boundary) dataUsingEncoding:self.stringEncoding] : [NSData data]);
    length += [closingBoundaryData length];

    return length;
}
           

第二種multipart的request建構方法

/**
        将原來request中的HTTPBodyStream内容寫入到指定檔案中,随後調用completionHandler處理,以此傳回新的request。
        出現原因:NSURLSessionTask中又一個bug,當streaming的内容源自于HTTP body時,請求不會發送Content-Length,特别是在Amazon S3的網絡請求中,multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:error:作為一個解決方法,這個方法或者和其他任意一個帶有HTTPBodyStream(不能為nil)的方法,接着将HTTPBodyStream的内容先寫到指定的檔案中,再傳回一個原來那個request的拷貝,其中該拷貝的HTTPBodyStream屬性值要置為空,然後,然後調用AFURLSessionManager -uploadTaskWithRequest:fromFile:progress:completionHandler或者将檔案内容轉換為NSData給HTTPBody
 問題位址:https://github.com/AFNetworking/AFNetworking/issues/1398
 */
- (NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)request
                             writingStreamContentsToFile:(NSURL *)fileURL
                                       completionHandler:(void (^)(NSError *error))handler
{
//    傳入的請求(原先的請求)不能為空,HTTPBodyStream不能為空
    NSParameterAssert(request.HTTPBodyStream);
//    特定的檔案路徑isFileURL(需要合法)
    NSParameterAssert([fileURL isFileURL]);

    NSInputStream *inputStream = request.HTTPBodyStream;
//    寫入檔案到指定的路徑
    NSOutputStream *outputStream = [[NSOutputStream alloc] initWithURL:fileURL append:NO];
    __block NSError *error = nil;

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, ), ^{
//        在目前RunLoop中執行流操作
        [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

        [inputStream open];
        [outputStream open];

        while ([inputStream hasBytesAvailable] && [outputStream hasSpaceAvailable]) {
            uint8_t buffer[];
//         每次從inputStream中讀取最多1024bytes大小的資料,放在buffer中,給outputStream寫入file
            NSInteger bytesRead = [inputStream read:buffer maxLength:];
            if (inputStream.streamError || bytesRead < ) {
                error = inputStream.streamError;
                break;
            }
//          将讀取的資料寫入置頂路徑
            NSInteger bytesWritten = [outputStream write:buffer maxLength:(NSUInteger)bytesRead];
            if (outputStream.streamError || bytesWritten < ) {
                error = outputStream.streamError;
                break;
            }
//             寫入完成
            if (bytesRead ==  && bytesWritten == ) {
                break;
            }
        }

        [outputStream close];
        [inputStream close];

        if (handler) {
//            回到主線程執行handler
            dispatch_async(dispatch_get_main_queue(), ^{
                handler(error);
            });
        }
    });
//   拷貝傳入的request(原先的request)并置空HTTPBodyStream
    NSMutableURLRequest *mutableRequest = [request mutableCopy];
    mutableRequest.HTTPBodyStream = nil;

    return mutableRequest;
}
           

讀取位元組的方法

- (NSInteger)read:(uint8_t *)buffer
        maxLength:(NSUInteger)length
{
//    輸入流關閉,無法擷取資料,傳回子節長度為0
    if ([self streamStatus] == NSStreamStatusClosed) {
        return ;
    }

    NSInteger totalNumberOfBytesRead = ;
//        一般來說都是直接讀取length長度的資料,但是考慮到最後一次需要讀出的資料長度(self.numberOfBytesInPacket)一般是小于length是以需要做一個判斷處理選取其中較小的一方
    while ((NSUInteger)totalNumberOfBytesRead < MIN(length, self.numberOfBytesInPacket)) {
//      如果目前的HTTPBodyPart讀取完成,就讀取下一個;
//        HTTPBodyPartEnumerator(一個枚舉)
        if (!self.currentHTTPBodyPart || ![self.currentHTTPBodyPart hasBytesAvailable]) {
            if (!(self.currentHTTPBodyPart = [self.HTTPBodyPartEnumerator nextObject])) {
                break;
            }
        } else {
            NSUInteger maxLength = MIN(length, self.numberOfBytesInPacket) - (NSUInteger)totalNumberOfBytesRead;
            NSInteger numberOfBytesRead = [self.currentHTTPBodyPart read:&buffer[totalNumberOfBytesRead] maxLength:maxLength];
//            讀取出錯
            if (numberOfBytesRead == -) {
                self.streamError = self.currentHTTPBodyPart.inputStream.streamError;
                break;
            } else {
//                totalNumberOfBytesRead目前讀取的位元組,作為下一次讀取的起始位元組
                totalNumberOfBytesRead += numberOfBytesRead;

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

    return totalNumberOfBytesRead;
}
           

單個bodyPart的讀取

- (NSInteger)read:(uint8_t *)buffer
        maxLength:(NSUInteger)length
{
    NSInteger totalNumberOfBytesRead = ;
//  使用分割符将對應的bodyPart封裝起來
    if (_phase == AFEncapsulationBoundaryPhase) {
        NSData *encapsulationBoundaryData = [([self hasInitialBoundary] ? AFMultipartFormInitialBoundary(self.boundary) : AFMultipartFormEncapsulationBoundary(self.boundary)) dataUsingEncoding:self.stringEncoding];
        totalNumberOfBytesRead += [self readData:encapsulationBoundaryData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
    }
// 讀取bodyPart的header部分,使用stringForHeaders擷取對應的header
    if (_phase == AFHeaderPhase) {
        NSData *headersData = [[self stringForHeaders] dataUsingEncoding:self.stringEncoding];
        totalNumberOfBytesRead += [self readData:headersData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
    }
//  内容主體,直接寫入到buffer中
    if (_phase == AFBodyPhase) {
        NSInteger numberOfBytesRead = ;
//      inputStream使用系統自帶方法讀取
        numberOfBytesRead = [self.inputStream read:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
        if (numberOfBytesRead == -) {
            return -;
        } else {
            totalNumberOfBytesRead += numberOfBytesRead;
//  内容讀取完成更換Phase
            if ([self.inputStream streamStatus] >= NSStreamStatusAtEnd) {
                [self transitionToNextPhase];
            }
        }
    }
//  如果是最後一個bodyPart隊形,在末尾加上分隔符
    if (_phase == AFFinalBoundaryPhase) {
        NSData *closingBoundaryData = ([self hasFinalBoundary] ? [AFMultipartFormFinalBoundary(self.boundary) dataUsingEncoding:self.stringEncoding] : [NSData data]);
        totalNumberOfBytesRead += [self readData:closingBoundaryData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
    }

    return totalNumberOfBytesRead;
}
           

Phase的切換

根據對應的階段切換到下一個階段,其中主要是inputStream的開關

- (BOOL)transitionToNextPhase {
    if (![[NSThread currentThread] isMainThread]) {
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self transitionToNextPhase];
        });
        return YES;
    }

    switch (_phase) {
        case AFEncapsulationBoundaryPhase:
            _phase = AFHeaderPhase;
            break;
        case AFHeaderPhase:
            [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
            [self.inputStream open];
            _phase = AFBodyPhase;
            break;
        case AFBodyPhase:
            [self.inputStream close];
            _phase = AFFinalBoundaryPhase;
            break;
        case AFFinalBoundaryPhase:
        default:
            _phase = AFEncapsulationBoundaryPhase;
            break;
    }
    _phaseReadOffset = ;

    return YES;
}
           

總結:上面就是所有關于multipart協定的分析和介紹,其實作方法可以說是一個對POST請求的在此封裝。傳輸多種參數,多種資料型态混合的資訊時會使用到multipart協定。

繼續閱讀