Golang

Что такое удалённые вызовы процедур?

Удалённый вызов процедур (RPC) — это подпрограмма в распределённых вычислениях. Реализация RPC напоминает реализацию локальных вызовов, но обычно они не идентичны. Как правило, RPC предполагает передачу параметра, имени объекта, имени функции на удалённые серверы, откуда затем на сторону клиента возвращаются обработанные результаты (запрос-ответ). RPC осуществляется с использованием протоколов TCP, UDP или HTTP.

В Golang существуют три типа реализаций, а именно:

  • net/rpc
  • net/rpc/jsonrpc
  • gRPC

net/rpc

Официальная документация Golang в пакете net/rpc использует encoding/gob в качестве методов кодирования или декодирования, поддерживающих протоколы TCP или HTTP. Но gob используется только в Golang, поэтому поддерживает лишь те серверы и взаимодействия на стороне клиента, которые написаны на Golang. Вот пример net/rpc на стороне сервера:

package main
import (
   "fmt"
   "log"
   "net"
   "net/rpc"
)
type Listener int
type Reply struct {
   Data string
}
func (l *Listener) GetLine(line []byte, reply *Reply) error {
   rv := string(line)
   fmt.Printf("Receive: %v\n", rv)
   *reply = Reply{rv}
   return nil
}
func main() {
   addy, err := net.ResolveTCPAddr("tcp", "0.0.0.0:12345")
   if err != nil {
     log.Fatal(err)
   }
   inbound, err := net.ListenTCP("tcp", addy)
   if err != nil {
     log.Fatal(err)
   }
   listener := new(Listener)
   rpc.Register(listener)
   rpc.Accept(inbound)
}

Примечательно, что функция GetLine добавляется к Listener. Она вернёт тип error, ожидая строку с какими-то данными и ответ со стороны клиента. Кроме того, функция наверняка является указателем, поэтому объявлена структура Reply для хранения соответствующих данных Data.

В функции main мы используем сначала net.ResolveTCPAddr, а затем net.ListenTCP для установки TCP-соединения, слушая порт 12345 со всех адресов. И, наконец, используем rpc.Register для регистрации соединения, которое должно прослушиваться, принимая все запросы с указанных TCP-соединений.

Пример net/rpc на стороне клиента:

package main
import (
   "bufio"
   "log"
   "net/rpc"
   "os"
)
type Reply struct {
   Data string
}
func main() {
   client, err := rpc.Dial("tcp", "localhost:12345")
   if err != nil {
      log.Fatal(err)
    }
   in := bufio.NewReader(os.Stdin)
   for {
      line, _, err := in.ReadLine()
      if err != nil {
         log.Fatal(err)
      }
      var reply Reply
      err = client.Call("Listener.GetLine", line, &reply)
      if err != nil {
        log.Fatal(err)
      }
      log.Printf("Reply: %v, Data: %v", reply, reply.Data)
    }
}

На стороне клиента для установления соединения с сервером и портами будет использован rpc.Dial, а это бесконечный цикл for с функцией ReadLine, которая принимает данные от портов получения. В случае возникновения обрывов на линии активируется client.Call и запускается функция GetLine. Благодаря этому процессу, reply будет сохраняться в базе данных, а мы сможем вызвать его с помощью reply.Data (по сути, это означает: что мы вводим, то и получаем на выходе). Попробуем запустить этот код:

❯ go run simple_server.go
Receive: hi
Receive: haha
❯ go run simple_client.go
hi
2019/12/05 18:19:14 Reply: {hi}, Data: hi
haha
2019/12/05 18:19:15 Reply: {haha}, Data: haha

net/rpc/jsonrpc

net/rpc поддерживает только Golang, поэтому библиотека Go использует net/rpc/jsonrpc для поддержки RPC в платформах на любом языке программирования. Для реализации аналогичного приведённому выше приложения надо лишь поменять rpc.Accept в функции main().

Вот пример net/rpc/jsonrpc на стороне сервера:

import "net/rpc/jsonrpc"
func main() {
   addy, err := net.ResolveTCPAddr("tcp", "0.0.0.0:12345")
   if err != nil {
     log.Fatal(err)
   }
   inbound, err := net.ListenTCP("tcp", addy)
   if err != nil {
     log.Fatal(err)
   }
   listener := new(Listener)
   rpc.Register(listener)
   for {
     conn, err := inbound.Accept()
     if err != nil {
       continue
     }
   jsonrpc.ServeConn(conn)
   }
}

Теперь пример net/rpc/jsonrpc на стороне клиента:

func main() {
   client, err := jsonrpc.Dial("tcp", "localhost:12345") //Меняется только эта строчка
   if err != nil {
     log.Fatal(err)
   }
   in := bufio.NewReader(os.Stdin)
   for {
     line, _, err := in.ReadLine()
     if err != nil {
       log.Fatal(err)
     }
   var reply Reply
   err = client.Call("Listener.GetLine", line, &reply)
     if err != nil {
       log.Fatal(err)
     }
   log.Printf("Reply: %v, Data: %v", reply, reply.Data)
   }
}

json-rpc основан на протоколе TCP и на настоящий момент не поддерживает метод HTTP. Результаты будут те же, что и в предыдущем примере:

❯ go run simple_server.go
Receive: hi
Receive: haha

❯ go run simple_client.go
hi
2019/12/05 20:20:19 Reply: {hi}, Data: hi
haha
2019/12/05 20:20:20 Reply: {haha}, Data: haha

Объект JSON в запросе имеет две похожие структуры: clientRequest и serverRequest.

type serverRequest struct {
   Method string           `json:"method"`
   Params *json.RawMessage `json:"params"`
   Id     *json.RawMessage `json:"id"`
}
type clientRequest struct {
   Method string         `json:"method"`
   Params [1]interface{} `json:"params"`
   Id     uint64         `json:"id"`
}

Мы могли бы использовать такую структуру для отправки сообщения и в других языках программирования. Попробуем в командной строке:

❯ echo -n "hihi" |base64  # параметры должны кодироваться в base64 encoded
aGloaQ==
~/strconv.code/rpc master*
❯ echo -e '{"method": "Listener.GetLine","params": ["aGloaQ=="], "id": 0}' | nc localhost 12345
{"id":0,"result":{"Data":"hihi"},"error":null}

gRPC

Тот факт, что jsonRPC может поддерживать другие языки, но не поддерживает метод HTTP, ограничивает его применение. Поэтому в эксплуатационной среде обычно используютThrift или gRPC.

gRPC — это популярная, высокопроизводительная и общедоступная RPC-платформа, созданная в Google. Она предназначена главным образом для параллельного выполнения задач современных приложений на основе стандартного протокола HTTP/2 и поддерживает такие языки, как Python, Golang и Java. Разработана в протоколе сериализации Protobuf.

Protobuf

Protobuf (сокращение от Protocol Buffers) — это всеязычный, платформенно-независимый механизм для сериализации структурированных данных в формате, похожем на XML и JSON. Легковесный и быстрый, он очень подходит для хранения данных или обмена ими в сети RPC. Первым делом устанавливаем Protobuf:

❯ brew install protobuf
❯ protoc --version
libprotoc 3.7.1
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}

Теперь пишем образец данных на основе proto3:

syntax = "proto3";
package simple;
// Запрос
message SimpleRequest {
string data = 1;
}
// Ответ
message SimpleResponse {
string data = 1;
}
// метод rpc
service Simple {
  rpc  GetLine (SimpleRequest) returns (SimpleResponse);
}

Запрос и ответ имеют здесь лишь по одной строчке с данными data. Сервис Simple имеет только один метод GetLine с входным SimpleRequest и возвращает SimpleResponse.

❯ mkdir src/simple
❯ protoc --go_out=plugins=grpc:src/simple simple.proto
❯ ll src/simple
total 8.0K
-rw-r--r-- 1 xiaoxi staff 7.0K Dec 05 21:43 simple.pb.go

Таким вот образом создаём файл simple.pb.go в каталоге src/simple для поддержки gRPC.

Как использовать gRPC

Сначала устанавливаем gRPC:

❯ go get -u google.golang.org/grpc

Затем импортируем src/simple в код:

package main
import (
   "fmt"
   "log"
   "net"
   pb "./src/simple"
   "golang.org/x/net/context"
   "google.golang.org/grpc"
)
type Listener int
func (l *Listener) GetLine(ctx context.Context, in *pb.SimpleRequest) (*pb.SimpleResponse, error) {
   rv := in.Data
   fmt.Printf("Receive: %v\n", rv)
   return &pb.SimpleResponse{Data: rv}, nil
}
func main() {
   addy, err := net.ResolveTCPAddr("tcp", "0.0.0.0:12345")
   if err != nil {
     log.Fatal(err)
   }
   inbound, err := net.ListenTCP("tcp", addy)
   if err != nil {
     log.Fatal(err)
   }
   s := grpc.NewServer()
   listener := new(Listener)
   pb.RegisterSimpleServer(s, listener)
   s.Serve(inbound)
}

Здесь pb "./src/simple" импортируется пакетом. Заметим, что в названии добавляется pb.

Первым параметром функции GetLine идёт context.Context. Второй параметр — *pb.Simple-Request (запрос определён в файле .proto). Эта функция вернёт (*pb.SimpleResponse, error), где pb.SimpleResponse соответствует определению в файле .proto. С другой стороны, SimpleRequest и SimpleResponse должны обозначаться большими буквами, несмотря на то, что в файле .proto они в верблюжьем регистре.

На стороне клиента:

package main
import (
   "bufio"
   "log"
   "os"
   pb "./src/simple"
   "golang.org/x/net/context"
   "google.golang.org/grpc"
)
func main() {
   conn, err := grpc.Dial("localhost:12345", grpc.WithInsecure())
     if err != nil {
       log.Fatal(err)
     }
   c := pb.NewSimpleClient(conn)
   in := bufio.NewReader(os.Stdin)
   for {
     line, _, err := in.ReadLine()
       if err != nil {
          log.Fatal(err)
       }
     reply, err := c.GetLine(context.Background(), &pb.SimpleRequest{Data: string(line)})
     if err != nil {
        log.Fatal(err)
     }
     log.Printf("Reply: %v, Data: %v", reply, reply.Data)
   }
}

Сначала устанавливаем соединение с помощью grpc.Dial("localhost:12345", rpc.WithInsecure()). Затем используем pb.NewSimpleClient для создания экземпляра simpleClient в формате XXXClient. (XXX определено раннее в файле .proto для значения simple в service Simple).

Для использования RPC вводится следующая команда:

reply, err := c.GetLine(context.Background(), &pb.SimpleRequest{Data: string(line)})

GetLine определяется в файле .proto ( rpc GetLine(SimpleRequest) возвращает (SimpleResponse)). Первый параметр — context.Background(). Вторым параметром идёт request. Так как строка имеет тип []byte, её надо перевести в string. Ответ reply представляет собой экземпляр SimpleReponse, который можно получить из reply.Data:

❯ go run grpc_server.go
Receive: hi
Receive: Haha
Receive: vvv
❯ go run grpc_client.go
hi
2019/12/06 07:57:48 Reply: data:"hi" , Data: hi
Haha
2019/12/06 07:57:51 Reply: data:"Haha" , Data: Haha
vvv
2019/12/06 07:57:53 Reply: data:"vvv" , Data: vvv

Заключение

Надеюсь, что после прочтения этой статьи у вас отложилось в голове какое-то представление о RPC (удалённом вызове процедур) и трёх типах реализации в Golang. В помощь будут и примеры кода для net/rpc, net/jsonrpc и grpc. Остаётся только написать собственный код!

Читайте также:


Перевод статьи: Kingsley Tan: What Are RPCs in Golang?

Предыдущая статьяХватит везде использовать ===
Следующая статьяПочему мы не используем лучшие практики CI/CD