Go: JSON-RPC сервер
JSON-RPC (JavaScript Object Notation Remote Procedure Call — JSON-вызов удалённых процедур) — протокол удалённого вызова процедур, использующий JSON для кодирования сообщений. Это очень простой протокол (очень похожий на XML-RPC), определяющий только несколько типов данных и команд. JSON-RPC поддерживает уведомления (информация, отправляемая на сервер, не требует ответа) и множественные вызовы. Мы будем использовать версию 2.0 протокола.
Ключевые особенности JSON-RPC 2.0:
- Наличие стандарта.
- Богатый выбор библиотек для различных платформ и языков.
- Легкий парсинг, работающий быстрее чем парсинг в XML-RPC или SOAP.
- Определены правила обработки ошибок
- Поддержка очереди вызовов (batch requests). Вместо N запросов достаточно отправить один
- Единая точка для API. Например, /rpc сможет обрабатывать различные методы.
- Поддержка именованных и опциональных параметров при вызове методов.
- В качестве транспорта используется HTTP или TCP Socket
Примеры простых запросов ниже:
--> {"jsonrpc": "2.0", "method": "math.sub", "params": {"one": 42, "two": 23}, "id": 3}
<-- {"jsonrpc": "2.0", "result": 19, "id": 3}
или такой:
--> {"jsonrpc": "2.0", "method": "math.sub", "params": [42, 23], "id": 1}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}
Также в версии 2.0 можно объединить несколько вызовов в один запрос:
--> [
{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
{"jsonrpc": "2.0", "method": "foobar", "id": "2"}
]
<-- [
{"jsonrpc": "2.0", "result": 7, "id": "1"},
{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found."}, "id": "2"}
]
В параметре jsonrpc указывается версия протокола (2.0), параметр method определяет вызываемый метод, id используется для связки запроса и ответа, params - параметры запроса.
Попробуем на практике поработать с JSON-RPC 2.0. Для напишем простенький сервер на Go, который обрабатываем метод Test.Hello, в котором ему передается хэш, содержащий Name, в ответ на что сервер генерирует строку Hello Name.
Во-первых нужно определить тип объекта, который будет обрабатывать запросы. Делаем это так:
type Test struct{}
type HelloArgs struct {
Name string
}
func (test *Test) Hello(args *HelloArgs, result *string) error {
*result = "Hello " + args.Name
return nil
}
В Go на тип объекта обработчика RPC вызовов накладываются следующие ограничения:
- Тип объекта должен быть экспортируемым
- Метод объекта должен быть экспортируемым
- Метод должен принимать два параметра оба встроенных типов или экспортируемых
- Второй аргумент метода должен быть указателем
- Тип возвращаемого значения должен быть error
Фактически метод должен выглядеть примерно следующим образом:
func (t *T) MethodName(argType T1, replyType *T2) error
Далее создаем RPC-сервер и регистрируем обработчик нашего метода, используя пакет net/rpc:
server := rpc.NewServer()
server.Register(&Test{})
В качестве транспорт будем использовать HTTP, для этого определим объект HTTP соединения:
type HttpConn struct {
in io.Reader
out io.Writer
}
func (c *HttpConn) Read(p []byte) (n int, err error) {
return c.in.Read(p)
}
func (c *HttpConn) Write(d []byte) (n int, err error) {
return c.out.Write(d)
}
func (c *HttpConn) Close() error {
return nil
}
В качестве серверного кодека воспользуемся "github.com/powerman/rpc-codec/jsonrpc2", сервер будет слушать 8080 порту, а точкой входа будет /rpc
listener, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
defer listener.Close()
http.Serve(listener, http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/rpc" {
serverCodec := jsonrpc2.NewServerCodec(&HttpConn{in: r.Body, out: w}, server )
w.Header().Set( "Content-type", "application/json" )
w.WriteHeader(200)
if err1 := server.ServeRequest(serverCodec) ; err1 != nil {
http.Error(w, "Error while serving JSON request", 500)
return
}
} else {
http.Error(w, "Unknown request", 404)
}
} ) )
После этого сервер готов и можно слать запрос
curl -H "Content-Type: application/json" -X POST -d \
'{"jsonrpc": "2.0", "method": "Test.Hello", "params:{"Name":"Mike"}, "id": "1"}' \
http://127.0.0.1:8080/rpc
В результате чего будет получен ответ:
{"jsonrpc": "2.0", "result": "Hello Mike", "id": 1}
Полный код программы ниже:
package main
import (
"io"
"net"
"net/http"
"net/rpc"
"github.com/powerman/rpc-codec/jsonrpc2"
)
type HttpConn struct {
in io.Reader
out io.Writer
}
func (c *HttpConn) Read(p []byte) (n int, err error) {
return c.in.Read(p)
}
func (c *HttpConn) Write(d []byte) (n int, err error) {
return c.out.Write(d)
}
func (c *HttpConn) Close() error {
return nil
}
type Test struct{}
type HelloArgs struct {
Name string
}
func (test *Test) Hello(args *HelloArgs, result *string) error {
*result = "Hello " + args.Name
return nil
}
func main() {
server := rpc.NewServer()
server.Register(&Test{})
listener, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
defer listener.Close()
http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/rpc" {
serverCodec := jsonrpc2.NewServerCodec(&HttpConn{in: r.Body, out: w}, server )
w.Header().Set( "Content-type", "application/json" )
w.WriteHeader(200)
if err1 := server.ServeRequest(serverCodec) ; err1 != nil {
http.Error(w, "Error while serving JSON request", 500)
return
}
} else {
http.Error(w, "Unknown request", 404)
}
} ) )
}
В случае, если мы хотипив в качестве транспорта использовать не HTTP, а TCP то код немного упростится:
package main
import (
"context"
"net"
"net/rpc"
"github.com/powerman/rpc-codec/jsonrpc2"
)
type Test struct{}
type TestArgs struct {
Name string
}
func (test *Test) Hello(args *TestArgs, result *string) error {
*result = "Hello " + args.Name
return nil
}
var RemoteAddrContextKey = "RemoteAddr"
func main() {
rpc.Register(&Test{})
lnTCP, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
panic(err)
}
defer lnTCP.Close()
for {
conn, err := lnTCP.Accept()
if err != nil {
return
}
ctx := context.WithValue(context.Background(), RemoteAddrContextKey, conn.RemoteAddr())
go jsonrpc2.ServeConnContext(ctx, conn)
}
}
15.06.2017