計(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之上。
計(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)嫉嘀。
請(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è)空行彩匕。
響應(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
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截獲。
當(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ì)被截獲卵迂。
- 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)單方便的。
相關(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ò)展到樹的最底端构捡,如下圖所示。
<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惑灵,RaptureXML和XMLDictionary山上。
下面的代碼演示了如何使用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”頭文件的路徑添到此處抡驼。
#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ù)中非常有代表性的JSONModel和YYModel琐凭。
說(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的新特性钙畔。
我們重點(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ì)幫助到你。
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è)試上面的代碼泌豆,效果如下圖所示。
上面的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è)用戶界面果元。
#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)行效果如下圖所示:
基于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上找到味廊。