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



Content Menu

  • No labels

2 Comments

  1. neo

    grpcx.Client 能否以ip:port 的方式连接server?

    若没有的话能否说说说这样设计的原因

    1. grpcx.Client.MustNewGrpcClientConn("demo")

      他这个demo参数名字叫serviceNameOrAddress,不传名字传address也可以的