首页 > 其他分享 >详解 Protobuf 以及在 Go 中如何使用 gRPC

详解 Protobuf 以及在 Go 中如何使用 gRPC

时间:2024-04-01 21:48:01浏览次数:13  
标签:Protobuf proto gRPC people JSON Go 序列化

什么是 RPC?

RPC 指的是远程过程调用(Remote Procedure Call),简单理解就是一个节点请求另一个节点提供的服务。假设有两台服务器 A 和 B,一个部署在 A 服务器上的应用,想要调用 B 服务器上某个应用提供的函数 / 方法。但由于不在同一个内存空间,所以不能直接调用,而是需要通过网络来表达调用的语义和传达调用的数据。

显然与 RPC 对应的则是本地过程调用,我们本地调用一个函数便是最常见的本地过程调用。但是很明显,将本地过程调用变成远程过程调用会面临各种各样的问题。我们举个例子:

func Add(a, b int) int {
	return a + b
}

func main() {
	sum := Add(1, 2)
	fmt.Println(sum)
}

以上便是一个本地过程调用,非常简单,但如果是远程过程调用就不一样了。假设上面的 Add 函数部署在另一个节点上,那么在本地要如何去调用呢?显然这么做的话,我们需要面临如下问题:


1)Call 的 ID 映射

远程服务中肯定不止一个函数,那我们要怎么告诉远程机器,调用的是 Add 函数,而不是 Sub 或其它的函数呢?首先在本地调用中,直接通过函数指针即可,编译器或解释器会自动帮我们找到指针指向的函数。但在远程调用中则不行,因为它们不在同一个节点,自然更不在同一进程,而两个进程的地址空间是不一样的。所以在 RPC 中,每个函数必须都有一个唯一的 ID,客户端在远程过程调用时,必须要附上这个 ID。然后客户端和服务端还需要各自维护一个 "函数和 Call ID 之间的映射关系",相同的函数对应的 Call ID 必须一致。当客户端需要进程远程调用时,根据映射关系找到函数对应的 Call ID,传递给服务端;然后服务端再根据 Call ID 找到要调用的函数,并进行调用。


2)序列化和反序列化

这个相信你很熟悉,在做 Web 开发的时候会经常用到。比如 Python 编写的 Web 服务返回一个字典,那么它要如何变成 Go 的 map 呢?显然是先将 Python 的字典序列化成 JSON,然后 Go 再将 JSON 反序列化成 map。而 JSON 便是两者之间的媒介,它是一种数据格式,也是一种协议。这在 RPC 中也是同理,因为是远程调用,那么必然要涉及的数据的传输。那么问题来了,我们调用的时候肯定是需要传递参数的,那这些参数要怎么传递呢?而且客户端和服务端使用的语言也可以不一样,比如客户端使用 Python,服务端使用 C++、Java 等等,而不同语言对应的数据结构不同,例如我们不可能在 C++、Java 里面操作 Python 中的字典、类实例等等。

所以还是协议,这是显而易见、最直接的解决办法。我们在传递参数的时候可以将内存中的对象序列化成一个可以在网络中传输的二进制对象,这个对象不是某个语言独有的,而是大家都认识。然后传输之后,服务端再将这个对象反序列化成对应语言的数据结构,同理服务端返回内容给客户端也是相同的过程。

所以我们还是想到了 HTTP + JSON,因为它们用的太广泛了,客户端发送 HTTP 请求,通过 JSON 传递参数;然后服务端处理来自客户端的请求,并将传递的 JSON 反序列化成对应的数据结构,并执行相应的逻辑;执行完毕之后,再将返回的结果也序列化成 JSON 交给客户端,客户端再将其反序列化。显然这是一个非常通用的流程,而实现了 RPC 的框架(gRPC)也是同样的套路,只不过它没有采用 HTTP + JSON 的方式,因为这种协议是非常松散的,至于 gRPC 到底用的是什么协议我们后面说。


3)网络传输

因为是远程调用,那么必然涉及到网络的传输,因此就需要有一个网络传输层。网络传输层需要把 Call ID 和序列化的参数字节流传递给服务端,服务端逻辑执行完毕之后再将结果序列化并返回给客户端。只要能完成这个过程,那么都可以作为传输层使用。因此 RPC 所使用的协议是可以有多种的,只要能完成传输即可,尽管大部分 RPC 框架使用的都是 TCP 协议,但其实 UDP 也可以,而 gRPC 则直接使用了 HTTP2。

RPC、HTTP、Restful 之间的区别?

关于这三者的概念可能有人会混淆,我们来解释一下。

首先我们说如果想实现 RPC,那么必须要先解决两个问题:

  • 1)数据的序列化和反序列化
  • 2)网络传输协议

想实现 RPC 需要依赖网络传输协议,而 HTTP 协议便是网络传输协议的一种,但 RPC 不仅可以使用 HTTP 协议,也可以使用 TCP 协议。所以结论很清晰了,HTTP 协议只是实现 RPC 框架的一种选择,你可以选择它,也可以不选择它,因此这两者不是竞争关系。

然后 RPC 和 Restful 之间也不是互斥的,我们对外提供服务的时候一般都是通过 HTTP 请求的方式,而 Restful 便是 HTTP 请求的一种风格,或者说规范。如果 RPC 框架选择了 HTTP 协议,那么便可以用 Restful 风格提供服务,所以 RPC 和 Restful 其实没有太大关系。

常用的序列化方式有哪些?

首先每种语言基本上都有自己原生的序列化方式,但这种序列化之后的结构也只有该语言才能解析,所以它的性能会高一些。但它不具备普适性,因为语言不会收敛于一种,所以这里我们只介绍多种语言都支持的序列化。


JSON

JSON 可能是我们最熟悉的一种序列化格式了,JSON 是典型的 Key-Value 格式,没有数据类型,是一种文本型序列化格式,JSON 的具体格式和特性,网上相关的资料非常多,这里就不再介绍了。它在应用上还是很广泛的,无论是前台 Web 使用 Ajax、用磁盘存储文本类型的数据,还是基于 HTTP 协议的通信,都会选择 JSON 格式。

但使用 JSON 进行序列化有两个问题,需要格外注意:

  • JSON 进行序列化的额外空间开销比较大,对于大数据量服务来说意味着需要巨大的内存和磁盘开销;
  • JSON 没有特别强的类型,因此像 Go 这种强类型语言,需要通过反射统一解决,所以性能不会太好;

所以如果 RPC 框架选用 JSON 来序列化,服务提供者与服务调用者之间传输的数据量需要相对较小才行,否则将严重影响性能。


Hessian

Hessian 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。Hessian 协议要比 JSON 更加紧凑,性能上也高效很多,而且生成的字节数也更小,因此有非常好的兼容性和稳定性,所以 Hessian 更加适合作为 RPC 框架远程通信的序列化协议。


Protobuf

Protobuf 是 Google 公司内部的混合语言数据标准,是一种轻便、高效的结构化数据存储格式,可以用于结构化数据的序列化,支持 Java、Python、C++、Go 等语言。Protobuf 使用的时候需要定义 IDL(Interface description language),然后使用不同语言的 IDL 编译器,生成序列化工具类,它的优点是:

  • 序列化后体积相比 JSON、Hessian 小很多;
  • IDL 能清晰地描述语义,所以足以帮助并保证应用程序之间的类型不会丢失;
  • 序列化和反序列化的速度很快,不需要通过反射获取类型;
  • 消息格式升级和兼容性不错,可以做到向后兼容;

关于序列化协议还有很多,那么面对这些序列化协议,在 RPC 框架中我们该如何选择呢?

首先你可能想到的是性能和效率,不错,这的确是一个非常值得参考的因素。因为序列化和反序列化过程是 RPC 调用的一个必须过程,那么序列化和反序列化的性能和效率势必将直接关系到 RPC 框架的性能和效率。

但除此之外,还有空间开销,也就是序列化之后的二进制数据的体积大小。序列化后的字节数据体积越小,网络传输的数据量就越小,传输数据的速度也就越快,由于 RPC 是远程调用,那么网络传输的速度将直接关系到请求响应的耗时。

而除了上面两点,还有最最重要的一点,相信你也猜到了,就是序列化协议的通用性和兼容性,该协议一定要能很好地兼容多语言。事实上在序列化的选择上,与序列化协议的效率、性能、序列化后的体积相比,其通用性和兼容性的优先级会更高,因为它直接关系到服务调用的稳定性和可用率,对于服务的性能来说,服务的可靠性显然更加重要。我们更加看重这种序列化协议在版本升级后的兼容性是否很好,是否支持更多的对象类型,是否是跨平台、跨语言的,是否有很多人已经用过并且踩过了很多的坑,其次我们才会去考虑性能、效率和空间开销。

当然还有一点要特别强调,除了序列化协议的通用性和兼容性,序列化协议的安全性也是非常重要的一个参考因素,甚至应该放在第一位去考虑。如果序列化存在安全漏洞,那么线上的服务就很可能被入侵。

综合以上因素,在业界首推的便是 Protobuf,而 gRPC 采用的序列化协议便是 Protobuf,下面我们就来详细地介绍一下。

详解 Protobuf

需要注意的是,Protobuf 只是一种序列化协议,它不一定非要和 RPC 框架搭配使用。我们在发送 HTTP 请求的时候,也可以使用 Protobuf,所以它和 JSON 的定位是一样的。不过既然有 JSON,还会出现 Protobuf,那就说明 Protobuf 相比 JSON 有很大的优势,那么优势都有哪些呢?

总结一下,Protobuf 全称为 Protocol Buffer,它是 Google 开发的一种轻量并且高效的结构化数据存储格式,性能要远远优于 JSON 和 XML。另外 Protobuf 经历了两个版本,分别是 Protobuf 2 和 Protobuf 3,目前主流的版本是 3,因为更加易用。

编写一个简单的 protobuf 文件

Protobuf 文件有自己的语法格式,所以相比 JSON 它的门槛要高一些。我们创建一个文件,文件名为 girl.proto。

protobuf 文件的后缀是 .proto

// syntax 负责指定使用哪一种 Protobuf 服务
// 注意:syntax 必须写在非注释的第一行
syntax = "proto3";

// 生成的 Go 源文件的包名,因为 Go 文件一定要有一个 package
// 而 .; 后面的 people 便负责指定 package 的名称
option go_package = ".;people";

// 把 UserInfo 当成 Python 中的类、或者 Go 的结构体
// 把 name 和 age 当成绑定在实例上的两个字段
message UserInfo {
  string name = 1;  // = 1 表示第 1 个参数
  int32 age = 2;
}

Protobuf 文件编写完成,然后我们要去 https://github.com/protocolbuffers/protobuf/releases 下载编译工具 protoc,它负责将 Protobuf 文件编译成 Go 源文件。这里我下载的是 protoc-25.3-win64.zip,解压之后,将里面的 bin 目录添加到环境变量中。

然后我们还要安装两个包:

  • go get -u google.golang.org/protobuf
  • go install google.golang.org/protobuf/cmd/protoc-gen-go

安装完之后我们就可以生成 Go 源文件了。

--go_out 负责指定在哪一个目录下生成 Go 源文件,这里是当前目录;-I 参数负责指定 .proto 文件的搜索路径,这里也是当前目录。命令执行完后会发现自动生成了一个 girl.pb.go 源文件,并且 package 是 people。

注意:生成的 girl.pb.go 文件位于当前的主目录中,该目录还存在一个 main.go,负责导入生成的 pb.go 文件。而一个目录中不能存在两个包名,所以我们还要再创建一个 people 目录,将 girl.pb.go 移动过去。

然后我们来看看采用 Protobuf 协议序列化之后的结果是什么,不是说它比较高效吗?那么怎能不看看它序列化之后的结果呢,以及它和 JSON 又有什么不一样呢?

package main

import (
	"GoProject/people"
	"fmt"
	"github.com/golang/protobuf/proto"
)

func main() {
	// 在 Protobuf 文件中定义了 message UserInfo,那么会自动生成一个名为 UserInfo 的结构体
	// 我们可以直接实例化它,而参数则是 Name 和 Age
	// 因为在 message UserInfo 里面指定的字段是 name 和 age,这里会自动变成大写
	user := people.UserInfo{Name: "satori", Age: 17}
	fmt.Println(user.Name, user.Age) // satori 17
	// 序列化成字节流
	data, _ := proto.Marshal(&user)
	fmt.Println(data)  // [10 6 115 97 116 111 114 105 16 17]
	fmt.Println(len(data)) // 10
}

使用 Protobuf 协议序列化之后的长度为 10,很明显比 JSON 短,平均能得到一倍的压缩。序列化我们知道了,那么如何反序列化呢?

package main

import (
	"GoProject/people"
	"fmt"
	"github.com/golang/protobuf/proto"
)

func main() {
	// 依旧是实例化一个结构体实例,但是不需要传参
	user := people.UserInfo{}
	data := []byte{10, 6, 115, 97, 116, 111, 114, 105, 16, 17}
	// 传入序列化之后的字节流以及结构体指针,进行反序列化
	_ = proto.Unmarshal(data, &user)
	fmt.Println(user.Name, user.Age) // satori 17
}

所以无论是 Protobuf 还是 JSON,都是将一个对象序列化成二进制字节串,然后根据序列化之后的字节串,再反序列出原来的对象。只不过采用 Protobuf 协议进行序列化和反序列化,速度会更快,并且序列化之后的数据压缩比更高,在传输的时候耗时也会更少。

然后还有一个关键地方的就是,JSON 这种数据结构比较松散。你在返回 JSON 的时候,需要告诉调用你接口的人,返回的 JSON 里面都包含哪些字段,以及类型是什么。但 Protobuf 则不需要,因为字段有哪些、以及相应的类型,都必须在文件里面定义好。别人只要拿到 .proto 文件,就知道你要返回什么样的数据了,一目了然。

Protobuf 的基础数据类型

在不涉及 gRPC 的时候,Protobuf 文件是非常简单的,你需要返回啥结构,那么直接在 .proto 文件里面使用标识符 message 定义即可。

message 名称 {
    类型 字段名 = 1;
    类型 字段名 = 2;
    类型 字段名 = 3;
}

但是类型我们需要说一下,之前用到了两个基础类型,分别是 string 和 int32,那么除了这两个还有哪些类型呢?

以上是基础类型,当然还有复合类型,我们一会单独说,先来演示一下基础类型。编写 .proto 文件:

// 文件名:basic_type.proto
syntax = "proto3";

option go_package = ".;people";

message BasicType {
    // 字段的名称可以和类型名称一致,这里为了清晰
    // 我们就直接将类型的名称用作字段名
    int32 int32 = 1;
    sint32 sint32 = 2;
    uint32 uint32 = 3;
    fixed32 fixed32 = 4;
    sfixed32 sfixed32 = 5;

    int64 int64 = 6;
    sint64 sint64 = 7;
    uint64 uint64 = 8;
    fixed64 fixed64 = 9;
    sfixed64 sfixed64 = 10;
    double double = 11;
    float float = 12;

    bool bool = 13;
    string string = 14;
    bytes bytes = 15;
}

然后来生成 Go 源文件,命令如下:

protoc --go_out=./people -I . basic_type.proto

这里直接将路径指定为 people 目录,执行命令之后会生成 basic_type.pb.go。

package main

import (
	"GoProject/people"
	"fmt"
	"github.com/golang/protobuf/proto"
)

func createBasicType() []byte {
	basicType := people.BasicType{
		Int32:    123,
		Sint32:   234,
		Uint32:   345,
		Fixed32:  456,
		Sfixed32: 789,

		Int64:    1230,
		Sint64:   2340,
		Uint64:   3450,
		Fixed64:  4560,
		Sfixed64: 7890,

		Double: 3.1415926,
		Float:  2.71,

		Bool: true,
		// 当名称为 string 时,会自动在结尾加一个下划线
		String_: "satori",
		Bytes:   []byte("satori"),
	}
	data, _ := proto.Marshal(&basicType)
	return data
}

func main() {
	basicType := people.BasicType{}
	_ = proto.Unmarshal(createBasicType(), &basicType)
	fmt.Println(basicType.Int64)   // 1230
	fmt.Println(basicType.String_) // satori
	fmt.Println(basicType.Double)  // 3.1415926
}

很简单,没有任何问题,以上就是 Protobuf 的基础类型。然后再来看看符合类型,以及一些特殊类型。

repeat 和 map

repeat 和 map 是一种复合类型,可以把它们当成 Go 的切片和 map。

// 文件名:girl.proto
syntax = "proto3";

option go_package = ".;people";

message UserInfo {
    // 对于 Go 而言,repeated 表示 hobby 字段的类型是切片
    // string 则表示切片里面的元素必须都是字符串
    repeated string hobby = 1;   
    // map<string, string> 表示 info 字段的类型是 map,键值对必须都是字符串
    map<string, string> info = 2;
}

我们执行命令,生成 Go 文件,然后导入测试一下。

package main

import (
	"GoProject/people"
	"fmt"
)

func main() {
	userInfo := people.UserInfo{
		Hobby: []string{"唱", "跳", "rap", "

标签:Protobuf,proto,gRPC,people,JSON,Go,序列化
From: https://www.cnblogs.com/wan-ming-zhu/p/18109432

相关文章

  • 搭建go环境并运行
    参考:https://blog.csdn.net/jmmyhans/article/details/134726820(安装)https://blog.csdn.net/weixin_37254196/article/details/110823033(443报错) 打算自己跑个go项目,于是乎尝试自己安装go的运行环境,不过也不难,操作如下windowsscoopinstallgolang//这边我的电脑用的scoo......
  • python计算机毕设【附源码】毕业生离校系统的设计与实现(django+mysql+论文)
    本系统(程序+源码)带文档lw万字以上  文末可获取本课题的源码和程序系统程序文件列表系统的选题背景和意义选题背景:随着互联网技术的飞速发展,信息化管理已经成为了现代教育体系中不可或缺的一部分。对于高校而言,毕业生离校系统的设计与实现是提高学校管理效率、优化毕业生......
  • CSGO赛事管理系统的设计与实现|Springboot+ Mysql+Java+ B/S结构(可运行源码+数据库+
    本项目包含可运行源码+数据库+LW,文末可获取本项目的所有资料。在世界范围内,CSGO赛事管理系统已经得到了非常广泛的应用,范围变的越来越广,功能也在不断的完善,操作简便、节省人力、成本低廉,但工作的效率确一直不断的提高。为CSGO赛事行业管理全程提供自动化和全面的纪录,避免出错......
  • Django 中集成 CKEditor 富文本编辑器详解
    Django中集成CKEditor富文本编辑器详解原文链接:https://blog.csdn.net/Rocky006/article/details/135130010概要在Web应用中,富文本编辑器是提高用户体验的重要组件之一。CKEditor是一款流行的、功能丰富的富文本编辑器。在Django项目中集成CKEditor不仅可以提升内容......
  • GO 介绍
    ######GO介绍######Go即Golang,是Google公司2009年11月正式对外公开的一门编程语言#Go是静态强类型语言,是区别于解析型语言的编译型语言(静态:是否需要编译强类型:不同类型不允许直接运算)。#python是动态强类型:不需要编译,不同类型不允许直接运算#js动态弱类型解析......
  • Django在Windows server IIS部署
    Django在WindowsserverIIS部署本文章转载于https://www.django.cn/article/show-21.html,详查看此文教程基于Windowsserver2012+Python3.6+IIS之上部署django的,同样适用于server2012之上的版本服务器和windows7以上的windows操作系统。提示:Python不要安装在windows用户目录下......
  • python golang 安装私有库的用法
    python安装在requirements.txt增加了github私有安装库的依赖git+https://${GITHUB_TOKEN}@github.com/YOLOTECHNOLOGY/bossjob-auth-lib.git@${GITHUB_LIB_BRANCH}当前该库无法通过pipenv直接安装,需要进入到虚拟环境,通过pipinstall进行安装安装前需要设置GITHUB_......
  • Django项目部署本地windows IIS(详细版)和static文件设置(页面样式正常显示)
    Django项目部署本地windowsIIS(详细版)和static文件设置(页面样式正常显示)原文链接:https://blog.csdn.net/hahahahanhanhan/article/details/134638020目录必要条件:一、下载并启用wfastcgi二、window安装IIS功能三、IIS管理器中添加网站1、复制项目2、复制wfastcgi.py文件......
  • Django+nginx+uwsgi +apscheduler任务重复执行问题
    Django+nginx+uwsgi+apscheduler任务重复执行问题1、问题描述通过Django+nginx+uwsgi部署项目,前端页面通过API接口动态添加、管理apscheduler定时任务。由于uwsgi启动多个worker,导致每个worker执行一次定时任务,导致任务的重复执行。如下图,因为uwsgi.ini文件......
  • Django项目windows部署教程,详细踩坑总结
    Django项目windows部署教程,详细踩坑总结本篇文章主要关于Django在Windows上利用Apache部署,如果你想通过IIS部署,推荐这篇文章在IIS服务器上部署django_djangoiis-CSDN博客,我之前尝试部署在IIS上,发现Django的定时任务失效了,上网查找了一番,普遍说法是被服务器回收了,经过了一番操作,......