跳到主要内容

go实现rpc和http服务

01 准备工作

下面两个工具,用来从pb文件生成数据结构和grpc服务代码

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

02 新建项目

新建一个项目,在项目目录下执行go mod init命令,完成go module初始化。

03 使用protobuf定义gRPC服务

syntax = "proto3";

package user;

//新版本需要的配置
option go_package = "./user";

import "google/api/annotations.proto";

message UserRequest {
int32 id = 1;
}

message UserResponse {
int32 id = 1;
string name = 2;
}

service UserService {
rpc GetUser(UserRequest) returns (UserResponse) {};
}

04 生成代码

protoc ./proto/user.proto \
--go_out=./proto \
--go-grpc_out=./proto --go-grpc_opt=require_unimplemented_servers=false

05 写server端和client端代码

server端

  • 声明一个结构体,结构体里引用生成代码中的UnimplementedXXX接口

  • 实现一个创建结构体示例的方法NewXXX

  • 给这个结构体实现方法

  • 写一个main方法,grpc服务启动和注册服务

// UserServer 定义接口结构体,直接引用生成的桩代码里的接口定义
type UserServer struct {
user.UnimplementedUserServiceServer
}

// NewUserServer 创建一个接口实例
func NewUserServer() *UserServer {
return &UserServer{}
}

// GetUser 实现接口
func (userService *UserServer) GetUser(ctx context.Context, req *user.UserRequest) (*user.UserResponse, error) {
response := &user.UserResponse{
Id: req.Id,
Name: "hello world",
}
return response, nil
}


// startGrpc 启动grpc服务
func startGrpc(ctx context.Context) {
// Create a listener on TCP port
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *grpcPort))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}

// 创建一个gRPC server对象
s := grpc.NewServer()
// 注册user server到server
user.RegisterUserServiceServer(s, internal.NewUserServer())
log.Println(fmt.Sprintf("server listening at %v", lis.Addr()))

// 启动gRPC Server
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

client端

  • 创建链接,创建client

  • 调用接口

func main() {
flag.Parse()
// set up a connection to server.
conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()

client := user.NewUserServiceClient(conn)

res, _ := client.GetUser(context.Background(), &user.UserRequest{Id: 1})
resp, err := json.Marshal(res)
fmt.Printf("%s", resp)
}

至此,一个grpc服务的服务端,客户端,都已经完成,并且能运行了。

06 使用gRPC-gateway为gRPC暴露http接口

gRPC-gateway是protobuf编译器protoc的插件,它先读取pb文件中的service定义的内容,并生成反向代理服务器(代码),该服务可以将rest api转换为gRPC。

目前有三种方式可以生成反向代理服务器(代码)

  • 不做任何修改直接生成,protoc-gen-grpc-gateway会按照默认规则映射Method和参数等HTTP配置

  • 给protobuf添加annotations,可以自定义Method和Path等HTTP配置

  • 使用外部配置,比较适用于不能修改源protobuf的情况下

我们使用第二种方式。

安装gRPC-gateway

go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest

代码生成

因为使用的是非内置的pb定义google/api/annotations.proto,需要在生成前先添加pb依赖,从官方仓库复制对应的pb文件到项目里。

https://github.com/googleapis/googleapis/blob/master/google/api/annotations.proto
https://github.com/googleapis/googleapis/blob/master/google/api/http.proto

然后在proto文件中引入import "google/api/annotations.proto";,并且在proto的service的方法中增加option块。

rpc GetUser(UserRequest) returns (UserResponse) {
//在这里添加google.api.http注释
option (google.api.http) = {
get: "/api/getuser"
};
};

运行命令

protoc ./proto/proto/user.proto \
--go_out=./proto \
--go-grpc_out=./proto --go-grpc_opt=require_unimplemented_servers=false \
--grpc-gateway_out=./proto

会在proto目录下额外生成一个user.pb.gw.go文件。 新生成的文件中,有注册相关访问,调用即可实现http接口的转发。

server端代码

下面两种方式都可以

// startGateWay 第一种方式,先启动gRPC,在以gRPC服务为基础创建gRPC-gateway服务
func startGateWay(ctx context.Context) {
//创建一个连接到我们刚刚启动的 gRPC 服务器的客户端连接
conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%v", *grpcPort), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
// 创建一个mux并注册
mux := runtime.NewServeMux()
if err := user.RegisterUserServiceHandler(ctx, mux, conn); err != nil {
log.Fatalln("Failed to register gateway:", err)
}
//创建并启动http server
gwServer := &http.Server{
Addr: fmt.Sprintf(":%d", *httpPort),
Handler: mux,
}
log.Println(fmt.Sprintf("gateway listening at %v", gwServer.Addr))

if err := gwServer.ListenAndServe(); err != nil {
log.Fatalln("Failed to start gateway:", err)
}
}
// startGateWay2 第2种方式,直接使用本地函数调用方法访问gRPC
func startGateWay2(ctx context.Context) {
// 创建一个mux并注册,使用本地函数调用方式
mux := runtime.NewServeMux()
if err := user.RegisterUserServiceHandlerServer(ctx, mux, internal.NewUserServer()); err != nil {
log.Fatalln("Failed to register gateway2:", err)
}
//创建并启动http server
gwServer := &http.Server{
Addr: fmt.Sprintf(":%d", *httpPort),
Handler: mux,
}
log.Println(fmt.Sprintf("gateway2 listening at %v", gwServer.Addr))

if err := gwServer.ListenAndServe(); err != nil {
log.Fatalln("Failed to start gateway2:", err)
}
}

附录:buf使用

buf是方便我们管理proto文件依赖,生成代码的工具。buf命令,要在proto目录下使用 安装buf

通过brew install buf安装buf工具

初始化buf

在proto目录下,使用buf mod init创建一个buf.yaml文件

添加依赖

在此文件中添加依赖buf.build/googleapis/googleapis

deps:
- buf.build/googleapis/googleapis

更新依赖

buf mod update buf从Buf Schema Registry(BSR)中获取依赖,把所有的deps更新到最新版,并且会生成buf.lock来固定版本

代码生成

创建一个buf.gen.yaml

version: v1
plugins:
- plugin: go
out: ./
- plugin: go-grpc
out: ./
- name: grpc-gateway
out: ./

然后执行buf generate ./ ./可以省略,会在当前目录下生成需要的go文件。

参考文章:文章中描述了对请求的精细化定制、生成swagger文件、同一个端口同时实现gRPC和http服务等内容,有需要再继续学习。 https://juejin.cn/post/7291245343269339151 https://www.liwenzhou.com/posts/Go/grpc-gateway/