Удалённый вызов процедур (RPC) — это подпрограмма в распределённых вычислениях. Реализация RPC напоминает реализацию локальных вызовов, но обычно они не идентичны. Как правило, RPC предполагает передачу параметра, имени объекта, имени функции на удалённые серверы, откуда затем на сторону клиента возвращаются обработанные результаты (запрос-ответ). RPC осуществляется с использованием протоколов TCP, UDP или HTTP.
В Golang существуют три типа реализаций, а именно:
net/rpc
net/rpc/jsonrpc
gRPC
Официальная документация 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
поддерживает только 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}
Тот факт, что jsonRPC
может поддерживать другие языки, но не поддерживает метод HTTP, ограничивает его применение. Поэтому в эксплуатационной среде обычно используютThrift
или gRPC
.
gRPC — это популярная, высокопроизводительная и общедоступная RPC-платформа, созданная в Google. Она предназначена главным образом для параллельного выполнения задач современных приложений на основе стандартного протокола HTTP/2 и поддерживает такие языки, как Python, Golang и Java. Разработана в протоколе сериализации 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:
❯ 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
. Остаётся только написать собственный код!
Перевод статьи
Комментарии