gRPC is there to attempt to solve the difficulties of API design.
gRPC is an open-source framework developed by Google which is now part of CNCF.
At a high level, it allows you to define REQUEST and RESPONSE for RPCs and handles the rest.
In the CLIENT code, it looks like you're just callng a function on the SERVER.
With gRPC, it is implemented cleanly and solves a lot of problems.
At the core of gRPC, you need to define the messages and services using Protocol Buffers
.
The rest of the gRPC code will be generated for you and you'll have to provide and implementation for it.
One
.proto
file can be used for over 12 different languages.
syntax = 'proto3'; message Greeting { string first_name = 1; } message GreetRequest { Greeting greeting = 1; } message Greeting { string result = 1; } service GreetService { rpc Greet(GreetRequest) returns (GreetResponse) {}; }
.proto
file.One benefit Protobuf is used the size of the messsage and savings through network benefit. A JSON comparison of an object for "person" vs a Protobuf shows a 55 byte vs 20 byte difference, event with small messages.
Parsing Protocol Buggers (binary format) means faster and more efficient communication. This has added benefit for mobile devices with slower CPUs.
Java, Go and C have native, pure bindings. The rest rely on GRPC-C.
gRPC only uses HTTP/2.
The following shows how easy it is to define the streaming contracts:
service GreetService { // Unary rpc Greet(GreetRequest) returns (GreetResponse) {}; // Streaming Server rpc GreetManyTimes(GreetManyTimesRequest) returns (stream GreetManyTimesResponse) {}; // Streaming Client rpc LongGreet(stream LongGreetRequest) returns (LongGreetResponse) {}; // Bidirectional Streaming rpc GreetEveryone(stream GreetEveryoneRequest) returns (stream GreetEveryoneResponse) {}; }
Google have 10 billion gRPC reqs per second internally.
go get -u google.golang.org/grpc go get -u google.golang.org/protobuf
After cloning https://github.com/simplesteph/grpc-go-course
and changing into Greet/greetpb
, we can run protoc greet.proto --go_out=plugins=grpc:.
to test the code generation.
Running
protoc greet/greetpb/greet.proto --go_out=plugins=grpc:.
will output the file togreet/greetpb/greet.pb.go
.
Instead of installing the above, you can do it manually. Inside of hello/hellopb/hello.proto
:
syntax = "proto3"; package hello; option go_package = "hello/hellopb"; service HelloService {}
Run protoc hello/hellopb/hello.proto --go_out=plugins=grpc:.
to generate hello/hellopb/hello.pb.go
.
This will show how to properly start & stop the Server on the defined port
.
You may need to run
go mod init github.com/okeeffed/project-name
at the root of the directory prior.
package main import ( "fmt" "log" "net" "github.com/okeeffed/grpc-go-course/hello/hellopb" "google.golang.org/grpc" ) type server struct{} func main() { fmt.Println("Hello world") // :50051 is default gRPC port lis, err := net.Listen("tcp", "0.0.0.0:50051") if err != nil { log.Fatalf("Failed to listen: %v", err) } s := grpc.NewServer() hellopb.RegisterHelloServiceServer(s, &server{}) if err := s.Serve(lis); err != nil { log.Fatalf("Failed to serve: %v", err) } }
package main import ( "fmt" "log" "github.com/okeeffed/grpc-go-course/hello/hellopb" "google.golang.org/grpc" ) func main() { fmt.Println("Hello from the client") // WithInsecure for now as we don't have SSL certificates conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure()) if err != nil { log.Fatalf("Could not connect: %v", err) } defer conn.Close() c := hellopb.NewHelloServiceClient(conn) fmt.Printf("Created client: %f", c) }
Now if we start the server and client in two different tabs we can see that we connect!
# Tab 1 > go run hello/hello_server/server.go Hello world # Tab 2 > go run hello/hello_client/client.go Hello from the client Created client: &{%!f(*grpc.ClientConn=&{0xc0001beb00 0x10e0e70 localhost:50051 {passthrough localhost:50051} localhost:50051 {<nil> <nil> [] [] <nil> <nil> {{1000000000 1.6 0.2 120000000000}} false false true 0 <nil> {grpc-go/1.31.0 0x1470140 false [] <nil> <nil> {0 0 false} <nil> 0 0 32768 32768 0 <nil>} [] <nil> 0 false true false <nil> <nil> <nil> <nil> 0x1472400 [] true} 0xc0001aaf40 {<nil> <nil> 0x1470140 0 {passthrough localhost:50051}} 0xc0002529f0 {{0 0} 0 0 0 0} 0xc0001b2900 0xc00018e6e0 map[0xc0001eb8c0:{}] {0 0 false} pick_first 0xc0001bec00 {<nil>} 0xc0001aaf20 0 0xc0001b6700 {0 0} <nil>})}%
We need to create a Request
and Response
.
We start by writing the definition - here I will do one for "Hello".
We update our Hello
Protobuffer to define Greeting
and a Request + Response:
syntax = "proto3"; package hello; option go_package = "hello/hellopb"; message Hello { string first_name = 1; string last_name = 2; } message HelloRequest { Hello hello = 1; } message HelloResponse { string result = 1; } service HelloService { // Unary rpc Hello(HelloRequest) returns (HelloResponse) {} }
We can now update the server.go
file to now handle our call from the Client:
func (*server) Hello(ctx context.Context, req *hellopb.HelloRequest) (*hellopb.HelloResponse, error) { // from the auto generated info firstName := req.GetHello().GetFirstName() result := "Hello " + firstName res := &hellopb.HelloResponse{ Result: result, } return res, nil }
We can now update the client to get the simple response example.
package main import ( "context" "fmt" "log" "github.com/okeeffed/grpc-go-course/hello/hellopb" "google.golang.org/grpc" ) func main() { fmt.Println("Hello from the client") // WithInsecure for now as we don't have SSL certificates conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure()) if err != nil { log.Fatalf("Could not connect: %v", err) } defer conn.Close() c := hellopb.NewHelloServiceClient(conn) doUnary(c) } func doUnary(c hellopb.HelloServiceClient) { fmt.Println("Starting doUnary RPC") // Create a request that takes the "Hello" struct req := &hellopb.HelloRequest{ Hello: &hellopb.Hello{ FirstName: "Dennis", LastName: "O'Keeffe", }, } res, err := c.Hello(context.Background(), req) if err != nil { log.Fatalf("Error calling greet %v", err) } log.Printf("Response from Greet: %v", res.Result) }
If we now start the server and client, you will see the response.
# Tab 1 > go run hello/hello_server/server.go Hello world # Tab 2 > go run hello/hello_client/client.go Hello from the client Starting doUnary RPC 2020/08/10 13:47:24 Response from Greet: Hello Dennis
Take "one" request and return many responses.
For the API, we need to add a receiver function for the server that takes a request and a stream:
func (*server) HelloManyTimes(req *hellopb.HelloManyTimesRequest, stream hellopb.HelloService_HelloManyTimesServer) error { fmt.Printf("Greet many times function was invoked with %v", req) // from the auto generated info firstName := req.GetHello().GetFirstName() for i := 0; i < 10; i++ { result := "Hello " + firstName + " number " + strconv.Itoa(i) res := &hellopb.HelloManyTimesResponse{ Result: result, } stream.Send(res) time.Sleep(1000 * time.Millisecond) } return nil }
For the client, it is fairly simple:
package main import ( "context" "fmt" "io" "log" "github.com/okeeffed/grpc-go-course/hello/hellopb" "google.golang.org/grpc" ) func main() { fmt.Println("Hello from the client") // WithInsecure for now as we don't have SSL certificates conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure()) if err != nil { log.Fatalf("Could not connect: %v", err) } defer conn.Close() c := hellopb.NewHelloServiceClient(conn) doServerStreaming(c) } func doServerStreaming(c hellopb.HelloServiceClient) { fmt.Println("Starting a Server Streaming RPC") // Create a request that takes the "Hello" struct req := &hellopb.HelloManyTimesRequest{ Hello: &hellopb.Hello{ FirstName: "Dennis", LastName: "O'Keeffe", }, } // returns client and err resStream, err := c.HelloManyTimes(context.Background(), req) if err != nil { log.Fatalf("Error calling greet %v", err) } for { msg, err := resStream.Recv() if err == io.EOF { // reached the end break } if err != nil { log.Fatalf("Error calling greet %v", err) } log.Printf("Response from GreetManyTimes: %v", msg.GetResult()) } }
The updated Protobuffer:
// ... rest omitted // Streaming Client message LongGreetRequest { Hello hello = 1; } message LongGreetResponse { string result = 1; } service HelloService { // ... rest omitted // Streaming Client rpc LongGreet(stream LongGreetRequest) returns (LongGreetResponse) {} }
Updating the API:
// only take a stream this time func (*server) LongHello(stream hellopb.HelloService_LongHelloServer) error { fmt.Printf("LongHello func invoked with streaming req") result := "Hello " for { req, err := stream.Recv() if err == io.EOF { // we finished return stream.SendAndClose(&hellopb.LongHelloResponse{ Result: result, }) } if err != nil { log.Fatalf("Error while reading client stream: %v", err) } firstName := req.GetHello().GetFirstName() result += "Hello " + firstName + "! " } }
As for the client:
func doClientStreaming(c hellopb.HelloServiceClient) { stream, err := c.LongHello(context.Background()) requests := []*hellopb.LongHelloRequest{ &hellopb.LongHelloRequest{ Hello: &hellopb.Hello{ FirstName: "Dennis", } }, &hellopb.LongHelloRequest{ Hello: &hellopb.Hello{ FirstName: "Stacy", } }, &hellopb.LongHelloRequest{ Hello: &hellopb.Hello{ FirstName: "Tracy", } }, &hellopb.LongHelloRequest{ Hello: &hellopb.Hello{ FirstName: "Macy", } } } if err != nil { log.Fatalf("Error calling hello %v", err) } // iterate over slice and send message individually for _, req := range requests { stream.Send(req) } stream.Send(*hellopb.HelloManyTimesRequest) }
BiDi is new thanks to HTTP/2. It can be used for things such as chat, etc.
We can also use it for a long-running client where we want to stream back and forward.
For the Protocol Buffer:
// BiDi message HelloEveryoneRequest { Hello hello = 1; } message HelloEveryoneResponse { Hello result = 1; } service HelloService { // ... rest omitted // BiDi rpc HelloEveryone(stream HelloEveryoneRequest) returns (stream HelloEveryoneResponse) {} }
For server:
func (*server) HelloEveryone(stream hellopb.HelloService_HelloEveryoneServer) error { fmt.Printf("HelloEveryone fn invoked w/ streaming req\n") result := "" for { req, err := stream.Recv() if err == io.EOF { // we finished return stream.Send(&hellopb.HelloEveryoneResponse{ Result: result, }) } if err != nil { log.Fatalf("Error reading client stream: %v", err) return err } firstName := req.GetHello().GetFirstName() result += "Hello " + firstName + "! " } }
For client:
func doBiDiStreaming(c hellopb.HelloServiceClient) { fmt.Println("Starting to do BiDi Streaming RPC") // we create a stream by invoking the client stream, err := c.HelloEveryone(context.Background()) if err != nil { log.Fatalf("Error while creating stream: %v", err) return } requests := []*hellopb.HelloEveryoneRequest{ &hellopb.HelloEveryoneRequest{ Hello: &hellopb.Hello{ FirstName: "Dennis", }, }, &hellopb.HelloEveryoneRequest{ Hello: &hellopb.Hello{ FirstName: "Stacy", }, }, &hellopb.HelloEveryoneRequest{ Hello: &hellopb.Hello{ FirstName: "Tracy", }, }, &hellopb.HelloEveryoneRequest{ Hello: &hellopb.Hello{ FirstName: "Macy", }, }, } waitc := make(chan struct{}) // we send a bunch of messages to the client go func() { // func to send a bunch for _, req := range requests { fmt.Printf("Sending message: %v\n", req) stream.Send(req) } stream.CloseSend() }() // we receive a bunch go func() { for { res, err := stream.Recv() if err == io.EOF { break } if err != nil { log.Fatalf("Error while receiving: %v", err) } fmt.Printf("Received: %v", res.GetResult()) } close(waitc) }() // block until everything is done <-waitc }