造轮子系列之grpc(一)

作为一个程序猿,对造轮子这事情可以说是情有独钟,几乎程序猿内心都存在一个梦想是去将开源的技术都实现一遍,所有从本篇开始,我会开一个造轮子系列。



前言

首先,看看这个,想必大家对下面这种简历看得比较多了吧?

  • 精通JAVA,Python,熟练掌握C++
  • 精通Redis,Memcached,Mysql
  • 精通Nginx配置,模块开发
  • 精通Kafka,ActiveMQ 等消息队列
  • 精通常用数据结构和算法
  • 精通网络编程,多线程编程技术,高性能服务器技术
  • 精通tcp/ip协议栈,熟悉内核网络子系统代码
  • 精通nginx代码及模块开发

上面每一条都涉及好多轮子,每一个都是精通,如果真能做到。那这个人可以说是码农中的战斗机。

那我们现在目标就是去做这个战斗机。而这个方法,就是自己去造轮子,造的目的不是为了在项目中使用自己造的轮子,而是为了去了解轮子的构造,然后自己动手去体会造轮子的过程。


后端的轮子们

说起后端的轮子们,大家都可以说出一大串来,我们大致来数一数啊。

  • 抗在最前面的:LVS,F5,HAProxy这类负载均衡
  • 接下来有Nginx,Apache,Lighttpd这类Http服务
  • http服务后则是各种容器,部署着我们的业务逻辑
  • 存储这边有Redis,Memcached这一类KV存储器和缓存系统
  • 如果是多机部署,肯定还有Kafka,ActiveMQ这种负责解耦的消息队列
  • 为了实现集群通信,肯定少不了Thrift这种RPC框架和Protobuf这种序列化技术
  • 再高端点,到了分布式领域了,就是更多的轮子了。。zookeeper、raft等等
  • 还有大数据系列hadoop。spark。。。。。

本文先开始我们的第一个轮子,服务器通信需要用的数据序列化反序列技术:protobuf。



基础轮子:protobuf

讲基础前,先附上一张极客时间中的一个技术需要从哪些角度来讲的图片,本文也会尽可能从这些个方面来讲。

造轮子系列之grpc(一)

  • 应用角度
  • 问题:”干什么用“
  • 技术规范:”怎么用“
  • 最佳实践:”怎么能用好“
  • 市场应用趋势:“谁用,用在哪”
  • 设计角度
  • 目标:“做到什么”
  • 实现原理:“怎么做到”
  • 优劣局限:“做得怎么样”
  • 演进趋势:“未来如何”

正文

我们先回顾下上一篇内容:protobuf。通过上一篇文章,我们知道了protobuf是一种序列化、反序列化协议,开发之初是为了解决接口的兼容性问题,目前是主要用在内部服务之间RPC调用和传递数据。

那我们现在就来看下,如果要用在rpc调用中传递数据,我们应该怎么做?

首先我们要知道rpc调用是干什么用的?

rpc即远程过程调用,主要是为了让我们能够像调用本地函数一样调用远程服务。

那既然是远程调用,远程服务就有可能跟我们在不同机器上,此时我们通信就需要走网络;或者服务就是本地的进程,我们可以选择unix socket方式,但是不管远程服务在哪,为了能够实现远程调用,我们首先都需要解决通信问题。

现在假设我们使用网络通信,传输层使用tcp协议,我们现在基于tcp来传输数据,我们首先要解决的就是:我们怎么界定数据的开头和结尾(tcp是基于流的协议)。

为了解决界定问题,我们回想下protobuf的编码方式:TLV(Tag,Length,Value),protobuf是能够根据读取到的头和length来知道后续还有多少长度的value,其中 Tag = (field_num << 3) | wire_type。field_num 就是在 proto 文件中,给每个字段指定唯一的数字标识,而 wire_type 用户标识后面的数据类型。

TLV方式只是编码了结构中的单个field,假设我们现在有一个结构:

造轮子系列之grpc(一)

此处我们定义了一个通用的message结构,里面有request结构和response结构,这样子拿到界定好开始和结束边界的数据后,我们就能够进行反序列化了。。但这还是回到了最开始的问题,因为tcp是字节流协议,我们无法界定一个message的开始和结束。

那怎么办呢?我们在序列化的message之后加上一个长度字段,这个时候,我么先读取长度,然后就知道了后续message的长度了,就能够进行反序列化了。

下面我们来实现下我们第一版的rpc通信。

我们先来看发送消息的代码:

造轮子系列之grpc(一)

先写消息头,再写二进制的消息体。

读消息的代码也是这样子,先读消息头,再读消息体。

完整的代码可以看GitHub https://github.com/zhuanxuhit/go-in-practice/tree/master/wheel/rpc/grpc/v1,欢迎各位关注,后续会不断更新轮子系列。

在这次实现中,我们也发现了很多问题,我们对于message到底代表是哪个请求,必须不停通过if else来判断,

造轮子系列之grpc(一)

扩展性非常之差,那有什么好的方法呢?

我们来看下现有的轮子的做法:分析包net/rpc

服务端上关键语句如下

造轮子系列之grpc(一)

rpc.Register(arith) 注册service名和方法

具体里面是将service名和方法注册到了一个map中,

造轮子系列之grpc(一)

然后我们重点看下服务端是如何读取请求,并且找到对应的处理逻辑的,此处我们关注点是:

  1. 如何界定一个消息的开始、结束
  2. 如何能够有扩展性,能够方便的新增方法

Net/rpc的消息头如下:

造轮子系列之grpc(一)

我们通过一个字符串 service.method 来指明请求的具体服务,同时response也是通过service.method来标明响应是哪个请求

造轮子系列之grpc(一)

同时request 和 response 中的 seq来将请求和响应联系起来。

消息头解析完后,net/rpc通过反射能知道具体的请求和返回参数,然后具体消息体body的编码采用的是glob,gob是Golang包自带的一个数据结构序列化的编码/解码工具,就跟我们之前介绍的protobuf功能是一样的。

界定消息开始、结束 可扩展性 我们的rpc 2字节消息头,指定后续protobuf的字节数 通过在request中不断新增XXXrequest net/rpc service.method 字符串来界定消息体字节 只要在服务端注册上 service.method 对应的处理方法

现在我们来实现下我们第二版的rpc通信,目的是可扩展。

先来看proto定义,MessageHeader定义了消息类型和消息体长度

造轮子系列之grpc(一)

服务端在处理数据时,先读取头,然后再读取body,完整的代码可以看 https://github.com/zhuanxuhit/go-in-practice/tree/master/wheel/rpc/grpc/v3

欢迎start,关注。

总结

本文介绍了rpc的基本概念,你应该知道为了实现rpc,我们要解决

  1. 通信问题
  2. 消息体边界界定

接着我们以网络通信为例子,介绍了如果要使用tcp作为传输协议,我们要如何设计消息协议,从而能够在字节流中界定出每个消息体,为此我们设计了两个版本的协议:

  1. 通过在消息头指定消息体长度,从而界定长度
  2. 在消息头中不仅有消息体长度和消息类型,提高可扩展性

后续我们会继续看gRPC的协议,看一个距离一个完整的rpc我们还差哪些,我们怎么能够造出我们自己的轮子来。。。

come on,关注我,大家一起造轮子。。。。。。。


分享到:


相關文章: