AFNetworking解析(三)

2023-05-16

本文回详细的介绍一下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-1.5
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 = 0;
    // 注意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 = 0;
    // 需要拼接上分割符
    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, 0), ^{
//        在当前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[1024];
//         每次从inputStream中读取最多1024bytes大小的数据,放在buffer中,给outputStream写入file
            NSInteger bytesRead = [inputStream read:buffer maxLength:1024];
            if (inputStream.streamError || bytesRead < 0) {
                error = inputStream.streamError;
                break;
            }
//          将读取的数据写入置顶路径
            NSInteger bytesWritten = [outputStream write:buffer maxLength:(NSUInteger)bytesRead];
            if (outputStream.streamError || bytesWritten < 0) {
                error = outputStream.streamError;
                break;
            }
//             写入完成
            if (bytesRead == 0 && bytesWritten == 0) {
                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 0;
    }

    NSInteger totalNumberOfBytesRead = 0;
//        一般来说都是直接读取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 == -1) {
                self.streamError = self.currentHTTPBodyPart.inputStream.streamError;
                break;
            } else {
//                totalNumberOfBytesRead当前读取的字节,作为下一次读取的起始字节
                totalNumberOfBytesRead += numberOfBytesRead;

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

    return totalNumberOfBytesRead;
}

单个bodyPart的读取

- (NSInteger)read:(uint8_t *)buffer
        maxLength:(NSUInteger)length
{
    NSInteger totalNumberOfBytesRead = 0;
//  使用分割符将对应的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 = 0;
//      inputStream使用系统自带方法读取
        numberOfBytesRead = [self.inputStream read:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
        if (numberOfBytesRead == -1) {
            return -1;
        } 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 = 0;

    return YES;
}

总结:上面就是所有关于multipart协议的分析和介绍,其实现方法可以说是一个对POST请求的在此封装。传输多种参数,多种资料型态混合的信息时会使用到multipart协议。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

AFNetworking解析(三) 的相关文章

随机推荐