iOS 網絡請求 NSURLSession 的上傳文件方法


NSURLSession/NSURLConnection的上傳文件方法

 

 

此篇文章的理論基礎主要是與HTTP網絡通信協議相關。為集中精力,可以先把TCP/IP協議這些置之不理,也就是先只關注HTTP的請求和響應的結構。HTTP完整的原理內容就此略過。在此只略提相關內容。文中涉及的設計源碼可以通過這里獲取 https://github.com/wuqingjian2015/uploadHelper,有意者可以去看看。

 

HTTP是干什么用的呢?

先考慮一下以下應用過程:

  1. 從客戶端向服務器端發起一個請求。
  2. 服務器端處理請求
  3. 服務器端發送一個響應。

那么,如果應用上面過程來實現上傳文件這個功能,需要做到幾方面:

  1. 要上傳的文件需要捆綁在這個請求里面。
  2. 服務器端能夠理解該請求,作出相關處理:如能提取出文件內容,存放在某一個文件目錄下;如能提取當中某些指令,調用相關的指令處理程序。
  3. 服務器端發送一個響應,客戶端應該能夠理解該響應內容。

HTTP協議就是解決以上這些問題的。它定義了請求體結構和響應體結構。只要客戶端或服務端遵守這個標准,它就能與任何遵守這一標准的應用程序通信。

如果再想實地觀察一下符合HTTP標准的請求體和響應體“長”什么樣,可以用一些抓包工具。我用了Wireshark和Charles。如果你的是網頁應用,可以在IE上按F12鍵調出開發工具窗口的網絡Tab。

在這里,我們只關注請求,了解響應StatusCode是200表示正常。

對於請求,因為iOS會自動設置其他內容,如果咱們不設置的話。下面只討論其中的

  1. 上傳到的目標地址
  2. 請求標頭中的Content-Type
  3. 以及請求正文的內容

如何設置目標地址?在創建NSURLRequest時,指定URL即可。如,

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:self.targetURL];

接下來,我們需要設置Content-Type的值為:multipart/form-data,同時制定boundary的值,該boundary會在設置請求正文時用到。到此為止,我們得到了這樣的一些代碼:

-(NSURLRequest *)createRequestHeader

{

    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:self.targetURL]; //指定目標地址

    //創建http header請求標頭內容

    //  Content-Type := multipart/form-data; boundary=---------------827292(任意)

    //  Content-Length := (文件長度)

    NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", self.boundary];

    [request setValue:contentType forHTTPHeaderField:@"Content-Type"]; //設置Content-Type

    [request setHTTPMethod:@"post"];//設置Method為POST

    return request;

}

 下面再來看請求正文怎么設置。在iOS中,由NSURLRequest.HTTPBody屬性來指定,其為NSData類型。謹記:這個有固定的格式,該格式必須正確,否則服務器端無法取得正確的內容。而這個問題無法通過抓包工具中體現出來。如下:

 格式:

beginBoundary

Content-Disposition: form-data; name="<服務器端需要知道的名字>"; filename="<服務器端這個傳上來的文件名>"
Content-Type: application/zip --根據不同的文件類型選擇不同的值

<空行> 

<二進制數據>

endBoundary

范例:

----KenApp299912318
Content-Disposition: form-data; name="<服務器端需要知道的名字>"; filename="<服務器端這個傳上來的文件名>"
Content-Type: application/zip --根據不同的文件類型選擇不同的值

<空行>

<二進制數據>

----KenApp299912318--

有代碼有真相:

-(NSData*)createDataForRequestHTTPBodyForSource

{

    NSMutableString *bodyHead = [[NSMutableString alloc] init];

    NSMutableData *data = [[NSMutableData alloc] init];

    

    NSString *fileName = [self.sourceURL lastPathComponent];

    NSString *name=@"uploadFile";

    

    NSData *fileContent = [NSData dataWithContentsOfURL:self.sourceURL];

    //創建http body請求體內容

    //  第一行: --827292

    [bodyHead appendString:self.beginBoundary];

    // [body appendFormat:@"--------------------"]

    //  Content-Disposition: form-data; name="uploadFile"; filename="xxxx.ext"

    [bodyHead appendFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n",name,fileName];

    //  Content-Type: application/x-zip-compressed

    //  (空行)

    [bodyHead appendFormat:@"Content-Type: application/zip\r\n\r\n"];

    //  (二進制數據)

    [data appendData:[bodyHead dataUsingEncoding:NSUTF8StringEncoding]];

    [data appendData:fileContent];

    //  最后一行:827292--

    

    [data appendData:[self.endBoundary dataUsingEncoding:NSUTF8StringEncoding]];

    

    return data;

}

 到目前為止,咱們知道怎么設置請求標頭和請求正文了。怎么用上這些結果呢?

如果是用NSURLConnection的話, 我們需要在同一個NSURLRequest中設置好這兩者。

再調用factory method [NSURLConnection connectionWithRequest: delegate:];

如果是用NSURLSession中uploadTask的話,需要在NSURLRequest中設置請求標頭(如下requestWithHeader),同時在NSData中設置請求正文(如下requestHTTPBody)。代碼例子如下,其中SSUploadHelper封裝了以上提到的處理過程。

/////////////////////////////////范 例////////////////////////

  SSUploadHelper *uploadHelper = [[SSUploadHelper alloc] initWithTarget:[NSURL URLWithString:@"http://192.168.31.172:5012/ArchFlow/upload"] forSource:self.downloadedLocation];

    NSURLSessionUploadTask *uploadTask = [self.ephemeralSession uploadTaskWithRequest:[uploadHelper requestWithHeader] fromData:[uploadHelper requestHTTPBody] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

        

        NSLog(@"Got response %@ with error %@.\n", response, error);

        NSLog(@"DATA:\n%@\nEND DATA\n",

              [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);

    }];

    

    [uploadTask resume];

//////////////////////////////////

至此,客戶端的設計基本完成了。為了在服務器端看到上傳到的文件,咱們需要搭建一個服務器環境了。我個人實現了一個基於Python的REST的微服務器,在處理到ArchFlow/upload的POST請求中,從request中獲取文件,並保存到本地目錄下。這是我在軟件架構時用到的工具服務器,在此基礎上作的臨時上傳文件功能。

//功能測試:

在服務器啟動的過程中,執行以上客戶端代碼,可以看到文件被拷貝到目標目錄下。

注意事項:

boundary的格式值得加倍注意,在請求標頭中指明的boundary,必須用到請求正文中。

剩下的就是耐心調試了。Good Luck!


免責聲明!

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



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