GoFrame
框架包含多个微服务组件,并提供了易用的 GRPC
脚手架模块和工具。脚手架由 grpcx
社区包实现: https://github.com/gogf/gf/tree/master/contrib/rpc/grpcx 包含多个模块。
服务端- Server
服务端由 grpcx.Server
模块维护,用于实现服务端对象的创建与维护。使用示例: https://github.com/gogf/gf/blob/master/example/rpc/grpcx/basic/server/main.go
服务端的创建往往结合配置文件一起使用,关于服务端配置文件的介绍请参考章节: 服务端配置
package main
import (
"github.com/gogf/gf/contrib/rpc/grpcx/v2"
"github.com/gogf/gf/example/rpc/grpcx/basic/controller"
)
func main() {
s := grpcx.Server.New()
controller.Register(s)
s.Run()
}
客户端- Client
客户端由 grpcx.Client
模块维护,用于实现客户端对象的创建与维护。使用示例: https://github.com/gogf/gf/blob/master/example/rpc/grpcx/basic/client/main.go
大部分场景下,服务间的访问使用的是服务名称。
package main
import (
"github.com/gogf/gf/contrib/rpc/grpcx/v2"
"github.com/gogf/gf/example/rpc/grpcx/basic/protobuf"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)
func main() {
var (
ctx = gctx.New()
conn = grpcx.Client.MustNewGrpcClientConn("demo")
client = protobuf.NewGreeterClient(conn)
)
res, err := client.SayHello(ctx, &protobuf.HelloRequest{Name: "World"})
if err != nil {
g.Log().Error(ctx, err)
return
}
g.Log().Debug(ctx, "Response:", res.Message)
}
常见问题
是否需要保存复用创建的 Client
对象?
每一个 grpc Client
对象实际上对应的是对一个目标服务的访问,该对象应该保存下来复用,而不是每一次都新建 Client
对象,这样可以提高效率、降低资源使用、使用方式对 GC
友好。
上下文- Ctx
上下文由 grpcx.Ctx
模块维护,用于实现客户端与服务端之间、服务与服务之间的自定义数据传递。包含以下常用方法:
func (c Ctx) NewIncoming(ctx context.Context, data ...g.Map) context.Context
func (c Ctx) NewOutgoing(ctx context.Context, data ...g.Map) context.Context
func (c Ctx) IncomingToOutgoing(ctx context.Context, keys ...string) context.Context
func (c Ctx) IncomingMap(ctx context.Context) *gmap.Map
func (c Ctx) OutgoingMap(ctx context.Context) *gmap.Map
func (c Ctx) SetIncoming(ctx context.Context, data g.Map) context.Context
func (c Ctx) SetOutgoing(ctx context.Context, data g.Map) context.Context
其中的 Outgoing
用在客户端,表示将要传递给服务端的自定义数据,一般是由 Key-Value
键值对组成的 Map
数据格式; Incoming
使用在服务端,表示服务端接收到的客户端提交数据。其中框架相关的一些内嵌信息也写入到 Incoming
键值对中,例如,链路跟踪信息、客户端版本信息等。使用示例:
server.go
package main
import (
"github.com/gogf/gf/contrib/rpc/grpcx/v2"
"github.com/gogf/gf/example/rpc/grpcx/ctx/controller"
)
func main() {
s := grpcx.Server.New()
controller.Register(s)
s.Run()
}
controller.go
package controller
import (
"context"
"github.com/gogf/gf/contrib/rpc/grpcx/v2"
"github.com/gogf/gf/example/rpc/grpcx/ctx/protobuf"
"github.com/gogf/gf/v2/frame/g"
)
type Controller struct {
protobuf.UnimplementedGreeterServer
}
func Register(s *grpcx.GrpcServer) {
protobuf.RegisterGreeterServer(s.Server, &Controller{})
}
// SayHello implements helloworld.GreeterServer
func (s *Controller) SayHello(ctx context.Context, in *protobuf.HelloRequest) (*protobuf.HelloReply, error) {
m := grpcx.Ctx.IncomingMap(ctx)
g.Log().Infof(ctx, `incoming data: %v`, m.Map())
return &protobuf.HelloReply{Message: "Hello " + in.GetName()}, nil
}
client.go
package main
import (
"github.com/gogf/gf/contrib/rpc/grpcx/v2"
"github.com/gogf/gf/example/rpc/grpcx/ctx/protobuf"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)
func main() {
var (
conn = grpcx.Client.MustNewGrpcClientConn("demo")
client = protobuf.NewGreeterClient(conn)
ctx = grpcx.Ctx.NewOutgoing(gctx.New(), g.Map{
"UserId": "1000",
"UserName": "john",
})
)
g.Log().Infof(ctx, `outgoing data: %v`, grpcx.Ctx.OutgoingMap(ctx).Map())
res, err := client.SayHello(ctx, &protobuf.HelloRequest{Name: "World"})
if err != nil {
g.Log().Error(ctx, err)
return
}
g.Log().Debug(ctx, "Response:", res.Message)
}
执行后,服务端输出:
$ go run server.go
2023-03-15 19:28:45.331 [DEBU] set default registry using file registry as no custom registry set
2023-03-15 19:28:45.331 [DEBU] service register: &{Head: Deployment: Namespace: Name:demo Version: Endpoints:10.35.12.81:51707 Metadata:map[protocol:grpc]}
2023-03-15 19:28:45.332 [INFO] pid[83753]: grpc server started listening on [:51707]
2023-03-15 19:28:54.093 [INFO] {d049db1a39944c1711bd9f37d58a88f9} incoming data: map[:authority:service/default/default/demo/latest/ content-type:application/grpc traceparent:00-d049db1a39944c1711bd9f37d58a88f9-adbd2ba657ffea45-01 user-agent:grpc-go/1.49.0 userid:1000 username:john]
2023-03-15 19:28:54.093 {d049db1a39944c1711bd9f37d58a88f9} /protobuf.Greeter/SayHello, 0.049ms, name:"World", message:"Hello World"
客户端输出:
$ go run client.go
2023-03-15 19:28:54.087 [INFO] {d049db1a39944c1711bd9f37d58a88f9} outgoing data: map[userid:1000 username:john]
2023-03-15 19:28:54.089 [DEBU] client conn updated with addresses [{"Addr":"10.35.12.81:51707","ServerName":"demo","Attributes":{},"BalancerAttributes":null,"Type":0,"Metadata":null}]
2023-03-15 19:28:54.093 [DEBU] {d049db1a39944c1711bd9f37d58a88f9} Response: Hello World
负载均衡- Balancer
负载均衡由 grpcx.Balancer
模块维护,用于实现当服务端存在多个访问地址时,客户端根据何种策略进行请求。当客户端未设置负载均衡策略时,默认使用轮训策略。关于负载均衡的详细介绍请参考章节: 服务负载均衡
我们这里使用 随机策略 来做示例, 随机的策略将会使得各个服务端接收到的请求数比较随机。
server.go
package main
import (
"github.com/gogf/gf/contrib/rpc/grpcx/v2"
"github.com/gogf/gf/example/rpc/grpcx/balancer/controller"
)
func main() {
s := grpcx.Server.New()
controller.Register(s)
s.Run()
}
client.go
package main
import (
"context"
"github.com/gogf/gf/contrib/rpc/grpcx/v2"
"github.com/gogf/gf/example/rpc/grpcx/balancer/protobuf"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)
func main() {
var (
ctx context.Context
conn = grpcx.Client.MustNewGrpcClientConn("demo", grpcx.Balancer.WithRandom())
client = protobuf.NewGreeterClient(conn)
)
for i := 0; i < 10; i++ {
ctx = gctx.New()
res, err := client.SayHello(ctx, &protobuf.HelloRequest{Name: "World"})
if err != nil {
g.Log().Error(ctx, err)
return
}
g.Log().Debug(ctx, "Response:", res.Message)
}
}
其中的 grpcx.Balancer.WithRandom()
表示使用随机的请求策略。
启动两个 server.go
服务端,随后运行 client.go
客户端,查看服务端的请求日志:
server1
终端输出:
$ go run server.go
2023-03-15 19:50:44.801 [DEBU] set default registry using file registry as no custom registry set
2023-03-15 19:50:44.802 [DEBU] service register: &{Head: Deployment: Namespace: Name:demo Version: Endpoints:10.35.12.81:53962 Metadata:map[protocol:grpc]}
2023-03-15 19:50:44.802 [INFO] pid[89290]: grpc server started listening on [:53962]
2023-03-15 19:50:57.282 {7025612f6d954c17c5f335051bf10899} /protobuf.Greeter/SayHello, 0.003ms, name:"World", message:"Hello World"
2023-03-15 19:50:57.283 {60567c2f6d954c17c7f335052ce05185} /protobuf.Greeter/SayHello, 0.002ms, name:"World", message:"Hello World"
2023-03-15 19:50:57.285 {f8d09b2f6d954c17ccf33505dff1a4ea} /protobuf.Greeter/SayHello, 0.002ms, name:"World", message:"Hello World"
2023-03-15 19:50:57.287 {f0fab02f6d954c17cdf33505438b2c80} /protobuf.Greeter/SayHello, 0.001ms, name:"World", message:"Hello World"
server2
终端输出:
$ go run server.go
2023-03-15 19:50:51.720 [DEBU] set default registry using file registry as no custom registry set
2023-03-15 19:50:51.721 [DEBU] service register: &{Head: Deployment: Namespace: Name:demo Version: Endpoints:10.35.12.81:53973 Metadata:map[protocol:grpc]}
2023-03-15 19:50:51.722 [INFO] pid[89351]: grpc server started listening on [:53973]
2023-03-15 19:50:57.280 {b89a0d2f6d954c17c4f33505a046817c} /protobuf.Greeter/SayHello, 0.002ms, name:"World", message:"Hello World"
2023-03-15 19:50:57.282 {28bf732f6d954c17c6f33505adedff5f} /protobuf.Greeter/SayHello, 0.002ms, name:"World", message:"Hello World"
2023-03-15 19:50:57.283 {9876832f6d954c17c8f3350580ed535b} /protobuf.Greeter/SayHello, 0.002ms, name:"World", message:"Hello World"
2023-03-15 19:50:57.284 {684e8b2f6d954c17c9f33505d56e4b05} /protobuf.Greeter/SayHello, 0.001ms, name:"World", message:"Hello World"
2023-03-15 19:50:57.284 {c045912f6d954c17caf3350599006197} /protobuf.Greeter/SayHello, 0.001ms, name:"World", message:"Hello World"
2023-03-15 19:50:57.284 {500a972f6d954c17cbf33505252b0e01} /protobuf.Greeter/SayHello, 0.001ms, name:"World", message:"Hello World"
客户端终端输出:
$ go run client.go
2023-03-15 19:50:57.278 [DEBU] client conn updated with addresses [{"Addr":"10.35.12.81:53962","ServerName":"demo","Attributes":{},"BalancerAttributes":null,"Type":0,"Metadata":null},{"Addr":"10.35.12.81:53973","ServerName":"demo","Attributes":{},"BalancerAttributes":null,"Type":0,"Metadata":null}]
2023-03-15 19:50:57.281 [DEBU] {b89a0d2f6d954c17c4f33505a046817c} Response: Hello World
2023-03-15 19:50:57.282 [DEBU] {7025612f6d954c17c5f335051bf10899} Response: Hello World
2023-03-15 19:50:57.282 [DEBU] {28bf732f6d954c17c6f33505adedff5f} Response: Hello World
2023-03-15 19:50:57.283 [DEBU] {60567c2f6d954c17c7f335052ce05185} Response: Hello World
2023-03-15 19:50:57.283 [DEBU] {9876832f6d954c17c8f3350580ed535b} Response: Hello World
2023-03-15 19:50:57.284 [DEBU] {684e8b2f6d954c17c9f33505d56e4b05} Response: Hello World
2023-03-15 19:50:57.284 [DEBU] {c045912f6d954c17caf3350599006197} Response: Hello World
2023-03-15 19:50:57.285 [DEBU] {500a972f6d954c17cbf33505252b0e01} Response: Hello World
2023-03-15 19:50:57.286 [DEBU] {f8d09b2f6d954c17ccf33505dff1a4ea} Response: Hello World
2023-03-15 19:50:57.287 [DEBU] {f0fab02f6d954c17cdf33505438b2c80} Response: Hello World
可以看到,客户端发送了 10
次请求,两个服务端分别接收到了 4
次和 6
次请求,请求的负载比较随机。
注册发现- Resolver
注册发现由 grpcx.Resolver
模块维护,该模块用于 GRPC
解析服务名称。 grpcx
组件默认情况下使用本地文件系统实现注册发现,仅用于测试使用。如果是生产环境,则需要使用其他的注册发现组件。一个简单示例:
server.go
package main
import (
"github.com/gogf/gf/contrib/registry/etcd/v2"
"github.com/gogf/gf/contrib/rpc/grpcx/v2"
"github.com/gogf/gf/example/rpc/grpcx/resolver/controller"
)
func main() {
grpcx.Resolver.Register(etcd.New("127.0.0.1:2379"))
s := grpcx.Server.New()
controller.Register(s)
s.Run()
}
其中的 grpcx.Resolver.Register(etcd.New("127.0.0.1:2379"))
用于设置服务注册发现的组件为 etcd
,仅支持 GRPC
协议。
client.go
package main
import (
"github.com/gogf/gf/contrib/registry/etcd/v2"
"github.com/gogf/gf/contrib/rpc/grpcx/v2"
"github.com/gogf/gf/example/rpc/grpcx/resolver/protobuf"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)
func main() {
grpcx.Resolver.Register(etcd.New("127.0.0.1:2379"))
var (
ctx = gctx.New()
conn = grpcx.Client.MustNewGrpcClientConn("demo")
client = protobuf.NewGreeterClient(conn)
)
res, err := client.SayHello(ctx, &protobuf.HelloRequest{Name: "World"})
if err != nil {
g.Log().Error(ctx, err)
return
}
g.Log().Debug(ctx, "Response:", res.Message)
}
启动 etcd
:
$ etcd
{"level":"info","ts":"2023-03-15T20:02:50.966+0800","caller":"etcdmain/etcd.go:73","msg":"Running: ","args":["etcd"]}
{"level":"info","ts":"2023-03-15T20:02:50.967+0800","caller":"etcdmain/etcd.go:100","msg":"failed to detect default host","error":"default host not supported on darwin_amd64"}
{"level":"warn","ts":"2023-03-15T20:02:50.967+0800","caller":"etcdmain/etcd.go:105","msg":"'data-dir' was empty; using default","data-dir":"default.etcd"}
{"level":"info","ts":"2023-03-15T20:02:50.967+0800","caller":"etcdmain/etcd.go:116","msg":"server has been already initialized","data-dir":"default.etcd","dir-type":"member"}
{"level":"info","ts":"2023-03-15T20:02:50.967+0800","caller":"embed/etcd.go:124","msg":"configuring peer listeners","listen-peer-urls":["http://localhost:2380"]}
{"level":"info","ts":"2023-03-15T20:02:50.968+0800","caller":"embed/etcd.go:132","msg":"configuring client listeners","listen-client-urls":["http://localhost:2379"]}
...
运行 server.go
:
$ go run server.go
2023-03-15 20:02:19.472 [DEBU] service register: &{Head: Deployment: Namespace: Name:demo Version: Endpoints:10.35.12.81:55066 Metadata:map[protocol:grpc]}
2023-03-15 20:02:19.516 [DEBU] etcd put success with key "/service/default/default/demo/latest/10.35.12.81:55066", value "{"protocol":"grpc"}", lease "7587869265929863945"
2023-03-15 20:02:19.516 [INFO] pid[92040]: grpc server started listening on [:55066]
2023-03-15 20:02:29.310 {88c4c04e0e964c17dddec53b1adb54f7} /protobuf.Greeter/SayHello, 0.001ms, name:"World", message:"Hello World"
运行 client.go
:
$ go run client.go
2023-03-15 20:02:29.297 [DEBU] Watch key "/service/default/default/demo/latest/"
2023-03-15 20:02:29.307 [DEBU] client conn updated with addresses [{"Addr":"10.35.12.81:55066","ServerName":"demo","Attributes":{},"BalancerAttributes":null,"Type":0,"Metadata":null}]
2023-03-15 20:02:29.308 [DEBU] client conn updated with addresses [{"Addr":"10.35.12.81:55066","ServerName":"demo","Attributes":{},"BalancerAttributes":null,"Type":0,"Metadata":null}]
2023-03-15 20:02:29.310 [DEBU] {88c4c04e0e964c17dddec53b1adb54f7} Response: Hello World