iOS開發筆記 - 網絡篇

計算機網絡基礎

  計算機網絡是多臺獨立自主的計算機互聯而成的系統的總稱,最初建立計算機網絡的目的是實現信息傳遞和資源共享。

  如果說計算機是第二次世界大戰的產物,那麼計算機網絡則是美蘇冷戰的產物。20世紀60年代初期,美國國防部領導的ARPA提出研究一種嶄新的、能夠適應現代戰爭的、生存性很強的通信系統並藉此來應對蘇聯核攻擊的威脅,這個決定促使了分組交換網的誕生,也奠定今天計算機網絡的原型,這是計算機網絡發展史上第一個里程碑式的事件。

  第二個里程碑式的事件是20世紀80年代初,國際標準化組織(ISO)提出了OSI/RM(開放系統互聯參考模型),該模型定義了計算機網絡的分層體系結構,雖然該模型並沒有成爲網絡設備製造商遵循的國際標準,但用分層的思想解決複雜系統設計問題的做法已經深入人心。成爲事實標準(de facto standard)的是TCP/IP模型,而TCP/IP協議簇(協議簇通常指彼此相關聯的一系列協議的總稱)也是構成今天的Internet的基石。不同於OSI/RM的七層結構,TCP/IP模型是一個四層模型,從上到下依次是應用層、傳輸層、網絡層、物理鏈路層。值得一提的是,傳輸層可以使用兩種不同的協議,一個是面向連接的傳輸控制協議(TCP),另一個是無連接的用戶數據報協議(UDP),我們耳熟能詳的的協議如HTTP、FTP、Telnet、POP3、DHCP、DNS、ICQ等都屬於應用層的協議,它們要麼構建在TCP之上,要麼構建在UDP之上。

圖1. TCP/IP模型和OSI/RM模型

  計算機網絡發展史上第三個里程碑事件應該是瀏覽器的問世。20世紀90年代初,英國人Timothy John Berners-Lee發明了瀏覽器,瀏覽器通過超文本傳輸協議(HTTP)跟服務器交換超文本數據,通過圖形用戶界面顯示從服務器獲得的超文本數據,這一切都讓使用Internet變得無比簡單,於是計算機網絡的用戶數量開始爆炸式的增長。

基於HTTP協議聯網

  在iOS開發中,如果應用程序需要的數據不在本地,而是通過網絡獲取的文字、圖片、音視頻等資源,那麼我們的應用程序就需要聯網,對於這種場景通常可以直接使用HTTP(Hyper-Text Transfer Protocol)向提供資源的服務器發出請求即可。HTTP協議對於很多人來說都不陌生,我們使用瀏覽器訪問Web服務器的時候使用的基本上都是使用HTTP協議(有些服務器需要使用HTTPS,它是在HTTP下層添加SSL[Secure Socket Layer],用於安全的傳輸HTTP協議數據)。目前越來越多的應用已經從瀏覽器延伸到移動客戶端,但是服務器端並不需要做出任何改變,iOS和Android的應用程序也可以通過HTTP協議和服務器通信。

  我們先來解釋一下什麼是協議以及HTTP到底是一個怎樣的協議。我們將任何可發送或接收信息的硬件或程序稱之爲實體,而協議則是控制兩個對等實體進行通信的規則的集合。簡單的說,協議就是通信雙方必須遵循的對話的標準和規範。HTTP是構建在TCP之上的協議,之所以選擇TCP作爲底層傳輸協議是因爲TCP除了可以保證可靠通信之外,還具備流量控制和擁塞控制的能力,如果這一點不能理解也不要緊,我麼只需要知道HTTP需要可靠的傳輸層協議的支持就夠了。

  HTTP有兩種類型的報文:請求報文和響應報文。請求報文和響應報文都是由三個部分組成的。我們可以用抓包工具截取請求和響應報文來看看它們的結構。

圖2. HTTP請求報文

  請求報文是由請求行、請求頭和消息體構成的。請求行包含了命令(通常是GET或POST)、資源和協議版本;請求頭是鍵值對映射形式的和請求相關的信息,如客戶端使用的語言、使用的瀏覽器等信息;消息體是客戶端發給服務器的數據;在請求頭和消息體之間有一個空行。

圖3. HTTP響應報文

  響應報文是由響應行、響應頭和消息體構成的。響應行包含了協議版本和狀態碼;響應頭是鍵值對形式的和響應相關的信息,如服務器的軟件版本、時間日期、緩存策略、響應內容類型等信息;消息體是服務器發給客戶端的數據;在響應頭和消息體之間有一個空行。

抓包工具

  • Charles

圖4. Charles啓動界面

  Charles是一個HTTP代理服務器,HTTP監視器,反轉代理服務器,它允許一個開發者查看所有連接互聯網的HTTP通信。很多iOS開發者都選擇Charles作爲抓包工具來獲取和測試網絡接口。通過下圖所示的菜單項可以將Charles設置爲Mac系統的HTTP代理,所有的HTTP數據都會被Charles截獲。

圖5. 將Charles設置Mac系統HTTP代理的菜單項

  當然,還可以將Charles設置爲手機的代理,只要讓安裝了Charles的Mac系統和手機使用相同的網絡,再將手機無線局域網的代理服務器設置爲Mac系統的IP地址即可,這樣手機上的HTTP數據也會被截獲。

圖6. 手機設置無線局域網代理服務器

圖7. 指定代理服務器的IP地址和端口號

  • Wireshark

      Wireshark(原名Ethereal,1998年由美國Gerald Combs首創研發,由世界各國100多位網絡專家和軟件人員共同參與此軟件的升級完善和維護,2006年5月更名爲Wireshark)是一個非常專業的網絡數據包截取和分析軟件,它直接截獲經過網卡的數據,並儘可能顯示出最爲詳細的數據包信息,是協議分析的利器。Wireshark比Charles更底層更專業,但是如果只做HTTP數據分析,Charles用起來還是非常簡單方便的。

圖8. Wireshark運行效果圖

相關API

  • NSURL

      NSURL是代表統一資源定位符(Universal Resource Locator,URL)的類。URL是互聯網上標準資源的地址,互聯網上的每個資源都有一個唯一的與之對應的URL。

      URL的格式如下所示:

協議://域名或IP地址:端口號/路徑/資源

  下面是百度logo的URL:

http://www.baidu.com:80/img/bd_logo1.png

說明:端口號是對IP地址的擴展。例如我們的服務器只有一個IP地址,但是我們可以在這臺服務器上開設多個服務,如Web服務、郵件服務和數據庫服務,當服務器收到一個請求時會根據端口號來區分到底請求的是Web服務還是郵件服務,或者是數據庫服務。我們在瀏覽器中輸入URL的時候通常都會省略端口號,因爲HTTP協議默認使用80端口,也就是說除非你訪問的Web服務器沒有使用80端口,你才需要輸入相應的端口號。

  下面的代碼演示瞭如何在iOS應用中通過URL獲取網絡數據。

  Objective-C代碼:

#import "ViewController.h"

#define CENTER_X CGRectGetWidth(self.view.bounds) / 2
#define CENTER_Y CGRectGetHeight(self.view.bounds) / 2

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    UIImageView *imageView = [[UIImageView alloc] initWithFrame:
        CGRectMake(0, 0, 320, 160)];
    imageView.center = CGPointMake(CENTER_X, CENTER_Y);
    [self.view addSubview:imageView];

    NSURL *url = [NSURL URLWithString:@"http://www.baidu.com/img/bd_logo1.png"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    imageView.image = [UIImage imageWithData:data];
}

@end

  Swift代碼:

import 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。

  • NSURLRequest / NSMutableURLRequest

      NSURLRequest / NSMutableURLRequest代表了客戶端向服務器發送的HTTP請求。通過請求對象可以設置請求的方法、請求頭、緩存策略、超時時間、消息體等。

  • NSURLResponse

      NSURLResponse代表了服務器發送給客戶端的HTTP響應。

  • NSURLConnection

      在iOS 7以前,基於HTTP協議聯網的操作最終都要由NSURLConnection類來完成,該類主要有兩個方法,一個用於發送同步請求,一個用於發送異步請求。

// 發送同步請求的方法
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request 
      returningResponse:(NSURLResponse **)response error:(NSError **)error;

// 發送異步請求的方法
+ (void)sendAsynchronousRequest:(NSURLRequest *)request 
      queue:(NSOperationQueue *)queue 
      completionHandler:(void (^)(NSURLResponse *response, NSData *data, NSError *connectionError))handler

提示:同步請求是阻塞式請求,這就意味着同步請求的方法在返回數據之前會一直阻塞;異步請求是非阻塞式請求,當服務器返回數據時可以回調的方式對數據進行處理。如果明白這一點,就很容易理解爲什麼上面的同步請求方法會返回NSData指針,而異步請求方法沒有返回值但有一個Block類型的參數(Block最適合用來書寫回調代碼)。

  • NSURLSession

      2013的WWDC,蘋果推出了NSURLConnection的繼任者NSURLSession。與NSURLConnection相比,NSURLsession最直接的改進就是可以配置每個會話(session)的緩存、協議、cookie以及證書策略(credential policy)等,而且你可以跨程序共享這些信息。每個NSURLSession對象都由一個NSURLSessionConfiguration對象來進行初始化,NSURLSessionConfiguration對象代表了會話的配置以及一些用來增強移動設備上性能的新選項。

      可以通過NSURLSession創建NSURLSessionTask(會話任務),會話任務有三個子類對應不同的場景,分別是:NSURLSessionDataTask(獲取數據的任務)、NSURLSessionDownloadTask(下載任務)和NSURLSessionUploadTask(上傳任務),我們通過HTTP協議可以完成的操作都屬於這三類任務之一。NSURLSessionTask主要有三個方法,分別是:resume(恢復任務)、suspend(掛起任務)和cancel(取消任務)。

  • NSURLSessionConfiguration

      如前面所述,NSURLSessionConfiguration代表了會話的配置,該類的三個創建對象的類方法很好的詮釋了NSURLSession類設計時所考慮的不同的使用場景。

// 返回一個標準的配置,標準配置會使用默認的緩存策略、超時時間等
+ (NSURLSessionConfiguration *)defaultSessionConfiguration;
// 返回一個臨時性的配置,這個配置中不會對緩存,Cookie和證書進行持久化存儲
// 對於實現無痕瀏覽這種功能來說這種配置是非常理想的
+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration;
// 返回一個後臺配置
// 後臺會話不同於普通的會話,它甚至可以在應用程序掛起,退出或者崩潰的情況下運行上傳和下載任務
// 初始化時指定的標識符,被用於向任何可能在進程外恢復後臺傳輸的守護進程(daemon)提供上下文
+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:
      (NSString *)identifier

數據解析

  通過HTTP從服務器獲得的數據通常都是JSON格式或XML格式的,下面對這兩種數據格式做一個簡單的介紹。

  • XML

      XML全稱可擴展標記語言(eXtensible Markup Language),被設計用來傳輸和存儲數據。在JSON被廣泛使用之前,XML是異構系統之間交換數據的事實標準,它是一種具有自我描述能力的傳輸數據的標記語言,如下所示。

<?xml version="1.0" encoding="ISO-8859-1"?>
<note>
    <to>Tove</to>
    <from>Jani</from>
    <heading>Reminder</heading>
    <body>Don't forget me this weekend!</body>
</note>

  XML文檔形成一種樹結構,它必須包含根元素。該元素是所有其他元素的父元素。這棵樹從根部開始,並擴展到樹的最底端,如下圖所示。

圖9. XML樹結構

<bookstore>
    <book category="COOKING">
        <title lang="en">Everyday Italian</title>
        <author>Giada De Laurentiis</author>
        <year>2005</year>
        <price>30.00</price>
    </book>
    <book category="CHILDREN">
        <title lang="en">Harry Potter</title>
        <author>J K. Rowling</author>
        <year>2005</year>
        <price>29.99</price>
    </book>
    <book category="WEB">
        <title lang="en">Learning XML</title>
        <author>Erik T. Ray</author>
        <year>2003</year>
        <price>39.95</price>
    </book>
</bookstore>

  XML的語法規則跟其他標籤語言(如HTML)基本一致,不過需要注意以下幾條:

1. 所有的XML元素都必須有一個關閉標籤
2. XML標籤對大小寫敏感
3. XML必須正確嵌套
4. XML文檔必須有根元素
5. XML屬性值必須加引號
6. XML中的特殊字符要使用實體引用
7. XML中的註釋是<!-- -->

  在XML文檔中查找信息可以使用XPath表達式,我們來看一個例子。

<?xml version="1.0" encoding="ISO-8859-1"?>
<bookstore>
    <book>
        <title lang="eng">Harry Potter</title>
        <price>29.99</price>
    </book>
    <book>
        <title lang="eng">Learning XML</title>
        <price>39.95</price>
    </book>
</bookstore>

XPath語法表

表達式 描述 例子 結果
nodename 選取此節點的所有子節點 bookstore 選取bookstore的所有子節點
/ 從根節點選取 /bookstore 選取根元素bookstore
// 從匹配選擇的當前節點選擇文檔中的節點,而不考慮它們的位置 //book 選取所有 book 子元素,而不管它們在文檔中的位置
. 選取當前節點
.. 選取當前節點的父節點
@ 選取屬性 //@lang 選取名爲lang的所有屬性

XPath的例子

路徑表達式 結果
/bookstore/book[1] 選取屬於bookstore子元素的第一個book元素。
/bookstore/book[last()] 選取屬於bookstore子元素的最後一個book元素。
/bookstore/book[last()-1] 選取屬於bookstore子元素的倒數第二個book元素。
/bookstore/book[position()<3] 選取最前面的兩個屬於bookstore元素的子元素的 book元素。
//title[@lang] 選取所有擁有名爲lang的屬性的title元素。
//title[@lang=’eng’] 選取所有title元素,且這些元素擁有值爲eng的lang屬性。
/bookstore/book[price>35.00] 選取bookstore元素的所有book元素,且其中的 price元素的值須大於35.00。
/bookstore/book[price>35.00]/title 選取bookstore元素中的book元素的所有 title元素,且其中的price元素的值須大於35.00。

提示:如果對上面很多概念不理解或者想對XML有一個更全面的瞭解,建議訪問RUNOOB.COM獲得更多的信息。

  解析XML數據主要有兩種方式:SAX和DOM。SAX解析屬於事件驅動型的順序解析,即從上至下解析XML文件,遇到標記、屬性、註釋、內容等都會引發事件回調,蘋果原生的NSXMLParser就屬於這種類型的解析,其優點在於速度快,內存佔用少,但是操作比較複雜。DOM是文檔對象模型的縮寫,顧名思義就是將整個XML文檔視爲一個對象,DOM解析的原理是先根據XML文檔的內容在內存中建立樹結構,再對樹結構進行解析,這種方式顯然需要更多的內存,但操作簡單且對XPath查詢提供了很好的支持。 第三方庫基本上都是用DOM解析,常用的有:GDataXML,KissXMLRaptureXMLXMLDictionary

  下面的代碼演示瞭如何使用KissXML解析開源中國編號爲44393的文章的相關鏈接。

Objective-C代碼:

#import "ViewController.h"
#import "CDDetailViewController.h"
#import "CDRelativeNews.h"
#import "DDXML.h"

@interface ViewController () <UITableViewDataSource, UITableViewDelegate>

@end

@implementation ViewController {
    UITableView *myTableView;
    // iOS 9開始支持泛型容器(有類型限定的數組、字典等)
    // 可以在Xcode 7中使用這項新的語言特性
    NSMutableArray<CDRelativeNews *> *dataArray;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    self.title = @"相關新聞鏈接";

    myTableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    myTableView.dataSource = self;
    myTableView.delegate = self;
    [self.view addSubview:myTableView];

    [self loadDataModel];
}

- (void)loadDataModel {
    if (!dataArray) {
        dataArray = [NSMutableArray array];
    }

    // 創建統一資源定位符對象
    NSURL *url = [NSURL URLWithString:
        @"http://www.oschina.net/action/api/news_detail?id=44393"];
    // 通過統一資源定位符從服務器獲得XML數據
    NSData *data = [NSData dataWithContentsOfURL:url];
    // 使用NSData對象創建XML文檔對象 文檔對象是將XML在內存中組織成一棵樹
    DDXMLDocument *doc = [[DDXMLDocument alloc] 
        initWithData:data options:0 error:nil];
    // 使用XPath語法從文檔對象模型中查找指定節點
    NSArray *array = [doc nodesForXPath:@"//relative" error:nil];
    // 循環取出節點並對節點下的子節點進行進一步解析
    for (DDXMLNode *node in array) {
        CDRelativeNews *model = [[CDRelativeNews alloc] init];
        // 取出當前節點的子節點並獲取其對應的值
        model.title = [node.children[0] stringValue];
        model.url = [node.children[1] stringValue];
        // 將模型對象添加到數組中
        [dataArray addObject:model];
    }
    // 刷新表格視圖
    [myTableView reloadData];
}

- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return dataArray.count;
}

- (UITableViewCell *) tableView:(UITableView *)tableView 
        cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CELL"];
    if (!cell) {
        cell = [[UITableViewCell alloc] 
            initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"CELL"];
    }

    cell.textLabel.text = dataArray[indexPath.row].title;

    return cell;
}

- (void) tableView:(UITableView *)tableView 
        didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    CDRelativeNews *model = dataArray[indexPath.row];
    CDDetailViewController *detailVC = [[CDDetailViewController alloc] init];
    detailVC.urlStr = model.url;
    [self.navigationController pushViewController:detailVC animated:YES];
}

@end

  用這個例子順便介紹一下如何在Swift中使用Objective-C實現兩種語言的混編。首先還是向項目中添加KissXML第三方庫,這個第三方庫用是Objective-C書寫的。在下面的例子中,我們創建了一個名爲“bridge.h”的頭文件,並在項目的“Build Settings”中找到“Objective-C Bridging Header”選項,將“bridge.h”頭文件的路徑添到此處。

圖10. 設置Objective-C的橋接頭文件

#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異常處理》

  • JSON

      JSON全稱JavaScript對象表達式(JavaScript Object Notation),是目前最流行的存儲和交換文本信息的語法,和XML相比,它更小、更快,更易解析,是一種輕量級的文本數據交換格式。

      JSON的語法規則可以簡單的總結成以下幾條:1. 數據在名/值對中;2. 數據由逗號分隔;3. 花括號保存對象;4. 方括號保存數組。

      例如:

{
  "name" : "駱昊",
  "age" : 35,
  "gender" : true,
  "car" : {
    "brand" : "Touareg",
    "maxSpeed" : 240
  },
  "favorites" : [
    "閱讀",
    "旅遊",
    "象棋"
  ],
  "mistress" : null
}

  JSON中的值可以是:

- 數字(整數或浮點數)
- 字符串(在雙引號中)
- 邏輯值(true 或 false)
- 數組(在方括號中)
- 對象(在花括號中)
- null

  不難看出,JSON用鍵值對的方式描述了JavaScript中的對象,它的形態跟Objective-C的NSDictionary以及Swift中的Dictionary類型是完全一致的,可以通過NSJSONSerialization類的兩個類方法實現JSON數據和字典或數組之間的相互轉換。

// 將數據轉換成對象(通常是數組或字典)
+ (id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError **)error;
// 將數組或字典裝換成JSON數據
+ (NSData *)dataWithJSONObject:(id)obj options:(NSJSONWritingOptions)opt error:(NSError **)error

  通過服務器獲得JSON數據後,最終需要將它轉換成我們程序中的對象。事實上,將JSON轉換成模型對象的操作在開發網絡應用中是很常見的,我們可以使用KVC(Key-Value Coding)的方式將一個字典賦值給一個對象的屬性,代碼如下所示。

說明:KVC通常翻譯爲鍵值編碼,它允許開發者通過名字訪問對象屬性,而無需調用明確的存取方法,這樣就可以實現在運行時而不是在編譯時確定屬性的綁定。這種間接訪問能讓代碼變得更靈活和更具複用性。

Objective-C代碼:

#import <Foundation/Foundation.h>

@interface CDPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, copy) NSArray<NSString *> *friends;

@end
#import "CDPerson.h"

@implementation CDPerson

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
}

- (NSString *) description {
    NSMutableString *mStr = [NSMutableString string];
    for (NSString *friendsName in _friends) {
        [mStr appendString:friendsName];
        [mStr appendString:@" "];
    }
    return [NSString stringWithFormat:@"姓名: %@\n年齡: %ld\n朋友: %@", 
        _name, _age, mStr];
}

@end
#import <Foundation/Foundation.h>
#import "CDPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSDictionary *dict = @{ @"name": @"駱昊", @"age":@(35), 
            @"friends":@[@"金庸", @"古龍", @"黃易"] };
        CDPerson *person = [[CDPerson alloc] init];
        [person setValuesForKeysWithDictionary:dict];

        NSLog(@"%@", person);
    }
    return 0;
}

Swift代碼:

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

  對於對象中關聯了其他對象或者對象的屬性跟字典中的鍵不完全匹配的場景,KVC就顯得不那麼方便了,但是已經有很多優秀的第三方庫幫助我們實現了JSON和模型對象的雙向轉換,下面我們介紹這些第三庫中非常有代表性的JSONModelYYModel

說明:JSONModel和YYModel都是用Objective-C開發的,下面我們也直接用Objective-C代碼爲大家介紹這些東西,不再提供雙語版的講解。

  • JSONModel
#import <Foundation/Foundation.h>
#import "JSONModel.h"

/**產品*/
@interface CDProduct: JSONModel

@property (nonatomic, assign) int id;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) double price;
@property (nonatomic, assign) int amount;

@end
#import "CDProduct.h"

@implementation CDProduct

- (NSString *)description {
    return [NSString stringWithFormat:@"商品編號: %d\n商品名稱: %@\n商品價格: %.2f\n商品數量: %d", 
        _id, _name, _price, _amount];
}

@end
#import <Foundation/Foundation.h>
#import "JSONModel.h"

// 通過協議來限定數組中的元素類型
@protocol CDProduct <NSObject>
@end

/**訂單*/
@interface CDOrder: JSONModel

@property (nonatomic, assign) int orderId;
@property (nonatomic, assign) double totalPrice;
@property (nonatomic, strong) NSArray<CDProduct> *products;

@end
#import "CDOrder.h"

@implementation CDOrder

// 該方法提供字典(JSON)中的鍵和對象屬性之間的映射關係
+ (JSONKeyMapper *)keyMapper {
    return [[JSONKeyMapper alloc] initWithDictionary:@{
        @"order_id": @"orderId",
        @"order_price": @"totalPrice"
    }];
}

- (NSString *)description {
    return [NSString stringWithFormat:@"訂單號: %d 總價: %.2f\n", 
        _orderId, _totalPrice];
}

@end
#import <Foundation/Foundation.h>
#import "CDOrder.h"
#import "CDProduct.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSDictionary *dict = @{
            @"order_id": @(104),
            @"order_price": @(108.85),
            @"products" : @[
                @{
                    @"id": @"123",
                    @"name": @"Product #1",
                    @"price": @(12.95),
                    @"amount": @(2)
                },
                @{
                    @"id": @"137",
                    @"name": @"Product #2",
                    @"price": @(82.95),
                    @"amount": @(1)
                }
            ]
        };

        CDOrder *model = [[CDOrder alloc] initWithDictionary:dict error:nil];
        NSLog(@"%@", model);
        for (CDProduct *product in model.products) {
            NSLog(@"%@", product);
        }
    }
    return 0;
}

  從上面的例子不難看出,JSONModel是有侵入性的,因爲你的模型類必須繼承JSONModel,這些對代碼的複用和遷移多多少少會產生影響。基於這樣的原因,更多的開發者在實現JSON和模型對象轉換時更喜歡選擇非侵入式的MJExtension,這裏我們就不介紹MJExtension,其實它已經做得非常好了,但是當YYModel橫空出世的時候,MJExtension瞬間就成了浮雲。YYModel和MJExtension一樣是沒有侵入性的,你的模型類不要跟第三方庫耦合在一起,而且YYModel提供了比MJExtension更優雅的配置方式,更強大的自動類型轉化能力,當然在性能上YYModel也更優,而且跟MJExtension不在一個數量級上。我們還是用上面的例子來演示如何使用YYModel。

  • YYModel
#import <Foundation/Foundation.h>

@class CDProduct;

/**訂單*/
@interface CDOrder: NSObject

@property (nonatomic, assign) int orderId;
@property (nonatomic, assign) double totalPrice;
@property (nonatomic, strong) NSArray<CDProduct *> *products;

@end
#import "CDOrder.h"

@implementation CDOrder
// 該方法提供屬性名和字典(JSON)中的鍵的映射關係
+ (NSDictionary *) modelCustomPropertyMapper {
    return @{
        @"orderId": @"order_id",
        @"totalPrice": @"order_price"
    };
}

// 該方法提供容器屬性中對象的類型
+ (NSDictionary *) modelContainerPropertyGenericClass {
    return @{
        @"products": NSClassFromString(@"CDProduct")
    };
}

- (NSString *)description {
    return [NSString stringWithFormat:@"訂單號: %d 總價: %.2f\n",
            _orderId, _totalPrice];
}

@end
#import <Foundation/Foundation.h>

/**產品*/
@interface CDProduct: NSObject

@property (nonatomic, assign) int id;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) double price;
@property (nonatomic, assign) int amount;

@end
#import "CDProduct.h"

@implementation CDProduct

- (NSString *)description {
    return [NSString stringWithFormat:@"商品編號: %d\n商品名稱: %@\n商品價格: %.2f\n商品數量: %d", 
        _id, _name, _price, _amount];
}

@end
#import <Foundation/Foundation.h>
#import "CDOrder.h"
#import "YYModel.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSDictionary *dict = @{
           @"order_id": @(104),
           @"order_price": @(108.85),
           @"products": @[
               @{
                   @"id": @"123",
                   @"name": @"Product #1",
                   @"price": @(12.95),
                   @"amount": @(2)
                },
               @{
                   @"id": @"137",
                   @"name": @"Product #2",
                   @"price": @(82.95),
                   @"amount": @(1)
                }
            ]
        };

        CDOrder *order = [CDOrder yy_modelWithDictionary:dict];
        NSLog(@"%@", order);
        for (id product in order.products) {
            NSLog(@"%@", product);
        }
    }
    return 0;
}

第三方庫

  如果要基於HTTP協議開發聯網的iOS應用程序,可以使用優秀的第三方庫來提升開發效率減少重複勞動,這些優秀的第三方庫中的佼佼者當屬AFNetworking

  • AFNetworking

      AFNetworking是基於URL加載系統的網絡框架,很多App都使用它實現聯網功能,它的2.x版本封裝了基於NSURLConnection和NSURLSession的兩套API。目前最新的3.x版本支持基於NSURLConnection聯網,同時引入了iOS 9的新特性。

圖11. URL加載系統的API

  我們重點探討AFURLSessionManager和AFHTTPSessionManager兩個類,因爲它們都是基於NSURLSession的,前者的用法可以在官方文檔上找到,而且用起來稍顯麻煩,AFHTTPSessionManager的用法如下所示。

  下面的代碼演示如何向服務器發送獲取數據的GET請求。

    // 創建HTTP會話管理器對象
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    // AFNetworking默認接受的MIME類型是application/json
    // 有些服務器雖然返回JSON格式的數據但MIME類型設置的是text/html
    // 通過下面的代碼可以指定支持的MIME類型有哪些
    manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:
        @"application/json", @"text/html", nil];
    // 向服務器發送GET請求獲取JSON數據
    [manager
        // 統一資源定位符
        GET:@""
        // 請求參數
        parameters:@{  }
        // 當完成進度變化時回調的Block
        progress:nil
        // 服務器響應成功要回調的Block
        success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        }
        // 服務器響應失敗要回調的Block
        failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        }
    ];

  下面的代碼演示瞭如何向服務器發送上傳數據的POST請求。

    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    [manager
        // 統一資源定位符
        POST:@""
        // 請求參數
        parameters:@{ }
        // 構造請求報文消息體的Block
        constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
            // 可以調用appendPartWithFileData:name:fileName:mimeType:等方法
            // 將上傳給服務器的數據放到請求報文的消息體中
        }
        // 當上傳進度變化時回調的Block
        progress:^(NSProgress * _Nonnull uploadProgress) {

        }
        // 服務器響應成功要回調的Block
        success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {    
        } 
        // 服務器響應失敗要回調的Block
        failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        }
    ];

  AFNetworking還封裝了判斷網絡可達性的功能,使用該功能的代碼如下所示:

    // 創建網絡可達性管理器
    AFNetworkReachabilityManager *manager = [AFNetworkReachabilityManager manager];
    // 設置當網絡狀況發生變化時要回調的Block
    [manager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
        switch (status) {
            case AFNetworkReachabilityStatusNotReachable:
                NSLog(@"沒有網絡連接");
                break;
            case AFNetworkReachabilityStatusReachableViaWiFi:
                NSLog(@"使用Wi-Fi");
                break;
            case AFNetworkReachabilityStatusReachableViaWWAN:
                NSLog(@"使用移動蜂窩網絡");
                break;
            default:
                break;
        }
    }];
    // 開始監控網絡狀況變換
    [manager startMonitoring];
  • MKNetworkingKit

      相比AFNetworking,MKNetworkingKit會顯得小衆一些,但它仍然是一個非常優秀的網絡框架,可以在Github上面找到它,目前的2.x版本也是基於NSURLSession封裝的,放棄了對NSURLConnection的使用。目前能找到的資料基本上介紹的是該框架1.x版本如何使用,如果想了解和使用這個框架,建議訪問作者本人的博客

基於套接字聯網

  套接字是一系列的用於實現網絡通信的標準函數的集合,最有名且被視爲標準的是Berkeley Socket API。Berkeley Socket API是在1983年發佈的BSD 4.2中引入的(後面統一稱之爲BSD套接字),隨後幾乎所有的操作系統都提供了BSD套接字的實現來幫助設備連接互聯網,就連微軟都參照了BSD套接字實現了用於Windows操作系統的Winsock。

說明: BSD是Unix衍生系統,是由加州大學伯克利分校開發和發佈的,如果你想對BSD操作系統的發展史有感性的瞭解,下面這張圖也許會幫助到你。

圖12. Unix操作系統簡史

  BSD套接字通常基於客戶端/服務器模式(C/S模式)來構建網絡應用,這種模式簡單的說就是參與網絡的通信的要麼是服務器,要麼是客戶機,最經典的例子就是通過瀏覽器訪問Web服務器,Web服務器提供資源而瀏覽器作爲客戶機請求獲得這些資源。套接字通信通常使用TCP或UDP作爲傳輸協議,如前所述TCP提供了可靠通信的保證,UDP則以更小的開銷提供不可靠的傳輸服務,例如視頻流數據對可靠性要求不高就可以選擇使用UDP進行傳輸,這樣可以消除TCP多次握手所帶來的開銷。

  下面的代碼創建一個基於TCP的Echo服務器來演示如何使用套接字實現網絡通信。所謂Echo服務器就是將客戶端發送的消息原封不動的發回去,雖然沒有什麼實際價值,但不失爲一個很好的例子。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

static const short SERVER_PORT = 1234;  // 端口
static const int MAX_Q_LEN = 64;        // 最大隊列長度
static const int MAX_MSG_LEN = 4096;    // 最大消息長度

void change_enter_to_tail_zero(char * const buffer, int pos) {
    for (int i = pos - 1; i >= 0; i--) {
        if (buffer[i] == '\r') {
            buffer[i] = '\0';
            break;
        }
    }
}

int main() {
    // 1. 調用socket函數創建套接字
    // 第一個參數指定使用IPv4協議進行通信(AF_INET6代表IPv6)
    // 第二個參數指定套接字的類型(SOCK_STREAM代表可靠的全雙工通信)
    // 第三個參數指定套接字使用的協議
    // 如果返回值是-1表示創建套接字時發生錯誤 否則返回服務器套接字文件描述符
    int serverSocketFD = socket(AF_INET, SOCK_STREAM, 0);
    if (serverSocketFD < 0) {
        perror("無法創建套接字!!!\n");
        exit(1);
    }

    // 代表服務器地址的結構體
    struct sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(SERVER_PORT);
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);

    // 2. 將套接字綁定到指定的地址和端口
    // 第一個參數指定套接字文件描述符
    // 第二個參數是上面代表地址的結構體變量的地址
    // 第三個參數是上面代表地址的結構體佔用的字節數
    // 如果返回值是-1表示綁定失敗
    int ret = bind(serverSocketFD, (struct sockaddr *)&serverAddr,
                   sizeof serverAddr);
    if (ret < 0) {
        perror("無法將套接字綁定到指定的地址!!!\n");
        close(serverSocketFD);
        exit(1);
    }

    // 3. 開啓監聽(監聽客戶端的連接)
    ret = listen(serverSocketFD, MAX_Q_LEN);
    if (ret < 0) {
        perror("無法開啓監聽!!!\n");
        close(serverSocketFD);
        exit(1);
    }

    bool serverIsRunning = true;
    while(serverIsRunning) {
        // 代表客戶端地址的結構體
        struct sockaddr_in clientAddr;
        socklen_t clientAddrLen = sizeof clientAddr;
        // 4. 接受客戶端的連接(從隊列中取出第一個連接請求)
        // 如果返回-1表示發生錯誤 否則返回客戶端套接字文件描述符
        // 該方法是一個阻塞方法 如果隊列中沒有連接就會一直阻塞
        int clientSocketFD = accept(serverSocketFD,
                    (struct sockaddr *)&clientAddr, &clientAddrLen);
        bool clientConnected = true;
        if (clientSocketFD < 0) {
            perror("接受客戶端連接時發生錯誤!!!\n");
            clientConnected = false;
        }

        while (clientConnected) {
            // 接受數據的緩衝區
            char buffer[MAX_MSG_LEN + 1];
            // 5. 接收客戶端發來的數據
            ssize_t bytesToRecv = recv(clientSocketFD, buffer,
                        sizeof buffer - 1, 0);
            if (bytesToRecv > 0) {
                buffer[bytesToRecv] = '\0';
                change_enter_to_tail_zero(buffer, (int)bytesToRecv);
        printf("%s\n", buffer);
                // 如果收到客戶端發來的bye消息服務器主動關閉
                if (!strcmp(buffer, "bye\r\n")) {
                    serverIsRunning = false;
                    clientConnected = false;
                }
                // 6. 將消息發回到客戶端
                ssize_t bytesToSend = send(clientSocketFD, buffer, 
                        bytesToRecv, 0);
                if (bytesToSend > 0) {
                    printf("Echo message has been sent.\n");
                }
            }
            else {
                printf("client socket closed!\n");
                clientConnected = false;
            }   
        }
        // 7. 關閉客戶端套接字
        close(clientSocketFD);
    }
    // 8. 關閉服務器套接字
    close(serverSocketFD);
    return 0;
}

  我們可以在終端中用telnet來測試上面的代碼,效果如下圖所示。

圖13. 在終端中用telnet測試Echo服務器

  上面的Echo服務器只能支持一個客戶端請求,當有多個客戶端連接到服務器時需要排隊等待,很明顯是不合適的。可以使用GCD(Grand Central Dispatch)來構建多線程服務器,將服務器和客戶端傳數據的那段代碼放到一個線程中執行。

#import <Foundation/Foundation.h>
#import <arpa/inet.h>

static const short SERVER_PORT = 1234;  // 端口
static const int MAX_Q_LEN = 64;        // 最大隊列長度
static const int MAX_MSG_LEN = 4096;    // 最大消息長度

void change_enter_to_tail_zero(char * const buffer, int pos) {
    for (int i = pos - 1; i >= 0; i--) {
        if (buffer[i] == '\r') {
            buffer[i] = '\0';
            break;
        }
    }
}

void handle_client_connection(int clientSocketFD) {
    bool clientConnected = true;
    while (clientConnected) {
        char buffer[MAX_MSG_LEN + 1];
        ssize_t bytesToRecv = recv(clientSocketFD, buffer,
                                   sizeof buffer - 1, 0);
        if (bytesToRecv > 0) {
            buffer[bytesToRecv] = '\0';
            change_enter_to_tail_zero(buffer, (int)bytesToRecv);
            printf("%s\n", buffer);
            if (!strcmp(buffer, "bye\r\n")) {
                clientConnected = false;
            }
            ssize_t bytesToSend = send(clientSocketFD, buffer,
                                       bytesToRecv, 0);
            if (bytesToSend > 0) {
                printf("Echo message has been sent.\n");
            }
        }
        else {
            printf("client socket closed!\n");
            clientConnected = false;
        }
    }
    close(clientSocketFD);
}

int main() {
    int serverSocketFD = socket(AF_INET, SOCK_STREAM, 0);
    if (serverSocketFD < 0) {
        perror("無法創建套接字!!!\n");
        exit(1);
    }

    struct sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(SERVER_PORT);
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);

    int ret = bind(serverSocketFD, (struct sockaddr *)&serverAddr,
                   sizeof serverAddr);
    if (ret < 0) {
        perror("無法將套接字綁定到指定的地址!!!\n");
        close(serverSocketFD);
        exit(1);
    }

    ret = listen(serverSocketFD, MAX_Q_LEN);
    if (ret < 0) {
        perror("無法開啓監聽!!!\n");
        close(serverSocketFD);
        exit(1);
    }

    while(true) {
        struct sockaddr_in clientAddr;
        socklen_t clientAddrLen = sizeof clientAddr;
        int clientSocketFD = accept(serverSocketFD,
                                    (struct sockaddr *)&clientAddr, &clientAddrLen);
        if (clientSocketFD < 0) {
            perror("接受客戶端連接時發生錯誤!!!\n");
        }
        else {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                handle_client_connection(clientSocketFD);
            });
        }
    }
    return 0;
}

基於蘋果底層API聯網

  蘋果底層提供了叫做CFNetwork的API來實現聯網的功能,它對BSD套接字做了一些必要的封裝,提供了更爲簡便的獲取網絡地址信息和檢查網絡狀態的方法,可以整合Run-Loop來避開使用多線程,此外CFNetwork還對FTP協議、HTTP協議進行了面向對象的封裝,你可以在不瞭解這些協議實現細節的情況下來使用這些協議。

  我們用CFNetwork來爲上面的Echo服務器寫一個專門的客戶端,這一次我們用Objective-C來做一些面向對象的封裝,代碼如下所示。

#import <Foundation/Foundation.h>

typedef NS_ENUM(NSUInteger, CFNetworkServerErrorCode) {
    NoError,
    SocketError,
    ConnectError
};

static const int kMaxMessageLength = 4096;
static const int kConnectionTimeout = 15;

@interface CDEchoClient : NSObject

@property (nonatomic) NSUInteger errorCode;
@property (nonatomic) CFSocketRef socket;

- (instancetype) initWithAddress:(NSString *) address port:(int) port;

- (NSString *) sendMessage:(NSString *) msg;

@end
#import "CDEchoClient.h"
#import <arpa/inet.h>

@implementation CDEchoClient

- (instancetype)initWithAddress:(NSString *)address port:(int)port {
    // 調用CFSocketCreate函數通過指定的協議和類型創建套接字
    // 第一個參數通常是NULL(使用默認的對象內存分配器)
    // 第二個參數AF_INET表示使用IPv4(如果指定成0或負數默認也是AF_INET)
    // 第三個參數是套接字類型(如果指定成0或負數默認也是SOCK_STREAM)
    // 第四個參數是協議(如果前一個參數是SOCK_STREAM默認爲TCP, 前一個參數是SOCK_DGRAM默認爲UDP)
    // 第五個參數和第六個參數是回調類型和回調函數
    // 第七個參數是保存數據的上下文環境
    self.socket = CFSocketCreate(NULL, AF_INET, SOCK_STREAM, 
        IPPROTO_TCP, 0,  NULL, NULL);
    if (!self.socket) {
        self.errorCode = SocketError;
    }
    else {
        // 表示服務器地址的結構體
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_len = sizeof(servaddr);
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(port);
        // 將字符串形式的地址轉換成網絡地址的結構體變量
        inet_pton(AF_INET, [address cStringUsingEncoding:NSUTF8StringEncoding], 
            &servaddr.sin_addr);
        // 將地址結構體轉換成CFDataRef類型
        CFDataRef connectAddr = CFDataCreate(NULL, 
            (unsigned char *)&servaddr, sizeof servaddr);
        // 調用CFSocketConnectToAddress函數連接遠端套接字(服務器)
        // 其中第三個參數代表連接的超時時間以秒爲單位
        // 如果函數返回kCFSocketSuccess表示連接成功 否則就是連接失敗或超時
        if (!connectAddr || CFSocketConnectToAddress(
            self.socket, connectAddr, kConnectionTimeout) != kCFSocketSuccess) {
            self.errorCode = ConnectError;
        }
    }
    return self;
}

- (NSString *) sendMessage:(NSString *) msg {
    char buffer[kMaxMessageLength];
    // 獲得本地套接字
    CFSocketNativeHandle sock = CFSocketGetNative(self.socket);
    const char *mess = [msg cStringUsingEncoding:NSUTF8StringEncoding];
    // 向服務器發送Echo消息
    send(sock, mess, strlen(mess) + 1, 0);
    // 接受服務器返回的消息
    recv(sock, buffer, sizeof buffer, 0);
    return [NSString stringWithUTF8String:buffer];
}

- (void) dealloc {
    if (self.socket) {
        CFRelease(self.socket);
        self.socket = NULL;
    }
}
@end

  用Storyboard做一個用戶界面。

圖14. 在Xcode中用Storyboard創建用戶界面

#import "ViewController.h"
#import "CDEchoClient.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UITextField *msgField;
@property (weak, nonatomic) IBOutlet UILabel *echoMsgLabel;

@end

@implementation ViewController {
    CDEchoClient *client;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    client = [[CDEchoClient alloc] initWithAddress:@"127.0.0.1" port:1234];
}

- (IBAction)sendButtonClicked:(id)sender {
    // 發送bye消息會斷開與服務器的連接 不能再發送消息
    if (client && client.errorCode == NoError) {
        NSString *msg = [self.msgField.text stringByTrimmingCharactersInSet:
            [NSCharacterSet whitespaceCharacterSet]];
        if (msg.length > 0) {
            [self.msgField resignFirstResponder];
            self.echoMsgLabel.text = [client sendMessage:msg];
        }
    }
    else {
        NSLog(@"Cannot send message!!!");
    }
}

@end

  我們可以先運行上面用套機字編寫的Echo服務器,再通過模擬器或真機來運行Echo客戶端,運行效果如下圖所示:

圖15. Echo客戶端運行效果

基於Bonjour的網絡設備發現

  Bonjour是Apple推出的適用於局域網(LAN)的零配置網絡協議,主要的目的是在缺少中心服務器的情況下解決網絡設備的IP獲取(在沒有DHCP服務的情況下用隨機的方式分配IP地址),名稱解析(用mDNS取代傳統的DNS服務)和服務發現(通過本地域名如“名稱.服務類型.傳輸協議類型.local.”中的服務類型來發現服務)等關鍵問題。想要對Bonjour有一個全面的瞭解,建議訪問蘋果官方網站上的Bonjour for Developers專區。

  發佈Bonjour服務

#import <Foundation/Foundation.h>

@interface CDMyBonjourService : NSObject <NSNetServiceDelegate> {
    NSNetService *service;
}

- (void) startServiceOfType:(NSString *) type port:(int) port;
- (void) stopService;

@end
#import "CDMyBonjourService.h"

@implementation CDMyBonjourService

- (void)startServiceOfType:(NSString *) type port:(int) port {
    service = [[NSNetService alloc] initWithDomain:@""
            type:type name:@"" port:port];
    if (service) {
        service.delegate = self;
        [service publish];
    }
}

- (void) stopService {
    [service stop];
}

#pragma mark NSNetServiceDelegate回調方法

- (void)netServiceWillPublish:(NSNetService *)sender {
}

- (void)netServiceDidPublish:(NSNetService *)sender {
}

- (void)netService:(NSNetService *)sender 
      didNotPublish:(NSDictionary<NSString *, NSNumber *> *)errorDict {
}

- (void)netServiceWillResolve:(NSNetService *)sender {
}

- (void)netServiceDidResolveAddress:(NSNetService *)sender {
}

- (void)netService:(NSNetService *)sender 
      didNotResolve:(NSDictionary<NSString *, NSNumber *> *)errorDict {
}

- (void)netServiceDidStop:(NSNetService *)sender {
}

- (void)netService:(NSNetService *)sender didUpdateTXTRecordData:(NSData *)data {
}

- (void)netService:(NSNetService *)sender 
      didAcceptConnectionWithInputStream:(NSInputStream *)inputStream 
      outputStream:(NSOutputStream *)outputStream  {
}
@end

  發現Bonjour服務

#import <Foundation/Foundation.h>

@interface CDMyBonjourServiceBrowser: NSObject <NSNetServiceBrowserDelegate> {
    NSNetServiceBrowser *serviceBrowser;
    NSMutableArray<NSNetService *> *servicesArray;
}

- (void) startBrowsingForType:(NSString *) type;
- (void) stopBrowsing;

@end
#import "CDMyBonjourServiceBrowser.h"

@implementation CDMyBonjourServiceBrowser

- (void) startBrowsingForType:(NSString *)type {
    serviceBrowser = [[NSNetServiceBrowser alloc] init];
    [serviceBrowser searchForServicesOfType:type inDomain:@""];
}

- (void) stopBrowsing {
    [serviceBrowser stop];
    [servicesArray removeAllObjects];
}

#pragma mark NSNetServiceBrowserDelegate回調方法

- (void)netServiceBrowserWillSearch:(NSNetServiceBrowser *)browser {
}

- (void)netServiceBrowserDidStopSearch:(NSNetServiceBrowser *)browser {
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser 
      didNotSearch:(NSDictionary<NSString *, NSNumber *> *)errorDict {
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser         
      didFindDomain:(NSString *)domainString moreComing:(BOOL)moreComing {
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser 
      didFindService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
    if (!servicesArray) {
        servicesArray = [NSMutableArray array];
    }
    // 將發現的服務添加到數組中
    [servicesArray addObject:aNetService];
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser 
      didRemoveDomain:(NSString *)domainString moreComing:(BOOL)moreComing {
}


- (void)netServiceBrowser:(NSNetServiceBrowser *)browser 
      didRemoveService:(NSNetService *)service moreComing:(BOOL)moreComing {
}

@end

注意:在發佈服務之前應該準備好對應的服務並啓動它。不過NSNetService的publish方法並不依賴它所發佈的服務,不管要發佈的服務是否就緒,該方法都可以成功的將服務發佈出去,但是如果服務沒有就緒,要使用這個服務的客戶端就會發現這個發佈出來的服務是個無效的服務。

  在上面的例子中,我們將發現的服務裝在一個數組中,當我們需要使用這些服務時,可以通過NSNetService對象解析出服務的地址和端口,對於基於HTTP的服務,我們可以使用蘋果的URL加載系統或者AFNetworking這樣的第三方庫來使用服務,對於其他的服務我們可以使用套接字或CFNetwork API來使用服務,對於使用同一個局域網中提供的服務,這種方式不是更加簡單方便嗎?

總結

  到此爲止,我們對iOS網絡應用開發的方方面面做了一個走馬觀花的講解,當然iOS開發中跟網絡相關的知識還遠不止這些,例如如何通過證書保證網絡通信的安全,如何有效的使用緩存來提升性能和減少網絡開銷以及URL緩存的過期模型和驗證模型等,這些內容打算以專題的形式在後面爲大家呈現。上面內容所有的代碼都可以在我的Github上找到。

發佈了136 篇原創文章 · 獲贊 4229 · 訪問量 302萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章