前言
-
真聊会天
- gRPC 是一种现代化开源的高性能 RPC 框架,能够运行于任意环境之中,最初由谷歌进行开发,使用的是 HTTP/2 作为传输协议
- 本文开袋即食,大致内容目录里面可以体现,主要帮助新手快速学习 GRPC
-
开发环境
- Windows x64
- go version go1.20 windows/amd64
- code --version 1.76.2
- libprotoc 22.1
- OpenSSL 3.1.0 14 Mar 2023 (Library: OpenSSL 3.1.0 14 Mar 2023)
-
代码地址
v0 准备工作
- 根据自己的开发环境下载 protoc ,并将其 bin 目录配置到环境变量中
- 在命令行中输入
protoc --version
即可查看是否配置成功 如果你懒得搜且开发环境和我类似,你也可以和我下载一样的
- 在命令行中输入
- 创建项目与 go mod 文件,在命令行中输入
go get google.golang.org/grpc
安装 grpc 的核心库 - 在命令行中输入
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
和go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
安装对应工具- 在命令行中分别输入
protoc-gen-go --version
和protoc-gen-go-grpc --version
即可查看是否配置成功 - 此时在 %GOPATH%/bin 目录下可以看到这两个工具:
- 在命令行中分别输入
V1 入门示例
-
在 server 端创建 pb 文件夹,编写 hello.proto 文件来定义服务
// 版本声明,使用Protocol Buffers v3版本 syntax = "proto3"; // 这部分的内容是关于最后生成的 go 文件是处在哪个目录哪个包中 // . 代表在当前目录生成 // 以 ; 号间隔 // service 代表了生成的 go 文件的包名是 service option go_package = ".;pb"; // 然后我们需要定义一个服务,在这个服务中需要有一个方法,这个方法可以接受客户端的参数,再返回服务端的响应 // 其实很容易可以看出,我们定义了一个 service ,称为 Greeter ,这个服务中有一个 rpc 方法,名为 SayHello // 这个方法会发送一个 HelloRequest ,然后返回一个 HelloResponse service Greeter { rpc SayHello (HelloRequest) returns (HelloResponse) {} } // message 关键字,其实你可以理解为 Golang 中的结构体 // 这里比较特别的是变量后面的“赋值” // 这里并不是赋值,而是定义在这个 message 中的位置 message HelloRequest { string name = 1; // 第一行就标识为 1 // int64 age = 2; // 第二行就标识为 2 } message HelloResponse { string reply = 1; }
-
在 client 端也创建一个 pb 文件夹,并将上面的 proto 文件拷贝到客户端的文件夹中
-
分别在服务端和客户端的 pb 目录下打开命令行,并执行下面两条命令:
protoc --go_out=. hello.proto
protoc --go-grpc_out=. hello.proto
pb 目录中会自动生成两个文件:
- hello.pb.go
- hello_grpc.pb.go
-
此时服务端的目录结构:
以及客户端的目录结构:
-
编写服务端 main.go 文件
package main import ( "context" "fmt" "net" "github.com/sanyewudezhuzi/gRPC_study/pb" "google.golang.org/grpc" ) // hello_server type server struct { pb.UnimplementedGreeterServer } func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) { fmt.Println("hello: " + req.Name) return &pb.HelloResponse{Reply: "Hello " + req.Name}, nil } func main() { // 监听本地的 9090 端口 listen, err := net.Listen("tcp", "127.0.0.1:9090") if err != nil { panic("failed to listen") } // 创建gRPC服务器 grpcServer := grpc.NewServer() // 在 grpc 客户端注册我们自己编写的服务 pb.RegisterGreeterServer(grpcServer, &server{}) // 启动服务 err = grpcServer.Serve(listen) if err != nil { panic("failed to server") } }
-
编写服务端 main.go 文件
package main import ( "context" "flag" "fmt" "log" "time" "github.com/sanyewudezhuzi/gRPC_study/pb" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) // hello_client const ( defaultName = "world" ) var ( addr = flag.String("addr", "127.0.0.1:9090", "IP address and port number of the tcp connection") name = flag.String("name", defaultName, "Name to greet") ) func main() { // 将用户传递至命令行的参数解析为对应变量值 flag.Parse() // 连接到server端,此处禁用安全传输 conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("failed to connect: %v", err) } defer conn.Close() // 建立连接 client := pb.NewGreeterClient(conn) // 执行RPC调用并打印收到的响应数据(这个方法在服务端实现并返回结果) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() res, err := client.SayHello(ctx, &pb.HelloRequest{Name: *name}) if err != nil { log.Fatalf("failed to sayhello: %v", err) } fmt.Println(res.GetReply()) }
-
先后执行服务端和客户端:
v2 加密认证
先唠嗑几个知识点:
key:服务器上的私钥文件,用于对发送给客户端数据的加密,以及对客户端接收到数据的解密
csr:证书签名请求文件,用于提交给证书颁发机构(CA)对证书签名
crt:有证书颁发机构(CA)签名后的证书,或者是开发者自签名的证书,包含证书持有人的信息,持有人的公钥,以及签署者的签名等信息
pem:是基于 Base64 编码的证书格式,扩展名包括 PEM 、CRT 、CER
在 v1 的基础上,于服务端中新增 key 目录
- 下载 openssl
- 在命令行中输入
openssl version
即可查看是否配置成功 如果你懒得搜且开发环境和我类似,你也可以和我下载一样的
- 在命令行中输入
- 在 key 目录下打开命令行,输入
openssl genrsa -out server.key 2048
生成私钥- 跟我一起念~ 私(sī)钥(yuè)~
- 在命令行输入
openssl req -new -x509 -key server.key -out server.crt -days 36500
生成证书,有关选项可以不填,全部回车即可 - 在命令行输入
openssl req -new -key server.key -out server.csr
生成 csr ,有关选项可以不填,全部回车即可 - 更改 openssl.cfg
- 在 openssl 安装路径的 bin 目录下找到 openssl.cfg 文件,将其拷贝到项目的 key 目录下
- 搜索
copy_extensions
,将copy_extensions = copy
开启 - 搜索
req_extensions
,将req_extensions = v3_req # The extensions to add to a certificate request
开启 - 搜索
v3_req
,添加字段**subjectAltName** = @alt_names
- 添加标签
[ alt_names ]
和字段DNS.1 = *.sanyewu.com
其中 sanyewu 可以替换为你所想要的域名
- 在命令行输入
openssl genpkey -algorithm RSA -out test.key
生成证书私钥 - 在命令行输入
openssl req -new -nodes -key test.key -out test.csr -days 3650 -subj "/C=cn/OU=myorg/O=mycomp/CN=myname" -config ./openssl.cfg -extensions v3_req
通过私钥 test.key 生成证书请求文件 test.csr - 在命令行输入
openssl x509 -req -days 365 -in test.csr -out test.pem -CA server.crt -CAkey server.key -CAcreateserial -extfile ./openssl.cfg -extensions v3_req
生成 SAN 证书 pem - 完成后你的 key 目录下应该有这 8 个文件:
更新服务端的 main.go
func main() {
// 生成证书
// cretFile: 证书文件的绝对路径
// keyFile: 私钥文件的绝对路径
creds, err := credentials.NewServerTLSFromFile(cretFile, keyFile)
if err != nil {
panic("failed to creds")
}
// 监听本地的 9090 端口
listen, err := net.Listen("tcp", "127.0.0.1:9090")
if err != nil {
panic("failed to listen")
}
// 创建gRPC服务器
// 配置证书
grpcServer := grpc.NewServer(grpc.Creds(creds))
// 在 grpc 客户端注册我们自己编写的服务
pb.RegisterGreeterServer(grpcServer, &server{})
// 启动服务
err = grpcServer.Serve(listen)
if err != nil {
panic("failed to server")
}
}
将 test.pem 拷贝到客户端的 key 目录下,并更新客户端的 main.go
func main() {
// 将用户传递至命令行的参数解析为对应变量值
flag.Parse()
// 获取证书
// cretFile: 证书文件的绝对路径
// serverNameOverride: 服务器的名称替代
creds, err := credentials.NewClientTLSFromFile(cretFile, *serverNameOverride)
if err != nil {
panic("failed to creds")
}
// 连接到server端
conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
defer conn.Close()
// 建立连接
client := pb.NewGreeterClient(conn)
// 执行RPC调用并打印收到的响应数据(这个方法在服务端实现并返回结果)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
res, err := client.SayHello(ctx, &pb.HelloRequest{Name: *name})
if err != nil {
log.Fatalf("failed to sayhello: %v", err)
}
fmt.Println(res.GetReply())
}
分别运行服务端和客户端
v3 流式传输
v3.0 服务端流式 rpc
服务端流 RPC 下,客户端发出一个请求,但不会立即得到一个响应,而是在服务端与客户端之间建立一个单向的流,服务端可以随时向流中写入多个响应消息,最后主动关闭流,而客户端需要监听这个流,不断获取响应直到流关闭
-
定义服务
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse) {}
-
服务端实现
func (s *server) LotsOfReplies(req *pb.HelloRequest, stream pb.Greeter_LotsOfRepliesServer) error { fmt.Println(req.GetName()) words := []string{ "你好 ", "hello ", "こんにちは ", "안녕하세요 ", "สวัสดี ", } for _, word := range words { data := &pb.HelloResponse{ Reply: word + req.GetName(), } // 使用 send 方法返回多个数据 if err := stream.Send(data); err != nil { return err } } return nil }
-
客户端实现
// LotsOfReplies 返回使用多种语言打招呼 func runLotsOfReplies(c pb.GreeterClient) { // server端流式RPC ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() stream, err := c.LotsOfReplies(ctx, &pb.HelloRequest{Name: *name}) if err != nil { log.Fatalln("failed to lotsofreplies:", err) } for { // 接收服务端返回的流式数据,当收到io.EOF或错误时退出 res, err := stream.Recv() if err == io.EOF { break } if err != nil { log.Fatalln("failed to recv:", err) } fmt.Println(res.GetReply()) } }
-
运行
v3.1 客户端流式 rpc
客户端传入多个请求对象,服务端返回一个响应结果
-
定义服务
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {}
-
服务端实现
// LotsOfGreetings 接收流式数据 func (s *server) LotsOfGreetings(stream pb.Greeter_LotsOfGreetingsServer) error { reply := "你好: " for { // 接收客户端发来的流式数据 res, err := stream.Recv() if err == io.EOF { // 最终统一回复 return stream.SendAndClose(&pb.HelloResponse{ Reply: reply, }) } if err != nil { return err } reply += res.GetName() fmt.Println("res: ", res.GetName()) } }
-
客户端实现
func runLotsOfGreeting(c pb.GreeterClient) { // 客户端流式RPC ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() stream, err := c.LotsOfGreetings(ctx) if err != nil { log.Fatalln("failed to lotsofgreetings:", err) } namelist := strings.Split(*names, ",") for _, name := range namelist { // 发送流式数据 err := stream.Send(&pb.HelloRequest{Name: name}) if err != nil { log.Fatalln("failed to send:", err) } } res, err := stream.CloseAndRecv() if err != nil { log.Fatalln("failed to closeandrecv:", err) } fmt.Println(res.GetReply()) }
-
运行
v3.2 双向流式 rpc
双向流式rpc 结合客户端流式rpc和服务端流式rpc,可以传入多个对象,返回多个响应对象
-
定义服务
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse) {}
-
服务端实现
// 从隔壁大佬偷的价值连城的 AI 模型 func aimodel(s string) string { s = strings.ReplaceAll(s, "吗", "") s = strings.ReplaceAll(s, "吧", "") s = strings.ReplaceAll(s, "你", "我") s = strings.ReplaceAll(s, "?", "!") s = strings.ReplaceAll(s, "?", "!") return s } // BidiHello 双向流式打招呼 func (s *server) BidiHello(stream pb.Greeter_BidiHelloServer) error { for { // 接收流式请求 res, err := stream.Recv() if res == nil { return err } if err != nil { log.Fatalln("failed to recv:", err) return err } // 对收到的数据做些处理 fmt.Println(res.GetName()) reply := aimodel(res.GetName()) // 返回流式响应 if err := stream.Send(&pb.HelloResponse{Reply: reply}); err != nil { return err } } }
-
客户端实现
func runBidiHello(c pb.GreeterClient) { // 双向流模式 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() stream, err := c.BidiHello(ctx) // 双向流要手动关闭来标记消息的结束 defer stream.CloseSend() if err != nil { log.Fatalln("failed to bidihello:", err) } wg := sync.WaitGroup{} wg.Add(1) go func() { defer wg.Done() for { // 接收服务端返回的响应 res, err := stream.Recv() if res == nil { return } if err != nil { log.Fatalln("failed to recv:", err) } fmt.Println("AI: ", res.GetReply()) } }() // 从标准输入获取用户输入 reader := bufio.NewReader(os.Stdin) for { cmd, _ := reader.ReadString('\n') cmd = strings.TrimSpace(cmd) if len(cmd) == 0 { continue } if strings.ToUpper(cmd) == "EXIT" { stream.Send(nil) break } // 将获取到的数据发送至服务端 if err := stream.Send(&pb.HelloRequest{Name: cmd}); err != nil { log.Fatalln("failed to send:", err) } } wg.Wait() }
-
运行
v4 metadata
在多个微服务的调用当中,信息交换常常是使用方法之间的参数传递的方式,但是在有些场景下,一些信息可能和 RPC 方法的业务参数没有直接的关联,所以不能作为参数的一部分,在 gRPC 中,可以使用元数据来存储这类信息。元数据最主要的作用:
- 提供RPC调用的元数据信息,例如用于链路追踪的traceId、调用时间、应用版本等等。
- 控制gRPC消息的格式,例如是否压缩或是否加密。
实际开发中,我们一般使用第三方库 google.golang.org/grpc/metada… 来操作元数据。
创建元数据
元数据的类型定义:
type md map[string][]string
metadata中的键是大小写不敏感的,由字母、数字和特殊字符 -
、_
、.
组成并且不能以 grpc-
开头(gRPC保留自用),二进制值的键名必须以 -bin
结尾
-
使用 metadata 库的
New()
函数来创建元数据md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})
-
使用 metadata 库的
Pairs()
函数来创建元数据md := metadata.Pairs( "key1", "val1", "key1", "val1-2", // "key1"的值将会是 []string{"val1", "val1-2"} "key2", "val2", "key-bin", string([]byte{96, 102}), // 二进制数据在发送前会进行(base64) 编码 )
-
使用
FromIncomingContext
从 RPC 请求的上下文中获取func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, err) { md, ok := metadata.FromIncomingContext(ctx) // do something with metadata }
客户端发送接收元数据
var database = map[string]string{
"zhuzi": "abc123",
}
// normalCall 普通调用
func (s *server) normalCall(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
// 设置 trailer
defer func() {
trailer := metadata.Pairs("timestamp", strconv.Itoa(int(time.Now().Unix())))
grpc.SetTrailer(ctx, trailer)
}()
// 获取 metadata
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.DataLoss, "failed to get metadata")
}
name := md["name"][0]
pwd := md["pwd"][0]
if p, ok := database[name]; ok && p == pwd {
fmt.Println("name:", name)
} else {
return nil, status.Error(codes.Unauthenticated, "invalid info")
}
// 发送 header
header := metadata.New(map[string]string{"greeting": name + " say: hello " + req.Name})
grpc.SendHeader(ctx, header)
return &pb.HelloResponse{Reply: req.Name}, nil
}
// streamCalls 流式调用
func (s server) streamCalls(stream pb.Greeter_BidiHelloServer) error {
// 设置 trailer
defer func() {
trailer := metadata.Pairs("timestamp", strconv.Itoa(int(time.Now().Unix())))
stream.SetTrailer(trailer)
}()
// 获取 metadata
md, ok := metadata.FromIncomingContext(stream.Context())
if !ok {
return status.Errorf(codes.DataLoss, "failed to get metadata")
}
name := md["name"][0]
pwd := md["pwd"][0]
if p, ok := database[name]; ok && p == pwd {
fmt.Println("name:", name)
} else {
return status.Error(codes.Unauthenticated, "invalid info")
}
// 发送数据
for {
res, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
log.Fatalln("failed to recv:", err)
return err
}
fmt.Println("get name: ", res.GetName())
if err := stream.Send(&pb.HelloResponse{Reply: name + "say: hello " + res.Name}); err != nil {
return err
}
}
}
服务端发送接收元数据
func runSayHello(c pb.GreeterClient, name string) {
// 创建 metadata
md := metadata.Pairs(
"name", "zhuzi",
"pwd", "abc123",
)
// 基于 metadata 创建 context
ctx := metadata.NewOutgoingContext(context.Background(), md)
// RPC 调用
var header, trailer metadata.MD
res, err := c.SayHello(
ctx,
&pb.HelloRequest{Name: name},
grpc.Header(&header), // 接收服务端发来的 header
grpc.Trailer(&trailer), // 接收服务端发来的 trailer
)
if err != nil {
log.Println("failed to call sayhello:", err)
return
}
// 从 header 中获取 greeting
if g, ok := header["greeting"]; ok {
fmt.Println(g[0])
} else {
log.Println("failed to get greeting")
return
}
// 获取响应
fmt.Println("res: ", res.GetReply())
// 从trailer中取timestamp
if t, ok := trailer["timestamp"]; ok {
fmt.Println("timestamp: ", t[0])
} else {
log.Println("failed to get timestamp")
}
}
func runBidiHello(c pb.GreeterClient) {
// 创建 metadata
md := metadata.Pairs(
"name", "zhuzi",
"pwd", "abc123",
)
// 基于 metadata 创建 context
ctx := metadata.NewOutgoingContext(context.Background(), md)
// RPC 调用
stream, err := c.BidiHello(ctx)
defer stream.CloseSend()
if err != nil {
log.Println("failed to call bidhello:", err)
return
}
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
for {
// 接收服务端返回的响应
res, err := stream.Recv()
if err == io.EOF {
return
}
if err != nil {
log.Fatalln("failed to recv:", err)
}
fmt.Println(res.GetReply())
}
}()
// 从标准输入获取用户输入
reader := bufio.NewReader(os.Stdin)
for {
cmd, _ := reader.ReadString('\n')
cmd = strings.TrimSpace(cmd)
if len(cmd) == 0 {
continue
}
if strings.ToUpper(cmd) == "EXIT" {
stream.Send(nil)
break
}
// 将获取到的数据发送至服务端
if err := stream.Send(&pb.HelloRequest{Name: cmd}); err != nil {
log.Fatalln("failed to send:", err)
}
}
wg.Wait()
// 结束时读取 trailer
trailer := stream.Trailer()
if t, ok := trailer["timestamp"]; ok {
fmt.Println("timestamp: ", t[0])
} else {
log.Println("failed to get timestamp")
}
}