06-NSURLSession
一、NSURLSession
1、簡介
- iOS7.0 推出,用於替代 NSURLConnection。
- 支援後臺執行的網路任務。
- 暫停,停止,重啟網路任務,不需要NSOperation 封裝。 > 請求可以使用同樣的 配置容器。
- 不同的 session 可以使用不同的私有儲存。
- block 和代理可以同時起作用。
- 直接從檔案系統上傳,下載。
結構圖
- 1、為了方便程式設計師使用,蘋果提供了一個全域性 session
- 2、所有的 任務(Task)都是由 session 發起的
- 3、所有的任務預設是掛起的,需要 resume
- 4、使用 NSURLSession 後,NSURLRequest 通常只用於 指定 HTTP 請求方法,而其他的額外資訊都是通過 NSUSLSessionConfiguration 設定
2、程式碼演練--獲取JSON資料
#pragma mark - 詳細的寫法
// 載入資料
- (void)loadData{
// 1.建立 url
NSURL *url = [NSURL URLWithString:@"http://localhost/demo.json"];
// 2.為了程式設計師方便開發,蘋果提供了一個全域性的 session 物件
// 獲取全域性的會話物件
// 在 session 中,request絕大多數情況下是可以省略的
NSURLSession *session = [NSURLSession sharedSession];
// 3.獲得資料任務物件
// **** 所有的任務都是由 session 發起的,不要通過 alloc init 建立任務物件
// **** 任務的回撥預設的就是非同步的
// **** 如果需要更新 UI,需要注意主執行緒回撥
NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
id result =[NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
NSLog(@"result = %@ ---%@",result,[NSThread currentThread]);
}];
// 4.繼續任務
[task resume];
}
#pragma mark - 簡化的寫法
- (void)loadData2{
// 1.建立 url
NSURL *url = [NSURL URLWithString:@"http://localhost/demo.json"];
// 2.執行任務
[self dataTask:url finished:^(id obj) {
NSLog(@"obj = %@ -- %@",obj,[NSThread currentThread]);
}];
}
- (void)dataTask:(NSURL *)url finished:(void (^)(id obj))finished{
NSAssert(finished != nil, @"必須傳人回撥");
// **** 所以的任務都是由 session 發起的,不要通過 alloc init 建立任務物件
// **** 任務的回撥預設的就是非同步的
// **** 如果需要更新 UI,需要注意主執行緒回撥
[[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
id result =[NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
// NSLog(@"result = %@ ---%@",result,[NSThread currentThread]);
dispatch_async(dispatch_get_main_queue(), ^{
finished(result);
});
}] resume];
}
二、下載和解壓縮
1、NSURLSession下載檔案
- (void)download{
// 1.建立下載 url
NSURL *url = [NSURL URLWithString:@"http://localhost/1234.mp4"];
NSLog(@"開始下載");
// 2.開始下載
[[[NSURLSession sharedSession] downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
NSLog(@"location = %@ -- %@",location,[NSThread currentThread]);
NSLog(@"下載完成");
// 非同步解壓縮
}] resume];
}
細節:
- 從Xcode6.0 到 Xcode 6.3 記憶體佔用一直很高,差不多是檔案大小
的 2.5 倍。 - 檔案下載完成後會被自動刪除!思考為什麼?
- 大多數情況下,下載的檔案型別以‘zip’居多,可以節約使用者的流量。
- 下載完成後解壓縮。
- 壓縮包就可以刪除。
2、解壓縮zip包
- (void)download{
// 1.建立下載 url
NSURL *url = [NSURL URLWithString:@"http://localhost/itcast/images.zip"];
NSLog(@"開始下載");
// 2.開始下載
[[[NSURLSession sharedSession] downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
NSLog(@"location = %@ -- %@",location,[NSThread currentThread]);
NSLog(@"下載完成");
// 獲得沙盒快取資料夾路徑
NSString *cacheDir = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"pkxing"];
// 非同步解壓縮
// location.path:沒有‘協議頭’的路徑
// location.absoluteString:包含 ‘協議頭‘’的完成 url 路徑
// 注意:解壓縮路徑最好自己新建一個專屬的資料夾,因為 zip 包中可能有多個檔案。
[SSZipArchive unzipFileAtPath:location.path toDestination:cacheDir delegate:self];
}] resume];
}
#pragma mark - SSZipArchiveDelegate 代理方法
/**
* 跟蹤解壓縮排度
*
* @param loaded 已經解壓的大小
* @param total 要解壓的檔案大小
*/
- (void)zipArchiveProgressEvent:(NSInteger)loaded total:(NSInteger)total {
NSLog(@"%f",(CGFloat)loaded/ total);
}
壓縮
// 將指定路徑下的檔案打包到指定的 zip 檔案中
[SSZipArchive createZipFileAtPath:@"/users/pkxing/desktop/abc.zip" withFilesAtPaths:@[@"/users/pkxing/desktop/多贏商城.ipa"]];
// 將指定的資料夾下所有的檔案打包到指定的zip 檔案中
[SSZipArchive createZipFileAtPath:@"/users/pkxing/desktop/abc.zip" withContentsOfDirectory:@"/users/pkxing/desktop/課件PPT模板"];
3、下載進度
-
1、監聽如何監聽下載進度?
- 通知/代理/block/KVO(監聽屬性值,極少用在這種情況)
- 檢視是否可以使用有代理:進入標頭檔案, 從 interface 向上滾兩下檢視是否有對應的協議。
- 檢視是否可以使用有通知:看標頭檔案底部。
- 檢視是否可以使用有block:通常和方法在一起。
- 通知/代理/block/KVO(監聽屬性值,極少用在這種情況)
-
2、要監聽session下載進度使用的是代理,此時不能使用'sharedSession'方法建立會話物件
- 使用 sharedSession 獲得的物件是全域性的物件。
- 多處地方呼叫獲得的物件都是同一個會話物件,而代理又是一對一的。
3、如果發起的任務傳遞了completionHandler回撥,不會觸發代理方法。
- (void)download{
// 1.建立下載 url
NSURL *url = [NSURL URLWithString:@"http://localhost/1234.mp4"];
NSLog(@"開始下載");
// 2.開始下載
/*
[[self.session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
NSLog(@"location = %@ -- %@",location,[NSThread currentThread]);
NSLog(@"下載完成");
}] resume];
*/
[[self.session downloadTaskWithURL:url] resume];
}
#pragma mark - NSURLSessionDownloadDelegate
/**
* 下載完畢回撥
*
* @param session 會話物件
* @param downloadTask 下載任務物件
* @param location 下載路徑
*/
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
NSLog(@"location = %@",location);
}
/**
* 接收到資料回撥
*
* @param session 會話物件
* @param downloadTask 下載任務物件
* @param bytesWritten 本次下載位元組數
* @param totalBytesWritten 已經下載位元組數
* @param totalBytesExpectedToWrite 總大小位元組數
*/
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
// 計算進度
CGFloat progress = (CGFloat)totalBytesWritten / totalBytesExpectedToWrite;
NSLog(@"progress = %f---%@",progress,[NSThread currentThread]);
}
/**
* 續傳代理方法:沒有什麼用
*/
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes{
NSLog(@"---%s",__func__);
}
#pragma mark - 懶載入會話物件
- (NSURLSession *)session {
if (_session == nil) {
// 建立會話配置物件
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
/**
引數:
configuration:會話配置,大多使用預設的。
delegate:代理,一般是控制器
delegateQueue:代理回撥的佇列,可以傳入 nil.
*/
_session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
}
return _session;
}
4、佇列的選擇
#pragma mark - 懶載入會話物件
- (NSURLSession *)session {
if (_session == nil) {
// 建立會話配置物件
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
_session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
}
return _session;
}
引數:
configuration:會話配置,大多使用預設的。
delegate:代理,一般是控制器。
delegateQueue:代理回撥的佇列。
可以傳人 nil,傳人 nil 等價於[[NSOperationQueue alloc] init]。
傳人[NSOperationQueue mainQueue],表示代理方法在主佇列非同步執行。
如果代理方法中沒有耗時操作,則選擇主佇列,有耗時操作,則選擇非同步佇列。
下載本身是由一個獨立的執行緒完成。無論選擇什麼佇列,都不會影響主執行緒。
5、暫停和繼續01
/**
* 開始下載
*/
- (IBAction)start{
// 1.建立下載 url
NSURL *url = [NSURL URLWithString:@"http://dlsw.baidu.com/sw-search-sp/soft/2a/25677/QQ_V4.0.3_setup.1435732931.dmg"];
self.task = [self.session downloadTaskWithURL:url];
[self.task resume];
}
/**
* 暫停下載
*/
- (IBAction)pause{
// 只有執行的任務才需要掛起
if (self.task.state == NSURLSessionTaskStateRunning) {
NSLog(@"pause = %@",self.task);
[self.task suspend];
}
}
/**
* 繼續下載
*/
- (IBAction)resume{
// 只有被掛起的任務才需要繼續
if (self.task.state == NSURLSessionTaskStateSuspended) {
NSLog(@"resume = %@",self.task);
[self.task resume];
}
}
#pragma mark - NSURLSessionDownloadDelegate
/**
* 下載完畢回撥
*
* @param session 會話物件
* @param downloadTask 下載任務物件
* @param location 下載路徑
*/
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
NSLog(@"location = %@",location);
}
/**
* 接收到資料回撥
*
* @param session 會話物件
* @param downloadTask 下載任務物件
* @param bytesWritten 本次下載位元組數
* @param totalBytesWritten 已經下載位元組數
* @param totalBytesExpectedToWrite 總大小位元組數
*/
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
// 計算進度
CGFloat progress = (CGFloat)totalBytesWritten / totalBytesExpectedToWrite;
NSLog(@"progress = %f---%@",progress,[NSThread currentThread]);
// 回到主執行緒更新進度條
dispatch_async(dispatch_get_main_queue(), ^{
self.progressView.progress = progress;
});
}
/**
* 續傳代理方法:沒有什麼用
*/
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes{
NSLog(@"---%s",__func__);
}
#pragma mark - 懶載入會話物件
- (NSURLSession *)session {
if (_session == nil) {
// 建立會話配置物件
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
/**
引數:
configuration:會話配置,大多使用預設的。
delegate:代理,一般是控制器
delegateQueue:代理回撥的佇列,可以傳入 nil.
*/
_session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
}
return _session;
}
NSURLConnection和 NSURLSessionTask 對比
NSURLConnection 不能掛起,只能開始和取消,一旦取消,如果需要再次啟動,需要新建connection
NSURLSessionTask 可以掛起/繼續/取消/完成
6、暫停和繼續02
// 記錄續傳資料
@property(nonatomic,strong) NSData *resumeData;
/**
* 開始下載
*/
- (IBAction)start{
// 1.建立下載 url
NSURL *url = [NSURL URLWithString:@"http://dlsw.baidu.com/sw-search-sp/soft/2a/25677/QQ_V4.0.3_setup.1435732931.dmg"];
self.task = [self.session downloadTaskWithURL:url];
[self.task resume];r
}
/**
* 暫停下載
*/
- (IBAction)pause{
// 如果任務已經被取消,不希望再次執行 block
// 在 oc中,可以給 nil 物件傳送任何訊息
[self.task cancelByProducingResumeData:^(NSData *resumeData) {
NSLog(@"length = %zd",resumeData.length);
// 記錄續傳資料
self.resumeData = resumeData;
// 清空任務
self.task = nil;
}];
}
/**
* 繼續下載
*/
- (IBAction)resume{
// 如果沒有續傳資料
if(self.resumeData == nil){
NSLog(@"沒有續傳資料");
return;
}
// 使用續傳資料開啟續傳下載
self.task = [self.session downloadTaskWithResumeData:self.resumeData];
// 清空續傳資料
self.resumeData = nil;
[self.task resume];
}
resumeData:
該引數包含了繼續下載檔案的位置資訊。也就是說,當你下載了10M得檔案資料,暫停了。那麼你下次繼續下載的時候是從第10M這個位置開始的,而不是從檔案最開始的位置開始下載。因而為了儲存這些資訊,所以才定義了這個NSData型別的這個屬性:resumeData
下載完成後,將檔案移動到指定的資料夾下面
#pragma mark - NSURLSessionDownloadDelegate
/**
* 下載完畢回撥
*
* @param session 會話物件
* @param downloadTask 下載任務物件
* @param location 下載路徑
*/
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
// 獲得 cache 資料夾路徑
NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// 獲得檔名
NSString *filePath = [cache stringByAppendingPathComponent:downloadTask.response.suggestedFilename];
// 將下載好的檔案移動到指定的資料夾
[[NSFileManager defaultManager] moveItemAtPath:location.path toPath:filePath error:NULL];
}
7、載入續傳資料
/**
NSURLSession中,斷點續傳的關鍵點就在 resumeData
1. 一旦取消任務,在resumeData 中會記錄住當前下載的資訊,格式是 plist 的
2. 可以將續傳資料寫入磁碟
3. 程式重新執行,從磁碟載入 resumeData,修改其中儲存的`臨時檔名`,因為每一次啟動 路徑會發生變化
4. 使用 resumeData 開啟一個續傳任務!
*/
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSURL *url = [NSURL URLWithString:@"http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.0.2.dmg"];
self.url = url;
// 判斷沙盒中是否有快取資料?如果有,載入快取資料
self.resumeData = [self loadResumeData:url];
if (self.resumeData != nil) {
// 使用快取資料新建下載任務
self.task = [self.session downloadTaskWithResumeData:self.resumeData];
} else {
// 如果沒有,直接下載
self.task = [self.session downloadTaskWithURL:url];
}
[self.task resume];
}
// 根據 URL 載入沙盒中的快取資料
/**
如果程式再次執行,NSHomeDirectory會發生變化!在iOS 8.0才會有!
需要解決的,將快取資料中的路徑名修改成正確的
*/
- (NSData *)loadResumeData:(NSURL *)url {
// 1. 判斷檔案是否存在
NSString *filePath = [self resumeDataPath:url];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
// 以字典的方式載入續傳資料
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:filePath];
// 1. 取出儲存的 - 臨時檔案的目錄
// 臨時目錄/CFNetworkDownload_p78VgR.tmp
NSString *localPath = dict[@"NSURLSessionResumeInfoLocalPath"];
NSString *fileName = localPath.lastPathComponent;
// 計算得到正確的臨時檔案目錄
localPath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
// 重新設定字典的鍵值
dict[@"NSURLSessionResumeInfoLocalPath"] = localPath;
// 字典轉二進位制資料,序列化(Plist的序列化)
return [NSPropertyListSerialization dataWithPropertyList:dict format:NSPropertyListXMLFormat_v1_0 options:0 error:NULL];
}
return nil;
}
/**
* 儲存續傳資料的檔案路徑
儲存到`臨時資料夾`中 - 儲存成 "url.字串的 md5.~resume"
*/
- (NSString *)resumeDataPath:(NSURL *)url {
// 1. 取得 md5 的字串
NSString *fileName = url.absoluteString.md5String;
// 2. 拼接臨時資料夾
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
// 3. 拼接副檔名,避免衝突
path = [path stringByAppendingPathExtension:@"~resume"];
NSLog(@"續傳資料檔案路徑 %@", path);
return path;
}
// 暫停
- (IBAction)pause {
NSLog(@"暫停");
// 取消下載任務 可以給 nil 傳送任何訊息,不會有任何不良反應
[self.task cancelByProducingResumeData:^(NSData *resumeData) {
NSLog(@"續傳資料長度 %tu", resumeData.length);
// 將續傳資料寫入磁碟
[resumeData writeToFile:[self resumeDataPath:self.url] atomically:YES];
// 記錄續傳資料
self.resumeData = resumeData;
// 釋放任務
self.task = nil;
}];
}
// 繼續
- (IBAction)resume {
NSLog(@"繼續");
if (self.resumeData == nil) {
NSLog(@"沒有續傳資料");
return;
}
self.task = [self.session downloadTaskWithResumeData:self.resumeData];
// 釋放續傳資料
self.resumeData = nil;
// 繼續任務
[self.task resume];
}
三、WebDav
WebDav伺服器是基於 Apache 的,使用的是 HTTP 協議,可以當作網路檔案伺服器使用。上傳檔案的大小沒有限制。
1、WebDav 的配置
WebDav完全可以當成一個網路共享的檔案伺服器使用!
# 切換目錄
$ cd /etc/apache2
$ sudo vim httpd.conf
# 查詢httpd-dav.conf
/httpd-dav.conf
"刪除行首#"
# 將游標定位到行首
0
# 刪除行首的註釋
x
# 儲存退出
:wq
# 切換目錄
$ cd /etc/apache2/extra
# 備份檔案(只要備份一次就行)
$ sudo cp httpd-dav.conf httpd-dav.conf.bak
# 編輯配置檔案
$ sudo vim httpd-dav.conf
"將Digest修改為Basic"
# 查詢Digest
/Digest
# 進入編輯模式
i
# 返回到命令列模式
如果MAC系統是10.11,則需要按下圖修改對應的路徑。
ESC
# 儲存退出
:wq
# 切換目錄,可以使用滑鼠拖拽的方式
$ cd 儲存put指令碼的目錄
# 以管理員許可權執行put配置指令碼
$ sudo ./put
設定兩次密碼: 123456
如果MAC系統是10.11,則會在使用者根目錄下生成三個檔案,如下圖:
注意:要在Mac 10.10以上配置Web-dav還需要在httpd.conf中開啟以下三個模組
LoadModule dav_module libexec/apache2/mod_dav.so
LoadModule dav_fs_module libexec/apache2/mod_dav_fs.so
LoadModule auth_digest_module libexec/apache2/mod_auth_digest.so
2、WebDav 上傳檔案(PUT)
先上傳圖片,再換成大檔案(視訊),不修改上傳的路徑,讓後面上傳的大檔案覆蓋之前的圖片。
- (void)webDavUpload{
// 1.URL -- 要上傳檔案的完整網路路徑,包括檔名
NSURL *url = [NSURL URLWithString:@"http://192.168.1.105/uploads/abc.png"];
// 2.建立請求物件
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 2.1 設定請求方法
request.HTTPMethod = @"PUT";
// 2.2 設定身份驗證
[request setValue:[self authString] forHTTPHeaderField:@"Authorization"];
// 3.上傳
NSURL *fileUrl = [[NSBundle mainBundle] URLForResource:@"001.png" withExtension:nil];
[[[NSURLSession sharedSession] uploadTaskWithRequest:request fromFile:fileUrl completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(@"data = %@,response = %@,error = %@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding],response,error);
}] resume];
}
/**
* 獲得授權字元字串
*/
- (NSString *)authString{
NSString *str = @"admin:123456";
return [@"BASIC " stringByAppendingString:[self base64:str]];
}
/**
* 將字串進行 base64編碼,返回編碼後的字串
*/
-(NSString *)base64:(NSString *)string{
// 轉換成二進位制資料
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
// 返回 base64編碼後的字串
return [data base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
}
狀態碼
401 Unauthorized:沒有許可權。需要身份驗證
201 新增檔案,建立成功。
204 沒有內容,檔案覆蓋成功,伺服器不知道該告訴我們什麼,所以沒有內容返回。
授權的字串格式
BASIC (admin:123456).base64。其中admin是使用者名稱,123456是密碼。
3、WebDav 上傳進度跟進
/**
* 獲得授權字元字串
*/
- (NSString *)authString{
NSString *str = @"admin:123456";
return [@"BASIC " stringByAppendingString:[self base64:str]];
}
/**
* 將字串進行 base64編碼,返回編碼後的字串
*/
-(NSString *)base64:(NSString *)string{
// 轉換成二進位制資料
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
// 返回 base64編碼後的字串
return [data base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
}
#pragma mark - 上傳操作
- (void)webDavUpload{
// 1.URL -- 要上傳檔案的完整網路路徑,包括檔名
NSURL *url = [NSURL URLWithString:@"http://192.168.1.105/uploads/abc.png"];
// 2.建立請求物件
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 2.1 設定請求方法
request.HTTPMethod = @"PUT";
// 2.2 設定身份驗證
[request setValue:[self authString] forHTTPHeaderField:@"Authorization"];
// 3.上傳
NSURL *fileUrl = [[NSBundle mainBundle] URLForResource:@"18-POST上傳檔案演練.mp4" withExtension:nil];
[[self.session uploadTaskWithRequest:request fromFile:fileUrl completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(@"data = %@,response = %@,error = %@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding],response,error);
}] resume];
}
#pragma mark - NSURLSessionTaskDelegate 代理方法
/**
* 上傳進度的跟進,只要實現這個方法就可以了
* @param bytesSent 本次上傳位元組數
* @param totalBytesSent 已經上傳的總位元組數
* @param totalBytesExpectedToSend 要上傳檔案的總大小
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
CGFloat progress = (CGFloat)totalBytesSent / totalBytesExpectedToSend;
NSLog(@"progress = %f",progress);
}
#pragma mark - 懶載入會話
- (NSURLSession *)session {
if (_session == nil) {
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
_session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
}
return _session;
}
4、WebDav 刪除檔案(Delete)
- (void)webDavDelete{
// 1.URL -- 指定要刪除檔案的 url
NSURL *url = [NSURL URLWithString:@"http://192.168.1.105/uploads/abc.mp4"];
// 2.建立請求物件
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 2.1 設定請求方法
request.HTTPMethod = @"DELETE";
// 2.2 設定身份驗證
[request setValue:[self authString] forHTTPHeaderField:@"Authorization"];
// 3.刪除
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(@"data = %@,response = %@,error = %@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding],response,error);
}] resume];
}
狀態碼
401 Unauthorized:沒有許可權。需要身份驗證
404 Not Found 檔案不存在
204 刪除成功
5、WebDav GET/HEAD檔案
- (void)webDavGet{
NSURL *url = [NSURL URLWithString:@"http://192.168.1.105/uploads/abc.png"];
[[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// 將返回的資料寫到檔案
[data writeToFile:@"/users/pkxing/desktop/abc.mp4" atomically:YES];
NSLog(@"%@===",response);
}] resume];
}
/**
GET & HEAD 都不需要身份驗證,只是獲取資源,不會破壞資源!
PUT & DELETE 會修改伺服器資源,需要有許可權的人執行,需要身份驗證
*/
- (void)webDavHead {
// 1. URL,要刪除的檔案網路路徑
NSURL *url = [NSURL URLWithString:@"http://192.168.40.2/uploads/321.png"];
// 2. request
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 2.1 HTTP方法
request.HTTPMethod = @"HEAD";
// 3. session
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
[data writeToFile:@"/Users/apple/Desktop/aa.mp4" atomically:YES];
// *** 不要只跟蹤 data,否則會以為什麼也沒有發生
NSLog(@"%@ | %@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding], response);
}] resume];
}
6、WebDav總結(PUT/DELETE/GET/HEAD)
-
需要許可權
- DELETE:刪除資源
- PUT:新增或修改資源
-
不需要許可權
- WebDav的'GET/HEAD' 請求不需要身份驗證,因為對伺服器的資源沒有任何的破壞。
不支援POST上傳,POST方法通常需要指令碼的支援,提交給伺服器的是二進位制資料,同時告訴伺服器資料型別。
四、NSURLSession注意點
The session object keeps a strong reference to the delegate until your app explicitly invalidates the session. If you do not invalidate the session by calling the invalidateAndCancel or resetWithCompletionHandler: method, your app leaks memory.
1、 在什麼時候取消網路會話?
方法一:在viewWillDisappear 取消網路會話
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// 取消網路會話
[self.session invalidateAndCancel];
self.session = nil;
}
方法二:在每一個任務完成後,取消會話,不推薦
[self.session finishTasksAndInvalidate];
self.session = nil;
完成任務並取消會話(會話一旦取消就無法再建立任務)
會造成 session 頻繁的銷燬&建立
Attempted to create a task in a session that has been invalidated
錯誤原因:嘗試在一個被取消的會話中建立一個任務。
方法三:建立一個‘網路管理單列’,單獨負責所有的網路訪問操作。
五、NSURLSession--POST上傳
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"001.png" ofType:nil];
NSDictionary *fileDict = @{@"other.png":[NSData dataWithContentsOfFile:filePath]};
// 資料引數
NSDictionary *params = @{@"username":@"zhang"};
[self upload:fileDict fieldName:@"userfile[]" params:params];
}
// 分割符
#define boundary @"itheima"
#pragma mark - 上傳操作
- (void)upload:(NSDictionary *)fileDict fieldName:(NSString *)fieldName params:(NSDictionary *)params{
// 1.URL -- 負責上傳檔案的指令碼
NSURL *url = [NSURL URLWithString:@"http://192.168.1.105/post/upload-m.php"];
// 2.建立請求物件
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 2.1 設定請求方法
request.HTTPMethod = @"POST";
// 設定Content-Type
//Content-Type multipart/form-data;boundary=傳值NB
NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",boundary];
[request setValue:contentType forHTTPHeaderField:@"Content-Type"];
// 不需要設定請求體 request.HTTPBody
// 獲得檔案資料
// session 上傳的資料 通過 fromData 指定
NSData *fromData = [self fileData:fileDict fieldName:fieldName params:params];
[[[NSURLSession sharedSession] uploadTaskWithRequest:request fromData:fromData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(@"%@--%@ -- %@",[NSJSONSerialization JSONObjectWithData:data options:0 error:NULL],response,error);
}] resume];
}
/**
* 返回要上傳檔案的檔案二進位制資料
*/
- (NSData *)fileData:(NSDictionary *)fileDict fieldName:(NSString *)fieldName params:(NSDictionary *)params{
NSMutableData *dataM = [NSMutableData data];
// 遍歷檔案字典 ---> 檔案資料
[fileDict enumerateKeysAndObjectsUsingBlock:^(NSString *fileName, NSData *fileData, BOOL *stop) {
NSMutableString *strM = [NSMutableString string];
[strM appendFormat:@"--%@\r\n",boundary];
[strM appendFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n",fieldName,fileName];
[strM appendString:@"Content-Type: application/octet-stream \r\n\r\n"];
// 插入 strM
[dataM appendData:[strM dataUsingEncoding:NSUTF8StringEncoding]];
// 插入 檔案二進位制資料
[dataM appendData:fileData];
// 插入 \r\n
[dataM appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
}];
// 遍歷普通引數
[params enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
NSMutableString *strM = [NSMutableString string];
[strM appendFormat:@"--%@\r\n",boundary];
[strM appendFormat:@"Content-Disposition: form-data; name=\"%@\" \r\n\r\n",key];
[strM appendFormat:@"%@",obj];
// 插入 普通引數
[dataM appendData:[strM dataUsingEncoding:NSUTF8StringEncoding]];
}];
// 插入 結束標記
NSString *tail = [NSString stringWithFormat:@"\r\n--%@--",boundary];
[dataM appendData:[tail dataUsingEncoding:NSUTF8StringEncoding]];
return dataM;
}
六、HTTPS
1、HTTPS 原理
Https是基於安全目的的Http通道,其安全基礎由SSL層來保證。最初由netscape公司研發,主要提供了通訊雙方的身份認證和加密通訊方法。現在廣泛應用於網際網路上安全敏感通訊。
-
Https與Http主要區別
- 協議基礎不同:Https在Http下加入了SSL層,
- 通訊方式不同:Https在資料通訊之前需要客戶端、伺服器進行握手(身份認證),建立連線後,傳輸資料經過加密,通訊埠443。Http傳輸資料不加密,明文,通訊埠80。
-
SSL協議基礎
- SSL協議位於TCP/IP協議與各種應用層協議之間,本身又分為兩層:
- SSL記錄協議(SSL Record Protocol):建立在可靠傳輸層協議(TCP)之上,為上層協議提供資料封裝、壓縮、加密等基本功能。
- SSL握手協議(SSL Handshake Procotol):在SSL記錄協議之上,用於實際資料傳輸前,通訊雙方進行身份認證、協商加密演算法、交換加密金鑰等。
- SSL協議位於TCP/IP協議與各種應用層協議之間,本身又分為兩層:
SSL協議通訊過程
(1) 瀏覽器傳送一個連線請求給伺服器,伺服器將自己的證照(包含伺服器公鑰S_PuKey)、對稱加密演算法種類及其他相關資訊返回客戶端;
(2) 客戶端瀏覽器檢查伺服器傳送到CA證照是否由自己信賴的CA中心簽發。若是,執行4步;否則,給客戶一個警告資訊:詢問是否繼續訪問。
(3) 客戶端瀏覽器比較證照裡的資訊,如證照有效期、伺服器域名和公鑰S_PK,與伺服器傳回的資訊是否一致,如果一致,則瀏覽器完成對伺服器的身份認證。
(4) 伺服器要求客戶端傳送客戶端證照(包含客戶端公鑰C_PuKey)、支援的對稱加密方案及其他相關資訊。收到後,伺服器進行相同的身份認證,若沒有通過驗證,則拒絕連線;
(5) 伺服器根據客戶端瀏覽器傳送到密碼種類,選擇一種加密程度最高的方案,用客戶端公鑰C_PuKey加密後通知到瀏覽器;
(6) 客戶端通過私鑰C_PrKey解密後,得知伺服器選擇的加密方案,並選擇一個通話金鑰key,接著用伺服器公鑰S_PuKey加密後傳送給伺服器;
(7) 伺服器接收到的瀏覽器傳送到訊息,用私鑰S_PrKey解密,獲得通話金鑰key。
(8) 接下來的資料傳輸都使用該對稱金鑰key進行加密。
上面所述的是雙向認證 SSL 協議的具體通訊過程,伺服器和使用者雙方必須都有證照。由此可見,SSL協議是通過非對稱金鑰機制保證雙方身份認證,並完成建立連線,在實際資料通訊時通過對稱金鑰機制保障資料安全性
2、NSURLSession--HTTPS
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// 建立 url
NSURL *url = [NSURL URLWithString:@"https://mail.itcast.cn"];
// 發起請求
[[self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(@"data = %@,response = %@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding],response);
}] resume];
}
NSURLConnection/CFURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9843)
錯誤原因:沒有信任證照
如何信任證照?
通過代理方法告訴伺服器信任證照
#pragma mark - NSURLSessionTaskDelegate 代理方法
// 收到伺服器發過來的證照後回撥
// 提示:此代理方法中的程式碼是固定的,只要會 cmd+c / cmd + v 就可以了
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
NSLog(@"protectionSpace - %@",challenge.protectionSpace);
// 判斷是否是信任伺服器證照
if(challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
// 使用受保護空間的伺服器信任建立憑據
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
// 通過 completionHandler 告訴伺服器信任證照
completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
}
}
引數介紹
challenge '挑戰' 安全質詢,詢問是否信任證照
completionHandler 對證照處置的回撥
- NSURLSessionAuthChallengeDisposition: 通過該引數告訴伺服器如何處置證照
NSURLSessionAuthChallengeUseCredential = 0, 使用指定的憑據,即信任伺服器證照
NSURLSessionAuthChallengePerformDefaultHandling = 1, 預設處理,忽略憑據。
NSURLSessionAuthChallengeCancelAuthenticationChallenge = 2, 整個請求取消,忽略憑據
NSURLSessionAuthChallengeRejectProtectionSpace = 3, 本次拒絕,下次再試
- NSURLCredential
#pragma mark - 懶載入 session
- (NSURLSession *)session {
if (_session == nil) {
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
_session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
}
return _session;
}
3、NSURLConnection--HTTPS
(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// 建立 url 物件
NSURL *url = [NSURL URLWithString:@"https://mail.itcast.cn"];
// 建立請求物件
NSURLRequest *request = [NSURLRequest requestWithURL:url];
// 傳送請求
[NSURLConnection connectionWithRequest:request delegate:self];
}
#pragma mark - NSURLConnection 代理方法
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge{
NSLog(@"change = %@",challenge.protectionSpace);
// 判斷是否是伺服器信任證照
if(challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
// 建立憑據
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
// 傳送信任告訴伺服器信任證照
[challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
}
}
/**
* 接收到伺服器響應
*/
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// 清空資料
[self.data setData:nil];
}
/**
* 接收到伺服器返回的資料呼叫
*/
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// 拼接資料
[self.data appendData:data];
}
/**
* 請求完畢呼叫
*/
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"self.data = %@",self.data);
}
- (NSMutableData *)data {
if (_data == nil) {
_data = [[NSMutableData alloc] init];
}
return _data;
}
七、AFNetworking
1、AFN介紹
- AFN
- 目前國內開發網路應用使用最多的第三方框架
- 是專為 MAC OS & iOS 設計的一套網路框架
- 對 NSURLConnection 和 NSURLSession 做了封裝
- 提供有豐富的 API
- 提供了完善的錯誤解決方案
- 使用非常簡單
學習第三方框架的步驟
-
獲取框架
- 克隆程式碼 $ git clone https://github.com/AFNetworking/AFNetworking.git
- 更新程式碼 $ git pull
執行演示程式
編寫測試程式
2、AFN演練
2.1、AFN--Get/Post
1、GET請求
- (void)get{
// 建立請求管理器
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
// 傳送請求
[manager GET:@"http://192.168.1.105/demo.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(@"%@---%@",responseObject,[responseObject class]);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(@"error = %@",error);
}];
}
2、Get請求
- (void)getLogin01{
// 建立請求管理器
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
// 傳送登入請求
[manager GET:@"http://192.168.1.105/login.php?username=zhangsan&password=zhang" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(@"%@---%@",responseObject,[responseObject class]);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(@"error = %@",error);
}];
}
3、Get請求
- (void)getLogin02{
// 建立請求管理器
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
// 封裝請求引數
NSDictionary *params = @{@"username":@"張三",@"password":@"zhang"};
// 傳送登入請求
[manager GET:@"http://192.168.1.105/login.php" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(@"%@---%@",responseObject,[responseObject class]);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(@"error = %@",error);
}];
}
4、POST請求
- (void)postLogin{
// 建立請求管理器
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
// 封裝請求引數
NSDictionary *params = @{@"username":@"zhangsan",@"password":@"zhang"};
// 傳送登入請求
[manager POST:@"http://192.168.1.105/login.php" parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(@"%@---%@",responseObject,[responseObject class]);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(@"error = %@",error);
}];
}
5、AFN的好處
沒有了 URL 的概念
完成回撥的結果已經做好了序列化
完成回撥在主執行緒,不需要考慮執行緒間的通訊
GET請求的引數可以使用字典封裝,不需要再記住 URL 的拼接格式
不需要新增百分號轉義,中文,特殊字元(空格,&)等。OC增加百分轉義的方法不能轉義所有特殊字元,AFN處理的很好。
POST請求不用設定HTTPMethod/HTTPBody
2.2、AFN--SAX解析
- (void)xml{
// 建立請求管理器
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
// 設定響應解析器為 xml 解析器
manager.responseSerializer = [AFXMLParserResponseSerializer serializer];
// 傳送登入請求
[manager GET:@"http://192.168.1.105/videos.xml" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(@"%@---%@",responseObject,[responseObject class]);
[HMSAXVideo saxParser:responseObject finished:^(NSArray *data) {
NSLog(@"%@",data);
}]
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(@"error = %@",error);
}];
}
2.3、AFN檔案上傳
- (void)postUpload {
AFHTTPSessionManager *mgr = [AFHTTPSessionManager manager];
// 上傳
NSDictionary *params = @{@"username": @"da xiagua"};
[mgr POST:@"http://localhost/upload/upload-m.php" parameters:params constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
/**
引數
1. 本地檔案 URL
2. name: 負責上傳檔案的欄位名,諮詢公司的後端程式設計師,或者有文件
3. error
*/
NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"04.jpg" withExtension:nil];
[formData appendPartWithFileURL:fileURL name:@"userfile[]" error:NULL];
// 上傳多個檔案
/**
引數
1. 本地檔案 URL
2. name: 負責上傳檔案的欄位名,諮詢公司的後端程式設計師,或者有文件
3. fileName: 儲存在伺服器的檔名
4. mimeType: 告訴伺服器上傳檔案的型別1
5. error
*/
NSURL *fileURL2 = [[NSBundle mainBundle] URLForResource:@"AppIcon.jpg" withExtension:nil];
[formData appendPartWithFileURL:fileURL2 name:@"userfile[]" fileName:@"001.jpg" mimeType:@"application/octet-stream" error:NULL];
} success:^(NSURLSessionDataTask *task, id responseObject) {
NSLog(@"%@", responseObject);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
NSLog(@"%@", error);
}];
}
2.4、AFN 檔案下載
- (void)download{
// 1. url
NSURL *url = [NSURL URLWithString:@"http://localhost/123.mp4"];
// 2. AFN 上傳
AFHTTPSessionManager *mgr = [AFHTTPSessionManager manager];
// 3. request
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 4. 開始下載任務
// *** 學習第三方框架的好處:可以發現自己的知識空缺點,跟大牛直接學習!
// iOS 7.0 之後推出的,專門用於跟蹤進度的類,可以跟蹤`進度樹`
NSProgress *progress = nil;
[[mgr downloadTaskWithRequest:request progress:&progress destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
NSLog(@"file = %@",targetPath);
return [documentsDirectoryURL URLByAppendingPathComponent:[response suggestedFilename]];
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
NSLog(@"response = %@,filePath = %@",response,filePath);
}] resume];
// 此處已經獲得進度物件 - `監聽`進度
NSLog(@"%@", progress);
// KVO監聽進度
[progress addObserver:self forKeyPath:@"completedUnitCount" options:0 context:nil];
}
// 是所有 KVO 統一呼叫的一個方法,最好判斷一下物件型別
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
// NSLog(@"%@", object);
// 判斷物件是否是 NSProgress 物件
if ([object isKindOfClass:[NSProgress class]]) {
NSProgress *p = object;
NSLog(@"%lld - %lld", p.completedUnitCount, p.totalUnitCount);
NSLog(@"%@ - %@", p.localizedDescription, p.localizedAdditionalDescription);
// *** 顯示百分比
NSLog(@"%f", p.fractionCompleted);
}
}