解决WebSocket关闭时返回码不一致的问题

当使用Firefox和Safari时,关闭WebSocket可以正常工作,但Istio和Chrome的组合会导致WebSocket关闭时返回"wasclean": false和返回码1006。本文介绍如何通过EnvoyFilter解决Websocket关闭时返回码不一致的问题。

前提条件

步骤一:部署示例应用

您可以使用本示例镜像或者自行构建Docker镜像部署应用,具体操作如下:

方式一:使用阿里云镜像部署应用

  1. 使用以下内容,创建sample.yaml文件。

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: websocket-test
      namespace: default
      labels:
        app: websocket-test
        version: current
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: websocket-test
          version: current
      template:
        metadata:
          labels:
            app: websocket-test
            version: current
        spec:
          containers:
          - name: websocket-test
            image: registry.cn-hangzhou.aliyuncs.com/aliacs-app-catalog/asm-websocketsample:v1
            imagePullPolicy: Always
            command: ["node", "ws.js"]
    
    
    ---
    apiVersion: v1
    kind: Service
    metadata:
      labels:
        app: websocket-test
      name: websocket-test
      namespace: default
    spec:
      ports:
      - name: http
        port: 80
        protocol: TCP
        targetPort: 9900
      selector:
        app: websocket-test
      type: ClusterIP
  2. 执行以下命令,在default命名空间下部署WebSocket服务端示例应用。

    命名空间default需启用Sidecar自动注入,具体操作,请参见启用自动注入

    kubectl apply -f sample.yaml

方式二:自行构建Docker镜像部署应用

  1. 此处以Node应用为例,package.json文件示例如下。

    {
      "name": "wssample",
      "version": "0.0.1",
      "main": "ws.js",
      "license": "UNLICENSED",
      "scripts": {
        "start": "node --trace-warnings ./ws.js"
      },
      "dependencies": {
        "ws": "^8.0.0"
      }
    }
  2. 执行以下代码,部署WebSocket服务端示例应用。

    const WebSocket = require("ws");
    const http = require("http");
    const wss = new WebSocket.Server({ noServer: true });
    
    const server = http.createServer()
    
    
    server.on("upgrade", async (request, socket, head) => {
      const handleAuth = (ws) => {
        wss.emit("connection", ws, request);
      };
      wss.handleUpgrade(request, socket, head, handleAuth);
    })
    
    wss.on("connection", (conn, req) => {
      // 默认应返回1005,但是有Sidecar的情况下,客户端收到的是1006。
      // conn.close()
      // 自定义的返回码4321。
      // 但是有Sidecar的情况下, 客户端收到的仍然是1006。
      // 当没有Sidecar 时, 客户端收到4321。
      conn.close(4321, "test")
    
    });
    
    server.listen({ host: '0.0.0.0', port: 9900 });
    
                            
  3. 使用以下内容,构建镜像的Dockerfile。

    FROM node:16.7.0-alpine3.14
    WORKDIR /root/app
    COPY . .
    RUN yarn install

步骤二:配置网格规则

  1. 登录ASM控制台

  2. 在左侧导航栏,选择服务网格 > 网格管理

  3. 网格管理页面,找到待配置的实例,单击实例的名称或在操作列中单击管理

  4. 创建网关规则。

    1. 在网格详情页面左侧导航栏,选择ASM网关 > 网关规则,然后在右侧页面,单击使用YAML创建

    2. 设置命名空间default,选择任意场景模板,将以下内容粘贴到YAML文本框,然后单击创建

      apiVersion: networking.istio.io/v1beta1
      kind: Gateway
      metadata:
        name: websocket-test
        namespace: default
      spec:
        selector:
          istio: ingressgateway
        servers:
          - hosts:
              - '*'
            port:
              name: http
              number: 80
              protocol: HTTP
  5. 创建目标规则。

    1. 在网格详情页面左侧导航栏,选择流量管理中心 > 目标规则,然后在右侧页面,单击使用YAML创建

    2. 设置命名空间default,选择任意场景模板,将以下内容粘贴到YAML文本框,然后单击创建

      apiVersion: networking.istio.io/v1alpha3
      kind: DestinationRule
      metadata:
        name: websocket-test
        namespace: default
      spec:
        host: websocket-test
        subsets:
        - name: current
          labels:
            version: current
  6. 创建虚拟服务。

    1. 在网格详情页面左侧导航栏,选择流量管理中心 > 虚拟服务,然后在右侧页面,单击使用YAML创建

    2. 设置命名空间default,选择任意场景模板,将以下内容粘贴到YAML文本框,然后单击创建

      apiVersion: networking.istio.io/v1alpha3
      kind: VirtualService
      metadata:
        name: websocket-test
        namespace: default
      spec:
        gateways:
        - websocket-test
        hosts:
        - '*'
        http:
        - name: default
          route:
          - destination:
              host: websocket-test
              subset: current

步骤三:访问WebSocket

本文以直接访问WebSocket和使用EnvoyFilter访问WebSocket两种方式进行对比,通过返回码(本文以自定义返回码4321为例)验证WebSocket的访问效果。

方式一:直接访问WebSocket

  1. 使用以下内容,创建client.html文件。

    请将YAML中入口网关地址修改为实际的入口网关IP。

    <!DOCTYPE html>
    <html>
    
    <head>
        <title>WebSocket example</title>
    
    </head>
    
    <body>
    
        <script>
            var ws = new WebSocket('ws://{替换为实际的入口网关IP地址}');
    
            ws.onopen = function (ev) {
                console.log(ev)
            };
            ws.onmessage = function (ev) {
                console.log("on msg", ev)
            };
            ws.onclose = function (ev) {
                console.log("on close", ev)
            };
            ws.onerror = function (ev) {
                console.log("on error", ev)
            };
    
        </script>
    </body>
    
    </html>
  2. 使用Chrome浏览器打开client.html文件,在键盘上按F12键,开启Developer Tools。

  3. 刷新页面,在Console页签下,查看返回码。

    如下图所示,返回码始终为1006,而不是自定义的返回码4321。1006

方式二:通过EnvoyFilter访问WebSocket

  1. 使用以下内容,创建EnvoyFilter。

    apiVersion: networking.istio.io/v1alpha3
    kind: EnvoyFilter
    metadata:
      labels:
        asm-system: 'true'
        provider: asm
      name: hack-to-fix-delayedclosetimeout-istio-upper-version
      namespace: istio-system
    spec:
      configPatches:
        - applyTo: NETWORK_FILTER
          match:
            listener:
              filterChain:
                filter:
                  name: envoy.filters.network.http_connection_manager
            proxy:
              proxyVersion: ^1.*.*
          patch:
            operation: MERGE
            value:
              typed_config:
                '@type': >-
                  type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                delayed_close_timeout: 0s
                    
  2. 将EnvoyFilter绑定至指定的工作负载或命名空间。具体操作,请参见步骤二:将Envoy过滤器模板绑定至工作负载或命名空间

  3. 使用Chrome浏览器打开client.html文件,在键盘上按F12键,开启Developer Tools。

  4. 刷新页面,在Console页签下,查看返回码。

    如下图所示,返回码变为4321,符合预期。因此,您可以通过EnvoyFilter解决WebSocket关闭时返回码不一致的问题。4321