GO gRPC

什么是grpc

详细文档:

https://doc.oschina.net/grpc?t=58008

  • gRpc 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。目前提供 C、Java 和 Go 语言版本,分别是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持.
  • gRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。
  • gRPC 默认使用 protocol buffers
  • 参考:grpc官方文档英文版grpc官方文档中文版
  • github : https://github.com/grpc/grpc-go

为什么我们要用grpc

  • 生态好:背靠Google。还有比如nginx也对grpc提供了支持,参考链接
  • 跨语言:跨语言,且自动生成sdk
  • 性能高:比如protobuf性能高过json, 比如http2.0性能高过http1.1
  • 强类型:编译器就给你解决了很大一部分问题
  • 流式处理(基于http2.0):支持客户端流式,服务端流式,双向流式

grpc 的优点是怎么实现的

grpc性能高:protobuf为什么比json性能高?

什么是protobuf?

  • Protobuf是由Google开发的二进制格式,用于在不同服务之间序列化数据。是一种IDL(interface description language)语言

他比json快多少?

为什么protobuf比json快?

  • protobuf的二进制数据流如下图

image-20230206152359575

json格式

{"content":"test","user":"test","user_id":"test"}
  • 对比json数据和protobuf数据格式可以知道
  • 体积小-无需分隔符:TLV存储方式不需要分隔符(逗号,双引号等)就能分隔字段,减少了分隔符的使用
  • 体积小-空字段省略:若字段没有被设置字段值,那么该字段序列化时的数据是完全不存在的,即不需要进行编码,而json会传key和空值的value
  • 体积小-tag二进制表示:是用字段的数字值然后转换成二进制进行表示的,比json的key用字符串表示更加省空间
  • 编解码快:tag的里面存储了字段的类型,可以直接知道value的长度,或者当value是字符串的时候,则用length存储了长度,可以直接从length后取n个字节就是value的值,而如果不知道value的长度,我们就必须要做字符串匹配
  • 细化了解protobuf的编码可以去看:varint 和 zigzag编码方式

grpc性能高:http2.0为什么比http1.1性能高?

多路复用

  • http2.0和http 1.* 还有 http1.1pipling的对比
  • 示意图

image-20230206152006833

  • http/1.* :一次请求,一个响应,建立一个连接用完关闭,每一个请求都要建立一个连接

  • http1.1 pipeling: Pipeling解决方式为,若干个请求排队串行化单线程处理,后面的请求等待前面请求的返回才能获得执行机会,一旦有某请求超时等,后续请求只能被阻塞,毫无办法,也就是人们常说的线头阻塞

  • http2: 多个请求可同时在一个连接上并行执行。某个请求任务耗时严重,不会影响到其它连接的正常执行

  • grpc 多路复用还有哪些优点

    • 减少了tcp的连接,降低了服务端和客户端对于内存,cpu等的压力
    • 减少了tcp的连接,保证了不频繁触发tcp重新建立,这样就不会频繁有慢启动
    • 减少了tcp的连接,使网络拥塞情况得以改善
  • 为什么http/1.1不能实现多路复用而http2.0可以?

    • 因为http/1.1传输是用的文本,而http2.0用的是二进制分帧传输

2. 头部压缩

  • 固定字段压缩:http可以通过http对body进行gzip压缩,这样可以节省带宽,但是报文中header也有很多字段没有进行压缩,比如cookie, user agent accept,这些有必要进行压缩

  • 避免重复:大量请求和响应的报文里面又很多字段值是重复的,所以有必要避免重复性

  • 编码改进:字段是ascii编码,效率低,改成二进制编码可以提高

  • 以上通过HPACK算法来进行实现,算法主要包含三个部分

    • 静态字典:将常用的header字段整成字典,比如{":method":"GET"} 就可以用单个数字 2来表示
    • 动态字典:没有在静态字典里面的一些头部字段,则用动态字典
    • Huffman 编码: 压缩编码

3. 二进制分帧

  • 在二进制分帧层上,HTTP 2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码 ,其中HTTP1.x的首部信息会被封装到Headers帧,而我们的request body则封装到Data帧里面。

  • 这样分帧以后这些帧就可以乱序发送,然后根据每个帧首部的流标识符号进行组装

  • 对比http/1.1因为是基于文本以换行符分割每一条key:value则会有以下问题:

    • 一次只能处理一个请求或者响应,因为这种以分隔符分割消息的数据,在完成之前不能停止解析
    • 解析这种数据无法预知需要多少内存,会给服务端有很大压力

4,. 服务器主动推送资源

  • 由于支持服务器主动推送资源,则可以省去一部分请求。比如你需要两个文件1.html,1.css,如果是http1.0则需要请求两次,服务端返回两次。但是http2.0则可以客户端请求一次,然后服务端直接回吐两次

protobuf 使用

上面我们也说protobuf 的各种优势,但其实他在我们实际应用到底怎么用呢?

微服务架构中,由于每个服务对应的代码库是独立运行的,无法直接调用,彼此间的通信就是个大问题.

gRPC可以实现将大的项目拆分为多个小且独立的业务模块,也就是服务。各服务间使用高效的protobuf协议进行RPC调用,gRPC默认使用protocol buffers,这是google开源的一套成熟的结构数据序列化机制

可以用proto files创建gRPC服务,用message类型来定义方法参数和返回类型,相当于提前定义好接口,并且这个接口一但更改,客户端和服务端都要改

当然也可以使用其他数据格式如JSON,甚至不用gRpc 直接用http 也可以,之所以用gRpc和protobuf是因为他的这些优点
当然如果服务不是特别多,直接http 反而更为方便

protoc 是什么

protoc是 protocol compiler,是protocol 编译器。
当我们从 https://github.com/protocolbu...官网安装protobuf后,在命令行,就会有一个 protoc shell脚本命令。
目前 protoc 编译器支持的语言有以下几种。
image.png
我们使用protoc 命令行可以把.proto 文件转译成各种编程语言对应的源码,包含数据类型定义、调用接口等。

protoc 命令把.proto文件转成go的过程有两步

  1. 解析.proto文件,转译成protobuf的原生数据结构在内存中保存;
  2. 把protobuf相关的数据结构传递给相应语言的编译插件,由插件负责根据接收到的protobuf原生结构渲染输出特定语言的模板。(对应go的protoc-gen-go)

安装

到github 地址,下载即可,这里使用win64

https://github.com/protocolbuffers/protobuf/releases

image-20230206162904269

下载解压后如下图

image-20230206164557696

bin目录添加到环境变量中,使其可以全局使用

image-20230206164841171

测试

随便那个路径开个CMD, 输入

protoc

可以看到如下图

image-20230206165327217

同时也有protoc编译工具的各个选项含义

protoc-gen-go

在官方文档里,
https://pkg.go.dev/github.com...里说明到
protoc-gen-go 是 Google protobuf编译器生成 Go 代码的插件。

因为protoc编译器,默认没有包含go语言代码生成器,所以需要单独安装插件

安装后会在GOPATH目录下生成可执行文件,protobuf的编译器插件protoc-gen-go,等下执行protoc命令会自动调用这个插件

安装

执行命令

go get -u github.com/golang/protobuf/protoc-gen-go

image-20230206163254279

会自动下载到,你的$GOPATH/pkg/mod里,自动在go.mod文件中添加依赖

image-20230206163644273

proto 语法

详细参考:

https://developers.google.cn/protocol-buffers/docs/proto3?hl=zh-cn

https://segmentfault.com/a/1190000007917576

https://developers.google.com/protocol-buffers/docs/gotutorial

hello World

写一个最基本的hello world gRpc 服务,

创建项目的基本步骤

# 创建项目目录
mkdir gRpc
# 切换到项目目录
cd gRpc
# 创建RPC协议目录
mkdir proto
# 初始化go模块配置,用来管理第三方依赖, 本例子,项目模块名是:gRpc
go mod init gRpc
# 添加依赖
go get -u github.com/golang/protobuf/protoc-gen-go

image-20230206171551903

image-20230206171630177

由于之前已经已经下载过,所有这个命令只会在go.mod文件中增加依赖

image-20230206171650247

此时的目录结构

image-20230206171838480

定义服务接口

定义服务,其实就是通过protobuf语法定义语言平台无关的接口。

文件:gRpc/proto/helloworld.proto

// 指定的当前proto语法的版本,有2和3,这里用 3
// 这个必须在第一行
syntax = "proto3";

// 指定包名和目录
// 这种不会 在 xxx.pb.go 文件插入包名,并且目录是当前目录不会创建文件夹
// 需要自己修改包名
// option go_package = "./";
// 这种会 在 xxx.pb.go 文件插入包名,并且会创建helloworld 文件夹
option go_package = "./gRpc";

// 定义Greeter服务
service Greeter {
  // 定义SayHello方法,接受HelloRequest消息, 并返回HelloResponse消息
  rpc SayHello (HelloRequest) returns (HelloResponse) {};
}

// 定义HelloRequest消息
message HelloRequest {
  // name字段
  string name = 1;
}

// 定义HelloResponse消息
message HelloResponse {
  // message字段
  string message = 1;
}

Greeter服务提供了一个SayHello接口,请求SayHello接口,

需要传递一个包含name字段的HelloRequest消息,

返回包含message字段的HelloResponse消息。

编译proto协议文件生成 xxx.pb.go 文件

上面通过proto定义的接口,没法直接在代码中使用,因此需要通过protoc编译器,将proto协议文件,编译成go语言代码。

# 切换到helloworld项目根目录,执行命令
protoc -I proto/ --go_out=plugins=grpc:proto proto/helloworld.proto

如果出现问题:参考文章最后的解决办法

protoc命令参数说明:

  • -I 指定代码输出目录,忽略服务定义的包名,否则会根据包名创建目录
  • --go_out 指定代码输出目录,格式:--go_out=plugins=grpc:目录名
  • 命令最后面的参数是proto协议文件

编译成功后在proto目录生成了/helloworld/helloworld.pb.go文件,里面包含了,我们的服务和接口定义。

image-20230207102531949

添加依赖

可以看到上面有很多依赖错误,使用

go mod tidy

更新下依赖

image-20230207104052144

实现服务端代码并启动

文件: server.go

package main

import (
	"log"
	"net"

	"golang.org/x/net/context"
	// 导入grpc包
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"

	// 导入刚才我们生成的代码所在的proto包。
	pb "gRpc/proto/gRpc"
)

// 定义server,用来实现proto文件,里面实现的Greeter服务里面的接口
type server struct{}

// 实现SayHello接口
// 第一个参数是上下文参数,所有接口默认都要必填
// 第二个参数是我们定义的HelloRequest消息
// 返回值是我们定义的HelloResponse消息,error返回值也是必须的。
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
	// 创建一个HelloResponse消息,设置Message字段,然后直接返回。
	return &pb.HelloResponse{Message: "Hello " + in.Name}, nil
}

func main() {
	// 监听127.0.0.1:50051地址
	lis, err := net.Listen("tcp", "127.0.0.1:50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	// 实例化grpc服务端
	s := grpc.NewServer()

	// 注册Greeter服务
	pb.RegisterGreeterServer(s, &server{})

	// 往grpc服务端注册反射服务
	reflection.Register(s)

	// 启动grpc服务
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

启动服务端

# 切换到项目根目录,运行命令
go run server.go

实现客户端代码

文件:client.go

package main

import (
	"log"
	"time"

	"golang.org/x/net/context"
	// 导入grpc包
	"google.golang.org/grpc"
	// 导入刚才我们生成的代码所在的proto包。
	pb "gRpc/proto/gRpc"
)

const (
	defaultName = "makalo"
)

func main() {
	// 连接grpc服务器
	conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	// 延迟关闭连接
	defer conn.Close()

	// 初始化Greeter服务客户端
	c := pb.NewGreeterClient(conn)

	// 初始化上下文,设置请求超时时间为1秒
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	// 延迟关闭请求会话
	defer cancel()

	// 调用SayHello接口,发送一条消息
	r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "makalo"})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}

	// 打印服务的返回的消息
	log.Printf("Greeting: %s", r.Message)
}

运行

# 切换到项目根目录,运行命令
go run client.go

运行结果,至此我们已经完成 rpc 通信

image-20230207105813578

目录结构

PS F:\go\src\gRpc> tree /F
文件夹 PATH 列表
卷序列号为 8DFC-EB47
F:.
│  client.go			客户端代码
│  go.mod				go mod 文件
│  go.sum				go mod 依赖关系文件
│  server.go			服务端代码
│
└─proto
    │  helloworld.proto	rpc协议文件
    │
    └─gRpc
            helloworld.pb.go	rpc协议go版本的代码

中间出现的错误

'protoc-gen-go' 不是内部或外部命令,也不是可运行的程序

protoc -I proto/ --go_out=plugins=grpc:proto proto/helloworld.proto

image-20230206174127446

去看看你的 $GOPATH/bin有没有 protoc-gen-go.exe,文件如果没有,就说虽然它下载了代码库但是,并没有编译

我们上面添加依赖的链接是

github.com/golang/protobuf/protoc-gen-go

我们到 $GOPATH/pkg/mod,按照上面目录依次往下找即可,找到源码库

image-20230206174728089

找到之后进入这代码库,重新编译生成可执行文件名称为protoc-gen-go.exe

go build -o protoc-gen-go.exe main.go

image-20230206175036936

将这个可执行文件剪切到$GOPATH/bin(其他目录也可以,只要能全局调用,只不过,这个目录我们一开始最开始搭环境的时候就配置了环境变量)

image-20230206175323711

vscode 中无法识别 protoc 命令

image-20230206175750727

重启vscode 终端

protoc-gen-go: unable to determine Go import path for "helloworld.proto"

protoc-gen-go: unable to determine Go import path for "helloworld.proto"

Please specify either:
        • a "go_package" option in the .proto source file, or
        • a "M" argument on the command line.

See https://developers.google.com/protocol-buffers/docs/reference/go-generated#package for more information.

--go_out: protoc-gen-go: Plugin failed with status code 1.

image-20230207095733584

需要指定 go_package 参数

option go_package = "./helloworld";

image-20230207101805307

参考:

https://developers.google.cn/protocol-buffers/docs/gotutorial?hl=zh-cn

https://blog.csdn.net/m0_61472414/article/details/127445617

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章