gRPC服务的架构图
RPC调用总的来说就是客户端调用存根的代码,然后存根代码和RPC库实现通信,服务端的存根收到了信息后交给具体的服务进行处理,之后再原路返回就完了。
这里附上一篇关于gRPC讲解的博客园的文章
Proto文件代码生成
项目的前后端都是使用的gRPC进行通信,都需要使用protoc编译器把之前定义好的proto文件进行编译生成对应的代码进行调用。
问题1 timestamp.proto文件找不到
timestamp.proto
是google的一个时间戳的包,因为在我们自己的proto文件中使用到了google.protobuf.Timestamp
,在proeo文件的最上方也要导入对应的proto文件import "google/protobuf/timestamp.proto";
我记得在下载protoc编译器的时候,压缩包下面就有一个include文件夹,其中就包含有timestamp.proto
文件
问题2 go代码生成对应的包名和位置对不上
项目的后端采用的是go,想要把proto文件编译成golang的代码。这就需要在proto文件中加上go_package的字段,比如:
syntax = "proto3";
package rpc.auth;
option go_package = "github.com/BigNoseCattyHome/aorb/backend/rpc/auth;auth";
import "google/protobuf/timestamp.proto";
// 定义消息,用于请求和响应结构
message LoginRequest {
string username = 1; // 用户名/用户ID
string password = 2; // 密码的md5摘要
string device_id = 3; // 设备ID
google.protobuf.Timestamp timestamp = 4; // 时间戳
string nonce = 5; // 随机数
}
然后在使用protoc编译的时候,在命令行中也要加上go的一些选项,比如:
protoc --go_out=. person.proto // 找到当前目录下的person.proto并生成go的代码,输出到当前目录(.)
但是又有了一个问题就是在该文件夹下面,会生成你的包名go_package的层级目录,最后才是你的最终代码。但是我想让proto文件直接生成在一个固定的文件夹位置,并且没有那么多的层级文件夹。
然后发现了有一个命令选项是--go_opt=paths=source_relative
,使得生成的Go代码文件的路径与对应的.proto
文件的路径保持一致
问题3 dart代码生成的时候老是存在找不到pb.xx
后来上google和stackoverflow看了半天,发现结果是依赖版本的问题,把protobuf的依赖版本从^2.1.0改为^3.1.0,重新拉取一下依赖就没问题了。
之后队友给力解决了这个问题,把proto代码生成写进了Makefile,总算解决了proto文件代码生成的问题
PROTO_PATH=./idl
GOOGLE_PROTO_PATH=/usr/local/include
OUTPUT_DART_PATH=./frontend/lib/generated/
GO_OUT_PATH=./backend/rpc
PROTOC_GEN_DART=$(shell which protoc-gen-dart)
proto:
@echo "Creating golang and dart grpc files..."
@for file in $(PROTO_PATH)/*.proto; do \
if [ -f "$$file" ]; then \
prefix=$$(basename "$$file" .proto); \
mkdir -p $(GO_OUT_PATH)/"$${prefix}"; \
mkdir -p $(OUTPUT_DART_PATH); \
echo "Created directory for $$prefix"; \
protoc -I$(PROTO_PATH) -I$(GOOGLE_PROTO_PATH) \
--go_out=$(GO_OUT_PATH)/$$prefix --go_opt=paths=source_relative \
--go-grpc_out=$(GO_OUT_PATH)/$$prefix --go-grpc_opt=paths=source_relative \
--dart_out=grpc:$(OUTPUT_DART_PATH) \
--plugin=protoc-gen-dart=$(PROTOC_GEN_DART) \
$$file; \
echo "Generated gRPC code for $$prefix"; \
fi; \
done
@protoc -I$(GOOGLE_PROTO_PATH) \
--dart_out=grpc:$(OUTPUT_DART_PATH) \
--plugin=protoc-gen-dart=$(PROTOC_GEN_DART) \
google/protobuf/timestamp.proto
@echo "Generated Dart code for Google's timestamp.proto"
问题4 在windows上无法运行脚本
在另外一个队友的电脑上,他是使用的windows进行开发,然后遇到了代码生成失败的问题。然后看了一下,大概的问题就是命令行工具不一样,有些命令识别不了,之后把脚本改成windows的一些版本就ok了。
网关中使用consul进行服务发现
我们项目的架构就是前端向后端发送gRPC请求,实际上就是向后端的网关进行发送,前端就不用管后端具体是怎么实现的了,只需要向网关中发送请求即可。
网关的职责就是接收来自前端的请求,然后把请求转发给具体的微服务,等微服务处理好之后,再返回给网关,网关再给人家前端传回去就好了。
项目中使用到了consul进行服务注册和发现,当微服务启动的时候,就会向consul发送一个微服务注册,告诉consul他自己微服务的名字和地址。然后网关通过consul客户端与consul程序进行交流,可以对在线的微服务根据服务名进行查询,获取到这些微服务实例的地址,然后把来自前端的请求准确地转发到这些实例中。
这里有两个问题
问题1 gRPC调用的方法名和在consul中注册的名字不同
一般来说,gRPC的方法名是服务接口的一部分,像这样:<package>.<service>/<method>
我们的网关依赖于根据名字查找微服务的地址,我们的微服务的注册名为AorB-AuthService
,而我们的gRPC的方法名为rpc.auth.AuthService/Register
(包名为rpc.auth
)
所以需要在网关中增加一个映射,把gRPC的方法名转为在consul中注册的名字。
// gRPC 服务名到 Consul 服务名的映射
var serviceNameMapping = map[string]string{
"rpc.auth.AuthService": config.AuthRpcServerName,
"rpc.user.UserService": config.UserRpcServerName,
"rpc.comment.CommentService": config.CommentRpcServerName,
"rpc.vote.VoteService": config.VoteRpcServerName,
"rpc.poll.QuestionService": config.PollRpcServerName,
"rpc.recommend.RecommendService": config.RecommendRpcServerName,
}
问题2 网关转发gRPC请求的前提是能够正确识别和接收gRPC请求
之前没有找到问题的时候情况be like:前端写好了注册的逻辑,点击发送的按钮,在前端的控制台上可以看见各个发送的数据均是正常,但是会收到gRPC的错误代码13,找不到对应的服务unknow service rpc.auth.AuthService
。后端也启动了网关和auth微服务,但是在后端上在对应的方法上添加了日志输出,也没有任何日志打印出来。
尝试直接与微服务通信
想了半天没有什么思路,然后就想排查一下后端微服务的功能是不是OK的,然后就直接把前端的请求发送到auth微服务上去,结果就是能够正常返回。这就说明了就是网关的转发的问题。锁定了目标在网关,感觉问题就解决一大半了。
网关修复
在查阅了很多资料后发现,网关中虽然是转发gRPC请求,但是他也是一个gRPC服务器,也需要能够正确接收并处理AuthService的请求,所以他也需要实现AuthService接口,不过服务的实现采用 auth.UnimplementedAuthServiceServer
就OK了,因为具体的处理也并不是在这里,他只用转发就好了。然后在gRPC服务器上注册 auth.RegisterAuthServiceServer(s, &GatewayServer{})
服务就可以正常实现转发了。
将RESTful代码重构为gRPC代码
因为之前项目开发的时候后端采用了RESTful,后来决定整体上使用gRPC进行通讯,RESTful写得不是很多,所以就想要把它转过来。
RESTful和gRPC他们之间的差异是在调用资源和传输的方式上有所差异,但是对于数据的操作部分,如service之类的代码其实并没有影响,代码的重构只用考虑:如何从RESTful风格下获取资源变为在gRPC调用下如何获取。
理解生成的存根代码
首先需要生成对应语言的存根(stub)代码,比如在proto文件中有这么一个文件:
syntax = "proto3";
package user;
option go_package = "github.com/BigNoseCattyHome/aorb/backend/rpc/user;user";
message CoinRecord {
int64 consume = 1; // 消耗的金币数
string question_id = 2; // 为其投币的问题ID
string user_id = 3; // 使用者的ID
}
// TODO optional 字段在后续开发过程中应该逐步取消
message User {
string avatar = 1; // 用户头像
repeated string blacklist = 2; // 屏蔽好友
optional double coins = 3; // 用户的金币数
repeated CoinRecord coins_record = 4; // 用户金币流水记录
repeated string followed = 5; // 关注者
repeated string follower = 6; // 被关注者
uint32 id = 7; // 用户ID
optional string ipaddress = 8; // IP归属地
string nickname = 9; // 用户昵称
repeated uint32 questions_ask = 10; // 发起过的问题
repeated uint32 questions_asw = 11; // 回答过的问题
repeated uint32 questions_collect = 12; // 收藏的问题
string username = 13; // 用户登录名
}
message UserRequest{
uint32 user_id = 1; // 用户id
uint32 actor_id = 2; // 发送请求的用户的id
}
message UserResponse{
int32 status_code = 1; // 状态码,0-成功,其他值-失败
string status_msg = 2; // 返回状态描述
User user = 3; // 用户信息
}
message UserExistRequest{
uint32 user_id = 1; // 用户id
}
message UserExistResponse{
int32 status_code = 1; // 状态码,0-成功,其他值-失败
string status_msg = 2; // 返回状态描述
bool existed = 3; // 是否存在用户
}
service UserService{
rpc GetUserInfo(UserRequest) returns (UserResponse);
rpc GetUserExistInformation(UserExistRequest) returns (UserExistResponse);
}
然后他生成了对应的存根代码文件就是user.pb.go
和user_grpc.pb.go
- 其中
user.pb.go
中就是对定义的各个数据结构比如UserRequest
进行定义以及序列化和反序列化的代码 user_grpc.pb.go
主要包括了UserServiceClient
和UserServiceServer
这两个接口的定义,以及他们中具体的方法
// UserServiceClient is the client API for UserService 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 UserServiceClient interface {
GetUserInfo(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*UserResponse, error)
GetUserExistInformation(ctx context.Context, in *UserExistRequest, opts ...grpc.CallOption) (*UserExistResponse, error)
}
type userServiceClient struct {
cc grpc.ClientConnInterface
}
func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient {
return &userServiceClient{cc}
}
func (c *userServiceClient) GetUserInfo(ctx context.Context, in *UserRequest, opts ...grpc.CallOption) (*UserResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UserResponse)
err := c.cc.Invoke(ctx, UserService_GetUserInfo_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) GetUserExistInformation(ctx context.Context, in *UserExistRequest, opts ...grpc.CallOption) (*UserExistResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UserExistResponse)
err := c.cc.Invoke(ctx, UserService_GetUserExistInformation_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// UserServiceServer is the server API for UserService service.
// All implementations must embed UnimplementedUserServiceServer
// for forward compatibility
type UserServiceServer interface {
GetUserInfo(context.Context, *UserRequest) (*UserResponse, error)
GetUserExistInformation(context.Context, *UserExistRequest) (*UserExistResponse, error)
mustEmbedUnimplementedUserServiceServer()
}
服务的具体实现
这些方法直接在你的handler中进行实现,比如在auth的handler中进行实现Register
无非是参数需要从定义的auth.RegisterRequest中获取,其他的就是你的正常的服务。
// Register 注册
func (a AuthServiceImpl) Register(context context.Context, request *auth.RegisterRequest) (*auth.RegisterResponse, error) {
log.Infof("Received Register request: %v", request)
// 解析参数
user := models.User{
Username: request.Username,
Password: request.Password,
Nickname: request.Nickname,
Avatar: request.Avatar,
Ipaddress: request.Ipaddress,
}
// 调用服务
err := services.RegisterUser(user)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "register failed: %v", err)
}
// 返回响应
registerResponse := &auth.RegisterResponse{
Success: true,
Message: "User registered successfully",
}
return registerResponse, nil
}
其他要做的
完成好这些实现之后,最后再写一个main函数启动你的微服务,向consul中注册微服务,管理好日志以便于日后查找问题就好了。