iOS网络开发

第一部分:网络通信协议基础知识

网络通信协议(Network Communication Protocol),通常简称为“网络协议”(Network Protocol),是对计算机之间通信的信息格式、能被收/发双方接受的传送信息内容的一组定义。网络协议实现了OSI七层参考模型功能,各层都有许多负责各个不同方面,解决不同问题的通信协议。各层网络协议,有上千种之多。下面附上一张图大家可以体会一下:
image description

OSI是Open System Interconnection的缩写,意为开放式系统互联。国际标准化组织(ISO)制定了OSI模型,该模型定义了不同计算机互联的标准,是设计和描述计算机网络通信的基本框架。OSI模型把网络通信的工作分为7层,分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。

我们主要了解是TCP/IP协议,它在OSI 3-7层的实现如下:
image description

TCP/IP协议

TCP/IP: Transmission Control Protocol/Internet Protocol的简写,中译名为传输控制协议/因特网互联协议,又名网络通讯协议,是Internet最基本的协议、Internet国际互联网络的基础,由网络层的IP协议和传输层的TCP协议组成。TCP/IP 定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准。通俗而言:TCP负责发现传输的问题,一有问题就发出信号,要求重新传输,直到所有数据安全正确地传输到目的地。而IP是给因特网的每一台联网设备规定一个地址。

TCP/IP协议并不完全符合OSI的七层参考模型,而TCP/IP通讯协议采用了4层的层级结构:网络接口层、网络层、传输层、应用层。每一层都呼叫它的下一层所提供的网络来完成自己的需求。

TCP/IP结构对应OSI

TCP/IP OSI
应用层 应用层,表示层,会话层
主机到主机层(TCP)(又称传输层) 传输层
网络层(IP)(又称互联层) 网络层
网络接口层(又称链路层) 数据链路层,物理层
  • 网络接口层

    对应的OSI物理层是定义物理介质的各种特性:机械特性、电子特性、功能特性、规程特性。对应的OSI数据链路层是负责接收IP数据包并通过网络发送,或者从网络上接收物理帧,抽出IP数据包,交给IP层。

  • 网络层(IP层)

    负责相邻计算机之间的通信。其功能包括三方面:

    1. 处理来自传输层的分组发送请求,收到请求后,将分组装入IP数据报,填充报头,选择去往信宿机的路径,然后将数据报发往适当的网络接口。
    2. 处理输入数据报:首先检查其合法性,然后进行寻径—假如该数据报已到达信宿机,则去掉报头,将剩下部分交给适当的传输协议;假如该数据报尚未到达信宿,则转发该数据报。
    3. 处理路径、流控、拥塞等问题。

      IP:互联网协议(Internet Protocol,IP),分为IPv4(Internet Protocol Version 4)、IPv6(Internet Protocol Version 6)。

  • 传输层

    提供应用程序间的通信。其功能包括:

    1. 格式化信息流;
    2. 提供可靠传输。

    为实现后者,传输层协议规定接收端必须发回确认,并且假如分组丢失,必须重新发送,即耳熟能详的“三次握手”过程,从而提供可靠的数据传输。
    传输层协议主要是:传输控制协议TCP(Transmission Control Protocol)和用户数据报协议UDP(User Datagram protocol)。

TCP 与 UDP

在TCP/IP协议族中,有两个互不相同的传输协议:
TCP(传输控制协议)

TCP是面向连接的通信协议,通过三次握手建立连接,通讯完成时要拆除连接,由于TCP是面向连接的所以只能用于端到端的通讯。TCP提供的是一种可靠的数据流服务,采用“带重传的肯定确认”技术来实现传输的可靠性。TCP还采用一种称为“滑动窗口”的方式进行流量控制,所谓窗口实际表示接收能力,用以限制发送方的发送速度。

UDP(用户数据报协议):UDP是面向无连接的通讯协议,UDP数据包括目的端口号和源端口号信息,由于通讯不需要连接,所以可以实现广播发送。UDP通讯时不需要接收方确认,属于不可靠的传输,可能会出现丢包现象,实际应用中要求程序员编程验证。

  • 应用层

    向用户提供一组常用的应用程序,比如电子邮件、文件传输访问、远程登录等。远程登录TELNET使用TELNET协议提供在网络其它主机上注册的接口。TELNET会话提供了基于字符的虚拟终端。文件传输访问FTP使用FTP协议来提供网络内机器间的文件拷贝功能。
    应用层协议主要包括如下几个:FTP、TELNET、DNS、SMTP、NFS、HTTP。

    • FTP(File Transfer Protocol)是文件传输协议,一般上传下载用FTP服务,数据端口是20H,控制端口是21H。
    • Telnet服务是用户远程登录服务,使用23H端口,使用明码传送,保密性差、简单方便。
    • DNS(Domain Name Service)是域名解析服务,提供域名到IP地址之间的转换,使用端口53。
    • SMTP(Simple Mail Transfer Protocol)是简单邮件传输协议,用来控制信件的发送、中转,使用端口25。
    • NFS(Network File System)是网络文件系统,用于网络中不同主机间的文件共享。
    • HTTP(Hypertext Transfer Protocol)是超文本传输协议,用于实现互联网中的WWW服务,使用端口80。

我们只讲一下开发中经常用到的HTTP协议。

HTTP协议

先推荐几篇文章:

《浅析HTTP协议》 |
《HTTP协议详解》1 |
《HTTP协议详解》2 |
《HTTP报文》

我摘抄精髓部分如下:

Http协议的主要特点

  1. 支持客户/服务器模式。
  2. 简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
  3. 灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
  4. 无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
  5. 无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

Http Request 报文结构

Request 报文结构由三部分组成:请求行+请求头+请求体

image description

请求行

请求行以一个方法符号开头,以空格分开,后面跟着请求的URI和协议的版本,格式如下:

1
Method Request-URI HTTP-Version CRLF
  • Method表示请求方法。请求方法(所有方法全为大写)有多种,各个方法的解释如下:
    • GET 请求获取Request-URI所标识的资源
    • POST 在Request-URI所标识的资源后附加新的数据
    • HEAD 请求获取由Request-URI所标识的资源的响应消息报头。并不返回请求数据体,而只返回请求头信息,常用用于在文件下载中取得文件大小、类型等信息。
    • PUT 请求服务器存储一个资源,并用Request-URI作为其标识
    • DELETE 请求服务器删除Request-URI所标识的资源
    • TRACE 请求服务器回送收到的请求信息,主要用于测试或诊断
    • CONNECT 保留将来使用
    • OPTIONS 请求查询服务器的性能,或者查询与资源相关的选项和需求
  • Request-URI是一个统一资源标识符,它和报文头的Host属性组成完整的请求URL
  • HTTP-Version表示请求的HTTP协议版本
  • CRLF表示回车和换行(除了作为结尾的CRLF外,不允许出现单独的CR或LF字符)

    CRLF — Carriage-Return Line-Feed 回车换行。回车(CR, ASCII 13, \r) 换行(LF, ASCII 10, \n)。

GET和POST的区别

  1. GET提交的数据会放在URL之后,以?分割URL和传输数据,参数之间以&相连,如EditPosts.aspx?name=test1&id=123456. POST方法是把提交的数据放在HTTP包的Body中.
  2. GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制.
  3. GET方式需要使用Request.QueryString来取得变量的值,而POST方式通过Request.Form来获取变量的值。
  4. GET方式提交数据,会带来安全问题,比如一个登录页面,通过GET方式提交数据时,用户名和密码将出现在URL上,如果页面可以被缓存或者其他人可以访问这台机器,就可以从历史记录获得该用户的账号和密码.
请求头

报文头包含若干个属性,格式为“属性名:属性值”,服务端据此获取客户端的信息。常见的HTTP请求报文头属性有:

  • Accept

    Accept请求报头域用于指定客户端接受哪些类型的信息。eg:Accept:image/gif,表明客户端希望接受GIF图象格式的资源;Accept:text/html,表明客户端希望接受html文本。Accept属性的值可以为一个或多个MIME类型的值。

    MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型。是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。类型众多,可以参考http://www.w3school.com.cn/media/media_mimeref.asp

    MIME类型通常在Content-Type中定义。客户端在Http请求中,需要知道Content-Type类型,尤其是上传文件时。

  • Accept-Charset

    Accept-Charset请求报头域用于指定客户端接受的字符集。eg:Accept-Charset:iso-8859-1,gb2312.如果在请求消息中没有设置这个域,缺省是任何字符集都可以接受。

  • Accept-Encoding

    Accept-Encoding请求报头域类似于Accept,但是它是用于指定可接受的内容编码。eg:Accept-Encoding:gzip.deflate.如果请求消息中没有设置这个域服务器假定客户端对各种内容编码都可以接受。

  • Accept-Language

    Accept-Language请求报头域类似于Accept,但是它是用于指定一种自然语言。eg:Accept-Language:zh-cn.如果请求消息中没有设置这个报头域,服务器假定客户端对各种语言都可以接受。

  • Authorization

    Authorization请求报头域主要用于证明客户端有权查看某个资源。当浏览器访问一个页面时,如果收到服务器的响应代码为401(未授权),可以发送一个包含Authorization请求报头域的请求,要求服务器对其进行验证。

  • Host(发送请求时,该报头域是必需的)

    Host请求报头域主要用于指定被请求资源的Internet主机和端口号,它通常从HTTP URL中提取出来的,eg:
    我们在浏览器中输入:http://www.guet.edu.cn/index.html
    浏览器发送的请求消息中,就会包含Host请求报头域,如下:
    Host:www.guet.edu.cn 此处使用缺省端口号80,若指定了端口号,则变成:Host:www.guet.edu.cn:指定端口号

  • User-Agent

    我们上网登陆论坛的时候,往往会看到一些欢迎信息,其中列出了你的操作系统的名称和版本,你所使用的浏览器的名称和版本,这往往让很多人感到很神奇,实际上,服务器应用程序就是从User-Agent这个请求报头域中获取到这些信息。User-Agent请求报头域允许客户端将它的操作系统、浏览器和其它属性告诉服务器。不过,这个报头域不是必需的,如果我们自己编写一个浏览器,不使用User-Agent请求报头域,那么服务器端就无法得知我们的信息了。

请求体

报文体将一个页面表单中的组件值通过param1=value1&param2=value2的键值对形式编码成一个格式化串,它承载多个请求参数的数据。不但报文体可以传递请求参数,请求行中URL也可以通过类似于“/chapter15/user.html? param1=value1&param2=value2”的方式传递请求参数。

Http Response 报文结构

Response 报文结构也由三部分组成:响应行+响应头+响应体

image description

响应行

响应行,或叫状态行,格式如下:

1
HTTP-Version Status-Code Reason-Phrase CRLF
  • HTTP-Version表示服务器HTTP协议的版本;
  • Status-Code表示服务器发回的响应状态代码。

    状态代码有三位数字组成,第一个数字定义了响应的类别,且有五种可能取值:

    • 1xx:指示信息—表示请求已接收,继续处理
    • 2xx:成功—表示请求已被成功接收、理解、接受
    • 3xx:重定向—要完成请求必须进行更进一步的操作
    • 4xx:客户端错误—请求有语法错误或请求无法实现
    • 5xx:服务器端错误—服务器未能实现合法的请求
  • Reason-Phrase表示状态代码的文本描述。
  • CRLF表示回车和换行(除了作为结尾的CRLF外,不允许出现单独的CR或LF字符
响应头

响应头允许服务器传递不能放在状态行中的附加响应信息,以及关于服务器的信息和对Request-URI所标识的资源进行下一步访问的信息。

常用的响应报头

  • Location

    Location响应报头域用于重定向接受者到一个新的位置。Location响应报头域常用在更换域名的时候。

  • Server

    Server响应报头域包含了服务器用来处理请求的软件信息。与User-Agent请求报头域是相对应的。

  • WWW-Authenticate

    WWW-Authenticate响应报头域必须被包含在401(未授权的)响应消息中,客户端收到401响应消息时候,并发送Authorization报头域请求服务器对其进行验证时,服务端响应报头就包含该报头域。eg:

    1
    WWW-Authenticate:Basic realm="Basic Auth Test!"  //可以看出服务器对请求资源采用的是基本验证机制。
响应体

响应报文体,即我们真正要的“干货”。

Request 、 Response 通用报文头

1、普通报头

在普通报头中,有少数报头域用于所有的请求和响应消息,但并不用于被传输的实体,只用于传输的消息。

  • Cache-Control

    用于指定缓存指令,缓存指令是单向的(响应中出现的缓存指令在请求中未必会出现),且是独立的(一个消息的缓存指令不会影响另一个消息处理的缓存机制),HTTP1.0使用的类似的报头域为Pragma。

    • 请求时的缓存指令包括:no-cache(用于指示请求或响应消息不能缓存)、no-store、max-age、max-stale、min-fresh、only-if-cached;
    • 响应时的缓存指令包括:public、private、no-cache、no-store、no-transform、must-revalidate、proxy-revalidate、max-age、s-maxage.

    eg:为了指示IE浏览器(客户端)不要缓存页面,服务器端的JSP程序可以编写如下

    1
    2
    response.sehHeader("Cache-Control","no-cache");
    //response.setHeader("Pragma","no-cache"); //作用相当于上述代码,通常两者合用

    这句代码将在发送的响应消息中设置普通报头域:Cache-Control:no-cache

  • Date普通报头域表示消息产生的日期和时间

  • Connection普通报头域允许发送指定连接的选项。例如指定连接是连续,或者指定“close”选项,通知服务器,在响应完成后,关闭连接。

2、实体报头

请求和响应报文都可以传送一个实体。一个实体由实体报头域和实体正文组成,但并不是说实体报头域和实体正文要在一起发送,可以只发送实体报头域。实体报头定义了关于实体正文(eg:有无实体正文)和请求所标识的资源的元信息。

常用的实体报头:

  • Content-Encoding

    Content-Encoding实体报头域被用作媒体类型的修饰符,它的值指示了已经被应用到实体正文的附加内容的编码,因而要获得Content-Type报头域中所引用的媒体类型,必须采用相应的解码机制。Content-Encoding这样用于记录文档的压缩方法,eg:Content-Encoding:gzip

  • Content-Language

    Content-Language实体报头域描述了资源所用的自然语言。没有设置该域则认为实体内容将提供给所有的语言阅读者。eg:Content-Language:da

  • Content-Length

    Content-Length实体报头域用于指明实体正文的长度,以字节方式存储的十进制数字来表示。

  • Content-Type

    Content-Type实体报头域用语指明发送给接收者的实体正文的媒体类型。eg:
    Content-Type:text/html;charset=ISO-8859-1
    Content-Type:text/html;charset=GB2312

    下面我们列出常用Content-Type,此处服务器端Content-Type与返回内容格式不符,会引起前端莫名其妙的错误,请注意此处内容:

    • 服务端需要返回一段普通文本给客户端:Content-Type=”text/plain”
    • 服务端需要返回一段HTML代码给客户端:Content-Type=”text/html”
    • 服务端需要返回一段XML代码给客户端:Content-Type=”text/xml”
    • 服务端需要返回一段json串给客户端:Content-Type=”application/Json”
  • Last-Modified

    Last-Modified实体报头域用于指示资源的最后修改日期和时间。

  • Expires

    Expires实体报头域给出响应过期的日期和时间。为了让代理服务器或浏览器在一段时间以后更新缓存中(再次访问曾访问过的页面时,直接从缓存中加载,缩短响应时间和降低服务器负载)的页面,我们可以使用Expires实体报头域指定页面过期的时间。eg:Expires:Thu,15 Sep 2006 16:23:12 GMT

    HTTP1.1的客户端和缓存必须将其他非法的日期格式(包括0)看作已经过期。eg:为了让浏览器不要缓存页面,我们也可以利用Expires实体报头域,设置为0,jsp中程序如下:response.setDateHeader(“Expires”,”0”);

基础知识,我们先了解这么多,我们赶快回归到iOS开发中。

第二部分:使用NSURLConnection请求和获取数据

NSURLConnection是Core Foundation / CFNetwork 框架的APIs之上的一个抽象,包含了一组Foundation框架中URL加载系统相互关联的组件:NSURLRequest,NSURLResponse,NSURLProtocol,NSURLCache,NSHTTPCookieStorage,NSURLCredentialStorage。

NSURLRequest 被传递给 NSURLConnection。被委托对象(遵守 NSURLConnectionDelegateNSURLConnectionDataDelegate协议)异步地返回一个 NSURLResponse 以及包含服务器返回信息的 NSData。

在一个请求被发送到服务器之前,系统会先查询共享的缓存信息,然后根据策略(policy)以及可用性(availability)的不同,一个已经被缓存的响应可能会被立即返回。如果没有缓存的响应可用,则这个请求将根据我们指定的策略来缓存它的响应以便将来的请求可以使用。

在把请求发送给服务器的过程中,服务器可能会发出鉴权查询(authentication challenge),这可以由共享的 cookie 或机密存储(credential storage)来自动响应,或者由被委托对象来响应。发送中的请求也可以被注册的 NSURLProtocol 对象所拦截,以便在必要的时候无缝地改变其加载行为。

使用NSURLConnection进行同步请求

提示:请您先忽略代码中的警告,下文会有说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#import "ViewController.h"

#define RFScreenWidth [UIScreen mainScreen].bounds.size.width
#define RFScreenHeight [UIScreen mainScreen].bounds.size.height

@interface ViewController ()
@property(nonatomic,strong) UITextView *textView;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
[self setupGUI];

[self httpSynchronousRequest];
}

- (void)setupGUI {
self.textView = [[UITextView alloc] initWithFrame:CGRectMake(8, 10, RFScreenWidth-16, 400)];
self.textView.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:self.textView];
}

/**
* 使用NSURLConnection发送同步请求
*/

- (void)httpSynchronousRequest {
// 1、请求地址
NSString *urlStr = @"http://aliang9585.github.io/";
//注意对于url中的中文是无法解析的,需要进行url编码(指定编码类型为utf-8)
//另外注意url解码使用stringByRemovingPercentEncoding方法
//urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlStr];

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

// 3、发送同步请求,在主线程执行
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil];
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
self.textView.text = text;
}

如果你使用的iOS版本在9.0及9.0之上:

1、会发现NSURLConnection相关方法发生警告:

‘sendSynchronousRequest:returningResponse:error:’ is deprecated: first deprecated in iOS 9.0 - Use [NSURLSession dataTaskWithRequest:completionHandler:]

提示你方法在iOS9.0之后会产生过时警告,希望你用NSURLSession的相关方法。我们在下一部分会讲到。

2、会发现无法获取数据,控制台出现相关消息:

HTTPRequestDemo[22299:3086490] App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app’s Info.plist file.

提示你HTTP不安全,如果非要使用,需要在Info.plist文件中进行设置。详情可见这里

苹果在iOS7.0时,就开始强迫开发者使用HTTPS了(主要强迫开发者在发布企业级应用时,下载地址用HTTPS协议。而对于代码中HTTP请求本身并无影响)。而今进入iOS9.0,对于代码中的HTTP请求默认会阻止。如果你想要进行HTTP请求,必须:

  1. 在Info.plist文件中加入一个App Transport Security Settings,类型是Dictionary;
  2. 在这个Dictionary中加入Allow Arbitrary Loads,类型是Boolean,值为YES

使用NSURLConnection进行异步请求

使用Block接收回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 使用NSURLConnection发送异步请求(使用block接收回调)
*/

- (void)httpAsynchronousRequest1 {
// 1、请求地址
NSString *urlStr = @"http://aliang9585.github.io/";
//urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlStr];

// 2、创建请求对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"GET"];//默认就是GET,你可以换成POST试试,会得到错误结果
[request setTimeoutInterval:10.0];

// 3、发送异步请求,使用block接收回调
// NSOperationQueue *queue = [[NSOperationQueue alloc]init];
NSOperationQueue *queue = [NSOperationQueue mainQueue];//以为要刷新UI,必须用主线程接收回调
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {

//responseCode : 还记得前文HTTP基础知识讲解的“响应行”吗,里面有个状态码。
//所以说,基础知识不可或缺啊:)
NSInteger responseCode = [(NSHTTPURLResponse *)response statusCode];
NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
self.textView.text = responseString;
NSLog(@"HttpResponseCode:%ld", responseCode);
}];
}

注:
1、代码中使用NSMutableURLRequest,可以在初始化之后随时设定相关属性(NSURLRequest在初始化时就要设定好)。常用方法有:

  • 设置请求超时等待时间(超过这个时间就算超时,请求失败)- (void)setTimeoutInterval:(NSTimeInterval)seconds;
  • 设置请求方法(比如GET和POST)- (void)setHTTPMethod:(NSString *)method;
  • 设置请求体- (void)setHTTPBody:(NSData *)data;
  • 设置请求头- (void)setValue:(NSString )value forHTTPHeaderField:(NSString )field;

2、GET请求中不存在请求体,因为所有的信息都写在URL里面。而POST请求,一般还要设定请求体:[request setHTTPBody:param]

3、 block接收回调后如需刷新UI,则sendAsynchronousRequest方法中的queue必须设定为主队列。

4、 block代码段:当服务器有返回数据的时候调用会开一条新的线程去发送请求,主线程继续往下走,当拿到服务器的返回数据的数据的时候再回调block,执行block代码段。这种情况不会卡住主线程。

5、队列的作用:决定这个block操作放在哪个线程执行。
刷新UI界面的操作应该放在主线程执行,不能放在子线程,在子线程处理UI相关操作会出现一些莫名的问题。

使用代理方法接收回调

代理方法接收回调使用起来比较繁琐,一般只用在上传、下载过程中,可以监听进度。
需要使用<NSURLConnectionDataDelegate>协议和<NSURLConnectionDelegate>协议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#pragma mark- NSURLConnectionDataDelegate

//当接收到服务器的响应(连通了服务器)时会调用
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response

//当接收到服务器的数据时会调用(可能会被调用多次,每次只传递部分数据)
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data

//当服务器的数据加载完毕时就会调用
-(void)connectionDidFinishLoading:(NSURLConnection *)connection

#pragma mark- NSURLConnectionDelegate

//请求错误(失败)的时候调用(请求超时\断网\没有网\,一般指客户端错误)
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* 使用NSURLConnection发送异步请求(使用代理方法接收回调)
*/

- (void)httpAsynchronousRequest2 {
// 1、请求地址
NSString *urlStr = @"http://aliang9585.github.io/";
//注意对于url中的中文是无法解析的,需要进行url编码(指定编码类型为utf-8)
//另外注意url解码使用stringByRemovingPercentEncoding方法
//urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlStr];

// 2、创建请求对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"GET"];//默认就是GET,你可以换成POST试试,会得到错误结果
//[request setHTTPBody:param];
[request setTimeoutInterval:10.0];

// 3、发送异步请求,使用代理方法接收响应
self.tempData = [[NSMutableData alloc] init];
NSURLConnection *conn = [NSURLConnection connectionWithRequest:request delegate:self];
[conn start];
NSLog(@"------connection start");
}

#pragma mark- NSURLConnectionDataDelegate

//当接收到服务器的响应(连通了服务器)时会调用
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {

//通过响应头中的Content-Length取得整个响应的总长度
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSDictionary *httpResponseHeaderFields = [httpResponse allHeaderFields];
NSInteger totalLength = [[httpResponseHeaderFields objectForKey:@"Content-Length"] integerValue];
NSLog(@"------connection didReceiveResponse,Content-Length : %ld",totalLength);
}

//当接收到服务器的数据时会调用(可能会被调用多次,每次只传递部分数据)
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSLog(@"------connection didReceiveData");
//连续接收数据
[self.tempData appendData:data];
//更新text
NSString *dataString = [[NSString alloc] initWithData:self.tempData encoding:NSUTF8StringEncoding];
self.textView.text = dataString;
}

//当服务器的数据加载完毕时就会调用
-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"------connection DidFinish");

}

#pragma mark- NSURLConnectionDelegate
//请求错误(失败)的时候调用(请求超时\断网\没有网\,一般指客户端错误)
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"------connection didFail");
}

别忘记头部加上要实现的Delegate协议

1
2
3
4
@interface ViewController ()<NSURLConnectionDataDelegate,NSURLConnectionDelegate>
@property(nonatomic,strong) UITextView *textView;
@property(nonatomic,strong) NSMutableData *tempData;
@end

第三部分:使用NSURLSession请求和获取数据

NSURLSession出现在iOS 7,它包含了与NSURLConnection相同的组件,例如NSURLRequest, NSURLCache等。NSURLSession的不同之处在于,它把 NSURLConnection替换为NSURLSession, NSURLSessionConfiguration,以及3个NSURLSessionTask的子类:NSURLSessionDataTask, NSURLSessionUploadTask, 和NSURLSessionDownloadTask。

与 NSURLConnection 相比,NSURLsession 最直接的改进就是可以配置每个 session 的缓存,协议,cookie,以及证书策略(credential policy),甚至跨程序共享这些信息。这将允许程序和网络基础框架之间相互独立,不会发生干扰。每个NSURLSession 对象都由一个 NSURLSessionConfiguration 对象来进行初始化,后者指定了刚才提到的那些策略以及一些用来增强移动设备上性能的新选项。

NSURLSession 中另一大块就是 session task。它负责处理数据的加载以及文件和数据在客户端与服务端之间的上传和下载。NSURLSessionTask 与 NSURLConnection 最大的相似之处在于它也负责数据的加载,最大的不同之处在于所有的 task 共享其创造者 NSURLSession 这一公共委托者(common delegate)。

使用NSURLSession的一般套路如下:

  1. 定义一个NSURLRequest
  2. 定义一个NSURLSessionConfiguration,配置各种网络参数
  3. 使用NSURLSession的工厂方法获取一个所需类型的NSURLSession
  4. 使用定义好的NSURLRequest和NSURLSession构建一个NSURLSessionTask
  5. 使用Delegate或者CompletionHandler处理任务执行过程的所有事件。

我们先看代码,再详细讨论NSURLSessionTask、NSURLSessionConfiguration。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#import "ViewController2.h"
#import "AppMacro.h"

@interface ViewController2 ()
@property(nonatomic,strong) UITextView *textView;
@end

@implementation ViewController2

- (void)viewDidLoad {
[super viewDidLoad];
[self setupGUI];
[self requestDataUseingBlock];
}

- (void)setupGUI {
self.textView = [[UITextView alloc] initWithFrame:CGRectMake(8, 10, RFScreenWidth-16, 400)];
self.textView.backgroundColor = [[UIColor greenColor] colorWithAlphaComponent:0.2];
[self.view addSubview:self.textView];
}

-(void)requestDataUseingBlock{
// 1、请求地址
NSString *urlStr = @"http://aliang9585.github.io/";
//iOS9之后该方法过时,建议用stringByAddingPercentEncodingWithAllowedCharacters
//urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
//stringByAddingPercentEncodingWithAllowedCharacters
urlStr = [urlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
NSURL *url = [NSURL URLWithString:urlStr];

// 2、定义一个NSURLRequest
NSURLRequest *request = [NSURLRequest requestWithURL:url];

//3、使用NSURLSession的工厂方法获取一个所需类型的NSURLSession
//这里sharedSession是一个单例,采用默认行为,具体[alt+点击]查看说明
NSURLSession *session=[NSURLSession sharedSession];

//4、使用定义好的NSURLRequest和NSURLSession构建一个NSURLSessionTask
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error) {
NSInteger responseCode = [(NSHTTPURLResponse *)response statusCode];
NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
//派发到主线程
dispatch_async(dispatch_get_main_queue(), ^{
self.textView.text = responseString;
});

NSLog(@"HttpResponseCode:%ld", responseCode);
}else{
NSLog(@"error :%@",error.localizedDescription);
}
}];

[dataTask resume];//恢复线程,启动任务
}
@end

注意:

关于请求地址url,我们习惯的做法是:

1
[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

>

但是该方法在iOS9之后过期,取而代之的是:

1
[urlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];

对于NSCharacterSet不太熟悉的同学可以参考下这里

NSURLSessionTask

NSURLsessionTask 是一个抽象类,其下有 3 个实体子类可以直接使用:NSURLSessionDataTask、NSURLSessionUploadTask、NSURLSessionDownloadTask。这 3 个子类封装了现代程序三个最基本的网络任务:获取数据,比如 JSON 或者 XML,上传文件和下载文件。他们的继承关系如下:

1
2
3
4
5
6
----------------继承关系-------------------
NSURLSessionTask
/ \
NSURLSessionDataTask NSURLSessionDownloadTask
|
NSURLSessionUploadTask

当一个 NSURLSessionDataTask 完成时,它会带有相关联的数据,而一个 NSURLSessionDownloadTask 任务结束时,它会带回已下载文件的一个临时的文件路径。因为一般来说,服务端对于一个上传任务的响应也会有相关数据返回,所以NSURLSessionUploadTask 继承自 NSURLSessionDataTask。

所有的 task 都是可以取消,暂停或者恢复的。当一个 download task 取消时,可以通过选项来创建一个恢复数据(resume data),然后可以传递给下一次新创建的 download task,以便继续之前的下载。

不同于直接使用 alloc-init 初始化方法,task 是由一个 NSURLSession 创建的。每个 task 的构造方法都对应有或者没有completionHandler 这个 block 的两个版本,例如:有这样两个构造方法 –dataTaskWithRequest: 和 –dataTaskWithRequest:completionHandler:。这与 NSURLConnection 的 -sendAsynchronousRequest:queue:completionHandler: 方法类似,通过指定 completionHandler 这个 block 将创建一个隐式的 delegate,来替代该 task 原来的 delegate——session。对于需要 override 原有 session task 的 delegate 的默认行为的情况,我们需要使用这种不带 completionHandler 的版本。

NSURLSessionTask 的工厂方法

NSURLSession 在 task 的构造方法上,延续了NSURLConnection的工厂方法模式:

1
2
3
4
5
6
7
// 同步请求
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil];

// 异步请求
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
// ...
}];

不同的是,task 不会立即运行,而是将该 task 对象先返回,允许我们进一步的配置,然后可以使用 resume 方法来让它开始运行:

1
2
3
4
5
6
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// ...
}];

[task resume];

Data task

Data task 可以通过 NSURL 或 NSURLRequest 创建(使用前者相当于是使用一个对于该 URL 进行标准 GET 请求的NSURLRequest,这是一种快捷方法):

1
2
3
4
5
//通过 NSURL 创建
NSURLSessionDataTask *dataTask2 = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// ...
}];
[dataTask2 resume];
Upload task

Upload task 的创建需要使用一个 request,另外加上一个要上传的 NSData 对象或者是一个本地文件的路径对应的 NSURL:

1
2
3
4
5
6
NSData *data = ...;
NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request fromData:data completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// ...
}];


[uploadTask resume];
Download task

Download task 也需要一个 request,不同之处在于 completionHandler 这个 block。Data task 和 upload task 会在任务完成时一次性返回,但是 Download task 是将数据一点点地写入本地的临时文件。所以在 completionHandler 这个 block 里,我们需要把文件从一个临时地址移动到一个永久的地址保存起来:

1
2
3
4
5
6
7
8
9
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {

NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSURL *documentsDirectoryURL = [NSURL fileURLWithPath:documentsPath];
NSURL *newFileLocation = [documentsDirectoryURL URLByAppendingPathComponent:[[response URL] lastPathComponent]];
[[NSFileManager defaultManager] copyItemAtURL:location toURL:newFileLocation error:nil];
}];

[downloadTask resume];

NSURLSession 的 delegate 方法

在 NSURLConnection 中有两个 delegate 方法可以表明一个网络请求已经结束:

  1. NSURLConnectionDataDelegate 中的 -connectionDidFinishLoading:
  2. NSURLConnectionDelegate 中的-connection:didFailWithError:
    而在NSURLSession 中改为一个 delegate 方法:NSURLSessionTaskDelegate 的 -URLSession:task:didCompleteWithError:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#import "ViewController2.h"
#import "AppMacro.h"

@interface ViewController2 ()<NSURLSessionTaskDelegate>

@property(nonatomic,strong) UITextView *textView;

@end

@implementation ViewController2

- (void)viewDidLoad {
[super viewDidLoad];
[self setupGUI];

[self requestDataUseingDelegate];
}

- (void)setupGUI {
self.textView = [[UITextView alloc] initWithFrame:CGRectMake(8, 10, RFScreenWidth-16, 400)];
self.textView.backgroundColor = [[UIColor greenColor] colorWithAlphaComponent:0.2];
[self.view addSubview:self.textView];
}


-(void)requestDataUseingDelegate {
NSString *urlStr = @"http://aliang9585.github.io/";
urlStr = [urlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
NSURL *url = [NSURL URLWithString:urlStr];
NSURLRequest *request = [NSURLRequest requestWithURL:url];

NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];

NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];

[dataTask resume];
}

#pragma mark - NSURLSessionDataDelegate

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
completionHandler(NSURLSessionResponseAllow);
NSLog(@"didReceiveResponse:Allow the load to continue");

//completionHandler(NSURLSessionResponseCancel);
//NSLog(@"didReceiveResponse:Cancel the load, this is the same as -[task cancel]");
}


//方法定义位置:@protocol NSURLSessionDataDelegate <NSURLSessionTaskDelegate>
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data
{
NSString * str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
self.textView.text = str;
NSLog(@"didReceiveData");
}

//方法定义位置:@protocol NSURLSessionTaskDelegate <NSURLSessionDelegate>
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
if(error == nil)
{
NSLog(@"task complete");
}
else
NSLog(@"task error :%@",error.localizedDescription);
}

@end

NSURLConnection 的一些 delegate 方法中增加了 completionHandler: 这个 block 作为参数。 由于增加了这个 block 作为参数,NSURLSession 实际上给 Foundation 框架引入了一种全新的模式。这种模式允许 delegate 方法可以安全地在主线程与运行,而不会阻塞主线程;Delgate 只需要简单地调用 dispatch_async 就可以切换到后台进行相关的操作,然后在操作完成时调用 completionHandler 即可。同时,它还可以有效地拥有多个返回值,而不需要我们使用笨拙的参数指针。

以 NSURLSessionTaskDelegate 的 -URLSession:task:didReceiveChallenge:completionHandler: 方法来举例,completionHandler 接受两个参数:NSURLSessionAuthChallengeDisposition 和 NSURLCredential,前者为应对鉴权查询的策略,后者为需要使用的证书(仅当前者——应对鉴权查询的策略为使用证书,即 NSURLSessionAuthChallengeUseCredential 时有效,否则该参数为NULL)。

想要查看更多关于 session task 的信息,可以查看 WWDC Session 705: “What’s New in Foundation Networking”

NSURLSessionConfiguration

NSURLSessionConfiguration 对象用于对 NSURLSession 对象进行初始化。NSURLSessionConfiguration 对以前NSMutableURLRequest 所提供的网络请求层的设置选项进行了扩充,提供给我们相当大的灵活性和控制权。从指定可用网络,到 cookie,安全性,缓存策略,再到使用自定义协议,启动事件的设置,以及用于移动设备优化的几个新属性,你会发现使用NSURLSessionConfiguration 可以找到几乎任何你想要进行配置的选项。

NSURLSession 在初始化时会把配置它的 NSURLSessionConfiguration 对象进行一次 copy,并保存到自己的 configuration属性中,而且这个属性是只读的。因此之后再修改最初配置 session 的那个 configuration 对象对于 session 是没有影响的。也就是说,configuration 只在初始化时被读取一次,之后都是不会变化的。

NSURLSessionConfiguration 的工厂方法

NSURLSessionConfiguration 有三个类工厂方法,这很好地说明了 NSURLSession 设计时所考虑的不同的使用场景。

  • +defaultSessionConfiguration 返回一个标准的 configuration,这个配置实际上与 NSURLConnection 的网络堆栈(networking stack)是一样的,具有相同的共享 NSHTTPCookieStorage,共享 NSURLCache 和共享NSURLCredentialStorage。

  • +ephemeralSessionConfiguration 返回一个预设配置,这个配置中不会对缓存,Cookie 和证书进行持久性的存储。这对于实现像秘密浏览这种功能来说是很理想的。

  • +backgroundSessionConfiguration:(NSString *)identifier 的独特之处在于,它会创建一个后台 session。后台 session 不同于常规的,普通的 session,它甚至可以在应用程序挂起,退出或者崩溃的情况下运行上传和下载任务。初始化时指定的标识符,被用于向任何可能在进程外恢复后台传输的守护进程(daemon)提供上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//-------创建NSURLSessionConfiguration
NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
//ephemeral 短暂的
NSURLSessionConfiguration *ephemeralConfigObject = [NSURLSessionConfiguration ephemeralSessionConfiguration];

//已过时
//NSURLSessionConfiguration *backgroundConfigObject = [NSURLSessionConfiguration backgroundSessionConfiguration: @"myBackgroundSessionIdentifier"];
//现在用
NSURLSessionConfiguration *backgroundConfigObject = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"myBackgroundSessionIdentifier"];

//------创建NSURLSession
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];

NSURLSession *ephemeralSession = [NSURLSession sessionWithConfiguration: ephemeralConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];

NSURLSession *backgroundSession = [NSURLSession sessionWithConfiguration: backgroundConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];

想要查看更多关于后台 session 的信息,可以查看 WWDC Session 204: “What’s New with Multitasking”

配置属性

NSURLSessionConfiguration拥有20几个配置属性。熟练掌握这些配置属性的用处,可以让应用程序充分地利用其网络环境。

基本配置
  • HTTPAdditionalHeaders

    指定了一组默认的可以设置出站请求(outbound request)的数据头。这对于跨 session 共享信息,如内容类型,语言,用户代理和身份认证,是很有用的。

1
2
3
4
5
6
7
8
9
10
11
12
NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];

NSString *userPasswordString = @"lyl:123456";
NSData * userPasswordData = [userPasswordString dataUsingEncoding:NSUTF8StringEncoding];
NSString *base64EncodedCredential = [userPasswordData base64EncodedStringWithOptions:0];
NSString *authString = [NSString stringWithFormat:@"Basic %@", base64EncodedCredential];
NSString *userAgentString = @"AppName/com.example.app (iPhone 5s; iOS 7.0.2; Scale/2.0)";

defaultConfigObject.HTTPAdditionalHeaders = @{@"Accept": @"application/json",
@"Accept-Language": @"en",
@"Authorization": authString,
@"User-Agent": userAgentString};
  • networkServiceType

    对标准的网络流量,网络电话,语音,视频,以及由一个后台进程使用的流量进行了区分。大多数应用程序都不需要设置这个。

  • allowsCellularAccess 和 discretionary

    被用于节省通过蜂窝网络连接的带宽。allowsCellularAccess表示当只有一个3G网络时,网络是否允许访问。而设置discretionary属性可以控制系统在一个合适的时机访问网络,比如有可用的WiFi,有充足的电量。这个属性主要针对后台回话的,所以在后台会话模式下默认是打开的。对于后台传输的情况,推荐大家使用discretionary 这个属性。

  • timeoutIntervalForRequest 和 timeoutIntervalForResource

    分别指定了对于请求和资源的超时间隔。许多开发人员试图使用 timeoutInterval 去限制发送请求的总时间,但其实它真正的含义是:分组(packet)之间的时间。实际上我们应该使用timeoutIntervalForResource 来规定整体超时的总时间,但应该只将其用于后台传输,而不是用户实际上可能想要去等待的任何东西。

  • HTTPMaximumConnectionsPerHost

    是 Foundation 框架中 URL 加载系统的一个新的配置选项。它曾经被 NSURLConnection 用于管理私有的连接池。现在有了 NSURLSession,开发者可以在需要时限制连接到特定主机的数量。

  • HTTPShouldUsePipelining

    这个属性在 NSMutableURLRequest 下也有,它可以被用于开启 HTTP 管线化(HTTP pipelining),这可以显著降低请求的加载时间,但是由于没有被服务器广泛支持,默认是禁用的。

  • sessionSendsLaunchEvents

    是另一个新的属性,该属性指定该 session 是否应该从后台启动。

  • connectionProxyDictionary

    指定了 session 连接中的代理服务器。同样地,大多数面向消费者的应用程序都不需要代理,所以基本上不需要配置这个属性。

HTTPCookieStorage 存储了 session 所使用的 cookie。默认情况下会使用 NSHTTPCookieShorage 的+sharedHTTPCookieStorage 这个单例对象,这与 NSURLConnection 是相同的。

HTTPCookieAcceptPolicy 决定了什么情况下 session 应该接受从服务器发出的 cookie。

HTTPShouldSetCookies 指定了请求是否应该使用 session 存储的 cookie,即 HTTPCookieSorage 属性的值。

安全策略

URLCredentialStorage 存储了 session 所使用的证书。默认情况下会使用 NSURLCredentialStorage 的+sharedCredentialStorage 这个单例对象,这与 NSURLConnection 是相同的。

TLSMaximumSupportedProtocol 和 TLSMinimumSupportedProtocol 确定 session 是否支持 SSL 协议。

缓存策略

URLCache 是 session 使用的缓存。默认情况下会使用 NSURLCache 的 +sharedURLCache 这个单例对象,这与NSURLConnection 是相同的。

requestCachePolicy specifies when a cached response should be returned for a request. This is equivalent toNSURLRequest -cachePolicy.

requestCachePolicy 指定了一个请求的缓存响应应该在什么时候返回。这相当于 NSURLRequest 的 -cachePolicy 方法。

自定义协议

protocolClasses 用来配置特定某个 session 所使用的自定义协议(该协议是 NSURLProtocol 的子类)的数组。

使用NSURLSession进行下载

普通下载

我们使用NSURLSessionDownloadTask执行下载任务,并且需要实现NSURLSessionDownloadDelegate以下的代理方法:

  • URLSession:downloadTask:didFinishDownloadingToURL:

    该方法提供app下载内容的临时存储目录,通常在该方法中必须打开文件来进行读取或者将下载内容移动到一个永久目录

  • URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:

    该方法提供提供了下载进度的状态信息,会多次调用,通常在此更新进度条。

  • URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:

    告诉app尝试恢复之前失败的下载.

  • URLSession:task:didCompleteWithError:

    不管任务是否成功,在完成后都会回调这个代理方法。

后台下载

如果将下载任务安排在后台会话中,在app非运行期间下载行为仍将继。如果将下载任务安排在系统默认会话或者临时会话中,当app重新启动时,下载也将重新开始。

当我们使用后台下载时,通常会保证只有一个后台会话。因此后台会经常会用dispatch_once:

1
2
3
4
5
6
7
8
9
10
- (NSURLSession *)backgroundSession
{
static NSURLSession *backgroundSession = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"backgroundTaskInViewController3"];
backgroundSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
});
return backgroundSession;
}

在iOS中,当一个后台下载任务完成后,会在后台状态下重新启动你的App并调用AppDelegate中的application:handleEventsForBackgroundURLSession:completionHandler: 方法。你需要在该方法中存储completionHandler。同时,你也可以利用identifier参数,与后台Session重新关联。

AppDelegate.h

1
2
3
4
5
6
7
8
#import <UIKit/UIKit.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;
@property (copy) void (^backgroundURLSessionCompletionHandler)();

@end

AppDelegate.m

1
2
3
4
5
6
......
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
{
NSLog(@"application:handleEventsForBackgroundURLSession:completionHandler: 执行 ");
self.backgroundURLSessionCompletionHandler = completionHandler;

}

当某个Session执行完了它所有的后台下载任务之后(一个Session可以开启多个task),该Session会调用其URLSessionDidFinishEventsForBackgroundURLSession: message代理方法。在这个代理方法中,你需要调用AppDelegate中的application:handleEventsForBackgroundURLSession:completionHandler: 方法存储的completionHandler

ViewController3.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
......
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
NSLog(@"Background URL session %@ finished events.\n", session);

AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
if(appDelegate.backgroundURLSessionCompletionHandler) {
// Need to copy the completion handler
void (^handler)() = appDelegate.backgroundURLSessionCompletionHandler;
appDelegate.backgroundURLSessionCompletionHandler = nil;
handler();
NSLog(@"后台下载任务成功完成");
}
}

注:网上许多示例代码关于后台下载的部分都是错误的,错误代码都是在URLSession: downloadTask:didFinishDownloadingToURL:处理本应在URLSessionDidFinishEventsForBackgroundURLSession: message方法中处理的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
> //错误做法:
> - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{
> ......
>
> //错误做法,应该在URLSessionDidFinishEventsForBackgroundURLSession中执行
> // if (session == self.backgroundSession) {
> // self.backgroundTask = nil;
> // // Get hold of the app delegate
> // AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
> // if(appDelegate.backgroundURLSessionCompletionHandler) {
> // // Need to copy the completion handler
> // void (^handler)() = appDelegate.backgroundURLSessionCompletionHandler;
> // appDelegate.backgroundURLSessionCompletionHandler = nil;
> // handler();
> // NSLog(@"后台下载任务成功完成");
> // }
> // }else {
> // self.partialData = nil;
> // self.task = nil;
> // NSLog(@"普通下载任务成功完成");
> // }
> ......
> }
>
>

>

完整代码如下,包含了普通下载、后台下载、暂停、断点续传:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#import "ViewController3.h"
#import "AppDelegate.h"

#define ScreenWidth [UIScreen mainScreen].bounds.size.width
#define ScreenHeight [UIScreen mainScreen].bounds.size.height

@interface ViewController3 ()<NSURLSessionDownloadDelegate>

@property (nonatomic,strong)UIImageView *imageView;
@property (nonatomic,strong)UIProgressView *progressIndicator;

@property (atomic, strong) NSURLSessionDownloadTask *task;
@property (atomic, strong) NSData *partialData;

@end

@implementation ViewController3

- (void)viewDidLoad {
[super viewDidLoad];
[self setupGUI];

self.backgroundSession.sessionDescription = @"Background session";
}

#pragma mark - getter/setter
- (NSURLSession *)session
{
//创建NSURLSession
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
return session;
}

- (NSURLRequest *)request
{
//创建请求
NSString *urlStr = @"http://pic.qiantucdn.com/10/84/27/15bOOOPIC51.jpg";
urlStr = [urlStr stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
NSURL *url = [NSURL URLWithString:urlStr];

NSURLRequest *request = [NSURLRequest requestWithURL:url];
return request;
}

- (NSURLSession *)backgroundSession
{
static NSURLSession *backgroundSession = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"backgroundTaskInViewController3"];
backgroundSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
});
return backgroundSession;
}

#pragma mark - 创建UI
- (void)setupGUI {

self.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, ScreenWidth, ScreenHeight-100)];
self.imageView.backgroundColor = [[UIColor redColor] colorWithAlphaComponent:0.2];
[self.view addSubview:_imageView];

self.progressIndicator = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
self.progressIndicator.frame = CGRectMake(0, ScreenHeight-100, ScreenWidth, 10);
self.progressIndicator.progress = 0;
[self.view addSubview:_progressIndicator];

UIButton *downloadButton = [UIButton buttonWithType:UIButtonTypeSystem];
downloadButton.frame = CGRectMake(20, ScreenHeight-60, 40, 40);
[downloadButton setTitle:@"下载" forState:UIControlStateNormal];
[downloadButton addTarget:self action:@selector(didClickDownloadButtonAction:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:downloadButton];

UIButton *cancleButton = [UIButton buttonWithType:UIButtonTypeSystem];
cancleButton.frame = CGRectMake(80, ScreenHeight-60, 40, 40);
[cancleButton setTitle:@"取消" forState:UIControlStateNormal];
[cancleButton addTarget:self action:@selector(didClickCancleButtonAction:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:cancleButton];

UIButton *resumableButton = [UIButton buttonWithType:UIButtonTypeSystem];
resumableButton.frame = CGRectMake(140, ScreenHeight-60, 40, 40);
[resumableButton setTitle:@"恢复" forState:UIControlStateNormal];
[resumableButton addTarget:self action:@selector(didClickResuableButtonAction:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:resumableButton];

UIButton *backgroundLoadButton = [UIButton buttonWithType:UIButtonTypeSystem];
backgroundLoadButton.frame = CGRectMake(ScreenWidth-100, ScreenHeight-60, 80, 40);
[backgroundLoadButton setTitle:@"后台下载" forState:UIControlStateNormal];
[backgroundLoadButton addTarget:self action:@selector(didClickBackgroundButtonAction:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:backgroundLoadButton];

}



#pragma mark - 按钮事件
#pragma mark 点击下载
- (void)didClickDownloadButtonAction:(UIButton *)button{
//用NSURLSession和NSURLRequest创建网络任务
self.task = [[self session] downloadTaskWithRequest:[self request]];
[self.task resume];
}

#pragma mark 点击取消
- (void)didClickCancleButtonAction:(UIButton *)button{
NSLog(@"Pause download task");
if (self.task) {
//取消下载任务,把已下载数据存起来
[self.task cancelByProducingResumeData:^(NSData *resumeData) {
self.partialData = resumeData;
self.task = nil;
}];
}

}

#pragma mark 恢复下载(断点续传)
- (void)didClickResuableButtonAction:(UIButton *)button{
NSLog(@"resume download task");
if (!self.task) {
//判断是否又已下载数据,有的话就断点续传,没有就完全重新下载
if (self.partialData) {
self.task = [[self session] downloadTaskWithResumeData:self.partialData];
}else{
self.task = [[self session] downloadTaskWithRequest:[self request]];
}
}
[self.task resume];
}

#pragma mark 后台下载模式
- (void)didClickBackgroundButtonAction:(UIButton *)button{
//NSString *url = @"http://pic.qiantucdn.com/58pic/11/01/40/11A58PIC6SJ.jpg";
NSString *url = @"http://cdn.hayageek.com/wp-content/uploads/2013/10/NSURLSession-Tasks-1024x517.png";
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
self.backgroundTask = [self.backgroundSession downloadTaskWithRequest:request];
self.imageView.hidden = YES;

[self.backgroundTask resume];
NSLog(@"后台下载任务启动");
}

#pragma mark - NSURLSessionDownloadTaskDelegate
// 下载完成
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{

NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *URLs = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
NSURL *documentsDirectory = URLs[0];
NSURL *destinationPath = [documentsDirectory URLByAppendingPathComponent:[location lastPathComponent]];
NSError *error;
[fileManager removeItemAtURL:destinationPath error:NULL];
BOOL success = [fileManager copyItemAtURL:location toURL:destinationPath error:&error];
if (success) {
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.hidden = NO;
UIImage *image = [UIImage imageWithContentsOfFile:[destinationPath path]];
self.imageView.image = image;
self.imageView.contentMode = UIViewContentModeScaleAspectFill;
});
}
//错误做法,应该在URLSessionDidFinishEventsForBackgroundURLSession中执行
// if (session == self.backgroundSession) {
// self.backgroundTask = nil;
// // Get hold of the app delegate
// AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
// if(appDelegate.backgroundURLSessionCompletionHandler) {
// // Need to copy the completion handler
// void (^handler)() = appDelegate.backgroundURLSessionCompletionHandler;
// appDelegate.backgroundURLSessionCompletionHandler = nil;
// handler();
// NSLog(@"后台下载任务成功完成");
// }
// }else {
// self.partialData = nil;
// self.task = nil;
// NSLog(@"普通下载任务成功完成");
// }
self.partialData = nil;
self.task = nil;
}

//不管任务是否成功,在完成后都会回调这个代理方法
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
// 如果error是nil,则证明下载是成功的,否则就要通过它来查询失败的原因。如果下载了一部分,这个error会包含一个NSData对象,如果后面要恢复任务可以用到
if (error == nil) {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"task complete");
//self.progressIndicator.hidden = YES;
});
}else {
NSLog(@"task error :%@",error.localizedDescription);
}

}

//传输进度
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{

//刷新进度条的delegate方法,同样的,获取数据,调用主线程刷新UI
double currentProgress = totalBytesWritten/(double)totalBytesExpectedToWrite;
dispatch_async(dispatch_get_main_queue(), ^{
self.progressIndicator.progress = currentProgress;
self.progressIndicator.hidden = NO;
});
}

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
NSLog(@"Background URL session %@ finished events.\n", session);

AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
if(appDelegate.backgroundURLSessionCompletionHandler) {
// Need to copy the completion handler
void (^handler)() = appDelegate.backgroundURLSessionCompletionHandler;
appDelegate.backgroundURLSessionCompletionHandler = nil;
handler();
NSLog(@"后台下载任务成功完成");
}
}


@end

第四部分:使用第三方网络框架AFNetworking请求和获取数据

iOS自带的NSURLConnection、NSURLSession都可以满足我们网络开发的需求,为什么还要使用第三方网络框架AFNetworking呢?

1、轻量级

AFNetworking是在NSURLConnection、NSURLSession基础上开发的轻量级框架,不会产生额外的性能损耗。

2、与时俱进

AFNetworking 1.0建立在NSURLConnection的基础API之上,AFNetworking 2.0开始使用NSURLConnection的基础API,以及较新基于NSURLSession的API的选项。 AFNetworking3.0现已完全基于NSURLSession的API,这降低了维护的负担,同时支持苹果增强关于NSURLSession提供的任何额外功能。

3、简化代码

使用原生URLSession请求数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)getDataUseingURLSessionDataTask{
NSURL *url = [NSURL URLWithString:@"http://aliang9585.github.io/"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
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(@"responseObject: %@", dataStr);
}else{
NSLog(@"error :%@",error.localizedDescription);
}
}];
[dataTask resume];
}

使用原生AFNetworking请求数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)getDataUseingAFNetworking {
NSURL *url = [NSURL URLWithString:@"http://aliang9585.github.io/"];
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager setResponseSerializer:[AFHTTPResponseSerializer new]];

[manager GET:url.absoluteString parameters:nil progress:nil success:^(NSURLSessionTask *task, id responseObject) {
NSData *data = (NSData *)responseObject;
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"responseObject: %@", dataStr);
} failure:^(NSURLSessionTask *operation, NSError *error) {
NSLog(@"Error: %@", error);
}];
}

也许,从上面代码中你可能没觉得减少多少代码,但是你要知道,使用AFNetworking时我加了[manager setResponseSerializer:[AFHTTPResponseSerializer new]];这一行代码,是因为AFNetworking自动把相应结果转换成了json。而我们真正开发工作中,服务器大多会返回json格式数据,AFNetworking很体贴的帮我们转换好了。当然,你也可以设置不同的ResponseSerializer来获取其它类型的数据:

1
2
3
4
5
6
7
8
//默认使用的是AFJSONResponseSerializer,得到的responseObject直接会转为json
//[manager setResponseSerializer:[AFJSONResponseSerializer new]];

//得到的responseObject直接会转为XML
//[manager setResponseSerializer:[AFXMLParserResponseSerializer new]];

//得到的responseObject直接会转为NSData
[manager setResponseSerializer:[AFHTTPResponseSerializer new]];

注:此处演示代码我们知道返回数据是html格式,因此使用了AFHTTPResponseSerializer。实际开发中我们可能会遇到明明接口是返回的JSON数据,可用 AFJSONResponseSerializer 就会报错,错误信息类似:

1
2
> Request failed: unacceptable content-type: text/html
>

原因就在于服务器HTTP响应头中的 Content-Type 与实际内容不符(请参考前文第一部分)。要修正这个问题,有以下2种方法:

  1. 告知服务器开发人员修改Response的Content-Type为application/Json
  2. iOS端使用[manager setResponseSerializer:[AFHTTPResponseSerializer new]];方法,这样默认得到数据转为NSData,之后再进行json转化。

此处还有一个坑,当服务器端设置Content-Type为text/plan时,我们会收到报错信息。查看下AFNetworking源码:

1
2
> self.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", nil];
>

会发现默认支持的MIME types中没有text/plan,这会导致我们请求服务器时,Request报头中Accept属性值中没有text/plan,换句话话说,相当于我们告诉服务器客户端不希望接受纯文本信息。

要解决这个问题,我们可以在acceptableContentTypes中手动添加一种MIME type:

1
2
> manager.responseSerializer.acceptableContentTypes = [manager.responseSerializer.acceptableContentTypes setByAddingObject:@"text/plan"];
>

AFNetworking虽然帮助我们简化了代码,但并不会阻止我们使用NSURLSession的高级特性,如NSURLSessionConfiguration 。上面例子中我们用AFHTTPSessionManager这个封装好的单例来进行网络请求和获取数据,它实际上使用了默认的URLSession和URLSessionConfiguration,当我们需要自定义URLSession、URLSessionConfiguration时,可以使用AFURLSessionManager(AFHTTPSessionManager的父类)来进行网络请求和获取数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)getDataWithDataTaskUseingAFNetworking {
NSURL *url = [NSURL URLWithString:@"http://aliang9585.github.io/"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
[manager setResponseSerializer:[AFHTTPResponseSerializer new]];
NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
if (error) {
NSLog(@"Error: %@", error);
} else {
NSLog(@"%@ %@", response, responseObject);
}
}];
[dataTask resume];
}

4、代码更易读
AFNetworking中基本上都是使用block处理回调。一些常用方法,如监听下载进度,使用NSURLConnection只能在代理方法URLSession: downloadTask: didWriteData: totalBytesWritten: totalBytesExpectedToWrite:中才能处理,造成代码东一块西一块,比较凌乱。而AFNetworking很贴心的把这些方法包含在了block当中,这会使代码更加易读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)downloadImg {
NSURL *url = [NSURL URLWithString:@"http://pic.qiantucdn.com/10/84/27/15bOOOPIC51.jpg"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];

NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
NSLog(@"下载进度----- %.2f %%",downloadProgress.fractionCompleted*100);
} destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
//
NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
return [documentsDirectoryURL URLByAppendingPathComponent:[response suggestedFilename]];
} completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
NSLog(@"File downloaded to: %@", filePath);
}];

[downloadTask resume];
}

关于AFNetworking更多内容,请大家移步AFNetworking Github地址AFNetworking文档

参考文章:

iOS7 Day-by-Day Github

实战iOS7之NSURLSession

iOS开发系列—网络开发

From NSURLConnection to NSURLSession以及中文翻译

iOS NSURLSession Example (HTTP GET, POST, Background Downlads )

使用AFNetworking, SDWebimage和OHHTTPStubs