推荐一个适合入门的学习地址: lixd/grpc-go-example: grpc go example 以及系列教程 (github.com)
下载
- protocol编译器
- 官方github:Releases · protocolbuffers/protobuf (github.com)
- 下载后解压到任意位置,要将bin目录配置到环境变量中
- 配置好后在控制台键入
protoc确定已经配置成功
- grpc核心库
go get google.golang.org/grpc
- 代码生成工具
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
开始
proto文件语法
- 说明使用的是
proto3语法
syntax = "proto3";
- 说明生成的go文件所在目录以及其包名,两个梣属之间用分号
;隔开
option go_package = ".;service";
- 定义一个服务
服务中的方法可以接受客户端的参数,然后再返回服务端的响应。
service Hello {
rpc SayHello(Request) returns (Response) {}
}
- message关键字,可以理解为go中的结构体
在变量等号后面的数字是指这个变量在这个message中的位置。
message Request {
string name = 1;
int64 age = 2;
}
message Response {
string Msg = 1;
}
- optional关键字:默认都是optional,在定义中是什么类型就生成什么类型
- repeated关键字:可重复字段,在go中会生成为切片
- 嵌套消息:可以在消息中再次嵌套一个消息体,像防御嵌套结构体
生成代码
在编写完proto文件后进行代码生成,进入到proto文件所在的目录后,执行以下命令:
protoc --go_out=. proto文件名
protoc --go-grpc_out=. proto文件名
# -I 参数表示import的proto文件要到哪里寻找
protoc -I ./proto --go_opt=paths=source_relative --go_out=../backend/pb --go-grpc_opt=paths=source_relative --go-grpc_out=../backend/pb proto/*.proto
上述两条命令都接受两个参数,第一个是生成的位置,第二个是指定的proto文件
实现方法
服务端代码实现
- 自行编写一个结构体,里面继承了
proto生成的代码文件中的Unimplemented+消息,然后在生成的_grpc.pb后缀的文件中将所有在proto文件中定义的方法粘贴下来,修改相应的错误,在自己的这个结构体中实现所有的方法。
定义自己的服务
type myServer struct {
service.UnimplementedHelloServer
}
// 实现方法
func (myServer) SayHello(ctx context.Context, req *service.Request) (*service.Response, error) {
msg := fmt.Sprintf("Hello!%v, now I know you are %v! ", req.Name, req.Age)
return &service.Response{Msg: msg}, nil
}
- 在服务端的
main函数中开启服务端监听:
listener, _ := net.Listen("tcp", ":9090")
- 创建grpc服务端
grpcServer := grpc.NewServer()
- 在grpc服务端中注册自己编写的服务:
service.RegisterHelloServer(grpcServer, &myServer{})
- 启动服务:
err := grpcServer.Serve(listener)
客户端代码实现
- 与服务端地址建立连接
conn, err := grpc.Dial("127.0.0.1:9090", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connevt: %v", err)
}
defer conn.Close()
- 建立grpc客户端
client := service.NewHelloClient(conn)
- 执行rpc调用
resp, _ := client.SayHello(context.Background(), &service.Request{Name: "LiP", Age: 20})
//对于proto中消息体里面字段的获取,可以使用Get+字段名()获取,grpc框架已经帮我们生成了。(但是为啥不直接 .+字段名获取?)
grpc stream
grpc流式传输适合用于需要服务端和客户端长时间数据交互的场景。
- proto文件
与普通的一致,不过在service中rpc函数的入口参数和出口参数需要带上stream关键字,哪个需要流哪个就带上
syntax = "proto3";
option go_package = ".;streamService";
service HelloWorld {
rpc ServerStreaming (Request) returns (stream Response) {}
rpc ClientStreaming (stream Request) returns (Response) {}
rpc Streaming (stream Request) returns (stream Response) {}
}
message Request {
string name = 1;
int32 age = 2;
}
message Response {
string msgHello = 1;
string msgWelcome = 2;
}
- 流类型
分别有三种类型:客户端流,服务端流,双向流。下面依次列出相应代码。
客户端流
- 服务端代码:
func (myServer) ClientStreaming(stream pb.HelloWorld_ClientStreamingServer) error {
var msg string
for {
req, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return err
}
msg = fmt.Sprintf("Now I know you are %v, aged %v.", req.Name, req.Age)
}
if err0 := stream.SendAndClose(&pb.Response{MsgHello: msg}); err0 != nil {
log.Fatalf("err0:%v", err0)
return err0
}
return nil
}
需要用一个循环来一直接收客户端的Request直到io.EOF,在接收到所有信息之后调用SendAndClose来返回Response并关闭客户端输入流
- 客户端代码:
func ClientStreaming(client pb.HelloWorldClient) {
stream, err := client.ClientStreaming(context.Background())
if err != nil {
log.Fatalf("err0:", err)
}
for i := 0; i < 10; i++ {
s := strconv.Itoa(i)
err := stream.Send(&pb.Request{Name: "LiP" + s, Age: 20})
if err != nil {
log.Fatalf("err1:", err)
}
}
resp, err := stream.CloseAndRecv()
if err != nil {
log.Fatalf("err2:", err)
}
fmt.Println(resp.MsgWelcome, resp.MsgHello)
}
在使用循环将信息输入完毕后需要调用CloseAndRecv函数来关闭输入流并接受响应。
服务端流
- 服务端代码:
func (myServer) ServerStreaming(req *pb.Request, stream pb.HelloWorld_ServerStreamingServer) error {
for i := 0; i < 10; i++ {
if err := stream.Send(&pb.Response{MsgHello: "Hello!" + req.Name, MsgWelcome: "Welcome to my world"}); err != nil {
log.Fatalf("erroe:%v", err)
return err
}
}
return nil
}
直接用Send函数发送响应即可。
- 客户端代码:
func ServerStreaming(c pb.HelloWorldClient) {
stream, err := c.ServerStreaming(context.Background(), &pb.Request{Name: "LiP", Age: 20})
if err != nil {
log.Fatalf("error: %v", err)
}
for {
resq, err := stream.Recv()
if err == io.EOF {
fmt.Println("Got all the messages.")
break
}
if err != nil {
fmt.Println("err:", err)
continue
}
fmt.Println(resq.MsgHello, resq.MsgWelcome)
}
}
用循环接收所有信息即可。
双向流
- 服务端代码:
func (myServer) Streaming(stream pb.HelloWorld_StreamingServer) error {
var wg sync.WaitGroup
msgChan := make(chan string, 20)
wg.Add(2)
go func() {
defer wg.Done()
for v := range msgChan {
stream.Send(&pb.Response{MsgHello: "Hello" + v + "!"})
}
}()
go func() {
defer wg.Done()
for {
req, err := stream.Recv()
if err == io.EOF {
break
}
msgChan <- req.Name
}
close(msgChan)
}()
wg.Wait()
return nil
}
启动双协程,一个用于接受一个用于发送,同时利用管道在两个协程之间通信。要记得在接收到io.EOF后需要将管道关闭,否则在读取协程中的管道遍历不会停止。
- 客户端代码:
func Streaming(client pb.HelloWorldClient) {
var wg sync.WaitGroup
stream, _ := client.Streaming(context.Background())
wg.Add(2)
go func() {
defer wg.Done()
for {
req, err := stream.Recv()
if err == io.EOF {
fmt.Println("Client Got all the messages.")
break
}
if err != nil {
fmt.Println("err:", err)
continue
}
fmt.Println(req.MsgHello, req.MsgWelcome)
}
}()
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
s := strconv.Itoa(i)
err := stream.Send(&pb.Request{Name: "LiP" + s, Age: 20})
if err != nil {
fmt.Println("send err:", err)
continue
}
time.Sleep(time.Second)
}
stream.CloseSend()
}()
wg.Wait()
}
与服务端相似,但是最后需要用CloseSend关闭输入流。
总结
在流中,只有客户端的流需要调用函数CloseAndRecv或CloseSend手动将其关闭,服务端的不需要。
grpc的认证
grpc的认证指的是多个server和多个client之间,如何识别对方是谁,并且可以安全的进行数据传输
- SSL/TLS认证方式(采用http2协议)
- 基于Token的认证方式(基于安全连接)
- 不采用任何措施连接,不安全(默认采用http1) -自定义的身份认证
SSL/TLS认证方式
TLS(Transport Layer Security,安全传输层),TLS是建立在传输层TCP协议之上的协议,服务于应用层,前身是SSL(Secure Socket Layer),它实现了将用一次的报文进行加密后再交由TCP进行传输的功能。 TLS协议主要解决如下三个网络安全问题:
- 保密,通过加密encryption实现,所有信息都加密传输。
- 完整性,通过MAX校验机制,一旦被篡改,通信双方会立刻发现
- 认证,双方认证,双方都可以配备证书,防止身份被冒充
- KEY:服务器上的私钥文件,用于发送给客户端数据的加密,以及对客户端收到的数据的解密
- CSR:证书签名请求文件,用于提交给证书颁发机构(CA)对证书签名
- CRT:有证书颁发机构(CA)签名后的证书,或者是开发者自签名的证书,包含证书持有人的信息,持有人的公钥,以及签署者的签名等信息
- PEM:是基于Base64编码的证书格式,扩展名包括PEM、CRT和CER
使用openssl生成证书
首先通过openssl生成证书和私钥
前提
- 下载:slproweb.com/products/Wi…
- 配置环境变量
- 命令行测试
openssl
生成证书
在go项目中新建一个目录专门用于存放证书文件等,所有命令都在这个目录下执行
- 生成私钥
openssl genrsa -out server.key 2048
其中server.key为输出的文件名
- 生成证书
openssl req -new -x509 -key server.key -out server.crt -days 36500
其中server.key为上面生成的私钥key文件,server.crt为生成的证书文件,-days 36500为证书的有效期。
输入指令后会有以下信息需要填写,可以全部回车,表示留空
# 国家名字
Country Name (2 letter code) [AU]:CN
# 省名
State or Province Name (full name) [Some-State]:HuBei
# 城市名
Locality Name (eg, city) []:Wuhan
# 组织名
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Huake
# 部门名
Organizational Unit Name (eg, section) []:Dian
# 服务器/网站名
Common Name (e.g. server FQDN or YOUR name) []:GRPCLearn
# 邮箱
Email Address []:3192997488@qq.com
- 生成csr
openssl req -new -key server.key -out server.csr
- 修改配置文件
- 将
openssl/bin目录下的openssl.cnf或openssl.cfg文件复制到当前目录下 - 打开
copy_extensions = copy - 打开
req_extensions = v3_req - 在
[v3_req]中添加`subjectAltName = @alt_names - 在文件的最后写下
[ alt_names ]
DNS.1 = *.dian.com
最后一个操作写的是域名,通过哪个域名可以访问到代码 5. 生成证书私钥test.key
openssl genpkey -algorithm RSA -out test.key
- 通过私钥test.key生成证书请求文件test.csr
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
- 生成SAN证书pem
openssl x509 -req -days 365 -in test.csr -out test.pem -CA server.crt -CAkey server.key -CAcreateserial -extfile ./openssl.cfg -extensions v3_req
-
梳理 最终使用到的只有
test.key和test.pem,其他文件都是签发证书需要 -
在服务端加入证书
creds, errC := credentials.NewServerTLSFromFile("../key/test.pem", "../key/test.key")
if errC != nil {
log.Fatalf("did not connevt: %v", errC)
}
然后在创建grpc服务端时加上证书信息
grpcServer := grpc.NewServer(grpc.Creds(creds))
- 在客户端加上证书
creds, _ := credentials.NewClientTLSFromFile("E:\GoProject\gRPCStudy\key\test.pem", "*.dian.com")
在连接时带上证书信息
conn, err := grpc.Dial("127.0.0.1:9090", grpc.WithTransportCredentials(creds))
自定义token验证
- 在客户端创建一个结构体,实现两个方法:
// map中要包含有需要验证的k-v对,如id,token等等
func GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
// 如果需要证书验证则返回true,否则返回false
func RequireTransportSecurity() bool
- 在服务端进行token的验证
// 此处获取到的token就是上面自己写的写的map
token, ok := metadata.FromIncomingContext(ctx)
if !ok {
....
}
....