grpc

2026年1月14日 · 734 字 · 4 分钟

讲解了grpc的简单实用,包括protoc,以及grpc的客户端,服务端,拦截器

grpc

grpc和http是对等的,都是属于应用层的方面

grpc通过protobuffer进行序列化,传输采用多路复用(适用于高并发的场景)

ps:这篇博客我代码的路径写的好烂,观看可能不是很好,但是懒得改了,凑活看吧(

proto语法与protoc工具

基本语法结构

syntax = "proto3"; //采用protubuffer v3版本的语法编写

package idl.student; //包名可以包含.但是不能有.  其他proto引用此proto时需要指定package,对成的go代码没有影响

option go_package = "idl/student;grpc_student"; //指定对成的go代码,分号前为go文件生成的路径(相对于go_out),分号后为包名

message student{//等同于go的结构体,转换为go代码会变成驼峰的形式
    string name = 2; //2是字段编号,每个字段编号不能重复,不能为0
    int64 id = 1;
    repeated string hobbies = 3; //repeated表示list,对用go的切片
    map<string,float> scores = 4; //map表示map,对用go的map
}

核心概念

语法元素 说明
syntax = "proto3" 指定使用 Protocol Buffers v3 版本语法
package 命名空间,用于避免消息类型命名冲突
option 编译器选项,影响代码生成行为
message 定义数据结构,等同于 Go 的 struct
repeated 表示数组/切片类型
map 表示字典类型

protoc 编译命令

生成前的目录结构:

web/
└── grpcs/
    └── student.proto
cd /web
protoc --go_out=./grpcs --proto_path=. ./grpcs/student.proto
参数 说明
--go_out 指定生成的 Go 代码输出目录
--proto_path 指定 proto 文件搜索目录(可简写为 -I)

生成后的目录结构:

web/
├── grpcs/
│   ├── student.proto
│   └── idl/
│       └── student/
│           └── student.pb.go

高级protoc命令选项

场景:导入其他proto文件并生成gRPC服务代码

当我们需要在一个 proto 文件中导入另一个 proto 文件,并生成 gRPC 服务端/客户端代码时,需要使用更多的 protoc 选项。

目录结构示例

生成前的目录结构:

web/
└── grpcs/
    ├── student.proto                          # 基础消息定义
    └── idl/
        └── service/
            └── student_service.proto          # 服务定义(import student.proto)

proto 文件示例

student.proto (基础消息):

syntax = "proto3";
package idl.student;
option go_package = "idl/student;grpc_student";

message student {
    string name = 1;
    int64 id = 2;
}

student_service.proto (服务定义):

syntax = "proto3";
package service.student;
option go_package = "idl/service/student;grpc_service_student";

import "grpcs/student.proto";  // 导入其他 proto 文件

message QueryStudentRequest {
    int64 Id = 1;
    string name = 2;
}

message QueryStudentResponse {
    repeated idl.student.student Students = 1;  // 使用导入的消息类型
}

service student {
    // Unary RPC - 一元调用
    rpc QueryStudent(QueryStudentRequest) returns (QueryStudentResponse);
    
    // Server streaming RPC - 服务端流式
    rpc QueryStudents2(StudentIds) returns (stream idl.student.student);
    
    // Client streaming RPC - 客户端流式
    rpc QueryStudents3(stream StudentId) returns (QueryStudentResponse);
    
    // Bidirectional streaming RPC - 双向流式
    rpc QueryStudents4(stream StudentId) returns (stream idl.student.student);
}

编译命令

web/ 目录下执行:

protoc \
  --go_out=./grpcs \
  --go-grpc_out=./grpcs \
  --proto_path=. \
  --go_opt=Mgrpcs/student.proto=goStudy/web/grpcs/idl/student \
  --go-grpc_opt=Mgrpcs/student.proto=goStudy/web/grpcs/idl/student \
  ./grpcs/idl/service/student_service.proto

参数详解

参数 说明
--go_out 指定生成 .pb.go 文件(消息定义)的输出目录,与option分号前组合
--go-grpc_out 指定生成 _grpc.pb.go 文件(gRPC 服务)的输出目录,与option分号前组合
--proto_path proto 文件的搜索根路径,可指定多个。import 语句会基于这些路径查找
--go_opt=M<proto路径>=<Go模块路径> 修改导入的 proto 文件在生成的 Go 代码中的 import 路径映射
--go-grpc_opt=M<proto路径>=<Go模块路径> --go_opt 类似,但用于 gRPC 生成器

关键注意事项

  1. proto_path 的作用

    • import "grpcs/student.proto" 会在 --proto_path 指定的目录下查找
    • 如果 --proto_path=.,则会在当前目录下查找 ./grpcs/student.proto
  2. 路径映射的关键点

    • M 后面必须是 proto 文件在 import 中使用的完整路径
    • ❌ 错误:Mstudent.proto=... (仅文件名)
    • ✅ 正确:Mgrpcs/student.proto=... (import 中的完整路径)
  3. Go 模块路径映射

    • 如果你的 Go 模块名是 goStudy
    • proto 文件的 go_packageidl/student
    • 则完整路径应该是:goStudy/web/grpcs/idl/student

生成后的目录结构

web/
└── grpcs/
    ├── student.proto
    └── idl/
        ├── student/
        │   └── student.pb.go                  # 消息定义代码
        └── service/
            ├── student_service.proto
            └── student/
                ├── student_service.pb.go       # 消息定义代码
                └── student_service_grpc.pb.go  # gRPC 服务代码(接口定义)

grpc服务端

查看我们刚刚生成的文件

type StudentServer interface {
	// Unary RPC
	QueryStudent(context.Context, *QueryStudentRequest) (*QueryStudentResponse, error)
	QueryStudents1(context.Context, *StudentIds) (*QueryStudentResponse, error)
	// Server streaming RPC
	QueryStudents2(*StudentIds, grpc.ServerStreamingServer[student.Student]) error
	// Client streaming RPC
	QueryStudents3(grpc.ClientStreamingServer[StudentId, QueryStudentResponse]) error
	// Bidirectional streaming RPC
	QueryStudents4(grpc.BidiStreamingServer[StudentId, student.Student]) error
	mustEmbedUnimplementedStudentServer()
}

作为服务端,我们需要完成刚刚生成的接口中的函数

import (
	"context"
	"fmt"
	grpc_service "goStudy/https/grpcs/idl/service/student"
	gprc_model "goStudy/https/grpcs/idl/student"
)

type Student struct {
	grpc_service.UnimplementedStudentServer
}

func (s *Student) QueryStudent(ctx context.Context, req *grpc_service.QueryStudentRequest) (*grpc_service.QueryStudentResponse, error) {
	fmt.Println("QueryStudent", req)
	resp := &grpc_service.QueryStudentResponse{
		Students: []*gprc_model.Student{
			{
				Name: "John Doe",
				Id:   1,
			},
		},
	}
	return resp, nil
}

运行

import (
	grpc_service "goStudy/web/grpcs/idl/service/student"
	"log"
	"net"

	"google.golang.org/grpc"
)

func main() {
	lis, err := net.Listen("tcp", "127.0.0.1:50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	server := grpc.NewServer()
	//注册实现
	grpc_service.RegisterStudentServer(server, &Student{})
	server.Serve(lis)
}

grpc客户端

这里开了3个协程调用

import (
	"context"
	"fmt"
	"log"

	grpc_service "goStudy/https/grpcs/idl/service/student"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func main() {
	coon, err := grpc.NewClient("127.0.0.1:50051",
		grpc.WithTransportCredentials(insecure.NewCredentials()), //必须加这一行,要不然报错
		//可以加一些全局选项
		grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(1024)), //设置最大接收消息大小为1kb
		grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(1024)), //设置最大发送消息大小为1kb
	)
	if err != nil {
		log.Fatalf("failed to connect: %v", err)
	}
	defer coon.Close()
	studentClient := grpc_service.NewStudentClient(coon)

	//开启三个协程,每个协程都调用一次QueryStudent,演示grpc的多路复用
	done := make(chan interface{}, 3)
	for i := 0; i < 3; i++ {
		go func() {
			ctx := context.Background()
			resp, err := studentClient.QueryStudent(ctx, &grpc_service.QueryStudentRequest{
				Id: 1,
			},
				//设置其他内容
				grpc.MaxCallRecvMsgSize(1024), //设置最大接收消息大小为1kb
			)
			if err != nil {
				log.Fatalf("failed to query student: %v", err)
			}
			fmt.Println(resp)
			done <- struct{}{} //完成了一个请求
		}()
	}
	for range 3 {
		<-done //阻塞,直到收到3个请求完成
	}
	fmt.Println("所有请求完成")
}

拦截器

有点类似于中间件

服务端

func timer(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
	begin := time.Now()
	resp, err = handler(ctx, req) //指的具体的接口实现
	elapsed := time.Since(begin)
	fmt.Printf("method %s took %s\n", info.FullMethod, elapsed)
	return
}

之后调用

这里演示普通的拦截器,其实也可以链式调用拦截器,在客户端演示

	server := grpc.NewServer(
		grpc.UnaryInterceptor(timer),
	)

客户端

func timer(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
	begin := time.Now()
	err := invoker(ctx, method, req, reply, cc, opts...)
	elapsed := time.Since(begin)
	fmt.Printf("method %s took %s\n", method, elapsed)
	return err
}

调用

	coon, err := grpc.NewClient("127.0.0.1:50051",
		grpc.WithTransportCredentials(insecure.NewCredentials()), //必须加这一行,要不然报错
		grpc.WithUnaryInterceptor(timer),                           //调用拦截器
		grpc.WithChainUnaryInterceptor(timer, timer),               //链式调用拦截器
	)

因为普通调用拦截器一次,链式调用2次,一共有3个协程,所以将会打印9次耗时日志

method /service.student.student/QueryStudent took 2.682709ms
method /service.student.student/QueryStudent took 2.691791ms
method /service.student.student/QueryStudent took 2.694834ms
method /service.student.student/QueryStudent took 2.671666ms
method /service.student.student/QueryStudent took 2.687ms
method /service.student.student/QueryStudent took 3.146792ms
Students:{name:"John Doe"  id:1}
Students:{name:"John Doe"  id:1}
method /service.student.student/QueryStudent took 2.518333ms
method /service.student.student/QueryStudent took 2.549041ms
method /service.student.student/QueryStudent took 2.553334ms
Students:{name:"John Doe"  id:1}
所有请求完成