zoukankan      html  css  js  c++  java
  • AFNetWorking 上传功能使用及源码分析

    使用方法比较多,这里列举两种:

    第一种:

    // 1. 使用AFHTTPSessionManager的接口
        AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
        
        [manager POST:@"http://123.123.123.1" parameters:@{@"aaa":@"111"} constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
            // 在这个block中设置需要上传的文件
            NSString *path = [[NSBundle mainBundle] pathForResource:@"image" ofType:@"jpg"];
            // 将本地图片数据拼接到formData中 指定name
    //        [formData appendPartWithFileURL:[NSURL fileURLWithPath:path] name:@"image" error:nil];
            
    //        //     或者使用这个接口拼接 指定name和filename
            NSData *picdata  =[NSData dataWithContentsOfFile:path];
            [formData appendPartWithFileData:picdata name:@"image" fileName:@"image.jpg" mimeType:@"image/jpeg"];
            
        } progress:^(NSProgress * _Nonnull uploadProgress) {
            NSLog(@"progress --- %@",uploadProgress.localizedDescription);
        } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
            NSLog(@"responseObject-------%@", responseObject);
            dispatch_async(dispatch_get_main_queue(), ^{
                self.tvres.text = [NSString stringWithFormat:@"响应结果:%@",responseObject];
            });
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            NSLog(@"Error-------%@", error);
        }];
        

    第二种:

     AFURLSessionManager *sessionManager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
        // request
        NSURL *url = [NSURL URLWithString:@"http://114.215.186.169:9002/api/demo/test/file"];
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
        [request setHTTPMethod:@"POST"];
        // bodydata
        //(2)请求头
        //上传任务,必须要添加的字段
        // 分隔符
        //multipart/form-data是必须的,是指提交的表单中有附件
        /*
         Multipart协议是基于post方法的组合实现,和post协议的主要区别在于请求头和请求体的不同
         multipart/form-data的请求头必须包含一个特殊的头信息:Content-Type,且其值也必须规定为multipart/form-data,同时还需要规定一个内容分割符用于分割请求体中的多个post的内容,如文件内容和文本内容自然需要分割开来,不然接收方就无法正常解析和还原这个文件了
         multipart/form-data的请求体也是一个字符串,不过和post的请求体不同的是它的构造方式,post是简单的name=value值连接,而multipart/form-data则是添加了分隔符等内容的构造体
         
         */
        NSString *boundary = @"---------------------------7db15a14291cce";
        NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; charset=utf-8;boundary=%@",boundary];
        // 设置Content-Type
        [request setValue:contentType forHTTPHeaderField:@"Content-Type"];
    
       //(3)请求体
        NSMutableString *startStr = [NSMutableString string];
        // 拼接头部格式
        [startStr appendFormat:@"--%@
    ",boundary];
        [startStr appendFormat:@"Content-disposition: form-data; name="image"; filename="image.jpg""];
        [startStr appendFormat:@"
    "];
        [startStr appendFormat:@"Content-Type: application/octet-stream"];
        [startStr appendFormat:@"
    
    "];
    
        
        NSMutableData *bodyData = [NSMutableData data];
        NSData *startData = [startStr dataUsingEncoding:NSUTF8StringEncoding];
        [bodyData appendData:startData];
    
    
        // 拼接上传文件数据
        NSString *path = [[NSBundle mainBundle] pathForResource:@"image" ofType:@"jpg"];
        NSData *picdata  =[NSData dataWithContentsOfFile:path];
        [bodyData appendData:picdata];
    
    
        // 拼接结尾格式
        NSString *endStr = [NSString stringWithFormat:@"
    --%@--
    ",boundary];
        NSData *endData = [endStr dataUsingEncoding:NSUTF8StringEncoding];
        [bodyData appendData:endData];
    
    
       NSURLSessionUploadTask *uploadtask = [sessionManager uploadTaskWithRequest:request fromData:bodyData progress:^(NSProgress * _Nonnull uploadProgress) {
            NSLog(@"progress --- %@",uploadProgress.localizedDescription);
        } completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
    
            NSLog(@"completion --- error:%@",error);
        }];
        [uploadtask resume];

    第二种是靠自己拼凑http header 和 body用于发送给服务端,下面主要分析第一种方法源码的实现。

    1 首先进入Post源码

    - (NSURLSessionDataTask *)POST:(NSString *)URLString
                        parameters:(id)parameters
         constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
                          progress:(nullable void (^)(NSProgress * _Nonnull))uploadProgress
                           success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
                           failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure
    {
       
        NSError *serializationError = nil;
         //最复杂的在这块
        NSMutableURLRequest *request = [self.requestSerializer multipartFormRequestWithMethod:@"POST" URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters constructingBodyWithBlock:block error:&serializationError];
        if (serializationError) {
            if (failure) {
                dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
                    failure(nil, serializationError);
                });
            }
    
            return nil;
        }
       __block NSURLSessionDataTask *task = [self uploadTaskWithStreamedRequest:request progress:uploadProgress completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
            if (error) {
                if (failure) {
                    failure(task, error);
                }
            } else {
                if (success) {
                    success(task, responseObject);
                }
            }
        }];
    
        [task resume];
    
        return task;
    }

    首先是创建需要的NSURLRequest,然后新建task,返回task,这里重点说一下标红的地方,NSURLRequest如何组成。

    multipartFormRequestWithMethod。

    - (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"]);
    
        //调用普通get/post 请求 方法
        //这个方法作用:通过 HTTPRequestHeaders 字典设置头部字段,parameters为空,没有处理参数
       
        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 消息体的一部分比如 aaa=111
                    [formData appendPartWithFormData:data name:[pair.field description]];
                }
            }
        }
    
        if (block) {
    //从block中添加数据,比如我们可以自己往里添加需要的键值对或者图片资源等 block(formData); } //设置一下MultipartRequest的bodyStream或者其特有的content-type
    return [formData requestByFinalizingMultipartFormData]; }

    进入 requestByFinalizingMultipartFormData 方法

    //****************************拼装最终的消息体*******************************/
    - (NSMutableURLRequest *)requestByFinalizingMultipartFormData {
        if ([self.bodyStream isEmpty]) {
            return self.request;
        }
    
        // Reset the initial and final boundaries to ensure correct Content-Length
        [self.bodyStream setInitialAndFinalBoundaries];
        //把bodyStream交给request处理,request 会调用
        /*
         [fileStream read:readBuffer maxLength:maxLength];
    
         */
        //然后我们重写read:,把数据存到eadBuffer
        [self.request setHTTPBodyStream:self.bodyStream];
        //重新修改Content-Type,表示body中有附件
        [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;
    }

    在这个方法里设置requst的输入流,最终是通过从这个流中读取数据配凑的消息体,当然这是request里面封装的,我们看不错,但是可以才猜出来,之后做分析

    然后就是重新设置Content-Type,Content-Length.

    下面分析下bodyStream:

    bodyStream 类对象为 AFMultipartBodyStream,这个类又继承自 NSInputStream,也就是说我们的bodyStream就是一个NSInputStream,里面拥有NSInputStream的功能,我们可以通过重写某些方法,实现我们自己读取数据流的逻辑。

    AFStreamingMultipartFormData类中的appendPart函数最终目的就是给bodyStream中HTTPBodyParts添加一个AFHTTPBodyPart对象,如下代码:

    //各种类型的简直对追加到会调用到这里
    - (void)appendPartWithHeaders:(NSDictionary *)headers
                             body:(NSData *)body
    {
        NSParameterAssert(body);
    
        AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init];
        bodyPart.stringEncoding = self.stringEncoding;
        bodyPart.headers = headers;
        bodyPart.boundary = self.boundary;
        bodyPart.bodyContentLength = [body length];
        bodyPart.body = body;
    
        [self.bodyStream appendHTTPBodyPart:bodyPart];
    }
    - (void)appendHTTPBodyPart:(AFHTTPBodyPart *)bodyPart {
        [self.HTTPBodyParts addObject:bodyPart];
    }

    也就是说所有的body消息都放在了HTTPBodyParts中,在操作字节流的时候,我们通过遍历这个数组,拼凑出http body的格式,然后上传给服务端。

    我们看AFMultipartBodyStream类中定义的-(NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)length,这个方法是对NSInputStream类的方法的重写,为什么要重写呢,我们先来看一下 NSInputStream的常规用法。

    要求,通过NSInputStream实现大文件上传,参考的网上,我们可以这么做:

    地址:https://blog.csdn.net/u010576399/article/details/51180331

    我们截取关键地方:

    看代码,我们看到有一个while循环,每次fileStream read若干字节到readBUffer,最后都追加到了body中,当文件流读取结束或者出错误,结束while循环。

    所以由此我们可以推断出:NSURLRequest底层也是有一个while循环,然后调用NSInputStream的read方法读取字节流,这里我们重写了这个read方法,那么读取的时候就会调用我们自己写的read方法,重写的read方法为如下代码:

    //读取字节的方法,把本地的内容读到buffer中,此方法由NSURLRequest调用,这里相当于重写
    - (NSInteger)read:(uint8_t *)buffer
            maxLength:(NSUInteger)length
    {
       // [super read:buffer maxLength:length]; 默认方法
        
       NSLog (@"%@",[NSThread currentThread]);
        ////    输入流关闭,无法获取数据,返回子节长度为0
        if ([self streamStatus] == NSStreamStatusClosed) {
            return 0;
        }
    
        NSInteger totalNumberOfBytesRead = 0;
        // length在mac 64上为32768
        // 每次读取32768的大小
        //  while ((NSUInteger)totalNumberOfBytesRead < MIN(length, self.numberOfBytesInPacket)) {
          while ((NSUInteger)totalNumberOfBytesRead < length) {
            // 如果当前读取的body不存在或者body没有可读字节
            //如果当前的HTTPBodyPart读取完成,就读取下一个;
            //  HTTPBodyPartEnumerator(一个枚举)
            if (!self.currentHTTPBodyPart || ![self.currentHTTPBodyPart hasBytesAvailable]) {
                //把下一个body赋值给当前的body 如果下一个为nil 就退出循环
                if (!(self.currentHTTPBodyPart = [self.HTTPBodyPartEnumerator nextObject])) {
                    break;
                }
            } else {
                // 当前body存在 且没有读完 且读取长度还小于32768
                // 剩余可读文件的大小
                NSUInteger maxLength = length - (NSUInteger)totalNumberOfBytesRead;
                // 调用bodypart的接口
                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];
                    }
                }
            }
        }
        //返回已经读取的字节大小,如果不为0或者-1,NSURLRequest会重新调用read方法,表示继续读取
        return totalNumberOfBytesRead;
    }

    这个方法就是NSURLRequest 的while循环里调用的read方法。

    这个read方法里面还有一个while,这个while是用来遍历httpBodyParts数组,用来吧数组里面的httpBodyPart都读取到buffer中,供NSURLRequst组装body消息体。

    标红的方法为重点方法,进入看一下:

    //单个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;
    }

    这是对单个bodyPart的读取,bodyPart分为4块,如图:

    这是一块上传附件的消息体片段,其余消息类似,read方法通过分别读取这四块内容加到buffer中。其中1,2,4 用了readData方法,3用了NSInputStream默认的方法。

    下面看一下readData方法定义:

    - (NSInteger)readData:(NSData *)data
               intoBuffer:(uint8_t *)buffer
                maxLength:(NSUInteger)length
    {
        /*_phaseReadOffset这个属性设计的很巧妙,当buffer空间不够的时候,requst会重新分配buffer,
         _phaseReadOffset用来记录上次读的索引,等buffer重新分配够空间后,接着读
         */
        NSRange range = NSMakeRange((NSUInteger)_phaseReadOffset, MIN([data length] - ((NSUInteger)_phaseReadOffset), length));
        [data getBytes:buffer range:range];////--Boundary+7586B2B18E70A6B1
    
        
        _phaseReadOffset += range.length;
        
        if (((NSUInteger)_phaseReadOffset) >= [data length]) {
            [self transitionToNextPhase];
        }
        
        return (NSInteger)range.length;
    }

    当读取完了一块之后,调用transitionToNextphase方法,如果没有读取完,比如NSURLRequst每次分配的buffer字节不够了,就会返回这次读取的字节数,然后NSURLRequst通过while循环再次读取,_phaseReadOffset会记录上次的读取索引结果,然后下次接着读。

    看下transitionToNextphase的定义:

    - (BOOL)transitionToNextPhase {
        if (![[NSThread currentThread] isMainThread]) {
            dispatch_sync(dispatch_get_main_queue(), ^{
                [self transitionToNextPhase];
            });
            return YES;
        }
        NSLog(@"当前线程=%@",[NSThread currentThread]);
        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;
    }

    总结:以上就是分析上传时候NSURLRequest的生成和处理过程,其实现方法可以说是一个对POST请求的在此封装。传输多种参数,多种资料型态混合的信息时会使用到multipart协议。

     参考资料:

    https://blog.csdn.net/cishengchangan/article/details/51939923

    https://blog.csdn.net/lizhengwei1989/article/details/75635261

  • 相关阅读:
    Ubuntu中安装gdal python版本
    python中在计算机视觉中的库及基础用法
    Google earth爬取卫星影像数据并进行标注路网的方法
    事务
    文件的下载,随机验证码(无验证)登录注册
    类的加载器和反射
    等待唤醒机制,UDP通信和TCP通信
    线程池,多线程,线程异步,同步和死锁,Lock接口
    多线程, Thread类,Runnable接口
    转换流,缓冲流
  • 原文地址:https://www.cnblogs.com/xiaonanxia/p/9728449.html
Copyright © 2011-2022 走看看