美文网首页ios 参考部分
AFNetworking(二)AFNetworking对form

AFNetworking(二)AFNetworking对form

作者: Yasic | 来源:发表于2019-01-20 17:06 被阅读53次

    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 用统一的代码模型处理,对外暴露的方法简洁一致,因而便于使用和理解。

    相关文章

      网友评论

        本文标题:AFNetworking(二)AFNetworking对form

        本文链接:https://www.haomeiwen.com/subject/eioxjqtx.html