我們在進行網路開發的時候,難免要從網路伺服器中獲取資料,很多時候還需要給伺服器提交資料,就現在來說,一般我們都會習慣使用 JSON 格式的資料,因為它方便好用,這次我們就一起來看看,如何用 Swift 發送 JSON-RPC 命令並獲取回復。
JSON-RPC
JSON-RPC是一個無狀態且羽量級的遠端程序呼叫(RPC)協議。 本規範主要定義了一些資料結構及其相關的處理規則。它允許運行在基於 socket, HTTP 等諸多不同消息傳輸環境的同一進程中。其使用JSON(RFC 4627)作為資料格式。
當我們需要從伺服器獲取資料的時候,要麼通過 URL 附帶資訊,要麼就需要使用一些特定的方法給伺服器提交資訊,比如常見的 HTTP POST 。
我們通過 HTTP 的 POST 來給伺服器發送 JSON 請求物件以表示一次 RPC 調用,按照 JSON-RPC 規則,我們需要傳入 4 個基本成員:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
jsonrpc //指定JSON-RPC协议版本的字符串,必须准确写为“2.0” method //包含所要调用方法名称的字符串,以rpc开头的方法名,用英文句号(U+002E or ASCII 46)连接的为预留给rpc内部的方法名及扩展名,且不能在其他地方使用。 params //调用方法所需要的结构化参数值,该成员参数可以被省略。 id //已建立客户端的唯一标识id,值必须包含一个字符串、数值或NULL空值。如果不包含该成员则被认定为是一个通知。该值一般不为NULL,若为数值则不应该包含小数。 |
好了,大概瞭解了這些資訊後,我們就可以嘗試寫出一個 RPC 調用物件:
1 |
{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1} |
關於格式規則部分我們就介紹這麼多,要瞭解更多關於 JSON-RPC 的資訊請看文章末尾「延伸閱讀」中的相關連結。
Swift 裡的網路請求
在 Swift 裡邊,我們使用網路請求的話還是會使用 Cocoa 裡的 API,一般我們常用的即 NSURLConnection ,使用它的 sendSynchronousRequest(_:returningResponse:) 方法來進行網路請求,但不幸的是這個方法將終止在 OS X 10.11 了,所以我們應當儘量不去使用它。
NSURLSession
它是徹底重構過的 NSURLConnection ,在 iOS 7 中引入,我們使用它來替代 NSURLConnection ,不過,既然重構了,那麼使用方法也會有些許的不同,現在我們就來嘗試看看使用 NSURLSession 獲取資料。
首先來說說 NSURLSession 並不是一個類,而是一個工廠,它最終會返回一個 NSURLSessionDataTask ,而我們需要調用 task 的 .resume() 方法來啟動連結,這裡我們用到 NSURLSession 的 dataTaskWithRequest(_:) 方法來產生 NSURLSessionDataTask 實例。
1 2 3 |
let s = NSURLRequest(URL: NSURL(string: "https://www.logcg.com/")!) NSURLSession.sharedSession().dataTaskWithRequest(s).resume() |
這裡我們創建了一個落格博客首頁的 Request ,然後從 NSURLSession 的 sharedSession() 方法直接獲取預設配置的 NSURLSession 實例,然後調用其中的 dataTaskWithRequest(_:) 方法,傳入 Request ,由於這東西是並行的我們直接順便調用 resume() 讓它立即啟動。
…… 等等,這麼執行好像什麼也沒有? 對,由於我們還要得到伺服器返回的 JSON 物件,所以我們還要給 dataTaskWithRequest(_:) 方法追加一個閉包進去——如果你不知道什麼是閉包,也沒關係,其實就是多傳入一個函數作為參數,這個函數用來處理獲取到的資料:
1 2 3 4 5 6 |
let s = NSURLRequest(URL: NSURL(string: "https://www.logcg.com/")!) NSURLSession.sharedSession().dataTaskWithRequest(s, completionHandler: { (data, res, e) -> Void in let t = NSString(data: data!, encoding: NSUTF8StringEncoding) print(t) }).resume() |
按照 dataTaskWithRequest(_:) 方法的聲明:
1 |
public func dataTaskWithRequest(request: NSURLRequest, completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionDataTask |
我們的函數需要接收三個實際參數,而且不能帶有傳回值,那麼我們的函數實際上也可以單獨聲明為這樣:
1 2 3 |
func handler (data:NSData?, response:NSURLResponse?, e:NSError?) { //Do something here. } |
總之,按照上邊的代碼執行,我們就可以在終端看到獲取到的落格首頁的 HTML 資料了。
對於要發送更多的資料進去,使用 NSURLRequest 必然是不夠的,這裡我們要使用 NSMutableURLRequest ,它是 NSURLRequest 的一個子類,擁有更多的功能,比如添加 HTTP 頭,以及添加我們所需要提交的 HTTP Body 資訊:
1 2 3 4 5 6 |
let request = NSMutableURLRequest(URL: NSURL(string: url)!) request.HTTPMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue("application/json", forHTTPHeaderField: "Accept") request.HTTPBody = //我们的 JSON 数据 |
稍後我們來把它提交到伺服器。
Swift 裡的 JSON
說起來,Swift 對 JSON 的處理至今也不盡人意,不過好在我們只需要簡單地將 RPC 調用封裝,所以用起來也不會那麼困難。
NSJSONSerialization
我們通過這個類將 Swift 裡的物件轉換為 JSON 物件,它能把 Swift 裡的 NSString、NSNumber、NSArray、NSDictionary、NSNull 與 JSON 互轉,這裡我們只用到 NSDictionary 就可以了,因為這個最方便。
我們只需要寫好一個對應的 NSDictionary 然後傳給 dataWithJSONObject 方法即可:
1 2 3 |
let data = ["jsonrpc": "2.0", "method": "subtract", "params":"getinfo", "id": 1] request.HTTPBody = try! NSJSONSerialization.dataWithJSONObject(data, options: NSJSONWritingOptions.PrettyPrinted) |
這樣,我們的 JSON-RPC 命令就寫好了!
通過 HTTP 進行 JSON-RPC 遠程調用
最後要做的就是把它發出去了!
不過,這個測試你可能需要在專案而不是 Playground 裡做,因為在 iOS 9 以後蘋果引入了新特性 App Transport Security (ATS),預設禁止了不安全的 HTTP 連結,如果你測試的位址是 HTTPS 則無所謂,如果你的位址是 HTTP,那你需要先禁用這個安全機制。
禁用 App Transport Security (ATS)
在你的專案當中找到專案的屬性,然後進入 Info 選項卡,在裡邊添加一條 NSAppTransportSecurity 類型的 Dictionary ,然後再在 NSAppTransportSecurity 裡邊添加一條 NSAllowsArbitraryLoads 鍵,對應的值為 Boolean 類型的 YES 。
如果你嘗試使用 CLI 專案測試,那估計你找不到 信息.plist中 檔...... 所以還是不要嘗試了,不然你不但得不到期望的結果,連錯誤提示都不會得到。
封裝一個類型方法
好了,做足了前期的準備,我們來看看如何封裝一個類來進行 RPC 調用:
首先我們寫一個叫做 JSONRPC 的類,這裡邊寫一個類型方法 郵政 :
1 2 3 4 5 6 |
class JSONRPC { class func post (params : NSDictionary, url : String, postCompleted : (succeeded: Bool, msg: String, data:String) -> ()) {} } |
這個類型方法接收一個 NSDictionary 的實際參數,它就是我們要發送的資料;然後是伺服器的位址,最後接收一個閉包,它用來產生回檔,這個閉包包含:
一個 succeeded 用來給你判斷是否成功, msg 返回 HTTP Response,如果成功無錯,則 數據 會包含返回的 JSON。
首先我們根據傳入的資訊來準備資料:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let request = NSMutableURLRequest(URL: NSURL(string: url)!) let session = NSURLSession.sharedSession() request.HTTPMethod = "POST" do { request.HTTPBody = try NSJSONSerialization.dataWithJSONObject(params, options: NSJSONWritingOptions.PrettyPrinted) } catch (let e) { print(e) return } request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue("application/json", forHTTPHeaderField: "Accept") |
然後我們把資料傳入,同時扔一個閉包進去根據結果來執行我們函數接收的那個函數(也就是回呼函數):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in let json = try? NSJSONSerialization.JSONObjectWithData(data!, options: .MutableLeaves) as? NSDictionary guard json != nil else { let jsonStr = NSString(data: data!, encoding: NSUTF8StringEncoding) print("Error could not parse JSON: \(jsonStr)") postCompleted(succeeded: false, msg:String(response),data: "Error") return } postCompleted(succeeded: true, msg:String(response),data: NSString(data: data!, encoding: NSUTF8StringEncoding)! as String) }).resume() |
總之,最終的類型方法是這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import Foundation class JSONRPC { class func post (params : NSDictionary, url : String, postCompleted : (succeeded: Bool, msg: String, data:String) -> ()) { let request = NSMutableURLRequest(URL: NSURL(string: url)!) let session = NSURLSession.sharedSession() request.HTTPMethod = "POST" do { request.HTTPBody = try NSJSONSerialization.dataWithJSONObject(params, options: NSJSONWritingOptions.PrettyPrinted) } catch (let e) { print(e) return } request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue("application/json", forHTTPHeaderField: "Accept") session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in let json = try? NSJSONSerialization.JSONObjectWithData(data!, options: .MutableLeaves) as? NSDictionary guard json != nil else { let jsonStr = NSString(data: data!, encoding: NSUTF8StringEncoding) print("Error could not parse JSON: \(jsonStr)") postCompleted(succeeded: false, msg:String(response),data: "Error") return } postCompleted(succeeded: true, msg:String(response),data: NSString(data: data!, encoding: NSUTF8StringEncoding)! as String) }).resume() } } |
測試
好了,最終我們來測試一下,這裡本地我運行了一個 Twisterd 用戶端進程,如果你不知道它是什麼...... 就去看看文章末尾的延伸閱讀...... 總之,我們來給它發送 RPC 指令:
1 2 3 4 5 6 7 8 |
let cmd:NSDictionary = ["jsonrpc":"2.0","method":"getinfo","id":"1"] JSONRPC.post(cmd, url: "http://user:pwd@127.0.0.1:28332") { (succeeded, msg, data) -> () in print(succeeded) print(msg) print(data) } |
最終,我們會得到結果:
1 2 3 4 5 6 7 8 9 10 |
true Optional(<NSHTTPURLResponse: 0x600000027620> { URL: http://user:pwd@127.0.0.1:28332/ } { status code: 200, headers { Connection = "keep-alive"; "Content-Length" = 1004; "Content-Security-Policy" = "script-src 'self' 'unsafe-eval'"; "Content-Type" = "application/json"; Date = "Fri, 12 Feb 2016 09:17:17 +0000"; Server = "bitcoin-json-rpc/v0.9.34.0-unk-beta"; } }) {"result":{"version":93400,"protocolversion":70003,"walletversion":60000,"blocks":124132,"timeoffset":0,"connections":25,"addrman_total":13235,"addrman_get":2500,"dht_nodes":111,"dht_global_nodes":444,"proxy":"","ext_port1":28333,"ext_port2":29333,"difficulty":0.01040237,"testnet":false,"public_server_mode":false,"errors":"This is a pre-release test build - use at your own risk","ext_addr_net1":"110.253.91.44","ext_addr_net2":"110.253.91.44","dht_torrents":761,"num_peers":235,"peerlist_size":30526,"num_active_requests":5,"download_rate":8330,"upload_rate":18828,"dht_download_rate":5722,"dht_upload_rate":15055,"ip_overhead_download_rate":1724,"ip_overhead_upload_rate":1974,"payload_download_rate":0,"payload_upload_rate":0,"total_download":123761426,"total_upload":177836912,"total_dht_download":92672437,"total_dht_upload":131644914,"total_ip_overhead_download":19615000,"total_ip_overhead_upload":23216840,"total_payload_download":1263802,"total_payload_upload":791702},"error":null,"id":null} |
接下來就是你操作這些結果的時間了:)
延伸閱讀
swift通過HttpRequest要求傳送json格式的資料
本文由 落格博客 原創撰寫:落格博客 » Swift 通過 http 發送 JSON-RPC 命令
轉載請保留出處和原文鏈接:https://www.logcg.com/archives/1554.html