RPC について初めて聞いたのは、コンピューター サイエンスを勉強していたときの分散システムのクラスでした。これはクールだと思いましたが、当時は、たとえば REST 標準を使用する代わりに RPC を使用する理由を正確に理解できなかったことを覚えています。時が経ち、私はレガシー システムの一部で SOAP を使用していた会社で働くことになりました。 「うーん、興味深い! RPC のように見えますが、XML を通過します。」と思ったのを覚えています。数年後、私は gRPC について初めて聞きましたが、それが何なのか、何を食べるのか、何のためにあるのか完全には理解できませんでした。
私のブログは個人的な文書を多く提供しているため、RPC とは何かから始めて gRPC に進むまで、私が学んだことをここに文書化するのは素晴らしいことだと思いました。
RPC は、Remote Procedure Call の頭字語です。言い換えれば、プロシージャ/コマンドをリモート サーバーに送信します。簡単に言えば、これは RPC です。次のように動作します:
RPC は UDP と TCP の両方で動作します。自分のユースケースにとって何が合理的なのかを判断するのはあなた次第です。応答の可能性やパケットの損失さえ気にしない場合は、UDP を使用します。それ以外の場合は、TCP を使用します。 RFC を読みたい方は、ここにリンクがあります!
どちらも API を設計する方法ですが、REST アーキテクチャには、RESTfull アーキテクチャを実現するために従う必要がある非常に明確に定義された原則があります。 RPC にも原則がありますが、それらはクライアントとサーバーの間で定義されます。 RPC クライアントの場合、ローカル プロシージャを呼び出しているかのように見えます。
もう 1 つの重要な点は、RPC の場合、接続が TCP であるか UDP であるかはあまり重要ではないということです。 REST API に関しては、RESTfull に従いたい場合は、UDP を使用できません。
さらに詳しく知りたい方には、RPC x REST に関するこの優れた AWS ガイドをお勧めします。
クライアントとサーバーという 2 つの主要なエンティティがあります。
サーバーは WEB サーバーであり、あらゆるマイクロサービスで一般的に使用されます。次に、使用する接続のタイプを定義しましょう。この場合、TCP が選択されました。
func main() { addr, err := net.ResolveTCPAddr("tcp", "0.0.0.0:52648") if err != nil { log.Fatal(err) } conn, err := net.ListenTCP("tcp", addr) if err != nil { log.Fatal(err) } defer conn.Close() // ... }
サーバーをインスタンス化したら、ハンドラー、つまり実行するプロシージャが必要になります。 HTTP 接続でどのような引数が取得され、何に応答するかを常に定義する必要があるということが重要です。概念実証を簡素化するために、引数構造を受け取り、その同じ構造に応答します。
type Args struct { Message string } type Handler int func (h *Handler) Ping(args *Args, reply *Args) error { fmt.Println("Received message: ", args.Message) switch args.Message { case "ping", "Ping", "PING": reply.Message = "pong" default: reply.Message = "I don't understand" } fmt.Println("Sending message: ", reply.Message) return nil }
プロセッサを作成したら、次は接続を受け入れるようにするだけです。
func main() { // ... h := new(Handler) log.Printf("Server listening at %v", conn.Addr()) s := rpc.NewServer() s.Register(h) s.Accept(conn) }
クライアントとサーバーは同じ定義された構造に従う必要があるため、ここでクライアントによって送信される引数の構造を再定義しましょう:
type Args struct { Message string }
作業を簡単にするために、対話型クライアントを作成しましょう。クライアントは STDIN のエントリを読み取り、新しいエントリを受信すると、それをサーバーに送信します。教育目的のため、受け取った回答を書き込みます。
func main() { client, err := rpc.Dial("tcp", "localhost:52648") if err != nil { log.Fatal(err) } for { log.Println("Please, inform the message:") scanner := bufio.NewScanner(os.Stdin) scanner.Scan() args := Args{Message: scanner.Text()} log.Println("Sent message:", args.Message) reply := &Args{} err = client.Call("Handler.Ping", args, reply) if err != nil { log.Fatal(err) } log.Println("Received message:", reply.Message) log.Println("-------------------------") } }
サーバーが実行されているアドレスと、どのハンドラー (プロシージャ) を実行するかを指定する必要があることがわかります。
重要な補足は、バイナリ データを転送しており、デフォルトで Go はエンコーディング/gob を使用することです。 JSON などの別の標準を使用する場合は、その新しいコーデックを受け入れるようにサーバーに指示する必要があります。
完全なコードを確認したい場合は、PoC にアクセスしてください。
gRPC は、RPC を使用してアプリケーションを作成するためのフレームワークです。このフレームワークは現在 CNCF によって維持されており、公式ドキュメントによると、Google によって作成されました。
gRPC は当初 Google によって作成されました。Google は 10 年以上にわたり、Stubby と呼ばれる単一の汎用 RPC インフラストラクチャを使用して、自社のデータセンター内およびデータセンター全体で実行されている多数のマイクロサービスを接続してきました。 2015 年 3 月、Google は Stubby の次のバージョンを構築し、オープンソースにすることを決定しました。その結果生まれたのが gRPC であり、現在では Google 以外の多くの組織で、マイクロサービスからコンピューティングの「ラスト ワンマイル」 (モバイル、ウェブ、モノのインターネット) に至るユースケースを強化するために使用されています。
gRPC は、さまざまなオペレーティング システムやアーキテクチャで動作することに加えて、次の利点もあります。
Para nossa sorte, Go é uma das 11 linguagens que tem bibliotecas oficiais para o gRPC! É importante falar que esse framework usa o Protocol Buffer para serializar a mensagem. O primeiro passo então é instalar o protobuf de forma local e os plugins para Go:
brew install protobuf go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
E adicionar os plugins ao seu PATH:
export PATH="$PATH:$(go env GOPATH)/bin"
Vamos então criar nossos arquivos .proto! Nesse arquivo vamos definir nosso serviço, quais os handlers que ele possui e para cada handler, qual a requisição e qual resposta esperadas.
syntax = "proto3"; option go_package = "github.com/mfbmina/poc_grpc/proto"; package ping_pong; service PingPong { rpc Ping (PingRequest) returns (PingResponse) {} } message PingRequest { string message = 1; } message PingResponse { string message = 1; }
Com o arquivo .proto, vamos fazer a mágica do gRPC + protobuf acontecer. Os plugins instalados acima, conseguem gerar tudo o que for necessário para um servidor ou cliente gRPC com o seguinte comando:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative proto/ping_pong.proto
Esse comando vai gerar dois arquivos: ping_pong.pb.go e ping_pong_grpc.pb.go. Recomendo dar uma olhada nesses arquivos para entender melhor a estrutura do servidor e do cliente. Com isso, podemos então construir o servidor:
Para conseguir comparar com o RPC comum, vamos utilizar a mesma lógica: recebemos PING e respondemos PONG. Aqui definimos um servidor e um handler para a requisição e usamos as definições vindas do protobuf para a requisição e resposta. Depois, é só iniciar o servidor:
type server struct { pb.UnimplementedPingPongServer } func (s *server) Ping(_ context.Context, in *pb.PingRequest) (*pb.PingResponse, error) { r := &pb.PingResponse{} m := in.GetMessage() log.Println("Received message:", m) switch m { case "ping", "Ping", "PING": r.Message = "pong" default: r.Message = "I don't understand" } log.Println("Sending message:", r.Message) return r, nil } func main() { l, err := net.Listen("tcp", ":50051") if err != nil { log.Fatal(err) } s := grpc.NewServer() pb.RegisterPingPongServer(s, &server{}) log.Printf("Server listening at %v", l.Addr()) err = s.Serve(l) if err != nil { log.Fatal(err) } }
Para consumir o nosso servidor, precisamos de um cliente. o cliente é bem simples também. A biblioteca do gRPC já implementa basicamente tudo que precisamos, então inicializamos um client e só chamamos o método RPC que queremos usar, no caso o Ping. Tudo vem importado do código gerado via plugins do protobuf.
func main() { conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatal(err) } defer conn.Close() c := pb.NewPingPongClient(conn) for { log.Println("Enter text: ") scanner := bufio.NewScanner(os.Stdin) scanner.Scan() msg := scanner.Text() log.Printf("Sending message: %s", msg) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() r, err := c.Ping(ctx, &pb.PingRequest{Message: msg}) if err != nil { log.Fatal(err) } log.Printf("Received message: %s", r.GetMessage()) log.Println("-------------------------") } }
Quem tiver interesse para ver o código completo, pode acessar a PoC gRPC.
O gRPC não é nada mais que uma abstração em cima do RPC convencional utilizando o protobuf como serializador e o protocolo http/2. Existem algumas considerações de performance ao se utilizar o http/2 e em alguns cenários, como em requisições com o corpo simples, o http/1 se mostra mais performático que o http/2. Recomendo a leitura deste benchmark e desta issue aberta no golang/go sobre o http/2. Contudo, em requisições de corpo complexo, como grande parte das que resolvemos dia a dia, gRPC se torna uma solução extremamente atraente devido ao serializador do protobuf, que é extremamente mais rápido que JSON. O Elton Minetto fez um blog post explicando melhor essas alternativas e realizando um benchmark. Um consideração também é o protobuf consegue resolver o problema de inconsistência de contratos entre servidor e cliente, contudo é necessário uma maneira fácil de distribuir os arquivos .proto.
Por fim, minha recomendação é use gRPC se sua equipe tiver a necessidade e a maturidade necessária para tal. Hoje, grande parte das aplicações web não necessitam da performance que gRPC visa propor e nem todos já trabalharam com essa tecnologia, o que pode causar uma menor velocidade e qualidade nas entregas. Como nessa postagem eu citei muitos links, decidi listar todas as referências abaixo:
Espero que vocês tenham gostado do tema e obrigado!
以上がgRPC: どこに住んでいますか?何を食べますか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。