使用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]);
}];
多文件上传整体封装
与单文件上传的整体封装思路一样。略