将 gRPC Web 与 Dart 结合使用

Using gRPC Web with Dart

我有一个具有以下堆栈的 Web 应用程序:

我已经定义了一些protobufs并成功地将它们编译到Go和Dart中。当我 运行 Go 服务器代码时,我能够成功地使用 Kreya 进行 gRPC 调用,但是当我尝试使用 grpc/grpc_web.dart 从 Flutter 进行相同的调用时,尽管我保持 运行ning以下错误:

gRPC Error (code: 2, codeName: UNKNOWN, message: HTTP request completed without a status
(potential CORS issue), details: null, rawResponse: , trailers: {})

这是我的 UI 代码:

class FiltersService {
  static ResponseFuture<Filters> getFilters() {

    GrpcWebClientChannel channel =
        GrpcWebClientChannel.xhr(Uri.parse('http://localhost:9000'));

    FiltersServiceClient clientStub = FiltersServiceClient(
      channel,
    );

    return clientStub.getFilters(Void());
  }
}

后端代码:

func StartServer() {
    log.Println("Starting server")
    listener, err := net.Listen("tcp", fmt.Sprintf(":%v", port))
    if err != nil {
        log.Fatalf("Unable to listen to port %v\n%v\n", port, err)
    }

    repositories.ConnectToMongoDB()

    grpcServer = grpc.NewServer()

    registerServices()

    if err = grpcServer.Serve(listener); err != nil {
        log.Fatalf("Failed to serve gRPC\n%v\n", err)
    }
}

// Register services defined in protobufs to call from UI
func registerServices() {
    cardsService := &services.CardsService{}
    protos.RegisterCardsServiceServer(grpcServer, cardsService)

    filtersService := &services.FiltersService{}
    protos.RegisterFiltersServiceServer(grpcServer, filtersService)
}

如前所述,使用 Kreya 调用时 API 调用成功,但 Dart 代码一直失败。

我尝试将 gRPC 服务器包装在 gRPC 网络代理中,但是 Dart 和 Kreya 也都失败了。这是我试过的代码:

func StartProxy() {
    log.Println("Starting server")
    listener, err := net.Listen("tcp", fmt.Sprintf(":%v", port))
    if err != nil {
        log.Fatalf("Unable to listen to port %v\n%v\n", port, err)
    }

    repositories.ConnectToMongoDB()

    grpcServer = grpc.NewServer()
    registerServices()
    grpcWebServer := grpcweb.WrapServer(grpcServer)

    httpServer := &http.Server{
        Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if r.ProtoMajor == 2 {
                grpcWebServer.ServeHTTP(w, r)
            } else {
                w.Header().Set("Access-Control-Allow-Origin", "*")
                w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
                w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, X-User-Agent, X-Grpc-Web")
                w.Header().Set("grpc-status", "")
                w.Header().Set("grpc-message", "")
                if grpcWebServer.IsGrpcWebRequest(r) {
                    grpcWebServer.ServeHTTP(w, r)
                }
            }
        }),
    }

    httpServer.Serve(listener)

}

func StartServer() {
    StartProxy()
}

我也知道 Envoy 代理可以用来代替这个 gRPC 网络代理,但是如果我这样做,我会将 Envoy 上的端点公开为 REST APIs,然后将请求作为 gRPC 调用转发。据我了解,这需要维护 2 个版本的数据模型——一个用于 UI 和 Envoy 之间的通信(在 JSON 中),另一个用于 Envoy 和服务器之间的通信(如 protobuf ).这是正确的理解吗?我怎样才能克服这个问题?

*** 编辑:*** 根据评论中的建议,我尝试使用 Envoy 代替 go 代理。但是,即使是现在,我也无法让它正常工作。我现在在尝试访问 Envoy 9001 公开的端口时得到 upstream connect error or disconnect/reset before headers. reset reason: overflow,尽管我可以直接从端口 9000.

上的 kreya 成功调用后端服务

这是我的 envoy.yaml:

static_resources:
  listeners:
    - name: listener_0
      address:
        socket_address: { address: host.docker.internal, port_value: 9001 }
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                codec_type: auto
                stat_prefix: ingress_http
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains: ["*"]
                      routes:
                        - match: { prefix: "/" }
                          route:
                            cluster: greeter_service
                            max_stream_duration:
                              grpc_timeout_header_max: 0s
                      cors:
                        allow_origin_string_match:
                          - prefix: "*"
                        allow_methods: GET, PUT, DELETE, POST, OPTIONS
                        allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
                        max_age: "1728000"
                        expose_headers: id,token,grpc-status,grpc-message
                http_filters:
                  - name: envoy.filters.http.grpc_web
                  - name: envoy.filters.http.cors
                  - name: envoy.filters.http.router
  clusters:
    - name: greeter_service
      connect_timeout: 0.25s
      type: logical_dns
      http2_protocol_options: {}
      lb_policy: round_robin
      # win/mac hosts: Use address: host.docker.internal instead of address: localhost in the line below
      load_assignment:
        cluster_name: cluster_0
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: host.docker.internal
                      port_value: 9000

我能够通过采纳评论中的建议并使用 Envoy 代理而不是 go-proxy 来解决这个问题,尽管根据链接[=44,该解决方案并不是完全开箱即用的=].

这是工作envoy.yaml

admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 9901
static_resources:
  listeners:
    - name: listener_0
      address:
        socket_address: { address: 0.0.0.0, port_value: 9000 }
      filter_chains:
        - filters:
            - name: envoy.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                codec_type: auto
                stat_prefix: ingress_http
                route_config:
                  name: local_route
                  virtual_hosts:
                    - name: local_service
                      domains: ["*"]
                      routes:
                        - match: { prefix: "/" }
                          route:
                            cluster: greeter_service
                            max_grpc_timeout: 0s
                      cors:
                        allow_origin_string_match:
                          - prefix: "*"
                        allow_methods: GET, PUT, DELETE, POST, OPTIONS
                        allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
                        max_age: "1728000"
                        expose_headers: custom-header-1,grpc-status,grpc-message
                http_filters:
                  - name: envoy.filters.http.grpc_web
                  - name: envoy.filters.http.cors
                  - name: envoy.filters.http.router
  clusters:
    - name: greeter_service
      connect_timeout: 0.25s
      type: logical_dns
      lb_policy: round_robin
      http2_protocol_options: {}
      load_assignment:
        cluster_name: cluster_0
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: host.docker.internal
                      port_value: 9001

正在工作Dockerfile

FROM envoyproxy/envoy:v1.20-latest
COPY ./envoy.yaml /etc/envoy/envoy.yaml
CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml -l debug

以及用于 运行 Envoy

的命令

docker build -t envoy .

前一个命令必须 运行 与 Dockerfile

在同一目录中

docker run -p 9000:9000 -p 9901:9901 envoy

其中 90009901 是我要公开并能够从外部访问的端口(在 envoy.yaml 中列出)

**注意:** 确保包含 http2_protocol_options: {}。根据网上的一些可能的解决方案,我将其删除,导致connection reset due to protocol error。我在这个问题上停留了几个小时,直到我看到 Envoy 可以将请求转发为 HTTP/2HTTP/3,并决定将其添加回来,这最终让我可以使用gRPC Web 客户端。

希望这对可能遇到此问题的其他人有所帮助。