GRPC安装和生成代码
注:
GRPC
是谷歌开源的一个可以跨语言的RPC
框架- 它有一个很重要的依赖文件:
protocol buffers
(proto3
),它类似于HTTP
传输中的url+json
- Documentation | gRPC
- Language Guide (proto 3) | Protocol Buffers Documentation (protobuf.dev)
Goland
安装protobuf
插件- 新建三个
Package
:client
、server
、pb
pb
包下新建hello_grpc.proto
文件
syntax = "proto3";
option go_package = "./;hello_grpc";
package hello_grpc;
message Req {
string message = 1;
}
message Res {
string message = 1;
}
service HelloGRPC{
// SayHey 方法名 Req 入参 Res 出参
rpc SayHey(Req) returns (Res);
}
- 安装grpc和proto的解释器(编译工具),在当前
pb
的上两级目录执行命令:
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
这俩安装完后,会自动生成两个可执行文件protoc-gen-go.exe
和protoc-gen-go-grpc.exe
,并放到$GOPATH
的bin
目录下
- 在当前
pb
的上一级目录执行下面命令,拉取grpc
代码包
go get google.golang.org/grpc
- 在
pb
目录下创建build.bat
文件:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./hello_grpc.proto
- 添加
bin
目录的绝对路径到环境变量Path
中 - 进入
pb
目录,打开powershell
(在Goland
的终端中可能会报错,在文件夹下右键打开终端),执行下面命令编译proto
,在当前目录下生成hello_grpc.pb.go
和hello_grpc_grpc.pb.go
文件:
.\build.bat
- 自此即通过
proto
自动生成grpc
的方法
编写客户端和服务端
server.go
:
package main
import (
"context"
"fmt"
hello_grpc "goclass/pb"
"google.golang.org/grpc"
"net"
)
//取出server
type server struct {
hello_grpc.UnimplementedHelloGRPCServer
}
//挂载方法
func (s *server) SayHey(ctx context.Context, req *hello_grpc.Req) (res *hello_grpc.Res, err error) {
// 客户端调用时的处理逻辑
fmt.Println(req.GetMessage())
return &hello_grpc.Res{Message: "我是从服务端返回的grpc的内容"}, nil
}
func main() {
//1.注册监听
l, _ := net.Listen("tcp", ":8888")
//2.注册一个server
s := grpc.NewServer()
hello_grpc.RegisterHelloGRPCServer(s, &server{})
s.Serve(l)
}
client.go
:
package main
import (
"context"
"fmt"
hello_grpc "goclass/pb"
"google.golang.org/grpc"
)
func main() {
//建立一个连接到服务器
conn, _ := grpc.Dial("localhost:8888", grpc.WithInsecure())
//最后要关闭连接
defer conn.Close()
//new一个客户端
client := hello_grpc.NewHelloGRPCClient(conn)
//调用client的方法
req, _ := client.SayHey(context.Background(), &hello_grpc.Req{Message: "我从客户端来"})
//打印返回信息
fmt.Println(req.GetMessage())
}
客户端运行结果:
服务端运行结果:
protobuf的基础使用方法
- 保证有
go.mod
文件,若没有的话,使用go mod init
生成 - 在
src
下新建pb
文件夹,这里我由于上面建过pb
,所以我新建pb_1
文件夹(下文统一用pb
指代) - 在
pb
文件夹下给protobuf
做好分包:在pb
下新建person
文件夹,在person
文件夹下新建person.proto
文件
syntax = "proto3";//声明语法版本 告诉编译器用proto3来解读
package person;//给当前proto分配包名称 这个和go的包不一样
option go_package = "goclass/pb/person;person";//包路径(从mod里的module下开始写);别名
message Person{
//声明格式:类型 key(下划线) = 唯一(标识)
//类比到具体语言中的数据类型:
//https://protobuf.dev/programming-guides/proto3/#scalar
string name = 1;
int32 age = 2;
bool sex = 3;
//声明切片(数组) 关键字repeated
repeated string test = 4;
//声明map格式:map<key,value> key = 标识;
map<string, string> test_map = 5;
}
//类型嵌套
message Home{
repeated Person persons = 1;
message visitor{
string name = 1;
}
Visitor visitor = 2;
}
- 在
pb
目录下创建build.bat
文件:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./person/person.proto
- 添加
bin
目录的绝对路径到环境变量Path
中(上面做过的就不用再添加了) - 进入
pb
目录,打开powershell
(在Goland
的终端中可能会报错,在文件夹下右键打开终端),执行下面命令编译proto
,在当前目录下生成person.pb.go
文件:
.\build.bat
- 自此即通过
proto
自动生成grpc
的方法 - 可以看到在
person.pb.go
文件中生成了对应的结构体
type Person struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
//声明格式:类型 key(下划线) = 唯一(标识)
//类比到具体语言中的数据类型:
//https://protobuf.dev/programming-guides/proto3/#scalar
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
Sex bool `protobuf:"varint,3,opt,name=sex,proto3" json:"sex,omitempty"`
//声明切片(数组) 关键字repeated
Test []string `protobuf:"bytes,4,rep,name=test,proto3" json:"test,omitempty"`
//声明map格式:map<key,value> key = 标识;
TestMap map[string]string `protobuf:"bytes,5,rep,name=test_map,json=testMap,proto3" json:"test_map,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
//类型嵌套
type Home struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Persons []*Person `protobuf:"bytes,1,rep,name=persons,proto3" json:"persons,omitempty"`
Visitor *Home_Visitor `protobuf:"bytes,2,opt,name=visitor,proto3" json:"visitor,omitempty"`
}
交叉引用
- 在
pb
新建home
文件夹,在home
文件夹下新建home.proto
文件
syntax = "proto3";//声明语法版本 告诉编译器用proto3来解读
package home;//给当前proto分配包名称 这个和go的包不一样
option go_package = "goclass/pb/home;home";//包路径(从mod里的module下开始写);别名
message Home{
string home_num = 1;
}
- 在
person.proto
中引入home.proto
syntax = "proto3";//声明语法版本 告诉编译器用proto3来解读
package person;//给当前proto分配包名称 这个和go的包不一样
option go_package = "goclass/pb_1/person;person";//包路径(从mod里的module下开始写);别名
import "home/home.proto";
message Person{
//声明格式:类型 key(下划线) = 唯一(标识)
//类比到具体语言中的数据类型:
//https://protobuf.dev/programming-guides/proto3/#scalar
string name = 1;
int32 age = 2;
bool sex = 3;
//声明切片(数组) 关键字repeated
repeated string test = 4;
//声明map格式:map<key,value> key = 标识;
map<string, string> test_map = 5;
home.Home i_home = 6;
}
//类型嵌套
message Home{
repeated Person persons = 1;
message Visitor{
string name = 1;
}
Visitor visitor = 2;
}
- 在
build.bat
中加入命令:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./home/home.proto
- 可以看到
person.pb.go
中出现了IHome
type Person struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
//声明格式:类型 key(下划线) = 唯一(标识)
//类比到具体语言中的数据类型:
//https://protobuf.dev/programming-guides/proto3/#scalar
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"`
Sex bool `protobuf:"varint,3,opt,name=sex,proto3" json:"sex,omitempty"`
//声明切片(数组) 关键字repeated
Test []string `protobuf:"bytes,4,rep,name=test,proto3" json:"test,omitempty"`
//声明map格式:map<key,value> key = 标识;
TestMap map[string]string `protobuf:"bytes,5,rep,name=test_map,json=testMap,proto3" json:"test_map,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
IHome *home.Home `protobuf:"bytes,6,opt,name=i_home,json=iHome,proto3" json:"i_home,omitempty"`
}
定义服务
- 在
person.proto
文件中添加:
service SearchService{
rpc Search(Person) returns (Person); //传统的 即刻响应
rpc SearchIn(stream Person) returns (Person); //入参为流
rpc SearchOut(Person) returns (stream Person);//出参为流
rpc SearchIO(stream Person) returns (stream Person);//出入参均为流
}
- 在
pb_1
目录下,执行下面命令生成grpc
方法:
.\build.bat
- 注意
person_grpc.pb.go
的SearchServiceClient
和SearchServiceServer
接口中的四个方法:
type SearchServiceClient interface {
Search(ctx context.Context, in *Person, opts ...grpc.CallOption) (*Person, error)
SearchIn(ctx context.Context, opts ...grpc.CallOption) (SearchService_SearchInClient, error)
SearchOut(ctx context.Context, in *Person, opts ...grpc.CallOption) (SearchService_SearchOutClient, error)
SearchIO(ctx context.Context, opts ...grpc.CallOption) (SearchService_SearchIOClient, error)
}
GRPC server的四种传输模式
紧接上面的定义服务继续学习
普通服务
- 普通服务就是之前做的那种,客户端调用一次服务端就马上返回一个值
- 参数是一个入参,一个回参就可以了
代码示例
- 删除上面学习中的
pb_1
中的home
目录,修改person_grpc.pb.go
syntax = "proto3";//声明语法版本 告诉编译器用proto3来解读
package person;//给当前proto分配包名称 这个和go的包不一样
option go_package = "goclass/pb_1/person;person";//包路径(从mod里的module下开始写);别名
message PersonReq{
//声明格式:类型 key(下划线) = 唯一(标识)
string name = 1;
int32 age = 2;
}
message PersonRes{
//声明格式:类型 key(下划线) = 唯一(标识)
string name = 1;
int32 age = 2;
}
service PersonService{
rpc Search(PersonReq) returns (PersonRes); //传统的 即刻响应
rpc SearchIn(stream PersonReq) returns (PersonRes); //入参为流
rpc SearchOut(PersonReq) returns (stream PersonRes);//出参为流
rpc SearchIO(stream PersonReq) returns (stream PersonRes);//出入参均为流
}
- 修改
build.bat
:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./person/person.proto
- 在
pb_1
目录下执行.\build.bat
- 编写服务端代码
server.go
package main
import (
"context"
"goclass/pb_1/person"
"google.golang.org/grpc"
"net"
)
//1.取出server
type personServer struct {
person.UnimplementedPersonServiceServer
}
//2.挂载方法
func (*personServer) Search(ctx context.Context, req *person.PersonReq) (*person.PersonRes, error) {
name := req.GetName()
res := &person.PersonRes{Name: "我收到了" + name + "的信息"}
return res, nil
}
func (*personServer) SearchIn(server person.PersonService_SearchInServer) error {
return nil
}
func (*personServer) SearchOut(*person.PersonReq, person.PersonService_SearchOutServer) error {
return nil
}
func (*personServer) SearchIO(person.PersonService_SearchIOServer) error {
return nil
}
func main() {
//3.注册服务
//3.1.注册监听
l, _ := net.Listen("tcp", ":8888")
//3.2.注册一个server
s := grpc.NewServer()
person.RegisterPersonServiceServer(s, &personServer{})
//4.创建监听
s.Serve(l)
}
- 编写客户端代码
client.go
package main
import (
"context"
"fmt"
"goclass/pb_1/person"
"google.golang.org/grpc"
)
func main() {
//建立一个连接到服务器
conn, _ := grpc.Dial("localhost:8888", grpc.WithInsecure())
//最后要关闭连接
defer conn.Close()
//new一个客户端
client := person.NewPersonServiceClient(conn)
//调用client的方法
//普通服务
res, err := client.Search(context.Background(), &person.PersonReq{Name: "I'm wyatt"})
if err != nil {
//打印返回信息
fmt.Println(err)
}
fmt.Println(res)
}
- 启动服务端,客户端发起请求,得到响应:
name:"我收到了I'm wyatt的信息"
流式传入(客户端流)
- 客户端往服务器发消息时是一个流,服务器会一直接收,最后返回接收成功与否
- 参数是一个流入,一个出参
代码示例
server.go
:
package main
import (
"context"
"fmt"
"goclass/pb_1/person"
"google.golang.org/grpc"
"net"
)
//1.取出server
type personServer struct {
person.UnimplementedPersonServiceServer
}
//2.挂载方法
func (*personServer) Search(ctx context.Context, req *person.PersonReq) (*person.PersonRes, error) {
name := req.GetName()
res := &person.PersonRes{Name: "我收到了" + name + "的信息"}
return res, nil
}
func (*personServer) SearchIn(server person.PersonService_SearchInServer) error {
for {
//接收并打印请求信息
req, err := server.Recv()
fmt.Println(req)
if err != nil {
server.SendAndClose(&person.PersonRes{Name: "完成了"})
break
}
}
return nil
}
func (*personServer) SearchOut(*person.PersonReq, person.PersonService_SearchOutServer) error {
return nil
}
func (*personServer) SearchIO(person.PersonService_SearchIOServer) error {
return nil
}
func main() {
//3.注册服务
//3.1.注册监听
l, _ := net.Listen("tcp", ":8888")
//3.2.注册一个server
s := grpc.NewServer()
person.RegisterPersonServiceServer(s, &personServer{})
//4.创建监听
s.Serve(l)
}
client.go
:
package main
import (
"context"
"fmt"
"goclass/pb_1/person"
"google.golang.org/grpc"
"time"
)
func main() {
//建立一个连接到服务器
conn, _ := grpc.Dial("localhost:8888", grpc.WithInsecure())
//最后要关闭连接
defer conn.Close()
//new一个客户端
client := person.NewPersonServiceClient(conn)
//调用client的方法
c, _ := client.SearchIn(context.Background())
i := 0
for {
if i > 10 {
res, _ := c.CloseAndRecv()
fmt.Println(res)
break
}
time.Sleep(1 * time.Second)
c.Send(&person.PersonReq{Name: "我是客户端发来的信息"})
i++
}
}
启动服务端,客户端发起请求:服务端每隔1s接收到一次客户端发来的信息我是客户端发来的信息
,10s后接收到nil
,服务端向客户端返回响应信息完成了
流式返回(服务端流)
- 客户端告知服务端要取一个流,服务端返回一个流
- 参数是一个入参,一个流出
server.go
:
package main
import (
"context"
"fmt"
"goclass/pb_1/person"
"google.golang.org/grpc"
"net"
"time"
)
//1.取出server
type personServer struct {
person.UnimplementedPersonServiceServer
}
//2.挂载方法
func (*personServer) Search(ctx context.Context, req *person.PersonReq) (*person.PersonRes, error) {
name := req.GetName()
res := &person.PersonRes{Name: "我收到了" + name + "的信息"}
return res, nil
}
func (*personServer) SearchIn(server person.PersonService_SearchInServer) error {
for {
//接收并打印请求信息
req, err := server.Recv()
fmt.Println(req)
if err != nil {
server.SendAndClose(&person.PersonRes{Name: "完成了"})
break
}
}
return nil
}
func (*personServer) SearchOut(req *person.PersonReq, server person.PersonService_SearchOutServer) error {
name := req.Name
i := 0
for {
if i > 10 {
break
}
time.Sleep(1 * time.Second)
i++
server.Send(&person.PersonRes{Name: "我拿到了" + name})
}
return nil
}
func (*personServer) SearchIO(person.PersonService_SearchIOServer) error {
return nil
}
func main() {
//3.注册服务
//3.1.注册监听
l, _ := net.Listen("tcp", ":8888")
//3.2.注册一个server
s := grpc.NewServer()
person.RegisterPersonServiceServer(s, &personServer{})
//4.创建监听
s.Serve(l)
}
client.go
:
package main
import (
"context"
"fmt"
"goclass/pb_1/person"
"google.golang.org/grpc"
)
func main() {
//建立一个连接到服务器
conn, _ := grpc.Dial("localhost:8888", grpc.WithInsecure())
//最后要关闭连接
defer conn.Close()
//new一个客户端
client := person.NewPersonServiceClient(conn)
//调用client的方法
c, _ := client.SearchOut(context.Background(), &person.PersonReq{Name: "wyatt"})
for {
req, err := c.Recv()
if err != nil {
fmt.Println(err)
break
}
fmt.Println(req)
}
}
启动服务端,客户端发起请求:客户端每隔1s接收到一次客户端的响应信息我是客户端发来的信息
,10s后接收到nil
,服务端向客户端返回响应信息完成了
流式出入(双向流)
- 双向流有两种形式:
- 一种是客户端不断的给服务端传,服务端也不断的给客户端回;
- 另一种是把channel接入,是即刻的,类似聊天室,客户端发一句,服务端回一句
代码示例
server.go
:
package main
import (
"context"
"fmt"
"goclass/pb_1/person"
"google.golang.org/grpc"
"net"
"time"
)
//1.取出server
type personServer struct {
person.UnimplementedPersonServiceServer
}
//2.挂载方法
func (*personServer) Search(ctx context.Context, req *person.PersonReq) (*person.PersonRes, error) {
name := req.GetName()
res := &person.PersonRes{Name: "我收到了" + name + "的信息"}
return res, nil
}
func (*personServer) SearchIn(server person.PersonService_SearchInServer) error {
for {
//接收并打印请求信息
req, err := server.Recv()
fmt.Println(req)
if err != nil {
server.SendAndClose(&person.PersonRes{Name: "完成了"})
break
}
}
return nil
}
func (*personServer) SearchOut(req *person.PersonReq, server person.PersonService_SearchOutServer) error {
name := req.Name
i := 0
for {
if i > 10 {
break
}
time.Sleep(1 * time.Second)
i++
server.Send(&person.PersonRes{Name: "我拿到了" + name})
}
return nil
}
func (*personServer) SearchIO(server person.PersonService_SearchIOServer) error {
i := 0
str := make(chan string)
//流式接收
go func() {
for {
i++
req, _ := server.Recv()
if i > 10 {
str <- "结束"
break
}
//保存到channel
str <- req.Name
}
}()
//流式发送
for {
//从channel中取出
s := <-str
if s == "结束" {
server.Send(&person.PersonRes{Name: s})
break
}
server.Send(&person.PersonRes{Name: s})
}
return nil
}
func main() {
//3.注册服务
//3.1.注册监听
l, _ := net.Listen("tcp", ":8888")
//3.2.注册一个server
s := grpc.NewServer()
person.RegisterPersonServiceServer(s, &personServer{})
//4.创建监听
s.Serve(l)
}
client.go
:
package main
import "C"
import (
"context"
"fmt"
"goclass/pb_1/person"
"google.golang.org/grpc"
"sync"
"time"
)
func main() {
//建立一个连接到服务器
conn, _ := grpc.Dial("localhost:8888", grpc.WithInsecure())
//最后要关闭连接
defer conn.Close()
//new一个客户端
client := person.NewPersonServiceClient(conn)
//调用client的方法
c, _ := client.SearchIO(context.Background())
//创建两个WaitGroup等待执行
wg := sync.WaitGroup{}
wg.Add(2)
//流式发送
go func() {
for {
time.Sleep(1 * time.Second)
err := c.Send(&person.PersonReq{Name: "wyatt"})
if err != nil {
wg.Done()
break
}
}
}()
//流式接收
go func() {
for {
req, err := c.Recv()
if err != nil {
fmt.Println(err)
wg.Done()
break
}
fmt.Println(req)
}
}()
wg.Wait()
}
启动服务端,客户端发起请求:客户端每隔1s发送一次信息Name: "wyatt"
,服务端接收到后返回该信息给客户端,客户端打印,10s后,服务端发送结束
信息给客户端,客户端接收到后打印,双向流结束
GRPCGateway的简单应用
- GRPCGateway:给grpc生成的文件附加一个http1.1的restful供外界访问
- gRPC-Gateway Documentation Website
使用步骤:
- prerequisites:由于之前装过
protoc-gen-go-grpc
和protoc-gen-go
,所以只需要装protoc-gen-go-grpc
就行:
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
-
Adding gRPC-Gateway annotations to an existing proto file:,先在
src
下新建pb_2
目录,然后安装例程中import
后的路径创建"google/api/annotations.proto"
和google/api/http.proto
,最后添加依赖包,将https://github.com/googleapis/googleapis/blob/master/google/api/annotations.proto
和https://github.com/googleapis/googleapis/blob/master/google/api/http.proto
中代码复制到自己创建的对应文件中: -
在
pb_2
目录下新建.\build.bat
文件,添加gateway
的生成语句:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out . --go-grpc_opt paths=source_relative --grpc-gateway_out . --grpc-gateway_opt paths=source_relative ./person/person.proto
/pb_2/person/person.proto
:
syntax = "proto3";//声明语法版本 告诉编译器用proto3来解读
package person;//给当前proto分配包名称 这个和go的包不一样
option go_package = "goclass/pb_2/person;person";//包路径(从mod里的module下开始写);别名
import "google/api/annotations.proto";
message PersonReq{
//声明格式:类型 key(下划线) = 唯一(标识)
string name = 1;
int32 age = 2;
}
message PersonRes{
//声明格式:类型 key(下划线) = 唯一(标识)
string name = 1;
int32 age = 2;
}
service PersonService{
//传统的 即刻响应
rpc Search(PersonReq) returns (PersonRes){
//加入对restful的描述
option(google.api.http)={
post:"/api/person",
body:"*"
};
};
}
server.go
:
package main
import (
"context"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"goclass/pb_2/person"
"google.golang.org/grpc"
"net"
"net/http"
"sync"
)
//1.取出server
type personServer struct {
person.UnimplementedPersonServiceServer
}
//2.挂载方法
func (*personServer) Search(ctx context.Context, req *person.PersonReq) (*person.PersonRes, error) {
name := req.GetName()
res := &person.PersonRes{Name: "我收到了" + name + "的信息,来自grpcGateWay"}
return res, nil
}
func main() {
wg := sync.WaitGroup{}
wg.Add(2)
go registerGateWay(&wg)
go registerGRPC(&wg)
wg.Wait()
}
func registerGateWay(wg *sync.WaitGroup) {
//创建一个客户端
conn, _ := grpc.DialContext(
context.Background(),
"127.0.0.1:8888",
grpc.WithBlock(),
grpc.WithInsecure(),
)
//创建一个Mux
mux := runtime.NewServeMux() //一个对外开放的mux
//创建http服务
gwServer := &http.Server{
Handler: mux,
Addr: ":8090",
}
//注册网关handler
_ = person.RegisterPersonServiceHandler(context.Background(), mux, conn)
//监听网关
gwServer.ListenAndServe()
wg.Done()
}
func registerGRPC(wg *sync.WaitGroup) {
//3.注册服务
//3.1.注册监听
l, _ := net.Listen("tcp", ":8888")
//3.2.注册一个server
s := grpc.NewServer()
person.RegisterPersonServiceServer(s, &personServer{})
//4.创建监听
s.Serve(l)
wg.Done()
}
- 在
pb_2
目录下执行.\build.bat
后,可以看到在person
目录下生成person.pb.gw.go
- API访问结果: