grpc学习记录(上)

grpc学习记录(上)(理论加实践)

1 grpc的基本概念

(1)首先我们知道HTTP 是超文本传输协议,它是用于互联网上网络浏览器和网络服务器之间通信的基础协议。

(2)RPC指的是远程过程调用(Remote Procedure Call),它是一种通信协议,用于在不同的计算机或进程之间进行通信和调用远程方法。RPC的工作原理是,一个应用程序通过发送请求(即调用远程方法)到另一个应用程序,然后等待接收返回结果。在这个过程中,应用程序不需要关心底层的网络细节,它只需要像调用本地方法一样调用远程方法。而grpc就是由google推出的rpc框架。

(3)那既然有了http协议,为什么还需要grpc协议呢?下面来看看httpgrpc的联系与区别。

  • 传输协议:HTTP使用基于文本的传输协议,而gRPC使用基于二进制的传输协议。HTTP使用的是HTTP/1.xHTTP/2协议,而gRPC使用的是HTTP/2协议作为底层传输协议。

  • 数据序列化:HTTP通常使用JSONXML等文本格式进行数据序列化,而gRPC使用Protocol Buffers作为默认的数据序列化机制,Protocol Buffers是一种二进制编码格式,相比文本格式,它更加紧凑和高效。

  • 方法调用:HTTP使用统一资源定位符(URL)来标识资源和执行操作,通常使用不同的HTTP方法(如GETPOSTPUTDELETE)来表示不同的操作。而gRPC使用定义在IDL中的方法来进行远程过程调用。

  • 可扩展性:gRPC支持双向流式数据传输,允许客户端和服务器同时发送和接收流式数据,这使得它更适合高并发和实时性要求较高的场景。而HTTP通常是单向的请求-响应模式。

    所以gRPC相比HTTP在性能、效率和灵活性方面有优势,尤其适用于构建大规模分布式系统和微服务架构。但对于简单的请求-响应场景,HTTP更好。

2 grpc-两数之和小demo

(1)本文主要使用的是go语言进行grpc的案例开发。在开发前首先要安装grpcProtocol 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文件生成gogrpc代码。

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文件就需要重新生成gogrpc,以及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)要使httpgrpc使用同一端口,则只需要修改上文中service端的代码即可。这样http APIgrpc 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 APIgrpc API都可以通过8081对服务进行访问了。

在这里插入图片描述
在这里插入图片描述

  • 22
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值