iOS网络模块大总结

作者: Kevin_wzx | 来源:发表于2017-04-21 16:47 被阅读58次

    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.png
    iOS:http://lib.csdn.net/base/1
    Android:http://lib.csdn.net/base/15

    HTTP有两种类型的报文:请求报文和响应报文。请求报文和响应报文都是由三个部分组成的。我们可以用抓包工具截取请求和响应报文来看看它们的结构:

    1279331-a944ae3a2aef54ed.png
    请求报文是由请求行、请求头和消息体构成的。请求行包含了命令(通常是GET或POST)、资源和协议版本;请求头是键值对映射形式的和请求相关的信息,如客户端使用的语言、使用的浏览器等信息;消息体是客户端发给服务器的数据;在请求头和消息体之间有一个空行。 1279331-5bbd44400cb5a58a.png
    响应报文是由响应行、响应头和消息体构成的。响应行包含了协议版本和状态码;响应头是键值对形式的和响应相关的信息,如服务器的软件版本、时间日期、缓存策略、响应内容类型等信息;消息体是服务器发给客户端的数据;在响应头和消息体之间有一个空行。

    3.抓包工具

    • Charles
    1279331-3c8b20098531a644.png

    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

    Wireshark(原名Ethereal,1998年由美国Gerald Combs首创研发,由世界各国100多位网络专家和软件人员共同参与此软件的升级完善和维护,2006年5月更名为Wireshark)是一个非常专业的网络数据包截取和分析软件,它直接截获经过网卡的数据,并尽可能显示出最为详细的数据包信息,是协议分析的利器。Wireshark比Charles更底层更专业,但是如果只做HTTP数据分析,Charles用起来还是非常简单方便的。

    1279331-574f0bd5f004fe61.png

    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)
        }
    
    }
    

    提示:iOS 9出于安全方面的考虑,不允许使用非安全的HTTP协议联网,如果要用需要修改项目的Info.plist文件,添加“App Transport Security Settings”键,其类型是Dictionary;在“App Transport Security Settings”下添加一个子元素,键是“Allow Arbitrary Loads”,类型是Boolean,将其值设置为YES

    屏幕快照 2017-04-21 下午3.45.33.png 屏幕快照 2017-04-21 下午3.46.07.png
    // 发送同步请求的方法
    + (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最适合用来书写回调代码)

    屏幕快照 2017-04-21 下午3.46.59.png 屏幕快照 2017-04-21 下午3.47.17.png
    // 返回一个标准的配置,标准配置会使用默认的缓存策略、超时时间等
    + (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.png
    JavaScript: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;
    }
    

    6. 第三方库

    如果要基于HTTP协议开发联网的iOS应用程序,可以使用优秀的第三方库来提升开发效率减少重复劳动,这些优秀的第三方库中的佼佼者当属AFNetworking(https://github.com/AFNetworking/AFNetworking)

    屏幕快照 2017-04-21 下午4.07.42.png
    下面的代码演示如何向服务器发送获取数据的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.png

    9.基于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

    相关文章

      网友评论

        本文标题:iOS网络模块大总结

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