使用NSURLConnection上傳文件
上傳文件,使用POST還是PUT請求?根據(jù)原來HTTP的定義使用PUT來做上傳,但是現(xiàn)在開發(fā)中,用的是POST。
1.單文件上傳
發(fā)送請求的步驟
1.設(shè)置url
2.設(shè)置request,設(shè)置請求頭、請求體
3.發(fā)送請求
設(shè)置請求頭
Content-Type multipart/form-data; boundary=一個字符串
這一行必須手動告訴服務(wù)器:本次上傳的是文件信息。如果上傳的是一個普通的字符串,則不需要寫這行代碼
設(shè)置請求體
上傳的格式需要自己寫。POST上傳文件的格式遵循W3C制定的標(biāo)準(zhǔn),但是OC沒有做封裝,自己寫的時候必須按照這個格式來寫。
格式如下:(這里上傳的是一個文件名為“JSON”的json本地文件)
--boundary //上邊界 //“boundary”是一個邊界,沒有實際的意義,可以用任意字符串來替代
Content-Disposition: form-data; name=xxx; filename=xxx
Content-Type: application/octet-stream
(空一行)
文件內(nèi)容的二進(jìn)制數(shù)據(jù)
--boundary-- //下邊界
- 請求體內(nèi)容分為三個部分:
1.上邊界部分,告訴服務(wù)器要做數(shù)據(jù)上傳,包含:
a. 服務(wù)器的接收字段name=xxx。xxx是負(fù)責(zé)上傳文件腳本中的 字段名,開發(fā)的時候,可以咨詢后端程序員,不需要自己設(shè)定。
b. 文件在服務(wù)器中保存的名稱filename=xxx。xxx可以自己指定,不一定和本地原本的文件名相同
c. 上傳文件的數(shù)據(jù)類型 application/octet-stream
2.上傳文件的數(shù)據(jù)部分(二進(jìn)制數(shù)據(jù))
3.下邊界部分,嚴(yán)格按照字符串格式來設(shè)置.
上邊界部分和下邊界部分的字符串,最后都要轉(zhuǎn)換成二進(jìn)制數(shù)據(jù),和文件部分的二進(jìn)制數(shù)據(jù)拼接在一起,作為請求體發(fā)送給服務(wù)器.
實現(xiàn)代碼如下:
#define bound @"boundary"
- (void)test{
NSURL *url = [NSURL URLWithString:@"xxxxxxx"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
//設(shè)置請求頭
NSString *headerStr = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",bound];
[request setValue:headerStr forHTTPHeaderField:@"Content-Type"];
//設(shè)置請求體
request.HTTPBody = [self setHttpBody];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
}];
}
//設(shè)置請求體
//必須手動設(shè)置換行,\r\n:保證一定會換行,所有服務(wù)器都識別(不是/r/n)
- (NSData *)setHttpBody{
NSMutableString *bodyHeaderStr = [NSMutableString stringWithFormat:@"--%@\r\n",bound];
//"userfile":服務(wù)器接收文件參數(shù)的key值,服務(wù)器端定義,不需要自己指定
//filename:文件上傳到服務(wù)器之后保存的名稱??梢宰约褐付?不一定和本地原本的文件名相同
[bodyHeaderStr appendFormat:@"Content-Disposition: form-data; name=%@; filename=%@\r\n",@"userfile",@"JSON"];
//Content-Type:上傳文件的文件類型 application/octet-stream :數(shù)據(jù)流格式,如果不知道文件類型,可以直接設(shè)置為這個格式
[bodyHeaderStr appendString:@"Content-Type: application/octet-stream\r\n\r\n"];//兩個換行
//文件內(nèi)容:二進(jìn)制
NSData *fileData = [NSData dataWithContentsOfFile:@"本地文件路徑"];
NSMutableString *bodyFooterStr = [NSMutableString stringWithFormat:@"\r\n--%@--",bound];
//拼接成二進(jìn)制數(shù)據(jù)
NSMutableData *bodyData = [NSMutableData data];
[bodyData appendData:[bodyHeaderStr dataUsingEncoding:NSUTF8StringEncoding]];
[bodyData appendData:fileData];
[bodyData appendData:[bodyFooterStr dataUsingEncoding:NSUTF8StringEncoding]];
return bodyData;
}
封裝設(shè)置請求體的方法
如果上傳的不再是json數(shù)據(jù)文件而是一張圖片,以上設(shè)置請求體方法的代碼需要作出改變。因此需要對此進(jìn)行封裝。
封裝的方法需要提供 文件路徑、服務(wù)器的接收字段name、文件上傳到服務(wù)器后保存的名稱filename 這些傳入?yún)?shù)。
上傳文件的時候,需要告訴服務(wù)器文件類型(即Content-Type),這時,需要獲取文件的 MIMEType.獲取文件的 MIMEType 方法:發(fā)送一個同步請求,通過 response 獲得。如果不想告訴服務(wù)器具體的文件類型,可以使用這個 Content-Type : application/octet-stream(8進(jìn)制流)
通過發(fā)送一個同步請求來獲得上傳的文件類型:
//動態(tài)獲得文件類型,通過發(fā)送一個同步請求
-(NSURLResponse *)getFileTypeWithPath:(NSString *)path{
//根據(jù)本地文件路徑,設(shè)置一個本地的url
//file 資源是本地計算機(jī)上的文件。格式file:///,注意后邊應(yīng)是三個斜杠(最后一個杠屬于傳入的路徑的一部分,所以下面只有兩個杠)。
NSString *urlstr = [NSString stringWithFormat:@"file://%@",path];
//發(fā)送一個同步請求來獲得文件類型
NSURL *url = [NSURL URLWithString:urlstr];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//(NSURLResponse *__autoreleasing _Nullable * _Nullable) 有兩個**,先指定一塊地址,內(nèi)容為空。等方法執(zhí)行完畢之后,會將返回的內(nèi)容存儲到這塊地址中。
NSURLResponse *response = nil;
// 同步請求: 阻塞當(dāng)前線程
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:nil];
//MIMEType就是需要的文件類型
//expectedContentLength文件的長度。一般在文件下載的時候使用,類型是lld
//suggestedFilename建議的文件名稱
NSLog(@"response: %@ %@ %lld",response.MIMEType, response.suggestedFilename, response.expectedContentLength);
return response;
}
封裝請求體設(shè)置:
/*
filePath:需要上傳的文件路徑。(手機(jī)訪問相冊!選擇一張圖片.是拿到圖片的二進(jìn)制數(shù)據(jù)?還是拿到圖片的路徑? -- 路徑)
key : 服務(wù)器接受文件的 key 值
name: 文件上傳到服務(wù)器之后保存的名稱
上傳文件的文件類型:根據(jù)文件路徑,自動獲得文件類型
*/
- (NSData *)packageWithPath:(NSString *)filePath fileKey:(NSString *)key fileName:(NSString *)name{
//根據(jù)文件路徑,發(fā)送同步請求,獲得文件信息
NSURLResponse *response = [self getFileTypeWithPath:filePath];
if (!name) {//如果沒有傳入name值,默認(rèn)使用建議的文件名
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];//兩個換行
//文件內(nèi)容:二進(jìn)制
NSData *fileData = [NSData dataWithContentsOfFile:filePath];
NSMutableString *bodyFooterStr = [NSMutableString stringWithFormat:@"\r\n--%@--",bound];
//拼接成二進(jìn)制數(shù)據(jù)
NSMutableData *bodyData = [NSMutableData data];
[bodyData appendData:[bodyHeaderStr dataUsingEncoding:NSUTF8StringEncoding]];
[bodyData appendData:fileData];
[bodyData appendData:[bodyFooterStr dataUsingEncoding:NSUTF8StringEncoding]];
return bodyData;
}
使用封裝后的方法設(shè)置請求體:
request.HTTPBody = [self packageWithPath:@"/Users/apple/Desktop/bd_logo1.png" fileKey:@"userfile" fileName:nil];
對文件上傳進(jìn)行整體封裝
POST請求的設(shè)置、發(fā)送以及請求體的封裝等全部放到一個工具類中進(jìn)行整體封裝,當(dāng)需要進(jìn)行文件上傳時,直接調(diào)用該工具類提供的接口即可
NetworkTool.h
// 定義兩個 Block : 1. 成功Block回調(diào) 2.失敗的 Block 回調(diào)!
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. 創(chuàng)建請求
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"];
// 設(shè)置請求體
request.HTTPBody = [self getHttpBodyWithFilePath:filePath FileKey:key FileName:name];
// 2. 發(fā)送請求
// 在系統(tǒng)內(nèi)的Block 中調(diào)用自己的 成功或者失敗的回調(diào)
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
// 成功或者失敗:根據(jù)服務(wù)器返回的參數(shù)判定
//簡單舉例
if (data && !connectionError) { // 成功
// 調(diào)用 成功的回調(diào)
if (Success) {
Success(data,response);
}
}else
{
if (fail) {
// 失敗之后的回調(diào)!
fail(connectionError);
}
}
}];
}
- (NSData *)packageWithPath:(NSString *)filePath fileKey:(NSString *)key fileName:(NSString *)name{
......
}
-(NSURLResponse *)getFileTypeWithPath:(NSString *)path{
......
}
// 獲得單例對象,只有通過這個方法獲得的才是單例對象
// 沒有把其他創(chuàng)建對象實例的方法也堵死了,從而可以讓別人自己選擇實例化對象的方法
+(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(@"網(wǎng)絡(luò)鏈接錯誤");
}];
2.多文件上傳(帶普通文本參數(shù))
多文件上傳和單文件上傳的思路相似,區(qū)別在于設(shè)置請求體。
另外,有些服務(wù)器可以在上傳文件的同時,提交一些文本內(nèi)容給服務(wù)器,比如:
1.新浪微博: 上傳圖片的同時,發(fā)送一條微博信息
2.購物評論: 購買商品之后發(fā)表評論的時候圖片+評論內(nèi)容
多文件+普通文本 上傳的請求體格式如下:
--boundary\r\n // 第一個文件參數(shù)//上邊界,不過也可以寫成這樣:\r\n--boundary\r\n
Content-Disposition: form-data; name=xxx; filename=xxx\r\n
Content-Type:image/jpeg\r\n\r\n
(空一行)
上傳文件的二進(jìn)制數(shù)據(jù)部分
\r\n--boundary\r\n // 第二個文件參數(shù)//上邊界 //文件一的下邊界可略,在這句之前插入文件一的下邊界\r\n--boundary--也可以
Content-Disposition: form-data; name=xxx; filename=xxx\r\n
Content-Type:text/plain\r\n\r\n
(空一行)
上傳文件的二進(jìn)制數(shù)據(jù)部分
\r\n--boundary\r\n //普通文本參數(shù) //上邊界
Content-Disposition: form-data; name="xxx"\r\n\r\n //name是服務(wù)器的接收字段,不需要自己制定
(空一行)
普通文本二進(jìn)制數(shù)據(jù)
\r\n--boundary-- // 下邊界
普通文本的上傳格式不需要Content-Type
封裝設(shè)置請求體的方法
思路:
1.由于文件內(nèi)容是可變的,因此創(chuàng)建一個文件參數(shù)字典以要上傳文件的本地路徑為key、文件在服務(wù)器中保存的名稱為value。
2.普通文本信息(字符串信息) 有可能有多個值。創(chuàng)建一個普通文本參數(shù)字典, 以服務(wù)器接受文本參數(shù)的key值為 key、以上傳的普通文本參數(shù)為 value
此處省略動態(tài)獲得上傳的文件類型的方法,直接設(shè)置上傳文件類型為application/octet-stream
#define bound @"boundary"
// 文件參數(shù)字典: fileName :key filePath:value
// fileDict 文件參數(shù)字典
// fileKey 服務(wù)器接受文件參數(shù)的key值
// paramaters 普通文本參數(shù)字典
- (NSData *)getHttpBodyWithFileDict:(NSDictionary *)fileDict fileKey:(NSString *)fileKey paramater:(NSDictionary *)paramaters
{
NSMutableData *data = [NSMutableData data];
// 遍歷文件參數(shù)字典,設(shè)置文件的格式
[fileDict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
// 取出每一條字典數(shù)據(jù): fileName : 服務(wù)器保存的名稱 , 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]];
// 將文件內(nèi)容添加到請求體中
[data appendData:[NSData dataWithContentsOfFile:filePath]];
}];
// 遍歷普通文本參數(shù)字典
[paramaters enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
// msgKey :服務(wù)器接受參數(shù)的key值 msgValue:上傳的文本參數(shù)
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;
}
使用封裝后的方法設(shè)置請求體:
......略創(chuàng)建請求
NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",kBounary];
[request setValue:contentType forHTTPHeaderField:@"Content-Type"];
// 設(shè)置請求體
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:@{@"服務(wù)器key1":@"普通文本1",@"服務(wù)器key2":@"普通文本2"}];
//發(fā)送請求
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSLog(@"%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}];
多文件上傳整體封裝
與單文件上傳的整體封裝思路一樣。略