iOS学习----------AFNetworking(3)request创建《post请求》

本文探讨了在iOS开发中使用AFNetworking进行POST请求的两种方式,特别是`multipartFormRequestWithMethod`方法及其内部实现。通过构建自定义请求头和处理HTTPBodyStream,确保了请求的正确发送。同时提到了`requestWithMultipartFormRequest`方法,虽然在AFN中未使用,但解释了其用于解决NSURLSessionTask的一个已知问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、发送post请求时,构建的requset使用的方法为
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method URLString:(NSString *)URLString parameters:(NSDictionary *)parameters constructingBodyWithBlock:(void (^)(id formData))block error:(NSError __autoreleasing )error

- (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"]);

   //先调用requestWithMethod方法来设置head 和 序列化参数
   //这时参数放在body中:[mutableRequest setHTTPBody:[query dataUsingEncoding:self.stringEncoding]]

    NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error];

//创建formData,这个formData绑定一个request、stringEncoding、boundary(分割线)、AFMultipartBodyStream(数据)
//AFMultipartBodyStream中包含:AFHTTPBodyPart(当前的bodyPart)、HTTPBodyParts(bodyPart的数量)...
    __block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];

    if (parameters) {
        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 {
                data = [[pair.value description] dataUsingEncoding:self.stringEncoding];
            }

            if (data) {
                [formData appendPartWithFormData:data name:[pair.field description]];
            }
        }
    }

    if (block) {
        block(formData);
    }
//构建multipart的请求头
    return [formData requestByFinalizingMultipartFormData];
}

multipart除了使用普通协议请求头的构建方法。还会在- [AFStreamingMultipartFormData requestByFinalizingMultipartFormData]构建自己独有的请求头。下面详细看这个方法

//创建请求头
- (NSMutableURLRequest *)requestByFinalizingMultipartFormData {
    if ([self.bodyStream isEmpty]) {
        return self.request;
    }

    //如果boduStream中有多个AFHTTPBodyPart 那么便利将每个AFHTTPBodyPart的前后分割线去掉,只在整个bodyStream的HTTPBodyParts数组的第一个和最后一个加上分割符
    [self.bodyStream setInitialAndFinalBoundaries];
    [self.request setHTTPBodyStream:self.bodyStream];
//    self.request.HTTPBodyStream = 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.boundary:分割线

//分隔符的创建
static NSString * AFCreateMultipartFormBoundary() {
     // 使用两个十六进制随机数拼接在Boundary后面来表示分隔符
    return [NSString stringWithFormat:@"Boundary+%08X%08X", arc4random(), arc4random()];
}

static NSString * const kAFMultipartFormCRLF = @"\r\n";
下面几种特殊情况的处理
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];
}

[self.bodyStream contentLength]:bodyStream的长度

// 计算上面那个bodyStream的总长度作为Content-Length
- (unsigned long long)contentLength {
    unsigned long long length = 0;
    for (AFHTTPBodyPart *bodyPart in self.HTTPBodyParts) {
        length += [bodyPart contentLength];
    }
    return length;
}

// AFHTTPBodyPart函数
// 计算上面每个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;
}

bodyStream的格式

AFStreamingMultipartFormData类中的appendPart*函数最终落脚点就是给bodyStream中HTTPBodyParts添加一个AFHTTPBodyPart对象(HTTPBodyParts数组中的元素)
通过以下方法添加

/*
 都是新建一个AFHTTPBodyPart对象bodyPart,然后给bodyPart设置各种参数,其中比较重要的参数是headers和body这两个。最后使用appendHTTPBodyPart:方法,将bodyPart添加到bodyStream的HTTPBodyParts上。

 appendPartWithFileURL:函数会首先检查fileURL是否可用,使用[fileURL isFileURL]检查文件位置格式是否正确。使用[fileURL checkResourceIsReachableAndReturnError:error]来检查该文件是否存在,是否能获取到。最后使用NSFileManager获取到文件attributes,并判断attributes是否存在。另外注意到此处直接使用的是fileURL作为AFHTTPBodyPart对象的body属性。
 appendPartWithFileData:和appendPartWithFormData:两个函数实现中,最后使用的是appendPartWithHeaders:构建AFHTTPBodyPart对象,详见代码。
 */

//根据文件位置构造数据源,使用文件类型名作为mimeType
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
                         name:(NSString *)name
                        error:(NSError * __autoreleasing *)error

//根据文件位置构造数据源,需要提供mimeType
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
                         name:(NSString *)name
                     fileName:(NSString *)fileName
                     mimeType:(NSString *)mimeType
                        error:(NSError * __autoreleasing *)error

//直接使用NSInputStream作为数据源
- (void)appendPartWithInputStream:(NSInputStream *)inputStream
                             name:(NSString *)name
                         fileName:(NSString *)fileName
                           length:(int64_t)length
                         mimeType:(NSString *)mimeType

//使用NSData作为数据源
- (void)appendPartWithFileData:(NSData *)data
                          name:(NSString *)name
                      fileName:(NSString *)fileName
                      mimeType:(NSString *)mimeType

 //使用NSData作为数据源,NSData并不是一个文件,可能只是一个字符串
- (void)appendPartWithFormData:(NSData *)data
                          name:(NSString *)name

2、另外一种创建multipart request的方法
- (NSMutableURLRequest )requestWithMultipartFormRequest:(NSURLRequest )request writingStreamContentsToFile:(NSURL *)fileURL
completionHandler:(void (^)(NSError *error))handler
该方法在AFN中暂时没有被用到。
方法解释:
将原来request中的HTTPBodyStream内容异步写入到指定文件中,随后调用completionHandler处理。最后返回新的request。

原因: NSURLSessionTask中有一个bug,当HTTP body的内容是来自NSStream的时候,request无法发送Content-Length到服务器端,此问题在Amazon S3的Web服务中尤为显著。作为一个解决方案,该函数的request参数使用的是multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:error:构建出的request,或者其他HTTPBodyStream属性不为空的request。接着将HTTPBodyStream的内容先写到指定的文件中,再返回一个原来那个request的拷贝,其中该拷贝的HTTPBodyStream属性值要置为空。至此,可以使用AFURLSessionManager -uploadTaskWithRequest:fromFile:progress:completionHandler:函数构建一个上传任务,或者将文件内容转变为NSData类型,并且指定给新request的HTTPBody属性。

- (NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)request
                             writingStreamContentsToFile:(NSURL *)fileURL
                                       completionHandler:(void (^)(NSError *error))handler
{
     // 原先request的HTTPBodyStream不能为空
    NSParameterAssert(request.HTTPBodyStream);
     // 文件路径要合法
    NSParameterAssert([fileURL isFileURL]);

    //获取到inputStream数据流
    NSInputStream *inputStream = request.HTTPBodyStream;

    // 使用outputStream将HTTPBodyStream的内容写入到路径为fileURL的文件中
    NSOutputStream *outputStream = [[NSOutputStream alloc] initWithURL:fileURL append:NO];
    __block NSError *error = nil;
 // 异步执行写入操作
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 指定在当前RunLoop中(currentRunLoop)运行inputStreamm/outputStream,意味着在currentRunLoop中处理流操作
        [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

        [inputStream open];
        [outputStream open];

        while ([inputStream hasBytesAvailable] && [outputStream hasSpaceAvailable]) {
            //初始化buffer空间
            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;
            }
            //将buffer空间中的数据写入到outputStream中即写入到文件中
            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];

        // 回到主进程执行handler
        if (handler) {
            dispatch_async(dispatch_get_main_queue(), ^{
                handler(error);
            });
        }
    });

    // 获取到新的request,并将新的request的HTTPBodyStream置为空
    //创建一个新的request
    NSMutableURLRequest *mutableRequest = [request mutableCopy];
    mutableRequest.HTTPBodyStream = nil;

    return mutableRequest;
}

其中有一个陌生的函数,就是将inputStream读取到buff中
- (NSInteger)read:(uint8_t *)buffer
maxLength:(NSUInteger)length

- (NSInteger)read:(uint8_t *)buffer
        maxLength:(NSUInteger)length
{
    if ([self streamStatus] == NSStreamStatusClosed) {
        return 0;
    }

    NSInteger totalNumberOfBytesRead = 0;

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
    // 一般来说都是直接读取length长度的数据,但是考虑到最后一次需要读出的数据长度(self.numberOfBytesInPacket)一般是小于length
    // 所以此处使用了MIN(length, self.numberOfBytesInPacket)
    while ((NSUInteger)totalNumberOfBytesRead < MIN(length, self.numberOfBytesInPacket)) {

        // 类似于我们构建request的逆向过程,我们对于HTTPBodyStream的读取也是分成一个一个AFHTTPBodyPart来的
        // 如果当前AFHTTPBodyPart对象读取完成,那么就使用enumerator读取下一个AFHTTPBodyPart
        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表示目前已经读取的字节数,可以作为读取后的数据放置于buffer的起始位置,如buffer[totalNumberOfBytesRead]
                totalNumberOfBytesRead += numberOfBytesRead;

                if (self.delay > 0.0f) {
                    [NSThread sleepForTimeInterval:self.delay];
                }
            }
        }
    }
#pragma clang diagnostic pop

    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,并读取到buffer中
    if (_phase == AFHeaderPhase) {
        NSData *headersData = [[self stringForHeaders] dataUsingEncoding:self.stringEncoding];
        totalNumberOfBytesRead += [self readData:headersData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
    }

    // 如果读取到的是bodyPart的内容主体,即inputStream,那么就直接使用inputStream写入数据到buffer中
    if (_phase == AFBodyPhase) {
        NSInteger numberOfBytesRead = 0;
        // 使用系统自带的NSInputStream的read:maxLength:函数读取
        numberOfBytesRead = [self.inputStream read:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
        if (numberOfBytesRead == -1) {
            return -1;
        } else {
            totalNumberOfBytesRead += numberOfBytesRead;
            // 如果内容主体都读取完了,那么很有可能下一次读取的就是下一个bodyPart的header
            // 所以此处要调用transitionToNextPhase,调整对应_phase
            if ([self.inputStream streamStatus] >= NSStreamStatusAtEnd) {
                [self transitionToNextPhase];
            }
        }
    }

    // 如果是最后一个AFHTTPBodyPart对象,那么就需要添加在末尾”--分隔符--"
    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;
}

3、AFJSONRequestSerializer、AFPropertyListRequestSerializer
请求参数的两种不同格式,一种事json 一种是plist,AFN对这两种格式的请求参数时创建的request 进行了处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值