grpc
学习记录(上)(理论加实践)
1 grpc
的基本概念
(1)首先我们知道HTTP
是超文本传输协议,它是用于互联网上网络浏览器和网络服务器之间通信的基础协议。
(2)RPC
指的是远程过程调用(Remote Procedure Call
),它是一种通信协议,用于在不同的计算机或进程之间进行通信和调用远程方法。RPC
的工作原理是,一个应用程序通过发送请求(即调用远程方法)到另一个应用程序,然后等待接收返回结果。在这个过程中,应用程序不需要关心底层的网络细节,它只需要像调用本地方法一样调用远程方法。而grpc
就是由google
推出的rpc
框架。
(3)那既然有了http
协议,为什么还需要grpc
协议呢?下面来看看http
和grpc
的联系与区别。
-
传输协议:
HTTP
使用基于文本的传输协议,而gRPC
使用基于二进制的传输协议。HTTP
使用的是HTTP/1.x
或HTTP/2
协议,而gRPC
使用的是HTTP/2
协议作为底层传输协议。 -
数据序列化:
HTTP
通常使用JSON
或XML
等文本格式进行数据序列化,而gRPC
使用Protocol Buffers
作为默认的数据序列化机制,Protocol Buffers
是一种二进制编码格式,相比文本格式,它更加紧凑和高效。 -
方法调用:
HTTP
使用统一资源定位符(URL
)来标识资源和执行操作,通常使用不同的HTTP
方法(如GET
、POST
、PUT
、DELETE
)来表示不同的操作。而gRPC
使用定义在IDL
中的方法来进行远程过程调用。 -
可扩展性:
gRPC
支持双向流式数据传输,允许客户端和服务器同时发送和接收流式数据,这使得它更适合高并发和实时性要求较高的场景。而HTTP
通常是单向的请求-响应模式。所以
gRPC
相比HTTP
在性能、效率和灵活性方面有优势,尤其适用于构建大规模分布式系统和微服务架构。但对于简单的请求-响应场景,HTTP
更好。
2 grpc
-两数之和小demo
(1)本文主要使用的是go
语言进行grpc
的案例开发。在开发前首先要安装grpc
和Protocol Buffers v3
,这2个插件在网上有教程可自行进行安装配置。
(2)要写一个grpc
的服务,首先就需要定义一个.proto
文件。该文件主要就是用来生成基于grpc
协议的一些结构体与接口。
syntax = "proto3";
option go_package = "add1/pb";
package pb;
service Add{
rpc TwoNumAdd (AddRequest) returns (AddResponse);
}
message AddRequest{
int32 a = 1;
int32 b = 2;
}
message AddResponse{
int32 reply = 1;
}
(3)使用Protocol Buffers v3
将以上定义的文件生成go
源码文件。此处要注意服务端和客户端要使用同一份.proto
文件,如果不使用同一份文件,定义的服务和消息都不一样肯定无法进行通信。
protoc --go_out=. --go_opt=paths=source_relative //将go文件生成到当前路径下
--go-grpc_out=. --go-grpc_opt=paths=source_relative //将go-grpc文件生成到当前路径下
pb/add.proto //根据pb下面的hello.proto来进行生成
(4)编写server
端的代码。
type server struct {
pb.UnimplementedAddServer
}
func (s *server) TwoNumAdd(ctx context.Context, in *pb.AddRequest) (*pb.AddResponse, error) {
reply := in.GetA() + in.GetB()
return &pb.AddResponse{
Reply: reply,
},nil
}
func main() {
l, err := net.Listen("tcp", ":8081")
if err != nil{
fmt.Printf("failed to listen, err:%v\n",err)
return
}
s := grpc.NewServer()
pb.RegisterAddServer(s,&server{})
err = s.Serve(l)
if err != nil{
fmt.Printf("failed to server, err:%v\n",err)
return
}
}
(5)编写客户端代码。编写完成后不要忘了使用.proto
文件生成go
和grpc
代码。
func main() {
conn, err := grpc.Dial("127.0.0.1:8081", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil{
log.Fatalf("grpc dial failed err:%v\n",err)
return
}
defer conn.Close()
c := pb.NewAddClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
resp, err := c.TwoNumAdd(ctx, &pb.AddRequest{A: 2222, B: 2})
if err != nil{
log.Printf("c.SayHello failed,err:%v\n",err)
return
}
// 拿到rpc执行结果
log.Println(resp.GetReply())
}
(6)分别运行service
端和客户端,就能得到得到返回信息,至此就完成了一个grpc
通信的小demo
。
(7)编写一个grpc
服务就分为以下的3个步骤。
- 编写
.proto
文件,定义服务的方法,请求的消息和响应的消息。 - 编写客户端代码,执行客户端。
- 编写客户端代码,执行客户端。
3 流式grpc
1 三种流式grpc
在grpc
中一共有4种类型的服务方法,分别如下所示。
(1) 普通的rpc
服务,也就是客户端发一个请求给服务端,然后服务端给客户端回一个消息。
// 在上一个例子种使用的就是普通的grpc服务
rpc SayHello(HelloRequest) returns (HelloResponse);
(2)服务端流式rpc
,也就是服务端和客户端建立一个单向的流,然后客户端不断向流中写入数据,客户端不断从流中接收数据。最后客户端向流中写完数据时,进行对流的关闭。
使用场景:比如在使用Open AI
进行提问时,客户端只需要写入问题,而服务端则会根据问题回答出多种不同的解决 方法。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
(3)客户端流式RPC
,和服务端很相似,只不过是客户端不断的向流中写入数据,然后服务端从流中读取数据。最后服务端将读取的数据进行处理,再返回给客户端一个处理后的响应结果。
使用场景:比如说一个机器的故障诊断系统,传感器将机器的信息不断写入流中,然后服务端对传感器的信息进行监 控,一旦出现问题就给传感器发送停止信号。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
(4)双向流式RPC
即客户端和服务端均为流式的RPC
,能发送多个请求对象也能接收到多个响应对象。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
通过对以上3种流式的grpc
进行对比,其实也就是在定义服务时,需要考虑清楚到底是客户端往流里写数据,还是服务端向流里写数据,或者2端都要往流里写数据,从而考虑定义哪种流式的rpc
。
使用场景:聊天软件。
2 双向流实现聊天小demo
对于以上3中流式的`rpc`,本文对最后一个双向流的`rpc`写一个简单的聊天`demo`。
(1)先编写.proto
文件。
syntax = "proto3"; //指明所使用的版本号
option go_package = "grpcTest2.2/pb"; //从项目中导入go代码的路径
package pb; //proto模块
//定义服务
service Chat{
//定义方法
rpc ChatWithFried (stream ChatRequest)returns(stream ChatResponse){}
}
//定义消息
message ChatRequest{
string infoSer = 1; //字段序号
}
message ChatResponse{
string infoCli = 1; //字段序号
}
(2)编写服务端代码。
type service struct {
pb.UnimplementedChatServer
}
func (s * service)ChatWithFried(stream pb.Chat_ChatWithFriedServer) error {
waitc := make(chan struct{})
go func() {
for {
// 接收客户端发送的消息
in, err := stream.Recv()
if err == io.EOF {
// read done.
close(waitc)
return
}
if err != nil {
log.Fatalf("c.BidiHello stream.Recv() failed, err: %v", err)
}
fmt.Printf("用户:%s\n", in.GetInfoSer())
}
}()
// 从标准输入获取用户输入
reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
for {
cmd, _ := reader.ReadString('\n') // 读到换行
cmd = strings.TrimSpace(cmd)
if len(cmd) == 0 {
continue
}
if strings.ToUpper(cmd) == "QUIT" {
break
}
// 将回复的消息发送到客户端
if err := stream.Send(&pb.ChatResponse{InfoCli: cmd}); err != nil {
log.Fatalf("c.BidiHello stream.Send(%v) failed: %v", cmd, err)
}
}
<-waitc
return nil
}
func main() {
//启动服务
l, err := net.Listen("tcp", ":8972")
if err != nil{
fmt.Print("failed to listen ,err:%v\n",err)
return
}
s := grpc.NewServer() //创建grpc服务
pb.RegisterChatServer(s,&service{}) //注册服务
err1 := s.Serve(l) //启动服务
if err1 != nil{
fmt.Println("failed to serve,err:%v\n",err)
return
}
}
(3)编写客户端代码
func runBidiHello(c pb.ChatClient) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
// 双向流模式
stream, err := c.ChatWithFried(ctx)
if err != nil {
log.Fatalf("c.BidiHello failed, err: %v", err)
}
waitc := make(chan struct{})
go func() {
for {
// 接收服务端返回的响应
in, err := stream.Recv()
if err == io.EOF {
// read done.
close(waitc)
return
}
if err != nil {
log.Fatalf("c.BidiHello stream.Recv() failed, err: %v", err)
}
fmt.Printf("客服:%s\n", in.GetInfoCli())
}
}()
// 从标准输入获取用户输入
reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
for {
cmd, _ := reader.ReadString('\n') // 读到换行
cmd = strings.TrimSpace(cmd)
if len(cmd) == 0 {
continue
}
if strings.ToUpper(cmd) == "QUIT" {
break
}
// 将获取到的数据发送至服务端
if err := stream.Send(&pb.ChatRequest{InfoSer: cmd}); err != nil {
log.Fatalf("c.BidiHello stream.Send(%v) failed: %v", cmd, err)
}
}
stream.CloseSend()
<-waitc
}
func main() {
flag.Parse()
// 连接到server端,此处使用不安全的传输方式(自己写demo时可以这样设置)
conn, err := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewChatClient(conn)
runBidiHello(c)
}
(4)其运行结果如下图所示,就实现了一个简单的聊天小demo
。
4 grpc-GateWay
-反向代理
1 grpc-GateWay
的定义
(1)当我们在编写grpc
服务端代码后,我们我希望不仅grpc
的客户端可以对其进行访问,还希望http
也可以对其访问,此时就需要生成一套RESTful
风格的API
供外界访问。此时就需要使用grpc-GetWay
工具进行反向代理。
其访问的主要流程如下图所示。
2 grpc-GateWay
使用不同端口
(1)需要导入grpc-GateWay
的包。
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2
(2)需要从https://github.com/googleapis/googleapis下的google
下的api
内导入以下几个包到项目中。本例导入的目录结构如下图所示。
(3)编写.proto
文件。此处使用两数之和小demo
中的代码进行演示。
syntax = "proto3";
option go_package = "add1/pb";
package pb;
import "google/api/annotations.proto";
service Add{
rpc TwoNumAdd (AddRequest) returns (AddResponse){
option (google.api.http) = {
post: "/v1/add"
body: "*"
};
};
}
message AddRequest{
int32 a = 1;
int32 b = 2;
}
message AddResponse{
int32 reply = 1;
}
(4)此时修改了.proto
文件就需要重新生成go
,grpc
,以及grpc-GateWay
的代码。
protoc -I=pb --go_out=pb --go_opt=paths=source_relative //生成go代码
--go-grpc_out=pb --go-grpc_opt=paths=source_relative //生成grpc代码
--grpc-gateway_out =pb --grpc-gateway_opt=paths=source_relative //生成grpcGateWay代码
pb/add.proto
(5)编写客户端代码。其实就是使用grpc_GateWay
创建一个反向代理,使得http
也可以对其进行访问。分别在8081
端端口提供了grpc
服务,在8090
端口提供了http
服务。
type server struct {
pb.UnimplementedAddServer
}
func (s *server) TwoNumAdd(ctx context.Context, in *pb.AddRequest) (*pb.AddResponse, error) {
reply := in.GetA() + in.GetB()
return &pb.AddResponse{
Reply: reply,
},nil
}
func main() {
//先一个grpc服务
l, err := net.Listen("tcp", ":8081")
if err != nil{
fmt.Printf("failed to listen, err:%v\n",err)
return
}
s := grpc.NewServer()
pb.RegisterAddServer(s,&server{})
log.Println("Serving gRPC on 0.0.0.0:8081")
go func() {
log.Fatalln(s.Serve(l))
}()
// 创建一个连接到我们刚刚启动的 gRPC 服务器的客户端连接
// gRPC-Gateway 就是通过它来代理请求(将HTTP请求转为RPC请求)
conn, err := grpc.DialContext(
context.Background(),
"0.0.0.0:8081",
grpc.WithBlock(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalln("Failed to dial server:", err)
}
gwmux := runtime.NewServeMux()
// 注册Greeter
err = pb.RegisterAddHandler(context.Background(), gwmux, conn)
if err != nil {
log.Fatalln("Failed to register gateway:", err)
}
gwServer := &http.Server{
Addr: ":8090",
Handler: gwmux,
}
// 8090端口提供gRPC-Gateway服务
log.Println("Serving gRPC-Gateway on http://0.0.0.0:8090")
log.Fatalln(gwServer.ListenAndServe())
}
(6)当完成了以上步骤后就可以使用postman
对其进行测试,测试结果如下。
3 grpc-GateWay
使用同一端口
(1)要使http
和grpc
使用同一端口,则只需要修改上文中service
端的代码即可。这样http API
和grpc API
就都使用了8081端口。
type server struct {
pb.UnimplementedAddServer
}
func (s *server) TwoNumAdd(ctx context.Context, in *pb.AddRequest) (*pb.AddResponse, error) {
reply := in.GetA() + in.GetB()
return &pb.AddResponse{
Reply: reply,
},nil
}
func main() {
l, err := net.Listen("tcp", ":8081")
if err != nil{
fmt.Printf("failed to listen, err:%v\n",err)
return
}
s := grpc.NewServer()
pb.RegisterAddServer(s,&server{})
// gRPC-Gateway mux
gwmux := runtime.NewServeMux()
dops := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
err = pb.RegisterAddHandlerFromEndpoint(context.Background(), gwmux, "127.0.0.1:8081", dops)
if err != nil {
log.Fatalln("Failed to register gwmux:", err)
}
mux := http.NewServeMux()
mux.Handle("/", gwmux)
// 定义HTTP server配置
gwServer := &http.Server{
Addr: "127.0.0.1:8081",
Handler: grpcHandlerFunc(s, mux), // 请求的统一入口
}
log.Println("Serving on http://127.0.0.1:8081")
log.Fatalln(gwServer.Serve(l)) // 启动HTTP服务
}
// grpcHandlerFunc 将gRPC请求和HTTP请求分别调用不同的handler处理
func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
grpcServer.ServeHTTP(w, r)
} else {
otherHandler.ServeHTTP(w, r)
}
}), &http2.Server{})
}
(2)继续使用postman
和客户端进行测试,会发现http API
和grpc API
都可以通过8081对服务进行访问了。