使用Apinto实现HTTP与gRPC的协议转换

发布时间 2023-03-27 16:06:11作者: Ken-L

gRPC背景介绍

gRPC是由 google开发的一个高性能、通用的开源RPC框架,主要面向移动应用开发且基于HTTP/2协议标准而设计,同时支持大多数流行的编程语言。

gRPC基于 HTTP/2协议传输,而HTTP/2相比HTTP1.x,有以下一些优势:

  • 采用二进制格式传输协议,支持多路复用。

  • 支持通过同一个连接发送多个并发的请求,支持流式传输。

  • 服务器可以对客户端的一个请求发送多个响应。

  • 对消息头进行了压缩传输,能够节省消息头占用的网络流量。gRPC使用Protocol Buffers作为序列化协议。

但同时,gRPC也有自身的局限性:

  • 浏览器支持有限:当下,不可能直接从浏览器调用gRPC服务。gRPC大量使用HTTP/2功能,没有浏览器提供支持gRPC客户机的Web请求所需的控制级别。例如,浏览器不允许调用者要求使用的HTTP/2,或者提供对底层HTTP/2框架的访问。

  • 不是人类可读的:HTTP API请求以文本形式发送,可以由人读取和创建。默认情况下,gRPC消息使用protobuf编码。虽然protobuf的发送和接收效率很高,但它的二进制格式是不可读的。protobuf需要在*.proto文件中指定的消息接口描述才能正确反序列化。需要额外的工具来分析线路上的Protobuf有效负载,并手工编写请求。

若需要将内部gRPC作为接口开放给外部用户或浏览器调用,则需要有第三方代理解决HTTP协议转换成gRPC协议的问题。

为此,我们在Apinto多协议支持的基础上,发布了 HTTP转gRPC插件(eolinker.com:apinto:http_to_grpc

Apinto HTTP转gRPC插件

Apinto 通过插件的方式支持 HTTP协议转换成gRPC协议请求,在http_to_grpc 插件中完成了 HTTP 客户端 与 gRPC Server 通讯时的协议转换及数据编解码工作,其通讯的过程如下:

img_v2_1fdc4822-2303-45f0-9360-2285dc4e233g.jpg

接下来通过一个完整的示例向大家演示怎样构建一个 HTTP 请求,并通过 Apinto 进行 HTTP协议 转换成 gRPC 协议请求。在以下的示例中,我们会将 Go 作为 gRPC Server 服务端处理程序,使用 Eolink 作为 HTTP 客户端,发起 HTTP 请求。

以下示例可以在Apinto仓库中获取。

配置 Protocol Buffer

1、创建示例文件msg.proto

syntax = "proto3";

option go_package = "github.com/eolinker/apinto/example/grpc/demo_service";

package Service;

// HelloRequest 请求
message HelloRequest {
  string name = 1;
}

// HelloResponse 响应,成功消息时,msg为json数据
message HelloResponse {
  string msg = 1;
  string err = 2;
}

该文件定义了示例消息类型,我们在这里定义了一个HelloRequestHelloResponse的message。

2、创建示例文件service.proto

syntax = "proto3";

option go_package = "github.com/eolinker/apinto/example/grpc/demo_service";
import "msg.proto";

package Service;

service Hello {
  rpc Hello(HelloRequest) returns (HelloResponse){};
  rpc StreamRequest(stream HelloRequest) returns (HelloResponse){};
  rpc StreamResponse(HelloRequest) returns (stream HelloResponse) {};
  rpc AllStream(stream HelloRequest)returns (stream HelloResponse) {};
}

该文件定义了服务Hello,引入了第一步创建的文件msg.proto,定义了四个方法,包含了一元 RPC客户端流服务端流双向流四种gRPC通信模式。

配置服务端程序

1、创建自动生成gRPC文件脚本(grpc.sh)

#!/bin/bash

OUT_DIR=demo_service

set -e

mkdir -p $OUT_DIR

protoc --grpc-gateway_opt logtostderr=true \
       --grpc-gateway_opt paths=source_relative \
       --grpc-gateway_opt generate_unbound_methods=true \
       --grpc-gateway_out=$OUT_DIR \
       --go_out=$OUT_DIR --go_opt=paths=source_relative     \
       --go-grpc_out=$OUT_DIR --go-grpc_opt=paths=source_relative *.proto

该脚本将生成gRPC客户端/服务端调用相关代码,并将其存储到demo_service目录下。

执行grpc.sh,生成服务端 Go 原始消息和服务/客户端存根。

./grpc.sh

2、实现服务端处理程序接口

package main

import (
 "fmt"

 service "github.com/eolinker/apinto/example/grpc/demo_service"

 "google.golang.org/grpc/metadata"

 "google.golang.org/grpc"

 "golang.org/x/net/context"
)

var _ service.HelloServer = (*Server)(nil)

type Server struct {
 service.UnimplementedHelloServer
}

func NewServer() *Server {
 return &Server{}
}

func (s *Server) Hello(ctx context.Context, request *service.HelloRequest) (*service.HelloResponse, error) {

 trailingMD, ok := metadata.FromIncomingContext(ctx)
 if ok {
  grpc.SetTrailer(ctx, trailingMD)
 }
 return &service.HelloResponse{
  Msg: fmt.Sprintf("hello,%s", request.Name),
 }, nil
}

上述代码重新定义了Hello方法:

  • HelloRequest中的name字段通过HelloResponsemsg字段封装成hello,%s的结果返回

  • 将请求的Header作为gRPC响应的Trailer头部返回

3、定义gRPC服务端入口文件

package main

import (
  "fmt"
  "net"

  "google.golang.org/grpc/reflection"

  service "github.com/eolinker/apinto/example/grpc/demo_service"

  "google.golang.org/grpc/credentials"

  "google.golang.org/grpc"
  "google.golang.org/grpc/grpclog"
)

func main() {
  Parse()
  err := listen()
  if err != nil {
    fmt.Println(err)
    return
  }
}

func listen() error {
  address := fmt.Sprintf("%s:%d", BindIP, ListenPort)
  l, err := net.Listen("tcp", address)
  if err != nil {
    grpclog.Fatalf("Failed to listen: %v", err)
  }

  var opts []grpc.ServerOption
  if TlsKey != "" && TlsPem != "" {
    creds, err := credentials.NewServerTLSFromFile(TlsPem, TlsKey)
    if err != nil {
      grpclog.Fatalf("Failed to generate credentials %v", err)
    }
    opts = append(opts, grpc.Creds(creds))
  }

  // 实例化grpc Server
  s := grpc.NewServer(opts...)
  ser := NewServer()
  // 注册HelloService
  service.RegisterHelloServer(s, ser)
  // 开启grpc反射
  reflection.Register(s)
  fmt.Println("Listen on " + address)

  return s.Serve(l)
}

在此处,gRPC服务端开启了gRPC反射,配置Apinto网关时,可选择绑定具体的Protobuf资源,也可以直接启用反射,动态获取gRPC服务端的 Protobuf 信息。

4、编译gRPC服务器程序

go build -o grpcServer server/.

配置Apinto

Apinto提供了可视化界面工具Apinto-Dashboard,降低初学者的使用成本,以下操作均在Apinto-Dashboard中进行配置。

1、在全局插件中新建 http_to_grpc 插件

image.png

2、创建gRPC服务

image.png

在这里,我们配置gRPC服务的相关信息,我们可以配置多个静态负载地址,这里我们填写了 127.0.0.1:9001

3、创建http路由,绑定grpc_demo上游服务

image.png

4、在路由中绑定 http_to_grpc 插件

由于gRPC服务端示例中,我们开启了gRPC反射,因此,在配置插件时,开启反射按钮即可

image.png

  • 当服务名称不填时,则默认使用 HTTP请求路径的第一个/和第二个/之间的值作为服务名

  • 当方法名称不填时,则默认使用 HTTP请求路径的第二个/和第三个/之间的值作为服务名

  • 即,若HTTP请求路径上/Service.Hello/Hello,则此时服务名称为Service.Hello,方法名称为Hello

关于Protobuf编码器

若gRPC未开启反射,我们需要先新建一个Protobuf编码器,绑定 http_to_grpc 插件时,绑定对应的编码器ID即可,详细步骤如下:

1、创建Protobuf编码器

image.png

2、在路由中绑定 http_to_grpc 插件

image.png

填写完后提交即可。

验证协议转换请求

1、启动gRPC服务器

./grpcServer

2、请求Service.Hello服务的Hello方法

在上文中,我们定义了Hello方法的功能:

  • HelloRequest中的name字段通过HelloResponsemsg字段封装成hello,%s的结果返回

  • 将请求的Header作为gRPC响应的Trailer头部返回

调用结果如下:

image.png

image.png

image.png