JSON可能是這個地球上最簡單的文本數據格式了,可讀、靈活、數據量小,編解碼方便、速度快,對Unicode和特殊字符支持的好。對比下XML,就知道額外的各種標籤節點需要浪費多少字節數。JSON字符默認都要使用Unicode形式,所有非ACSII字符都可以用\uXXXX表示,而不需要額外的轉義。相比之下,XML裏需要使用轉義或是CDATA(類似HTML裏的PRE標籤)、或是Base64才能表示特殊數據。當然缺點也很明顯,比二進制數據結構的數據量大,編解碼慢,沒有完備的類型系統,表達能力有限。
JSON-RPC是一個使用json對象作爲數據載體的遠程過程調用(Remote Process Call,RPC)技術。
"Does distributed computing have to be any harder than this? I don't think so." -- Jan-Klaas Kollhof
JSON-RPC的設計目標就是兩個字:簡單
。我們知道一個rpc框架是爲了2個系統間的交互通信,這就需要定義一箇中間的數據傳輸格式。爲了跟系統本身用的平臺數據結構轉換,需要提供一套序列化和反序列化這個數據格式的功能。然後就是需要某種通信協議來傳輸實際遠程調用的數據。最後還需要通信的兩端有實現的代碼樁(stub&skeleton),這一般是基於動態代理或AOP實現的代理,一個可供調用的接口結構,使得框架隱藏了其他所有的技術細節(數據格式、序列化、網絡傳輸等),程序裏能像本地方法調用一樣調用遠程的方法。總結一下:
- 數據格式
- 序列化功能
- 通信協議
- 代碼樁
JSON-RPC規範裏只顯式規定了數據格式(即JSON),建議了通信協議(TCP或是HTTP),序列化和代碼樁沒有提及。當然序列化比較好辦,各種語言裏都有豐富的JSON序列化庫(參見參考資料1)。那麼代碼樁就完全留給了實現JSON-RPC規範的框架自己去處理了,通信協議這一塊的具體處理也大部分需要框架自己考慮。
我們知道兩個系統的通信,可能是同步阻塞的(每個請求要等響應完成以後再發下一個請求),也可能是異步非阻塞的(請求1發完,繼續發請求2,哪個響應回來就處理哪個響應);也可能是單向的(one-way,發了就不管了、不要響應信息),也可能是請求-響應的(request-response),每個請求都需要有顯式的回覆。
JSON-RPC1.0裏,把通信的雙方當做對等的兩個端。
- 定義瞭如何發請求和返回結果、出錯信息,
- 如何處理單向和請求-響應的交互過程,
- 以及雙向異步請求如何匹配結果和請求,
- 最後還添加了一個簡單的類型提示(class hinting)擴展功能(想法很好,但是就兩句話沒細節,挺雞肋的)。
我們來具體看看規範的內容。
請求(request)
請求端發送一個JSON對象表示自己要調用的方法,示例如下:
{ "method": "echo", "params": ["Hello JSON-RPC"], "id": 1}
其中包含三個關鍵元素:
- method:表示要調用的方法
- params:表示參數的數組
- id:表示這次請求是需要響應的,服務方需要提供一個同樣是id爲1的響應信息。
這個id字段非常有意思,一般的同步RPC都不需要考慮這個東西,因爲一般情況下,一個RPC請求只包含一個請求,並且請求會等待響應信息返回,在這之前一直會阻塞調用方的處理線程,這樣整個RPC纔像是調用的本地方法。
響應(response)
一個典型的響應信息如下:
{ "result": "Hello JSON-RPC", "error": null, "id": 1}
關鍵元素也是3個:
- result:表示返回結果,如果出錯result必須爲null
- error:表示出錯,如果請求成功,則error必須爲null
- id:表示爲此響應對應的請求id
通知(notification)
通知信息類似一個請求,但是id必須爲null。通知代表單向的請求,不能回覆,可以由請求方發送給服務方,也可以由服務方發送給請求方(協議原則上要求雙方對等,都可以是請求方或服務方,這一點上HTTP其實是不滿足的)。
通信(transport)
- TCP
協議推薦通信雙方在TCP下使用字節流(byte stream)的方式交互。此時雙方是對等的。關閉連接前必須給所有未應答的請求方發送一個錯誤信息。無效的請求或響應將會導致連接關閉。
- HTTP
在一些限制的條件下,也可以使用HTTP作爲通信協議。此時通信雙方明顯不在對等,有了客戶端和服務器端。
考慮到HTTP的開銷問題,協議允許一次POST可以帶上多個rpc請求或通知。由於HTTP下,server不能夠主動訪問client,所以server可以在HTTP響應中附帶上自己的請求和通知。這個地方協議們沒有說清楚具體操作,細節也是個蛋疼的事兒。
類型提示(class hinting)
非常簡單,就是加了個屬性表示構造函數constructor
,可以用參數[param1,...]
來初始化對象。而且如果對象初始化的時候調用了這個構造器,那麼相應的屬性(比如prop1
)也會添加到對象,示例代碼:
{"__jsonclass__":["constructor", [param1,...]], "prop1": ...}
協議沒有具體說清楚這個地方,具體怎麼操作,怎麼對應原生環境裏的類型,蛋疼++。
一個多次通信的例子
JSON-RPC過程如下,其中-->
代表發送數據到服務方,<--
代表服務方的響應:
--> {"method": "postMessage", "params": ["Hello all!"], "id": 99}
<-- {"result": 1, "error": null, "id": 99}
<-- {"method": "handleMessage", "params": ["user1", "we were just talking"], "id": null}
<-- {"method": "handleMessage", "params": ["user3", "sorry, gotta go now, ttyl"], "id": null}
--> {"method": "postMessage", "params": ["I have a question:"], "id": 101}
<-- {"method": "userLeft", "params": ["user3"], "id": null}
<-- {"result": 1, "error": null, "id": 101}
關於JSON-RPC1.0規範的進一步討論
- 關於通知
通知機制也可以用於類似FTP協議的NOOP,或是其他通訊協議裏的Keep Alive心跳機制。在具體使用的過程中,定時的發送通知來確認雙方都在線,並且可以捎帶異步處理的信息。
- 關於id
請求中id字段的作用,就跟JMS協議裏的correlationID一樣,可以起到區分多個請求的作用,這樣又兩個好處:
- 多個請求可以打包成一個請求,返回也可以返回多個響應,處理方按照id區分多個數據即可;
- 可以做異步的響應,反正我不保證順序了,也能根據id來匹配原來是哪個請求來着。
特別是配合HTTP和通知功能,使得異步調用變得可能:客戶端直接發送請求1後返回,繼續發送請求2,請求3...等等;服務端在處理完成請求1後,可能把響應放到請求2或3的HTTP response報文返回,或者在接下里的某次交互裏返回即可。
- 關於類型
JSON-RPC跟XML-RPC非常類型,但是藉助於XML本身的schema結構,XML-RPC定義了一套基礎的數據類型、以及在基礎上的構造複雜數據類型的能力,這樣XML-RPC中就可以用於更復雜的業務交互場景。而JSON-RPC使用JS原生的弱類型,只能表示非常簡單和模糊的元數據結構,不利於複雜場景和實現代碼樁的生成工具。如果有一些更有效的schema約束,則可以把JSON-RPC應用得更廣泛。
JSON-RPC參見:http://www.jsonrpc.org/specification