使用JSONRPC 2.0規範解決多語言RPC交互的方案

動機

最近做的一個項目比較大,分了許多模塊,但是由於不同的開源技術使用的語言不同,不同模塊使用的語言可能不同,但基本上是使用Java和Python實現的。當各模塊需要進行交互的時候,問題就出現了,模塊不能像Jar包或者Python模塊那樣引入,Java有它的JVM,Python有它的解釋器,單機調用只能是用native方案。但native明顯與os有關,換個環境又不知道會有什麼兼容問題出現。

具體問題還是要具體分析,考慮的因素有很多。在該實際項目中,關於效率需要考慮的因素有:
1. 單輸入文本的處理時間
2. 網絡傳輸時間
3. 機器的配置

經過測試,單輸入文本處理時間>>網絡傳輸時間,而且沒有很高配置的機器,如果用native方法,需要跑幾個模塊,機器可能負荷不了,所以考慮構建成分佈式,以提高效率。

設計分佈式系統需要考慮機器間如何交互。最簡單的又通用的就是使用互聯網協議,如http協議,由於它是個應用層協議,所以效率肯定不會高,但爲了簡單起見,而且在網絡傳輸效率基本可以忽略不計的情況下,我們還是選用了這個應用層協議。

http協議只是作爲通信協議,但是數據傳輸的格式還是要規範的,傳統的有xml和一些序列化方案,近年流行更加輕量級和通用的json格式,它被廣泛用於web數據傳輸。本文選用的就是json格式。

於是朝着這個方向去調研,找到了json-rpc 2.0規範。

JSON-RPC 2.0簡介

JSON-RPC is a stateless, light-weight remote procedure call (RPC) protocol. Primarily this specification defines several data structures and the rules around their processing. It is transport agnostic in that the concepts can be used within the same process, over sockets, over http, or in many various message passing environments. It uses JSON (RFC 4627) as data format.

JSON-RPC是一個無狀態的、輕量級的遠程過程調用(RPC)協議。本規範主要圍繞它的處理方式定義了幾個數據結構和規則。這個概念可用於在同一進程中、套接字或HTTP之間、或其他很多消息傳遞的環境中傳輸數據。它使用JSON (RFC 4627)作爲數據格式。

好了,此處關鍵詞:JSON、HTTP。
既然是規範,應當被很多人應用,而且有詳盡文檔,就不需要自己傻乎乎地去寫詳細的交互文檔。

下面介紹一下規範中定義的對象必須有的成員:

jsonrpc
    A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0".

method
    A String containing the name of the method to be invoked. Method names that begin with the word rpc followed by a period character (U+002E or ASCII 46) are reserved for rpc-internal methods and extensions and MUST NOT be used for anything else.

params
    A Structured value that holds the parameter values to be used during the invocation of the method. This member MAY be omitted.

id
    An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is not included it is assumed to be a notification. The value SHOULD normally not be Null [1] and Numbers SHOULD NOT contain fractional parts [2] 
    The Server MUST reply with the same value in the Response object if included. This member is used to correlate the context between the two objects.

當發起rpc調用時,服務器必須回覆一個響應,通知除外。響應被表示成一個單一的對象,包含下列的成員:
jsonrpc
指定JSON-RPC版本的字符串,它必須是“2.0”。
result
當調用成功時,該成員是必須的。
如果調用方法出現錯誤時,必須不包含該成員。
該成員的值由服務器上調用的方法決定。
error
當調用發生錯誤時,該成員是必須的。
在調用期間如果沒有錯誤產生,必須不包含該成員。
該成員的值必須是一個5.1節定義的對象。
id
該成員是必須的。
它的值必須與請求對象中的id成員的值相同。
如果檢查請求對象中的id時發生錯誤(如:轉換錯誤或無效的請求),它必須爲Null。
必須包含result或error成員,但是兩個成員都必須不能同時包含。

Java Server

既然是使用http協議,那就需要一個web容器是裝載。(不裝載也可以,自己去實現一個http容器咯,或者去找開源的,其實也挺大的)

Java有一段時間沒用了,以前用Java是做web開發,用經典的Spring框架做對象管理,SpringMVC管理整個web框架,數據層框架用Hibernate或者JPA等。聽說最近有個框架很火,叫Spring Boot,它能夠快速地構建web應用,而且配置純Java化,通過一個函數即可啓動,像Python的Flask那樣的方便,實質它默認使用的底層容器還是Tomcat,簡化了我們的操作而已。

用傳統的web應用構建方式的成本跟學習Spring Boot框架的成本之間衡量了一下,選擇了後者,因爲前者再用也是沒有什麼收益,後者卻能體驗到新框架,而且現在的框架網站上的Quick Start都很容易實現。但是提高效率的方法不應是單個單個地學習,而是聯繫起來學習,所以我直接去github上找java jsonrpc的項目。於是定位到了一個叫jsonrpc4j的開源項目,Star299,肯定沒找錯了。

直接看它的Wiki,還真有Spring Boot的Quick Start。

這裏省點力,直接貼:
Server
Configuration
To get the entire system working, you need to define the AutoJsonRpcServiceImplExporter bean in your @Configuration class:

package example.jsonrpc4j.springboot;

import com.googlecode.jsonrpc4j.spring.AutoJsonRpcServiceImplExporter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ApplicationConfig {

    @Bean
    public static AutoJsonRpcServiceImplExporter autoJsonRpcServiceImplExporter() {
        AutoJsonRpcServiceImplExporter exp = new AutoJsonRpcServiceImplExporter();
        //in here you can provide custom HTTP status code providers etc. eg:
        //exp.setHttpStatusCodeProvider();
        //exp.setErrorResolver();
        return exp;
    }
}

Service
Then create your service interface. My example is a simple calculator endpoint:

package example.jsonrpc4j.springboot.api;

import com.googlecode.jsonrpc4j.JsonRpcParam;
import com.googlecode.jsonrpc4j.JsonRpcService;

@JsonRpcService("/calculator")
public interface ExampleServerAPI {
    int multiplier(@JsonRpcParam(value = "a") int a, @JsonRpcParam(value = "b") int b);
}

And implement your interface like this:

package example.jsonrpc4j.springboot.api;

import com.googlecode.jsonrpc4j.spring.AutoJsonRpcServiceImpl;
import org.springframework.stereotype.Service;

@Service
@AutoJsonRpcServiceImpl
public class ExampleServerAPIImpl implements ExampleServerAPI {
    @Override
    public int multiplier(int a, int b) {
        return a * b;
    }
}

在pom.xml加入:

<!-- jsonrpc4j -->
        <dependency>
            <groupId>com.github.briandilley.jsonrpc4j</groupId>
            <artifactId>jsonrpc4j</artifactId>
            <version>1.4.6</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>1.4.1.RELEASE</version>
        </dependency>

Spring Boot主類:

package example.jsonrpc4j.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

測試:

curl -H "Content-Type:application/json" -d '{"id":"1","jsonrpc":"2.0","method":"multiplier","params":{"a":5,"b":6}}' http://localhost:8080/calculator

{'jsonrpc': '2.0', 'id': '1', 'result': '30'}

Spring Boot整合jsonrpc4j的注意事項:
1. jsonrpc的API類應該在啓動Spring Boot的java類的下一級名爲api的包中。
2. 更改端口需要在src/main/resource文件夾中添加application.properties文件,內容如下:

management.port: 9001
management.address: 0.0.0.0
spring.data.mongodb.host=192.168.0.2

Python Server

這裏我使用Flask-jsonrpc模塊,直接pip安裝。
有點尿急,也直接貼吧:

Create your application and initialize the Flask-JSONRPC.

from flask import Flask
from flask_jsonrpc import JSONRPC

app = Flask(__name__)
jsonrpc = JSONRPC(app, '/api')

Write JSON-RPC methods.

@jsonrpc.method('App.index')
def index():
    return u'Welcome to Flask JSON-RPC'

All code of example run.py.

$ python run.py
 * Running on http://0.0.0.0:5000/

Test:

$ curl -i -X POST \
   -H "Content-Type: application/json; indent=4" \
   -d '{
    "jsonrpc": "2.0",
    "method": "App.index",
    "params": {},
    "id": "1"
}' http://localhost:5000/api
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 77
Server: Werkzeug/0.8.3 Python/2.7.3
Date: Fri, 14 Dec 2012 19:26:56 GMT

{
  "jsonrpc": "2.0",
  "id": "1",
  "result": "Welcome to Flask JSON-RPC"
}

如果是Python調用它,可以

>>> from flask_jsonrpc.proxy import ServiceProxy
>>> server = ServiceProxy('http://localhost:5000/api')
>>>
>>> server.App.index()
{'jsonrpc': '2.0', 'id': '91bce374-462f-11e2-af55-f0bf97588c3b', 'result': 'Welcome to Flask JSON-RPC'}

Flask-jsonrpc有一個優點就是它有一個api管理頁面:
Flask-jsonrpc API管理界面

交互方法

以上我們可以看到我們已經可以通過http協議,加上一些json字符串就可以實現調用了。

在實際項目中,我們只需要實現客戶端使用語言的jsonrpc 2.0規範的http調用方法即可,如上一節中的Python使用ServiceProxy對象調用。

還有一定值得注意的是,交互的對象必須要能轉爲json格式,否則需要自己寫轉json字符串的方法。

總結

總感覺這是個笨笨的方法,是因爲http協議笨重嗎?
反正我覺得比native好,少侵入代碼,又能形成分佈式,而且有個好處就是,前端應用可以直接用js調用。
其實RPC調用就是一個將模塊服務化的過程,一個模塊能夠向多個模塊提供服務,例如可以想一些公共功能服務化,就不需要重複代碼。
由於分佈式知識有限,不能進行更多方法的對比,日後如果有所學習,一定會更新。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章