iOS网络模块大总结
1.计算机网络基础
屏幕快照 2017-04-21 下午3.35.35.png计算机网络:http://lib.csdn.net/base/37
1279331-376cb79f33954a4d.png 屏幕快照 2017-04-21 下午3.36.14.png
2.基于HTTP协议联网
屏幕快照 2017-04-21 下午3.36.50.pngiOS:http://lib.csdn.net/base/1
Android:http://lib.csdn.net/base/15
HTTP有两种类型的报文:请求报文和响应报文。请求报文和响应报文都是由三个部分组成的。我们可以用抓包工具截取请求和响应报文来看看它们的结构:
1279331-a944ae3a2aef54ed.png请求报文是由请求行、请求头和消息体构成的。请求行包含了命令(通常是GET或POST)、资源和协议版本;请求头是键值对映射形式的和请求相关的信息,如客户端使用的语言、使用的浏览器等信息;消息体是客户端发给服务器的数据;在请求头和消息体之间有一个空行。 1279331-5bbd44400cb5a58a.png
响应报文是由响应行、响应头和消息体构成的。响应行包含了协议版本和状态码;响应头是键值对形式的和响应相关的信息,如服务器的软件版本、时间日期、缓存策略、响应内容类型等信息;消息体是服务器发给客户端的数据;在响应头和消息体之间有一个空行。
3.抓包工具
- Charles
Charles是一个HTTP代理服务器,HTTP监视器,反转代理服务器,它允许一个开发者查看所有连接互联网的HTTP通信。很多iOS开发者都选择Charles作为抓包工具来获取和测试网络接口。通过下图所示的菜单项可以将Charles设置为Mac系统的HTTP代理,所有的HTTP数据都会被Charles截获。
1279331-0043300de305e37d.png当然,还可以将Charles设置为手机的代理,只要让安装了Charles的Mac系统和手机使用相同的网络,再将手机无线局域网的代理服务器设置为Mac系统的IP地址即可,这样手机上的HTTP数据也会被截获。
1279331-7cd37da27499163e.png![1279331-180c7be9879374d7.png](https://img.haomeiwen.com/i1389082/f3c401391a8cbf72.png?imageMogr2
/auto-orient/strip%7CimageView2/2/w/1240)
- Wireshark
1279331-574f0bd5f004fe61.pngWireshark(原名Ethereal,1998年由美国Gerald Combs首创研发,由世界各国100多位网络专家和软件人员共同参与此软件的升级完善和维护,2006年5月更名为Wireshark)是一个非常专业的网络数据包截取和分析软件,它直接截获经过网卡的数据,并尽可能显示出最为详细的数据包信息,是协议分析的利器。Wireshark比Charles更底层更专业,但是如果只做HTTP数据分析,Charles用起来还是非常简单方便的。
4.相关API
屏幕快照 2017-04-21 下午3.43.30.png数据库:http://lib.csdn.net/base/14
swift:http://lib.csdn.net/base/1
下面的代码演示了如何在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 UIKit
class 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)
}
}
屏幕快照 2017-04-21 下午3.45.33.png 屏幕快照 2017-04-21 下午3.46.07.png提示:iOS 9出于安全方面的考虑,不允许使用非安全的HTTP协议联网,如果要用需要修改项目的Info.plist文件,添加“App Transport Security Settings”键,其类型是Dictionary;在“App Transport Security Settings”下添加一个子元素,键是“Allow Arbitrary Loads”,类型是Boolean,将其值设置为YES
// 发送同步请求的方法
+ (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
屏幕快照 2017-04-21 下午3.46.59.png 屏幕快照 2017-04-21 下午3.47.17.png提示:同步请求是阻塞式请求,这就意味着同步请求的方法在返回数据之前会一直阻塞;异步请求是非阻塞式请求,当服务器返回数据时可以回调的方式对数据进行处理。如果明白这一点,就很容易理解为什么上面的同步请求方法会返回NSData指针,而异步请求方法没有返回值但有一个Block类型的参数(Block最适合用来书写回调代码)
// 返回一个标准的配置,标准配置会使用默认的缓存策略、超时时间等
+ (NSURLSessionConfiguration *)defaultSessionConfiguration;
// 返回一个临时性的配置,这个配置中不会对缓存,Cookie和证书进行持久化存储
// 对于实现无痕浏览这种功能来说这种配置是非常理想的
+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration;
// 返回一个后台配置
// 后台会话不同于普通的会话,它甚至可以在应用程序挂起,退出或者崩溃的情况下运行上传和下载任务
// 初始化时指定的标识符,被用于向任何可能在进程外恢复后台传输的守护进程(daemon)提供上下文
+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:
(NSString *)identifier
5.数据解析
通过HTTP从服务器获得的数据通常都是JSON格式或XML格式的,下面对这两种数据格式做一个简单的介绍
1.XML
屏幕快照 2017-04-21 下午3.48.51.png<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>
屏幕快照 2017-04-21 下午3.49.35.png
屏幕快照 2017-04-21 下午3.49.46.png
屏幕快照 2017-04-21 下午3.49.59.png
RUNOOB.COM(菜鸟教程):http://www.runoob.com
KissXML:https://github.com/robbiehanson/KissXML
RaptureXML:https://github.com/ZaBlanc/RaptureXML
XMLDictionary:https://github.com/nicklockwood/XMLDictionary
下面的代码演示了如何使用KissXML解析开源中国(http://www.oschina.net )编号为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”头文件的路径添到此处。
1279331-6a8eeef5ddf35330.png#ifndef bridge_h
#define bridge_h
#import "DDXML.h"
#endif /* bridge_h */
import UIKit
class 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异常处理》:http://www.jianshu.com/p/96a7db3fde00
2.JSON
屏幕快照 2017-04-21 下午3.57.29.pngJavaScript:http://lib.csdn.net/base/18
// 将数据转换成对象(通常是数组或字典)
+ (id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError **)error;
// 将数组或字典装换成JSON数据
+ (NSData *)dataWithJSONObject:(id)obj options:(NSJSONWritingOptions)opt error:(NSError **)error
屏幕快照 2017-04-21 下午3.58.32.png
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 Foundation
class 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)
屏幕快照 2017-04-21 下午4.00.39.png
JSONModel:https://github.com/jsonmodel/jsonmodel
YYModel:https://github.com/ibireme/YYModel
- 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(https://github.com/CoderMJLee/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;
}
- MJExtension(简介及使用)
相关链接:https://www.jianshu.com/p/2677cb446a7f
iOS字典数组中有字典数组怎么解析:https://www.jianshu.com/p/0fa9f8a5698a
6. 第三方库
屏幕快照 2017-04-21 下午4.07.42.png如果要基于HTTP协议开发联网的iOS应用程序,可以使用优秀的第三方库来提升开发效率减少重复劳动,这些优秀的第三方库中的佼佼者当属AFNetworking(https://github.com/AFNetworking/AFNetworking)
下面的代码演示如何向服务器发送获取数据的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];
屏幕快照 2017-04-21 下午4.10.45.png
MKNetworkingKit:http://blog.mugunthkumar.com/ios-components/mknetworkkit/
7.基于套接字联网
屏幕快照 2017-04-21 下午4.15.17.png 1279331-aef35a7cd984761a.jpg.png 屏幕快照 2017-04-21 下午4.15.59.png下面的代码创建一个基于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来测试上面的代码,效果如下图所示:
1279331-792f57c32ee29133.png上面的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;
}
8.基于苹果底层API联网
屏幕快照 2017-04-21 下午4.18.48.png我们用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做一个用户界面:
1279331-02aa26975153bd95.png#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客户端,运行效果如下图所示:
1279331-2a90ad7200e0386b.png9.基于Bonjour的网络设备发现
Bonjour是Apple推出的适用于局域网(LAN)的零配置网络协议,主要的目的是在缺少中心服务器的情况下解决网络设备的IP获取(在没有DHCP服务的情况下用随机的方式分配IP地址),名称解析(用mDNS取代传统的DNS服务)和服务发现(通过本地域名如“名称.服务类型.传输协议类型.local.”中的服务类型来发现服务)等关键问题。想要对Bonjour有一个全面的了解,建议访问苹果官方网站上的Bonjour for Developers专区:https://developer.apple.com/bonjour/
- 发布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
屏幕快照 2017-04-21 下午4.25.05.png
10.总结
到此为止,我们对iOS网络应用开发的方方面面做了一个走马观花的讲解,当然iOS开发中跟网络相关的知识还远不止这些,例如如何通过证书保证网络通信的安全,如何有效的使用缓存来提升性能和减少网络开销以及URL缓存的过期模型和验证模型等,这些内容打算以专题的形式在后面为大家呈现。上面内容所有的代码都可以在Github上找到:https://github.com/jackfrued/iOS_Note_Networking