B站微服务框架Kratos详细教程(5)- gRPC
简介kratos的grpc框架 不是直接使用的google的grpc,http也是对grpc接口做了封装(上一节我们就学习了使用pb生成http相关接口)。官方原文:GRPC Warden:基于官方gRPC开发,集成discovery服务发现,并融合P2C负载均衡;快速开始这次我们需要新建2个项目,用于展示RCP的相互调用,由于都是可以调用对方提供的服务,所以两个服务都可以作为客户端和服务端。新建
简介
kratos
的grpc框架 不是直接使用的google
的grpc
,http
也是对grpc
接口做了封装(上一节我们就学习了使用pb
生成http
相关接口)。
官方原文:
GRPC Warden
:基于官方gRPC
开发,集成discovery
服务发现,并融合P2C负载均衡;
快速开始
这次我们需要新建2个项目,用于展示RCP的相互调用,由于都是可以调用对方提供的服务,所以两个服务都可以作为客户端和服务端。
新建项目(服务1):
kratos new rpcserver1 #这里不指定创建类型,项目会同时生成http和grpc服务
新建项目(服务2)
kratos new rpcserver2 #这里不指定创建类型,项目会同时生成http和grpc服务
当然,你也可以添加--grpc
参数,只生成grpc
项目,这里为了方便后面测试,所以不做指定(实际项目中,其实也可能http
和grpc
都会用到)。
生成目录概览:
├── 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
严格按照此格式声明方法使其能够在bm
和warden
内共用。
重新生成一下代码:
cd rpcserver2/api/
kratos tool protoc
client调用
对于 client 端,前提必须有对应 proto 文件生成的代码,那么有两种选择:
- 拷贝proto文件到自己项目下并且执行代码生成
- 直接import服务端的api package
这里我开启了go mod
模式,无法引入其他项目的包(或许我没找到方法吧)
所以选择直接拷贝一份
rpcserver1
新建目录server2api
拷贝文件rpcserver2/api/api.proto
和rpcserver2/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
target
为gRPC
用于服务发现的目标,使用标准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
调用了rpcserver2
的登录接口。
rpcserver2
输出
查看浏览器输出
浏览器返回了rpcserver2
的结果。
由于rpcserver1
也提供了rpc
服务,所以其实也可以在rpcserver2
中调用rpcserver1
的服务,这里就不作演示了。
篇幅比较长,可能大家有点懵逼
最后附上本教程的项目源码:
更多推荐
所有评论(0)