1.网络涉及到的基本概念
①客户端 : 移动应用/桌面应用
②服务端 : 为客户端提供服务,提供数据和资源的机器。
③请求,响应,数据库服务器等
服务器按开发阶段来划分:
1)远程服务器(外网服务器/正式服务器)
应用上线后使用的服务器,供全体用户使用,速度取决于服务器性能和用户网速 。2)本地服务器(内网服务器/测试服务器)
应用处于开发测试阶段使用的服务器,供开发人员和测试人员使用,属于局域网,速度快,开发效率高。
2.如何找到服务器获取所需资源
途径: URL(Uniform Resoure Locator:统一资源定位器)
基本URL: 协议、服务器名称(或IP地址)、路径和文件名,如“协议://IP地址/路径”
注意:不同的协议,代表着不同的资源查找方式,资源传输方式
基本通信过程:客户端发请求给服务器,服务器接收到请求之后响应
常见协议如下:
①http协议
概念:超文本传输协议,访问的是远程的网络资源,是网络开发中最常用的协议
格式:http://
优点: 1)简单快速(协议简单,服务器端程序规模小,通信速度快) 2)灵活(允许传输各种数据)
注意:1.1之前版本是非持续连接的
②file协议
概念:访问的是本地计算机上的资源 (可以省略主机地址)
格式:file://
③mailto协议
概念:访问的是电子邮件的地址
格式:mailto:
④FTP协议
概念:访问的是共享主机上的资源
格式:ftp://
注意:
- 我们使用的网络是在TCP/IP协议簇上运作的,而Http属于他内部的一个子集
- TCP协议簇中最重要的一点就是分层设计(OSI规定7层,实际只有4层)
这里我主要讲Http协议(请求网络资源)
1.Http通信大致可以分为两大步骤:
请求:客户端向服务器索要数据
响应:服务器返回客户端相应的数据
注意:
①发送请求的时候把请求头和请求体(请求体是非必须的)包装成一个请求对象
②服务器端对请求进行响应,在响应信息中包含响应头和响应体,响应信息是对服务器端的描述,
具体的信息放在响应体中传递给客户端
③网络请求返回的常见状态码
【200】:请求成功
【400】:客户端请求的语法错误,服务器无法解析
【404】:无法找到资源,客户端原因
【500】:服务器内部错误,无法完成请求
小结:状态码中4开头的是客户端原因;5开头的是服务器原因
④发送请求的URL如果包含中文,发送请求前需要进行转码处理(注意浏览器的URL内部已经做了转码处理)
iOS中如何对URL进行转码:stringByAddingPercentEscapesUsingEncoding (NSUTF8StringEncoding)
2 Http请求方法: get, post, put, head, delete, options, trace,connect,patch, copy, lock, mkcol, move等
常见GET和POST请求的对比 :
- GET请求参数直接跟在URL后面
- POST请求的参数放在请求体中
建议:【除简单数据查询使用GET外,其它的一律使用POST请求】
3.iOS中发送HTTP请求的方案
1)苹果原生
①NSURLConnection 03年推出的古老技术
②NSURLSession 13年推出iOS7之后,以取代NSURLConnection
③CFNetwork 底层技术、C语言的
2)第三方框架
①ASIHttpRequest (可惜已经停止更新)
②AFNetworking (主流)
③MKNetworkKit
4.iOS 中发送http网络请求方案实战(原生)
NSURLConnection
使用步骤:
①确定好请求方式 :GET/POST(前者创建的是不可变请求对象,后者创建可变请求对象)
②确定好同步请求还是异步请求(开发中常见为异步请求)
1.NSURLConnection发送GET请求
①发送同步请求
-(void)sendSyncNetwork{
//1 创建不可变请求对象
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login?username=520it&pwd=520&type=JSON"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//2 使用NSURLConnection发送同步请求
NSURLResponse *response = nil;
NSError *error = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response error:&error];
//3 解析服务器返回的数据
NSLog(@"%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);}
②发送异步请求(2种,block和代理)
1)block
-(void)sendAsyncNetwork{
//1 创建请求对象
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login?username=520it&pwd=520it&type=JSON"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//2发送异步请求
/* 第一个参数:请求对象
第二个参数:操作队列->(线程) 决定completionHandler回调在哪个线程中处理(主队列-主线程)
第三个参数:completionHandler回调 完成之后的回调
response:响应头信息
data:响应体信息
connectionError:错误信息
*/
[NSURLConnection sendAsynchronousRequest: request queue[[NSOperationQueue alloc]init]
completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
//3 解析服务器返回的数据
NSLog(@"%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);
}];
}
2)代理
-(void)sendAsyncNetworkDelegate
{
//1 创建请求对象
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login?username=520it&pwd=520it&type=JSON"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//2 设置代理 发送网络请求
//NSURLConnection *connect = [[NSURLConnection alloc]initWithRequest:request delegate:self];
//设置代理的第二种方法 startImmediately是否要马上发送请求
NSURLConnection *connect = [[NSURLConnection alloc]initWithRequest:request delegate:self startImmediately:NO];
//发送请求
[connect start];//此方式仅仅当选择的是不立即发送请求时,当后面需要发送请求时才需要使用。
}
#pragma mark NSURLConnectionDataDelegate
//01 接受到响应(一次)
-(void)connection:(NSURLConnection *)connection didReceiveResponse: (NSURLResponse *)response{
NSLog(@"didReceiveResponse");
}
//02 接收到服务器返回的数据(多次)
-(void)connection:(NSURLConnection *)connection
didReceiveData:(NSData *)data{
NSLog(@"didReceiveData--%zd",data.length);//最终数据长度
[self.resultData appendData:data];
}
//03 失败
-(void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error{
NSLog(@"didFailWithError");
}
//04 完成之后
-(void)connectionDidFinishLoading:(NSURLConnection *)connection{
NSLog(@"connectionDidFinishLoading");
//解析服务器返回的数据
NSLog(@"%@",[[NSString alloc]initWithData:self.resultData encoding:NSUTF8StringEncoding
]);
}
2.NSURLConnection发送POST请求 (这里只讨论主流:异步 + block )
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
//1创建可变请求对象(因为默认发送GET)
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; //+ 修改请求方法为POST
request.HTTPMethod = @"POST";
//+ 设置参数(请求体)
//username=520it&pwd=520it&type=JSON
request.HTTPBody = [@"username=520it&pwd=520it&type=JSON" dataUsingEncoding:NSUTF8StringEncoding];
//2 发送异步请求
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue alloc] init]
completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
//3 解析服务器返回的数据
NSLog(@"%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]); }];
}
3.NSURLConneection与Runloop补充
①设置代理的方法中,如果立即发送请求,那么内部会自动将connect作为一个source添加到runloop中
②设置代理的方法中,如果设置为NO不立即发送请求,那么内部不会添加connect到runloop中,需调用start方法才会添加,调用start那么start内部会把当前的connect添加到runloop并且设置模式为默认,子线程中运行循环不存在,那么会主动创建。
注意:如果在子线程中发送网络请求,则子线程的runloop需要手动创建并开启,才能处理任务。
NSURLSession
1.使用步骤:
①创建NSURLSession对象(单例对象 或 自定义Session对象【调用类方法设置代理】)
②创建task (NSURLSessiontask是个抽象类,必须使用它的子类)
②执行task (resume)
2.开发实战
方案一: 通过Block 回调响应体
-(void)getData
{
//1.创建会话对象
NSURLSession *session = [NSURLSession sharedSession];
//2.根据会话对象创建Task(请求任务)
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login?username=520it&pwd=520it&type=JSON"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//灵活多变,可变请求对象则可以为POST
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//注意:创建任务时还可以dataTaskWithURL ,则请求方式只能是GET
//解析服务器返回的数据
NSLog(@"%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);
NSLog(@"%@",[NSThread currentThread]);//默认completionHandler在子线程中执行
}];
//3.执行Task(发送请求)
[dataTask resume];
}
注意: 简便方式如下:
-(void)getData{
[[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/loginusername=520it&pwd=520it&type=JSON"]
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//解析数据
NSLog(@"%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]);
}] resume];
}
方案二:通过代理获得响应体
-(void)delaget{
//1.创建会话对象 设置代理
/*
第三个参数:队列->线程(代理方法在哪个线程中执行)|如果该参数传递nil 那么在子线程中执行(默认)
*/
NSURLSession *session = [NSURLSession sessionWithConfiguration:
[NSURLSessionConfiguration defaultSessionConfiguration] delegate: self
delegateQueue:nil];
//2.创建请求任务 (这里举例POST请求)
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
request.HTTPBody = [@"username=520it&pwd=520it&type=JSON" dataUsingEncoding:NSUTF8StringEncoding];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
//3.执行Task
[dataTask resume];
}
#pragma mark NSURLSessionDataDelegate
//01 接收到服务器响应的时候调用
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler{
NSLog(@"didReceiveResponse---%@",[NSThread currentThread]);//子线程
self.resultData = [NSMutableData data]; //tips
//需要告诉系统是否接收服务器返回的数据
completionHandler(NSURLSessionResponseAllow
);
}
//02 接受到服务器返回数据的时候调用 (可能被调用多次)
-(void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data{
//拼接服务器返回的数据
[self.resultData appendData:data];
}
//03 请求完成或者是失败的时候调用
-(void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error{
//解析服务器返回的数据
NSLog(@"%@",[[NSString alloc]initWithData:self.resultData encoding:NSUTF8StringEncoding]);
}
3.实战总结
①创建Task的方法中如果需要传入“请求对象”,那么请求方式的方式可以是GET/POST,具体取决于你。反之方法参数直接传URL,那么请求方式只能是GET(因为没有单独的请求体)
②如果是想利用block发送请求Task,则利用sharedSession创建单例会话对象;反之利用代理方法,则自定义会话对象
5.数据解析
1.须知: 服务器返回给客户端的数据一般都是JSON格式或者XML格式(文件下载除外)
2.数据格式介绍:
①JSON
1)JSON是一种轻量级的数据格式,一般用于数据交互
2)JSON的格式很像OC中的字典和数组
{"name" : "jack", "age" : 10}
{"names" : ["jack", "rose", "jim”]}
3)标准JSON格式的注意点:key必须用双引号
4)JSON 与OC 对照表
JSON OC
大括号 { } NSDictionary
中括号 [ ] NSArray
双引号 " " NSString
数字 10、10.8 NSNumber
在iOS中,JSON的常见解析方案有4种:
第三方框架:JSONKit、SBJson、TouchJSON(性能从左到右,越差)
苹果原生(自带):NSJSONSerialization(性能最好)
NSJSONSerialization的常见方法:
-
JSON数据 -> OC对象 (反序列化,得到OC的字典或数组 => 模型 故常用)
+(id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError **)error;
-
OC对象 -> JSON数据 (序列化)
+(NSData *)dataWithJSONObject:(id)obj options:(NSJSONWritingOptions)opt error:(NSError **)error;
②XML
1)全称是Extensible Markup Language,译作“可扩展标记语言”。跟JSON一样,也是常用的一种用于交互的数据格式
一般也叫XML文档(XML Document)
- 一个常见的XML文档一般由以下三部分组成
- 文档声明
- 元素(Element)
- 属性(Attribute
XML语法 – 文档声明
在XML文档的最前面,必须编写一个文档声明,用来声明XML文档的类型
最简单的声明:
用encoding属性说明文档的字符编码
XML语法 – 元素(Element)
①一个元素包括了开始标签和结束标签
拥有内容的元素:
没有内容的元素:
没有内容的元素简写:
②一个元素可以嵌套若干个子元素(不能出现交叉嵌套)
③规范的XML文档最多只有1个根元素,其他元素都是根元素的子孙元素
XML语法 –元素的注意: XML中的所有空格和换行,都会当做具体内容处理
故下面两个元素的内容是不一样的
第1个
第2个
XML语法 – 属性(Attribute)
①一个元素可以拥有多个属性
video元素拥有name和length两个属性
②属性值必须用 双引号"" 或者 单引号'' 括住
③实际上,属性表示的信息也可以用子元素来表示,比如
XML解析
1.须知:XML的解析方式有2种
- DOM:一次性将整个XML文档加载进内存,比较适合解析小文件
- SAX:从根元素开始,按顺序一个元素一个元素往下解析,比较适合解析大文件
2.iOS中的XML解析方式:
①苹果原生
NSXMLParser:SAX方式解析,使用简单,文件大小通吃
②第三方框架
libxml2:纯C语言,默认包含在iOS SDK中,同时支持DOM和SAX方式解析
GDataXML:DOM方式解析,由Google开发,基于libxml2
3.XML解析方式的选择建议
大文件:NSXMLParser、libxml2
小文件:GDataXML、NSXMLParser、libxml2
注意:相比之下,JSON的体积小于XML,所以服务器返回给移动端的数据格式以JSON居多
6.数据解析实战
1.复杂JSON解析
1.解析步骤:
①确定服务器返回给客户端的数据是数组还是字典亦或是其他:直接百度json在线格式化或者把JSON数据写plist文件再观察
②. 一般反序列化成OC对象(字典或数组)再面向模型开发
2.经验总结:
①反序列化(JSON -> OC对象)方法中,options参数不能乱填 (=> 确定响应体类型很重要!!!)
NSJSONReadingMutableContainers = (1UL << 0), 是可变的
NSJSONReadingMutableLeaves = (1UL << 1), 得到的字典中所有的string都是可变的
NSJSONReadingAllowFragments = (1UL << 2) 当最外层既不是字典也不是数组的时候必须使用该枚举
id obj = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
注意:kNilOptions 表示0 默认的意思
②并不是所有的对象都可以序列化
1)最外层的对象必须是 NSArray or NSDictionary里面所有的对象都只能是 NSString, NSNumber, NSArray, NSDictionary, or NSNull
2)所有字典的keys are NSStrings
3)NSNumbers 必须是合法并且不能是无穷大
故 保险起见,序列化之前会进行判断一下是否可以序列化
if(![NSJSONSerialization isValidJSONObject:array])
{
NSLog(@"该对象不支持序列化处理");
return;
}
③json文件中的数据为NSData,不能通过字典获取
//NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle]
pathForResource:@"test.json" ofType:nil]];
NSData *jsonData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle]
pathForResource:@"test.json" ofType:nil]];
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:nil];
④在将JSON数据写入文件的时候,一般会对JSON数据进行排版
//JSON -->OC对象 反序列化处理
id obj = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
NSLog(@"%@--%@",[obj class],obj);
//对json数据进行排版
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:obj options:NSJSONWritingPrettyPrinted error:nil];
//写json数据
[jsonData writeToFile:@"/Users/wuyuanping/Desktop/test.json" atomically:YES];
⑤面向模型开发会存在的问题【容易出错|重命名|关键字等】
服务器返回的数据很多,我们只要一部分 => 可能并不能直接使用KVC
服务器提供的属性名字是关键字 => 需要给属性重新命名
属性过多转换费时且无技术含量 => 一般会用框架字典转模型(或者运行时)
拓展:
1.字典转模型框架:
- Mantle:需要继承自MTModel
- JSONModel:需要继承自JSONModel
- MJExtension:不需要继承,无代码侵入性
- YYModel :高性能
2.选择和使用框架的注意点:
①侵入性
②易用性,是否容易上手
③可扩展性,很容易给这个框架增加新的功能
MJExtension使用举例:
//1.把字典数组转换为模型数组
self.videos = [YPVideo objectArrayWithKeyValuesArray:videoArray];
//2.重命名模型属性的名称
//第一种重命名属性名称的方法,有一定的代码侵入性)
//设置字典中的id被模型中的ID替换
+(NSDictionary *)replacedKeyFromPropertyName
{
return @{
@"ID":@"id"
};
}
//第二种重命名属性名称的方法,代码侵入性为零
[YPVideo setupReplacedKeyFromPropertyName:^NSDictionary *{
return @{
@"ID":@"id"
};
}];
//3.MJExtension框架内部实现原理-运行时
代码示例
1.JSON解析
- (void)viewDidLoad{
[super viewDidLoad];
[[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/video?type=JSON"]
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//输出结果表明:返回的是字典(元素为存放字典的数组)
// NSLog(@"%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
//解析服务器返回的数据 JSON-->OC对象(反序列化)
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
//把字典数组->模型数组
NSArray *arrayM = dict[@"videos"];
NSMutableArray *arrM = [NSMutableArray array];
for (NSDictionary *dict in arrayM) {
[arrM addObject:[YPVideo videoWithDict:dict]];
}
self.videos = arrM;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
//刷新UI,得跳到主线程中
[self.tableView reloadData];
}];
}] resume];
}
2.XML解析
解析步骤
①创建解析器
②设置代理,遵守代理协议,实现代理方法,在代理方法中解析元素
③开始解析
- (void)viewDidLoad{
[super viewDidLoad];
[[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"http://120.25.226.186:32812/video?type=XML"]
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//01 创建XML解析器
NSXMLParser *parser = [[NSXMLParser alloc]initWithData:data];
//02 设置代理
parser.delegate = self;
//03 开始解析 该方法本身是阻塞
[parser parse]; //跳到他的代理方法
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
//刷新UI
[self.tableView reloadData];
}];
}] resume];
}
#pragma mark NSXMLParserDelegate
//01 开始解析XML文档
-(void)parserDidStartDocument:(NSXMLParser *)parser{
NSLog(@"parserDidStartDocument");
}
//02 开始解析文档中某个元素的时候调用 该方法可能会被调用多次
-(void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict{
//过滤掉根元素,否则报错
if ([elementName isEqualToString:@"videos"]) { return; }
NSLog(@"didStartElement--%@--%@",elementName,attributeDict);
//检验参数代表什么
//每当得到一个字典 就应该转换为模型 并添加到全局的可变数组
[self.videos addObject:[YPVideo mj_objectWithKeyValues:attributeDict]];
}
//03 某个元素解析完毕的时候调用
-(void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName{
NSLog(@"didEndElement");
}
//04 整个XML文档解析完毕
-(void)parserDidEndDocument:(NSXMLParser *)parser{
NSLog(@"parserDidEndDocument");
}
使用GDataXML解析XML 步骤:
前提:
- 配置环境 :
① 先导入框架 #import "GDataXMLNode.h"
② 配置环境(GDataXML框架是MRC的,所以还需要告诉编译器以MRC的方式处理GDataXML的代码) - 步骤:
//1.加载XML文档(使用的是DOM的方式一口气把整个XML文档都吞下)
GDataXMLDocument *doc = [[GDataXMLDocument alloc]initWithData:data options:kNilOptions error:nil];
//2.获取XML文档的根元素,根据根元素取出XML中的每个子元素
NSArray * elements = [doc.rootElement elementsForName:@"video"];
//3. 取出每个子元素的属性并转换为模型
for (GDataXMLElement *ele in elements) {
YPVideo *video = [[YPVideo alloc]init];
video.name = [ele attributeForName:@"name"].stringValue;
video.length = [eleattributeForName:@"length"].stringValue.integerValue;
video.url = [ele attributeForName:@"url"].stringValue;
video.image = [ele attributeForName:@"image"].stringValue;
video.ID = [ele attributeForName:@"id"].stringValue;
//4. 把转换好的模型添加到tableView的数据源self.videos数组中
[self.videos addObject:video];
}
案例 - NSURLSession实现大文件下载
问题解决思路:
①文件下载内存飙升 => 直接将数据写入沙盒 (文件句柄 或者 输出流)
②断点下载 => 设置请求头 (Range)
③离线断点下载 => 代理方法(可以实时将下载数据存入沙盒)
注意:输出流创建文件路径是懒加载,故不论调用几次,只会创建一个文件夹,
对比使用文件句柄指针得要手动控制只创建一个文件夹,轻松不少。