Tạo HTTP Request với URLSession

by Sam
811 views

Alamofire là thư viện về HTTP Networking được biết đến nhiều nhất trong lập trình iOS sử dụng Swift. Vậy nếu không sử dụng Alamofire thì chúng ta thực hiện các HTTP request như thế nào? Dưới đây là một trong những cách để thực hiện các request với URL Loading System được cung cấp ở ngay thư viện cơ bản nhất Foundation của Apple.

URL Loading System bao gồm các cấu trúc, giao thức để làm việc với URL và giao tiếp với server. Ở bài viết này chúng ta sẽ làm việc chủ yếu với lớp URLSession.

1. Thực hiện Request đơn giản với URLSession

Lấy ví dụ thực hiện search key word “Son Tung MTP” với iTunes Store API, chúng ta cần thực hiện một request với thông tin sau đây:

 let iTunesHostURL = "https://itunes.apple.com/search?term=Son+Tung+MTP"
 guard let url = URL(string: iTunesHostURL) else {
     print("URL Not valid")
     return
 } 

Để thực thi request trên, ta sử dụng URLSession như sau:

 let task1 = session.dataTask(with: url, completionHandler: { data, response, error in
     if let error = error {
         print(error)
     }
     if let response = response {
         print(response)
     }
     if let data = data, let dataString = String(data: data, encoding: .utf8) {
         print(dataString)
     }
 })
 task1.resume() 

Nhân vật chính là phương thức dataTask(with:completionHandler:) của URLSession, ở đây ta chỉ cần khởi tạo một URL với đường dẫn sẵn có, sau đó xử lý dữ liệu bên trong completionHandler block của phương thức trên. Với TH trên, thông tin in ra của response (HTTP status code, headers) hiển thị trên output log như sau:

{ Status Code: 200, Headers { "Cache-Control" = ( "max-age=86400" ); "Content-Disposition" = ( "attachment; filename=1.txt" ); "Content-Encoding" = ( gzip ); "Content-Length" = ( 7849 ); "Content-Type" = ( "text/javascript; charset=utf-8" ); Date = ( "Tue, 08 Dec 2020 08:55:43 GMT" ); "Strict-Transport-Security" = ( "max-age=31536000" ); Vary = ( "Accept-Encoding" ); "apple-originating-system" = ( MZStoreServices ); "apple-seq" = ( 0 ); "apple-timing-app" = ( "277 ms" ); "apple-tk" = ( false ); b3 = ( "ab8b7390acfee63d8fddd6db794d7776-27de63d447f98e57" ); "x-apple-application-instance" = ( 2007320 ); "x-apple-application-site" = ( ST11 ); "x-apple-jingle-correlation-key" = ( VOFXHEFM73TD3D6523NXSTLXOY ); "x-apple-orig-url" = ( "https://itunes.apple.com/search?term=Son+Tung+MTP" ); "x-apple-partner" = ( "origin.0" ); "x-apple-request-uuid" = ( "ab8b7390-acfe-e63d-8fdd-d6db794d7776" ); "x-apple-translated-wo-url" = ( "/WebObjects/MZStoreServices.woa/ws/wsSearch?term=Son+Tung+MTP&urlDesc=" ); "x-b3-spanid" = ( 27de63d447f98e57 ); "x-b3-traceid" = ( ab8b7390acfee63d8fddd6db794d7776 ); "x-cache" = ( "TCP_MISS from a113-171-230-176.deploy.akamaitechnologies.com (AkamaiGHost/10.2.2.1-31386017) (-)" ); "x-cache-remote" = ( "TCP_MISS from a23-67-57-164.deploy.akamaitechnologies.com (AkamaiGHost/10.2.2.1-31386017) (-)" ); "x-content-type-options" = ( nosniff ); "x-true-cache-key" = ( "/L/itunes.apple.com/search vcd=2897 ci2=term=Son+Tung+MTP///" ); "x-webobjects-loadaverage" = ( 0 ); } }

Thông tin data trả về sau khi được convert thành String hiển thị lên output log như sau:

{ "resultCount":48, "results": [ {"wrapperType":"track", "kind":"song", "artistId":705007874, "collectionId":1380326325, "trackId":1380326334, "artistName":"Sơn Tùng M-TP", "collectionName":"Lạc Trôi - Single", "trackName":"Lạc Trôi", "collectionCensoredName":"Lạc Trôi - Single", "trackCensoredName":"Lạc Trôi", "artistViewUrl":"https://music.apple.com/us/artist/s%C6%A1n-t%C3%B9ng-m-tp/705007874?uo=4", "collectionViewUrl":"https://music.apple.com/us/album/l%E1%BA%A1c-tr%C3%B4i/1380326325?i=1380326334&uo=4", "trackViewUrl":"https://music.apple.com/us/album/l%E1%BA%A1c-tr%C3%B4i/1380326325?i=1380326334&uo=4", "previewUrl":"https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview118/v4/d3/ee/c7/d3eec748-a929-3ed5-4850-f79a13c34dbb/mzaf_4185904769253643682.plus.aac.p.m4a", "artworkUrl30":"https://is5-ssl.mzstatic.com/image/thumb/Music118/v4/02/81/33/028133f4-db9c-2665-bfd2-7a38389355fc/source/30x30bb.jpg", "artworkUrl60":"https://is5-ssl.mzstatic.com/image/thumb/Music118/v4/02/81/33/028133f4-db9c-2665-bfd2-7a38389355fc/source/60x60bb.jpg", "artworkUrl100":"https://is5-ssl.mzstatic.com/image/thumb/Music118/v4/02/81/33/028133f4-db9c-2665-bfd2-7a38389355fc/source/100x100bb.jpg", "collectionPrice":1.29, "trackPrice":1.29, "releaseDate":"2016-12-31T12:00:00Z", "collectionExplicitness":"notExplicit", "trackExplicitness":"notExplicit", "discCount":1, "discNumber":1, "trackCount":1, "trackNumber":1, "trackTimeMillis":232889, "country":"USA", "currency":"USD", "primaryGenreName":"Alternative", "isStreamable":true}...]}

2. Thực hiện Request tuỳ chỉnh HTTP Request Methods

Có tổng cộng 9 loại HTTP Request Methods, hai loại phổ biến nhất là GET và POST. Đối với các API được định nghĩa Method, chúng ta sẽ sử dụng thêm URLRequest để tuỳ chỉnh các methods trên. Tiếp tục với ví dụ ở trên, ta sẽ định nghĩa HTTP method là GET như sau:

 var request = URLRequest(url: url)
 request.httpMethod = "GET"

Đối với các HTTP Methods khác, ta chỉ cần thay đổi giá trị httpMethod dưới dạng string như “POST”, “PUT”, …
Để gửi request với URLRequest, ta sử dụng phương thức dataTask(with:completionHandler:), kết quả vẫn không đổi so với ví dụ request ở trên:

 let task = session.dataTask(with: request, completionHandler: { data, response, error in
     if let error = error {
         print(error)
     }
     if let response = response {
         print(response)
     }
     if let data = data, let dataString = String(data: data, encoding: .utf8) {
         print(dataString)
     }
 })
 task.resume() 

3. Thực hiện Request tuỳ chỉnh HTTP Header

Đối với nhiều hệ thống, việc yêu cầu thêm các thông tin của Client khi gửi request lên là bắt buộc, và sẽ được truyền lên trên HTTP Headers của Request. Ví dụ với một request yêu cầu client gửi lên các thông tin Authorization, Content-Type, Accept-Language, ta sẽ tuỳ chỉnh URLRequest như sau:

 var request = URLRequest(url: url)
 request.httpMethod = "GET"
 request.setValue("Basic bGluaG5iMTpsaW5obmIx", forHTTPHeaderField: "Authorization")
 request.setValue("application/json; charse=UTF-8", forHTTPHeaderField: "Content-Type")
 request.setValue("en-US", forHTTPHeaderField: "Accept-Language") 

4. Thực hiện Request tuỳ chỉnh HTTP Body

HTTP Body có rất nhiều kiểu, ví dụ như raw data (string, json), x-www-form-urlencoded, form-data, … Bài viết sẽ tập trung vào một số kiểu body phổ biến:

4.1 HTTP Body với x-www-form-urlencoded

x-www-form-urlencoded có dạng key-value và sẽ được mã hoá theo dạng URL Encoding (Percent Encoding) khi gửi request. Để gửi một request với body là x-www-form-urlencoded ta cần cài đặt HTTP Header như sau:

 request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")

Đối với phần body, lấy ví dụ dữ liệu truyền lên như sau:

 let requestDictionary = ["term":"Son Tung MTP", "country":"VN", "lang":"vi_vn"]

Chúng ta sẽ sử dụng URLComponent để chứa các thành phần key-value, sau đó encode thành URL

 var requestBodyComponent = URLComponents()
 requestBodyComponent.queryItems = [URLQueryItem(name: "term", value: "Son Tung MTP"),
                                            URLQueryItem(name: "country", value: "VN"),
                                            URLQueryItem(name: "lang", value: "vi_vn")] 
print(requestBodyComponent.string) // print -> "?term=Son%20Tung%20MTP&country=VN&lang=vi_vn" 

Sau đó, ta chỉ cần convert URL Encoded String thành Data và truyền vào body của request:

 request.httpBody = requestBodyComponent.query?.data(using: .utf8) 

4.2 HTTP Body với JSON

JSON là một kiểu dữ liệu rất phổ biến và được hỗ trợ rất tốt. Do đó, việc sử dụng request với JSON cũng rất dễ dàng. Vẫn với ví dụ dictionary ở trên, chúng ta muốn truyền đi ở dạng Json như sau “{\”country\”:\”VN\”,\”lang\”:\”vi_vn\”,\”term\”:\”Son Tung MTP\”}”:

 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let requestDictionary = ["term":"Son Tung MTP", "country":"VN", "lang":"vi_vn"]
 guard let jsonData = try? JSONEncoder().encode(requestDictionary) else {
     print("Error: Failed to JSON data")
     return
 } 
 request.httpBody = jsonData

Chúng ta có thể sử dụng nhiều kiểu dữ liệu đầu vào để encode thành json Data với phương thức JSONEncoder().encode(_ value: T), chỉ cần kiểu dữ liệu đó có thuộc tính Encodable. Vì vậy, các lớp để chứa dữ liệu cho request body dạng JSon thường được kế thừa Encodable hoặc rộng hơn là Codable.

4.3 HTTP Body với form data

Kiểu dữ liệu multipart/form-data thường được sử dụng khi request có chứa các file đính kèm cần upload (Ví dụ: JPG, PNG, …). Lấy ví dụ với trường hợp cần upload một UIImage dạng PNG:

 let uploadImage = UIImage(named: "sample")

Trong trường hợp này, HTTP Header và Body sẽ có dạng như sau:

Content-Type:multipart/form-data; boundary=--26142EB6-EDB0-4F36-A4EE-079B11F200C3
--26142EB6-EDB0-4F36-A4EE-079B11F200C3

Content-Disposition: form-data; name="image_field"; filename="sample.jpg"
Content-Type: image/jpg

￘¢\u{10}䩆䥆\u{01}Ā\0£Œ䕸楦\0䵍*\0\u{08}\u{05}Ē\u{03}\0\u{01}\u{01}\0Ě\u{05}\0\u{01}\0Jě\u{05}\0\u{01 // Image Data
--26142EB6-EDB0-4F36-A4EE-079B11F200C3--

Đầu tiên, với header sẽ có format như sau:

Content-Type:multipart/form-data; boundary=\(boundary)

Giá trị boundary là free defined bởi người dùng và tuân theo một số ràng buộc.

Đổi với phần body, mỗi một part sẽ có format như sau:

--\(boundary)  // Start part 1
 Content-Disposition: form-data; name="\(partName)"

\(partData)
--\(boundary) // Start part 2
 Content-Disposition: form-data; name="\(partName)"
 Content-Type: \(contentType) // application/json

\(partData)
--\(boundary) // Start part 3
 Content-Disposition: form-data; name="\(partName)"; filename="\(fileNameOnlyForFile)"
 Content-Type: \(contentType) // image/jpg

\(partData)

Trong đó, trường filename là optional, chỉ sử dụng khi upload file, đối với dữ liệu plain thì không cần thiết. Trường contentType có một số loại như mimeType của image, application/json hoặc không cần thiết đối với plain text. partData chính là value của tham part cần truyền lên dưới dạng Data.

Ở cuối cùng của HTTP Body, ta sẽ thêm một block nữa để đánh dấu kết thúc của phần body:

--\(boundary)--

Sample code sẽ trông như sau:

 let uploadImage = UIImage(named: "sample")
 guard let imageData = uploadImage?.jpegData(compressionQuality: 1) else {
     return
 }

 let boundary = "Boundary-\(UUID().uuidString)"
 request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
 
 let httpBody = NSMutableData()
// Loop following 5 block for each form part
 httpBody.appendString("--\(boundary)\r\n")
 httpBody.appendString("Content-Disposition: form-data; name=\"image_field\"; filename=\"sample.jpg\"\r\n")
 httpBody.appendString("Content-Type: image/jpg\r\n\r\n")
 httpBody.append(imageData)
 httpBody.appendString("\r\n")

// Add the ending block for body
 httpBody.appendString("--\(boundary)--")
 
 request.httpBody = httpBody as Data 
 extension NSMutableData {
   func appendString(_ string: String) {
     if let data = string.data(using: .utf8) {
       self.append(data)
     }
   }
 } 

5. Thực hiện Request download file

Để thực hiện download một file, ta có thể sử dụng downloadTask(with:completionHandler:), dưới đây là một ví dụ download file jpg và hiển thị lên UI:

 let iTunesHostURL = "https://phongvu.vn/cong-nghe/wp-content/uploads/2018/07/dota_2_traxe_drow_ranger_art_milimalism_97328_1920x1080.jpg"
 guard let url = URL(string: iTunesHostURL) else {
     print("URL Not valid")
     return
 }
 
 let task = URLSession.shared.downloadTask(with: url) { localURL, urlResponse, error in
     guard let localURL = localURL,
           let data = try? Data.init(contentsOf: localURL),
           let image = UIImage.init(data: data) else {
         print("Failed to read downloaded file to image")
         return
     }
     DispatchQueue.main.async {
         let imageview = UIImageView.init(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
         imageview.image = image
         imageview.contentMode = .scaleToFill
         self.view.addSubview(imageview)
     }
 }
 task.resume() 

6. Một số chú ý khi sử dụng URLSession

  1. Completion Handler được thực thi ở delegate queue, do đó các cập nhật liên quan đến các thành phần thuộc UIKit (chỉ thực thi được trên main thread) cần đặc biệt chú ý khi sử dụng bên trong completionHandler.
  2. Kể từ iOS 9, mặc định các Request HTTP sẽ bị block theo chính sách AppTransportSecurity của Apple. Do đó nếu request không phải HTTPS mà là HTTP, cần cài đặt lại thông tin như sau trong info.plist
 <key>NSAppTransportSecurity</key>
 <dict>
     <key>NSAllowsArbitraryLoads</key>
     <true/>
 </dict> 

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.