」工欲善其事,必先利其器。「—孔子《論語.錄靈公》
首頁 > 程式設計 > gRPC:你住在哪裡?你吃什麼?

gRPC:你住在哪裡?你吃什麼?

發佈於2024-11-08
瀏覽:502

A primeira vez que ouvi falar sobre RPC foi em uma aula de sistema distribuídos, ainda quando estava cursando a graduação em Ciência da Computação. Achei legal, mas na época lembro de não compreender exatamente o porque eu usaria RPC ao invés de usar o padrão REST, por exemplo. Passa o tempo, e vou trabalhar em uma empresa em que parte do sistema legado era utilizando SOAP. Lembro de pensar: "hmm, interessante! Parece com RPC, mas traféga XML". Anos depois, ouço pela primeira vez falar sobre gRPC, mas nunca entendi complementamente o que era, o que comia e pra que servia.

Como meu blog serve muito de documentação pessoal, achei legal documentar aqui o que aprendi sobre, começando sobre o que é RPC e depois indo para o gRPC.

Vamos lá, o que é RPC?

RPC é uma sigla para Remote Procedure Call (em Português Chamada de Procedimento Remoto). Ou seja, você envia procedimentos/comandos para um servidor remoto. Sendo simples e direto, isso é RPC. Ele funciona da seguinte forma:

gRPC: onde vive? o que come?

O RPC funciona tanto sobre UDP, quanto TCP. Cabe a você ver o que faz sentido para seu caso de uso! Se você não se importa com uma eventual resposta ou até mesmo em perder pacotes, UDP. Caso contrário, use TCP. Para aqueles que gostam de ler as RFCs, pode encontrar o link aqui!

OK, mas como o RPC se difere de uma chamada REST, por exemplo?

Ambos são maneiras de arquiteturar APIs, porém, a arquitetura REST possuí principíos muito bem definidos e que devem ser seguidos para se ter uma arquitetura RESTfull. O RPC até possui principios, mas eles são definidos entre cliente e servidor. Para o cliente RPC, é como se ele tivesse chamando um procedimento local.

Outro ponto importante é que para o RPC, não importa muito se a conexão é TCP ou UDP. Já para APIs REST, se você quiser seguir o RESTfull, não vai conseguir utilizar UDP.

Para quem quiser saber mais sobre, recomendo este excelente guia da AWS sobre RPC x REST.

E como implementar um servidor RPC com Go?

Temos duas entidades principais, o cliente e o servidor.

Começando pelo servidor...

O servidor é um servidor WEB, comumente usado em qualquer microsserviço. Vamos definir então o tipo de conexão que vamos utilizar, para nosso caso, TCP foi o escolhido:

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()

  // ...
}

Com o nosso servidor instânciado, vamos precisar de um handler, ou seja, nosso procedimento a ser executado. É importante dizer que precisamos definir sempre o que vai vir de argumentos e o que vamos responder na nossa conexão HTTP. Para simplificar nossa prova de conceito, vamos receber uma estrutura de argumentos e responder essa mesma estrutura:

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
}

Tendo o nosso processador criado, agora é só fazer ele aceitar as conexões:

func main() {
  // ...

  h := new(Handler)
  log.Printf("Server listening at %v", conn.Addr())
  s := rpc.NewServer()
  s.Register(h)
  s.Accept(conn)
}

Definindo o cliente...

Como o cliente e o servidor precisam seguir a mesma estrutura definida, vamos redefinir aqui a estrutura de argumentos a ser enviada pelo nosso cliente:

type Args struct {
  Message string
}

Para facilitar, vamos fazer um cliente interativo: ele vai ficar lendo entradas no STDIN e ao receber uma nova entrada, ele envia para o nosso servidor. Para fins didáticos, vamos escrever a resposta recebida.

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("-------------------------")
  }
}

Pode-se notar que precisamos fornecer o endereço de onde o servidor está rodando e qual o Handler (procedimento) que queremos executar.

Um adendo importante é que estamos trafegando dados binários e por padrão o Go vai utilizar o encoding/gob. Se quiser utilizar um outro padrão, como por exemplo JSON, vai ser preciso dizer ao seu servidor que aceite aquele codec novo.

Para quem quiser ver o código completo, é só acessar a PoC.

E o que é gRPC?

gRPC é um framework para se escrever aplicações usando RPC! Esse framework hoje é mantido pela CNCF e segundo a documentação oficial foi criado pela 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).

Além de funcionar em diversos sistemas operacionais e em diversas arquiteturas, o gRPC ainda possui as seguintes vantagens:

  • Bibliotecas idiomáticas em 11 linguagens;
  • Framework simples para definição do seu serviço e extremamente performático.
  • Fluxo bi-direcional de dados utilizando http/2 para transporte;
  • Funcionalidades extensíveis como autenticação, tracing, balanceador de carga e verificador de saúde.

E como utilizar o gRPC com Go?

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"

A mágica do protobuf...

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:

Construindo 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)
  }
}

E o cliente...

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.

Considerações finais

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:

  • RPC
  • RPC RFC
  • RPC x REST
  • PoC RPC
  • net/rpc
  • encoding/gob
  • CNCF - Cloud Native Computing Foundation
  • gRPC
  • Protocol Buffer
  • PoC gRPC
  • http/1 x http/2 x gRPC
  • http/2 issue
  • JSON x Protobuffers X Flatbuffers

Espero que vocês tenham gostado do tema e obrigado!

版本聲明 本文轉載於:https://dev.to/mfbmina/grpc-onde-vive-o-que-come-5049?1如有侵犯,請聯絡[email protected]刪除
最新教學 更多>
  • Java中如何使用觀察者模式實現自定義事件?
    Java中如何使用觀察者模式實現自定義事件?
    在Java 中創建自定義事件的自定義事件在許多編程場景中都是無關緊要的,使組件能夠基於特定的觸發器相互通信。本文旨在解決以下內容:問題語句我們如何在Java中實現自定義事件以促進基於特定事件的對象之間的交互,定義了管理訂閱者的類界面。 以下代碼片段演示瞭如何使用觀察者模式創建自定義事件: args...
    程式設計 發佈於2025-05-05
  • Java是否允許多種返回類型:仔細研究通用方法?
    Java是否允許多種返回類型:仔細研究通用方法?
    在Java中的多個返回類型:一種誤解類型:在Java編程中揭示,在Java編程中,Peculiar方法簽名可能會出現,可能會出現,使開發人員陷入困境,使開發人員陷入困境。 getResult(string s); ,其中foo是自定義類。該方法聲明似乎擁有兩種返回類型:列表和E。但這確實是如此嗎...
    程式設計 發佈於2025-05-05
  • 為什麼我會收到MySQL錯誤#1089:錯誤的前綴密鑰?
    為什麼我會收到MySQL錯誤#1089:錯誤的前綴密鑰?
    mySQL錯誤#1089:錯誤的前綴鍵錯誤descript [#1089-不正確的前綴鍵在嘗試在表中創建一個prefix鍵時會出現。前綴鍵旨在索引字符串列的特定前綴長度長度,以便更快地搜索這些前綴。 理解prefix keys `這將在整個Movie_ID列上創建標準主鍵。主密鑰對於唯一識...
    程式設計 發佈於2025-05-05
  • 如何有效地選擇熊貓數據框中的列?
    如何有效地選擇熊貓數據框中的列?
    在處理數據操作任務時,在Pandas DataFrames 中選擇列時,選擇特定列的必要條件是必要的。在Pandas中,選擇列的各種選項。 選項1:使用列名 如果已知列索引,請使用ILOC函數選擇它們。請注意,python索引基於零。 df1 = df.iloc [:,0:2]#使用索引0和1 ...
    程式設計 發佈於2025-05-05
  • 如何在Chrome中居中選擇框文本?
    如何在Chrome中居中選擇框文本?
    選擇框的文本對齊:局部chrome-inly-ly-ly-lyly solument 您可能希望將文本中心集中在選擇框中,以獲取優化的原因或提高可訪問性。但是,在CSS中的選擇元素中手動添加一個文本 - 對屬性可能無法正常工作。 初始嘗試 state)</option> < o...
    程式設計 發佈於2025-05-05
  • 為什麼不使用CSS`content'屬性顯示圖像?
    為什麼不使用CSS`content'屬性顯示圖像?
    在Firefox extemers屬性為某些圖像很大,&& && && &&華倍華倍[華氏華倍華氏度]很少見,卻是某些瀏覽屬性很少,尤其是特定於Firefox的某些瀏覽器未能在使用內容屬性引用時未能顯示圖像的情況。這可以在提供的CSS類中看到:。 googlepic { 內容:url(&...
    程式設計 發佈於2025-05-05
  • 在JavaScript中如何獲取實際渲染的字體,當CSS字體屬性未定義時?
    在JavaScript中如何獲取實際渲染的字體,當CSS字體屬性未定義時?
    Accessing Actual Rendered Font when Undefined in CSSWhen accessing the font properties of an element, the JavaScript object.style.fontFamily and objec...
    程式設計 發佈於2025-05-05
  • 為什麼Microsoft Visual C ++無法正確實現兩台模板的實例?
    為什麼Microsoft Visual C ++無法正確實現兩台模板的實例?
    在Microsoft Visual C 中,Microsoft consions用戶strate strate strate strate strate strate strate strate strate strate strate strate strate strate strate st...
    程式設計 發佈於2025-05-05
  • Java數組中元素位置查找技巧
    Java數組中元素位置查找技巧
    在Java數組中檢索元素的位置 利用Java的反射API將數組轉換為列表中,允許您使用indexof方法。 (primitives)(鏈接到Mishax的解決方案) 用於排序陣列的數組此方法此方法返回元素的索引,如果發現了元素的索引,或一個負值,指示應放置元素的插入點。
    程式設計 發佈於2025-05-05
  • 如何在其容器中為DIV創建平滑的左右CSS動畫?
    如何在其容器中為DIV創建平滑的左右CSS動畫?
    通用CSS動畫,用於左右運動 ,我們將探索創建一個通用的CSS動畫,以向左和右移動DIV,從而到達其容器的邊緣。該動畫可以應用於具有絕對定位的任何div,無論其未知長度如何。 問題:使用左直接導致瞬時消失 更加流暢的解決方案:混合轉換和左 [並實現平穩的,線性的運動,我們介紹了線性的轉換。...
    程式設計 發佈於2025-05-05
  • 在Oracle SQL中如何提取下劃線前的子字符串?
    在Oracle SQL中如何提取下劃線前的子字符串?
    [ 在oracle sql 解決方案: Explanation:SUBSTR function extracts a substring starting from the specified position (0) and continuing for a specified length.IN...
    程式設計 發佈於2025-05-05
  • Python中何時用"try"而非"if"檢測變量值?
    Python中何時用"try"而非"if"檢測變量值?
    使用“ try“ vs.” if”來測試python 在python中的變量值,在某些情況下,您可能需要在處理之前檢查變量是否具有值。在使用“如果”或“ try”構建體之間決定。 “ if” constructs result = function() 如果結果: 對於結果: ...
    程式設計 發佈於2025-05-05
  • 為什麼儘管有效代碼,為什麼在PHP中捕獲輸入?
    為什麼儘管有效代碼,為什麼在PHP中捕獲輸入?
    在php ;?>" method="post">The intention is to capture the input from the text box and display it when the submit button is clicked.但是,輸出...
    程式設計 發佈於2025-05-05
  • 如何使用Depimal.parse()中的指數表示法中的數字?
    如何使用Depimal.parse()中的指數表示法中的數字?
    在嘗試使用Decimal.parse(“ 1.2345e-02”中的指數符號表示法表示的字符串時,您可能會遇到錯誤。這是因為默認解析方法無法識別指數符號。 成功解析這樣的字符串,您需要明確指定它代表浮點數。您可以使用numbersTyles.Float樣式進行此操作,如下所示:[&& && && ...
    程式設計 發佈於2025-05-05
  • 反射動態實現Go接口用於RPC方法探索
    反射動態實現Go接口用於RPC方法探索
    在GO 使用反射來實現定義RPC式方法的界面。例如,考慮一個接口,例如:鍵入myService接口{ 登錄(用戶名,密碼字符串)(sessionId int,錯誤錯誤) helloworld(sessionid int)(hi String,錯誤錯誤) } 替代方案而不是依靠反射...
    程式設計 發佈於2025-05-05

免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。

Copyright© 2022 湘ICP备2022001581号-3