概览
大部分应用程序都或多或少会牵扯到网络开发,例如说新浪微博、微信等,这些应用本身可能采用iOS开发,但是所有的数据支撑都是基于后台网络服务器的。如今,网络编程越来越普遍,孤立的应用通常是没有生命力的。今天就会给大家介绍这部分内容
Web请求和响应
使用代理方法
做过Web开发的朋友应该很清楚,Http是无连接的请求。每个请求request服务器都有一个对应的响应response,无论是asp.net、jsp、php都是基于这种机制开发的。
在Web开发中主要的请求方法有如下几种:
1.GET请求:get是获取数据的意思,数据以明文在URL中传递,受限于URL长度,所以传输数据量比较小。
2.POST请求:post是向服务器提交数据的意思,提交的数据以实际内容形式存放到消息头进行传递,无法在浏览器url中查看到,大小没有限制。
3.HEAD请求:请求头信息,并不返回请求数据体,而只返回请求头信息,常用用于在文件下载中取得文件大小、类型等信息。
程序的实现需要借助几个对象:
1.NSURLRequest:建立了一个请求,可以指定缓存策略,超时时间。和NSURLRequest对应的还有一个NSMutableURLRequest,如果请求定义为NSMutableURLRequest则可以指定请求方法(GET或POST)等信息。
2.NSURLConnection:用于发送请求,可以指定请求和代理。当前调用NSURLConnection的start方法后开始发送异步请求。
程序代码如下:
#import "KCMainViewController.h"
@interface KCMainViewController ()<NSURLConnectionDataDelegate>{
NSMutableData *_data;//响应数据
UITextField *_textField;
UIButton *_button;
UIProgressView *_progressView;
UILabel *_label;
long long _totalLength;
}
@end
@implementation KCMainViewController
#pragma mark - UI方法
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}
#pragma mark - 私有方法
#pragma mark 界面布局
-(void)layoutUI{
//地址栏
_textField=[[UITextField alloc]initWithFrame:CGRectMake(10, 50, 300, 25)];
_textField.borderStyle=UITextBorderStyleRoundedRect;
_textField.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0];
_textField.text=@"简约至上:交互式设计四策略.epub";
[self.view addSubview:_textField];
//进度条
_progressView=[[UIProgressView alloc]initWithFrame:CGRectMake(10, 100, 300, 25)];
[self.view addSubview:_progressView];
//状态显示
_label=[[UILabel alloc]initWithFrame:CGRectMake(10, 130, 300, 25)];
_label.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0];
[self.view addSubview:_label];
//下载按钮
_button=[[UIButton alloc]initWithFrame:CGRectMake(10, 500, 300, 25)];
[_button setTitle:@"下载" forState:UIControlStateNormal];
[_button setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal];
[_button addTarget:self action:@selector(sendRequest) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_button];
}
#pragma mark 更新进度
-(void)updateProgress{
if (_data.length==_totalLength) {
_label.text=@"下载完成";
}else{
_label.text=@"正在下载...";
[_progressView setProgress:(float)_data.length/_totalLength];
}
}
#pragma mark 发送数据请求
-(void)sendRequest{
NSString *urlStr=[NSString stringWithFormat:@"http://192.168.1.208/FileDownload.aspx?file=%@",_textField.text];
//注意对于url中的中文是无法解析的,需要进行url编码(指定编码类型为utf-8)
//另外注意url解码使用stringByRemovingPercentEncoding方法
urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
//创建url链接
NSURL *url=[NSURL URLWithString:urlStr];
/*创建请求
cachePolicy:缓存策略
a.NSURLRequestUseProtocolCachePolicy 协议缓存,根据response中的Cache-Control字段判断缓存是否有效,如果缓存有效则使用缓存数据否则重新从服务器请求
b.NSURLRequestReloadIgnoringLocalCacheData 不使用缓存,直接请求新数据
c.NSURLRequestReloadIgnoringCacheData 等同于 SURLRequestReloadIgnoringLocalCacheData
d.NSURLRequestReturnCacheDataElseLoad 直接使用缓存数据不管是否有效,没有缓存则重新请求
eNSURLRequestReturnCacheDataDontLoad 直接使用缓存数据不管是否有效,没有缓存数据则失败
timeoutInterval:超时时间设置(默认60s)
*/
NSURLRequest *request=[[NSURLRequest alloc]initWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:15.0f];
//创建连接
NSURLConnection *connection=[[NSURLConnection alloc]initWithRequest:request delegate:self];
//启动连接
[connection start];
}
#pragma mark - 连接代理方法
#pragma mark 开始响应
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
NSLog(@"receive response.");
_data=[[NSMutableData alloc]init];
_progressView.progress=0;
//通过响应头中的Content-Length取得整个响应的总长度
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSDictionary *httpResponseHeaderFields = [httpResponse allHeaderFields];
_totalLength = [[httpResponseHeaderFields objectForKey:@"Content-Length"] longLongValue];
}
pragma mark 接收响应数据(根据响应内容的大小此方法会被重复调用)
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
NSLog(@"receive data.");
//连续接收数据
[_data appendData:data];
//更新进度
[self updateProgress];
}
#pragma mark 数据接收完成
-(void)connectionDidFinishLoading:(NSURLConnection *)connection{
NSLog(@"loading finish.");
//数据接收完保存文件(注意苹果官方要求:下载数据只能保存在缓存目录)
NSString *savePath=[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
savePath=[savePath stringByAppendingPathComponent:_textField.text];
[_data writeToFile:savePath atomically:YES];
NSLog(@"path:%@",savePath);
}
#pragma mark 请求失败
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
//如果连接超时或者连接地址错误可能就会报错
NSLog(@"connection error,error detail is:%@",error.localizedDescription);
}
@end
主要注意:
1.根据响应数据大小不同可能会多次执行- (void)connection:(NSURLConnection )connection didReceiveData:(NSData )data方法。
2.URL中不能出现中文(例如上面使用GET传参数时,file参数就可能是中文),需要对URL进行编码,否则会出错。
简化请求方法
当然,对于上面文件下载这种大数据响应的情况使用代理方法处理响应具有一定的优势(可以获得传输进度)。但是如果现响应数据不是文件而是一段字符串(注意web请求的数据可以是字符串或者二进制,上面文件下载示例中响应数据是二进制),那么采用代理方法处理服务器响应就未免有些太麻烦了。其实苹果官方已经提供了下面两种方法处理一般的请求:
+ (void)sendAsynchronousRequest:request: queue:queue:completionHandler:发送一个异步请求
+ (NSData *)sendSynchronousRequest: returningResponse: error:发送一个同步请求
-(void)sendRequest{
NSString *urlStr=[NSString stringWithFormat:@"%@",kURL];
//注意对于url中的中文是无法解析的,需要进行url编码(指定编码类型位utf-8)
//另外注意url解码使用stringByRemovingPercentEncoding方法
urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
//创建url链接
NSURL *url=[NSURL URLWithString:urlStr];
/*创建可变请求*/
NSMutableURLRequest *requestM=[[NSMutableURLRequest alloc]initWithURL:url cachePolicy:0 timeoutInterval:5.0f];
[requestM setHTTPMethod:@"POST"];//设置位post请求
//创建post参数
NSString *bodyDataStr=[NSString stringWithFormat:@"userName=%@&password=%@",_userName,_password];
NSData *bodyData=[bodyDataStr dataUsingEncoding:NSUTF8StringEncoding];
[requestM setHTTPBody:bodyData];
//发送一个异步请求
[NSURLConnection sendAsynchronousRequest:requestM queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (!connectionError) {
/*json序列化
options:序列化选项,枚举类型,但是可以指定为枚举以外的类型,例如指定为0则可以返回NSDictionary或者NSArray
a.NSJSONReadingMutableContainers:返回NSMutableDictionary或NSMutableArray
b.NSJSONReadingMutableLeaves:返回NSMutableString字符串
c.NSJSONReadingAllowFragments:可以解析JSON字符串的外层既不是字典类型(NSMutableDictionary、NSDictionary)又不是数组类型(NSMutableArray、NSArray)的数据,但是必须是有效的JSON字符串
error:错误信息
*/
NSError *error;
//将对象序列化为字典
NSDictionary *dic= [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
}else{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
}];
}
}];
}
可以看到使用NSURLConnection封装的静态方法可以直接获得NSData,不需要使用代理一步步自己组装数据。这里采用了POST方式发送请求,使用POST发送请求需要组装数据体,不过数据长度不像GET方式存在限制。从iOS5开始苹果官方提供了JSON序列化和反序列化相关方法(上面程序中仅仅用到了反序列化方法,序列化使用dataWithJSONObject:options:opt error:方法)方便的对数组和字典进行序列化和反序列化。但是注意反序列化参数设置,程序中设置成了0,直接反序列化为不可变对象以提高性能。
注意:
1.现在多数情况下互联网数据都是以JSON格式进行传输,但是有时候也会面对XML存储。在IOS中可以使用NSXMLParser进行XML解析,由于实际使用并不多,在此不再赘述。
2.使用KVC给对象赋值时(通常是NSDictionary或NSMutalbeDictionary)注意对象的属性最好不要定义为基本类型(如int),否则如果属性值为null则会报错,最后定义为ObjC对象类型(如使用NSNumber代替int等);
图片缓存
开发Web类的应用图片缓存问题不得不提及,因为图片的下载相当耗时。对于前面的微博数据,头像和微博类型图标在数据库中是以链接形式存放的,取得链接后还必须进行对应的图片加载。大家都知道图片往往要比文本内容大得多,在UITableView中上下滚动就会重新加载数据,对于文本由于已经加载到本地自然不存在问题,但是对于图片来说如果每次都必须重新从服务器端加载就不太合适了。
解决图片加载的办法有很多,可以事先存储到内存中,也可以保存到临时文件。在内存中存储虽然简单但是往往不可取,因为程序重新启动之后还面临这重新请求的问题,类似于新浪微博、QQ、微信等应用一般会存储在文件中,这样应用程序即使重启也会从文件中读取。但是使用文件缓存图片可能就要自己做很多事情,例如缓存文件是否过期?缓存数据越来越大如何管理存储空间?
这些问题其实很多第三方框架已经做的很好了,实际开发中往往会采用一些第三方框架来处理图片。例如这里可以选用SDWebImage框架。SDWebImage使用起来相当简单,开发者不必过多关心它的缓存和多线程加载问题,一个方法就可以解决。
扩展–文件分段下载
通过前面的演示大家应该对于iOS的Web请求有了大致的了解,可以通过代理方法接收数据也可以直接通过静态方法接收数据,但是实际开发中更推荐使用静态方法。关于前面的文件下载示例,更多的是希望大家了解代理方法接收响应数据的过程,实际开发中也不可能使用这种方法进行文件下载。这种下载有个致命的问题:不适合进行大文件分段下载。因为代理方法在接收数据时虽然表面看起来是每次读取一部分响应数据,事实上它只有一次请求并且也只接收了一次服务器响应,只是当响应数据较大时系统会重复调用数据接收方法,每次将已读取的数据拿出一部分交给数据接收方法。这样一来对于上G的文件进行下载,如果中途暂停的话再次请求还是从头开始下载,不适合大文件断点续传(另外说明一点,上面NSURLConnection示例中使用了NSMutableData进行数据接收和追加只是为了方便演示,实际开发建议直接写入文件)。
实际开发文件下载的时候不管是通过代理方法还是静态方法执行请求和响应,我们都会分批请求数据,而不是一次性请求数据。假设一个文件有1G,那么只要每次请求1M的数据,请求1024次也就下载完了。那么如何让服务器每次只返回1M的数据呢?
在网络开发中可以在请求的头文件中设置一个range信息,它代表请求数据的大小。通过这个字段配合服务器端可以精确的控制每次服务器响应的数据范围。例如指定bytes=0-1023,然后在服务器端解析Range信息,返回该文件的0到1023之间的数据的数据即可(共1024Byte)。这样,只要在每次发送请求控制这个头文件信息就可以做到分批请求。
当然,为了让整个数据保持完整,每次请求的数据都需要逐步追加直到整个文件请求完成。但是如何知道整个文件的大小?其实在前面的文件下载演示中大家可以看到,可以通过头文件信息获取整个文件大小。但是这么做的话就必须请求整个数据,这样分段下载就没有任何意义了。所幸在WEB开发中我们还有另一种请求方法“HEAD”,通过这种请求服务器只会响应头信息,其他数据不会返回给客户端,这样一来整个数据的大小也就可以得到了。下面给出完整的程序代码,关键的地方已经给出注释(为了简化代码,这里没有使用代理方法):
#import "KCMainViewController.h"
#define kUrl @"http://192.168.1.208/FileDownload.aspx"
#define kFILE_BLOCK_SIZE (1024) //每次1KB
@interface KCMainViewController ()<NSURLConnectionDataDelegate>{
UITextField *_textField;
UIButton *_button;
UIProgressView *_progressView;
UILabel *_label;
long long _totalLength;
long long _loadedLength;
}
@end
@implementation KCMainViewController
#pragma mark - UI方法
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}
#pragma mark - 私有方法
#pragma mark 界面布局
-(void)layoutUI{
//地址栏
_textField=[[UITextField alloc]initWithFrame:CGRectMake(10, 50, 300, 25)];
_textField.borderStyle=UITextBorderStyleRoundedRect;
_textField.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0];
_textField.text=@"1.jpg";
// _textField.text=@"1.jpg";
[self.view addSubview:_textField];
//进度条
_progressView=[[UIProgressView alloc]initWithFrame:CGRectMake(10, 100, 300, 25)];
[self.view addSubview:_progressView];
//状态显示
_label=[[UILabel alloc]initWithFrame:CGRectMake(10, 130, 300, 25)];
_label.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0];
[self.view addSubview:_label];
//下载按钮
_button=[[UIButton alloc]initWithFrame:CGRectMake(10, 500, 300, 25)];
[_button setTitle:@"下载" forState:UIControlStateNormal];
[_button setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal];
[_button addTarget:self action:@selector(downloadFileAsync) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_button];
}
#pragma mark 更新进度
-(void)updateProgress{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
if (_loadedLength==_totalLength) {
_label.text=@"下载完成";
}else{
_label.text=@"正在下载...";
}
[_progressView setProgress:(double)_loadedLength/_totalLength];
}];
}
#pragma mark 取得请求链接
-(NSURL *)getDownloadUrl:(NSString *)fileName{
NSString *urlStr=[NSString stringWithFormat:@"%@?file=%@",kUrl,fileName];
urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url=[NSURL URLWithString:urlStr];
return url;
}
#pragma mark 取得保存地址(保存在沙盒缓存目录)
-(NSString *)getSavePath:(NSString *)fileName{
NSString *path=[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
return [path stringByAppendingPathComponent:fileName];
}
#pragma mark 文件追加
-(void)fileAppend:(NSString *)filePath data:(NSData *)data{
//以可写方式打开文件
NSFileHandle *fileHandle=[NSFileHandle fileHandleForWritingAtPath:filePath];
//如果存在文件则追加,否则创建
if (fileHandle) {
[fileHandle seekToEndOfFile];
[fileHandle writeData:data];
[fileHandle closeFile];//关闭文件
}else{
[data writeToFile:filePath atomically:YES];//创建文件
}
}
#pragma mark 取得文件大小
-(long long)getFileTotlaLength:(NSString *)fileName{
NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:[self getDownloadUrl:fileName] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:5.0f];
//设置为头信息请求
[request setHTTPMethod:@"HEAD"];
NSURLResponse *response;
NSError *error;
//注意这里使用了同步请求,直接将文件大小返回
[NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
if (error) {
NSLog(@"detail error:%@",error.localizedDescription);
}
//取得内容长度
return response.expectedContentLength;
}
#pragma mark 下载指定块大小的数据
-(void)downloadFile:(NSString *)fileName startByte:(long long)start endByte:(long long)end{
NSString *range=[NSString stringWithFormat:@"Bytes=%lld-%lld",start,end];
NSLog(@"%@",range);
// NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:[self getDownloadUrl:fileName]];
NSMutableURLRequest *request= [NSMutableURLRequest requestWithURL:[self getDownloadUrl:fileName] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:5.0f];
//通过请求头设置数据请求范围
[request setValue:range forHTTPHeaderField:@"Range"];
NSURLResponse *response;
NSError *error;
//注意这里使用同步请求,避免文件块追加顺序错误
NSData *data= [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
if(!error){
NSLog(@"dataLength=%lld",(long long)data.length);
[self fileAppend:[self getSavePath:fileName] data:data];
}
else{
NSLog(@"detail error:%@",error.localizedDescription);
}
}
#pragma mark 文件下载
-(void)downloadFile{
_totalLength=[self getFileTotlaLength:_textField.text];
_loadedLength=0;
long long startSize=0;
long long endSize=0;
//分段下载
while(startSize< _totalLength){
endSize=startSize+kFILE_BLOCK_SIZE-1;
if (endSize>_totalLength) {
endSize=_totalLength-1;
}
[self downloadFile:_textField.text startByte:startSize endByte:endSize];
//更新进度
_loadedLength+=(endSize-startSize)+1;
[self updateProgress];
startSize+=kFILE_BLOCK_SIZE;
}
}
#pragma mark 异步下载文件
-(void)downloadFileAsync{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self downloadFile];
});
}
@end
扩展–文件上传
在做WEB应用程序开发时,如果要上传一个文件往往会给form设置一个enctype=”multipart/form-data”的属性,不设置这个值在后台无法正常接收文件。在WEB开发过程中,form的这个属性其实本质就是指定请求头中Content-Type类型,当然使用GET方法提交就不用说了,必须使用URL编码。但是如果使用POST方法传递数据其实也是类似的,同样需要进行编码,具体编码方式其实就是通过enctype属性进行设置的。常用的属性值有:
1.application/x-www-form-urlencoded:默认值,发送前对所有发送数据进行url编码,支持浏览器访问,通常文本内容提交常用这种方式。
2.multipart/form-data:多部分表单数据,支持浏览器访问,不进行任何编码,通常用于文件传输(此时传递的是二进制数据) 。
3.text/plain:普通文本数据类型,支持浏览器访问,发送前其中的空格替换为“+”,但是不对特殊字符编码。
4.application/json:json数据类型,浏览器访问不支持 。
5.text/xml:xml数据类型,浏览器访问不支持。
要实现文件上传,必须采用POST上传,同时请求类型必须是multipart/form-data。在Web开发中,开发人员不必过多的考虑mutiparty/form-data更多的细节,一般使用file控件即可完成文件上传。但是在iOS中如果要实现文件上传,就没有那么简单了,我们必须了解这种数据类型的请求是如何工作的。
下面是在浏览器中上传一个文件时,发送的请求头:
这是发送的请求体内容:
在请求头中,最重要的就是Content-Type,它的值分为两部分:前半部分是内容类型,前面已经解释过了;后面是边界boundary用来分隔表单中不同部分的数据,后面一串数字是浏览器自动生成的,它的格式并不固定,可以是任意字符。和请求体中的源代码部分进行对比不难发现其实boundary的内容和请求体的数据部分前的字符串相比少了两个“–”。请求体中Content-Disposition中指定了表单元素的name属性和文件名称,同时指定了Content-Type表示文件类型。当然,在请求体中最重要的就是后面的数据部分,它其实就是二进制字符串。由此可以得出以下结论,请求体内容由如下几部分按顺序执行组成:
--boundary
Content-Disposition:form-data;name=”表单控件名称”;filename=”上传文件名称”
Content-Type:文件MIME Types
文件二进制数据;
--boundary--
了解这些信息后,只要使用POST方法给服务器端发送请求并且请求内容按照上面的格式设置即可。
下面是实现代码:
#import "KCMainViewController.h"
#define kUrl @"http://192.168.1.208/FileUpload.aspx"
#define kBOUNDARY_STRING @"KenshinCui"
@interface KCMainViewController ()<NSURLConnectionDataDelegate>{
UITextField *_textField;
UIButton *_button;
}
@end
@implementation KCMainViewController
#pragma mark - UI方法
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}
#pragma mark - 私有方法
#pragma mark 界面布局
-(void)layoutUI{
//地址栏
_textField=[[UITextField alloc]initWithFrame:CGRectMake(10, 50, 300, 25)];
_textField.borderStyle=UITextBorderStyleRoundedRect;
_textField.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0];
_textField.text=@"pic.jpg";
[self.view addSubview:_textField];
//上传按钮
_button=[[UIButton alloc]initWithFrame:CGRectMake(10, 500, 300, 25)];
[_button setTitle:@"上传" forState:UIControlStateNormal];
[_button setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal];
[_button addTarget:self action:@selector(uploadFile) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_button];
}
#pragma mark 取得请求链接
-(NSURL *)getUploadUrl:(NSString *)fileName{
NSString *urlStr=[NSString stringWithFormat:@"%@?file=%@",kUrl,fileName];
urlStr=[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url=[NSURL URLWithString:urlStr];
return url;
}
#pragma mark 取得mime types
-(NSString *)getMIMETypes:(NSString *)fileName{
return @"image/jpg";
}
#pragma mark 取得数据体
-(NSData *)getHttpBody:(NSString *)fileName{
NSMutableData *dataM=[NSMutableData data];
NSString *strTop=[NSString stringWithFormat:@"--%@\nContent-Disposition: form-data; name=\"file1\"; filename=\"%@\"\nContent-Type: %@\n\n",kBOUNDARY_STRING,fileName,[self getMIMETypes:fileName]];
NSString *strBottom=[NSString stringWithFormat:@"\n--%@--",kBOUNDARY_STRING];
NSString *filePath=[[NSBundle mainBundle] pathForResource:fileName ofType:nil];
NSData *fileData=[NSData dataWithContentsOfFile:filePath];
[dataM appendData:[strTop dataUsingEncoding:NSUTF8StringEncoding]];
[dataM appendData:fileData];
[dataM appendData:[strBottom dataUsingEncoding:NSUTF8StringEncoding]];
return dataM;
}
#pragma mark 上传文件
-(void)uploadFile{
NSString *fileName=_textField.text;
NSMutableURLRequest *request= [NSMutableURLRequest requestWithURL:[self getUploadUrl:fileName] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:5.0f];
request.HTTPMethod=@"POST";
NSData *data=[self getHttpBody:fileName];
//通过请求头设置
[request setValue:[NSString stringWithFormat:@"%lu",(unsigned long)data.length] forHTTPHeaderField:@"Content-Length"];
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@",kBOUNDARY_STRING] forHTTPHeaderField:@"Content-Type"];
//设置数据体
request.HTTPBody=data;
//发送请求
[NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc]init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if(connectionError){
NSLog(@"error:%@",connectionError.localizedDescription);
}
}];
}
@end
NSURLSession
NSURLConnection是2003年伴随着Safari一起发行的网络开发API,距今已经有十一年。当然,在这十一年间它表现的相当优秀,有大量的应用基础,这也是为什么前面花了那么长时间对它进行详细介绍的原因。但是这些年伴随着iPhone、iPad的发展,对于NSURLConnection设计理念也提出了新的挑战。在2013年WWDC上苹果揭开了NSURLSession的面纱,将它作为NSURLConnection的继任者。相比较NSURLConnection,NSURLSession提供了配置会话缓存、协议、cookie和证书能力,这使得网络架构和应用程序可以独立工作、互不干扰。另外,NSURLSession另一个重要的部分是会话任务,它负责加载数据,在客户端和服务器端进行文件的上传下载。
通过前面的介绍大家可以看到,NSURLConnection完成的三个主要任务:获取数据(通常是JSON、XML等)、文件上传、文件下载。其实在NSURLSession时代,他们分别由三个任务来完成:NSURLSessionData、NSURLSessionUploadTask、NSURLSessionDownloadTask,这三个类都是NSURLSessionTask这个抽象类的子类,相比直接使用NSURLConnection,NSURLSessionTask支持任务的暂停、取消和恢复,并且默认任务运行在其他非主线程中,具体关系图如下:
数据请求
-(void)loadJsonData{
//1.创建url
NSString *urlStr=[NSString stringWithFormat:@"http://192.168.1.208/ViewStatus.aspx?userName=%@&password=%@",@"KenshinCui",@"123"];
urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url=[NSURL URLWithString:urlStr];
//2.创建请求
NSURLRequest *request=[NSURLRequest requestWithURL:url];
//3.创建会话(这里使用了一个全局会话)并且启动任务
NSURLSession *session=[NSURLSession sharedSession];
//从会话创建任务
NSURLSessionDataTask *dataTask=[session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error) {
NSString *dataStr=[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",dataStr);
}else{
NSLog(@"error is :%@",error.localizedDescription);
}
}];
[dataTask resume];//恢复线程,启动任务
}
文件上传
#pragma mark 取得mime types
-(NSString *)getMIMETypes:(NSString *)fileName{
return @"image/jpg";
}
#pragma mark 取得数据体
-(NSData *)getHttpBody:(NSString *)fileName{
NSString *boundary=@"KenshinCui";
NSMutableData *dataM=[NSMutableData data];
NSString *strTop=[NSString stringWithFormat:@"--%@\nContent-Disposition: form-data; name=\"file1\"; filename=\"%@\"\nContent-Type: %@\n\n",boundary,fileName,[self getMIMETypes:fileName]];
NSString *strBottom=[NSString stringWithFormat:@"\n--%@--",boundary];
NSString *filePath=[[NSBundle mainBundle] pathForResource:fileName ofType:nil];
NSData *fileData=[NSData dataWithContentsOfFile:filePath];
[dataM appendData:[strTop dataUsingEncoding:NSUTF8StringEncoding]];
[dataM appendData:fileData];
[dataM appendData:[strBottom dataUsingEncoding:NSUTF8StringEncoding]];
return dataM;
}
#pragma mark 上传文件
-(void)uploadFile{
NSString *fileName=@"pic.jpg";
//1.创建url
NSString *urlStr=@"http://192.168.1.208/FileUpload.aspx";
urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url=[NSURL URLWithString:urlStr];
//2.创建请求
NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:url];
request.HTTPMethod=@"POST";
//3.构建数据
NSString *path=[[NSBundle mainBundle] pathForResource:fileName ofType:nil];
NSData *data=[self getHttpBody:fileName];
request.HTTPBody=data;
[request setValue:[NSString stringWithFormat:@"%lu",(unsigned long)data.length] forHTTPHeaderField:@"Content-Length"];
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@",@"KenshinCui"] forHTTPHeaderField:@"Content-Type"];
//4.创建会话
NSURLSession *session=[NSURLSession sharedSession];
NSURLSessionUploadTask *uploadTask=[session uploadTaskWithRequest:request fromData:data completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error) {
NSString *dataStr=[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",dataStr);
}else{
NSLog(@"error is :%@",error.localizedDescription);
}
}];
[uploadTask resume];
}
文件下载
使用NSURLSessionDownloadTask下载文件的过程与前面差不多,需要注意的是文件下载文件之后会自动保存到一个临时目录,需要开发人员自己将此文件重新放到其他指定的目录中。
-(void)downloadFile{
//1.创建url
NSString *fileName=@"1.jpg";
NSString *urlStr=[NSString stringWithFormat: @"http://192.168.1.208/FileDownload.aspx?file=%@",fileName];
urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url=[NSURL URLWithString:urlStr];
//2.创建请求
NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:url];
//3.创建会话(这里使用了一个全局会话)并且启动任务
NSURLSession *session=[NSURLSession sharedSession];
NSURLSessionDownloadTask *downloadTask=[session downloadTaskWithRequest:request completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
if (!error) {
//注意location是下载后的临时保存路径,需要将它移动到需要保存的位置
NSError *saveError;
NSString *cachePath=[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *savePath=[cachePath stringByAppendingPathComponent:fileName];
NSLog(@"%@",savePath);
NSURL *saveUrl=[NSURL fileURLWithPath:savePath];
[[NSFileManager defaultManager] copyItemAtURL:location toURL:saveUrl error:&saveError];
if (!saveError) {
NSLog(@"save sucess.");
}else{
NSLog(@"error is :%@",saveError.localizedDescription);
}
}else{
NSLog(@"error is :%@",error.localizedDescription);
}
}];
[downloadTask resume];
}
会话
NSURLConnection通过全局状态来管理cookies、认证信息等公共资源,这样如果遇到两个连接需要使用不同的资源配置情况时就无法解决了,但是这个问题在NSURLSession中得到了解决。NSURLSession同时对应着多个连接,会话通过工厂方法来创建,同一个会话中使用相同的状态信息。NSURLSession支持进程三种会话:
1.defaultSessionConfiguration:进程内会话(默认会话),用硬盘来缓存数据。
2.ephemeralSessionConfiguration:临时的进程内会话(内存),不会将cookie、缓存储存到本地,只会放到内存中,当应用程序退出后数据也会消失。
3.backgroundSessionConfiguration:后台会话,相比默认会话,该会话会在后台开启一个线程进行网络数据处理。
下面将通过一个文件下载功能对两种会话进行演示,在这个过程中也会用到任务的代理方法对上传操作进行更加细致的控制。下面先看一下使用默认会话下载文件,代码中演示了如何通过NSURLSessionConfiguration进行会话配置,如果通过代理方法进行文件下载进度展示(类似于前面中使用NSURLConnection代理方法,其实下载并未分段,如果需要分段需要配合后台进行),同时在这个过程中可以准确控制任务的取消、挂起和恢复。
#import "KCMainViewController.h"
@interface KCMainViewController ()<NSURLSessionDownloadDelegate>{
UITextField *_textField;
UIProgressView *_progressView;
UILabel *_label;
UIButton *_btnDownload;
UIButton *_btnCancel;
UIButton *_btnSuspend;
UIButton *_btnResume;
NSURLSessionDownloadTask *_downloadTask;
}
@end
@implementation KCMainViewController
#pragma mark - UI方法
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}
#pragma mark 界面布局
-(void)layoutUI{
//地址栏
_textField=[[UITextField alloc]initWithFrame:CGRectMake(10, 50, 300, 25)];
_textField.borderStyle=UITextBorderStyleRoundedRect;
_textField.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0];
_textField.text=@"[Objective-C.程序设计(第4版)].(斯蒂芬).林冀等.扫描版[电子书www.minxue.net].pdf";
[self.view addSubview:_textField];
//进度条
_progressView=[[UIProgressView alloc]initWithFrame:CGRectMake(10, 100, 300, 25)];
[self.view addSubview:_progressView];
//状态显示
_label=[[UILabel alloc]initWithFrame:CGRectMake(10, 130, 300, 25)];
_label.textColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0];
[self.view addSubview:_label];
//下载按钮
_btnDownload=[[UIButton alloc]initWithFrame:CGRectMake(20, 500, 50, 25)];
[_btnDownload setTitle:@"下载" forState:UIControlStateNormal];
[_btnDownload setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal];
[_btnDownload addTarget:self action:@selector(downloadFile) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_btnDownload];
//取消按钮
_btnCancel=[[UIButton alloc]initWithFrame:CGRectMake(100, 500, 50, 25)];
[_btnCancel setTitle:@"取消" forState:UIControlStateNormal];
[_btnCancel setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal];
[_btnCancel addTarget:self action:@selector(cancelDownload) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_btnCancel];
//挂起按钮
_btnSuspend=[[UIButton alloc]initWithFrame:CGRectMake(180, 500, 50, 25)];
[_btnSuspend setTitle:@"挂起" forState:UIControlStateNormal];
[_btnSuspend setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal];
[_btnSuspend addTarget:self action:@selector(suspendDownload) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_btnSuspend];
//恢复按钮
_btnResume=[[UIButton alloc]initWithFrame:CGRectMake(260, 500, 50, 25)];
[_btnResume setTitle:@"恢复" forState:UIControlStateNormal];
[_btnResume setTitleColor:[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0] forState:UIControlStateNormal];
[_btnResume addTarget:self action:@selector(resumeDownload) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_btnResume];
}
#pragma mark 设置界面状态
-(void)setUIStatus:(int64_t)totalBytesWritten expectedToWrite:(int64_t)totalBytesExpectedToWrite{
dispatch_async(dispatch_get_main_queue(), ^{
_progressView.progress=(float)totalBytesWritten/totalBytesExpectedToWrite;
if (totalBytesWritten==totalBytesExpectedToWrite) {
_label.text=@"下载完成";
[UIApplication sharedApplication].networkActivityIndicatorVisible=NO;
_btnDownload.enabled=YES;
}else{
_label.text=@"正在下载...";
[UIApplication sharedApplication].networkActivityIndicatorVisible=YES;
}
});
}
#pragma mark 文件下载
-(void)downloadFile{
//1.创建url
NSString *fileName=_textField.text;
NSString *urlStr=[NSString stringWithFormat: @"http://192.168.1.208/FileDownload.aspx?file=%@",fileName];
urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url=[NSURL URLWithString:urlStr];
//2.创建请求
NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:url];
//3.创建会话
//默认会话
NSURLSessionConfiguration *sessionConfig=[NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest=5.0f;//请求超时时间
sessionConfig.allowsCellularAccess=true;//是否允许蜂窝网络下载(2G/3G/4G)
//创建会话
NSURLSession *session=[NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];//指定配置和代理
_downloadTask=[session downloadTaskWithRequest:request];
[_downloadTask resume];
}
#pragma mark 取消下载
-(void)cancelDownload{
[_downloadTask cancel];
}
#pragma mark 挂起下载
-(void)suspendDownload{
[_downloadTask suspend];
}
#pragma mark 恢复下载下载
-(void)resumeDownload{
[_downloadTask resume];
}
#pragma mark - 下载任务代理
#pragma mark 下载中(会多次调用,可以记录下载进度)
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
[self setUIStatus:totalBytesWritten expectedToWrite:totalBytesExpectedToWrite];
}
#pragma mark 下载完成
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{
NSError *error;
NSString *cachePath=[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *savePath=[cachePath stringByAppendingPathComponent:_textField.text];
NSLog(@"%@",savePath);
NSURL *saveUrl=[NSURL fileURLWithPath:savePath];
[[NSFileManager defaultManager] copyItemAtURL:location toURL:saveUrl error:&error];
if (error) {
NSLog(@"Error is:%@",error.localizedDescription);
}
}
#pragma mark 任务完成,不管是否下载成功
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
[self setUIStatus:0 expectedToWrite:0];
if (error) {
NSLog(@"Error is:%@",error.localizedDescription);
}
}
@end
NSURLSession支持程序的后台下载和上传,苹果官方将其称为进程之外的上传和下载,这些任务都是交给后台守护线程完成的,而非应用程序本身。即使文件在下载和上传过程中崩溃了也可以继续运行(注意如果用户强制退关闭应用程序,NSURLSession会断开连接)。下面看一下如何在后台进行文件下载,这在实际开发中往往很有效,例如在手机上缓存一个视频在没有网络的时候观看(为了简化程序这里不再演示任务的取消、挂起等操作)。下面对前面的程序稍作调整使程序能在后台完成下载操作:
#import "KCMainViewController.h"
#import "AppDelegate.h"
@interface KCMainViewController ()<NSURLSessionDownloadDelegate>{
NSURLSessionDownloadTask *_downloadTask;
NSString *_fileName;
}
@end
@implementation KCMainViewController
#pragma mark - UI方法
- (void)viewDidLoad {
[super viewDidLoad];
[self downloadFile];
}
#pragma mark 取得一个后台会话(保证一个后台会话,这通常很有必要)
-(NSURLSession *)backgroundSession{
static NSURLSession *session;
static dispatch_once_t token;
dispatch_once(&token, ^{
NSURLSessionConfiguration *sessionConfig=[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.cmjstudio.URLSession"];
sessionConfig.timeoutIntervalForRequest=5.0f;//请求超时时间
sessionConfig.discretionary=YES;//系统自动选择最佳网络下载
sessionConfig.HTTPMaximumConnectionsPerHost=5;//限制每次最多一个连接
//创建会话
session=[NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];//指定配置和代理
});
return session;
}
#pragma mark 文件下载
-(void)downloadFile{
_fileName=@"1.mp4";
NSString *urlStr=[NSString stringWithFormat: @"http://192.168.1.208/FileDownload.aspx?file=%@",_fileName];
urlStr =[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url=[NSURL URLWithString:urlStr];
NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:url];
//后台会话
_downloadTask=[[self backgroundSession] downloadTaskWithRequest:request];
[_downloadTask resume];
}
#pragma mark - 下载任务代理
#pragma mark 下载中(会多次调用,可以记录下载进度)
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
// [NSThread sleepForTimeInterval:0.5];
// NSLog(@"%.2f",(double)totalBytesWritten/totalBytesExpectedToWrite);
}
#pragma mark 下载完成
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{
NSError *error;
NSString *cachePath=[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *savePath=[cachePath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@",[NSDate date]]];
NSLog(@"%@",savePath);
NSURL *saveUrl=[NSURL fileURLWithPath:savePath];
[[NSFileManager defaultManager] copyItemAtURL:location toURL:saveUrl error:&error];
if (error) {
NSLog(@"didFinishDownloadingToURL:Error is %@",error.localizedDescription);
}
}
#pragma mark 任务完成,不管是否下载成功
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
if (error) {
NSLog(@"DidCompleteWithError:Error is %@",error.localizedDescription);
}
}
@end
运行上面的程序会发现即使程序退出到后台也能正常完成文件下载。为了提高用户体验,通常会在下载时设置文件下载进度,但是通过前面的介绍可以知道:当程序进入后台后,事实上任务是交给iOS系统来调度的,具体什么时候下载完成就不得而知,例如有个较大的文件经过一个小时下载完了,正常打开应用程序看到的此文件下载进度应该在100%的位置,但是由于程序已经在后台无法更新程序UI,而此时可以通过应用程序代理方法进行UI更新。具体原理如下图所示:
当NSURLSession在后台开启几个任务之后,如果有其中几个任务完成后系统会调用此应用程序的-(void)application:(UIApplication )application handleEventsForBackgroundURLSession:(NSString )identifier completionHandler:(void (^)())completionHandler代理方法;此方法会包含一个competionHandler(此操作表示应用完成所有处理工作),通常我们会保存此对象;直到最后一个任务完成,此时会重新通过会话标识(上面sessionConfig中设置的)找到对应的会话并调用NSURLSession的-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session代理方法,在这个方法中通常可以进行UI更新,并调用completionHandler通知系统已经完成所有操作。具体两个方法代码示例如下:
-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler{
//backgroundSessionCompletionHandler是自定义的一个属性
self.backgroundSessionCompletionHandler=completionHandler;
}
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session{
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
//Other Operation....
if (appDelegate.backgroundSessionCompletionHandler) {
void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler;
appDelegate.backgroundSessionCompletionHandler = nil;
completionHandler();
}
}