iOS網(wǎng)絡(luò)編程讀書筆記

iOS網(wǎng)絡(luò)編程讀書筆記

Facade Tester客戶端門面模式的實(shí)例(被動(dòng)版本化)

被動(dòng)版本化,所以硬編碼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ò)請(qǐng)求,用到的是[NSData dataWithContensOfURL:]; 數(shù)據(jù)用SJSONSerialization JSONObjectWithData:data..]進(jìn)行序列化然后就能使用了。

在應(yīng)用委托中定義可以使得在實(shí)現(xiàn)服務(wù)定位器時(shí)更具有靈活性

#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 都會(huì)將結(jié)果以JSON的形式輸出,因此使用JSON來表示服務(wù)定位器比較好。服務(wù)定位器用來探測(cè)天氣與股票報(bào)價(jià)API端點(diǎn)。該結(jié)構(gòu)將端點(diǎn)的所有版本都組合到了一個(gè)文件中。

在應(yīng)用啟動(dòng)和返回到前臺(tái)時(shí)加載服務(wù)定位器,將URL存儲(chǔ)為應(yīng)用委托中的屬性.(更復(fù)雜的應(yīng)用則需要專門的網(wǎng)絡(luò)管理器,由它處理服務(wù)定位器的加載,其他viewcontroller 也會(huì)使用它針對(duì)特定的網(wǎng)絡(luò)調(diào)用查詢端點(diǎn)(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];
        });
    }
});

}

這個(gè)json運(yùn)行于服務(wù)器上,應(yīng)用就能請(qǐng)求到serviceLocator并解析.

HTTP協(xié)議

一個(gè)URL只對(duì)應(yīng)一個(gè)資源,多個(gè)URL可對(duì)應(yīng)同一個(gè)資源.(localhost是個(gè)例外)
URL由5部分構(gòu)成:

http://user:password@hostname:port/absolute-path?query

協(xié)議 認(rèn)證 主機(jī)名 端口 絕對(duì)路徑 查詢字符串

協(xié)議

除了HTTP外還可以使用FTP協(xié)議。iOS應(yīng)用中常用的另外一種協(xié)議還有FILE ,用于請(qǐng)求在應(yīng)用sandbox中的本地資源,如果使用字符串而沒有使用協(xié)議創(chuàng)建NSURL對(duì)象,那么默認(rèn)就會(huì)使用FILE協(xié)議。

端口

HTTP默認(rèn)端口是80,HTTPS默認(rèn)端口443(有些網(wǎng)絡(luò)代理和防火墻會(huì)處于安全或者隱私考慮等原因阻塞非標(biāo)準(zhǔn)端口)

絕對(duì)路徑

很多REST服務(wù)使用路徑來傳遞值,用來唯一標(biāo)識(shí)數(shù)據(jù)庫(kù)中存儲(chǔ)的實(shí)體。比如/customer/456/address/0指定索引為0的地址,具有標(biāo)識(shí)456的用戶.

查詢字符串

多個(gè)查詢參數(shù)用&字符分隔,查詢字符串不可以包含回車、空格與換行符。
iOS為NSString對(duì)象提供了一個(gè)方法來執(zhí)行URL的百分號(hào)編碼

    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編碼,不會(huì)對(duì)&字符編碼,因此也不會(huì)改變URL參數(shù)的分隔.URL編碼會(huì)編碼& ? 和其他標(biāo)點(diǎn)符號(hào),如果查詢的字符串包含了這些字符,那么需要實(shí)現(xiàn)一種更為徹底地編碼方法。

請(qǐng)求內(nèi)容

HTTP請(qǐng)求包含三部分:請(qǐng)求行、請(qǐng)求頭與請(qǐng)求體。請(qǐng)求行和請(qǐng)求頭是文本行,通過回車/換行符分隔(值為13的字節(jié),或是0x0D/值為10的字節(jié),或是0x0A).在HTTP請(qǐng)求中這樣使用文本值,使得它們很容易構(gòu)建、解析和調(diào)試??招校▋H包含回車/換行符或是僅有換行符)將請(qǐng)求頭與請(qǐng)求體分隔開。

HTTP請(qǐng)求實(shí)例:

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

請(qǐng)求行是發(fā)送給服務(wù)器的第一行數(shù)據(jù)。請(qǐng)求行包括3方面主要信息:HTTP請(qǐng)求方法、請(qǐng)求URI與HTTP版本.

標(biāo)準(zhǔn)請(qǐng)求方法都是大寫。

GET

從服務(wù)器獲取一段內(nèi)容(用HTTP術(shù)語(yǔ)來說,就是實(shí)體entity),不會(huì)導(dǎo)致服務(wù)器端的數(shù)據(jù)發(fā)生變化,GET請(qǐng)求通常不包括請(qǐng)求體,不過也可以包含。

POST

使用客戶端提供的數(shù)據(jù)更新實(shí)體.POST請(qǐng)求通常會(huì)在請(qǐng)求體中加入應(yīng)用服務(wù)器(運(yùn)行php等?)所需的信息.POST請(qǐng)求是非冪等(待查)的.這意味著如果處理多個(gè)請(qǐng)求,那么結(jié)果與處理單個(gè)請(qǐng)求是不同的。

HEAD

獲取響應(yīng)的元數(shù)據(jù)而無(wú)需檢索響應(yīng)的全部?jī)?nèi)容。該方法通常用于檢查服務(wù)器最近的內(nèi)容變化而無(wú)須檢索全部?jī)?nèi)容

PUT

使用客戶端提供的數(shù)據(jù)添加實(shí)體。PUT請(qǐng)求通常將應(yīng)用服務(wù)器所需的信息放在請(qǐng)求體中來創(chuàng)建新的實(shí)體。在通常情況下,PUT請(qǐng)求是冪等的,這意味著多個(gè)請(qǐng)求的處理會(huì)產(chǎn)生相同的結(jié)果。

DELETE

根據(jù)URI的內(nèi)容或客戶端提供的請(qǐng)求體來刪除實(shí)體。DELETE請(qǐng)求是REST服務(wù)接口中使用最為頻繁的請(qǐng)求。

說明:
HTTP規(guī)范允許HTTP客戶端與服務(wù)器之間的中介添加、刪除、重排序以及修改HTTP頭。因此,應(yīng)用發(fā)出的請(qǐng)求在到達(dá)服務(wù)器時(shí)可能會(huì)出現(xiàn)以下的情況:添加新的頭,修改已有的頭或者刪除某些頭。

雖然使用了有狀態(tài)的TCP傳輸層,但HTTP卻是個(gè)無(wú)狀態(tài)協(xié)議。這意味著HTTP服務(wù)器并不會(huì)保留關(guān)于某個(gè)請(qǐng)求的任何信息以用在未來的請(qǐng)求中。Cookie提供了一種方式,可以將一些簡(jiǎn)單地狀態(tài)信息存儲(chǔ)在客戶端,并在后續(xù)的請(qǐng)求中與服務(wù)器進(jìn)行通信。
HTTP頭之后是可選的請(qǐng)求體。請(qǐng)求體可以使任意的字節(jié)序列,通過一個(gè)空行與頭分割開來,請(qǐng)求體必須遵循客戶端與服務(wù)器之間預(yù)先確定的數(shù)據(jù)編碼。對(duì)于Web瀏覽器來說,這通常是表單編碼的數(shù)據(jù),但對(duì)于移動(dòng)應(yīng)用來說,通常是JSON數(shù)據(jù)。在iOS中NSURLRequest及其子類NSMutableURLRequest提供了必要的方法與屬性來構(gòu)建幾乎所有的HTTP請(qǐng)求。
在HTTP服務(wù)器與應(yīng)用服務(wù)器處理完請(qǐng)求后,HTTP響應(yīng)會(huì)通過同一個(gè)TCP socket[待查]返回給客戶端.HTTP響應(yīng)的結(jié)構(gòu)類似于HTTP請(qǐng)求,第一行也是狀態(tài)行,后面是頭,然后是響應(yīng)體。
簡(jiǎn)單的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個(gè)域,域之間通過空格分隔。第一個(gè)域是響應(yīng)HTTP的版本。接下來的兩個(gè)域提供了表示請(qǐng)求結(jié)果的狀態(tài)值。首先是一個(gè)3位的整數(shù)值,包含了請(qǐng)求的結(jié)果代碼,最后是條說明語(yǔ)句,提供了關(guān)于代碼的簡(jiǎn)短文本說明。在大多數(shù)情況下,數(shù)值能夠完全說明狀態(tài)。
緊跟狀態(tài)行的是響應(yīng)頭:響應(yīng)頭之間通過回車/換行符進(jìn)行分隔。每個(gè)頭都包含了關(guān)于響應(yīng)的元數(shù)據(jù),包括數(shù)據(jù)的上一次修改時(shí)間、客戶端可以緩存數(shù)據(jù)多少時(shí)間、數(shù)據(jù)的編碼方式以及在隨后的請(qǐng)求中提交的狀態(tài)信息。
響應(yīng)體是通過空行與響應(yīng)頭分隔開的。響應(yīng)體可以包含任意數(shù)量的二進(jìn)制字符。與客戶端通信的響應(yīng)體的長(zhǎng)度可以通過請(qǐng)求的Content-Length頭或者塊編碼體現(xiàn)。塊編碼響應(yīng)包含Transfer-Encoding頭,并且?guī)в衏hunked值。塊編碼體包含一個(gè)或(多個(gè))(體片段).每個(gè)片段都有起始行,指定塊中字節(jié)數(shù)量,iOS URL加載系統(tǒng)向應(yīng)用隱藏了這種復(fù)雜性。
在iOS的URL加載系統(tǒng)中,NSURLResponse及其子類NSHTTPURLResponse封裝了請(qǐng)求返回的數(shù)據(jù)。該繼承體系中有兩個(gè)對(duì)象,因?yàn)閁RL加載可以基于非HTTP URL實(shí)現(xiàn)數(shù)據(jù)的請(qǐng)求。比如,對(duì)URL file://的請(qǐng)求就不會(huì)包含任何頭信息.

高層iOS HTTP API

主要有3個(gè)方法可以執(zhí)行HTTP請(qǐng)求和接受響應(yīng):

  • 同步。啟動(dòng)線程 的 代碼會(huì)阻塞,直到整個(gè)響應(yīng)加載完畢并返回到調(diào)用方法為止。該技術(shù)最容易實(shí)現(xiàn),不過局限性也最大。
  • 隊(duì)列式異步。起始代碼創(chuàng)建一個(gè)請(qǐng)求,并將其放到一個(gè)隊(duì)列中以在后臺(tái)線程中執(zhí)行。
  • 異步。起始代碼 開啟一個(gè)請(qǐng)求,該請(qǐng)求運(yùn)行在起始線程中,不過在請(qǐng)求處理時(shí)會(huì)調(diào)用委托方法,該方法的實(shí)現(xiàn)最為復(fù)雜,不過在處理響應(yīng)時(shí)卻提供了最大的靈活性。(在特定時(shí)刻執(zhí)行回調(diào)函數(shù),方便進(jìn)行操作)
    所有這三個(gè)請(qǐng)求都由相同的4個(gè)對(duì)象構(gòu)成:NSURL,NSURLRequese,NSURLConnection,NSURLResponse
NSURL

可以通過NSURL對(duì)象輕松管理URL值并訪問URL所指向的內(nèi)容。NSURL可以指向文件資源,也可以指向網(wǎng)絡(luò)資源,同時(shí)在這兩類資源類型的使用上沒有任何區(qū)別。從URL加載數(shù)據(jù)示例代碼:

NSURL *url = [NSURL urlWithString:mysteryString];
NSData *data = [NSData dataWithContentsOfURL:url];

mysteryString的值可以引用文件或網(wǎng)絡(luò)資源,而代碼的行為是一樣的。主要的差別在于加載mysteryString所引用的資源的時(shí)間上。如果URL引用的是網(wǎng)絡(luò)資源,就會(huì)在后臺(tái)現(xiàn)成執(zhí)行代碼,這樣在數(shù)據(jù)加載時(shí)用戶界面就不會(huì)暫停下來。(也就是自動(dòng)轉(zhuǎn)到后臺(tái)操作咯?不錯(cuò))
創(chuàng)建NSURL對(duì)象最常見的方式是使用類方法 URLWithString:進(jìn)行實(shí)例化。該方法會(huì)創(chuàng)建一個(gè)NSURL對(duì)象,并使用提供的NSString對(duì)象的內(nèi)容對(duì)其進(jìn)行初始化,下面這段代碼說明了這一點(diǎn):

NSURL *url = [NSURL URLWithString:@"http://www.wiley.com/path1"];

NSURL對(duì)象提供了很多訪問器方法來讀取URL各個(gè)部分的值。每個(gè)訪問器都可以只讀訪問URL的一部分。scheme訪問器會(huì)返回一個(gè)包含該URL所用協(xié)議的NSString對(duì)象。如果目標(biāo)URL沒有指定某個(gè)特定部分,那么返回值就為nil,考慮之前創(chuàng)建的url對(duì)象,下面代碼會(huì)打印出 Port is nil.

if (url.port == nil){
    NSLog(@"Port is nil");
} else {
    NSLog(@"Port is not nil");
}

如果URL包含查詢字符串,那么query訪問器方法就會(huì)包含所有需要查詢參數(shù)的值。根據(jù)RFC 3986的要求,在創(chuàng)建NSURL對(duì)象前,URL字符串的內(nèi)容需要以百分號(hào)編碼。比如,如果執(zhí)行下述代碼片段,那么查詢參數(shù)的值就為q=iOS+Networking

NSURL *url = [NSURL URLWithString:@"http://google.com?q=iOS+Networking"];

NSURL對(duì)象是不可變的,這意味著無(wú)法先構(gòu)建空的NSURL對(duì)象,然后通過調(diào)用對(duì)象的賦值方法(有時(shí)也叫setter)方法來裝配其屬性。對(duì)象要么1.通過NSString對(duì)象,要么2.通過另一個(gè)NSURL對(duì)象實(shí)例化.如果用于實(shí)例化NSURL對(duì)象的字符串是不合法的,那么創(chuàng)建方法就會(huì)返回nil。在使用URL對(duì)象進(jìn)行網(wǎng)絡(luò)請(qǐng)求前,應(yīng)該驗(yàn)證URL對(duì)象是合法的。

NSURLRequest

創(chuàng)建好NSURL對(duì)象后,接下來就需要完成執(zhí)行HTTP請(qǐng)求所需的下一步了:創(chuàng)建NSURLRequest對(duì)象。NSURLRequest對(duì)象包含了加載URL內(nèi)容所需的信息,并且獨(dú)立于URL中指定的協(xié)議。iOS中的URL加載系統(tǒng)支持HTTP,HTTPS,F(xiàn)TP,F(xiàn)ILE URL內(nèi)容的加載。URL加載系統(tǒng)提供了一種擴(kuò)展方式以處理新的協(xié)議,方式是創(chuàng)建NSURLProtocol自雷,然后將返回結(jié)果提供給URL加載系統(tǒng)。
創(chuàng)建NSURLRequest對(duì)象最簡(jiǎn)單的方式是通過類方法和提供的NSURL對(duì)象。下述代碼片段展現(xiàn)了使用默認(rèn)值來創(chuàng)建請(qǐng)求對(duì)象的過程

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)值表示請(qǐng)求使用URL協(xié)議指定的請(qǐng)求緩存規(guī)則,請(qǐng)求有著標(biāo)準(zhǔn)的請(qǐng)求超時(shí)。如果URL是HTTP或HTTPS,那么請(qǐng)求方法將是GET。并且使用操作系統(tǒng)提供的默認(rèn)頭。
下列示例展示了如何使用自定義的緩存和超時(shí)值來創(chuàng)建NSURLRequest對(duì)象。構(gòu)建URL加載系統(tǒng)的代碼忽略了所有緩存,如果完成請(qǐng)求連接的時(shí)間超過20s,將會(huì)發(fā)生錯(cuò)誤.
NSURLRequest對(duì)象提供了幾個(gè)訪問器方法來獲取請(qǐng)求的屬性,由于NSURLRequest類是不可變的,因此無(wú)法更改這些屬性(readonly)。除了URL、緩存策略和超時(shí)值之外,如果要修改其他屬性,那么請(qǐng)用NSMutableURLRequest類。
NSMutableURLRequest是NSURLRequest的子類,提供了賦值方法以修改請(qǐng)求的屬性。下述代碼片段展示了使用一小段消息體來創(chuàng)建一個(gè)簡(jiǎn)單的POST請(qǐng)求,它包含了以UTF8編碼的一個(gè)NSString對(duì)象的字節(jié)。URL加載系統(tǒng)會(huì)自動(dòng)裝配請(qǐng)求的Content-Length頭:

    NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
[req setHTTPMethod:@"POST"];
[req setHTTPBody:[@"Post body" dataUsingEncoding:NSUTF8StringEncoding]];

有兩種方式可以向NSURLRequest提供HTTP體:在內(nèi)存中(就像之前的示例一樣)或是通過NSInputStream。代碼可以通過輸入流提供請(qǐng)求體而無(wú)需將整個(gè)體加載到內(nèi)存中。如果發(fā)送諸如照片或視頻等大容量?jī)?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對(duì)象包含HTTP與非HTTP請(qǐng)求的屬性,因此訪問非 HTTP URL的代碼需要將特定于HTTP的屬性的值設(shè)為nil.

NSURLConnection

NSURLConnection對(duì)象是URL加載系統(tǒng)活動(dòng)的中心,但提供的接口卻不多,只提供了用于初始化、開啟與取消連接的方法。。
回到上面提到的用于執(zhí)行HTTP請(qǐng)求和獲取響應(yīng)的主要方法上來,NSURLConnection類通過3種不同的操作模式發(fā)揮作用:同步,異步,隊(duì)列式異步。同步模式是最易于使用的,不過卻有很多限制,這使得它不太適合更加高級(jí)的交互。異步模式提供了很大的靈活性,不過其代價(jià)就是增加了代碼的復(fù)雜性。隊(duì)列式異步模式提供了異步模式的后臺(tái)操作,同時(shí)又保持了同步模式的簡(jiǎn)單性。
在異步模式下操作時(shí),NSURLConnection對(duì)象會(huì)調(diào)用委托對(duì)象來指引連接流、處理到來的數(shù)據(jù)、處理認(rèn)證并對(duì)錯(cuò)誤做出響應(yīng)。

NSURLResponse

NSURLReponse對(duì)象會(huì)再URL加載請(qǐng)求完畢后返回。響應(yīng)對(duì)象的內(nèi)容根據(jù)請(qǐng)求的類型及成功與否會(huì)有較大變化。如下列表介紹了從請(qǐng)求返回的各種對(duì)象。還有兩個(gè)對(duì)象也可能來自于URL加載請(qǐng)求:NSError對(duì)象和NSData對(duì)象。如果請(qǐng)求有問題或者客戶端無(wú)法連接到服務(wù)器,就會(huì)產(chǎn)生NSError對(duì)象。如果有響應(yīng)返回,那么生成的NSData對(duì)象就會(huì)包含響應(yīng)體。如果生成了NSError對(duì)象,就不會(huì)再生成NSURLResponse與NSData 對(duì)象。

  • MIMEType--結(jié)果數(shù)據(jù)的MIME類型。該值來自于服務(wù)器,如果客戶端框架認(rèn)為服務(wù)器有錯(cuò),那么可以修改,如果服務(wù)器沒有提供,還可以由客戶端框架提供。

  • expectedContentLength--該值可能由請(qǐng)求返回,也可能不返回,返回值可能與返回內(nèi)容的實(shí)際大小不同。如果返回內(nèi)容的大小未知,那么該值將等于NSURLResponseUnknownLength

  • suggestedFilename--要么是服務(wù)器提供的內(nèi)容文件名,要么來自于URL和MIME
    URL--返回內(nèi)容的URL。由于重定向和標(biāo)準(zhǔn)化等原因,該URL可能與提供的URL不同。

  • textEncodingName--最初的數(shù)據(jù)源所用的文本編碼名。如果響應(yīng)中沒有使用文本編碼,那么該值將會(huì)是nil.
    URL加載系統(tǒng)提供了一個(gè)名為NSHTTPURLResponse的NSURLResponse子類,它包含特定于HTTP請(qǐng)求的屬性。該類對(duì)于確定HTTP請(qǐng)求的結(jié)果是必需的。它有如下參數(shù):

  • 響應(yīng)頭--該屬性返回響應(yīng)頭值得NSDictionary對(duì)象。字典的鍵是頭的名字,每個(gè)鍵的值是頭的值。HTTP規(guī)范孕育一個(gè)請(qǐng)求有多個(gè)同名的頭。NSHTTPURLResponse通過返回一個(gè)包含所有頭值的NSString對(duì)象(頭值之間用逗號(hào)分隔)來處理這一點(diǎn)。

  • HTTP狀態(tài)碼--來自于響應(yīng)狀態(tài)行的整數(shù)狀態(tài)碼。NSHTTPURLResponse類有一個(gè)類方法可以針對(duì)任意狀態(tài)返回本地化的字符串說明。

同步請(qǐng)求

同步請(qǐng)求是iOS中最簡(jiǎn)單的請(qǐng)求類型,不過簡(jiǎn)單的代價(jià)則是縮減的功能與靈活性的降低。在發(fā)出同步請(qǐng)求時(shí),請(qǐng)求所處的線程就會(huì)阻塞,直到請(qǐng)求完成或失敗為止。同步請(qǐng)求通常用于創(chuàng)建HTTP GET請(qǐng)求后在后臺(tái)線程中獲取已知大小的資源。比如使用同步請(qǐng)求在后臺(tái)線程中可以輕松獲取圖片并顯示在單元格中。
同步請(qǐng)求展示了獲取URL數(shù)據(jù)的最簡(jiǎn)單方式,在iOS API中有很多輔助方法的底層使用的都是同步請(qǐng)求。比如,NSString stringWithContentsOfURL:方法會(huì)創(chuàng)建一個(gè)NSString實(shí)例,然后根據(jù)URL的內(nèi)容從任意服務(wù)器獲取這些內(nèi)容。如果URL使用了FILE(如:file://foo.txt)協(xié)議,就會(huì)從本地文件系統(tǒng)獲取內(nèi)容。如果URL使用了HTTP(如:http://www.wiley.com)協(xié)議,就會(huì)從遠(yuǎn)程服務(wù)器獲取內(nèi)容。因此,除非知道URL是FILE URL,否則在使用這些輔助方法從URL中獲取內(nèi)容時(shí)務(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)用者中接受一個(gè)NSString對(duì)象.當(dāng)URL對(duì)象構(gòu)建完畢后,會(huì)實(shí)例化一個(gè)NSURLRequest對(duì)象。在該例中,代碼重寫了默認(rèn)的緩存策略和超時(shí)時(shí)間。注意:這里通過NSURLRequestReloadIgnoringLocalAndRemoteCacheData將緩存策略設(shè)為用不緩存。這樣可以更好地表現(xiàn)出同步請(qǐng)求對(duì)UI的影響,因?yàn)閁I線程會(huì)被阻塞。通常情況下,代碼不會(huì)重寫所有緩存,不過重寫默認(rèn)的超時(shí)時(shí)間倒是比較常見的,這里為請(qǐng)求指定30s的超時(shí)時(shí)間。
創(chuàng)建好請(qǐng)求后,代碼會(huì)調(diào)用NSURLConnection的類方法sendSynchronousRequest:returningReponse:error:來執(zhí)行請(qǐng)求。該方法將請(qǐng)求和兩個(gè)指針作為傳入?yún)?shù):一個(gè)指向NSURLResponse對(duì)象,它會(huì)由服務(wù)器的響應(yīng)進(jìn)行裝配;另一個(gè)指向NSError對(duì)象,如果請(qǐng)求失敗,該對(duì)象中就包含詳細(xì)的錯(cuò)誤信息。response指針指向NSURLResponse的一個(gè)實(shí)例,然而,它將是用于處理所有HTTP請(qǐng)求的NSHTTPURLResponse子類實(shí)例。如果NSError不為nil,那就說明請(qǐng)求在某個(gè)底層失敗了,然而,如果為nil,那就表明請(qǐng)求沒有因?yàn)榫W(wǎng)絡(luò)錯(cuò)誤或是不合法的URL而失敗。但請(qǐng)求仍舊可能在語(yǔ)義上失敗,比如服務(wù)器響應(yīng)說遇到了內(nèi)部服務(wù)器錯(cuò)誤等。error指針指向的NSError對(duì)象的內(nèi)容包含了關(guān)于錯(cuò)誤的詳細(xì)說明信息。
代碼清單3-1中的代碼會(huì)檢查NSError對(duì)象是否存在,以及NSHTTPURLResponse對(duì)象的狀態(tài)碼。如果兩者都標(biāo)識(shí)為成功,那么方法就會(huì)繼續(xù)執(zhí)行。
sendSynchronousRequest:returingResponse:error方法會(huì)以NSData對(duì)象的形式返回HTTP相應(yīng)體。由于源以XML形式表示,因此請(qǐng)求成功的NSData對(duì)象會(huì)被XMLReader讀取器解析到NSDictionary中,接下來會(huì)遍歷該字典,將RSS條目列表返回給調(diào)用者。
創(chuàng)建同步調(diào)用相當(dāng)簡(jiǎn)單,僅需幾行代碼就可以成功從服務(wù)器獲取數(shù)據(jù),不過這種簡(jiǎn)單性是以有限的使用場(chǎng)景和增加缺陷的風(fēng)險(xiǎn)為代價(jià)的。
同步請(qǐng)求的best practice:

  • 只在后臺(tái)線程中使用同步請(qǐng)求,除非確定請(qǐng)求訪問的是本地文件資源,否則請(qǐng)不要在main_queue中使用!
  • 只有在知道返回?cái)?shù)據(jù)不會(huì)超出應(yīng)用的內(nèi)存時(shí)才可以使用同步請(qǐng)求。記住,整個(gè)響應(yīng)體都會(huì)位于代碼的內(nèi)存中。如果響應(yīng)很大,那么可能導(dǎo)致應(yīng)用出現(xiàn)內(nèi)存溢出(memory leak)的問題。此外,當(dāng)代碼將響應(yīng)解析為所需的格式時(shí)可能需要復(fù)制(類似copy語(yǔ)義?)返回的數(shù)據(jù),這會(huì)導(dǎo)致內(nèi)存增加一倍。
  • 在處理返回的數(shù)據(jù)前,驗(yàn)證錯(cuò)誤與調(diào)用返回的HTTP響應(yīng)狀態(tài)碼(一般是200?)
  • 如果源URL需要驗(yàn)證,那么不要使用同步請(qǐng)求。因?yàn)橥娇蚣懿⒉恢С謱?duì)認(rèn)證請(qǐng)求作出響應(yīng)。唯一的例外是BASIC認(rèn)證,因?yàn)檫@時(shí)認(rèn)證信息可以通過URL或請(qǐng)求頭進(jìn)行傳遞。以這種方式執(zhí)行認(rèn)證會(huì)增加應(yīng)用與服務(wù)器之間的耦合度,從而導(dǎo)致整個(gè)應(yīng)用變得脆弱。如果請(qǐng)求不使用HTTPS協(xié)議,那么還是會(huì)再明文中傳遞認(rèn)證信息。
  • 如果需要向用戶提供進(jìn)度條,那么不要使用同步請(qǐng)求,因?yàn)檎?qǐng)求是原子的(個(gè)人理解:加鎖后,一次只能由一個(gè)東西對(duì)它訪問,因而不能顯示進(jìn)度條),無(wú)法提供中間的進(jìn)度提示信息。
  • 如果需要流解析器[待查]來漸進(jìn)解析響應(yīng)數(shù)據(jù),那么不要使用同步請(qǐng)求
  • 如果在請(qǐng)求完成前需要取消,那么不要使用同步請(qǐng)求。

隊(duì)列式異步請(qǐng)求

隊(duì)列式異步請(qǐng)求類似于同步請(qǐng)求。程序提供NSURLRequest對(duì)象,URL加載系統(tǒng)嘗試加載請(qǐng)求而不會(huì)與調(diào)用代碼之間存在任何其他的交互。這兩種方式之間的主要差別在于URL加載系統(tǒng)執(zhí)行的隊(duì)列式異步請(qǐng)求位于隊(duì)列中,可能位于后臺(tái)線程上。隊(duì)列式異步請(qǐng)求的概念是在iOS 5.0中增加的。
iOS提供了一種叫做操作隊(duì)列的設(shè)施(NSOperationQueue)。這些隊(duì)列可以讓程序描述待執(zhí)行的操作,然后以 FIFO的順序提交操作供隊(duì)列執(zhí)行。隊(duì)列框架提供了優(yōu)先級(jí)順序以及根據(jù)操作依賴的順序,不過URL加載系統(tǒng)并沒有使用這些設(shè)施。
在代碼構(gòu)建隊(duì)列式異步請(qǐng)求前,首先需要?jiǎng)?chuàng)建隊(duì)列,里面是執(zhí)行的請(qǐng)求。下述代碼展示了如何創(chuàng)建操作隊(duì)列:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];

操作隊(duì)列可以并發(fā)執(zhí)行多個(gè)操作。在默認(rèn)情況下,并發(fā)操作的數(shù)量是由iOS根據(jù)系統(tǒng)情況決定的??梢酝ㄟ^調(diào)用創(chuàng)建隊(duì)列的setMaxConcurrentOperationCount:方法來重寫默認(rèn)值。當(dāng)應(yīng)用啟動(dòng)時(shí),一個(gè)隊(duì)列就會(huì)自動(dòng)創(chuàng)建,可以通過調(diào)用NSOperationQueue的mainQueue類方法來獲取該隊(duì)列。不要使用該隊(duì)列做網(wǎng)絡(luò)請(qǐng)求,因?yàn)闀?huì)在主線程上操作,長(zhǎng)時(shí)間操作會(huì)凍結(jié)用戶界面。如果從測(cè)試應(yīng)用的分隔控件中選擇隊(duì)列選項(xiàng)并tap刷新按鈕,那么會(huì)發(fā)現(xiàn)刷新按鈕會(huì)立刻返回默認(rèn)狀態(tài)。同時(shí)表現(xiàn)為tableView會(huì)被清空。之所以會(huì)出現(xiàn)這種情況是因?yàn)檎?qǐng)求是排隊(duì)的,主運(yùn)行循環(huán)會(huì)繼續(xù)執(zhí)行而不會(huì)等待請(qǐng)求完成。

- (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)建和處理排隊(duì)請(qǐng)求的結(jié)果。注意這種另類的delegate方式(不知道寫法優(yōu)劣如何)。與同步請(qǐng)求一樣,首先會(huì)創(chuàng)建一個(gè)NSURL對(duì)象,然后向其傳遞一個(gè)新的NSURLRequest對(duì)象。請(qǐng)求創(chuàng)建完畢之后,如果queue不存在,那么代碼會(huì)創(chuàng)建一個(gè)名為queue的NSOperationQueue對(duì)象。該變量在FeedLoader類的實(shí)現(xiàn)中被聲明為靜態(tài)變量。通常情況下,應(yīng)用會(huì)在啟動(dòng)時(shí)在應(yīng)用委托中創(chuàng)建一個(gè)隊(duì)列,然后在整個(gè)應(yīng)用中都使用該隊(duì)列。由于知道隊(duì)列已經(jīng)存在,因此代碼會(huì)調(diào)用NSURLConnection來執(zhí)行隊(duì)列中的請(qǐng)求,并在操作完成或失敗后調(diào)用一個(gè)塊(就是那個(gè) completionHandler)。當(dāng)請(qǐng)求位于隊(duì)列中時(shí),doQueueRequest:delegate方法會(huì)返回到調(diào)用者。由于方法會(huì)在URL加載完畢前返回(異步),因此當(dāng)加載完畢時(shí)需要由一個(gè)委托類去調(diào)用(id類型的delegate)。由于這里使用了異步完成模式,因此代碼需要實(shí)現(xiàn)委托或通知模式,從而將接受到的數(shù)據(jù)傳遞給最初的請(qǐng)求對(duì)象。
待執(zhí)行的代碼塊通過sendAsynchronousRequest:queue:completionHandler方法的completionHandler參數(shù)傳遞。completion塊驗(yàn)證請(qǐng)求沒有生成錯(cuò)誤,并且HTTP狀態(tài)碼為200.這表示成功。如果請(qǐng)求是成功的,那么返回的數(shù)據(jù)就會(huì)被解析到NSDictionary中。接下來,代碼會(huì)驗(yàn)證所提供的委托類支持setVideos:方法(respondsToSelector:)。如果支持,就會(huì)在主線程上調(diào)用該方法,并提供RSS源返回的條目數(shù)組。setVideos:之所以要在主線程上調(diào)用,是因?yàn)閏ompletion塊是在后臺(tái)線程在執(zhí)行的。如果委托方法在該context中執(zhí)行并操作UI界面,那么結(jié)果是undefined,大多數(shù)情況下都是不正確的。
隊(duì)列式異步請(qǐng)求的best practice:

  • 只有在知道返回的數(shù)據(jù)不會(huì)超過應(yīng)用的內(nèi)存時(shí)才能使用隊(duì)列式異步請(qǐng)求操作。記住,整個(gè)響應(yīng)體都會(huì)位于代碼的內(nèi)存中。如果響應(yīng)很大,那么可能導(dǎo)致應(yīng)用出現(xiàn)memory leak問題。此外,當(dāng)代碼將響應(yīng)解析為所需格式時(shí)可能需要復(fù)制返回的數(shù)據(jù),這會(huì)導(dǎo)致內(nèi)存增加一倍。
  • 為所有操作使用單一的NSOprationQueue,根據(jù)服務(wù)器的能力以及預(yù)期的網(wǎng)絡(luò)狀況控制當(dāng)前操作的最大數(shù)量。
  • 在處理返回的數(shù)據(jù)前驗(yàn)證錯(cuò)誤與調(diào)用返回的HTTP響應(yīng)狀態(tài)碼
  • 如果源URL需要驗(yàn)證,那么不要使用隊(duì)列式異步請(qǐng)求,因?yàn)樵摴δ懿⒉恢С謱?duì)認(rèn)證請(qǐng)求作出響應(yīng)。如果服務(wù)需要驗(yàn)證,那么可以將BASIC認(rèn)證信息放在提供給請(qǐng)求的URL中(感覺相當(dāng)不安全?。?/li>
  • 如果需要向用戶提供進(jìn)度條,那么不要使用隊(duì)列式異步請(qǐng)求,因?yàn)檎?qǐng)求是原子的,無(wú)法提供中間的進(jìn)度指示信息。
  • 如果需要流解析器來漸進(jìn)解析響應(yīng)數(shù)據(jù),那么不要使用隊(duì)列式異步請(qǐng)求。
  • 如果請(qǐng)求在完成前需要取消,那么不要使用隊(duì)列式異步請(qǐng)求。(跟同步請(qǐng)求有相當(dāng)一部分的類型要求)

異步請(qǐng)求

異步請(qǐng)求使用與同步和隊(duì)列式異步請(qǐng)求相同的對(duì)象,只不過又增加了另一個(gè)對(duì)象,即NSURLConnectionDelegate對(duì)象
協(xié)議處理器在HTTP協(xié)議過程中處理時(shí),會(huì)在連接的重要階段調(diào)用委托方法.
協(xié)議處理器在調(diào)用方法前會(huì)先驗(yàn)證委托實(shí)現(xiàn)了該方法。如果沒有實(shí)現(xiàn),那么協(xié)議處理器就會(huì)假定一個(gè)默認(rèn)值并在連接中繼續(xù)處理。代碼清單3-3包含了使用異步技術(shù)初始化URL加載請(qǐng)求的代碼。一開始,該方法與之前的技術(shù)很類似:創(chuàng)建NSURL對(duì)象,然后用來構(gòu)建請(qǐng)求。當(dāng)請(qǐng)求構(gòu)建網(wǎng)壁厚,代碼會(huì)創(chuàng)建NSURLConnection對(duì)象并將自身作為委托對(duì)象。在URL內(nèi)容加載時(shí),協(xié)議處理器會(huì)調(diào)用委托類并提供關(guān)于請(qǐng)求狀態(tài)的信息。借助于這些回調(diào),委托類可以調(diào)整協(xié)議處理器的行為。
創(chuàng)建好連接后,代碼會(huì)開始請(qǐng)求。在連接創(chuàng)建與開啟連接之間,應(yīng)用可以修改委托消息傳遞給委托類的方式。代碼可以指定不同的運(yùn)行循環(huán)或操作隊(duì)列來傳遞回調(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)用實(shí)現(xiàn)了幾個(gè)委托方法供調(diào)用,同時(shí)又有幾個(gè)方法沒有實(shí)現(xiàn)。委托方法是由兩個(gè)協(xié)議定義的:NSURLConnectionDelegate與NSURLConnectionDataDelegate.接下來會(huì)回顧已經(jīng)實(shí)現(xiàn)的方法和未實(shí)現(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ù)器的重定向請(qǐng)求,就會(huì)調(diào)用該方法。HTTP重定向指的是這樣一種HTTP響應(yīng),它們通知客戶端要尋找的內(nèi)容位于另一個(gè)不同的URL中。如果應(yīng)用從內(nèi)容分發(fā)網(wǎng)絡(luò)加載內(nèi)容,那么重定向請(qǐng)求就是很常見的.該委托方法總是在其他傳遞響應(yīng)或體數(shù)據(jù)的方法前得到調(diào)用。由于請(qǐng)求可以執(zhí)行多個(gè)重定向,因此對(duì)于某個(gè)請(qǐng)求來說,該方法可能不會(huì)被調(diào)用,也可能被調(diào)用多次。如果委托沒有實(shí)現(xiàn)該方法,那么協(xié)議處理器就會(huì)將重定向轉(zhuǎn)向新的位置。通過實(shí)現(xiàn)該方法,代碼可以攔截重定向,并且根據(jù)重定向的特點(diǎn)終止連接或修改請(qǐng)求。在該例中,代碼會(huì)執(zhí)行調(diào)試功能,將重定向請(qǐng)求頭信息的日志打印出來,然后再執(zhí)行重定向。

(void)connection:didReceiveResponse:

如代碼清單3-5.在協(xié)議處理器從響應(yīng)頭構(gòu)建出響應(yīng)對(duì)象后,會(huì)調(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)對(duì)象時(shí)會(huì)調(diào)用didReceiveResponse方法。如果在接收到足夠的數(shù)據(jù)來構(gòu)建響應(yīng)對(duì)象前出現(xiàn)了錯(cuò)誤,就不會(huì)調(diào)用該方法。在示例代碼中,委托方法會(huì)驗(yàn)證響應(yīng)對(duì)象的HTTP狀態(tài)。如果狀態(tài)不是200,那么請(qǐng)求的加載就會(huì)被取消,提供下載進(jìn)度的視圖會(huì)被重置。如果狀態(tài)是200,那么代碼會(huì)更新進(jìn)度視圖,方式是講所需的數(shù)據(jù)量加起來,然后創(chuàng)建臨時(shí)文件來接收HTTP響應(yīng)體,臨時(shí)文件稍后會(huì)被傳給另一個(gè)委托方法。
該方法可能會(huì)被協(xié)議處理器調(diào)用多次;因此,代碼必須重新開始這一場(chǎng)景。在實(shí)例中,重新開始 邏輯 包括重置進(jìn)度顯示器。如果之前響應(yīng)的臨時(shí)文件存在,那么還需要將其刪除。

connection:didReceiveData:

如代碼清單3-6所示。當(dāng)協(xié)議處理器接收到部分或全部響應(yīng)體時(shí)會(huì)調(diào)用該方法。該方法可能不會(huì)被調(diào)用,也可能會(huì)調(diào)用多次,并且調(diào)用總是跟在最初的connection:didReceiveResponse:之后。如果需要漸進(jìn)地解析響應(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];
}

該方法可能不會(huì)被調(diào)用,也可能調(diào)用多次,這取決于響應(yīng)體的大小。在每次調(diào)用時(shí),協(xié)議處理器就會(huì)在data參數(shù)中傳遞部分體數(shù)據(jù)。該委托方法負(fù)責(zé)聚集所提供的數(shù)據(jù)對(duì)象,然后處理他們或是將其存儲(chǔ)起來。所提供的數(shù)據(jù)塊大小可能與應(yīng)用協(xié)議的語(yǔ)法邊界不一致,換句話說,如果代碼接收的是XML文檔,那么數(shù)據(jù)對(duì)象可能與文檔中的元素邊界不一致,在示例中,應(yīng)用首先將connection:didReceiveResponse:方法中接受到得字節(jié)追加到數(shù)據(jù)文件中。

connection:didFailWithError:

如代碼清單3-7所示,當(dāng)連接失敗時(shí)會(huì)調(diào)用這個(gè)委托方法,該方法可能會(huì)在請(qǐng)求處理的任何階段得到調(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)用的最后一個(gè)方法。示例應(yīng)用只是在連接失敗時(shí)打印出錯(cuò)誤信息。如果臨時(shí)下載文件存在,那么將會(huì)關(guān)閉臨時(shí)文件,并調(diào)整進(jìn)度指示器以反映出中斷的下載。一旦該方法返回,協(xié)議處理器將取消請(qǐng)求。這個(gè)方法很適合分析器采用,這樣就可以對(duì)應(yīng)用調(diào)用的端點(diǎn)失敗率作出定量度量。

connectionDidFinishLoading委托方法

如代碼清單3-8所示,該例實(shí)現(xiàn)的最后一個(gè)委托方法是 connectionDidFinishLoading.當(dāng)整個(gè)請(qǐng)求完成加載并且接收到的所有數(shù)據(jù)都被傳遞給委托后,就會(huì)調(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)用的最后一個(gè)方法,并且此調(diào)用與connection:didFailWithError:的調(diào)用是互斥的。示例應(yīng)用會(huì)講聚集所有接收到數(shù)據(jù)的文件關(guān)閉掉,根據(jù)最初請(qǐng)求的URL將文件移到某個(gè)位置,然后通過NSNotificationCenter通知視圖控制器下載完畢。
連接委托還可以實(shí)現(xiàn)其他幾個(gè)方法來增加可以信息并實(shí)現(xiàn)對(duì)連接的控制。下面將介紹這些方法

connection:needNewBodyStream:

該方法是可選的,只是用于向請(qǐng)求體的輸入流發(fā)出請(qǐng)求。如果協(xié)議處理器由于出現(xiàn)錯(cuò)誤或是認(rèn)證等原因需要重新傳遞請(qǐng)求體就會(huì)調(diào)用該方法。如下面的代碼片段所示,該方法簽名會(huì)接收NSURLConnection對(duì)象(用于請(qǐng)求新的數(shù)據(jù)流)以及觸發(fā)該委托回調(diào)的request:

- (NSInputStream *)connection: (NSURLConnection *)connection needNewBodyStream:(nonnull NSURLRequest *)request

connection:didSendBodyData:totalBytesWritten:totalBytesExpectedToWrite

如下代碼片段所示,該可選的委托方法對(duì)象提供了上傳進(jìn)度信息:

- (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten totalBytesWritten:(NSInteger)totalBytesWritten totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite{
}

協(xié)議處理器會(huì)在不確定的時(shí)間間隔內(nèi)調(diào)用該委托方法以報(bào)告上傳進(jìn)度。bytesWritten與totalBytesWritten值可能不會(huì)一直增加。這是因?yàn)槿绻霈F(xiàn)錯(cuò)誤或是認(rèn)證問題,就需要重新傳遞請(qǐng)求體。如果想向用戶提供上傳進(jìn)度指示器,那么應(yīng)該實(shí)現(xiàn)該方法。

connection:willCacheResponse:

如下代碼片段展示,該可選方法向委托提供了一種方式來檢測(cè)與修改協(xié)議控制器所緩存的響應(yīng):

- (NSCachedURLResponse *)connnection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse

NSCachedURLResponse對(duì)象包含了NSURLResponse對(duì)象與請(qǐng)求返回來的以NSData形式存在的數(shù)據(jù)。該對(duì)象還包含了響應(yīng)保持所需的存儲(chǔ)策略,包括持久化儲(chǔ)存、僅內(nèi)存存儲(chǔ)或不允許存儲(chǔ)。緩存下來的響應(yīng)對(duì)象還包含目錄userInfo,可被應(yīng)用用來存儲(chǔ)緩存請(qǐng)求的元數(shù)據(jù)。如果該委托實(shí)現(xiàn)返回nil,那么響應(yīng)就不會(huì)被存儲(chǔ)下來。

認(rèn)證委托方法

有5個(gè)委托方法與URL請(qǐng)求的客戶端認(rèn)證有關(guān)。

異步請(qǐng)求與運(yùn)行循環(huán)

異步請(qǐng)求需要運(yùn)行循環(huán)。當(dāng)數(shù)據(jù)傳遞到服務(wù)器或是被客戶端接收時(shí),運(yùn)行循環(huán)用于實(shí)現(xiàn)事件與委托對(duì)象之間的通信。異步請(qǐng)求在發(fā)出時(shí),會(huì)在當(dāng)前線程的運(yùn)行循環(huán)上操作。這個(gè)實(shí)現(xiàn)細(xì)節(jié)是很重要的,因?yàn)樵贕CD塊中或是通過NSOperationQueue創(chuàng)建的線程并沒有運(yùn)行循環(huán)。因此,如果在后臺(tái)線程上發(fā)出了異步請(qǐng)求,那么還需要確定線程是由運(yùn)行循環(huán)還是使用了別的運(yùn)行循環(huán)。如下代碼片段展示了如何顯式地將請(qǐng)求處理指定給運(yùn)行循環(huán):

NSURLRequest *request;
NSURLConnection *connection1 = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection1 scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[connection1 start];

第一個(gè)操作創(chuàng)建了NSURLConnection對(duì)象,不過并沒有立刻啟動(dòng)方法,這樣就可以進(jìn)一步初始化了。下一行代碼獲取到主線程的運(yùn)行循環(huán)(即runloop),然后將它提供給連接,作為其運(yùn)行循環(huán)。最后,連接通過start方法開始處理。如果不想再主運(yùn)行循環(huán)中執(zhí)行異步請(qǐng)求,那么需要在另一個(gè)線程上創(chuàng)建runloop,然后針對(duì)這個(gè)新創(chuàng)建的runloop調(diào)度connection.

異步請(qǐng)求的best practice:

  • 對(duì)于大的上傳或下載來說,請(qǐng)使用異步請(qǐng)求以減少應(yīng)用的內(nèi)存占用量
  • 在需要認(rèn)證的情況下請(qǐng)使用異步請(qǐng)求
  • 如果需要向用戶提供進(jìn)度反饋,那么請(qǐng)使用異步請(qǐng)求
  • 在后臺(tái)線程上使用異步請(qǐng)求時(shí)要小心,請(qǐng)?zhí)峁┮粋€(gè)runloop
  • 對(duì)于可以在后臺(tái)線程的請(qǐng)求隊(duì)列中輕松調(diào)度和完成的簡(jiǎn)單請(qǐng)求來說,這時(shí)使用異步請(qǐng)求有些過猶不及
  • 如果使用輸入流來上傳數(shù)據(jù),請(qǐng)實(shí)現(xiàn)connection:newBodyStream:方法以避免對(duì)輸入流的復(fù)制

高級(jí)HTTP操作

HTTP頭在提供可修改服務(wù)器響應(yīng)的元數(shù)據(jù)以及向HTTP客戶端提供額外信息方面扮演著重要角色?;谶@一點(diǎn),iOS開發(fā)者常常需要操縱請(qǐng)求頭或是查看請(qǐng)求頭。比如,有些服務(wù)器需要自定義的認(rèn)證頭來提供關(guān)于用戶身份的信息。標(biāo)準(zhǔn)的URL加載系統(tǒng)并不會(huì)自動(dòng)添加這些頭,不過它提供了通過代碼添加的方法。

本節(jié)將會(huì)介紹更多地HTTP操作以及可以通過iOS的URL加載系統(tǒng)執(zhí)行的操作。并且將會(huì)分別介紹如何創(chuàng)建與使用其他的HTTP請(qǐng)求方法、如何處理HTTP cookie以及關(guān)于HTTP的高級(jí)用法

使用請(qǐng)求方法

根據(jù)定義,GET請(qǐng)求不應(yīng)該包含HTTP體,而只應(yīng)該包含請(qǐng)求行和請(qǐng)求頭。HTTP服務(wù)器會(huì)返回由URL指定的資源內(nèi)容。網(wǎng)絡(luò)設(shè)備常常會(huì)假定GEET請(qǐng)求完整的上下文位于請(qǐng)求行中,并根據(jù)這些數(shù)據(jù)來緩存響應(yīng)。如果GET請(qǐng)求包含會(huì)修改請(qǐng)求所返回內(nèi)容的請(qǐng)求體,那么由于中間網(wǎng)絡(luò)設(shè)備的緩存行為,你可能會(huì)得到錯(cuò)誤的結(jié)果。根據(jù)約定,GET請(qǐng)求不應(yīng)導(dǎo)致服務(wù)器上的數(shù)據(jù)發(fā)生任何變化。

HTML瀏覽器最早通過POST請(qǐng)求來發(fā)送或提交HTML表單,使用的是一種特定的數(shù)據(jù)編碼,通過application/x-www-form-urlencoded這種Content-Type來指定。iOS應(yīng)用通常都會(huì)使用POST請(qǐng)求向服務(wù)器發(fā)送XML或JSON數(shù)據(jù)。下述代碼片段展示了如何創(chuàng)建JSON對(duì)象并將其作為請(qǐng)求體:

    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)建了一個(gè)簡(jiǎn)單的NSDictionary對(duì)象,里面放置了一些虛構(gòu)出來的動(dòng)物的名值對(duì)。然后通過內(nèi)建的JSON庫(kù),創(chuàng)建一個(gè)NSData對(duì)象來表示該字典。接下來將該NSData對(duì)象提供給NSMutableURLRequest作為請(qǐng)求體。該代碼產(chǎn)生的JSON如下所示(第四章將會(huì)詳細(xì)介紹構(gòu)建和解析請(qǐng)求和響應(yīng)負(fù)載的過程):

{
    "weight" : "20",
    "animal" : "dog",
    "name" : "fido"
}

使用HEAD方法的請(qǐng)求會(huì)指示HTTP服務(wù)器只返回關(guān)于所請(qǐng)求的HTTP頭信息。HEAD請(qǐng)求通常沒有請(qǐng)求體,也沒有響應(yīng)體返回。它們常常用于驗(yàn)證緩存的數(shù)據(jù)與服務(wù)器上的數(shù)據(jù),同時(shí)又不必獲取緩存資源的整個(gè)內(nèi)容。

PUT請(qǐng)求類似于POST,因?yàn)樗偸怯姓?qǐng)求體,但從語(yǔ)義上來說,兩者有如下重要差別:PUT請(qǐng)求用于向服務(wù)器添加新的資源,而POST請(qǐng)求只用于更新服務(wù)器上的資源。在使用RESTful服務(wù)時(shí),這種語(yǔ)義上的差別是非常重要的。

操作cookie

cookie是HTTP協(xié)議在首個(gè)版本之后加入的一個(gè)重要組件。它向服務(wù)器提供了追蹤回話的能力,同時(shí)又無(wú)需維持客戶端與服務(wù)器之間的連接。在瀏覽器客戶端,cookie值是由服務(wù)器通過請(qǐng)求提供的,然后被放到隨后的請(qǐng)求中。由于設(shè)計(jì)cookie的目的是追蹤會(huì)話狀態(tài),因此它們通常會(huì)非常小,基本是幾十到幾百個(gè)字節(jié)。

從服務(wù)器發(fā)送的cookie有幾個(gè)屬性用于確定cookie的值、何時(shí)返回到服務(wù)器以及客戶端應(yīng)該保存cookie的時(shí)間,這些屬性有:

  • name--cookie的名字,從同一個(gè)DNS域返回的所有cookie名都是唯一的。只有name和value這兩個(gè)屬性才會(huì)在后續(xù)的請(qǐng)求中發(fā)送給服務(wù)器
  • value--由向服務(wù)器發(fā)送的下一個(gè)請(qǐng)求返回的值。
  • domain--后續(xù)請(qǐng)求在cookie中包含的DNS域。比如,擁有域值domain1.com的cookie不應(yīng)該返回給domain2.com。如果忽略掉,那么客戶端就會(huì)將URL的主機(jī)名當(dāng)作域。如果域的最前面是個(gè)原點(diǎn)(.),那么cookie就會(huì)返回給發(fā)送到該域及其子域的任何請(qǐng)求。如果沒有最前面的原點(diǎn),那么cookie就只會(huì)包含在發(fā)送給該域而非其子域的請(qǐng)求中。
  • path--path限制發(fā)送給請(qǐng)求的cookie都是針對(duì)指定的URL路徑。如果與DNS域搭配使用,那么path屬性就可以限制只會(huì)將cookie發(fā)送給服務(wù)器上優(yōu)先且精確的URL集合。
  • expiration date--cookie不再隨請(qǐng)求發(fā)送的 日期和時(shí)間,cookie會(huì)在這個(gè)時(shí)間點(diǎn)從客戶端存儲(chǔ)中移除
  • session ONLY--指定cookie是在當(dāng)前瀏覽器會(huì)話時(shí)間內(nèi)返回還是一直持續(xù)到過期日期,以二者之間先到的時(shí)間為準(zhǔn),在iOS應(yīng)用中,回話指的是OS加載應(yīng)用到終止應(yīng)用之間的應(yīng)用生命周期
  • secure--指定cookie只會(huì)在HTTPS連接而非HTTP連接
  • comment--用于向用戶說明cookie目的的注釋值
  • comment URL-向用戶提供了一個(gè)HTML文檔,用于說明cookie的目的
  • HTTP ONLY--指示器,告訴客戶端不要與javascript應(yīng)用共享cookie以防止跨站腳本攻擊
  • version--cookie遵循的HTTP cookie規(guī)范版本

雖然不是瀏覽器,但iOS應(yīng)用依然可以再HTTP連接中方便地使用cookie。URL加載框架幫我們做了大量繁雜的工作以利用協(xié)議的這個(gè)特性。下面是應(yīng)用經(jīng)常要使用到cookie的3個(gè)地方:

  • 檢索cookie值
  • 顯式刪除cookie
  • 手工將cookie添加到請(qǐng)求中

URL加載設(shè)施會(huì)自動(dòng)處理所有HTTP與HTTPS請(qǐng)求中的cookie。會(huì)將返回的cookie保存在響應(yīng)中,然后按照cookie處理規(guī)則將其添加到隨后的請(qǐng)求中。只有在cookie的domain屬性提供的DNS域

URI、URL和URN之間的區(qū)別與聯(lián)系:
  • URI:Uniform Resource Identifier,統(tǒng)一資源標(biāo)識(shí)符;
  • URL:Uniform Resource Locator,統(tǒng)一資源定位符;
  • URN:Uniform Resource Name,統(tǒng)一資源名稱。

其中,URL,URN是URI的子集。

HTTP的 URL示例:

使用超級(jí)文本傳輸協(xié)議HTTP,提供超級(jí)文本信息服務(wù)的資源。
例一:其計(jì)算機(jī)域名為www.peopledaily.com.cn。超級(jí)文本文件(文件類型為.html)是在目錄/channel下的welcome.htm。這是中國(guó)人民日?qǐng)?bào)的一臺(tái)計(jì)算機(jī)。

http://www.peopledaily.com.cn/channel/welcome.htm 

例二:其計(jì)算機(jī)域名為www.rol.cn.net。超級(jí)文本文件(文件類型為.html)是在目錄/talk下的talk1.htm。這是瑞得聊天室的地址,可由此進(jìn)入瑞得聊天室的第1室

http://www.rol.cn.net/talk/talk1.htm

文件的URL示例:

例一:代表存放主機(jī)ftp.yoyodyne.com上的pub/files/目錄下的一個(gè)文件,文件名是foobar.txt。

file://ftp.yoyodyne.com/pub/files/foobar.txt 

例二:代表主機(jī)ftp.yoyodyne.com上的目錄/pub。

file://ftp.yoyodyne.com/pub 

例三:代表主機(jī)ftp.yoyodyne.com上的根目錄

file://ftp.yoyodyne.com/ 
文件的URL:
用URL表示文件時(shí),服務(wù)器方式用file表示,后面要有主機(jī)IP地址、文件的存取路徑(即目錄)和文件名等信息。有時(shí)可以省略目錄和文件名,但“/”符號(hào)不能省略。

URI

一般由三部分組成:

  • 訪問資源的命名機(jī)制。
  • 存放資源的主機(jī)名。
  • 資源自身的名稱,由路徑表示。
URL的格式

由下列三部分組成:

  • 是協(xié)議(或稱為服務(wù)方式);
  • 是存有該資源的主機(jī)IP地址(有時(shí)也包括端口號(hào));
  • 是主機(jī)資源的具體地址。,如目錄和文件名等。

第一部分和第二部分之間用“://”符號(hào)隔開,第二部分和第三部分用“/”符號(hào)隔開。第一部分和第二部分是不可缺少的,第三部分有時(shí)可以省略。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容