iOS網(wǎng)絡(luò)編程讀書筆記
Facade Tester客戶端門面模式的實例(被動版本化)
被動版本化版述,所以硬編碼URL.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
FTAppDelegate *appDelegate = (FTAppDelegate*)[[UIApplication sharedApplication] delegate];
if (appDelegate.urlForWeatherVersion1 != nil) {
NSError *error = nil;
NSData *data = [NSData dataWithContentsOfURL:appDelegate.urlForWeatherVersion1
options:NSDataReadingUncached
error:&error];
if (error == nil) {
NSDictionary *weatherDictionary = [NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingMutableLeaves
error:&error];
if (error == nil) {
v1_city = [weatherDictionary objectForKey:@"city"];
v1_state = [weatherDictionary objectForKey:@"state"];
v1_temperature = [weatherDictionary objectForKey:@"currentTemperature"];
// update the table on the UI thread
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
} else {
NSLog(@"Unable to parse weather because of error: %@", error);
[self showParseError];
}
} else {
[self showLoadError];
}
} else {
[self showLoadError];
}
});
}
通過GCD在global線程中加載網(wǎng)絡(luò)請求襟齿,用到的是[NSData dataWithContensOfURL:]; 數(shù)據(jù)用SJSONSerialization JSONObjectWithData:data..]進行序列化然后就能使用了姻锁。
在應(yīng)用委托中定義可以使得在實現(xiàn)服務(wù)定位器時更具有靈活性
#import <UIKit/UIKit.h>
@interface FTAppDelegate : UIResponder <UIApplicationDelegate, UITabBarControllerDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) UITabBarController *tabBarController;
// normally you wouldn't put these here!
@property (strong, nonatomic) NSURL *urlForWeatherVersion1;
@property (strong, nonatomic) NSURL *urlForWeatherVersion2;
@property (strong, nonatomic) NSURL *urlForStockVersion1;
@property (strong, nonatomic) NSURL *urlForStockVersion2;
@end
由于大多數(shù)Web Service 都會將結(jié)果以JSON的形式輸出,因此使用JSON來表示服務(wù)定位器比較好猜欺。服務(wù)定位器用來探測天氣與股票報價API端點位隶。該結(jié)構(gòu)將端點的所有版本都組合到了一個文件中。
在應(yīng)用啟動和返回到前臺時加載服務(wù)定位器开皿,將URL存儲為應(yīng)用委托中的屬性.(更復(fù)雜的應(yīng)用則需要專門的網(wǎng)絡(luò)管理器涧黄,由它處理服務(wù)定位器的加載,其他viewcontroller 也會使用它針對特定的網(wǎng)絡(luò)調(diào)用查詢端點(api))
- (void)loadServiceLocator {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error = nil;
#warning Replace the following line with your own domain and path to the service locator
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://example.com/api/serviceLocator.json"]
options:NSDataReadingUncached
error:&error];
if (error == nil) {
NSDictionary *locatorDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:&error];
if (error == nil) {
self.urlForStockVersion1 = [self findURLForServiceNamed:@"stockQuote" version:1 inDictionary:locatorDictionary];
self.urlForStockVersion2 = [self findURLForServiceNamed:@"stockQuote" version:2 inDictionary:locatorDictionary];
self.urlForWeatherVersion1 = [self findURLForServiceNamed:@"weather" version:1 inDictionary:locatorDictionary];
self.urlForWeatherVersion2 = [self findURLForServiceNamed:@"weather" version:2 inDictionary:locatorDictionary];
} else {
NSLog(@"Unable to parse service locator because of error: %@", error);
// inform the user on the UI thread
dispatch_async(dispatch_get_main_queue(), ^{
[[[UIAlertView alloc] initWithTitle:@"Error"
message:@"Unable to parse service locator."
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
});
}
} else {
NSLog(@"Unable to load service locator because of error: %@", error);
// inform the user on the UI thread
dispatch_async(dispatch_get_main_queue(), ^{
[[[UIAlertView alloc] initWithTitle:@"Error"
message:@"Unable to load service locator. Did you remember to update the URL to your own copy of it?"
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
});
}
});
}
這個json運行于服務(wù)器上赋荆,應(yīng)用就能請求到serviceLocator并解析.
HTTP協(xié)議
一個URL只對應(yīng)一個資源笋妥,多個URL可對應(yīng)同一個資源.(localhost是個例外)
URL由5部分構(gòu)成:
http://user:password@hostname:port/absolute-path?query
協(xié)議 認(rèn)證 主機名 端口 絕對路徑 查詢字符串
協(xié)議
除了HTTP外還可以使用FTP協(xié)議。iOS應(yīng)用中常用的另外一種協(xié)議還有FILE ,用于請求在應(yīng)用sandbox中的本地資源窄潭,如果使用字符串而沒有使用協(xié)議創(chuàng)建NSURL對象春宣,那么默認(rèn)就會使用FILE協(xié)議。
端口
HTTP默認(rèn)端口是80,HTTPS默認(rèn)端口443(有些網(wǎng)絡(luò)代理和防火墻會處于安全或者隱私考慮等原因阻塞非標(biāo)準(zhǔn)端口)
絕對路徑
很多REST服務(wù)使用路徑來傳遞值嫉你,用來唯一標(biāo)識數(shù)據(jù)庫中存儲的實體月帝。比如/customer/456/address/0指定索引為0的地址,具有標(biāo)識456的用戶.
查詢字符串
多個查詢參數(shù)用&字符分隔幽污,查詢字符串不可以包含回車嚷辅、空格與換行符。
iOS為NSString對象提供了一個方法來執(zhí)行URL的百分號編碼
NSString *urlString = @"http://myhost.com?query=This is a question";
NSString *encoded = [urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
結(jié)果為http://myhost.com?query=This%20is%20a%20question.該編碼不同于URL編碼油挥,不會對&字符編碼潦蝇,因此也不會改變URL參數(shù)的分隔.URL編碼會編碼& ? 和其他標(biāo)點符號,如果查詢的字符串包含了這些字符深寥,那么需要實現(xiàn)一種更為徹底地編碼方法攘乒。
請求內(nèi)容
HTTP請求包含三部分:請求行、請求頭與請求體惋鹅。請求行和請求頭是文本行则酝,通過回車/換行符分隔(值為13的字節(jié),或是0x0D/值為10的字節(jié),或是0x0A).在HTTP請求中這樣使用文本值沽讹,使得它們很容易構(gòu)建般卑、解析和調(diào)試∷郏空行(僅包含回車/換行符或是僅有換行符)將請求頭與請求體分隔開蝠检。
HTTP請求實例:
GET /search?source=ig&h1=en&rlz=&q=ios&btnG=Google+Search HTTP/1.1
HOST: www.google.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:11.0)...
Accept: text/html,application/xhtml+xml,application/xml:q=0.9,*/*;q=0.8
Accept-Language: en,en-us;q=0.7,en-ca;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: http://www.google.com/ig?hl=en&source=webhp
Cookie: PREF=ID=fdf9979...
請求行是發(fā)送給服務(wù)器的第一行數(shù)據(jù)。請求行包括3方面主要信息:HTTP請求方法挚瘟、請求URI與HTTP版本.
標(biāo)準(zhǔn)請求方法都是大寫叹谁。
GET
從服務(wù)器獲取一段內(nèi)容(用HTTP術(shù)語來說,就是實體entity),不會導(dǎo)致服務(wù)器端的數(shù)據(jù)發(fā)生變化,GET請求通常不包括請求體乘盖,不過也可以包含焰檩。
POST
使用客戶端提供的數(shù)據(jù)更新實體.POST請求通常會在請求體中加入應(yīng)用服務(wù)器
(運行php等?)所需的信息.POST請求是非冪等(待查)的.這意味著如果處理多個請求订框,那么結(jié)果與處理單個請求是不同的析苫。
HEAD
獲取響應(yīng)的元數(shù)據(jù)而無需檢索響應(yīng)的全部內(nèi)容。該方法通常用于檢查服務(wù)器最近的內(nèi)容變化而無須檢索全部內(nèi)容
PUT
使用客戶端提供的數(shù)據(jù)添加實體穿扳。PUT請求通常將應(yīng)用服務(wù)器所需的信息放在請求體中來創(chuàng)建新的實體衩侥。在通常情況下,PUT請求是冪等的纵揍,這意味著多個請求的處理會產(chǎn)生相同的結(jié)果顿乒。
DELETE
根據(jù)URI的內(nèi)容或客戶端提供的請求體來刪除實體。DELETE請求是REST服務(wù)接口中使用最為頻繁的請求泽谨。
說明:
HTTP規(guī)范允許HTTP客戶端與服務(wù)器之間的中介添加璧榄、刪除、重排序以及修改HTTP頭吧雹。因此骨杂,應(yīng)用發(fā)出的請求在到達服務(wù)器時可能會出現(xiàn)以下的情況:添加新的頭,修改已有的頭或者刪除某些頭雄卷。
雖然使用了有狀態(tài)的TCP傳輸層搓蚪,但HTTP卻是個無狀態(tài)協(xié)議。這意味著HTTP服務(wù)器并不會保留關(guān)于某個請求的任何信息以用在未來的請求中丁鹉。Cookie提供了一種方式妒潭,可以將一些簡單地狀態(tài)信息存儲在客戶端,并在后續(xù)的請求中與服務(wù)器進行通信揣钦。
HTTP頭之后是可選的請求體雳灾。請求體可以使任意的字節(jié)序列,通過一個空行與頭分割開來冯凹,請求體必須遵循客戶端與服務(wù)器之間預(yù)先確定的數(shù)據(jù)編碼谎亩。對于Web瀏覽器來說,這通常是表單編碼的數(shù)據(jù),但對于移動應(yīng)用來說匈庭,通常是JSON數(shù)據(jù)夫凸。在iOS中NSURLRequest及其子類NSMutableURLRequest提供了必要的方法與屬性來構(gòu)建幾乎所有的HTTP請求。
在HTTP服務(wù)器與應(yīng)用服務(wù)器處理完請求后阱持,HTTP響應(yīng)會通過同一個TCP socket[待查]返回給客戶端.HTTP響應(yīng)的結(jié)構(gòu)類似于HTTP請求夭拌,第一行也是狀態(tài)行,后面是頭衷咽,然后是響應(yīng)體啼止。
簡單的HTTP響應(yīng)示例:
HTTP/1.1 200 OK
Date: Tue, 27 Mar 2012 12:25;12 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=UTF-8
Content-Encoding: gzip
Transfer-Encoding: chunked
Server: gws
<!doctype html><html itemscope="itemscope"
itemtype="http://schema.org/Webpage">
<head><meta itemprop="image" content="/images/google_favicon_128.png"/>
<title>ios - Google Search</title>
<script>window.google=(kEI:"prlxT5qtNqe70AHh873aAQ",
getEI:function(a)(var b;
while(a&&!(a.getAttribute&&(b=a.getAttribute("eid"
狀態(tài)行包括3個域,域之間通過空格分隔兵罢。第一個域是響應(yīng)HTTP的版本。接下來的兩個域提供了表示請求結(jié)果的狀態(tài)值滓窍。首先是一個3位的整數(shù)值,包含了請求的結(jié)果代碼卖词,最后是條說明語句,提供了關(guān)于代碼的簡短文本說明吏夯。在大多數(shù)情況下此蜈,數(shù)值能夠完全說明狀態(tài)。
緊跟狀態(tài)行的是響應(yīng)頭:響應(yīng)頭之間通過回車/換行符進行分隔噪生。每個頭都包含了關(guān)于響應(yīng)的元數(shù)據(jù)裆赵,包括數(shù)據(jù)的上一次修改時間、客戶端可以緩存數(shù)據(jù)多少時間跺嗽、數(shù)據(jù)的編碼方式以及在隨后的請求中提交的狀態(tài)信息战授。
響應(yīng)體是通過空行與響應(yīng)頭分隔開的。響應(yīng)體可以包含任意數(shù)量的二進制字符桨嫁。與客戶端通信的響應(yīng)體的長度可以通過請求的Content-Length頭或者塊編碼體現(xiàn)植兰。塊編碼響應(yīng)包含Transfer-Encoding頭,并且?guī)в衏hunked值璃吧。塊編碼體包含一個或(多個)(體片段).每個片段都有起始行楣导,指定塊中字節(jié)數(shù)量,iOS URL加載系統(tǒng)向應(yīng)用隱藏了這種復(fù)雜性畜挨。
在iOS的URL加載系統(tǒng)中筒繁,NSURLResponse及其子類NSHTTPURLResponse封裝了請求返回的數(shù)據(jù)。該繼承體系中有兩個對象巴元,因為URL加載可以基于非HTTP URL實現(xiàn)數(shù)據(jù)的請求毡咏。比如,對URL file://的請求就不會包含任何頭信息.
高層iOS HTTP API
主要有3個方法可以執(zhí)行HTTP請求和接受響應(yīng):
- 同步务冕。啟動線程 的 代碼會阻塞血当,直到整個響應(yīng)加載完畢并返回到調(diào)用方法為止。該技術(shù)最容易實現(xiàn),不過局限性也最大臊旭。
- 隊列式異步落恼。起始代碼創(chuàng)建一個請求,并將其放到一個隊列中以在后臺線程中執(zhí)行离熏。
- 異步佳谦。起始代碼 開啟一個請求,該請求運行在起始線程中滋戳,不過在請求處理時會調(diào)用委托方法钻蔑,該方法的實現(xiàn)最為復(fù)雜,不過在處理響應(yīng)時卻提供了最大的靈活性奸鸯。(在特定時刻執(zhí)行回調(diào)函數(shù)咪笑,方便進行操作)
所有這三個請求都由相同的4個對象構(gòu)成:NSURL,NSURLRequese,NSURLConnection,NSURLResponse
NSURL
可以通過NSURL對象輕松管理URL值并訪問URL所指向的內(nèi)容。NSURL可以指向文件資源娄涩,也可以指向網(wǎng)絡(luò)資源窗怒,同時在這兩類資源類型的使用上沒有任何區(qū)別。從URL加載數(shù)據(jù)示例代碼:
NSURL *url = [NSURL urlWithString:mysteryString];
NSData *data = [NSData dataWithContentsOfURL:url];
mysteryString的值可以引用文件或網(wǎng)絡(luò)資源蓄拣,而代碼的行為是一樣的扬虚。主要的差別在于加載mysteryString所引用的資源的時間上。如果URL引用的是網(wǎng)絡(luò)資源球恤,就會在后臺現(xiàn)成執(zhí)行代碼
辜昵,這樣在數(shù)據(jù)加載時用戶界面就不會暫停下來。(也就是自動轉(zhuǎn)到后臺操作咯咽斧?不錯)
創(chuàng)建NSURL對象最常見的方式是使用類方法 URLWithString:進行實例化堪置。該方法會創(chuàng)建一個NSURL對象,并使用提供的NSString對象的內(nèi)容對其進行初始化收厨,下面這段代碼說明了這一點:
NSURL *url = [NSURL URLWithString:@"http://www.wiley.com/path1"];
NSURL對象提供了很多訪問器方法來讀取URL各個部分的值晋柱。每個訪問器都可以只讀訪問URL的一部分。scheme訪問器會返回一個包含該URL所用協(xié)議的NSString對象诵叁。如果目標(biāo)URL沒有指定某個特定部分雁竞,那么返回值就為nil,考慮之前創(chuàng)建的url對象,下面代碼會打印出 Port is nil.
if (url.port == nil){
NSLog(@"Port is nil");
} else {
NSLog(@"Port is not nil");
}
如果URL包含查詢字符串拧额,那么query訪問器方法就會包含所有需要查詢參數(shù)的值碑诉。根據(jù)RFC 3986的要求,在創(chuàng)建NSURL對象前侥锦,URL字符串的內(nèi)容需要以百分號編碼进栽。比如,如果執(zhí)行下述代碼片段恭垦,那么查詢參數(shù)的值就為q=iOS+Networking
NSURL *url = [NSURL URLWithString:@"http://google.com?q=iOS+Networking"];
NSURL對象是不可變的快毛,這意味著無法先構(gòu)建空的NSURL對象格嗅,然后通過調(diào)用對象的賦值方法(有時也叫setter)方法來裝配其屬性。對象要么1.通過NSString對象唠帝,要么2.通過另一個NSURL對象實例化.如果用于實例化NSURL對象的字符串是不合法的屯掖,那么創(chuàng)建方法就會返回nil
。在使用URL對象進行網(wǎng)絡(luò)請求前襟衰,應(yīng)該驗證URL對象是合法的贴铜。
NSURLRequest
創(chuàng)建好NSURL對象后,接下來就需要完成執(zhí)行HTTP請求所需的下一步了:創(chuàng)建NSURLRequest對象瀑晒。NSURLRequest對象包含了加載URL內(nèi)容所需的信息绍坝,并且獨立于URL中指定的協(xié)議。iOS中的URL加載系統(tǒng)支持HTTP苔悦,HTTPS轩褐,F(xiàn)TP,F(xiàn)ILE URL內(nèi)容的加載玖详。URL加載系統(tǒng)提供了一種擴展方式以處理新的協(xié)議灾挨,方式是創(chuàng)建NSURLProtocol自雷,然后將返回結(jié)果提供給URL加載系統(tǒng)竹宋。
創(chuàng)建NSURLRequest對象最簡單的方式是通過類方法和提供的NSURL對象。下述代碼片段展現(xiàn)了使用默認(rèn)值來創(chuàng)建請求對象的過程
NSURL *url = [NSURL URLWithString:@"https://gdata.youtube.com/feeds/api/standardfeeds/top_rated"];
if (url == nil) {
NSLog(@"Invalid URL");
return;
}
NSURLRequest *request = [NSURLRequest requestWithURL:url];
if (request == nil) {
NSLog(@"Invalid request");
return;
}
使用默認(rèn)值表示請求使用URL協(xié)議指定的請求緩存規(guī)則地技,請求有著標(biāo)準(zhǔn)的請求超時蜈七。如果URL是HTTP或HTTPS,那么請求方法將是GET莫矗。并且使用操作系統(tǒng)提供的默認(rèn)頭飒硅。
下列示例展示了如何使用自定義的緩存和超時值來創(chuàng)建NSURLRequest對象。構(gòu)建URL加載系統(tǒng)的代碼忽略了所有緩存作谚,如果完成請求連接的時間超過20s,將會發(fā)生錯誤.
NSURLRequest對象提供了幾個訪問器方法來獲取請求的屬性三娩,由于NSURLRequest類是不可變的,因此無法更改這些屬性(readonly)妹懒。除了URL雀监、緩存策略和超時值之外,如果要修改其他屬性眨唬,那么請用NSMutableURLRequest類会前。
NSMutableURLRequest是NSURLRequest的子類,提供了賦值方法以修改請求的屬性匾竿。下述代碼片段展示了使用一小段消息體來創(chuàng)建一個簡單的POST請求瓦宜,它包含了以UTF8編碼的一個NSString對象的字節(jié)。URL加載系統(tǒng)會自動裝配請求的Content-Length頭:
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
[req setHTTPMethod:@"POST"];
[req setHTTPBody:[@"Post body" dataUsingEncoding:NSUTF8StringEncoding]];
有兩種方式可以向NSURLRequest提供HTTP體:在內(nèi)存中(就像之前的示例一樣)或是通過NSInputStream岭妖。代碼可以通過輸入流提供請求體而無需將整個體加載到內(nèi)存中临庇。如果發(fā)送諸如照片或視頻等大容量內(nèi)容反璃,那么使用輸入流是最佳選擇。下述代碼片段展示了如何通過輸入流創(chuàng)建POST方法假夺,需要事先將NSString srcFilePath設(shè)定為應(yīng)用包或是沙盒中的文件路徑
NSArray *srcArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *srcFilePath = srcArray[0];
srcFilePath = [srcFilePath stringByAppendingString:@"/filePath"];
NSInputStream *inStream = [NSInputStream inputStreamWithFileAtPath:srcFilePath];
[req setHTTPBodyStream:inStream];
[req setHTTPMethod:@"POST"];
由于NSURLRequest對象包含HTTP與非HTTP請求的屬性淮蜈,因此訪問非 HTTP URL的代碼需要將特定于HTTP的屬性的值設(shè)為nil.
NSURLConnection
NSURLConnection對象是URL加載系統(tǒng)活動的中心,但提供的接口卻不多侄泽,只提供了用于初始化礁芦、開啟與取消連接的方法。悼尾。
回到上面提到的用于執(zhí)行HTTP請求和獲取響應(yīng)的主要方法上來柿扣,NSURLConnection類通過3種不同的操作模式發(fā)揮作用:同步,異步闺魏,隊列式異步未状。同步模式是最易于使用的,不過卻有很多限制析桥,這使得它不太適合更加高級的交互司草。異步模式提供了很大的靈活性,不過其代價就是增加了代碼的復(fù)雜性泡仗。隊列式異步模式提供了異步模式的后臺操作埋虹,同時又保持了同步模式的簡單性。
在異步模式下操作時娩怎,NSURLConnection對象會調(diào)用委托對象來指引連接流搔课、處理到來的數(shù)據(jù)、處理認(rèn)證并對錯誤做出響應(yīng)截亦。
NSURLResponse
NSURLReponse對象會再URL加載請求完畢后返回爬泥。響應(yīng)對象的內(nèi)容根據(jù)請求的類型及成功與否會有較大變化。如下列表介紹了從請求返回的各種對象崩瓤。還有兩個對象也可能來自于URL加載請求:NSError對象和NSData對象袍啡。如果請求有問題或者客戶端無法連接到服務(wù)器,就會產(chǎn)生NSError對象却桶。如果有響應(yīng)返回境输,那么生成的NSData對象就會包含響應(yīng)體。如果生成了NSError對象颖系,就不會再生成NSURLResponse與NSData 對象畴嘶。
MIMEType--結(jié)果數(shù)據(jù)的MIME類型。該值來自于服務(wù)器集晚,如果客戶端框架認(rèn)為服務(wù)器有錯窗悯,那么可以修改,如果服務(wù)器沒有提供偷拔,還可以由客戶端框架提供蒋院。
expectedContentLength--該值可能由請求返回亏钩,也可能不返回,返回值可能與返回內(nèi)容的實際大小不同欺旧。如果返回內(nèi)容的大小未知姑丑,那么該值將等于NSURLResponseUnknownLength
suggestedFilename--要么是服務(wù)器提供的內(nèi)容文件名,要么來自于URL和MIME
URL--返回內(nèi)容的URL辞友。由于重定向和標(biāo)準(zhǔn)化等原因栅哀,該URL可能與提供的URL不同。textEncodingName--最初的數(shù)據(jù)源所用的文本編碼名称龙。如果響應(yīng)中沒有使用文本編碼留拾,那么該值將會是nil.
URL加載系統(tǒng)提供了一個名為NSHTTPURLResponse的NSURLResponse子類,它包含特定于HTTP請求的屬性鲫尊。該類對于確定HTTP請求的結(jié)果是必需的痴柔。它有如下參數(shù):響應(yīng)頭--該屬性返回響應(yīng)頭值得NSDictionary對象。字典的鍵是頭的名字疫向,每個鍵的值是頭的值咳蔚。HTTP規(guī)范孕育一個請求有多個同名的頭。NSHTTPURLResponse通過返回一個包含所有頭值的NSString對象(頭值之間用逗號分隔)來處理這一點搔驼。
HTTP狀態(tài)碼--來自于響應(yīng)狀態(tài)行的整數(shù)狀態(tài)碼谈火。NSHTTPURLResponse類有一個類方法可以針對任意狀態(tài)返回本地化的字符串說明。
同步請求
同步請求是iOS中最簡單的請求類型舌涨,不過簡單的代價則是縮減的功能與靈活性的降低堆巧。在發(fā)出同步請求時,請求所處的線程就會阻塞泼菌,直到請求完成或失敗為止。同步請求通常用于創(chuàng)建HTTP GET請求后在后臺線程中獲取已知大小的資源啦租。比如使用同步請求在后臺線程中可以輕松獲取圖片并顯示在單元格中哗伯。
同步請求展示了獲取URL數(shù)據(jù)的最簡單方式,在iOS API中有很多輔助方法的底層使用的都是同步請求篷角。比如焊刹,NSString stringWithContentsOfURL:方法會創(chuàng)建一個NSString實例,然后根據(jù)URL的內(nèi)容從任意服務(wù)器獲取這些內(nèi)容恳蹲。如果URL使用了FILE(如:file://foo.txt)協(xié)議虐块,就會從本地文件系統(tǒng)獲取內(nèi)容。如果URL使用了HTTP(如:http://www.wiley.com)協(xié)議嘉蕾,就會從遠(yuǎn)程服務(wù)器獲取內(nèi)容贺奠。因此,除非知道URL是FILE URL错忱,否則在使用這些輔助方法從URL中獲取內(nèi)容時務(wù)必小心儡率。
- (NSArray *) doSyncRequest:(NSString *)urlString {
// make the NSURL object from the string
NSURL *url = [NSURL URLWithString:urlString];
// Create the request object with a 30 second timeout and a cache policy to always retrieve the
// feed regardless of cachability.
NSURLRequest *request =
[NSURLRequest requestWithURL:url
cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData
timeoutInterval:30.0];
// Send the request and wait for a response
NSHTTPURLResponse *response;
NSError *error;
NSData *data = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
// check for an error
if (error != nil) {
NSLog(@"Error on load = %@", [error localizedDescription]);
return nil;
}
// check the HTTP status
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode != 200) {
return nil;
}
NSLog(@"Headers: %@", [httpResponse allHeaderFields]);
}
// Parse the data returned into an NSDictionary
NSDictionary *dictionary =
[XMLReader dictionaryForXMLData:data
error:&error];
// Dump the dictionary to the log file
NSLog(@"feed = %@", dictionary);
NSArray *entries =[self getEntriesArray:dictionary];
// return the list if items from the feed.
return entries;
}
代碼清單3-1從包含URL的調(diào)用者中接受一個NSString對象.當(dāng)URL對象構(gòu)建完畢后挂据,會實例化一個NSURLRequest對象。在該例中儿普,代碼重寫了默認(rèn)的緩存策略和超時時間崎逃。注意:這里通過NSURLRequestReloadIgnoringLocalAndRemoteCacheData將緩存策略設(shè)為用不緩存。這樣可以更好地表現(xiàn)出同步請求對UI的影響眉孩,因為UI線程會被阻塞个绍。通常情況下,代碼不會重寫所有緩存浪汪,不過重寫默認(rèn)的超時時間倒是比較常見的巴柿,這里為請求指定30s的超時時間。
創(chuàng)建好請求后吟宦,代碼會調(diào)用NSURLConnection的類方法sendSynchronousRequest:returningReponse:error:來執(zhí)行請求篮洁。該方法將請求和兩個指針作為傳入?yún)?shù):一個指向NSURLResponse對象,它會由服務(wù)器的響應(yīng)進行裝配殃姓;另一個指向NSError對象袁波,如果請求失敗,該對象中就包含詳細(xì)的錯誤信息蜗侈。response指針指向NSURLResponse的一個實例篷牌,然而,它將是用于處理所有HTTP請求的NSHTTPURLResponse子類實例踏幻。如果NSError不為nil枷颊,那就說明請求在某個底層失敗了,然而该面,如果為nil,那就表明請求沒有因為網(wǎng)絡(luò)錯誤或是不合法的URL而失敗夭苗。但請求仍舊可能在語義上失敗,比如服務(wù)器響應(yīng)說遇到了內(nèi)部服務(wù)器錯誤等隔缀。error指針指向的NSError對象的內(nèi)容包含了關(guān)于錯誤的詳細(xì)說明信息题造。
代碼清單3-1中的代碼會檢查NSError對象是否存在,以及NSHTTPURLResponse對象的狀態(tài)碼猾瘸。如果兩者都標(biāo)識為成功界赔,那么方法就會繼續(xù)執(zhí)行。
sendSynchronousRequest:returingResponse:error方法會以NSData對象的形式返回HTTP相應(yīng)體牵触。由于源以XML形式表示淮悼,因此請求成功的NSData對象會被XMLReader讀取器解析到NSDictionary中,接下來會遍歷該字典揽思,將RSS條目列表返回給調(diào)用者袜腥。
創(chuàng)建同步調(diào)用相當(dāng)簡單,僅需幾行代碼就可以成功從服務(wù)器獲取數(shù)據(jù)钉汗,不過這種簡單性是以有限的使用場景和增加缺陷的風(fēng)險為代價的瞧挤。
同步請求的best practice:
- 只在后臺線程中使用同步請求锡宋,除非確定請求訪問的是本地文件資源,否則請不要在main_queue中使用特恬!
- 只有在知道返回數(shù)據(jù)不會超出應(yīng)用的內(nèi)存時才可以使用同步請求执俩。記住,整個響應(yīng)體都會位于代碼的內(nèi)存中癌刽。如果響應(yīng)很大役首,那么可能導(dǎo)致應(yīng)用出現(xiàn)內(nèi)存溢出(memory leak)的問題。此外显拜,當(dāng)代碼將響應(yīng)解析為所需的格式時可能需要復(fù)制(類似copy語義衡奥?)返回的數(shù)據(jù),這會導(dǎo)致內(nèi)存增加一倍远荠。
- 在處理返回的數(shù)據(jù)前矮固,驗證錯誤與調(diào)用返回的HTTP響應(yīng)狀態(tài)碼(一般是200?)
- 如果源URL需要驗證譬淳,那么不要使用同步請求档址。因為同步框架并不支持對認(rèn)證請求作出響應(yīng)。唯一的例外是BASIC認(rèn)證邻梆,因為這時認(rèn)證信息可以通過URL或請求頭進行傳遞守伸。以這種方式執(zhí)行認(rèn)證會增加應(yīng)用與服務(wù)器之間的耦合度,從而導(dǎo)致整個應(yīng)用變得脆弱浦妄。如果請求不使用HTTPS協(xié)議尼摹,那么還是會再明文中傳遞認(rèn)證信息领迈。
- 如果需要向用戶提供進度條形入,那么不要使用同步請求桌硫,因為請求是原子的(個人理解:加鎖后玻褪,一次只能由一個東西對它訪問,因而不能顯示進度條),無法提供中間的進度提示信息蝗砾。
- 如果需要流解析器[待查]來漸進解析響應(yīng)數(shù)據(jù)颁褂,那么不要使用同步請求
- 如果在請求完成前需要取消仆潮,那么不要使用同步請求故黑。
隊列式異步請求
隊列式異步請求類似于同步請求。程序提供NSURLRequest對象庭砍,URL加載系統(tǒng)嘗試加載請求而不會與調(diào)用代碼之間存在任何其他的交互场晶。這兩種方式之間的主要差別在于URL加載系統(tǒng)執(zhí)行的隊列式異步請求位于隊列中,可能位于后臺線程上怠缸。隊列式異步請求的概念是在iOS 5.0中增加的诗轻。
iOS提供了一種叫做操作隊列的設(shè)施(NSOperationQueue)。這些隊列可以讓程序描述待執(zhí)行的操作揭北,然后以 FIFO的順序提交操作供隊列執(zhí)行扳炬。隊列框架提供了優(yōu)先級順序以及根據(jù)操作依賴的順序吏颖,不過URL加載系統(tǒng)并沒有使用這些設(shè)施。
在代碼構(gòu)建隊列式異步請求前恨樟,首先需要創(chuàng)建隊列半醉,里面是執(zhí)行的請求。下述代碼展示了如何創(chuàng)建操作隊列:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
操作隊列可以并發(fā)執(zhí)行多個操作劝术。在默認(rèn)情況下缩多,并發(fā)操作的數(shù)量是由iOS根據(jù)系統(tǒng)情況決定的⊙可以通過調(diào)用創(chuàng)建隊列的setMaxConcurrentOperationCount:方法來重寫默認(rèn)值衬吆。當(dāng)應(yīng)用啟動時,一個隊列就會自動創(chuàng)建绳泉,可以通過調(diào)用NSOperationQueue的mainQueue類方法來獲取該隊列逊抡。不要使用該隊列做網(wǎng)絡(luò)請求,因為會在主線程上操作零酪,長時間操作會凍結(jié)用戶界面冒嫡。如果從測試應(yīng)用的分隔控件中選擇隊列選項并tap刷新按鈕,那么會發(fā)現(xiàn)刷新按鈕會立刻返回默認(rèn)狀態(tài)蛾娶。同時表現(xiàn)為tableView會被清空灯谣。之所以會出現(xiàn)這種情況是因為請求是排隊的,主運行循環(huán)會繼續(xù)執(zhí)行而不會等待請求完成蛔琅。
- (void) doQueuedRequest:(NSString *)urlString delegate:(id)delegate {
// make the NSURL object
NSURL *url = [NSURL URLWithString:urlString];
// create the request object with a no cache policy and a 30 second timeout.
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:30.0];
// If the queue doesn't exist, create one.
if (queue == nil) {
queue = [[NSOperationQueue alloc] init];
}
// send the request and specify the code to execute when the request completes or fails.
[NSURLConnection sendAsynchronousRequest:request
queue:queue
completionHandler:^(NSURLResponse *response,
NSData *data,
NSError *error) {
if (error != nil) {
NSLog(@"Error on load = %@", [error localizedDescription]);
} else {
// check the HTTP status
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode != 200) {
return;
}
NSLog(@"Headers: %@", [httpResponse allHeaderFields]);
}
// parse the results and make a dictionary
NSDictionary *dictionary =
[XMLReader dictionaryForXMLData:data
error:&error];
NSLog(@"feed = %@", dictionary);
// get the dictionary entries.
NSArray *entries =[self getEntriesArray:dictionary];
// call the delegate
if ([delegate respondsToSelector:@selector(setVideos:)]) {
[delegate performSelectorOnMainThread:@selector(setVideos:)
withObject:entries
waitUntilDone:YES];
}
}
}];
}
代碼清單3-2展示的方法用于創(chuàng)建和處理排隊請求的結(jié)果胎许。注意這種另類的delegate方式(不知道寫法優(yōu)劣如何)。與同步請求一樣罗售,首先會創(chuàng)建一個NSURL對象辜窑,然后向其傳遞一個新的NSURLRequest對象。請求創(chuàng)建完畢之后寨躁,如果queue不存在穆碎,那么代碼會創(chuàng)建一個名為queue的NSOperationQueue對象。該變量在FeedLoader類的實現(xiàn)中被聲明為靜態(tài)變量职恳。通常情況下所禀,應(yīng)用會在啟動時在應(yīng)用委托中創(chuàng)建一個隊列,然后在整個應(yīng)用中都使用該隊列放钦。由于知道隊列已經(jīng)存在色徘,因此代碼會調(diào)用NSURLConnection來執(zhí)行隊列中的請求,并在操作完成或失敗后調(diào)用一個塊(就是那個 completionHandler)操禀。當(dāng)請求位于隊列中時褂策,doQueueRequest:delegate方法會返回到調(diào)用者。由于方法會在URL加載完畢前返回(異步),因此當(dāng)加載完畢時需要由一個委托類去調(diào)用(id類型的delegate)。由于這里使用了異步完成模式斤寂,因此代碼需要實現(xiàn)委托或通知模式耿焊,從而將接受到的數(shù)據(jù)傳遞給最初的請求對象。
待執(zhí)行的代碼塊通過sendAsynchronousRequest:queue:completionHandler方法的completionHandler參數(shù)傳遞遍搞。completion塊驗證請求沒有生成錯誤罗侯,并且HTTP狀態(tài)碼為200.這表示成功。如果請求是成功的尾抑,那么返回的數(shù)據(jù)就會被解析到NSDictionary中歇父。接下來,代碼會驗證所提供的委托類支持setVideos:方法(respondsToSelector:)再愈。如果支持榜苫,就會在主線程上調(diào)用該方法,并提供RSS源返回的條目數(shù)組翎冲。setVideos:之所以要在主線程上調(diào)用垂睬,是因為completion塊是在后臺線程在執(zhí)行的。如果委托方法在該context中執(zhí)行并操作UI界面抗悍,那么結(jié)果是undefined驹饺,大多數(shù)情況下都是不正確的。
隊列式異步請求的best practice:
- 只有在知道返回的數(shù)據(jù)不會超過應(yīng)用的內(nèi)存時才能使用隊列式異步請求操作缴渊。記住赏壹,整個響應(yīng)體都會位于代碼的內(nèi)存中。如果響應(yīng)很大衔沼,那么可能導(dǎo)致應(yīng)用出現(xiàn)memory leak問題蝌借。此外,當(dāng)代碼將響應(yīng)解析為所需格式時可能需要復(fù)制返回的數(shù)據(jù)指蚁,這會導(dǎo)致內(nèi)存增加一倍菩佑。
- 為所有操作使用單一的NSOprationQueue,根據(jù)服務(wù)器的能力以及預(yù)期的網(wǎng)絡(luò)狀況控制當(dāng)前操作的最大數(shù)量凝化。
- 在處理返回的數(shù)據(jù)前驗證錯誤與調(diào)用返回的HTTP響應(yīng)狀態(tài)碼
- 如果源URL需要驗證稍坯,那么不要使用隊列式異步請求,因為該功能并不支持對認(rèn)證請求作出響應(yīng)搓劫。如果服務(wù)需要驗證瞧哟,那么可以將BASIC認(rèn)證信息放在提供給請求的URL中(感覺相當(dāng)不安全啊)
- 如果需要向用戶提供進度條枪向,那么不要使用隊列式異步請求勤揩,因為請求是原子的,無法提供中間的進度指示信息遣疯。
- 如果需要流解析器來漸進解析響應(yīng)數(shù)據(jù)雄可,那么不要使用隊列式異步請求。
- 如果請求在完成前需要取消缠犀,那么不要使用隊列式異步請求数苫。(跟同步請求有相當(dāng)一部分的類型要求)
異步請求
異步請求使用與同步和隊列式異步請求相同的對象,只不過又增加了另一個對象辨液,即NSURLConnectionDelegate對象
協(xié)議處理器在HTTP協(xié)議過程中處理時虐急,會在連接的重要階段調(diào)用委托方法.
協(xié)議處理器在調(diào)用方法前會先驗證委托實現(xiàn)了該方法。如果沒有實現(xiàn)滔迈,那么協(xié)議處理器就會假定一個默認(rèn)值并在連接中繼續(xù)處理止吁。代碼清單3-3包含了使用異步技術(shù)初始化URL加載請求的代碼。一開始燎悍,該方法與之前的技術(shù)很類似:創(chuàng)建NSURL對象敬惦,然后用來構(gòu)建請求。當(dāng)請求構(gòu)建網(wǎng)壁厚谈山,代碼會創(chuàng)建NSURLConnection對象并將自身作為委托對象俄删。在URL內(nèi)容加載時,協(xié)議處理器會調(diào)用委托類并提供關(guān)于請求狀態(tài)的信息奏路。借助于這些回調(diào)畴椰,委托類可以調(diào)整協(xié)議處理器的行為。
創(chuàng)建好連接后鸽粉,代碼會開始請求斜脂。在連接創(chuàng)建與開啟連接之間,應(yīng)用可以修改委托消息傳遞給委托類的方式触机。代碼可以指定不同的運行循環(huán)或操作隊列來傳遞回調(diào)帚戳。
/**
* Creates a UUID to use as the temporary file name during the download
*/
- (NSString *)createUUID
{
CFUUIDRef uuidRef = CFUUIDCreate(NULL);
CFStringRef uuidStringRef = CFUUIDCreateString(NULL, uuidRef);
CFRelease(uuidRef);
NSString *uuid = [NSString stringWithString:(__bridge NSString *)
uuidStringRef];
CFRelease(uuidStringRef);
return uuid;
}
3-3
- (void) start {
NSLog(@"Starting to download %@", srcURL);
// create the URL
NSURL *url = [NSURL URLWithString:srcURL];
// Create the request
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// create the connection with the target request and this class as the delegate
self.conn =
[NSURLConnection connectionWithRequest:request
delegate:self];
// start the connection
[self.conn start];
}
示例應(yīng)用實現(xiàn)了幾個委托方法供調(diào)用,同時又有幾個方法沒有實現(xiàn)威兜。委托方法是由兩個協(xié)議定義的:NSURLConnectionDelegate與NSURLConnectionDataDelegate.接下來會回顧已經(jīng)實現(xiàn)的方法和未實現(xiàn)的方法.
- (NSURLRequest *)connection:willSendRequest:redirectResponse:
#pragma mark NSURLConnectionDelegate methods
/**
* This delegate method is called when the NSURLConnection gets a 300 series response that indicates
* that the request needs to be redirected. It is implemented here to display any redirects that might
* occur. This method is optional. If omitted the client will follow all redirects.
**/
- (NSURLRequest *)connection:(NSURLConnection *)connection
willSendRequest:(NSURLRequest *)request
redirectResponse:(NSURLResponse *)redirectResponse {
// Dump debugging information
NSLog(@"Redirect request for %@ redirecting to %@", srcURL, request.URL);
NSLog(@"All headers = %@",
[(NSHTTPURLResponse *)redirectResponse allHeaderFields]);
// Follow the redirect
return request;
}
如果協(xié)議處理器接收到來自服務(wù)器的重定向請求销斟,就會調(diào)用該方法。HTTP重定向指的是這樣一種HTTP響應(yīng)椒舵,它們通知客戶端要尋找的內(nèi)容位于另一個不同的URL中蚂踊。如果應(yīng)用從內(nèi)容分發(fā)網(wǎng)絡(luò)
加載內(nèi)容,那么重定向請求就是很常見的.該委托方法總是在其他傳遞響應(yīng)或體數(shù)據(jù)的方法前得到調(diào)用笔宿。由于請求可以執(zhí)行多個重定向犁钟,因此對于某個請求來說,該方法可能不會被調(diào)用泼橘,也可能被調(diào)用多次涝动。如果委托沒有實現(xiàn)該方法,那么協(xié)議處理器就會將重定向轉(zhuǎn)向新的位置炬灭。通過實現(xiàn)該方法醋粟,代碼可以攔截重定向,并且根據(jù)重定向的特點終止連接或修改請求。在該例中米愿,代碼會執(zhí)行調(diào)試功能厦凤,將重定向請求頭信息的日志打印出來,然后再執(zhí)行重定向育苟。
(void)connection:didReceiveResponse:
如代碼清單3-5.在協(xié)議處理器從響應(yīng)頭構(gòu)建出響應(yīng)對象后较鼓,會調(diào)用該方法。
/**
* This delegate method is called when the NSURLConnection connects to the server. It contains the
* NSURLResponse object with the headers returned by the server. This method may be called multiple times.
* Therefore, it is important to reset the data on each call. Do not assume that it is the first call
* of this method.
**/
- (void) connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse *)response {
NSLog(@"Received response from request to url %@", srcURL);
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSLog(@"All headers = %@", [httpResponse allHeaderFields]);
if (httpResponse.statusCode != 200) {// something went wrong, abort the whole thing
// reset the download counts
if (downloadSize != 0L) {
[progressView addAmountToDownload:-downloadSize];
[progressView addAmountDownloaded:-totalDownloaded];
}
[connection cancel];
return;
}
NSFileManager *fm = [NSFileManager defaultManager];
// If we have a temp file already, close it and delete it
if (self.tempFile != nil) {
[self.outputHandle closeFile];
NSError *error;
[fm removeItemAtPath:self.tempFile error:&error];
}
// remove any pre-existing target file
NSError *error;
[fm removeItemAtPath:targetFile error:&error];
// get the temporary directory name and make a temp file name
NSString *tempDir = NSTemporaryDirectory();
self.tempFile = [tempDir stringByAppendingPathComponent:[self createUUID]];
NSLog(@"Writing content to %@", self.tempFile);
// create and open the temporary file
[fm createFileAtPath:self.tempFile contents:nil attributes:nil];
self.outputHandle = [NSFileHandle fileHandleForWritingAtPath:self.tempFile];
// prime the download progress view
NSString *contentLengthString = [[httpResponse allHeaderFields] objectForKey:@"Content-length"];
// reset the download counts
if (downloadSize != 0L) {
[progressView addAmountToDownload:-downloadSize];
[progressView addAmountDownloaded:-totalDownloaded];
}
downloadSize = [contentLengthString longLongValue];
totalDownloaded = 0L;
[progressView addAmountToDownload:downloadSize];
}
當(dāng)協(xié)議處理器接收到足夠的數(shù)據(jù)來創(chuàng)建URL響應(yīng)對象時會調(diào)用didReceiveResponse方法违柏。如果在接收到足夠的數(shù)據(jù)來構(gòu)建響應(yīng)對象前出現(xiàn)了錯誤博烂,就不會調(diào)用該方法。在示例代碼中漱竖,委托方法會驗證響應(yīng)對象的HTTP狀態(tài)禽篱。如果狀態(tài)不是200,那么請求的加載就會被取消馍惹,提供下載進度的視圖會被重置谆级。如果狀態(tài)是200,那么代碼會更新進度視圖讼积,方式是講所需的數(shù)據(jù)量加起來肥照,然后創(chuàng)建臨時文件來接收HTTP響應(yīng)體,臨時文件稍后會被傳給另一個委托方法勤众。
該方法可能會被協(xié)議處理器調(diào)用多次舆绎;因此,代碼必須重新開始這一場景们颜。在實例中吕朵,重新開始 邏輯 包括重置進度顯示器。如果之前響應(yīng)的臨時文件存在窥突,那么還需要將其刪除努溃。
connection:didReceiveData:
如代碼清單3-6所示。當(dāng)協(xié)議處理器接收到部分或全部響應(yīng)體時會調(diào)用該方法阻问。該方法可能不會被調(diào)用梧税,也可能會調(diào)用多次,并且調(diào)用總是跟在最初的connection:didReceiveResponse:之后称近。如果需要漸進地解析響應(yīng)第队,那么流處理器應(yīng)該充分利用該方法。
/**
* This delegate method is called for each chunk of data received from the server. The chunk size
* is dependent on the network type and the server configuration.
*/
- (void)connection:(NSURLConnection *)connection
didReceiveData:(NSData *)data {
// figure out how many bytes in this chunk
totalDownloaded+=[data length];
// Uncomment if you want a packet by packet log of the bytes received.
NSLog(@"Received %lld of %lld (%f%%) bytes of data for URL %@",
totalDownloaded,
downloadSize,
((double)totalDownloaded/(double)downloadSize)*100.0,
srcURL);
// inform the progress view that data is downloaded
[progressView addAmountDownloaded:[data length]];
// save the bytes received
[self.outputHandle writeData:data];
}
該方法可能不會被調(diào)用刨秆,也可能調(diào)用多次凳谦,這取決于響應(yīng)體的大小。在每次調(diào)用時衡未,協(xié)議處理器就會在data參數(shù)中傳遞部分體數(shù)據(jù)尸执。該委托方法負(fù)責(zé)聚集所提供的數(shù)據(jù)對象家凯,然后處理他們或是將其存儲起來。所提供的數(shù)據(jù)塊大小可能與應(yīng)用協(xié)議的語法邊界不一致如失,換句話說肆饶,如果代碼接收的是XML文檔,那么數(shù)據(jù)對象可能與文檔中的元素邊界不一致岖常,在示例中,應(yīng)用首先將connection:didReceiveResponse:方法中接受到得字節(jié)追加到數(shù)據(jù)文件中葫督。
connection:didFailWithError:
如代碼清單3-7所示竭鞍,當(dāng)連接失敗時會調(diào)用這個委托方法,該方法可能會在請求處理的任何階段得到調(diào)用橄镜。
/**
* This delegate methodis called if the connection cannot be established to the server.
* The error object will have a description of the error
**/
- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error {
NSLog(@"Load failed with error %@",
[error localizedDescription]);
NSFileManager *fm = [NSFileManager defaultManager];
// If we have a temp file already, close it and delete it
if (self.tempFile != nil) {
[self.outputHandle closeFile];
NSError *error;
[fm removeItemAtPath:self.tempFile error:&error];
}
// reset the progress view
if (downloadSize != 0L) {
[progressView addAmountToDownload:-downloadSize];
[progressView addAmountDownloaded:-totalDownloaded];
}
}
如果被調(diào)用偎快,那么該方法將是該連接調(diào)用的最后一個方法。示例應(yīng)用只是在連接失敗時打印出錯誤信息洽胶。如果臨時下載文件存在晒夹,那么將會關(guān)閉臨時文件,并調(diào)整進度指示器以反映出中斷的下載姊氓。一旦該方法返回丐怯,協(xié)議處理器將取消請求。這個方法很適合分析器采用翔横,這樣就可以對應(yīng)用調(diào)用的端點失敗率作出定量度量读跷。
connectionDidFinishLoading委托方法
如代碼清單3-8所示,該例實現(xiàn)的最后一個委托方法是 connectionDidFinishLoading.當(dāng)整個請求完成加載并且接收到的所有數(shù)據(jù)都被傳遞給委托后禾唁,就會調(diào)用該委托方法效览。
/**
* This delegate method is called when the data load is complete. The delegate will be released
* following this call
**/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// close the file
[self.outputHandle closeFile];
// Move the file to the target location
NSFileManager *fm = [NSFileManager defaultManager];
NSError *error;
[fm moveItemAtPath:self.tempFile
toPath:self.targetFile
error:&error];
// Notify any concerned classes that the download is complete
[[NSNotificationCenter defaultCenter]
postNotificationName:kDownloadComplete
object:nil
userInfo:nil];
}
該方法是為連接調(diào)用的最后一個方法,并且此調(diào)用與connection:didFailWithError:的調(diào)用是互斥的荡短。示例應(yīng)用會講聚集所有接收到數(shù)據(jù)的文件關(guān)閉掉丐枉,根據(jù)最初請求的URL將文件移到某個位置,然后通過NSNotificationCenter通知視圖控制器下載完畢掘托。
連接委托還可以實現(xiàn)其他幾個方法來增加可以信息并實現(xiàn)對連接的控制瘦锹。下面將介紹這些方法
connection:needNewBodyStream:
該方法是可選的,只是用于向請求體的輸入流發(fā)出請求闪盔。如果協(xié)議處理器由于出現(xiàn)錯誤或是認(rèn)證等原因需要重新傳遞請求體就會調(diào)用該方法沼本。如下面的代碼片段所示,該方法簽名會接收NSURLConnection對象(用于請求新的數(shù)據(jù)流)以及觸發(fā)該委托回調(diào)的request:
- (NSInputStream *)connection: (NSURLConnection *)connection needNewBodyStream:(nonnull NSURLRequest *)request
connection:didSendBodyData:totalBytesWritten:totalBytesExpectedToWrite
如下代碼片段所示锭沟,該可選的委托方法對象提供了上傳進度信息:
- (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite{
}
協(xié)議處理器會在不確定的時間間隔內(nèi)調(diào)用該委托方法以報告上傳進度抽兆。bytesWritten與totalBytesWritten值可能不會一直增加。這是因為如果出現(xiàn)錯誤或是認(rèn)證問題族淮,就需要重新傳遞請求體辫红。如果想向用戶提供上傳進度指示器凭涂,那么應(yīng)該實現(xiàn)該方法。
connection:willCacheResponse:
如下代碼片段展示贴妻,該可選方法向委托提供了一種方式來檢測與修改協(xié)議控制器所緩存的響應(yīng):
- (NSCachedURLResponse *)connnection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse
NSCachedURLResponse對象包含了NSURLResponse對象與請求返回來的以NSData形式存在的數(shù)據(jù)切油。該對象還包含了響應(yīng)保持所需的存儲策略,包括持久化儲存名惩、僅內(nèi)存存儲或不允許存儲澎胡。緩存下來的響應(yīng)對象還包含目錄userInfo,可被應(yīng)用用來存儲緩存請求的元數(shù)據(jù)娩鹉。如果該委托實現(xiàn)返回nil,那么響應(yīng)就不會被存儲下來攻谁。
認(rèn)證委托方法
有5個委托方法與URL請求的客戶端認(rèn)證有關(guān)。
異步請求與運行循環(huán)
異步請求需要運行循環(huán)弯予。當(dāng)數(shù)據(jù)傳遞到服務(wù)器或是被客戶端接收時戚宦,運行循環(huán)用于實現(xiàn)事件與委托對象之間的通信。異步請求在發(fā)出時锈嫩,會在當(dāng)前線程的運行循環(huán)上操作受楼。這個實現(xiàn)細(xì)節(jié)是很重要的,因為在GCD塊中或是通過NSOperationQueue創(chuàng)建的線程并沒有運行循環(huán)呼寸。因此艳汽,如果在后臺線程上發(fā)出了異步請求,那么還需要確定線程是由運行循環(huán)還是使用了別的運行循環(huán)对雪。如下代碼片段展示了如何顯式地將請求處理指定給運行循環(huán):
NSURLRequest *request;
NSURLConnection *connection1 = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection1 scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[connection1 start];
第一個操作創(chuàng)建了NSURLConnection對象骚灸,不過并沒有立刻啟動方法,這樣就可以進一步初始化了慌植。下一行代碼獲取到主線程的運行循環(huán)(即runloop)甚牲,然后將它提供給連接,作為其運行循環(huán)蝶柿。最后丈钙,連接通過start方法開始處理。如果不想再主運行循環(huán)中執(zhí)行異步請求交汤,那么需要在另一個線程上創(chuàng)建runloop雏赦,然后針對這個新創(chuàng)建的runloop調(diào)度connection.
異步請求的best practice:
- 對于大的上傳或下載來說,請使用異步請求以減少應(yīng)用的內(nèi)存占用量
- 在需要認(rèn)證的情況下請使用異步請求
- 如果需要向用戶提供進度反饋芙扎,那么請使用異步請求
- 在后臺線程上使用異步請求時要小心星岗,請?zhí)峁┮粋€runloop
- 對于可以在后臺線程的請求隊列中輕松調(diào)度和完成的簡單請求來說,這時使用異步請求有些過猶不及
- 如果使用輸入流來上傳數(shù)據(jù)戒洼,請實現(xiàn)connection:newBodyStream:方法以避免對輸入流的復(fù)制
高級HTTP操作
HTTP頭在提供可修改服務(wù)器響應(yīng)的元數(shù)據(jù)以及向HTTP客戶端提供額外信息方面扮演著重要角色俏橘。基于這一點圈浇,iOS開發(fā)者常常需要操縱請求頭或是查看請求頭寥掐。比如靴寂,有些服務(wù)器需要自定義的認(rèn)證頭來提供關(guān)于用戶身份的信息。標(biāo)準(zhǔn)的URL加載系統(tǒng)并不會自動添加這些頭召耘,不過它提供了通過代碼添加的方法百炬。
本節(jié)將會介紹更多地HTTP操作以及可以通過iOS的URL加載系統(tǒng)執(zhí)行的操作。并且將會分別介紹如何創(chuàng)建與使用其他的HTTP請求方法污它、如何處理HTTP cookie以及關(guān)于HTTP的高級用法
使用請求方法
根據(jù)定義剖踊,GET請求不應(yīng)該包含HTTP體,而只應(yīng)該包含請求行和請求頭衫贬。HTTP服務(wù)器會返回由URL指定的資源內(nèi)容德澈。網(wǎng)絡(luò)設(shè)備常常會假定GEET請求完整的上下文位于請求行中,并根據(jù)這些數(shù)據(jù)來緩存響應(yīng)祥山。如果GET請求包含會修改請求所返回內(nèi)容的請求體,那么由于中間網(wǎng)絡(luò)設(shè)備的緩存行為掉伏,你可能會得到錯誤的結(jié)果缝呕。根據(jù)約定,GET請求不應(yīng)導(dǎo)致服務(wù)器上的數(shù)據(jù)發(fā)生任何變化斧散。
HTML瀏覽器最早通過POST請求來發(fā)送或提交HTML表單供常,使用的是一種特定的數(shù)據(jù)編碼,通過application/x-www-form-urlencoded這種Content-Type來指定鸡捐。iOS應(yīng)用通常都會使用POST請求向服務(wù)器發(fā)送XML或JSON數(shù)據(jù)栈暇。下述代碼片段展示了如何創(chuàng)建JSON對象并將其作為請求體:
NSError *error1;
NSDictionary *dict = @{
@"animal" : @"dog",
@"name" : @"fido",
@"weight" : @"20"
};
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:&error1];
NSURL *url;
NSLog(@"Json = %@", [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]);
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:jsonData];
上述代碼首先創(chuàng)建了一個簡單的NSDictionary對象,里面放置了一些虛構(gòu)出來的動物的名值對箍镜。然后通過內(nèi)建的JSON庫源祈,創(chuàng)建一個NSData對象來表示該字典。接下來將該NSData對象提供給NSMutableURLRequest作為請求體色迂。該代碼產(chǎn)生的JSON如下所示(第四章將會詳細(xì)介紹構(gòu)建和解析請求和響應(yīng)負(fù)載的過程):
{
"weight" : "20",
"animal" : "dog",
"name" : "fido"
}
使用HEAD方法的請求會指示HTTP服務(wù)器只返回關(guān)于所請求的HTTP頭信息香缺。HEAD請求通常沒有請求體,也沒有響應(yīng)體返回歇僧。它們常常用于驗證緩存的數(shù)據(jù)與服務(wù)器上的數(shù)據(jù)图张,同時又不必獲取緩存資源的整個內(nèi)容。
PUT請求類似于POST诈悍,因為它總是有請求體祸轮,但從語義上來說,兩者有如下重要差別:PUT請求用于向服務(wù)器添加新的資源侥钳,而POST請求只用于更新服務(wù)器上的資源适袜。在使用RESTful服務(wù)時,這種語義上的差別是非常重要的舷夺。
操作cookie
cookie是HTTP協(xié)議在首個版本之后加入的一個重要組件痪蝇。它向服務(wù)器提供了追蹤回話的能力鄙陡,同時又無需維持客戶端與服務(wù)器之間的連接。在瀏覽器客戶端躏啰,cookie值是由服務(wù)器通過請求提供的趁矾,然后被放到隨后的請求中。由于設(shè)計cookie的目的是追蹤會話狀態(tài)给僵,因此它們通常會非常小毫捣,基本是幾十到幾百個字節(jié)。
從服務(wù)器發(fā)送的cookie有幾個屬性用于確定cookie的值帝际、何時返回到服務(wù)器以及客戶端應(yīng)該保存cookie的時間蔓同,這些屬性有:
- name--cookie的名字,從同一個DNS域返回的所有cookie名都是唯一的蹲诀。只有name和value這兩個屬性才會在后續(xù)的請求中發(fā)送給服務(wù)器
- value--由向服務(wù)器發(fā)送的下一個請求返回的值斑粱。
- domain--后續(xù)請求在cookie中包含的DNS域。比如脯爪,擁有域值domain1.com的cookie不應(yīng)該返回給domain2.com则北。如果忽略掉,那么客戶端就會將URL的主機名當(dāng)作域痕慢。如果域的最前面是個原點(.)尚揣,那么cookie就會返回給發(fā)送到該域及其子域的任何請求。如果沒有最前面的原點掖举,那么cookie就只會包含在發(fā)送給該域而非其子域的請求中快骗。
- path--path限制發(fā)送給請求的cookie都是針對指定的URL路徑。如果與DNS域搭配使用塔次,那么path屬性就可以限制只會將cookie發(fā)送給服務(wù)器上優(yōu)先且精確的URL集合方篮。
- expiration date--cookie不再隨請求發(fā)送的 日期和時間,cookie會在這個時間點從客戶端存儲中移除
- session ONLY--指定cookie是在當(dāng)前瀏覽器會話時間內(nèi)返回還是一直持續(xù)到過期日期励负,以二者之間先到的時間為準(zhǔn)恭取,在iOS應(yīng)用中,回話指的是OS加載應(yīng)用到終止應(yīng)用之間的應(yīng)用生命周期
- secure--指定cookie只會在HTTPS連接而非HTTP連接
- comment--用于向用戶說明cookie目的的注釋值
- comment URL-向用戶提供了一個HTML文檔熄守,用于說明cookie的目的
- HTTP ONLY--指示器蜈垮,告訴客戶端不要與javascript應(yīng)用共享cookie以防止跨站腳本攻擊
- version--cookie遵循的HTTP cookie規(guī)范版本
雖然不是瀏覽器,但iOS應(yīng)用依然可以再HTTP連接中方便地使用cookie裕照。URL加載框架幫我們做了大量繁雜的工作以利用協(xié)議的這個特性攒发。下面是應(yīng)用經(jīng)常要使用到cookie的3個地方:
- 檢索cookie值
- 顯式刪除cookie
- 手工將cookie添加到請求中
URL加載設(shè)施會自動處理所有HTTP與HTTPS請求中的cookie。會將返回的cookie保存在響應(yīng)中晋南,然后按照cookie處理規(guī)則將其添加到隨后的請求中惠猿。只有在cookie的domain屬性提供的DNS域
URI、URL和URN之間的區(qū)別與聯(lián)系:
- URI:Uniform Resource Identifier负间,統(tǒng)一資源標(biāo)識符偶妖;
- URL:Uniform Resource Locator姜凄,統(tǒng)一資源定位符;
- URN:Uniform Resource Name趾访,統(tǒng)一資源名稱态秧。
其中,URL,URN是URI的子集扼鞋。
HTTP的 URL示例:
使用超級文本傳輸協(xié)議HTTP申鱼,提供超級文本信息服務(wù)的資源。
例一:其計算機域名為www.peopledaily.com.cn云头。超級文本文件(文件類型為.html)是在目錄/channel下的welcome.htm捐友。這是中國人民日報的一臺計算機。
http://www.peopledaily.com.cn/channel/welcome.htm
例二:其計算機域名為www.rol.cn.net溃槐。超級文本文件(文件類型為.html)是在目錄/talk下的talk1.htm匣砖。這是瑞得聊天室的地址,可由此進入瑞得聊天室的第1室
http://www.rol.cn.net/talk/talk1.htm
文件的URL示例:
例一:代表存放主機ftp.yoyodyne.com上的pub/files/目錄下的一個文件昏滴,文件名是foobar.txt猴鲫。
file://ftp.yoyodyne.com/pub/files/foobar.txt
例二:代表主機ftp.yoyodyne.com上的目錄/pub。
file://ftp.yoyodyne.com/pub
例三:代表主機ftp.yoyodyne.com上的根目錄
file://ftp.yoyodyne.com/
文件的URL:
用URL表示文件時影涉,服務(wù)器方式用file表示变隔,后面要有主機IP地址规伐、文件的存取路徑(即目錄)和文件名等信息蟹倾。有時可以省略目錄和文件名,但“/”符號不能省略猖闪。
URI
一般由三部分組成:
- 訪問資源的命名機制鲜棠。
- 存放資源的主機名。
- 資源自身的名稱培慌,由路徑表示豁陆。
URL的格式
由下列三部分組成:
- 是協(xié)議(或稱為服務(wù)方式);
- 是存有該資源的主機IP地址(有時也包括端口號)吵护;
- 是主機資源的具體地址盒音。,如目錄和文件名等馅而。
第一部分和第二部分之間用“://”符號隔開祥诽,第二部分和第三部分用“/”符號隔開。第一部分和第二部分是不可缺少的瓮恭,第三部分有時可以省略雄坪。