iOS 移动端开发@IT·互联网程序员

iOS网络模块大总结

2017-04-21  本文已影响58人  Kevin_wzx

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.抓包工具

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(原名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的文章的相关链接:

#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
#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

#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/

#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
#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

上一篇下一篇

猜你喜欢

热点阅读