NSURLConnection笔记-上传文件

使用NSURLConnection上传文件
上传文件,使用POST还是PUT请求?根据原来HTTP的定义使用PUT来做上传,但是现在开发中,用的是POST。

1.单文件上传

发送请求的步骤
1.设置url
2.设置request,设置请求头、请求体
3.发送请求

设置请求头
Content-Type multipart/form-data; boundary=一个字符串
这一行必须手动告诉服务器:本次上传的是文件信息。如果上传的是一个普通的字符串,则不需要写这行代码

设置请求体
上传的格式需要自己写。POST上传文件的格式遵循W3C制定的标准,但是OC没有做封装,自己写的时候必须按照这个格式来写。
格式如下:(这里上传的是一个文件名为“JSON”的json本地文件)

--boundary //上边界 //“boundary”是一个边界,没有实际的意义,可以用任意字符串来替代
Content-Disposition: form-data; name=xxx; filename=xxx
Content-Type: application/octet-stream
(空一行)
文件内容的二进制数据
--boundary-- //下边界
  • 请求体内容分为三个部分:
    1.上边界部分,告诉服务器要做数据上传,包含:
    a. 服务器的接收字段name=xxx。xxx是负责上传文件脚本中的 字段名,开发的时候,可以咨询后端程序员,不需要自己设定。
    b. 文件在服务器中保存的名称filename=xxx。xxx可以自己指定,不一定和本地原本的文件名相同
    c. 上传文件的数据类型 application/octet-stream
    2.上传文件的数据部分(二进制数据)
    3.下边界部分,严格按照字符串格式来设置.

上边界部分和下边界部分的字符串,最后都要转换成二进制数据,和文件部分的二进制数据拼接在一起,作为请求体发送给服务器.

实现代码如下:

#define bound @"boundary"

- (void)test{
    NSURL *url = [NSURL URLWithString:@"xxxxxxx"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    
    request.HTTPMethod = @"POST";
    //设置请求头
    NSString *headerStr = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",bound];
    [request setValue:headerStr forHTTPHeaderField:@"Content-Type"];
    //设置请求体
    request.HTTPBody = [self setHttpBody];
 
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
        
    }];
}

//设置请求体
//必须手动设置换行,\r\n:保证一定会换行,所有服务器都识别(不是/r/n)
- (NSData *)setHttpBody{
    NSMutableString *bodyHeaderStr = [NSMutableString stringWithFormat:@"--%@\r\n",bound];
    //"userfile":服务器接收文件参数的key值,服务器端定义,不需要自己指定
    //filename:文件上传到服务器之后保存的名称。可以自己指定,不一定和本地原本的文件名相同
    [bodyHeaderStr appendFormat:@"Content-Disposition: form-data; name=%@; filename=%@\r\n",@"userfile",@"JSON"];
    //Content-Type:上传文件的文件类型 application/octet-stream :数据流格式,如果不知道文件类型,可以直接设置为这个格式
    [bodyHeaderStr appendString:@"Content-Type: application/octet-stream\r\n\r\n"];//两个换行

    //文件内容:二进制
    NSData *fileData = [NSData dataWithContentsOfFile:@"本地文件路径"];
    
    NSMutableString *bodyFooterStr = [NSMutableString stringWithFormat:@"\r\n--%@--",bound];
    
    //拼接成二进制数据
    NSMutableData *bodyData = [NSMutableData data];
    [bodyData appendData:[bodyHeaderStr dataUsingEncoding:NSUTF8StringEncoding]];
    [bodyData appendData:fileData];
    [bodyData appendData:[bodyFooterStr dataUsingEncoding:NSUTF8StringEncoding]];
    return bodyData;
}

封装设置请求体的方法

如果上传的不再是json数据文件而是一张图片,以上设置请求体方法的代码需要作出改变。因此需要对此进行封装。
封装的方法需要提供 文件路径、服务器的接收字段name、文件上传到服务器后保存的名称filename 这些传入参数。
上传文件的时候,需要告诉服务器文件类型(即Content-Type),这时,需要获取文件的 MIMEType.获取文件的 MIMEType 方法:发送一个同步请求,通过 response 获得。如果不想告诉服务器具体的文件类型,可以使用这个 Content-Type : application/octet-stream(8进制流)

通过发送一个同步请求来获得上传的文件类型:

//动态获得文件类型,通过发送一个同步请求
-(NSURLResponse *)getFileTypeWithPath:(NSString *)path{
    //根据本地文件路径,设置一个本地的url
    //file 资源是本地计算机上的文件。格式file:///,注意后边应是三个斜杠(最后一个杠属于传入的路径的一部分,所以下面只有两个杠)。
    NSString *urlstr = [NSString stringWithFormat:@"file://%@",path];
    //发送一个同步请求来获得文件类型
    NSURL *url = [NSURL URLWithString:urlstr];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    //(NSURLResponse *__autoreleasing  _Nullable * _Nullable) 有两个**,先指定一块地址,内容为空。等方法执行完毕之后,会将返回的内容存储到这块地址中。
    NSURLResponse *response = nil;
    // 同步请求: 阻塞当前线程
    [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:nil];
    //MIMEType就是需要的文件类型
    //expectedContentLength文件的长度。一般在文件下载的时候使用,类型是lld
    //suggestedFilename建议的文件名称
    NSLog(@"response: %@ %@ %lld",response.MIMEType, response.suggestedFilename, response.expectedContentLength);
    return response;
}

封装请求体设置:

/*
 filePath:需要上传的文件路径。(手机访问相册!选择一张图片.是拿到图片的二进制数据?还是拿到图片的路径? -- 路径)
 key : 服务器接受文件的 key 值
 name: 文件上传到服务器之后保存的名称
 上传文件的文件类型:根据文件路径,自动获得文件类型
*/
- (NSData *)packageWithPath:(NSString *)filePath fileKey:(NSString *)key fileName:(NSString *)name{
    //根据文件路径,发送同步请求,获得文件信息
    NSURLResponse *response = [self getFileTypeWithPath:filePath];
    
    if (!name) {//如果没有传入name值,默认使用建议的文件名
        name = response.suggestedFilename;
    }
    
    NSMutableString *bodyHeaderStr = [NSMutableString stringWithFormat:@"--%@\r\n",bound];
    [bodyHeaderStr appendFormat:@"Content-Disposition: form-data; name=%@; filename=%@\r\n",key,name];
    [bodyHeaderStr appendFormat:@"Content-Type: %@\r\n\r\n",response.MIMEType];//两个换行
    
    //文件内容:二进制
    NSData *fileData = [NSData dataWithContentsOfFile:filePath];
    
    NSMutableString *bodyFooterStr = [NSMutableString stringWithFormat:@"\r\n--%@--",bound];
    
    //拼接成二进制数据
    NSMutableData *bodyData = [NSMutableData data];
    [bodyData appendData:[bodyHeaderStr dataUsingEncoding:NSUTF8StringEncoding]];
    [bodyData appendData:fileData];
    [bodyData appendData:[bodyFooterStr dataUsingEncoding:NSUTF8StringEncoding]];
    return bodyData;
}

使用封装后的方法设置请求体:
request.HTTPBody = [self packageWithPath:@"/Users/apple/Desktop/bd_logo1.png" fileKey:@"userfile" fileName:nil];

对文件上传进行整体封装

POST请求的设置、发送以及请求体的封装等全部放到一个工具类中进行整体封装,当需要进行文件上传时,直接调用该工具类提供的接口即可

NetworkTool.h
// 定义两个 Block : 1. 成功Block回调 2.失败的 Block 回调!
typedef void(^SuccessBlock)(NSData *data, NSURLResponse *response);
typedef void(^failBlock)(NSError *error);

NetworkTool.m
#define bound @"boundary"

- (void)POSTFileWithUrlString:(NSString *)urlString FilePath:(NSString *)filePath FileKey:(NSString *)key FileName:(NSString *)name SuccessBlock:(SuccessBlock)Success FailBlock:(failBlock)fail
{
    // 1. 创建请求
    NSURL *url = [NSURL URLWithString:urlString];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    
    request.HTTPMethod = @"POST";
    NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",bound];
    [request setValue:contentType forHTTPHeaderField:@"Content-Type"];
    // 设置请求体
    request.HTTPBody = [self getHttpBodyWithFilePath:filePath FileKey:key FileName:name];
      
    // 2. 发送请求
    // 在系统内的Block 中调用自己的 成功或者失败的回调
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        // 成功或者失败:根据服务器返回的参数判定
        //简单举例
        if (data && !connectionError) {  // 成功            
            // 调用 成功的回调
            if (Success) {
                Success(data,response);
            }
        }else
        {
            if (fail) {
                // 失败之后的回调!
                fail(connectionError);
            }
        }
    }];
}

- (NSData *)packageWithPath:(NSString *)filePath fileKey:(NSString *)key fileName:(NSString *)name{
    ......
}

-(NSURLResponse *)getFileTypeWithPath:(NSString *)path{
    ......
}

// 获得单例对象,只有通过这个方法获得的才是单例对象
// 没有把其他创建对象实例的方法也堵死了,从而可以让别人自己选择实例化对象的方法
+(instancetype)sharedNetworkTool{
    static id _instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[self alloc] init];
    }); 
    return _instance;
}

在ViewController中,需要上传文件时:

CZNetworkTool *tool = [CZNetworkTool sharedNetworkTool];    
[tool POSTFileWithUrlString:@"xxxurl" FilePath:@"xxx本地文件路径" FileKey:@"userfile" FileName:nil SuccessBlock:^(NSData *data, NSURLResponse *response) {
        NSLog(@"请求成功");
    } FailBlock:^(NSError *error) {
        NSLog(@"网络链接错误");
    }];

2.多文件上传(带普通文本参数)

多文件上传和单文件上传的思路相似,区别在于设置请求体。
另外,有些服务器可以在上传文件的同时,提交一些文本内容给服务器,比如:
1.新浪微博: 上传图片的同时,发送一条微博信息
2.购物评论: 购买商品之后发表评论的时候图片+评论内容

多文件+普通文本 上传的请求体格式如下:

--boundary\r\n           // 第一个文件参数//上边界,不过也可以写成这样:\r\n--boundary\r\n 
Content-Disposition: form-data; name=xxx; filename=xxx\r\n
Content-Type:image/jpeg\r\n\r\n        
(空一行)        
上传文件的二进制数据部分    
\r\n--boundary\r\n    // 第二个文件参数//上边界 //文件一的下边界可略,在这句之前插入文件一的下边界\r\n--boundary--也可以
Content-Disposition: form-data; name=xxx; filename=xxx\r\n
Content-Type:text/plain\r\n\r\n
(空一行)                
上传文件的二进制数据部分  
\r\n--boundary\r\n    //普通文本参数 //上边界
Content-Disposition: form-data; name="xxx"\r\n\r\n    //name是服务器的接收字段,不需要自己制定
(空一行)     
普通文本二进制数据     
\r\n--boundary--       // 下边界

普通文本的上传格式不需要Content-Type

封装设置请求体的方法

思路:
1.由于文件内容是可变的,因此创建一个文件参数字典以要上传文件的本地路径为key、文件在服务器中保存的名称为value。
2.普通文本信息(字符串信息) 有可能有多个值。创建一个普通文本参数字典, 以服务器接受文本参数的key值为 key、以上传的普通文本参数为 value

此处省略动态获得上传的文件类型的方法,直接设置上传文件类型为application/octet-stream

#define bound @"boundary"
// 文件参数字典: fileName :key filePath:value
// fileDict 文件参数字典
// fileKey 服务器接受文件参数的key值
// paramaters 普通文本参数字典
- (NSData *)getHttpBodyWithFileDict:(NSDictionary *)fileDict fileKey:(NSString *)fileKey paramater:(NSDictionary *)paramaters
{
    NSMutableData *data = [NSMutableData data];
    // 遍历文件参数字典,设置文件的格式
    [fileDict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        // 取出每一条字典数据: fileName : 服务器保存的名称 , filePath: 文件路径
        NSString *fileName = key;
        NSString *filePath = obj;    
    
        //文件的上边界
        NSMutableString *headerStrM = [NSMutableString stringWithFormat:@"\r\n--%@\r\n", bound];        
        [headerStrM appendFormat:@"Content-Disposition: form-data; name=%@; filename=%@\r\n",fileKey,fileName];
        [headerStrM appendFormat:@"Content-Type: application/octet-stream\r\n\r\n"];
        
        // 将文件的上边界添加到请求体中!
        [data appendData:[headerStrM dataUsingEncoding:NSUTF8StringEncoding]];        
        // 将文件内容添加到请求体中
        [data appendData:[NSData dataWithContentsOfFile:filePath]];
        
    }];   
    // 遍历普通文本参数字典
    [paramaters enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {        
        // msgKey :服务器接受参数的key值 msgValue:上传的文本参数
        NSString *msgKey = key;
        NSString *msgValue = obj;
        
        // 普通文本信息上边界
        NSMutableString *headerStrM = [NSMutableString stringWithFormat:@"\r\n--%@\r\n", bound];       
        [headerStrM appendFormat:@"Content-Disposition: form-data; name=%@\r\n\r\n",msgKey];
        
        [data appendData:[headerStrM dataUsingEncoding:NSUTF8StringEncoding]];        
        // 普通文本信息;
        [data appendData:[msgValue dataUsingEncoding:NSUTF8StringEncoding]];
    }]; 
    // 3. 下边界 (只添加一次)
    NSMutableString *footerStrM = [NSMutableString stringWithFormat:@"\r\n--%@--",bound];
    [data appendData:[footerStrM dataUsingEncoding:NSUTF8StringEncoding]];    
    return data;
}

使用封装后的方法设置请求体:

......略创建请求

NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",kBounary];
[request setValue:contentType forHTTPHeaderField:@"Content-Type"];
// 设置请求体
NSString *fileName1 = @"文件1";
NSString *filePath1 = @"本地路径1xxx";    
NSString *fileName2 = @"文件2";
NSString *filePath2 = @"本地路径2xxx";

NSDictionary *fileDict = @{fileName1:filePath1,fileName2 :filePath2};
request.HTTPBody = [self getHttpBodyWithFileDict:fileDict fileKey:@"xxx" paramater:@{@"服务器key1":@"普通文本1",@"服务器key2":@"普通文本2"}];

//发送请求
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
    NSLog(@"%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);        
}];

多文件上传整体封装

与单文件上传的整体封装思路一样。略

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,497评论 18 139
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,802评论 6 13
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,947评论 4 60
  • 自己去医院,确实不是一种什么好的体验,但也不是太糟糕的体验。感觉自己没什么不好,还是要多去爱别人,多去爱这...
    6a7bdcc57e6f阅读 109评论 1 0
  • 今天的话题是如何通过整理来提升在婆家的地位? 作为初为人妻 初为儿媳 初为人母,同时要扮演三个角色的我,如何把这个...
    知恩艺生阅读 618评论 0 0