The first time I heard about RPC was in a distributed systems class, when I was studying Computer Science. I thought it was cool, but at the time I remember not understanding exactly why I would use RPC instead of using the REST standard, for example. Time passes, and I go to work in a company where part of the legacy system was using SOAP. I remember thinking: "hmm, interesting! It looks like RPC, but passes through XML". Years later, I heard about gRPC for the first time, but I never fully understood what it was, what it ate and what it was for.
As my blog serves a lot of personal documentation, I thought it would be cool to document here what I learned about, starting with what RPC is and then moving on to gRPC.
RPC is an acronym for Remote Procedure Call. In other words, you send procedures/commands to a remote server. Simply put, this is RPC. It works as follows:
RPC works over both UDP and TCP. It's up to you to see what makes sense for your use case! If you don't mind a possible response or even losing packets, UDP. Otherwise, use TCP. For those who enjoy reading the RFCs, you can find the link here!
Both are ways of architecting APIs, however, the REST architecture has very well-defined principles that must be followed to have a RESTfull architecture. RPC even has principles, but they are defined between client and server. For the RPC client, it is as if it were calling a local procedure.
Another important point is that for RPC, it doesn't matter much whether the connection is TCP or UDP. As for REST APIs, if you want to follow RESTfull, you won't be able to use UDP.
For those who want to know more about it, I recommend this excellent AWS guide on RPC x REST.
We have two main entities, the client and the server.
The server is a WEB server, commonly used in any microservice. Let's then define the type of connection we will use, in our case, TCP was chosen:
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() // ... }
With our server instanced, we will need a handler, that is, our procedure to be executed. It is important to say that we always need to define what arguments will come from and what we will respond to in our HTTP connection. To simplify our proof of concept, we will receive an argument structure and respond to that same structure:
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 }
Having created our processor, now just make it accept connections:
func main() { // ... h := new(Handler) log.Printf("Server listening at %v", conn.Addr()) s := rpc.NewServer() s.Register(h) s.Accept(conn) }
As the client and server need to follow the same defined structure, here we will redefine the argument structure to be sent by our client:
type Args struct { Message string }
To make it easier, let's make an interactive client: it will read entries in STDIN and when it receives a new entry, it sends it to our server. For educational purposes, we will write the answer received.
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("-------------------------") } }
You can see that we need to provide the address where the server is running and which Handler (procedure) we want to execute.
An important addendum is that we are transporting binary data and by default Go will use encoding/gob. If you want to use another standard, such as JSON, you will need to tell your server to accept that new codec.
For those who want to see the complete code, just access the PoC.
gRPC is a framework for writing applications using RPC! This framework is currently maintained by the CNCF and according to the official documentation it was created by Google:
gRPC was initially created by Google, which has used a single general-purpose RPC infrastructure called Stubby to connect the large number of microservices running within and across its data centers for over a decade. In March 2015, Google decided to build the next version of Stubby and make it open source. The result was gRPC, which is now used in many organizations outside of Google to power use cases from microservices to the "last mile" of computing (mobile, web, and Internet of Things).
In addition to working on different operating systems and on different architectures, gRPC also has the following advantages:
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 중국어 웹사이트의 기타 관련 기사를 참조하세요!