diff --git a/client/grpc/codec.go b/client/grpc/codec.go new file mode 100644 index 00000000..00c95da1 --- /dev/null +++ b/client/grpc/codec.go @@ -0,0 +1,209 @@ +package grpc + +import ( + b "bytes" + "encoding/json" + "fmt" + "strings" + + "go-micro.dev/v5/codec" + "go-micro.dev/v5/codec/bytes" + "google.golang.org/grpc" + "google.golang.org/grpc/encoding" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/runtime/protoiface" + "google.golang.org/protobuf/runtime/protoimpl" +) + +type jsonCodec struct{} +type protoCodec struct{} +type bytesCodec struct{} +type wrapCodec struct{ encoding.Codec } + +var useNumber bool + +var ( + defaultGRPCCodecs = map[string]encoding.Codec{ + "application/json": jsonCodec{}, + "application/proto": protoCodec{}, + "application/protobuf": protoCodec{}, + "application/octet-stream": protoCodec{}, + "application/grpc": protoCodec{}, + "application/grpc+json": jsonCodec{}, + "application/grpc+proto": protoCodec{}, + "application/grpc+bytes": bytesCodec{}, + } +) + +// UseNumber fix unmarshal Number(8234567890123456789) to interface(8.234567890123457e+18). +func UseNumber() { + useNumber = true +} + +func (w wrapCodec) String() string { + return w.Codec.Name() +} + +func (w wrapCodec) Marshal(v interface{}) ([]byte, error) { + b, ok := v.(*bytes.Frame) + if ok { + return b.Data, nil + } + return w.Codec.Marshal(v) +} + +func (w wrapCodec) Unmarshal(data []byte, v interface{}) error { + b, ok := v.(*bytes.Frame) + if ok { + b.Data = data + return nil + } + return w.Codec.Unmarshal(data, v) +} + +func (protoCodec) Marshal(v interface{}) ([]byte, error) { + switch m := v.(type) { + case *bytes.Frame: + return m.Data, nil + case proto.Message: + return proto.Marshal(m) + case protoiface.MessageV1: + // #2333 compatible with etcd legacy proto.Message + m2 := protoimpl.X.ProtoMessageV2Of(m) + return proto.Marshal(m2) + } + return nil, fmt.Errorf("failed to marshal: %v is not type of *bytes.Frame or proto.Message", v) +} + +func (protoCodec) Unmarshal(data []byte, v interface{}) error { + switch m := v.(type) { + case proto.Message: + return proto.Unmarshal(data, m) + case protoiface.MessageV1: + // #2333 compatible with etcd legacy proto.Message + m2 := protoimpl.X.ProtoMessageV2Of(m) + return proto.Unmarshal(data, m2) + } + return fmt.Errorf("failed to unmarshal: %v is not type of proto.Message", v) +} + +func (protoCodec) Name() string { + return "proto" +} + +func (bytesCodec) Marshal(v interface{}) ([]byte, error) { + b, ok := v.(*[]byte) + if !ok { + return nil, fmt.Errorf("failed to marshal: %v is not type of *[]byte", v) + } + return *b, nil +} + +func (bytesCodec) Unmarshal(data []byte, v interface{}) error { + b, ok := v.(*[]byte) + if !ok { + return fmt.Errorf("failed to unmarshal: %v is not type of *[]byte", v) + } + *b = data + return nil +} + +func (bytesCodec) Name() string { + return "bytes" +} + +func (jsonCodec) Marshal(v interface{}) ([]byte, error) { + if b, ok := v.(*bytes.Frame); ok { + return b.Data, nil + } + + if pb, ok := v.(proto.Message); ok { + bytes, err := protojson.Marshal(pb) + if err != nil { + return nil, err + } + return bytes, nil + } + + return json.Marshal(v) +} + +func (jsonCodec) Unmarshal(data []byte, v interface{}) error { + if len(data) == 0 { + return nil + } + if b, ok := v.(*bytes.Frame); ok { + b.Data = data + return nil + } + if pb, ok := v.(proto.Message); ok { + return protojson.Unmarshal(data, pb) + } + + dec := json.NewDecoder(b.NewReader(data)) + if useNumber { + dec.UseNumber() + } + return dec.Decode(v) +} + +func (jsonCodec) Name() string { + return "json" +} + +type grpcCodec struct { + // headers + id string + target string + method string + endpoint string + + s grpc.ClientStream + c encoding.Codec +} + +func (g *grpcCodec) ReadHeader(m *codec.Message, mt codec.MessageType) error { + md, err := g.s.Header() + if err != nil { + return err + } + if m == nil { + m = new(codec.Message) + } + if m.Header == nil { + m.Header = make(map[string]string, len(md)) + } + for k, v := range md { + m.Header[k] = strings.Join(v, ",") + } + m.Id = g.id + m.Target = g.target + m.Method = g.method + m.Endpoint = g.endpoint + return nil +} + +func (g *grpcCodec) ReadBody(v interface{}) error { + if f, ok := v.(*bytes.Frame); ok { + return g.s.RecvMsg(f) + } + return g.s.RecvMsg(v) +} + +func (g *grpcCodec) Write(m *codec.Message, v interface{}) error { + // if we don't have a body + if v != nil { + return g.s.SendMsg(v) + } + // write the body using the framing codec + return g.s.SendMsg(&bytes.Frame{Data: m.Body}) +} + +func (g *grpcCodec) Close() error { + return g.s.CloseSend() +} + +func (g *grpcCodec) String() string { + return g.c.Name() +} diff --git a/client/grpc/error.go b/client/grpc/error.go new file mode 100644 index 00000000..1c53d1dc --- /dev/null +++ b/client/grpc/error.go @@ -0,0 +1,69 @@ +package grpc + +import ( + "net/http" + + "go-micro.dev/v5/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func microError(err error) error { + // no error + switch err { + case nil: + return nil + } + + if verr, ok := err.(*errors.Error); ok { + return verr + } + + // grpc error + s, ok := status.FromError(err) + if !ok { + return err + } + + // return first error from details + if details := s.Details(); len(details) > 0 { + return microError(details[0].(error)) + } + + // try to decode micro *errors.Error + if e := errors.Parse(s.Message()); e.Code > 0 { + return e // actually a micro error + } + + // fallback + return errors.New("go.micro.client", s.Message(), microStatusFromGrpcCode(s.Code())) +} + +func microStatusFromGrpcCode(code codes.Code) int32 { + switch code { + case codes.OK: + return http.StatusOK + case codes.InvalidArgument: + return http.StatusBadRequest + case codes.DeadlineExceeded: + return http.StatusRequestTimeout + case codes.NotFound: + return http.StatusNotFound + case codes.AlreadyExists: + return http.StatusConflict + case codes.PermissionDenied: + return http.StatusForbidden + case codes.Unauthenticated: + return http.StatusUnauthorized + case codes.FailedPrecondition: + return http.StatusPreconditionFailed + case codes.Unimplemented: + return http.StatusNotImplemented + case codes.Internal: + return http.StatusInternalServerError + case codes.Unavailable: + return http.StatusServiceUnavailable + } + + return http.StatusInternalServerError +} diff --git a/client/grpc/go.mod b/client/grpc/go.mod new file mode 100644 index 00000000..e44e4164 --- /dev/null +++ b/client/grpc/go.mod @@ -0,0 +1,35 @@ +module go-micro.dev/v5/client/grpc + +go 1.24 + +require ( + go-micro.dev/v5 v5.5.1-0.20250507183911-01b8394c8119 + google.golang.org/grpc v1.53.0 + google.golang.org/grpc/examples v0.0.0-20211102180624-670c133e568e + google.golang.org/protobuf v1.33.0 +) + +require ( + github.com/bitly/go-simplejson v0.5.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/miekg/dns v1.1.50 // indirect + github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/urfave/cli/v2 v2.25.7 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + go.etcd.io/bbolt v1.4.0 // indirect + golang.org/x/mod v0.9.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.7.0 // indirect + google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect +) diff --git a/client/grpc/go.sum b/client/grpc/go.sum new file mode 100644 index 00000000..ba70cbac --- /dev/null +++ b/client/grpc/go.sum @@ -0,0 +1,175 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go-micro.dev/v5 v5.5.1-0.20250507183911-01b8394c8119 h1:hKzFHTSwaYvPS0sTrWpUcS2HFFbJte//n91NMWFFk4k= +go-micro.dev/v5 v5.5.1-0.20250507183911-01b8394c8119/go.mod h1:pplSGFQk5So43OrzGAg1gH434XydWybOTyFhv/nR0pA= +go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= +go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/grpc/examples v0.0.0-20211102180624-670c133e568e h1:m7aQHHqd0q89mRwhwS9Bx2rjyl/hsFAeta+uGrHsQaU= +google.golang.org/grpc/examples v0.0.0-20211102180624-670c133e568e/go.mod h1:gID3PKrg7pWKntu9Ss6zTLJ0ttC0X9IHgREOCZwbCVU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/client/grpc/grpc.go b/client/grpc/grpc.go new file mode 100644 index 00000000..493f0524 --- /dev/null +++ b/client/grpc/grpc.go @@ -0,0 +1,709 @@ +// Package grpc provides a gRPC client +package grpc + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "reflect" + "strings" + "sync/atomic" + "time" + + "go-micro.dev/v5/broker" + "go-micro.dev/v5/client" + "go-micro.dev/v5/cmd" + raw "go-micro.dev/v5/codec/bytes" + "go-micro.dev/v5/errors" + "go-micro.dev/v5/metadata" + "go-micro.dev/v5/registry" + "go-micro.dev/v5/selector" + pnet "go-micro.dev/v5/util/net" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/encoding" + gmetadata "google.golang.org/grpc/metadata" +) + +type grpcClient struct { + opts client.Options + pool *pool + once atomic.Value +} + +func init() { + cmd.DefaultClients["grpc"] = NewClient + + encoding.RegisterCodec(wrapCodec{jsonCodec{}}) + encoding.RegisterCodec(wrapCodec{protoCodec{}}) + encoding.RegisterCodec(wrapCodec{bytesCodec{}}) +} + +// secure returns the dial option for whether its a secure or insecure connection. +func (g *grpcClient) secure(addr string) grpc.DialOption { + // first we check if theres'a tls config + if g.opts.Context != nil { + if v := g.opts.Context.Value(tlsAuth{}); v != nil { + tls := v.(*tls.Config) + creds := credentials.NewTLS(tls) + // return tls config if it exists + return grpc.WithTransportCredentials(creds) + } + } + + // default config + tlsConfig := &tls.Config{} + defaultCreds := grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)) + + // check if the address is prepended with https + if strings.HasPrefix(addr, "https://") { + return defaultCreds + } + + // if no port is specified or port is 443 default to tls + _, port, err := net.SplitHostPort(addr) + // assuming with no port its going to be secured + if port == "443" { + return defaultCreds + } else if err != nil && strings.Contains(err.Error(), "missing port in address") { + return defaultCreds + } + + // other fallback to insecure + return grpc.WithInsecure() +} + +func (g *grpcClient) next(request client.Request, opts client.CallOptions) (selector.Next, error) { + service, address, _ := pnet.Proxy(request.Service(), opts.Address) + + // return remote address + if len(address) > 0 { + return func() (*registry.Node, error) { + return ®istry.Node{ + Address: address[0], + }, nil + }, nil + } + + // get next nodes from the selector + next, err := g.opts.Selector.Select(service, opts.SelectOptions...) + if err != nil { + if err == selector.ErrNotFound { + return nil, errors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error()) + } + return nil, errors.InternalServerError("go.micro.client", "error selecting %s node: %s", service, err.Error()) + } + + return next, nil +} + +func (g *grpcClient) call(ctx context.Context, node *registry.Node, req client.Request, rsp interface{}, opts client.CallOptions) error { + var header map[string]string + + address := node.Address + + if md, ok := metadata.FromContext(ctx); ok { + header = make(map[string]string, len(md)) + for k, v := range md { + header[strings.ToLower(k)] = v + } + } else { + header = make(map[string]string) + } + + // set timeout in nanoseconds + header["timeout"] = fmt.Sprintf("%d", opts.RequestTimeout) + // set the content type for the request + header["x-content-type"] = req.ContentType() + + md := gmetadata.New(header) + ctx = gmetadata.NewOutgoingContext(ctx, md) + + cf, err := g.newGRPCCodec(req.ContentType()) + if err != nil { + return errors.InternalServerError("go.micro.client", err.Error()) + } + + maxRecvMsgSize := g.maxRecvMsgSizeValue() + maxSendMsgSize := g.maxSendMsgSizeValue() + + var grr error + + var dialCtx context.Context + var cancel context.CancelFunc + if opts.DialTimeout > 0 { + dialCtx, cancel = context.WithTimeout(ctx, opts.DialTimeout) + } else { + dialCtx, cancel = context.WithCancel(ctx) + } + defer cancel() + + grpcDialOptions := []grpc.DialOption{ + g.secure(address), + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(maxRecvMsgSize), + grpc.MaxCallSendMsgSize(maxSendMsgSize), + ), + } + + if opts := g.getGrpcDialOptions(); opts != nil { + grpcDialOptions = append(grpcDialOptions, opts...) + } + + cc, err := g.pool.getConn(dialCtx, address, grpcDialOptions...) + if err != nil { + return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error sending request: %v", err)) + } + defer func() { + // defer execution of release + g.pool.release(address, cc, grr) + }() + + ch := make(chan error, 1) + + go func() { + grpcCallOptions := []grpc.CallOption{ + grpc.ForceCodec(cf), + grpc.CallContentSubtype(cf.Name())} + if opts := callOpts(opts); opts != nil { + grpcCallOptions = append(grpcCallOptions, opts...) + } + err := cc.Invoke(ctx, methodToGRPC(req.Service(), req.Endpoint()), req.Body(), rsp, grpcCallOptions...) + ch <- microError(err) + }() + + select { + case err := <-ch: + grr = err + case <-ctx.Done(): + grr = errors.Timeout("go.micro.client", "%v", ctx.Err()) + } + + return grr +} + +func (g *grpcClient) stream(ctx context.Context, node *registry.Node, req client.Request, rsp interface{}, opts client.CallOptions) error { + var header map[string]string + + address := node.Address + + if md, ok := metadata.FromContext(ctx); ok { + header = make(map[string]string, len(md)) + for k, v := range md { + header[k] = v + } + } else { + header = make(map[string]string) + } + + // set timeout in nanoseconds + if opts.StreamTimeout > time.Duration(0) { + header["timeout"] = fmt.Sprintf("%d", opts.StreamTimeout) + } + // set the content type for the request + header["x-content-type"] = req.ContentType() + + md := gmetadata.New(header) + + // WebSocket connection adds the `Connection: Upgrade` header. + // But as per the HTTP/2 spec, the `Connection` header makes the request malformed + delete(md, "connection") + + ctx = gmetadata.NewOutgoingContext(ctx, md) + + cf, err := g.newGRPCCodec(req.ContentType()) + if err != nil { + return errors.InternalServerError("go.micro.client", err.Error()) + } + + var dialCtx context.Context + var cancel context.CancelFunc + if opts.DialTimeout > 0 { + dialCtx, cancel = context.WithTimeout(ctx, opts.DialTimeout) + } else { + dialCtx, cancel = context.WithCancel(ctx) + } + defer cancel() + + wc := wrapCodec{cf} + + grpcDialOptions := []grpc.DialOption{ + g.secure(address), + } + + if opts := g.getGrpcDialOptions(); opts != nil { + grpcDialOptions = append(grpcDialOptions, opts...) + } + + cc, err := g.pool.getConn(dialCtx, address, grpcDialOptions...) + if err != nil { + return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error sending request: %v", err)) + } + + desc := &grpc.StreamDesc{ + StreamName: req.Service() + req.Endpoint(), + ClientStreams: true, + ServerStreams: true, + } + + grpcCallOptions := []grpc.CallOption{ + grpc.ForceCodec(wc), + grpc.CallContentSubtype(cf.Name()), + } + if opts := callOpts(opts); opts != nil { + grpcCallOptions = append(grpcCallOptions, opts...) + } + + // create a new canceling context + newCtx, cancel := context.WithCancel(ctx) + + st, err := cc.NewStream(newCtx, desc, methodToGRPC(req.Service(), req.Endpoint()), grpcCallOptions...) + if err != nil { + // we need to cleanup as we dialed and created a context + // cancel the context + cancel() + // close the connection + cc.Close() + // now return the error + return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error creating stream: %v", err)) + } + + codec := &grpcCodec{ + s: st, + c: wc, + } + + // set request codec + if r, ok := req.(*grpcRequest); ok { + r.codec = codec + } + + // setup the stream response + stream := &grpcStream{ + context: ctx, + request: req, + response: &response{ + conn: cc.ClientConn, + stream: st, + codec: cf, + gcodec: codec, + }, + stream: st, + cancel: cancel, + release: func(err error) { + g.pool.release(address, cc, err) + }, + } + + // set the stream as the response + val := reflect.ValueOf(rsp).Elem() + val.Set(reflect.ValueOf(stream).Elem()) + return nil +} + +func (g *grpcClient) poolMaxStreams() int { + if g.opts.Context == nil { + return DefaultPoolMaxStreams + } + v := g.opts.Context.Value(poolMaxStreams{}) + if v == nil { + return DefaultPoolMaxStreams + } + return v.(int) +} + +func (g *grpcClient) poolMaxIdle() int { + if g.opts.Context == nil { + return DefaultPoolMaxIdle + } + v := g.opts.Context.Value(poolMaxIdle{}) + if v == nil { + return DefaultPoolMaxIdle + } + return v.(int) +} + +func (g *grpcClient) maxRecvMsgSizeValue() int { + if g.opts.Context == nil { + return DefaultMaxRecvMsgSize + } + v := g.opts.Context.Value(maxRecvMsgSizeKey{}) + if v == nil { + return DefaultMaxRecvMsgSize + } + return v.(int) +} + +func (g *grpcClient) maxSendMsgSizeValue() int { + if g.opts.Context == nil { + return DefaultMaxSendMsgSize + } + v := g.opts.Context.Value(maxSendMsgSizeKey{}) + if v == nil { + return DefaultMaxSendMsgSize + } + return v.(int) +} + +func (g *grpcClient) newGRPCCodec(contentType string) (encoding.Codec, error) { + codecs := make(map[string]encoding.Codec) + if g.opts.Context != nil { + if v := g.opts.Context.Value(codecsKey{}); v != nil { + codecs = v.(map[string]encoding.Codec) + } + } + if c, ok := codecs[contentType]; ok { + return wrapCodec{c}, nil + } + if c, ok := defaultGRPCCodecs[contentType]; ok { + return wrapCodec{c}, nil + } + return nil, fmt.Errorf("unsupported Content-Type: %s", contentType) +} + +func (g *grpcClient) Init(opts ...client.Option) error { + size := g.opts.PoolSize + ttl := g.opts.PoolTTL + + for _, o := range opts { + o(&g.opts) + } + + // update pool configuration if the options changed + if size != g.opts.PoolSize || ttl != g.opts.PoolTTL { + g.pool.Lock() + g.pool.size = g.opts.PoolSize + g.pool.ttl = int64(g.opts.PoolTTL.Seconds()) + g.pool.Unlock() + } + + return nil +} + +func (g *grpcClient) Options() client.Options { + return g.opts +} + +func (g *grpcClient) NewMessage(topic string, msg interface{}, opts ...client.MessageOption) client.Message { + return newGRPCEvent(topic, msg, g.opts.ContentType, opts...) +} + +func (g *grpcClient) NewRequest(service, method string, req interface{}, reqOpts ...client.RequestOption) client.Request { + return newGRPCRequest(service, method, req, g.opts.ContentType, reqOpts...) +} + +func (g *grpcClient) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error { + if req == nil { + return errors.InternalServerError("go.micro.client", "req is nil") + } else if rsp == nil { + return errors.InternalServerError("go.micro.client", "rsp is nil") + } + // make a copy of call opts + callOpts := g.opts.CallOptions + for _, opt := range opts { + opt(&callOpts) + } + + next, err := g.next(req, callOpts) + if err != nil { + return err + } + + // check if we already have a deadline + d, ok := ctx.Deadline() + if !ok { + // no deadline so we create a new one + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, callOpts.RequestTimeout) + defer cancel() + } else { + // got a deadline so no need to setup context + // but we need to set the timeout we pass along + opt := client.WithRequestTimeout(time.Until(d)) + opt(&callOpts) + } + + // should we noop right here? + select { + case <-ctx.Done(): + return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408) + default: + } + + // make copy of call method + gcall := g.call + + // wrap the call in reverse + for i := len(callOpts.CallWrappers); i > 0; i-- { + gcall = callOpts.CallWrappers[i-1](gcall) + } + + // return errors.New("go.micro.client", "request timeout", 408) + call := func(i int) error { + // call backoff first. Someone may want an initial start delay + t, err := callOpts.Backoff(ctx, req, i) + if err != nil { + return errors.InternalServerError("go.micro.client", err.Error()) + } + + // only sleep if greater than 0 + if t.Seconds() > 0 { + time.Sleep(t) + } + + // select next node + node, err := next() + service := req.Service() + if err != nil { + if err == selector.ErrNotFound { + return errors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error()) + } + return errors.InternalServerError("go.micro.client", "error selecting %s node: %s", service, err.Error()) + } + + // make the call + err = gcall(ctx, node, req, rsp, callOpts) + g.opts.Selector.Mark(service, node, err) + if verr, ok := err.(*errors.Error); ok { + return verr + } + + return err + } + + ch := make(chan error, callOpts.Retries+1) + var gerr error + + for i := 0; i <= callOpts.Retries; i++ { + go func(i int) { + ch <- call(i) + }(i) + + select { + case <-ctx.Done(): + return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408) + case err := <-ch: + // if the call succeeded lets bail early + if err == nil { + return nil + } + + retry, rerr := callOpts.Retry(ctx, req, i, err) + if rerr != nil { + return rerr + } + + if !retry { + return err + } + + gerr = err + } + } + + return gerr +} + +func (g *grpcClient) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) { + // make a copy of call opts + callOpts := g.opts.CallOptions + for _, opt := range opts { + opt(&callOpts) + } + + next, err := g.next(req, callOpts) + if err != nil { + return nil, err + } + + // #200 - streams shouldn't have a request timeout set on the context + + // should we noop right here? + select { + case <-ctx.Done(): + return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408) + default: + } + + // make a copy of stream + gstream := g.stream + + // wrap the call in reverse + for i := len(callOpts.CallWrappers); i > 0; i-- { + gstream = callOpts.CallWrappers[i-1](gstream) + } + + call := func(i int) (client.Stream, error) { + // call backoff first. Someone may want an initial start delay + t, err := callOpts.Backoff(ctx, req, i) + if err != nil { + return nil, errors.InternalServerError("go.micro.client", err.Error()) + } + + // only sleep if greater than 0 + if t.Seconds() > 0 { + time.Sleep(t) + } + + node, err := next() + service := req.Service() + if err != nil { + if err == selector.ErrNotFound { + return nil, errors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error()) + } + return nil, errors.InternalServerError("go.micro.client", "error selecting %s node: %s", service, err.Error()) + } + + // make the call + stream := &grpcStream{} + err = g.stream(ctx, node, req, stream, callOpts) + + g.opts.Selector.Mark(service, node, err) + return stream, err + } + + type response struct { + stream client.Stream + err error + } + + ch := make(chan response, callOpts.Retries+1) + var grr error + + for i := 0; i <= callOpts.Retries; i++ { + go func(i int) { + s, err := call(i) + ch <- response{s, err} + }(i) + + select { + case <-ctx.Done(): + return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408) + case rsp := <-ch: + // if the call succeeded lets bail early + if rsp.err == nil { + return rsp.stream, nil + } + + retry, rerr := callOpts.Retry(ctx, req, i, err) + if rerr != nil { + return nil, rerr + } + + if !retry { + return nil, rsp.err + } + + grr = rsp.err + } + } + + return nil, grr +} + +func (g *grpcClient) Publish(ctx context.Context, p client.Message, opts ...client.PublishOption) error { + var options client.PublishOptions + for _, o := range opts { + o(&options) + } + + md, ok := metadata.FromContext(ctx) + if !ok { + md = make(map[string]string) + } + md["Content-Type"] = p.ContentType() + md["Micro-Topic"] = p.Topic() + + cf, err := g.newGRPCCodec(p.ContentType()) + if err != nil { + return errors.InternalServerError("go.micro.client", err.Error()) + } + + var body []byte + + // passed in raw data + if d, ok := p.Payload().(*raw.Frame); ok { + body = d.Data + } else { + // set the body + b, err := cf.Marshal(p.Payload()) + if err != nil { + return errors.InternalServerError("go.micro.client", err.Error()) + } + body = b + } + + if !g.once.Load().(bool) { + if err = g.opts.Broker.Connect(); err != nil { + return errors.InternalServerError("go.micro.client", err.Error()) + } + g.once.Store(true) + } + + topic := p.Topic() + + // get the exchange + if len(options.Exchange) > 0 { + topic = options.Exchange + } + + return g.opts.Broker.Publish(topic, &broker.Message{ + Header: md, + Body: body, + }, broker.PublishContext(options.Context)) +} + +func (g *grpcClient) String() string { + return "grpc" +} + +func (g *grpcClient) getGrpcDialOptions() []grpc.DialOption { + if g.opts.CallOptions.Context == nil { + return nil + } + + v := g.opts.CallOptions.Context.Value(grpcDialOptions{}) + + if v == nil { + return nil + } + + opts, ok := v.([]grpc.DialOption) + + if !ok { + return nil + } + + return opts +} + +func newClient(opts ...client.Option) client.Client { + options := client.NewOptions() + // default content type for grpc + options.ContentType = "application/grpc+proto" + + for _, o := range opts { + o(&options) + } + + rc := &grpcClient{ + opts: options, + } + rc.once.Store(false) + + rc.pool = newPool(options.PoolSize, options.PoolTTL, rc.poolMaxIdle(), rc.poolMaxStreams()) + + c := client.Client(rc) + + // wrap in reverse + for i := len(options.Wrappers); i > 0; i-- { + c = options.Wrappers[i-1](c) + } + + return c +} + +func NewClient(opts ...client.Option) client.Client { + return newClient(opts...) +} diff --git a/client/grpc/grpc_pool.go b/client/grpc/grpc_pool.go new file mode 100644 index 00000000..5507a9e6 --- /dev/null +++ b/client/grpc/grpc_pool.go @@ -0,0 +1,218 @@ +package grpc + +import ( + "context" + "sync" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/connectivity" +) + +type pool struct { + size int + ttl int64 + + // max streams on a *poolConn + maxStreams int + // max idle conns + maxIdle int + + sync.Mutex + conns map[string]*streamsPool +} + +type streamsPool struct { + // head of list + head *poolConn + // busy conns list + busy *poolConn + // the size of list + count int + // idle conn + idle int +} + +type poolConn struct { + // grpc conn + *grpc.ClientConn + err error + addr string + + // pool and streams pool + pool *pool + sp *streamsPool + streams int + created int64 + + // list + pre *poolConn + next *poolConn + in bool +} + +func newPool(size int, ttl time.Duration, idle int, ms int) *pool { + if ms <= 0 { + ms = 1 + } + if idle < 0 { + idle = 0 + } + return &pool{ + size: size, + ttl: int64(ttl.Seconds()), + maxStreams: ms, + maxIdle: idle, + conns: make(map[string]*streamsPool), + } +} + +func (p *pool) getConn(dialCtx context.Context, addr string, opts ...grpc.DialOption) (*poolConn, error) { + now := time.Now().Unix() + p.Lock() + sp, ok := p.conns[addr] + if !ok { + sp = &streamsPool{head: &poolConn{}, busy: &poolConn{}, count: 0, idle: 0} + p.conns[addr] = sp + } + // while we have conns check streams and then return one + // otherwise we'll create a new conn + conn := sp.head.next + for conn != nil { + // check conn state + // https://github.com/grpc/grpc/blob/master/doc/connectivity-semantics-and-api.md + switch conn.GetState() { + case connectivity.Connecting: + conn = conn.next + continue + case connectivity.Shutdown: + next := conn.next + if conn.streams == 0 { + removeConn(conn) + sp.idle-- + } + conn = next + continue + case connectivity.TransientFailure: + next := conn.next + if conn.streams == 0 { + removeConn(conn) + conn.ClientConn.Close() + sp.idle-- + } + conn = next + continue + case connectivity.Ready: + case connectivity.Idle: + } + // a old conn + if now-conn.created > p.ttl { + next := conn.next + if conn.streams == 0 { + removeConn(conn) + conn.ClientConn.Close() + sp.idle-- + } + conn = next + continue + } + // a busy conn + if conn.streams >= p.maxStreams { + next := conn.next + removeConn(conn) + addConnAfter(conn, sp.busy) + conn = next + continue + } + // a idle conn + if conn.streams == 0 { + sp.idle-- + } + // a good conn + conn.streams++ + p.Unlock() + return conn, nil + } + p.Unlock() + + // create new conn + cc, err := grpc.DialContext(dialCtx, addr, opts...) + if err != nil { + return nil, err + } + conn = &poolConn{cc, nil, addr, p, sp, 1, time.Now().Unix(), nil, nil, false} + + // add conn to streams pool + p.Lock() + if sp.count < p.size { + addConnAfter(conn, sp.head) + } + p.Unlock() + + return conn, nil +} + +func (p *pool) release(addr string, conn *poolConn, err error) { + p.Lock() + p, sp, created := conn.pool, conn.sp, conn.created + // try to add conn + if !conn.in && sp.count < p.size { + addConnAfter(conn, sp.head) + } + if !conn.in { + p.Unlock() + conn.ClientConn.Close() + return + } + // a busy conn + if conn.streams >= p.maxStreams { + removeConn(conn) + addConnAfter(conn, sp.head) + } + conn.streams-- + // if streams == 0, we can do something + if conn.streams == 0 { + // 1. it has errored + // 2. too many idle conn or + // 3. conn is too old + now := time.Now().Unix() + if err != nil || sp.idle >= p.maxIdle || now-created > p.ttl { + removeConn(conn) + p.Unlock() + conn.ClientConn.Close() + return + } + sp.idle++ + } + p.Unlock() +} + +func (conn *poolConn) Close() { + conn.pool.release(conn.addr, conn, conn.err) +} + +func removeConn(conn *poolConn) { + if conn.pre != nil { + conn.pre.next = conn.next + } + if conn.next != nil { + conn.next.pre = conn.pre + } + conn.pre = nil + conn.next = nil + conn.in = false + conn.sp.count-- + return +} + +func addConnAfter(conn *poolConn, after *poolConn) { + conn.next = after.next + conn.pre = after + if after.next != nil { + after.next.pre = conn + } + after.next = conn + conn.in = true + conn.sp.count++ + return +} diff --git a/client/grpc/grpc_pool_test.go b/client/grpc/grpc_pool_test.go new file mode 100644 index 00000000..3e714163 --- /dev/null +++ b/client/grpc/grpc_pool_test.go @@ -0,0 +1,63 @@ +package grpc + +import ( + "context" + "net" + "testing" + "time" + + "google.golang.org/grpc" + pb "google.golang.org/grpc/examples/helloworld/helloworld" +) + +func testPool(t *testing.T, size int, ttl time.Duration, idle int, ms int) { + // setup server + l, err := net.Listen("tcp", ":0") + if err != nil { + t.Errorf("failed to listen: %v", err) + } + defer l.Close() + + s := grpc.NewServer() + pb.RegisterGreeterServer(s, &greeterServer{}) + + go s.Serve(l) + defer s.Stop() + + // zero pool + p := newPool(size, ttl, idle, ms) + + for i := 0; i < 10; i++ { + // get a conn + cc, err := p.getConn(context.TODO(), l.Addr().String(), grpc.WithInsecure()) + if err != nil { + t.Fatal(err) + } + + rsp := pb.HelloReply{} + + err = cc.Invoke(context.TODO(), "/helloworld.Greeter/SayHello", &pb.HelloRequest{Name: "John"}, &rsp) + if err != nil { + t.Fatal(err) + } + + if rsp.Message != "Hello John" { + t.Errorf("Got unexpected response %v", rsp.Message) + } + + // release the conn + p.release(l.Addr().String(), cc, nil) + + p.Lock() + if i := p.conns[l.Addr().String()].count; i > size { + p.Unlock() + t.Errorf("pool size %d is greater than expected %d", i, size) + } + p.Unlock() + } +} + +func TestGRPCPool(t *testing.T) { + testPool(t, 0, time.Minute, 10, 2) + testPool(t, 2, time.Minute, 10, 1) +} diff --git a/client/grpc/grpc_test.go b/client/grpc/grpc_test.go new file mode 100644 index 00000000..ccefa31c --- /dev/null +++ b/client/grpc/grpc_test.go @@ -0,0 +1,112 @@ +package grpc + +import ( + "context" + "net" + "testing" + + "go-micro.dev/v5/client" + "go-micro.dev/v5/errors" + "go-micro.dev/v5/registry" + "go-micro.dev/v5/selector" + pgrpc "google.golang.org/grpc" + pb "google.golang.org/grpc/examples/helloworld/helloworld" +) + +// server is used to implement helloworld.GreeterServer. +type greeterServer struct { + pb.UnimplementedGreeterServer +} + +// SayHello implements helloworld.GreeterServer. +func (g *greeterServer) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { + if in.Name == "Error" { + return nil, &errors.Error{Id: "1", Code: 99, Detail: "detail"} + } + return &pb.HelloReply{Message: "Hello " + in.Name}, nil +} + +func TestGRPCClient(t *testing.T) { + l, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + defer l.Close() + + s := pgrpc.NewServer() + pb.RegisterGreeterServer(s, &greeterServer{}) + + go s.Serve(l) + defer s.Stop() + + // create mock registry + r := registry.NewMemoryRegistry() + + // register service + r.Register(®istry.Service{ + Name: "helloworld", + Version: "test", + Nodes: []*registry.Node{ + { + Id: "test-1", + Address: l.Addr().String(), + Metadata: map[string]string{ + "protocol": "grpc", + }, + }, + }, + }) + + // create selector + se := selector.NewSelector( + selector.Registry(r), + ) + + // create client + c := NewClient( + client.Registry(r), + client.Selector(se), + ) + + testMethods := []string{ + "/helloworld.Greeter/SayHello", + "Greeter.SayHello", + } + + for _, method := range testMethods { + req := c.NewRequest("helloworld", method, &pb.HelloRequest{ + Name: "John", + }) + + rsp := pb.HelloReply{} + + err = c.Call(context.TODO(), req, &rsp) + if err != nil { + t.Fatal(err) + } + + if rsp.Message != "Hello John" { + t.Fatalf("Got unexpected response %v", rsp.Message) + } + } + + req := c.NewRequest("helloworld", "/helloworld.Greeter/SayHello", &pb.HelloRequest{ + Name: "Error", + }) + + rsp := pb.HelloReply{} + + err = c.Call(context.TODO(), req, &rsp) + if err == nil { + t.Fatal("nil error received") + } + + verr, ok := err.(*errors.Error) + if !ok { + t.Fatalf("invalid error received %#+v\n", err) + } + + if verr.Code != 99 && verr.Id != "1" && verr.Detail != "detail" { + t.Fatalf("invalid error received %#+v\n", verr) + } +} diff --git a/client/grpc/message.go b/client/grpc/message.go new file mode 100644 index 00000000..6caaa684 --- /dev/null +++ b/client/grpc/message.go @@ -0,0 +1,40 @@ +package grpc + +import ( + "go-micro.dev/v5/client" +) + +type grpcEvent struct { + topic string + contentType string + payload interface{} +} + +func newGRPCEvent(topic string, payload interface{}, contentType string, opts ...client.MessageOption) client.Message { + var options client.MessageOptions + for _, o := range opts { + o(&options) + } + + if len(options.ContentType) > 0 { + contentType = options.ContentType + } + + return &grpcEvent{ + payload: payload, + topic: topic, + contentType: contentType, + } +} + +func (g *grpcEvent) ContentType() string { + return g.contentType +} + +func (g *grpcEvent) Topic() string { + return g.topic +} + +func (g *grpcEvent) Payload() interface{} { + return g.payload +} diff --git a/client/grpc/options.go b/client/grpc/options.go new file mode 100644 index 00000000..35b53fa5 --- /dev/null +++ b/client/grpc/options.go @@ -0,0 +1,143 @@ +// Package grpc provides a gRPC options +package grpc + +import ( + "context" + "crypto/tls" + + "go-micro.dev/v5/client" + "google.golang.org/grpc" + "google.golang.org/grpc/encoding" +) + +var ( + // DefaultPoolMaxStreams maximum streams on a connectioin + // (20). + DefaultPoolMaxStreams = 20 + + // DefaultPoolMaxIdle maximum idle conns of a pool + // (50). + DefaultPoolMaxIdle = 50 + + // DefaultMaxRecvMsgSize maximum message that client can receive + // (4 MB). + DefaultMaxRecvMsgSize = 1024 * 1024 * 4 + + // DefaultMaxSendMsgSize maximum message that client can send + // (4 MB). + DefaultMaxSendMsgSize = 1024 * 1024 * 4 +) + +type poolMaxStreams struct{} +type poolMaxIdle struct{} +type codecsKey struct{} +type tlsAuth struct{} +type maxRecvMsgSizeKey struct{} +type maxSendMsgSizeKey struct{} +type grpcDialOptions struct{} +type grpcCallOptions struct{} + +// maximum streams on a connectioin. +func PoolMaxStreams(n int) client.Option { + return func(o *client.Options) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, poolMaxStreams{}, n) + } +} + +// maximum idle conns of a pool. +func PoolMaxIdle(d int) client.Option { + return func(o *client.Options) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, poolMaxIdle{}, d) + } +} + +// gRPC Codec to be used to encode/decode requests for a given content type. +func Codec(contentType string, c encoding.Codec) client.Option { + return func(o *client.Options) { + codecs := make(map[string]encoding.Codec) + if o.Context == nil { + o.Context = context.Background() + } + if v := o.Context.Value(codecsKey{}); v != nil { + codecs = v.(map[string]encoding.Codec) + } + codecs[contentType] = c + o.Context = context.WithValue(o.Context, codecsKey{}, codecs) + } +} + +// AuthTLS should be used to setup a secure authentication using TLS. +func AuthTLS(t *tls.Config) client.Option { + return func(o *client.Options) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, tlsAuth{}, t) + } +} + +// MaxRecvMsgSize set the maximum size of message that client can receive. +func MaxRecvMsgSize(s int) client.Option { + return func(o *client.Options) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, maxRecvMsgSizeKey{}, s) + } +} + +// MaxSendMsgSize set the maximum size of message that client can send. +func MaxSendMsgSize(s int) client.Option { + return func(o *client.Options) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, maxSendMsgSizeKey{}, s) + } +} + +// DialOptions to be used to configure gRPC dial options. +func DialOptions(opts ...grpc.DialOption) client.CallOption { + return func(o *client.CallOptions) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, grpcDialOptions{}, opts) + } +} + +// CallOptions to be used to configure gRPC call options. +func CallOptions(opts ...grpc.CallOption) client.CallOption { + return func(o *client.CallOptions) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, grpcCallOptions{}, opts) + } +} + +func callOpts(opts client.CallOptions) []grpc.CallOption { + if opts.Context == nil { + return nil + } + + v := opts.Context.Value(grpcCallOptions{}) + + if v == nil { + return nil + } + + options, ok := v.([]grpc.CallOption) + + if !ok { + return nil + } + + return options +} diff --git a/client/grpc/request.go b/client/grpc/request.go new file mode 100644 index 00000000..fd5716de --- /dev/null +++ b/client/grpc/request.go @@ -0,0 +1,87 @@ +package grpc + +import ( + "fmt" + "strings" + + "go-micro.dev/v5/client" + "go-micro.dev/v5/codec" +) + +type grpcRequest struct { + service string + method string + contentType string + request interface{} + opts client.RequestOptions + codec codec.Codec +} + +// service Struct.Method /service.Struct/Method. +func methodToGRPC(service, method string) string { + // no method or already grpc method + if len(method) == 0 || method[0] == '/' { + return method + } + + // assume method is Foo.Bar + mParts := strings.Split(method, ".") + if len(mParts) != 2 { + return method + } + + if len(service) == 0 { + return fmt.Sprintf("/%s/%s", mParts[0], mParts[1]) + } + + // return /pkg.Foo/Bar + return fmt.Sprintf("/%s.%s/%s", service, mParts[0], mParts[1]) +} + +func newGRPCRequest(service, method string, request interface{}, contentType string, reqOpts ...client.RequestOption) client.Request { + var opts client.RequestOptions + for _, o := range reqOpts { + o(&opts) + } + + // set the content-type specified + if len(opts.ContentType) > 0 { + contentType = opts.ContentType + } + + return &grpcRequest{ + service: service, + method: method, + request: request, + contentType: contentType, + opts: opts, + } +} + +func (g *grpcRequest) ContentType() string { + return g.contentType +} + +func (g *grpcRequest) Service() string { + return g.service +} + +func (g *grpcRequest) Method() string { + return g.method +} + +func (g *grpcRequest) Endpoint() string { + return g.method +} + +func (g *grpcRequest) Codec() codec.Writer { + return g.codec +} + +func (g *grpcRequest) Body() interface{} { + return g.request +} + +func (g *grpcRequest) Stream() bool { + return g.opts.Stream +} diff --git a/client/grpc/request_test.go b/client/grpc/request_test.go new file mode 100644 index 00000000..c73d675b --- /dev/null +++ b/client/grpc/request_test.go @@ -0,0 +1,41 @@ +package grpc + +import ( + "testing" +) + +func TestMethodToGRPC(t *testing.T) { + testData := []struct { + service string + method string + expect string + }{ + { + "helloworld", + "Greeter.SayHello", + "/helloworld.Greeter/SayHello", + }, + { + "helloworld", + "/helloworld.Greeter/SayHello", + "/helloworld.Greeter/SayHello", + }, + { + "", + "/helloworld.Greeter/SayHello", + "/helloworld.Greeter/SayHello", + }, + { + "", + "Greeter.SayHello", + "/Greeter/SayHello", + }, + } + + for _, d := range testData { + method := methodToGRPC(d.service, d.method) + if method != d.expect { + t.Fatalf("expected %s got %s", d.expect, method) + } + } +} diff --git a/client/grpc/response.go b/client/grpc/response.go new file mode 100644 index 00000000..6e3a529f --- /dev/null +++ b/client/grpc/response.go @@ -0,0 +1,44 @@ +package grpc + +import ( + "strings" + + "go-micro.dev/v5/codec" + "go-micro.dev/v5/codec/bytes" + "google.golang.org/grpc" + "google.golang.org/grpc/encoding" +) + +type response struct { + conn *grpc.ClientConn + stream grpc.ClientStream + codec encoding.Codec + gcodec codec.Codec +} + +// Read the response. +func (r *response) Codec() codec.Reader { + return r.gcodec +} + +// read the header. +func (r *response) Header() map[string]string { + md, err := r.stream.Header() + if err != nil { + return map[string]string{} + } + hdr := make(map[string]string, len(md)) + for k, v := range md { + hdr[k] = strings.Join(v, ",") + } + return hdr +} + +// Read the undecoded response. +func (r *response) Read() ([]byte, error) { + f := &bytes.Frame{} + if err := r.gcodec.ReadBody(f); err != nil { + return nil, err + } + return f.Data, nil +} diff --git a/client/grpc/stream.go b/client/grpc/stream.go new file mode 100644 index 00000000..c1e29240 --- /dev/null +++ b/client/grpc/stream.go @@ -0,0 +1,84 @@ +package grpc + +import ( + "context" + "io" + "sync" + + "go-micro.dev/v5/client" + "google.golang.org/grpc" +) + +// Implements the streamer interface. +type grpcStream struct { + sync.RWMutex + closed bool + err error + stream grpc.ClientStream + request client.Request + response client.Response + context context.Context + cancel func() + release func(error) +} + +func (g *grpcStream) Context() context.Context { + return g.context +} + +func (g *grpcStream) Request() client.Request { + return g.request +} + +func (g *grpcStream) Response() client.Response { + return g.response +} + +func (g *grpcStream) Send(msg interface{}) error { + if err := g.stream.SendMsg(msg); err != nil { + g.setError(err) + return err + } + return nil +} + +func (g *grpcStream) Recv(msg interface{}) (err error) { + if err = g.stream.RecvMsg(msg); err != nil { + if err != io.EOF { + g.setError(err) + } + return err + } + return +} + +func (g *grpcStream) Error() error { + g.RLock() + defer g.RUnlock() + return g.err +} + +func (g *grpcStream) setError(e error) { + g.Lock() + g.err = e + g.Unlock() +} + +func (g *grpcStream) CloseSend() error { + return g.stream.CloseSend() +} + +func (g *grpcStream) Close() error { + g.Lock() + defer g.Unlock() + + if g.closed { + return nil + } + // cancel the context + g.cancel() + g.closed = true + // release back to pool + g.release(g.err) + return nil +} diff --git a/server/grpc/codec.go b/server/grpc/codec.go new file mode 100644 index 00000000..e05b3bb0 --- /dev/null +++ b/server/grpc/codec.go @@ -0,0 +1,186 @@ +package grpc + +import ( + "encoding/json" + "strings" + + b "bytes" + + "github.com/golang/protobuf/jsonpb" + "github.com/golang/protobuf/proto" + "go-micro.dev/v5/codec" + "go-micro.dev/v5/codec/bytes" + "google.golang.org/grpc" + "google.golang.org/grpc/encoding" + "google.golang.org/grpc/metadata" +) + +type jsonCodec struct{} +type bytesCodec struct{} +type protoCodec struct{} +type wrapCodec struct{ encoding.Codec } + +var jsonpbMarshaler = &jsonpb.Marshaler{ + EnumsAsInts: false, + EmitDefaults: false, + OrigName: true, +} + +var ( + defaultGRPCCodecs = map[string]encoding.Codec{ + "application/json": jsonCodec{}, + "application/proto": protoCodec{}, + "application/protobuf": protoCodec{}, + "application/octet-stream": protoCodec{}, + "application/grpc": protoCodec{}, + "application/grpc+json": jsonCodec{}, + "application/grpc+proto": protoCodec{}, + "application/grpc+bytes": bytesCodec{}, + } +) + +func (w wrapCodec) String() string { + return w.Codec.Name() +} + +func (w wrapCodec) Marshal(v interface{}) ([]byte, error) { + b, ok := v.(*bytes.Frame) + if ok { + return b.Data, nil + } + return w.Codec.Marshal(v) +} + +func (w wrapCodec) Unmarshal(data []byte, v interface{}) error { + b, ok := v.(*bytes.Frame) + if ok { + b.Data = data + return nil + } + if v == nil { + return nil + } + return w.Codec.Unmarshal(data, v) +} + +func (protoCodec) Marshal(v interface{}) ([]byte, error) { + m, ok := v.(proto.Message) + if !ok { + return nil, codec.ErrInvalidMessage + } + return proto.Marshal(m) +} + +func (protoCodec) Unmarshal(data []byte, v interface{}) error { + m, ok := v.(proto.Message) + if !ok { + return codec.ErrInvalidMessage + } + return proto.Unmarshal(data, m) +} + +func (protoCodec) Name() string { + return "proto" +} + +func (jsonCodec) Marshal(v interface{}) ([]byte, error) { + if pb, ok := v.(proto.Message); ok { + s, err := jsonpbMarshaler.MarshalToString(pb) + return []byte(s), err + } + + return json.Marshal(v) +} + +func (jsonCodec) Unmarshal(data []byte, v interface{}) error { + if len(data) == 0 { + return nil + } + if pb, ok := v.(proto.Message); ok { + return jsonpb.Unmarshal(b.NewReader(data), pb) + } + return json.Unmarshal(data, v) +} + +func (jsonCodec) Name() string { + return "json" +} + +func (bytesCodec) Marshal(v interface{}) ([]byte, error) { + b, ok := v.(*[]byte) + if !ok { + return nil, codec.ErrInvalidMessage + } + return *b, nil +} + +func (bytesCodec) Unmarshal(data []byte, v interface{}) error { + b, ok := v.(*[]byte) + if !ok { + return codec.ErrInvalidMessage + } + *b = data + return nil +} + +func (bytesCodec) Name() string { + return "bytes" +} + +type grpcCodec struct { + // headers + id string + target string + method string + endpoint string + + s grpc.ServerStream + c encoding.Codec +} + +func (g *grpcCodec) ReadHeader(m *codec.Message, mt codec.MessageType) error { + md, _ := metadata.FromIncomingContext(g.s.Context()) + if m == nil { + m = new(codec.Message) + } + if m.Header == nil { + m.Header = make(map[string]string, len(md)) + } + for k, v := range md { + m.Header[k] = strings.Join(v, ",") + } + m.Id = g.id + m.Target = g.target + m.Method = g.method + m.Endpoint = g.endpoint + return nil +} + +func (g *grpcCodec) ReadBody(v interface{}) error { + // caller has requested a frame + if f, ok := v.(*bytes.Frame); ok { + return g.s.RecvMsg(f) + } + return g.s.RecvMsg(v) +} + +func (g *grpcCodec) Write(m *codec.Message, v interface{}) error { + // if we don't have a body + if v != nil { + b, err := g.c.Marshal(v) + if err != nil { + return err + } + m.Body = b + } + // write the body using the framing codec + return g.s.SendMsg(&bytes.Frame{Data: m.Body}) +} + +func (g *grpcCodec) Close() error { + return nil +} + +func (g *grpcCodec) String() string { + return "grpc" +} diff --git a/server/grpc/context.go b/server/grpc/context.go new file mode 100644 index 00000000..c4708160 --- /dev/null +++ b/server/grpc/context.go @@ -0,0 +1,16 @@ +package grpc + +import ( + "context" + + "go-micro.dev/v5/server" +) + +func setServerOption(k, v interface{}) server.Option { + return func(o *server.Options) { + if o.Context == nil { + o.Context = context.Background() + } + o.Context = context.WithValue(o.Context, k, v) + } +} diff --git a/server/grpc/error.go b/server/grpc/error.go new file mode 100644 index 00000000..a8034a36 --- /dev/null +++ b/server/grpc/error.go @@ -0,0 +1,42 @@ +package grpc + +import ( + "net/http" + + "go-micro.dev/v5/errors" + "google.golang.org/grpc/codes" +) + +func microError(err *errors.Error) codes.Code { + switch err { + case nil: + return codes.OK + } + + switch err.Code { + case http.StatusOK: + return codes.OK + case http.StatusBadRequest: + return codes.InvalidArgument + case http.StatusRequestTimeout: + return codes.DeadlineExceeded + case http.StatusNotFound: + return codes.NotFound + case http.StatusConflict: + return codes.AlreadyExists + case http.StatusForbidden: + return codes.PermissionDenied + case http.StatusUnauthorized: + return codes.Unauthenticated + case http.StatusPreconditionFailed: + return codes.FailedPrecondition + case http.StatusNotImplemented: + return codes.Unimplemented + case http.StatusInternalServerError: + return codes.Internal + case http.StatusServiceUnavailable: + return codes.Unavailable + } + + return codes.Unknown +} diff --git a/server/grpc/extractor.go b/server/grpc/extractor.go new file mode 100644 index 00000000..4c766c34 --- /dev/null +++ b/server/grpc/extractor.go @@ -0,0 +1,125 @@ +package grpc + +import ( + "fmt" + "reflect" + "strings" + + "go-micro.dev/v5/registry" +) + +func extractValue(v reflect.Type, d int) *registry.Value { + if d == 3 { + return nil + } + if v == nil { + return nil + } + + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + arg := ®istry.Value{ + Name: v.Name(), + Type: v.Name(), + } + + switch v.Kind() { + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + f := v.Field(i) + if f.PkgPath != "" { + continue + } + val := extractValue(f.Type, d+1) + if val == nil { + continue + } + + // if we can find a json tag use it + if tags := f.Tag.Get("json"); len(tags) > 0 { + parts := strings.Split(tags, ",") + if parts[0] == "-" || parts[0] == "omitempty" { + continue + } + val.Name = parts[0] + } + + // if there's no name default it + if len(val.Name) == 0 { + val.Name = v.Field(i).Name + } + + arg.Values = append(arg.Values, val) + } + case reflect.Slice: + p := v.Elem() + if p.Kind() == reflect.Ptr { + p = p.Elem() + } + arg.Type = "[]" + p.Name() + } + + return arg +} + +func extractEndpoint(method reflect.Method) *registry.Endpoint { + if method.PkgPath != "" { + return nil + } + + var rspType, reqType reflect.Type + var stream bool + mt := method.Type + + switch mt.NumIn() { + case 3: + reqType = mt.In(1) + rspType = mt.In(2) + case 4: + reqType = mt.In(2) + rspType = mt.In(3) + default: + return nil + } + + // are we dealing with a stream? + switch rspType.Kind() { + case reflect.Func, reflect.Interface: + stream = true + } + + request := extractValue(reqType, 0) + response := extractValue(rspType, 0) + + ep := ®istry.Endpoint{ + Name: method.Name, + Request: request, + Response: response, + Metadata: make(map[string]string), + } + + if stream { + ep.Metadata = map[string]string{ + "stream": fmt.Sprintf("%v", stream), + } + } + + return ep +} + +func extractSubValue(typ reflect.Type) *registry.Value { + var reqType reflect.Type + switch typ.NumIn() { + case 1: + reqType = typ.In(0) + case 2: + reqType = typ.In(1) + case 3: + reqType = typ.In(2) + default: + return nil + } + return extractValue(reqType, 0) +} diff --git a/server/grpc/extractor_test.go b/server/grpc/extractor_test.go new file mode 100644 index 00000000..ccc08aa2 --- /dev/null +++ b/server/grpc/extractor_test.go @@ -0,0 +1,64 @@ +package grpc + +import ( + "context" + "reflect" + "testing" + + "go-micro.dev/v5/registry" +) + +type testHandler struct{} + +type testRequest struct{} + +type testResponse struct{} + +func (t *testHandler) Test(ctx context.Context, req *testRequest, rsp *testResponse) error { + return nil +} + +func TestExtractEndpoint(t *testing.T) { + handler := &testHandler{} + typ := reflect.TypeOf(handler) + + var endpoints []*registry.Endpoint + + for m := 0; m < typ.NumMethod(); m++ { + if e := extractEndpoint(typ.Method(m)); e != nil { + endpoints = append(endpoints, e) + } + } + + if i := len(endpoints); i != 1 { + t.Errorf("Expected 1 endpoint, have %d", i) + } + + if endpoints[0].Name != "Test" { + t.Errorf("Expected handler Test, got %s", endpoints[0].Name) + } + + if endpoints[0].Request == nil { + t.Error("Expected non nil request") + } + + if endpoints[0].Response == nil { + t.Error("Expected non nil request") + } + + if endpoints[0].Request.Name != "testRequest" { + t.Errorf("Expected testRequest got %s", endpoints[0].Request.Name) + } + + if endpoints[0].Response.Name != "testResponse" { + t.Errorf("Expected testResponse got %s", endpoints[0].Response.Name) + } + + if endpoints[0].Request.Type != "testRequest" { + t.Errorf("Expected testRequest type got %s", endpoints[0].Request.Type) + } + + if endpoints[0].Response.Type != "testResponse" { + t.Errorf("Expected testResponse type got %s", endpoints[0].Response.Type) + } +} diff --git a/server/grpc/go.mod b/server/grpc/go.mod new file mode 100644 index 00000000..9404b219 --- /dev/null +++ b/server/grpc/go.mod @@ -0,0 +1,36 @@ +module go-micro.dev/v5/server/grpc + +go 1.24 + +toolchain go1.24.1 + +require ( + github.com/golang/protobuf v1.5.4 + go-micro.dev/v5 v5.5.1-0.20250507183911-01b8394c8119 + golang.org/x/net v0.23.0 + google.golang.org/grpc v1.53.0 +) + +require ( + github.com/bitly/go-simplejson v0.5.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/miekg/dns v1.1.50 // indirect + github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/urfave/cli/v2 v2.25.7 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + go.etcd.io/bbolt v1.4.0 // indirect + golang.org/x/mod v0.9.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.7.0 // indirect + google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect + google.golang.org/protobuf v1.33.0 // indirect +) diff --git a/server/grpc/go.sum b/server/grpc/go.sum new file mode 100644 index 00000000..bd8684a5 --- /dev/null +++ b/server/grpc/go.sum @@ -0,0 +1,99 @@ +github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go-micro.dev/v5 v5.5.1-0.20250507183911-01b8394c8119 h1:hKzFHTSwaYvPS0sTrWpUcS2HFFbJte//n91NMWFFk4k= +go-micro.dev/v5 v5.5.1-0.20250507183911-01b8394c8119/go.mod h1:pplSGFQk5So43OrzGAg1gH434XydWybOTyFhv/nR0pA= +go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= +go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/grpc/grpc.go b/server/grpc/grpc.go new file mode 100644 index 00000000..3a248515 --- /dev/null +++ b/server/grpc/grpc.go @@ -0,0 +1,1039 @@ +// Package grpc provides a grpc server +package grpc + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "reflect" + "runtime/debug" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/golang/protobuf/proto" + "go-micro.dev/v5/broker" + "go-micro.dev/v5/cmd" + "go-micro.dev/v5/errors" + "go-micro.dev/v5/logger" + meta "go-micro.dev/v5/metadata" + "go-micro.dev/v5/registry" + "go-micro.dev/v5/server" + "go-micro.dev/v5/util/addr" + "go-micro.dev/v5/util/backoff" + mgrpc "go-micro.dev/v5/util/grpc" + mnet "go-micro.dev/v5/util/net" + "golang.org/x/net/netutil" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/encoding" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/status" +) + +func init() { + cmd.DefaultServers["grpc"] = NewServer +} + +var ( + // DefaultMaxMsgSize define maximum message size that server can send + // or receive. Default value is 4MB. + DefaultMaxMsgSize = 1024 * 1024 * 4 +) + +const ( + defaultContentType = "application/grpc" +) + +type grpcServer struct { + rpc *rServer + srv *grpc.Server + exit chan chan error + wg *sync.WaitGroup + + sync.RWMutex + opts server.Options + handlers map[string]server.Handler + subscribers map[*subscriber][]broker.Subscriber + // marks the serve as started + started bool + // used for first registration + registered bool + + // registry service instance + rsvc *registry.Service +} + +func init() { + encoding.RegisterCodec(wrapCodec{jsonCodec{}}) + encoding.RegisterCodec(wrapCodec{protoCodec{}}) + encoding.RegisterCodec(wrapCodec{bytesCodec{}}) +} + +func newGRPCServer(opts ...server.Option) server.Server { + options := newOptions(opts...) + + // create a grpc server + srv := &grpcServer{ + opts: options, + rpc: &rServer{ + serviceMap: make(map[string]*service), + logger: options.Logger, + }, + handlers: make(map[string]server.Handler), + subscribers: make(map[*subscriber][]broker.Subscriber), + exit: make(chan chan error), + wg: wait(options.Context), + } + + // configure the grpc server + srv.configure() + + return srv +} + +type grpcRouter struct { + h func(context.Context, server.Request, interface{}) error + m func(context.Context, server.Message) error +} + +func (r grpcRouter) ProcessMessage(ctx context.Context, msg server.Message) error { + return r.m(ctx, msg) +} + +func (r grpcRouter) ServeRequest(ctx context.Context, req server.Request, rsp server.Response) error { + return r.h(ctx, req, rsp) +} + +func (g *grpcServer) configure(opts ...server.Option) { + g.Lock() + defer g.Unlock() + + // Don't reprocess where there's no config + if len(opts) == 0 && g.srv != nil { + return + } + + // Optionally use injected grpc.Server if there's a one + var srv *grpc.Server + if srv = g.getGrpcServer(); srv != nil { + g.srv = srv + } + + for _, o := range opts { + o(&g.opts) + } + + g.rsvc = nil + + // NOTE: injected grpc.Server doesn't have g.handler registered + if srv != nil { + return + } + + maxMsgSize := g.getMaxMsgSize() + + gopts := []grpc.ServerOption{ + grpc.MaxRecvMsgSize(maxMsgSize), + grpc.MaxSendMsgSize(maxMsgSize), + grpc.UnknownServiceHandler(g.handler), + } + + if creds := g.getCredentials(); creds != nil { + gopts = append(gopts, grpc.Creds(creds)) + } + + if opts := g.getGrpcOptions(); opts != nil { + gopts = append(gopts, opts...) + } + + g.srv = grpc.NewServer(gopts...) +} + +func (g *grpcServer) getMaxMsgSize() int { + if g.opts.Context == nil { + return DefaultMaxMsgSize + } + s, ok := g.opts.Context.Value(maxMsgSizeKey{}).(int) + if !ok { + return DefaultMaxMsgSize + } + return s +} + +func (g *grpcServer) getCredentials() credentials.TransportCredentials { + if g.opts.Context != nil { + if v, ok := g.opts.Context.Value(tlsAuth{}).(*tls.Config); ok && v != nil { + return credentials.NewTLS(v) + } + } + return nil +} + +func (g *grpcServer) getGrpcOptions() []grpc.ServerOption { + if g.opts.Context == nil { + return nil + } + + opts, ok := g.opts.Context.Value(grpcOptions{}).([]grpc.ServerOption) + if !ok || opts == nil { + return nil + } + + return opts +} + +func (g *grpcServer) getListener() net.Listener { + if g.opts.Context == nil { + return nil + } + + if l, ok := g.opts.Context.Value(netListener{}).(net.Listener); ok && l != nil { + return l + } + + return nil +} + +func (g *grpcServer) getGrpcServer() *grpc.Server { + if g.opts.Context == nil { + return nil + } + + if srv, ok := g.opts.Context.Value(grpcServerKey{}).(*grpc.Server); ok && srv != nil { + return srv + } + + return nil +} + +func (g *grpcServer) handler(srv interface{}, stream grpc.ServerStream) error { + if g.wg != nil { + g.wg.Add(1) + defer g.wg.Done() + } + + fullMethod, ok := grpc.MethodFromServerStream(stream) + if !ok { + return status.Errorf(codes.Internal, "method does not exist in context") + } + + serviceName, methodName, err := mgrpc.ServiceMethod(fullMethod) + if err != nil { + return status.New(codes.InvalidArgument, err.Error()).Err() + } + + // get grpc metadata + gmd, ok := metadata.FromIncomingContext(stream.Context()) + if !ok { + gmd = metadata.MD{} + } + + // copy the metadata to go-micro.metadata + md := meta.Metadata{} + for k, v := range gmd { + md[k] = strings.Join(v, ", ") + } + + // timeout for server deadline + to := md["timeout"] + + // get content type + ct := defaultContentType + + if ctype, ok := md["x-content-type"]; ok { + ct = ctype + } + if ctype, ok := md["content-type"]; ok { + ct = ctype + } + + delete(md, "x-content-type") + delete(md, "timeout") + + // create new context + ctx := meta.NewContext(stream.Context(), md) + + // get peer from context + if p, ok := peer.FromContext(stream.Context()); ok { + md["Remote"] = p.Addr.String() + ctx = peer.NewContext(ctx, p) + } + + // set the timeout if we have it + if len(to) > 0 { + if n, err := strconv.ParseUint(to, 10, 64); err == nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, time.Duration(n)) + defer cancel() + } + } + + // process via router + if g.opts.Router != nil { + cc, err := g.newGRPCCodec(ct) + if err != nil { + return errors.InternalServerError("go.micro.server", err.Error()) + } + codec := &grpcCodec{ + method: fmt.Sprintf("%s.%s", serviceName, methodName), + endpoint: fmt.Sprintf("%s.%s", serviceName, methodName), + target: g.opts.Name, + s: stream, + c: cc, + } + + // create a client.Request + request := &rpcRequest{ + service: mgrpc.ServiceFromMethod(fullMethod), + contentType: ct, + method: fmt.Sprintf("%s.%s", serviceName, methodName), + codec: codec, + stream: true, + } + + response := &rpcResponse{ + header: make(map[string]string), + codec: codec, + } + + // create a wrapped function + handler := func(ctx context.Context, req server.Request, rsp interface{}) error { + return g.opts.Router.ServeRequest(ctx, req, rsp.(server.Response)) + } + + // execute the wrapper for it + for i := len(g.opts.HdlrWrappers); i > 0; i-- { + handler = g.opts.HdlrWrappers[i-1](handler) + } + + r := grpcRouter{h: handler} + + // serve the actual request using the request router + if err := r.ServeRequest(ctx, request, response); err != nil { + if _, ok := status.FromError(err); ok { + return err + } + return status.Errorf(codes.Internal, "%v", err.Error()) + } + + return nil + } + + // process the standard request flow + g.rpc.mu.Lock() + service := g.rpc.serviceMap[serviceName] + g.rpc.mu.Unlock() + + if service == nil { + return status.New(codes.Unimplemented, fmt.Sprintf("unknown service %s", serviceName)).Err() + } + + mtype := service.method[methodName] + if mtype == nil { + return status.New(codes.Unimplemented, fmt.Sprintf("unknown service %s.%s", serviceName, methodName)).Err() + } + + // process unary + if !mtype.stream { + return g.processRequest(stream, service, mtype, ct, ctx) + } + + // process stream + return g.processStream(stream, service, mtype, ct, ctx) +} + +func (g *grpcServer) processRequest(stream grpc.ServerStream, service *service, mtype *methodType, ct string, ctx context.Context) error { + for { + var argv, replyv reflect.Value + + // Decode the argument value. + argIsValue := false // if true, need to indirect before calling. + if mtype.ArgType.Kind() == reflect.Ptr { + argv = reflect.New(mtype.ArgType.Elem()) + } else { + argv = reflect.New(mtype.ArgType) + argIsValue = true + } + + // Unmarshal request + if err := stream.RecvMsg(argv.Interface()); err != nil { + return err + } + + if argIsValue { + argv = argv.Elem() + } + + // reply value + replyv = reflect.New(mtype.ReplyType.Elem()) + + function := mtype.method.Func + var returnValues []reflect.Value + + cc, err := g.newGRPCCodec(ct) + if err != nil { + return errors.InternalServerError("go.micro.server", err.Error()) + } + b, err := cc.Marshal(argv.Interface()) + if err != nil { + return err + } + + // create a client.Request + r := &rpcRequest{ + service: g.opts.Name, + contentType: ct, + method: fmt.Sprintf("%s.%s", service.name, mtype.method.Name), + body: b, + payload: argv.Interface(), + } + + // define the handler func + fn := func(ctx context.Context, req server.Request, rsp interface{}) (err error) { + defer func() { + if r := recover(); r != nil { + logger.Extract(ctx).Errorf("panic recovered: %v, stack: %s", r, string(debug.Stack())) + err = errors.InternalServerError("go.micro.server", "panic recovered: %v", r) + } + }() + returnValues = function.Call([]reflect.Value{service.rcvr, mtype.prepareContext(ctx), reflect.ValueOf(argv.Interface()), reflect.ValueOf(rsp)}) + + // The return value for the method is an error. + if rerr := returnValues[0].Interface(); rerr != nil { + err = rerr.(error) + } + + return err + } + + // wrap the handler func + for i := len(g.opts.HdlrWrappers); i > 0; i-- { + fn = g.opts.HdlrWrappers[i-1](fn) + } + statusCode := codes.OK + statusDesc := "" + // execute the handler + if appErr := fn(ctx, r, replyv.Interface()); appErr != nil { + var errStatus *status.Status + switch verr := appErr.(type) { + case *errors.Error: + // micro.Error now proto based and we can attach it to grpc status + statusCode = microError(verr) + statusDesc = verr.Error() + verr.Detail = strings.ToValidUTF8(verr.Detail, "") + errStatus, err = status.New(statusCode, statusDesc).WithDetails(verr) + if err != nil { + return err + } + case proto.Message: + // user defined error that proto based we can attach it to grpc status + statusCode = convertCode(appErr) + statusDesc = appErr.Error() + errStatus, err = status.New(statusCode, statusDesc).WithDetails(verr) + if err != nil { + return err + } + default: + // default case user pass own error type that not proto based + statusCode = convertCode(verr) + statusDesc = verr.Error() + errStatus = status.New(statusCode, statusDesc) + } + + return errStatus.Err() + } + + if err := stream.SendMsg(replyv.Interface()); err != nil { + return err + } + return status.New(statusCode, statusDesc).Err() + } +} + +func (g *grpcServer) processStream(stream grpc.ServerStream, service *service, mtype *methodType, ct string, ctx context.Context) error { + opts := g.opts + + r := &rpcRequest{ + service: opts.Name, + contentType: ct, + method: fmt.Sprintf("%s.%s", service.name, mtype.method.Name), + stream: true, + } + + ss := &rpcStream{ + request: r, + s: stream, + } + + function := mtype.method.Func + var returnValues []reflect.Value + + // Invoke the method, providing a new value for the reply. + fn := func(ctx context.Context, req server.Request, stream interface{}) error { + returnValues = function.Call([]reflect.Value{service.rcvr, mtype.prepareContext(ctx), reflect.ValueOf(stream)}) + if err := returnValues[0].Interface(); err != nil { + return err.(error) + } + + return nil + } + + for i := len(opts.HdlrWrappers); i > 0; i-- { + fn = opts.HdlrWrappers[i-1](fn) + } + + statusCode := codes.OK + statusDesc := "" + + if appErr := fn(ctx, r, ss); appErr != nil { + var err error + var errStatus *status.Status + switch verr := appErr.(type) { + case *errors.Error: + // micro.Error now proto based and we can attach it to grpc status + statusCode = microError(verr) + statusDesc = verr.Error() + verr.Detail = strings.ToValidUTF8(verr.Detail, "") + errStatus, err = status.New(statusCode, statusDesc).WithDetails(verr) + if err != nil { + return err + } + case proto.Message: + // user defined error that proto based we can attach it to grpc status + statusCode = convertCode(appErr) + statusDesc = appErr.Error() + errStatus, err = status.New(statusCode, statusDesc).WithDetails(verr) + if err != nil { + return err + } + default: + // default case user pass own error type that not proto based + statusCode = convertCode(verr) + statusDesc = verr.Error() + errStatus = status.New(statusCode, statusDesc) + } + return errStatus.Err() + } + + return status.New(statusCode, statusDesc).Err() +} + +func (g *grpcServer) newGRPCCodec(contentType string) (encoding.Codec, error) { + codecs := make(map[string]encoding.Codec) + if g.opts.Context != nil { + if v, ok := g.opts.Context.Value(codecsKey{}).(map[string]encoding.Codec); ok && v != nil { + codecs = v + } + } + if c, ok := codecs[contentType]; ok { + return c, nil + } + if c, ok := defaultGRPCCodecs[contentType]; ok { + return c, nil + } + return nil, fmt.Errorf("Unsupported Content-Type: %s", contentType) +} + +func (g *grpcServer) Options() server.Options { + g.RLock() + opts := g.opts + g.RUnlock() + + return opts +} + +func (g *grpcServer) Init(opts ...server.Option) error { + g.configure(opts...) + return nil +} + +func (g *grpcServer) NewHandler(h interface{}, opts ...server.HandlerOption) server.Handler { + return newRpcHandler(h, opts...) +} + +func (g *grpcServer) Handle(h server.Handler) error { + if err := g.rpc.register(h.Handler()); err != nil { + return err + } + + g.handlers[h.Name()] = h + return nil +} + +func (g *grpcServer) NewSubscriber(topic string, sb interface{}, opts ...server.SubscriberOption) server.Subscriber { + return newSubscriber(topic, sb, opts...) +} + +func (g *grpcServer) Subscribe(sb server.Subscriber) error { + sub, ok := sb.(*subscriber) + if !ok { + return fmt.Errorf("invalid subscriber: expected *subscriber") + } + if len(sub.handlers) == 0 { + return fmt.Errorf("invalid subscriber: no handler functions") + } + + if err := validateSubscriber(sb); err != nil { + return err + } + + g.Lock() + if _, ok = g.subscribers[sub]; ok { + g.Unlock() + return fmt.Errorf("subscriber %v already exists", sub) + } + + g.subscribers[sub] = nil + g.Unlock() + return nil +} + +func (g *grpcServer) Register() error { + g.RLock() + rsvc := g.rsvc + config := g.opts + g.RUnlock() + + log := g.opts.Logger + + regFunc := func(service *registry.Service) error { + var regErr error + + for i := 0; i < 3; i++ { + // set the ttl + rOpts := []registry.RegisterOption{registry.RegisterTTL(config.RegisterTTL)} + // attempt to register + if err := config.Registry.Register(service, rOpts...); err != nil { + // set the error + regErr = err + // backoff then retry + time.Sleep(backoff.Do(i + 1)) + continue + } + // success so nil error + regErr = nil + break + } + + return regErr + } + + // if service already filled, reuse it and return early + if rsvc != nil { + if err := regFunc(rsvc); err != nil { + return err + } + return nil + } + + var err error + var advt, host, port string + var cacheService bool + + // check the advertise address first + // if it exists then use it, otherwise + // use the address + if len(config.Advertise) > 0 { + advt = config.Advertise + } else { + advt = config.Address + } + + if cnt := strings.Count(advt, ":"); cnt >= 1 { + // ipv6 address in format [host]:port or ipv4 host:port + host, port, err = net.SplitHostPort(advt) + if err != nil { + return err + } + } else { + host = advt + } + + if ip := net.ParseIP(host); ip != nil { + cacheService = true + } + + addr, err := addr.Extract(host) + if err != nil { + return err + } + + // make copy of metadata + md := meta.Copy(config.Metadata) + + // register service + node := ®istry.Node{ + Id: config.Name + "-" + config.Id, + Address: mnet.HostPort(addr, port), + Metadata: md, + } + + node.Metadata["broker"] = config.Broker.String() + node.Metadata["registry"] = config.Registry.String() + node.Metadata["server"] = g.String() + node.Metadata["transport"] = g.String() + node.Metadata["protocol"] = "grpc" + + g.RLock() + // Maps are ordered randomly, sort the keys for consistency + var handlerList []string + for n, e := range g.handlers { + // Only advertise non internal handlers + if !e.Options().Internal { + handlerList = append(handlerList, n) + } + } + sort.Strings(handlerList) + + var subscriberList []*subscriber + for e := range g.subscribers { + // Only advertise non internal subscribers + if !e.Options().Internal { + subscriberList = append(subscriberList, e) + } + } + sort.Slice(subscriberList, func(i, j int) bool { + return subscriberList[i].topic > subscriberList[j].topic + }) + + endpoints := make([]*registry.Endpoint, 0, len(handlerList)+len(subscriberList)) + for _, n := range handlerList { + endpoints = append(endpoints, g.handlers[n].Endpoints()...) + } + for _, e := range subscriberList { + endpoints = append(endpoints, e.Endpoints()...) + } + g.RUnlock() + + service := ®istry.Service{ + Name: config.Name, + Version: config.Version, + Nodes: []*registry.Node{node}, + Endpoints: endpoints, + } + + g.RLock() + registered := g.registered + g.RUnlock() + + if !registered { + log.Logf(logger.InfoLevel, "Registry [%s] Registering node: %s", config.Registry.String(), node.Id) + } + + // register the service + if err := regFunc(service); err != nil { + return err + } + + // already registered? don't need to register subscribers + if registered { + return nil + } + + g.Lock() + defer g.Unlock() + + for sb := range g.subscribers { + handler := g.createSubHandler(sb, g.opts) + var opts []broker.SubscribeOption + if queue := sb.Options().Queue; len(queue) > 0 { + opts = append(opts, broker.Queue(queue)) + } + + if cx := sb.Options().Context; cx != nil { + opts = append(opts, broker.SubscribeContext(cx)) + } + + if !sb.Options().AutoAck { + opts = append(opts, broker.DisableAutoAck()) + } + + log.Logf(logger.InfoLevel, "Subscribing to topic: %s", sb.Topic()) + + sub, err := config.Broker.Subscribe(sb.Topic(), handler, opts...) + if err != nil { + return err + } + g.subscribers[sb] = []broker.Subscriber{sub} + } + + g.registered = true + if cacheService { + g.rsvc = service + } + + return nil +} + +func (g *grpcServer) Deregister() error { + var err error + var advt, host, port string + + g.RLock() + config := g.opts + g.RUnlock() + + log := g.opts.Logger + + // check the advertise address first + // if it exists then use it, otherwise + // use the address + if len(config.Advertise) > 0 { + advt = config.Advertise + } else { + advt = config.Address + } + + if cnt := strings.Count(advt, ":"); cnt >= 1 { + // ipv6 address in format [host]:port or ipv4 host:port + host, port, err = net.SplitHostPort(advt) + if err != nil { + return err + } + } else { + host = advt + } + + addr, err := addr.Extract(host) + if err != nil { + return err + } + + node := ®istry.Node{ + Id: config.Name + "-" + config.Id, + Address: mnet.HostPort(addr, port), + } + + service := ®istry.Service{ + Name: config.Name, + Version: config.Version, + Nodes: []*registry.Node{node}, + } + + log.Logf(logger.InfoLevel, "Deregistering node: %s", node.Id) + + if err := config.Registry.Deregister(service); err != nil { + return err + } + + g.Lock() + g.rsvc = nil + + if !g.registered { + g.Unlock() + return nil + } + + g.registered = false + + wg := sync.WaitGroup{} + for sb, subs := range g.subscribers { + for _, sub := range subs { + wg.Add(1) + go func(s broker.Subscriber) { + defer wg.Done() + log.Logf(logger.InfoLevel, "Unsubscribing from topic: %s", s.Topic()) + s.Unsubscribe() + }(sub) + } + g.subscribers[sb] = nil + } + wg.Wait() + + g.Unlock() + return nil +} + +func (g *grpcServer) Start() error { + g.RLock() + if g.started { + g.RUnlock() + return nil + } + g.RUnlock() + + config := g.Options() + log := config.Logger + + // micro: config.Transport.Listen(config.Address) + var ( + ts net.Listener + err error + ) + + if l := g.getListener(); l != nil { + ts = l + } else { + // check the tls config for secure connect + if tc := config.TLSConfig; tc != nil { + ts, err = tls.Listen("tcp", config.Address, tc) + // otherwise just plain tcp listener + } else { + ts, err = net.Listen("tcp", config.Address) + } + if err != nil { + return err + } + } + + if g.opts.Context != nil { + if c, ok := g.opts.Context.Value(maxConnKey{}).(int); ok && c > 0 { + ts = netutil.LimitListener(ts, c) + } + } + + log.Logf(logger.InfoLevel, "Server [grpc] Listening on %s", ts.Addr().String()) + g.Lock() + g.opts.Address = ts.Addr().String() + g.Unlock() + + // only connect if we're subscribed + if len(g.subscribers) > 0 { + // connect to the broker + if err := config.Broker.Connect(); err != nil { + log.Logf(logger.ErrorLevel, "Broker [%s] connect error: %v", config.Broker.String(), err) + return err + } + + log.Logf(logger.InfoLevel, "Broker [%s] Connected to %s", config.Broker.String(), config.Broker.Address()) + } + + // use RegisterCheck func before register + if err = g.opts.RegisterCheck(g.opts.Context); err != nil { + log.Logf(logger.ErrorLevel, "Server %s-%s register check error: %s", config.Name, config.Id, err) + } else { + // announce self to the world + if err := g.Register(); err != nil { + log.Logf(logger.ErrorLevel, "Server register error: %v", err) + } + } + + // micro: go ts.Accept(s.accept) + go func() { + if err := g.srv.Serve(ts); err != nil { + log.Logf(logger.ErrorLevel, "gRPC Server start error: %v", err) + } + }() + + go func() { + t := new(time.Ticker) + + // only process if it exists + if g.opts.RegisterInterval > time.Duration(0) { + // new ticker + t = time.NewTicker(g.opts.RegisterInterval) + } + + // return error chan + var ( + err error + ch chan error + ) + + Loop: + for { + select { + // register self on interval + case <-t.C: + g.RLock() + registered := g.registered + g.RUnlock() + rerr := g.opts.RegisterCheck(g.opts.Context) + if rerr != nil && registered { + log.Logf(logger.ErrorLevel, "Server %s-%s register check error: %s, deregister it", config.Name, config.Id, rerr) + // deregister self in case of error + if err := g.Deregister(); err != nil { + log.Logf(logger.ErrorLevel, "Server %s-%s deregister error: %s", config.Name, config.Id, err) + } + } else if rerr != nil && !registered { + log.Logf(logger.ErrorLevel, "Server %s-%s register check error: %s", config.Name, config.Id, rerr) + continue + } + if err := g.Register(); err != nil { + log.Log(logger.ErrorLevel, "Server register error: ", err) + } + // wait for exit + case ch = <-g.exit: + break Loop + } + } + + // deregister self + if err := g.Deregister(); err != nil { + log.Log(logger.ErrorLevel, "Server deregister error: ", err) + } + + // wait for waitgroup + if g.wg != nil { + g.wg.Wait() + } + + // stop the grpc server + exit := make(chan bool) + + go func() { + g.srv.GracefulStop() + close(exit) + }() + + select { + case <-exit: + case <-time.After(time.Second): + g.srv.Stop() + } + + log.Logf(logger.InfoLevel, "Broker [%s] Disconnected from %s", config.Broker.String(), config.Broker.Address()) + // disconnect broker + if err = config.Broker.Disconnect(); err != nil { + log.Logf(logger.ErrorLevel, "Broker [%s] disconnect error: %v", config.Broker.String(), err) + } + + // close transport + ch <- err + }() + + // mark the server as started + g.Lock() + g.started = true + g.Unlock() + + return nil +} + +func (g *grpcServer) Stop() error { + g.RLock() + if !g.started { + g.RUnlock() + return nil + } + g.RUnlock() + + ch := make(chan error) + g.exit <- ch + + var err error + select { + case err = <-ch: + g.Lock() + g.rsvc = nil + g.started = false + g.Unlock() + } + + return err +} + +func (g *grpcServer) String() string { + return "grpc" +} + +func NewServer(opts ...server.Option) server.Server { + return newGRPCServer(opts...) +} diff --git a/server/grpc/handler.go b/server/grpc/handler.go new file mode 100644 index 00000000..4b9c9e8f --- /dev/null +++ b/server/grpc/handler.go @@ -0,0 +1,66 @@ +package grpc + +import ( + "reflect" + + "go-micro.dev/v5/registry" + "go-micro.dev/v5/server" +) + +type rpcHandler struct { + name string + handler interface{} + endpoints []*registry.Endpoint + opts server.HandlerOptions +} + +func newRpcHandler(handler interface{}, opts ...server.HandlerOption) server.Handler { + options := server.HandlerOptions{ + Metadata: make(map[string]map[string]string), + } + + for _, o := range opts { + o(&options) + } + + typ := reflect.TypeOf(handler) + hdlr := reflect.ValueOf(handler) + name := reflect.Indirect(hdlr).Type().Name() + + var endpoints []*registry.Endpoint + + for m := 0; m < typ.NumMethod(); m++ { + if e := extractEndpoint(typ.Method(m)); e != nil { + e.Name = name + "." + e.Name + + for k, v := range options.Metadata[e.Name] { + e.Metadata[k] = v + } + + endpoints = append(endpoints, e) + } + } + + return &rpcHandler{ + name: name, + handler: handler, + endpoints: endpoints, + opts: options, + } +} + +func (r *rpcHandler) Name() string { + return r.name +} + +func (r *rpcHandler) Handler() interface{} { + return r.handler +} + +func (r *rpcHandler) Endpoints() []*registry.Endpoint { + return r.endpoints +} + +func (r *rpcHandler) Options() server.HandlerOptions { + return r.opts +} diff --git a/server/grpc/options.go b/server/grpc/options.go new file mode 100644 index 00000000..e9a48a3a --- /dev/null +++ b/server/grpc/options.go @@ -0,0 +1,95 @@ +package grpc + +import ( + "context" + "crypto/tls" + "net" + + "go-micro.dev/v5/broker" + "go-micro.dev/v5/codec" + "go-micro.dev/v5/logger" + "go-micro.dev/v5/registry" + "go-micro.dev/v5/server" + "go-micro.dev/v5/transport" + "google.golang.org/grpc" + "google.golang.org/grpc/encoding" +) + +type codecsKey struct{} +type grpcOptions struct{} +type netListener struct{} +type maxMsgSizeKey struct{} +type maxConnKey struct{} +type tlsAuth struct{} +type grpcServerKey struct{} + +// gRPC Codec to be used to encode/decode requests for a given content type. +func Codec(contentType string, c encoding.Codec) server.Option { + return func(o *server.Options) { + codecs := make(map[string]encoding.Codec) + if o.Context == nil { + o.Context = context.Background() + } + if v, ok := o.Context.Value(codecsKey{}).(map[string]encoding.Codec); ok && v != nil { + codecs = v + } + codecs[contentType] = c + o.Context = context.WithValue(o.Context, codecsKey{}, codecs) + } +} + +// AuthTLS should be used to setup a secure authentication using TLS. +func AuthTLS(t *tls.Config) server.Option { + return setServerOption(tlsAuth{}, t) +} + +// MaxConn specifies maximum number of max simultaneous connections to server. +func MaxConn(n int) server.Option { + return setServerOption(maxConnKey{}, n) +} + +// Listener specifies the net.Listener to use instead of the default. +func Listener(l net.Listener) server.Option { + return setServerOption(netListener{}, l) +} + +// Server specifies a *grpc.Server to use instead of the default +// This is for rare use case where user need to expose grpc.Server for +// customization. Please NOTE however user injected grpcServer doesn't support +// server Handler abstraction. +func Server(srv *grpc.Server) server.Option { + return setServerOption(grpcServerKey{}, srv) +} + +// Options to be used to configure gRPC options. +func Options(opts ...grpc.ServerOption) server.Option { + return setServerOption(grpcOptions{}, opts) +} + +// MaxMsgSize set the maximum message in bytes the server can receive and +// send. Default maximum message size is 4 MB. +func MaxMsgSize(s int) server.Option { + return setServerOption(maxMsgSizeKey{}, s) +} + +func newOptions(opt ...server.Option) server.Options { + opts := server.Options{ + Codecs: make(map[string]codec.NewCodec), + Metadata: map[string]string{}, + Broker: broker.DefaultBroker, + Registry: registry.DefaultRegistry, + RegisterCheck: server.DefaultRegisterCheck, + Transport: transport.DefaultTransport, + Address: server.DefaultAddress, + Name: server.DefaultName, + Id: server.DefaultId, + Version: server.DefaultVersion, + Logger: logger.DefaultLogger, + } + + for _, o := range opt { + o(&opts) + } + + return opts +} diff --git a/server/grpc/request.go b/server/grpc/request.go new file mode 100644 index 00000000..21ac6c12 --- /dev/null +++ b/server/grpc/request.go @@ -0,0 +1,90 @@ +package grpc + +import ( + "go-micro.dev/v5/codec" + "go-micro.dev/v5/codec/bytes" +) + +type rpcRequest struct { + service string + method string + contentType string + codec codec.Codec + header map[string]string + body []byte + stream bool + payload interface{} +} + +type rpcMessage struct { + topic string + contentType string + payload interface{} + header map[string]string + body []byte + codec codec.Codec +} + +func (r *rpcRequest) ContentType() string { + return r.contentType +} + +func (r *rpcRequest) Service() string { + return r.service +} + +func (r *rpcRequest) Method() string { + return r.method +} + +func (r *rpcRequest) Endpoint() string { + return r.method +} + +func (r *rpcRequest) Codec() codec.Reader { + return r.codec +} + +func (r *rpcRequest) Header() map[string]string { + return r.header +} + +func (r *rpcRequest) Read() ([]byte, error) { + f := &bytes.Frame{} + if err := r.codec.ReadBody(f); err != nil { + return nil, err + } + return f.Data, nil +} + +func (r *rpcRequest) Stream() bool { + return r.stream +} + +func (r *rpcRequest) Body() interface{} { + return r.payload +} + +func (r *rpcMessage) ContentType() string { + return r.contentType +} + +func (r *rpcMessage) Topic() string { + return r.topic +} + +func (r *rpcMessage) Payload() interface{} { + return r.payload +} + +func (r *rpcMessage) Header() map[string]string { + return r.header +} + +func (r *rpcMessage) Body() []byte { + return r.body +} + +func (r *rpcMessage) Codec() codec.Reader { + return r.codec +} diff --git a/server/grpc/response.go b/server/grpc/response.go new file mode 100644 index 00000000..ec3162a3 --- /dev/null +++ b/server/grpc/response.go @@ -0,0 +1,27 @@ +package grpc + +import ( + "go-micro.dev/v5/codec" +) + +type rpcResponse struct { + header map[string]string + codec codec.Codec +} + +func (r *rpcResponse) Codec() codec.Writer { + return r.codec +} + +func (r *rpcResponse) WriteHeader(hdr map[string]string) { + for k, v := range hdr { + r.header[k] = v + } +} + +func (r *rpcResponse) Write(b []byte) error { + return r.codec.Write(&codec.Message{ + Header: r.header, + Body: b, + }, nil) +} diff --git a/server/grpc/server.go b/server/grpc/server.go new file mode 100644 index 00000000..bb2413dd --- /dev/null +++ b/server/grpc/server.go @@ -0,0 +1,182 @@ +package grpc + +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// +// Meh, we need to get rid of this shit + +import ( + "context" + "errors" + "reflect" + "sync" + "unicode" + "unicode/utf8" + + "go-micro.dev/v5/logger" + "go-micro.dev/v5/server" +) + +var ( + // Precompute the reflect type for error. Can't use error directly + // because Typeof takes an empty interface value. This is annoying. + typeOfError = reflect.TypeOf((*error)(nil)).Elem() +) + +type methodType struct { + method reflect.Method + ArgType reflect.Type + ReplyType reflect.Type + ContextType reflect.Type + stream bool +} + +type service struct { + name string // name of service + rcvr reflect.Value // receiver of methods for the service + typ reflect.Type // type of the receiver + method map[string]*methodType // registered methods +} + +// server represents an RPC Server. +type rServer struct { + mu sync.Mutex // protects the serviceMap + serviceMap map[string]*service + logger logger.Logger +} + +// Is this an exported - upper case - name? +func isExported(name string) bool { + rune, _ := utf8.DecodeRuneInString(name) + return unicode.IsUpper(rune) +} + +// Is this type exported or a builtin? +func isExportedOrBuiltinType(t reflect.Type) bool { + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + // PkgPath will be non-empty even for an exported type, + // so we need to check the type name as well. + return isExported(t.Name()) || t.PkgPath() == "" +} + +// prepareEndpoint() returns a methodType for the provided method or nil +// in case if the method was unsuitable. +func prepareEndpoint(method reflect.Method, log logger.Logger) *methodType { + mtype := method.Type + mname := method.Name + var replyType, argType, contextType reflect.Type + var stream bool + + // Endpoint() must be exported. + if method.PkgPath != "" { + return nil + } + + switch mtype.NumIn() { + case 3: + // assuming streaming + argType = mtype.In(2) + contextType = mtype.In(1) + stream = true + case 4: + // method that takes a context + argType = mtype.In(2) + replyType = mtype.In(3) + contextType = mtype.In(1) + default: + log.Logf(logger.ErrorLevel, "method %v of %v has wrong number of ins: %v", mname, mtype, mtype.NumIn()) + return nil + } + + if stream { + // check stream type + streamType := reflect.TypeOf((*server.Stream)(nil)).Elem() + if !argType.Implements(streamType) { + log.Logf(logger.ErrorLevel, "%v argument does not implement Streamer interface: %v", mname, argType) + return nil + } + } else { + // if not stream check the replyType + + // First arg need not be a pointer. + if !isExportedOrBuiltinType(argType) { + log.Logf(logger.ErrorLevel, "%v argument type not exported: %v", mname, argType) + return nil + } + + if replyType.Kind() != reflect.Ptr { + log.Logf(logger.ErrorLevel, "method %v reply type not a pointer: %v", mname, replyType) + return nil + } + + // Reply type must be exported. + if !isExportedOrBuiltinType(replyType) { + log.Logf(logger.ErrorLevel, "method %v reply type not exported: %v", mname, replyType) + return nil + } + } + + // Endpoint() needs one out. + if mtype.NumOut() != 1 { + log.Logf(logger.ErrorLevel, "method %v has wrong number of outs: %v", mname, mtype.NumOut()) + return nil + } + // The return type of the method must be error. + if returnType := mtype.Out(0); returnType != typeOfError { + log.Logf(logger.ErrorLevel, "method %v returns %v not error", mname, returnType.String()) + return nil + } + return &methodType{method: method, ArgType: argType, ReplyType: replyType, ContextType: contextType, stream: stream} +} + +func (server *rServer) register(rcvr interface{}) error { + server.mu.Lock() + defer server.mu.Unlock() + log := server.logger + if server.serviceMap == nil { + server.serviceMap = make(map[string]*service) + } + s := new(service) + s.typ = reflect.TypeOf(rcvr) + s.rcvr = reflect.ValueOf(rcvr) + sname := reflect.Indirect(s.rcvr).Type().Name() + if sname == "" { + logger.Fatalf("rpc: no service name for type %v", s.typ.String()) + } + if !isExported(sname) { + s := "rpc Register: type " + sname + " is not exported" + log.Log(logger.ErrorLevel, s) + return errors.New(s) + } + if _, present := server.serviceMap[sname]; present { + return errors.New("rpc: service already defined: " + sname) + } + s.name = sname + s.method = make(map[string]*methodType) + + // Install the methods + for m := 0; m < s.typ.NumMethod(); m++ { + method := s.typ.Method(m) + if mt := prepareEndpoint(method, log); mt != nil { + s.method[method.Name] = mt + } + } + + if len(s.method) == 0 { + s := "rpc Register: type " + sname + " has no exported methods of suitable type" + log.Log(logger.ErrorLevel, s) + return errors.New(s) + } + server.serviceMap[s.name] = s + return nil +} + +func (m *methodType) prepareContext(ctx context.Context) reflect.Value { + if contextv := reflect.ValueOf(ctx); contextv.IsValid() { + return contextv + } + return reflect.Zero(m.ContextType) +} diff --git a/server/grpc/stream.go b/server/grpc/stream.go new file mode 100644 index 00000000..cbbb0057 --- /dev/null +++ b/server/grpc/stream.go @@ -0,0 +1,38 @@ +package grpc + +import ( + "context" + + "go-micro.dev/v5/server" + "google.golang.org/grpc" +) + +// rpcStream implements a server side Stream. +type rpcStream struct { + s grpc.ServerStream + request server.Request +} + +func (r *rpcStream) Close() error { + return nil +} + +func (r *rpcStream) Error() error { + return nil +} + +func (r *rpcStream) Request() server.Request { + return r.request +} + +func (r *rpcStream) Context() context.Context { + return r.s.Context() +} + +func (r *rpcStream) Send(m interface{}) error { + return r.s.SendMsg(m) +} + +func (r *rpcStream) Recv(m interface{}) error { + return r.s.RecvMsg(m) +} diff --git a/server/grpc/subscriber.go b/server/grpc/subscriber.go new file mode 100644 index 00000000..c852d8c6 --- /dev/null +++ b/server/grpc/subscriber.go @@ -0,0 +1,291 @@ +package grpc + +import ( + "context" + "fmt" + "reflect" + "runtime/debug" + "strings" + + "go-micro.dev/v5/broker" + "go-micro.dev/v5/errors" + "go-micro.dev/v5/logger" + "go-micro.dev/v5/metadata" + "go-micro.dev/v5/registry" + "go-micro.dev/v5/server" +) + +const ( + subSig = "func(context.Context, interface{}) error" +) + +type handler struct { + method reflect.Value + reqType reflect.Type + ctxType reflect.Type +} + +type subscriber struct { + topic string + rcvr reflect.Value + typ reflect.Type + subscriber interface{} + handlers []*handler + endpoints []*registry.Endpoint + opts server.SubscriberOptions +} + +func newSubscriber(topic string, sub interface{}, opts ...server.SubscriberOption) server.Subscriber { + options := server.SubscriberOptions{ + AutoAck: true, + } + + for _, o := range opts { + o(&options) + } + + var endpoints []*registry.Endpoint + var handlers []*handler + + if typ := reflect.TypeOf(sub); typ.Kind() == reflect.Func { + h := &handler{ + method: reflect.ValueOf(sub), + } + + switch typ.NumIn() { + case 1: + h.reqType = typ.In(0) + case 2: + h.ctxType = typ.In(0) + h.reqType = typ.In(1) + } + + handlers = append(handlers, h) + + endpoints = append(endpoints, ®istry.Endpoint{ + Name: "Func", + Request: extractSubValue(typ), + Metadata: map[string]string{ + "topic": topic, + "subscriber": "true", + }, + }) + } else { + hdlr := reflect.ValueOf(sub) + name := reflect.Indirect(hdlr).Type().Name() + + for m := 0; m < typ.NumMethod(); m++ { + method := typ.Method(m) + h := &handler{ + method: method.Func, + } + + switch method.Type.NumIn() { + case 2: + h.reqType = method.Type.In(1) + case 3: + h.ctxType = method.Type.In(1) + h.reqType = method.Type.In(2) + } + + handlers = append(handlers, h) + + endpoints = append(endpoints, ®istry.Endpoint{ + Name: name + "." + method.Name, + Request: extractSubValue(method.Type), + Metadata: map[string]string{ + "topic": topic, + "subscriber": "true", + }, + }) + } + } + + return &subscriber{ + rcvr: reflect.ValueOf(sub), + typ: reflect.TypeOf(sub), + topic: topic, + subscriber: sub, + handlers: handlers, + endpoints: endpoints, + opts: options, + } +} + +func validateSubscriber(sub server.Subscriber) error { + typ := reflect.TypeOf(sub.Subscriber()) + var argType reflect.Type + + if typ.Kind() == reflect.Func { + name := "Func" + switch typ.NumIn() { + case 2: + argType = typ.In(1) + default: + return fmt.Errorf("subscriber %v takes wrong number of args: %v required signature %s", name, typ.NumIn(), subSig) + } + if !isExportedOrBuiltinType(argType) { + return fmt.Errorf("subscriber %v argument type not exported: %v", name, argType) + } + if typ.NumOut() != 1 { + return fmt.Errorf("subscriber %v has wrong number of outs: %v require signature %s", + name, typ.NumOut(), subSig) + } + if returnType := typ.Out(0); returnType != typeOfError { + return fmt.Errorf("subscriber %v returns %v not error", name, returnType.String()) + } + } else { + hdlr := reflect.ValueOf(sub.Subscriber()) + name := reflect.Indirect(hdlr).Type().Name() + + for m := 0; m < typ.NumMethod(); m++ { + method := typ.Method(m) + + switch method.Type.NumIn() { + case 3: + argType = method.Type.In(2) + default: + return fmt.Errorf("subscriber %v.%v takes wrong number of args: %v required signature %s", + name, method.Name, method.Type.NumIn(), subSig) + } + + if !isExportedOrBuiltinType(argType) { + return fmt.Errorf("%v argument type not exported: %v", name, argType) + } + if method.Type.NumOut() != 1 { + return fmt.Errorf( + "subscriber %v.%v has wrong number of outs: %v require signature %s", + name, method.Name, method.Type.NumOut(), subSig) + } + if returnType := method.Type.Out(0); returnType != typeOfError { + return fmt.Errorf("subscriber %v.%v returns %v not error", name, method.Name, returnType.String()) + } + } + } + + return nil +} + +func (g *grpcServer) createSubHandler(sb *subscriber, opts server.Options) broker.Handler { + return func(p broker.Event) (err error) { + defer func() { + if r := recover(); r != nil { + g.opts.Logger.Log(logger.ErrorLevel, "panic recovered: ", r) + g.opts.Logger.Log(logger.ErrorLevel, string(debug.Stack())) + err = errors.InternalServerError("go.micro.server", "panic recovered: %v", r) + } + }() + + msg := p.Message() + // if we don't have headers, create empty map + if msg.Header == nil { + msg.Header = make(map[string]string) + } + + ct := msg.Header["Content-Type"] + if len(ct) == 0 { + msg.Header["Content-Type"] = defaultContentType + ct = defaultContentType + } + cf, err := g.newGRPCCodec(ct) + if err != nil { + return err + } + + hdr := make(map[string]string, len(msg.Header)) + for k, v := range msg.Header { + hdr[k] = v + } + delete(hdr, "Content-Type") + ctx := metadata.NewContext(context.Background(), hdr) + + results := make(chan error, len(sb.handlers)) + + for i := 0; i < len(sb.handlers); i++ { + handler := sb.handlers[i] + + var isVal bool + var req reflect.Value + + if handler.reqType.Kind() == reflect.Ptr { + req = reflect.New(handler.reqType.Elem()) + } else { + req = reflect.New(handler.reqType) + isVal = true + } + if isVal { + req = req.Elem() + } + + if err = cf.Unmarshal(msg.Body, req.Interface()); err != nil { + return err + } + + fn := func(ctx context.Context, msg server.Message) error { + var vals []reflect.Value + if sb.typ.Kind() != reflect.Func { + vals = append(vals, sb.rcvr) + } + if handler.ctxType != nil { + vals = append(vals, reflect.ValueOf(ctx)) + } + + vals = append(vals, reflect.ValueOf(msg.Payload())) + + returnValues := handler.method.Call(vals) + if rerr := returnValues[0].Interface(); rerr != nil { + return rerr.(error) + } + return nil + } + + for i := len(opts.SubWrappers); i > 0; i-- { + fn = opts.SubWrappers[i-1](fn) + } + + if g.wg != nil { + g.wg.Add(1) + } + go func() { + if g.wg != nil { + defer g.wg.Done() + } + err := fn(ctx, &rpcMessage{ + topic: sb.topic, + contentType: ct, + payload: req.Interface(), + header: msg.Header, + body: msg.Body, + }) + results <- err + }() + } + var errors []string + for i := 0; i < len(sb.handlers); i++ { + if rerr := <-results; rerr != nil { + errors = append(errors, rerr.Error()) + } + } + if len(errors) > 0 { + err = fmt.Errorf("subscriber error: %s", strings.Join(errors, "\n")) + } + + return err + } +} + +func (s *subscriber) Topic() string { + return s.topic +} + +func (s *subscriber) Subscriber() interface{} { + return s.subscriber +} + +func (s *subscriber) Endpoints() []*registry.Endpoint { + return s.endpoints +} + +func (s *subscriber) Options() server.SubscriberOptions { + return s.opts +} diff --git a/server/grpc/util.go b/server/grpc/util.go new file mode 100644 index 00000000..dfb467ab --- /dev/null +++ b/server/grpc/util.go @@ -0,0 +1,49 @@ +package grpc + +import ( + "context" + "io" + "os" + "sync" + + "google.golang.org/grpc/codes" +) + +// convertCode converts a standard Go error into its canonical code. Note that +// this is only used to translate the error returned by the server applications. +func convertCode(err error) codes.Code { + switch err { + case nil: + return codes.OK + case io.EOF: + return codes.OutOfRange + case io.ErrClosedPipe, io.ErrNoProgress, io.ErrShortBuffer, io.ErrShortWrite, io.ErrUnexpectedEOF: + return codes.FailedPrecondition + case os.ErrInvalid: + return codes.InvalidArgument + case context.Canceled: + return codes.Canceled + case context.DeadlineExceeded: + return codes.DeadlineExceeded + } + switch { + case os.IsExist(err): + return codes.AlreadyExists + case os.IsNotExist(err): + return codes.NotFound + case os.IsPermission(err): + return codes.PermissionDenied + } + return codes.Unknown +} + +func wait(ctx context.Context) *sync.WaitGroup { + if ctx == nil { + return nil + } + wg, ok := ctx.Value("wait").(*sync.WaitGroup) + if !ok { + return nil + } + return wg +} diff --git a/transport/grpc/go.mod b/transport/grpc/go.mod new file mode 100644 index 00000000..67338bdc --- /dev/null +++ b/transport/grpc/go.mod @@ -0,0 +1,36 @@ +module go-micro.dev/v5/transport/grpc + +go 1.24 + +toolchain go1.24.1 + +require ( + go-micro.dev/v5 v5.5.1-0.20250507183911-01b8394c8119 + google.golang.org/grpc v1.65.0 + google.golang.org/protobuf v1.34.1 +) + +require ( + github.com/bitly/go-simplejson v0.5.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/miekg/dns v1.1.50 // indirect + github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/urfave/cli/v2 v2.25.7 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + go.etcd.io/bbolt v1.4.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect +) diff --git a/transport/grpc/go.sum b/transport/grpc/go.sum new file mode 100644 index 00000000..feaba960 --- /dev/null +++ b/transport/grpc/go.sum @@ -0,0 +1,99 @@ +github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go-micro.dev/v5 v5.5.1-0.20250507183911-01b8394c8119 h1:hKzFHTSwaYvPS0sTrWpUcS2HFFbJte//n91NMWFFk4k= +go-micro.dev/v5 v5.5.1-0.20250507183911-01b8394c8119/go.mod h1:pplSGFQk5So43OrzGAg1gH434XydWybOTyFhv/nR0pA= +go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= +go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= +google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/transport/grpc/grpc.go b/transport/grpc/grpc.go new file mode 100644 index 00000000..30f4b2df --- /dev/null +++ b/transport/grpc/grpc.go @@ -0,0 +1,180 @@ +// Package grpc provides a grpc transport +package grpc + +import ( + "context" + "crypto/tls" + "net" + + "go-micro.dev/v5/cmd" + "go-micro.dev/v5/transport" + maddr "go-micro.dev/v5/util/addr" + mnet "go-micro.dev/v5/util/net" + mls "go-micro.dev/v5/util/tls" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + + pb "go-micro.dev/v5/transport/grpc/proto" +) + +type grpcTransport struct { + opts transport.Options +} + +type grpcTransportListener struct { + listener net.Listener + secure bool + tls *tls.Config +} + +func init() { + cmd.DefaultTransports["grpc"] = NewTransport +} + +func getTLSConfig(addr string) (*tls.Config, error) { + hosts := []string{addr} + + // check if its a valid host:port + if host, _, err := net.SplitHostPort(addr); err == nil { + if len(host) == 0 { + hosts = maddr.IPs() + } else { + hosts = []string{host} + } + } + + // generate a certificate + cert, err := mls.Certificate(hosts...) + if err != nil { + return nil, err + } + + return &tls.Config{Certificates: []tls.Certificate{cert}}, nil +} + +func (t *grpcTransportListener) Addr() string { + return t.listener.Addr().String() +} + +func (t *grpcTransportListener) Close() error { + return t.listener.Close() +} + +func (t *grpcTransportListener) Accept(fn func(transport.Socket)) error { + var opts []grpc.ServerOption + + // setup tls if specified + if t.secure || t.tls != nil { + config := t.tls + if config == nil { + var err error + addr := t.listener.Addr().String() + config, err = getTLSConfig(addr) + if err != nil { + return err + } + } + + creds := credentials.NewTLS(config) + opts = append(opts, grpc.Creds(creds)) + } + + // new service + srv := grpc.NewServer(opts...) + + // register service + pb.RegisterTransportServer(srv, µTransport{addr: t.listener.Addr().String(), fn: fn}) + + // start serving + return srv.Serve(t.listener) +} + +func (t *grpcTransport) Dial(addr string, opts ...transport.DialOption) (transport.Client, error) { + dopts := transport.DialOptions{ + Timeout: transport.DefaultDialTimeout, + } + + for _, opt := range opts { + opt(&dopts) + } + + options := []grpc.DialOption{ + grpc.WithTimeout(dopts.Timeout), + } + + if t.opts.Secure || t.opts.TLSConfig != nil { + config := t.opts.TLSConfig + if config == nil { + config = &tls.Config{ + InsecureSkipVerify: true, + } + } + creds := credentials.NewTLS(config) + options = append(options, grpc.WithTransportCredentials(creds)) + } else { + options = append(options, grpc.WithInsecure()) + } + + // dial the server + conn, err := grpc.Dial(addr, options...) + if err != nil { + return nil, err + } + + // create stream + stream, err := pb.NewTransportClient(conn).Stream(context.Background()) + if err != nil { + return nil, err + } + + // return a client + return &grpcTransportClient{ + conn: conn, + stream: stream, + local: "localhost", + remote: addr, + }, nil +} + +func (t *grpcTransport) Listen(addr string, opts ...transport.ListenOption) (transport.Listener, error) { + var options transport.ListenOptions + for _, o := range opts { + o(&options) + } + + ln, err := mnet.Listen(addr, func(addr string) (net.Listener, error) { + return net.Listen("tcp", addr) + }) + if err != nil { + return nil, err + } + + return &grpcTransportListener{ + listener: ln, + tls: t.opts.TLSConfig, + secure: t.opts.Secure, + }, nil +} + +func (t *grpcTransport) Init(opts ...transport.Option) error { + for _, o := range opts { + o(&t.opts) + } + return nil +} + +func (t *grpcTransport) Options() transport.Options { + return t.opts +} + +func (t *grpcTransport) String() string { + return "grpc" +} + +func NewTransport(opts ...transport.Option) transport.Transport { + var options transport.Options + for _, o := range opts { + o(&options) + } + return &grpcTransport{opts: options} +} diff --git a/transport/grpc/grpc_test.go b/transport/grpc/grpc_test.go new file mode 100644 index 00000000..78f9c1a2 --- /dev/null +++ b/transport/grpc/grpc_test.go @@ -0,0 +1,111 @@ +package grpc + +import ( + "net" + "testing" + + "go-micro.dev/v5/transport" +) + +func expectedPort(t *testing.T, expected string, lsn transport.Listener) { + _, port, err := net.SplitHostPort(lsn.Addr()) + if err != nil { + t.Errorf("Expected address to be `%s`, got error: %v", expected, err) + } + + if port != expected { + lsn.Close() + t.Errorf("Expected address to be `%s`, got `%s`", expected, port) + } +} + +func TestGRPCTransportPortRange(t *testing.T) { + tp := NewTransport() + + lsn1, err := tp.Listen(":44444-44448") + if err != nil { + t.Errorf("Did not expect an error, got %s", err) + } + expectedPort(t, "44444", lsn1) + + lsn2, err := tp.Listen(":44444-44448") + if err != nil { + t.Errorf("Did not expect an error, got %s", err) + } + expectedPort(t, "44445", lsn2) + + lsn, err := tp.Listen(":0") + if err != nil { + t.Errorf("Did not expect an error, got %s", err) + } + + lsn.Close() + lsn1.Close() + lsn2.Close() +} + +func TestGRPCTransportCommunication(t *testing.T) { + tr := NewTransport() + + l, err := tr.Listen(":0") + if err != nil { + t.Errorf("Unexpected listen err: %v", err) + } + defer l.Close() + + fn := func(sock transport.Socket) { + defer sock.Close() + + for { + var m transport.Message + if err := sock.Recv(&m); err != nil { + return + } + + if err := sock.Send(&m); err != nil { + return + } + } + } + + done := make(chan bool) + + go func() { + if err := l.Accept(fn); err != nil { + select { + case <-done: + default: + t.Errorf("Unexpected accept err: %v", err) + } + } + }() + + c, err := tr.Dial(l.Addr()) + if err != nil { + t.Errorf("Unexpected dial err: %v", err) + } + defer c.Close() + + m := transport.Message{ + Header: map[string]string{ + "X-Content-Type": "application/json", + }, + Body: []byte(`{"message": "Hello World"}`), + } + + if err := c.Send(&m); err != nil { + t.Errorf("Unexpected send err: %v", err) + } + + var rm transport.Message + + if err := c.Recv(&rm); err != nil { + t.Errorf("Unexpected recv err: %v", err) + } + + if string(rm.Body) != string(m.Body) { + t.Errorf("Expected %v, got %v", m.Body, rm.Body) + } + + close(done) +} diff --git a/transport/grpc/handler.go b/transport/grpc/handler.go new file mode 100644 index 00000000..95bda092 --- /dev/null +++ b/transport/grpc/handler.go @@ -0,0 +1,42 @@ +package grpc + +import ( + "runtime/debug" + + "go-micro.dev/v5/errors" + "go-micro.dev/v5/logger" + "go-micro.dev/v5/transport" + pb "go-micro.dev/v5/transport/grpc/proto" + "google.golang.org/grpc/peer" +) + +// microTransport satisfies the pb.TransportServer inteface. +type microTransport struct { + addr string + fn func(transport.Socket) +} + +func (m *microTransport) Stream(ts pb.Transport_StreamServer) (err error) { + sock := &grpcTransportSocket{ + stream: ts, + local: m.addr, + } + + p, ok := peer.FromContext(ts.Context()) + if ok { + sock.remote = p.Addr.String() + } + + defer func() { + if r := recover(); r != nil { + logger.Error(r, string(debug.Stack())) + sock.Close() + err = errors.InternalServerError("go.micro.transport", "panic recovered: %v", r) + } + }() + + // execute socket func + m.fn(sock) + + return err +} diff --git a/transport/grpc/proto/transport.pb.go b/transport/grpc/proto/transport.pb.go new file mode 100644 index 00000000..81bba223 --- /dev/null +++ b/transport/grpc/proto/transport.pb.go @@ -0,0 +1,169 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.32.0 +// protoc v4.25.3 +// source: proto/transport.proto + +package transport + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Message struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Header map[string]string `protobuf:"bytes,1,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Body []byte `protobuf:"bytes,2,opt,name=body,proto3" json:"body,omitempty"` +} + +func (x *Message) Reset() { + *x = Message{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_transport_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Message) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Message) ProtoMessage() {} + +func (x *Message) ProtoReflect() protoreflect.Message { + mi := &file_proto_transport_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Message.ProtoReflect.Descriptor instead. +func (*Message) Descriptor() ([]byte, []int) { + return file_proto_transport_proto_rawDescGZIP(), []int{0} +} + +func (x *Message) GetHeader() map[string]string { + if x != nil { + return x.Header + } + return nil +} + +func (x *Message) GetBody() []byte { + if x != nil { + return x.Body + } + return nil +} + +var File_proto_transport_proto protoreflect.FileDescriptor + +var file_proto_transport_proto_rawDesc = []byte{ + 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x17, 0x67, 0x6f, 0x2e, 0x6d, 0x69, 0x63, 0x72, + 0x6f, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x67, 0x72, 0x70, 0x63, + 0x22, 0x9e, 0x01, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x44, 0x0a, 0x06, + 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x67, + 0x6f, 0x2e, 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x48, + 0x65, 0x61, 0x64, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, + 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x1a, 0x39, 0x0a, 0x0b, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x32, 0x5f, 0x0a, 0x09, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x52, + 0x0a, 0x06, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x20, 0x2e, 0x67, 0x6f, 0x2e, 0x6d, 0x69, + 0x63, 0x72, 0x6f, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x20, 0x2e, 0x67, 0x6f, 0x2e, + 0x6d, 0x69, 0x63, 0x72, 0x6f, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x28, 0x01, + 0x30, 0x01, 0x42, 0x13, 0x5a, 0x11, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x3b, 0x74, 0x72, + 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proto_transport_proto_rawDescOnce sync.Once + file_proto_transport_proto_rawDescData = file_proto_transport_proto_rawDesc +) + +func file_proto_transport_proto_rawDescGZIP() []byte { + file_proto_transport_proto_rawDescOnce.Do(func() { + file_proto_transport_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_transport_proto_rawDescData) + }) + return file_proto_transport_proto_rawDescData +} + +var file_proto_transport_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_proto_transport_proto_goTypes = []interface{}{ + (*Message)(nil), // 0: go.micro.transport.grpc.Message + nil, // 1: go.micro.transport.grpc.Message.HeaderEntry +} +var file_proto_transport_proto_depIdxs = []int32{ + 1, // 0: go.micro.transport.grpc.Message.header:type_name -> go.micro.transport.grpc.Message.HeaderEntry + 0, // 1: go.micro.transport.grpc.Transport.Stream:input_type -> go.micro.transport.grpc.Message + 0, // 2: go.micro.transport.grpc.Transport.Stream:output_type -> go.micro.transport.grpc.Message + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_proto_transport_proto_init() } +func file_proto_transport_proto_init() { + if File_proto_transport_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proto_transport_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Message); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_proto_transport_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_transport_proto_goTypes, + DependencyIndexes: file_proto_transport_proto_depIdxs, + MessageInfos: file_proto_transport_proto_msgTypes, + }.Build() + File_proto_transport_proto = out.File + file_proto_transport_proto_rawDesc = nil + file_proto_transport_proto_goTypes = nil + file_proto_transport_proto_depIdxs = nil +} diff --git a/transport/grpc/proto/transport.pb.micro.go b/transport/grpc/proto/transport.pb.micro.go new file mode 100644 index 00000000..f9c43979 --- /dev/null +++ b/transport/grpc/proto/transport.pb.micro.go @@ -0,0 +1,166 @@ +// Code generated by protoc-gen-micro. DO NOT EDIT. +// source: proto/transport.proto + +package transport + +import ( + fmt "fmt" + proto "google.golang.org/protobuf/proto" + math "math" +) + +import ( + context "context" + client "go-micro.dev/v5/client" + server "go-micro.dev/v5/server" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ client.Option +var _ server.Option + +// Client API for Transport service + +type TransportService interface { + Stream(ctx context.Context, opts ...client.CallOption) (Transport_StreamService, error) +} + +type transportService struct { + c client.Client + name string +} + +func NewTransportService(name string, c client.Client) TransportService { + return &transportService{ + c: c, + name: name, + } +} + +func (c *transportService) Stream(ctx context.Context, opts ...client.CallOption) (Transport_StreamService, error) { + req := c.c.NewRequest(c.name, "Transport.Stream", &Message{}) + stream, err := c.c.Stream(ctx, req, opts...) + if err != nil { + return nil, err + } + return &transportServiceStream{stream}, nil +} + +type Transport_StreamService interface { + Context() context.Context + SendMsg(interface{}) error + RecvMsg(interface{}) error + CloseSend() error + Close() error + Send(*Message) error + Recv() (*Message, error) +} + +type transportServiceStream struct { + stream client.Stream +} + +func (x *transportServiceStream) CloseSend() error { + return x.stream.CloseSend() +} + +func (x *transportServiceStream) Close() error { + return x.stream.Close() +} + +func (x *transportServiceStream) Context() context.Context { + return x.stream.Context() +} + +func (x *transportServiceStream) SendMsg(m interface{}) error { + return x.stream.Send(m) +} + +func (x *transportServiceStream) RecvMsg(m interface{}) error { + return x.stream.Recv(m) +} + +func (x *transportServiceStream) Send(m *Message) error { + return x.stream.Send(m) +} + +func (x *transportServiceStream) Recv() (*Message, error) { + m := new(Message) + err := x.stream.Recv(m) + if err != nil { + return nil, err + } + return m, nil +} + +// Server API for Transport service + +type TransportHandler interface { + Stream(context.Context, Transport_StreamStream) error +} + +func RegisterTransportHandler(s server.Server, hdlr TransportHandler, opts ...server.HandlerOption) error { + type transport interface { + Stream(ctx context.Context, stream server.Stream) error + } + type Transport struct { + transport + } + h := &transportHandler{hdlr} + return s.Handle(s.NewHandler(&Transport{h}, opts...)) +} + +type transportHandler struct { + TransportHandler +} + +func (h *transportHandler) Stream(ctx context.Context, stream server.Stream) error { + return h.TransportHandler.Stream(ctx, &transportStreamStream{stream}) +} + +type Transport_StreamStream interface { + Context() context.Context + SendMsg(interface{}) error + RecvMsg(interface{}) error + Close() error + Send(*Message) error + Recv() (*Message, error) +} + +type transportStreamStream struct { + stream server.Stream +} + +func (x *transportStreamStream) Close() error { + return x.stream.Close() +} + +func (x *transportStreamStream) Context() context.Context { + return x.stream.Context() +} + +func (x *transportStreamStream) SendMsg(m interface{}) error { + return x.stream.Send(m) +} + +func (x *transportStreamStream) RecvMsg(m interface{}) error { + return x.stream.Recv(m) +} + +func (x *transportStreamStream) Send(m *Message) error { + return x.stream.Send(m) +} + +func (x *transportStreamStream) Recv() (*Message, error) { + m := new(Message) + if err := x.stream.Recv(m); err != nil { + return nil, err + } + return m, nil +} diff --git a/transport/grpc/proto/transport.proto b/transport/grpc/proto/transport.proto new file mode 100644 index 00000000..bc7325db --- /dev/null +++ b/transport/grpc/proto/transport.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +option go_package = "./proto;transport"; + +package go.micro.transport.grpc; + +service Transport { + rpc Stream(stream Message) returns (stream Message) {} +} + +message Message { + map header = 1; + bytes body = 2; +} diff --git a/transport/grpc/proto/transport_grpc.pb.go b/transport/grpc/proto/transport_grpc.pb.go new file mode 100644 index 00000000..dcd5d8a0 --- /dev/null +++ b/transport/grpc/proto/transport_grpc.pb.go @@ -0,0 +1,139 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v4.25.3 +// source: proto/transport.proto + +package transport + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + Transport_Stream_FullMethodName = "/go.micro.transport.grpc.Transport/Stream" +) + +// TransportClient is the client API for Transport service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type TransportClient interface { + Stream(ctx context.Context, opts ...grpc.CallOption) (Transport_StreamClient, error) +} + +type transportClient struct { + cc grpc.ClientConnInterface +} + +func NewTransportClient(cc grpc.ClientConnInterface) TransportClient { + return &transportClient{cc} +} + +func (c *transportClient) Stream(ctx context.Context, opts ...grpc.CallOption) (Transport_StreamClient, error) { + stream, err := c.cc.NewStream(ctx, &Transport_ServiceDesc.Streams[0], Transport_Stream_FullMethodName, opts...) + if err != nil { + return nil, err + } + x := &transportStreamClient{stream} + return x, nil +} + +type Transport_StreamClient interface { + Send(*Message) error + Recv() (*Message, error) + grpc.ClientStream +} + +type transportStreamClient struct { + grpc.ClientStream +} + +func (x *transportStreamClient) Send(m *Message) error { + return x.ClientStream.SendMsg(m) +} + +func (x *transportStreamClient) Recv() (*Message, error) { + m := new(Message) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// TransportServer is the server API for Transport service. +// All implementations should embed UnimplementedTransportServer +// for forward compatibility +type TransportServer interface { + Stream(Transport_StreamServer) error +} + +// UnimplementedTransportServer should be embedded to have forward compatible implementations. +type UnimplementedTransportServer struct { +} + +func (UnimplementedTransportServer) Stream(Transport_StreamServer) error { + return status.Errorf(codes.Unimplemented, "method Stream not implemented") +} + +// UnsafeTransportServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to TransportServer will +// result in compilation errors. +type UnsafeTransportServer interface { + mustEmbedUnimplementedTransportServer() +} + +func RegisterTransportServer(s grpc.ServiceRegistrar, srv TransportServer) { + s.RegisterService(&Transport_ServiceDesc, srv) +} + +func _Transport_Stream_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(TransportServer).Stream(&transportStreamServer{stream}) +} + +type Transport_StreamServer interface { + Send(*Message) error + Recv() (*Message, error) + grpc.ServerStream +} + +type transportStreamServer struct { + grpc.ServerStream +} + +func (x *transportStreamServer) Send(m *Message) error { + return x.ServerStream.SendMsg(m) +} + +func (x *transportStreamServer) Recv() (*Message, error) { + m := new(Message) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// Transport_ServiceDesc is the grpc.ServiceDesc for Transport service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Transport_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "go.micro.transport.grpc.Transport", + HandlerType: (*TransportServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Stream", + Handler: _Transport_Stream_Handler, + ServerStreams: true, + ClientStreams: true, + }, + }, + Metadata: "proto/transport.proto", +} diff --git a/transport/grpc/socket.go b/transport/grpc/socket.go new file mode 100644 index 00000000..5acef8b2 --- /dev/null +++ b/transport/grpc/socket.go @@ -0,0 +1,97 @@ +package grpc + +import ( + "go-micro.dev/v5/transport" + pb "go-micro.dev/v5/transport/grpc/proto" + "google.golang.org/grpc" +) + +type grpcTransportClient struct { + conn *grpc.ClientConn + stream pb.Transport_StreamClient + + local string + remote string +} + +type grpcTransportSocket struct { + stream pb.Transport_StreamServer + local string + remote string +} + +func (g *grpcTransportClient) Local() string { + return g.local +} + +func (g *grpcTransportClient) Remote() string { + return g.remote +} + +func (g *grpcTransportClient) Recv(m *transport.Message) error { + if m == nil { + return nil + } + + msg, err := g.stream.Recv() + if err != nil { + return err + } + + m.Header = msg.Header + m.Body = msg.Body + return nil +} + +func (g *grpcTransportClient) Send(m *transport.Message) error { + if m == nil { + return nil + } + + return g.stream.Send(&pb.Message{ + Header: m.Header, + Body: m.Body, + }) +} + +func (g *grpcTransportClient) Close() error { + return g.conn.Close() +} + +func (g *grpcTransportSocket) Local() string { + return g.local +} + +func (g *grpcTransportSocket) Remote() string { + return g.remote +} + +func (g *grpcTransportSocket) Recv(m *transport.Message) error { + if m == nil { + return nil + } + + msg, err := g.stream.Recv() + if err != nil { + return err + } + + m.Header = msg.Header + m.Body = msg.Body + return nil +} + +func (g *grpcTransportSocket) Send(m *transport.Message) error { + if m == nil { + return nil + } + + return g.stream.Send(&pb.Message{ + Header: m.Header, + Body: m.Body, + }) +} + +func (g *grpcTransportSocket) Close() error { + return nil +}