iOS網絡1——NSURLConnection使用詳解


原文在此

一、整體介紹

NSURLConnection是蘋果提供的原生網絡訪問類,但是蘋果很快會將其廢棄,且由NSURLSession(iOS7以后)來替代。目前使用最廣泛的第三方網絡框架AFNetworking最新版本已棄用了NSURLConnection,那我們學習它還有什么用呢?

  • 首先,蘋果棄用它還是需要時間的,最起碼到iOS10之后;
  • 現在還有一些老項目會使用NSURLConnection,特別是2013年之前的項目,用戶量基礎還是很大的;
  • 另外,不得不承認,有些公司還在用類似ASI這些經典的網絡框架,所以還是很有必要學習NSURLConnection的。

二、使用的一般步驟

1 NSURL:請求地址,定義一個網絡資源路徑:

NSURL *url = [NSURL URLWithString:@"協議://主機地址/路徑?參數&參數"];

解釋如下:

  • 協議:不同的協議,代表着不同的資源查找方式、資源傳輸方式,比如常用的http,ftp等
  • 主機地址:存放資源的主機的IP地址(域名)
  • 路徑:資源在主機中的具體位置
  • 參數:參數可有可無,也可以多個。如果帶參數的話,用“?”號后面接參數,多個參數的話之間用&隔開

2 NSURLRequest:請求,根據前面的NSURL建立一個請求:

NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:15.0];

參數解釋如下:

  • url:資源路徑
  • cachePolicy:緩存策略(無論使用哪種緩存策略,都會在本地緩存數據),類型為美劇類型,取值如下:
    • NSURLRequestUseProtocolCachePolicy = 0 //默認的緩存策略,使用協議的緩存策略
    • NSURLRequestReloadIgnoringLocalCacheData = 1 //每次都從網絡加載
    • NSURLRequestReturnCacheDataElseLoad = 2 //返回緩存否則加載,很少使用
    • NSURLRequestReturnCacheDataDontLoad = 3 //只返回緩存,沒有也不加載,很少使用
  • timeoutInterval:超時時長,默認60s

另外,還可以設置其它一些信息,比如請求頭,請求體等等,如下:

注意,下面的request應為NSMutableURLRequest,即可變類型

// 告訴服務器數據為json類型
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; 
// 設置請求體(json類型)
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:@{@"userid":@"123456"} options:NSJSONWritingPrettyPrinted error:nil];
request.HTTPBody = jsonData; 

3 發送請求:

NSURLConnection默認的請求類型為GET,下面分異步和同步兩種介紹

異步請求
[NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
	// 有的時候,服務器訪問正常,但是會沒有數據!
	// 以下的 if 是比較標准的錯誤 處理代碼!
	if (connectionError != nil || data == nil) {
		//給用戶的提示信息
		NSLog(@"網絡不給力");
		return;
	}
}];

參數說明如下:

  • completionHandler:請求響應后(或者請求超時)執行的代碼,queue為代碼添加到的隊列,即block執行的線程
    • NSURLResponse 為服務器的響應,真實類型為NSHTTPURLResponse,通常只在“下載”功能時,才會使用;下面是協議頭的參數:
      • URL:響應的URL,有的時候,訪問一個URL地址,服務器可能會出現重定向,會定位到新的地址!
      • MIMEType(Content-Type):服務器告訴客戶端,可以用什么軟件打開二進制數據!網絡之所以豐富多采,是因為有豐富的客戶端軟件!栗子:windows上提示安裝 Flash 插件
      • expectedContentLength:預期的內容長度,要下載的文件長度,下載文件時非常有用
      • suggestedFilename:"建議"的文件名,方便用戶直接保存,很多時候,用戶並不關心要保存成什么名字!
      • textEncodingName:文本的編碼名稱 @"UTF8",大多數都是 UTF8
      • statusCode:狀態碼,在做下載操作的時候,需要判斷一下
      • allHeaderFields:所有的響應頭字典時候,用戶並不關心要保存成什么名字!
    • NSData 服務器返回的數據,例如json、xml(現在用的少)
    • NSError 網絡訪問錯誤碼

注意,block的執行線程為queue,如果block涉及到UI操作,則必須回到主線程:[NSOperationQueue mainQueue]

發送同步請求
// 同步請求,代碼會阻塞在這里一直等待服務器返回,如果data為nil則請求失敗,當獲取少量數據時可以使用此方法。
// request參數同上。
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil]; 

三、舉例說明

分三個部分:

  1. 網絡請求(json、xml數據)
  2. 文件下載
  3. 文件上傳,這里例子不再給出,請參考這里:http://www.cnblogs.com/mddblog/p/5215453.html

1 一般的網絡請求

/// 兩種請求方式
typedef enum {
    MethodGET,
    MethodPOST
}Method;

/// 請求成功后,直接將jsonData轉為jsonDict
+ (void)connectionRequestWithMethod:(Method)method   URLString:(NSString *)URLString parameters:(NSDictionary *)dict success:(void (^)(id JSON))success fail:(void (^)(NSError *error))fail {
    // 簡單的轉碼,如果參數帶有?&特殊字符,下面方法不適合
    URLString = [URLString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    // 1.創建請求
    NSURL *url = [NSURL URLWithString:URLString];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    // 請求方式,默認為GET
    if (method == MethodPOST) {
        request.HTTPMethod = @"POST";
    }
    // 根據需要設置
    [request setValue:@"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" forHTTPHeaderField:@"Accept"];
    
    // 2.設置請求頭 Content-Type  返回格式
    [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    
    // 3.設置請求體 NSDictionary --> NSData
    if (dict != nil) {
        NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil];
        request.HTTPBody = data;
    }
    // 4.發送請求
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        if ((data != nil) && (connectionError == nil)) {
            NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
            if (success) {
                success(jsonDict);
            }
        } else {
            if (fail) {
                fail(connectionError);
            }
        }
        
    }];
}

2 文件下載

下面舉一個實現斷點續傳的網絡下載,要實現斷點續傳,還需要用到NSURLConnectionDataDelegate代理,NSFileHandle文件操作或者數據流的形式(NSOutputStream),這里以NSFileHandle為例,訪問的服務器采用Apache搭建的本地服務器,具體見代碼:

@interface ViewController () <NSURLConnectionDataDelegate>

/// 文件下載完畢之后,在本地保存的路徑
@property (nonatomic, copy) NSString *filePath;
/// 需要下載的文件的總大小!
@property (nonatomic, assign) long long serverFileLength;
/// 當前已經下載長度
@property (nonatomic, assign) long long localFileLength;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];    
}
/// 點擊屏幕事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self downloadFile];
}
/// 下載文件
- (void)downloadFile {
    // 文件存儲路徑
    self.filePath = @"/Users/username/Desktop//陶喆 - 愛很簡單.mp3";
    // 要下載的網絡文件,采用Apache搭建的本地服務器
    NSString *urlStr = @"http://localhost/陶喆 - 愛很簡單.mp3";
    // 簡單的轉碼,如果參數帶有?&特殊字符,下面方法不適合
    urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSURL *url = [NSURL URLWithString:urlStr];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    // 設置請求方法為 HEAD 方法,這里只是頭,數據量少,用同步請求也可以,不過最好還是用異步請求
    request.HTTPMethod = @"HEAD";
    // 無論是否會引起循環引用,block里面都使用弱引用
    __weak typeof(self) weakSelf = self;
    [NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
        // 出錯則返回
        if (connectionError != nil) {
            NSLog(@"connectionError = %@",connectionError);
            return ;
        }
        // 記錄需要下載的文件總長度
        long long serverFileLength = response.expectedContentLength;
        weakSelf.serverFileLength = serverFileLength;
        // 初始化已下載文件大小
        weakSelf.localFileLength = 0;
        
        NSDictionary *dict = [[NSFileManager defaultManager] attributesOfItemAtPath:weakSelf.filePath error:NULL];
        
        long long  localFileLength = [dict[NSFileSize] longLongValue];
        
        // 如果沒有本地文件,直接下載!
        if (!localFileLength) {
            // 下載新文件
            [weakSelf getFileWithUrlString:urlStr];
        }
        // 如果已下載的大小,大於服務器文件大小,肯定出錯了,刪除文件並從新下載
        if (localFileLength > serverFileLength) {
            // 刪除文件 remove
            [[NSFileManager defaultManager]  removeItemAtPath:self.filePath error:NULL];
            
            // 下載新文件
            [self getFileWithUrlString:urlStr];
            
        } else if (localFileLength == serverFileLength) {
            NSLog(@"文件已經下載完畢");
        } else if (localFileLength && localFileLength < serverFileLength) {
            // 文件下載了一半,則使用斷點續傳
            self.localFileLength = localFileLength;
            
            [self getFileWithUrlString:urlStr WithStartSize:localFileLength endSize:serverFileLength-1];
        }
    }];
    
}
/// 重新開始下載文件(代理方法監聽下載過程)
-(void)getFileWithUrlString:(NSString *)urlString
{
    NSURL *url = [NSURL URLWithString:urlString];
    // 默認就是 GET 請求
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    
    // 開始請求,並設置代理為self
    NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];
    
    // 設置代理的執行線程,一般不在主線程
    [conn setDelegateQueue:[[NSOperationQueue alloc] init]];
    
    // NSUrlConnection 的代理方法是一個特殊的事件源!
    // 開啟運行循環!
    CFRunLoopRun();
}

/* 斷點續傳(代理方法監聽下載過程)
 * startSize:本次斷點續傳開始的位置
 * endSize:本地斷點續傳結束的位置
 */
-(void)getFileWithUrlString:(NSString *)urlString WithStartSize:(long long)startSize endSize:(long long)endSize
{
    // 1. 創建請求!
    NSURL *url = [NSURL URLWithString:urlString];
    // 默認就是 GET 請求
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    
    // 設置斷點續傳信息
    NSString *range = [NSString stringWithFormat:@"Bytes=%lld-%lld",startSize,endSize];
    [request setValue:range forHTTPHeaderField:@"Range"];
    
    // NSUrlConnection 下載過程!
    NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];
    
    // 設置代理的執行線程// 傳一個非主隊列!
    [conn setDelegateQueue:[[NSOperationQueue alloc] init]];
    
    // NSUrlConnection 的代理方法是一個特殊的事件源!
    // 開啟運行循環!
    CFRunLoopRun();
}

#pragma mark - NSURLConnectionDataDelegate
/// 1. 接收到服務器響應
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    // 服務器響應
    // response.expectedContentLength的大小是要下載文件的大小,而不是文件的總大小。比如請求頭設置了Range[requestM setValue:rangeStr forHTTPHeaderField:@"Range"];則返回的大小為range范圍內的大小
}
/// 2. 接收到數據
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    //data當前接收到的網絡數據;
    // 如果這個文件不存在,響應的文件句柄就不會創建!
    NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:self.filePath];
    // 在這個路徑下已經有文件了!
    if (fileHandle) {
        // 將文件句柄移動到文件的末尾
        [fileHandle seekToEndOfFile];
        // 寫入文件的意思(會將data寫入到文件句柄所操縱的文件!)
        [fileHandle writeData:data];
        
        [fileHandle closeFile];
        
    } else {
        // 第一次調用這個方法的時候,在本地還沒有文件路徑(沒有這個文件)!
        [data writeToFile:self.filePath atomically:YES];
    }
}
/// 3. 接收完成
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSLog(@"下載完成");
}
/// 4. 網絡錯誤
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    NSLog(@"下載錯誤 %@", error);
}
@end

四、NSData 數據的反序列化

服務器返回的NSData數據類型,事先已經知道,因此可以直接返序列化為實際的類型,例如:json(字典或數組).plist(字典或數組)textxml等:

  • 加載數據
// 加載本地
NSString *path = [[NSBundle mainBundle] pathForResource:@"文件名 " ofType:nil];
NSData *jsonData = [NSData dataWithContentsOfFile:path];

// 從網絡直接加載
NSURL *url = [[NSBundle mainBundle] URLForResource:@"topic_news.json" withExtension:nil];
NSData *data = [NSData dataWithContentsOfURL:url];

  • .plist反序列化

這種很少使用,只是蘋果自己的格式

// 關於選型參數
// NSPropertyListImmutable = 0,不可變  
// NSPropertyListMutableContainers = 1 << 0,容器可變
// NSPropertyListMutableContainersAndLeaves = 1 << 1,容器和葉子可變
// 通常后續直接做字典轉模型,不需要關心是否可變,所以一般直接賦值為0
id result = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:NULL];
  • 關於json數據

    • json數據的本質是字符串,根格式只有兩種可能:數組或字典。

      • 反序列化:從服務器接收到的二進制數據 轉換成 字典或者數組
      // 假設json數據位:[{鍵值對},{鍵值對}],則可以將數據轉化為字典或數組
      

    NSArray *dictArray = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:nil];
    ```

      - 序列化:將字典或者數組 轉換成 二進制數據,准備發送給服務器
      
      ```objc
      // obj為字典或數組
      NSData *data = [NSJSONSerialization dataWithJSONObject:obj options:0 error:NULL];
      // 轉化前可以判斷是否可以序列化:
      + (BOOL)isValidJSONObject:(id)obj;
      ```
    
    • 一個對象能夠被轉換成 JSON 必須符合以下條件:
      • 頂級節點,必須是一個 NSArray or NSDictionary
      • 所有的對象必須是 NSString, NSNumber, NSArray, NSDictionary, or NSNull
      • 所有字典的 key 都必須是 NSString
      • NSNumber 不能為空或者無窮大

注意事項:

  • 請求的緩存策略使用 NSURLRequestReloadIgnoringCacheData,忽略本地緩存
  • 服務器響應結束后,要記錄 Etag,服務器內容和本地緩存對比是否變化的重要依據!
  • 在發送請求時,設置 If-None-Match,並且傳入 etag
  • 連接結束后,要判斷響應頭的狀態碼,如果是 304,說明本地緩存內容沒有發生變化,此時可以使用本地緩存來加載數據,如果緩存文件被意外刪除,程序依然運行但會報錯,加載數據也失敗:NSCachedURLResponse *cachedURLRes = [[NSURLCache sharedURLCache] cachedResponseForRequest:requestM];//cachedURLRes為nil
  • GET 緩存的數據會保存在 Cache 目錄中 /bundleId 下,Cache.db 中

iOS9訪問http網頁

  1. 在Info.plist中添加NSAppTransportSecurity類型Dictionary;
  2. 在NSAppTransportSecurity下添加NSAllowsArbitraryLoads類型Boolean,值設為YES


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM