gRPC payload 的默认格式是 Protobuf,但是 gRPC-Go 的实现中也对外暴露了 Codec interface ,它支持任意的 payload 编码。我们可以使用任何一种格式,包括你自己定义的二进制格式、flatbuffers、或者JSON 格式。

        通过google.golang.org/grpc@v1.50.1/encoding/encoding.go  的注册方法:

func RegisterCodec(codec Codec) {
if codec == nil {
panic("cannot register a nil Codec")
}
if codec.Name() == "" {
panic("cannot register Codec with empty string result for Name()")
}
contentSubtype := strings.ToLower(codec.Name())
registeredCodecs[contentSubtype] = codec
}

我们只需要定义我们自定义格式的Codec接口,就可以使用grpc传输我们需要的格式google.golang.org/grpc@v1.50.1/encoding/encoding.go

type Codec interface {
// Marshal returns the wire format of v.
Marshal(v interface{}) ([]byte, error)
// Unmarshal parses the wire format into v.
Unmarshal(data []byte, v interface{}) error
// Name returns the name of the Codec implementation. The returned string
// will be used as part of content type in transmission. The result must be
// static; the result cannot change between calls.
Name() string
}

      首先我们自定义一个Codec,根据反射判断传入的参数类型,如果是proto.Message格式就用proto格式序列化和反序列化,如果是string类型(已经序列化成json格式了)我们直接不用处理,如果是其他格式,使用json的序列化方法和反序列化方法来进行处理。

package codec


import (
"bytes"
"encoding/json"


"github.com/gogo/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
"google.golang.org/grpc/encoding"
)


func init() {
encoding.RegisterCodec(JSON{
Marshaler: jsonpb.Marshaler{
EmitDefaults: true,
OrigName: true,
},
})
}


type JSON struct {
jsonpb.Marshaler
jsonpb.Unmarshaler
}


// Name is name of JSON
func (j JSON) Name() string {
return "json"
}


func (j JSON) Marshal(v interface{}) (out []byte, err error) {
if pm, ok := v.(proto.Message); ok {
b := new(bytes.Buffer)
err := j.Marshaler.Marshal(b, pm)
if err != nil {
return nil, err
}
return b.Bytes(), nil
}
if val, ok := v.(string); ok {
return []byte(val), nil
}


return json.Marshal(v)
}


func (j JSON) Unmarshal(data []byte, v interface{}) (err error) {
if pm, ok := v.(proto.Message); ok {
b := bytes.NewBuffer(data)
return j.Unmarshaler.Unmarshal(b, pm)
}
if vv, ok := v.(*string); ok {
*vv = string(data)
return
}
return json.Unmarshal(data, v)
}

        引用我们自己定义的codec即可实现注册,因为注册方法encoding.RegisterCodec写在init里面了

        下面通过一个例子来使用我们自定义的自适应的codec

syntax = "proto3";
package test;
option go_package = "learn/json/grpc-json/rpc";
//定义服务
service TestService {
//注意:这里是returns 不是return
rpc SayHello(Request) returns (Response){
}
rpc SayHello1(Request) returns (Response){
}
}
//定义参数类型
message Request {
string message=1;
}
message Response {
string message=1;
}

生成代码

protoc --go-grpc_out=. learn/json/grpc-json/rpc/hello.proto

定义服务端

package rpc


import (
context "context"
  "fmt"
  
"google.golang.org/grpc/metadata"


_ "learn/json/grpc-json/codec"
)


type HelloService struct {
}


func (s *HelloService) mustEmbedUnimplementedTestServiceServer() {}


func (s *HelloService) SayHello(ctx context.Context, r *Request) (*Response, error) {
md, ok := metadata.FromIncomingContext(ctx)
fmt.Println("SayHello", ctx, r, md, ok, md["head"])
return &Response{
Message: "SayHello",
}, nil
}
func (s *HelloService) SayHello1(ctx context.Context, r *Request) (*Response, error) {
fmt.Println("SayHello1", ctx, r)
return &Response{
Message: "SayHello1",
}, nil
}

注意需要在我们的服务端注册我们的codec

 _ "learn/json/grpc-json/codec"

启动server服务

// git submodule add https://github.com/johanbrandhorst/grpc-json-example
package main


import (
"flag"
"fmt"
"io/ioutil"
"net"
"os"


"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/grpclog"


"github.com/johanbrandhorst/grpc-json-example/insecure"


  "learn/learn/json/grpc-json/rpc"
)


var (
gRPCPort = flag.Int("grpc-port", 10000, "The gRPC server port")
)


var log grpclog.LoggerV2


func init() {
log = grpclog.NewLoggerV2(os.Stdout, ioutil.Discard, ioutil.Discard)
grpclog.SetLoggerV2(log)
}


func main() {
flag.Parse()
addr := fmt.Sprintf("localhost:%d", *gRPCPort)
lis, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalln("Failed to listen:", err)
}
s := grpc.NewServer(
grpc.Creds(credentials.NewServerTLSFromCert(&insecure.Cert)),
)
  rpc.RegisterTestServiceServer(s, &rpc.HelloService{})
// Serve gRPC Server
log.Info("Serving gRPC on https://", addr)
log.Fatal(s.Serve(lis))
}

这个时候我们就可以测试我们的json格式传输是不是work

echo -en '\x00\x00\x00\x00\x16{"message":"xiazemin"}' | curl -ss -k --http2 \
-H "Content-Type: application/grpc+json" \
-H "TE:trailers" \
--data-binary @- \
https://localhost:10000/test.TestService/SayHello | od -bc

返回值是

0000000   000 000 000 000 026 173 042 155 145 163 163 141 147 145 042 072
\0 \0 \0 \0 026 { " m e s s a g e " :
0000020 042 123 141 171 110 145 154 154 157 042 175
" S a y H e l l o " }
0000033

可以看到已经成功了,解释下

\x00\x00\x00\x00\x16

的含义,这是http2 的message payload header

  • 第一个自己表示是否压缩 :Compression boolean (1 byte)

  • 后面四个字节表示我们请求数据的大小:Payload size (4 bytes)

  • 我们这\x16 表示我们传输的json的格式大小是22字节,可以自己数一下。

        当然我也可以通过go客户端来发送json格式请求,我们先定义一个flag类型来接受curl 的http 头部格式

type arrayFlags []string


func (i *arrayFlags) String() string {
return fmt.Sprint(*i)
}
func (i *arrayFlags) Set(value string) error {
*i = append(*i, value)
return nil
}

            把得到的参数注入到metaData里面,然后在启动连接的时候指定我们的编解码格式。

package main


import (
"context"
"flag"
"fmt"
"net"
"strings"


"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"


  "learn/learn/json/grpc-json-example/insecure"
"learn/learn/json/grpc-json/rpc"
)


type arrayFlags []string


func (i *arrayFlags) String() string {
return fmt.Sprint(*i)
}
func (i *arrayFlags) Set(value string) error {
*i = append(*i, value)
return nil
}


var (
headers arrayFlags
addr string
port string
method string
data string
)


func init() {
flag.Var(&headers, "H", "-H 'mirror:mirror' -H 'content-type:application/json'")
flag.StringVar(&addr, "addr", "localhost", "The address of the server to connect to")
flag.StringVar(&port, "port", "10000", "The port to connect to")
flag.StringVar(&method, "m", "test.TestService/SayHello", "the method wang to call")
flag.StringVar(&data, "d", "{}", "the data wang to send")
flag.Parse()
}


func main() {
ctx := context.Background()
if headers != nil {
md := metadata.MD{}
for _, header := range headers {
pairs := strings.Split(header, ":")
if len(pairs) != 2 {
panic(fmt.Sprintf("invalid header %s", header))
} else {
md[strings.Trim(pairs[0], " ")] = append(md[strings.Trim(pairs[0], " ")], strings.Trim(pairs[1], " "))
}
    }
ctx = metadata.NewOutgoingContext(ctx, md)
}


conn, err := grpc.DialContext(ctx, net.JoinHostPort(addr, port),
    grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(insecure.CertPool, "")),
grpc.WithDefaultCallOptions(grpc.CallContentSubtype(rpc.JSON{}.Name())),
  )
if err != nil {
panic(err)
}
defer conn.Close()


c := rpc.NewTestServiceClient(conn)
resp, err := c.SayHello(ctx, &rpc.Request{Message: "xiazemin"})
if err != nil {
panic(err)
}
fmt.Println(resp)

reply1 := new(string)
err = grpc.Invoke(ctx, method, data, reply1, conn)
if err != nil {
panic(err)
}
fmt.Println("response:")
  fmt.Println(*reply1)
}

            这里我们发起了两种请求,一种是普通的grpc请求,另一种就是我们自定定义的json格式,测试下

go run learn/json/grpc-json/client/main.go -H 'head:h1' -H 'head:h2' -d '{"message":"xiazemin"}' -m test.TestService/SayHello -addr 127.0.0.1 -port 10000
message:"SayHello"
response:
{"message":"SayHello"}

可以看到两种方式都是work的,说明了我们的codec具有自适应能力的。

        当然,我们也可以定义普通的go类型发起请求,也是能处理的,比如:

  err = grpc.Invoke(ctx, method, map[string]interface{}{"message": "xiaz"}, &reply, conn)
if err != nil {
panic(err)
}
fmt.Println("response:")
fmt.Println(string(reply.Msg))

总的来说,grpc框架整体的灵活性还是挺大的,它给我们提供了默认选项,非常好用,生产中我们也可以根据自己的需求灵活自定义。