简介

kratos的grpc框架 不是直接使用的googlegrpchttp也是对grpc接口做了封装(上一节我们就学习了使用pb生成http相关接口)。

官方原文:

  • GRPC Warden:基于官方gRPC开发,集成discovery服务发现,并融合P2C负载均衡;

快速开始

这次我们需要新建2个项目,用于展示RCP的相互调用,由于都是可以调用对方提供的服务,所以两个服务都可以作为客户端和服务端。

新建项目(服务1):

kratos new rpcserver1 	#这里不指定创建类型,项目会同时生成http和grpc服务

新建项目(服务2)

kratos new rpcserver2 	#这里不指定创建类型,项目会同时生成http和grpc服务

当然,你也可以添加--grpc参数,只生成grpc项目,这里为了方便后面测试,所以不做指定(实际项目中,其实也可能httpgrpc都会用到)。

生成目录概览:

├── CHANGELOG.md 
├── OWNERS
├── README.md
├── api                     # api目录为对外保留的proto文件及生成的pb.go文件
│   ├── api.bm.go			# gRPC server的pb协议文件
│   ├── api.pb.go           # 通过go generate生成的pb.go文件 (可直接执行 kratos tool protoc)
│   ├── api.proto
│   └── client.go
├── cmd
│   └── main.go             # cmd目录为main所在
├── configs                 # configs为配置文件目录
│   ├── application.toml    # 应用的自定义配置文件,可能是一些业务开关如:useABtest = true
│   ├── db.toml             # db相关配置
│   ├── grpc.toml           # grpc相关配置
│   ├── http.toml           # http相关配置
│   ├── memcache.toml       # memcache相关配置
│   └── redis.toml          # redis相关配置
├── go.mod
├── go.sum
└── internal                # internal为项目内部包,包括以下目录:
│   ├── dao                 # dao层,用于数据库、cache、MQ、依赖某业务grpc|http等资源访问
│   │   ├── dao.bts.go
│   │   ├── dao.go
│   │   ├── db.go
│   │   ├── mc.cache.go
│   │   ├── mc.go
│   │   └── redis.go
│   ├── di                  # 依赖注入层 采用wire静态分析依赖
│   │   ├── app.go
│   │   ├── wire.go         # wire 声明
│   │   └── wire_gen.go     # go generate 生成的代码 (可直接执行 kratos tool protoc)
│   ├── model               # model层,用于声明业务结构体
│   │   └── model.go
│   ├── server              # server层,用于初始化grpc和http server
│   │   ├── grpc            # grpc层,用于初始化grpc server和定义method
│   │   │   └── server.go
│   │   └── http            # http层,用于初始化http server和声明handler
│   │       └── server.go
│   └── service             # service层,用于业务逻辑处理,且为方便http和grpc共用方法,建议入参和出参保持grpc风格,且使用pb文件生成代码
│       └── service.go
└── test                    # 测试资源层 用于存放测试相关资源数据 如docker-compose配置 数据库初始化语句等
    └── docker-compose.yaml

注册server

进入 internal/server/grpc 目录打开 server.go 文件,可以看到以下代码启动一个 gRPC服务 RegisterDemoServer

package grpc

import (
	pb "rpcserver1/api"

	"github.com/go-kratos/kratos/pkg/conf/paladin"
	"github.com/go-kratos/kratos/pkg/net/rpc/warden"
)

// New new a grpc server.
func New(svc pb.DemoServer) (ws *warden.Server, err error) {
	var (
		cfg warden.ServerConfig
		ct paladin.TOML
	)
	if err = paladin.Get("grpc.toml").Unmarshal(&ct); err != nil {
		return
	}
	if err = ct.Get("Server").UnmarshalTOML(&cfg); err != nil {
		return
	}
	ws = warden.NewServer(&cfg)

	// RegisterDemoServer方法是在"api"目录下代码生成的 
	//对应proto文件内自定义的service名字,请使用正确方法名替换
	pb.RegisterDemoServer(ws.Server(), svc)
	ws, err = ws.Start()
	return
}

修改服务名字

rpcserver1/api/api.proto修改为以下内容:

service MyServer1 {
  rpc Ping(.google.protobuf.Empty) returns (.google.protobuf.Empty);
  rpc SayHello(HelloReq) returns (.google.protobuf.Empty);
  rpc SayHelloURL(HelloReq) returns (HelloResp) {
    option (google.api.http) = {
      get: "/myserver1/say_hello"
    };
  };
}

rpcserver2/api/api.proto修改为以下内容:

service MyServer2 {
  rpc Ping(.google.protobuf.Empty) returns (.google.protobuf.Empty);
  rpc SayHello(HelloReq) returns (.google.protobuf.Empty);
  rpc SayHelloURL(HelloReq) returns (HelloResp) {
    option (google.api.http) = {
      get: "/myserver2/say_hello"
    };
  };
}

分别进入api目录,生成.pb.go文件:

cd rpcserver1/api/
kratos tool protoc
#打开第二个命令窗口
cd rpcserver2/api/
kratos tool protoc

打开 internal/server/grpc/server.go 文件
这时你会发现文件存在一些错误,这是因为我们把服务名字改掉的原因,需要手动修改为正确的名字
修改 internal/server/grpc/server.go 为以下内容(rpcserver2同理):

package grpc

import (
	pb "rpcserver1/api"

	"github.com/go-kratos/kratos/pkg/conf/paladin"
	"github.com/go-kratos/kratos/pkg/net/rpc/warden"
)

// New new a grpc server.
func New(svc pb.MyServer1Server) (ws *warden.Server, err error) {
	var (
		cfg warden.ServerConfig
		ct paladin.TOML
	)
	if err = paladin.Get("grpc.toml").Unmarshal(&ct); err != nil {
		return
	}
	if err = ct.Get("Server").UnmarshalTOML(&cfg); err != nil {
		return
	}
	ws = warden.NewServer(&cfg)

	// RegisterDemoServer方法是在"api"目录下代码生成的
	//对应proto文件内自定义的service名字,请使用正确方法名替换
	pb.RegisterMyServer1Server(ws.Server(), svc)
	ws, err = ws.Start()
	return
}

http服务也有相关错误,修改 internal/server/http/server.go 为以下内容(rpcserver2同理):

package http

import (
	"net/http"

	pb "rpcserver1/api"
	"rpcserver1/internal/model"
	"github.com/go-kratos/kratos/pkg/conf/paladin"
	"github.com/go-kratos/kratos/pkg/log"
	bm "github.com/go-kratos/kratos/pkg/net/http/blademaster"
)

var svc pb.MyServer1Server

// New new a bm server.
func New(s pb.MyServer1Server) (engine *bm.Engine, err error) {
	var (
		cfg bm.ServerConfig
		ct paladin.TOML
	)
	if err = paladin.Get("http.toml").Unmarshal(&ct); err != nil {
		return
	}
	if err = ct.Get("Server").UnmarshalTOML(&cfg); err != nil {
		return
	}
	svc = s
	engine = bm.DefaultServer(&cfg)
	pb.RegisterMyServer1BMServer(engine, s)
	initRouter(engine)
	err = engine.Start()
	return
}

func initRouter(e *bm.Engine) {
	e.Ping(ping)
	g := e.Group("/rpcserver1")
	{
		g.GET("/start", howToStart)
	}
}

func ping(ctx *bm.Context) {
	if _, err := svc.Ping(ctx, nil); err != nil {
		log.Error("ping error(%v)", err)
		ctx.AbortWithStatus(http.StatusServiceUnavailable)
	}
}

// example for http request handler.
func howToStart(c *bm.Context) {
	k := &model.Kratos{
		Hello: "Golang 大法好 !!!",
	}
	c.JSON(k, nil)
}

还有api目录下的client.go也出现错误,我们也需要修正,后面需要用到(rpcserver2同理):

package api
import (
	"context"
	"fmt"

	"github.com/go-kratos/kratos/pkg/net/rpc/warden"

	"google.golang.org/grpc"
)

// AppID .
const AppID = "TODO: ADD APP ID"

// NewClient new grpc client
func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (MyServer1Client, error) {
	client := warden.NewClient(cfg, opts...)
	cc, err := client.Dial(context.Background(), fmt.Sprintf("discovery://default/%s", AppID))
	if err != nil {
		return nil, err
	}
	return NewMyServer1Client(cc), nil
}

// 生成 gRPC 代码
//go:generate kratos tool protoc --grpc --bm api.proto

修改internal/service/service.go代码(rpcserver2同理):

package service

import (
	"context"
	"fmt"

	pb "rpcserver1/api"
	"rpcserver1/internal/dao"
	"github.com/go-kratos/kratos/pkg/conf/paladin"

	"github.com/golang/protobuf/ptypes/empty"
	"github.com/google/wire"
)

var Provider = wire.NewSet(New, wire.Bind(new(pb.MyServer1Server), new(*Service)))

// Service service.
type Service struct {
	ac  *paladin.Map
	dao dao.Dao
}

// New new a service and return.
func New(d dao.Dao) (s *Service, cf func(), err error) {
	s = &Service{
		ac:  &paladin.TOML{},
		dao: d,
	}
	cf = s.Close
	err = paladin.Watch("application.toml", s.ac)
	return
}

// SayHello grpc demo func.
func (s *Service) SayHello(ctx context.Context, req *pb.HelloReq) (reply *empty.Empty, err error) {
	reply = new(empty.Empty)
	fmt.Printf("server1 say hello: %s", req.Name)
	return
}

// SayHelloURL bm demo func.
func (s *Service) SayHelloURL(ctx context.Context, req *pb.HelloReq) (reply *pb.HelloResp, err error) {
	reply = &pb.HelloResp{
		Content: "server1 say hello: " + req.Name,
	}
	fmt.Printf("server1 say hello url: %s", req.Name)
	return
}

// Ping ping the resource.
func (s *Service) Ping(ctx context.Context, e *empty.Empty) (*empty.Empty, error) {
	return &empty.Empty{}, s.dao.Ping(ctx)
}

// Close close the resource.
func (s *Service) Close() {
}

修改配置文件端口
rpcserver1修改为以下内容:

[Server]
    addr = "0.0.0.0:8001"
    timeout = "1s"

[Server]
    addr = "0.0.0.0:9001"
    timeout = "1s"

rpcserver2修改为以下内容:

[Server]
    addr = "0.0.0.0:8002"
    timeout = "1s"

[Server]
    addr = "0.0.0.0:9002"
    timeout = "1s"

启动两个项目测试一下:

cd rpcserver1/cmd/
kratos run
#打开第二个命令窗口
cd rpcserver2/cmd/
kratos run

打开浏览器分别访问一下是否正常:

http://localhost:8001/myserver1/say_hello?name=aaa
http://localhost:8001/myserver2/say_hello?name=bbb

访问正常,那基本服务搭建好了,下面进入正题:

rpc服务

修改rpcserver2/api/api.proto文件内容,新增一个Login服务接口

// 定义项目 API 的 proto 文件 可以同时描述 gRPC 和 HTTP API
// protobuf 文件参考:
//  - https://developers.google.com/protocol-buffers/
syntax = "proto3";

import "github.com/gogo/protobuf/gogoproto/gogo.proto";
import "google/protobuf/empty.proto";
import "google/api/annotations.proto";

// package 命名使用 {appid}.{version} 的方式, version 形如 v1, v2 ..
package service2.v1;

// NOTE: 最后请删除这些无用的注释 (゜-゜)つロ

option go_package = "api";
option (gogoproto.goproto_getters_all) = false;

service MyServer2 {
  rpc Ping(.google.protobuf.Empty) returns (.google.protobuf.Empty);
  rpc Login(LoginReq) returns (LoginResp);
  rpc SayHello(HelloReq) returns (.google.protobuf.Empty);
  rpc SayHelloURL(HelloReq) returns (HelloResp) {
    option (google.api.http) = {
      get: "/myserver2/say_hello"
    };
  };
}

message HelloReq {
  string name = 1 [(gogoproto.moretags) = 'form:"name" validate:"required"'];
}

message HelloResp {
  string Content = 1 [(gogoproto.jsontag) = 'content'];
}

message LoginReq {
  string username = 1 [(gogoproto.moretags) = 'form:"username" validate:"required"'];
  string passwd = 2 [(gogoproto.moretags) = 'form:"passwd" validate:"required"'];
}

message LoginResp {
  string Content = 1 [(gogoproto.jsontag) = 'content'];
}

rpcserver2/internal/service/service.go增加以下代码

//登录服务接口逻辑
func (s *Service) Login(ctx context.Context, req *pb.LoginReq) (reply *pb.LoginResp, err error) {
	content := fmt.Sprintf("server2 login username: %s, passwd: %s", req.Username, req.Passwd)
	reply = &pb.LoginResp{
		Content: content,
	}
	fmt.Printf("server2 login username: %s, passwd: %s", req.Username, req.Passwd)
	return
}

注意方法的入参和出参,都是按照gRPC的方法声明对应的:

  • 第一个参数必须是 context.Context ,第二个必须是proto内定义的 message 对应生成的结构体
  • 第一个返回值必须是proto内定义的 message 对应生成的结构体,第二个参数必须是 error
  • http框架bm中,如果共用proto文件生成bm代码,那么也可以直接使用该service方法

建议service严格按照此格式声明方法使其能够在bmwarden内共用。

重新生成一下代码:

cd rpcserver2/api/
kratos tool protoc

client调用

对于 client 端,前提必须有对应 proto 文件生成的代码,那么有两种选择:

  • 拷贝proto文件到自己项目下并且执行代码生成
  • 直接import服务端的api package

这里我开启了go mod模式,无法引入其他项目的包(或许我没找到方法吧)

所以选择直接拷贝一份

rpcserver1新建目录server2api
拷贝文件rpcserver2/api/api.protorpcserver2/api/client.go到目录rpcserver1/server2api

这里有点坑,注意新建目录和修改包名,不然会和rpcserver1原来api冲突

修改rpcserver1/server2api/api.proto文件包名

// 定义项目 API 的 proto 文件 可以同时描述 gRPC 和 HTTP API
// protobuf 文件参考:
//  - https://developers.google.com/protocol-buffers/
syntax = "proto3";

import "github.com/gogo/protobuf/gogoproto/gogo.proto";
import "google/protobuf/empty.proto";
import "google/api/annotations.proto";

// package 命名使用 {appid}.{version} 的方式, version 形如 v1, v2 ..
package service2.v1;

option go_package = "server2api";
option (gogoproto.goproto_getters_all) = false;

service MyServer2 {
  rpc Ping(.google.protobuf.Empty) returns (.google.protobuf.Empty);
  rpc Login(LoginReq) returns (LoginResp);
  rpc SayHello(HelloReq) returns (.google.protobuf.Empty);
  rpc SayHelloURL(HelloReq) returns (HelloResp) {
    option (google.api.http) = {
      get: "/myserver2/say_hello"
    };
  };
}

message HelloReq {
  string name = 1 [(gogoproto.moretags) = 'form:"name" validate:"required"'];
}

message HelloResp {
  string Content = 1 [(gogoproto.jsontag) = 'content'];
}

message LoginReq {
  string username = 1 [(gogoproto.moretags) = 'form:"username" validate:"required"'];
  string passwd = 2 [(gogoproto.moretags) = 'form:"passwd" validate:"required"'];
}

message LoginResp {
  string Content = 1 [(gogoproto.jsontag) = 'content'];
}

修改rpcserver2/api/client.go包名,并且target参数修改为direct模式

package server2api
import (
	"context"
	"fmt"

	"github.com/go-kratos/kratos/pkg/net/rpc/warden"

	"google.golang.org/grpc"
)

// AppID .
const AppID = "127.0.0.1:9002"

func NewClient(cfg *warden.ClientConfig, opts ...grpc.DialOption) (MyServer2Client, error) {
	client := warden.NewClient(cfg, opts...)
	cc, err := client.Dial(context.Background(), fmt.Sprintf("direct://default/%s", AppID))
	if err != nil {
		return nil, err
	}
	return NewMyServer2Client(cc), nil
}

// 生成 gRPC 代码
//go:generate kratos tool protoc --grpc --bm api.proto

targetgRPC用于服务发现的目标,使用标准url资源格式提供给resolver用于服务发现。 warden 默认使 用 direct 直连方式,直接与 server 端进行连接。如果在使用其他服务发现组件请看 warden服务发现

生成一下代码:

cd rpcserver1/server2api/
kratos tool protoc

修改rpcserver1/api/api.proto文件内容

// 定义项目 API 的 proto 文件 可以同时描述 gRPC 和 HTTP API
// protobuf 文件参考:
//  - https://developers.google.com/protocol-buffers/
syntax = "proto3";

import "github.com/gogo/protobuf/gogoproto/gogo.proto";
import "google/protobuf/empty.proto";
import "google/api/annotations.proto";

// package 命名使用 {appid}.{version} 的方式, version 形如 v1, v2 ..
package service1.v1;

option go_package = "api";
option (gogoproto.goproto_getters_all) = false;

service MyServer1 {
  rpc Ping(.google.protobuf.Empty) returns (.google.protobuf.Empty);
  //直接提供http服务, 方便测试
  rpc LoginUrl(LoginReq) returns (LoginResp){
    option (google.api.http) = {
      get: "/myserver1/login"
    };
  };
  rpc SayHello(HelloReq) returns (.google.protobuf.Empty);
  rpc SayHelloURL(HelloReq) returns (HelloResp) {
    option (google.api.http) = {
      get: "/myserver1/say_hello"
    };
  };
}

message HelloReq {
  string name = 1 [(gogoproto.moretags) = 'form:"name" validate:"required"'];
}

message HelloResp {
  string Content = 1 [(gogoproto.jsontag) = 'content'];
}

message LoginReq {
  string username = 1 [(gogoproto.moretags) = 'form:"username" validate:"required"'];
  string passwd = 2 [(gogoproto.moretags) = 'form:"passwd" validate:"required"'];
}

message LoginResp {
  string Content = 1 [(gogoproto.jsontag) = 'content'];
}

修改文件rpcserver1/internal/service/service.go,增加接口


//登录服务接口逻辑
func (s *Service) LoginUrl(ctx context.Context, req *pb.LoginReq) (reply *pb.LoginResp, err error) {
	fmt.Printf("server1 login username: %s, passwd: %s", req.Username, req.Passwd)

	cfg := &warden.ClientConfig{}
	paladin.Get("grpc.toml").UnmarshalTOML(cfg)

	var demoClient pb2.MyServer2Client
	if demoClient,err = pb2.NewClient(cfg); err != nil {
		panic(err)
	}

	reply2, err := demoClient.Login(ctx, (*pb2.LoginReq)(req))
	reply = (*pb.LoginResp)(reply2)

	return
}

在这里不直接输出内容,而是调用了rpcserver2的登录接口。
官方案例是建议在dao中调用,其实不管哪里都可以使用rpc调用,这只是规范点。
这里为了简化理解,所以直接在service里调用,大家理解后各自封装就好。

重新生成一下代码:

cd rpcserver1/api/
kratos tool protoc

到此为止,终于大功告成,可以运行一下代码测试了

cd rpcserver1/api/
kratos tool protoc
#打开第二个命令窗口
cd rpcserver2/api/
kratos tool protoc

打开浏览器访问:

http://localhost:8001/myserver1/login?username=aaa&passwd=111111

查看控制台输出
rpcserver1输出:
rpcserver1输出
可以看到rpcserver1调用了rpcserver2的登录接口。

rpcserver2输出
rpcserver2输出
查看浏览器输出
浏览器输出
浏览器返回了rpcserver2的结果。

由于rpcserver1也提供了rpc服务,所以其实也可以在rpcserver2中调用rpcserver1的服务,这里就不作演示了。

篇幅比较长,可能大家有点懵逼

最后附上本教程的项目源码:

https://download.csdn.net/download/uisoul/12833537

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐