Simple tutorial in how to create a protobuf message and its wireformat by using reflection packages go provides.
Almost always, you just generate go packages for protobuf pb.go
and its corresponding transport over gRPC echo_grpc.pb.go
. You would then use both to ‘just invoke’ an API and all the gory details are taken care of for you:
import echo "github.com/salrashid123/grpc_wireformat/grpc_services/src/echo"
c := echo.NewEchoServerClient(conn)
r, err := c.SayHello(ctx, &echo.EchoRequest{FirstName: "sal", LastName: "mander", MiddleName: &echo.Middle{
Name: "a",
}})
This article is just an exercise to show what you can do under the hood using ‘first principles’.
We manually construct a protobuf message for the following .proto
:
syntax = "proto3";
package echo;
service EchoServer {
rpc SayHello (EchoRequest) returns (EchoReply) {}
}
message Middle {
string name = 1;
}
message EchoRequest {
string first_name = 1;
string last_name = 2;
Middle middle_name = 3;
}
message EchoReply {
string message = 1;
}
by
Load and Register its binary protobuf definition echo.pb
Create an EchoRequest
message using protoreflect and dynamicpb
Either
a. Create Message Descriptor add fields
b. Create Message using anypb and protojson
Encode that message into gRPC’s Unary wireformat
Send that to a gRPC server using _an ordinary net/http
client over http2
Recieve the wireformat response
decode the wireformat to a protobuf message
Convert the message to EchoReply
print the contents of EchoReply
What does that prove? not much, its just academic thing i did…learning something new has its perpetual rewards..
What you can do is use this to construct an arbitrary Terraform DataSource
You can find the source here
for some background, also see
So, let just see a normal gRPC client server:
# run server
cd grpc_services/
go run src/grpc_server.go --grpcport :50051
# run client
go run src/grpc_client.go --host localhost:50051
What this does is just send back a unary response..nothing to see here, move along
Next is what this article is about. grpc_client_dynamic.go
does a couple of things:
echo.pb
First step is for your go app to even know about the protobuf…so we need to load it so protoreflect knows about it
protoFile, err := ioutil.ReadFile("grpc_services/src/echo/echo.pb")
fileDescriptors := &descriptorpb.FileDescriptorSet{}
err = proto.Unmarshal(protoFile, fileDescriptors)
pb := fileDescriptors.GetFile()[0]
fd, err := protodesc.NewFile(pb, protoregistry.GlobalFiles)
err = protoregistry.GlobalFiles.RegisterFile(fd)
Next we construct our echo.EchoRequest
using the protodescriptor from step 1.
I found two ways to do this: in 3a
below, we will “strongly type” create a message and in 3b
, we will create a message using a JSON string. (the latter is even more subject to simple typos)
(a)
Create Message Descriptor for both messages add fieldsIn the following, you know which type you want to create so we do this by hand:
// create the inner message
echoRequestInnerMessageType, err := protoregistry.GlobalTypes.FindMessageByName("echo.Middle")
echoRequestInnerMessageDescriptor := echoRequestInnerMessageType.Descriptor()
// add a field
inner_name := echoRequestInnerMessageDescriptor.Fields().ByName("name")
reflectEchoInnerRequest := echoRequestInnerMessageType.New()
reflectEchoInnerRequest.Set(inner_name, protoreflect.ValueOfString("a"))
// now create the outer EchoRequest message
echoRequestMessageType, err := protoregistry.GlobalTypes.FindMessageByName("echo.EchoRequest")
echoRequestMessageDescriptor := echoRequestMessageType.Descriptor()
// setup the outer objects fields
fname := echoRequestMessageDescriptor.Fields().ByName("first_name")
lname := echoRequestMessageDescriptor.Fields().ByName("last_name")
mname := echoRequestMessageDescriptor.Fields().ByName("middle_name")
// now add the fields and the Middle message
// note the types, the message is of type Message
reflectEchoRequest := echoRequestMessageType.New()
reflectEchoRequest.Set(fname, protoreflect.ValueOfString("sal"))
reflectEchoRequest.Set(lname, protoreflect.ValueOfString("mander"))
reflectEchoRequest.Set(mname, protoreflect.ValueOfMessage(reflectEchoInnerRequest))
fmt.Printf("EchoRequest: %v\n", reflectEchoRequest)
in, err := proto.Marshal(reflectEchoRequest.Interface())
fmt.Printf("Encoded EchoRequest using protoreflect %s\n", hex.EncodeToString(in))
Note that we’re manually defining everything…its excruciating
(b)
Create Message using anypb and protojsonIn the following, we will “just create” a message using its JSON format. Remember to set the @type:
field in json
j := `{ "@type": "echo.EchoRequest", "firstName": "sal", "lastName": "mander", "middleName": { "name": "a"}}`
a, err := anypb.New(echoRequestMessageType.New().Interface())
err = protojson.Unmarshal([]byte(j), a)
fmt.Printf("Encoded EchoRequest using protojson and anypb %v\n", hex.EncodeToString(a.Value))
Either way, we need to convert the proto message into a wireformat. For this we use lencode
var out bytes.Buffer
enc := lencode.NewEncoder(&out, lencode.SeparatorOpt([]byte{0}))
// (a) to send the manually generated message:
err = enc.Encode(in)
// (b) to send the json->protobuf message
err = enc.Encode(a.Value)
You might be asking …WTF is lencode.SeparatorOpt([]byte{0})
?!
…weeellll, thats just the wireformat position that signals compression..it works here. See parsing gRPC messages from Envoy TAP
We’re now ready to transmit the wireformat message to our grpc server
client := http.Client{
Transport: &http2.Transport{
TLSClientConfig: &tlsConfig,
},
}
reader := bytes.NewReader(out.Bytes())
resp, err := client.Post("https://localhost:50051/echo.EchoServer/SayHello", "application/grpc", reader)
read in the bytes inside resp.Body
Use lencode
to unmarshall the payload
respMessage := lencode.NewDecoder(bytesReader, lencode.SeparatorOpt([]byte{0}))
respMessageBytes, err := respMessage.Decode()
EchoReply
We now do the inverse of the outbound steps still using the descriptors we originally setup
echoReplyMessageType, err := protoregistry.GlobalTypes.FindMessageByName("echo.EchoReply")
echoReplyMessageDescriptor := echoReplyMessageType.Descriptor()
pmr := echoReplyMessageType.New()
err = proto.Unmarshal(respMessageBytes, pmr.Interface())
msg := echoReplyMessageDescriptor.Fields().ByName("message")
fmt.Printf("EchoReply.Message using protoreflect: %s\n", pmr.Get(msg).String())
EchoReply
We now have the message back…we can print it.
done
TO run it end-to end, keep the server running and invoke the client
$ go run grpc_client_dynamic.go
Loading package echo
Registering MessageType: Middle
Registering MessageType: EchoRequest
Registering MessageType: EchoReply
EchoRequest: first_name:"sal" last_name:"mander" middle_name:{name:"a"}
Encoded EchoRequest using protoreflect 0a0373616c12066d616e6465721a030a0161
Encoded EchoRequest using protojson and anypb 0a0373616c12066d616e6465721a030a0161
wire encoded EchoRequest: 00000000120a0373616c12066d616e6465721a030a0161
wire encoded EchoReply 00000000140a1248656c6c6f2073616c2061206d616e646572
Encoded EchoReply 0a1248656c6c6f2073616c2061206d616e646572
EchoReply.Message using protoreflect: Hello sal a mander
EchoReply as string JSON: {"message":"Hello sal a mander"}
What the output shows is how we loaded the echo.pb
, then constructed the Message from either explicitly creating it or by converting a JSON Message over.
Once that was done, we sent the wire-encoded message to the server and reversed the process.
Did i mention you can also use curl
to call the endpoint…
the trick is to use the wire encoded format (since, you know, curl send stuff on the wire
echo -n '00000000120a0373616c12066d616e6465721a030a0161' | xxd -r -p - frame.bin
curl -v --raw -X POST --http2-prior-knowledge \
-H "Content-Type: application/grpc" \
-H "TE: trailers" \
--data-binary @frame.bin \
http://localhost:50051/echo.EchoServer/SayHello -o resp.bin
to decode
$ xxd -p resp.bin
00000000140a1248656c6c6f2073616c2061206d616e646572
## remove the prefix headers 0000000014
$ echo -n "0a1248656c6c6f2073616c2061206d616e646572" | \
xxd -r -p | \
protoc --decode echo.EchoReply grpc_services/src/echo/echo.proto
message: "Hello sal a mander"
Now look at the message decoded with wireshark
This repo also contains an end-to-end sample of github.com/jhump/protoreflect.
Using that library makes certain things a lot easer as it wraps some of the legwork for you. You can also “just load” a .proto
file that includes specifications of the Message and gRPC server.
To use that,
cd jhump_client/
$ go run grpc_client_jhump.go
> service echo.EchoServer
* method echo.EchoServer.SayHello (echo.EchoRequest) echo.EchoReply
- message echo.Middle
- message echo.EchoRequest
- message echo.EchoReply
Looking for serviceName echo.EchoServer methodName SayHello
Response: {
"message": "Hello sal a mander"
}
The default gRPC server here also has gRPC Reflection enabled for inspection.
To use this, you have to install grpc_cli (which, TBH, is way too cumbersome!)
Anyway
$ grpc_cli ls localhost:50051
echo.EchoServer
grpc.health.v1.Health
grpc.reflection.v1alpha.ServerReflection
$ grpc_cli ls localhost:50051 echo.EchoServer -l
filename: src/echo/echo.proto
package: echo;
service EchoServer {
rpc SayHello(echo.EchoRequest) returns (echo.EchoReply) {}
}
$ grpc_cli ls localhost:50051 echo.EchoServer.SayHello -l
rpc SayHello(echo.EchoRequest) returns (echo.EchoReply) {}
$ grpc_cli type localhost:50051 echo.EchoRequest
message EchoRequest {
string first_name = 1 [json_name = "firstName"];
string last_name = 2 [json_name = "lastName"];
.echo.Middle middle_name = 3 [json_name = "middleName"];
}
grpc_cli call localhost:50051 echo.EchoServer.SayHello "first_name: 'sal' last_name: 'mander' middle_name: {name: 'a'}"
connecting to localhost:50051
message: "Hello sal a mander"
Rpc succeeded with OK status
done
This site supports webmentions. Send me a mention via this form.