使用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;
    }
}

测试:

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

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调用就是一个将模块服务化的过程,一个模块能够向多个模块提供服务,例如可以想一些公共功能服务化,就不需要重复代码。
由于分布式知识有限,不能进行更多方法的对比,日后如果有所学习,一定会更新。

推荐阅读更多精彩内容