Go 源码分析:json 格式请求 grpc 服务
2022-11-1 08:54:8 Author: Go语言中文网(查看原文) 阅读量:9 收藏

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

        通过google.golang.org/[email protected]/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/[email protected]/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 JSONfunc (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-examplepackage 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 10000message:"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框架整体的灵活性还是挺大的,它给我们提供了默认选项,非常好用,生产中我们也可以根据自己的需求灵活自定义。


推荐阅读

福利
我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。关注公众号 「polarisxu」,回复 ebook 获取;还可以回复「进群」,和数万 Gopher 交流学习。


文章来源: http://mp.weixin.qq.com/s?__biz=MzAxMTA4Njc0OQ==&mid=2651453647&idx=1&sn=46191e45fa21c61f3bc6e989daf8fd0d&chksm=80bb263db7ccaf2b9c718a54cdda24ae2d5f66f8701f7b1f80a5259a72bc6964d2f27e26bbef#rd
如有侵权请联系:admin#unsafe.sh