iOS开发笔记 - 网络篇
来源:互联网 发布:mac队tac队zat队mat队 编辑:程序博客网 时间:2024/05/21 10:10
计算机网络基础
计算机网络是多台独立自主的计算机互联而成的系统的总称,最初建立计算机网络的目的是实现信息传递和资源共享。
如果说计算机是第二次世界大战的产物,那么计算机网络则是美苏冷战的产物。20世纪60年代初期,美国国防部领导的ARPA提出研究一种崭新的、能够适应现代战争的、生存性很强的通信系统并藉此来应对苏联核攻击的威胁,这个决定促使了分组交换网的诞生,也奠定今天计算机网络的原型,这是计算机网络发展史上第一个里程碑式的事件。
第二个里程碑式的事件是20世纪80年代初,国际标准化组织(ISO)提出了OSI/RM(开放系统互联参考模型),该模型定义了计算机网络的分层体系结构,虽然该模型并没有成为网络设备制造商遵循的国际标准,但用分层的思想解决复杂系统设计问题的做法已经深入人心。成为事实标准(de facto standard)的是TCP/IP模型,而TCP/IP协议簇(协议簇通常指彼此相关联的一系列协议的总称)也是构成今天的Internet的基石。不同于OSI/RM的七层结构,TCP/IP模型是一个四层模型,从上到下依次是应用层、传输层、网络层、物理链路层。值得一提的是,传输层可以使用两种不同的协议,一个是面向连接的传输控制协议(TCP),另一个是无连接的用户数据报协议(UDP),我们耳熟能详的的协议如HTTP、FTP、Telnet、POP3、DHCP、DNS、ICQ等都属于应用层的协议,它们要么构建在TCP之上,要么构建在UDP之上。
计算机网络发展史上第三个里程碑事件应该是浏览器的问世。20世纪90年代初,英国人Timothy John Berners-Lee发明了浏览器,浏览器通过超文本传输协议(HTTP)跟服务器交换超文本数据,通过图形用户界面显示从服务器获得的超文本数据,这一切都让使用Internet变得无比简单,于是计算机网络的用户数量开始爆炸式的增长。
基于HTTP协议联网
在iOS开发中,如果应用程序需要的数据不在本地,而是通过网络获取的文字、图片、音视频等资源,那么我们的应用程序就需要联网,对于这种场景通常可以直接使用HTTP(Hyper-Text Transfer Protocol)向提供资源的服务器发出请求即可。HTTP协议对于很多人来说都不陌生,我们使用浏览器访问Web服务器的时候使用的基本上都是使用HTTP协议(有些服务器需要使用HTTPS,它是在HTTP下层添加SSL[Secure Socket Layer],用于安全的传输HTTP协议数据)。目前越来越多的应用已经从浏览器延伸到移动客户端,但是服务器端并不需要做出任何改变,iOS和Android的应用程序也可以通过HTTP协议和服务器通信。
我们先来解释一下什么是协议以及HTTP到底是一个怎样的协议。我们将任何可发送或接收信息的硬件或程序称之为实体,而协议则是控制两个对等实体进行通信的规则的集合。简单的说,协议就是通信双方必须遵循的对话的标准和规范。HTTP是构建在TCP之上的协议,之所以选择TCP作为底层传输协议是因为TCP除了可以保证可靠通信之外,还具备流量控制和拥塞控制的能力,如果这一点不能理解也不要紧,我么只需要知道HTTP需要可靠的传输层协议的支持就够了。
HTTP有两种类型的报文:请求报文和响应报文。请求报文和响应报文都是由三个部分组成的。我们可以用抓包工具截取请求和响应报文来看看它们的结构。
请求报文是由请求行、请求头和消息体构成的。请求行包含了命令(通常是GET或POST)、资源和协议版本;请求头是键值对映射形式的和请求相关的信息,如客户端使用的语言、使用的浏览器等信息;消息体是客户端发给服务器的数据;在请求头和消息体之间有一个空行。
响应报文是由响应行、响应头和消息体构成的。响应行包含了协议版本和状态码;响应头是键值对形式的和响应相关的信息,如服务器的软件版本、时间日期、缓存策略、响应内容类型等信息;消息体是服务器发给客户端的数据;在响应头和消息体之间有一个空行。
抓包工具
- Charles
Charles是一个HTTP代理服务器,HTTP监视器,反转代理服务器,它允许一个开发者查看所有连接互联网的HTTP通信。很多iOS开发者都选择Charles作为抓包工具来获取和测试网络接口。通过下图所示的菜单项可以将Charles设置为Mac系统的HTTP代理,所有的HTTP数据都会被Charles截获。
当然,还可以将Charles设置为手机的代理,只要让安装了Charles的Mac系统和手机使用相同的网络,再将手机无线局域网的代理服务器设置为Mac系统的IP地址即可,这样手机上的HTTP数据也会被截获。
Wireshark
Wireshark(原名Ethereal,1998年由美国Gerald Combs首创研发,由世界各国100多位网络专家和软件人员共同参与此软件的升级完善和维护,2006年5月更名为Wireshark)是一个非常专业的网络数据包截取和分析软件,它直接截获经过网卡的数据,并尽可能显示出最为详细的数据包信息,是协议分析的利器。Wireshark比Charles更底层更专业,但是如果只做HTTP数据分析,Charles用起来还是非常简单方便的。
相关API
NSURL
NSURL是代表统一资源定位符(Universal Resource Locator,URL)的类。URL是互联网上标准资源的地址,互联网上的每个资源都有一个唯一的与之对应的URL。
URL的格式如下所示:
协议://域名或IP地址:端口号/路径/资源
下面是百度logo的URL:
http://www.baidu.com:80/img/bd_logo1.png
说明:端口号是对IP地址的扩展。例如我们的服务器只有一个IP地址,但是我们可以在这台服务器上开设多个服务,如Web服务、邮件服务和数据库服务,当服务器收到一个请求时会根据端口号来区分到底请求的是Web服务还是邮件服务,或者是数据库服务。我们在浏览器中输入URL的时候通常都会省略端口号,因为HTTP协议默认使用80端口,也就是说除非你访问的Web服务器没有使用80端口,你才需要输入相应的端口号。
下面的代码演示了如何在iOS应用中通过URL获取网络数据。
Objective-C代码:
#import "ViewController.h"#define CENTER_X CGRectGetWidth(self.view.bounds) / 2#define CENTER_Y CGRectGetHeight(self.view.bounds) / 2@interface ViewController ()@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; UIImageView *imageView = [[UIImageView alloc] initWithFrame: CGRectMake(0, 0, 320, 160)]; imageView.center = CGPointMake(CENTER_X, CENTER_Y); [self.view addSubview:imageView]; NSURL *url = [NSURL URLWithString:@"http://www.baidu.com/img/bd_logo1.png"]; NSData *data = [NSData dataWithContentsOfURL:url]; imageView.image = [UIImage imageWithData:data];}@end
Swift代码:
import UIKitclass ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let imageView = UIImageView(frame: CGRectMake(0, 0, 320, 160)) imageView.center = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2) self.view.addSubview(imageView) guard let url = NSURL(string: "http://www.baidu.com/img/bd_logo1.png") else { return } guard let data = NSData(contentsOfURL: url) else { return } imageView.image = UIImage(data: data) }}
提示:iOS 9出于安全方面的考虑,不允许使用非安全的HTTP协议联网,如果要用需要修改项目的Info.plist文件,添加“App Transport Security Settings”键,其类型是Dictionary;在“App Transport Security Settings”下添加一个子元素,键是“Allow Arbitrary Loads”,类型是Boolean,将其值设置为YES。
NSURLRequest / NSMutableURLRequest
NSURLRequest / NSMutableURLRequest代表了客户端向服务器发送的HTTP请求。通过请求对象可以设置请求的方法、请求头、缓存策略、超时时间、消息体等。
NSURLResponse
NSURLResponse代表了服务器发送给客户端的HTTP响应。
NSURLConnection
在iOS 7以前,基于HTTP协议联网的操作最终都要由NSURLConnection类来完成,该类主要有两个方法,一个用于发送同步请求,一个用于发送异步请求。
// 发送同步请求的方法+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error;// 发送异步请求的方法+ (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse *response, NSData *data, NSError *connectionError))handler
提示:同步请求是阻塞式请求,这就意味着同步请求的方法在返回数据之前会一直阻塞;异步请求是非阻塞式请求,当服务器返回数据时可以回调的方式对数据进行处理。如果明白这一点,就很容易理解为什么上面的同步请求方法会返回NSData指针,而异步请求方法没有返回值但有一个Block类型的参数(Block最适合用来书写回调代码)。
NSURLSession
2013的WWDC,苹果推出了NSURLConnection的继任者NSURLSession。与NSURLConnection相比,NSURLsession最直接的改进就是可以配置每个会话(session)的缓存、协议、cookie以及证书策略(credential policy)等,而且你可以跨程序共享这些信息。每个NSURLSession对象都由一个NSURLSessionConfiguration对象来进行初始化,NSURLSessionConfiguration对象代表了会话的配置以及一些用来增强移动设备上性能的新选项。
可以通过NSURLSession创建NSURLSessionTask(会话任务),会话任务有三个子类对应不同的场景,分别是:NSURLSessionDataTask(获取数据的任务)、NSURLSessionDownloadTask(下载任务)和NSURLSessionUploadTask(上传任务),我们通过HTTP协议可以完成的操作都属于这三类任务之一。NSURLSessionTask主要有三个方法,分别是:resume(恢复任务)、suspend(挂起任务)和cancel(取消任务)。
NSURLSessionConfiguration
如前面所述,NSURLSessionConfiguration代表了会话的配置,该类的三个创建对象的类方法很好的诠释了NSURLSession类设计时所考虑的不同的使用场景。
// 返回一个标准的配置,标准配置会使用默认的缓存策略、超时时间等+ (NSURLSessionConfiguration *)defaultSessionConfiguration;// 返回一个临时性的配置,这个配置中不会对缓存,Cookie和证书进行持久化存储// 对于实现无痕浏览这种功能来说这种配置是非常理想的+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration;// 返回一个后台配置// 后台会话不同于普通的会话,它甚至可以在应用程序挂起,退出或者崩溃的情况下运行上传和下载任务// 初始化时指定的标识符,被用于向任何可能在进程外恢复后台传输的守护进程(daemon)提供上下文+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier: (NSString *)identifier
数据解析
通过HTTP从服务器获得的数据通常都是JSON格式或XML格式的,下面对这两种数据格式做一个简单的介绍。
XML
XML全称可扩展标记语言(eXtensible Markup Language),被设计用来传输和存储数据。在JSON被广泛使用之前,XML是异构系统之间交换数据的事实标准,它是一种具有自我描述能力的传输数据的标记语言,如下所示。
<?xml version="1.0" encoding="ISO-8859-1"?><note> <to>Tove</to> <from>Jani</from> <heading>Reminder</heading> <body>Don't forget me this weekend!</body></note>
XML文档形成一种树结构,它必须包含根元素。该元素是所有其他元素的父元素。这棵树从根部开始,并扩展到树的最底端,如下图所示。
<bookstore> <book category="COOKING"> <title lang="en">Everyday Italian</title> <author>Giada De Laurentiis</author> <year>2005</year> <price>30.00</price> </book> <book category="CHILDREN"> <title lang="en">Harry Potter</title> <author>J K. Rowling</author> <year>2005</year> <price>29.99</price> </book> <book category="WEB"> <title lang="en">Learning XML</title> <author>Erik T. Ray</author> <year>2003</year> <price>39.95</price> </book></bookstore>
XML的语法规则跟其他标签语言(如HTML)基本一致,不过需要注意以下几条:
1. 所有的XML元素都必须有一个关闭标签2. XML标签对大小写敏感3. XML必须正确嵌套4. XML文档必须有根元素5. XML属性值必须加引号6. XML中的特殊字符要使用实体引用7. XML中的注释是<!-- -->
在XML文档中查找信息可以使用XPath表达式,我们来看一个例子。
<?xml version="1.0" encoding="ISO-8859-1"?><bookstore> <book> <title lang="eng">Harry Potter</title> <price>29.99</price> </book> <book> <title lang="eng">Learning XML</title> <price>39.95</price> </book></bookstore>
XPath语法表
XPath的例子
提示:如果对上面很多概念不理解或者想对XML有一个更全面的了解,建议访问RUNOOB.COM获得更多的信息。
解析XML数据主要有两种方式:SAX和DOM。SAX解析属于事件驱动型的顺序解析,即从上至下解析XML文件,遇到标记、属性、注释、内容等都会引发事件回调,苹果原生的NSXMLParser就属于这种类型的解析,其优点在于速度快,内存占用少,但是操作比较复杂。DOM是文档对象模型的缩写,顾名思义就是将整个XML文档视为一个对象,DOM解析的原理是先根据XML文档的内容在内存中建立树结构,再对树结构进行解析,这种方式显然需要更多的内存,但操作简单且对XPath查询提供了很好的支持。 第三方库基本上都是用DOM解析,常用的有:GDataXML,KissXML,RaptureXML和XMLDictionary。
下面的代码演示了如何使用KissXML解析开源中国编号为44393的文章的相关链接。
Objective-C代码:
#import "ViewController.h"#import "CDDetailViewController.h"#import "CDRelativeNews.h"#import "DDXML.h"@interface ViewController () <UITableViewDataSource, UITableViewDelegate>@end@implementation ViewController { UITableView *myTableView; // iOS 9开始支持泛型容器(有类型限定的数组、字典等) // 可以在Xcode 7中使用这项新的语言特性 NSMutableArray<CDRelativeNews *> *dataArray;}- (void)viewDidLoad { [super viewDidLoad]; self.title = @"相关新闻链接"; myTableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain]; myTableView.dataSource = self; myTableView.delegate = self; [self.view addSubview:myTableView]; [self loadDataModel];}- (void)loadDataModel { if (!dataArray) { dataArray = [NSMutableArray array]; } // 创建统一资源定位符对象 NSURL *url = [NSURL URLWithString: @"http://www.oschina.net/action/api/news_detail?id=44393"]; // 通过统一资源定位符从服务器获得XML数据 NSData *data = [NSData dataWithContentsOfURL:url]; // 使用NSData对象创建XML文档对象 文档对象是将XML在内存中组织成一棵树 DDXMLDocument *doc = [[DDXMLDocument alloc] initWithData:data options:0 error:nil]; // 使用XPath语法从文档对象模型中查找指定节点 NSArray *array = [doc nodesForXPath:@"//relative" error:nil]; // 循环取出节点并对节点下的子节点进行进一步解析 for (DDXMLNode *node in array) { CDRelativeNews *model = [[CDRelativeNews alloc] init]; // 取出当前节点的子节点并获取其对应的值 model.title = [node.children[0] stringValue]; model.url = [node.children[1] stringValue]; // 将模型对象添加到数组中 [dataArray addObject:model]; } // 刷新表格视图 [myTableView reloadData];}- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return dataArray.count;}- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CELL"]; if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"CELL"]; } cell.textLabel.text = dataArray[indexPath.row].title; return cell;}- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { CDRelativeNews *model = dataArray[indexPath.row]; CDDetailViewController *detailVC = [[CDDetailViewController alloc] init]; detailVC.urlStr = model.url; [self.navigationController pushViewController:detailVC animated:YES];}@end
用这个例子顺便介绍一下如何在Swift中使用Objective-C实现两种语言的混编。首先还是向项目中添加KissXML第三方库,这个第三方库用是Objective-C书写的。在下面的例子中,我们创建了一个名为“bridge.h”的头文件,并在项目的“Build Settings”中找到“Objective-C Bridging Header”选项,将“bridge.h”头文件的路径添到此处。
#ifndef bridge_h#define bridge_h#import "DDXML.h"#endif /* bridge_h */
import UIKitclass ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { var myTableView: UITableView? var dataArray = [RelativeNews]() override func viewDidLoad() { super.viewDidLoad() self.title = "相关新闻链接" myTableView = UITableView(frame: self.view.bounds, style: .Plain) myTableView!.dataSource = self myTableView!.delegate = self self.view.addSubview(myTableView!) self.loadDataModel() } func loadDataModel() { guard let url = NSURL(string: "http://www.oschina.net/action/api/news_detail?id=44393") else { return } guard let data = NSData(contentsOfURL: url) else { return } do { // 用通过URL获取的XML数据构造文档对象模型 // 然后使用XPath语法全文查找relative节点 for node in try DDXMLDocument(data: data, options: 0) .nodesForXPath("//relative") { // 将数组中的元素类型转换为DDXMLNode if let relative = node as? DDXMLNode { // 用children方法取DDXMLNode对象的子节点的数组 if let children = relative.children() as? [DDXMLNode] { let model = RelativeNews() model.title = children[0].stringValue() model.url = children[1].stringValue() dataArray.append(model) } } } myTableView!.reloadData() } catch { print("Error occured while handling XML") } } func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return dataArray.count } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { var cell = tableView.dequeueReusableCellWithIdentifier("CELL") if cell == nil { cell = UITableViewCell(style: .Default, reuseIdentifier: "CELL") } let model = dataArray[indexPath.row] cell?.textLabel?.text = model.title return cell! } func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { let model = dataArray[indexPath.row] let detailVC = DetailViewController() detailVC.urlStr = model.url self.navigationController?.pushViewController(detailVC, animated: true) }}
说明:上面的代码中使用了Swift 2.x的异常处理机制,如果不了解可以看看简书上的这篇文章《Swift 2.0异常处理》。
JSON
JSON全称JavaScript对象表达式(JavaScript Object Notation),是目前最流行的存储和交换文本信息的语法,和XML相比,它更小、更快,更易解析,是一种轻量级的文本数据交换格式。
JSON的语法规则可以简单的总结成以下几条:1. 数据在名/值对中;2. 数据由逗号分隔;3. 花括号保存对象;4. 方括号保存数组。
例如:
{ "name" : "骆昊", "age" : 35, "gender" : true, "car" : { "brand" : "Touareg", "maxSpeed" : 240 }, "favorites" : [ "阅读", "旅游", "象棋" ], "mistress" : null}
JSON中的值可以是:
- 数字(整数或浮点数)- 字符串(在双引号中)- 逻辑值(true 或 false)- 数组(在方括号中)- 对象(在花括号中)- null
不难看出,JSON用键值对的方式描述了JavaScript中的对象,它的形态跟Objective-C的NSDictionary以及Swift中的Dictionary类型是完全一致的,可以通过NSJSONSerialization类的两个类方法实现JSON数据和字典或数组之间的相互转换。
// 将数据转换成对象(通常是数组或字典)+ (id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError **)error;// 将数组或字典装换成JSON数据+ (NSData *)dataWithJSONObject:(id)obj options:(NSJSONWritingOptions)opt error:(NSError **)error
通过服务器获得JSON数据后,最终需要将它转换成我们程序中的对象。事实上,将JSON转换成模型对象的操作在开发网络应用中是很常见的,我们可以使用KVC(Key-Value Coding)的方式将一个字典赋值给一个对象的属性,代码如下所示。
说明:KVC通常翻译为键值编码,它允许开发者通过名字访问对象属性,而无需调用明确的存取方法,这样就可以实现在运行时而不是在编译时确定属性的绑定。这种间接访问能让代码变得更灵活和更具复用性。
Objective-C代码:
#import <Foundation/Foundation.h>@interface CDPerson : NSObject@property (nonatomic, copy) NSString *name;@property (nonatomic, assign) NSUInteger age;@property (nonatomic, copy) NSArray<NSString *> *friends;@end
#import "CDPerson.h"@implementation CDPerson- (void)setValue:(id)value forUndefinedKey:(NSString *)key {}- (NSString *) description { NSMutableString *mStr = [NSMutableString string]; for (NSString *friendsName in _friends) { [mStr appendString:friendsName]; [mStr appendString:@" "]; } return [NSString stringWithFormat:@"姓名: %@\n年龄: %ld\n朋友: %@", _name, _age, mStr];}@end
#import <Foundation/Foundation.h>#import "CDPerson.h"int main(int argc, const char * argv[]) { @autoreleasepool { NSDictionary *dict = @{ @"name": @"骆昊", @"age":@(35), @"friends":@[@"金庸", @"古龙", @"黄易"] }; CDPerson *person = [[CDPerson alloc] init]; [person setValuesForKeysWithDictionary:dict]; NSLog(@"%@", person); } return 0;}
Swift代码:
import Foundationclass Person: NSObject { var name: String = "" var age: UInt = 0 var friends: [String] = [] override func setValue(value: AnyObject?, forUndefinedKey key: String) { } override var description: String { get { var mStr = String() for friendName in friends { mStr.appendContentsOf("\(friendName) ") } return "姓名: \(name)\n年龄: \(age)\n朋友: \(mStr)" } }}
var dict = [ "name": "骆昊", "age": 35, "friends": ["金庸", "古龙", "黄易"] ]var person = Person()person.setValuesForKeysWithDictionary(dict)print(person.description)
对于对象中关联了其他对象或者对象的属性跟字典中的键不完全匹配的场景,KVC就显得不那么方便了,但是已经有很多优秀的第三方库帮助我们实现了JSON和模型对象的双向转换,下面我们介绍这些第三库中非常有代表性的JSONModel和YYModel。
说明:JSONModel和YYModel都是用Objective-C开发的,下面我们也直接用Objective-C代码为大家介绍这些东西,不再提供双语版的讲解。
- JSONModel
#import <Foundation/Foundation.h>#import "JSONModel.h"/**产品*/@interface CDProduct: JSONModel@property (nonatomic, assign) int id;@property (nonatomic, copy) NSString *name;@property (nonatomic, assign) double price;@property (nonatomic, assign) int amount;@end
#import "CDProduct.h"@implementation CDProduct- (NSString *)description { return [NSString stringWithFormat:@"商品编号: %d\n商品名称: %@\n商品价格: %.2f\n商品数量: %d", _id, _name, _price, _amount];}@end
#import <Foundation/Foundation.h>#import "JSONModel.h"// 通过协议来限定数组中的元素类型@protocol CDProduct <NSObject>@end/**订单*/@interface CDOrder: JSONModel@property (nonatomic, assign) int orderId;@property (nonatomic, assign) double totalPrice;@property (nonatomic, strong) NSArray<CDProduct> *products;@end
#import "CDOrder.h"@implementation CDOrder// 该方法提供字典(JSON)中的键和对象属性之间的映射关系+ (JSONKeyMapper *)keyMapper { return [[JSONKeyMapper alloc] initWithDictionary:@{ @"order_id": @"orderId", @"order_price": @"totalPrice" }];}- (NSString *)description { return [NSString stringWithFormat:@"订单号: %d 总价: %.2f\n", _orderId, _totalPrice];}@end
#import <Foundation/Foundation.h>#import "CDOrder.h"#import "CDProduct.h"int main(int argc, const char * argv[]) { @autoreleasepool { NSDictionary *dict = @{ @"order_id": @(104), @"order_price": @(108.85), @"products" : @[ @{ @"id": @"123", @"name": @"Product #1", @"price": @(12.95), @"amount": @(2) }, @{ @"id": @"137", @"name": @"Product #2", @"price": @(82.95), @"amount": @(1) } ] }; CDOrder *model = [[CDOrder alloc] initWithDictionary:dict error:nil]; NSLog(@"%@", model); for (CDProduct *product in model.products) { NSLog(@"%@", product); } } return 0;}
从上面的例子不难看出,JSONModel是有侵入性的,因为你的模型类必须继承JSONModel,这些对代码的复用和迁移多多少少会产生影响。基于这样的原因,更多的开发者在实现JSON和模型对象转换时更喜欢选择非侵入式的MJExtension,这里我们就不介绍MJExtension,其实它已经做得非常好了,但是当YYModel横空出世的时候,MJExtension瞬间就成了浮云。YYModel和MJExtension一样是没有侵入性的,你的模型类不要跟第三方库耦合在一起,而且YYModel提供了比MJExtension更优雅的配置方式,更强大的自动类型转化能力,当然在性能上YYModel也更优,而且跟MJExtension不在一个数量级上。我们还是用上面的例子来演示如何使用YYModel。
- YYModel
#import <Foundation/Foundation.h>@class CDProduct;/**订单*/@interface CDOrder: NSObject@property (nonatomic, assign) int orderId;@property (nonatomic, assign) double totalPrice;@property (nonatomic, strong) NSArray<CDProduct *> *products;@end
#import "CDOrder.h"@implementation CDOrder// 该方法提供属性名和字典(JSON)中的键的映射关系+ (NSDictionary *) modelCustomPropertyMapper { return @{ @"orderId": @"order_id", @"totalPrice": @"order_price" };}// 该方法提供容器属性中对象的类型+ (NSDictionary *) modelContainerPropertyGenericClass { return @{ @"products": NSClassFromString(@"CDProduct") };}- (NSString *)description { return [NSString stringWithFormat:@"订单号: %d 总价: %.2f\n", _orderId, _totalPrice];}@end
#import <Foundation/Foundation.h>/**产品*/@interface CDProduct: NSObject@property (nonatomic, assign) int id;@property (nonatomic, copy) NSString *name;@property (nonatomic, assign) double price;@property (nonatomic, assign) int amount;@end
#import "CDProduct.h"@implementation CDProduct- (NSString *)description { return [NSString stringWithFormat:@"商品编号: %d\n商品名称: %@\n商品价格: %.2f\n商品数量: %d", _id, _name, _price, _amount];}@end
#import <Foundation/Foundation.h>#import "CDOrder.h"#import "YYModel.h"int main(int argc, const char * argv[]) { @autoreleasepool { NSDictionary *dict = @{ @"order_id": @(104), @"order_price": @(108.85), @"products": @[ @{ @"id": @"123", @"name": @"Product #1", @"price": @(12.95), @"amount": @(2) }, @{ @"id": @"137", @"name": @"Product #2", @"price": @(82.95), @"amount": @(1) } ] }; CDOrder *order = [CDOrder yy_modelWithDictionary:dict]; NSLog(@"%@", order); for (id product in order.products) { NSLog(@"%@", product); } } return 0;}
第三方库
如果要基于HTTP协议开发联网的iOS应用程序,可以使用优秀的第三方库来提升开发效率减少重复劳动,这些优秀的第三方库中的佼佼者当属AFNetworking。
AFNetworking
AFNetworking是基于URL加载系统的网络框架,很多App都使用它实现联网功能,它的2.x版本封装了基于NSURLConnection和NSURLSession的两套API。目前最新的3.x版本支持基于NSURLConnection联网,同时引入了iOS 9的新特性。
我们重点探讨AFURLSessionManager和AFHTTPSessionManager两个类,因为它们都是基于NSURLSession的,前者的用法可以在官方文档上找到,而且用起来稍显麻烦,AFHTTPSessionManager的用法如下所示。
下面的代码演示如何向服务器发送获取数据的GET请求。
// 创建HTTP会话管理器对象 AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; // AFNetworking默认接受的MIME类型是application/json // 有些服务器虽然返回JSON格式的数据但MIME类型设置的是text/html // 通过下面的代码可以指定支持的MIME类型有哪些 manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects: @"application/json", @"text/html", nil]; // 向服务器发送GET请求获取JSON数据 [manager // 统一资源定位符 GET:@"" // 请求参数 parameters:@{ } // 当完成进度变化时回调的Block progress:nil // 服务器响应成功要回调的Block success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { } // 服务器响应失败要回调的Block failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { } ];
下面的代码演示了如何向服务器发送上传数据的POST请求。
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; [manager // 统一资源定位符 POST:@"" // 请求参数 parameters:@{ } // 构造请求报文消息体的Block constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) { // 可以调用appendPartWithFileData:name:fileName:mimeType:等方法 // 将上传给服务器的数据放到请求报文的消息体中 } // 当上传进度变化时回调的Block progress:^(NSProgress * _Nonnull uploadProgress) { } // 服务器响应成功要回调的Block success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { } // 服务器响应失败要回调的Block failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { } ];
AFNetworking还封装了判断网络可达性的功能,使用该功能的代码如下所示:
// 创建网络可达性管理器 AFNetworkReachabilityManager *manager = [AFNetworkReachabilityManager manager]; // 设置当网络状况发生变化时要回调的Block [manager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) { switch (status) { case AFNetworkReachabilityStatusNotReachable: NSLog(@"没有网络连接"); break; case AFNetworkReachabilityStatusReachableViaWiFi: NSLog(@"使用Wi-Fi"); break; case AFNetworkReachabilityStatusReachableViaWWAN: NSLog(@"使用移动蜂窝网络"); break; default: break; } }]; // 开始监控网络状况变换 [manager startMonitoring];
MKNetworkingKit
相比AFNetworking,MKNetworkingKit会显得小众一些,但它仍然是一个非常优秀的网络框架,可以在Github上面找到它,目前的2.x版本也是基于NSURLSession封装的,放弃了对NSURLConnection的使用。目前能找到的资料基本上介绍的是该框架1.x版本如何使用,如果想了解和使用这个框架,建议访问作者本人的博客。
基于套接字联网
套接字是一系列的用于实现网络通信的标准函数的集合,最有名且被视为标准的是Berkeley Socket API。Berkeley Socket API是在1983年发布的BSD 4.2中引入的(后面统一称之为BSD套接字),随后几乎所有的操作系统都提供了BSD套接字的实现来帮助设备连接互联网,就连微软都参照了BSD套接字实现了用于Windows操作系统的Winsock。
说明: BSD是Unix衍生系统,是由加州大学伯克利分校开发和发布的,如果你想对BSD操作系统的发展史有感性的了解,下面这张图也许会帮助到你。
BSD套接字通常基于客户端/服务器模式(C/S模式)来构建网络应用,这种模式简单的说就是参与网络的通信的要么是服务器,要么是客户机,最经典的例子就是通过浏览器访问Web服务器,Web服务器提供资源而浏览器作为客户机请求获得这些资源。套接字通信通常使用TCP或UDP作为传输协议,如前所述TCP提供了可靠通信的保证,UDP则以更小的开销提供不可靠的传输服务,例如视频流数据对可靠性要求不高就可以选择使用UDP进行传输,这样可以消除TCP多次握手所带来的开销。
下面的代码创建一个基于TCP的Echo服务器来演示如何使用套接字实现网络通信。所谓Echo服务器就是将客户端发送的消息原封不动的发回去,虽然没有什么实际价值,但不失为一个很好的例子。
#include <stdio.h>#include <stdlib.h>#include <string.h>#include <stdbool.h>#include <unistd.h>#include <sys/types.h>#include <sys/socket.h>#include <arpa/inet.h>static const short SERVER_PORT = 1234; // 端口static const int MAX_Q_LEN = 64; // 最大队列长度static const int MAX_MSG_LEN = 4096; // 最大消息长度void change_enter_to_tail_zero(char * const buffer, int pos) { for (int i = pos - 1; i >= 0; i--) { if (buffer[i] == '\r') { buffer[i] = '\0'; break; } }}int main() { // 1. 调用socket函数创建套接字 // 第一个参数指定使用IPv4协议进行通信(AF_INET6代表IPv6) // 第二个参数指定套接字的类型(SOCK_STREAM代表可靠的全双工通信) // 第三个参数指定套接字使用的协议 // 如果返回值是-1表示创建套接字时发生错误 否则返回服务器套接字文件描述符 int serverSocketFD = socket(AF_INET, SOCK_STREAM, 0); if (serverSocketFD < 0) { perror("无法创建套接字!!!\n"); exit(1); } // 代表服务器地址的结构体 struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(SERVER_PORT); serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 2. 将套接字绑定到指定的地址和端口 // 第一个参数指定套接字文件描述符 // 第二个参数是上面代表地址的结构体变量的地址 // 第三个参数是上面代表地址的结构体占用的字节数 // 如果返回值是-1表示绑定失败 int ret = bind(serverSocketFD, (struct sockaddr *)&serverAddr, sizeof serverAddr); if (ret < 0) { perror("无法将套接字绑定到指定的地址!!!\n"); close(serverSocketFD); exit(1); } // 3. 开启监听(监听客户端的连接) ret = listen(serverSocketFD, MAX_Q_LEN); if (ret < 0) { perror("无法开启监听!!!\n"); close(serverSocketFD); exit(1); } bool serverIsRunning = true; while(serverIsRunning) { // 代表客户端地址的结构体 struct sockaddr_in clientAddr; socklen_t clientAddrLen = sizeof clientAddr; // 4. 接受客户端的连接(从队列中取出第一个连接请求) // 如果返回-1表示发生错误 否则返回客户端套接字文件描述符 // 该方法是一个阻塞方法 如果队列中没有连接就会一直阻塞 int clientSocketFD = accept(serverSocketFD, (struct sockaddr *)&clientAddr, &clientAddrLen); bool clientConnected = true; if (clientSocketFD < 0) { perror("接受客户端连接时发生错误!!!\n"); clientConnected = false; } while (clientConnected) { // 接受数据的缓冲区 char buffer[MAX_MSG_LEN + 1]; // 5. 接收客户端发来的数据 ssize_t bytesToRecv = recv(clientSocketFD, buffer, sizeof buffer - 1, 0); if (bytesToRecv > 0) { buffer[bytesToRecv] = '\0'; change_enter_to_tail_zero(buffer, (int)bytesToRecv); printf("%s\n", buffer); // 如果收到客户端发来的bye消息服务器主动关闭 if (!strcmp(buffer, "bye\r\n")) { serverIsRunning = false; clientConnected = false; } // 6. 将消息发回到客户端 ssize_t bytesToSend = send(clientSocketFD, buffer, bytesToRecv, 0); if (bytesToSend > 0) { printf("Echo message has been sent.\n"); } } else { printf("client socket closed!\n"); clientConnected = false; } } // 7. 关闭客户端套接字 close(clientSocketFD); } // 8. 关闭服务器套接字 close(serverSocketFD); return 0;}
我们可以在终端中用telnet来测试上面的代码,效果如下图所示。
上面的Echo服务器只能支持一个客户端请求,当有多个客户端连接到服务器时需要排队等待,很明显是不合适的。可以使用GCD(Grand Central Dispatch)来构建多线程服务器,将服务器和客户端传数据的那段代码放到一个线程中执行。
#import <Foundation/Foundation.h>#import <arpa/inet.h>static const short SERVER_PORT = 1234; // 端口static const int MAX_Q_LEN = 64; // 最大队列长度static const int MAX_MSG_LEN = 4096; // 最大消息长度void change_enter_to_tail_zero(char * const buffer, int pos) { for (int i = pos - 1; i >= 0; i--) { if (buffer[i] == '\r') { buffer[i] = '\0'; break; } }}void handle_client_connection(int clientSocketFD) { bool clientConnected = true; while (clientConnected) { char buffer[MAX_MSG_LEN + 1]; ssize_t bytesToRecv = recv(clientSocketFD, buffer, sizeof buffer - 1, 0); if (bytesToRecv > 0) { buffer[bytesToRecv] = '\0'; change_enter_to_tail_zero(buffer, (int)bytesToRecv); printf("%s\n", buffer); if (!strcmp(buffer, "bye\r\n")) { clientConnected = false; } ssize_t bytesToSend = send(clientSocketFD, buffer, bytesToRecv, 0); if (bytesToSend > 0) { printf("Echo message has been sent.\n"); } } else { printf("client socket closed!\n"); clientConnected = false; } } close(clientSocketFD);}int main() { int serverSocketFD = socket(AF_INET, SOCK_STREAM, 0); if (serverSocketFD < 0) { perror("无法创建套接字!!!\n"); exit(1); } struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(SERVER_PORT); serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); int ret = bind(serverSocketFD, (struct sockaddr *)&serverAddr, sizeof serverAddr); if (ret < 0) { perror("无法将套接字绑定到指定的地址!!!\n"); close(serverSocketFD); exit(1); } ret = listen(serverSocketFD, MAX_Q_LEN); if (ret < 0) { perror("无法开启监听!!!\n"); close(serverSocketFD); exit(1); } while(true) { struct sockaddr_in clientAddr; socklen_t clientAddrLen = sizeof clientAddr; int clientSocketFD = accept(serverSocketFD, (struct sockaddr *)&clientAddr, &clientAddrLen); if (clientSocketFD < 0) { perror("接受客户端连接时发生错误!!!\n"); } else { dispatch_async(dispatch_get_global_queue(0, 0), ^{ handle_client_connection(clientSocketFD); }); } } return 0;}
基于苹果底层API联网
苹果底层提供了叫做CFNetwork的API来实现联网的功能,它对BSD套接字做了一些必要的封装,提供了更为简便的获取网络地址信息和检查网络状态的方法,可以整合Run-Loop来避开使用多线程,此外CFNetwork还对FTP协议、HTTP协议进行了面向对象的封装,你可以在不了解这些协议实现细节的情况下来使用这些协议。
我们用CFNetwork来为上面的Echo服务器写一个专门的客户端,这一次我们用Objective-C来做一些面向对象的封装,代码如下所示。
#import <Foundation/Foundation.h>typedef NS_ENUM(NSUInteger, CFNetworkServerErrorCode) { NoError, SocketError, ConnectError};static const int kMaxMessageLength = 4096;static const int kConnectionTimeout = 15;@interface CDEchoClient : NSObject@property (nonatomic) NSUInteger errorCode;@property (nonatomic) CFSocketRef socket;- (instancetype) initWithAddress:(NSString *) address port:(int) port;- (NSString *) sendMessage:(NSString *) msg;@end
#import "CDEchoClient.h"#import <arpa/inet.h>@implementation CDEchoClient- (instancetype)initWithAddress:(NSString *)address port:(int)port { // 调用CFSocketCreate函数通过指定的协议和类型创建套接字 // 第一个参数通常是NULL(使用默认的对象内存分配器) // 第二个参数AF_INET表示使用IPv4(如果指定成0或负数默认也是AF_INET) // 第三个参数是套接字类型(如果指定成0或负数默认也是SOCK_STREAM) // 第四个参数是协议(如果前一个参数是SOCK_STREAM默认为TCP, 前一个参数是SOCK_DGRAM默认为UDP) // 第五个参数和第六个参数是回调类型和回调函数 // 第七个参数是保存数据的上下文环境 self.socket = CFSocketCreate(NULL, AF_INET, SOCK_STREAM, IPPROTO_TCP, 0, NULL, NULL); if (!self.socket) { self.errorCode = SocketError; } else { // 表示服务器地址的结构体 struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_len = sizeof(servaddr); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(port); // 将字符串形式的地址转换成网络地址的结构体变量 inet_pton(AF_INET, [address cStringUsingEncoding:NSUTF8StringEncoding], &servaddr.sin_addr); // 将地址结构体转换成CFDataRef类型 CFDataRef connectAddr = CFDataCreate(NULL, (unsigned char *)&servaddr, sizeof servaddr); // 调用CFSocketConnectToAddress函数连接远端套接字(服务器) // 其中第三个参数代表连接的超时时间以秒为单位 // 如果函数返回kCFSocketSuccess表示连接成功 否则就是连接失败或超时 if (!connectAddr || CFSocketConnectToAddress( self.socket, connectAddr, kConnectionTimeout) != kCFSocketSuccess) { self.errorCode = ConnectError; } } return self;}- (NSString *) sendMessage:(NSString *) msg { char buffer[kMaxMessageLength]; // 获得本地套接字 CFSocketNativeHandle sock = CFSocketGetNative(self.socket); const char *mess = [msg cStringUsingEncoding:NSUTF8StringEncoding]; // 向服务器发送Echo消息 send(sock, mess, strlen(mess) + 1, 0); // 接受服务器返回的消息 recv(sock, buffer, sizeof buffer, 0); return [NSString stringWithUTF8String:buffer];}- (void) dealloc { if (self.socket) { CFRelease(self.socket); self.socket = NULL; }}@end
用Storyboard做一个用户界面。
#import "ViewController.h"#import "CDEchoClient.h"@interface ViewController ()@property (weak, nonatomic) IBOutlet UITextField *msgField;@property (weak, nonatomic) IBOutlet UILabel *echoMsgLabel;@end@implementation ViewController { CDEchoClient *client;}- (void)viewDidLoad { [super viewDidLoad]; client = [[CDEchoClient alloc] initWithAddress:@"127.0.0.1" port:1234];}- (IBAction)sendButtonClicked:(id)sender { // 发送bye消息会断开与服务器的连接 不能再发送消息 if (client && client.errorCode == NoError) { NSString *msg = [self.msgField.text stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceCharacterSet]]; if (msg.length > 0) { [self.msgField resignFirstResponder]; self.echoMsgLabel.text = [client sendMessage:msg]; } } else { NSLog(@"Cannot send message!!!"); }}@end
我们可以先运行上面用套机字编写的Echo服务器,再通过模拟器或真机来运行Echo客户端,运行效果如下图所示:
基于Bonjour的网络设备发现
Bonjour是Apple推出的适用于局域网(LAN)的零配置网络协议,主要的目的是在缺少中心服务器的情况下解决网络设备的IP获取(在没有DHCP服务的情况下用随机的方式分配IP地址),名称解析(用mDNS取代传统的DNS服务)和服务发现(通过本地域名如“名称.服务类型.传输协议类型.local.”中的服务类型来发现服务)等关键问题。想要对Bonjour有一个全面的了解,建议访问苹果官方网站上的Bonjour for Developers专区。
发布Bonjour服务
#import <Foundation/Foundation.h>@interface CDMyBonjourService : NSObject <NSNetServiceDelegate> { NSNetService *service;}- (void) startServiceOfType:(NSString *) type port:(int) port;- (void) stopService;@end
#import "CDMyBonjourService.h"@implementation CDMyBonjourService- (void)startServiceOfType:(NSString *) type port:(int) port { service = [[NSNetService alloc] initWithDomain:@"" type:type name:@"" port:port]; if (service) { service.delegate = self; [service publish]; }}- (void) stopService { [service stop];}#pragma mark NSNetServiceDelegate回调方法- (void)netServiceWillPublish:(NSNetService *)sender {}- (void)netServiceDidPublish:(NSNetService *)sender {}- (void)netService:(NSNetService *)sender didNotPublish:(NSDictionary<NSString *, NSNumber *> *)errorDict {}- (void)netServiceWillResolve:(NSNetService *)sender {}- (void)netServiceDidResolveAddress:(NSNetService *)sender {}- (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary<NSString *, NSNumber *> *)errorDict {}- (void)netServiceDidStop:(NSNetService *)sender {}- (void)netService:(NSNetService *)sender didUpdateTXTRecordData:(NSData *)data {}- (void)netService:(NSNetService *)sender didAcceptConnectionWithInputStream:(NSInputStream *)inputStream outputStream:(NSOutputStream *)outputStream {}@end
发现Bonjour服务
#import <Foundation/Foundation.h>@interface CDMyBonjourServiceBrowser: NSObject <NSNetServiceBrowserDelegate> { NSNetServiceBrowser *serviceBrowser; NSMutableArray<NSNetService *> *servicesArray;}- (void) startBrowsingForType:(NSString *) type;- (void) stopBrowsing;@end
#import "CDMyBonjourServiceBrowser.h"@implementation CDMyBonjourServiceBrowser- (void) startBrowsingForType:(NSString *)type { serviceBrowser = [[NSNetServiceBrowser alloc] init]; [serviceBrowser searchForServicesOfType:type inDomain:@""];}- (void) stopBrowsing { [serviceBrowser stop]; [servicesArray removeAllObjects];}#pragma mark NSNetServiceBrowserDelegate回调方法- (void)netServiceBrowserWillSearch:(NSNetServiceBrowser *)browser {}- (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)browser {}- (void)netServiceBrowser:(NSNetServiceBrowser *)browser didNotSearch:(NSDictionary<NSString *, NSNumber *> *)errorDict {}- (void)netServiceBrowser:(NSNetServiceBrowser *)browser didFindDomain:(NSString *)domainString moreComing:(BOOL)moreComing {}- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didFindService:(NSNetService *)aNetService moreComing:(BOOL)moreComing { if (!servicesArray) { servicesArray = [NSMutableArray array]; } // 将发现的服务添加到数组中 [servicesArray addObject:aNetService];}- (void)netServiceBrowser:(NSNetServiceBrowser *)browser didRemoveDomain:(NSString *)domainString moreComing:(BOOL)moreComing {}- (void)netServiceBrowser:(NSNetServiceBrowser *)browser didRemoveService:(NSNetService *)service moreComing:(BOOL)moreComing {}@end
注意:在发布服务之前应该准备好对应的服务并启动它。不过NSNetService的publish方法并不依赖它所发布的服务,不管要发布的服务是否就绪,该方法都可以成功的将服务发布出去,但是如果服务没有就绪,要使用这个服务的客户端就会发现这个发布出来的服务是个无效的服务。
在上面的例子中,我们将发现的服务装在一个数组中,当我们需要使用这些服务时,可以通过NSNetService对象解析出服务的地址和端口,对于基于HTTP的服务,我们可以使用苹果的URL加载系统或者AFNetworking这样的第三方库来使用服务,对于其他的服务我们可以使用套接字或CFNetwork API来使用服务,对于使用同一个局域网中提供的服务,这种方式不是更加简单方便吗?
总结
到此为止,我们对iOS网络应用开发的方方面面做了一个走马观花的讲解,当然iOS开发中跟网络相关的知识还远不止这些,例如如何通过证书保证网络通信的安全,如何有效的使用缓存来提升性能和减少网络开销以及URL缓存的过期模型和验证模型等,这些内容打算以专题的形式在后面为大家呈现。上面内容所有的代码都可以在我的Github上找到。
- iOS开发笔记 - 网络篇
- iOS网络开发学习笔记
- iOS开发网络篇
- iOS开发网络篇
- iOS开发网络篇
- iOS开发笔记--iOS网络七层模型&&网络数据包
- iOS开发网络篇-检测网络状态
- iOS开发网络篇-检测网络状态
- IOS开发笔记 - 基于SDWebImage的网络图片加载处理
- IOS开发笔记 - 基于SDWebImage的网络图片加载处理
- IOS开发笔记 - 基于SDWebImage的网络图片加载处理
- ios开发判断网络连接及网络异常(ios自学笔记)
- IOS开发笔记 代码篇
- IOS开发笔记 术语篇
- IOS开发笔记 框架篇
- iOS开发笔记 - 工具篇
- IOS开发 笔记 ---plist篇
- IOS 开发笔记 UINavigationBar篇
- 2016年第七届蓝桥杯省赛(C/C++ A组)
- ACM常用算法汇总
- C++上机实验1-1
- 编程学习笔记之c++相关::vector学习心得
- 表的修改+结构
- iOS开发笔记 - 网络篇
- 安卓开发小知识-Animation入门
- 设计原则——开放封闭原则(OCP)
- 排序算法的总结
- 【前端工具】fis-plus【01】前端开发环境配置
- APP 旋转前保存数据
- SDAU 贪心专题 04 节目安排
- UNP卷一学习笔记:基本UDP套接字编程
- ionic框架简述