美文网首页
IOS基础:网络请求(上)

IOS基础:网络请求(上)

作者: 时光啊混蛋_97boy | 来源:发表于2020-10-23 10:10 被阅读0次

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

  • 一、网络协议
    • TCP/IP协议的层次划分
    • 1、HTTP协议
    • 2、HTTPS协议(附Charles抓包)
    • 3、TCP/UDP协议
    • 4、DNS解析
    • 5、Session/Cookie
    • 6、网络数据解析方式
  • 二、NSURLConnection
    • 1、简介
    • 2、使用
  • 三、NSURLSession
    • 简介
    • 1、实现 GET 请求
    • 2、实现POST请求
    • 3、下载数据
    • 4、上传数据
    • 5、Demo演示
  • 三、AFNetworking框架
    • 简介
    • 1、实现GET请求
    • 2、实现 POST请求
    • 3、下载数据
    • 4、上传数据
  • 四、断点续传和后台下载(基于AFNetworking)
    • 1、简介
    • 2、原理介绍
    • 3、Demo演示
  • 五、拼接URL进行网络请求
    • 1、计算签名
    • 2、拼接请求参数的URL
    • 3、进行网络请求
    • 4、调用网络请求方法
  • Demo
  • 参考文献

前言:古老协议

从上个世纪 90 年代互联网开始兴起一直到现在,大部分的互联网流量传输只使用了几个网络协议。使用IPv4进行路由,使用TCP 进行连接层面的流量控制,使用 SSL/TLS 协议实现传输安全,使用 DNS进行域名解析,使用 HTTP 进行应用数据的传输。

而且近三十年来,这几个协议的发展都非常缓慢。TCP 主要是拥塞控制算法的改进,SSL/TLS 基本上停留在原地,几个小版本的改动主要是密码套件的升级,IPv4虽然有一个大的进步,实现了IPv6DNS 也增加了一个安全的 DNSSEC,但和IPv6一样,部署进度较慢。

随着移动互联网快速发展以及物联网的逐步兴起,网络交互的场景越来越丰富,网络传输的内容也越来越庞大,用户对网络传输效率和 WEB 响应速度的要求也越来越高。一方面是历史悠久使用广泛的古老协议,另外一方面用户的使用场景对传输性能的要求又越来越高。

一、网络协议

TCP/IP协议的层次划分

通常使用的网络是在TCP/IP协议族的基础上运作的。HTTP属于它的一个子集。

TCP/IP协议族各层作用

应用层:决定了向用户提供应用服务时通讯的活动(HTTPFTPHTTPS协议)
传输层:为上层应用层提供处于网络连接中的两台计算机之间的数据传输(TCP:传输控制协议、UDP:用户数据报协议)
网络层:用来处理在网络上流动的数据包。规定通过怎样的路径到达对方计算机,并把数据包传送给对方。(IP:选择传输线路)
链路层:用来处理连接网络的硬件部分。(控制操作系统、硬件的设备驱动,NIC:网卡、光纤等硬件范畴的)

应用层

HTTPHTTPSFTP是建立在TCP/IP协议之上的应用层协议,使用URL来定位资源,数据交互格式主要采用JSONXML等。URL是统一资源定位符,URL的形式为协议名://主机名/主机内资源的路径URL 的端口号为8080。用NSURL 来访问资源的形式( scheme ) 包括:
http:超文本链接协议
https:超文本传输安全协议http的安全版本,是超文本传输协议和SSL/TLS的组合
ftp:文件传输协议

TCP/IP通信传输流

[客户端]HTTP客户端(应用层)<->TCP(传输层)<->IP(网络层)<->网络(链路层)<->IP(网络层)<->TCP(传输层)<->HTTP服务器(应用层)[服务器]。

1、HTTP协议:超文本传输协议

HTTP的特点1:无连接

HTTP协议支持 C/S 网络结构,是无连接协议,即需要TCP的建立连接和释放连接,但是每一次请求时建立连接,服务器处理完客户端的请求后,应答给客户端,然后断开连接,不会一直占用网络资源。

解决方案:HTTP的持久连接:
connection: 连接方式为持久连接Keep-Alive
time:持续时间
max:该连接最多能发出多少个HTTP请求和响应

如何判断持久连接结束?
法一、保持持久连接报文实体必须要有正确Content-Length,这样事务处理才能正确的检测出一条报文的结束。
法二、最后会有一个空的chunked,可以根据其来判断持久连接是否结束。

HTTP的持久连接

HTTP的特点2:无状态

多次发送HTTP请求的时候,如果是同一个用户,但server端不知道是同一个用户。

解决方案:cookie/session
详见我的文章:HTTPCookie,看这一篇文章就够了

HTTP的请求报文:

POST /app/game/game_list/ HTTP/1.1
Content-Length: 10
Content-Type: application/x-www-form-urlencoded
Host: zhushou.72g.com
Connection: Keep-Alive
Accept-Encoding: gzip

Post:HTTP请求方法的一种
包括POSTGETPUTDELETE等。GET方法是向指定的资源发出请求,发送的信息”显式“地跟在 URL后面, GET方法应该只用于读取数据,例如静态图片等,不安全的,POST方法是向指定资源提交数据,请求服务器进行处理,例如提交表单或者上传文件等 , 数据被包含在请求体(BODY)中,把“信内容"装入信封中。

/app/game/game_list/:是url中除服务器地址外的path
HTTP/1.1:使用的http协议版本
Content-Lengthbody中内容的长度
Host: zhushou.72g.com:服务器地址
Connection: Keep-Alive:客户端和服务器保持长时间链接
Accept-Encoding: gzip:接收的响应数据,编码格式应该为gzip

Content-Type:body中内容的类型,默认为application/x-www-form-urlencoded,使用url编码的表单数据类型,还可以指定内容的编码:charset=utf-8。其他常见类型:1、multipart/form-data,这又是一个常见的 POST 数据提交的方式。2、application/json用来告诉服务端消息主体是序列化后的JSON字符串。

HTTP的响应报文:

1)报文协议及版本

2)响应状态码及状态描述:
1xx消息,一般是告诉客户端,请求已经收到了,正在处理,别急...
2xx处理成功。(200 响应成功)请求收悉、我明白你要的、请求已受理、已经处理完成等信息。
3xx重定向到其它地方。(301、302 发生了网络的重定向)它让客户端再发起一个请求以完成整个处理。
4xx处理发生错误,责任在客户端。(401、404 客户端发起的请求本身存在某些问题)如客户端的请求一个不存在的资源,客户端未被授权,禁止访问等。
5xx处理发生错误,责任在服务端。(501、502 server本身是有问题的) 如服务端抛出异常,路由出错,HTTP版本不支持等。

3)响应报文头,也是由多个属性组成,如Content-Type:响应报文的类型,Content-Encoding:响应报文的编码
4)响应报文体,即我们真正要的“干货”

HTTP 通讯过程及缺点

HTTP 为了快速的处理大量事务,所以设计的很简单:将HTTP的报文丢给传输层,利用传输层的TCP协议或者 UDP 协议来进行数据传输,然后再利用网络层的IP协议和数据链路层来进行最终的数据包传输。

对于传输层,直接将应用层HTTP报文封装,然后直接传输,在这里不会做数据的加密,不会验证通信双方的身份,也不会校验报文的完整性。所以会导致以下问题:
1、由于通信报文是使用明文传输的,所以内容很容易被窃听。
2、由于不验证通信双方身份,这里很容易遭遇伪装(伪装服务器或者伪装客户端等),服务器也容易遭受 Dos 攻击。
3、不校验报文的完整性,会出现篡改报文信息的情况。

对于以上出现的问题,我们有必要对传输的报文做加密处理,校验报文的完整性,以及验证通信双方的身份。对于上面 HTTP 出现的问题,我们需要定制一些机制和协议来完善:HTTP + 加密 + 认证 + 完整性保护 = HTTPS

2、HTTPS协议(附Charles抓包)

简介

超文本传输安全协议,是超文本传输协议和SSL/TLS的组合 , 用以提供加密通信及对网络服务器身份的鉴定。使用https://代替http://, 且使用端口 443,而HTTP使用端口80来与TCP/IP进行通信。IOS默认非 HTTPS的网络是被禁止的,在Xcode程属性文件lnfo.plist中添加AppTransport Security Settings键,并在该键下面添加 Exception Domains键ATS白名单(例外名单)。

HTTPS 的通信流程

非对称加密:公钥在网络传递,私钥保存在客户端,中间人无法截获,因此安全性高,但耗时,连接建立过程使用。

非对称加密

对称加密:中间人攻击截取TCP传输的私钥,安全性低,但快,后续通行过程使用。

对称加密
  • 对于报文的加密:有对称加密和非对称加密方式,HTTPS 采用混合加密机制,在交换密钥建立连接环节,使用非对称加密,在之后通信交换报文阶段则使用对称加密方式,因为非对称加密方式虽然能保证安全性,但是是非常耗时的。
  • 对于验证通信双方身份:使用CA 证书来校验身份。
  • SSL 协议具有完整性校验。
HTTPS 的通信流程
  1. 客户端将自己支持的加密算法发送给服务器,请求服务器证书;
  2. 服务器选取一组加密算法,并将证书返回给客户端;
  3. 客户端校验证书合法性,生成随机对称密钥,用公钥加密后发送给服务器;
  4. 服务器用私钥解密出对称密钥,返回一个响应,HTTPS连接建立完成;
  5. 随后双方通过这个对称密钥进行安全的数据通信。

在这个过程中,正常的话,如果哪个步骤出现问题,链接都会停止,无法进行通信,这个是https简单的校验的一个过程介绍。

再解释得通俗一点:

  1. 客户端发起HTTPS请求。就是用户在浏览器里输入一个https网址,然后连接到server的443端口。
  2. 服务端的配置。采用HTTPS协议的服务器必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面。这套证书其实就是一对公钥和私钥。公钥相当于锁,可以全世界任何一个人,私钥相当于钥匙,只有自己拥有,所以只有本人才可以开锁。
  3. 传送证书。这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等。
  4. 客户端解析证书。由客户端的TLS/SSL来完成。首先会验证公钥是否有效,比如颁发机构,过期时间等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随机值,然后用证书对该随机值进行加密。随机值此时就相当于锁里面的武功秘籍,只有拥有唯一钥匙的人才可以拥有它成为武林至尊。
  5. 传送加密信息。传送的是用证书加密后的随机值,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。
  6. 服务段解密信息。服务端用私钥解密后,得到了客户端传过来的随机值(对称密钥),然后把内容通过该值进行对称加密。所谓对称加密就是,将信息和私钥通过某种算法混合在一起,这样除非知道私钥,不然无法获取内容,而正好客户端和服务端都知道这个私钥,所以只要加密算法够彪悍,私钥够复杂,数据就够安全。
  7. 传输加密后的信息。服务端用私钥加密后的信息,可以在客户端被还原。
  8. 客户端解密信息。客户端用之前生成的私钥解密服务端传过来的信息,于是获取了解密后的内容。整个过程第三方即使监听到了数据,也束手无策。

总结一下:

  1. 用户发起请求,服务器响应后返回一个证书,证书中包含一些基本信息和公钥。
  2. 用户拿到证书后,去验证这个证书是否合法,不合法,则请求终止。
  3. 合法则生成一个随机数,作为对称加密的密钥,用服务器返回的公钥对这个随机数加密,然后返回给服务器。
  4. 服务器拿到加密后的随机数,利用私钥解密,然后再用解密后的随机数(对称密钥),把需要返回的数据加密,加密完成后数据传输给用户。
  5. 最后用户拿到加密的数据,用一开始的那个随机数(对称密钥),进行数据解密,整个过程完成。

当然这仅仅是一个单向认证,https还会有双向认证,仅仅多了服务端验证客户端这一步。

Charles抓包

Charles作为一个中间人代理,在客户端给服务器端发消息的时候,会截取客户端发送给服务器的请求,然后伪装成客户端与服务器进行通信;服务器返回数据时将截取的数据发送给客户端,伪装成服务器与客户端进行通信。

那么,https在抓包工具中是如何实现抓包的呢? 抓包工具就是在上面的过程中,证书认证生成通信密钥中做了手脚。以青花瓷(Charles)为例,大家使用青花瓷抓http请求时,由于没有做安全校验,很容易就实现了数据拦截和转发。

至于https呢? 抓取https包的时候,青花瓷会要求使用者对抓包的设备(手机或模拟器)安装一个证书,安装这个证书的时候,其实是安装了一个根证书(允许颁发CA的一个证书机构的根证书),当你安装了该根证书之后,该证书机构颁发的其他证书,默认都会被你的系统所信任,这个就是青花瓷完成https抓包的一个重要前提!!

当客户端设置了代理,并且开始发出网络请求的时候,这个网络请求的校验过程就会变成这样:


Charles抓包
  1. 当客户端Client对服务器Server发送请求(带着随机数和加密算法),由于青花瓷做了代理,请求被青花瓷拦截,处理(青花瓷的角色现在对于Client来说是服务器),青花瓷将客户端带的随机数和加密算法处理,然后返回自己的证书通过客户端校验,获取到客户端提交的请求参数等数据。

  2. 青花瓷作为客户端(自己去产生随机数和携带支持的加密算法)去请求刚刚Client想要请求的Server,然后,Server会和青花瓷完成上面讲的那个完整的校验,并且读取青花瓷带来的具体请求,返回正常的数据结果。

  3. 青花瓷得到服务器数据的返回结果之后,开始继续和过程1中的Client以服务器的身份去做处理,首先收到客户端的随机数和加密算法,自己生成一个随机数和选择一个客户端的加密算法,然后青花瓷会返回一个伪造的CA证书(公钥和真实的server不一样,但是域名是一样的,或者说除了域名是一致的,其他的都不是一致的,而且这个签发机构是青花瓷之前让你安装的根证书签发的,所以当返回这个证书的时候,你的客户端的信任链是可以完成的,会被系统信任),然后Client在这个伪造的证书(对于青花瓷和Client是真实证书(验证信任链和证书信息都通过了),但是和真实的域名对应的证书来看,是伪造证书)的基础上和青花瓷通信,然后青花瓷再和Server通信,成了一个中间人的角色,这样整个过程的数据传输都被青花瓷给监听到了。至此,中间人攻击的过程就完成了。

上面👆说得太冗杂了,但是很详细,这里简单明了总结下:

  1. 首先 Charles 假冒了客户端,拿到服务器的 CA 证书
  2. 然后 Charles 假冒了服务器,给客户端发送了一张自己制作的证书,客户端信任该证书
  3. Charles 再次假冒服务器,拿到客户端的对称密钥
  4. Charles 再次假冒客户端,将对称密钥加密发送给服务器,让服务器认为这次通信是没问题的,服务器发送成功响应
  5. 最后 Charles 假冒服务器将成功响应发给客户端
  6. 建立连接,客户端与 Charles 建立连接,Charles 与服务器建立连接(中间人攻击)

客户端怎么防止被抓包呢?
法一:当进行网络请求的时候,客户端判断当前是否设置了代理,如果设置了代理,不允许进行访问。附带判断是否设置代理的代码:

+ (BOOL)getProxyStatus {
    NSDictionary *proxySettings = NSMakeCollectable([(NSDictionary *)CFNetworkCopySystemProxySettings() autorelease]);
    NSArray *proxies = NSMakeCollectable([(NSArray *)CFNetworkCopyProxiesForURL((CFURLRef)[NSURL URLWithString:@"http://www.google.com"], (CFDictionaryRef)proxySettings) autorelease]);
    NSDictionary *settings = [proxies objectAtIndex:0];
    
    NSLog(@"host=%@", [settings objectForKey:(NSString *)kCFProxyHostNameKey]);
    NSLog(@"port=%@", [settings objectForKey:(NSString *)kCFProxyPortNumberKey]);
    NSLog(@"type=%@", [settings objectForKey:(NSString *)kCFProxyTypeKey]);
    
    if ([[settings objectForKey:(NSString *)kCFProxyTypeKey] isEqualToString:@"kCFProxyTypeNone"])
    {
        //没有设置代理
        return NO;
    }
    else
    {
        //设置代理了
        return YES;
    }
}

法二:客户端本地做证书校验,并且设置不仅仅校验公钥,设置完整的正式校验模式,这样的话,证书会校验请求的时候不仅仅校验域名,会将证书中的公钥及其他信息也进行校验,这样的话,中间人伪造的证书就无法通过验证,无法进行抓包。校验代码如下:

+(AFSecurityPolicy*)customSecurityPolicy
{
    // /先导入证书
    NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"cer"];//证书的路径
    NSData *certData = [NSData dataWithContentsOfFile:cerPath];
    // AFSSLPinningModeCertificate 使用证书验证模式 (AFSSLPinningModeCertificate是证书所有字段都一样才通过认证,AFSSLPinningModePublicKey只认证公钥那一段,AFSSLPinningModeCertificate更安全。但是单向认证不能防止“中间人攻击”)
    AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
    // allowInvalidCertificates 是否允许无效证书(也就是自建的证书),默认为NO
    // 如果是需要验证自建证书,需要设置为YES
    securityPolicy.allowInvalidCertificates = YES;

    //validatesDomainName 是否需要验证域名,默认为YES;
    //假如证书的域名与你请求的域名不一致,需把该项设置为NO;如设成NO的话,即服务器使用其他可信任机构颁发的证书,也可以建立连接,这个非常危险,建议打开。

    //置为NO,主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。
    //如置为NO,建议自己添加对应域名的校验逻辑。
    securityPolicy.validatesDomainName = YES;
    NSSet<NSData*> * set = [[NSSet alloc]initWithObjects:certData  , nil];
    securityPolicy.pinnedCertificates = set;
    
     
    return securityPolicy;
}

3、TCP/UDP协议

TCP协议(传输控制协议)

特点一:面向连接

1: TCP的三次握手建立连接
2: 在该连接上进行HTTP的请求和响应
3: TCP的四次挥手进行连接的释放

连接建立流程

TCP为什么三次握手?
当网络原因服务端没有收到客户端的请求,且没有给客户端反馈。超时后客户端会再次向服务端发送请求。当网络畅通后服务器段收到最开始的请求并反馈给客户端。导致客户端认为自己未发送这个请求,服务端认为自己发送了一个新的请求,导致服务端性能浪费。

TCP为什么四次挥手?
TCP是双向通讯协议,客户端发送FIN终止报文到服务端,告诉服务端,客户端不会再发送数据了,但是服务端还有消息未发送完毕,为防止客户端消息等待,服务端会首先发送ACK确认报文到客户端,并关闭消息接收服务。但是服务端会将剩余消息发送完毕,然后发送FIN终止报文到客户端,告诉客户端不会再发送消息,然后客户端会回复ACK确认报文表示收到,并关闭服务,当服务端收到ACK确认报文后,验证正常则会关闭所有的服务,TCP连接中断完毕。

特点二:可靠传输

即保证传输的数据无差错(不丢失)、无重复按顺序安全到达。

情况一:无差错

无差错情况

情况二:超时重传(差错校验,保证不丢失)

超时重传情况

情况三:确认丢失(ACK确认报文丢失)

确认丢失情况

情况四:确认迟到(ACK确认报文迟到)

确认迟到情况

特点三:面向字节流

面向字节流
  • 发送缓冲、接收缓冲
  • TCP连接通道:拼接成一个报文,一次性发送给接收方
  • 面向字节流:不管发送方一次性提交给缓冲是多大的数据,对于TCP它会根据实际的情况进行划分(10个字节划分成6个字节和4个字节分两次发送给接收方,它也可能拼接,将2和3拼接成5发送)

特点四:流量控制

发送窗口:

发送窗口
  • 根据接收窗口的实际接收速率(比如3G网络情况下手机接收很缓慢)来要求发送窗口调整发送速率
  • 根据接收窗口还能接受的大小(比如手机内存不够了,只能接收两个字节)要求发送窗口调整发送窗口大小,最后发送的字节数目减少

接收窗口:
字节按序到达,根据序号来控制。

接收窗口

特点五:拥塞控制

慢开始、拥塞避免。

拥塞控制

UDP协议(用户数据包协议)

  • 无连接:不需要事先建立好连接,也没有释放连接的过程。
  • 尽最大努力交付:不保证可靠传输。
  • 面向报文:不管应用层报文的大小,不作合并和拆分,直接送到运输层。

4、DNS解析

什么是DNS解析?
域名到IP地址的映射,DNS解析请求采用UDP数据报且是明文的。解析过程,客户端向DNS服务发送域名请求,DNS服务根据域名返回对应的IP地址,客户端根据IP地址请求Server端服务器。

什么是DNS解析?

DNS查询方式有哪两种?

递归查询:

递归查询

迭代查询:

迭代查询

DNS劫持问题?
因为DNS解析使用UDP数据报且是明文的。客户端发送域名请求时,容易被钓鱼DNS劫持返回错误的IP地址。

DNS劫持与HTTP有什么关系?
没有关系,DNS劫持是在HTTP建立连接之前。
DNS解析通过UDP数据报,访问53端口。

DNS 劫持解决方案?

解决方案一:httpDNS

httpDNS

HTTPDNS使用HTTP协议进行域名解析,代替现有基于UDPDNS协议,域名解析请求直接发送到阿里云的HTTPDNS服务器,从而绕过运营商的Local DNS,能够避免Local DNS造成的域名劫持问题。

简单说来,像这样:使用DNS协议向DNS服务器的53端口进行请求----->使用HTTP协议向DNS服务器的80端口进行请求。

解决方案二:长连接

长连接

5、Session/Cookie

SessionCookie是对HTTP协议无状态特点(再次请求,无法记住用户)的补偿。详见我的文章 HTTPCookie,看这一篇文章就够了,这里只是简单介绍下。

Cookie

什么是Cookie?
Cookie主要用来记录用户状态,区分用户,状态保存在客户端。

Cookie
  • 客户端向服务器发送请求,服务生成Cookie同响应数据一同返回给客户端,客户端保存Cookie
  • 客户端发送的Cookie是在HTTP请求报文的Cookie首部字段中。
  • 服务端设置HTTP响应报文的Set-Cookie首部字段。

怎样修改Cookie?

  • Cookie覆盖旧Cookie
  • 覆盖时新Cookie字段要与原Cookie保持一致。
  • 设置Cookieexpires=过去的时间点,或者maxAge=0,这样可以删除Cookie

怎样保证Cookie的安全?

  • Cookie进行加密处理。
  • 只在https上携带Cookie
  • 设置CookiehttpOnly ,防止跨站脚本攻击原理。

Session

什么是Session?
Session主要用来记录用户状态,区分用户,状态保存在服务器端。

Session
  • 当有客户端访问服务器时,服务器根据需求设置Session,将会话信息保存在服务器上,并返回一个SessionID给客户端,客户端保存到内存中,称之为无过期时间的Cookie
  • 客户端关闭后Cookie被清除,他并没有存在于用户的Cookie临时文件中。

Session和Cookie的关系是怎样的?
Session需要依赖于Cookie机制。

6、网络数据解析方式

JSON解析

所用的字节数一般要比XML少,是"轻量级"的数据交换格式。

// JSON格式
[NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&error];
{to:"云龙同学 ",conent:"你好! \n今天上午,我到你家来想向你借一本《小学生常用成语词典》 . 可是不巧,你不在 。 我准各晚上 6时再来借书 。 六你在家里等我,谢 谢! ",from:" 谢佳培 ",date: "2020年 6 月 08 日"}
XML 解析

XML 数据交换格式:

  • 声明<?xml version="l.O" encoding="UTF-8'?>版本和使用的字符集,这里为 1.0版,使用中文UTF-8字符
  • 根元素 <note>是根元素的开始标签,</note>是根元素的结束 标签 。 根元素只有一个
  • 如果开始标签和结束标签之间没有内容,可以写成<from/>, 这称为"空标签"
  • 属性: <Note id="1">中,id="l"Note元素的一个属性,ID是属性名, 1是属性值
  • 命名空间: 用于为XML文档提供名字唯一的元素和属性, 以xmlns: 开头的内容都属于命名 空 间
  • 限定名: 定义了元素和属性的合法标识符, 限定名通常在 XML文档中用作特定元素或属性引用,标签<soap:Body>就是合法的限定名,前缀soap是由命名空间定义的
<?xml version="l.O"encoding="lJTF-8"?> <note>
<to>云龙同学 <Ito>
<conent>你好! \n今天上午 , 我到你家来想向你借一本《小学生常用成语词典》
结构 化 文档 。
. 可是不巧 , 你不在 。 我准各晚上 6叶再来借书 。 讨你在家里等我 , 谢谢! </conent>
<from>关 东升 </from>
<date>2012年 12 月 08 日 </date> </note>

解析XML文档:

  • SAX模式:是一种基于事件驱动的解析模式。程序从上到下读取XML文档,如果遇到开始标签、结束标签和属性 等 ,就会触发相应的事件。弊端那就是只能读取XML文档, 不能写入XML文档,它的优点是解析速度快。iOS 重点推荐使用 SAX模式解析 。
  • DOM模式:XML文档作为 一 棵树状结构进行分析,获取节点的内容以及相关属性。新增、删除和修改节点的内容,能够修改XML文档 。
//url data IO 都可以创建解析对象
let parser = XMLParser(contentsOf: url)!    
//发送parse消息,开始解析文档
parser.parse()

//文档开始的时候触发
//1次 初始化解析过程中的一些成员变量
parserDidStartDocument

//文档出错的时候触发
//该方法一般在调试阶段使用,实际发布去掉,用户会被专业的错误信息吓坏的
parseErrorOccurred

//遇到一个开始标签时候触发
didStartElement

//取出属性ID attribute属性 键是属性名字 值是属性值
let id = attributeDict["id"]

//存放解析出来的Note元素数据 
//数据 id date content userID
let id = attributeDict["id"]

//遇到字符串时候触发
foundCharacters
//过滤回车符和空格 字符集
string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)

//添加
let dict = self.listData.lastObject as! NSMutableDictionary
dict["CDate"] = string

//遇到结束标签时候出发
didEndElement

//遇到文档结束时候触发
//清理成员变量
parserDidEndDocument
//同时将数据返回给表示层 视图控制器 
NotificationCenter.default.post

二、NSURLConnection

NSURLConnection是苹果提供的原生网络访问类,但是苹果很快会将其废弃,且由NSURLSession(iOS7以后)来替代。目前使用最广泛的第三方网络框架AFNetworking最新版本已弃用了NSURLConnection,那我们学习它还有什么用呢?完善知识面广度吧,不喜欢的可以略过。

1、简介

访问http网页需要在Info.plist中添加NSAppTransportSecurity类型Dictionary,在NSAppTransportSecurity下添加NSAllowsArbitraryLoads类型Boolean,值设为YES

a、NSURL

NSURL是请求地址,定义一个网络资源路径。

  • 协议:不同的协议,代表着不同的资源查找方式、资源传输方式,比如常用的httpftp
  • 主机地址:存放资源的主机的IP地址(域名)
  • 路径:资源在主机中的具体位置
  • 参数:参数可有可无,也可以多个。如果带参数的话,用“?”号后面接参数,多个参数的话之间用&隔开
NSURL *url = [NSURL URLWithString:@"协议://主机地址/路径?参数&参数"];
b、NSURLRequest

根据前面的NSURL建立一个请求。

//url:资源路径
//timeoutInterval:超时时长,默认60s
//cachePolicy:缓存策略(无论使用哪种缓存策略,都会在本地缓存数据),
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:15.0];

// cachePolicy为枚举类型
- NSURLRequestUseProtocolCachePolicy = 0 //默认的缓存策略,使用协议的缓存策略
- NSURLRequestReloadIgnoringLocalCacheData = 1 //每次都从网络加载
- NSURLRequestReturnCacheDataElseLoad = 2 //返回缓存否则加载,很少使用
- NSURLRequestReturnCacheDataDontLoad = 3 //只返回缓存,没有也不加载,很少使用

requestNSMutableURLRequest,即可变类型,可以设置请求头,请求体等。

// 告诉服务器数据为json类型
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; 

// 设置请求体(json类型)
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:@{@"userid":@"123456"} options:NSJSONWritingPrettyPrinted error:nil];
request.HTTPBody = jsonData; 
c、发送请求

NSURLConnection默认的请求类型为GET

异步请求
/**
  * queue:代码添加到的队列,即block执行的线程
  * completionHandler:请求响应后(或者请求超时)执行的代码,
  * NSURLResponse 为服务器的响应,真实类型为NSHTTPURLResponse,通常只在“下载”功能时,才会使用 
  * NSData 服务器返回的数据,例如json、xml(现在用的少)
  * NSError 网络访问错误码
  */
[NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
    // 有的时候,服务器访问正常,但是会没有数据!
    // 以下的 if 是比较标准的错误 处理代码!
    if (connectionError != nil || data == nil) {
        //给用户的提示信息
        NSLog(@"网络不给力");
        return;
    }
}];

NSHTTPURLResponse协议头的参数

  • URL:响应的URL,有的时候,访问一个URL地址,服务器可能会出现重定向,会定位到新的地址!
  • MIMEType(Content-Type):服务器告诉客户端,可以用什么软件打开二进制数据!
  • expectedContentLength:预期的内容长度,要下载的文件长度,下载文件时非常有用
  • suggestedFilename:"建议"的文件名,方便用户直接保存,很多时候,用户并不关心要保存成什么名字!
  • textEncodingName:文本的编码名称,大多数都是UTF8
  • statusCode:状态码,在做下载操作的时候,需要判断一下
  • allHeaderFields:所有的响应头字典
发送同步请求
// 同步请求,代码会阻塞在这里一直等待服务器返回,如果data为nil则请求失败,当获取少量数据时可以使用此方法
// request参数同上
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil]; 
d、缓存
  • 求的缓存策略使用 NSURLRequestReloadIgnoringCacheData,忽略本地缓存
  • 服务器响应结束后,要记录Etag,服务器内容和本地缓存对比是否变化的重要依据!
  • 在发送请求时,设置If-None-Match,并且传入etag
  • 连接结束后,要判断响应头的状态码,如果是 304,说明本地缓存内容没有发生变化,此时可以使用本地缓存来加载数据,如果缓存文件被意外删除,程序依然运行但会报错,加载数据也失败:
NSCachedURLResponse *cachedURLRes = [[NSURLCache sharedURLCache] cachedResponseForRequest:requestM];//cachedURLRes为nil
  • GET 缓存的数据会保存在 Cache 目录中 /bundleId 下,Cache.db

2、使用

a、网络请求(json、xml数据)
/// 两种请求方式
typedef enum {
    MethodGET,
    MethodPOST
} Method;

/// 请求成功后,直接将jsonData转为jsonDict
+ (void)connectionRequestWithMethod:(Method)method   URLString:(NSString *)URLString parameters:(NSDictionary *)dict success:(void (^)(id JSON))success fail:(void (^)(NSError *error))fail {
    // 简单的转码,如果参数带有?&特殊字符,下面方法不适合
    URLString = [URLString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    // 1.创建请求
    NSURL *url = [NSURL URLWithString:URLString];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

    // 请求方式,默认为GET
    if (method == MethodPOST) {
        request.HTTPMethod = @"POST";
    }

    // 根据需要设置
    [request setValue:@"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" forHTTPHeaderField:@"Accept"];
    
    // 2.设置请求头 Content-Type  返回格式
    [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    
    // 3.设置请求体 NSDictionary --> NSData
    if (dict != nil) {
        NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil];
        request.HTTPBody = data;
    }

    // 4.发送请求
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        if ((data != nil) && (connectionError == nil)) {
            NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
            if (success) {
                success(jsonDict);
            }
        } else {
            if (fail) {
                fail(connectionError);
            }
        }  
    }];
}
b、文件下载

实现断点续传,需要用到NSURLConnectionDataDelegate代理,NSFileHandle文件操作或者数据流的形式(NSOutputStream),这里以NSFileHandle为例,访问的服务器采用Apache搭建的本地服务器。

@interface ViewController () <NSURLConnectionDataDelegate>

/// 文件下载完毕之后,在本地保存的路径
@property (nonatomic, copy) NSString *filePath;
/// 需要下载的文件的总大小!
@property (nonatomic, assign) long long serverFileLength;
/// 当前已经下载长度
@property (nonatomic, assign) long long localFileLength;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];    
}
/// 点击屏幕事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self downloadFile];
}
/// 下载文件
- (void)downloadFile {
    // 文件存储路径
    self.filePath = @"/Users/username/Desktop//陶喆 - 爱很简单.mp3";
    // 要下载的网络文件,采用Apache搭建的本地服务器
    NSString *urlStr = @"http://localhost/陶喆 - 爱很简单.mp3";

    // 简单的转码,如果参数带有?&特殊字符,下面方法不适合
    urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSURL *url = [NSURL URLWithString:urlStr];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

    // 设置请求方法为 HEAD 方法,这里只是头,数据量少,用同步请求也可以,不过最好还是用异步请求
    request.HTTPMethod = @"HEAD";
    // 无论是否会引起循环引用,block里面都使用弱引用
    __weak typeof(self) weakSelf = self;
    [NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
        // 出错则返回
        if (connectionError != nil) {
            NSLog(@"connectionError = %@",connectionError);
            return ;
        }

        // 记录需要下载的文件总长度
        long long serverFileLength = response.expectedContentLength;
        weakSelf.serverFileLength = serverFileLength;

        // 初始化已下载文件大小
        weakSelf.localFileLength = 0;
        
        NSDictionary *dict = [[NSFileManager defaultManager] attributesOfItemAtPath:weakSelf.filePath error:NULL];
        
        long long  localFileLength = [dict[NSFileSize] longLongValue];
        
        // 如果没有本地文件,直接下载!
        if (!localFileLength) {
            // 下载新文件
            [weakSelf getFileWithUrlString:urlStr];
        }

        // 如果已下载的大小,大于服务器文件大小,肯定出错了,删除文件并从新下载
        if (localFileLength > serverFileLength) {
            // 删除文件 remove
            [[NSFileManager defaultManager]  removeItemAtPath:self.filePath error:NULL];
            
            // 下载新文件
            [self getFileWithUrlString:urlStr];
            
        } else if (localFileLength == serverFileLength) {
            NSLog(@"文件已经下载完毕");
        } else if (localFileLength && localFileLength < serverFileLength) {
            // 文件下载了一半,则使用断点续传
            self.localFileLength = localFileLength;
            
            [self getFileWithUrlString:urlStr WithStartSize:localFileLength endSize:serverFileLength-1];
        }
    }];
    
}

/// 重新开始下载文件(代理方法监听下载过程)
-(void)getFileWithUrlString:(NSString *)urlString
{
    NSURL *url = [NSURL URLWithString:urlString];
    // 默认就是 GET 请求
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    
    // 开始请求,并设置代理为self
    NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];
    
    // 设置代理的执行线程,一般不在主线程
    [conn setDelegateQueue:[[NSOperationQueue alloc] init]];
    
    // NSUrlConnection 的代理方法是一个特殊的事件源!
    // 开启运行循环!
    CFRunLoopRun();
}

/* 断点续传(代理方法监听下载过程)
 * startSize:本次断点续传开始的位置
 * endSize:本地断点续传结束的位置
 */
-(void)getFileWithUrlString:(NSString *)urlString WithStartSize:(long long)startSize endSize:(long long)endSize
{
    // 1. 创建请求!
    NSURL *url = [NSURL URLWithString:urlString];
    // 默认就是 GET 请求
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    
    // 设置断点续传信息
    NSString *range = [NSString stringWithFormat:@"Bytes=%lld-%lld",startSize,endSize];
    [request setValue:range forHTTPHeaderField:@"Range"];
    
    // NSUrlConnection 下载过程!
    NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];
    
    // 设置代理的执行线程// 传一个非主队列!
    [conn setDelegateQueue:[[NSOperationQueue alloc] init]];
    
    // NSUrlConnection 的代理方法是一个特殊的事件源!
    // 开启运行循环!
    CFRunLoopRun();
}

#pragma mark - NSURLConnectionDataDelegate
/// 1. 接收到服务器响应
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    // 服务器响应
    // response.expectedContentLength的大小是要下载文件的大小,而不是文件的总大小。比如请求头设置了Range[requestM setValue:rangeStr forHTTPHeaderField:@"Range"];则返回的大小为range范围内的大小
}
/// 2. 接收到数据
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    // data当前接收到的网络数据;
    // 如果这个文件不存在,响应的文件句柄就不会创建!
    NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.filePath];
    // 在这个路径下已经有文件了!
    if (fileHandle) {
        // 将文件句柄移动到文件的末尾
        [fileHandle seekToEndOfFile];
        // 写入文件的意思(会将data写入到文件句柄所操纵的文件!)
        [fileHandle writeData:data];
        
        [fileHandle closeFile];
        
    } else {
        // 第一次调用这个方法的时候,在本地还没有文件路径(没有这个文件)!
        [data writeToFile:self.filePath atomically:YES];
    }
}
/// 3. 接收完成
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSLog(@"下载完成");
}
/// 4. 网络错误
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    NSLog(@"下载错误 %@", error);
}
@end
c、NSData 数据的反序列化

服务器返回的NSData数据类型,事先已经知道,因此可以直接返序列化为实际的类型,例如:json(字典或数组)、.plist(字典或数组)、textxml等:

// 加载本地
NSString *path = [[NSBundle mainBundle] pathForResource:@"文件名 " ofType:nil];
NSData *jsonData = [NSData dataWithContentsOfFile:path];

// 从网络直接加载
NSURL *url = [[NSBundle mainBundle] URLForResource:@"topic_news.json" withExtension:nil];
NSData *data = [NSData dataWithContentsOfURL:url];

// .plist反序列化,这种很少使用,只是苹果自己的格式
// 通常后续直接做字典转模型,不需要关心是否可变,所以一般直接赋值为0
id result = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:NULL];

// 关于选型参数
NSPropertyListImmutable = 0, // 不可变  
NSPropertyListMutableContainers = 1 << 0, // 容器可变
NSPropertyListMutableContainersAndLeaves = 1 << 1, // 容器和叶子可变

json数据的本质是字符串,根格式只有两种可能:数组或字典。一个对象能够被转换成JSON必须符合以下条件:

  • 顶级节点,必须是一个 NSArray or NSDictionary
  • 所有的对象必须是 NSString, NSNumber, NSArray, NSDictionary, NSNull
  • 所有字典的 key 都必须是 NSString
  • NSNumber 不能为空或者无穷大

反序列化:从服务器接收到的二进制数据转换成字典或者数组

// 假设json数据位:[{键值对},{键值对}],则可以将数据转化为字典或数组
NSArray *dictArray = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:nil];

序列化:将字典或者数组转换成二进制数据,准备发送给服务器

// obj为字典或数组
NSData *data = [NSJSONSerialization dataWithJSONObject:obj options:0 error:NULL];
// 转化前可以判断是否可以序列化:
+ (BOOL)isValidJSONObject:(id)obj;

三、NSURLSession

苹果对它的定位是作为NSURLConnection的替代者,使用最广泛的第三方网络框架:AFNetworkingSDWebImage等等都使用了NSURLSession

对比NSURLConnect优势

  1. NSURLConnect每次进行HTTP请求的时候 ,都需要三次握手,四次挥手等操作,无法复用通道,
    NSURLSessionHTTP(Keep-Alive)可以复用链接通道,即可以三次握手建立链接之后继续复用此通道。

  2. NSURLSession针对下载/上传等复杂的网络操作提供了专门的解决方案,针对普通、上传和下载分别对应三种不同的网络请求任务:NSURLSessionDataTaskNSURLSessionUploadTaskNSURLSessionDownloadTask

  3. 下载任务不需要内存拼接dataNSURLSession创建本地临时文件,写到临时文件,优化内存。

简介

系统提供的网络通信API,用来替代NSURLConnection网络通信库。

  • NSURLSession可以为每个用户会话 (session) 配置缓存、协议
  • 支持安全证书策略
  • 支持 FTPHTTPHTTPS等网络通信协议
  • 支持实现后台下载和上传,以及断点续传功能
  • 还可以取消、恢复、挂起网络请求操作。
Session

Session翻译为中文意思是会话,在七层网络协议中有物理层->数据链路层->网络层->传输层->会话层->表示层->应用层,可以将NSURLSession类理解为会话层,用于管理网络接口的创建、维护、删除等等工作,我们要做的工作也只是会话层之后的层即可,底层的工作NSURLSession已经帮我们封装好了。另外还有一些Session,比如AVAudioSession用于音视频访问,WCSession用于WatchOS通讯,它们都是建立一个会话,并管理会话,封装一些底层,方便我们使用。举一反三。

  • 会话NSURLSession:指应用程序与服务器建立的通信对象,每一个会话协调一组相关的数据传输任务。
  • 简单会话:不可配置会话,只能执行基本的网络,shared静态单例获得该对象
  • 默认会话 (default session) :可以配置,defaultSessionConfiguration获得默认会话配置对象
  • 短暂会话 (ephemeral session):不存储任何数据在磁盘中,所有的数据都是缓存的,当应用结束会话时,它们被自动释放
  • 后台会话 (background session):当按下home键后仍然可以在后台进行上传下载操作,需要通过唯一的identity标示
  • URLSessionConfiguration:指定NSURLSession的配置信息。这些配置信息决定了NSURLSession的种类,HTTP的额外headers,请求的timeout时间,Cookie的接受策略等配置信息。
架构

关键类:NSULRSessionNSURSLessionConfigurationNSURLSessinoTask
代理:NSURLSessinoDelegateNSURLSesisonTaskDelegateNSURLSessionDataDelegateNSURLSessoinDownloadDlegate

会话任务NSURLSessionTask:所有任务都可以取消、暂停或恢复。
数据任务 (datatask):请求网络资源
上传任务 (uploadtask): 通常以文件的形式发送数据,支持后台上传
下载任务 (downloadtask): 以文件形式接收数据 , 支持后台下载

为了方便使用,苹果提供了一个全局的NSURLSession单例,如同NSURLConnection一样。这样做的缺陷就是不能监控,如果想要监控每一个请求,则必须通过代理来监听,我们知道单例是一对多的,而代理是一对一,因此必须自己实例化单独的Session任务对象(NSURLConnection则很难),来实现单独监控。

系统一共提供了5种任务类,继承关系如下图所示。其中NSURLSessionTask为抽象类,不能实现网络访问,NSURLSessionStreanTask(以流的方式进行网络访问)使用的比较少,使用的多的是dataTaskdownloadTaskuploadTask,即图中红色框框圈的部分,基本满足了网络访问的基本需求:获取数据(通常是JSONXML等)、文件上传、文件下载。这三个类都是NSURLSessionTask这个抽象类的子类,相比直接使用NSURLConnectionNSURLSessionTask支持任务的暂停、取消和恢复,并且默认任务运行在其他非主线程中。

关键类

根据图中代理协议的名字不难发现,每一个任务类都有相对应的代理协议,只有NSURLSessionUploadTask没有对应的代理协议,因为NSURLSessionUploadTask继承自NSURLSessionDataTask,因此NSURLSessionDataDelegate即为NSURLSessionUploadTask对应的代理协议。

代理

1、实现 GET 请求

a、开始请求网络服务

使用简单会话实现的网络请求不能进行任何配置,简单会话是在非主队列中执行的,当遇到表视图刷新这种更新 UI界面的操作时,要切换回主队列执行。可以使用默认会话,然后进行配置为mainQueue,由于会话任务是在主线程中执行不需要再放到并发队列方法dispatch_async

// 实现 GET 请求
- (void)startRequest
{
    //1、指定请求的 URL
    //请求的参数全部暴露在 URL后面,这是 GET请求方法的典型特征
    //注册用户邮箱  type:数据交互类型(JSON 、 XML和 SOAP) action:add、 remove、 modify和query)
    NSString *strURL = [[NSString alloc] initWithFormat:@"http://www.xiejiapei.com/service/mynotes/WebServic.php?email=%@&type%@&action=%@",@"<用户邮箱>","JSON",@"query"];
    //URL字符串允许的字符集
    strURL = [strURL stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
    //将字符串转换为 URL字符串
    NSURL *url = [NSURL URLWithString:strURL];

    //2、构造网络请求对象 NSURLRequest
    NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
    //3、创建简单会话对象
    NSURLSession *session = [NSURLSession sharedSession];
    //创建默认配置对象
    NSURLSessionConfiguration *defaultConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    //设置缓存策略
    defaultConfig.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
    //设置网络服务类型 决定了网络请求的形式
    defaultConfig.networkServiceType = NSURLNetworkServiceTypeDefault;
    //设置请求超时时间
    defaultConfig.timeoutIntervalForRequest = 15;
    //设置请求头
    //defaultConfig.HTTPAdditionalHeaders
    //网络属性  是否使用移动流量
    defaultConfig.allowsCellularAccess = YES;
    
    //3、创建默认会话对象
    //本例网络请求任务之后回调的是代码块,而非委托对象的方法,delegate参数被赋值为 nil
    //delegateQueue参数是设置会话任务执行所在的操作队列,NSOperationQueue是操作队列, 内部封装了线程
    //由于会话任务是在主线程中执行不需要再放到并发队列方法dispatch_async
    NSURLSession *session = [NSURLSession sessionWithConfiguration:defaultConfig delegate:nil delegateQueue:[NSOperationQueue mainQueue]];

    /* 4、数据任务 (NSURLSessionDataTask) 对象
     * 第一个参数是NSURLRequest请求对象
     * 第二个参数completionHandler是请求完成回调的代码块
     * data参数是从服务器返回的数据
     * response是从服务器返回的应答对象
     * error是错误对象,如果 error为 nil, 则说明请求过程没有错误发生
     */
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"请求完成...");
       
        //将响应对象转化为NSHTTPURLResponse对象
        NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
        //获取网络连接的状态码,200成功  404网页未找到
        NSLog(@"状态码:%li", httpResponse.statusCode);
        if (httpResponse.statusCode == 200) {
            NSLog(@"请求成功");
        }
        //获取响应头
        NSLog(@"响应头:%@", httpResponse.allHeaderFields);
        
        //获取响应体
        if (!error) {
            NSDictionary *resDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
            //调用 reloadView: 方法是在 GCD主队列中执行的
            //简单会话是在非主队列中执行的,当遇到表视图刷新这种更新 UI界面的操作时,要切换回主队列执行
            dispatch_async(dispatch_get_main_queue(), ^{
                //在请求完成时调用 reloadView:方法,该方法用千重新加载表视图中的数据
                [self reloadView:resDict];
            });
        } else {
            NSLog(@"error: %@", error.localizedDescription);
        }
    }];
    //5、在会话任务对象上调用 resume方法开始执行任务,新创建的任务默认情况下是暂停的
    [task resume];
}
b、重新加载表视图
- (void)reloadView:(NSDictionary *)res
{
    //从服务器返回的JSON格式数据有两种情况,成功返回或者失败,ResultCode数据项用于说明该结果
    NSNumber *resultCode = res[@"ResultCode"];
    //当ResultCode大于等于0时,说明服务器端操作成功
    if ([resultCode integerValue] >= 0)
    {
        //取得从服务端返回的数据
        self.listData = res[@"Record"];
        [self.tableView reloadData];
    }
    else
    {
        //为了减少网络传输,只传递消息代码,不传递消息内容
        //根据结果编码获得结果消息
        NSString *errorStr = [resultCode errorMessage];
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"错误信息" message:errorStr preferredStyle:UIAlertControllerStyleAlert];
        UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil];
        [alertController addAction:okAction];
        //显示错误消息
        [self presentViewController:alertController animated:true completion:nil];
    }
}
c、代理方法
//已经接受到响应头
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
    //通过状态码来判断石是否成功
    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
    if (httpResponse.statusCode == 200)
    {
        NSLog(@"请求成功");
        NSLog(@"%@", httpResponse.allHeaderFields);

        //初始化接受数据的NSData变量
        _data = [[NSMutableData alloc] init];

        //执行completionHandler Block回调来继续接收响应体数据
        /*
         NSURLSessionResponseCancel 取消接受
         NSURLSessionResponseAllow  继续接受
         NSURLSessionResponseBecomeDownload 将当前任务 转化为一个下载任务
         NSURLSessionResponseBecomeStream   将当前任务 转化为流任务
         */
        completionHandler(NSURLSessionResponseAllow);
    }
    else
    {
        NSLog(@"请求失败");
    }
}

//接受到数据包时调用的代理方法
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    NSLog(@"收到了一个数据包");

    //拼接完整数据
    [_data appendData:data];
    NSLog(@"接受到了%li字节的数据", data.length);
}

//数据接收完毕时调用的代理方法
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    NSLog(@"数据接收完成");

    if (error)
    {
        NSLog(@"数据接收出错!");
        //清空出错的数据
        _data = nil;
    }
    else
    {
        //数据传输成功无误,JSON解析数据
        NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:_data options:NSJSONReadingMutableLeaves error:nil];
        NSLog(@"%@", dic);
    }
}

2、实现POST请求

使用POST方法请求的关键是用 NSMutableURLRequest类替代NSURLRequest类。

// 实现 Post 请求
- (void)startPostRequest
{
    //在这个URL字符串后面没有参数(即没有?号后面的内容)
    NSString *strURL = @"http://www.xiejiapei.com/service/mynotes/WebService.php";
    NSURL *url = [NSURL URLWithString:strURL];
    
    //参数字符串:请求参数放到请求体中
    NSString *post = [NSString stringWithFormat:@"email=%@&type=%@&action=%@", @"<用户邮箱>", @"JSON", @"query"];
    //postData就是请求参数:将参数字符串转换成NSData类型,编码一定要采用UTF-8
    NSData *postData = [post dataUsingEncoding:NSUTF8StringEncoding];

    //创建可变的请求对象NSMutableURLRequest,所以可以通过属性设置其内容
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    //HTTPMethod属性用于设置HTTP请求方法为POST
    [request setHTTPMethod:@"POST"];
    //HTTPBody属性用于设置请求数据
    [request setHTTPBody:postData];

    //创建会话
    NSURLSessionConfiguration *defaultConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration: defaultConfig delegate: nil delegateQueue: [NSOperationQueue mainQueue]];
    
    //创建任务
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        //JSON解析
        NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
        NSLog(@"%@", dic);
    }];
    
    [dataTask resume];
}

3、下载数据

点击按钮开始、暂停、继续下载:

#define kResumeDataPath [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/resumeData.plist"]

@interface NSURLSessionViewController ()<NSURLSessionDownloadDelegate>

@property(nonatomic, strong) UIProgressView *progressView;
@property(nonatomic, strong) UILabel *progressLabel;

@end

@implementation NSURLSessionViewController
{
    NSURLSession *_session;
    NSURLSessionDownloadTask *_downLoadTask;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    //创建会话,delegate为self
    NSURLSessionConfiguration *defaultConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    _session = [NSURLSession sessionWithConfiguration:defaultConfig delegate:self delegateQueue:[NSOperationQueue mainQueue]];
}

//开始下载
- (void)startDownLoad
{
    //设置文件下载地址
    NSString *strURL = [[NSString alloc] initWithFormat:@"http://218.76.27.57:8080/海贼王.jpg"];
    NSURL *url = [NSURL URLWithString:strURL];
    
    //创建下载任务
    _downLoadTask = [_session downloadTaskWithURL:url];
    //开始执行任务
    [_downLoadTask resume];
}

//暂停下载
- (void)pauseDownLoad
{
    //判断当前的下载状态
    if (_downLoadTask && _downLoadTask.state == NSURLSessionTaskStateRunning)
    {
        //取消当前任务,将任务的信息保存的文件中
        [_downLoadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {

            //将数据写入到文件,方便下次读取
            //下载链接,已下载的数据大小,已下载的临时文件文件名
            [resumeData writeToFile:kResumeDataPath atomically:YES];
            NSLog(@"将数据写入到文件:%@", kResumeDataPath);
        }];

        _downLoadTask = nil;
    }
}

//继续下载
- (void)resumeDownLoad
{
    //获取已经保存的数据
    NSData *data = [NSData dataWithContentsOfFile:kResumeDataPath];
    if (data)
    {
        //重建下载任务
        _downLoadTask = [_session downloadTaskWithResumeData:data];
        //继续任务
        [_downLoadTask resume];
        //移除文件
        [[NSFileManager defaultManager] removeItemAtPath:kResumeDataPath error:nil];
    }
}

实现NSURLSessionDownloadDelegate委托协议:

#pragma mark - NSURLSessionDownloadDelegate

 /** 文件下载完成
  *  @param session      网络会话
  *  @param downloadTask 下载任务
  *  @param location     下载完成的文件,在本地磁盘中的位置,是个保存数据的本地临时文件
  */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
    NSLog(@"下载完成");
    NSLog(@"临时文件: %@\n", location);
    
    //拼接文件的目标路径
    NSString *downloadsDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, TRUE) objectAtIndex:0];
    NSString *downloadStrPath = [downloadsDir stringByAppendingPathComponent:@"歌曲.mp3"];
    NSURL *downloadURLPath = [NSURL fileURLWithPath:downloadStrPath];
    
    //将临时文件,移动到沙盒路径中
    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    //用于判断在沙箱 Documents 目录下是否存在 海贼王.jpg文件
    NSError *error = nil;
    if ([fileManager fileExistsAtPath:downloadStrPath])
    {
        //如果存在,删除 海贼王.jpg文件,这可以防止最新下载的文件不能覆盖之前遗留的 海贼王.jpg文件
        [fileManager removeItemAtPath:downloadStrPath error:&error];
        if (error)
        {
            NSLog(@"删除文件失败: %@", error.localizedDescription);
        }
    }
    
    error = nil;
    if ([fileManager moveItemAtURL:location toURL:downloadURLPath error:&error])
    {
        NSLog(@"文件保存地址: %@", downloadStrPath);
        
        //将下载时保存数据的本地临时文件移动到沙箱 Documents 目录下,并命名为 海贼王.jpg文件
        //沙箱Documents目录下不能有 海贼王.jpg名字的文件,否则无法完成移动操作
        UIImage *image = [UIImage imageWithContentsOfFile:downloadStrPath];
        
        //文件下载并移动成功后,构建UIImage对象,然后再把UIImage对象赋值给图片视图
        //在界面中我 们就可以看到下载的图片了
        self.imageView.image = image;
    }
    else
    {
        NSLog(@"保存文件失败: %@", error.localizedDescription);
    }
}

/** 接受到一部分数据后 调用的方法
 *  @param session                   网络会话对象
 *  @param downloadTask              下载任务对象
 *  @param bytesWritten              当前数据包写入的数据字节数
 *  @param totalBytesWritten         当前总共写入的数据字节数
 *  @param totalBytesExpectedToWrite 完整的文章总大小字节数
 */
- (void)URLSession:(NSURLSession *)session
      downloadTask:(NSURLSessionDownloadTask *)downloadTask
      didWriteData:(int64_t)bytesWritten
 totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{

    NSLog(@"获取到一个数据包:%lli", bytesWritten);
    NSLog(@"已经写入的数据包大小:%lli", totalBytesWritten);
    NSLog(@"总文件大小:%lli", totalBytesExpectedToWrite);

    //计算当前下载任务进度
    CGFloat progress = (CGFloat)totalBytesWritten / totalBytesExpectedToWrite;

    //更新进度条的进度,属于更新UI操作,需要在主队列(主线程所在队列)中执行
    //由于配置会话时设置的是主队列(主线程所在队列),所以更新UI的操作不必放在 dispatch_async中执行
    _progressView.progress = progress;
    _progressLabel.text = [NSString stringWithFormat:@"%.2f%%", progress * 100];
}

4、上传数据

可以采用NSURLSession API中的NSURLSessionUploadTask任务实现, 由于上传数据时需要模拟HTML表单上传数据,数据采用multipart/form-data格式, 即将数据分割成小段进行上传, 具体实现非常复杂 。 NSURLSessionUploadTask任务没有屏蔽这些复杂性, 不推荐使用 NSURLSessionUploadTask任务,而推荐采用AFNetworking网络请求框架。

#define kBoundary @"ABC"

/*
 multipart/form-data 上传数据时 所需要的数据格式
 HTTP请求头:
 ....
 multipart/form-data; charset=utf-8;boundary=AaB03x
 ....

 HTTP请求体:
 --AaB03x
 Content-Disposition: form-data; name="key1"

 value1
 --AaB03x
 Content-Disposition: form-data; name="key2"

 value2
 --AaB03x
 Content-Disposition: form-data; name="key3"; filename="file"
 Content-Type: application/octet-stream

 图片数据...
 --AaB03x--
 */


/**
*  包装请求体
*
*  @param token 用户密钥
*  @param text  微博正文
*  @param image 上传的图片
*
*  @return multipart/form-data
*/
- (NSData *)bodyDataWithToken:(NSString *)token
                    text:(NSString *)text
                   image:(UIImage *)image
{

    //key1 = @"access_token"    vlaue1 = 2.00hd363CtKpsnBedca9b3f35tBYiPD
    //key2 = @"status"          value2 = text
    //key3 = @"pic"             value3 = image;

    //包装数据到请求体中
    NSMutableString *mString = [[NSMutableString alloc] init];

    //token
    [mString appendFormat:@"--%@\r\n", kBoundary];
    //拼接带有双引号的字符串 需要添加\在双引号之前
    [mString appendFormat:@"Content-Disposition: form-data; name=\"access_token\"\r\n\r\n"];
    //拼接value1
    [mString appendFormat:@"%@\r\n", token];
    [mString appendFormat:@"--%@\r\n", kBoundary];

    //微博正文
    //key2
    [mString appendFormat:@"Content-Disposition: form-data; name=\"status\"\r\n\r\n"];
    //拼接value2
    [mString appendFormat:@"%@\r\n", text];
    [mString appendFormat:@"--%@\r\n", kBoundary];

    //图片
    [mString appendFormat:@"Content-Disposition: form-data; name=\"pic\"; filename=\"file\"\r\n"];
    [mString appendFormat:@"Content-Type: application/octet-stream\r\n\r\n"];

    NSLog(@"%@", mString);
    //将字符串  转化为NSData
    NSMutableData *bodyData = [[mString dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];

    //拼接图片数据
    //将图片转化为数据
    NSData *imageData = UIImageJPEGRepresentation(image, 1);
    [bodyData appendData:imageData];

    //结尾字符串  结束符
    NSString *endString = [NSString stringWithFormat:@"\r\n--%@--\r\n", kBoundary];
    NSData *endData = [endString dataUsingEncoding:NSUTF8StringEncoding];

    [bodyData appendData:endData];

    return [bodyData copy];
}


- (void)uploadImage
{
    //构建URL
    NSURL *url = [NSURL URLWithString:@"https://upload.api.weibo.com/2/statuses/upload.json"];
    //构建请求对象
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];

    //设置请求对象
    request.URL = url;
    request.HTTPMethod = @"POST";
    //请求头
    //格式: multipart/form-data; charset=utf-8;boundary=AaB03x
    //拼接请求头字符串
    NSString *string = [NSString stringWithFormat:@"multipart/form-data; charset=utf-8; boundary=%@", kBoundary];
    //设置请求头
    [request setValue:string forHTTPHeaderField:@"Content-Type"];

    //图片和文本
    UIImage *ali = [UIImage imageNamed:@"ali.jpg"];
    NSString *text = @"发送微博";
    NSString *token = @"2.00hd363CtKpsnBedca9b3f35tBYiP";
    //创建bodyData
    NSData *bodyData = [self bodyDataWithToken:token text:text image:ali];


    //创建会话
    NSURLSession *session = [NSURLSession sharedSession];
    //创建上传任务
    NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request fromData:bodyData completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

        NSHTTPURLResponse *httpRes = (NSHTTPURLResponse *)response;
        NSLog(@"状态码:%li", httpRes.statusCode);
        if (error) {
            NSLog(@"上传出错");
        } else {
            NSLog(@"上传成功");
        }
    }];

    //开始任务
    [uploadTask resume];
}

5、Demo演示

运行效果
a、Get方式
- (void)httpGet
{
    NSURL *getURL = [NSURL URLWithString:@"https://www.v2ex.com/api/topics/hot.json"];
    
    // 下载数据
    NSURLSessionDataTask *getTask = [self.URLSession dataTaskWithURL:getURL completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        if (!error)
        {
            NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
            NSLog(@"Get方式获取到的数据为:%@", dict);
        }
        else
        {
            NSLog(@"Get方式获取数据出错了:%@", error);
        }
    }];
    [getTask resume];
}
b、Post方式
- (void)httpPost
{
    NSDictionary *dict = @{@"home": @"post"};
    NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil];
    NSURL *postURL = [NSURL URLWithString:@"https://www.v2ex.com/api/topics/hot.json"];
    
    NSMutableURLRequest *postRequest = [NSMutableURLRequest requestWithURL:postURL];
    postRequest.HTTPMethod = @"POST";
    postRequest.HTTPBody = data;
    [postRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    
    // 上传数据
    NSURLSessionDataTask *postTask = [self.URLSession uploadTaskWithRequest:postRequest fromData:data completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        if (!error)
        {
            NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
            NSLog(@"Post方式上传获取到服务端返回到结果为:%@", dict);
        }
        else
        {
            NSLog(@"Post方式上传出错了:%@", error);
        }
    }];
    [postTask resume];
}
c、抓取后台数据
- (void)fetchData
{
    if (self.topicURLString.length < 1)
    {
        NSLog(@"用于获取数据的URL不能为空");
        return;
    }
    NSURL *topicURL = [NSURL URLWithString:self.topicURLString];
    
    NSURLSessionDataTask *topicTask = [self.URLSession dataTaskWithURL:topicURL completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        if (!error)
        {
            NSArray *topicList = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
            [self parseTopics:topicList];
        }
        else
        {
            NSLog(@"获取数据出错了:%@", error);
        }
    }];
    [topicTask resume];
}
d、图片下载
- (void)downloadImage:(NSString *)imageURL forCell:(TopicCell *)cell
{
    NSURL *downloadURL = [NSURL URLWithString:imageURL];
    
    NSURLSessionDownloadTask *task = [self.URLSession downloadTaskWithURL:downloadURL completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        // 将图片写入缓存
        NSURL *cachedImageURL = [self writeImageToCacheFromLocation:location forDownloadURL:downloadURL];
        
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageCacheDict[imageURL] = cachedImageURL;
            cell.memberAvatarImageView.image = [self imageForURL:cachedImageURL];
        });
    }];
    [task resume];
}
e、图片缓存
// 根据URL获取图片
- (UIImage *)imageForURL:(NSURL *)imageURL
{
    NSData *data = [NSData dataWithContentsOfURL:imageURL];
    UIImage *image = [UIImage imageWithData:data];
    return image;
}

- (NSURL *)writeImageToCacheFromLocation:(NSURL *)location forDownloadURL:(NSURL *)downloadURL
{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSURL *applicationSupportURL = [[fileManager URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] lastObject];
    
    // 图片目录路径
    NSString *imageDirPath = [applicationSupportURL.path stringByAppendingPathComponent:@"image"];
    if (![fileManager fileExistsAtPath:imageDirPath])
    {
        [fileManager createDirectoryAtPath:imageDirPath withIntermediateDirectories:YES attributes:nil error:nil];
    }
    
    // 图片路径
    NSString *fileName = [downloadURL.path lastPathComponent];
    NSString *imagePath = [imageDirPath stringByAppendingPathComponent:fileName];
    NSURL *imageURL = [NSURL fileURLWithPath:imagePath];
    
    // 移动图片
    [fileManager copyItemAtURL:location toURL:imageURL error:nil];
    return imageURL;
}
f、转化Model
- (void)parseTopics:(NSArray<NSDictionary *> *)topics
{
    NSMutableArray *topicArray = [NSMutableArray array];
    
    [topics enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        
        TopicModel *topicModel = [[TopicModel alloc] init];
        topicModel.node = [self nodeModelOfTopic:obj];
        topicModel.member = [self memberModelOfTopic:obj];
        
        topicModel.last_reply_by = obj[@"last_reply_by"];
        topicModel.last_touched = [obj[@"last_touched"] integerValue];
        topicModel.title = obj[@"title"];
        topicModel.url = obj[@"url"];
        topicModel.created = [obj[@"created"] integerValue];
        topicModel.content = obj[@"content"];
        topicModel.content_rendered = obj[@"content_rendered"];
        topicModel.last_modified = [obj[@"last_modified"] integerValue];
        topicModel.replies = [obj[@"replies"] integerValue];
        topicModel.topicID = [obj[@"id"] integerValue];
        
        [topicArray addObject:topicModel];
    }];
    self.topicArray = topicArray;
    
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.tableView reloadData];
    });
}
g、显示页面
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    TopicCell *cell = [tableView dequeueReusableCellWithIdentifier:TopicCellReuseIdentifier forIndexPath:indexPath];
    
    TopicModel *topicModel = self.topicArray[indexPath.row];
    cell.titleLabel.text = topicModel.title;
    cell.nodeNameLabel.text = topicModel.node.name;
    cell.memberNameLabel.text = topicModel.member.username;
    cell.replyCountLabel.text = @(topicModel.replies).stringValue;
    
    NSString *imageURL = topicModel.member.avatar_normal;
    NSURL *cachedImageURL = self.imageCacheDict[imageURL];
    if (cachedImageURL)// 图片有被缓存过
    {
        
        cell.memberAvatarImageView.image = [self imageForURL:cachedImageURL];
    }
    else// 未缓存则下载
    {
        [self downloadImage:imageURL forCell:cell];
    }
    
    return cell;
}

四、AFNetworking框架

简介

NSURLSession仍然不是很理想,易用性不够。AFNetworking框架是Alamofire基金会支持的项目,因此能够获得比较稳定的技术支持,以及后续的升级和维护。支持ARC、断点续传、同步异步请求、图片缓存到内存、后台下载、下载进度、上传进度、缓存离线请求、cookieHTTPS

a、特点
  • AFNetworking 构建在 NSURLSessionNSOperation,以及其他熟悉的 Foundation 技术之上。它拥有良好的架构,丰富的 API,以及模块化构建方式,使得使用起来非常轻松。
  • AFNetworking 可以用于发送 HTTP 请求,接收 HTTP 的响应,但是不会缓存服务器的响应,不能执行 HTML 页面中的 javascript 代码。
  • AFNetworking 内置支持 JSONPlist 文件和 XML 文件的半自动序列化和反序列化,可以对 JSON 格式的请求响应数据自动做反序列化,XML 格式数据需要手动反序列化,使用比较方便。
  • AFNetworking 中回调函数将在主线程中进行,程序员不需要关心线程间通讯问题。
  • AFNetworking 具有完善的错误处理机制。
b、关键类

AFHTTPSesssionManager:单例子类,统一管理全局的所有网络访问。
AFNetworkReachabilityManager:网络状态监测
AFSecurityPolicy:安全策略

c、序列化 & 反序列化

AFURLRequestSerialization:设置请求的数据格式
AFURLResponseSerialization:设置响应的数据格式
AFHTTPRequestSerializer:二进制-默认
AFJSONRequestSerializer:JSON
AFXMLParserResponseSerializer:XMLParser-SAX 解析
AFXMLDocumentResponseSerializer:XMLDocument-DOM 解析
AFImageResponseSerializer:图片

1、实现GET请求

- (void)startGetRequest
{
    NSString *urlString = @"http://piao.163.com/m/cinema/list.html?app_id=1&mobileType=iPhone&ver=2.6&channel=appstore&deviceId=9E89CB6D-A62F-438C-8010-19278D46A8A6&apiVer=6&city=110000";
    
    //1、创建manager
    AFHTTPSessionManager *manger = [AFHTTPSessionManager manager];

    //2、设置发送和接收的数据类型
    //发送数据格式
    //AFHTTPRequestSerializer 使用 key1=value1&key2=value2的形式来上传数据 默认
    //AFJSONRequestSerializer 使用Json格式上传数据 [AFJSONRequestSerializer serializerWithWritingOptions:NSJSONWritingPrettyPrinted];
    manger.requestSerializer = [AFHTTPRequestSerializer serializer];
    
    //接受数据格式
    //AFHTTPResponseSerializer 不做解析操作
    //AFJSONResponseSerializer 自动进行Json解析  默认
    //AFXMLParserResponseSerializer 接受XML数据
    manger.responseSerializer = [AFJSONResponseSerializer serializerWithReadingOptions:NSJSONReadingMutableLeaves];
    
    [manger GET:urlString parameters:nil headers:nil progress:^(NSProgress * _Nonnull downloadProgress) {
        
        NSLog(@"%lli/%lli", downloadProgress.completedUnitCount, downloadProgress.totalUnitCount);
        
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {

        NSLog(@"请求成功");
        
        //responseObject是从服务器返回的JSON对象,可以是字典或数组类型
        NSLog(@"responseObject%@", responseObject);
        
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        
        NSLog(@"请求失败");
        NSLog(@"error : %@", error.localizedDescription);
    }];
            
}

2、实现 POST请求

- (void)startPostRequest
{
    //1、创建manager
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];

    //2、请求参数
    NSString *urlString = @"http://piao.163.com/m/cinema/schedule.html?app_id=1&mobileType=iPhone&ver=2.6&channel=appstore&deviceId=9E89CB6D-A62F-438C-8010-19278D46A8A6&apiVer=6&city=110000";
    NSDictionary *parameters = @{@"cinema_id" : @"1533"};


    //3、发起POST请求
    [manager POST:urlString parameters:parameters headers:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        
        NSLog(@"请求成功:%@", responseObject);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        
        NSLog(@"请求失败");
    }];
}

3、下载数据

- (void)startDownloadRequest
{
    //下载地址
    NSString *urlString = @"http://218.76.27.57:8080/chinaschool_rs02/135275/153903/160861/160867/1370744550357.mp3";
    
    //创建manager
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    
    //创建请求对象
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:urlString]];
    
    
    //创建下载任务
    NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
        
        NSLog(@"%lli/%lli", downloadProgress.completedUnitCount, downloadProgress.totalUnitCount);
    } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
        
        //接受到响应头,需要制定保存路径
        NSLog(@"状态码:%li", ((NSHTTPURLResponse *)response).statusCode);
        
        //targetPath 零时文件保存路径
        //返回值 指定的保存路径
        NSString *filePath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/歌曲.mp3"];
        NSLog(@"%@", filePath);
        
        //创建一个沙盒路径下的子路径,设定保存的文件夹位置
        return [NSURL fileURLWithPath:filePath];
        
    } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
        NSLog(@"下载完成");
    }];
    
    [downloadTask resume];
}

4、上传数据

- (void)startUploadRequest
{
    //设置请求网址
    NSString *urlString = @"https://api.weibo.com/2/statuses/upload.json";
    NSString *token = @"2.00hd363CtKpsnBedca9b3f35tBYiP";
    
    //获取输入的文字和图片
    UIImage *image = _imageView.image;
    NSString *text = _textField.text;
    if (image == nil || text.length == 0)
    {
        return;
    }
    
    //构造参数字典
    NSDictionary *dic = @{@"access_token" : token, @"status" : text};
    
    //创建HTTP请求序列化对象,封装了HTTP请求参数(放在 URL 问号之后的部分)和表单数据
    AFHTTPRequestSerializer *serializer = [AFHTTPRequestSerializer serializer];
    
    //创建请求对象
    /* 该方法可以发送 multipart/form-data格式的表单数据
     * method参数的请求方法一般是 POST
     * URLString 参数是上传时的服务器地址
     * parameters是请求参数,它是字典结构;
     * constructingBodyWithBlock是请求体代码块
     * error参数是错误对象
     */
    NSMutableURLRequest *request = [serializer multipartFormRequestWithMethod:@"POST" URLString:urlString parameters:dic constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
        
        //图片数据的拼接
        NSData *imageData = UIImageJPEGRepresentation(image, 1);
        
        /* 添加 multipart/form-data格式 的表单数据,这种格式的数据被分割成小段进行上传
         * 该方法的第一个参数是要上传的文件路径
         * 第二个参数name是与数据相关的名称,一般是 file,相当于 HTML中表单内的选择文件控件 <input type="file" >类型
         * 第三个参数 fileName是文件名,是放在请求头中的文件名,不能为空
         * 第四个参数 mimeType是数据相关的MIME类型
        */
        [formData appendPartWithFileData:imageData name:@"pic" fileName:@"image.png" mimeType:@"image/png"];
        
    } error:nil];
    
    //创建manager
    AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    
    /* 用于创建NSURLSessionUploadTask上传会话任务
     * request参数是请求对象
     * progress参数代码块用来获得当前运行的进度,代码块的参数upload Progress也是 NSProgress类型
     * completionHandler参数也是代码库,是从服务器返回应答数据时回调
     */
    NSURLSessionUploadTask *uploadTask = [manager uploadTaskWithStreamedRequest:request progress:^(NSProgress * _Nonnull uploadProgress) {
        
        //回到主线程中 刷新界面
        [_progressView performSelectorOnMainThread:@selector(setProgress:) withObject:@(uploadProgress.fractionCompleted) waitUntilDone:YES];
        
        dispatch_async(dispatch_get_main_queue(), ^{
            //上传进度
            [_progressView setProgress:uploadProgress.fractionCompleted];
        });
        
    } completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
        
        NSLog(@"上传结束,状态码:%li", ((NSHTTPURLResponse *)response).statusCode);
    }];
    
    [uploadTask resume];
}

四、断点续传和后台下载(基于AFNetworking)

续文见下篇 IOS基础:网络请求(下)


Demo

Demo在我的Github上,欢迎下载。
NetworkRequestDemo

参考文献

最详细的MD5签名的原理和流程
漫画趣解MD5算法
Http协议请求和响应报文字段详解
iOS 如何防止https抓包(中间人攻击),及charles抓包原理
如何用 Charles 抓 HTTPS 的包?
HTTP学习笔记
iOS网络NSURLConnection使用详解
iOS学习笔记-----URLSession的使用
iOS学习笔记-----AFNetworking的使用

相关文章

网友评论

      本文标题:IOS基础:网络请求(上)

      本文链接:https://www.haomeiwen.com/subject/ngaumktx.html