从零开始实现一个RPC框架(零)

前言

背景

最近决心开始学习go语言,可是苦于没有实际的应用场景,学习始终停留在hello world层面,看过的教程和资料印象也不深入。因而决定从go自带的rpc实现开始切入,了解一下go语言在实际场景下是如何使用的,包括异常处理、代理和过滤、go routine的用法等等,同时也简单了解了一下其余rpc的go语言实现,好比thrift和grpc等等。一阵蜻蜓点水,稍微加深了印象,也开始慢慢体会到go语言和java语言的种种差别和共性。接下来,为了进一步巩固学习效果,也算是为了对本身目前为止的职业生涯作一次复习和汇报,决定使用go语言从零开始构建一个比较完整的RPC(或者说是微服务)框架。java

微服务框架和RPC框架git

本文中提到RPC框架,指的是提供基础的RPC调用支持的框架;而本文中提到的微服务框架,指的是包含一些服务治理相关的功能(好比服务注册发现、负载均衡、链路追踪等)的RPC框架。github

调研

在动手开始作以前,须要先了解学习一下其余现有的产品,能够从中学习一下优秀的经验和方法,这里列举一下初步了解到的几个框架:apache

  • grpc google推出的微服务框架,支持10种语言,支持基于http2的双向的流式通信
  • go-micro 一个开源的微服务框架,比较独特的是支持Async Messaging,像是mq同样的subpub功能
  • thrift-go thrift是facebook捐献给apache的rpc框架(不包含服务治理相关的功能),根据官方文档,thrift支持20种语言的RPC调用
  • rpcx rpcx是一个国人开发并开源的微服务框架,宣传的特性是“快、易用却功能强大”,官网上的介绍提到性能是grpc的两倍。这里附上做者(应该是)的博客

以上就是目前了解过的几个已有的框架,比较惭愧的是了解得都不够深刻,后续还要持续学习。设计模式

Pluggable Interfacesbash

值得一提的是除了thrift,其余三个称得上微服务框架的产品,其特性都包含Pluggable Interfaces,也就是能够经过插件替换部分功能。经过插件实现可替换的功能,实际上在一个微服务框架中基本是最低要求了,不然后续的功能扩展将会变得十分困难,相信我,这里是饱含血泪的经验之谈。网络

需求分析

在开始着手设计甚至是编写代码之前,咱们首先分析一下咱们的需求(来自学习软件工程中的成果)。同时对于一部分可能不太熟悉RPC相关细节的同窗来讲,对咱们后面要作的事情心中也可以有一个大体的概念。这里就直接列举几个功能性需求:负载均衡

  • 支持RPC调用,包括同步调用和异步调用
  • 支持服务治理的相关功能,包括:
    • 服务注册与发现
    • 服务负载均衡
    • 限流和熔断
    • 身份认证
    • 监控和链路追踪
    • 健康检查,包括端到端的心跳以及注册中心对服务实例的检查
  • 支持插件,对于有多种实现的功能(好比负载均衡),须要以插件的形式提供实现,同时须要支持自定义插件 至于非功能性需求好比性能要好,要够稳定这类的暂时不重点关注。

系统设计

分层

有了大体的需求,接下来就能够开始着手设计了。首先咱们将框架划分为若干层,层与层之间约定经过接口交互。这里就不要问为何须要分层了,非要问就是经验。分层做为一种经典到不能在经典的设计模式,几乎在软件开发过程当中无处不在,在RPC框架当中也十分适用,下面画出大体的层次图:框架

  • service 是面向用户的接口,好比客户端和服务端实例的初始化和运行等等
  • client和server表示客户端和服务端的实例,它们负责发出请求和返回响应
  • selector 表示负载均衡,或者叫作loadbanlancer,它负责决定具体要向哪一个server发出请求
  • registery 表示注册中心,server在初始化完毕甚至是运行时都要向注册中心注册自身的相关信息,这样client才能从注册中心查找到须要的server
  • codec 表示编解码,也就是将对象和二进制数据互相转换
  • protocol 表示通讯协议,也就是二进制数据是如何组成的,RPC框架中不少功能都须要协议层的支持
  • transport 表示通信,它负责具体的网络通信,将按照protocol组装好的二进制数据经过网络发送出去,并根据protocol指定的方式从网络读取数据

上面提到的各个层,除了service,实际上能够提供多种实现,因此应该都以plugin的方式实现。异步

这样一来按照咱们划分的层次,一个客户端从发出请求到收到响应的流程大概就是这样:

服务端的逻辑比较相似,这里就不画图了。

过滤器链

经过上面的层次划分能够看到,一个请求或者响应实际上会依次穿过各个层而后经过网络发送或者到达用户逻辑,因此咱们采用相似过滤器链同样的方式处理请求和响应,以此来达到对扩展开放,对修改关闭的效果。这样一来对于一些附加功能好比熔断降级和限流、身份认证等功能均可以在过滤器中实现。

消息协议

接下来设计具体的消息协议,所谓消息协议大概就是两台计算机为了互相通讯而作的约定。举个例子,TCP协议约定了一个TCP数据包的具体格式,好比前2个byte表示源端口,第3和第4个byte表示目标端口,接下来是序号和确认序号等等。而在咱们的RPC框架中,也须要定义本身的协议。通常来讲,网络协议都分为head和body部分,head是一些元数据,是协议自身须要的数据,body则是上一层传递来的数据,只须要原封不动的接着传递下去就是了。

接下来咱们就试着定义本身的协议:

-------------------------------------------------------------------------------------------------
|2byte|1byte  |4byte       |4byte        | header length |(total length - header length - 4byte)|
-------------------------------------------------------------------------------------------------
|magic|version|total length|header length|     header    |                    body              |
-------------------------------------------------------------------------------------------------
复制代码

根据上面的协议,一个消息体由如下几个部分严格按照顺序组成:

  • 两个byte的magic number开头,这样一来咱们就能够快速的识别出非法的请求
  • 一个byte表示协议的版本,目前能够一概设置为0
  • 4个byte表示消息体剩余部分的总长度(total length)
  • 4个byte表示消息头的长度(header length)
  • 消息头(header),其长度根据前面解析出的长度(header length)决定
  • 消息体(body),其长度为前面解析出的总长度减去消息头所占的长度(total length - 4 - header length)

协议中消息头的数据主要是RPC调用过程当中的元数据,元数据跟方法参数和响应无关,主要记录额外的信息以及实现附属功能好比链路追踪、身份认证等等;消息体的数据则是由实际的请求参数或者响应编码而来。 在实际的处理中,消息头在发送端一般是一个结构体,在发送时会被编码成二进制添加在消息头的前面,在接收端接收时又解码成一个结构体,交给程序进行处理。这里试着列举消息头包含的各个信息:

type Header struct {
        Seq uint64 //序号, 用来惟一标识请求或响应
        MessageType byte //消息类型,用来标识一个消息是请求仍是响应
        CompressType byte //压缩类型,用来标识一个消息的压缩方式
        SerializeType byte //序列化类型,用来标识消息体采用的编码方式
        StatusCode byte //状态类型,用来标识一个请求是正常仍是异常
        ServiceName string //服务名
        MethodName string  //方法名
        Error string //方法调用发生的异常
        MetaData map[string]string //其余元数据
}

复制代码

结语

第一篇文章就到此为止了,主要先作一下准备,整理一下思路,若是有不正确或者不合理的部分还请你们多多指教。

相关文章
相关标签/搜索