JSON-RPC over HTTPなサーバを作る


JSON-RPC 2.0 over HTTPなRPCサーバをrubyで実装してみる。

RESTfullに定義できないアクションを持つAPIサーバを作る必要があってRPCサーバか、じゃJSON-RPCで作ってみるかと。どんなものかと週末手を動かしてみた。

JSON-RPC 2.0 の仕様はこちらで薄い仕様なのでさくっと読むべし。なお、JSON-RPC 2.0 にはトランスポート層の指定がないので、TCP、UDP、websocket、HTTPなど用途に応じて決めれる。今回はHTTP上で動作させることにした。

例として足し算するsum(10,20) => 30こういうRPCを作ってみる。

$ curl -v -s -S \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d  '{"jsonrpc": "2.0", "method": "sum", "params": [10, 20], "id": 1}' \
   http://localhost:8080  
> POST / HTTP/1.1
> Host: localhost:8080
> Accept: application/json
> Content-Type: application/json
> Content-Length: 64
> 
< HTTP/1.1 200 OK
< Date: Sun, 20 Mar 2016 13:39:21 GMT
< Connection: close
< Content-Length: 36
< X-Runtime: 0.004520
< 
{
  "jsonrpc": "2.0",
  "result": 30,
  "id": 1
}

方針

サーバ実装の方針を立てる

  • HTTP部分
    • Rack
    • WAF不要。つまりRails/Sinatra使わない
    • Webサーバ(Webric, unicorn, pumaなど好きなもの)
  • JSON-RPCサーバ
    • リクエスト、呼び出し関数へのディスパッチ処理、レスポンス)実装が必要
    • 自作する? しない?

HTTP層はRackに乗っかるとして、WAF(RailsやSinatraなど)が不要なのは、RPCサーバゆえエントリーポイント(パス)は1個、つまりルーティング不要、またレスポンスはjsonで返すだけでviewレンダリングがいらないので、WAFいらないやとなった。シンプルだ。

図示するとサーバのスタックとしては以下になる。

  +----------------------------+  ^
  |      RPC Handlers          |  |
  +----------------------------+  | rack app
  |  JSON-RPC implementation   |  |
  +----------------------------+  v
  |          Rack              |
  +----------------------------+
  |  Web Server(Unicorn or etc)|
  +----------------------------+
           ^   |
       req |   v resp    HTTP

          Client        

作る

JSON-RPCの仕様薄いから自作するかとか一瞬おもったけど、一旦落ち着いてRubyの実装を探したところjimsonというgemが自分が実装したいイメージと同じだったので、実装せず使わせてもらう。

jimsonには

  • JSON-RPC clientクラス
  • JSON-RPC serverクラス (中身はrack app)
  • JSON-RPC request, responseクラス
  • 各種エラークラス
  • handler用のベース

が同封されているので、RPC handler、serverクラスはjimsonがベースにすることになる。

handlerクラス

RPCの実装部分。簡単ですね。

# my_handler.rb

require 'jimson'

class MyHandler
  extend Jimson::Handler 

  def sum(a,b)
    a + b
  end
end

rackup

# config.ru
require 'jimson'
require_relative 'my_handler'

# middlewares that you want to use
use Rack::Reloader,0
use Rack::Runtime

run Jimson::Server.new(MyHandler.new)

こう書いて、$ bundle exec rackup とか $ bundle exec unicorn とかすればよろしい。

ネスト

ちなみにjimsonはfoo.hello, bar.baz.sayといったnapespaceつきのJSON-RPC methodも定義可能。

# config.ru

require 'jimson'
# require handlers 

router = Jimson::Router.new
router.draw do
  namespace 'foo', Foo   # Foo is expected to implement hello()
  namespace 'bar' do    
    namespace 'baz', Baz # Baz is expected to implement say()
  end
end

run Jimson::Server.new(router)

2016/4/13 update: なお、.区切でしかnamespace表現出来ない仕様だったのでAdd Router :ns_sep option for custom namespace #31でpull reqして任意のセパレータ使えるようお願い中。

ToDo

  • リクエストパラメタのバリデーション。外部から渡された値をハンドラにバリデーション無しで渡すのは危険なので、json schemaとか?
  • APIのversioning