【问题标题】:Using gRPC Web with Dart将 gRPC Web 与 Dart 一起使用
【发布时间】:2022-01-20 21:50:29
【问题描述】:

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

  • UI:Flutter Web/Dart
  • 服务器:开始
  • 通信协议:gRPC/gRPC-Web

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

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

这是我的用户界面代码:

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 Web 代理中,但是 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 Web 代理,但是如果我这样做,我会将 Envoy 上的端点公开为 REST API,然后它将请求作为 gRPC 调用转发.据我了解,这需要维护 2 个版本的数据模型——一个用于 UI 和 Envoy 之间的通信(以 JSON 格式),另一个用于 Envoy 和服务器之间的通信(作为 protobuf)。这是正确的理解吗?我怎样才能超越这个?

*** 编辑:*** 根据 cmets 中的建议,我尝试使用 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 作为你的 grpc 调用的网关代理。您不需要为它创建另一个数据模型。在这里查看我的答案:stackoverflow.com/questions/59558293/…
  • 而且不需要添加http handler,使用grpc server即可。
  • @ישואוהבאותך 感谢您的回复。我一直在考虑您的建议,能够在 docker 容器中本地运行特使,但是我无法将请求转发到我的服务器。我不断收到upstream connect error or disconnect/reset before headers. reset reason: overflow。我已经更新了 OP 以显示 Envoy 配置
  • socket_address: { address: host.docker.internal, port_value: 9001 } 必须是您的 Flutter 客户端从 Web 访问的地址。尝试将其更改为socket_address: { address: 0.0.0.0, port_value: 9000 }。 p.s 我还没有测试容器化部分。
  • 在你的envoy.yamladdress: host.docker.internalport_value: 9000末尾应该是你的grpc服务器地址。

标签: dart grpc envoyproxy grpc-go grpc-web


【解决方案1】:

我能够通过接受 cmets 中的建议并使用 Envoy 代理而不是 go-proxy 来解决该问题,但根据链接的帖子,该解决方案并非完全开箱即用。

这是工作的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 客户端进行 gRPC 调用。

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

【讨论】:

    猜你喜欢
    • 2020-07-12
    • 2021-08-07
    • 2022-08-07
    • 2021-11-13
    • 2019-08-03
    • 1970-01-01
    • 2017-01-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多