【grpc】关于grpc的第一次尝试

自己瞎琢磨的,有错勿怪。

一、rpc相关的历史补充(后续补充)

1、对tcp协议的补充
tcp协议面向连接、可靠、基于字节流,但是会出现"黏包"问题,分不清字节从哪开始,从哪里结束(分不清夏洛特+烦恼
还是夏洛+特烦恼),因此基于tcp协议上补充了很多其他的协议,一个就是http,还有一个就是rpc。
http是基于链接,主打的是接受和回复
rpc主打的就是远程调用,直接调用你客户端的函数
rpc协议先于http发明

2、有了rpc为什么要有http
原先的软件模式是客户端连自己的服务器,使用自定义的rpc协议
但是浏览器的出现,使得浏览器需要可以连接所有的服务器,这样自定义的rpc协议就被淘汰,更规则的http协议就出来了,
rpc协议就多用于公司内部服务器之间的调用

3、grpc
现在意义上的一些grpc协议或者框架沿用的是"远程调用"的内核,其不一定和http协议冲突,甚至本身就是基于http协议,比如
grpc就基于的http2

4、为什么要使用protobuf
tcp协议传输的body之中的内容是字节流,需要转化成结构体等等结构,这个过程叫"序列化",反之就叫做"反序列化",我们
常用的反序列化格式就是json,但是grpc使用的是protobuf
protobuf转化的空间和时间效率都比json高,支持多语言,但是描述性较差,通用性还有点欠缺,我只会这个八股式的描述,
科学上来描述就有点难了

二、rpc理解

微服务会在不同的端口使用不同的语言提供相异的服务,端口之间的通信就使用rpc。这边的grpc的“g”我原先意味是golang,后来发现是google。

在golang关于rpc的官方包中,rpc主要有使用http/tcp协议,使用gob/json两个不同的维度,我之前的文章中也写过相关的例子。

grpc原先应该是cpp使用的,但是后来支持了不同的语言,特殊之处在于使用protobuf(似乎可以换成json但是我暂时没有看到这部分)作为通信的格式,此外通信方面使用的是HTTP2,所以我们会处理相关的stream,也就是流,能让我们的通信有更灵活的操作。

三、protobuf

首先可能要学习protobuf的相关知识,这里我就不写了,它的out路径方面我感觉还是有点复杂的。下面我把自己的例子放出来

syntax="proto3";
package test;//这个test其实会影响到生成的interface或者是client和server结构体的命名
option go_package="my_grpc_project/proto"; // 这是一个绝对路径的引入,从自己的项目路径开始 所以在编译的时候应该加上--go_opt=paths=source_relative 表示这个路径是相对的
import "test1.proto"; //得知import的文件应该和主文件在同一个文件夹下面
// 下面这结构体就是用来打印输出信息的
message QustMessage{
    string msg=1;
}
// 定义一下从简单到双向流的四种行为
service Test{
    rpc SimpleTest(QustMessage)returns(test1.ReplyMessage){}; //包名作为前缀,让其识别
    rpc ServerStream(QustMessage)returns(stream test1.ReplyMessage){};
    rpc ClientStream(stream QustMessage)returns(test1.ReplyMessage){};
    rpc BothStream(stream QustMessage)returns(stream test1.ReplyMessage){};
};

这里定义了一个QustMessage的信息对象,里面就一个msg的string。此外,我尝试了一下引入外部的test1.proto,test1.proto定义了ReplyMessage,如下:

syntax="proto3";
package test1;
option go_package="my_grpc_project/proto";
message ReplyMessage{
    string msg=1;
}

回到上文的protobuf文件,我在service当中定义了四种服务,分别是SimpleTest(发送一个请求消息,返回一个回复消息)、ServerStream(发送一个请求消息,服务器推送一堆消息)、ClientStream(客户端发送一堆请求消息,服务器返回一个消息)和BothStream(双方都发送一堆消息)。注意这里的行为更像是一种定义,没有写服务的实体,这些服务所对应的具体内容后期是需要你自己补充的。

这一步说一下,vscode可能对引入路径之类的产生报错,不要管,没事的,另外就是路径的问题,也就是go_package部分,我是建议全部写成“.”,也就是生成在本文件夹内。

然后我们需要使用指令生成相关的文件,这里生成的文件有两种,一中是简单的pd.go的文件,这种文件是把对应的protobuf转换成go的结构体,并附加一些对应的方法,比如我这边ReplyMessage当中有一个msg的string,会生成如下的方法让你获取对应的信息

func (x *ReplyMessage) GetMsg() string {
	if x != nil {
		return x.Msg
	}
	return ""
}

类似的还有String(输出)和Reset(重置ReplyMessage)的方法。

另外一个文件就是对应的test_grpc.pb.go这样类型的文件,这个文件里生成的内容会辅助你生成grpc的客户端和服务端。我会在下面具体谈一谈。

看到test_grpc.pb.go,你就应该知道protobuf生成文件名称的格式规则,就是protobuf中的package名+pb.go和package_grpc.pb.go。(注意protobuf中的package不是golang之中的package)

我这边没有写生成protobuf和grpc文件的终端代码,我觉得还是自己操作一下,贯通整个流程比较好

四、test_grpc.pb.go

如果你成功生成了这个文件,你可能需要看一看其中的长长的内容。这一部分其实有点难以表达。就拿BothStream作一个例子吧

客户端部分

首先是客户端部分,首先会创建一个整体的客户端,客户端调用BothStream方法

func (c *testClient) BothStream(ctx context.Context, opts ...grpc.CallOption) (Test_BothStreamClient, error) {
	stream, err := c.cc.NewStream(ctx, &Test_ServiceDesc.Streams[2], "/test.Test/BothStream", opts...)
	if err != nil {
		return nil, err
	}
	x := &testBothStreamClient{stream}
	return x, nil
}

这个方法生成了一个Test_BothStreamClient的接口和对应方法

type Test_BothStreamClient interface {
	Send(*QustMessage) error
	Recv() (*ReplyMessage, error)
	grpc.ClientStream
}

type testBothStreamClient struct {
	grpc.ClientStream
}

func (x *testBothStreamClient) Send(m *QustMessage) error {
	return x.ClientStream.SendMsg(m)
}

func (x *testBothStreamClient) Recv() (*ReplyMessage, error) {
	m := new(ReplyMessage)
	if err := x.ClientStream.RecvMsg(m); err != nil {
		return nil, err
	}
	return m, nil
}

实际上就是返回了一个testBothStreamClient,这结构体有Send和Recv方法以及grpc.ClientStream原有的方法。作为能发送stream的客户端,明显需要收发两个方法。

服务端

再看一下服务器端:

func _Test_BothStream_Handler(srv interface{}, stream grpc.ServerStream) error {
	return srv.(TestServer).BothStream(&testBothStreamServer{stream})
}

type Test_BothStreamServer interface {
	Send(*ReplyMessage) error
	Recv() (*QustMessage, error)
	grpc.ServerStream
}

type testBothStreamServer struct {
	grpc.ServerStream
}

func (x *testBothStreamServer) Send(m *ReplyMessage) error {
	return x.ServerStream.SendMsg(m)
}

func (x *testBothStreamServer) Recv() (*QustMessage, error) {
	m := new(QustMessage)
	if err := x.ServerStream.RecvMsg(m); err != nil {
		return nil, err
	}
	return m, nil
}

服务器端基本上也一样,也是接受两个方法,很合理,毕竟两边都是流,只是多了一个如下的内部函数

func _Test_BothStream_Handler(srv interface{}, stream grpc.ServerStream) error {
	return srv.(TestServer).BothStream(&testBothStreamServer{stream})
}

这个函数,我们将在服务器和客户端建立起来的时候再讨论。

其他的三个函数都是类似的,虽然方法上有点区别,可以通过查看test_grpc.pb.go去理解。

五、调用

客户端

首先是客户端发起调用,这部分直接跟着官网写,这里为了方便,依旧是调用BothStream

func main() {
	conn, err := grpc.Dial("localhost:996",         grpc.WithTransportCredentials(insecure.NewCredentials()))
	// 这上面的安全参数居然一定要写
	if err != nil {
		log.Fatal("dial fail: ", err)

	}
	defer conn.Close()
	testClient := pb.NewTestClient(conn)
	// 创建一个context
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	// 开始调用相关的函数
	
	// 调用第四个函数,这第四个函数采取的是一来一回的模式
	testBothStreamClient, err := testClient.BothStream(ctx)
	if err != nil {
		fmt.Println("both stream 调用失败:", err)
	}
	msgArr := []string{"你", "好", "啊"}
	index := 0
	giveout := func(index int) (err error) {
		if index > len(msgArr)-1 {
			err = io.EOF
			return
		}
		err = testBothStreamClient.Send(&pb.QustMessage{Msg: msgArr[index]})
		return
	}
	for {
		err1 := giveout(index)
		if err1 == io.EOF {
			testBothStreamClient.CloseSend()
			fmt.Println(err1)
			break
		}
		if err1 != nil {
			fmt.Println("1", err1)
			break
		}
		res4, err := testBothStreamClient.Recv()
		if err != nil {
			fmt.Println("close and Recv fail:", err)
		}
		fmt.Println("the forth func", res4.GetMsg())
		index++
	}
}

这边我写了一个giveout函数,用for循环去发送msgArr := []string{"你", "好", "啊"}字体,采取了一问一答的方式,接受服务器端发送的信息

服务器

下面是服务器端,照理照抄官网

package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"strconv"

	pb "my_grpc_project/proto"
	"net"

	"google.golang.org/grpc"
)

type server struct {
	pb.UnimplementedTestServer
}

// 第四个函数
func (s *server) BothStream(tbss pb.Test_BothStreamServer) (err error) {
	for {
		res, err := tbss.Recv()
		if err == io.EOF {
			fmt.Println("全部接受完成")
			break
		}
		if err != nil {
			fmt.Println("接受失败")
			break
		}
		err = tbss.Send(&pb.ReplyMessage{Msg: fmt.Sprintf("接收到你的消息:%s\n", res.GetMsg())})
		if err != nil {
			fmt.Println("发送信息失败")
			break
		}
	}
	return
}
func main() {
	listener, err := net.Listen("tcp", "localhost:996")
	if err != nil {
		log.Fatalln("listen fail: ", err)
	}
	var opt []grpc.ServerOption
	grpcServer := grpc.NewServer(opt...)
	pb.RegisterTestServer(grpcServer, &server{})
	fmt.Println("grpc客户端启动,正在localhost:996进行监听")
	err = grpcServer.Serve(listener)
	if err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

服务器端需要具体写一下对应的BothStream方法,服务器端接受请求的时候就会调用这个方法,我们上面还有一个问题,既然会调用我自己定义的方法,那么在test_grpc.pb.go当中为什么会有这个函数呢

func _Test_BothStream_Handler(srv interface{}, stream grpc.ServerStream) error {
	return srv.(TestServer).BothStream(&testBothStreamServer{stream})
}

以我目前的知识,我认为,应该和开启服务端的这个内容一起来看

pb.RegisterTestServer(grpcServer, &server{})

我在注册服务器的时候,将这个server带着自定义方法的server也传了进去,这应该对应的就是_Test_BothStream_Handler函数的srv参数,每次服务端调用实际的BothStream方法的时候,实际上调用的_Test_BothStream_Handler函数,而在这个函数之中调用srv中的你自己定义的方法。所以大概就是这样的一个流程。我还讲整个测试的内容传入了gitee,需要的可以看看,包含完整的四个函数的调用,当然这只是初步,并没有包含更细节的配置。my_grpc_project: grpc使用测试icon-default.png?t=N7T8https://gitee.com/huangfengnt/my_grpc_project

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值