介绍

我猜测大部分长期使用 Java 的开发者应该较少会接触 gRPC,毕竟在 Java 圈子里大部分使用的还是 Dubbo/SpringClound 这两类服务框架。

我也是近段时间有机会从零开始重构业务才接触到 gRPC 的,当时选择 gRPC 时也有几个原因:

  • 基于云原生的思路开发部署项目,而在云原生中 gRPC 几乎已经是标准的通讯协议了。
  • 开发语言选择了 Go,在 Go 圈子中 gRPC 显然是更好的选择。
  • 公司内部有部分业务使用的是 Python 开发,在多语言兼容性上 gRPC 支持的非常好。

经过线上一年多的平稳运行,可以看出 gRPC 还是非常稳定高效的;rpc 框架中最核心的几个要点:

  • 序列化
  • 通信协议
  • IDL(接口描述语言)

这些在 gRPC 中分别对应的是:

  • 基于 Protocol Buffer 序列化协议,性能高效。
  • 基于 HTTP/2 标准协议开发,自带 stream、多路复用等特性;同时由于是标准协议,第三方工具的兼容性会更好(比如负载均衡、监控等)
  • 编写一份 .proto 接口文件,便可生成常用语言代码。

HTTP/2

学习 gRPC 之前首先得知道它是通过什么协议通信的,我们日常不管是开发还是应用基本上接触到最多的还是 HTTP/1.1 协议。

由于 HTTP/1.1 是一个文本协议,对人类非常友好,相反的对机器性能就比较低。

需要反复对文本进行解析,效率自然就低了;要对机器更友好就得采用二进制,HTTP/2 自然做到了。

除此之外还有其他优点:

  • 多路复用:可以并行的收发消息,互不影响
  • HPACK 节省 header 空间,避免 HTTP1.1 对相同的 header 反复发送。

Protocol

gRPC 采用的是 Protocol 序列化,发布时间比 gRPC 早一些,所以也不仅只用于 gRPC,任何需要序列化 IO 操作的场景都可以使用它。

它会更加的省空间、高性能;之前在开发 https://github.com/crossoverJie/cim 时就使用它来做数据交互。

package order.v1;

service OrderService{

  rpc Create(OrderApiCreate) returns (Order) {}

  rpc Close(CloseApiCreate) returns (Order) {}

  // 服务端推送
rpc ServerStream(OrderApiCreate) returns (stream Order) {} // 客户端推送
rpc ClientStream(stream OrderApiCreate) returns (Order) {} // 双向推送
rpc BdStream(stream OrderApiCreate) returns (stream Order) {}
} message OrderApiCreate{
int64 order_id = 1;
repeated int64 user_id = 2;
string remark = 3;
repeated int32 reason_id = 4;
}

使用起来也是非常简单的,只需要定义自己的 .proto 文件,便可用命令行工具生成对应语言的 SDK。

具体可以参考官方文档:

https://grpc.io/docs/languages/go/generated-code/

调用

	protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
test.proto



生成代码之后编写服务端就非常简单了,只需要实现生成的接口即可。

func (o *Order) Create(ctx context.Context, in *v1.OrderApiCreate) (*v1.Order, error) {
// 获取 metadata
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.DataLoss, "failed to get metadata")
}
fmt.Println(md)
fmt.Println(in.OrderId)
return &v1.Order{
OrderId: in.OrderId,
Reason: nil,
}, nil
}

客户端也非常简单,只需要依赖服务端代码,创建一个 connection 然后就和调用本地方法一样了。

这是经典的 unary(一元)调用,类似于 http 的请求响应模式,一个请求对应一次响应。

Server stream

gRPC 除了常规的 unary 调用之外还支持服务端推送,在一些特定场景下还是很有用的。

func (o *Order) ServerStream(in *v1.OrderApiCreate, rs v1.OrderService_ServerStreamServer) error {
for i := 0; i < 5; i++ {
rs.Send(&v1.Order{
OrderId: in.OrderId,
Reason: nil,
})
}
return nil
}

服务端的推送如上所示,调用 Send 函数便可向客户端推送。

	for {
msg, err := rpc.RecvMsg()
if err == io.EOF {
marshalIndent, _ := json.MarshalIndent(msgs, "", "\t")
fmt.Println(msg)
return
}
}

客户端则通过一个循环判断当前接收到的数据包是否已经截止来获取服务端消息。

为了能更直观的展示这个过程,优化了之前开发的一个 gRPC 客户端,可以直观的调试 stream 调用。

上图便是一个服务端推送示例。

Client Stream

除了支持服务端推送之外,客户端也支持。

客户端在同一个连接中一直向服务端发送数据,服务端可以并行处理消息。


// 服务端代码
func (o *Order) ClientStream(rs v1.OrderService_ClientStreamServer) error {
var value []int64
for {
recv, err := rs.Recv()
if err == io.EOF {
rs.SendAndClose(&v1.Order{
OrderId: 100,
Reason: nil,
})
log.Println(value)
return nil
}
value = append(value, recv.OrderId)
log.Printf("ClientStream receiv msg %v", recv.OrderId)
}
log.Println("ClientStream finish")
return nil
} // 客户端代码
for i := 0; i < 5; i++ {
messages, _ := GetMsg(data)
rpc.SendMsg(messages[0])
}
receive, err := rpc.CloseAndReceive()

代码与服务端推送类似,只是角色互换了。

Bidirectional Stream

同理,当客户端、服务端同时都在发送消息也是支持的。

// 服务端
func (o *Order) BdStream(rs v1.OrderService_BdStreamServer) error {
var value []int64
for {
recv, err := rs.Recv()
if err == io.EOF {
log.Println(value)
return nil
}
if err != nil {
panic(err)
}
value = append(value, recv.OrderId)
log.Printf("BdStream receiv msg %v", recv.OrderId)
rs.SendMsg(&v1.Order{
OrderId: recv.OrderId,
Reason: nil,
})
}
return nil
}
// 客户端
for i := 0; i < 5; i++ {
messages, _ := GetMsg(data)
// 发送消息
rpc.SendMsg(messages[0])
// 接收消息
receive, _ := rpc.RecvMsg()
marshalIndent, _ := json.MarshalIndent(receive, "", "\t")
fmt.Println(string(marshalIndent))
}
rpc.CloseSend()

其实就是将上诉两则合二为一。

通过调用示例很容易理解。

元数据

gRPC 也支持元数据传输,类似于 HTTP 中的 header

	// 客户端写入
metaStr := `{"lang":"zh"}`
var m map[string]string
err := json.Unmarshal([]byte(metaStr), &m)
md := metadata.New(m)
// 调用时将 ctx 传入即可
ctx := metadata.NewOutgoingContext(context.Background(), md) // 服务端接收
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.DataLoss, "failed to get metadata")
}
fmt.Println(md)

gRPC gateway

gRPC 虽然功能强大使用也很简单,但对于浏览器、APP的支持还是不如 REST 应用广泛(浏览器也支持,但应用非常少)。

为此社区便创建了 https://github.com/grpc-ecosystem/grpc-gateway 项目,可以将 gRPC 服务暴露为 RESTFUL API。

为了让测试可以习惯用 postman 进行接口测试,我们也将 gRPC 服务代理出去,更方便的进行测试。

反射调用

作为一个 rpc 框架,泛化调用也是必须支持的,可以方便开发配套工具;gRPC 是通过反射支持的,通过拿到服务名称、pb 文件进行反射调用。

https://github.com/jhump/protoreflect 这个库封装了常见的反射操作。

上图中看到的可视化 stream 调用也是通过这个库实现的。

负载均衡

由于 gRPC 是基于 HTTP/2 实现的,客户端和服务端会保持长连接;这时做负载均衡就不像是 HTTP 那样简单了。

而我们使用 gRPC 想达到效果和 HTTP 是一样的,需要对请求进行负载均衡而不是连接。

通常有两种做法:

  • 客户端负载均衡
  • 服务端负载均衡

客户端负载均衡在 rpc 调用中应用广泛,比如 Dubbo 就是使用的客户端负载均衡。

gRPC 中也提供有相关接口,具体可以参考官方demo。

https://github.com/grpc/grpc-go/blob/87eb5b7502/examples/features/load_balancing/README.md

客户端负载均衡相对来说对开发者更灵活(可以自定义适合自己的策略),但相对的也需要自己维护这块逻辑,如果有多种语言那就得维护多份。

所以在云原生这个大基调下,更推荐使用服务端负载均衡。

可选方案有:

  • istio
  • envoy
  • apix

这块我们也在研究,大概率会使用 envoy/istio

总结

gRPC 内容还是非常多的,本文只是作为一份入门资料希望能让不了解 gRPC 的能有一个基本认识;这在云原生时代确实是一门必备技能。

对文中的 gRPC 客户端感兴趣的朋友,可以参考这里的源码:

https://github.com/crossoverJie/ptg

5分钟学会 gRPC的更多相关文章

  1. 5分钟学会使用Less预编译器

    5分钟学会使用Less预编译器 Less是什么? LESS CSS是一种动态样式语言,属于CSS预处理语言的一种,它使用类似CSS的语法为CSS赋予了动态语言的特性,如变量.继承.运算.函数等,更方便 ...

  2. 【grunt第二弹】30分钟学会使用grunt打包前端代码(02)

    前言 上一篇博客,我们简单的介绍了grunt的使用,一些基础点没能覆盖,我们今天有必要看看一些基础知识 [grunt第一弹]30分钟学会使用grunt打包前端代码 配置任务/grunt.initCon ...

  3. 《量化投资:以MATLAB为工具》连载(2)基础篇-N分钟学会MATLAB(中)

    http://www.matlabsky.com/thread-43937-1-1.html   <量化投资:以MATLAB为工具>连载(3)基础篇-N分钟学会MATLAB(下)     ...

  4. 《量化投资:以MATLAB为工具》连载(1)基础篇-N分钟学会MATLAB(上)

    http://blog.sina.com.cn/s/blog_4cf8aad30102uylf.html <量化投资:以MATLAB为工具>连载(1)基础篇-N分钟学会MATLAB(上) ...

  5. [分享] 史上最简单的封装教程,五分钟学会封装系统(以封装Windows 7为例)

    [分享] 史上最简单的封装教程,五分钟学会封装系统(以封装Windows 7为例) 踏雁寻花 发表于 2015-8-23 23:31:28 https://www.itsk.com/thread-35 ...

  6. 50分钟学会Laravel 50个小技巧

    50分钟学会Laravel 50个小技巧 时间 2015-12-09 17:13:45  Yuansir-web菜鸟 原文  http://www.yuansir-web.com/2015/12/09 ...

  7. 10分钟学会Linux

    10分钟学会Linux有点夸张,可是能够让一个新手初步熟悉Linux中最重要最主要的知识,本文翻译的英文网页在众多Linux入门学习的资料中还是很不错的. 英文地址:http://freeengine ...

  8. PHP学习过程_Symfony_(3)_整理_十分钟学会Symfony

    这篇文章主要介绍了Symfony学习十分钟入门教程,详细介绍了Symfony的安装配置,项目初始化,建立Bundle,设计实体,添加约束,增删改查等基本操作技巧,需要的朋友可以参考下 (此文章已被多人 ...

  9. 30分钟学会使用Spring Web Services基础开发

    时隔一年终于又推出了一篇30分钟系列,上一篇<30分钟学会反向Ajax>是2016年7月的事情了.时光荏苒,岁月穿梭.虽然一直还在从事Java方面的开发工作,但是私下其实更喜欢使用C++. ...

随机推荐

  1. django之集成七牛云对象存储

    Python3 + Django2.0 集成 "七牛云" 对象存储 (SDK文档地址:http://developer.qiniu.com/kodo/api/3928/error- ...

  2. nextcloud个人云搭建

    nextcloud个人云搭建 目录 nextcloud个人云搭建 树莓派安装系统 安装OMV5 安装dockcer 挂载硬盘进行映射(使用u盘测试的) 不足 配置数据库 使用docker拉取postg ...

  3. Lesson4——Pandas DataFrame结构

    pandas目录 思维导图 1 简介 DataFrame 是 Pandas 的重要数据结构之一,也是在使用 Pandas 进行数据分析过程中最常用的结构之一. 2 认识DataFrame结构 Data ...

  4. 【第十二期】腾讯后台实习初试、复试、HR面经 (许愿OC)

    楼主投的很晚属于正常批才开始,初试面试官比较重基础,复试面试官比较看综合能力,HR小姐姐声音好听,腾讯面试官都特别nice! 一面: 看你项目很多,你挨个给我介绍一遍吧 我:一大堆按着简历介绍 日志文 ...

  5. Redis 源码简洁剖析 06 - quicklist 和 listpack

    quicklist 为什么要设计 quicklist 特点 数据结构 quicklistCreate quicklistDelIndex quicklistDelEntry quicklistInse ...

  6. 羽夏闲谈—— C 的 scanf 的高级用法

    前言   今天看到博友发了个有关scanf的使用的注意事项,就是讨论缓冲区残存数据的问题,用简单的代码示例复述一下: #define _CRT_SECURE_NO_WARNINGS #include ...

  7. MySQL手写代码相关变量

    原创:转载需注明原创地址 https://www.cnblogs.com/fanerwei222/p/11777682.html 手写一些SQL代码时候需要用到的关键字. DELIMITER, BEG ...

  8. hibernate中的映射文件xxx.hbm.xml详解总结

    转自 http://blog.csdn.net/a9529lty/article/details/6454924 一.hibernate映射文件的作用: Hibernate映射文件是Hibernate ...

  9. MyBatis加强(4)~mybatis 插件开发

    一.插件介绍[动态代理] 1.插件[动态代理]:mybatis 允许在已经映射的语句的执行过程的某个时机进行拦截增强的机制. 2.mybatis中的组件动态代理的运用: MyBatis 在四大组件对象 ...

  10. JDK线程池

    简介 多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力,但频繁的创建线程的开销是很大的,那么如何来减少这部分的开销了,那么就要考虑使用线程 ...