该文章内容发布已经超过一年,请注意检查文章中内容是否过时。

dubbo-go 中 REST 协议实现

在社区小伙伴们的努力下,Apache/dubbo-go 在 v1.4.0 中支持 REST 协议了。

什么是 REST 协议

REST 是 REpresentational State Transfer(表述性状态转移)的简写,是一种软件架构风格。虽然 REST 架构风格不是一定要基于 HTTP 协议进行传输,但是因为 HTTP 协议的通用性和易用性,现在越来越多的 web 服务采用基于 HTTP 协议实现 RESTful 架构。

在 dubbo-go 中的 REST 协议指的是一种基于 HTTP 协议的远程调用方式。简单的来讲,REST 协议就是把dubbo 服务发布成 RESTful 风格的 HTTP 接口并且能够能像调用 dubbo 接口一样的方式调用 HTTP 接口。

为什么要支持 REST 协议

在没有 REST 协议之前,小伙伴们是否一直在苦恼这样几个问题:

  1. 传统的 web 服务不能直接调用我们发布的 dubbo 服务
  2. 前端不能直接调用 dubbo 服务
  3. dubbo 服务不能发布 Open API

上述问题,就是 REST 协议解决的核心问题。现在我们很多应用场景都是需要与异构的系统进行交互,而 REST 采用的 HTTP 通信协议非常适合用来打通异构系统,如图:

img

REST 协议没那么简单

REST 协议核心要解决一个问题:Go 方法到 HTTP 接口的双向映射。普通 HTTP 调用 dubbo-go 服务,考虑的是HTTP 到 Go 方法的映射;而 dubbo-go 服务调用 HTTP 服务,则是 ** Go** 方法到 HTTP 接口的映射。

下面是我们要与 Go 方法要做映射的 HTTP 请求协议内容:

POST /path/{pathParam}?queryParam=1 HTTP/1.1
Accept: application/json
Content-Type: application/json
Host: http://localhost:8080
{"id":1111}

在服务提供方,当上面这种请求发送到服务器时,我们要把它路由到下面这个 Go 方法中,在服务消费方,我们也可以通过调用下面的 Go 方法把方法参数转化为上面的HTTP请求:

type Provider struct {
    // 该方法应该对应上面的http请求
    GetResult func(pathParam string, queryParam string, body interface{}, host string) (*Result, error)
}

在消费方调用 GetResult 时,传入各个参数的值:

  • 变量 pathParam 的内容应该是字符串 “pathParam”;
  • 变量 queryParam 的内容应该是字符串 “1” ;
  • 变量 body 应该是有以字符串 “id” 为 key ,1111 为 value 的一个map;
  • 当然 host 变量的内容应该是字符串 “http://localhost:8080” 。

在服务端执行 GetResult 方法时,得到的参数会与消费方调用时传入的值相同。

总结下来,我们要建立以下这些映射关系

  1. 路径映射
  2. Header 处理(固定 Header 和 Header 值也是参数两种情况)
  3. POST or GET or …(HTTP 方法映射)
  4. 参数映射

要完成这种映射,我们首先要解决的是,如何知道这种映射关系?

答案只有一个,通过用户配置。而用户配置所面临的困难是,复杂且琐碎。(解决思路是提供大量默认配置减轻配置的负担,自定义配置方式允许用户使用自己熟悉的配置形式)

另外一个难点在于,使用何种 web 框架的问题。有些公司内部使用的是自研的 web 框架,他们有成熟的技术基础和运维能力。于是就会考虑说,能不能让 dubbo-go 在支持 REST 协议的时候,能够让他们将 REST 协议使用的web 框架替换成他们所期望的呢?

如何建立HTTP接口与方法的映射关系

下面我举一个HTTP接口与方法映射的具体例子:

Go 结构体定义如下:

type UserProvider struct {
    GetUser func(id string, name string, age int) (*User, error)
}

要发布的HTTP接口形式是: http://127.0.0.1/UserProvider/GetUser/{id}?name=test&age=1

服务端配置如下:

services:
  "UserProvider":
    //注册中心
    registry: "zookeeper"
    //启用REST协议
    protocol: "rest"
    //DUBBO的接口名
    interface: "com.ikurento.user.UserProvider"
    // 服务接口路径
    rest_path: "/UserProvider"
    methods:
      - name: "GetUser"
        // 方法接口路径
        rest_path: "/GetUser/{id}"
        // HTTP方法
        rest_method: "GET"
        // HTTP查询参数
        rest_query_params: "1:name,2:age"
        // HTTP路径参数
        rest_path_params: "0:id"
        // 可以提供的内容类型
        rest_produces: "application/json;application/xml"
        // 可以接受的客户端参数类型
        rest_consumes: "application/json;charset=utf-8,*/*"
        // HTTP Body
        rest_body: -1

在配置文件中我们定义了方法的路径,HTTP方法等接口参数,这里需要注意的是路径参数和查询参数的配置方式,0:name 的意思是查询参数 name 对应 GetUser 方法的第一个参数,还有 rest_body 配置的数字也是对应这方法的参数,这里没有 body 参数所有就配置了 -1

REST协议的调用过程

img

上图展示了用户在 Consumer 端调用 GetUser 方法到 Provdier 端 GetUser 方法被执行的整个过程,在 RestClientRestServer 中分别**实现了 Go 方法参数到 HTTP 请求的转换和 HTTP 请求到 Go 方法的转换,这是最为核心和复杂的部分。**换言之,我们在这里实现了前面提到的 Go 方法和 HTTP 请求的双向映射。

这里我们可以注意到 RestClientRestServer 是可以用户自行扩展的,下面我将具体介绍一下在REST协议中有哪些扩展点设计。

REST协议的扩展点设计

基于 dubbo-go 良好的 extension 扩展设计,我们定义了多个扩展点,用户可以自定义功能实现。

自定义HTTP服务器

RestServer的扩展接口:

type RestServer interface {
    
    // sever启动函数
    Start(url common.URL)
    
    // 发布接口
    Deploy(restMethodConfig *rest_config.RestMethodConfig, routeFunc func(request RestServerRequest, response RestServerResponse))
    
    // 删除接口
    UnDeploy(restMethodConfig *rest_config.RestMethodConfig)
    
    // server关闭
    Destroy()
}

在 dubbo-go 的 v1.4.0 中默认使用 go-restful 作为 HTTP 服务器,如果用户想用其他 HTTP 容器可以实现上面的接口,并在配置文件中配置使用自己自定义的服务器。

这个接口中,最核心的方法是 Deploy,在 restMethodConfig 方法参数中有用户配置的接口路径等一系列参数,routeFunc 是 HTTP 接口需要被路由执行的函数。不同的http服务器会有不同的 request 和 response ,所以我们定义了 RestServerRequest 接口和 RestServerResponse 接口让用户进行适配。

自定义HTTP客户端

RestClient 的扩展接口:

// RestOptions
type RestOptions struct {
    RequestTimeout time.Duration
    ConnectTimeout time.Duration
}
// RestClientRequest
type RestClientRequest struct {
    Header      http.Header
    Location    string
    Path        string
    Method      string
    PathParams  map[string]string
    QueryParams map[string]string
    Body        interface{}
}
// RestClient user can implement this client interface to send request
type RestClient interface {
    Do(request *RestClientRequest, res interface{}) error
}

最后的请求到客户端时,都会被封装为 RestRequest,用户可以非常简单快速的扩展自己的 Client 端。RestOptions 中有一些客户端的超时配置,在创建自己的客户端时需要根据这些配置初始化客户端。

自定义 REST 配置形式

前面提到,REST 协议一个很麻烦的地方在于,配置很繁琐很琐碎。Go 不同于 Java,可以通过注解的形式来简化配置。

所以我们考虑到用户不同的使用习惯和公司的配置风格,提供了这个扩展点。

ConfigReader 的扩展接口:

type ConfigReader interface {
   
    // Consumer配置读取
    ReadConsumerConfig(reader *bytes.Buffer) error
    
    // Provider配置读取
    ReadProviderConfig(reader *bytes.Buffer) error
}

ReadConsumerConfigReadProviderConfig 方法的参数是配置文件的文件流,在实现方法中可以再次解析,也可以使用二次编译或者硬编码方式等其他方式读取配置。这是一个通用的配置读取接口,以后可以用来扩展 REST 配置之外的其他配置,所以需要在方法中调用方法设置配置,如下:

// 设置Rest的消费者配置
config.SetRestConsumerServiceConfigMap(restConsumerServiceConfigMap)
// 设置Rest的提供者配置
config.SetRestProviderServiceConfigMap(restProviderServiceConfigMap)

如何添加 HTTP 过滤器

因为不同 HTTP 服务器的过滤器,拦截器或者是 middleware 添加方式都不同,所以我们很难定义一个接口满足所有服务器。因此我们单独为 go-restful 定义了一个添加 filter 的方法,这里我们需要注意的一点是必须在 REST 接口发布前添加filter。

server_impl.AddGoRestfulServerFilter(func(request *restful.Request, response *restful.Response, chain *restful.FilterChain) {
    // 鉴权等功能
    chain.ProcessFilter(request, response)
})
// 启动dubbo服务,发布rest等接口
config.Load()

展望

以上是关于 REST 协议的一些介绍,具体的实现我就不详细讲了,大家可以去参阅源码。

如果想看具体的Example,请参考:

https://github.com/dubbogo/dubbo-samples/tree/master/golang/general/rest

REST 未来需要支持 HTTPS 协议和基于 open tracing 标准 api 的链路追踪。REST 的配置信息未来也不是 REST协议独有的,这些配置信息未来可以作为每个 dubbo 接口的元数据,存储到元数据中心,为网关提供 HTTP 协议与 dubbo 协议之间的映射关系。

作者:蒋超,github id Patrick0308,在 杭州贝安云科技有限公司 任职服务开发工程师。

最后修改 February 23, 2023: Update for seo / img alt (35090e3f9b4)