Go微服务(三)——gRPC详细入门 上

本文涉及的产品
注册配置 MSE Nacos/ZooKeeper,118元/月
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
任务调度 XXL-JOB 版免费试用,400 元额度,开发版规格
简介: Go微服务(三)——gRPC详细入门 上

篇幅可能较长,可以先收藏,方便后续观看。

文章名称 地址
Go微服务(一)——RPC详细入门 前往
Go微服务(二)——Protobuf详细入门 前往
Go微服务(三)——gRPC详细入门 前往

GRPC入门

这里会联合protobuf语法以及protobuf如何去定义rpc服务,前面我们只生成了结构体,现在我们要让他为我们同时把接口生成,有了响应的接口,我们就再也不用去手写接口了。

1. gRPC技术介绍

1.1 什么是gRPC

gRPC是什么可以用官网的一句话来概括:

A high-performance, open-source universal RPC framework

一个高性能、开源的通用 RPC 框架

1.2 为什么我们要用gRPC

视频里的:

跨语言的RPC技术:

主流语言都支持。

基于Protobuf 消息格式:

高效二进制协议。

基于HTTP2协议通讯:

丰富的周边生态(网关支持/SSL)

单一场链接避免了多TCP连接开销。

服务端推动Sever Push

头部压缩和二进制格式。

Protobuf ON HTTP2:

博客里的:

生态好:背靠Google。还有比如nginx也对grpc提供了支持。

跨语言:跨语言,主流语言都支持,且自动生成sdk

性能高:比如protobuf性能高过json, 比如http2.0性能高过http1.1

强类型:编译器就给你解决了很大一部分问题

流式处理(基于http2.0):支持客户端流式,服务端流式,双向流式

1.3 gRPC有什么好处以及在什么场景下需要用gRPC

既然是 server/client 模型,那么我们直接用 Restful API 不是也可以满足吗,为什么还需要RPC呢?下面我们就来看看RPC到底有哪些优势:

1.3.1 gRPC vs. Restful API

gRPC和restful API都提供了一套通信机制,用于server/client模型通信,而且它们都使用http作为底层的传输协议(严格地说, gRPC使用的http2.0,而restful api则不一定)。不过gRPC还是有些特有的优势,如下:

gRPC可以通过protobuf来定义接口,从而可以有更加严格的接口约束条件

另外,通过protobuf可以将数据序列化为二进制编码,这会大幅减少需要传输的数据量,从而大幅提高性能。

gRPC可以方便地支持流式通信(理论上通过http2.0就可以使用streaming模式, 但是通常web服务的restful api似乎很少这么用,通常的流式数据应用如视频流,一般都会使用专门的协议如HLS,RTMP等,这些就不是我们通常web服务了,而是有专门的服务器应用。)

1.3.2 使用场景

需要对接口进行严格约束的情况,比如我们提供了一个公共的服务,很多人,甚至公司外部的人也可以访问这个服务,这时对于接口我们希望有更加严格的约束,我们不希望客户端给我们传递任意的数据,尤其是考虑到安全性的因素,我们通常需要对接口进行更加严格的约束。这时gRPC就可以通过protobuf来提供严格的接口约束。

对于性能有更高的要求时。有时我们的服务需要传递大量的数据,而又希望不影响我们的性能,这个时候也可以考虑gRPC服务,因为通过protobuf我们可以将数据压缩编码转化为二进制格式,通常传递的数据量要小得多,而且通过http2我们可以实现异步的请求,从而大大提高了通信效率。

但是,通常我们不会去单独使用gRPC,而是将gRPC作为一个部件进行使用,这是因为在生产环境,我们面对大并发的情况下,需要使用分布式系统来去处理,而gRPC并没有提供分布式系统相关的一些必要组件。而且,真正的线上服务还需要提供包括负载均衡,限流熔断,监控报警,服务注册和发现等等必要的组件。

1.4 gRPC 的优点是怎么实现的(可跳过)

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

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

他比json快六倍;

为什么protobuf比json快?

protobuf的二进制数据流:

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的对比:

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

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

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

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

减少了tcp的连接,降低了服务端和客户端对于内存,cpu等的压力

减少了tcp的连接,保证了不频繁触发tcp重新建立,这样就不会频繁有慢启动

减少了tcp的连接,使网络拥塞情况得以改善

为什么http/1.1不能实现多路复用而http2.0可以?

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

头部压缩:

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

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

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

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

静态字典:将常用的header字段整成字典,比如{“method”:“GET”} 就可以用单个数字 2来表示;

动态字典:没有在静态字典里面的一些头部字段,则用动态字典

Huffman 编码: 压缩编码

二进制分帧:

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

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

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

一次只能处理一个请求或者响应,因为这种以分隔符分割消息的数据,在完成之前不能停止解析。

解析这种数据无法预知需要多少内存,会给服务端有很大压力。

服务器主动推送资源:

由于支持服务器主动推送资源,则可以省去一部分请求。

比如你需要两个文件1.html,1.css,如果是http1.0则需要请求两次,服务端返回两次。但是http2.0则可以客户端请求一次,然后服务端直接回吐两次。

2. gRPC Hello World

2.1 插件安装:

# protoc-gen-go 插件之前在protobuf教程中已经安装
# 
# go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
# 安装protoc-gen-go-grpc插件
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

查看当前grpc插件的版本:

protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.2.0

2.2 编写.proto文件

./service/service.proto

syntax = "proto3";
package hello;
option go_package = "MicroServiceStudy01/08-grpc/service";
message Request{
  string value = 1;
}
message Response{
  string value = 1;
}
// The HelloService service definition
// service 关键字
// HelloService 服务名称 对应接口的名称
// service服务会对应.pb.go文件里interface,里面的rpc对应接口中的函数
service HelloService{
  rpc Hello (Request) returns (Response){}
  rpc Channel(stream Request) returns (stream Response) {}
}

生成代码:

# D:\code\GoLandProjects\src\MicroServiceStudy01\
protoc -I . \
--go_out=./08-grpc/service \
--go_opt=module="MicroServiceStudy01/08-grpc/service" \
-go-grpc_out=./08-grpc/service \
--go-grpc_opt=module="MicroServiceStudy01/08-grpc/service" \
./08-grpc/service/service.proto

--go_out=......:proto-gen-go 插件编译产物的存放目录

--go_opt=......:protoc—gen-go 插件的opt参数,采用go moudle模式.

--go-grpc_out=.....:proto-gen-go-grpc 插件编译产物的存放目录

--go-grpc_opt=......:proto-gen-go-grpc 插件的opt参数,采用go moudle模式.

可能大家会感觉生成代码的命令内容比较多,后面会有有一些小技巧,真正去做代码生成的时候,很少会写这么多,这是现在的临时性工作。

我们可以看到,除了之前我们见过的.pb.go文件,还多了个_grpc.pb.go文件,里面是我们proto文件里定义的service对应生成的代码。

生成的客户端代码:

...
// HelloServiceClient是HelloService服务的客户端API。
type HelloServiceClient interface {
  Hello(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
  Channel(ctx context.Context, opts ...grpc.CallOption) (HelloService_ChannelClient, error)
}
...

生成的服务端代码:

...
// HelloServiceServer 是 HelloService 服务的服务端 API。
// 所有实现都必须嵌入 UnimplementedHelloServiceServer
// 为了向前兼容
type HelloServiceServer interface {
  Hello(context.Context, *Request) (*Response, error)
  Channel(HelloService_ChannelServer) error
  mustEmbedUnimplementedHelloServiceServer()
}
...

我们在生成的service_grpc.pb.go文件中要注意一个部分:

// UnimplementedHelloServiceServer 必须嵌入到向前兼容的实现中。
type UnimplementedHelloServiceServer struct {
}
func (UnimplementedHelloServiceServer) Hello(context.Context, *Request) (*Response, error) {
  return nil, status.Errorf(codes.Unimplemented, "method Hello not implemented")
}
func (UnimplementedHelloServiceServer) Channel(HelloService_ChannelServer) error {
  return status.Errorf(codes.Unimplemented, "method Channel not implemented")
}
func (UnimplementedHelloServiceServer) mustEmbedUnimplementedHelloServiceServer() {}

生成的这个文件里有一个叫做UnimplementedHelloServiceServer的对象,这个对象它是实现了我们的HelloSerivce接口,但是它实现了过后给你反的是一个错误信息,如果你没有任何对象实现这个接口,而你又调用了这个服务的话,它会给你返回一个return status.Errorf(codes.Unimplemented, "method Channel not implemented")这样的错误信息。

2.3 server服务端

./server/server.go

type HelloService struct {
    // UnimplementedHelloServiceServer这个结构体是必须要内嵌进来的
    // 也就是说我们定义的这个结构体对象必须继承UnimplementedHelloServiceServer。
    // 嵌入之后,我们就已经实现了GRPC这个服务的接口,但是实现之后我们什么都没做,没有写自己的业务逻辑,
    // 我们要重写实现的这个接口里的函数,这样才能提供一个真正的rpc的能力。
  service.UnimplementedHelloServiceServer
}
var _ service.HelloServiceServer = (*HelloService)(nil)
// Hello 重写实现的接口里的Hello函数
func (p *HelloService) Hello(ctx context.Context, req *service.Request) (*service.Response, error){
  resp := &service.Response{}
  resp.Value = "hello:" + req.Value
  return resp, nil
}
func main(){
  // 首先通过grpc.NewServer() 构造一个grpc服务对象
  grpcServer:=grpc.NewServer()
  // 然后通过grpc插件生成的RegisterHelloServiceServer函数注册我们实现的HelloService服务。
  service.RegisterHelloServiceServer(grpcServer,new(HelloService))
  listen,err:=net.Listen("tcp",":1234")
  if err!=nil{
    log.Fatal("Listen TCP err:", err)
  }
  //最后通过grpcServer.Serve(listen) 在一个监听端口上提供gRPC服务
  grpcServer.Serve(listen)
}

注意两点:

内嵌:

Unimplementedxxx这个结构体是必须要内嵌进来的,也就是说我们定义的这个结构体对象必须继承Unimplementedxxx。

嵌入之后,我们就已经实现了GRPC这个服务的接口,但是实现之后我们什么都没做,没有写自己的业务逻辑,我们要重写实现的这个接口里的函数,这样才能提供一个真正的rpc的能力。

重写

2.4 client客户端

client/client.go

func main() {
  // grpc.Dial负责和gRPC服务建立连接
  conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
  // 这里会提示,WithInsecure已被弃用,
    // 如果你不想继续使用WithInsecure,可以使用
    // 函数insecure.NewCredentials()返回credentials.TransportCredentials的一个实现。
  // 您可以将其作为DialOption与grpc.WithTransportCredentials一起使用:
  // 但是,API标记为实验性的,因此即使他们已经添加了弃用警告,您也不必立即切换。
  //conn, err := grpc.Dial("localhost:1234",grpc.WithTransportCredentials(insecure.NewCredentials()))
  if err != nil {
    log.Fatal("Dial err: ", err)
  }
  defer conn.Close()
  // NewHelloServiceClient函数是xxx_grpc.pb.go中自动生成的函数,
  // 基于已经建立的连接构造HelloServiceClient对象,
  // 返回的client其实是一个HelloServiceClient接口对象
  //
  client := service.NewHelloServiceClient(conn)
  // 通过接口定义的方法就可以调用服务端对应gRPC服务提供的方法
  req := &service.Request{Value: "小亮"}
  reply, err := client.Hello(context.Background(), req)
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println(reply.GetValue())
}

client := service.NewHelloServiceClient(conn)

func NewHelloServiceClient(cc grpc.ClientConnInterface) HelloServiceClient {
  return &helloServiceClient{cc}
}

NewHelloServiceClient函数是xxx_grpc.pb.go中自动生成的函数,基于已经建立的连接构造HelloServiceClient对象,返回的client其实是一个HelloServiceClient接口对象。

需要的参数是grpc拨号后放回的连接对象。

context.Background():

返回一个非nil的空Context。它永远不会被取消,没有值,并且没有截止日期。它通常由主函数使用,初始化和测试,并作为传入的顶级上下文请求。

相关文章
|
12天前
|
Go C语言
Go语言入门:分支结构
本文介绍了Go语言中的条件语句,包括`if...else`、`if...else if`和`switch`结构,并通过多个练习详细解释了它们的用法。`if...else`用于简单的条件判断;`if...else if`处理多条件分支;`switch`则适用于基于不同值的选择逻辑。特别地,文章还介绍了`fallthrough`关键字,用于优化重复代码。通过实例如判断年龄、奇偶数、公交乘车及成绩等级等,帮助读者更好地理解和应用这些结构。
34 14
|
3月前
|
Kubernetes Cloud Native 开发者
云原生入门:从容器到微服务
本文将带你走进云原生的世界,从容器技术开始,逐步深入到微服务架构。我们将通过实际代码示例,展示如何利用云原生技术构建和部署应用。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和启示。
|
3月前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
3月前
|
Go 数据处理 API
Go语言在微服务架构中的应用与优势
本文摘要采用问答形式,以期提供更直接的信息获取方式。 Q1: 为什么选择Go语言进行微服务开发? A1: Go语言的并发模型、简洁的语法和高效的编译速度使其成为微服务架构的理想选择。 Q2: Go语言在微服务架构中有哪些优势? A2: 主要优势包括高性能、高并发处理能力、简洁的代码和强大的标准库。 Q3: 文章将如何展示Go语言在微服务中的应用? A3: 通过对比其他语言和展示Go语言在实际项目中的应用案例,来说明其在微服务架构中的优势。
|
3月前
|
存储 设计模式 安全
Go语言中的并发编程:从入门到精通###
本文深入探讨了Go语言中并发编程的核心概念与实践技巧,旨在帮助读者从理论到实战全面掌握Go的并发机制。不同于传统的技术文章摘要,本部分将通过一系列生动的案例和代码示例,直观展示Go语言如何优雅地处理并发任务,提升程序性能与响应速度。无论你是Go语言初学者还是有一定经验的开发者,都能在本文中找到实用的知识与灵感。 ###
|
3月前
|
JavaScript Java Go
探索Go语言在微服务架构中的优势
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出。本文将深入探讨Go语言在构建微服务时的性能优势,包括其在内存管理、网络编程、并发模型以及工具链支持方面的特点。通过对比其他流行语言,我们将揭示Go语言如何成为微服务架构中的一股清流。
161 53
|
3月前
|
Serverless Go
Go语言中的并发编程:从入门到精通
本文将深入探讨Go语言中并发编程的核心概念和实践,包括goroutine、channel以及sync包等。通过实例演示如何利用这些工具实现高效的并发处理,同时避免常见的陷阱和错误。
|
3月前
|
Cloud Native 持续交付 云计算
云原生入门指南:从容器到微服务
【10月更文挑战第28天】在数字化转型的浪潮中,云原生技术成为推动现代软件开发的关键力量。本篇文章将带你了解云原生的基本概念,探索它如何通过容器化、微服务架构以及持续集成和持续部署(CI/CD)的实践来提升应用的可伸缩性、灵活性和可靠性。你将学习到如何利用这些技术构建和部署在云端高效运行的应用,并理解它们对DevOps文化的贡献。
88 2
|
3月前
|
监控 API 持续交付
后端开发中的微服务架构:从入门到精通
【10月更文挑战第26天】 在当今的软件开发领域,微服务架构已经成为了众多企业和开发者的首选。本文将深入探讨微服务架构的核心概念、优势以及实施过程中可能遇到的挑战。我们将从基础开始,逐步深入了解如何构建、部署和管理微服务。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的见解和实用的建议。
74 0
|
3月前
|
Kubernetes 关系型数据库 MySQL
Kubernetes入门:搭建高可用微服务架构
【10月更文挑战第25天】在快速发展的云计算时代,微服务架构因其灵活性和可扩展性备受青睐。本文通过一个案例分析,展示了如何使用Kubernetes将传统Java Web应用迁移到Kubernetes平台并改造成微服务架构。通过定义Kubernetes服务、创建MySQL的Deployment/RC、改造Web应用以及部署Web应用,最终实现了高可用的微服务架构。Kubernetes不仅提供了服务发现和负载均衡的能力,还通过各种资源管理工具,提升了系统的可扩展性和容错性。
197 3