iOS開發(fā)筆記 - 網(wǎng)絡(luò)篇

計(jì)算機(jī)網(wǎng)絡(luò)基礎(chǔ)##

計(jì)算機(jī)網(wǎng)絡(luò)是多臺(tái)獨(dú)立自主的計(jì)算機(jī)互聯(lián)而成的系統(tǒng)的總稱迁筛,最初建立計(jì)算機(jī)網(wǎng)絡(luò)的目的是實(shí)現(xiàn)信息傳遞和資源共享丹泉。

如果說(shuō)計(jì)算機(jī)是第二次世界大戰(zhàn)的產(chǎn)物,那么計(jì)算機(jī)網(wǎng)絡(luò)則是美蘇冷戰(zhàn)的產(chǎn)物。20世紀(jì)60年代初期陋气,美國(guó)國(guó)防部領(lǐng)導(dǎo)的ARPA提出研究一種嶄新的、能夠適應(yīng)現(xiàn)代戰(zhàn)爭(zhēng)的迫筑、生存性很強(qiáng)的通信系統(tǒng)并藉此來(lái)應(yīng)對(duì)蘇聯(lián)核攻擊的威脅宪赶,這個(gè)決定促使了分組交換網(wǎng)的誕生,也奠定今天計(jì)算機(jī)網(wǎng)絡(luò)的原型脯燃,這是計(jì)算機(jī)網(wǎng)絡(luò)發(fā)展史上第一個(gè)里程碑式的事件搂妻。

第二個(gè)里程碑式的事件是20世紀(jì)80年代初,國(guó)際標(biāo)準(zhǔn)化組織(ISO)提出了OSI/RM(開放系統(tǒng)互聯(lián)參考模型)辕棚,該模型定義了計(jì)算機(jī)網(wǎng)絡(luò)的分層體系結(jié)構(gòu)欲主,雖然該模型并沒(méi)有成為網(wǎng)絡(luò)設(shè)備制造商遵循的國(guó)際標(biāo)準(zhǔn)邓厕,但用分層的思想解決復(fù)雜系統(tǒng)設(shè)計(jì)問(wèn)題的做法已經(jīng)深入人心。成為事實(shí)標(biāo)準(zhǔn)(de facto standard)的是TCP/IP模型扁瓢,而TCP/IP協(xié)議簇(協(xié)議簇通常指彼此相關(guān)聯(lián)的一系列協(xié)議的總稱)也是構(gòu)成今天的Internet的基石详恼。不同于OSI/RM的七層結(jié)構(gòu),TCP/IP模型是一個(gè)四層模型引几,從上到下依次是應(yīng)用層昧互、傳輸層、網(wǎng)絡(luò)層伟桅、物理鏈路層敞掘。值得一提的是,傳輸層可以使用兩種不同的協(xié)議楣铁,一個(gè)是面向連接的傳輸控制協(xié)議(TCP)玖雁,另一個(gè)是無(wú)連接的用戶數(shù)據(jù)報(bào)協(xié)議(UDP),我們耳熟能詳?shù)牡膮f(xié)議如HTTP盖腕、FTP赫冬、Telnet、POP3赊堪、DHCP面殖、DNS、ICQ等都屬于應(yīng)用層的協(xié)議哭廉,它們要么構(gòu)建在TCP之上脊僚,要么構(gòu)建在UDP之上。

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

計(jì)算機(jī)網(wǎng)絡(luò)發(fā)展史上第三個(gè)里程碑事件應(yīng)該是瀏覽器的問(wèn)世遵绰。20世紀(jì)90年代初辽幌,英國(guó)人Timothy John Berners-Lee發(fā)明了瀏覽器,瀏覽器通過(guò)超文本傳輸協(xié)議(HTTP)跟服務(wù)器交換超文本數(shù)據(jù)椿访,通過(guò)圖形用戶界面顯示從服務(wù)器獲得的超文本數(shù)據(jù)乌企,這一切都讓使用Internet變得無(wú)比簡(jiǎn)單,于是計(jì)算機(jī)網(wǎng)絡(luò)的用戶數(shù)量開始爆炸式的增長(zhǎng)成玫。

基于HTTP協(xié)議聯(lián)網(wǎng)##

在iOS開發(fā)中加酵,如果應(yīng)用程序需要的數(shù)據(jù)不在本地,而是通過(guò)網(wǎng)絡(luò)獲取的文字哭当、圖片猪腕、音視頻等資源,那么我們的應(yīng)用程序就需要聯(lián)網(wǎng)钦勘,對(duì)于這種場(chǎng)景通陈希可以直接使用HTTP(Hyper-Text Transfer Protocol)向提供資源的服務(wù)器發(fā)出請(qǐng)求即可。HTTP協(xié)議對(duì)于很多人來(lái)說(shuō)都不陌生彻采,我們使用瀏覽器訪問(wèn)Web服務(wù)器的時(shí)候使用的基本上都是使用HTTP協(xié)議(有些服務(wù)器需要使用HTTPS腐缤,它是在HTTP下層添加SSL[Secure Socket Layer]捌归,用于安全的傳輸HTTP協(xié)議數(shù)據(jù))。目前越來(lái)越多的應(yīng)用已經(jīng)從瀏覽器延伸到移動(dòng)客戶端岭粤,但是服務(wù)器端并不需要做出任何改變惜索,iOS和Android的應(yīng)用程序也可以通過(guò)HTTP協(xié)議和服務(wù)器通信。

我們先來(lái)解釋一下什么是協(xié)議以及HTTP到底是一個(gè)怎樣的協(xié)議绍在。我們將任何可發(fā)送或接收信息的硬件或程序稱之為實(shí)體门扇,而協(xié)議則是控制兩個(gè)對(duì)等實(shí)體進(jìn)行通信的規(guī)則的集合。簡(jiǎn)單的說(shuō)偿渡,協(xié)議就是通信雙方必須遵循的對(duì)話的標(biāo)準(zhǔn)和規(guī)范臼寄。HTTP是構(gòu)建在TCP之上的協(xié)議,之所以選擇TCP作為底層傳輸協(xié)議是因?yàn)門CP除了可以保證可靠通信之外溜宽,還具備流量控制和擁塞控制的能力吉拳,如果這一點(diǎn)不能理解也不要緊,我么只需要知道HTTP需要可靠的傳輸層協(xié)議的支持就夠了适揉。

HTTP有兩種類型的報(bào)文:請(qǐng)求報(bào)文和響應(yīng)報(bào)文留攒。請(qǐng)求報(bào)文和響應(yīng)報(bào)文都是由三個(gè)部分組成的。我們可以用抓包工具截取請(qǐng)求和響應(yīng)報(bào)文來(lái)看看它們的結(jié)構(gòu)嫉嘀。

圖2. HTTP請(qǐng)求報(bào)文

請(qǐng)求報(bào)文是由請(qǐng)求行炼邀、請(qǐng)求頭和消息體構(gòu)成的。請(qǐng)求行包含了命令(通常是GET或POST)剪侮、資源和協(xié)議版本拭宁;請(qǐng)求頭是鍵值對(duì)映射形式的和請(qǐng)求相關(guān)的信息,如客戶端使用的語(yǔ)言瓣俯、使用的瀏覽器等信息杰标;消息體是客戶端發(fā)給服務(wù)器的數(shù)據(jù);在請(qǐng)求頭和消息體之間有一個(gè)空行彩匕。

圖3. HTTP響應(yīng)報(bào)文

響應(yīng)報(bào)文是由響應(yīng)行腔剂、響應(yīng)頭和消息體構(gòu)成的。響應(yīng)行包含了協(xié)議版本和狀態(tài)碼驼仪;響應(yīng)頭是鍵值對(duì)形式的和響應(yīng)相關(guān)的信息掸犬,如服務(wù)器的軟件版本、時(shí)間日期绪爸、緩存策略登渣、響應(yīng)內(nèi)容類型等信息;消息體是服務(wù)器發(fā)給客戶端的數(shù)據(jù)毡泻;在響應(yīng)頭和消息體之間有一個(gè)空行。

抓包工具###

  • Charles
圖4. Charles啟動(dòng)界面

Charles是一個(gè)HTTP代理服務(wù)器粘优,HTTP監(jiān)視器仇味,反轉(zhuǎn)代理服務(wù)器呻顽,它允許一個(gè)開發(fā)者查看所有連接互聯(lián)網(wǎng)的HTTP通信。很多iOS開發(fā)者都選擇Charles作為抓包工具來(lái)獲取和測(cè)試網(wǎng)絡(luò)接口丹墨。通過(guò)下圖所示的菜單項(xiàng)可以將Charles設(shè)置為Mac系統(tǒng)的HTTP代理廊遍,所有的HTTP數(shù)據(jù)都會(huì)被Charles截獲。

圖5. 將Charles設(shè)置Mac系統(tǒng)HTTP代理的菜單項(xiàng)

當(dāng)然贩挣,還可以將Charles設(shè)置為手機(jī)的代理喉前,只要讓安裝了Charles的Mac系統(tǒng)和手機(jī)使用相同的網(wǎng)絡(luò),再將手機(jī)無(wú)線局域網(wǎng)的代理服務(wù)器設(shè)置為Mac系統(tǒng)的IP地址即可王财,這樣手機(jī)上的HTTP數(shù)據(jù)也會(huì)被截獲卵迂。

圖6. 手機(jī)設(shè)置無(wú)線局域網(wǎng)代理服務(wù)器
圖7. 指定代理服務(wù)器的IP地址和端口號(hào)
  • Wireshark

Wireshark(原名Ethereal,1998年由美國(guó)Gerald Combs首創(chuàng)研發(fā)绒净,由世界各國(guó)100多位網(wǎng)絡(luò)專家和軟件人員共同參與此軟件的升級(jí)完善和維護(hù)见咒,2006年5月更名為Wireshark)是一個(gè)非常專業(yè)的網(wǎng)絡(luò)數(shù)據(jù)包截取和分析軟件,它直接截獲經(jīng)過(guò)網(wǎng)卡的數(shù)據(jù)挂疆,并盡可能顯示出最為詳細(xì)的數(shù)據(jù)包信息改览,是協(xié)議分析的利器。Wireshark比Charles更底層更專業(yè)缤言,但是如果只做HTTP數(shù)據(jù)分析宝当,Charles用起來(lái)還是非常簡(jiǎn)單方便的。

圖8. Wireshark運(yùn)行效果圖

相關(guān)API###

  • NSURL

NSURL是代表統(tǒng)一資源定位符(Universal Resource Locator胆萧,URL)的類庆揩。URL是互聯(lián)網(wǎng)上標(biāo)準(zhǔn)資源的地址,互聯(lián)網(wǎng)上的每個(gè)資源都有一個(gè)唯一的與之對(duì)應(yīng)的URL鸳碧。

URL的格式如下所示:

協(xié)議://域名或IP地址:端口號(hào)/路徑/資源

下面是百度logo的URL:

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

說(shuō)明:端口號(hào)是對(duì)IP地址的擴(kuò)展盾鳞。例如我們的服務(wù)器只有一個(gè)IP地址,但是我們可以在這臺(tái)服務(wù)器上開設(shè)多個(gè)服務(wù)瞻离,如Web服務(wù)腾仅、郵件服務(wù)和數(shù)據(jù)庫(kù)服務(wù),當(dāng)服務(wù)器收到一個(gè)請(qǐng)求時(shí)會(huì)根據(jù)端口號(hào)來(lái)區(qū)分到底請(qǐng)求的是Web服務(wù)還是郵件服務(wù)套利,或者是數(shù)據(jù)庫(kù)服務(wù)推励。我們?cè)跒g覽器中輸入U(xiǎn)RL的時(shí)候通常都會(huì)省略端口號(hào),因?yàn)镠TTP協(xié)議默認(rèn)使用80端口肉迫,也就是說(shuō)除非你訪問(wèn)的Web服務(wù)器沒(méi)有使用80端口验辞,你才需要輸入相應(yīng)的端口號(hào)。

下面的代碼演示了如何在iOS應(yīng)用中通過(guò)URL獲取網(wǎng)絡(luò)數(shù)據(jù)喊衫。

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協(xié)議聯(lián)網(wǎng),如果要用需要修改項(xiàng)目的Info.plist文件,添加“App Transport Security Settings”鍵壳贪,其類型是Dictionary陵珍;在“App Transport Security Settings”下添加一個(gè)子元素,鍵是“Allow Arbitrary Loads”违施,類型是Boolean互纯,將其值設(shè)置為YES。

  • NSURLRequest / NSMutableURLRequest

NSURLRequest / NSMutableURLRequest代表了客戶端向服務(wù)器發(fā)送的HTTP請(qǐng)求磕蒲。通過(guò)請(qǐng)求對(duì)象可以設(shè)置請(qǐng)求的方法留潦、請(qǐng)求頭、緩存策略辣往、超時(shí)時(shí)間兔院、消息體等。

  • NSURLResponse

NSURLResponse代表了服務(wù)器發(fā)送給客戶端的HTTP響應(yīng)排吴。

  • NSURLConnection

在iOS 7以前秆乳,基于HTTP協(xié)議聯(lián)網(wǎng)的操作最終都要由NSURLConnection類來(lái)完成,該類主要有兩個(gè)方法钻哩,一個(gè)用于發(fā)送同步請(qǐng)求屹堰,一個(gè)用于發(fā)送異步請(qǐng)求。

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

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

提示:同步請(qǐng)求是阻塞式請(qǐng)求街氢,這就意味著同步請(qǐng)求的方法在返回?cái)?shù)據(jù)之前會(huì)一直阻塞扯键;異步請(qǐng)求是非阻塞式請(qǐng)求,當(dāng)服務(wù)器返回?cái)?shù)據(jù)時(shí)可以回調(diào)的方式對(duì)數(shù)據(jù)進(jìn)行處理珊肃。如果明白這一點(diǎn)荣刑,就很容易理解為什么上面的同步請(qǐng)求方法會(huì)返回NSData指針,而異步請(qǐng)求方法沒(méi)有返回值但有一個(gè)Block類型的參數(shù)(Block最適合用來(lái)書寫回調(diào)代碼)伦乔。

  • NSURLSession

2013的WWDC厉亏,蘋果推出了NSURLConnection的繼任者NSURLSession。與NSURLConnection相比烈和,NSURLsession最直接的改進(jìn)就是可以配置每個(gè)會(huì)話(session)的緩存爱只、協(xié)議、cookie以及證書策略(credential policy)等招刹,而且你可以跨程序共享這些信息恬试。每個(gè)NSURLSession對(duì)象都由一個(gè)NSURLSessionConfiguration對(duì)象來(lái)進(jìn)行初始化,NSURLSessionConfiguration對(duì)象代表了會(huì)話的配置以及一些用來(lái)增強(qiáng)移動(dòng)設(shè)備上性能的新選項(xiàng)疯暑。

可以通過(guò)NSURLSession創(chuàng)建NSURLSessionTask(會(huì)話任務(wù))训柴,會(huì)話任務(wù)有三個(gè)子類對(duì)應(yīng)不同的場(chǎng)景,分別是:NSURLSessionDataTask(獲取數(shù)據(jù)的任務(wù))妇拯、NSURLSessionDownloadTask(下載任務(wù))和NSURLSessionUploadTask(上傳任務(wù))幻馁,我們通過(guò)HTTP協(xié)議可以完成的操作都屬于這三類任務(wù)之一。NSURLSessionTask主要有三個(gè)方法,分別是:resume(恢復(fù)任務(wù))宣赔、suspend(掛起任務(wù))和cancel(取消任務(wù))预麸。

  • NSURLSessionConfiguration

如前面所述,NSURLSessionConfiguration代表了會(huì)話的配置儒将,該類的三個(gè)創(chuàng)建對(duì)象的類方法很好的詮釋了NSURLSession類設(shè)計(jì)時(shí)所考慮的不同的使用場(chǎng)景。

// 返回一個(gè)標(biāo)準(zhǔn)的配置对蒲,標(biāo)準(zhǔn)配置會(huì)使用默認(rèn)的緩存策略钩蚊、超時(shí)時(shí)間等
+ (NSURLSessionConfiguration *)defaultSessionConfiguration;
// 返回一個(gè)臨時(shí)性的配置,這個(gè)配置中不會(huì)對(duì)緩存蹈矮,Cookie和證書進(jìn)行持久化存儲(chǔ)
// 對(duì)于實(shí)現(xiàn)無(wú)痕瀏覽這種功能來(lái)說(shuō)這種配置是非常理想的
+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration;
// 返回一個(gè)后臺(tái)配置
// 后臺(tái)會(huì)話不同于普通的會(huì)話砰逻,它甚至可以在應(yīng)用程序掛起,退出或者崩潰的情況下運(yùn)行上傳和下載任務(wù)
// 初始化時(shí)指定的標(biāo)識(shí)符泛鸟,被用于向任何可能在進(jìn)程外恢復(fù)后臺(tái)傳輸?shù)氖刈o(hù)進(jìn)程(daemon)提供上下文
+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:
      (NSString *)identifier

數(shù)據(jù)解析###

通過(guò)HTTP從服務(wù)器獲得的數(shù)據(jù)通常都是JSON格式或XML格式的蝠咆,下面對(duì)這兩種數(shù)據(jù)格式做一個(gè)簡(jiǎn)單的介紹。

  • XML

XML全稱可擴(kuò)展標(biāo)記語(yǔ)言(eXtensible Markup Language)北滥,被設(shè)計(jì)用來(lái)傳輸和存儲(chǔ)數(shù)據(jù)刚操。在JSON被廣泛使用之前,XML是異構(gòu)系統(tǒng)之間交換數(shù)據(jù)的事實(shí)標(biāo)準(zhǔn)再芋,它是一種具有自我描述能力的傳輸數(shù)據(jù)的標(biāo)記語(yǔ)言菊霜,如下所示。

<?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文檔形成一種樹結(jié)構(gòu)济赎,它必須包含根元素鉴逞。該元素是所有其他元素的父元素。這棵樹從根部開始司训,并擴(kuò)展到樹的最底端构捡,如下圖所示。

圖9. XML樹結(jié)構(gòu)
<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的語(yǔ)法規(guī)則跟其他標(biāo)簽語(yǔ)言(如HTML)基本一致壳猜,不過(guò)需要注意以下幾條:

1. 所有的XML元素都必須有一個(gè)關(guān)閉標(biāo)簽
2. XML標(biāo)簽對(duì)大小寫敏感
3. XML必須正確嵌套
4. XML文檔必須有根元素
5. XML屬性值必須加引號(hào)
6. XML中的特殊字符要使用實(shí)體引用
7. XML中的注釋是<!-- -->

在XML文檔中查找信息可以使用XPath表達(dá)式勾徽,我們來(lái)看一個(gè)例子。

<?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語(yǔ)法表

表達(dá)式 描述 例子 結(jié)果
nodename 選取此節(jié)點(diǎn)的所有子節(jié)點(diǎn) bookstore 選取bookstore的所有子節(jié)點(diǎn)
/ 從根節(jié)點(diǎn)選取 /bookstore 選取根元素bookstore
// 從匹配選擇的當(dāng)前節(jié)點(diǎn)選擇文檔中的節(jié)點(diǎn)蓖谢,而不考慮它們的位置 //book 選取所有 book 子元素捂蕴,而不管它們?cè)谖臋n中的位置
. 選取當(dāng)前節(jié)點(diǎn)
.. 選取當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)
@ 選取屬性 //@lang 選取名為lang的所有屬性

XPath的例子

路徑表達(dá)式 結(jié)果
/bookstore/book[1] 選取屬于bookstore子元素的第一個(gè)book元素。
/bookstore/book[last()] 選取屬于bookstore子元素的最后一個(gè)book元素闪幽。
/bookstore/book[last()-1] 選取屬于bookstore子元素的倒數(shù)第二個(gè)book元素啥辨。
/bookstore/book[position()<3] 選取最前面的兩個(gè)屬于bookstore元素的子元素的 book元素。
//title[@lang] 選取所有擁有名為lang的屬性的title元素盯腌。
//title[@lang='eng'] 選取所有title元素溉知,且這些元素?fù)碛兄禐閑ng的lang屬性。
/bookstore/book[price>35.00] 選取bookstore元素的所有book元素,且其中的 price元素的值須大于35.00级乍。
/bookstore/book[price>35.00]/title 選取bookstore元素中的book元素的所有 title元素舌劳,且其中的price元素的值須大于35.00。

提示:如果對(duì)上面很多概念不理解或者想對(duì)XML有一個(gè)更全面的了解玫荣,建議訪問(wèn)RUNOOB.COM獲得更多的信息甚淡。

解析XML數(shù)據(jù)主要有兩種方式:SAX和DOM。SAX解析屬于事件驅(qū)動(dòng)型的順序解析捅厂,即從上至下解析XML文件贯卦,遇到標(biāo)記、屬性焙贷、注釋撵割、內(nèi)容等都會(huì)引發(fā)事件回調(diào),蘋果原生的NSXMLParser就屬于這種類型的解析辙芍,其優(yōu)點(diǎn)在于速度快啡彬,內(nèi)存占用少,但是操作比較復(fù)雜故硅。DOM是文檔對(duì)象模型的縮寫庶灿,顧名思義就是將整個(gè)XML文檔視為一個(gè)對(duì)象,DOM解析的原理是先根據(jù)XML文檔的內(nèi)容在內(nèi)存中建立樹結(jié)構(gòu)契吉,再對(duì)樹結(jié)構(gòu)進(jìn)行解析跳仿,這種方式顯然需要更多的內(nèi)存,但操作簡(jiǎn)單且對(duì)XPath查詢提供了很好的支持捐晶。 第三方庫(kù)基本上都是用DOM解析菲语,常用的有:GDataXML,KissXML惑灵,RaptureXMLXMLDictionary山上。

下面的代碼演示了如何使用KissXML解析開源中國(guó)編號(hào)為44393的文章的相關(guān)鏈接。

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開始支持泛型容器(有類型限定的數(shù)組英支、字典等)
    // 可以在Xcode 7中使用這項(xiàng)新的語(yǔ)言特性
    NSMutableArray<CDRelativeNews *> *dataArray;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.title = @"相關(guān)新聞鏈接";
    
    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];
    }
    
    // 創(chuàng)建統(tǒng)一資源定位符對(duì)象
    NSURL *url = [NSURL URLWithString:
        @"http://www.oschina.net/action/api/news_detail?id=44393"];
    // 通過(guò)統(tǒng)一資源定位符從服務(wù)器獲得XML數(shù)據(jù)
    NSData *data = [NSData dataWithContentsOfURL:url];
    // 使用NSData對(duì)象創(chuàng)建XML文檔對(duì)象 文檔對(duì)象是將XML在內(nèi)存中組織成一棵樹
    DDXMLDocument *doc = [[DDXMLDocument alloc] 
        initWithData:data options:0 error:nil];
    // 使用XPath語(yǔ)法從文檔對(duì)象模型中查找指定節(jié)點(diǎn)
    NSArray *array = [doc nodesForXPath:@"http://relative" error:nil];
    // 循環(huán)取出節(jié)點(diǎn)并對(duì)節(jié)點(diǎn)下的子節(jié)點(diǎn)進(jìn)行進(jìn)一步解析
    for (DDXMLNode *node in array) {
        CDRelativeNews *model = [[CDRelativeNews alloc] init];
        // 取出當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)并獲取其對(duì)應(yīng)的值
        model.title = [node.children[0] stringValue];
        model.url = [node.children[1] stringValue];
        // 將模型對(duì)象添加到數(shù)組中
        [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

用這個(gè)例子順便介紹一下如何在Swift中使用Objective-C實(shí)現(xiàn)兩種語(yǔ)言的混編佩憾。首先還是向項(xiàng)目中添加KissXML第三方庫(kù),這個(gè)第三方庫(kù)用是Objective-C書寫的干花。在下面的例子中妄帘,我們創(chuàng)建了一個(gè)名為“bridge.h”的頭文件,并在項(xiàng)目的“Build Settings”中找到“Objective-C Bridging Header”選項(xiàng)池凄,將“bridge.h”頭文件的路徑添到此處抡驼。

圖10. 設(shè)置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 = "相關(guān)新聞鏈接"
        
        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 {
            // 用通過(guò)URL獲取的XML數(shù)據(jù)構(gòu)造文檔對(duì)象模型
            // 然后使用XPath語(yǔ)法全文查找relative節(jié)點(diǎn)
            for node in try DDXMLDocument(data: data, options: 0)
                    .nodesForXPath("http://relative") {
                // 將數(shù)組中的元素類型轉(zhuǎn)換為DDXMLNode
                if let relative = node as? DDXMLNode {
                    // 用children方法取DDXMLNode對(duì)象的子節(jié)點(diǎn)的數(shù)組
                    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)
    }
}

說(shuō)明:上面的代碼中使用了Swift 2.x的異常處理機(jī)制,如果不了解可以看看簡(jiǎn)書上的這篇文章《Swift 2.0異常處理》肿仑。

  • JSON

JSON全稱JavaScript對(duì)象表達(dá)式(JavaScript Object Notation)精绎,是目前最流行的存儲(chǔ)和交換文本信息的語(yǔ)法,和XML相比囊骤,它更小、更快雷蹂,更易解析,是一種輕量級(jí)的文本數(shù)據(jù)交換格式杯道。

JSON的語(yǔ)法規(guī)則可以簡(jiǎn)單的總結(jié)成以下幾條:1. 數(shù)據(jù)在名/值對(duì)中匪煌;2. 數(shù)據(jù)由逗號(hào)分隔;3. 花括號(hào)保存對(duì)象党巾;4. 方括號(hào)保存數(shù)組虐杯。

例如:

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

JSON中的值可以是:

- 數(shù)字(整數(shù)或浮點(diǎn)數(shù))
- 字符串(在雙引號(hào)中)
- 邏輯值(true 或 false)
- 數(shù)組(在方括號(hào)中)
- 對(duì)象(在花括號(hào)中)
- null

不難看出,JSON用鍵值對(duì)的方式描述了JavaScript中的對(duì)象昧港,它的形態(tài)跟Objective-C的NSDictionary以及Swift中的Dictionary類型是完全一致的,可以通過(guò)NSJSONSerialization類的兩個(gè)類方法實(shí)現(xiàn)JSON數(shù)據(jù)和字典或數(shù)組之間的相互轉(zhuǎn)換支子。

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

通過(guò)服務(wù)器獲得JSON數(shù)據(jù)后创肥,最終需要將它轉(zhuǎn)換成我們程序中的對(duì)象。事實(shí)上值朋,將JSON轉(zhuǎn)換成模型對(duì)象的操作在開發(fā)網(wǎng)絡(luò)應(yīng)用中是很常見的叹侄,我們可以使用KVC(Key-Value Coding)的方式將一個(gè)字典賦值給一個(gè)對(duì)象的屬性,代碼如下所示昨登。

說(shuō)明:KVC通常翻譯為鍵值編碼趾代,它允許開發(fā)者通過(guò)名字訪問(wèn)對(duì)象屬性,而無(wú)需調(diào)用明確的存取方法丰辣,這樣就可以實(shí)現(xiàn)在運(yùn)行時(shí)而不是在編譯時(shí)確定屬性的綁定撒强。這種間接訪問(wèn)能讓代碼變得更靈活和更具復(fù)用性。

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)

對(duì)于對(duì)象中關(guān)聯(lián)了其他對(duì)象或者對(duì)象的屬性跟字典中的鍵不完全匹配的場(chǎng)景笙什,KVC就顯得不那么方便了飘哨,但是已經(jīng)有很多優(yōu)秀的第三方庫(kù)幫助我們實(shí)現(xiàn)了JSON和模型對(duì)象的雙向轉(zhuǎn)換,下面我們介紹這些第三庫(kù)中非常有代表性的JSONModelYYModel琐凭。

說(shuō)明:JSONModel和YYModel都是用Objective-C開發(fā)的芽隆,下面我們也直接用Objective-C代碼為大家介紹這些東西,不再提供雙語(yǔ)版的講解统屈。

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

/**產(chǎn)品*/
@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:@"商品編號(hào): %d\n商品名稱: %@\n商品價(jià)格: %.2f\n商品數(shù)量: %d", 
        _id, _name, _price, _amount];
}

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

// 通過(guò)協(xié)議來(lái)限定數(shù)組中的元素類型
@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)中的鍵和對(duì)象屬性之間的映射關(guān)系
+ (JSONKeyMapper *)keyMapper {
    return [[JSONKeyMapper alloc] initWithDictionary:@{
        @"order_id": @"orderId",
        @"order_price": @"totalPrice"
    }];
}

- (NSString *)description {
    return [NSString stringWithFormat:@"訂單號(hào): %d 總價(jià): %.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是有侵入性的,因?yàn)槟愕哪P皖惐仨毨^承JSONModel愁憔,這些對(duì)代碼的復(fù)用和遷移多多少少會(huì)產(chǎn)生影響腕扶。基于這樣的原因惩淳,更多的開發(fā)者在實(shí)現(xiàn)JSON和模型對(duì)象轉(zhuǎn)換時(shí)更喜歡選擇非侵入式的MJExtension蕉毯,這里我們就不介紹MJExtension乓搬,其實(shí)它已經(jīng)做得非常好了,但是當(dāng)YYModel橫空出世的時(shí)候代虾,MJExtension瞬間就成了浮云进肯。YYModel和MJExtension一樣是沒(méi)有侵入性的,你的模型類不要跟第三方庫(kù)耦合在一起棉磨,而且YYModel提供了比MJExtension更優(yōu)雅的配置方式江掩,更強(qiáng)大的自動(dòng)類型轉(zhuǎn)化能力,當(dāng)然在性能上YYModel也更優(yōu)乘瓤,而且跟MJExtension不在一個(gè)數(shù)量級(jí)上环形。我們還是用上面的例子來(lái)演示如何使用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)中的鍵的映射關(guān)系
+ (NSDictionary *) modelCustomPropertyMapper {
    return @{
        @"orderId": @"order_id",
        @"totalPrice": @"order_price"
    };
}

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

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

@end
#import <Foundation/Foundation.h>

/**產(chǎn)品*/
@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:@"商品編號(hào): %d\n商品名稱: %@\n商品價(jià)格: %.2f\n商品數(shù)量: %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;
}

第三方庫(kù)###

如果要基于HTTP協(xié)議開發(fā)聯(lián)網(wǎng)的iOS應(yīng)用程序衙傀,可以使用優(yōu)秀的第三方庫(kù)來(lái)提升開發(fā)效率減少重復(fù)勞動(dòng)抬吟,這些優(yōu)秀的第三方庫(kù)中的佼佼者當(dāng)屬AFNetworking

  • AFNetworking

AFNetworking是基于URL加載系統(tǒng)的網(wǎng)絡(luò)框架统抬,很多App都使用它實(shí)現(xiàn)聯(lián)網(wǎng)功能火本,它的2.x版本封裝了基于NSURLConnection和NSURLSession的兩套API。目前最新的3.x版本支持基于NSURLConnection聯(lián)網(wǎng)聪建,同時(shí)引入了iOS 9的新特性钙畔。

圖11. URL加載系統(tǒng)的API

我們重點(diǎn)探討AFURLSessionManager和AFHTTPSessionManager兩個(gè)類,因?yàn)樗鼈兌际腔贜SURLSession的金麸,前者的用法可以在官方文檔上找到擎析,而且用起來(lái)稍顯麻煩,AFHTTPSessionManager的用法如下所示挥下。

下面的代碼演示如何向服務(wù)器發(fā)送獲取數(shù)據(jù)的GET請(qǐng)求揍魂。

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

下面的代碼演示了如何向服務(wù)器發(fā)送上傳數(shù)據(jù)的POST請(qǐng)求。

    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    [manager
        // 統(tǒng)一資源定位符
        POST:@""
        // 請(qǐng)求參數(shù)
        parameters:@{ }
        // 構(gòu)造請(qǐng)求報(bào)文消息體的Block
        constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
            // 可以調(diào)用appendPartWithFileData:name:fileName:mimeType:等方法
            // 將上傳給服務(wù)器的數(shù)據(jù)放到請(qǐng)求報(bào)文的消息體中
        }
        // 當(dāng)上傳進(jìn)度變化時(shí)回調(diào)的Block
        progress:^(NSProgress * _Nonnull uploadProgress) {
            
        }
        // 服務(wù)器響應(yīng)成功要回調(diào)的Block
        success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {    
        } 
        // 服務(wù)器響應(yīng)失敗要回調(diào)的Block
        failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        }
    ];

AFNetworking還封裝了判斷網(wǎng)絡(luò)可達(dá)性的功能见秽,使用該功能的代碼如下所示:

    // 創(chuàng)建網(wǎng)絡(luò)可達(dá)性管理器
    AFNetworkReachabilityManager *manager = [AFNetworkReachabilityManager manager];
    // 設(shè)置當(dāng)網(wǎng)絡(luò)狀況發(fā)生變化時(shí)要回調(diào)的Block
    [manager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
        switch (status) {
            case AFNetworkReachabilityStatusNotReachable:
                NSLog(@"沒(méi)有網(wǎng)絡(luò)連接");
                break;
            case AFNetworkReachabilityStatusReachableViaWiFi:
                NSLog(@"使用Wi-Fi");
                break;
            case AFNetworkReachabilityStatusReachableViaWWAN:
                NSLog(@"使用移動(dòng)蜂窩網(wǎng)絡(luò)");
                break;
            default:
                break;
        }
    }];
    // 開始監(jiān)控網(wǎng)絡(luò)狀況變換
    [manager startMonitoring];
  • MKNetworkingKit

相比AFNetworking愉烙,MKNetworkingKit會(huì)顯得小眾一些,但它仍然是一個(gè)非常優(yōu)秀的網(wǎng)絡(luò)框架解取,可以在Github上面找到它步责,目前的2.x版本也是基于NSURLSession封裝的,放棄了對(duì)NSURLConnection的使用禀苦。目前能找到的資料基本上介紹的是該框架1.x版本如何使用蔓肯,如果想了解和使用這個(gè)框架,建議訪問(wèn)作者本人的博客振乏。

基于套接字聯(lián)網(wǎng)##

套接字是一系列的用于實(shí)現(xiàn)網(wǎng)絡(luò)通信的標(biāo)準(zhǔn)函數(shù)的集合蔗包,最有名且被視為標(biāo)準(zhǔn)的是Berkeley Socket API。Berkeley Socket API是在1983年發(fā)布的BSD 4.2中引入的(后面統(tǒng)一稱之為BSD套接字)慧邮,隨后幾乎所有的操作系統(tǒng)都提供了BSD套接字的實(shí)現(xiàn)來(lái)幫助設(shè)備連接互聯(lián)網(wǎng)调限,就連微軟都參照了BSD套接字實(shí)現(xiàn)了用于Windows操作系統(tǒng)的Winsock舟陆。

說(shuō)明: BSD是Unix衍生系統(tǒng),是由加州大學(xué)伯克利分校開發(fā)和發(fā)布的耻矮,如果你想對(duì)BSD操作系統(tǒng)的發(fā)展史有感性的了解秦躯,下面這張圖也許會(huì)幫助到你。

圖12. Unix操作系統(tǒng)簡(jiǎn)史

BSD套接字通绸勺埃基于客戶端/服務(wù)器模式(C/S模式)來(lái)構(gòu)建網(wǎng)絡(luò)應(yīng)用踱承,這種模式簡(jiǎn)單的說(shuō)就是參與網(wǎng)絡(luò)的通信的要么是服務(wù)器,要么是客戶機(jī)哨免,最經(jīng)典的例子就是通過(guò)瀏覽器訪問(wèn)Web服務(wù)器茎活,Web服務(wù)器提供資源而瀏覽器作為客戶機(jī)請(qǐng)求獲得這些資源。套接字通信通常使用TCP或UDP作為傳輸協(xié)議琢唾,如前所述TCP提供了可靠通信的保證载荔,UDP則以更小的開銷提供不可靠的傳輸服務(wù),例如視頻流數(shù)據(jù)對(duì)可靠性要求不高就可以選擇使用UDP進(jìn)行傳輸采桃,這樣可以消除TCP多次握手所帶來(lái)的開銷身辨。

下面的代碼創(chuàng)建一個(gè)基于TCP的Echo服務(wù)器來(lái)演示如何使用套接字實(shí)現(xiàn)網(wǎng)絡(luò)通信。所謂Echo服務(wù)器就是將客戶端發(fā)送的消息原封不動(dòng)的發(fā)回去芍碧,雖然沒(méi)有什么實(shí)際價(jià)值,但不失為一個(gè)很好的例子号俐。

#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;        // 最大隊(duì)列長(zhǎng)度
static const int MAX_MSG_LEN = 4096;    // 最大消息長(zhǎng)度

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. 調(diào)用socket函數(shù)創(chuàng)建套接字
    // 第一個(gè)參數(shù)指定使用IPv4協(xié)議進(jìn)行通信(AF_INET6代表IPv6)
    // 第二個(gè)參數(shù)指定套接字的類型(SOCK_STREAM代表可靠的全雙工通信)
    // 第三個(gè)參數(shù)指定套接字使用的協(xié)議
    // 如果返回值是-1表示創(chuàng)建套接字時(shí)發(fā)生錯(cuò)誤 否則返回服務(wù)器套接字文件描述符
    int serverSocketFD = socket(AF_INET, SOCK_STREAM, 0);
    if (serverSocketFD < 0) {
        perror("無(wú)法創(chuàng)建套接字!!!\n");
        exit(1);
    }
    
    // 代表服務(wù)器地址的結(jié)構(gòu)體
    struct sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(SERVER_PORT);
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    
    // 2. 將套接字綁定到指定的地址和端口
    // 第一個(gè)參數(shù)指定套接字文件描述符
    // 第二個(gè)參數(shù)是上面代表地址的結(jié)構(gòu)體變量的地址
    // 第三個(gè)參數(shù)是上面代表地址的結(jié)構(gòu)體占用的字節(jié)數(shù)
    // 如果返回值是-1表示綁定失敗
    int ret = bind(serverSocketFD, (struct sockaddr *)&serverAddr,
                   sizeof serverAddr);
    if (ret < 0) {
        perror("無(wú)法將套接字綁定到指定的地址!!!\n");
        close(serverSocketFD);
        exit(1);
    }
    
    // 3. 開啟監(jiān)聽(監(jiān)聽客戶端的連接)
    ret = listen(serverSocketFD, MAX_Q_LEN);
    if (ret < 0) {
        perror("無(wú)法開啟監(jiān)聽!!!\n");
        close(serverSocketFD);
        exit(1);
    }
    
    bool serverIsRunning = true;
    while(serverIsRunning) {
        // 代表客戶端地址的結(jié)構(gòu)體
        struct sockaddr_in clientAddr;
        socklen_t clientAddrLen = sizeof clientAddr;
        // 4. 接受客戶端的連接(從隊(duì)列中取出第一個(gè)連接請(qǐng)求)
        // 如果返回-1表示發(fā)生錯(cuò)誤 否則返回客戶端套接字文件描述符
        // 該方法是一個(gè)阻塞方法 如果隊(duì)列中沒(méi)有連接就會(huì)一直阻塞
        int clientSocketFD = accept(serverSocketFD,
                    (struct sockaddr *)&clientAddr, &clientAddrLen);
        bool clientConnected = true;
        if (clientSocketFD < 0) {
            perror("接受客戶端連接時(shí)發(fā)生錯(cuò)誤!!!\n");
            clientConnected = false;
        }
       
        while (clientConnected) {
            // 接受數(shù)據(jù)的緩沖區(qū)
            char buffer[MAX_MSG_LEN + 1];
            // 5. 接收客戶端發(fā)來(lái)的數(shù)據(jù)
            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);
                // 如果收到客戶端發(fā)來(lái)的bye消息服務(wù)器主動(dòng)關(guān)閉
                if (!strcmp(buffer, "bye\r\n")) {
                    serverIsRunning = false;
                    clientConnected = false;
                }
                // 6. 將消息發(fā)回到客戶端
                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. 關(guān)閉客戶端套接字
        close(clientSocketFD);
    }
    // 8. 關(guān)閉服務(wù)器套接字
    close(serverSocketFD);
    return 0;
}

我們可以在終端中用telnet來(lái)測(cè)試上面的代碼泌豆,效果如下圖所示。

圖13. 在終端中用telnet測(cè)試Echo服務(wù)器

上面的Echo服務(wù)器只能支持一個(gè)客戶端請(qǐng)求吏饿,當(dāng)有多個(gè)客戶端連接到服務(wù)器時(shí)需要排隊(duì)等待踪危,很明顯是不合適的≈砺洌可以使用GCD(Grand Central Dispatch)來(lái)構(gòu)建多線程服務(wù)器贞远,將服務(wù)器和客戶端傳數(shù)據(jù)的那段代碼放到一個(gè)線程中執(zhí)行。

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

static const short SERVER_PORT = 1234;  // 端口
static const int MAX_Q_LEN = 64;        // 最大隊(duì)列長(zhǎng)度
static const int MAX_MSG_LEN = 4096;    // 最大消息長(zhǎng)度

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("無(wú)法創(chuàng)建套接字!!!\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("無(wú)法將套接字綁定到指定的地址!!!\n");
        close(serverSocketFD);
        exit(1);
    }
    
    ret = listen(serverSocketFD, MAX_Q_LEN);
    if (ret < 0) {
        perror("無(wú)法開啟監(jiān)聽!!!\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("接受客戶端連接時(shí)發(fā)生錯(cuò)誤!!!\n");
        }
        else {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                handle_client_connection(clientSocketFD);
            });
        }
    }
    return 0;
}

基于蘋果底層API聯(lián)網(wǎng)##

蘋果底層提供了叫做CFNetwork的API來(lái)實(shí)現(xiàn)聯(lián)網(wǎng)的功能笨忌,它對(duì)BSD套接字做了一些必要的封裝蓝仲,提供了更為簡(jiǎn)便的獲取網(wǎng)絡(luò)地址信息和檢查網(wǎng)絡(luò)狀態(tài)的方法,可以整合Run-Loop來(lái)避開使用多線程官疲,此外CFNetwork還對(duì)FTP協(xié)議袱结、HTTP協(xié)議進(jìn)行了面向?qū)ο蟮姆庋b,你可以在不了解這些協(xié)議實(shí)現(xiàn)細(xì)節(jié)的情況下來(lái)使用這些協(xié)議途凫。

我們用CFNetwork來(lái)為上面的Echo服務(wù)器寫一個(gè)專門的客戶端垢夹,這一次我們用Objective-C來(lái)做一些面向?qū)ο蟮姆庋b,代碼如下所示维费。

#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 {
    // 調(diào)用CFSocketCreate函數(shù)通過(guò)指定的協(xié)議和類型創(chuàng)建套接字
    // 第一個(gè)參數(shù)通常是NULL(使用默認(rèn)的對(duì)象內(nèi)存分配器)
    // 第二個(gè)參數(shù)AF_INET表示使用IPv4(如果指定成0或負(fù)數(shù)默認(rèn)也是AF_INET)
    // 第三個(gè)參數(shù)是套接字類型(如果指定成0或負(fù)數(shù)默認(rèn)也是SOCK_STREAM)
    // 第四個(gè)參數(shù)是協(xié)議(如果前一個(gè)參數(shù)是SOCK_STREAM默認(rèn)為TCP, 前一個(gè)參數(shù)是SOCK_DGRAM默認(rèn)為UDP)
    // 第五個(gè)參數(shù)和第六個(gè)參數(shù)是回調(diào)類型和回調(diào)函數(shù)
    // 第七個(gè)參數(shù)是保存數(shù)據(jù)的上下文環(huán)境
    self.socket = CFSocketCreate(NULL, AF_INET, SOCK_STREAM, 
        IPPROTO_TCP, 0,  NULL, NULL);
    if (!self.socket) {
        self.errorCode = SocketError;
    }
    else {
        // 表示服務(wù)器地址的結(jié)構(gòu)體
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_len = sizeof(servaddr);
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(port);
        // 將字符串形式的地址轉(zhuǎn)換成網(wǎng)絡(luò)地址的結(jié)構(gòu)體變量
        inet_pton(AF_INET, [address cStringUsingEncoding:NSUTF8StringEncoding], 
            &servaddr.sin_addr);
        // 將地址結(jié)構(gòu)體轉(zhuǎn)換成CFDataRef類型
        CFDataRef connectAddr = CFDataCreate(NULL, 
            (unsigned char *)&servaddr, sizeof servaddr);
        // 調(diào)用CFSocketConnectToAddress函數(shù)連接遠(yuǎn)端套接字(服務(wù)器)
        // 其中第三個(gè)參數(shù)代表連接的超時(shí)時(shí)間以秒為單位
        // 如果函數(shù)返回kCFSocketSuccess表示連接成功 否則就是連接失敗或超時(shí)
        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];
    // 向服務(wù)器發(fā)送Echo消息
    send(sock, mess, strlen(mess) + 1, 0);
    // 接受服務(wù)器返回的消息
    recv(sock, buffer, sizeof buffer, 0);
    return [NSString stringWithUTF8String:buffer];
}

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

用Storyboard做一個(gè)用戶界面果元。

圖14. 在Xcode中用Storyboard創(chuàng)建用戶界面
#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 {
    // 發(fā)送bye消息會(huì)斷開與服務(wù)器的連接 不能再發(fā)送消息
    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

我們可以先運(yùn)行上面用套機(jī)字編寫的Echo服務(wù)器促王,再通過(guò)模擬器或真機(jī)來(lái)運(yùn)行Echo客戶端,運(yùn)行效果如下圖所示:

圖15. Echo客戶端運(yùn)行效果

基于Bonjour的網(wǎng)絡(luò)設(shè)備發(fā)現(xiàn)##

Bonjour是Apple推出的適用于局域網(wǎng)(LAN)的零配置網(wǎng)絡(luò)協(xié)議而晒,主要的目的是在缺少中心服務(wù)器的情況下解決網(wǎng)絡(luò)設(shè)備的IP獲扔恰(在沒(méi)有DHCP服務(wù)的情況下用隨機(jī)的方式分配IP地址),名稱解析(用mDNS取代傳統(tǒng)的DNS服務(wù))和服務(wù)發(fā)現(xiàn)(通過(guò)本地域名如“名稱.服務(wù)類型.傳輸協(xié)議類型.local.”中的服務(wù)類型來(lái)發(fā)現(xiàn)服務(wù))等關(guān)鍵問(wèn)題欣硼。想要對(duì)Bonjour有一個(gè)全面的了解题翰,建議訪問(wèn)蘋果官方網(wǎng)站上的Bonjour for Developers專區(qū)。

發(fā)布Bonjour服務(wù)

#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回調(diào)方法

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

發(fā)現(xiàn)Bonjour服務(wù)

#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回調(diào)方法

- (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];
    }
    // 將發(fā)現(xiàn)的服務(wù)添加到數(shù)組中
    [servicesArray addObject:aNetService];
}

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


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

@end

注意:在發(fā)布服務(wù)之前應(yīng)該準(zhǔn)備好對(duì)應(yīng)的服務(wù)并啟動(dòng)它诈胜。不過(guò)NSNetService的publish方法并不依賴它所發(fā)布的服務(wù)豹障,不管要發(fā)布的服務(wù)是否就緒,該方法都可以成功的將服務(wù)發(fā)布出去焦匈,但是如果服務(wù)沒(méi)有就緒血公,要使用這個(gè)服務(wù)的客戶端就會(huì)發(fā)現(xiàn)這個(gè)發(fā)布出來(lái)的服務(wù)是個(gè)無(wú)效的服務(wù)。

在上面的例子中缓熟,我們將發(fā)現(xiàn)的服務(wù)裝在一個(gè)數(shù)組中累魔,當(dāng)我們需要使用這些服務(wù)時(shí),可以通過(guò)NSNetService對(duì)象解析出服務(wù)的地址和端口够滑,對(duì)于基于HTTP的服務(wù)垦写,我們可以使用蘋果的URL加載系統(tǒng)或者AFNetworking這樣的第三方庫(kù)來(lái)使用服務(wù),對(duì)于其他的服務(wù)我們可以使用套接字或CFNetwork API來(lái)使用服務(wù)彰触,對(duì)于使用同一個(gè)局域網(wǎng)中提供的服務(wù)梯投,這種方式不是更加簡(jiǎn)單方便嗎?

總結(jié)##

到此為止况毅,我們對(duì)iOS網(wǎng)絡(luò)應(yīng)用開發(fā)的方方面面做了一個(gè)走馬觀花的講解分蓖,當(dāng)然iOS開發(fā)中跟網(wǎng)絡(luò)相關(guān)的知識(shí)還遠(yuǎn)不止這些,例如如何通過(guò)證書保證網(wǎng)絡(luò)通信的安全尔许,如何有效的使用緩存來(lái)提升性能和減少網(wǎng)絡(luò)開銷以及URL緩存的過(guò)期模型和驗(yàn)證模型等么鹤,這些內(nèi)容打算以專題的形式在后面為大家呈現(xiàn)。上面內(nèi)容所有的代碼都可以在我的Github上找到味廊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蒸甜,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子余佛,更是在濱河造成了極大的恐慌迅皇,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件衙熔,死亡現(xiàn)場(chǎng)離奇詭異登颓,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)红氯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門框咙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)咕痛,“玉大人,你說(shuō)我怎么就攤上這事喇嘱≤怨保” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵者铜,是天一觀的道長(zhǎng)腔丧。 經(jīng)常有香客問(wèn)我,道長(zhǎng)作烟,這世上最難降的妖魔是什么愉粤? 我笑而不...
    開封第一講書人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮拿撩,結(jié)果婚禮上衣厘,老公的妹妹穿的比我還像新娘。我一直安慰自己压恒,他們只是感情好影暴,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著探赫,像睡著了一般型宙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上伦吠,一...
    開封第一講書人閱讀 49,760評(píng)論 1 289
  • 那天早歇,我揣著相機(jī)與錄音,去河邊找鬼讨勤。 笑死,一個(gè)胖子當(dāng)著我的面吹牛晨另,可吹牛的內(nèi)容都是我干的潭千。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼借尿,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼刨晴!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起路翻,我...
    開封第一講書人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤狈癞,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后茂契,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蝶桶,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年掉冶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了真竖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脐雪。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖恢共,靈堂內(nèi)的尸體忽然破棺而出战秋,到底是詐尸還是另有隱情,我是刑警寧澤讨韭,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布脂信,位于F島的核電站,受9級(jí)特大地震影響透硝,放射性物質(zhì)發(fā)生泄漏狰闪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一蹬铺、第九天 我趴在偏房一處隱蔽的房頂上張望尝哆。 院中可真熱鬧,春花似錦甜攀、人聲如沸秋泄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)恒序。三九已至,卻和暖如春谁撼,著一層夾襖步出監(jiān)牢的瞬間歧胁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工厉碟, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留喊巍,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓箍鼓,卻偏偏與公主長(zhǎng)得像崭参,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子款咖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容