文章目录
- 一:简介
- 1. 什么是grpc
- 2. 为什么我们要用grpc
- 二:grpc的hello world
- 1、 定义hello.proto文件
- 2、生成xxx_grpc.pb.go文件
- 3、生成xxx.pb.go结构体文件
- 4、编写服务代码service.go
- 5、编写客户端代码client.go
- 三、服务端流式传输:文件下载
- 文件下载
- 四、客户端流式传输:文件上传
- 文件上传
- 五、双向流:聊天
代码地址:https://gitee.com/lymgoforIT/golang-trick/tree/master/32-grpc
一:简介
1. 什么是grpc
gRpc
是一个高性能、开源和通用的 RPC
框架,面向移动和 HTTP2
设计。目前提供 C、Java 和 Go
语言版本,分别是:grpc
, grpc-java
, grpc-go
. 其中 C
版本支持C, C++, Node.js, Python, Ruby, Objective-C, PHP
和 C#
支持.
gRPC
基于 HTTP2
标准设计,带来诸如双向流、流控、头部压缩、单 TCP
连接上的多复用请求等特。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。
参考: grpc官方文档中文版
2. 为什么我们要用grpc
- 生态好:背靠
Google
。还有比如nginx
也对grpc
提供了支持 - 跨语言:跨语言,且自动生成
sdk
- 性能高:比如
protobuf
性能高过json
, 比如http2.0
性能高过http1.1
- 强类型:编译器就给你解决了很大一部分问题
- 流式处理(基于
http2.0
):支持客户端流式,服务端流式,双向流式
二:grpc的hello world
1、 定义hello.proto文件
syntax = "proto3"; // 指定proto版本,不指定时默认是proto2
package hello_grpc; // 指定默认包名
// 指定golang包名,当编译为pb.go时,这个包名会替换上面package指定的名字
option go_package = "/hello_grpc";
message HelloRequest {
string name = 1;
string message = 2;
}
message HelloResponse {
string name = 1;
string message = 2;
}
// 定义rpc服务
service HelloService {
// 定义函数
rpc SayHello(HelloRequest) returns (HelloResponse) {}
}
2、生成xxx_grpc.pb.go文件
protoc --proto_path=32-grpc/grpc_proto --go-grpc_out=32-grpc/grpc_proto 32-grpc/grpc_proto/hello.proto
解释:
protoc
为我们安装的protocol buffer
的命令--proto_path
选项为指定proto
文件所在的路径,可以换为-I
--go-grpc_out
指明要生成的是grpc
文件,且指明了生成后文件要放在哪个目录- 最后的参数为
proto
文件,可以写多个,也可以写.
,表示当前目录下所有proto
文件
执行后如下:报红的地方为找不到对应的结构体,所以我们还需要生成结构体的pb.go
文件
3、生成xxx.pb.go结构体文件
protoc --proto_path=32-grpc/grpc_proto --go_out=32-grpc/grpc_proto 32-grpc/grpc_proto/hello.proto
命令和生成gprc
文件几乎一致,只是--go-grpc_out
选项换成了--go_out
选项而已
注意:
2
和3
两个生成pb.go
的命令也可以写成一个命令,如下
protoc --proto_path=32-grpc/grpc_proto --go_out=32-grpc/grpc_proto --go-grpc_out=32-grpc/grpc_proto 32-grpc/grpc_proto/hello.proto
pb
描述和rpc
⽅法之前旧版⽣成是在⼀个⽂件中,⽬前新版本pb
和⽅法
已经分离⽣成的⽂件格式特征如下:
4、编写服务代码service.go
package main
import (
"context"
"fmt"
"golang-trick/32-grpc/grpc_proto/hello_grpc"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
)
type HelloService struct {
hello_grpc.UnimplementedHelloServiceServer
}
func (h HelloService) SayHello(ctx context.Context, request *hello_grpc.HelloRequest) (*hello_grpc.HelloResponse, error) {
fmt.Println(request)
return &hello_grpc.HelloResponse{
Name: "lym",
Message: "ok",
}, nil
}
func main() {
// 监听端口
listen, err := net.Listen("tcp", ":8080")
if err != nil {
grpclog.Fatalf("Failed to listen err:%v", err)
}
// 创建一个grpc服务器实例
s := grpc.NewServer()
server := HelloService{}
// 将server结构体注册为grpc服务
hello_grpc.RegisterHelloServiceServer(s, &server)
fmt.Println("grpc server running:9090")
// 开始处理客户端请求
err = s.Serve(listen)
}
解释:
HelloService
是我们自定义的结构体,需要实现hello_grpc
中的HelloServiceServer
接口,但是该接口包含一个私有的mustEmbedUnimplementedHelloServiceServer
方法,导致无法实现该接口,目前解决办法就是让我们的结构体包含hello_grpc.UnimplementedHelloServiceServer
,从而实现hello_grpc
中的HelloServiceServer
接口
// HelloServiceServer is the server API for HelloService service.
// All implementations must embed UnimplementedHelloServiceServer
// for forward compatibility
type HelloServiceServer interface {
// 定义函数
SayHello(context.Context, *HelloRequest) (*HelloResponse, error)
mustEmbedUnimplementedHelloServiceServer()
}
运行服务端
5、编写客户端代码client.go
package main
import (
"context"
"fmt"
"golang-trick/32-grpc/grpc_proto/hello_grpc"
"log"
"google.golang.org/grpc"
)
func main() {
addr := ":8080"
//使用grpc.Dial 创建一个到指定的地址的 grpc 连接
conn, err := grpc.Dial(addr, grpc.WithInsecure())
if err != nil {
log.Fatalf(fmt.Sprintf("grpc connect adddr[%s] failed,err:%v", addr, err))
}
defer conn.Close()
// 初始化客户端
client := hello_grpc.NewHelloServiceClient(conn)
// 调用远程服务端方法
resp, err := client.SayHello(context.Background(), &hello_grpc.HelloRequest{
Name: "lym",
Message: "我是客户端",
})
fmt.Println(resp, err)
}
运行客户端
成功收到服务端的响应
且服务端也打印出了客户端发来的请求
三、服务端流式传输:文件下载
grpc
共有四种传输方式:
- 普通交互:如上面
hello world
示例,客户端发送一次请求,服务端响应一次。 - 服务端流式:客户端发送一次请求,服务端响应多次
- 客户端流式:客户端发送多次请求,服务端响应一次
- 双向流式:客户端和服务端有问有答一样
而上面四次方式proto
文件写法上区别就在于stream
关键字的有无以及所在位置
如普通式:
// 定义rpc服务
service Service {
// 定义函数
rpc SayHello(Request) returns (Response) {}
}
服务端流式:
// 定义rpc服务
service ServiceStream {
// 定义函数
rpc SayHello(Request) returns (stream Response) {}
}
文件下载
因为要下载的文件可能很大,服务端不能一次就把整个文件响应回去,因此需要用到服务端流式,多次发送
,目录结构以及文件大致如下:
1、首先编写stream.proto
文件,注意响应多了stream
关键字
syntax = "proto3"; // 指定proto版本,不指定时默认是proto2
package stream; // 指定默认包名
// 指定golang包名,当编译为pb.go时,这个包名会替换上面package指定的名字
option go_package = "/stream";
message Request {
string name = 1;
}
message FileResponse {
string file_name = 1;
bytes content = 2;
}
service ServiceStream {
rpc DownLoadFile(Request) returns (stream FileResponse){};
}
2、生成pb.go
文件,这个就不赘述了
protoc --proto_path=32-grpc/grpc_proto --go_out=32-grpc/grpc_proto --go-grpc_out=32-grpc/grpc_proto 32-grpc/grpc_proto/stream.proto
3、服务端代码stream_service.go
。看代码注释即可, 不难
package main
import (
"fmt"
"golang-trick/32-grpc/grpc_proto/stream"
"io"
"net"
"os"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
)
type ServiceStream struct {
stream.UnimplementedServiceStreamServer
}
func (s ServiceStream) DownLoadFile(request *stream.Request, resp stream.ServiceStream_DownLoadFileServer) error {
fmt.Println(request)
// 获取要下载的文件
file, err := os.Open("32-grpc/static/prometheus+granfana企业级监控实战v5.pdf")
if err != nil {
return err
}
defer file.Close()
for {
buf := make([]byte, 1024)
_, err := file.Read(buf)
if err == io.EOF {
break
}
if err != nil {
break
}
// 可以通过接口上的resp对象的Send方法不断给客户端响应
resp.Send(&stream.FileResponse{
Content: buf,
})
}
// return 后表明本次响应结束,不会再Send
return nil
}
func main() {
// 监听端口
listen, err := net.Listen("tcp", ":8080")
if err != nil {
grpclog.Fatalf("Failed to listen err:%v", err)
}
// 创建一个grpc服务器实例
s := grpc.NewServer()
server := ServiceStream{}
// 将server结构体注册为grpc服务
stream.RegisterServiceStreamServer(s, &server)
fmt.Println("grpc server running:8080")
// 开始处理客户端请求
err = s.Serve(listen)
}
4、客户端代码client.go
,请求下载文件
package main
import (
"bufio"
"context"
"fmt"
"golang-trick/32-grpc/grpc_proto/stream"
"io"
"log"
"os"
"google.golang.org/grpc"
)
func main() {
addr := ":8080"
//使用grpc.Dial 创建一个到指定的地址的 grpc 连接
conn, err := grpc.Dial(addr, grpc.WithInsecure())
if err != nil {
log.Fatalf(fmt.Sprintf("grpc connect adddr[%s] failed,err:%v", addr, err))
}
defer conn.Close()
// 初始化客户端
client := stream.NewServiceStreamClient(conn)
resp, err := client.DownLoadFile(context.Background(), &stream.Request{Name: "下载文件"})
if err != nil {
log.Fatalln(err)
}
file, err := os.OpenFile("32-grpc/static/下载的pdf文件.pdf", os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
log.Fatalln(err)
}
defer file.Close()
writer := bufio.NewWriter(file)
for {
// 客户端从服务端得到的响应是一个stream,所以可以通过Recv不断接收,直到遇到io.EOF表明服务端响应完毕了
recv, err := resp.Recv()
if err == io.EOF {
break
}
fmt.Println(fmt.Sprintf("写入数据 %d 字节", len(recv.Content)))
// 将每轮接收到的内容写入到文件中
writer.Write(recv.Content)
}
writer.Flush()
}
测试:运行服务端和客户端后,如下,接收到了文件
四、客户端流式传输:文件上传
文件上传
实际上,客户端流式和服务端流式思路基本是完全一致的,就是在request
前加stream
关键字即可,然后文件上传和文件下载也是类似的,只是发送方变为了客户端,然后服务端不断的接收,知道收到io.EOF
时,响应给客户端接收成功的消息。文件结构大致如下:
1、proto
文件,注意包名修改,以及request
前加了stream
关键字
syntax = "proto3"; // 指定proto版本,不指定时默认是proto2
package client_stream; // 指定默认包名
// 指定golang包名,当编译为pb.go时,这个包名会替换上面package指定的名字
option go_package = "/client_stream";
message FileRequest {
string file_name = 1;
bytes content = 2; // 对应go的[]byte类型
}
message Response {
string text = 1;
}
service ClientStream {
rpc UploadFile(stream FileRequest) returns (Response){};
}
2、生成pb.go
文件
protoc --proto_path=32-grpc/grpc_proto --go_out=32-grpc/grpc_proto --go-grpc_out=32-grpc/grpc_proto 32-grpc/grpc_proto/client_stream.proto
3、服务端代码file_upload_service.go
注意方法上响应只有error
,response
没有写在方法上,而是通过req.SendAndClose
返回的响应结果
package main
import (
"bufio"
"fmt"
"golang-trick/32-grpc/grpc_proto/client_stream"
"io"
"log"
"net"
"os"
"google.golang.org/grpc"
"google.golang.org/grpc/grpclog"
)
type FileUploadService struct {
client_stream.UnimplementedClientStreamServer
}
func (f FileUploadService) UploadFile(req client_stream.ClientStream_UploadFileServer) error {
// 这里文件名我们写死了,实际应该用客户端传过来的
file, err := os.OpenFile("32-grpc/static/上传的png文件.png", os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
log.Fatalln(err)
}
defer file.Close()
writer := bufio.NewWriter(file)
for {
recv, err := req.Recv()
if err == io.EOF {
break
}
fmt.Println(fmt.Sprintf("写入数据 %d 字节", len(recv.Content)))
writer.Write(recv.Content)
}
writer.Flush()
// 注意方法上响应只有error,response是从这里返回的,而没有写在方法上
req.SendAndClose(&client_stream.Response{Text: "服务端接收完成啦!"})
return nil
}
func main() {
// 监听端口
listen, err := net.Listen("tcp", ":8080")
if err != nil {
grpclog.Fatalf("Failed to listen err:%v", err)
}
// 创建一个grpc服务器实例
s := grpc.NewServer()
server := FileUploadService{}
// 将server结构体注册为grpc服务
client_stream.RegisterClientStreamServer(s, &server)
fmt.Println("grpc server running:8080")
// 开始处理客户端请求
err = s.Serve(listen)
}
4、客户端代码file_upload_client.go
注意:全部上传完成后,才告知服务端发送结束了,并通过resp, err := stream.CloseAndRecv()
接收服务端的响应
package main
import (
"context"
"fmt"
"golang-trick/32-grpc/grpc_proto/client_stream"
"io"
"log"
"os"
"google.golang.org/grpc"
)
func main() {
addr := ":8080"
//使用grpc.Dial 创建一个到指定的地址的 grpc 连接
conn, err := grpc.Dial(addr, grpc.WithInsecure())
if err != nil {
log.Fatalf(fmt.Sprintf("grpc connect adddr[%s] failed,err:%v", addr, err))
}
defer conn.Close()
// 初始化客户端
client := client_stream.NewClientStreamClient(conn)
stream, err := client.UploadFile(context.Background())
if err != nil {
log.Fatalln(err)
}
file, err := os.Open("32-grpc/static/21.png")
if err != nil {
log.Fatalln(err)
}
defer file.Close()
for {
buf := make([]byte, 1024)
_, err := file.Read(buf)
if err == io.EOF {
break
}
if err != nil {
break
}
stream.Send(&client_stream.FileRequest{
Content: buf,
})
}
// 全部上传完成后,在这里告知服务端发送结束了,并接收服务端的响应
resp, err := stream.CloseAndRecv()
fmt.Println(resp, err)
}
启动服务端和客户端,可以看到上传成功
五、双向流:聊天
双向流能想到的最简单的场景就是聊天,一来一回的,就是在proto
文件的接口上request
和response
前都加上stream
,具体如何使用就要用的时候再查下吧,哈哈哈