开头
访问网络
实际用处
基础要点
- 访问网络,可以理解为A(客户端)通过一种方式(协议)到B(服务端)送(上传)/拿(下载)/告诉(传输)/听到(取回)消息的过程
- 而这个协议往往是http或者https,这个服务端则是一个网站,而后面的动作则是
post/get/put等方式
- 在实际访问过程中,需要进行的操作实际上只有,
确定地址(访问网站),确定路径(方式)和钥匙(访问体),取回消息,而具体显示内容这一工作,一般由浏览器自己去解析,或者另外进行数据处理
- 因为我对网络的理解没有特别深,所以这部分主要以,我在仿写项目中的案例,进行梳理,尽量能梳理清楚,覆盖能用到的功能
- OC中目前访问网络主要用到
NSURLSessionTask
- 下面的案例,是建立在本地上的
apache服务器,也可以用这网址测试httpbin
请求和响应
- 请求
request,包含请求行,请求头,请求体(POST方式有)。请求行包括请求方式,资源地址,协议版本;请求头包含申请格式、处理缓存的方式、显示的客户端型号等;请求体包含POST方式的参数
- 响应
response,包含响应行,响应头,响应体。响应行包含协议的版本,访问的状态结果;响应头包含格式键值对;响应体包含显示的正文
NSURLSessionTask
- NSURLSessionDataTask
get/post
- NSURLSessionUploadTask
put
- NSURLSessionDownloadTask 下载
GET
实际用处
- 获取网络资源、数据
- 搜索
- 效率高,但是能被浏览器缓存,不安全
基础用法
NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/demo.json"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
NSLog(@"连接错误 %@", error);
return;
}
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode == 200 || httpResponse.statusCode == 304) {
NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", str);
NSLog(@"%@", data);
}
else{
NSLog(@"服务器内部错误");
}
}];
[task resume];
NSString *name = @"zhang san"
NSString *pwd = @"123"
NSString *strUrl = [NSString stringWithFormat:@"http://127.0.0.1/php/login.php?username=%@&password=%@", name, pwd]
//当地址中出现空格或者汉子 url返回空。下面这个函数可以转义字符串中的空格
strUrl = [strUrl stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]
NSURL *url = [NSURL URLWithString:strUrl]
小结
- 这里也可以看出,
GET访问的方式其实就是将需要的参数显行的放在网址中,具体结构为网址?参数1=值&参数2=值....
- 也因为这种结构的原因的,
GET不适用于传输大量参数,或者传输隐私需求较高的参数
POST
实际用处
- 发送隐私数据,发送比较多的数据
- 不会被浏览器缓存
- 效率低,但是相对安全
基础用法
NSString *strUrl = @"http://127.0.0.1/php/login.php";
NSURL *url = [NSURL URLWithString:strUrl];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"post";
NSString *body = @"username=zhang san&password=123";
request.HTTPBody = [body dataUsingEncoding:NSUTF8StringEncoding];
NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
NSLog(@"连接错误 %@", error);
return;
}
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode == 200 || httpResponse.statusCode == 304) {
NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", str);
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
NSLog(@"%@", dic[@"userName"]);
}
else{
NSLog(@"服务器内部错误");
}
}];
[task resume];
自定义请求体上传文件
- 主要是为了整理一下自定义请求体
- 谷歌浏览器抓不到请求体,我用的safari
- 请求体内容
------WebKitFormBoundaryOMAsfP2G4gU9nai3
Content-Disposition: form-data; name="userfile"; filename="99.png"
Content-Type: image/png
文件二进制数据
------WebKitFormBoundaryOMAsfP2G4gU9nai3--
- 对应的请求头中的内容。
Content-Type是表单类型,由html中设置,----WebKitFormBoundaryUmfoTOZhwXatboQB是浏览器自己生成的分割符
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryUmfoTOZhwXatboQB
- (NSData *)makeBody:(NSString *)fieldName andfilePath:(NSString *)filePath{
NSMutableData *mData = [NSMutableData data];
NSMutableString *mString = [NSMutableString new];
[mString appendFormat:@"--%@\r\n", kBOUNDARY];
[mString appendFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", fieldName, [filePath lastPathComponent]];
[mString appendString:@"Content-Type: application/octet-stream\r\n"];
[mString appendString:@"\r\n"];
[mData appendData:[mString dataUsingEncoding:NSUTF8StringEncoding]];
NSData *data = [NSData dataWithContentsOfFile:filePath];
[mData appendData:data];
NSString *end = [NSString stringWithFormat:@"\r\n--%@--",kBOUNDARY];
[mData appendData:[end dataUsingEncoding:NSUTF8StringEncoding]];
return mData.copy;
}
request.HTTPBody = [self makeBody:fieldName andfilePath:filePath]
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", kBOUNDARY] forHTTPHeaderField:@"Content-Type"];
小结
- POST方式其实与GET相差无几,只不过是传参数的方式不同罢了
- 在实际开发中,在面对隐私数据传输的时候,大部分解决方法是通过编码方式,进行数据保护
- 在AFN中参数可以通过参数字典的方式进行传输,也比较方便
- 自定义请求体本质上就是通过拼接字符串,来组成指定格式的数据,在实际开发中,根据前端需要的数据格式,进行对应的拼接和内容填充即可
PUT
基础用法
NSString *path = [[NSBundle mainBundle] pathForResource:@"06.jpg" ofType:nil];
NSURL *url = [NSURL URLWithString:@"https://httpbin.org/put"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"put";
NSData *data = [NSData dataWithContentsOfFile:path];
NSLog(@"%@", data);
NSURLSessionTask *task = [[NSURLSession sharedSession] uploadTaskWithRequest:request fromData:data completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
NSLog(@"连接错误 %@", error);
return;
}
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode == 200 || httpResponse.statusCode == 304) {
NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@", str);
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
NSLog(@"%@", dic[@"headers"]);
}
else{
NSLog(@"服务器内部错误");
}
}];
[task resume];
NSURL *fileUrl = [[NSBundle mainBundle] URLForResource:@"06.jpg" withExtension:nil]
put权限
- 在实际使用时,put需要设置请求头中的
Authorization,但是现在mac好像配不了上传的webDav服务器,就只能用httpbin测试一下功能行不行,具体要用的时候,设置请求头就好
[request setValue:@"Basic YWRtaW46MTIzNDU2" forHTTPHeaderField:@"Authorization"];
下载
基础用法
NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/itcast/images/head1.png"]
NSURLSessionDownloadTask *downloadTask = [[NSURLSession sharedSession] downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%@", location)
}]
[downloadTask resume]
- 但是,这有一个问题。由于
NSURLSessionTask的特性,下载的数据会保存在tmp的目录下,下载完毕后就会被删除,所以需要另外保存
NSString *saveLocation = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"head1.png"];
[[NSFileManager defaultManager] copyItemAtPath:location.path toPath:saveLocation error:NULL];
断点续传
- 断点续传的含义就是,当一个下载开始,在任何时段,都可以进行任意的暂停和继续,并且保存先前下载好的数据
- 那么,就需要用到代理,和全局的任务对象,进行统一操作。另外,基本上所有的对象,都是全局对象
- 控件是通过
storyboard添加的
- 对象设置,对象的设置结合下面可以更理解
@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, strong) NSData *resumeData;
@property (nonatomic, strong) NSMutableDictionary *downloadCache;
@property (nonatomic, copy) NSString *urlStr;
@property (nonatomic, copy) NSString *saveLaction;
@property (nonatomic, copy) NSString *fileName;
@property (nonatomic, copy) NSString *tmpPath;
@property (nonatomic, strong) NSFileManager *fileManager;
- 懒加载,根据下载任务的不同,会话,下载缓存池,文件管理器实际上是不会变的,所以使用懒加载生成
- (NSURLSession *)session{
if (!_session) {
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]
_session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]]
}
return _session
}
- (NSMutableDictionary *)downloadCache{
if (!_downloadCache) {
_downloadCache = [NSMutableDictionary dictionaryWithCapacity:10]
}
return _downloadCache
}
- (NSFileManager *)fileManager{
if (!_fileManager) {
_fileManager = [NSFileManager defaultManager]
}
return _fileManager
}
- 设置接口内容,这里就以单个网站做事例,如果要在一个页面控制多个内容的下载,实质上就通过按钮的值来赋值即可,通过数组或者字典来保存
- (void)viewDidLoad {
[super viewDidLoad];
NSString *urlStr = @"http://127.0.0.1/3.zip";
self.fileName = [urlStr lastPathComponent];
self.urlStr = urlStr;
NSString *tmpStr = [NSString stringWithFormat:@"%@.tmp", [self.fileName stringByDeletingPathExtension]];
self.tmpPath = [NSTemporaryDirectory() stringByAppendingPathComponent:tmpStr];
self.saveLaction = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:self.fileName];
}
- 代理方法,需要注意的是,使用代理,就不要使用回调函数,底层优先使用回调。
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location{
NSLog(@"%@", location);
[self.fileManager copyItemAtPath:location.path toPath:self.saveLaction error:NULL];
NSLog(@"%@", self.saveLaction);
[self.fileManager removeItemAtPath:self.tmpPath error:nil];
[self.downloadCache removeObjectForKey:self.urlStr];
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
float process = totalBytesWritten * 1.0 / totalBytesExpectedToWrite;
self.processView.progress = process;
NSLog(@"%f", process);
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes{
float offset = fileOffset * 1.0 / expectedTotalBytes;
NSLog(@"续传 %f", offset);
}
- 准备了下载任务,抓到了下载过程的状态,就可以进行开始、暂停、下载的处理。那么,就要明确一下,什么情况下可以开始,什么情况下可以暂停,什么情况下可以继续
- 开始,没有同名下载;保存目录下没有当前文件;缓存的目录下没有缓存数据
- 暂停,有任务
- 继续,已有保存好的数据;当前没有任务
- (void)download{
NSURL *url = [NSURL URLWithString:self.urlStr];
NSURLSessionDownloadTask *downloadTask = [self.session downloadTaskWithURL:url];
self.downloadCache[self.urlStr] = downloadTask;
[downloadTask resume];
}
- (IBAction)startClick:(id)sender {
if (self.downloadCache[self.urlStr]) {
NSLog(@"已经在下载了");
return;
}
if ([self.fileManager fileExistsAtPath:self.saveLaction] || [self.fileManager fileExistsAtPath:self.tmpPath]) {
NSLog(@"文件已经存在");
return;
}
[self download];
}
- (IBAction)pauseClick:(id)sender {
if (!self.downloadCache[self.urlStr]) {
NSLog(@"没有任务");
return;
}
[self.downloadCache[self.urlStr] cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
self.resumeData = resumeData;
[self.resumeData writeToFile:self.tmpPath atomically:YES];
NSLog(@"已经保存数据");
self.downloadCache[self.urlStr] = nil;
}];
}
- (IBAction)resumeClick:(id)sender {
if (self.downloadCache[self.urlStr]) {
NSLog(@"已经有下载任务了");
return;
}
if ([self.fileManager fileExistsAtPath:self.tmpPath]) {
self.resumeData = [NSData dataWithContentsOfFile:self.tmpPath];
NSLog(@"本地有暂停的数据");
}
if (self.resumeData == nil) {
NSLog(@"本地没有暂停的数据");
return;
}
self.downloadCache[self.urlStr] = [self.session downloadTaskWithResumeData:self.resumeData];
[self.downloadCache[self.urlStr] resume];
self.resumeData = nil;
}
断点续传要点总结
- 对于字典来说,无论是
remove还是直接设置为nil,还是在没有设置这个键的时候,都不会影响判断。一句话,只要这个键没有对应的值,那么就是nil,而字典不会管这个字典里有没有这个键
tmp文件,其实只是个plist,保存缓存文件的相关信息,并不直接保存缓存数据,系统是通过这个文件,寻找缓存数据在进行加载的,有机会了解底层的处理,这里挖个坑
网络数据解析
JSON
- 具有自我描述性、更易理解
- 本质上是一个字典,这个字典中可以放字符串,数据,字典
- 字典JSON实例
{
"word" : "今天天气真好",
"name" : "Tommy",
"array" : ["Time is short", "Always be humble", "how are u"],
"dic" : {
"age" : 18,
"address" : "China"
"adult" : true
}
}
NSString *path = [[NSBundle mainBundle] pathForResource:@"test.json" ofType:nil]
NSData *data = [NSData dataWithContentsOfFile:path]
id json = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL]
- 这里会有一个问题,直接
NSLog只会输出中文的编码,所以要冲重写description
- 下面的例子写在
NSDictionary+JSON.h的扩展中
- (NSString *)description
{
NSMutableString *mStr = [NSMutableString string];
[mStr appendFormat:@"(\r\n"];
[self enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
[mStr appendFormat:@"\t%@ : %@,\r\n", key, obj];
}];
[mStr appendString:@")"];
return mStr.copy;
}
- 并不是所有的值需要在App上显示
- 如果字典中仍包含字典,则需要用两层模型来解析
- (instancetype)initWithDict:(NSDictionary *)dict
{
if (self = [super init]) {
[self setValuesForKeysWithDictionary:dict];
}
return self;
}
+(instancetype)testModelWithDict:(NSDictionary *)dict
{
return [[self alloc] initWithDict:dict];
}
- (void)setValue:(id)value forKey:(NSString *)key {
if ([key isEqualToString:@"dic"]) {
self.dic = [TestDicModel testDicModel:value];
return;
}
[super setValue:value forKey:key];
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"%@ %@", key, value);
}
- (NSString *)description
{
return [NSString stringWithFormat:@"%@\r\n{\r\n\tname:%@,\r\n \tarray:%@,\r\n\tdic:%@,\r\n\tword:%@\r\n}", [super description], self.name, self.array, self.dic, self.word];
}
XML
- 可扩展标记语言
- 类似HTML,目的是存储和传输数据
- 解析步骤,寻找过程中在234步中循环
1. 加载xml数据
2. 找开始节点
3. 找节点之间的内容
4. 找结束节点
5. 直到最后一个节点,解析结束
- XML实例,将下边的
XML转换成videos模型数组
<?xml version="1.0" encoding="utf-8"?>
<videos>
<video videoId="1">
<name>01.C语言-语法预览</name>
<length>320</length>
<videoURL>/itcast/videos/01.C语言-语法预览.mp4</videoURL>
<imageURL>/itcast/images/head1.png</imageURL>
<desc>C语言-语法预览</desc>
<teacher>李雷</teacher>
</video>
<video videoId="2">
<name>02.C语言-第一个C程序</name>
<length>2708</length>
<videoURL>/itcast/videos/02.C语言-第一个C程序.mp4</videoURL>
<imageURL>/itcast/images/head2.png</imageURL>
<desc>C语言-第一个C程序</desc>
<teacher>李雷</teacher>
</video>
<video videoId="3">
<name>03.C语言-函数</name>
<length>822</length>
<videoURL>/itcast/videos/03.C语言-函数.mp4</videoURL>
<imageURL>/itcast/images/head3.png</imageURL>
<desc>C语言-函数</desc>
<teacher>韩梅梅</teacher>
</video>
</videos>
- 上面只是为了更好的展示
XML的结构,在实际解析中,XML是不允许随意增加换行和空格,否则在下面的解析中会导致,内容错误
<?xml version="1.0" encoding="utf-8"?>
<videos><video videoId="1"><name>01.C语言-语法预览</name><length>320</length><videoURL>/itcast/videos/01.C语言-语法预览.mp4</videoURL><imageURL>/itcast/images/head1.png</imageURL><desc>C语言-语法预览</desc><teacher>李雷</teacher></video><video videoId="2"><name>02.C语言-第一个C程序</name><length>2708</length><videoURL>/itcast/videos/02.C语言-第一个C程序.mp4</videoURL><imageURL>/itcast/images/head2.png</imageURL><desc>C语言-第一个C程序</desc><teacher>李雷</teacher></video><video videoId="3"><name>03.C语言-函数</name><length>822</length><videoURL>/itcast/videos/03.C语言-函数.mp4</videoURL><imageURL>/itcast/images/head3.png</imageURL><desc>C语言-函数</desc><teacher>韩梅梅</teacher></video></videos>
- 开始解析,利用
NSXMLParser类的代理NSXMLParserDelegate方法进行
NSString *path = [[NSBundle mainBundle] pathForResource:@"demo.xml" ofType:nil]
NSData *data = [NSData dataWithContentsOfFile:path]
//解析数据
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data]
//设置代理
parser.delegate = self
//开始执行代理的方法,代理的方法中开始解析
[parser parse]
KVC的赋值方式,本质上是将模型对象属性的地址指向数据中值的地址,进行的是浅拷贝,所以要将对象中的属性定义为copy进行深拷贝。特别的,当要将NSString通过浅拷贝赋值给NSNumber时,会出错,是因为错误的指向了类,所以要通过copy将内容拷贝一份赋值
- 在找节点内容的时候,一般会分段找,所以要通过可变字符串进行拼接,并且在找到结束节点的时候,通过
KVC赋值,并且清零,保证过程中正常使用
- (void)parserDidStartDocument:(NSXMLParser *)parser{
NSLog(@"1 开始解析文档 %@", [NSThread currentThread]);
}
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary<NSString *,NSString *> *)attributeDict{
NSLog(@"2 开始找节点 %@-----%@", elementName, attributeDict);
if ([elementName isEqualToString:@"video"]) {
self.currentVideo = [Video new];
self.currentVideo.videoId = @([attributeDict[@"videoId"] intValue]);
[self.videos addObject:self.currentVideo];
}
}
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
NSLog(@"3 找节点之间的内容 %@", string);
[self.mString appendString:string];
}
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName{
NSLog(@"4 找结束节点 %@", elementName);
if (![elementName isEqualToString:@"video"] && ![elementName isEqualToString:@"videos"]) {
[self.currentVideo setValue:self.mString forKey:elementName];
}
[self.mString setString:@""];
}
- (void)parserDidEndDocument:(NSXMLParser *)parser{
NSLog(@"5 结束解析文档");
NSLog(@"%@", self.videos);
}
- (void)parser:(NSXMLParser *)parser parseErrorOccurred:(NSError *)parseError{
NSLog(@"出错");
}
@property (nonatomic, strong) NSMutableArray *videos;
@property (nonatomic, strong) Video *currentVideo;
@property (nonatomic, copy) NSMutableString *mString;
- (NSMutableArray *)videos{
if (!_videos) {
_videos = [NSMutableArray arrayWithCapacity:10];
}
return _videos;
}
- (NSMutableString *)mString{
if (!_mString) {
_mString = [NSMutableString new];
}
return _mString;
}
@property (nonatomic, copy) NSNumber *videoId;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSNumber *length;
@property (nonatomic, copy) NSString *videoURL;
@property (nonatomic, copy) NSString *imageURL;
@property (nonatomic, copy) NSString *desc;
@property (nonatomic, copy) NSString *teacher;
NSData *data = [NSData dataWithContentsOfFile: [[NSBundle mainBundle] pathForResource:@"demo.xml" ofType:nil]]
GDataXMLDocument *document = [[GDataXMLDocument alloc] initWithData:data error:NULL]
//获取xml根节点
GDataXMLElement *rootElement = document.rootElement
//获取所有子节点 遍历
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:10]
for (GDataXMLElement *element in rootElement.children) {
Video *v = [Video new]
[mArray addObject:v]
//给对象的属性赋值
//1.遍历video的子标签
for (GDataXMLElement *subElement in element.children) {
//属性赋值
[v setValue:subElement.stringValue forKey:subElement.name]
}
//2.遍历video的所有属性
NSLog(@"%@", element.attributes)
for (GDataXMLNode *attr in element.attributes) {
[v setValue:attr.stringValue forKey:attr.name]
}
}
总结
- 无论是
JSON还是XML,都只是一种存储数据的模型,说到底,还是键值对的形式,而因为是键值对的形式,就可以进行转模型,通过一个模型对象进行管理,核心处理思路也是围绕数组转模型进行的