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









 
архив

подписка