In gRPC, a client application can directly call a method on a server application on a different machine as if it were a local object, making it easier for you to create distributed applications and services. As in many RPC systems, gRPC is based around the idea of defining a service, specifying the methods that can be called remotely with their parameters and return types. On the server side, the server implements this interface and runs a gRPC server to handle client calls. On the client side, the client has a stub (referred to as just a client in some languages) that provides the same methods as the server.
It is a protocol that is build on top of HTTP/2, and it has some handy features. These included the ability to make streaming calls from the client and server side, or Bi-directional streaming. It serializes and deserializes data using Protocol Buffers, and it also provides code generation through the gRPC compiler to currently 11 different languages.
gRPC clients and servers can run and talk to each other in a variety of environments - from servers inside Google to your own desktop - and can be written in any of gRPC’s supported languages. So, for example, you can easily create a gRPC server in Java with clients in Go, Python, or Ruby. In addition, the latest Google APIs will have gRPC versions of their interfaces, letting you easily build Google functionality into your applications
No input rpc parameter defines If you don’t want any input or output parameters, you can use the well-known proto google.protobuf.Empty. However, this is discouraged as it prevents you from adding parameters to the method in the future. Instead, you would be encouraged to follow the normal practice of having a message for the request, but simply with no contents.
1 2 3 4 5
service Greeter { rpc SayHello (SayHelloRequest) returns (SayHelloResponse) {} }
message SayHelloRequest {} // service has no input
implicit stream context stream also has context as well like unary rpc call which passed context explicitly, while for stream it’s implicitly!
1 2
ctx := stream.Context() <-ctx.Done
Metadata Metadata is information about a particular RPC call (such as authentication details) in the form of a list of key-value pairs, where the keys are strings and the values are typically strings, but can be binary data. Metadata is opaque to gRPC itself - it lets the client provide information associated with the call to the server and vice versa.
As grpc based on http2, in order to understand deeply, we need to learn some core concepts of http2. A “stream” is an independent, bidirectional sequence of frames exchanged between the client and server within an HTTP/2 connection.
Stream
A bidirectional flow of bytes within an established connection, which may carry one or more messages.
A single HTTP/2 connection can contain multiple concurrently open streams, with either endpoint interleaving frames from multiple streams.
Streams can be established and used unilaterally or shared by either the client or server.
Streams can be closed by either endpoint.
Streams are identified by an integer. Stream identifiers are assigned to streams by the endpoint initiating the stream.
Message
A complete sequence of frames that map to a logical request or response message.
Frame
The smallest unit of communication in HTTP/2, each containing a frame header, which at a minimum identifies the stream to which the frame belongs.
All communication is performed over a single TCP connection that can carry any number of bidirectional streams.
Each stream has a unique identifier and optional priority information that is used to carry bidirectional messages.
Each message is a logical HTTP message, such as a request, or response, which consists of one or more frames.
The frame is the smallest unit of communication that carries a specific type of data—e.g., HTTP headers, message payload, and so on. Frames from different streams may be interleaved and then reassembled via the embedded stream identifier in the header of each frame.
Stream is not http2 connection(mostly tcp connection), it’s also bidirectional stuff, close stream does not means close its underlaying connection.
gRPC service methods have exactly one input message and exactly one output message, handler is called only when its get the whole message. Typically, these messages are used as input and output to only one method. This is on purpose, as it allows easily adding new parameters later (to the messages) while maintaining backward compatibility.
In a unary rpc call, the client sends a single request and the server responds with a single message.
syntax = "proto3"; package greet; //used by proto itself(independent with different language) // to prevent naming conflicts between different projects(protos). // import "google/protobuf/timestamp.proto";
option go_package = "github.com/hello/runtime/proto/greet"; // used by protoc when generate go specific code
// The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} }
// The request message containing the user's name. message HelloRequest { string name = 1; }
// The response message containing the greetings message HelloReply { string message = 1; }
# Install the protocol compiler plugins for Go using the following commands # protoc-gen-go: go plugin or gogo/protobuf # proto-gen-go-grpc: go rpc plugin $ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26 $ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
compile rpc if multiple protos belong to same package, you must provide all of them to protoc command!!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# put your source at $GOPATH/src/github.com/hello $ cd$GOPATH/src/github.com/hello
# If the paths=import flag is specified, the output file is placed in a directory named after the Go package's import path For example, an input file pro/hello.proto with a Go import path of github.com/hello/runtime/proto/greet results in an output file at github.com/hello/runtime/proto/greet/hello.pb.go. This is the default output mode if a paths flag is not specified.
# If the paths=source_relative flag is specified, the output file is placed in the same relative directory as the input file. For example, an input file pro/hello.proto results in an output file at pro/hello.pb.go
# --go_out and --go-grpc_out is based path, all other paths are relative to it!!!
// server is used to implement greet.GreeterServer. type server struct { pb.UnimplementedGreeterServer // must be first field // your staff here }
/// return value: HelloReply, error // SayHello implements greet.GreeterServer func(s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { md, ok := metadata.FromIncomingContext(ctx) if ok { // metadata is map: var MD map[string][]string // key is string, while value is string array!!! log.Printf("metadata: %v", md.Get("k1")) } log.Printf("Received: %v", in.GetName()) // connection is close when returns!!! return &pb.HelloReply{Message: "Hi " + in.GetName()}, nil }
funcmain() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() // register services(server{}) with rpc server pb.RegisterGreeterServer(s, &server{}) // for debugging reflection.Register(s) log.Printf("server listening at %v", lis.Addr()) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
defer con.Close() //client for specific service c := pb.NewGreeterClient(con) req := pb.HelloRequest{Name: "tom"} res, err := c.SayHello(context.Background(), &req) if err != nil { log.Fatalf("Error on Echo rpc call: %v\n", err) } else { fmt.Printf("Response: %v\n", res) } }
client stream rpc call
In a client streaming rpc call, the client sends a bunch of requests and once it is done streaming, the server will return a single message.
stream here is stream of particular request message, the unit is request but byte!!
Making a client stream request is useful when the client needs to send resources one by one to the server, so they can be processed right away, and once the streaming call is finished the client gets the response.
For client stream, client side needs a stream to send and also server needs a stream to receive messages, both side need to change.
Steps
client send one request, then another
client notify server I finished the stream
server receive request one by one
server see client finish the stream send one reply.
syntax = "proto3"; package greet; //used by proto itself(independent with different language) // to prevent naming conflicts between different projects(protos). // import "google/protobuf/timestamp.proto";
option go_package = "github.com/hello/runtime/protocols/greet"; // used by protoc when generate go specific code
service Greeter { // Sends a greeting rpc SayHello (stream HelloRequest) returns (HelloReply) {} }
// The request message containing the user's name. message HelloRequest { string name = 1; }
// The response message containing the greetings message HelloReply { string message = 1; }
// server is used to implement greet.GreeterServer. type greetServer struct { pb.UnimplementedGreeterServer // must be first field // your staff here }
// SayHello implements greet.GreeterServer func(s *greetServer) SayHello(stream pb.Greeter_SayHelloServer) error { data := []string{} for { req, err := stream.Recv() if err == io.EOF { // response message is returned through stream, not return value for unary rpc call return stream.SendAndClose(&pb.HelloReply{Message: strings.Join(data, ",")}) }
if err != nil { return fmt.Errorf("internal error") } data = append(data, req.GetName()) } }
funcmain() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() // register services(server{}) with rpc server pb.RegisterGreeterServer(s, &greetServer{}) // for debugging reflection.Register(s) log.Printf("server listening at %v", lis.Addr()) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
defer con.Close() //client for specific service c := pb.NewGreeterClient(con) req1 := pb.HelloRequest{Name: "tom"} req2 := pb.HelloRequest{Name: "jack"}
// rpc retuns a stream handler stream, err := c.SayHello(context.Background()) if err != nil { log.Fatalf("Error on Echo rpc call: %v\n", err) } else { // send several requests through stream err = stream.Send(&req1) if err != nil { log.Fatalf("Error on sending: %v\n", err) } fmt.Printf("sent: %v\n", req1.GetName()) time.Sleep(10 * time.Second) err = stream.Send(&req2) if err != nil { log.Fatalf("Error on sending: %v\n", err) } fmt.Printf("sent: %v\n", req2.GetName()) }
// this will call CloseSend to notify I sent all, then receive res, err := stream.CloseAndRecv() if err != nil { log.Fatalf("Error on recv: %v\n", err) } else { fmt.Printf("Response :%s\n", res.GetMessage()) } }
server stream rpc call
For server stream, server side needs a stream to send and also client needs a stream to receive response messages, both side need to change.
syntax = "proto3"; package greet; //used by proto itself(independent with different language) // to prevent naming conflicts between different projects(protos). // import "google/protobuf/timestamp.proto";
option go_package = "github.com/hello/runtime/protocols/greet"; // used by protoc when generate go specific code
service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (stream HelloReply) {} }
// The request message containing the user's name. message HelloRequest { string name = 1; }
// The response message containing the greetings message HelloReply { string message = 1; }
// server is used to implement greet.GreeterServer. type greetServer struct { pb.UnimplementedGreeterServer // must be first field // your staff here }
opts := grpc.WithInsecure() // WithBlock() blocks here until, error or connection is setup con, err := grpc.Dial("localhost:50051", opts, grpc.WithInsecure(), grpc.WithBlock()) if err != nil { log.Fatalf("Error connecting: %v \n", err) }
defer con.Close() //client for specific service c := pb.NewGreeterClient(con) req1 := pb.HelloRequest{Name: "tom"}
stream, err := c.SayHello(context.Background(), &req1) if err != nil { log.Fatalf("Error on sending: %v\n", err) } else { for { res, err := stream.Recv() if err == io.EOF {// server close the stream, all is done!!! break } if err != nil { fmt.Println(err) break } fmt.Printf("response: %v\n", res) } } }
bi-direction stream rpc call
In a bi-directional streaming rpc call, both the client and the server sends multiple messages to each other. Using this type of rpc call, can be a little bit more complicated, since you have to take care of error handling from the server side and the client side, plus in some cases it can add more latency. And perhaps it could be a better option to use a unary call.
Both the client and server can stop receiving or sending messages at any point in time, either because some errors occurred or because some other business logic happened.
syntax = "proto3"; package greet; //used by proto itself(independent with different language) // to prevent naming conflicts between different projects(protos). // import "google/protobuf/timestamp.proto";
option go_package = "github.com/hello/runtime/protocols/greet"; // used by protoc when generate go specific code
service Greeter { // Sends a greeting rpc SayHello (stream HelloRequest) returns (stream HelloReply) {} }
// The request message containing the user's name. message HelloRequest { string name = 1; }
// The response message containing the greetings message HelloReply { string message = 1; }
// server is used to implement greet.GreeterServer. type greetServer struct { pb.UnimplementedGreeterServer // must be first field // your staff here }
On way to edit source code to include reflection in your grpc server or without change see below
1 2 3 4 5
+ import "google.golang.org/grpc/reflection" s := grpc.NewServer() pb.RegisterGreeterService(s, &pb.GreeterService{SayHello: sayHello}) + // Register reflection service on gRPC server. + reflection.Register(s)
Run grpcui as below
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
$ GO111MODULE=on go install github.com/fullstorydev/grpcui/cmd/grpcui@latest # -bind: webserver address # -port: webserver port # -plaintext: connect grpc server without tls # localhost:50051: grpc server # With this command, you must register your rpc with reflection as above $ grpcui -open-browser=false -bind=10.0.2.15 -port=8000 -plaintext localhost:50051
# if you can NOT change your source code, there is an another way to run grpcui # xxx.proto who defines rpc call and message. # -proto can be relative path $ grpcui -open-browser=false -proto=./path/to/xxx.proto -bind=10.0.2.15 -port=8000 -plaintext localhost:50051
# More advanced, if your xxx.proto import other protos, you need to add -import-path to let grpcui to find them # other protos path relative to --import-path(used for imported protos) $ grpcui -open-browser=false -proto=./path/to/xxx.proto -import-path=/path/to/depen/ -bind=10.0.2.15 -port=8000 -plaintext localhost:50051
# then open browser at http://10.0.2.15:8000/
command line tool
On way to edit source code to include reflection in your grpc server or without change see below
1 2 3 4 5
+ import "google.golang.org/grpc/reflection" s := grpc.NewServer() pb.RegisterGreeterService(s, &pb.GreeterService{SayHello: sayHello}) + // Register reflection service on gRPC server. + reflection.Register(s)
# if you can't register your grpc server with reflection, you can pass -proto and -import-path to grpcurl as grpcui does!!
gogo
gogoprotobuf is a fork of golang/protobuf with extra code generation features.
This code generation is used to achieve:
fast marshalling and unmarshalling
more canonical Go structures
goprotobuf compatibility
less typing by optionally generating extra helper code
peace of mind by optionally generating test and benchmark code
other serialization formats
gogo depends on grpc when generates grpc stub code, that means it justs wrapper grpc only. so for grpc, you must install grpc as well. go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.2, make sure install proper version tested by gogo.
Install
Choose one binary and install it, different binaries have different speed and customization. more refer to gogo.
Usage
1 2 3 4 5 6 7 8
# can only github.com/golang/protobuf/proto protoc --gofast_out=. myproto.proto