文章目錄
- 一:簡介
- 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 mainimport ("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 mainimport ("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 mainimport ("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 后表明本次響應結束,不會再Sendreturn 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 mainimport ("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 mainimport ("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 mainimport ("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
,具體如何使用就要用的時候再查下吧,哈哈哈