深入解析POST上传-->AFNetworking的底层理解

一. POST单文件上传-简单使用

1. 创建请求

  • 实例化请求并设置基本参数
    // 0. 获取服务器端口的地址
    NSURL *url = [NSURL URLWithString:@"http://localhost/upload/upload.php"];

    #warning:对于POST请求,必须手动设置其请求方法,因此要使用可变请求
    // 1. 创建请求
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

    // 2. 设置请求方式
    request.HTTPMethod = @"POST";

    // 3. 告诉服务器本次上传文件的相关信息
    // 固定格式: 设置Content-Type
    // Content-Type: multipart/form-data; boundary=---------------------------198596859919834017191791522499
    // Content-Type:本次上传文件类型信息,包含boundary
    // boundary:本次上传文件的边界(自己随意设置,只要三个地方一致即可)
    NSString *type = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", kBoundary];
    [request setValue:type forHTTPHeaderField:@"Content-Type"];
  • 重难点: 设置请求体,分为三个部分
    • 上边界部分,告诉服务器要做数据上传,包含了

      • userfile -> 负责上传文件脚本中的 字段名,开发的时候,可以咨询后端程序员
      • filename -> 将文件保存在服务器上的文件名称
      • Content-Type -> 客户端告诉服务器上传文件的文件类型(如果不想写文件类型,统一用 application/octet-stream[8进制流])
    • 上传文件的数据部分(即文件内容的二进制数据)

    • 下边界部分,严格按照字符串格式来设置:--boundary--

    // 实例化请求体
    NSMutableData *data = [NSMutableData data];

//    -----------------------------198596859919834017191791522499
//    Content-Disposition: form-data; name="userfile"; filename="WPFNetWorkTool.h"
//    Content-Type: application/octet-stream
#warning 有些服务器可以直接使用 \n,但是新浪微博如果使用 \n 上传文件,服务器会返回“没有权限”的错误! 因此一定要注意安全换行:\r\n
    // 1. 拼接上传文件的上边界信息
    NSMutableString *headerStrM = [NSMutableString stringWithFormat:@"\r\n--%@\r\n", kBoundary];

    // name=%@ :服务器接收参数的key值,后台工作人员告诉我们
    // filename=%@ :文件上传到服务器的存储名,若不设置则为默认名,名称保持不变
    [headerStrM appendFormat:@"Content-Disposition: form-data; name=%@; filename=%@\r\n", @"userfile", @"123" ];

    // Content-Type: application/octet-stream 表明文件的上传类型,乱写类型不会影响上传,但是不符合规范
    [headerStrM appendString:@"Content-Type: application/octet-stream\r\n\r\n"];

    // 将上传文件的上边界信息添加到请求体中
    [data appendData:[headerStrM dataUsingEncoding:NSUTF8StringEncoding]];

    // 2. 设置文件内容
    // 文件地址
    NSString *filePath = @"/Users/wangpengfei/Desktop/WPFNetWorkTool.h";

    // 将文件转化为二进制形式
    NSData *fileData = [NSData dataWithContentsOfFile:filePath];

    // 将文件内容添加到请求体中
    [data appendData:fileData];

    // 3. 设置文件的下边界
    // -----------------------------198596859919834017191791522499--
    NSString *footerStrM = [NSString stringWithFormat:@"\r\n--%@--", kBoundary];

    NSLog(@"footerStrM--->%@", footerStrM);

    // 将下边界添加到请求体中
    [data appendData:[footerStrM dataUsingEncoding:NSUTF8StringEncoding]];

    // 4. 设置请求体
    request.HTTPBody = data;

常见的 Content-Type 类型:

大类型 小类型
image png
image jpg
image gif
text html
application json

2. 发送请求

[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

        NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
        /*
        打印结果:
            {"userfile":{"name":"123","type":"application\/octet-stream","tmp_name":"\/private\/var\/tmp\/phpEk0KCK","error":0,"size":0}}
        */

    }] resume];

二. POST单文件上传-简单封装结构体

1. 获得本地文件响应头信息,使用同步方法*重难点*

通过响应头信息,可以获得文件的类型/长度/建议的名称.
  • MIMEType :就是文件类型
  • suggestedFilename : 推荐文件名(本地存储名)
  • expectedContentLength : 文件长度

如果文件比较大,不建议发送本地请求.发送本地请求,会将文件从沙盒中加载到内存中,造成内存开销.

    - (NSURLResponse *)getFileResponseWithFilePath:(NSString *)filePath {
    // 动态获取文件类型

    // 1. 获取文件路径,根据路径获取 url 地址,本地协议名  file://
    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"file://%@", filePath]];

    // 2. 创建请求
    NSURLRequest *request = [NSURLRequest requestWithURL:url];

    // NSURLSession 没有同步请求的方法
    // 利用 NSURLConnection 发送同步请求

    // 定义一片空的地址
    NSURLResponse *response = nil;

    // &response 二级指针
    [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:NULL];

    return response;
}

2. 封装请求体格式

  • filePath : 上传文件的路径
  • fileKey : 服务器接受文件的 key 值
  • fileName : 上传文件在服务器中保存的名称(可选)
    - (NSData *)setupHttpBodyWithFilePath:(NSString *)filePath fileKey:(NSString *)fileKey fileName:(NSString *)fileName;
  • 当用户没有设置fileName时,调用方法一:设置文件为默认名
    // 调用获得本地文件信息的方法
    NSURLResponse *response = [self getFileResponseWithFilePath:filePath];

    if (!fileName) {
        fileName = response.suggestedFilename;
    }

三. POST单文件上传封装

1. 取出已封装好的单例类WPFNetWorkTool,封装以下方法

  • urlString:网络接口
  • filePath:需要上传的文件路径
  • fileKey:服务器接收用户上传文件的key值
  • fileName:上传文件存储到服务器的名称
  • success:上传成功时调用的block
  • fail:上传失败时调用的block
    - (void)POSTFileWithUrlString:(NSString *)urlString
     filePath:(NSString *)filePath fileKey:(NSString *)fileKey
     fileName:(NSString *)fileName
     success:(successBlock)success fail:(failBlock)fail;

2. 在发送请求方法中增加以下内容

[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        // 成功
        if (data && !error) {
            id responseObj = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
            // 如果不能解析JSON数据
            if (!responseObj) {
                responseObj = data;
            }
            // 执行回调
            if (success) {
                success(responseObj, response);
            }
        // 失败
        } else {
            // 执行回调
            if (fail) {
                fail(error);
            }
        }
    }] resume];

3. 封装方法的调用

// POST文件上传
[[WPFNetWorkTool sharedTool] POSTFileWithUrlString:@"http://localhost/upload/upload.php" filePath:@"/Users/wangpengfei/Desktop/葵花宝典/下载工具/WPFNetWorkTool/WPFNetWorkTool.h" fileKey:@"userfile" fileName:NULL success:^(id obj, NSURLResponse *response) {
    NSLog(@"obj--->%@", obj);
} fail:^(NSError *error) {
    NSLog(@"error--->%@", error);
}];

#warning 图片等文件可以显示name,但是oc程序文件不能显示
/*
打印结果:
    obj--->{
    userfile =     {
        error = 0;
        name = "(null)";
        size = 1702;
        "tmp_name" = "/private/var/tmp/phposR0Tv";
        type = "application/octet-stream";
    };
}
*/

四. POST多文件上传-简单使用

多文件上传和单文件上传的基本思路是一样的,唯一的区别在于对请求体的封装.

  • 多文件上传的请求体格式

      // 第一个文件上边界及参数
      \r\n--boundary\r\n
      Content-Disposition: form-data; name=userfile[]; filename=美女\r\n
      Content-Type:image/jpeg\r\n\r\n
    
      第一个文件的二进制数据部分
    
      // 第二个文件上边界及参数
      \r\n--boundary\r\n
      Content-Disposition: form-data; name=userfile[]; filename=JSON\r\n
      Content-Type:text/plain\r\n\r\n
    
      第二个文件的二进制数据部分
    
      // 下边界
      \r\n--boundary--
    
  • 1.设置第一个文件的上边界及参数

    NSMutableString *headerStrM1 = [NSMutableString stringWithFormat:@"\r\n--%@\r\n", kBoundary];

    // name=%@ :服务器接收参数的key值,后台工作人员告诉我们
    // filename=%@ :文件上传到服务器的存储名,若不设置则为默认名,名称保持不变
#warning @"userfile[]"后台人员提供的数据
    [headerStrM1 appendFormat:@"Content-Disposition: form-data; name=%@; filename=%@\r\n", @"userfile[]", @"234" ];

    // Content-Type: application/octet-stream 表明文件的上传类型,乱写类型不会影响上传,但是不符合规范
    [headerStrM1 appendString:@"Content-Type: application/octet-stream\r\n\r\n"];

    // 将上传文件的上边界信息添加到请求体中
    [data appendData:[headerStrM1 dataUsingEncoding:NSUTF8StringEncoding]];
  • 2.设置第一个文件的二进制数据
    // 文件地址
    NSString *filePath1 = @"/Users/wangpengfei/Desktop/photo/IMG_5544.jpg";

    // 将文件转化为二进制形式
    NSData *fileData1 = [NSData dataWithContentsOfFile:filePath1];

    // 将文件内容添加到请求体中
    [data appendData:fileData1];
  • 3.设置第二个文件的上边界及参数
NSMutableString *headerStrM2 = [NSMutableString stringWithFormat:@"\r\n--%@\r\n", kBoundary];

    // name=%@ :服务器接收参数的key值,后台工作人员告诉我们
    // filename=%@ :文件上传到服务器的存储名,若不设置则为默认名,名称保持不变

    [headerStrM2 appendFormat:@"Content-Disposition: form-data; name=%@; filename=%@\r\n", @"userfile[]", @"123" ];

    [headerStrM2 appendString:@"Content-Type: application/octet-stream\r\n\r\n"];

    // 将上传文件的上边界信息添加到请求体中
    [data appendData:[headerStrM2 dataUsingEncoding:NSUTF8StringEncoding]];
  • 4.设置第二个文件的二进制数据
// 文件地址
    NSString *filePath2 = @"/Users/wangpengfei/Desktop/photo/beauty1.jpg";

    // 将文件转化为二进制形式
    NSData *fileData2 = [NSData dataWithContentsOfFile:filePath2];

    // 将文件内容添加到请求体中
    [data appendData:fileData2];
  • 5.设置下边界
    NSString *footerStrM = [NSString stringWithFormat:@"\r\n--%@--", kBoundary];

    // 将下边界添加到请求体中
    [data appendData:[footerStrM dataUsingEncoding:NSUTF8StringEncoding]];

五. POST多文件上传-添加普通参数

  • 有些网络请求,客户端需要告诉服务器一些必要的数据,服务器根据客户端传过来的数据(参数)去数据库检索出对应的数据.返回给客户端.
  • 必须参数: 必须附带的参数(登录时候的账号和密码).
  • 可选参数: 可以自由选择是否告诉给服务器的参数.
  • 典型应用:
    • 新浪微博: 上传图片的同时,发送一条微博信息!
    • 购物评论: 购买商品之后发表评论的时候图片+评论内容!
  • 多个参数之间以 & 分割.参数是'无序'的.
  • 大公司: 能够附带参数,就会尽量多的附带参数.网络监测大数据开发/页面检测都必须由客户端发送参数给服务器.

普通参数的格式如下:

-----------------------------16778832101575341713442286528
Content-Disposition: form-data; name="username"

Wpf

普通参数添加的位置:最后一个文件的二进制内容下边界之间

使用一个可变字符串连接所有参数的全部信息(上边界和具体内容),然后统一转化为二进制形式

  • 设置第一个参数的上边界
    NSMutableString *parameterStr = [NSMutableString stringWithFormat:@"\r\n--%@\r\n", kBoundary];
    // name=%@ :服务器接收普通文本参数的key值.后端人员告诉我们.
    // 文本参数也有可能有多个...
    [parameterStr appendFormat:@"Content-Disposition: form-data; name=%@\r\n\r\n", @"username"];
  • 设置第一个参数的内容
    [parameterStr appendString:@"WangPengfei"];
  • 设置第二个参数的上边界
    [parameterStr appendFormat:@"\r\n--%@\r\n", kBoundary];
    [parameterStr appendFormat:@"Content-Disposition: form-data; name=%@\r\n\r\n", @"password"];
  • 设置第二个参数的内容
    [parameterStr appendString:@"123321"];
  • 统一转化为二进制形式
    [data appendData:[parameterStr dataUsingEncoding:NSUTF8StringEncoding]];

六. 简单封装

1. 将文件名和文件地址参数key值和参数具体值分别封装为字典

  • 设置文件字典
    // 设置上传文件在服务器存储的名称
    NSString *name1 = @"photo.jpg";
    NSString *name2 = @"math.h";
    NSString *name3 = @"video.json";

    // 设置文件地址
    NSString *filePath1 = @"/Users/wangpengfei/Desktop/IMG_5097.jpg";
    NSString *filePath2 = @"/Users/wangpengfei/Desktop/Math.m";
    NSString *filePath3 = @"/Users/wangpengfei/Desktop/vedios.json";

    NSDictionary *fileDict = @{
                               name1:filePath1,
                               name2:filePath2,
                               name3:filePath3
                               };
  • 设置普通文本参数字典
    NSDictionary *parameters = @{
                                 @"username":@"wpf",
                                 @"password":@"12300",
                                 @"age":@"24"
                                 };

2. 方法的封装

  • 方法名
    - (NSData *)getHttpBodyWithFileKey:(NSString *)fileKey fileDict:(NSDictionary *)fileDict parameters:(NSDictionary *)parameters
  • 遍历文件字典
    // 遍历文件参数字典,取出文件字典中的 key值 和 value 值
    [fileDict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {

        // 上传文件在服务器中保存的名称
        NSString *fileName = key;
        // 上传文件在本地的路径
        NSString *filePath = obj;

        // 上传文件的请求体格式
        // 1. 文件的上边界
        // 1.1 获取文件上边界的字符串
        NSMutableString *headerStrM1 = [NSMutableString stringWithFormat:@"\r\n--%@\r\n", kBoundary];

        [headerStrM1 appendFormat:@"Content-Disposition: form-data; name=%@; filename=%@\r\n", fileKey, fileName];

        [headerStrM1 appendFormat:@"Content-Type: %@\r\n\r\n", @"application/octet-stream"];

        // 1.2 将字符串转为二进制数据,并添加到请求体中
        [data appendData:[headerStrM1 dataUsingEncoding:NSUTF8StringEncoding]];

        // 2. 获取文件的二进制数据,并添加到请求体中
        [data appendData:[NSData dataWithContentsOfFile:filePath]];
    }];
  • 遍历普通文本参数字典
    [parameters enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        //  参数格式:
        //    -----------------------------16778832101575341713442286528
        //    Content-Disposition: form-data; name="username"
        //
        //    Wpf

        NSString *parameterKey = key;
        NSString *parameterValue = obj;

        // 1. 普通参数的上边界
        NSMutableString *parameterStr = [NSMutableString stringWithFormat:@"\r\n--%@\r\n", kBoundary];
        // name=%@ :服务器接收普通文本参数的key值.后端人员告诉我们.
        // 文本参数也有可能有多个...
        [parameterStr appendFormat:@"Content-Disposition: form-data; name=%@\r\n\r\n", parameterKey];

        // 2. 第一个普通参数的内容
        [parameterStr appendString:parameterValue];

        [data appendData:[parameterStr dataUsingEncoding:NSUTF8StringEncoding]];
    }];

如果在 iOS 中,要实现POST上传文件,需要按照上述格式,拼接数据!

因为:格式是 W3C 指定的标准格式,苹果没有做任何封装!其他语言,都做了封装!

3. 方法的调用

  • 设置请求体
    request.HTTPBody = [self getHttpBodyWithFileKey:@"userfile[]" fileDict:fileDict parameters:parameters];

七. POST多文件上传方法的封装

  • 基本参数
  • urlString:网络接口
  • fileKey:服务器接收用户上传文件的key值
  • fileDict:文件字典
  • parameters:普通文本参数的字典
  • success:上传成功时调用的block
  • fail:上传失败时调用的block
  • 方法名
    - (void)POSTMoreFileWithUrlString:(NSString *)urlString
     fileKey:(NSString *)fileKey fileDict:(NSDictionary *)fileDict
     parameters:(NSDictionary *)parameters
     success:(successBlock)success fail:(failBlock)fail;
  • 其他改动同POST单文件的深入封装-上传封装

八. AFN-第三方框架的使用

  • AFN 能够同时实现上传一个文件,有些格式的文件,用 AFN 无法上传!
  • ASI 能够同时实现上传多个文件,MRC的,2012年就停止更新了,设计的目标平台, iOS 2.0/iOS 3.0 !

1. 上传文件

    // 1. 创建管理者
    AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager];

    // 2. 发送请求
    [mgr POST:@"http://localhost/upload/upload.php" parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
        // formData :设置上传文件所需要的参数,两种上传方法:
        // <1> 通过本地文件的 url 上传
        {
            NSString *fromFile = @"/Users/wangpengfei/Desktop/meinv.jpg";

            NSURL *url = [NSURL URLWithString:@"file:///Users/wangpengfei/Desktop/IMG_5544.jpg"];
            // url :需要上传文件的文件路径
            // name :服务器接收的文件名.
            // fileName: 文件在服务器中保存的名字
            // mimeType : 文件类型
            [formData appendPartWithFileURL:url name:@"userfile" fileName:@"beauty.jpg" mimeType:@"image/jpg" error:NULL];
        }
        // <2> 通过文件的 二进制数据 上传
        {
            NSData *data = [NSData dataWithContentsOfFile:zipFile];

            [formData appendPartWithFileData:data name:@"userfile" fileName:@"beauty.zip" mimeType:@"gzip"];
        }

        } success:^(AFHTTPRequestOperation *operation, id responseObject) {
            // 上传成功之后的回调
            NSLog(@"%@",responseObject);

        } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
            //  上传失败之后的回调
            NSLog(@"error-->%@", error);
    }];

2. 监测网络状态

    // 创建 网络状态管理者
    AFNetworkReachabilityManager *mgr = [AFNetworkReachabilityManager sharedManager];

    // 监测网络状态的改变
    [mgr setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
        // 当网络状态发生改变的时候调用这个block
        switch (status) {
            case AFNetworkReachabilityStatusReachableViaWiFi:
            NSLog(@"WIFI网络");
            break;

            case AFNetworkReachabilityStatusReachableViaWWAN:
            NSLog(@"蜂窝网络");
            break;

            case AFNetworkReachabilityStatusNotReachable:
            NSLog(@"没有网络");
            break;

            case AFNetworkReachabilityStatusUnknown:
            NSLog(@"未知网络");
            break;
            default:
                break;
        }
    }];

    // 开始监控
    [mgr startMonitoring];

3. Reachability 监测网络状态(第三方框架)

  • 注册通知观察者,网络状态改变时,接收通知!
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(InternetStatusChanged) name:kReachabilityChangedNotification object:nil];

    // 控制器销毁时,移除通知观察者.
    -(void)dealloc
    {
        [[NSNotificationCenter defaultCenter] removeObserver:self];
    }
  • 根据当前网络状态,做出不同的响应.
    - (void)InternetStatusChanged
    {
        NSLog(@"网络状态改变了");

        if ([Reachability reachabilityForLocalWiFi].currentReachabilityStatus == ReachableViaWiFi) {
            NSLog(@"Wifi 网络");
        }
        if ([Reachability reachabilityForInternetConnection].currentReachabilityStatus == ReachableViaWWAN) {
            NSLog(@"蜂窝移动网络");
        }
        if ([Reachability reachabilityForInternetConnection].currentReachabilityStatus == NotReachable)
        {
            NSLog(@"没有网络");
        }
    }
  • 创建 Reachability 对象,开始监测网络状态的改变
    - (void)MonitorInternetStatus
    {
        Reachability *reachability = [Reachability reachabilityForInternetConnection];

        [reachability startNotifier];

        self.reachability = reachability;
    }

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    {
        [self MonitorInternetStatus];
    }

推荐阅读更多精彩内容

  • /*----------------- 01 POST上传单个文件 -----------------*/ 重点:...
    蓝心儿的蓝色之旅阅读 1,945评论 0 5
  • iOS开发系列--网络开发 概览 大部分应用程序都或多或少会牵扯到网络开发,例如说新浪微博、微信等,这些应用本身可...
    lichengjin阅读 2,947评论 2 7
  • 小文件下载如果文件比较小,下载方式会比较多直接用NSData的+ (id)dataWithContentsOfUR...
    醉叶惜秋阅读 550评论 0 0
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 8,406评论 6 13
  • 我想多年后 我也会如现在般的累或者是超过如今这般 但也依旧在鼓励自己要在坚持下再努力下再把生活变得充实一些再阳光一...
    柒亓玘呮阅读 28评论 0 0
  • 在24.2.0版本之前,v4 库所有模块都集中在一起。为了提高效率、减少方法数量以及缩小 APP 体积,此库拆分成...
    sunrain_阅读 1,009评论 0 1
  • 最后我才明白,旅馆里终究只能住着旅客,别人说的长厢厮守、浓浓烈烈,终究只能在远方。 “如果你愿意,请在我的耳朵上旅...
    知骤暖阅读 288评论 1 3
  • 七先生:小宝,你怎么那么容易被我套路啊?哈哈 猫小姐:因为我爱你呀!(言外之意,我爱你,愿意被你套路) 七先生:不...
    肋骨小姐T阅读 87评论 0 0
  • 前几天我不小心摔了一下导致脚骨折。在家里养病的时候,妹妹带着她的儿子过来照顾我和我闺女。对于我骨折这件事情,家里两...
    幸福兜了一个圈阅读 469评论 0 1