diff --git a/src/components/starlight/ContentPanel.astro b/src/components/starlight/ContentPanel.astro
index 042f07bcca..3e0fcc60fe 100644
--- a/src/components/starlight/ContentPanel.astro
+++ b/src/components/starlight/ContentPanel.astro
@@ -2,7 +2,7 @@
import type { Props } from "../props";
---
-
+
@@ -28,4 +28,9 @@ import type { Props } from "../props";
}
}
}
+ @media (min-width: 100rem) {
+ .large-screen-width .max-unfold{
+ max-width: 100%!important;
+ }
+ }
diff --git a/src/components/starlight/TwoColumnContent.astro b/src/components/starlight/TwoColumnContent.astro
index 42e065e03b..9965c580da 100644
--- a/src/components/starlight/TwoColumnContent.astro
+++ b/src/components/starlight/TwoColumnContent.astro
@@ -90,6 +90,14 @@ import { Icon } from "@astrojs/starlight/components";
);
}
}
+ @media (min-width: 100rem) {
+ .collapsed {
+ max-width: 20rem!important;
+ }
+ .max-unfold .sl-markdown-content .expressive-code{
+ max-width: calc(100vw - var(--sl-sidebar-width) - 20rem);
+ }
+ }
}
@@ -151,7 +159,21 @@ import { Icon } from "@astrojs/starlight/components";
}
}
- document.addEventListener("astro:page-load", toggleSidebar);
+ // 大屏幕下文档三栏宽度自适应 100rem
+ function largeScreen() {
+ const toggleBtn = document.getElementById("toggle-btn");
+ const remwidth = parseFloat(getComputedStyle(document.documentElement).fontSize) * 100;
+ const windowWidth = window.innerWidth;
+ if (toggleBtn && windowWidth > remwidth) {
+ toggleBtn.click();
+ }
+ };
+
+ document.addEventListener("astro:page-load", ()=> {
+ toggleSidebar()
+ largeScreen()
+ });
+
document.addEventListener("DOMContentLoaded", toggleSidebar);
document.addEventListener("astro:page-load", addBackBtnClick);
document.addEventListener("DOMContentLoaded", addBackBtnClick);
diff --git a/src/constant.ts b/src/constant.ts
index 51611c498b..153437481c 100644
--- a/src/constant.ts
+++ b/src/constant.ts
@@ -489,6 +489,14 @@ export const COMMUNITY_MENU_LIST = [
en: "Blog",
},
},
+ {
+ label: "电子书",
+ target: "_self",
+ link: "/docs/ebook/wasm14/",
+ translations: {
+ en: "E-book",
+ },
+ },
],
},
];
diff --git a/src/content/docs/ebook/_sidebar.json b/src/content/docs/ebook/_sidebar.json
new file mode 100644
index 0000000000..a93f8fbc4f
--- /dev/null
+++ b/src/content/docs/ebook/_sidebar.json
@@ -0,0 +1,52 @@
+[
+ {
+ "label": "Wasm 插件和开发篇",
+ "translations": {
+ "en": "Development Wasm Plugin"
+ },
+ "items": [
+ {
+ "label": "Wasm 插件介绍和开发自定义插件",
+ "translations": {
+ "en": "Plugins and Development custom"
+ },
+ "link": "docs/ebook/wasm14/"
+ },
+ {
+ "label": "Wasm 插件原理",
+ "translations": {
+ "en": "Wasm Plugin principle"
+ },
+ "link": "docs/ebook/wasm15/"
+ },
+ {
+ "label": "Higress 插件GoSDK与处理流程",
+ "translations": {
+ "en": "Higress GoSDK Processing "
+ },
+ "link": "docs/ebook/wasm16/"
+ },
+ {
+ "label": "HTTP 调用",
+ "translations": {
+ "en": "HTTP Call"
+ },
+ "link": "docs/ebook/wasm17/"
+ },
+ {
+ "label": "Redis 调用",
+ "translations": {
+ "en": "Redis Call"
+ },
+ "link": "docs/ebook/wasm18/"
+ },
+ {
+ "label": "Wasm 生效原理",
+ "translations": {
+ "en": "Wasm Effective Principle "
+ },
+ "link": "docs/ebook/wasm19/"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/src/content/docs/ebook/en/wasm14.md b/src/content/docs/ebook/en/wasm14.md
new file mode 100644
index 0000000000..878af95f5c
--- /dev/null
+++ b/src/content/docs/ebook/en/wasm14.md
@@ -0,0 +1,1072 @@
+---
+title: Wasm 插件介绍和开发自定义插件
+keywords: [Higress]
+---
+
+# Wasm 插件介绍和开发自定义插件
+
+本章开始进入 Wasm 插件开发篇,主要介绍 Wasm 插件配置、Higress WasmPlugin CRD 以及如何开发自定义插件。
+
+## 1 测试环境准备
+
+> Higress 本地测试环境网关地址是 127.0.0.1,端口是 80 和 443。
+
+准备 echo-server 和 Ingress, 其 YAML 配置如下:
+
+```yaml
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: higress-course
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: echo-server
+ namespace: higress-course
+spec:
+ selector:
+ app: echo-server
+ ports:
+ - protocol: TCP
+ port: 8080
+ targetPort: 3000
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: echo-server
+ namespace: higress-course
+ labels:
+ app: echo-server
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: echo-server
+ template:
+ metadata:
+ labels:
+ app: echo-server
+ spec:
+ containers:
+ - name: echo-server
+ image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-server:1.3.0
+ env:
+ - name: POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ resources:
+ requests:
+ cpu: 10m
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-foo
+ namespace: higress-course
+spec:
+ ingressClassName: higress
+ rules:
+ - host: "foo.com"
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: echo-server
+ port:
+ number: 8080
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-bar
+ namespace: higress-course
+spec:
+ ingressClassName: higress
+ rules:
+ - host: "bar.com"
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: echo-server
+ port:
+ number: 8080
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-baz
+ namespace: higress-course
+spec:
+ ingressClassName: higress
+ rules:
+ - host: "baz.com"
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: echo-server
+ port:
+ number: 8080
+```
+
+## 2 Wasm 插件配置
+
+Higress WasmPlugin 在 Istio WasmPlugin 的基础上进行了扩展,支持全局、路由、域名、服务级别的配置。这 4 个配置优先级是:路由级 > 域名级 > 服务级 > 全局,对于没有匹配到具体路由、域名、服务级别的请求才会应用全局配置。
+
+下面以 Higress 官方提供的 [custom-response](https://higress.io/zh-cn/docs/plugins/transformation/custom-response) 插件为例进行介绍。custom-response 插件支持配置自定义响应,包括 HTTP 响应状态码、HTTP 响应头,以及 HTTP 响应体。custom-response 插件不仅可以用于模拟响应,还可以根据特定状态码返回自定义响应。例如,在触发网关限流策略时,返回自定义响应。
+
+应用 custom-response 插件,YAML 配置如下:
+
+```yaml
+apiVersion: extensions.higress.io/v1alpha1
+kind: WasmPlugin
+metadata:
+ name: custom-response
+ namespace: higress-system
+spec:
+ priority: 200
+ # 配置会全局生效,但如果被下面规则匹配到,则会改为执行命中规则的配置
+ defaultConfig:
+ headers:
+ - key1=value1
+ "body": "{\"hello\":\"foo\"}"
+ matchRules:
+ # 域名级生效配置
+ - domain:
+ - bar.com
+ config:
+ headers:
+ - key2=value2
+ "body": "{\"hello\":\"bar\"}"
+ # 路由级生效配置
+ - ingress:
+ - higress-course/ingress-baz
+ # higress-course 命名空间下名为 ingress-baz 的 ingress 会应用下面这个配置
+ config:
+ headers:
+ - key3=value3
+ "body": "{\"hello\":\"baz\"}"
+ url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/custom-response:1.0.0
+```
+
+访问 foo.com,由于请求没有匹配任何域名级或路由级配置,因此最终应用了全局配置。
+
+```shell
+curl -v -H "Host: foo.com" http://127.0.0.1/
+
+* Trying 127.0.0.1:80...
+* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
+> GET / HTTP/1.1
+> Host: foo.com
+> User-Agent: curl/8.1.2
+> Accept: */*
+>
+< HTTP/1.1 200 OK
+< key1: value1
+< content-type: application/json; charset=utf-8
+< content-length: 15
+< date: Sun, 14 Jul 2024 02:45:51 GMT
+< server: istio-envoy
+<
+* Connection #0 to host 127.0.0.1 left intact
+{"hello":"foo"}
+```
+
+访问 bar.com,请求匹配域名级配置。
+
+```shell
+curl -v -H "Host: bar.com" http://127.0.0.1/
+
+* Trying 127.0.0.1:80...
+* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
+> GET / HTTP/1.1
+> Host: bar.com
+> User-Agent: curl/8.1.2
+> Accept: */*
+>
+< HTTP/1.1 200 OK
+< key2: value2
+< content-type: application/json; charset=utf-8
+< content-length: 15
+< date: Sun, 14 Jul 2024 02:47:51 GMT
+< server: istio-envoy
+<
+* Connection #0 to host 127.0.0.1 left intact
+{"hello":"bar"}
+```
+
+访问 baz.com,请求匹配路由级配置。
+
+```shell
+curl -v -H "Host: baz.com" http://127.0.0.1/
+
+* Trying 127.0.0.1:80...
+* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
+> GET / HTTP/1.1
+> Host: baz.com
+> User-Agent: curl/8.1.2
+> Accept: */*
+>
+< HTTP/1.1 200 OK
+< key3: value3
+< content-type: application/json; charset=utf-8
+< content-length: 15
+< date: Sun, 14 Jul 2024 08:44:03 GMT
+< server: istio-envoy
+<
+* Connection #0 to host 127.0.0.1 left intact
+{"hello":"baz"}
+```
+
+测试完成后删除 `custom-response` WasmPlugin,避免对后续测试产生影响。
+
+```shell
+kubectl delete wasmplugin custom-response -n higress-system
+```
+
+## 3 Higress WasmPlugin CRD
+
+Higress WasmPlugin CRD 在 [Istio WasmPlugin CRD](https://istio.io/latest/docs/reference/config/proxy_extensions/wasm-plugin/) 的基础上进行了扩展,新增 `defaultConfig` 和 `matchRules` 字段,用于配置插件的默认配置和路由级、域名级配置。
+
+主要配置如下:
+
+| 字段名称 | 数据类型 | 填写要求 | 描述 |
+|------------------|-----------------|----|-------------------------------------------------------------------------------------------------------------|
+| `pluginName` | string | 选填 | 插件名称 |
+| `phase` | string | 选填 | 插件插入插件链中的位置,默认是 UNSPECIFIED_PHASE |
+| `priority` | int | 选填 | 插件执行优先级,默认为 0,在相同 `phase` 下,值越大越先处理请求,但越后处理响应 |
+| `imagePullPolicy` | string | 选填 | 插件镜像拉取策略,可选值有:`UNSPECIFIED_POLICY`(默认值)、`IfNotPresent`、`Always` |
+| `imagePullSecret` | string | 选填 | 用于拉取 OCI 镜像的凭据。与 WasmPlugin 在同一命名空间中的Kubernetes Secret 的名称 |
+| `url` | string | 必填 | Wasm 文件或 OCI 容器的 URL,默认为 `oci://`,引用 OCI 镜像。同时支持 `file://`,用于容器本地的 Wasm 文件,以及 `http[s]://`,用于引用远程托管的 Wasm 文件 |
+| `Sha256` | string | 选填 | 用于验证 Wasm 文件或 OCI 容器的 SHA256 校验和 |
+| `defaultConfig` | object | 选填 | 插件默认配置,全局生效于没有匹配具体域名和路由配置的请求 |
+| `defaultConfigDisable`| bool | 选填 | 插件默认配置是否失效,默认值是 false |
+| `matchRules` | array of object | 选填 | 匹配域名或路由生效的配置 |
+
+`phase` 配置说明:
+
+| 字段名称 | 描述 |
+|--------------------|------------------------------------------------------------------------|
+| `UNSPECIFIED_PHASE` | 在插件过滤器链的末端,在路由器之前插入插件,如果没有指定插件的 `phase`,则默认设置为 `UNSPECIFIED_PHASE` |
+| `AUTHN` | 在 Istio 认证过滤器之前插入插件 |
+| `AUTHZ` | 在 Istio 授权过滤器之前且在 Istio 认证过滤器之后插入插件 |
+| `STATS` | 在 Istio 统计过滤器之前且在 Istio 授权过滤器之后插入插件 |
+
+`matchRules` 中每一项的配置字段说明:
+
+| 字段名称 | 数据类型 | 填写要求 | 配置示例 | 描述 |
+|-----------------|-------|--------------------------------------|--------------------------------|-----------------------------------------|
+| `ingress` | 字符串数组 | `ingress`、`domain` 和 `service` 中必填一项 | ["default/foo", "default/bar"] | 匹配 ingress 资源对象,匹配格式为: `命名空间/ingress名称` |
+| `domain` | 字符串数组 | `ingress`、`domain` 和 `service` 中必填一项| ["example.com", "*.test.com"] | 匹配域名,支持泛域名 |
+| `service` | 字符串数组 | `ingress`、`domain` 和 `service` 中必填一项 | ["echo-server.higress-course.svc.cluster.local"] | 匹配服务名称 |
+| `config` | 对象 | 选填 | - | 匹配后生效的插件配置 |
+| `configDisable` | bool | 选填 | false | 配置是否生效,默认设置为 false |
+
+
+## 4 自定义插件开发
+
+开发一个简单日志插件 `easy-logger`, 这个插件根据配置记录请求和响应到网关日志中。整个过程涉及到插件开发环境准备、开发和测试、部署和验证。
+
+### 4.1 环境准备
+
+环境准备如下:
+
+- Docker 官方安装连接:https://docs.docker.com/engine/install/
+- Go 版本: >= 1.19 (需要支持范型特性),官方安装链接:https://go.dev/doc/install
+
+如果选择用 TinyGo 在本地构建 Wasm 文件,再拷贝到 Docker 镜像中,需要安装 TinyGo,其环境要求如下:
+
+- TinyGo 版本: >= 0.28.1 (建议版本 0.28.1) 官方安装链接:https://tinygo.org/getting-started/install/
+
+### 4.2 开发和测试
+
+#### 4.2.1 初始化工程目录
+
+1. 新建一个工程目录文件 easy-logger。
+
+```shell
+mkdir easy-logger
+```
+2. 在所建目录下执行以下命令,初始化 Go 工程。
+
+```shell
+go mod init easy-logger
+```
+
+go.mod 文件中 go 版本需要设置为 1.19,由于在 4.3.3 节中我们将使用 1.19 版本的 wasm-go-builder 镜像来构建插件,因此需要保持两者的 go 版本一致。
+
+```shell
+module easy-logger
+
+go 1.19
+```
+
+3. 国内环境可能需要设置下载依赖包的代理
+
+```shell
+go env -w GOPROXY=https://proxy.golang.com.cn,direct
+```
+
+4. 下载构建插件的依赖
+
+```shell
+go get github.com/higress-group/proxy-wasm-go-sdk
+go get github.com/alibaba/higress/plugins/wasm-go@main
+go get github.com/tidwall/gjson
+```
+
+#### 4.2.2 编写 main.go 文件
+首先,我们编写 easy-logger 插件的基本框架,暂时只读取我们设置的配置参数,不在请求和响应阶段进行任何处理。
+
+```golang
+package main
+
+import (
+ "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+ "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
+ "github.com/tidwall/gjson"
+)
+
+func main() {
+ wrapper.SetCtx(
+ // 插件名称
+ "easy-logger",
+ // 设置自定义函数解析插件配置
+ wrapper.ParseConfigBy(parseConfig),
+ // 设置自定义函数处理请求头
+ wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
+ // 设置自定义函数处理请求体
+ wrapper.ProcessRequestBodyBy(onHttpRequestBody),
+ // 设置自定义函数处理响应头
+ wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
+ // 设置自定义函数处理响应体
+ wrapper.ProcessResponseBodyBy(onHttpResponseBody),
+ // 设置自定义函数处理流式请求体
+ //wrapper.ProcessStreamingRequestBodyBy(onHttpStreamingRequestBody),
+ // 设置自定义函数处理流式响应体
+ //wrapper.ProcessStreamingResponseBodyBy(onHttpStreamingResponseBody),
+ )
+}
+
+// 自定义插件配置
+type LoggerConfig struct {
+ // 是否打印请求
+ request bool
+ // 是否打印响应
+ response bool
+ // 打印响应状态码,* 表示打印所有状态响应,500,502,503 表示打印 HTTP 500、502、503 状态响应,默认是 *
+ responseStatusCodes string
+}
+
+func parseConfig(json gjson.Result, config *LoggerConfig, log wrapper.Log) error {
+ log.Debugf("parseConfig()")
+ config.request = json.Get("request").Bool()
+ config.response = json.Get("response").Bool()
+ config.responseStatusCodes = json.Get("responseStatusCodes").String()
+ if config.responseStatusCodes == "" {
+ config.responseStatusCodes = "*"
+ }
+ log.Debugf("parse config:%v", config)
+ return nil
+}
+
+func onHttpRequestHeaders(ctx wrapper.HttpContext, config LoggerConfig, log wrapper.Log) types.Action {
+ log.Debugf("onHttpRequestHeaders()")
+ return types.ActionContinue
+}
+
+func onHttpRequestBody(ctx wrapper.HttpContext, config LoggerConfig, body []byte, log wrapper.Log) types.Action {
+ log.Debugf("onHttpRequestBody()")
+ return types.ActionContinue
+}
+
+func onHttpResponseBody(ctx wrapper.HttpContext, config LoggerConfig, body []byte, log wrapper.Log) types.Action {
+ log.Debugf("onHttpResponseBody()")
+ return types.ActionContinue
+}
+
+func onHttpResponseHeaders(ctx wrapper.HttpContext, config LoggerConfig, log wrapper.Log) types.Action {
+ log.Debugf("onHttpResponseHeaders()")
+ return types.ActionContinue
+}
+```
+
+Higress 插件 SDK 开发涉及到以下内容:
+
+- wrapper.HttpContext:请求上下文。
+- LoggerConfig:自定义插件配置。
+- wrapper.Log:插件日志工具。
+- wrapper.ProcessXXXX:插件回调钩子函数。
+- proxywasm:提供插件工具函数包。
+
+wrapper 插件回调钩子函数包含以下函数,可以根据实际业务需求选择设置以下钩子函数:
+
+- wrapper.ParseConfigBy(parseConfig):解析插件配置。
+- wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders):设置自定义函数处理请求头。
+- wrapper.ProcessRequestBodyBy(onHttpRequestBody):设置自定义函数处理请求体。
+- wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders):设置自定义函数处理响应头。
+- wrapper.ProcessResponseBodyBy(onHttpResponseBody):设置自定义函数处理响应体。
+- wrapper.ProcessStreamingRequestBodyBy(onHttpStreamingRequestBody):设置自定义函数处理流式请求体。
+- wrapper.ProcessStreamingResponseBodyBy(onHttpStreamingResponseBody):设置自定义函数处理流式响应体。
+
+关于 Higress 插件 SDK 内容会在后续章节中详细展开。
+
+#### 4.3.3 本地测试
+
+1. 第一步:在插件目录下创建文件 envoy.yaml,内容如下。网关在 10000 端口监听 HTTP 请求,将请求转发到 echo-server 服务。
+
+```yaml
+admin:
+ address:
+ socket_address:
+ protocol: TCP
+ address: 0.0.0.0
+ port_value: 9901
+static_resources:
+ listeners:
+ - name: listener_0
+ address:
+ socket_address:
+ protocol: TCP
+ address: 0.0.0.0
+ port_value: 10000
+ 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
+ scheme_header_transformation:
+ scheme_to_overwrite: https
+ stat_prefix: ingress_http
+ route_config:
+ name: local_route
+ virtual_hosts:
+ - name: local_service
+ domains: ["*"]
+ routes:
+ - match:
+ prefix: "/"
+ route:
+ cluster: echo-server
+ http_filters:
+ - name: wasmdemo
+ typed_config:
+ "@type": type.googleapis.com/udpa.type.v1.TypedStruct
+ type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ value:
+ config:
+ name: wasmdemo
+ vm_config:
+ runtime: envoy.wasm.runtime.v8
+ code:
+ local:
+ filename: /etc/envoy/plugin.wasm
+ configuration:
+ "@type": "type.googleapis.com/google.protobuf.StringValue"
+ value: |-
+ {
+ "request": true,
+ "response": true,
+ "responseStatusCodes": "200,500,502,503"
+ }
+ - name: envoy.filters.http.router
+ clusters:
+ - name: echo-server
+ connect_timeout: 30s
+ type: LOGICAL_DNS
+ dns_lookup_family: V4_ONLY
+ lb_policy: ROUND_ROBIN
+ load_assignment:
+ cluster_name: echo-server
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: echo-server
+ port_value: 3000
+```
+
+插件通过本地文件的方式加载到 Envoy 中,插件配置的如下:
+
+```yaml
+ configuration:
+ "@type": "type.googleapis.com/google.protobuf.StringValue"
+ value: |-
+ {
+ "request": true,
+ "response": true,
+ "responseStatusCodes": "200,500,502,503"
+ }
+```
+
+2. 第二步:在插件目录下创建文件 docker-compose.yaml,内容如下:
+
+```yaml
+version: '3.9'
+services:
+ envoy:
+ image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v1.4.1
+ entrypoint: /usr/local/bin/envoy
+ # 注意这里对 Wasm 开启了 debug 级别日志,在生产环境部署时请使用默认的 info 级别
+ # 如果需要将 Envoy 的日志级别调整为 debug,将 --log-level 参数设置为 debug
+ command: -c /etc/envoy/envoy.yaml --log-level info --log-path /etc/envoy/envoy.log --component-log-level wasm:debug
+ depends_on:
+ - echo-server
+ networks:
+ - wasmtest
+ ports:
+ - "10000:10000"
+ - "9901:9901"
+ volumes:
+ - ./envoy.yaml:/etc/envoy/envoy.yaml
+ - ./build/plugin.wasm:/etc/envoy/plugin.wasm
+ - ./envoy.log:/etc/envoy/envoy.log
+ echo-server:
+ image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-server:1.3.0
+ networks:
+ - wasmtest
+ ports:
+ - "3000:3000"
+networks:
+ wasmtest: {}
+```
+3. 第三步:在插件目录下创建文件 Dockerfile,内容如下:
+
+```yaml
+ARG BUILDER=higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go1.19-tinygo0.28.1-oras1.0.0
+FROM $BUILDER as builder
+
+ARG GOPROXY
+ENV GOPROXY=${GOPROXY}
+
+ARG EXTRA_TAGS=""
+ENV EXTRA_TAGS=${EXTRA_TAGS}
+
+WORKDIR /workspace
+COPY . .
+RUN go mod tidy
+RUN tinygo build -o /main.wasm -scheduler=none -gc=custom -tags="custommalloc nottinygc_finalizer $EXTRA_TAGS" -target=wasi ./main.go
+
+FROM scratch as output
+COPY --from=builder /main.wasm plugin.wasm
+```
+
+4. 第四步:在插件目录下创建文件 Makefile,内容如下:
+
+```shell
+PLUGIN_NAME ?= hello-world
+BUILDER_REGISTRY ?= higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/
+REGISTRY ?= higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/
+GO_VERSION ?= 1.19
+TINYGO_VERSION ?= 0.28.1
+ORAS_VERSION ?= 1.0.0
+HIGRESS_VERSION ?= 1.4.1
+USE_HIGRESS_TINYGO ?= true
+BUILDER ?= ${BUILDER_REGISTRY}wasm-go-builder:go${GO_VERSION}-tinygo${TINYGO_VERSION}-oras${ORAS_VERSION}
+BUILD_TIME := $(shell date "+%Y%m%d-%H%M%S")
+COMMIT_ID := $(shell git rev-parse --short HEAD 2>/dev/null)
+IMAGE_TAG = $(if $(strip $(PLUGIN_VERSION)),${PLUGIN_VERSION},${BUILD_TIME}-${COMMIT_ID})
+IMG ?= ${REGISTRY}${PLUGIN_NAME}:${IMAGE_TAG}
+GOPROXY := $(shell go env GOPROXY)
+EXTRA_TAGS ?= proxy_wasm_version_0_2_100
+
+.DEFAULT:
+local-docker-build:
+ DOCKER_BUILDKIT=1 docker build --build-arg BUILDER=${BUILDER} \
+ --build-arg GOPROXY=$(GOPROXY) \
+ --build-arg EXTRA_TAGS=$(EXTRA_TAGS) \
+ -t ${IMG} \
+ --output build \
+ .
+ @echo ""
+ @echo "output wasm file: ./build/plugin.wasm"
+
+build-image:
+ DOCKER_BUILDKIT=1 docker build --build-arg BUILDER=${BUILDER} \
+ --build-arg GOPROXY=$(GOPROXY) \
+ --build-arg EXTRA_TAGS=$(EXTRA_TAGS) \
+ -t ${IMG} \
+ .
+ @echo ""
+ @echo "image: ${IMG}"
+
+build-push: build-image
+ docker push ${IMG}
+
+local-build:
+ tinygo build -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer $(EXTRA_TAGS)' \
+ -o ./build/plugin.wasm main.go
+ @echo ""
+ @echo "wasm: ./build/plugin.wasm"
+
+local-run:
+ echo > ./envoy.log
+ docker compose down
+ docker compose up -d
+
+local-all: local-build local-run
+local-docker-all: local-docker-build local-run
+```
+
+请将 Makefile 文件中镜像仓库地址 `REGISTRY ?= higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/` 换成自己的镜像仓库地址。
+
+其命令说明如下:
+- `make local-docker-build`: 本地 Docker 环境构建插件,生成插件文件 ./build/plugin.wasm。
+- `make local-build`: 本地 TinyGo 构建插件,生成插件文件 ./build/plugin.wasm。
+- `make local-run`: 本地启动测试环境。
+- `PLUGIN_NAME=easy-logger PLUGIN_VERSION=1.0.0 make build-image` 构建 easy-logger 插件镜像,插件版本为 1.0.0。
+- `PLUGIN_NAME=easy-logger PLUGIN_VERSION=1.0.0 make build-push` 构建 easy-logger 插件镜像,插件版本为 1.0.0,同时推送到镜像仓库。
+- `make local-docker-all`: 本地 Docker 环境构建插件,生成插件文件 build/plugin.wasm,同时启动本地测试环境。
+- `make local-all`: 本地 TinyGo 构建插件,生成插件文件 ./build/plugin.wasm,同时启动本地测试环境。
+
+注意用 TinyGo 本地构建命令如下:
+```shell
+tinygo build -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer proxy_wasm_version_0_2_100' -o ./build/plugin.wasm main.go
+```
+
+5. 第五步:本地 Docker 环境构建和启动测试环境
+
+本地 Docker 环境构建和启动测试环境,命令如下:
+
+```shell
+make local-docker-all
+```
+
+本地启动测试环境后,插件目录整体文件结构如下:
+
+```shell
+tree
+.
+├── Dockerfile
+├── Makefile
+├── build
+│ └── plugin.wasm # 构建生成的 Wasm 文件
+├── docker-compose.yaml
+├── envoy.log # Envoy 日志文件
+├── envoy.yaml
+├── go.mod
+├── go.sum
+└── main.go
+```
+
+执行以下命令通过网关访问 echo-server。
+
+```shell
+curl -X POST -v http://127.0.0.1:10000/hello \
+-H "Content-type: application/json" -H 'host:foo.com' \
+-d '{"username":["unamexxxx"], "password":["pswdxxxx"]}'
+
+* Trying 127.0.0.1:10000...
+* Connected to 127.0.0.1 (127.0.0.1) port 10000 (#0)
+> POST /hello HTTP/1.1
+> Host:foo.com
+> User-Agent: curl/8.1.2
+> Accept: */*
+> Content-type: application/json
+> Content-Length: 50
+>
+< HTTP/1.1 200 OK
+< content-type: application/json
+< x-content-type-options: nosniff
+< date: Sat, 20 Jul 2024 04:39:46 GMT
+< content-length: 642
+< req-cost-time: 48
+< req-arrive-time: 1721450386098
+< resp-start-time: 1721450386146
+< x-envoy-upstream-service-time: 30
+< server: envoy
+<
+{
+ "path": "/hello",
+ "host": "foo.com",
+ "method": "POST",
+ "proto": "HTTP/1.1",
+ "headers": {
+ "Accept": [
+ "*/*"
+ ],
+ "Content-Length": [
+ "50"
+ ],
+ "Content-Type": [
+ "application/json"
+ ],
+ "Original-Host": [
+ "foo.com"
+ ],
+ "Req-Start-Time": [
+ "1721450386098"
+ ],
+ "User-Agent": [
+ "curl/8.1.2"
+ ],
+ "X-Envoy-Expected-Rq-Timeout-Ms": [
+ "15000"
+ ],
+ "X-Forwarded-Proto": [
+ "https"
+ ],
+ "X-Request-Id": [
+ "2f9ff093-7891-4c55-992b-874f7ba00d0e"
+ ]
+ },
+ "namespace": "",
+ "ingress": "",
+ "service": "",
+ "pod": "",
+ "body": {
+ "password": [
+ "pswdxxxx"
+ ],
+ "username": [
+ "unamexxxx"
+ ]
+ }
+* Connection #0 to host 127.0.0.1 left intact
+}
+```
+
+查看插件目录下 envoy.log 文件,可以看到 easy-logger 插件的日志输出。
+
+```shell
+[2024-07-20 04:08:19.990][22][debug][wasm] [external/envoy/source/extensions/common/wasm/wasm.cc:146] Thread-Local Wasm created 10 now active
+[2024-07-20 04:08:19.993][22][debug][wasm] [external/envoy/source/extensions/common/wasm/context.cc:1308] wasm log: [easy-logger] parseConfig()
+[2024-07-20 04:08:19.993][22][debug][wasm] [external/envoy/source/extensions/common/wasm/context.cc:1308] wasm log: [easy-logger] parse config:&{request:true response:true responseStatusCodes:200,500,502,503}
+[2024-07-20 04:08:19.993][1][warning][main] [external/envoy/source/server/server.cc:715] there is no configured limit to the number of allowed active connections. Set a limit via the runtime key overload.global_downstream_max_connections
+[2024-07-20 04:39:46.114][29][debug][wasm] [external/envoy/source/extensions/common/wasm/context.cc:1308] wasm log wasmdemo: [easy-logger] onHttpRequestHeaders()
+[2024-07-20 04:39:46.116][29][debug][wasm] [external/envoy/source/extensions/common/wasm/context.cc:1308] wasm log wasmdemo: [easy-logger] onHttpRequestBody()
+[2024-07-20 04:39:46.147][29][debug][wasm] [external/envoy/source/extensions/common/wasm/context.cc:1308] wasm log wasmdemo: [easy-logger] onHttpResponseHeaders()
+[2024-07-20 04:39:46.147][29][debug][wasm] [external/envoy/source/extensions/common/wasm/context.cc:1308] wasm log wasmdemo: [easy-logger] onHttpResponseBody()
+```
+
+到这里表示整体开发和测试环境已经完成,下面就是完善插件功能,然后重新测试。
+
+### 4.3 完善插件功能
+
+接下来,我们将通过自定义函数来处理请求和响应信息。通过设置插件参数,我们可以控制是否打印请求和响应信息,并根据指定的响应状态码决定是否记录响应内容。
+
+```golang
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+ "github.com/google/uuid"
+ "github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
+ "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
+ "github.com/tidwall/gjson"
+)
+
+func main() {
+ wrapper.SetCtx(
+ // 插件名称
+ "easy-logger",
+ // 设置自定义函数解析插件配置
+ wrapper.ParseConfigBy(parseConfig),
+ // 设置自定义函数处理请求头
+ wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
+ // 设置自定义函数处理请求体
+ wrapper.ProcessRequestBodyBy(onHttpRequestBody),
+ // 设置自定义函数处理响应头
+ wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
+ // 设置自定义函数处理响应体
+ wrapper.ProcessResponseBodyBy(onHttpResponseBody),
+ // 设置自定义函数处理流式请求体
+ //wrapper.ProcessStreamingRequestBodyBy(onHttpStreamingRequestBody),
+ // 设置自定义函数处理流式响应体
+ //wrapper.ProcessStreamingResponseBodyBy(onHttpStreamingResponseBody),
+ )
+}
+
+// 自定义插件配置
+type LoggerConfig struct {
+ // 是否打印请求
+ request bool
+ // 是否打印响应
+ response bool
+ // 打印响应状态码,* 表示打印所有状态响应,500,502,503 表示打印 HTTP 500、502、503 状态响应,默认是 *
+ responseStatusCodes string
+}
+
+func parseConfig(json gjson.Result, config *LoggerConfig, log wrapper.Log) error {
+ log.Debugf("parseConfig()")
+ config.request = json.Get("request").Bool()
+ config.response = json.Get("response").Bool()
+ config.responseStatusCodes = json.Get("responseStatusCodes").String()
+ if config.responseStatusCodes == "" {
+ config.responseStatusCodes = "*"
+ }
+ log.Debugf("parse config:%+v", config)
+ return nil
+}
+
+func onHttpRequestHeaders(ctx wrapper.HttpContext, config LoggerConfig, log wrapper.Log) types.Action {
+ log.Debugf("onHttpRequestHeaders()")
+ requestId := uuid.New().String()
+ ctx.SetContext("requestId", requestId)
+ if !config.request {
+ return types.ActionContinue
+ }
+ // 获取并打印请求头
+ headers, _ := proxywasm.GetHttpRequestHeaders()
+ var build strings.Builder
+ build.WriteString("\n===========request headers===============\n")
+ build.WriteString(fmt.Sprintf("requestId:%s\n", requestId))
+ for _, values := range headers {
+ build.WriteString(fmt.Sprintf("%s:%s\n", values[0], values[1]))
+ }
+ log.Infof(build.String())
+ // 继续处理请求
+ return types.ActionContinue
+}
+
+func onHttpRequestBody(ctx wrapper.HttpContext, config LoggerConfig, body []byte, log wrapper.Log) types.Action {
+ log.Debugf("onHttpRequestBody()")
+ // 打印请求体
+ if config.request {
+ var build strings.Builder
+ build.WriteString("\n===========request body===============\n")
+ requestId := ctx.GetContext("requestId").(string)
+ build.WriteString(fmt.Sprintf("requestId:%s\n", requestId))
+ build.WriteString(fmt.Sprintf("body:%s\n", string(body)))
+ log.Infof(build.String())
+ }
+ return types.ActionContinue
+}
+
+func onHttpResponseHeaders(ctx wrapper.HttpContext, config LoggerConfig, log wrapper.Log) types.Action {
+ log.Debugf("onHttpResponseHeaders()")
+ // 添加自定义响应头
+ proxywasm.AddHttpResponseHeader("x-easy-logger", "1.0.0")
+ if !config.response {
+ return types.ActionContinue
+ }
+ // 获取响应状态码
+ statusCode, _ := proxywasm.GetHttpResponseHeader(":status")
+ logResponseBody := false
+ // 根据响应状态码决定是否打印响应体
+ if config.responseStatusCodes == "*" || strings.Contains(config.responseStatusCodes, statusCode) {
+ logResponseBody = true
+ }
+ // 将是否记录响应体的信息存储在上下文中,在 onHttpResponseBody 阶段获取上下文判断是否打印响应体
+ ctx.SetContext("logResponseBody", logResponseBody)
+ // 获取响应头
+ headers, _ := proxywasm.GetHttpResponseHeaders()
+ // 打印响应头
+ var build strings.Builder
+ build.WriteString("\n===========response headers===============\n")
+ requestId := ctx.GetContext("requestId").(string)
+ build.WriteString(fmt.Sprintf("requestId:%s\n", requestId))
+ for _, values := range headers {
+ build.WriteString(fmt.Sprintf("%s:%s\n", values[0], values[1]))
+ }
+ log.Infof(build.String())
+ return types.ActionContinue
+}
+
+func onHttpResponseBody(ctx wrapper.HttpContext, config LoggerConfig, body []byte, log wrapper.Log) types.Action {
+ log.Debugf("onHttpResponseBody()")
+ // 获取在 onHttpRequestHeaders 阶段设置的上下文
+ logResponseBody, ok := ctx.GetContext("logResponseBody").(bool)
+ if !ok {
+ return types.ActionContinue
+ }
+ // 打印响应体
+ if logResponseBody {
+ var build strings.Builder
+ build.WriteString("\n===========response body===============\n")
+ requestId := ctx.GetContext("requestId").(string)
+ build.WriteString(fmt.Sprintf("requestId:%s\n", requestId))
+ build.WriteString(fmt.Sprintf("body:%s\n", string(body)))
+ log.Infof(build.String())
+ }
+ return types.ActionContinue
+}
+```
+
+
+### 4.4 部署插件和验证
+
+1. 构建插件镜像
+
+```shell
+PLUGIN_NAME=easy-logger PLUGIN_VERSION=1.0.0 make build-push
+```
+
+2. 部署插件
+
+easy-logger 插件部署 YAML 如下:
+```yaml
+apiVersion: extensions.higress.io/v1alpha1
+kind: WasmPlugin
+metadata:
+ name: easy-logger
+ namespace: higress-system
+spec:
+ priority: 300
+ matchRules:
+ # 域名级生效配置
+ - domain:
+ - foo.com
+ config:
+ request: true
+ response: true
+ responseStatusCodes: "200,500,502,503"
+ url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/easy-logger:1.0.0
+```
+
+3. 验证插件
+
+- 设置网关插件的日志级别为 debug。
+
+```shell
+kubectl exec
-n higress-system -- \
+curl -X POST http://127.0.0.1:15000/logging?wasm=debug
+```
+
+- 请求访问
+
+```shell
+curl -X POST -v http://127.0.0.1/hello \
+-H "Content-type: application/json" -H 'host:foo.com' \
+-d '{"username":["unamexxxx"],"password":["pswdxxxx"]}'
+```
+
+- 查看网关的日志,可以看到输出了请求和响应的详细信息
+
+```shell
+kubectl logs -f -n higress-system
+
+[Envoy (Epoch 0)] [2024-07-20 04:56:55.251][39][debug][wasm] wasm log higress-system.easy-logger: [easy-logger] onHttpRequestHeaders()
+[Envoy (Epoch 0)] [2024-07-20 04:56:55.252][39][info][wasm] wasm log higress-system.easy-logger: [easy-logger]
+===========request headers===============
+requestId:a791e8e6-8126-4a1d-92f0-a0333b706c1d
+:authority:foo.com
+:path:/hello
+:method:POST
+:scheme:http
+user-agent:curl/8.1.2
+accept:*/*
+content-type:application/json
+content-length:50
+x-forwarded-for:192.168.65.1
+x-forwarded-proto:http
+x-envoy-internal:true
+x-request-id:2ad88049-6ba3-4f3d-bc81-dc29fa48ffce
+x-envoy-decorator-operation:echo-server.higress-course.svc.cluster.local:8080/*
+
+[Envoy (Epoch 0)] [2024-07-20 04:56:55.254][39][debug][wasm] wasm log higress-system.easy-logger: [easy-logger] onHttpRequestBody()
+[Envoy (Epoch 0)] [2024-07-20 04:56:55.254][39][info][wasm] wasm log higress-system.easy-logger: [easy-logger]
+===========request body===============
+requestId:a791e8e6-8126-4a1d-92f0-a0333b706c1d
+body:{"username":["unamexxxx"],"password":["pswdxxxx"]}
+
+[Envoy (Epoch 0)] [2024-07-20 04:56:55.256][39][debug][wasm] wasm log higress-system.easy-logger: [easy-logger] onHttpResponseHeaders()
+[Envoy (Epoch 0)] [2024-07-20 04:56:55.256][39][info][wasm] wasm log higress-system.easy-logger: [easy-logger]
+===========response headers===============
+requestId:a791e8e6-8126-4a1d-92f0-a0333b706c1d
+:status:200
+content-type:application/json
+x-content-type-options:nosniff
+date:Sat, 20 Jul 2024 04:56:55 GMT
+content-length:993
+req-cost-time:8
+req-arrive-time:1721451415248
+resp-start-time:1721451415256
+x-envoy-upstream-service-time:2
+x-easy-logger:1.0.0
+
+[Envoy (Epoch 0)] [2024-07-20 04:56:55.257][39][debug][wasm] wasm log higress-system.easy-logger: [easy-logger] onHttpResponseBody()
+[Envoy (Epoch 0)] [2024-07-20 04:56:55.257][39][info][wasm] wasm log higress-system.easy-logger: [easy-logger]
+===========response body===============
+requestId:a791e8e6-8126-4a1d-92f0-a0333b706c1d
+body:{
+ "path": "/hello",
+ "host": "foo.com",
+ "method": "POST",
+ "proto": "HTTP/1.1",
+ "headers": {
+ "Accept": [
+ "*/*"
+ ],
+ "Content-Length": [
+ "50"
+ ],
+ "Content-Type": [
+ "application/json"
+ ],
+ "Original-Host": [
+ "foo.com"
+ ],
+ "Req-Start-Time": [
+ "1721451415248"
+ ],
+ "User-Agent": [
+ "curl/8.1.2"
+ ],
+ "X-B3-Sampled": [
+ "0"
+ ],
+ "X-B3-Spanid": [
+ "f642c00a89551b07"
+ ],
+ "X-B3-Traceid": [
+ "dfab58b011681d29f642c00a89551b07"
+ ],
+ "X-Envoy-Attempt-Count": [
+ "1"
+ ],
+ "X-Envoy-Decorator-Operation": [
+ "echo-server.higress-course.svc.cluster.local:8080/*"
+ ],
+ "X-Envoy-Internal": [
+ "true"
+ ],
+ "X-Forwarded-For": [
+ "192.168.65.1"
+ ],
+ "X-Forwarded-Proto": [
+ "http"
+ ],
+ "X-Request-Id": [
+ "2ad88049-6ba3-4f3d-bc81-dc29fa48ffce"
+ ]
+ },
+ "namespace": "higress-course",
+ "ingress": "",
+ "service": "",
+ "pod": "echo-server-6f4df5fcff-nksqz",
+ "body": {
+ "password": [
+ "pswdxxxx"
+ ],
+ "username": [
+ "unamexxxx"
+ ]
+ }
+}
+```
+
+
+
+
+
+
+
diff --git a/src/content/docs/ebook/en/wasm15.md b/src/content/docs/ebook/en/wasm15.md
new file mode 100644
index 0000000000..ad6d670ac1
--- /dev/null
+++ b/src/content/docs/ebook/en/wasm15.md
@@ -0,0 +1,804 @@
+---
+title: Wasm 插件原理
+keywords: [Higress]
+---
+
+# Wasm 插件原理
+
+本章主要介绍 Proxy-Wasm Go SDK 和 Wasm 插件基本原理。
+
+## 1 Wasm、TinyGo、Proxy-Wasm Go SDK
+
+### 1.1 Wasm
+
+#### 1.1.1 什么是 Wasm ?
+
+ [WebAssembly(简称 Wasm)](https://webassembly.org/) 是操作堆栈虚拟机的二进制指令集,Wasm 可以在 Web 浏览器中运行或者其他环境比如服务器端应用程序运行。Wasm有以下特点:
+
+- 高效性能:提供了接近机器码的性能。
+- 跨平台:Wasm 是一种与平台无关的格式,可以在任何支持它的平台上运行,包括浏览器和服务器。
+- 安全性:Wasm 在一个内存安全的沙箱环境中运行,这意味着它可以安全地执行不受信任的代码,而不会访问或修改主机系统的其他部分。
+- 可移植性:Wasm 模块可以被编译成 WebAssembly 二进制文件,这些文件可以被传输和加载到支持 Wasm 的任何环境中。
+- 多语言支持:Wasm 支持多种编程语言,开发者可以使用 C、C++、Rust、Go 等多种语言编写代码,然后编译成 Wasm 格式。
+
+#### 1.1.2 Wasm 模块
+
+Wasm 模块主要有以下两种格式:
+- 二进制格式:Wasm 的主要编码格式,以 .wasm 后缀结尾。
+- 文本格式:主要是为了方便开发者理解 Wasm 模块,以 .wat 后缀结尾,相当于汇编语言程序。
+
+Wasm 模块二进制格式是 Wasm 二进制文件,Wasm 模块二进制格式也是以魔数和版本号开头,之后就是模块的主体内容,这些内容根据不同用途被分别放在不同的段(Section) 中。一共定义了 12 种段,每种段分配了 ID(从 0 到 11),依次有如下 12 个段:自定义段、类型段、导入段、函数段、表段、内存段、全局段、导出段、起始段、元素段、代码段、数据段。
+Wasm 模块二进制格式的组成如下图(图片来源 [WebAssembly 解释器实现篇](https://github.com/mcuking/blog/issues/96/))所示:
+![img](https://img.alicdn.com/imgextra/i1/O1CN01rLuxGp1zIX413ZQ0g_!!6000000006691-0-tps-1784-1266.jpg)
+
+每一个不同的段都描述了这个 Wasm 模块的一部分信息。而模块内的所有段放在一起,便描述了这个 Wasm 模块的全部信息:
+- 内存段和数据段:内存段是线性内存(linear memory)用于存储程序的运行时动态数据。数据段用于存储初始化内存的静态数据。内存可以从外部宿主导入,同时内存对象也可以导出到外部宿主环境。
+- 表段和元素段:表段用于存储对象引用,目前对象只能是函数,因此可以通过表段实现函数指针的功能。元素段用于存储初始化表段的数据。表对象可以从外部宿主导入,同时表对象也可以导出到外部宿主环境。
+- 起始段:起始段用于存储起始函数的索引,即指定了一个在加载时自动运行的函数。起始函数主要作用:1. 在模块加载后进行初始化工作; 2. 将模块变成可执行文件。
+- 全局段:全局段用于存储全局变量的信息(全局变量的值类型、可变性、初始化表达式等)。
+- 函数段、代码段和类型段:这三个段均是用于存储表达函数的数据。其中
+ - 类型段:类型段用于存储模块内所有的函数签名(函数签名记录了函数的参数和返回值的类型和数量),注意若存在多个函数的函数签名相同,则存储一份即可。
+ - 函数段:函数段用于存储函数对应的函数签名索引,注意是函数签名的索引,而不是函数索引。
+ - 代码段:代码段用于存储函数的字节码和局部变量,也就是函数体内的局部变量和代码所对应的字节码。
+- 导入段和导出段:导出段用于存储导出项信息(导出项的成员名、类型,以及在对应段中的索引等)。导入段用于存储导入项信息(导入项的成员名、类型,以及从哪个模块导入等)。导出/导入项类型有 4 种:函数、表、内存、全局变量。
+- 自定义段:自定义段主要用于保存调试符号等和运行无关的信息。
+
+关于 Wasm 模块二进制格式详细内容可以参考 [Wasm 模块 Binary Format](https://webassembly.github.io/spec/core/binary/modules.html)。
+
+Wasm 模块 wat 文本格式 使用了 `S- 表达式` 的形式来表达 Wasm 模块及其相关定义。关于 wat 格式的更多介绍可以参考 [理解 WebAssembly 文本格式](https://developer.mozilla.org/zh-CN/docs/WebAssembly/Understanding_the_text_format)。
+下图(图片来源 [WebAssembly 解释器实现篇](https://github.com/mcuking/blog/issues/96/))就是使用 C 语言编写的阶乘函数,以及对应的 Wasm 文本格式和二进制格式。
+![img](https://img.alicdn.com/imgextra/i4/O1CN01VcoLBQ1ZcWL5XHYIR_!!6000000003215-0-tps-1892-878.jpg)
+
+可以通过 [WebAssembly Code Explorer](https://wasdk.github.io/wasmcodeexplorer/) 更直观地查看 Wasm 二进制格式和文本格式之间的关联。也可以通过 [wabt](https://github.com/WebAssembly/wabt) 提供工具 ,可以方便的进行 Wasm 二进制格式和文本格式的转换。
+
+#### 1.1.3 Wasm 指令集
+
+Wasm 指令集包含如下内容:
+- Wasm 指令主要分为控制指令、参数指令、变量指令、内存指令和数值指令,每条指令包含操作码和操作数。感兴趣的可以点击查看下 [Wasm 所有的操作码](https://pengowray.github.io/wasm-ops/), 可视化表格直观地展示了 Wasm 所有的操作码。
+- 只有四种数据类型: i32、i64、f32、f64
+- 指令基于栈,并且支持递归调用。例如 i32.add 从栈弹出两个 i32 类型的值,并将它们相加,然后将结果压入栈。
+- 从内存读取数据
+ - i32.load 从内存中读取一个 i32 类型的值。
+ - i32.const value 将一个 i32 类型的值压入栈。
+ - 从线性内存读取数据
+
+关于更多 Wasm 解释器实现原理的可以参考 [WebAssembly 解释器实现篇](https://github.com/mcuking/blog/issues/96)。
+
+### 1.2 TinyGo
+
+[TinyGo](https://tinygo.org/) 是一个 Go 语言编译器,它专注于生成小型、高效的 Go 程序,特别是为嵌入式系统和 WebAssembly 环境设计。 TinyGo 与 Go 语言的标准编译器不同,它有以下优势:
+
+- 生成小型二进制文件:TinyGo 优化了生成的二进制文件的大小,使其非常适合资源受限的环境。
+- 简化的 Go 标准库:TinyGo 提供了一个简化版本的 Go 标准库,减少了依赖和复杂性。TinyGo 支持标准库详情:https://tinygo.org/docs/reference/lang-support/stdlib/ 。
+- 跨平台编译:TinyGo 支持跨平台编译,允许开发者为不同的目标平台生成代码。
+- 支持 WebAssembly:通过使用 TinyGo,开发者可以为 WebAssembly 环境编写高效的 Go 应用程序,同时利用 Go 语言的简洁性和易用性。
+
+“为什么不使用官方 Go 编译器?”的问题,如果对细节感兴趣,请参考 Go 仓库中的相关 issue:
+
+- https://github.com/golang/go/issues/25612
+- https://github.com/golang/go/issues/31105
+- https://github.com/golang/go/issues/38248
+
+这些 issue 讨论了官方 Go 编译器在生成 Wasm 支持方面的限制和进展。 TinyGo 作为一个替代方案,能够生成适合 [Proxy-Wasm ABI](https://github.com/proxy-wasm/spec) 规范的 Wasm 二进制文件,这使得它成为开发 Proxy-Wasm 应用程序的理想选择。
+
+### 1.3 Proxy-Wasm Go SDK
+
+[Proxy-Wasm Go SDK](https://github.com/higress-group/proxy-wasm-go-sdk) 依赖于 TinyGo, 同时 Proxy-Wasm Go SDK 是基于 [Proxy-Wasm ABI](https://github.com/proxy-wasm/spec) 规范使用 Go 编程语言扩展网络代理(例如 Envoyproxy)的 SDK, 而 Proxy-Wasm ABI 定义了网络代理和在网络代理内部运行的 Wasm 虚拟机之间的接口。
+通过这个 SDK,可以轻松地生成符合 Proxy-Wasm 规范的 Wasm 二进制文件,而无需了解 Proxy-Wasm ABI 规范,同时开发人员可以依赖这个 SDK 的 Go API 来开发插件扩展 Enovy 功能。
+
+
+## 2 Wasm VM、插件和 Envoy 配置
+
+Wasm 虚拟机(Wasm VM) 或简称 VM 指的是加载 Wasm 程序的实例。 在 Envoy 中,VM 通常在每个线程中创建并相互隔离。因此 Wasm 程序将复制到 Envoy 所创建的线程里,并在这些虚拟机上加载并执行。
+插件提供了一种灵活的方式来扩展和自定义 Envoy 的行为。Proxy-Wasm 规范允许在每个 VM 中配置多个插件。因此一个 VM 可以被多个插件共同使用。Envoy 中有三种类型插件: `Http Filter`、`Network Filter` 和 `Wasm Service`。
+
+- `Http Filter` 是一种处理 Http 协议的插件,例如操作 Http 请求头、正文等。
+- `Network Filter` 是一种处理 Tcp 协议的插件,例如操作 Tcp 数据帧、连接建立等。
+- `Wasm Service` 是在单例 VM 中运行的插件类型(即在 Envoy 主线程中只有一个实例)。它主要用于执行与 `Network Filter` 或 `Http Filter` 并行的一些额外工作,如聚合指标、日志等。这样的单例 VM 本身也被称为 `Wasm Service`。
+
+其架构如下图(图片来源 [Proxy-Wasm Go SDK](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/doc/OVERVIEW.md)):
+![img](https://img.alicdn.com/imgextra/i4/O1CN018UJzEX1YlqnAmBV4u_!!6000000003100-0-tps-2321-1190.jpg)
+
+
+### 2.1 Envoy 配置
+
+所有类型插件的配置都包含 `vm_config` 用于配置 Wasm VM, 和 `configuration` 用于配置插件实例。
+
+```yaml
+vm_config:
+ vm_id: "foo"
+ runtime: "envoy.wasm.runtime.v8"
+ configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: '{"my-vm-env": "dev"}'
+ code:
+ local:
+ filename: "example.wasm"
+configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: '{"my-plugin-config": "bar"}'
+```
+
+配置说明如下:
+
+| 字段 | 描述 |
+|--------------------------|-----------------------------------------------|
+| `vm_config` | 配置 Wasm VM |
+| `vm_config.vm_id` | 用于跨 VM 通信的语义隔离。详情请参考 跨 VM 通信 部分。 |
+| `vm_config.runtime` | 指定 Wasm 运行时类型。默认为 envoy.wasm.runtime.v8。 |
+| `vm_config.configuration` | 用于设置 VM 的配置数据 |
+| `vm_config.code` | Wasm 二进制文件的位置 |
+| `configuration` | 对应于每个插件实例配置(在下面介绍的 PluginContext)。 |
+
+> 完全相同的 vm_config 配置的多个插件它们之间共享一个 Wasm VM,单个 Wasm VM 用于多个 Http Filter 或 Network Filter,可以提升内存/CPU 资源效率、降低启动延迟。
+> 完整的 Envoy API 配置可以 [参考 Envoy 文档](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/wasm/v3/wasm.proto#envoy-v3-api-msg-extensions-wasm-v3-pluginconfig)。
+
+Envoy Wasm 运行时目前有以下几种选择:
+- envoy.wasm.runtime.null:这表示一个空的沙盒(null sandbox)环境,Wasm 模块必须被编译并链接到 Envoy 的二进制文件中。这种方式适用于那些需要将 Wasm 模块与 Envoy 二进制文件一起分发的部署场景。
+- envoy.wasm.runtime.v8: 基于 V8 JavaScript 引擎的运行时。
+- envoy.wasm.runtime.wamr: WAMR (WebAssembly Micro Runtime) 运行时。
+- envoy.wasm.runtime.wasmtime: Wasmtime 运行时。
+
+不同的运行时有各自的优缺点,比如 [V8](https://v8.dev/) 性能较好但容器体积较大,[WAMR](https://github.com/bytecodealliance/wasm-micro-runtime) 和 [wasmtime](https://wasmtime.dev/) 则相对轻量。
+
+> [待补充?] envoy v8 runtime 如何加载 wasm 和 如何和 envoy 交互原理。
+
+### 2.2 Http Filter 配置
+
+Http Filter 插件配置设置为 `envoy.filter.http.wasm`,Http Filter 插件可以处理 HTTP 请求和响应。 其主要配置如下:
+```yaml
+http_filters:
+ - name: envoy.filters.http.wasm
+ typed_config:
+ "@type": type.googleapis.com/udpa.type.v1.TypedStruct
+ type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ value:
+ config:
+ configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: |
+ {
+ "header": "x-wasm-header",
+ "value": "demo-wasm"
+ }
+ vm_config:
+ runtime: "envoy.wasm.runtime.v8"
+ code:
+ local:
+ filename: "./examples/http_headers/main.wasm"
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+```
+
+这时 Envoy 会在每个工作线程中实例化一个 Wasm 虚拟机,该虚拟机将专门用于处理该线程上的 HTTP 请求和响应。每个虚拟机都会加载和执行 WebAssembly 代码,允许对 HTTP 流量进行自定义处理,如修改头信息、处理请求和响应体等。
+完整的配置可以参考 [envoy.yaml](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/examples/http_headers/envoy.yaml) 。
+
+
+### 2.3 Network Filter 配置
+
+`Network Filter` 插件配置设置为 `envoy.filters.network.wasm`,`Network Filter` 插件可以处理 TCP 请求和响应。 其主要配置如下:
+
+```yaml
+filter_chains:
+- filters:
+ - name: envoy.filters.network.wasm
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm
+ config:
+ vm_config: { ... }
+ # ... plugin config follows
+ - name: envoy.tcp_proxy
+```
+
+这时 Envoy 会在每个工作线程中实例化一个 Wasm 虚拟机,该虚拟机将专门用于处理该线程上的 TCP 请求和响应。每个虚拟机都会加载和执行 WebAssembly 代码,允许对 TCP 流量进行自定义处理等。
+完整的配置可以参考 [envoy.yaml](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/examples/network/envoy.yaml) 。
+
+
+### 2.4 Wasm Service 配置
+
+`Wasm Service` 插件配置设置为 `envoy.bootstrap.wasm`。插件在 Envoy 启动时加载的,其主要配置如下:
+
+```yaml
+bootstrap_extensions:
+- name: envoy.bootstrap.wasm
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.wasm.v3.WasmService
+ singleton: true
+ config:
+ vm_config: { ... }
+ # ... plugin config follows
+```
+
+`singleton` 设置为 true 时,生成虚拟机(VM)是单例,并且运行在 Envoy 的主线程上,因此它不会阻塞任何工作线程。
+
+完整的配置可以参考 [envoy.yaml](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/examples/shared_queue/envoy.yaml) 。
+
+### 2.5 每个线程中多个插件共享一个 VM
+
+每个线程中多个插件共享一个 VM,其主要配置如下:
+
+```yaml
+static_resources:
+ listeners:
+ - name: http-header-operation
+ address:
+ socket_address:
+ address: 0.0.0.0
+ port_value: 18000
+ filter_chains:
+ - filters:
+ - name: envoy.http_connection_manager
+ typed_config:
+ # ....
+ http_filters:
+ - name: envoy.filters.http.wasm
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ config:
+ configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: "http-header-operation"
+ vm_config:
+ vm_id: "my-vm-id"
+ runtime: "envoy.wasm.runtime.v8"
+ configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: "my-vm-configuration"
+ code:
+ local:
+ filename: "all-in-one.wasm"
+ - name: envoy.filters.http.router
+
+ - name: http-body-operation
+ address:
+ socket_address:
+ address: 0.0.0.0
+ port_value: 18001
+ filter_chains:
+ - filters:
+ - name: envoy.http_connection_manager
+ typed_config:
+ # ....
+ http_filters:
+ - name: envoy.filters.http.wasm
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ config:
+ configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: "http-body-operation"
+ vm_config:
+ vm_id: "my-vm-id"
+ runtime: "envoy.wasm.runtime.v8"
+ configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: "my-vm-configuration"
+ code:
+ local:
+ filename: "all-in-one.wasm"
+ - name: envoy.filters.http.router
+
+ - name: tcp-total-data-size-counter
+ address:
+ socket_address:
+ address: 0.0.0.0
+ port_value: 18002
+ filter_chains:
+ - filters:
+ - name: envoy.filters.network.wasm
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm
+ config:
+ configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: "tcp-total-data-size-counter"
+ vm_config:
+ vm_id: "my-vm-id"
+ runtime: "envoy.wasm.runtime.v8"
+ configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: "my-vm-configuration"
+ code:
+ local:
+ filename: "all-in-one.wasm"
+ - name: envoy.tcp_proxy
+ typed_config: # ...
+```
+
+
+
+在 `18000` 和 `18001` 监听器上的 Http 过滤器链以及 `18002` 上的网络过滤器链中,vm_config 字段都是相同的。在这种情况下,Envoy 中的多个插件将使用同一个 Wasm 虚拟机。 为了重用相同的 VM,所有的 vm_config.vm_id、vm_config.runtime、vm_config.configuration 和 vm_config.code 必须相同。
+
+通过这种配置方式允许为不同的过滤器重用同一个 Wasm 虚拟机,通过为每个 `PluginContext` 提供了一个隔离的环境,使得插件能够独立运行,同时共享同一个虚拟机的执行环境,虚拟机只需要加载和初始化一次即可为多个插件服务,这不仅可以减少内存占用,还可以降低启动时的延迟。
+
+完整的配置可以参考 [envoy.yaml](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/examples/shared_queue/envoy.yaml) 。
+
+## 3 Proxy-Wasm Go SDK API
+
+上面介绍插件概念和插件配置,下面开始深入探讨 Proxy-Wasm Go SDK 的 API。
+
+### 3.1 Contexts
+
+上下文(Contexts) 是 Proxy-Wasm Go SDK 中的接口集合,它们在 [types](https://github.com/higress-group/proxy-wasm-go-sdk/tree/main/proxywasm/types) 包中定义。
+有四种类型的上下文:VMContext、PluginContext、TcpContext 和 HttpContext。它们的关系如下图:
+```
+ Wasm Virtual Machine
+ (.vm_config.code)
+┌────────────────────────────────────────────────────────────────┐
+│ Your program (.vm_config.code) TcpContext │
+│ │ ╱ (Tcp stream) │
+│ │ 1: 1 ╱ │
+│ │ 1: N ╱ 1: N │
+│ VMContext ────────── PluginContext │
+│ (Plugin) ╲ 1: N │
+│ ╲ │
+│ ╲ HttpContext │
+│ (Http stream) │
+└────────────────────────────────────────────────────────────────┘
+```
+
+1) VMContext 对应于每个 .vm_config.code,每个 VM 中只存在一个 VMContext。
+2) VMContext 是 PluginContexts 的父上下文,负责创建 PluginContext。
+3) PluginContext 对应于一个 Plugin 实例。一个 PluginContext 对应于 Http Filter、Network Filter、Wasm Service 的 configuration 字段配置。
+4) PluginContext 是 TcpContext 和 HttpContext 的父上下文,并且负责为 处理 Http 流的Http Filter 或 处理 Tcp 流的 Network Filter 创建上下文。
+5) TcpContext 负责处理每个 Tcp 流。
+6) HttpContext 负责处理每个 Http 流。
+
+因此,自定义插件要实现 `VMContext` 和 `PluginContext`。 同时 `Http Filter` 或 `Network Filter`,要分别实现 `HttpContext` 或 `TcpContext`。
+
+首先 VMContext 定义如下:
+
+```go
+type VMContext interface {
+ // OnVMStart 在 VM 创建和调用 main 函数后被调用。
+ // 在此调用期间,可以通过 GetVMConfiguration 获取在 vm_config.configuration 设置的配置。
+ // 这主要用于执行 Wasm VM 范围内的初始化。
+ OnVMStart(vmConfigurationSize int) OnVMStartStatus
+
+ // NewPluginContext 用于为每个插件配置创建 PluginContext。
+ NewPluginContext(contextID uint32) PluginContext
+}
+```
+
+VMContext 负责通过 NewPluginContext 方法创建 PluginContext。同时在 VM 启动阶段调用 OnVMStart,并且可以通过 `GetVMConfiguration` hostcall API 获取 vm_config.configuration 的值。这样就可以进行 VM 范围内的插件初始化并控制 VMContext 的行为。
+
+PluginContext,定义如下(省略了一些方法):
+```go
+type PluginContext interface {
+ // OnPluginStart 在所有插件上下文上调用(如果在这是 VM 上下文,则在 OnVmStart 之后)。
+ // 在此调用期间,可以通过 GetPluginConfiguration 获取 envoy.yaml 中 config.configuration 设置的配置。
+ OnPluginStart(pluginConfigurationSize int) OnPluginStartStatus
+
+ // 以下函数用于在流上创建上下文,
+ // *必须* 实现它们中的任一个,对应于扩展点。例如,如果您配置此插件上下文在 Http 过滤器上运行,那么必须实现 NewHttpContext。
+ // 对 Tcp 过滤器也是如此。
+ //
+ // NewTcpContext 用于为每个 Tcp 流创建 TcpContext。
+ NewTcpContext(contextID uint32) TcpContext
+ // NewHttpContext 用于为每个 Http 流创建 HttpContext。
+ NewHttpContext(contextID uint32) HttpContext
+}
+```
+
+`PluginContext` 有 `OnPluginStart` 方法,创建插件时调用,可以通过 GetPluginConfiguration hostcall API 获取 plugin config 中 configuration 字段的值。
+另外 `PluginContext` 有 `NewTcpContext` 和 `NewHttpContext` 方法,为每个 Http 或 Tcp 流创建上下文时调用。 关于 HttpContext 和 TcpContext 的详细定义请参考 [context.go](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/proxywasm/types/context.go) 。
+
+### 3.2 Hostcall API
+
+Hostcall API 是指在 Wasm 模块内调用 Envoy 提供的功能。这些功能通常用于获取外部数据或与 Envoy 交互。在开发 Wasm 插件时,需要访问网络请求的元数据、修改请求或响应头、记录日志等,这些都可以通过 Hostcall API 来实现。
+Hostcall API 在 proxywasm 包的 [hostcall.go](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/proxywasm/hostcall.go) 中定义。
+Hostcall API 包括配置和初始化、定时器设置、上下文管理、插件完成、共享队列管理、Redis 操作、Http 调用、TCP 流操作、HTTP 请求/响应头和体操作、共享数据操作、日志操作、属性和元数据操作、指标操作。具体函数名称和描述如下:
+
+#### 1.配置和初始化
+| 函数名 | 描述 |
+|--------------------------------|--------------------------------------------------------------|
+| `GetVMConfiguration` | 获取在 `vm_config.configuration` 字段中指定的配置。此功能仅在 `types.PluginContext.OnVMStart` 调用期间可用。 |
+| `GetPluginConfiguration` | 获取在 `config.configuration` 字段中指定的配置。此功能仅在 `types.PluginContext.OnPluginStart` 调用期间可用。 |
+
+#### 2.定时器设置
+| 函数名 | 描述 |
+|--------------------------------|--------------------------------------------------------------|
+| `SetTickPeriodMilliSeconds` | 设置 `types.PluginContext.OnTick` 调用的tick间隔。此功能仅对 `types.PluginContext` 有效。 |
+
+#### 3.上下文管理
+| 函数名 | 描述 |
+|--------------------------------|--------------------------------------------------------------|
+| `SetEffectiveContext` | 设置有效上下文为 `context_id`。通常用于在接收到 `types.PluginContext.OnQueueReady` 或 `types.PluginContext.OnTick` 后更改上下文。 |
+
+#### 4.插件完成
+| 函数名 | 描述 |
+|--------------------------------|-----------------------------------------------------------------------------------|
+| `PluginDone` | 当 `OnPluginDone` 返回 false,表示插件处于待定状态,在删除之前必须调用此函数。此功能仅对 `types.PluginContext` 有效。 |
+
+#### 5.共享队列管理
+| 函数名 | 描述 |
+|--------------------------------|-------------------------------------------------------------|
+| `RegisterSharedQueue` | 在此插件上下文中注册共享队列。 |
+| `ResolveSharedQueue` | 获取给定 `vmID` 和 `queueName` 的队列ID。 |
+| `EnqueueSharedQueue` | 将数据排队到给定队列ID的共享队列。 |
+| `DequeueSharedQueue` | 从给定队列ID的共享队列中出队数据。 |
+
+#### 6.Redis 操作
+| 函数名 | 描述 |
+|--------------------------------|--------------------------------------------------------------|
+| `RedisInit` | 初始化Redis连接。 |
+| `DispatchRedisCall` | 发送Redis调用。 |
+| `GetRedisCallResponse` | 获取Redis调用响应。 |
+
+#### 7.HTTP 调用
+| 函数名 | 描述 |
+|--------------------------------|--------------------------------------------------------------|
+| `DispatchHttpCall` | 向远程集群分派HTTP调用。此功能可被所有上下文使用。 |
+| `GetHttpCallResponseHeaders` | 用于检索由远程集群返回的HTTP响应头,此功能仅在传递给 `DispatchHttpCall` 的 "callback" 函数中可用。 |
+| `GetHttpCallResponseBody` | 用于检索由远程集群返回的HTTP响应体,此功能仅在传递给 `DispatchHttpCall` 的 "callback" 函数中可用。 |
+| `GetHttpCallResponseTrailers` | 用于检索由远程集群返回的HTTP响应尾随头,此功能仅在传递给 `DispatchHttpCall` 的 "callback" 函数中可用。 |
+
+#### 8.TCP 流操作
+| 函数名 | 描述 |
+|--------------------------------|--------------------------------------------------------------|
+| `GetDownstreamData` | 用于检索在宿主中缓冲的TCP下游数据。此功能仅在 `types.TcpContext.OnDownstreamData` 期间可用。 |
+| `AppendDownstreamData` | 将给定字节追加到宿主中缓冲的TCP下游数据。此功能仅在 `types.TcpContext.OnDownstreamData` 期间可用。 |
+| `PrependDownstreamData` | 将给定字节前缀到宿主中缓冲的TCP下游数据。此功能仅在 `types.TcpContext.OnDownstreamData` 期间可用。 |
+| `ReplaceDownstreamData` | 用给定字节替换宿主中缓冲的TCP下游数据。此功能仅在 `types.TcpContext.OnDownstreamData` 期间可用。 |
+| `GetUpstreamData` | 用于检索在宿主中缓冲的TCP上游数据。此功能仅在 `types.TcpContext.OnUpstreamData` 期间可用。 |
+| `AppendUpstreamData` | 将给定字节追加到宿主中缓冲的TCP上游数据。此功能仅在 `types.TcpContext.OnUpstreamData` 期间可用。 |
+| `PrependUpstreamData` | 将给定字节前缀到宿主中缓冲的TCP上游数据。此功能仅在 `types.TcpContext.OnUpstreamData` 期间可用。 |
+| `ReplaceUpstreamData` | 用给定字节替换宿主中缓冲的TCP上游数据。此功能仅在 `types.TcpContext.OnUpstreamData` 期间可用。 |
+| `ContinueTcpStream` | 在返回 `types.Action.Pause` 后,继续TCP连接的处理。此功能仅对 `types.TcpContext` 有效。 |
+| `CloseDownstream` | 关闭Tcp上下文中的下游TCP连接。此功能仅对 `types.TcpContext` 有效。 |
+| `CloseUpstream` | 关闭Tcp上下文中的上游TCP连接。此功能仅对 `types.TcpContext` 有效。 |
+
+#### 9.HTTP 请求/响应头和体操作
+| 函数名 | 描述 |
+|-------------------------------|--------------------------------------------------------------|
+| `GetHttpRequestHeaders` | 获取HTTP请求头。此功能仅在 `types.HttpContext.OnHttpRequestHeaders` 和 `types.HttpContext.OnHttpStreamDone` 期间可用。 |
+| `ReplaceHttpRequestHeaders` | 用给定的头替换HTTP请求头。此功能仅在 `types.HttpContext.OnHttpRequestHeaders` 期间可用。 |
+| `GetHttpRequestHeader` | 获取给定 "key" 的HTTP请求头的值。此功能仅在 `types.HttpContext.OnHttpRequestHeaders` 和 `types.HttpContext.OnHttpStreamDone` 期间可用。 |
+| `RemoveHttpRequestHeader` | 移除请求头中给定 "key" 的所有值。此功能仅在 `types.HttpContext.OnHttpRequestHeaders` 期间可用。 |
+| `ReplaceHttpRequestHeader` | 替换请求头中给定 "key" 的值。此功能仅在 `types.HttpContext.OnHttpRequestHeaders` 期间可用。 |
+| `AddHttpRequestHeader` | 向请求头添加给定 "key" 的值。此功能仅在 `types.HttpContext.OnHttpRequestHeaders` 期间可用。 |
+| `GetHttpRequestBody` | 获取整个HTTP请求体。此功能仅在 `types.HttpContext.OnHttpRequestBody` 期间可用。 |
+| `AppendHttpRequestBody` | 向HTTP请求体缓冲区追加给定字节。此功能仅在 `types.HttpContext.OnHttpRequestBody` 期间可用。 |
+| `PrependHttpRequestBody` | 向HTTP请求体缓冲区前缀给定字节。此功能仅在 `types.HttpContext.OnHttpRequestBody` 期间可用。 |
+| `ReplaceHttpRequestBody` | 用给定字节替换HTTP请求体缓冲区。此功能仅在 `types.HttpContext.OnHttpRequestBody` 期间可用。 |
+| `GetHttpRequestTrailers` | 获取HTTP请求尾随头。此功能仅在 types.HttpContext.OnHttpRequestTrailers 和 types.HttpContext.OnHttpStreamDone 期间可用。 |
+| `ReplaceHttpRequestTrailers` | 用给定的尾随头替换HTTP请求尾随头。此功能仅在 types.HttpContext.OnHttpRequestTrailers 期间可用。 |
+| `GetHttpRequestTrailer` | 获取给定 "key" 的HTTP请求尾随头的值。此功能仅在 types.HttpContext.OnHttpRequestTrailers 和 types.HttpContext.OnHttpStreamDone 期间可用。 |
+| `RemoveHttpRequestTrailer` | 移除请求尾随头中给定 "key" 的所有值。此功能仅在 types.HttpContext.OnHttpRequestTrailers 期间可用。 |
+| `ReplaceHttpRequestTrailer` | 替换请求尾随头中给定 "key" 的值。此功能仅在 types.HttpContext.OnHttpRequestTrailers 期间可用。 |
+| `AddHttpRequestTrailer` | 向请求尾随头添加给定 "key" 的值。此功能仅在 types.HttpContext.OnHttpRequestTrailers 期间可用。 |
+| `ResumeHttpRequest` | 继续停止的HTTP请求处理。此功能仅在 types.HttpContext 期间可用。 |
+| `GetHttpResponseHeaders` | 获取HTTP响应头。此功能仅在 types.HttpContext.OnHttpResponseHeaders 和 types.HttpContext.OnHttpStreamDone 期间可用。 |
+| `ReplaceHttpResponseHeaders` | 用给定的头替换HTTP响应头。此功能仅在 types.HttpContext.OnHttpResponseHeaders 期间可用。 |
+| `GetHttpResponseHeader ` | 获取给定 "key" 的HTTP响应头的值。此功能仅在 types.HttpContext.OnHttpResponseHeaders 和 types.HttpContext.OnHttpStreamDone 期间可用。 |
+| `RemoveHttpResponseHeader` | 移除响应头中给定 "key" 的所有值。此功能仅在 types.HttpContext.OnHttpResponseHeaders 期间可用。 |
+| `ReplaceHttpResponseHeader` | 替换响应头中给定 "key" 的值。此功能仅在 types.HttpContext.OnHttpResponseHeaders 期间可用。 |
+| `AddHttpResponseHeader` | 向响应头添加给定 "key" 的值。此功能仅在 types.HttpContext.OnHttpResponseHeaders 期间可用。 |
+| `GetHttpResponseBody` | 获取整个HTTP响应体。此功能仅在 types.HttpContext.OnHttpResponseBody 期间可用。 |
+| `AppendHttpResponseBody` | 向HTTP响应体缓冲区追加给定字节。此功能仅在 types.HttpContext.OnHttpResponseBody 期间可用。 |
+| `PrependHttpResponseBody` | 向HTTP响应体缓冲区前缀给定字节。此功能仅在 types.HttpContext.OnHttpResponseBody 期间可用。 |
+| `ReplaceHttpResponseBody` | 用给定字节替换HTTP响应体缓冲区。此功能仅在 types.HttpContext.OnHttpResponseBody 期间可用。 |
+| `GetHttpResponseTrailers` | 获取HTTP响应尾随头。此功能仅在 types.HttpContext.OnHttpResponseTrailers 和 types.HttpContext.OnHttpStreamDone 期间可用。 |
+| `ReplaceHttpResponseTrailers` | 用给定的尾随头替换HTTP响应尾随头。此功能仅在 types.HttpContext.OnHttpResponseTrailers 期间可用。 |
+| `GetHttpResponseTrailer` | 获取给定 "key" 的HTTP响应尾随头的值。此功能仅在 types.HttpContext.OnHttpResponseTrailers 和 types.HttpContext.OnHttpStreamDone 期间可用。 |
+| `RemoveHttpResponseTrailer` | 移除响应尾随头中给定 "key" 的所有值。此功能仅在 types.HttpContext.OnHttpResponseTrailers 期间可用。 |
+| `ReplaceHttpResponseTrailer` | 替换响应尾随头中给定 "key" 的值。此功能仅在 types.HttpContext.OnHttpResponseHeaders 期间可用。 |
+| `AddHttpResponseTrailer` | 向响应尾随头添加给定 "key" 的值。此功能仅在 types.HttpContext.OnHttpResponseHeaders 期间可用。 |
+| `ResumeHttpResponse` | 继续停止的HTTP响应处理。此功能仅在 types.HttpContext 期间可用。 |
+| `SendHttpResponse` | 向下游发送HTTP响应。调用此函数后,您必须返回 types.Action.Pause 以停止初始HTTP请求/响应的进一步处理。 |
+
+
+#### 10.共享数据操作
+| 函数名 | 描述 |
+|--------------------------------|---------|
+| `GetSharedData` | 获取共享数据。 |
+| `SetSharedData` | 设置共享数据。 |
+
+#### 11.日志操作
+| 函数名 | 描述 |
+|--------------------------------|--------------------------------------------------------------|
+| `LogTrace` | 以 Trace 日志级别发出消息。 |
+| `LogTracef` | 根据格式说明符格式化并发出 Trace 日志级别的日志。 |
+| `LogDebug` | 以 Debug 日志级别发出消息。 |
+| `LogDebugf` | 根据格式说明符格式化并发出 Debug 日志级别的日志。 |
+| `LogInfo` | 以 Info 日志级别发出消息。 |
+| `LogInfof` | 根据格式说明符格式化并发出 Info 日志级别的日志。 |
+| `LogWarn` | 以 Warn 日志级别发出消息。 |
+| `LogWarnf` | 根据格式说明符格式化并发出 Warn 日志级别的日志。 |
+| `LogError` | 以 Error 日志级别发出消息。 |
+| `LogErrorf` | 根据格式说明符格式化并发出 Error 日志级别的日志。 |
+| `LogCritical` | 以 Critical 日志级别发出消息。 |
+| `LogCriticalf` | 根据格式说明符格式化并发出 Critical 日志级别的日志。 |
+
+#### 12.指标操作
+| 函数名 | 描述 |
+|--------------------------------|--------------------------------------------------------------|
+| `DefineCounterMetric` | 为名称定义一个计数器指标。返回一个 `MetricCounter` 用于后续操作。 |
+| `DefineGaugeMetric` | 为名称定义一个计量器指标。返回一个 `MetricGauge` 用于后续操作。 |
+| `DefineHistogramMetric` | 为名称定义一个直方图指标。返回一个 `MetricHistogram` 用于后续操作。 |
+| `MetricCounter.Value` | 获取 `MetricCounter` 的当前值。 |
+| `MetricCounter.Increment` | 将 `MetricCounter` 的当前值增加指定的偏移量。 |
+| `MetricGauge.Value` | 获取 `MetricGauge` 的当前值。 |
+| `MetricGauge.Add` | 将 `MetricGauge` 的当前值增加指定的偏移量。 |
+| `MetricHistogram.Value` | 获取 `MetricHistogram` 的当前值。 |
+| `MetricHistogram.Record` | 为 `MetricHistogram` 记录一个值。 |
+
+
+
+### 3.3 插件调用入口 Entrypoint
+
+当 Envoy 创建 VM 时,在虚拟机内部创建 `VMContext` 之前,它会在启动阶段调用插件程序的 `main` 函数。所以必须在 `main` 函数中传递插件自定义的 `VMContext` 实现。
+[proxywasm](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/proxywasm/) 包的 `SetVMContext` 函数是入口点。`main` 函数如下:
+
+```go
+func main() {
+ proxywasm.SetVMContext(&myVMContext{})
+}
+
+type myVMContext struct { .... }
+
+var _ types.VMContext = &myVMContext{}
+
+// Implementations follow...
+```
+
+## 4 跨虚拟机通信
+
+Envoy 中的跨虚拟机通信(Cross-VM communications)允许在不同线程中运行 的Wasm 虚拟机(VMs)之间进行数据交换和通信。这在需要在多个VMs之间聚合数据、统计信息或缓存数据等场景中非常有用。
+跨虚拟机通信主要有两种方式:
+
+- 共享数据(Shared Data):
+ - 共享数据是一种在所有 VMs 之间共享的键值存储,可以用于存储和检索简单的数据项。
+ - 它适用于存储小的、不经常变化的数据,例如配置参数或统计信息。
+- 共享队列(Shared Queue):
+ - 共享队列允许VMs之间进行更复杂的数据交换,支持发送和接收更丰富的数据结构。
+ - 队列可以用于实现任务调度、异步消息传递等模式。
+
+### 4.1 共享数据(Shared Data)
+
+如果想要在所有 Wasm 虚拟机(VMs)运行的多个工作线程间拥有全局请求计数器,或者想要缓存一些应被所有 Wasm VMs 使用的数据,那么共享数据(Shared Data)或等效的共享键值存储(Shared KVS)就会发挥作用。
+共享数据本质上是一个跨所有VMs共享的键值存储(即跨 VM 或跨线程)。
+
+共享数据 KVS 是根据 vm_config 中指定的创建的。可以在所有 Wasm VMs 之间共享一个键值存储,而它们不必具有相同的二进制文件 `vm_config.code`,唯一的要求是具有相同的 vm_id。
+
+![img](https://img.alicdn.com/imgextra/i2/O1CN01fLn4Lr1GXxhKORL9t_!!6000000000633-0-tps-1784-1266.jpg)
+
+在上图(图片来源 [Proxy-Wasm Go SDK](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/doc/OVERVIEW.md))中,可以看到即使它们具有不同的二进制文件( hello.wasm 和 bye.wasm ),"vm_id=foo"的 VMs 也共享相同的共享数据存储。
+hostcall.go 中定义共享数据相关的 API如下:
+```go
+// GetSharedData 用于检索给定 "key" 的值。
+// 返回的 "cas" 应用于 SetSharedData 以实现该键的线程安全更新。
+func GetSharedData(key string) (value []byte, cas uint32, err error)
+
+// SetSharedData 用于在共享数据存储中设置键值对。
+// 共享数据存储按主机中的 "vm_config.vm_id" 定义。
+//
+// 当给定的 CAS 值与当前值不匹配时,将返回 ErrorStatusCasMismatch。
+// 这表明其他 Wasm VM 已经成功设置相同键的值,并且该键的当前 CAS 已递增。
+// 建议在遇到此错误时实现重试逻辑。
+//
+// 将 cas 设置为 0 将永远不会返回 ErrorStatusCasMismatch 并且总是成功的,
+// 但这并不是线程安全的,即可能在您调用此函数时另一个 VM 已经设置了该值,
+// 看到的值与存储时的值已经不同。
+func SetSharedData(key string, value []byte, cas uint32) error
+```
+
+共享数据 API 是其线程安全性和跨 VM 安全性,这通过 "cas" ([Compare-And-Swap](https://en.wikipedia.org/wiki/Compare-and-swap))值来实现。如何使用 GetSharedData 和 SetSharedData 函数可以参考 [示例](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/examples/shared_data/main.go)。
+
+
+### 4.2 共享队列 Shared Queue
+
+如果要在请求/响应处理的同时跨所有 Wasm VMs 聚合指标,或者将一些跨 VM 聚合的信息推送到远程服务器,可以通过 *Shared Queue* 来实现。
+
+*Shared Queue* 是为 `vm_id` 和队列名称的组合创建的 FIFO(先进先出)队列。并为该组合(`vm_id`,名称)分配了一个唯一的 *queue id*,该 ID 用于入队/出队操作。
+
+“入队”和“出队”等操作具有线程安全性和跨 VM 安全性。在 hostcall.go 中与 *Shared Queue* 相关 API 如下:
+
+```go
+// DequeueSharedQueue 从给定 queueID 的共享队列中出队数据。
+// 要获取目标队列的 queue id,请先使用 "ResolveSharedQueue"。
+func DequeueSharedQueue(queueID uint32) ([]byte, error)
+
+// RegisterSharedQueue 在此插件上下文中注册共享队列。
+// "注册" 意味着每当该 queueID 上有新数据入队时,将对此插件上下文调用 OnQueueReady。
+// 仅适用于 types.PluginContext。返回的 queueID 可用于 Enqueue/DequeueSharedQueue。
+// 请注意 "name" 必须在所有共享相同 "vm_id" 的 Wasm VMs 中是唯一的。使用 "vm_id" 来分隔共享队列的命名空间。
+//
+// 只有在调用 RegisterSharedQueue 之后,ResolveSharedQueue("此 vm_id", "名称") 才能成功
+// 通过其他 VMs 检索 queueID。
+func RegisterSharedQueue(name string) (queueID uint32, err error)
+
+// EnqueueSharedQueue 将数据入队到给定 queueID 的共享队列。
+// 要获取目标队列的 queue id,请先使用 "ResolveSharedQueue"。
+func EnqueueSharedQueue(queueID uint32, data []byte) error
+
+// ResolveSharedQueue 获取给定 vmID 和队列名称的 queueID。
+// 返回的 queueID 可用于 Enqueue/DequeueSharedQueue。
+func ResolveSharedQueue(vmID, queueName string) (queueID uint32, err error)
+```
+
+`RegisterSharedQueue` 和 `DequeueSharedQueue` 由队列的“消费者”使用,而 `ResolveSharedQueue` 和 `EnqueueSharedQueue` 是为队列“生产者”准备的。请注意:
+
+- RegisterSharedQueue 用于为调用者的 name 和 vm_id 创建共享队列。使用一个队列,那么必须先由一个 VM 调用这个函数。这可以由 PluginContext 调用,因此可以认为“消费者” = PluginContexts。
+- ResolveSharedQueue 用于获取 name 和 vm_id 的 queue id。这是为“生产者”准备的。
+
+这两个调用都返回一个队列 ID,该 ID 用于 DequeueSharedQueue 和 EnqueueSharedQueue。同时当队列中入队新数据时 消费者 PluginContext 中有 OnQueueReady(queueID uint32) 接口会收到通知。
+还强烈建议由 Envoy 的主线程上的单例 Wasm Service 创建共享队列。否则 OnQueueReady 将在工作线程上调用,这会阻塞它们处理 Http 或 Tcp 流。
+
+![img](https://img.alicdn.com/imgextra/i1/O1CN01s1cT1s28xb7OKkEg0_!!6000000007999-0-tps-2378-1316.jpg)
+在上图(图片来源 [Proxy-Wasm Go SDK](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/doc/OVERVIEW.md))中展示共享队列工作原理,更详细如何使用共享队列可以参考 [示例](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/examples/shared_queue/main.go)。
+
+
+## 5 限制和注意事项
+
+以下是在使用 Proxy-Wasm Go SDK 和 Proxy-Wasm 编写插件时需要注意事项。
+
+### 5.1 一些标准库不可用
+
+一些现有的标准库不可用(可导入但运行时 panic / 无法导入)。这有几个原因:
+1. TinyGo 的 WASI 目标不支持某些系统调用。
+2. TinyGo 没有实现 reflect 包的全部功能。
+3. [Proxy-Wasm C++ 主机](https://github.com/proxy-wasm/proxy-wasm-cpp-host) 尚未支持某些 WASI API。
+4. TinyGo 或 Proxy-Wasm 中不支持一些语言特性:包括 `recover` 和 `goroutine`。
+
+随着 TinyGo 和 Proxy-Wasm 的发展,这些问题将得到缓解。
+
+### 5.2 由于垃圾回收导致的性能开销
+
+由于 GC,使用 Go/TinyGo 会带来性能开销,尽管可以认为与代理中的其他操作相比,GC 的开销足够小。
+TinyGo 允许禁用 GC,但由于内部需要使用映射(隐式引起分配)来保存虚拟机的状态,可以通过 `alloc(uintptr)` [接口](https://github.com/tinygo-org/tinygo/blob/v0.14.1/src/runtime/gc_none.go#L13) 使用 `-gc=custom` 选项设置 proxy-wasm 定制的 GC 算法。
+
+### 5.3 `recover` 未实现
+
+在 TinyGo 中,`recover` 未实现(https://github.com/tinygo-org/tinygo/issues/891)。这也意味着依赖 `recover` 的代码将无法按预期工作。
+
+### 5.4 Goroutine 不支持
+
+在 TinyGo 中,Goroutine 通过 LLVM 的协程实现(见[这篇博客文章](https://aykevl.nl/2019/02/tinygo-goroutines))。 在 Envoy 中,Wasm 模块以事件驱动的方式运行,因此一旦主函数退出,“调度器”就不再执行。因此不能像普通主机环境中那样使用 Goroutine 。
+在以事件驱动方式执行的 Wasm VM 线程中如何处理 Goroutine 的问题尚未有解决方案。建议使用实现 `OnTick` 函数来处理任何异步任务。
+
+## 6 插件开发样例
+
+用 Proxy-Wasm Go SDK 实现一个简单的插件,具体样例如下:
+
+```golang
+package main
+
+import (
+ "github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
+ "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
+)
+
+// 插件入口
+func main() {
+ proxywasm.SetVMContext(&vmContext{})
+}
+
+// VM 上下文
+type vmContext struct {
+ // Embed the default VM context here,
+ types.DefaultVMContext
+ // 这里添加 VM 配置
+}
+
+// VM 启动回调
+func (*vmContext) OnVMStart(vmConfigurationSize int) types.OnVMStartStatus {
+ proxywasm.LogInfof("OnVMStart()")
+ // 获取 VM 配置
+ _, err := proxywasm.GetVMConfiguration()
+ if err != nil {
+ proxywasm.LogCriticalf("error reading vm configuration: %v", err)
+ }
+ // 这里解析 VM 配置
+ return types.OnVMStartStatusOK
+}
+
+// 生成插件上下文
+func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {
+ proxywasm.LogInfof("NewPluginContex()")
+ return &pluginContext{}
+}
+
+// 插件上下文
+type pluginContext struct {
+ // Embed the default plugin context here,
+ types.DefaultPluginContext
+ // 这里添加插件配置
+}
+
+// Http 上下文
+type httpContext struct {
+ // Embed the default root http context here,
+ // so that we don't need to reimplement all the methods.
+ types.DefaultHttpContext
+ // 这里添加http 上下文属性
+ requestBodySize int
+ responseBodySize int
+}
+
+// 生成 Http 上下文
+func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {
+ proxywasm.LogInfof("NewHttpContext()")
+ return &httpContext{}
+}
+
+// 插件启动回调,
+func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {
+ proxywasm.LogInfof("OnPluginStart()")
+ // 获取插件配置
+ _, err := proxywasm.GetPluginConfiguration()
+ if err != nil {
+ proxywasm.LogCriticalf("error reading plugin configuration: %v", err)
+ }
+ // 这里解析插件配置
+
+ return types.OnPluginStartStatusOK
+}
+
+// http 请求头回调
+func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
+ proxywasm.LogInfof("OnHttpRequestHeaders()")
+ // 这里处理请求头回调
+ return types.ActionContinue
+}
+
+// http 请求体回调,注意这里流式处理
+func (ctx *httpContext) OnHttpRequestBody(bodySize int, endOfStream bool) types.Action {
+ proxywasm.LogInfof("OnHttpRequestBody()")
+ ctx.requestBodySize += bodySize
+ if !endOfStream {
+ // Wait until we see the entire body to replace.
+ return types.ActionPause
+ }
+ _, err := proxywasm.GetHttpRequestBody(0, ctx.requestBodySize)
+ if err != nil {
+ proxywasm.LogErrorf("failed to get request body: %v", err)
+ return types.ActionContinue
+ }
+
+ return types.ActionContinue
+}
+
+// http 响应头回调
+func (ctx *httpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
+ proxywasm.LogInfof("OnHttpResponseHeaders()")
+ // 这里响应头回调
+ return types.ActionContinue
+}
+
+// http 响应体回调, 注意这里流式处理
+func (ctx *httpContext) OnHttpResponseBody(bodySize int, endOfStream bool) types.Action {
+ proxywasm.LogInfof("OnHttpResponseBody()")
+ ctx.responseBodySize += bodySize
+ // 判断是否响应体结束
+ if !endOfStream {
+ // Wait until we see the entire body to replace.
+ return types.ActionPause
+ }
+ _, err := proxywasm.GetHttpResponseBody(0, ctx.responseBodySize)
+ if err != nil {
+ proxywasm.LogErrorf("failed to get response body: %v", err)
+ return types.ActionContinue
+ }
+ return types.ActionContinue
+}
+```
+核心步骤如下:
+- 入口注册 vmContext
+- VM 启动回调时候解析 VM 配置
+- 由 vmContext 生成 pluginContext
+- 插件启动回调时候解析插件配置
+- 对于每个 http 流,pluginContext 生成 httpContext
+- 生成的 httpContext 处理请求头、请求体、响应头、响应体,这里要注意的是处理 OnHttpRequestBody 和 OnHttpResponseBody 回调是流式处理
+
+可以通过 [开发样例](https://github.com/higress-group/proxy-wasm-go-sdk/tree/main/examples) 查看更多 Proxy-Wasm Go SDK 插件开发样例。
+
+## 参考
+- [1] [proxy-wasm-go-sdk doc](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/doc/OVERVIEW.md)
+- [2] [proxy-wasm-go-sdk example](https://github.com/higress-group/proxy-wasm-go-sdk/tree/main/examples)
+- [3] [WebAssembly 解释器实现篇](https://github.com/mcuking/blog/issues/96/)
+- [4] [理解 WebAssembly 文本格式](https://developer.mozilla.org/zh-CN/docs/WebAssembly/Understanding_the_text_format)
+- [5] [Wasm Module Binary Format](https://webassembly.github.io/spec/core/binary/modules.html)
+- [6] [WebAssembly 究竟是什么?](https://www.bilibili.com/video/BV1WK42117dW)
+- [7] [WebAssembly 在 MOSN 中的实践 - 基础框架篇](https://mosn.io/blog/posts/mosn-wasm-framework/)
\ No newline at end of file
diff --git a/src/content/docs/ebook/en/wasm16.md b/src/content/docs/ebook/en/wasm16.md
new file mode 100644
index 0000000000..0e154e326f
--- /dev/null
+++ b/src/content/docs/ebook/en/wasm16.md
@@ -0,0 +1,651 @@
+---
+title: Higress 插件 Go SDK 与处理流程
+keywords: [Higress]
+---
+
+# Higress 插件 Go SDK 与处理流程
+
+本章开始介绍详细 Higress 插件开发 SDK 、插件开发流程和插件开发注意事项。
+
+## 1 Higress 插件 Go SDK
+
+Higress 插件 Go SDK 在 proxy-wasm-go-sdk 上封装了一层,简化插件开发和增强功能。其代码位置:https://github.com/alibaba/higress/tree/main/plugins/wasm-go/pkg ,代码文件结构如下:
+
+```shell
+tree
+.
+├── matcher
+│ ├── rule_matcher.go
+│ ├── rule_matcher_test.go
+│ └── utils.go
+└── wrapper
+ ├── cluster_wrapper.go
+ ├── cluster_wrapper_test.go
+ ├── http_wrapper.go
+ ├── log_wrapper.go
+ ├── plugin_wrapper.go
+ ├── redis_wrapper.go
+ └── request_wrapper.go
+ └── response_wrapper.go
+```
+Higress 插件 Go SDK 主要增强功能如下:
+- matcher 包提供全局、路由、域名级别配置的解析功能。
+- wrapper 包下 log_wrapper.go 封装和简化插件日志的输出功能。
+- wrapper 包下 cluster_wrapper.go、redis_wrapper.go、http_wrapper.go 封装 Http 和 Redis Host Function Call。
+- wrapper 包下 plugin_wrapper.go 封装 proxy-wasm-go-sdk 的 VMContext、PluginContext、HttpContext、插件配置解析功能。
+- wrapper 包下 request_wrapper.go、response_wrapper.go 提供关于请求和响应公共方法。
+
+本章主要集中介绍 plugin_wrapper.go 提供 VMContext、PluginContext、HttpContext、插件配置解析功能。
+
+## 2 Higress 插件 Go SDK 开发
+
+相对应于 proxy-wasm-go-sdk 中的 VMContext、PluginContext、HttpContext 3 个上下文, 在 Higress 插件 Go SDK 中是 CommonVmCtx、CommonPluginCtx、CommonHttpCtx 3 个支持泛型的 struct。 3 个 struct 的核心内容如下:
+
+```golang
+type CommonVmCtx[PluginConfig any] struct {
+ // proxy-wasm-go-sdk DefaultVMContext 默认实现
+ types.DefaultVMContext
+ // 插件名称
+ pluginName string
+ // 插件日志工具
+ log Log
+ hasCustomConfig bool
+ // 插件配置解析函数
+ parseConfig ParseConfigFunc[PluginConfig]
+ // 插件路由、域名、服务级别配置解析函数
+ parseRuleConfig ParseRuleConfigFunc[PluginConfig]
+ // 以下是自定义插件回调钩子函数
+ onHttpRequestHeaders onHttpHeadersFunc[PluginConfig]
+ onHttpRequestBody onHttpBodyFunc[PluginConfig]
+ onHttpStreamingRequestBody onHttpStreamingBodyFunc[PluginConfig]
+ onHttpResponseHeaders onHttpHeadersFunc[PluginConfig]
+ onHttpResponseBody onHttpBodyFunc[PluginConfig]
+ onHttpStreamingResponseBody onHttpStreamingBodyFunc[PluginConfig]
+ onHttpStreamDone onHttpStreamDoneFunc[PluginConfig]
+}
+
+type CommonPluginCtx[PluginConfig any] struct {
+ // proxy-wasm-go-sdk DefaultPluginContext 默认实现
+ types.DefaultPluginContext
+ // 解析后保存路由、域名、服务级别配置和全局插件配置
+ matcher.RuleMatcher[PluginConfig]
+ // 引用 CommonVmCtx
+ vm *CommonVmCtx[PluginConfig]
+ // tickFunc 数组
+ onTickFuncs []TickFuncEntry
+}
+
+type CommonHttpCtx[PluginConfig any] struct {
+ // proxy-wasm-go-sdk DefaultHttpContext 默认实现
+ types.DefaultHttpContext
+ // 引用 CommonPluginCtx
+ plugin *CommonPluginCtx[PluginConfig]
+ // 当前 Http 上下文下匹配插件配置,可能是路由、域名、服务级别配置或者全局配置
+ config *PluginConfig
+ // 是否处理请求体
+ needRequestBody bool
+ // 是否处理响应体
+ needResponseBody bool
+ // 是否处理流式请求体
+ streamingRequestBody bool
+ // 是否处理流式响应体
+ streamingResponseBody bool
+ // 非流式处理缓存请求体大小
+ requestBodySize int
+ // 非流式处理缓存响应体大小
+ responseBodySize int
+ // Http 上下文 ID
+ contextID uint32
+ // 自定义插件设置自定义插件上下文
+ userContext map[string]interface{}
+}
+```
+
+它们的关系如下图:
+![img](https://img.alicdn.com/imgextra/i3/O1CN01nkPR171qsrwfUK0WW_!!6000000005552-2-tps-1640-600.png)
+
+### 2.1 启动入口和 VM 上下文(CommonVmCtx)
+
+```golang
+func main() {
+ wrapper.SetCtx(
+ // 插件名称
+ "hello-world",
+ // 设置自定义函数解析插件配置,这个方法适合插件全局配置和路由、域名、服务级别配置内容规则是一样
+ wrapper.ParseConfigBy(parseConfig),
+ // 设置自定义函数解析插件全局配置和路由、域名、服务级别配置,这个方法适合插件全局配置和路由、域名、服务级别配置内容规则不一样
+ wrapper.ParseOverrideConfigBy(parseConfig, parseRuleConfig)
+ // 设置自定义函数处理请求头
+ wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
+ // 设置自定义函数处理请求体
+ wrapper.ProcessRequestBodyBy(onHttpRequestBody),
+ // 设置自定义函数处理响应头
+ wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
+ // 设置自定义函数处理响应体
+ wrapper.ProcessResponseBodyBy(onHttpResponseBody),
+ // 设置自定义函数处理流式请求体
+ wrapper.ProcessStreamingRequestBodyBy(onHttpStreamingRequestBody),
+ // 设置自定义函数处理流式响应体
+ wrapper.ProcessStreamingResponseBodyBy(onHttpStreamingResponseBody),
+ // 设置自定义函数处理流式请求完成
+ wrappper.ProcessStreamDoneBy(onHttpStreamDone)
+ )
+}
+```
+> 可以根据实际业务需要来选择设置回调钩子函数。
+
+跟踪一下 wrapper.SetCtx 的实现:
+- 创建 CommonVmCtx 对象同时设置自定义插件回调钩子函数。
+- 然后再调用 proxywasm.SetVMContext 设置 VMContext。
+
+```golang
+func SetCtx[PluginConfig any](pluginName string, setFuncs ...SetPluginFunc[PluginConfig]) {
+ // 调用 proxywasm.SetVMContext 设置 VMContext
+ proxywasm.SetVMContext(NewCommonVmCtx(pluginName, setFuncs...))
+}
+
+func NewCommonVmCtx[PluginConfig any](pluginName string, setFuncs ...SetPluginFunc[PluginConfig]) *CommonVmCtx[PluginConfig] {
+ ctx := &CommonVmCtx[PluginConfig]{
+ pluginName: pluginName,
+ log: Log{pluginName},
+ hasCustomConfig: true,
+ }
+ // CommonVmCtx 里设置自定义插件回调钩子函数
+ for _, set := range setFuncs {
+ set(ctx)
+ }
+ ...
+ return ctx
+```
+
+### 2.2 插件上下文(CommonPluginCtx)
+
+#### 2.2.1 创建 CommonPluginCtx 对象
+通过 CommonVmCtx 的 NewPluginContext 方法创建 CommonPluginCtx 对象, 设置 CommonPluginCtx 的 vm 引用。
+```golang
+func (ctx *CommonVmCtx[PluginConfig]) NewPluginContext(uint32) types.PluginContext {
+ return &CommonPluginCtx[PluginConfig]{
+ vm: ctx,
+ }
+}
+```
+
+#### 2.2.2 插件启动和插件配置解析
+
+CommonPluginCtx 的 OnPluginStart 部分核心代码如下:
+```golang
+func (ctx *CommonPluginCtx[PluginConfig]) OnPluginStart(int) types.OnPluginStartStatus {
+ // 调用 proxywasm.GetPluginConfiguration 获取插件配置
+ data, err := proxywasm.GetPluginConfiguration()
+ globalOnTickFuncs = nil
+ ...
+ var jsonData gjson.Result
+ // 插件配置转成 json
+ jsonData = gjson.ParseBytes(data)
+
+ // 设置 parseOverrideConfig
+ var parseOverrideConfig func(gjson.Result, PluginConfig, *PluginConfig) error
+ if ctx.vm.parseRuleConfig != nil {
+ parseOverrideConfig = func(js gjson.Result, global PluginConfig, cfg *PluginConfig) error {
+ // 解析插件路由、域名、服务级别插件配置
+ return ctx.vm.parseRuleConfig(js, global, cfg, ctx.vm.log)
+ }
+ }
+ ...
+ // 解析插件配置
+ err = ctx.ParseRuleConfig(jsonData,
+ func(js gjson.Result, cfg *PluginConfig) error {
+ // 解析插件全局或者当 parseRuleConfig 没有设置时候同时解析路由、域名、服务级别插件配置
+ return ctx.vm.parseConfig(js, cfg, ctx.vm.log)
+ },
+ parseOverrideConfig,
+ )
+ ...
+ if globalOnTickFuncs != nil {
+ ctx.onTickFuncs = globalOnTickFuncs
+ ...
+ }
+ return types.OnPluginStartStatusOK
+}
+```
+
+可以发现在解析插件配置过程中有两个回调钩子函数,parseConfig 和 parseRuleConfig。
+- parseConfig :解析插件全局配置,如果 parseRuleConfig 没有设置,那么 parseConfig 会同时解析全局配置和路由、域名、服务级别配置。也就是说插件全局配置和路由、域名、服务级别配置规则是一样。
+- parseRuleConfig: 解析路由、域名、服务级别插件配置。如果设置 parseRuleConfig,也就是说插件全局配置和路由、域名、服务级别配置规则是不同的。
+
+
+> 这里我们不进一步分析插件解析过程,后续在插件生效原理章节从控制面和数据面详细分析插件全局、路由、域名、服务级别生效原理。
+
+大部分情况下插件全局配置和路由、域名、服务级别配置规则是一样的,因此在定义插件时只需要调用 wrapper.ParseConfigBy(parseConfig) 来设置插件配置解析回调钩子函数。
+而有些插件(如 [basic-auth](https://higress.io/docs/latest/plugins/authentication/basic-auth/))的全局配置和路由、域名、服务级别配置规则是不一样的。baisc-auth 插件配置 YAML 样例如下:
+```yaml
+apiVersion: extensions.higress.io/v1alpha1
+kind: WasmPlugin
+metadata:
+ name: cpp-basic-auth
+ namespace: higress-system
+spec:
+ defaultConfig:
+ consumers:
+ - credential: admin:123456
+ name: consumer1
+ - credential: guest:abc
+ name: consumer2
+ global_auth: false
+ defaultConfigDisable: false
+ matchRules:
+ - config:
+ allow:
+ - consumer1
+ configDisable: false
+ ingress:
+ - higress-conformance-infra/wasmplugin-cpp-basic-auth
+ url: file:///opt/plugins/wasm-cpp/extensions/basic-auth/plugin.wasm
+```
+
+可以看出 matchRule 下 config 配置内容和 defaultConfig 配置内容不一样。所以在开发插件的时候,需要同时设置 parseConfig 和 parseRuleConfig 两个回调钩子函数。
+baisc-auth 部分核心代码如下:
+```golang
+func main() {
+ wrapper.SetCtx(
+ "basic-auth",
+ // 要同时设置 parseConfig 和 parseRuleConfig 回调钩子函数
+ // ParseOverrideConfigBy 函数的第一个参数接收 parseConfig 回调钩子函数,第二个参数接收 parseRuleConfig 回调钩子函数
+ wrapper.ParseOverrideConfigBy(parseGlobalConfig, parseOverrideRuleConfig),
+ wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
+ )
+}
+
+// 自定义插件配置
+type BasicAuthConfig struct {
+ // 插件全局配置内容
+ globalAuth *bool `yaml:"global_auth"`
+ consumers []Consumer `yaml:"consumers"`
+ ...
+ // 插件路由、域名、服务级别配置内容
+ allow []string `yaml:"allow"`
+}
+
+type Consumer struct {
+ name string `yaml:"name"`
+ credential string `yaml:"credential"`
+}
+
+// 解析插件全局配置回调钩子函数
+func parseGlobalConfig(json gjson.Result, global *BasicAuthConfig, log wrapper.Log) error {
+ log.Debug("global config")
+ // 解析插件全局配置
+ ...
+ consumers := json.Get("consumers")
+ ...
+ globalAuth := json.Get("global_auth")
+ ...
+ return nil
+}
+
+// 解析插件路由、域名、服务级别配置回调钩子函数
+func parseOverrideRuleConfig(json gjson.Result, global BasicAuthConfig, config *BasicAuthConfig, log wrapper.Log) error {
+ log.Debug("domain/route config")
+ // 这里要注意要用全局配置内容复制到路由、域名、服务级别配置中,这样后续在 HttpContext 中可以获取当前 Http 请求下插件配置包括全局配置和路由、域名、服务级别配置
+ *config = global
+ // 解析插件路由、域名、服务级别配置
+ allow := json.Get("allow")
+ ...
+ for _, item := range allow.Array() {
+ config.allow = append(config.allow, item.String())
+ }
+ ...
+ return nil
+}
+```
+开发这种类型插件需要注意:
+- 自定义插件配置 struct 要包含全局配置内容和路由、域名、服务级别配置内容。
+- wrapper.ParseOverrideConfigBy 要同时设置 parseConfig 和 parseRuleConfig 回调钩子函数。
+- 在 parseRuleConfig 回调钩子函数处理中,全局配置内容要复制到路由、域名、服务级别配置中,这样后续在 HttpContext 中可以获取当前 Http 请求下插件配置包括全局配置和路由、域名、服务级别配置内容。
+
+### 2.3 HTTP 上下文(CommonHttpCtx)
+
+#### 2.3.1 创建 CommonHttpCtx
+
+CommonPluginCtx 的 NewHttpContext 部分核心代码如下:
+
+```golang
+func (ctx *CommonPluginCtx[PluginConfig]) NewHttpContext(contextID uint32) types.HttpContext {
+ httpCtx := &CommonHttpCtx[PluginConfig]{
+ plugin: ctx,
+ contextID: contextID,
+ userContext: map[string]interface{}{},
+ }
+ // 根据插件配置判断设置是否需要处理请求和响应的 body
+ if ctx.vm.onHttpRequestBody != nil || ctx.vm.onHttpStreamingRequestBody != nil {
+ httpCtx.needRequestBody = true
+ }
+ if ctx.vm.onHttpResponseBody != nil || ctx.vm.onHttpStreamingResponseBody != nil {
+ httpCtx.needResponseBody = true
+ }
+ if ctx.vm.onHttpStreamingRequestBody != nil {
+ httpCtx.streamingRequestBody = true
+ }
+ if ctx.vm.onHttpStreamingResponseBody != nil {
+ httpCtx.streamingResponseBody = true
+ }
+
+ return httpCtx
+}
+```
+#### 2.3.2 OnHttpRequestHeaders
+
+OnHttpRequestHeaders 核心代码如下:
+```golang
+func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
+ // 获取当前 HTTP 请求生效插件配置
+ config, err := ctx.plugin.GetMatchConfig()
+ ...
+ // 设置插件配置到 HttpContext
+ ctx.config = config
+ // 如果请求 content-type 是 octet-stream/grpc 或者定义 content-encoding,则不处理请求 body
+ if IsBinaryRequestBody() {
+ ctx.needRequestBody = false
+ }
+ ...
+ // 调用自定义插件 onHttpRequestHeaders 回调钩子函数
+ return ctx.plugin.vm.onHttpRequestHeaders(ctx, *config, ctx.plugin.vm.log)
+}
+```
+主要处理逻辑如下:
+- 获取匹配当前 HTTP 请求插件配置,可能是路由、域名、服务级别配置或者全局配置。
+- 设置插件配置到 HttpContext。
+- 如果请求 content-type 是 octet-stream/grpc 或者定义 content-encoding,则不处理请求 body。
+- 调用自定义插件 onHttpRequestHeaders 回调钩子函数。
+
+关于插件配置可以看出, Higress 插件 Go SDK 封装如下:
+- 在插件启动时候,解析插件路由、域名、服务级别插件配置和全局配置保存到 CommonPluginCtx 中。
+- 在 onHttpRequestHeaders 阶段,根据当前 HTTP 上下文中路由、域名、服务等信息匹配插件配置,返回路由、域名、服务级别配置或者全局配置。然后把匹配到插件配置设置到 HttpContext 对象的 config 属性中,这样自定义插件的所有回调钩子函数就可以获取到这个配置。
+
+#### 2.3.3 OnHttpRequestBody
+
+OnHttpRequestBody 核心代码如下:
+```golang
+func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestBody(bodySize int, endOfStream bool) types.Action {
+ ...
+ // 如果不需要处理请求 body,则直接返回,继续后续处理
+ if !ctx.needRequestBody {
+ return types.ActionContinue
+ }
+ // 先判断是否要需要进行流式处理,如果需要则调用自定义插件 onHttpStreamingRequestBody 回调钩子函数
+ if ctx.plugin.vm.onHttpStreamingRequestBody != nil && ctx.streamingRequestBody {
+ chunk, _ := proxywasm.GetHttpRequestBody(0, bodySize)
+ // 调用自定义插件 onHttpStreamingRequestBody 回调钩子函数
+ modifiedChunk := ctx.plugin.vm.onHttpStreamingRequestBody(ctx, *ctx.config, chunk, endOfStream, ctx.plugin.vm.log)
+ err := proxywasm.ReplaceHttpRequestBody(modifiedChunk)
+ ...
+ return types.ActionContinue
+ }
+ // 再判断是否要需要进行非流式处理,需要缓存请求 body,等读取整个请求 body 后调用自定义插件 onHttpRequestBody 回调钩子函数
+ if ctx.plugin.vm.onHttpRequestBody != nil {
+ ctx.requestBodySize += bodySize
+ if !endOfStream {
+ return types.ActionPause
+ }
+ body, err := proxywasm.GetHttpRequestBody(0, ctx.requestBodySize)
+ ...
+ // 调用自定义插件 onHttpRequestBody 回调钩子函数
+ return ctx.plugin.vm.onHttpRequestBody(ctx, *ctx.config, body, ctx.plugin.vm.log)
+ }
+ return types.ActionContinue
+}
+```
+
+核心逻辑如下:
+- 如果 ctx.needRequestBody 为 false 时,则直接返回,继续后续处理。
+- 当 ctx.streamingRequestBody 为 true 时,同时自定义插件有 onHttpStreamingRequestBody 回调钩子函数,则调用自定义插件 onHttpStreamingRequestBody 回调钩子函数。
+- 当自定义插件有 onHttpRequestBody 回调钩子函数,需要缓存请求 body,等读取整个请求 body 后调用自定义插件 onHttpRequestBody 回调钩子函数。
+
+#### 2.3.4 OnHttpResponseHeaders
+
+OnHttpResponseHeaders 核心代码如下:
+```golang
+func (ctx *CommonHttpCtx[PluginConfig]) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
+ ...
+ // To avoid unexpected operations, plugins do not read the binary content body
+ if IsBinaryResponseBody() {
+ ctx.needResponseBody = false
+ }
+ ...
+ return ctx.plugin.vm.onHttpResponseHeaders(ctx, *ctx.config, ctx.plugin.vm.log)
+}
+
+```
+主要处理逻辑如下:
+- 如果响应 content-type 是 octet-stream/grpc 或者定义 content-encoding,则不处理响应 body。
+- 调用自定义插件 onHttpResponseHeaders 回调钩子函数。
+
+#### 2.3.5 OnHttpResponseBody
+
+OnHttpResponseBody 核心代码如下:
+```golang
+func (ctx *CommonHttpCtx[PluginConfig]) OnHttpResponseBody(bodySize int, endOfStream bool) types.Action {
+ ...
+ // 如果不需要处理响应 body,则直接返回,继续后续处理
+ if !ctx.needResponseBody {
+ return types.ActionContinue
+ }
+ // 先判断是否要需要进行流式处理,如果需要则调用自定义插件 onHttpStreamingResponseBod 回调钩子函数
+ if ctx.plugin.vm.onHttpStreamingResponseBody != nil && ctx.streamingResponseBody {
+ chunk, _ := proxywasm.GetHttpResponseBody(0, bodySize)
+ // 调用自定义插件 onHttpStreamingResponseBody 回调钩子函数
+ modifiedChunk := ctx.plugin.vm.onHttpStreamingResponseBody(ctx, *ctx.config, chunk, endOfStream, ctx.plugin.vm.log)
+ ...
+ return types.ActionContinue
+ }
+ // 再判断是否要需要进行非流式处理,需要缓存响应 body,等读取整个响应 body 后调用自定义插件 onHttpResponseBody 回调钩子函数
+ if ctx.plugin.vm.onHttpResponseBody != nil {
+ ctx.responseBodySize += bodySize
+ if !endOfStream {
+ return types.ActionPause
+ }
+ body, err := proxywasm.GetHttpResponseBody(0, ctx.responseBodySize)
+ ...
+ // 调用自定义插件 onHttpResponseBody 回调钩子函数
+ return ctx.plugin.vm.onHttpResponseBody(ctx, *ctx.config, body, ctx.plugin.vm.log)
+ }
+ return types.ActionContinue
+}
+```
+
+核心逻辑如下:
+- 当 ctx.needResponseBody 为 false 时,则直接返回,继续后续处理。
+- 当 ctx.streamingResponseBody 为 true 时,同时自定义插件有 onHttpStreamingResponseBody 回调钩子函数,则调用自定义插件 onHttpStreamingResponseBody 回调钩子函数。
+- 当自定义插件有 onHttpResponseBody 回调钩子函数,需要缓存响应 body,等读取整个响应 body 后调用自定义插件 onHttpResponseBody 回调钩子函数。
+
+#### 2.3.6 OnHttpStreamDone
+
+OnHttpStreamDone 核心代码如下:
+```golang
+func (ctx *CommonHttpCtx[PluginConfig]) OnHttpStreamDone() {
+ ...
+ ctx.plugin.vm.onHttpStreamDone(ctx, *ctx.config, ctx.plugin.vm.log)
+}
+```
+
+OnHttpStreamDone 比较简单,自定义插件有 onHttpStreamDone 回调钩子函数,则调用自定义插件 onHttpStreamDone 回调钩子函数。
+
+#### 2.3.7 CommonHttpCtx 方法
+
+CommonHttpCtx 提供以下方法,自定义插件可以调用,其代码和注释如下:
+```golang
+// 设置自定义上下文,这个上下文可以在自定义插件所有回调钩子函数中可以获取
+func (ctx *CommonHttpCtx[PluginConfig]) SetContext(key string, value interface{}) {
+ ctx.userContext[key] = value
+}
+// 获取自定义上下文,这个上下文可以在自定义插件所有回调钩子函数中可以获取
+func (ctx *CommonHttpCtx[PluginConfig]) GetContext(key string) interface{} {
+ return ctx.userContext[key]
+}
+// 获取 bool 类型自定义上下文
+func (ctx *CommonHttpCtx[PluginConfig]) GetBoolContext(key string, defaultValue bool) bool {
+ if b, ok := ctx.userContext[key].(bool); ok {
+ return b
+ }
+ return defaultValue
+}
+// 获取 string 类型自定义上下文
+func (ctx *CommonHttpCtx[PluginConfig]) GetStringContext(key, defaultValue string) string {
+ if s, ok := ctx.userContext[key].(string); ok {
+ return s
+ }
+ return defaultValue
+}
+// 获取请求 scheme
+func (ctx *CommonHttpCtx[PluginConfig]) Scheme() string {
+ proxywasm.SetEffectiveContext(ctx.contextID)
+ return GetRequestScheme()
+}
+// 获取请求 host
+func (ctx *CommonHttpCtx[PluginConfig]) Host() string {
+ proxywasm.SetEffectiveContext(ctx.contextID)
+ return GetRequestHost()
+}
+// 获取请求 path
+func (ctx *CommonHttpCtx[PluginConfig]) Path() string {
+ proxywasm.SetEffectiveContext(ctx.contextID)
+ return GetRequestPath()
+}
+// 获取请求 method
+func (ctx *CommonHttpCtx[PluginConfig]) Method() string {
+ proxywasm.SetEffectiveContext(ctx.contextID)
+ return GetRequestMethod()
+}
+
+// 调用这个方法可以禁止读取请求 body 和处理。
+// 比如在 onHttpRequestHeaders 回调钩子函数中根据某些条件时调用,可以跳过后续的请求 body 读取和处理
+func (ctx *CommonHttpCtx[PluginConfig]) DontReadRequestBody() {
+ ctx.needRequestBody = false
+}
+
+// 调用这个方法禁止读取响应 body,
+// 比如在 onHttpResponseHeaders 回调钩子函数中根据某些条件时调用,可以跳过后续的响应 body 读取和处理
+func (ctx *CommonHttpCtx[PluginConfig]) DontReadResponseBody() {
+ ctx.needResponseBody = false
+}
+// 调用这个方法禁止请求流式处理
+// 比如在 onHttpRequestHeaders 回调钩子函数中根据某些条件时调用,跳过后续的 onHttpStreamingRequestBody 流式处理, 转成 onHttpRequestBody 处理
+func (ctx *CommonHttpCtx[PluginConfig]) BufferRequestBody() {
+ ctx.streamingRequestBody = false
+}
+
+// 调用这个方法禁止响应流式处理
+// 比如在 onHttpResponseHeaders 回调钩子函数中根据某些条件时调用,跳过后续的 onHttpStreamingResponseBody 流式处理, 转成 onHttpResponseBody 处理
+func (ctx *CommonHttpCtx[PluginConfig]) BufferResponseBody() {
+ ctx.streamingResponseBody = false
+}
+
+// 调用这个方法可以禁止重新计算路由,Envoy 默认在 Http 头发生变更时会重新计算路由,调用这个方法,可以禁止重新计算路由。
+// 比如在 onHttpRequestHeaders 回调钩子函数中根据某些条件时调用这个方法,可以禁止重新计算路由。
+// 关于 DisableReroute 使用场景可以参考 Higress 官方提供 [ai-proxy 插件](https://github.com/alibaba/higress/blob/main/plugins/wasm-go/extensions/ai-proxy/main.go#L76)
+func (ctx *CommonHttpCtx[PluginConfig]) DisableReroute() {
+ _ = proxywasm.SetProperty([]string{"clear_route_cache"}, []byte("off"))
+}
+// 设置请求 body buffer limit
+// 关于 DisableReroute 使用场景可以参考 Higress 官方提供 [ai-proxy 插件](https://github.com/alibaba/higress/blob/main/plugins/wasm-go/extensions/ai-proxy/main.go#L81)
+func (ctx *CommonHttpCtx[PluginConfig]) SetRequestBodyBufferLimit(size uint32) {
+ ctx.plugin.vm.log.Infof("SetRequestBodyBufferLimit: %d", size)
+ _ = proxywasm.SetProperty([]string{"set_decoder_buffer_limit"}, []byte(strconv.Itoa(int(size))))
+}
+// 设置响应 body buffer limit
+func (ctx *CommonHttpCtx[PluginConfig]) SetResponseBodyBufferLimit(size uint32) {
+ ctx.plugin.vm.log.Infof("SetResponseBodyBufferLimit: %d", size)
+ _ = proxywasm.SetProperty([]string{"set_encoder_buffer_limit"}, []byte(strconv.Itoa(int(size))))
+}
+```
+
+核心内容如下:
+- 在 onHttpRequestHeaders 阶段:
+ - 调用 DontReadRequestBody 方法,可以跳过读取请求 body 和处理。
+ - 调用 BufferRequestBody 方法,可以跳过请求 onHttpStreamingRequestBody 流式处理,转成 onHttpRequestBody 处理。
+ - 调用 DisableReroute 方法,可以禁止重新计算路由。
+- 在 onHttpResponseHeaders 阶段:
+ - 调用 DontReadResponseBody 方法,可以跳过读取响应 body 和处理。
+ - 调用 BufferResponseBody 方法,可以跳过响应 onHttpStreamingResponseBody 流式处理,转成 onHttpResponseBody 处理。
+- SetContext 和 GetContext 方法,可以设置和获取自定义上下文,在自定义插件所有回调钩子函数中可以使用。
+- SetRequestBodyBufferLimit 和 SetResponseBodyBufferLimit 方法,可以设置请求 body buffer limit 和响应 body buffer limit。
+
+### 2.4 Types.Action
+
+在自定义插件中 onHttpRequestHeaders、onHttpRequestBody、onHttpResponseHeaders、onHttpResponseBody 返回值类型为 types.Action。通过 types.Action 枚举值来控制插件的运行流程,常见的返回值如下:
+
+1. types.ActionContinue:继续后续处理,比如继续读取请求 body,或者继续读取响应 body。
+
+2. types.ActionPause: 暂停后续处理,比如在 onHttpRequestHeaders 通过 Http 或者 Redis 调用外部服务获取认证信息,在调用外部服务回调钩子函数中调用 proxywasm.ResumeHttpRequest() 来恢复后续处理 或者调用 proxywasm.SendHttpResponseWithDetail() 来返回响应。
+
+
+#### 2.4.1 编译插件注意事项
+
+1. Higress 需要编译时启用 `EXTRA_TAGS=proxy_wasm_version_0_2_100` 标签来修改 Proxy Wasm ABI。 TinyGo 本地构建命令如下:
+```shell
+tinygo build -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer proxy_wasm_version_0_2_100' -o ./build/plugin.wasm main.go
+```
+2. Makefile 文件默认启用 `proxy_wasm_version_0_2_100` 标签,所以不需要修改 Makefile 文件。
+
+#### 2.4.2 Header 状态码
+
+1. HeaderContinue:
+
+表示当前 filter 已经处理完毕,可以继续交给下一个 filter 处理。 types.ActionContinue 对应就是这个状态。
+
+2. HeaderStopIteration:
+
+表示 header 还不能继续交给下一个 filter 来处理。 但是并不停止从连接读数据,继续触发 body data 的处理。 这样可以在 body data 处理阶段可以更新 Http 请求头内容。 如果 body data 要交给下一个 filter 处理, 这时 header 是也会被一起交给下一个 filter 处理。
+
+3. HeaderContinueAndEndStream:
+
+表示 header 可以继续交给下一个 filter 处理,但是下一个 filter 收到的 end_stream = false,也就是标记请求还未结束。以便当前 filter 再增加 body。
+
+4. HeaderStopAllIterationAndBuffer:
+
+停止所有迭代,表示 header 不能继续交给下一个 filter,并且当前 filter 也不能收到 body data。 并对当前过滤器及后续过滤器的头部、数据和尾部进行缓冲。如果缓存大小超过了 buffer limit,在请求阶段就直接返回 413,响应阶段就直接返回 500。
+同时需要调用 proxywasm.ResumeHttpRequest()、 proxywasm.ResumeHttpResponse() 或者 proxywasm.SendHttpResponseWithDetail() 函数来恢复后续处理。
+
+5. HeaderStopAllIterationAndWatermark:
+
+同上,区别是,当缓存超过了 buffer limit 会触发流控,也就是暂停从连接上读数据。 types.ActionPause 实际上对应就是这个状态。
+
+> 关于 types.HeaderStopIteration 和 HeaderStopAllIterationAndWatermark 的使用场景可以参考 Higress 官方提供 [ai-transformer 插件](https://github.com/alibaba/higress/blob/main/plugins/wasm-go/extensions/ai-transformer/main.go#L93-L99) 和 [ai-quota 插件](https://github.com/alibaba/higress/blob/main/plugins/wasm-go/extensions/ai-quota/main.go#L179) 。
+
+#### 2.4.3 Data 状态码
+
+1. DataContinue:
+
+和 header 类似,表示当前 filter 已经处理完毕,可以继续交给下一个 filter 处理。 如果 header 之前返回的是 HeaderStopIteration,且尚未交给下一个 filter 处理,那么此时 header 和 data 也会被交给下一个 filter 处理。types.ActionContinue 对应就是这个状态。
+
+2. DataStopIterationAndBuffer:
+
+表示当前 data 不能继续交给下一个 filter 处理,并且将当前 data 缓存起来。 与 header 类似,如果达到 buffer limit,在请求阶段就直接返回 413,响应阶段就直接返回 500。
+同时需要调用 proxywasm.ResumeHttpRequest()、 proxywasm.ResumeHttpResponse() 或者 proxywasm.SendHttpResponseWithDetail() 函数来恢复后续处理。
+
+3. DataStopIterationAndWatermark:
+
+同上,只是达到 buffer limit 会触发流控。types.ActionPause 实际上对应就是这个状态。
+
+4. DataStopIterationNoBuffer:
+
+表示当前 data 不能继续交给下一个 filter,但是不缓存当前 data 。
+
+
+### 2.5 Envoy 请求缓存区限制
+
+当自定义插件使用 `onHttpRequestBody` 非流式传输,当请求超过 `downstream` 缓存区限制(默认是 32k)。Envoy 会给用户返回 413, 同时报 `request_payload_too_large` 错误。
+比如在 AI 长上下文中场景中可能会碰到这个问题,这个问题可以通过参考 [全局配置说明](https://higress.io/docs/latest/user/configmap/) 调整 Downstream 配置项 `connectionBufferLimits` 解决, 或者 使用 `SetRequestBodyBufferLimit` 方法设置请求 body buffer limit 解决。 关于如何使用 `SetRequestBodyBufferLimit` 可以参考 Higress 官方提供 [ai-proxy 插件](https://github.com/alibaba/higress/blob/main/plugins/wasm-go/extensions/ai-proxy/main.go#L81) 的实现。
+
+
+## 3 Envoy 属性(Attributes)
+
+属性是 Envoy 的一个特性,允许用户在插件中设置和获取这些属性,可以通过 `proxywasm.GetProperty` 和 `proxywasm.SetProperty` 方法获取和设置。
+Envoy 预定义属性包括请求属性、响应属性、连接属性、Upstream 属性、Wasm 属性、和 Metadata 等属性, 具体可以参考 [Envoy 属性](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes)。
+同时用户也可以设置自定义属性,这些属性可以在插件链中不同插件共享。
+
+
+## 参考
+- [1] [Envoy 开发入门:搞懂 http filter 状态码](https://uncledou.site/2022/envoy-filter-status/)
+
+
diff --git a/src/content/docs/ebook/en/wasm17.md b/src/content/docs/ebook/en/wasm17.md
new file mode 100644
index 0000000000..b8ff87ed12
--- /dev/null
+++ b/src/content/docs/ebook/en/wasm17.md
@@ -0,0 +1,633 @@
+---
+title: HTTP 调用
+keywords: [Higress]
+---
+
+# HTTP 调用
+
+这章主要介绍如何使用 Higress 插件 Go SDK 实现 HTTP 调用。
+
+## 1 Envoy 集群(Cluster)名称和服务发现来源
+
+Higress 插件的 Go SDK 在进行 HTTP 和 Redis 调用时,是通过指定的集群名称来识别并连接到相应的 Envoy 集群。 此外,Higress 利用 [McpBridge](https://higress.io/docs/latest/user/mcp-bridge/) 支持多种服务发现机制,包括静态配置(static)、DNS、Kubernetes 服务、Eureka、Consul、Nacos、以及 Zookeeper 等。
+每种服务发现机制对应的集群名称生成规则都有所不同,这些规则在 cluster_wrapper.go 代码文件中有所体现。
+为了包装不同的服务发现机制,Higress 插件 Go SDK 定义了 Cluster 接口,该接口包含两个方法:ClusterName 和 HostName。
+```golang
+type Cluster interface {
+ // 返回 Envoy 集群名称
+ ClusterName() string
+ // 返回 Hostname, 在 HTTP 调用服务时候,用于设置 Http host 请求头
+ HostName() string
+}
+```
+
+### 1.1 静态配置(static)
+```golang
+type StaticIpCluster struct {
+ ServiceName string
+ Port int64
+ Host string
+}
+```
+- 集群名称规则为:`outbound|||.static`。
+- HostName 规则为:默认为 。
+
+### 1.2 DNS 配置(dns)
+```golang
+type DnsCluster struct {
+ ServiceName string
+ Domain string
+ Port int64
+}
+
+```
+- 集群名称规则为:`outbound|||.dns`。
+- HostName 规则为:如果设置 Host,返回 Host,否则返回。
+
+### 1.3 Kubernetes 服务(kubernetes)
+```golang
+
+type K8sCluster struct {
+ ServiceName string
+ Namespace string
+ Port int64
+ Version string
+ Host string
+}
+```
+- 集群名称规则为:`outbound|||..svc.cluster.local`。
+- HostName 规则为:如果设置 Host,返回 Host,否则返回 ..svc.cluster.local。
+
+### 1.4 Nacos
+```golang
+
+type NacosCluster struct {
+ ServiceName string
+ // use DEFAULT-GROUP by default
+ Group string
+ NamespaceID string
+ Port int64
+ // set true if use edas/sae registry
+ IsExtRegistry bool
+ Version string
+ Host string
+}
+```
+- 集群名称规则为:`outbound|||...nacos`。
+- HostName 规则为:如果设置 Host,返回 Host,否则返回 。
+
+### 1.5 Consul
+```golang
+type ConsulCluster struct {
+ ServiceName string
+ Datacenter string
+ Port int64
+ Host string
+}
+```
+- 集群名称规则为:`outbound|||..consul`。
+- HostName 规则为:如果设置 Host,返回 Host,否则返回 。
+
+### 1.6 FQDN
+
+```golang
+
+type FQDNCluster struct {
+ FQDN string
+ Host string
+ Port int64
+}
+```
+- 集群名称规则为:`outbound|||`。
+- HostName 规则为:如果设置 Host,返回 Host,否则返回 ``。
+
+## 2 HTTP 调用
+http_wrapper.go 部分核心代码如下:
+```golang
+// 回调函数
+type ResponseCallback func(statusCode int, responseHeaders http.Header, responseBody []byte)
+
+// HTTP 调用接口
+type HttpClient interface {
+ Get(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Head(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Options(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Post(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Put(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Patch(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Delete(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Connect(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Trace(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Call(method, path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
+}
+
+// 实现 httpClient 接口
+type ClusterClient[C Cluster] struct {
+ cluster C
+}
+```
+ClusterClient Get、Head、Options、Post、PUT、Patch、Delete、Connect、Trace、Call 方法最后调用 HttpCall 方法,其核心代码如下:
+
+```golang
+func HttpCall(cluster Cluster, method, path string, headers [][2]string, body []byte,
+ callback ResponseCallback, timeoutMillisecond ...uint32) error {
+
+ // 删除 :method, :path, :authority
+ for i := len(headers) - 1; i >= 0; i-- {
+ key := headers[i][0]
+ if key == ":method" || key == ":path" || key == ":authority" {
+ headers = append(headers[:i], headers[i+1:]...)
+ }
+ }
+ // 设置 timeout
+ var timeout uint32 = 500
+ if len(timeoutMillisecond) > 0 {
+ timeout = timeoutMillisecond[0]
+ }
+ // 重新设置 :method, :path, :authority
+ headers = append(headers, [2]string{":method", method}, [2]string{":path", path}, [2]string{":authority", cluster.HostName()})
+ requestID := uuid.New().String()
+ // 调用 HTTP 请求
+ _, err := proxywasm.DispatchHttpCall(cluster.ClusterName(), headers, body, nil, timeout, func(numHeaders, bodySize, numTrailers int) {
+ // 获取 HTTP 响应 body 和 headers
+ respBody, err := proxywasm.GetHttpCallResponseBody(0, bodySize)
+ ...
+ respHeaders, err := proxywasm.GetHttpCallResponseHeaders()
+ ...
+ code := http.StatusBadGateway
+ var normalResponse bool
+ headers := make(http.Header)
+ for _, h := range respHeaders {
+ if h[0] == ":status" {
+ code, err = strconv.Atoi(h[1])
+ ..
+ }
+ headers.Add(h[0], h[1])
+ }
+ ...
+ // 调用自定义插件回调函数
+ callback(code, headers, respBody)
+ })
+ ...
+ return err
+}
+```
+
+## 3 easy-jwt 插件开发
+
+在实际业务场景中,可能需要独立认证授权服务,来完成每个请求的认证和授权,现在开发一个简单的 easy-jwt 插件来演示如何在 Wasm 插件进行 HTTP 调用。
+其插件核心流程如下图:
+![img](https://img.alicdn.com/imgextra/i1/O1CN01DPi2w6244OvEejP1q_!!6000000007337-0-tps-1488-682.jpg)
+
+Token Server 提供 2 个接口:
+- /api/token/auth: 认证令牌接口
+- /api/token/create: 生成令牌接口
+
+### 3.1 插件部分核心代码
+
+```golang
+package main
+...
+
+const (
+ AuthUIDHeader = "x-auth-user"
+)
+
+func main() {
+ wrapper.SetCtx(
+ // 插件名称
+ "easy-jwt",
+ // 设置自定义函数解析插件配置
+ wrapper.ParseConfigBy(parseConfig),
+ // 设置自定义函数处理请求头
+ wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
+ )
+}
+
+// 自定义插件配置
+type JwtConfig struct {
+ // HTTP Client
+ client wrapper.HttpClient
+ // 令牌服务器的完全限定域名
+ tokenServerFQDN string
+ // 令牌服务器的端口
+ tokenServerPort int
+ // HTTP请求头中包含令牌的字段名称
+ tokenFromHeaderName string
+ // 令牌前缀,如Bearer
+ tokenFromHeaderPrefix string
+ // 插件将忽略令牌验证 UR L列表
+ ignoreUrls []string
+ // 匿名令牌,用于未认证的请求
+ anonymousToken string
+ // 匿名用户ID
+ anonymousUID int
+ // 当令牌验证失败时返回的 HTTP 状态码
+ responseErrorStatusCode uint32
+ // 返回的错误信息格式
+ responseErrorBody string
+}
+
+func parseConfig(json gjson.Result, config *JwtConfig, log wrapper.Log) error {
+ log.Debugf("parseConfig()")
+ // 解析插件配置
+ config.tokenServerFQDN = json.Get("tokenServerFQDN").String()
+ config.tokenServerPort = int(json.Get("tokenServerPort").Int())
+ config.tokenFromHeaderName = json.Get("tokenFromHeaderName").String()
+ config.tokenFromHeaderPrefix = json.Get("tokenFromHeaderPrefix").String()
+ config.anonymousUID = int(json.Get("anonymousUID").Int())
+ config.anonymousToken = json.Get("anonymousToken").String()
+ config.responseErrorBody = json.Get("responseErrorBoy").String()
+ config.responseErrorStatusCode = uint32(json.Get("responseErrorStatusCode").Int())
+ config.responseErrorBody = json.Get("responseErrorBody").String()
+ config.ignoreUrls = make([]string, 0)
+ for _, item := range json.Get("ignoreUrls").Array() {
+ config.ignoreUrls = append(config.ignoreUrls, item.String())
+ }
+ // 设置 HTTP Client
+ config.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
+ FQDN: json.Get("tokenServerFQDN").String(),
+ Port: json.Get("tokenServerPort").Int(),
+ })
+ log.Debugf("parseConfig result:%+v", config)
+ return nil
+}
+
+func onHttpRequestHeaders(ctx wrapper.HttpContext, config JwtConfig, log wrapper.Log) types.Action {
+ // 首先检查请求的路径是否在 ignoreUrls 列表中,如果是,则添加匿名用户ID到请求头并继续处理请求
+ rawPath := ctx.Path()
+ path, _ := url.Parse(rawPath)
+ for _, url := range config.ignoreUrls {
+ if isPathMatch(path.Path, url) {
+ proxywasm.AddHttpRequestHeader(AuthUIDHeader, fmt.Sprintf("%d", config.anonymousUID))
+ return types.ActionContinue
+ }
+ }
+ // 如果请求头中包含令牌,插件将尝试从请求头中提取令牌
+ token, err := extractTokenFromHeader(ctx, config)
+ if err != nil {
+ log.Debugf("extractTokenFromHeader() error: %v", err)
+ body := fmt.Sprintf(config.responseErrorBody, err.Error())
+ proxywasm.SendHttpResponse(config.responseErrorStatusCode, [][2]string{{"content-type", "application/json"}}, []byte(body), -1)
+ return types.ActionContinue
+ }
+ // 如果是匿名令牌,则添加匿名用户ID到请求头并继续处理请求
+ if len(config.anonymousToken) > 0 && config.anonymousToken == token {
+ proxywasm.AddHttpRequestHeader(AuthUIDHeader, fmt.Sprintf("%d", config.anonymousUID))
+ return types.ActionContinue
+ }
+
+ authRequest, _ := json.Marshal(map[string]string{"token": token})
+ log.Debugf("call token-server with auth request:%s", string(authRequest))
+ // 插件将使用配置的HTTP客户端向令牌服务器发送POST请求,以验证令牌的有效性
+ err2 := config.client.Post(
+ "/api/token/auth",
+ [][2]string{{"content-type", "application/json"}},
+ authRequest,
+ func(statusCode int, responseHeaders http.Header, responseBody []byte) {
+ defer func() {
+ // 保证恢复请求
+ _ = proxywasm.ResumeHttpRequest()
+ }()
+
+ log.Debugf("auth response status:%d, response:%s", statusCode, string(responseBody))
+ var jsonData gjson.Result
+ jsonData = gjson.ParseBytes(responseBody)
+ if statusCode != 200 {
+ // 如果响应状态码不是200,表示验证失败,插件将直接发送错误响应给客户端。
+ message := jsonData.Get("message").String()
+ body := fmt.Sprintf(config.responseErrorBody, message)
+ proxywasm.SendHttpResponse(config.responseErrorStatusCode, [][2]string{{"content-type", "application/json"}}, []byte(body), -1)
+ } else {
+ // 如果验证成功,插件将从响应中提取用户ID,并将其添加到后续请求头中
+ uid := jsonData.Get("uid").Int()
+ proxywasm.AddHttpRequestHeader(AuthUIDHeader, fmt.Sprintf("%d", uid))
+ }
+ },
+ 2000,
+ )
+
+ if err2 != nil {
+ // 如果连接失败,则直接发送错误响应给客户端。
+ log.Debugf("call token server error:%v", err2)
+ body := fmt.Sprintf(config.responseErrorBody, err2.Error())
+ proxywasm.SendHttpResponse(config.responseErrorStatusCode, [][2]string{{"content-type", "application/json"}}, []byte(body), -1)
+ return types.ActionContinue
+ }
+ // 暂停请求处理,直到调用 proxywasm.ResumeHttpRequest() 恢复请求
+ return types.ActionPause
+}
+
+func extractTokenFromHeader(ctx wrapper.HttpContext, config JwtConfig) (string, error) {
+ ...
+}
+
+func isPathMatch(path string, url string) bool {
+ ...
+}
+```
+
+核心流程如下:
+- 初始化插件
+- 解析配置
+- onHttpRequestHeaders 处理
+ - 检查请求路径是否在 ignoreUrls 列表中
+ - 是:添加匿名 UID 到请求头,继续处理请求
+ - 否:继续
+ - 从请求头中提取令牌,检查令牌是否存在
+ - 存在:继续
+ - 不存在:返回错误,发送响应
+ - 验证令牌
+ - 如果令牌是匿名令牌,添加匿名 UID 到请求头,继续处理请求
+ - 如果令牌不是匿名令牌,调用认证服务 /api/token/auth 接口验证令牌
+ - 如果验证成功,从响应中提取 UID,添加到请求头中,继续处理请求
+ - 如果验证失败,返回错误,发送响应
+
+### 3.2 部署和验证
+
+1. 部署 YAML 如下:
+```yaml
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: higress-course
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: echo-server
+ namespace: higress-course
+spec:
+ selector:
+ app: echo-server
+ ports:
+ - protocol: TCP
+ port: 8080
+ targetPort: 3000
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: echo-server
+ namespace: higress-course
+ labels:
+ app: echo-server
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: echo-server
+ template:
+ metadata:
+ labels:
+ app: echo-server
+ spec:
+ containers:
+ - name: echo-server
+ image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-server:1.3.0
+ env:
+ - name: POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ resources:
+ requests:
+ cpu: 10m
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: echo-server
+ namespace: higress-course
+spec:
+ selector:
+ app: echo-server
+ ports:
+ - protocol: TCP
+ port: 8080
+ targetPort: 3000
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: token-server
+ namespace: higress-course
+ labels:
+ app: token-server
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: token-server
+ template:
+ metadata:
+ labels:
+ app: token-server
+ spec:
+ containers:
+ - name: token-server
+ image: registry.cn-hangzhou.aliyuncs.com/2456868764/token-server:1.0.0
+ env:
+ - name: POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ resources:
+ requests:
+ cpu: 10m
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: token-server
+ namespace: higress-course
+spec:
+ selector:
+ app: token-server
+ ports:
+ - protocol: TCP
+ port: 9090
+ targetPort: 9090
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-foo
+ namespace: higress-course
+spec:
+ ingressClassName: higress
+ rules:
+ - host: "foo.com"
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: echo-server
+ port:
+ number: 8080
+---
+apiVersion: networking.higress.io/v1
+kind: McpBridge
+metadata:
+ name: default
+ namespace: higress-system
+spec:
+ registries:
+ - name: token-server
+ domain: token-server.higress-course.svc.cluster.local
+ port: 9090
+ type: dns
+---
+apiVersion: extensions.higress.io/v1alpha1
+kind: WasmPlugin
+metadata:
+ name: easy-jwt
+ namespace: higress-system
+spec:
+ priority: 200
+ matchRules:
+ - ingress:
+ - higress-course/ingress-foo
+ config:
+ tokenServerFQDN: "token-server.dns"
+ tokenServerPort: 9090
+ tokenFromHeaderName: "Authorization"
+ tokenFromHeaderPrefix: "Bearer "
+ anonymousToken: "AnonymousToken"
+ anonymousUID: 0
+ responseErrorStatusCode: 401
+ responseErrorBody: "{\"message\":\"%s\"}"
+ url: oci://registry.cn-hangzhou.aliyuncs.com/2456868764/easy-jwt:1.0.0
+```
+2. 获取令牌
+
+获取 uid 为 100 的用户的访问令牌,其命令如下,其中 `` 是 token-server pod 名称。
+```shell
+kubectl exec -n higress-course -- curl -X POST http://127.0.0.1:9090/api/token/create -d '{"uid":100}' -H "content-type:application/json"
+
+{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVSUQiOjEwMCwiZXhwIjoxNzU0Mzg2MzQ4fQ.jncbLJqBern5DYCFvED3moiCvg6sUn5jdlllhneuHrY"}%
+```
+
+3. 请求验证
+
+```shell
+curl http://127.0.0.1/hello -X POST -d "{}" -H "host:foo.com" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVSUQiOjEwMCwiZXhwIjoxNzU0Mzg2MzQ4fQ.jncbLJqBern5DYCFvED3moiCvg6sUn5jdlllhneuHrY" -H "content-type:application/json"
+
+{
+ "path": "/hello",
+ "host": "foo.com",
+ "method": "POST",
+ "proto": "HTTP/1.1",
+ "headers": {
+ "Accept": [
+ "*/*"
+ ],
+ "Authorization": [
+ "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVSUQiOjEwMCwiZXhwIjoxNzU0Mzg2MzQ4fQ.jncbLJqBern5DYCFvED3moiCvg6sUn5jdlllhneuHrY"
+ ],
+ "Content-Length": [
+ "2"
+ ],
+ "Content-Type": [
+ "application/json"
+ ],
+ "Original-Host": [
+ "foo.com"
+ ],
+ "Req-Start-Time": [
+ "1722850461721"
+ ],
+ "User-Agent": [
+ "curl/8.1.2"
+ ],
+ "X-Auth-User": [
+ "100"
+ ],
+ "X-B3-Sampled": [
+ "0"
+ ],
+ "X-B3-Spanid": [
+ "642eab8e332d6500"
+ ],
+ "X-B3-Traceid": [
+ "d9b9e94203603997642eab8e332d6500"
+ ],
+ "X-Envoy-Attempt-Count": [
+ "1"
+ ],
+ "X-Envoy-Decorator-Operation": [
+ "echo-server.higress-course.svc.cluster.local:8080/*"
+ ],
+ "X-Envoy-Internal": [
+ "true"
+ ],
+ "X-Forwarded-For": [
+ "192.168.65.1"
+ ],
+ "X-Forwarded-Proto": [
+ "http"
+ ],
+ "X-Request-Id": [
+ "47ff21bc-c3d5-4932-8bfb-361d268d319d"
+ ]
+ },
+ "namespace": "higress-course",
+ "ingress": "",
+ "service": "",
+ "pod": "echo-server-6f4df5fcff-nksqz",
+ "body": {}
+}
+```
+可以看到请求头中包含了 `X-Auth-User` 同时值为 100 。
+
+## 4 ext-auth 插件
+
+Higress 官方提供 [ext-auth](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ext-auth) 插件,其功能更加丰富。 ext-auth 插件实现了向外部授权服务发送鉴权请求,以检查客户端请求是否得到授权。该插件实现时参考了 Envoy 原生的 [ext_authz filter](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_authz_filter),实现了原生 filter 中对接 HTTP 服务的部分能力。
+
+## 5 Envoy Cluster 不存在问题
+
+在默认情况下,Higress 控制面只下发和路由关联的服务到 Envoy Cluster 中,因此有可能在实际开发过程中,发现对应调用 HTTP 服务在 Envoy Cluster 中不存在。
+有 3 种方案去解决:
+- helm 参数 global.onlyPushRouteCluster, 默认值为 true, 只推送路由关联的 Cluster 到 Envoy Cluster 中。修改为 false 即可。
+- 创建一个新路由关联到对应的调用的 HTTP 服务。
+- 通过 McpBridge 配置,添加调用的 HTTP 服务。
+
+上面 easy-jwt 插件中调用 token-server 服务,是通过 McpBridge 配置,添加 dns 类型服务,其配置如下:
+
+```yaml
+apiVersion: networking.higress.io/v1
+kind: McpBridge
+metadata:
+ name: default
+ namespace: higress-system
+spec:
+ registries:
+ - name: token-server
+ domain: token-server.higress-course.svc.cluster.local
+ port: 9090
+ type: dns
+```
+
+## 6 HTTP 回调链问题
+
+在实际开发过程中,可能会遇到 HTTP 回调链的情况,比如在 onHttpRequestHeader 处理阶段,需要调用两个 HTTP 服务,这个时候在 onHttpRequestHeader 阶段中,要先调用第一个 HTTP 服务,在第一个 HTTP 服务的响应回调函数中,再发起第二个 HTTP 服务的调用。
+以此类推。这种情况 Redis 调用也是一样处理。 关于回调链可以参考 Higress 官方提供 [ai-agent](https://github.com/alibaba/higress/blob/main/plugins/wasm-go/extensions/ai-agent/main.go#L169) 插件功能。
+
+## 参考
+- [1][Mcp Bridge 配置说明](https://higress.io/docs/latest/user/mcp-bridge/)
+
+
+
+
+
+
diff --git a/src/content/docs/ebook/en/wasm18.md b/src/content/docs/ebook/en/wasm18.md
new file mode 100644
index 0000000000..4b690a58ba
--- /dev/null
+++ b/src/content/docs/ebook/en/wasm18.md
@@ -0,0 +1,720 @@
+---
+title: Redis 调用
+keywords: [Higress]
+---
+
+# Redis 调用
+
+本章介绍如何在插件中调用 Redis、本地开发环境搭建、以及开发基于令牌桶限流插件。
+
+## 1 Redis 调用
+
+Higress 插件的 Go SDK 中 redis_wrapper.go 封装 Redis 调用, 部分核心代码如下:
+
+```shell
+// Redis 回调函数
+type RedisResponseCallback func(response resp.Value)
+
+// Redis 调用接口
+type RedisClient interface {
+ // 初始化接口
+ Init(username, password string, timeout int64) error
+ // with this function, you can call redis as if you are using redis-cli
+ }
+ // 命令接口
+ Command(cmds []interface{}, callback RedisResponseCallback) error
+ // Lua脚本接口
+ Eval(script string, numkeys int, keys, args []interface{}, callback RedisResponseCallback) error
+
+ // 以下是 Redis 各种命令接口
+ // Key
+ Del(key string, callback RedisResponseCallback) error
+ Exists(key string, callback RedisResponseCallback) error
+ Expire(key string, ttl int, callback RedisResponseCallback) error
+ Persist(key string, callback RedisResponseCallback) error
+ ...
+}
+
+// RedisClusterClient, Redis 调用接口具体实现
+type RedisClusterClient[C Cluster] struct {
+ cluster C
+}
+
+func RedisInit(cluster Cluster, username, password string, timeout uint32) error {
+ return proxywasm.RedisInit(cluster.ClusterName(), username, password, timeout)
+}
+// 真正调用 Redis 的函数
+func RedisCall(cluster Cluster, respQuery []byte, callback RedisResponseCallback) error {
+ requestID := uuid.New().String()
+ _, err := proxywasm.DispatchRedisCall(
+ cluster.ClusterName(),
+ respQuery,
+ func(status int, responseSize int) {
+ response, err := proxywasm.GetRedisCallResponse(0, responseSize)
+ var responseValue resp.Value
+ // 获取 Redis 回调结果 responseValue
+ ...
+ if callback != nil {
+ // 调用回调函数
+ callback(responseValue)
+ }
+ })
+ ...
+ return err
+}
+```
+
+所有调用 Redis 的接口,最终通过 RedisCall 调用 Redis, 同时回调 RedisResponseCallback 回调函数。
+
+## 2 令牌桶限流
+
+常见的限流算法有固定窗口限流算法、滑动窗口限流算法、漏桶限流算法、令牌桶限流算法等。这里主要介绍令牌桶限流算法。
+令牌桶算法原理:
+- 令牌以固定的频率被添加到令牌桶中。
+- 如果令牌数量满了,超过令牌桶容量的限制,那就丢弃。
+- 系统在接受到一个用户请求时,都会先去令牌桶要一个令牌。如果拿到令牌,那么就处理这个请求的业务逻辑。
+- 如果拿不到令牌,就直接拒绝这个请求。
+
+令牌桶算法允许一定量的突发请求,因为桶可以存储一定数量的令牌,从而在短期内处理更多的请求。具体原理见下图:
+![img](https://img.alicdn.com/imgextra/i2/O1CN018T2vsi1bbGU9PeVx6_!!6000000003483-0-tps-902-922.jpg)
+
+关于 QPS 限流算法和令牌桶算法两种限流算法优缺点,可以参考:[限流算法选择](https://help.aliyun.com/document_detail/149952.html)。
+
+## 3 本地开发环境搭建
+
+### 3.1 初始化工程目录
+
+1. 新建一个工程目录文件 cluster-bucket-limit。
+
+```shell
+mkdir cluster-bucket-limit
+```
+2. 在所建目录下执行以下命令,初始化 Go 工程。
+
+```shell
+go mod init cluster-bucket-limit
+```
+更详细信息参考第十四章 Wasm 插件介绍和开发自定义插件。
+
+### 3.2 Makefile、Dockerfile、docker-compose.yaml、envoy.yaml 文件
+
+1. Makefile、Dockerfile
+
+Makefile、Dockerfile 文件参考第十四章 Wasm 插件介绍和开发自定义插件。
+
+2. docker-compose.yaml
+
+```yaml
+version: '3.9'
+services:
+ envoy:
+ image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v1.4.1
+ entrypoint: /usr/local/bin/envoy
+ # 注意这里对wasm开启了debug级别日志,正式部署时则默认info级别
+ command: -c /etc/envoy/envoy.yaml --log-level info --log-path /etc/envoy/envoy.log --component-log-level wasm:debug
+ depends_on:
+ - echo-server
+ networks:
+ - wasmtest
+ ports:
+ - "10000:10000"
+ - "9901:9901"
+ volumes:
+ - ./envoy.yaml:/etc/envoy/envoy.yaml
+ - ./build/plugin.wasm:/etc/envoy/plugin.wasm
+ - ./envoy.log:/etc/envoy/envoy.log
+ echo-server:
+ image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-server:1.3.0
+ networks:
+ - wasmtest
+ ports:
+ - "3000:3000"
+ redis:
+ image: registry.cn-hangzhou.aliyuncs.com/2456868764/redis:latest
+ environment:
+ - ALLOW_EMPTY_PASSWORD=yes
+ networks:
+ wasmtest:
+ ipv4_address: 172.20.0.100
+ ports:
+ - "6379:6379"
+networks:
+ wasmtest:
+ ipam:
+ config:
+ - subnet: 172.20.0.0/24
+
+```
+
+3. envoy.yaml 文件
+
+envoy.yaml 配置文件如下:
+```yaml
+admin:
+ address:
+ socket_address:
+ protocol: TCP
+ address: 0.0.0.0
+ port_value: 9901
+static_resources:
+ listeners:
+ - name: listener_0
+ address:
+ socket_address:
+ protocol: TCP
+ address: 0.0.0.0
+ port_value: 10000
+ 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
+ scheme_header_transformation:
+ scheme_to_overwrite: https
+ stat_prefix: ingress_http
+ route_config:
+ name: local_route
+ virtual_hosts:
+ - name: local_service
+ domains: ["*"]
+ routes:
+ - match:
+ prefix: "/"
+ route:
+ cluster: echo-server
+ http_filters:
+ - name: wasmdemo
+ typed_config:
+ "@type": type.googleapis.com/udpa.type.v1.TypedStruct
+ type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ value:
+ config:
+ name: wasmdemo
+ vm_config:
+ runtime: envoy.wasm.runtime.v8
+ code:
+ local:
+ filename: /etc/envoy/plugin.wasm
+ configuration:
+ "@type": "type.googleapis.com/google.protobuf.StringValue"
+ value: |-
+ {
+ "keys": [
+ "authorization"
+ ],
+ "in_header": true,
+ "limits": [
+ {
+ "name": "credential1",
+ "consumer": "Bearer credential1",
+ "rate": 2,
+ "capacity": 4
+ },
+ {
+ "name": "all",
+ "consumer": "*",
+ "rate": 1,
+ "capacity": 2
+ }
+ ],
+ "rejected_code": 429,
+ "rejected_msg": "Too Many Requests",
+ "show_limit_quota_header": true,
+ "redis":{
+ "service_name": "redis.static",
+ "service_port": 6379,
+ "timeout": 2000
+ }
+ }
+ - name: envoy.filters.http.router
+ clusters:
+ - name: echo-server
+ connect_timeout: 30s
+ type: LOGICAL_DNS
+ # Comment out the following line to test on v6 networks
+ dns_lookup_family: V4_ONLY
+ lb_policy: ROUND_ROBIN
+ load_assignment:
+ cluster_name: echo-server
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: echo-server
+ port_value: 3000
+ - name: outbound|6379||redis.static
+ connect_timeout: 30s
+ type: STATIC
+ load_assignment:
+ cluster_name: outbound|6379||redis.static
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: 172.20.0.100
+ port_value: 6379
+```
+envoy.yaml 配置文件增加了 `outbound|6379||redis.static` 集群,用于连接 Redis 服务。Redis 连接目前不支持 DNS 域名配置,只支持 IP 地址配置。
+因此这里 Redis 的 IP 地址是 `172.20.0.100`。
+
+## 3 令牌桶限流插件开发
+
+### 3.1 插件配置和配置解析
+
+插件配置和配置解析部分核心代码如下:
+```golang
+// LimitConfig 定义了限流插件的配置结构。
+type LimitConfig struct {
+ Keys []string `yaml:"keys"` // 定义了用于提取限流信息的HTTP请求头字段名称。
+ InQuery bool `yaml:"in_query,omitempty"` // 标识是否从查询参数中获取限流信息。
+ InHeader bool `yaml:"in_header,omitempty"` // 标识是否从请求头中获取限流信息。
+ Limits []LimitItem `yaml:"limits"` // 包含具体的限流规则项。
+ RejectedCode uint32 `yaml:"rejected_code"` // 请求超过阈值被拒绝时返回的 HTTP 状态码。
+ RejectedMsg string `yaml:"rejected_msg"` // 请求超过阈值被拒绝时返回的响应体。
+ ShowLimitQuotaHeader bool `yaml:"show_limit_quota_header"` // 标识是否在响应头中显示限流配额信息。
+ RedisInfo RedisInfo `yaml:"redis"` // 定义了与 Redis 交互所需的信息。
+ RedisClient wrapper.RedisClient // Redis客户端,用于执行Redis命令。
+}
+
+// LimitItem 定义了具体的限流项,包括限流名称、消费者标识、请求速率和容量。
+type LimitItem struct {
+ Name string `yaml:"name"` // 限流项的名称。
+ Consumer string `yaml:"consumer"` // 限流项关联的消费者标识,用于匹配特定的请求。
+ Rate int `yaml:"rate"` // 每秒放入桶内的令牌数量。
+ Capacity int `yaml:"capacity"` // 限流桶的最大容量。
+}
+
+// RedisInfo 定义了连接Redis所需的详细信息。
+type RedisInfo struct {
+ ServiceName string `required:"true" yaml:"service_name" json:"service_name"` // Redis 服务名或地址。
+ ServicePort int `yaml:"service_port" json:"service_port"` // Redis 服务端口。
+ Username string `yaml:"username" json:"username"` // 连接 Redis 的用户名,如果需要。
+ Password string `yaml:"password" json:"password"` // 连接 Redis 的密码,如果需要。
+ Timeout int `yaml:"timeout" json:"timeout"` // 连接 Redis 的超时时间(毫秒)。
+}
+
+// LimitContext 定义了限流上下文,存储了限流相关的信息。
+type LimitContext struct {
+ Allowed int // 表示当前请求是否被允许。
+ Count int // 限流桶当前的计数。
+ Remaining int // 限流桶剩余的容量。
+ Reset int // 限流桶重置时间(秒)。
+}
+
+// 解析配置,这里忽略插件配置解析的细节。主要显示 Redis 部分解析配置。
+func parseConfig(json gjson.Result, config *LimitConfig, log wrapper.Log) error {
+ // keys
+ names := json.Get("keys")
+ ...
+ // in_query and in_header
+ in_query := json.Get("in_query")
+ in_header := json.Get("in_header")
+ ...
+ // parse limit
+ limits := json.Get("limits")
+ ...
+ config.ShowLimitQuotaHeader = json.Get("show_limit_quota_header").Bool()
+
+ // parse redis
+ redisConfig := json.Get("redis")
+ if !redisConfig.Exists() {
+ return errors.New("missing redis in config")
+ }
+ serviceName := redisConfig.Get("service_name").String()
+ if serviceName == "" {
+ return errors.New("redis service name must not be empty")
+ }
+ servicePort := int(redisConfig.Get("service_port").Int())
+ if servicePort == 0 {
+ if strings.HasSuffix(serviceName, ".static") {
+ // use default logic port which is 80 for static service
+ servicePort = 80
+ } else {
+ servicePort = 6379
+ }
+ }
+ username := redisConfig.Get("username").String()
+ password := redisConfig.Get("password").String()
+ timeout := int(redisConfig.Get("timeout").Int())
+ if timeout == 0 {
+ timeout = 1000
+ }
+ config.RedisInfo.ServiceName = serviceName
+ config.RedisInfo.ServicePort = servicePort
+ config.RedisInfo.Username = username
+ config.RedisInfo.Password = password
+ config.RedisInfo.Timeout = timeout
+ config.RedisClient = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
+ FQDN: serviceName,
+ Port: int64(servicePort),
+ })
+ log.Debugf("parseConfig()+%+v", config)
+ return config.RedisClient.Init(username, password, int64(timeout))
+}
+```
+
+这里忽略插件配置解析的细节,主要显示 Redis 解析配置。可以看出这里需要调用 `wrapper.NewRedisClusterClient` 方法初始化 RedisClient 和 RedisClient `Init` 方法初始化 Redis 连接。
+
+### 3.2 插件限流 Lua 脚本
+令牌桶限流的 Lua 脚本如下:
+```lua
+local tokens_key = KEYS[1]
+local timestamp_key = KEYS[2]
+--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)
+
+local rate = tonumber(ARGV[1])
+local capacity = tonumber(ARGV[2])
+local now = tonumber(ARGV[3])
+local requested = tonumber(ARGV[4])
+local unit = tonumber(ARGV[5])
+
+local fill_time = capacity/rate
+local ttl = math.floor(fill_time*2*unit)
+
+--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
+--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
+--redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
+--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
+--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
+--redis.log(redis.LOG_WARNING, "ttl " .. ttl)
+
+local last_tokens = tonumber(redis.call("get", tokens_key))
+if last_tokens == nil then
+ last_tokens = capacity
+end
+--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)
+
+local last_refreshed = tonumber(redis.call("get", timestamp_key))
+if last_refreshed == nil then
+ last_refreshed = 0
+end
+--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)
+
+local delta = math.max(0, (now-last_refreshed)/unit)
+local filled_tokens = math.min(capacity, last_tokens + math.floor(delta*rate))
+local allowed = filled_tokens >= requested
+local new_tokens = filled_tokens
+local allowed_num = 0
+if allowed then
+ new_tokens = filled_tokens - requested
+ allowed_num = 1
+end
+
+--redis.log(redis.LOG_WARNING, "delta " .. delta)
+--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
+--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
+--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)
+
+if ttl > 0 then
+ redis.call("setex", tokens_key, ttl, new_tokens)
+ redis.call("setex", timestamp_key, ttl, now)
+end
+
+return { allowed_num, new_tokens, capacity, ttl}
+```
+Lua 脚本是在 Redis 中执行的,用于实现令牌桶限流算法。下面是对脚本参数和原理的分析:
+
+1. 参数解释:
+- KEYS[1] 和 KEYS[2]:这两个参数是通过 Redis 调用传递的键(keys),通常用于存储令牌桶的当前令牌数(tokens_key)和最后刷新时间(timestamp_key)。
+- ARGV[1] 到 ARGV[5]:这些参数是通过 Redis 调用传递的参数,用于配置限流策略。
+ - rate:单位时间内生成的令牌数量。
+ - capacity:令牌桶的容量,即最多可以容纳的令牌数。
+ - now:当前时间,通常以时间戳表示,以秒单位。
+ - requested:当前请求需要的令牌数。
+ - unit:令牌生成的时间单位,1 表示秒,60 表示分钟,3600 表示小时。
+2. 脚本原理:
+- 初始化变量:根据传入的参数初始化令牌桶的填充时间和 TTL(生存时间)。
+- 获取当前状态:从 Redis 中获取当前的令牌数(last_tokens)和最后刷新时间(last_refreshed)。如果不存在,则初始化为令牌桶的容量。
+- 计算令牌填充:
+ - delta:自上次刷新以来经过的单位时间。
+ - filled_tokens:根据 delta 和 rate 计算应该填充的令牌数,但不能超过桶的容量。
+- 判断是否允许请求:
+ - allowed:如果当前令牌数加上填充的令牌数大于等于请求的令牌数,则允许请求。
+- 更新令牌数:
+ - 如果请求被允许,从当前令牌数中减去请求的令牌数,更新 new_tokens。
+- 设置新的状态:
+ - 如果 TTL 大于 0,则更新 Redis 中的令牌数和时间戳,设置新的 TTL。
+- 返回结果:
+ - 返回一个包含限流结果的数组,包括是否允许请求、新的令牌数、桶容量和 TTL。
+
+### 3.3 插件限流具体实现
+
+限流主要实现在插件 `onHttpRequestHeaders` 方法中,部分核心代码如下:
+
+```golang
+// onHttpRequestHeaders 函数在处理 HTTP 请求头时被调用,用于执行限流逻辑。
+func onHttpRequestHeaders(ctx wrapper.HttpContext, config LimitConfig, log wrapper.Log) types.Action {
+ log.Debugf("onHttpRequestHeaders()")
+ // 初始化 tokens 切片,用于存储从请求头或查询参数中提取的 tokens 信息
+ var tokens []string
+
+ // 如果配置指定从请求头中获取 tokens 信息
+ if config.InHeader {
+ // 遍历配置中定义的所有键(key),尝试从请求头中获取每个键的值
+ for _, key := range config.Keys {
+ // 使用 proxywasm 库的 GetHttpRequestHeader 函数获取请求头中的值
+ value, err := proxywasm.GetHttpRequestHeader(key)
+ // 如果没有错误且值不为空,则将值添加到 tokens 切片中
+ if err == nil && value != "" {
+ tokens = append(tokens, value)
+ }
+ }
+ } else if config.InQuery {
+ // 如果配置指定从查询参数中获取 tokens 信息
+ // 获取 ":path" 请求头以获取请求 URL
+ requestUrl, _ := proxywasm.GetHttpRequestHeader(":path")
+ // 解析 URL 并获取查询参数
+ url, _ := url.Parse(requestUrl)
+ queryValues := url.Query()
+
+ // 遍历配置中定义的所有键
+ for _, key := range config.Keys {
+ // 从查询参数中获取每个键的值
+ values, ok := queryValues[key]
+ // 如果查询参数存在且有值,则将值添加到 tokens 切片中
+ if ok && len(values) > 0 {
+ tokens = append(tokens, values...)
+ }
+ }
+ }
+
+ // 如果从请求中提取了多于一个的 tokens,返回错误处理
+ if len(tokens) > 1 {
+ return deniedMultiKeyAuthData()
+ } else if len(tokens) <= 0 {
+ // 如果没有提取到 tokens,返回错误处理
+ return deniedNoKeyAuthData()
+ }
+
+ // 提取第一个 token 作为主要的令牌信息
+ limitKey := strings.Split(tokens[0], ",")[0]
+ log.Debugf("limitKey:%s", limitKey)
+
+ // 根据提取的 limitKey 查找对应的限流项
+ limitItem := findLimitItem(config, limitKey)
+ log.Debugf("limitItem:%+v", limitItem)
+
+ // 如果没有找到对应的限流项,继续处理请求
+ if limitItem.Consumer == "" {
+ return types.ActionContinue
+ }
+
+ // 构建 Redis 脚本需要的键,用于操作令牌桶
+ tokenKey := fmt.Sprintf(ClusterRateLimitFormat, limitKey, "token")
+ expireKey := fmt.Sprintf(ClusterRateLimitFormat, limitKey, "expire")
+
+ // 获取当前时间,用于计算令牌桶的填充状态
+ now := time.Now()
+ // 将当前时间转换为 Unix 时间戳(秒)
+ unixTimestamp := now.UnixNano()
+ milliseconds := unixTimestamp / 1e6
+ seconds := milliseconds / 1000
+
+ // 构建调用 Redis 脚本所需的参数
+ keys := []interface{}{tokenKey, expireKey}
+ args := []interface{}{limitItem.Rate, limitItem.Capacity, seconds, 1, 1}
+
+ // 调用 Redis 脚本执行限流逻辑
+ err := config.RedisClient.Eval(BucketTokenScript, 2, keys, args, func(response resp.Value) {
+ log.Debugf("RedisClient.Eval(),keys:%+v,args:%+v", keys, args)
+ // 检查脚本返回的结果是否包含 4 个元素
+ resultArray := response.Array()
+ if len(resultArray) != 4 {
+ log.Errorf("redis response parse error, response: %v", response)
+ proxywasm.ResumeHttpRequest()
+ return
+ }
+ // 根据脚本返回的结果创建 LimitContext 对象
+ context := LimitContext{
+ Allowed: resultArray[0].Integer(),
+ Remaining: resultArray[1].Integer(),
+ Count: resultArray[2].Integer(),
+ Reset: resultArray[3].Integer(),
+ }
+ log.Debugf("context:%+v", context)
+ // 如果请求未被允许(Allowed <= 0),触发限流逻辑
+ if context.Allowed <= 0 {
+ log.Debugf("request rejected")
+ rejected(config, context)
+ return
+ } else {
+ // 将限流上下文存储在 HttpContext 中,供后续处理使用
+ ctx.SetContext(LimitContextKey, context)
+ }
+ // 恢复 HTTP 请求处理
+ proxywasm.ResumeHttpRequest()
+ })
+ // 如果调用 Redis 脚本时出现错误,记录错误并继续处理请求
+ if err != nil {
+ log.Errorf("redis call failed: %v", err)
+ return types.ActionContinue
+ }
+ // 暂停处理当前请求头,等待 Redis 脚本调用完成
+ return types.HeaderStopAllIterationAndWatermark
+}
+
+// findLimitItem 函数用于在给定的配置中查找与特定消费者匹配的限流项。如果没有找到具体的匹配项,它将返回一个默认的 LimitItem 结构。
+func findLimitItem(config LimitConfig, key string) LimitItem {
+ // 遍历配置中的所有限流项
+ for _, limitItem := range config.Limits {
+ // 检查当前限流项的消费者字段是否与提供的 key 匹配,且消费者不是通配符"*"
+ if limitItem.Consumer == key && limitItem.Consumer != "*" {
+ // 如果找到匹配的限流项,返回这个限流项
+ return limitItem
+ }
+ }
+ // 再次遍历配置中的所有限流项,这次是为了查找通配符"*"的消费者
+ // 通配符"*"表示这个限流项适用于所有消费者
+ for _, limitItem := range config.Limits {
+ if limitItem.Consumer == "*" {
+ // 如果找到通配符限流项,返回这个限流项
+ return limitItem
+ }
+ }
+ // 则返回一个空的 LimitItem 结构,表示没有找到任何适用的限流规则
+ return LimitItem{}
+}
+```
+onHttpRequestHeaders 函数的核心逻辑可以概括为以下几个步骤:
+- 获取 Tokens:根据配置(config.InHeader 或 config.InQuery),从请求头或查询参数中提取用于限流的 tokens 信息。
+- 验证 Tokens:检查提取的 tokens 是否存在且数量合理(不能多于一个),如果不符合要求,返回相应的错误处理。
+- 查找限流项:使用提取的 tokens 查找配置中的限流规则(LimitItem),如果没有找到适用的限流规则,则允许请求继续。
+- 执行限流逻辑:如果找到限流规则,构建 Redis 脚本需要的键和参数,然后调用 Redis 脚本执行限流算法。
+- 处理 Redis 脚本结果:根据 Redis 脚本返回的结果,创建 LimitContext 对象并根据算法结果决定是否允许请求继续:
+ - 如果请求被拒绝(context.Allowed \<= 0),执行限流逻辑并通知客户端。
+ - 如果请求被允许,将 LimitContext 对象存储在 HttpContext 中,供后续处理使用。
+
+
+## 4 测试和验证
+
+1. 正常流量
+```shell
+curl -X POST -v http://127.0.0.1:10000/hello \
+ -H 'Authorization: Bearer credential1' \
+ -H 'Content-type: application/json' \
+ -H 'host:foo.com' \
+ -d '{"username":["unamexxxx"],"password":["pswdxxxx"]}'
+
+* Trying 127.0.0.1:10000...
+* Connected to 127.0.0.1 (127.0.0.1) port 10000 (#0)
+> POST /hello HTTP/1.1
+> Host:foo.com
+> User-Agent: curl/8.1.2
+> Accept: */*
+> Authorization: Bearer credential1
+> Content-type: application/json
+> Content-Length: 50
+>
+< HTTP/1.1 200 OK
+< content-type: application/json
+< x-content-type-options: nosniff
+< date: Tue, 20 Aug 2024 08:55:13 GMT
+< content-length: 692
+< req-cost-time: 42
+< req-arrive-time: 1724144113842
+< resp-start-time: 1724144113885
+< x-envoy-upstream-service-time: 8
+< x-ratelimit-limit: 4
+< x-ratelimit-remaining: 3
+< server: envoy
+<
+{
+ "path": "/hello",
+ "host": "foo.com",
+ "method": "POST",
+ "proto": "HTTP/1.1",
+ "headers": {
+ "Accept": [
+ "*/*"
+ ],
+ "Authorization": [
+ "Bearer credential1"
+ ],
+ "Content-Length": [
+ "50"
+ ],
+ "Content-Type": [
+ "application/json"
+ ],
+ "Original-Host": [
+ "foo.com"
+ ],
+ "Req-Start-Time": [
+ "1724144113842"
+ ],
+ "User-Agent": [
+ "curl/8.1.2"
+ ],
+ "X-Envoy-Expected-Rq-Timeout-Ms": [
+ "15000"
+ ],
+ "X-Forwarded-Proto": [
+ "https"
+ ],
+ "X-Request-Id": [
+ "b40e9ebb-f36c-4e22-b8fc-2559c9495f43"
+ ]
+ },
+ "namespace": "",
+ "ingress": "",
+ "service": "",
+ "pod": "",
+ "body": {
+ "password": [
+ "pswdxxxx"
+ ],
+ "username": [
+ "unamexxxx"
+ ]
+ }
+```
+可以看到请求被允许,并且返回了相应的响应。
+```shell
+< x-ratelimit-limit: 4
+< x-ratelimit-remaining: 3
+```
+
+2. 触发流控
+
+```shell
+for i in $(seq 1 10); do
+ curl -X POST -v http://127.0.0.1:10000/hello \
+ -H 'Authorization: Bearer credential1' \
+ -H 'Content-type: application/json' \
+ -H 'host:foo.com' \
+ -d '{"username":["unamexxxx"],"password":["pswdxxxx"]}'
+done
+
+> POST /hello HTTP/1.1
+> Host:foo.com
+> User-Agent: curl/8.1.2
+> Accept: */*
+> Authorization: Bearer credential1
+> Content-type: application/json
+> Content-Length: 50
+>
+< HTTP/1.1 429 Too Many Requests
+< x-ratelimit-limit: 4
+< x-ratelimit-remaining: 0
+< x-ratelimit-reset: 4
+< content-length: 17
+< content-type: text/plain
+< date: Tue, 20 Aug 2024 08:56:57 GMT
+< server: envoy
+<
+* Connection #0 to host 127.0.0.1 left intact
+Too Many Requests%
+```
+
+## 参考
+- [1] [限流算法选择](https://help.aliyun.com/document_detail/149952.html)
+
+
+
+
+
+
+
+
diff --git a/src/content/docs/ebook/en/wasm19.md b/src/content/docs/ebook/en/wasm19.md
new file mode 100644
index 0000000000..78abbbdf28
--- /dev/null
+++ b/src/content/docs/ebook/en/wasm19.md
@@ -0,0 +1,260 @@
+---
+title: Wasm 生效原理
+keywords: [Higress]
+---
+
+
+# Wasm 生效原理
+
+这一章主要介绍 Wasm 的生效原理包括全局/路由/域名/服务级别生效原理、Wasm插件的 phase & priority、以及 Wasm 插件分发的原理。
+
+## 1 测试插件链结构
+
+这里以 custom-response、transformer、key-auth、easy-logger 四个插件组成插件链为例,介绍 Wasm插件的生效原理。其插件链如下图:
+
+![img](https://img.alicdn.com/imgextra/i2/O1CN01sSmytv1DfnczmUj0j_!!6000000000244-2-tps-1830-460.png)
+
+
+## 2 全局/路由/域名/服务级生效原理
+
+以插件 custom-response 为例,其插件配置如下:
+```yaml
+apiVersion: extensions.higress.io/v1alpha1
+kind: WasmPlugin
+metadata:
+ name: custom-response
+ namespace: higress-system
+spec:
+ priority: 200
+ phase: AUTHN
+ # 配置会全局生效,但如果被下面规则匹配到,则会改为执行命中规则的配置
+ defaultConfig:
+ headers:
+ - key1=value1
+ "body": "{\"hello\":\"foo\"}"
+ matchRules:
+ # 域名级生效配置
+ - domain:
+ - foo.com
+ config:
+ headers:
+ - key2=value2
+ "body": "{\"hello\":\"foo\"}"
+ - ingress:
+ - higress-course/ingress-bar
+ # higress-course 命名空间下名为 ingress-bar 的 ingress 会应用下面这个配置
+ config:
+ headers:
+ - key3=value3
+ "body": "{\"hello\":\"bar\"}"
+ - service:
+ - echo-server.higress-course.svc.cluster.local
+ config:
+ headers:
+ - key4=value4
+ "body": "{\"hello\":\"echo server\"}"
+ url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/custom-response:1.0.0
+ imagePullPolicy: Always
+```
+
+Higress Controller 控制面会把 Higress WasmPlugin 配置转换成 Istio WasmPlugin 配置,同时通过 MCP over Xds 同步到 Istio Discovery, 然后下发到 Envoy 。
+这里可以通过 Higress Controller Debug 接口查看转换后的 Istio WasmPlugin 配置:
+
+```shell
+kubectl exec -c higress-core -n higress-system -- curl http://127.0.0.1:8888/debug/configz
+```
+以下是 custom-response 插件转换成 Istio WasmPlugin YAML 配置如下:
+```yaml
+kind: WasmPlugin
+apiVersion: extensions.istio.io/v1alpha1
+metadata:
+ name: custom-response
+ namespace: higress-system
+spec:
+ imagePullPolicy: Always
+ phase: AUTHN
+ pluginConfig:
+ _rules_:
+ - _match_domain_:
+ - foo.com
+ body: '{"hello":"foo"}'
+ headers:
+ - key2=value2
+ - _match_route_:
+ - higress-course/ingress-bar
+ body: '{"hello":"bar"}'
+ headers:
+ - key3=value3
+ - _match_service_:
+ - echo-server.higress-course.svc.cluster.local
+ body: '{"hello":"echo server"}'
+ headers:
+ - key4=value4
+ body: '{"hello":"foo"}'
+ headers:
+ - key1=value1
+ priority: 200
+ selector:
+ matchLabels:
+ higress: higress-system-higress-gateway
+ url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/custom-response:1.0.0
+```
+发现在 pluginConfig 中增加了 `_rules_` 规则列表,规则中可以指定匹配方式,并填写对应生效的配置:
+- `_match_domain_`:匹配域名生效,填写域名即可,支持通配符。
+- `_match_route_`:匹配 Ingress 生效,匹配格式为:Ingress 所在命名空间 + "/" + Ingress 名称。
+- `_match_service_`:匹配服务生效,填写服务即可,支持通配符。
+
+Higress Controller 控制面转换代码逻辑在 `pkg/ingress/config/ingress_config.go` 文件的 `convertIstioWasmPlugin` func 中实现,其实现代码逻辑比较简单。
+```golang
+func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*extensions.WasmPlugin, error) {
+ ...
+}
+```
+
+同时在第`十六章 Higress 插件 Go SDK 与处理流程`中介绍 `CommonPluginCtx` 插件上下文在插件启动时候解析全局/路由/域名/服务级配置,其代码逻辑在 `plugins/wasm-go/pkg/matcher/rule_matcher.go` 文件的 `ParseRuleConfig` func 中实现。
+
+```golang
+func (m *RuleMatcher[PluginConfig]) ParseRuleConfig(config gjson.Result,
+ parsePluginConfig func(gjson.Result, *PluginConfig) error,
+ parseOverrideConfig func(gjson.Result, PluginConfig, *PluginConfig) error) error {
+ ...
+}
+```
+另外在插件 `OnHttpRequestHeaders` 阶段根据当前请求的 `:authority`、`route_name`、`cluster_name` 获取对应的域名、路由、服务级和全局插件配置。其代码逻辑在 `plugins/wasm-go/pkg/matcher/rule_matcher.go` 文件的 `GetMatchConfig` func 中实现。
+```golang
+func (m RuleMatcher[PluginConfig]) GetMatchConfig() (*PluginConfig, error) {
+ host, err := proxywasm.GetHttpRequestHeader(":authority")
+ ...
+ routeName, err := proxywasm.GetProperty([]string{"route_name"})
+ ...
+ serviceName, err := proxywasm.GetProperty([]string{"cluster_name"})
+ ...
+}
+```
+这里代码逻辑相对比较简单,这里就不再赘述了,有兴趣同学可以直接看源代码。
+
+## 3 Wasm插件的 phase 和 priority
+
+Wasm 插件 phase 有 `UNSPECIFIED_PHASE`、`AUTHN`、`AUTHZ`、`STATS` 四个值,分别对应插件过滤器链的末端、Istio 认证过滤器之前、Istio 授权过滤器之前且在 Istio 认证过滤器之后、Istio 统计过滤器之前且在 Istio 授权过滤器之后。
+同时在相同 phase 情况下,priority 值越大,插件在插件链位置越靠前。 关于认证和授权相关内容可以参考 [Istio 安全](https://istio.io/latest/zh/docs/concepts/security/)官方文档。其插件链结构如下图:
+
+![img](https://img.alicdn.com/imgextra/i4/O1CN017aWyas29NFISP7P4o_!!6000000008055-2-tps-1274-1114.png)
+
+可以通过导出 Envoy 配置查看插件链结构:
+
+```shell
+kubectl exec -n higress-system -- curl http://127.0.0.1:15000/config_dump
+```
+其 Enovy 插件链结构 YAML 格式如下:
+```yaml
+name: envoy.filters.network.http_connection_manager
+typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+ stat_prefix: outbound_0.0.0.0_80
+ http_filters:
+ - name: envoy.filters.http.cors
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
+ - name: higress-system.custom-response
+ config_discovery:
+ config_source:
+ ads: {}
+ initial_fetch_timeout: 0s
+ resource_api_version: V3
+ default_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ apply_default_config_without_warming: true
+ type_urls:
+ - type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ - type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ - name: higress-system.wasm-transformer
+ config_discovery:
+ config_source:
+ ads: {}
+ initial_fetch_timeout: 0s
+ resource_api_version: V3
+ default_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ apply_default_config_without_warming: true
+ type_urls:
+ - type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ - type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ - name: envoy.filters.http.rbac
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC
+ - name: envoy.filters.http.local_ratelimit
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
+ stat_prefix: http_local_rate_limiter
+ - name: higress-system.wasm-keyauth
+ config_discovery:
+ config_source:
+ ads: {}
+ initial_fetch_timeout: 0s
+ resource_api_version: V3
+ default_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ apply_default_config_without_warming: true
+ type_urls:
+ - type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ - type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ - name: higress-system.easy-logger
+ config_discovery:
+ config_source:
+ ads: {}
+ initial_fetch_timeout: 0s
+ resource_api_version: V3
+ default_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ apply_default_config_without_warming: true
+ type_urls:
+ - type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ - type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ - name: envoy.filters.http.fault
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+```
+
+## 4 Wasm 插件分发的原理
+
+Wasm 插件是通过 OCI 实现了 wasm 文件更新,直接热加载,不会导致任何连接中断,业务流量完全无损。其插件分发流程如下图:
+
+![img](https://img.alicdn.com/imgextra/i4/O1CN01rx9nle1TI0uWQqI3Q_!!6000000002358-0-tps-1498-1058.jpg)
+
+OCI 分发流程如下:
+
+1. 当 Higress WasmPlugin 资源更新时,Higress Core 监听到这个变化,同时把 Higress WasmPlugin 转成 Istio WasmPlugin。
+2. Higress Core 将转成 Istio WasmPlugin 通过 MCP Over Xds 推送给 Discovery。
+3. Discovery 通过 Pilot Agent 的 ADS 连接,通过 LDS 协议下发给 Plot Agent。
+4. Pilot Agent 将 LDS 响应直接代理转发给 Envoy。
+5. Envoy 根据 Listener 里插件配置,通过 ECDS (Extension Config Discovery Service) 订阅插件配置。
+6. Pilot Agent 代理 ECDS 协议请求到 Discovery, 同时拦截 ECDS 协议响应。
+7. Pilot Agent 根据 ECDS 响应里插件 OCI 配置,从 Registry Hub 下载镜像。
+8. Pilot Agent 把镜像里插件 Wasm 文件解压到本地,同时修改 ECDS 响应里插件地址到本地 Wasm 文件路径,然后把 ECDS 协议响应返回给 Envoy。
+9. Envoy 根据 ECDS 协议响应,加载本地 Wasm 文件。
+
+注意第 5 步没有直接下发插件配置。而是下发 config_discovery 配置。下面是 Envoy 导出 `custom-response` 插件配置。
+
+```yaml
+- name: higress-system.custom-response
+ config_discovery:
+ config_source:
+ ads: {}
+ initial_fetch_timeout: 0s
+ resource_api_version: V3
+ default_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ apply_default_config_without_warming: true
+ type_urls:
+ - type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ - type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+```
+关于 ECDS 配置,可以参考 [Envoy ECDS](https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/extension/v3/config_discovery.proto)。
+
+
+## 参考
+- [1][Higress 实战:30 行代码写一个 Wasm Go插件](https://mp.weixin.qq.com/s/daYa4MSo3XelpjnIFuxhKw)
+
diff --git a/src/content/docs/ebook/zh-cn/wasm14.md b/src/content/docs/ebook/zh-cn/wasm14.md
new file mode 100644
index 0000000000..878af95f5c
--- /dev/null
+++ b/src/content/docs/ebook/zh-cn/wasm14.md
@@ -0,0 +1,1072 @@
+---
+title: Wasm 插件介绍和开发自定义插件
+keywords: [Higress]
+---
+
+# Wasm 插件介绍和开发自定义插件
+
+本章开始进入 Wasm 插件开发篇,主要介绍 Wasm 插件配置、Higress WasmPlugin CRD 以及如何开发自定义插件。
+
+## 1 测试环境准备
+
+> Higress 本地测试环境网关地址是 127.0.0.1,端口是 80 和 443。
+
+准备 echo-server 和 Ingress, 其 YAML 配置如下:
+
+```yaml
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: higress-course
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: echo-server
+ namespace: higress-course
+spec:
+ selector:
+ app: echo-server
+ ports:
+ - protocol: TCP
+ port: 8080
+ targetPort: 3000
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: echo-server
+ namespace: higress-course
+ labels:
+ app: echo-server
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: echo-server
+ template:
+ metadata:
+ labels:
+ app: echo-server
+ spec:
+ containers:
+ - name: echo-server
+ image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-server:1.3.0
+ env:
+ - name: POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ resources:
+ requests:
+ cpu: 10m
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-foo
+ namespace: higress-course
+spec:
+ ingressClassName: higress
+ rules:
+ - host: "foo.com"
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: echo-server
+ port:
+ number: 8080
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-bar
+ namespace: higress-course
+spec:
+ ingressClassName: higress
+ rules:
+ - host: "bar.com"
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: echo-server
+ port:
+ number: 8080
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-baz
+ namespace: higress-course
+spec:
+ ingressClassName: higress
+ rules:
+ - host: "baz.com"
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: echo-server
+ port:
+ number: 8080
+```
+
+## 2 Wasm 插件配置
+
+Higress WasmPlugin 在 Istio WasmPlugin 的基础上进行了扩展,支持全局、路由、域名、服务级别的配置。这 4 个配置优先级是:路由级 > 域名级 > 服务级 > 全局,对于没有匹配到具体路由、域名、服务级别的请求才会应用全局配置。
+
+下面以 Higress 官方提供的 [custom-response](https://higress.io/zh-cn/docs/plugins/transformation/custom-response) 插件为例进行介绍。custom-response 插件支持配置自定义响应,包括 HTTP 响应状态码、HTTP 响应头,以及 HTTP 响应体。custom-response 插件不仅可以用于模拟响应,还可以根据特定状态码返回自定义响应。例如,在触发网关限流策略时,返回自定义响应。
+
+应用 custom-response 插件,YAML 配置如下:
+
+```yaml
+apiVersion: extensions.higress.io/v1alpha1
+kind: WasmPlugin
+metadata:
+ name: custom-response
+ namespace: higress-system
+spec:
+ priority: 200
+ # 配置会全局生效,但如果被下面规则匹配到,则会改为执行命中规则的配置
+ defaultConfig:
+ headers:
+ - key1=value1
+ "body": "{\"hello\":\"foo\"}"
+ matchRules:
+ # 域名级生效配置
+ - domain:
+ - bar.com
+ config:
+ headers:
+ - key2=value2
+ "body": "{\"hello\":\"bar\"}"
+ # 路由级生效配置
+ - ingress:
+ - higress-course/ingress-baz
+ # higress-course 命名空间下名为 ingress-baz 的 ingress 会应用下面这个配置
+ config:
+ headers:
+ - key3=value3
+ "body": "{\"hello\":\"baz\"}"
+ url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/custom-response:1.0.0
+```
+
+访问 foo.com,由于请求没有匹配任何域名级或路由级配置,因此最终应用了全局配置。
+
+```shell
+curl -v -H "Host: foo.com" http://127.0.0.1/
+
+* Trying 127.0.0.1:80...
+* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
+> GET / HTTP/1.1
+> Host: foo.com
+> User-Agent: curl/8.1.2
+> Accept: */*
+>
+< HTTP/1.1 200 OK
+< key1: value1
+< content-type: application/json; charset=utf-8
+< content-length: 15
+< date: Sun, 14 Jul 2024 02:45:51 GMT
+< server: istio-envoy
+<
+* Connection #0 to host 127.0.0.1 left intact
+{"hello":"foo"}
+```
+
+访问 bar.com,请求匹配域名级配置。
+
+```shell
+curl -v -H "Host: bar.com" http://127.0.0.1/
+
+* Trying 127.0.0.1:80...
+* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
+> GET / HTTP/1.1
+> Host: bar.com
+> User-Agent: curl/8.1.2
+> Accept: */*
+>
+< HTTP/1.1 200 OK
+< key2: value2
+< content-type: application/json; charset=utf-8
+< content-length: 15
+< date: Sun, 14 Jul 2024 02:47:51 GMT
+< server: istio-envoy
+<
+* Connection #0 to host 127.0.0.1 left intact
+{"hello":"bar"}
+```
+
+访问 baz.com,请求匹配路由级配置。
+
+```shell
+curl -v -H "Host: baz.com" http://127.0.0.1/
+
+* Trying 127.0.0.1:80...
+* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
+> GET / HTTP/1.1
+> Host: baz.com
+> User-Agent: curl/8.1.2
+> Accept: */*
+>
+< HTTP/1.1 200 OK
+< key3: value3
+< content-type: application/json; charset=utf-8
+< content-length: 15
+< date: Sun, 14 Jul 2024 08:44:03 GMT
+< server: istio-envoy
+<
+* Connection #0 to host 127.0.0.1 left intact
+{"hello":"baz"}
+```
+
+测试完成后删除 `custom-response` WasmPlugin,避免对后续测试产生影响。
+
+```shell
+kubectl delete wasmplugin custom-response -n higress-system
+```
+
+## 3 Higress WasmPlugin CRD
+
+Higress WasmPlugin CRD 在 [Istio WasmPlugin CRD](https://istio.io/latest/docs/reference/config/proxy_extensions/wasm-plugin/) 的基础上进行了扩展,新增 `defaultConfig` 和 `matchRules` 字段,用于配置插件的默认配置和路由级、域名级配置。
+
+主要配置如下:
+
+| 字段名称 | 数据类型 | 填写要求 | 描述 |
+|------------------|-----------------|----|-------------------------------------------------------------------------------------------------------------|
+| `pluginName` | string | 选填 | 插件名称 |
+| `phase` | string | 选填 | 插件插入插件链中的位置,默认是 UNSPECIFIED_PHASE |
+| `priority` | int | 选填 | 插件执行优先级,默认为 0,在相同 `phase` 下,值越大越先处理请求,但越后处理响应 |
+| `imagePullPolicy` | string | 选填 | 插件镜像拉取策略,可选值有:`UNSPECIFIED_POLICY`(默认值)、`IfNotPresent`、`Always` |
+| `imagePullSecret` | string | 选填 | 用于拉取 OCI 镜像的凭据。与 WasmPlugin 在同一命名空间中的Kubernetes Secret 的名称 |
+| `url` | string | 必填 | Wasm 文件或 OCI 容器的 URL,默认为 `oci://`,引用 OCI 镜像。同时支持 `file://`,用于容器本地的 Wasm 文件,以及 `http[s]://`,用于引用远程托管的 Wasm 文件 |
+| `Sha256` | string | 选填 | 用于验证 Wasm 文件或 OCI 容器的 SHA256 校验和 |
+| `defaultConfig` | object | 选填 | 插件默认配置,全局生效于没有匹配具体域名和路由配置的请求 |
+| `defaultConfigDisable`| bool | 选填 | 插件默认配置是否失效,默认值是 false |
+| `matchRules` | array of object | 选填 | 匹配域名或路由生效的配置 |
+
+`phase` 配置说明:
+
+| 字段名称 | 描述 |
+|--------------------|------------------------------------------------------------------------|
+| `UNSPECIFIED_PHASE` | 在插件过滤器链的末端,在路由器之前插入插件,如果没有指定插件的 `phase`,则默认设置为 `UNSPECIFIED_PHASE` |
+| `AUTHN` | 在 Istio 认证过滤器之前插入插件 |
+| `AUTHZ` | 在 Istio 授权过滤器之前且在 Istio 认证过滤器之后插入插件 |
+| `STATS` | 在 Istio 统计过滤器之前且在 Istio 授权过滤器之后插入插件 |
+
+`matchRules` 中每一项的配置字段说明:
+
+| 字段名称 | 数据类型 | 填写要求 | 配置示例 | 描述 |
+|-----------------|-------|--------------------------------------|--------------------------------|-----------------------------------------|
+| `ingress` | 字符串数组 | `ingress`、`domain` 和 `service` 中必填一项 | ["default/foo", "default/bar"] | 匹配 ingress 资源对象,匹配格式为: `命名空间/ingress名称` |
+| `domain` | 字符串数组 | `ingress`、`domain` 和 `service` 中必填一项| ["example.com", "*.test.com"] | 匹配域名,支持泛域名 |
+| `service` | 字符串数组 | `ingress`、`domain` 和 `service` 中必填一项 | ["echo-server.higress-course.svc.cluster.local"] | 匹配服务名称 |
+| `config` | 对象 | 选填 | - | 匹配后生效的插件配置 |
+| `configDisable` | bool | 选填 | false | 配置是否生效,默认设置为 false |
+
+
+## 4 自定义插件开发
+
+开发一个简单日志插件 `easy-logger`, 这个插件根据配置记录请求和响应到网关日志中。整个过程涉及到插件开发环境准备、开发和测试、部署和验证。
+
+### 4.1 环境准备
+
+环境准备如下:
+
+- Docker 官方安装连接:https://docs.docker.com/engine/install/
+- Go 版本: >= 1.19 (需要支持范型特性),官方安装链接:https://go.dev/doc/install
+
+如果选择用 TinyGo 在本地构建 Wasm 文件,再拷贝到 Docker 镜像中,需要安装 TinyGo,其环境要求如下:
+
+- TinyGo 版本: >= 0.28.1 (建议版本 0.28.1) 官方安装链接:https://tinygo.org/getting-started/install/
+
+### 4.2 开发和测试
+
+#### 4.2.1 初始化工程目录
+
+1. 新建一个工程目录文件 easy-logger。
+
+```shell
+mkdir easy-logger
+```
+2. 在所建目录下执行以下命令,初始化 Go 工程。
+
+```shell
+go mod init easy-logger
+```
+
+go.mod 文件中 go 版本需要设置为 1.19,由于在 4.3.3 节中我们将使用 1.19 版本的 wasm-go-builder 镜像来构建插件,因此需要保持两者的 go 版本一致。
+
+```shell
+module easy-logger
+
+go 1.19
+```
+
+3. 国内环境可能需要设置下载依赖包的代理
+
+```shell
+go env -w GOPROXY=https://proxy.golang.com.cn,direct
+```
+
+4. 下载构建插件的依赖
+
+```shell
+go get github.com/higress-group/proxy-wasm-go-sdk
+go get github.com/alibaba/higress/plugins/wasm-go@main
+go get github.com/tidwall/gjson
+```
+
+#### 4.2.2 编写 main.go 文件
+首先,我们编写 easy-logger 插件的基本框架,暂时只读取我们设置的配置参数,不在请求和响应阶段进行任何处理。
+
+```golang
+package main
+
+import (
+ "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+ "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
+ "github.com/tidwall/gjson"
+)
+
+func main() {
+ wrapper.SetCtx(
+ // 插件名称
+ "easy-logger",
+ // 设置自定义函数解析插件配置
+ wrapper.ParseConfigBy(parseConfig),
+ // 设置自定义函数处理请求头
+ wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
+ // 设置自定义函数处理请求体
+ wrapper.ProcessRequestBodyBy(onHttpRequestBody),
+ // 设置自定义函数处理响应头
+ wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
+ // 设置自定义函数处理响应体
+ wrapper.ProcessResponseBodyBy(onHttpResponseBody),
+ // 设置自定义函数处理流式请求体
+ //wrapper.ProcessStreamingRequestBodyBy(onHttpStreamingRequestBody),
+ // 设置自定义函数处理流式响应体
+ //wrapper.ProcessStreamingResponseBodyBy(onHttpStreamingResponseBody),
+ )
+}
+
+// 自定义插件配置
+type LoggerConfig struct {
+ // 是否打印请求
+ request bool
+ // 是否打印响应
+ response bool
+ // 打印响应状态码,* 表示打印所有状态响应,500,502,503 表示打印 HTTP 500、502、503 状态响应,默认是 *
+ responseStatusCodes string
+}
+
+func parseConfig(json gjson.Result, config *LoggerConfig, log wrapper.Log) error {
+ log.Debugf("parseConfig()")
+ config.request = json.Get("request").Bool()
+ config.response = json.Get("response").Bool()
+ config.responseStatusCodes = json.Get("responseStatusCodes").String()
+ if config.responseStatusCodes == "" {
+ config.responseStatusCodes = "*"
+ }
+ log.Debugf("parse config:%v", config)
+ return nil
+}
+
+func onHttpRequestHeaders(ctx wrapper.HttpContext, config LoggerConfig, log wrapper.Log) types.Action {
+ log.Debugf("onHttpRequestHeaders()")
+ return types.ActionContinue
+}
+
+func onHttpRequestBody(ctx wrapper.HttpContext, config LoggerConfig, body []byte, log wrapper.Log) types.Action {
+ log.Debugf("onHttpRequestBody()")
+ return types.ActionContinue
+}
+
+func onHttpResponseBody(ctx wrapper.HttpContext, config LoggerConfig, body []byte, log wrapper.Log) types.Action {
+ log.Debugf("onHttpResponseBody()")
+ return types.ActionContinue
+}
+
+func onHttpResponseHeaders(ctx wrapper.HttpContext, config LoggerConfig, log wrapper.Log) types.Action {
+ log.Debugf("onHttpResponseHeaders()")
+ return types.ActionContinue
+}
+```
+
+Higress 插件 SDK 开发涉及到以下内容:
+
+- wrapper.HttpContext:请求上下文。
+- LoggerConfig:自定义插件配置。
+- wrapper.Log:插件日志工具。
+- wrapper.ProcessXXXX:插件回调钩子函数。
+- proxywasm:提供插件工具函数包。
+
+wrapper 插件回调钩子函数包含以下函数,可以根据实际业务需求选择设置以下钩子函数:
+
+- wrapper.ParseConfigBy(parseConfig):解析插件配置。
+- wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders):设置自定义函数处理请求头。
+- wrapper.ProcessRequestBodyBy(onHttpRequestBody):设置自定义函数处理请求体。
+- wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders):设置自定义函数处理响应头。
+- wrapper.ProcessResponseBodyBy(onHttpResponseBody):设置自定义函数处理响应体。
+- wrapper.ProcessStreamingRequestBodyBy(onHttpStreamingRequestBody):设置自定义函数处理流式请求体。
+- wrapper.ProcessStreamingResponseBodyBy(onHttpStreamingResponseBody):设置自定义函数处理流式响应体。
+
+关于 Higress 插件 SDK 内容会在后续章节中详细展开。
+
+#### 4.3.3 本地测试
+
+1. 第一步:在插件目录下创建文件 envoy.yaml,内容如下。网关在 10000 端口监听 HTTP 请求,将请求转发到 echo-server 服务。
+
+```yaml
+admin:
+ address:
+ socket_address:
+ protocol: TCP
+ address: 0.0.0.0
+ port_value: 9901
+static_resources:
+ listeners:
+ - name: listener_0
+ address:
+ socket_address:
+ protocol: TCP
+ address: 0.0.0.0
+ port_value: 10000
+ 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
+ scheme_header_transformation:
+ scheme_to_overwrite: https
+ stat_prefix: ingress_http
+ route_config:
+ name: local_route
+ virtual_hosts:
+ - name: local_service
+ domains: ["*"]
+ routes:
+ - match:
+ prefix: "/"
+ route:
+ cluster: echo-server
+ http_filters:
+ - name: wasmdemo
+ typed_config:
+ "@type": type.googleapis.com/udpa.type.v1.TypedStruct
+ type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ value:
+ config:
+ name: wasmdemo
+ vm_config:
+ runtime: envoy.wasm.runtime.v8
+ code:
+ local:
+ filename: /etc/envoy/plugin.wasm
+ configuration:
+ "@type": "type.googleapis.com/google.protobuf.StringValue"
+ value: |-
+ {
+ "request": true,
+ "response": true,
+ "responseStatusCodes": "200,500,502,503"
+ }
+ - name: envoy.filters.http.router
+ clusters:
+ - name: echo-server
+ connect_timeout: 30s
+ type: LOGICAL_DNS
+ dns_lookup_family: V4_ONLY
+ lb_policy: ROUND_ROBIN
+ load_assignment:
+ cluster_name: echo-server
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: echo-server
+ port_value: 3000
+```
+
+插件通过本地文件的方式加载到 Envoy 中,插件配置的如下:
+
+```yaml
+ configuration:
+ "@type": "type.googleapis.com/google.protobuf.StringValue"
+ value: |-
+ {
+ "request": true,
+ "response": true,
+ "responseStatusCodes": "200,500,502,503"
+ }
+```
+
+2. 第二步:在插件目录下创建文件 docker-compose.yaml,内容如下:
+
+```yaml
+version: '3.9'
+services:
+ envoy:
+ image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v1.4.1
+ entrypoint: /usr/local/bin/envoy
+ # 注意这里对 Wasm 开启了 debug 级别日志,在生产环境部署时请使用默认的 info 级别
+ # 如果需要将 Envoy 的日志级别调整为 debug,将 --log-level 参数设置为 debug
+ command: -c /etc/envoy/envoy.yaml --log-level info --log-path /etc/envoy/envoy.log --component-log-level wasm:debug
+ depends_on:
+ - echo-server
+ networks:
+ - wasmtest
+ ports:
+ - "10000:10000"
+ - "9901:9901"
+ volumes:
+ - ./envoy.yaml:/etc/envoy/envoy.yaml
+ - ./build/plugin.wasm:/etc/envoy/plugin.wasm
+ - ./envoy.log:/etc/envoy/envoy.log
+ echo-server:
+ image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-server:1.3.0
+ networks:
+ - wasmtest
+ ports:
+ - "3000:3000"
+networks:
+ wasmtest: {}
+```
+3. 第三步:在插件目录下创建文件 Dockerfile,内容如下:
+
+```yaml
+ARG BUILDER=higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/wasm-go-builder:go1.19-tinygo0.28.1-oras1.0.0
+FROM $BUILDER as builder
+
+ARG GOPROXY
+ENV GOPROXY=${GOPROXY}
+
+ARG EXTRA_TAGS=""
+ENV EXTRA_TAGS=${EXTRA_TAGS}
+
+WORKDIR /workspace
+COPY . .
+RUN go mod tidy
+RUN tinygo build -o /main.wasm -scheduler=none -gc=custom -tags="custommalloc nottinygc_finalizer $EXTRA_TAGS" -target=wasi ./main.go
+
+FROM scratch as output
+COPY --from=builder /main.wasm plugin.wasm
+```
+
+4. 第四步:在插件目录下创建文件 Makefile,内容如下:
+
+```shell
+PLUGIN_NAME ?= hello-world
+BUILDER_REGISTRY ?= higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/
+REGISTRY ?= higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/
+GO_VERSION ?= 1.19
+TINYGO_VERSION ?= 0.28.1
+ORAS_VERSION ?= 1.0.0
+HIGRESS_VERSION ?= 1.4.1
+USE_HIGRESS_TINYGO ?= true
+BUILDER ?= ${BUILDER_REGISTRY}wasm-go-builder:go${GO_VERSION}-tinygo${TINYGO_VERSION}-oras${ORAS_VERSION}
+BUILD_TIME := $(shell date "+%Y%m%d-%H%M%S")
+COMMIT_ID := $(shell git rev-parse --short HEAD 2>/dev/null)
+IMAGE_TAG = $(if $(strip $(PLUGIN_VERSION)),${PLUGIN_VERSION},${BUILD_TIME}-${COMMIT_ID})
+IMG ?= ${REGISTRY}${PLUGIN_NAME}:${IMAGE_TAG}
+GOPROXY := $(shell go env GOPROXY)
+EXTRA_TAGS ?= proxy_wasm_version_0_2_100
+
+.DEFAULT:
+local-docker-build:
+ DOCKER_BUILDKIT=1 docker build --build-arg BUILDER=${BUILDER} \
+ --build-arg GOPROXY=$(GOPROXY) \
+ --build-arg EXTRA_TAGS=$(EXTRA_TAGS) \
+ -t ${IMG} \
+ --output build \
+ .
+ @echo ""
+ @echo "output wasm file: ./build/plugin.wasm"
+
+build-image:
+ DOCKER_BUILDKIT=1 docker build --build-arg BUILDER=${BUILDER} \
+ --build-arg GOPROXY=$(GOPROXY) \
+ --build-arg EXTRA_TAGS=$(EXTRA_TAGS) \
+ -t ${IMG} \
+ .
+ @echo ""
+ @echo "image: ${IMG}"
+
+build-push: build-image
+ docker push ${IMG}
+
+local-build:
+ tinygo build -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer $(EXTRA_TAGS)' \
+ -o ./build/plugin.wasm main.go
+ @echo ""
+ @echo "wasm: ./build/plugin.wasm"
+
+local-run:
+ echo > ./envoy.log
+ docker compose down
+ docker compose up -d
+
+local-all: local-build local-run
+local-docker-all: local-docker-build local-run
+```
+
+请将 Makefile 文件中镜像仓库地址 `REGISTRY ?= higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/` 换成自己的镜像仓库地址。
+
+其命令说明如下:
+- `make local-docker-build`: 本地 Docker 环境构建插件,生成插件文件 ./build/plugin.wasm。
+- `make local-build`: 本地 TinyGo 构建插件,生成插件文件 ./build/plugin.wasm。
+- `make local-run`: 本地启动测试环境。
+- `PLUGIN_NAME=easy-logger PLUGIN_VERSION=1.0.0 make build-image` 构建 easy-logger 插件镜像,插件版本为 1.0.0。
+- `PLUGIN_NAME=easy-logger PLUGIN_VERSION=1.0.0 make build-push` 构建 easy-logger 插件镜像,插件版本为 1.0.0,同时推送到镜像仓库。
+- `make local-docker-all`: 本地 Docker 环境构建插件,生成插件文件 build/plugin.wasm,同时启动本地测试环境。
+- `make local-all`: 本地 TinyGo 构建插件,生成插件文件 ./build/plugin.wasm,同时启动本地测试环境。
+
+注意用 TinyGo 本地构建命令如下:
+```shell
+tinygo build -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer proxy_wasm_version_0_2_100' -o ./build/plugin.wasm main.go
+```
+
+5. 第五步:本地 Docker 环境构建和启动测试环境
+
+本地 Docker 环境构建和启动测试环境,命令如下:
+
+```shell
+make local-docker-all
+```
+
+本地启动测试环境后,插件目录整体文件结构如下:
+
+```shell
+tree
+.
+├── Dockerfile
+├── Makefile
+├── build
+│ └── plugin.wasm # 构建生成的 Wasm 文件
+├── docker-compose.yaml
+├── envoy.log # Envoy 日志文件
+├── envoy.yaml
+├── go.mod
+├── go.sum
+└── main.go
+```
+
+执行以下命令通过网关访问 echo-server。
+
+```shell
+curl -X POST -v http://127.0.0.1:10000/hello \
+-H "Content-type: application/json" -H 'host:foo.com' \
+-d '{"username":["unamexxxx"], "password":["pswdxxxx"]}'
+
+* Trying 127.0.0.1:10000...
+* Connected to 127.0.0.1 (127.0.0.1) port 10000 (#0)
+> POST /hello HTTP/1.1
+> Host:foo.com
+> User-Agent: curl/8.1.2
+> Accept: */*
+> Content-type: application/json
+> Content-Length: 50
+>
+< HTTP/1.1 200 OK
+< content-type: application/json
+< x-content-type-options: nosniff
+< date: Sat, 20 Jul 2024 04:39:46 GMT
+< content-length: 642
+< req-cost-time: 48
+< req-arrive-time: 1721450386098
+< resp-start-time: 1721450386146
+< x-envoy-upstream-service-time: 30
+< server: envoy
+<
+{
+ "path": "/hello",
+ "host": "foo.com",
+ "method": "POST",
+ "proto": "HTTP/1.1",
+ "headers": {
+ "Accept": [
+ "*/*"
+ ],
+ "Content-Length": [
+ "50"
+ ],
+ "Content-Type": [
+ "application/json"
+ ],
+ "Original-Host": [
+ "foo.com"
+ ],
+ "Req-Start-Time": [
+ "1721450386098"
+ ],
+ "User-Agent": [
+ "curl/8.1.2"
+ ],
+ "X-Envoy-Expected-Rq-Timeout-Ms": [
+ "15000"
+ ],
+ "X-Forwarded-Proto": [
+ "https"
+ ],
+ "X-Request-Id": [
+ "2f9ff093-7891-4c55-992b-874f7ba00d0e"
+ ]
+ },
+ "namespace": "",
+ "ingress": "",
+ "service": "",
+ "pod": "",
+ "body": {
+ "password": [
+ "pswdxxxx"
+ ],
+ "username": [
+ "unamexxxx"
+ ]
+ }
+* Connection #0 to host 127.0.0.1 left intact
+}
+```
+
+查看插件目录下 envoy.log 文件,可以看到 easy-logger 插件的日志输出。
+
+```shell
+[2024-07-20 04:08:19.990][22][debug][wasm] [external/envoy/source/extensions/common/wasm/wasm.cc:146] Thread-Local Wasm created 10 now active
+[2024-07-20 04:08:19.993][22][debug][wasm] [external/envoy/source/extensions/common/wasm/context.cc:1308] wasm log: [easy-logger] parseConfig()
+[2024-07-20 04:08:19.993][22][debug][wasm] [external/envoy/source/extensions/common/wasm/context.cc:1308] wasm log: [easy-logger] parse config:&{request:true response:true responseStatusCodes:200,500,502,503}
+[2024-07-20 04:08:19.993][1][warning][main] [external/envoy/source/server/server.cc:715] there is no configured limit to the number of allowed active connections. Set a limit via the runtime key overload.global_downstream_max_connections
+[2024-07-20 04:39:46.114][29][debug][wasm] [external/envoy/source/extensions/common/wasm/context.cc:1308] wasm log wasmdemo: [easy-logger] onHttpRequestHeaders()
+[2024-07-20 04:39:46.116][29][debug][wasm] [external/envoy/source/extensions/common/wasm/context.cc:1308] wasm log wasmdemo: [easy-logger] onHttpRequestBody()
+[2024-07-20 04:39:46.147][29][debug][wasm] [external/envoy/source/extensions/common/wasm/context.cc:1308] wasm log wasmdemo: [easy-logger] onHttpResponseHeaders()
+[2024-07-20 04:39:46.147][29][debug][wasm] [external/envoy/source/extensions/common/wasm/context.cc:1308] wasm log wasmdemo: [easy-logger] onHttpResponseBody()
+```
+
+到这里表示整体开发和测试环境已经完成,下面就是完善插件功能,然后重新测试。
+
+### 4.3 完善插件功能
+
+接下来,我们将通过自定义函数来处理请求和响应信息。通过设置插件参数,我们可以控制是否打印请求和响应信息,并根据指定的响应状态码决定是否记录响应内容。
+
+```golang
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
+ "github.com/google/uuid"
+ "github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
+ "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
+ "github.com/tidwall/gjson"
+)
+
+func main() {
+ wrapper.SetCtx(
+ // 插件名称
+ "easy-logger",
+ // 设置自定义函数解析插件配置
+ wrapper.ParseConfigBy(parseConfig),
+ // 设置自定义函数处理请求头
+ wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
+ // 设置自定义函数处理请求体
+ wrapper.ProcessRequestBodyBy(onHttpRequestBody),
+ // 设置自定义函数处理响应头
+ wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
+ // 设置自定义函数处理响应体
+ wrapper.ProcessResponseBodyBy(onHttpResponseBody),
+ // 设置自定义函数处理流式请求体
+ //wrapper.ProcessStreamingRequestBodyBy(onHttpStreamingRequestBody),
+ // 设置自定义函数处理流式响应体
+ //wrapper.ProcessStreamingResponseBodyBy(onHttpStreamingResponseBody),
+ )
+}
+
+// 自定义插件配置
+type LoggerConfig struct {
+ // 是否打印请求
+ request bool
+ // 是否打印响应
+ response bool
+ // 打印响应状态码,* 表示打印所有状态响应,500,502,503 表示打印 HTTP 500、502、503 状态响应,默认是 *
+ responseStatusCodes string
+}
+
+func parseConfig(json gjson.Result, config *LoggerConfig, log wrapper.Log) error {
+ log.Debugf("parseConfig()")
+ config.request = json.Get("request").Bool()
+ config.response = json.Get("response").Bool()
+ config.responseStatusCodes = json.Get("responseStatusCodes").String()
+ if config.responseStatusCodes == "" {
+ config.responseStatusCodes = "*"
+ }
+ log.Debugf("parse config:%+v", config)
+ return nil
+}
+
+func onHttpRequestHeaders(ctx wrapper.HttpContext, config LoggerConfig, log wrapper.Log) types.Action {
+ log.Debugf("onHttpRequestHeaders()")
+ requestId := uuid.New().String()
+ ctx.SetContext("requestId", requestId)
+ if !config.request {
+ return types.ActionContinue
+ }
+ // 获取并打印请求头
+ headers, _ := proxywasm.GetHttpRequestHeaders()
+ var build strings.Builder
+ build.WriteString("\n===========request headers===============\n")
+ build.WriteString(fmt.Sprintf("requestId:%s\n", requestId))
+ for _, values := range headers {
+ build.WriteString(fmt.Sprintf("%s:%s\n", values[0], values[1]))
+ }
+ log.Infof(build.String())
+ // 继续处理请求
+ return types.ActionContinue
+}
+
+func onHttpRequestBody(ctx wrapper.HttpContext, config LoggerConfig, body []byte, log wrapper.Log) types.Action {
+ log.Debugf("onHttpRequestBody()")
+ // 打印请求体
+ if config.request {
+ var build strings.Builder
+ build.WriteString("\n===========request body===============\n")
+ requestId := ctx.GetContext("requestId").(string)
+ build.WriteString(fmt.Sprintf("requestId:%s\n", requestId))
+ build.WriteString(fmt.Sprintf("body:%s\n", string(body)))
+ log.Infof(build.String())
+ }
+ return types.ActionContinue
+}
+
+func onHttpResponseHeaders(ctx wrapper.HttpContext, config LoggerConfig, log wrapper.Log) types.Action {
+ log.Debugf("onHttpResponseHeaders()")
+ // 添加自定义响应头
+ proxywasm.AddHttpResponseHeader("x-easy-logger", "1.0.0")
+ if !config.response {
+ return types.ActionContinue
+ }
+ // 获取响应状态码
+ statusCode, _ := proxywasm.GetHttpResponseHeader(":status")
+ logResponseBody := false
+ // 根据响应状态码决定是否打印响应体
+ if config.responseStatusCodes == "*" || strings.Contains(config.responseStatusCodes, statusCode) {
+ logResponseBody = true
+ }
+ // 将是否记录响应体的信息存储在上下文中,在 onHttpResponseBody 阶段获取上下文判断是否打印响应体
+ ctx.SetContext("logResponseBody", logResponseBody)
+ // 获取响应头
+ headers, _ := proxywasm.GetHttpResponseHeaders()
+ // 打印响应头
+ var build strings.Builder
+ build.WriteString("\n===========response headers===============\n")
+ requestId := ctx.GetContext("requestId").(string)
+ build.WriteString(fmt.Sprintf("requestId:%s\n", requestId))
+ for _, values := range headers {
+ build.WriteString(fmt.Sprintf("%s:%s\n", values[0], values[1]))
+ }
+ log.Infof(build.String())
+ return types.ActionContinue
+}
+
+func onHttpResponseBody(ctx wrapper.HttpContext, config LoggerConfig, body []byte, log wrapper.Log) types.Action {
+ log.Debugf("onHttpResponseBody()")
+ // 获取在 onHttpRequestHeaders 阶段设置的上下文
+ logResponseBody, ok := ctx.GetContext("logResponseBody").(bool)
+ if !ok {
+ return types.ActionContinue
+ }
+ // 打印响应体
+ if logResponseBody {
+ var build strings.Builder
+ build.WriteString("\n===========response body===============\n")
+ requestId := ctx.GetContext("requestId").(string)
+ build.WriteString(fmt.Sprintf("requestId:%s\n", requestId))
+ build.WriteString(fmt.Sprintf("body:%s\n", string(body)))
+ log.Infof(build.String())
+ }
+ return types.ActionContinue
+}
+```
+
+
+### 4.4 部署插件和验证
+
+1. 构建插件镜像
+
+```shell
+PLUGIN_NAME=easy-logger PLUGIN_VERSION=1.0.0 make build-push
+```
+
+2. 部署插件
+
+easy-logger 插件部署 YAML 如下:
+```yaml
+apiVersion: extensions.higress.io/v1alpha1
+kind: WasmPlugin
+metadata:
+ name: easy-logger
+ namespace: higress-system
+spec:
+ priority: 300
+ matchRules:
+ # 域名级生效配置
+ - domain:
+ - foo.com
+ config:
+ request: true
+ response: true
+ responseStatusCodes: "200,500,502,503"
+ url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/easy-logger:1.0.0
+```
+
+3. 验证插件
+
+- 设置网关插件的日志级别为 debug。
+
+```shell
+kubectl exec -n higress-system -- \
+curl -X POST http://127.0.0.1:15000/logging?wasm=debug
+```
+
+- 请求访问
+
+```shell
+curl -X POST -v http://127.0.0.1/hello \
+-H "Content-type: application/json" -H 'host:foo.com' \
+-d '{"username":["unamexxxx"],"password":["pswdxxxx"]}'
+```
+
+- 查看网关的日志,可以看到输出了请求和响应的详细信息
+
+```shell
+kubectl logs -f -n higress-system
+
+[Envoy (Epoch 0)] [2024-07-20 04:56:55.251][39][debug][wasm] wasm log higress-system.easy-logger: [easy-logger] onHttpRequestHeaders()
+[Envoy (Epoch 0)] [2024-07-20 04:56:55.252][39][info][wasm] wasm log higress-system.easy-logger: [easy-logger]
+===========request headers===============
+requestId:a791e8e6-8126-4a1d-92f0-a0333b706c1d
+:authority:foo.com
+:path:/hello
+:method:POST
+:scheme:http
+user-agent:curl/8.1.2
+accept:*/*
+content-type:application/json
+content-length:50
+x-forwarded-for:192.168.65.1
+x-forwarded-proto:http
+x-envoy-internal:true
+x-request-id:2ad88049-6ba3-4f3d-bc81-dc29fa48ffce
+x-envoy-decorator-operation:echo-server.higress-course.svc.cluster.local:8080/*
+
+[Envoy (Epoch 0)] [2024-07-20 04:56:55.254][39][debug][wasm] wasm log higress-system.easy-logger: [easy-logger] onHttpRequestBody()
+[Envoy (Epoch 0)] [2024-07-20 04:56:55.254][39][info][wasm] wasm log higress-system.easy-logger: [easy-logger]
+===========request body===============
+requestId:a791e8e6-8126-4a1d-92f0-a0333b706c1d
+body:{"username":["unamexxxx"],"password":["pswdxxxx"]}
+
+[Envoy (Epoch 0)] [2024-07-20 04:56:55.256][39][debug][wasm] wasm log higress-system.easy-logger: [easy-logger] onHttpResponseHeaders()
+[Envoy (Epoch 0)] [2024-07-20 04:56:55.256][39][info][wasm] wasm log higress-system.easy-logger: [easy-logger]
+===========response headers===============
+requestId:a791e8e6-8126-4a1d-92f0-a0333b706c1d
+:status:200
+content-type:application/json
+x-content-type-options:nosniff
+date:Sat, 20 Jul 2024 04:56:55 GMT
+content-length:993
+req-cost-time:8
+req-arrive-time:1721451415248
+resp-start-time:1721451415256
+x-envoy-upstream-service-time:2
+x-easy-logger:1.0.0
+
+[Envoy (Epoch 0)] [2024-07-20 04:56:55.257][39][debug][wasm] wasm log higress-system.easy-logger: [easy-logger] onHttpResponseBody()
+[Envoy (Epoch 0)] [2024-07-20 04:56:55.257][39][info][wasm] wasm log higress-system.easy-logger: [easy-logger]
+===========response body===============
+requestId:a791e8e6-8126-4a1d-92f0-a0333b706c1d
+body:{
+ "path": "/hello",
+ "host": "foo.com",
+ "method": "POST",
+ "proto": "HTTP/1.1",
+ "headers": {
+ "Accept": [
+ "*/*"
+ ],
+ "Content-Length": [
+ "50"
+ ],
+ "Content-Type": [
+ "application/json"
+ ],
+ "Original-Host": [
+ "foo.com"
+ ],
+ "Req-Start-Time": [
+ "1721451415248"
+ ],
+ "User-Agent": [
+ "curl/8.1.2"
+ ],
+ "X-B3-Sampled": [
+ "0"
+ ],
+ "X-B3-Spanid": [
+ "f642c00a89551b07"
+ ],
+ "X-B3-Traceid": [
+ "dfab58b011681d29f642c00a89551b07"
+ ],
+ "X-Envoy-Attempt-Count": [
+ "1"
+ ],
+ "X-Envoy-Decorator-Operation": [
+ "echo-server.higress-course.svc.cluster.local:8080/*"
+ ],
+ "X-Envoy-Internal": [
+ "true"
+ ],
+ "X-Forwarded-For": [
+ "192.168.65.1"
+ ],
+ "X-Forwarded-Proto": [
+ "http"
+ ],
+ "X-Request-Id": [
+ "2ad88049-6ba3-4f3d-bc81-dc29fa48ffce"
+ ]
+ },
+ "namespace": "higress-course",
+ "ingress": "",
+ "service": "",
+ "pod": "echo-server-6f4df5fcff-nksqz",
+ "body": {
+ "password": [
+ "pswdxxxx"
+ ],
+ "username": [
+ "unamexxxx"
+ ]
+ }
+}
+```
+
+
+
+
+
+
+
diff --git a/src/content/docs/ebook/zh-cn/wasm15.md b/src/content/docs/ebook/zh-cn/wasm15.md
new file mode 100644
index 0000000000..ad6d670ac1
--- /dev/null
+++ b/src/content/docs/ebook/zh-cn/wasm15.md
@@ -0,0 +1,804 @@
+---
+title: Wasm 插件原理
+keywords: [Higress]
+---
+
+# Wasm 插件原理
+
+本章主要介绍 Proxy-Wasm Go SDK 和 Wasm 插件基本原理。
+
+## 1 Wasm、TinyGo、Proxy-Wasm Go SDK
+
+### 1.1 Wasm
+
+#### 1.1.1 什么是 Wasm ?
+
+ [WebAssembly(简称 Wasm)](https://webassembly.org/) 是操作堆栈虚拟机的二进制指令集,Wasm 可以在 Web 浏览器中运行或者其他环境比如服务器端应用程序运行。Wasm有以下特点:
+
+- 高效性能:提供了接近机器码的性能。
+- 跨平台:Wasm 是一种与平台无关的格式,可以在任何支持它的平台上运行,包括浏览器和服务器。
+- 安全性:Wasm 在一个内存安全的沙箱环境中运行,这意味着它可以安全地执行不受信任的代码,而不会访问或修改主机系统的其他部分。
+- 可移植性:Wasm 模块可以被编译成 WebAssembly 二进制文件,这些文件可以被传输和加载到支持 Wasm 的任何环境中。
+- 多语言支持:Wasm 支持多种编程语言,开发者可以使用 C、C++、Rust、Go 等多种语言编写代码,然后编译成 Wasm 格式。
+
+#### 1.1.2 Wasm 模块
+
+Wasm 模块主要有以下两种格式:
+- 二进制格式:Wasm 的主要编码格式,以 .wasm 后缀结尾。
+- 文本格式:主要是为了方便开发者理解 Wasm 模块,以 .wat 后缀结尾,相当于汇编语言程序。
+
+Wasm 模块二进制格式是 Wasm 二进制文件,Wasm 模块二进制格式也是以魔数和版本号开头,之后就是模块的主体内容,这些内容根据不同用途被分别放在不同的段(Section) 中。一共定义了 12 种段,每种段分配了 ID(从 0 到 11),依次有如下 12 个段:自定义段、类型段、导入段、函数段、表段、内存段、全局段、导出段、起始段、元素段、代码段、数据段。
+Wasm 模块二进制格式的组成如下图(图片来源 [WebAssembly 解释器实现篇](https://github.com/mcuking/blog/issues/96/))所示:
+![img](https://img.alicdn.com/imgextra/i1/O1CN01rLuxGp1zIX413ZQ0g_!!6000000006691-0-tps-1784-1266.jpg)
+
+每一个不同的段都描述了这个 Wasm 模块的一部分信息。而模块内的所有段放在一起,便描述了这个 Wasm 模块的全部信息:
+- 内存段和数据段:内存段是线性内存(linear memory)用于存储程序的运行时动态数据。数据段用于存储初始化内存的静态数据。内存可以从外部宿主导入,同时内存对象也可以导出到外部宿主环境。
+- 表段和元素段:表段用于存储对象引用,目前对象只能是函数,因此可以通过表段实现函数指针的功能。元素段用于存储初始化表段的数据。表对象可以从外部宿主导入,同时表对象也可以导出到外部宿主环境。
+- 起始段:起始段用于存储起始函数的索引,即指定了一个在加载时自动运行的函数。起始函数主要作用:1. 在模块加载后进行初始化工作; 2. 将模块变成可执行文件。
+- 全局段:全局段用于存储全局变量的信息(全局变量的值类型、可变性、初始化表达式等)。
+- 函数段、代码段和类型段:这三个段均是用于存储表达函数的数据。其中
+ - 类型段:类型段用于存储模块内所有的函数签名(函数签名记录了函数的参数和返回值的类型和数量),注意若存在多个函数的函数签名相同,则存储一份即可。
+ - 函数段:函数段用于存储函数对应的函数签名索引,注意是函数签名的索引,而不是函数索引。
+ - 代码段:代码段用于存储函数的字节码和局部变量,也就是函数体内的局部变量和代码所对应的字节码。
+- 导入段和导出段:导出段用于存储导出项信息(导出项的成员名、类型,以及在对应段中的索引等)。导入段用于存储导入项信息(导入项的成员名、类型,以及从哪个模块导入等)。导出/导入项类型有 4 种:函数、表、内存、全局变量。
+- 自定义段:自定义段主要用于保存调试符号等和运行无关的信息。
+
+关于 Wasm 模块二进制格式详细内容可以参考 [Wasm 模块 Binary Format](https://webassembly.github.io/spec/core/binary/modules.html)。
+
+Wasm 模块 wat 文本格式 使用了 `S- 表达式` 的形式来表达 Wasm 模块及其相关定义。关于 wat 格式的更多介绍可以参考 [理解 WebAssembly 文本格式](https://developer.mozilla.org/zh-CN/docs/WebAssembly/Understanding_the_text_format)。
+下图(图片来源 [WebAssembly 解释器实现篇](https://github.com/mcuking/blog/issues/96/))就是使用 C 语言编写的阶乘函数,以及对应的 Wasm 文本格式和二进制格式。
+![img](https://img.alicdn.com/imgextra/i4/O1CN01VcoLBQ1ZcWL5XHYIR_!!6000000003215-0-tps-1892-878.jpg)
+
+可以通过 [WebAssembly Code Explorer](https://wasdk.github.io/wasmcodeexplorer/) 更直观地查看 Wasm 二进制格式和文本格式之间的关联。也可以通过 [wabt](https://github.com/WebAssembly/wabt) 提供工具 ,可以方便的进行 Wasm 二进制格式和文本格式的转换。
+
+#### 1.1.3 Wasm 指令集
+
+Wasm 指令集包含如下内容:
+- Wasm 指令主要分为控制指令、参数指令、变量指令、内存指令和数值指令,每条指令包含操作码和操作数。感兴趣的可以点击查看下 [Wasm 所有的操作码](https://pengowray.github.io/wasm-ops/), 可视化表格直观地展示了 Wasm 所有的操作码。
+- 只有四种数据类型: i32、i64、f32、f64
+- 指令基于栈,并且支持递归调用。例如 i32.add 从栈弹出两个 i32 类型的值,并将它们相加,然后将结果压入栈。
+- 从内存读取数据
+ - i32.load 从内存中读取一个 i32 类型的值。
+ - i32.const value 将一个 i32 类型的值压入栈。
+ - 从线性内存读取数据
+
+关于更多 Wasm 解释器实现原理的可以参考 [WebAssembly 解释器实现篇](https://github.com/mcuking/blog/issues/96)。
+
+### 1.2 TinyGo
+
+[TinyGo](https://tinygo.org/) 是一个 Go 语言编译器,它专注于生成小型、高效的 Go 程序,特别是为嵌入式系统和 WebAssembly 环境设计。 TinyGo 与 Go 语言的标准编译器不同,它有以下优势:
+
+- 生成小型二进制文件:TinyGo 优化了生成的二进制文件的大小,使其非常适合资源受限的环境。
+- 简化的 Go 标准库:TinyGo 提供了一个简化版本的 Go 标准库,减少了依赖和复杂性。TinyGo 支持标准库详情:https://tinygo.org/docs/reference/lang-support/stdlib/ 。
+- 跨平台编译:TinyGo 支持跨平台编译,允许开发者为不同的目标平台生成代码。
+- 支持 WebAssembly:通过使用 TinyGo,开发者可以为 WebAssembly 环境编写高效的 Go 应用程序,同时利用 Go 语言的简洁性和易用性。
+
+“为什么不使用官方 Go 编译器?”的问题,如果对细节感兴趣,请参考 Go 仓库中的相关 issue:
+
+- https://github.com/golang/go/issues/25612
+- https://github.com/golang/go/issues/31105
+- https://github.com/golang/go/issues/38248
+
+这些 issue 讨论了官方 Go 编译器在生成 Wasm 支持方面的限制和进展。 TinyGo 作为一个替代方案,能够生成适合 [Proxy-Wasm ABI](https://github.com/proxy-wasm/spec) 规范的 Wasm 二进制文件,这使得它成为开发 Proxy-Wasm 应用程序的理想选择。
+
+### 1.3 Proxy-Wasm Go SDK
+
+[Proxy-Wasm Go SDK](https://github.com/higress-group/proxy-wasm-go-sdk) 依赖于 TinyGo, 同时 Proxy-Wasm Go SDK 是基于 [Proxy-Wasm ABI](https://github.com/proxy-wasm/spec) 规范使用 Go 编程语言扩展网络代理(例如 Envoyproxy)的 SDK, 而 Proxy-Wasm ABI 定义了网络代理和在网络代理内部运行的 Wasm 虚拟机之间的接口。
+通过这个 SDK,可以轻松地生成符合 Proxy-Wasm 规范的 Wasm 二进制文件,而无需了解 Proxy-Wasm ABI 规范,同时开发人员可以依赖这个 SDK 的 Go API 来开发插件扩展 Enovy 功能。
+
+
+## 2 Wasm VM、插件和 Envoy 配置
+
+Wasm 虚拟机(Wasm VM) 或简称 VM 指的是加载 Wasm 程序的实例。 在 Envoy 中,VM 通常在每个线程中创建并相互隔离。因此 Wasm 程序将复制到 Envoy 所创建的线程里,并在这些虚拟机上加载并执行。
+插件提供了一种灵活的方式来扩展和自定义 Envoy 的行为。Proxy-Wasm 规范允许在每个 VM 中配置多个插件。因此一个 VM 可以被多个插件共同使用。Envoy 中有三种类型插件: `Http Filter`、`Network Filter` 和 `Wasm Service`。
+
+- `Http Filter` 是一种处理 Http 协议的插件,例如操作 Http 请求头、正文等。
+- `Network Filter` 是一种处理 Tcp 协议的插件,例如操作 Tcp 数据帧、连接建立等。
+- `Wasm Service` 是在单例 VM 中运行的插件类型(即在 Envoy 主线程中只有一个实例)。它主要用于执行与 `Network Filter` 或 `Http Filter` 并行的一些额外工作,如聚合指标、日志等。这样的单例 VM 本身也被称为 `Wasm Service`。
+
+其架构如下图(图片来源 [Proxy-Wasm Go SDK](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/doc/OVERVIEW.md)):
+![img](https://img.alicdn.com/imgextra/i4/O1CN018UJzEX1YlqnAmBV4u_!!6000000003100-0-tps-2321-1190.jpg)
+
+
+### 2.1 Envoy 配置
+
+所有类型插件的配置都包含 `vm_config` 用于配置 Wasm VM, 和 `configuration` 用于配置插件实例。
+
+```yaml
+vm_config:
+ vm_id: "foo"
+ runtime: "envoy.wasm.runtime.v8"
+ configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: '{"my-vm-env": "dev"}'
+ code:
+ local:
+ filename: "example.wasm"
+configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: '{"my-plugin-config": "bar"}'
+```
+
+配置说明如下:
+
+| 字段 | 描述 |
+|--------------------------|-----------------------------------------------|
+| `vm_config` | 配置 Wasm VM |
+| `vm_config.vm_id` | 用于跨 VM 通信的语义隔离。详情请参考 跨 VM 通信 部分。 |
+| `vm_config.runtime` | 指定 Wasm 运行时类型。默认为 envoy.wasm.runtime.v8。 |
+| `vm_config.configuration` | 用于设置 VM 的配置数据 |
+| `vm_config.code` | Wasm 二进制文件的位置 |
+| `configuration` | 对应于每个插件实例配置(在下面介绍的 PluginContext)。 |
+
+> 完全相同的 vm_config 配置的多个插件它们之间共享一个 Wasm VM,单个 Wasm VM 用于多个 Http Filter 或 Network Filter,可以提升内存/CPU 资源效率、降低启动延迟。
+> 完整的 Envoy API 配置可以 [参考 Envoy 文档](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/wasm/v3/wasm.proto#envoy-v3-api-msg-extensions-wasm-v3-pluginconfig)。
+
+Envoy Wasm 运行时目前有以下几种选择:
+- envoy.wasm.runtime.null:这表示一个空的沙盒(null sandbox)环境,Wasm 模块必须被编译并链接到 Envoy 的二进制文件中。这种方式适用于那些需要将 Wasm 模块与 Envoy 二进制文件一起分发的部署场景。
+- envoy.wasm.runtime.v8: 基于 V8 JavaScript 引擎的运行时。
+- envoy.wasm.runtime.wamr: WAMR (WebAssembly Micro Runtime) 运行时。
+- envoy.wasm.runtime.wasmtime: Wasmtime 运行时。
+
+不同的运行时有各自的优缺点,比如 [V8](https://v8.dev/) 性能较好但容器体积较大,[WAMR](https://github.com/bytecodealliance/wasm-micro-runtime) 和 [wasmtime](https://wasmtime.dev/) 则相对轻量。
+
+> [待补充?] envoy v8 runtime 如何加载 wasm 和 如何和 envoy 交互原理。
+
+### 2.2 Http Filter 配置
+
+Http Filter 插件配置设置为 `envoy.filter.http.wasm`,Http Filter 插件可以处理 HTTP 请求和响应。 其主要配置如下:
+```yaml
+http_filters:
+ - name: envoy.filters.http.wasm
+ typed_config:
+ "@type": type.googleapis.com/udpa.type.v1.TypedStruct
+ type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ value:
+ config:
+ configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: |
+ {
+ "header": "x-wasm-header",
+ "value": "demo-wasm"
+ }
+ vm_config:
+ runtime: "envoy.wasm.runtime.v8"
+ code:
+ local:
+ filename: "./examples/http_headers/main.wasm"
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+```
+
+这时 Envoy 会在每个工作线程中实例化一个 Wasm 虚拟机,该虚拟机将专门用于处理该线程上的 HTTP 请求和响应。每个虚拟机都会加载和执行 WebAssembly 代码,允许对 HTTP 流量进行自定义处理,如修改头信息、处理请求和响应体等。
+完整的配置可以参考 [envoy.yaml](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/examples/http_headers/envoy.yaml) 。
+
+
+### 2.3 Network Filter 配置
+
+`Network Filter` 插件配置设置为 `envoy.filters.network.wasm`,`Network Filter` 插件可以处理 TCP 请求和响应。 其主要配置如下:
+
+```yaml
+filter_chains:
+- filters:
+ - name: envoy.filters.network.wasm
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm
+ config:
+ vm_config: { ... }
+ # ... plugin config follows
+ - name: envoy.tcp_proxy
+```
+
+这时 Envoy 会在每个工作线程中实例化一个 Wasm 虚拟机,该虚拟机将专门用于处理该线程上的 TCP 请求和响应。每个虚拟机都会加载和执行 WebAssembly 代码,允许对 TCP 流量进行自定义处理等。
+完整的配置可以参考 [envoy.yaml](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/examples/network/envoy.yaml) 。
+
+
+### 2.4 Wasm Service 配置
+
+`Wasm Service` 插件配置设置为 `envoy.bootstrap.wasm`。插件在 Envoy 启动时加载的,其主要配置如下:
+
+```yaml
+bootstrap_extensions:
+- name: envoy.bootstrap.wasm
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.wasm.v3.WasmService
+ singleton: true
+ config:
+ vm_config: { ... }
+ # ... plugin config follows
+```
+
+`singleton` 设置为 true 时,生成虚拟机(VM)是单例,并且运行在 Envoy 的主线程上,因此它不会阻塞任何工作线程。
+
+完整的配置可以参考 [envoy.yaml](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/examples/shared_queue/envoy.yaml) 。
+
+### 2.5 每个线程中多个插件共享一个 VM
+
+每个线程中多个插件共享一个 VM,其主要配置如下:
+
+```yaml
+static_resources:
+ listeners:
+ - name: http-header-operation
+ address:
+ socket_address:
+ address: 0.0.0.0
+ port_value: 18000
+ filter_chains:
+ - filters:
+ - name: envoy.http_connection_manager
+ typed_config:
+ # ....
+ http_filters:
+ - name: envoy.filters.http.wasm
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ config:
+ configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: "http-header-operation"
+ vm_config:
+ vm_id: "my-vm-id"
+ runtime: "envoy.wasm.runtime.v8"
+ configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: "my-vm-configuration"
+ code:
+ local:
+ filename: "all-in-one.wasm"
+ - name: envoy.filters.http.router
+
+ - name: http-body-operation
+ address:
+ socket_address:
+ address: 0.0.0.0
+ port_value: 18001
+ filter_chains:
+ - filters:
+ - name: envoy.http_connection_manager
+ typed_config:
+ # ....
+ http_filters:
+ - name: envoy.filters.http.wasm
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ config:
+ configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: "http-body-operation"
+ vm_config:
+ vm_id: "my-vm-id"
+ runtime: "envoy.wasm.runtime.v8"
+ configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: "my-vm-configuration"
+ code:
+ local:
+ filename: "all-in-one.wasm"
+ - name: envoy.filters.http.router
+
+ - name: tcp-total-data-size-counter
+ address:
+ socket_address:
+ address: 0.0.0.0
+ port_value: 18002
+ filter_chains:
+ - filters:
+ - name: envoy.filters.network.wasm
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.network.wasm.v3.Wasm
+ config:
+ configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: "tcp-total-data-size-counter"
+ vm_config:
+ vm_id: "my-vm-id"
+ runtime: "envoy.wasm.runtime.v8"
+ configuration:
+ "@type": type.googleapis.com/google.protobuf.StringValue
+ value: "my-vm-configuration"
+ code:
+ local:
+ filename: "all-in-one.wasm"
+ - name: envoy.tcp_proxy
+ typed_config: # ...
+```
+
+
+
+在 `18000` 和 `18001` 监听器上的 Http 过滤器链以及 `18002` 上的网络过滤器链中,vm_config 字段都是相同的。在这种情况下,Envoy 中的多个插件将使用同一个 Wasm 虚拟机。 为了重用相同的 VM,所有的 vm_config.vm_id、vm_config.runtime、vm_config.configuration 和 vm_config.code 必须相同。
+
+通过这种配置方式允许为不同的过滤器重用同一个 Wasm 虚拟机,通过为每个 `PluginContext` 提供了一个隔离的环境,使得插件能够独立运行,同时共享同一个虚拟机的执行环境,虚拟机只需要加载和初始化一次即可为多个插件服务,这不仅可以减少内存占用,还可以降低启动时的延迟。
+
+完整的配置可以参考 [envoy.yaml](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/examples/shared_queue/envoy.yaml) 。
+
+## 3 Proxy-Wasm Go SDK API
+
+上面介绍插件概念和插件配置,下面开始深入探讨 Proxy-Wasm Go SDK 的 API。
+
+### 3.1 Contexts
+
+上下文(Contexts) 是 Proxy-Wasm Go SDK 中的接口集合,它们在 [types](https://github.com/higress-group/proxy-wasm-go-sdk/tree/main/proxywasm/types) 包中定义。
+有四种类型的上下文:VMContext、PluginContext、TcpContext 和 HttpContext。它们的关系如下图:
+```
+ Wasm Virtual Machine
+ (.vm_config.code)
+┌────────────────────────────────────────────────────────────────┐
+│ Your program (.vm_config.code) TcpContext │
+│ │ ╱ (Tcp stream) │
+│ │ 1: 1 ╱ │
+│ │ 1: N ╱ 1: N │
+│ VMContext ────────── PluginContext │
+│ (Plugin) ╲ 1: N │
+│ ╲ │
+│ ╲ HttpContext │
+│ (Http stream) │
+└────────────────────────────────────────────────────────────────┘
+```
+
+1) VMContext 对应于每个 .vm_config.code,每个 VM 中只存在一个 VMContext。
+2) VMContext 是 PluginContexts 的父上下文,负责创建 PluginContext。
+3) PluginContext 对应于一个 Plugin 实例。一个 PluginContext 对应于 Http Filter、Network Filter、Wasm Service 的 configuration 字段配置。
+4) PluginContext 是 TcpContext 和 HttpContext 的父上下文,并且负责为 处理 Http 流的Http Filter 或 处理 Tcp 流的 Network Filter 创建上下文。
+5) TcpContext 负责处理每个 Tcp 流。
+6) HttpContext 负责处理每个 Http 流。
+
+因此,自定义插件要实现 `VMContext` 和 `PluginContext`。 同时 `Http Filter` 或 `Network Filter`,要分别实现 `HttpContext` 或 `TcpContext`。
+
+首先 VMContext 定义如下:
+
+```go
+type VMContext interface {
+ // OnVMStart 在 VM 创建和调用 main 函数后被调用。
+ // 在此调用期间,可以通过 GetVMConfiguration 获取在 vm_config.configuration 设置的配置。
+ // 这主要用于执行 Wasm VM 范围内的初始化。
+ OnVMStart(vmConfigurationSize int) OnVMStartStatus
+
+ // NewPluginContext 用于为每个插件配置创建 PluginContext。
+ NewPluginContext(contextID uint32) PluginContext
+}
+```
+
+VMContext 负责通过 NewPluginContext 方法创建 PluginContext。同时在 VM 启动阶段调用 OnVMStart,并且可以通过 `GetVMConfiguration` hostcall API 获取 vm_config.configuration 的值。这样就可以进行 VM 范围内的插件初始化并控制 VMContext 的行为。
+
+PluginContext,定义如下(省略了一些方法):
+```go
+type PluginContext interface {
+ // OnPluginStart 在所有插件上下文上调用(如果在这是 VM 上下文,则在 OnVmStart 之后)。
+ // 在此调用期间,可以通过 GetPluginConfiguration 获取 envoy.yaml 中 config.configuration 设置的配置。
+ OnPluginStart(pluginConfigurationSize int) OnPluginStartStatus
+
+ // 以下函数用于在流上创建上下文,
+ // *必须* 实现它们中的任一个,对应于扩展点。例如,如果您配置此插件上下文在 Http 过滤器上运行,那么必须实现 NewHttpContext。
+ // 对 Tcp 过滤器也是如此。
+ //
+ // NewTcpContext 用于为每个 Tcp 流创建 TcpContext。
+ NewTcpContext(contextID uint32) TcpContext
+ // NewHttpContext 用于为每个 Http 流创建 HttpContext。
+ NewHttpContext(contextID uint32) HttpContext
+}
+```
+
+`PluginContext` 有 `OnPluginStart` 方法,创建插件时调用,可以通过 GetPluginConfiguration hostcall API 获取 plugin config 中 configuration 字段的值。
+另外 `PluginContext` 有 `NewTcpContext` 和 `NewHttpContext` 方法,为每个 Http 或 Tcp 流创建上下文时调用。 关于 HttpContext 和 TcpContext 的详细定义请参考 [context.go](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/proxywasm/types/context.go) 。
+
+### 3.2 Hostcall API
+
+Hostcall API 是指在 Wasm 模块内调用 Envoy 提供的功能。这些功能通常用于获取外部数据或与 Envoy 交互。在开发 Wasm 插件时,需要访问网络请求的元数据、修改请求或响应头、记录日志等,这些都可以通过 Hostcall API 来实现。
+Hostcall API 在 proxywasm 包的 [hostcall.go](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/proxywasm/hostcall.go) 中定义。
+Hostcall API 包括配置和初始化、定时器设置、上下文管理、插件完成、共享队列管理、Redis 操作、Http 调用、TCP 流操作、HTTP 请求/响应头和体操作、共享数据操作、日志操作、属性和元数据操作、指标操作。具体函数名称和描述如下:
+
+#### 1.配置和初始化
+| 函数名 | 描述 |
+|--------------------------------|--------------------------------------------------------------|
+| `GetVMConfiguration` | 获取在 `vm_config.configuration` 字段中指定的配置。此功能仅在 `types.PluginContext.OnVMStart` 调用期间可用。 |
+| `GetPluginConfiguration` | 获取在 `config.configuration` 字段中指定的配置。此功能仅在 `types.PluginContext.OnPluginStart` 调用期间可用。 |
+
+#### 2.定时器设置
+| 函数名 | 描述 |
+|--------------------------------|--------------------------------------------------------------|
+| `SetTickPeriodMilliSeconds` | 设置 `types.PluginContext.OnTick` 调用的tick间隔。此功能仅对 `types.PluginContext` 有效。 |
+
+#### 3.上下文管理
+| 函数名 | 描述 |
+|--------------------------------|--------------------------------------------------------------|
+| `SetEffectiveContext` | 设置有效上下文为 `context_id`。通常用于在接收到 `types.PluginContext.OnQueueReady` 或 `types.PluginContext.OnTick` 后更改上下文。 |
+
+#### 4.插件完成
+| 函数名 | 描述 |
+|--------------------------------|-----------------------------------------------------------------------------------|
+| `PluginDone` | 当 `OnPluginDone` 返回 false,表示插件处于待定状态,在删除之前必须调用此函数。此功能仅对 `types.PluginContext` 有效。 |
+
+#### 5.共享队列管理
+| 函数名 | 描述 |
+|--------------------------------|-------------------------------------------------------------|
+| `RegisterSharedQueue` | 在此插件上下文中注册共享队列。 |
+| `ResolveSharedQueue` | 获取给定 `vmID` 和 `queueName` 的队列ID。 |
+| `EnqueueSharedQueue` | 将数据排队到给定队列ID的共享队列。 |
+| `DequeueSharedQueue` | 从给定队列ID的共享队列中出队数据。 |
+
+#### 6.Redis 操作
+| 函数名 | 描述 |
+|--------------------------------|--------------------------------------------------------------|
+| `RedisInit` | 初始化Redis连接。 |
+| `DispatchRedisCall` | 发送Redis调用。 |
+| `GetRedisCallResponse` | 获取Redis调用响应。 |
+
+#### 7.HTTP 调用
+| 函数名 | 描述 |
+|--------------------------------|--------------------------------------------------------------|
+| `DispatchHttpCall` | 向远程集群分派HTTP调用。此功能可被所有上下文使用。 |
+| `GetHttpCallResponseHeaders` | 用于检索由远程集群返回的HTTP响应头,此功能仅在传递给 `DispatchHttpCall` 的 "callback" 函数中可用。 |
+| `GetHttpCallResponseBody` | 用于检索由远程集群返回的HTTP响应体,此功能仅在传递给 `DispatchHttpCall` 的 "callback" 函数中可用。 |
+| `GetHttpCallResponseTrailers` | 用于检索由远程集群返回的HTTP响应尾随头,此功能仅在传递给 `DispatchHttpCall` 的 "callback" 函数中可用。 |
+
+#### 8.TCP 流操作
+| 函数名 | 描述 |
+|--------------------------------|--------------------------------------------------------------|
+| `GetDownstreamData` | 用于检索在宿主中缓冲的TCP下游数据。此功能仅在 `types.TcpContext.OnDownstreamData` 期间可用。 |
+| `AppendDownstreamData` | 将给定字节追加到宿主中缓冲的TCP下游数据。此功能仅在 `types.TcpContext.OnDownstreamData` 期间可用。 |
+| `PrependDownstreamData` | 将给定字节前缀到宿主中缓冲的TCP下游数据。此功能仅在 `types.TcpContext.OnDownstreamData` 期间可用。 |
+| `ReplaceDownstreamData` | 用给定字节替换宿主中缓冲的TCP下游数据。此功能仅在 `types.TcpContext.OnDownstreamData` 期间可用。 |
+| `GetUpstreamData` | 用于检索在宿主中缓冲的TCP上游数据。此功能仅在 `types.TcpContext.OnUpstreamData` 期间可用。 |
+| `AppendUpstreamData` | 将给定字节追加到宿主中缓冲的TCP上游数据。此功能仅在 `types.TcpContext.OnUpstreamData` 期间可用。 |
+| `PrependUpstreamData` | 将给定字节前缀到宿主中缓冲的TCP上游数据。此功能仅在 `types.TcpContext.OnUpstreamData` 期间可用。 |
+| `ReplaceUpstreamData` | 用给定字节替换宿主中缓冲的TCP上游数据。此功能仅在 `types.TcpContext.OnUpstreamData` 期间可用。 |
+| `ContinueTcpStream` | 在返回 `types.Action.Pause` 后,继续TCP连接的处理。此功能仅对 `types.TcpContext` 有效。 |
+| `CloseDownstream` | 关闭Tcp上下文中的下游TCP连接。此功能仅对 `types.TcpContext` 有效。 |
+| `CloseUpstream` | 关闭Tcp上下文中的上游TCP连接。此功能仅对 `types.TcpContext` 有效。 |
+
+#### 9.HTTP 请求/响应头和体操作
+| 函数名 | 描述 |
+|-------------------------------|--------------------------------------------------------------|
+| `GetHttpRequestHeaders` | 获取HTTP请求头。此功能仅在 `types.HttpContext.OnHttpRequestHeaders` 和 `types.HttpContext.OnHttpStreamDone` 期间可用。 |
+| `ReplaceHttpRequestHeaders` | 用给定的头替换HTTP请求头。此功能仅在 `types.HttpContext.OnHttpRequestHeaders` 期间可用。 |
+| `GetHttpRequestHeader` | 获取给定 "key" 的HTTP请求头的值。此功能仅在 `types.HttpContext.OnHttpRequestHeaders` 和 `types.HttpContext.OnHttpStreamDone` 期间可用。 |
+| `RemoveHttpRequestHeader` | 移除请求头中给定 "key" 的所有值。此功能仅在 `types.HttpContext.OnHttpRequestHeaders` 期间可用。 |
+| `ReplaceHttpRequestHeader` | 替换请求头中给定 "key" 的值。此功能仅在 `types.HttpContext.OnHttpRequestHeaders` 期间可用。 |
+| `AddHttpRequestHeader` | 向请求头添加给定 "key" 的值。此功能仅在 `types.HttpContext.OnHttpRequestHeaders` 期间可用。 |
+| `GetHttpRequestBody` | 获取整个HTTP请求体。此功能仅在 `types.HttpContext.OnHttpRequestBody` 期间可用。 |
+| `AppendHttpRequestBody` | 向HTTP请求体缓冲区追加给定字节。此功能仅在 `types.HttpContext.OnHttpRequestBody` 期间可用。 |
+| `PrependHttpRequestBody` | 向HTTP请求体缓冲区前缀给定字节。此功能仅在 `types.HttpContext.OnHttpRequestBody` 期间可用。 |
+| `ReplaceHttpRequestBody` | 用给定字节替换HTTP请求体缓冲区。此功能仅在 `types.HttpContext.OnHttpRequestBody` 期间可用。 |
+| `GetHttpRequestTrailers` | 获取HTTP请求尾随头。此功能仅在 types.HttpContext.OnHttpRequestTrailers 和 types.HttpContext.OnHttpStreamDone 期间可用。 |
+| `ReplaceHttpRequestTrailers` | 用给定的尾随头替换HTTP请求尾随头。此功能仅在 types.HttpContext.OnHttpRequestTrailers 期间可用。 |
+| `GetHttpRequestTrailer` | 获取给定 "key" 的HTTP请求尾随头的值。此功能仅在 types.HttpContext.OnHttpRequestTrailers 和 types.HttpContext.OnHttpStreamDone 期间可用。 |
+| `RemoveHttpRequestTrailer` | 移除请求尾随头中给定 "key" 的所有值。此功能仅在 types.HttpContext.OnHttpRequestTrailers 期间可用。 |
+| `ReplaceHttpRequestTrailer` | 替换请求尾随头中给定 "key" 的值。此功能仅在 types.HttpContext.OnHttpRequestTrailers 期间可用。 |
+| `AddHttpRequestTrailer` | 向请求尾随头添加给定 "key" 的值。此功能仅在 types.HttpContext.OnHttpRequestTrailers 期间可用。 |
+| `ResumeHttpRequest` | 继续停止的HTTP请求处理。此功能仅在 types.HttpContext 期间可用。 |
+| `GetHttpResponseHeaders` | 获取HTTP响应头。此功能仅在 types.HttpContext.OnHttpResponseHeaders 和 types.HttpContext.OnHttpStreamDone 期间可用。 |
+| `ReplaceHttpResponseHeaders` | 用给定的头替换HTTP响应头。此功能仅在 types.HttpContext.OnHttpResponseHeaders 期间可用。 |
+| `GetHttpResponseHeader ` | 获取给定 "key" 的HTTP响应头的值。此功能仅在 types.HttpContext.OnHttpResponseHeaders 和 types.HttpContext.OnHttpStreamDone 期间可用。 |
+| `RemoveHttpResponseHeader` | 移除响应头中给定 "key" 的所有值。此功能仅在 types.HttpContext.OnHttpResponseHeaders 期间可用。 |
+| `ReplaceHttpResponseHeader` | 替换响应头中给定 "key" 的值。此功能仅在 types.HttpContext.OnHttpResponseHeaders 期间可用。 |
+| `AddHttpResponseHeader` | 向响应头添加给定 "key" 的值。此功能仅在 types.HttpContext.OnHttpResponseHeaders 期间可用。 |
+| `GetHttpResponseBody` | 获取整个HTTP响应体。此功能仅在 types.HttpContext.OnHttpResponseBody 期间可用。 |
+| `AppendHttpResponseBody` | 向HTTP响应体缓冲区追加给定字节。此功能仅在 types.HttpContext.OnHttpResponseBody 期间可用。 |
+| `PrependHttpResponseBody` | 向HTTP响应体缓冲区前缀给定字节。此功能仅在 types.HttpContext.OnHttpResponseBody 期间可用。 |
+| `ReplaceHttpResponseBody` | 用给定字节替换HTTP响应体缓冲区。此功能仅在 types.HttpContext.OnHttpResponseBody 期间可用。 |
+| `GetHttpResponseTrailers` | 获取HTTP响应尾随头。此功能仅在 types.HttpContext.OnHttpResponseTrailers 和 types.HttpContext.OnHttpStreamDone 期间可用。 |
+| `ReplaceHttpResponseTrailers` | 用给定的尾随头替换HTTP响应尾随头。此功能仅在 types.HttpContext.OnHttpResponseTrailers 期间可用。 |
+| `GetHttpResponseTrailer` | 获取给定 "key" 的HTTP响应尾随头的值。此功能仅在 types.HttpContext.OnHttpResponseTrailers 和 types.HttpContext.OnHttpStreamDone 期间可用。 |
+| `RemoveHttpResponseTrailer` | 移除响应尾随头中给定 "key" 的所有值。此功能仅在 types.HttpContext.OnHttpResponseTrailers 期间可用。 |
+| `ReplaceHttpResponseTrailer` | 替换响应尾随头中给定 "key" 的值。此功能仅在 types.HttpContext.OnHttpResponseHeaders 期间可用。 |
+| `AddHttpResponseTrailer` | 向响应尾随头添加给定 "key" 的值。此功能仅在 types.HttpContext.OnHttpResponseHeaders 期间可用。 |
+| `ResumeHttpResponse` | 继续停止的HTTP响应处理。此功能仅在 types.HttpContext 期间可用。 |
+| `SendHttpResponse` | 向下游发送HTTP响应。调用此函数后,您必须返回 types.Action.Pause 以停止初始HTTP请求/响应的进一步处理。 |
+
+
+#### 10.共享数据操作
+| 函数名 | 描述 |
+|--------------------------------|---------|
+| `GetSharedData` | 获取共享数据。 |
+| `SetSharedData` | 设置共享数据。 |
+
+#### 11.日志操作
+| 函数名 | 描述 |
+|--------------------------------|--------------------------------------------------------------|
+| `LogTrace` | 以 Trace 日志级别发出消息。 |
+| `LogTracef` | 根据格式说明符格式化并发出 Trace 日志级别的日志。 |
+| `LogDebug` | 以 Debug 日志级别发出消息。 |
+| `LogDebugf` | 根据格式说明符格式化并发出 Debug 日志级别的日志。 |
+| `LogInfo` | 以 Info 日志级别发出消息。 |
+| `LogInfof` | 根据格式说明符格式化并发出 Info 日志级别的日志。 |
+| `LogWarn` | 以 Warn 日志级别发出消息。 |
+| `LogWarnf` | 根据格式说明符格式化并发出 Warn 日志级别的日志。 |
+| `LogError` | 以 Error 日志级别发出消息。 |
+| `LogErrorf` | 根据格式说明符格式化并发出 Error 日志级别的日志。 |
+| `LogCritical` | 以 Critical 日志级别发出消息。 |
+| `LogCriticalf` | 根据格式说明符格式化并发出 Critical 日志级别的日志。 |
+
+#### 12.指标操作
+| 函数名 | 描述 |
+|--------------------------------|--------------------------------------------------------------|
+| `DefineCounterMetric` | 为名称定义一个计数器指标。返回一个 `MetricCounter` 用于后续操作。 |
+| `DefineGaugeMetric` | 为名称定义一个计量器指标。返回一个 `MetricGauge` 用于后续操作。 |
+| `DefineHistogramMetric` | 为名称定义一个直方图指标。返回一个 `MetricHistogram` 用于后续操作。 |
+| `MetricCounter.Value` | 获取 `MetricCounter` 的当前值。 |
+| `MetricCounter.Increment` | 将 `MetricCounter` 的当前值增加指定的偏移量。 |
+| `MetricGauge.Value` | 获取 `MetricGauge` 的当前值。 |
+| `MetricGauge.Add` | 将 `MetricGauge` 的当前值增加指定的偏移量。 |
+| `MetricHistogram.Value` | 获取 `MetricHistogram` 的当前值。 |
+| `MetricHistogram.Record` | 为 `MetricHistogram` 记录一个值。 |
+
+
+
+### 3.3 插件调用入口 Entrypoint
+
+当 Envoy 创建 VM 时,在虚拟机内部创建 `VMContext` 之前,它会在启动阶段调用插件程序的 `main` 函数。所以必须在 `main` 函数中传递插件自定义的 `VMContext` 实现。
+[proxywasm](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/proxywasm/) 包的 `SetVMContext` 函数是入口点。`main` 函数如下:
+
+```go
+func main() {
+ proxywasm.SetVMContext(&myVMContext{})
+}
+
+type myVMContext struct { .... }
+
+var _ types.VMContext = &myVMContext{}
+
+// Implementations follow...
+```
+
+## 4 跨虚拟机通信
+
+Envoy 中的跨虚拟机通信(Cross-VM communications)允许在不同线程中运行 的Wasm 虚拟机(VMs)之间进行数据交换和通信。这在需要在多个VMs之间聚合数据、统计信息或缓存数据等场景中非常有用。
+跨虚拟机通信主要有两种方式:
+
+- 共享数据(Shared Data):
+ - 共享数据是一种在所有 VMs 之间共享的键值存储,可以用于存储和检索简单的数据项。
+ - 它适用于存储小的、不经常变化的数据,例如配置参数或统计信息。
+- 共享队列(Shared Queue):
+ - 共享队列允许VMs之间进行更复杂的数据交换,支持发送和接收更丰富的数据结构。
+ - 队列可以用于实现任务调度、异步消息传递等模式。
+
+### 4.1 共享数据(Shared Data)
+
+如果想要在所有 Wasm 虚拟机(VMs)运行的多个工作线程间拥有全局请求计数器,或者想要缓存一些应被所有 Wasm VMs 使用的数据,那么共享数据(Shared Data)或等效的共享键值存储(Shared KVS)就会发挥作用。
+共享数据本质上是一个跨所有VMs共享的键值存储(即跨 VM 或跨线程)。
+
+共享数据 KVS 是根据 vm_config 中指定的创建的。可以在所有 Wasm VMs 之间共享一个键值存储,而它们不必具有相同的二进制文件 `vm_config.code`,唯一的要求是具有相同的 vm_id。
+
+![img](https://img.alicdn.com/imgextra/i2/O1CN01fLn4Lr1GXxhKORL9t_!!6000000000633-0-tps-1784-1266.jpg)
+
+在上图(图片来源 [Proxy-Wasm Go SDK](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/doc/OVERVIEW.md))中,可以看到即使它们具有不同的二进制文件( hello.wasm 和 bye.wasm ),"vm_id=foo"的 VMs 也共享相同的共享数据存储。
+hostcall.go 中定义共享数据相关的 API如下:
+```go
+// GetSharedData 用于检索给定 "key" 的值。
+// 返回的 "cas" 应用于 SetSharedData 以实现该键的线程安全更新。
+func GetSharedData(key string) (value []byte, cas uint32, err error)
+
+// SetSharedData 用于在共享数据存储中设置键值对。
+// 共享数据存储按主机中的 "vm_config.vm_id" 定义。
+//
+// 当给定的 CAS 值与当前值不匹配时,将返回 ErrorStatusCasMismatch。
+// 这表明其他 Wasm VM 已经成功设置相同键的值,并且该键的当前 CAS 已递增。
+// 建议在遇到此错误时实现重试逻辑。
+//
+// 将 cas 设置为 0 将永远不会返回 ErrorStatusCasMismatch 并且总是成功的,
+// 但这并不是线程安全的,即可能在您调用此函数时另一个 VM 已经设置了该值,
+// 看到的值与存储时的值已经不同。
+func SetSharedData(key string, value []byte, cas uint32) error
+```
+
+共享数据 API 是其线程安全性和跨 VM 安全性,这通过 "cas" ([Compare-And-Swap](https://en.wikipedia.org/wiki/Compare-and-swap))值来实现。如何使用 GetSharedData 和 SetSharedData 函数可以参考 [示例](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/examples/shared_data/main.go)。
+
+
+### 4.2 共享队列 Shared Queue
+
+如果要在请求/响应处理的同时跨所有 Wasm VMs 聚合指标,或者将一些跨 VM 聚合的信息推送到远程服务器,可以通过 *Shared Queue* 来实现。
+
+*Shared Queue* 是为 `vm_id` 和队列名称的组合创建的 FIFO(先进先出)队列。并为该组合(`vm_id`,名称)分配了一个唯一的 *queue id*,该 ID 用于入队/出队操作。
+
+“入队”和“出队”等操作具有线程安全性和跨 VM 安全性。在 hostcall.go 中与 *Shared Queue* 相关 API 如下:
+
+```go
+// DequeueSharedQueue 从给定 queueID 的共享队列中出队数据。
+// 要获取目标队列的 queue id,请先使用 "ResolveSharedQueue"。
+func DequeueSharedQueue(queueID uint32) ([]byte, error)
+
+// RegisterSharedQueue 在此插件上下文中注册共享队列。
+// "注册" 意味着每当该 queueID 上有新数据入队时,将对此插件上下文调用 OnQueueReady。
+// 仅适用于 types.PluginContext。返回的 queueID 可用于 Enqueue/DequeueSharedQueue。
+// 请注意 "name" 必须在所有共享相同 "vm_id" 的 Wasm VMs 中是唯一的。使用 "vm_id" 来分隔共享队列的命名空间。
+//
+// 只有在调用 RegisterSharedQueue 之后,ResolveSharedQueue("此 vm_id", "名称") 才能成功
+// 通过其他 VMs 检索 queueID。
+func RegisterSharedQueue(name string) (queueID uint32, err error)
+
+// EnqueueSharedQueue 将数据入队到给定 queueID 的共享队列。
+// 要获取目标队列的 queue id,请先使用 "ResolveSharedQueue"。
+func EnqueueSharedQueue(queueID uint32, data []byte) error
+
+// ResolveSharedQueue 获取给定 vmID 和队列名称的 queueID。
+// 返回的 queueID 可用于 Enqueue/DequeueSharedQueue。
+func ResolveSharedQueue(vmID, queueName string) (queueID uint32, err error)
+```
+
+`RegisterSharedQueue` 和 `DequeueSharedQueue` 由队列的“消费者”使用,而 `ResolveSharedQueue` 和 `EnqueueSharedQueue` 是为队列“生产者”准备的。请注意:
+
+- RegisterSharedQueue 用于为调用者的 name 和 vm_id 创建共享队列。使用一个队列,那么必须先由一个 VM 调用这个函数。这可以由 PluginContext 调用,因此可以认为“消费者” = PluginContexts。
+- ResolveSharedQueue 用于获取 name 和 vm_id 的 queue id。这是为“生产者”准备的。
+
+这两个调用都返回一个队列 ID,该 ID 用于 DequeueSharedQueue 和 EnqueueSharedQueue。同时当队列中入队新数据时 消费者 PluginContext 中有 OnQueueReady(queueID uint32) 接口会收到通知。
+还强烈建议由 Envoy 的主线程上的单例 Wasm Service 创建共享队列。否则 OnQueueReady 将在工作线程上调用,这会阻塞它们处理 Http 或 Tcp 流。
+
+![img](https://img.alicdn.com/imgextra/i1/O1CN01s1cT1s28xb7OKkEg0_!!6000000007999-0-tps-2378-1316.jpg)
+在上图(图片来源 [Proxy-Wasm Go SDK](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/doc/OVERVIEW.md))中展示共享队列工作原理,更详细如何使用共享队列可以参考 [示例](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/examples/shared_queue/main.go)。
+
+
+## 5 限制和注意事项
+
+以下是在使用 Proxy-Wasm Go SDK 和 Proxy-Wasm 编写插件时需要注意事项。
+
+### 5.1 一些标准库不可用
+
+一些现有的标准库不可用(可导入但运行时 panic / 无法导入)。这有几个原因:
+1. TinyGo 的 WASI 目标不支持某些系统调用。
+2. TinyGo 没有实现 reflect 包的全部功能。
+3. [Proxy-Wasm C++ 主机](https://github.com/proxy-wasm/proxy-wasm-cpp-host) 尚未支持某些 WASI API。
+4. TinyGo 或 Proxy-Wasm 中不支持一些语言特性:包括 `recover` 和 `goroutine`。
+
+随着 TinyGo 和 Proxy-Wasm 的发展,这些问题将得到缓解。
+
+### 5.2 由于垃圾回收导致的性能开销
+
+由于 GC,使用 Go/TinyGo 会带来性能开销,尽管可以认为与代理中的其他操作相比,GC 的开销足够小。
+TinyGo 允许禁用 GC,但由于内部需要使用映射(隐式引起分配)来保存虚拟机的状态,可以通过 `alloc(uintptr)` [接口](https://github.com/tinygo-org/tinygo/blob/v0.14.1/src/runtime/gc_none.go#L13) 使用 `-gc=custom` 选项设置 proxy-wasm 定制的 GC 算法。
+
+### 5.3 `recover` 未实现
+
+在 TinyGo 中,`recover` 未实现(https://github.com/tinygo-org/tinygo/issues/891)。这也意味着依赖 `recover` 的代码将无法按预期工作。
+
+### 5.4 Goroutine 不支持
+
+在 TinyGo 中,Goroutine 通过 LLVM 的协程实现(见[这篇博客文章](https://aykevl.nl/2019/02/tinygo-goroutines))。 在 Envoy 中,Wasm 模块以事件驱动的方式运行,因此一旦主函数退出,“调度器”就不再执行。因此不能像普通主机环境中那样使用 Goroutine 。
+在以事件驱动方式执行的 Wasm VM 线程中如何处理 Goroutine 的问题尚未有解决方案。建议使用实现 `OnTick` 函数来处理任何异步任务。
+
+## 6 插件开发样例
+
+用 Proxy-Wasm Go SDK 实现一个简单的插件,具体样例如下:
+
+```golang
+package main
+
+import (
+ "github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
+ "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
+)
+
+// 插件入口
+func main() {
+ proxywasm.SetVMContext(&vmContext{})
+}
+
+// VM 上下文
+type vmContext struct {
+ // Embed the default VM context here,
+ types.DefaultVMContext
+ // 这里添加 VM 配置
+}
+
+// VM 启动回调
+func (*vmContext) OnVMStart(vmConfigurationSize int) types.OnVMStartStatus {
+ proxywasm.LogInfof("OnVMStart()")
+ // 获取 VM 配置
+ _, err := proxywasm.GetVMConfiguration()
+ if err != nil {
+ proxywasm.LogCriticalf("error reading vm configuration: %v", err)
+ }
+ // 这里解析 VM 配置
+ return types.OnVMStartStatusOK
+}
+
+// 生成插件上下文
+func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext {
+ proxywasm.LogInfof("NewPluginContex()")
+ return &pluginContext{}
+}
+
+// 插件上下文
+type pluginContext struct {
+ // Embed the default plugin context here,
+ types.DefaultPluginContext
+ // 这里添加插件配置
+}
+
+// Http 上下文
+type httpContext struct {
+ // Embed the default root http context here,
+ // so that we don't need to reimplement all the methods.
+ types.DefaultHttpContext
+ // 这里添加http 上下文属性
+ requestBodySize int
+ responseBodySize int
+}
+
+// 生成 Http 上下文
+func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext {
+ proxywasm.LogInfof("NewHttpContext()")
+ return &httpContext{}
+}
+
+// 插件启动回调,
+func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus {
+ proxywasm.LogInfof("OnPluginStart()")
+ // 获取插件配置
+ _, err := proxywasm.GetPluginConfiguration()
+ if err != nil {
+ proxywasm.LogCriticalf("error reading plugin configuration: %v", err)
+ }
+ // 这里解析插件配置
+
+ return types.OnPluginStartStatusOK
+}
+
+// http 请求头回调
+func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
+ proxywasm.LogInfof("OnHttpRequestHeaders()")
+ // 这里处理请求头回调
+ return types.ActionContinue
+}
+
+// http 请求体回调,注意这里流式处理
+func (ctx *httpContext) OnHttpRequestBody(bodySize int, endOfStream bool) types.Action {
+ proxywasm.LogInfof("OnHttpRequestBody()")
+ ctx.requestBodySize += bodySize
+ if !endOfStream {
+ // Wait until we see the entire body to replace.
+ return types.ActionPause
+ }
+ _, err := proxywasm.GetHttpRequestBody(0, ctx.requestBodySize)
+ if err != nil {
+ proxywasm.LogErrorf("failed to get request body: %v", err)
+ return types.ActionContinue
+ }
+
+ return types.ActionContinue
+}
+
+// http 响应头回调
+func (ctx *httpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
+ proxywasm.LogInfof("OnHttpResponseHeaders()")
+ // 这里响应头回调
+ return types.ActionContinue
+}
+
+// http 响应体回调, 注意这里流式处理
+func (ctx *httpContext) OnHttpResponseBody(bodySize int, endOfStream bool) types.Action {
+ proxywasm.LogInfof("OnHttpResponseBody()")
+ ctx.responseBodySize += bodySize
+ // 判断是否响应体结束
+ if !endOfStream {
+ // Wait until we see the entire body to replace.
+ return types.ActionPause
+ }
+ _, err := proxywasm.GetHttpResponseBody(0, ctx.responseBodySize)
+ if err != nil {
+ proxywasm.LogErrorf("failed to get response body: %v", err)
+ return types.ActionContinue
+ }
+ return types.ActionContinue
+}
+```
+核心步骤如下:
+- 入口注册 vmContext
+- VM 启动回调时候解析 VM 配置
+- 由 vmContext 生成 pluginContext
+- 插件启动回调时候解析插件配置
+- 对于每个 http 流,pluginContext 生成 httpContext
+- 生成的 httpContext 处理请求头、请求体、响应头、响应体,这里要注意的是处理 OnHttpRequestBody 和 OnHttpResponseBody 回调是流式处理
+
+可以通过 [开发样例](https://github.com/higress-group/proxy-wasm-go-sdk/tree/main/examples) 查看更多 Proxy-Wasm Go SDK 插件开发样例。
+
+## 参考
+- [1] [proxy-wasm-go-sdk doc](https://github.com/higress-group/proxy-wasm-go-sdk/blob/main/doc/OVERVIEW.md)
+- [2] [proxy-wasm-go-sdk example](https://github.com/higress-group/proxy-wasm-go-sdk/tree/main/examples)
+- [3] [WebAssembly 解释器实现篇](https://github.com/mcuking/blog/issues/96/)
+- [4] [理解 WebAssembly 文本格式](https://developer.mozilla.org/zh-CN/docs/WebAssembly/Understanding_the_text_format)
+- [5] [Wasm Module Binary Format](https://webassembly.github.io/spec/core/binary/modules.html)
+- [6] [WebAssembly 究竟是什么?](https://www.bilibili.com/video/BV1WK42117dW)
+- [7] [WebAssembly 在 MOSN 中的实践 - 基础框架篇](https://mosn.io/blog/posts/mosn-wasm-framework/)
\ No newline at end of file
diff --git a/src/content/docs/ebook/zh-cn/wasm16.md b/src/content/docs/ebook/zh-cn/wasm16.md
new file mode 100644
index 0000000000..0e154e326f
--- /dev/null
+++ b/src/content/docs/ebook/zh-cn/wasm16.md
@@ -0,0 +1,651 @@
+---
+title: Higress 插件 Go SDK 与处理流程
+keywords: [Higress]
+---
+
+# Higress 插件 Go SDK 与处理流程
+
+本章开始介绍详细 Higress 插件开发 SDK 、插件开发流程和插件开发注意事项。
+
+## 1 Higress 插件 Go SDK
+
+Higress 插件 Go SDK 在 proxy-wasm-go-sdk 上封装了一层,简化插件开发和增强功能。其代码位置:https://github.com/alibaba/higress/tree/main/plugins/wasm-go/pkg ,代码文件结构如下:
+
+```shell
+tree
+.
+├── matcher
+│ ├── rule_matcher.go
+│ ├── rule_matcher_test.go
+│ └── utils.go
+└── wrapper
+ ├── cluster_wrapper.go
+ ├── cluster_wrapper_test.go
+ ├── http_wrapper.go
+ ├── log_wrapper.go
+ ├── plugin_wrapper.go
+ ├── redis_wrapper.go
+ └── request_wrapper.go
+ └── response_wrapper.go
+```
+Higress 插件 Go SDK 主要增强功能如下:
+- matcher 包提供全局、路由、域名级别配置的解析功能。
+- wrapper 包下 log_wrapper.go 封装和简化插件日志的输出功能。
+- wrapper 包下 cluster_wrapper.go、redis_wrapper.go、http_wrapper.go 封装 Http 和 Redis Host Function Call。
+- wrapper 包下 plugin_wrapper.go 封装 proxy-wasm-go-sdk 的 VMContext、PluginContext、HttpContext、插件配置解析功能。
+- wrapper 包下 request_wrapper.go、response_wrapper.go 提供关于请求和响应公共方法。
+
+本章主要集中介绍 plugin_wrapper.go 提供 VMContext、PluginContext、HttpContext、插件配置解析功能。
+
+## 2 Higress 插件 Go SDK 开发
+
+相对应于 proxy-wasm-go-sdk 中的 VMContext、PluginContext、HttpContext 3 个上下文, 在 Higress 插件 Go SDK 中是 CommonVmCtx、CommonPluginCtx、CommonHttpCtx 3 个支持泛型的 struct。 3 个 struct 的核心内容如下:
+
+```golang
+type CommonVmCtx[PluginConfig any] struct {
+ // proxy-wasm-go-sdk DefaultVMContext 默认实现
+ types.DefaultVMContext
+ // 插件名称
+ pluginName string
+ // 插件日志工具
+ log Log
+ hasCustomConfig bool
+ // 插件配置解析函数
+ parseConfig ParseConfigFunc[PluginConfig]
+ // 插件路由、域名、服务级别配置解析函数
+ parseRuleConfig ParseRuleConfigFunc[PluginConfig]
+ // 以下是自定义插件回调钩子函数
+ onHttpRequestHeaders onHttpHeadersFunc[PluginConfig]
+ onHttpRequestBody onHttpBodyFunc[PluginConfig]
+ onHttpStreamingRequestBody onHttpStreamingBodyFunc[PluginConfig]
+ onHttpResponseHeaders onHttpHeadersFunc[PluginConfig]
+ onHttpResponseBody onHttpBodyFunc[PluginConfig]
+ onHttpStreamingResponseBody onHttpStreamingBodyFunc[PluginConfig]
+ onHttpStreamDone onHttpStreamDoneFunc[PluginConfig]
+}
+
+type CommonPluginCtx[PluginConfig any] struct {
+ // proxy-wasm-go-sdk DefaultPluginContext 默认实现
+ types.DefaultPluginContext
+ // 解析后保存路由、域名、服务级别配置和全局插件配置
+ matcher.RuleMatcher[PluginConfig]
+ // 引用 CommonVmCtx
+ vm *CommonVmCtx[PluginConfig]
+ // tickFunc 数组
+ onTickFuncs []TickFuncEntry
+}
+
+type CommonHttpCtx[PluginConfig any] struct {
+ // proxy-wasm-go-sdk DefaultHttpContext 默认实现
+ types.DefaultHttpContext
+ // 引用 CommonPluginCtx
+ plugin *CommonPluginCtx[PluginConfig]
+ // 当前 Http 上下文下匹配插件配置,可能是路由、域名、服务级别配置或者全局配置
+ config *PluginConfig
+ // 是否处理请求体
+ needRequestBody bool
+ // 是否处理响应体
+ needResponseBody bool
+ // 是否处理流式请求体
+ streamingRequestBody bool
+ // 是否处理流式响应体
+ streamingResponseBody bool
+ // 非流式处理缓存请求体大小
+ requestBodySize int
+ // 非流式处理缓存响应体大小
+ responseBodySize int
+ // Http 上下文 ID
+ contextID uint32
+ // 自定义插件设置自定义插件上下文
+ userContext map[string]interface{}
+}
+```
+
+它们的关系如下图:
+![img](https://img.alicdn.com/imgextra/i3/O1CN01nkPR171qsrwfUK0WW_!!6000000005552-2-tps-1640-600.png)
+
+### 2.1 启动入口和 VM 上下文(CommonVmCtx)
+
+```golang
+func main() {
+ wrapper.SetCtx(
+ // 插件名称
+ "hello-world",
+ // 设置自定义函数解析插件配置,这个方法适合插件全局配置和路由、域名、服务级别配置内容规则是一样
+ wrapper.ParseConfigBy(parseConfig),
+ // 设置自定义函数解析插件全局配置和路由、域名、服务级别配置,这个方法适合插件全局配置和路由、域名、服务级别配置内容规则不一样
+ wrapper.ParseOverrideConfigBy(parseConfig, parseRuleConfig)
+ // 设置自定义函数处理请求头
+ wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
+ // 设置自定义函数处理请求体
+ wrapper.ProcessRequestBodyBy(onHttpRequestBody),
+ // 设置自定义函数处理响应头
+ wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
+ // 设置自定义函数处理响应体
+ wrapper.ProcessResponseBodyBy(onHttpResponseBody),
+ // 设置自定义函数处理流式请求体
+ wrapper.ProcessStreamingRequestBodyBy(onHttpStreamingRequestBody),
+ // 设置自定义函数处理流式响应体
+ wrapper.ProcessStreamingResponseBodyBy(onHttpStreamingResponseBody),
+ // 设置自定义函数处理流式请求完成
+ wrappper.ProcessStreamDoneBy(onHttpStreamDone)
+ )
+}
+```
+> 可以根据实际业务需要来选择设置回调钩子函数。
+
+跟踪一下 wrapper.SetCtx 的实现:
+- 创建 CommonVmCtx 对象同时设置自定义插件回调钩子函数。
+- 然后再调用 proxywasm.SetVMContext 设置 VMContext。
+
+```golang
+func SetCtx[PluginConfig any](pluginName string, setFuncs ...SetPluginFunc[PluginConfig]) {
+ // 调用 proxywasm.SetVMContext 设置 VMContext
+ proxywasm.SetVMContext(NewCommonVmCtx(pluginName, setFuncs...))
+}
+
+func NewCommonVmCtx[PluginConfig any](pluginName string, setFuncs ...SetPluginFunc[PluginConfig]) *CommonVmCtx[PluginConfig] {
+ ctx := &CommonVmCtx[PluginConfig]{
+ pluginName: pluginName,
+ log: Log{pluginName},
+ hasCustomConfig: true,
+ }
+ // CommonVmCtx 里设置自定义插件回调钩子函数
+ for _, set := range setFuncs {
+ set(ctx)
+ }
+ ...
+ return ctx
+```
+
+### 2.2 插件上下文(CommonPluginCtx)
+
+#### 2.2.1 创建 CommonPluginCtx 对象
+通过 CommonVmCtx 的 NewPluginContext 方法创建 CommonPluginCtx 对象, 设置 CommonPluginCtx 的 vm 引用。
+```golang
+func (ctx *CommonVmCtx[PluginConfig]) NewPluginContext(uint32) types.PluginContext {
+ return &CommonPluginCtx[PluginConfig]{
+ vm: ctx,
+ }
+}
+```
+
+#### 2.2.2 插件启动和插件配置解析
+
+CommonPluginCtx 的 OnPluginStart 部分核心代码如下:
+```golang
+func (ctx *CommonPluginCtx[PluginConfig]) OnPluginStart(int) types.OnPluginStartStatus {
+ // 调用 proxywasm.GetPluginConfiguration 获取插件配置
+ data, err := proxywasm.GetPluginConfiguration()
+ globalOnTickFuncs = nil
+ ...
+ var jsonData gjson.Result
+ // 插件配置转成 json
+ jsonData = gjson.ParseBytes(data)
+
+ // 设置 parseOverrideConfig
+ var parseOverrideConfig func(gjson.Result, PluginConfig, *PluginConfig) error
+ if ctx.vm.parseRuleConfig != nil {
+ parseOverrideConfig = func(js gjson.Result, global PluginConfig, cfg *PluginConfig) error {
+ // 解析插件路由、域名、服务级别插件配置
+ return ctx.vm.parseRuleConfig(js, global, cfg, ctx.vm.log)
+ }
+ }
+ ...
+ // 解析插件配置
+ err = ctx.ParseRuleConfig(jsonData,
+ func(js gjson.Result, cfg *PluginConfig) error {
+ // 解析插件全局或者当 parseRuleConfig 没有设置时候同时解析路由、域名、服务级别插件配置
+ return ctx.vm.parseConfig(js, cfg, ctx.vm.log)
+ },
+ parseOverrideConfig,
+ )
+ ...
+ if globalOnTickFuncs != nil {
+ ctx.onTickFuncs = globalOnTickFuncs
+ ...
+ }
+ return types.OnPluginStartStatusOK
+}
+```
+
+可以发现在解析插件配置过程中有两个回调钩子函数,parseConfig 和 parseRuleConfig。
+- parseConfig :解析插件全局配置,如果 parseRuleConfig 没有设置,那么 parseConfig 会同时解析全局配置和路由、域名、服务级别配置。也就是说插件全局配置和路由、域名、服务级别配置规则是一样。
+- parseRuleConfig: 解析路由、域名、服务级别插件配置。如果设置 parseRuleConfig,也就是说插件全局配置和路由、域名、服务级别配置规则是不同的。
+
+
+> 这里我们不进一步分析插件解析过程,后续在插件生效原理章节从控制面和数据面详细分析插件全局、路由、域名、服务级别生效原理。
+
+大部分情况下插件全局配置和路由、域名、服务级别配置规则是一样的,因此在定义插件时只需要调用 wrapper.ParseConfigBy(parseConfig) 来设置插件配置解析回调钩子函数。
+而有些插件(如 [basic-auth](https://higress.io/docs/latest/plugins/authentication/basic-auth/))的全局配置和路由、域名、服务级别配置规则是不一样的。baisc-auth 插件配置 YAML 样例如下:
+```yaml
+apiVersion: extensions.higress.io/v1alpha1
+kind: WasmPlugin
+metadata:
+ name: cpp-basic-auth
+ namespace: higress-system
+spec:
+ defaultConfig:
+ consumers:
+ - credential: admin:123456
+ name: consumer1
+ - credential: guest:abc
+ name: consumer2
+ global_auth: false
+ defaultConfigDisable: false
+ matchRules:
+ - config:
+ allow:
+ - consumer1
+ configDisable: false
+ ingress:
+ - higress-conformance-infra/wasmplugin-cpp-basic-auth
+ url: file:///opt/plugins/wasm-cpp/extensions/basic-auth/plugin.wasm
+```
+
+可以看出 matchRule 下 config 配置内容和 defaultConfig 配置内容不一样。所以在开发插件的时候,需要同时设置 parseConfig 和 parseRuleConfig 两个回调钩子函数。
+baisc-auth 部分核心代码如下:
+```golang
+func main() {
+ wrapper.SetCtx(
+ "basic-auth",
+ // 要同时设置 parseConfig 和 parseRuleConfig 回调钩子函数
+ // ParseOverrideConfigBy 函数的第一个参数接收 parseConfig 回调钩子函数,第二个参数接收 parseRuleConfig 回调钩子函数
+ wrapper.ParseOverrideConfigBy(parseGlobalConfig, parseOverrideRuleConfig),
+ wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
+ )
+}
+
+// 自定义插件配置
+type BasicAuthConfig struct {
+ // 插件全局配置内容
+ globalAuth *bool `yaml:"global_auth"`
+ consumers []Consumer `yaml:"consumers"`
+ ...
+ // 插件路由、域名、服务级别配置内容
+ allow []string `yaml:"allow"`
+}
+
+type Consumer struct {
+ name string `yaml:"name"`
+ credential string `yaml:"credential"`
+}
+
+// 解析插件全局配置回调钩子函数
+func parseGlobalConfig(json gjson.Result, global *BasicAuthConfig, log wrapper.Log) error {
+ log.Debug("global config")
+ // 解析插件全局配置
+ ...
+ consumers := json.Get("consumers")
+ ...
+ globalAuth := json.Get("global_auth")
+ ...
+ return nil
+}
+
+// 解析插件路由、域名、服务级别配置回调钩子函数
+func parseOverrideRuleConfig(json gjson.Result, global BasicAuthConfig, config *BasicAuthConfig, log wrapper.Log) error {
+ log.Debug("domain/route config")
+ // 这里要注意要用全局配置内容复制到路由、域名、服务级别配置中,这样后续在 HttpContext 中可以获取当前 Http 请求下插件配置包括全局配置和路由、域名、服务级别配置
+ *config = global
+ // 解析插件路由、域名、服务级别配置
+ allow := json.Get("allow")
+ ...
+ for _, item := range allow.Array() {
+ config.allow = append(config.allow, item.String())
+ }
+ ...
+ return nil
+}
+```
+开发这种类型插件需要注意:
+- 自定义插件配置 struct 要包含全局配置内容和路由、域名、服务级别配置内容。
+- wrapper.ParseOverrideConfigBy 要同时设置 parseConfig 和 parseRuleConfig 回调钩子函数。
+- 在 parseRuleConfig 回调钩子函数处理中,全局配置内容要复制到路由、域名、服务级别配置中,这样后续在 HttpContext 中可以获取当前 Http 请求下插件配置包括全局配置和路由、域名、服务级别配置内容。
+
+### 2.3 HTTP 上下文(CommonHttpCtx)
+
+#### 2.3.1 创建 CommonHttpCtx
+
+CommonPluginCtx 的 NewHttpContext 部分核心代码如下:
+
+```golang
+func (ctx *CommonPluginCtx[PluginConfig]) NewHttpContext(contextID uint32) types.HttpContext {
+ httpCtx := &CommonHttpCtx[PluginConfig]{
+ plugin: ctx,
+ contextID: contextID,
+ userContext: map[string]interface{}{},
+ }
+ // 根据插件配置判断设置是否需要处理请求和响应的 body
+ if ctx.vm.onHttpRequestBody != nil || ctx.vm.onHttpStreamingRequestBody != nil {
+ httpCtx.needRequestBody = true
+ }
+ if ctx.vm.onHttpResponseBody != nil || ctx.vm.onHttpStreamingResponseBody != nil {
+ httpCtx.needResponseBody = true
+ }
+ if ctx.vm.onHttpStreamingRequestBody != nil {
+ httpCtx.streamingRequestBody = true
+ }
+ if ctx.vm.onHttpStreamingResponseBody != nil {
+ httpCtx.streamingResponseBody = true
+ }
+
+ return httpCtx
+}
+```
+#### 2.3.2 OnHttpRequestHeaders
+
+OnHttpRequestHeaders 核心代码如下:
+```golang
+func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
+ // 获取当前 HTTP 请求生效插件配置
+ config, err := ctx.plugin.GetMatchConfig()
+ ...
+ // 设置插件配置到 HttpContext
+ ctx.config = config
+ // 如果请求 content-type 是 octet-stream/grpc 或者定义 content-encoding,则不处理请求 body
+ if IsBinaryRequestBody() {
+ ctx.needRequestBody = false
+ }
+ ...
+ // 调用自定义插件 onHttpRequestHeaders 回调钩子函数
+ return ctx.plugin.vm.onHttpRequestHeaders(ctx, *config, ctx.plugin.vm.log)
+}
+```
+主要处理逻辑如下:
+- 获取匹配当前 HTTP 请求插件配置,可能是路由、域名、服务级别配置或者全局配置。
+- 设置插件配置到 HttpContext。
+- 如果请求 content-type 是 octet-stream/grpc 或者定义 content-encoding,则不处理请求 body。
+- 调用自定义插件 onHttpRequestHeaders 回调钩子函数。
+
+关于插件配置可以看出, Higress 插件 Go SDK 封装如下:
+- 在插件启动时候,解析插件路由、域名、服务级别插件配置和全局配置保存到 CommonPluginCtx 中。
+- 在 onHttpRequestHeaders 阶段,根据当前 HTTP 上下文中路由、域名、服务等信息匹配插件配置,返回路由、域名、服务级别配置或者全局配置。然后把匹配到插件配置设置到 HttpContext 对象的 config 属性中,这样自定义插件的所有回调钩子函数就可以获取到这个配置。
+
+#### 2.3.3 OnHttpRequestBody
+
+OnHttpRequestBody 核心代码如下:
+```golang
+func (ctx *CommonHttpCtx[PluginConfig]) OnHttpRequestBody(bodySize int, endOfStream bool) types.Action {
+ ...
+ // 如果不需要处理请求 body,则直接返回,继续后续处理
+ if !ctx.needRequestBody {
+ return types.ActionContinue
+ }
+ // 先判断是否要需要进行流式处理,如果需要则调用自定义插件 onHttpStreamingRequestBody 回调钩子函数
+ if ctx.plugin.vm.onHttpStreamingRequestBody != nil && ctx.streamingRequestBody {
+ chunk, _ := proxywasm.GetHttpRequestBody(0, bodySize)
+ // 调用自定义插件 onHttpStreamingRequestBody 回调钩子函数
+ modifiedChunk := ctx.plugin.vm.onHttpStreamingRequestBody(ctx, *ctx.config, chunk, endOfStream, ctx.plugin.vm.log)
+ err := proxywasm.ReplaceHttpRequestBody(modifiedChunk)
+ ...
+ return types.ActionContinue
+ }
+ // 再判断是否要需要进行非流式处理,需要缓存请求 body,等读取整个请求 body 后调用自定义插件 onHttpRequestBody 回调钩子函数
+ if ctx.plugin.vm.onHttpRequestBody != nil {
+ ctx.requestBodySize += bodySize
+ if !endOfStream {
+ return types.ActionPause
+ }
+ body, err := proxywasm.GetHttpRequestBody(0, ctx.requestBodySize)
+ ...
+ // 调用自定义插件 onHttpRequestBody 回调钩子函数
+ return ctx.plugin.vm.onHttpRequestBody(ctx, *ctx.config, body, ctx.plugin.vm.log)
+ }
+ return types.ActionContinue
+}
+```
+
+核心逻辑如下:
+- 如果 ctx.needRequestBody 为 false 时,则直接返回,继续后续处理。
+- 当 ctx.streamingRequestBody 为 true 时,同时自定义插件有 onHttpStreamingRequestBody 回调钩子函数,则调用自定义插件 onHttpStreamingRequestBody 回调钩子函数。
+- 当自定义插件有 onHttpRequestBody 回调钩子函数,需要缓存请求 body,等读取整个请求 body 后调用自定义插件 onHttpRequestBody 回调钩子函数。
+
+#### 2.3.4 OnHttpResponseHeaders
+
+OnHttpResponseHeaders 核心代码如下:
+```golang
+func (ctx *CommonHttpCtx[PluginConfig]) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action {
+ ...
+ // To avoid unexpected operations, plugins do not read the binary content body
+ if IsBinaryResponseBody() {
+ ctx.needResponseBody = false
+ }
+ ...
+ return ctx.plugin.vm.onHttpResponseHeaders(ctx, *ctx.config, ctx.plugin.vm.log)
+}
+
+```
+主要处理逻辑如下:
+- 如果响应 content-type 是 octet-stream/grpc 或者定义 content-encoding,则不处理响应 body。
+- 调用自定义插件 onHttpResponseHeaders 回调钩子函数。
+
+#### 2.3.5 OnHttpResponseBody
+
+OnHttpResponseBody 核心代码如下:
+```golang
+func (ctx *CommonHttpCtx[PluginConfig]) OnHttpResponseBody(bodySize int, endOfStream bool) types.Action {
+ ...
+ // 如果不需要处理响应 body,则直接返回,继续后续处理
+ if !ctx.needResponseBody {
+ return types.ActionContinue
+ }
+ // 先判断是否要需要进行流式处理,如果需要则调用自定义插件 onHttpStreamingResponseBod 回调钩子函数
+ if ctx.plugin.vm.onHttpStreamingResponseBody != nil && ctx.streamingResponseBody {
+ chunk, _ := proxywasm.GetHttpResponseBody(0, bodySize)
+ // 调用自定义插件 onHttpStreamingResponseBody 回调钩子函数
+ modifiedChunk := ctx.plugin.vm.onHttpStreamingResponseBody(ctx, *ctx.config, chunk, endOfStream, ctx.plugin.vm.log)
+ ...
+ return types.ActionContinue
+ }
+ // 再判断是否要需要进行非流式处理,需要缓存响应 body,等读取整个响应 body 后调用自定义插件 onHttpResponseBody 回调钩子函数
+ if ctx.plugin.vm.onHttpResponseBody != nil {
+ ctx.responseBodySize += bodySize
+ if !endOfStream {
+ return types.ActionPause
+ }
+ body, err := proxywasm.GetHttpResponseBody(0, ctx.responseBodySize)
+ ...
+ // 调用自定义插件 onHttpResponseBody 回调钩子函数
+ return ctx.plugin.vm.onHttpResponseBody(ctx, *ctx.config, body, ctx.plugin.vm.log)
+ }
+ return types.ActionContinue
+}
+```
+
+核心逻辑如下:
+- 当 ctx.needResponseBody 为 false 时,则直接返回,继续后续处理。
+- 当 ctx.streamingResponseBody 为 true 时,同时自定义插件有 onHttpStreamingResponseBody 回调钩子函数,则调用自定义插件 onHttpStreamingResponseBody 回调钩子函数。
+- 当自定义插件有 onHttpResponseBody 回调钩子函数,需要缓存响应 body,等读取整个响应 body 后调用自定义插件 onHttpResponseBody 回调钩子函数。
+
+#### 2.3.6 OnHttpStreamDone
+
+OnHttpStreamDone 核心代码如下:
+```golang
+func (ctx *CommonHttpCtx[PluginConfig]) OnHttpStreamDone() {
+ ...
+ ctx.plugin.vm.onHttpStreamDone(ctx, *ctx.config, ctx.plugin.vm.log)
+}
+```
+
+OnHttpStreamDone 比较简单,自定义插件有 onHttpStreamDone 回调钩子函数,则调用自定义插件 onHttpStreamDone 回调钩子函数。
+
+#### 2.3.7 CommonHttpCtx 方法
+
+CommonHttpCtx 提供以下方法,自定义插件可以调用,其代码和注释如下:
+```golang
+// 设置自定义上下文,这个上下文可以在自定义插件所有回调钩子函数中可以获取
+func (ctx *CommonHttpCtx[PluginConfig]) SetContext(key string, value interface{}) {
+ ctx.userContext[key] = value
+}
+// 获取自定义上下文,这个上下文可以在自定义插件所有回调钩子函数中可以获取
+func (ctx *CommonHttpCtx[PluginConfig]) GetContext(key string) interface{} {
+ return ctx.userContext[key]
+}
+// 获取 bool 类型自定义上下文
+func (ctx *CommonHttpCtx[PluginConfig]) GetBoolContext(key string, defaultValue bool) bool {
+ if b, ok := ctx.userContext[key].(bool); ok {
+ return b
+ }
+ return defaultValue
+}
+// 获取 string 类型自定义上下文
+func (ctx *CommonHttpCtx[PluginConfig]) GetStringContext(key, defaultValue string) string {
+ if s, ok := ctx.userContext[key].(string); ok {
+ return s
+ }
+ return defaultValue
+}
+// 获取请求 scheme
+func (ctx *CommonHttpCtx[PluginConfig]) Scheme() string {
+ proxywasm.SetEffectiveContext(ctx.contextID)
+ return GetRequestScheme()
+}
+// 获取请求 host
+func (ctx *CommonHttpCtx[PluginConfig]) Host() string {
+ proxywasm.SetEffectiveContext(ctx.contextID)
+ return GetRequestHost()
+}
+// 获取请求 path
+func (ctx *CommonHttpCtx[PluginConfig]) Path() string {
+ proxywasm.SetEffectiveContext(ctx.contextID)
+ return GetRequestPath()
+}
+// 获取请求 method
+func (ctx *CommonHttpCtx[PluginConfig]) Method() string {
+ proxywasm.SetEffectiveContext(ctx.contextID)
+ return GetRequestMethod()
+}
+
+// 调用这个方法可以禁止读取请求 body 和处理。
+// 比如在 onHttpRequestHeaders 回调钩子函数中根据某些条件时调用,可以跳过后续的请求 body 读取和处理
+func (ctx *CommonHttpCtx[PluginConfig]) DontReadRequestBody() {
+ ctx.needRequestBody = false
+}
+
+// 调用这个方法禁止读取响应 body,
+// 比如在 onHttpResponseHeaders 回调钩子函数中根据某些条件时调用,可以跳过后续的响应 body 读取和处理
+func (ctx *CommonHttpCtx[PluginConfig]) DontReadResponseBody() {
+ ctx.needResponseBody = false
+}
+// 调用这个方法禁止请求流式处理
+// 比如在 onHttpRequestHeaders 回调钩子函数中根据某些条件时调用,跳过后续的 onHttpStreamingRequestBody 流式处理, 转成 onHttpRequestBody 处理
+func (ctx *CommonHttpCtx[PluginConfig]) BufferRequestBody() {
+ ctx.streamingRequestBody = false
+}
+
+// 调用这个方法禁止响应流式处理
+// 比如在 onHttpResponseHeaders 回调钩子函数中根据某些条件时调用,跳过后续的 onHttpStreamingResponseBody 流式处理, 转成 onHttpResponseBody 处理
+func (ctx *CommonHttpCtx[PluginConfig]) BufferResponseBody() {
+ ctx.streamingResponseBody = false
+}
+
+// 调用这个方法可以禁止重新计算路由,Envoy 默认在 Http 头发生变更时会重新计算路由,调用这个方法,可以禁止重新计算路由。
+// 比如在 onHttpRequestHeaders 回调钩子函数中根据某些条件时调用这个方法,可以禁止重新计算路由。
+// 关于 DisableReroute 使用场景可以参考 Higress 官方提供 [ai-proxy 插件](https://github.com/alibaba/higress/blob/main/plugins/wasm-go/extensions/ai-proxy/main.go#L76)
+func (ctx *CommonHttpCtx[PluginConfig]) DisableReroute() {
+ _ = proxywasm.SetProperty([]string{"clear_route_cache"}, []byte("off"))
+}
+// 设置请求 body buffer limit
+// 关于 DisableReroute 使用场景可以参考 Higress 官方提供 [ai-proxy 插件](https://github.com/alibaba/higress/blob/main/plugins/wasm-go/extensions/ai-proxy/main.go#L81)
+func (ctx *CommonHttpCtx[PluginConfig]) SetRequestBodyBufferLimit(size uint32) {
+ ctx.plugin.vm.log.Infof("SetRequestBodyBufferLimit: %d", size)
+ _ = proxywasm.SetProperty([]string{"set_decoder_buffer_limit"}, []byte(strconv.Itoa(int(size))))
+}
+// 设置响应 body buffer limit
+func (ctx *CommonHttpCtx[PluginConfig]) SetResponseBodyBufferLimit(size uint32) {
+ ctx.plugin.vm.log.Infof("SetResponseBodyBufferLimit: %d", size)
+ _ = proxywasm.SetProperty([]string{"set_encoder_buffer_limit"}, []byte(strconv.Itoa(int(size))))
+}
+```
+
+核心内容如下:
+- 在 onHttpRequestHeaders 阶段:
+ - 调用 DontReadRequestBody 方法,可以跳过读取请求 body 和处理。
+ - 调用 BufferRequestBody 方法,可以跳过请求 onHttpStreamingRequestBody 流式处理,转成 onHttpRequestBody 处理。
+ - 调用 DisableReroute 方法,可以禁止重新计算路由。
+- 在 onHttpResponseHeaders 阶段:
+ - 调用 DontReadResponseBody 方法,可以跳过读取响应 body 和处理。
+ - 调用 BufferResponseBody 方法,可以跳过响应 onHttpStreamingResponseBody 流式处理,转成 onHttpResponseBody 处理。
+- SetContext 和 GetContext 方法,可以设置和获取自定义上下文,在自定义插件所有回调钩子函数中可以使用。
+- SetRequestBodyBufferLimit 和 SetResponseBodyBufferLimit 方法,可以设置请求 body buffer limit 和响应 body buffer limit。
+
+### 2.4 Types.Action
+
+在自定义插件中 onHttpRequestHeaders、onHttpRequestBody、onHttpResponseHeaders、onHttpResponseBody 返回值类型为 types.Action。通过 types.Action 枚举值来控制插件的运行流程,常见的返回值如下:
+
+1. types.ActionContinue:继续后续处理,比如继续读取请求 body,或者继续读取响应 body。
+
+2. types.ActionPause: 暂停后续处理,比如在 onHttpRequestHeaders 通过 Http 或者 Redis 调用外部服务获取认证信息,在调用外部服务回调钩子函数中调用 proxywasm.ResumeHttpRequest() 来恢复后续处理 或者调用 proxywasm.SendHttpResponseWithDetail() 来返回响应。
+
+
+#### 2.4.1 编译插件注意事项
+
+1. Higress 需要编译时启用 `EXTRA_TAGS=proxy_wasm_version_0_2_100` 标签来修改 Proxy Wasm ABI。 TinyGo 本地构建命令如下:
+```shell
+tinygo build -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer proxy_wasm_version_0_2_100' -o ./build/plugin.wasm main.go
+```
+2. Makefile 文件默认启用 `proxy_wasm_version_0_2_100` 标签,所以不需要修改 Makefile 文件。
+
+#### 2.4.2 Header 状态码
+
+1. HeaderContinue:
+
+表示当前 filter 已经处理完毕,可以继续交给下一个 filter 处理。 types.ActionContinue 对应就是这个状态。
+
+2. HeaderStopIteration:
+
+表示 header 还不能继续交给下一个 filter 来处理。 但是并不停止从连接读数据,继续触发 body data 的处理。 这样可以在 body data 处理阶段可以更新 Http 请求头内容。 如果 body data 要交给下一个 filter 处理, 这时 header 是也会被一起交给下一个 filter 处理。
+
+3. HeaderContinueAndEndStream:
+
+表示 header 可以继续交给下一个 filter 处理,但是下一个 filter 收到的 end_stream = false,也就是标记请求还未结束。以便当前 filter 再增加 body。
+
+4. HeaderStopAllIterationAndBuffer:
+
+停止所有迭代,表示 header 不能继续交给下一个 filter,并且当前 filter 也不能收到 body data。 并对当前过滤器及后续过滤器的头部、数据和尾部进行缓冲。如果缓存大小超过了 buffer limit,在请求阶段就直接返回 413,响应阶段就直接返回 500。
+同时需要调用 proxywasm.ResumeHttpRequest()、 proxywasm.ResumeHttpResponse() 或者 proxywasm.SendHttpResponseWithDetail() 函数来恢复后续处理。
+
+5. HeaderStopAllIterationAndWatermark:
+
+同上,区别是,当缓存超过了 buffer limit 会触发流控,也就是暂停从连接上读数据。 types.ActionPause 实际上对应就是这个状态。
+
+> 关于 types.HeaderStopIteration 和 HeaderStopAllIterationAndWatermark 的使用场景可以参考 Higress 官方提供 [ai-transformer 插件](https://github.com/alibaba/higress/blob/main/plugins/wasm-go/extensions/ai-transformer/main.go#L93-L99) 和 [ai-quota 插件](https://github.com/alibaba/higress/blob/main/plugins/wasm-go/extensions/ai-quota/main.go#L179) 。
+
+#### 2.4.3 Data 状态码
+
+1. DataContinue:
+
+和 header 类似,表示当前 filter 已经处理完毕,可以继续交给下一个 filter 处理。 如果 header 之前返回的是 HeaderStopIteration,且尚未交给下一个 filter 处理,那么此时 header 和 data 也会被交给下一个 filter 处理。types.ActionContinue 对应就是这个状态。
+
+2. DataStopIterationAndBuffer:
+
+表示当前 data 不能继续交给下一个 filter 处理,并且将当前 data 缓存起来。 与 header 类似,如果达到 buffer limit,在请求阶段就直接返回 413,响应阶段就直接返回 500。
+同时需要调用 proxywasm.ResumeHttpRequest()、 proxywasm.ResumeHttpResponse() 或者 proxywasm.SendHttpResponseWithDetail() 函数来恢复后续处理。
+
+3. DataStopIterationAndWatermark:
+
+同上,只是达到 buffer limit 会触发流控。types.ActionPause 实际上对应就是这个状态。
+
+4. DataStopIterationNoBuffer:
+
+表示当前 data 不能继续交给下一个 filter,但是不缓存当前 data 。
+
+
+### 2.5 Envoy 请求缓存区限制
+
+当自定义插件使用 `onHttpRequestBody` 非流式传输,当请求超过 `downstream` 缓存区限制(默认是 32k)。Envoy 会给用户返回 413, 同时报 `request_payload_too_large` 错误。
+比如在 AI 长上下文中场景中可能会碰到这个问题,这个问题可以通过参考 [全局配置说明](https://higress.io/docs/latest/user/configmap/) 调整 Downstream 配置项 `connectionBufferLimits` 解决, 或者 使用 `SetRequestBodyBufferLimit` 方法设置请求 body buffer limit 解决。 关于如何使用 `SetRequestBodyBufferLimit` 可以参考 Higress 官方提供 [ai-proxy 插件](https://github.com/alibaba/higress/blob/main/plugins/wasm-go/extensions/ai-proxy/main.go#L81) 的实现。
+
+
+## 3 Envoy 属性(Attributes)
+
+属性是 Envoy 的一个特性,允许用户在插件中设置和获取这些属性,可以通过 `proxywasm.GetProperty` 和 `proxywasm.SetProperty` 方法获取和设置。
+Envoy 预定义属性包括请求属性、响应属性、连接属性、Upstream 属性、Wasm 属性、和 Metadata 等属性, 具体可以参考 [Envoy 属性](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes)。
+同时用户也可以设置自定义属性,这些属性可以在插件链中不同插件共享。
+
+
+## 参考
+- [1] [Envoy 开发入门:搞懂 http filter 状态码](https://uncledou.site/2022/envoy-filter-status/)
+
+
diff --git a/src/content/docs/ebook/zh-cn/wasm17.md b/src/content/docs/ebook/zh-cn/wasm17.md
new file mode 100644
index 0000000000..b8ff87ed12
--- /dev/null
+++ b/src/content/docs/ebook/zh-cn/wasm17.md
@@ -0,0 +1,633 @@
+---
+title: HTTP 调用
+keywords: [Higress]
+---
+
+# HTTP 调用
+
+这章主要介绍如何使用 Higress 插件 Go SDK 实现 HTTP 调用。
+
+## 1 Envoy 集群(Cluster)名称和服务发现来源
+
+Higress 插件的 Go SDK 在进行 HTTP 和 Redis 调用时,是通过指定的集群名称来识别并连接到相应的 Envoy 集群。 此外,Higress 利用 [McpBridge](https://higress.io/docs/latest/user/mcp-bridge/) 支持多种服务发现机制,包括静态配置(static)、DNS、Kubernetes 服务、Eureka、Consul、Nacos、以及 Zookeeper 等。
+每种服务发现机制对应的集群名称生成规则都有所不同,这些规则在 cluster_wrapper.go 代码文件中有所体现。
+为了包装不同的服务发现机制,Higress 插件 Go SDK 定义了 Cluster 接口,该接口包含两个方法:ClusterName 和 HostName。
+```golang
+type Cluster interface {
+ // 返回 Envoy 集群名称
+ ClusterName() string
+ // 返回 Hostname, 在 HTTP 调用服务时候,用于设置 Http host 请求头
+ HostName() string
+}
+```
+
+### 1.1 静态配置(static)
+```golang
+type StaticIpCluster struct {
+ ServiceName string
+ Port int64
+ Host string
+}
+```
+- 集群名称规则为:`outbound|||.static`。
+- HostName 规则为:默认为 。
+
+### 1.2 DNS 配置(dns)
+```golang
+type DnsCluster struct {
+ ServiceName string
+ Domain string
+ Port int64
+}
+
+```
+- 集群名称规则为:`outbound|||.dns`。
+- HostName 规则为:如果设置 Host,返回 Host,否则返回。
+
+### 1.3 Kubernetes 服务(kubernetes)
+```golang
+
+type K8sCluster struct {
+ ServiceName string
+ Namespace string
+ Port int64
+ Version string
+ Host string
+}
+```
+- 集群名称规则为:`outbound|||..svc.cluster.local`。
+- HostName 规则为:如果设置 Host,返回 Host,否则返回 ..svc.cluster.local。
+
+### 1.4 Nacos
+```golang
+
+type NacosCluster struct {
+ ServiceName string
+ // use DEFAULT-GROUP by default
+ Group string
+ NamespaceID string
+ Port int64
+ // set true if use edas/sae registry
+ IsExtRegistry bool
+ Version string
+ Host string
+}
+```
+- 集群名称规则为:`outbound|||...nacos`。
+- HostName 规则为:如果设置 Host,返回 Host,否则返回 。
+
+### 1.5 Consul
+```golang
+type ConsulCluster struct {
+ ServiceName string
+ Datacenter string
+ Port int64
+ Host string
+}
+```
+- 集群名称规则为:`outbound|||..consul`。
+- HostName 规则为:如果设置 Host,返回 Host,否则返回 。
+
+### 1.6 FQDN
+
+```golang
+
+type FQDNCluster struct {
+ FQDN string
+ Host string
+ Port int64
+}
+```
+- 集群名称规则为:`outbound|||`。
+- HostName 规则为:如果设置 Host,返回 Host,否则返回 ``。
+
+## 2 HTTP 调用
+http_wrapper.go 部分核心代码如下:
+```golang
+// 回调函数
+type ResponseCallback func(statusCode int, responseHeaders http.Header, responseBody []byte)
+
+// HTTP 调用接口
+type HttpClient interface {
+ Get(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Head(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Options(path string, headers [][2]string, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Post(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Put(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Patch(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Delete(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Connect(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Trace(path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
+ Call(method, path string, headers [][2]string, body []byte, cb ResponseCallback, timeoutMillisecond ...uint32) error
+}
+
+// 实现 httpClient 接口
+type ClusterClient[C Cluster] struct {
+ cluster C
+}
+```
+ClusterClient Get、Head、Options、Post、PUT、Patch、Delete、Connect、Trace、Call 方法最后调用 HttpCall 方法,其核心代码如下:
+
+```golang
+func HttpCall(cluster Cluster, method, path string, headers [][2]string, body []byte,
+ callback ResponseCallback, timeoutMillisecond ...uint32) error {
+
+ // 删除 :method, :path, :authority
+ for i := len(headers) - 1; i >= 0; i-- {
+ key := headers[i][0]
+ if key == ":method" || key == ":path" || key == ":authority" {
+ headers = append(headers[:i], headers[i+1:]...)
+ }
+ }
+ // 设置 timeout
+ var timeout uint32 = 500
+ if len(timeoutMillisecond) > 0 {
+ timeout = timeoutMillisecond[0]
+ }
+ // 重新设置 :method, :path, :authority
+ headers = append(headers, [2]string{":method", method}, [2]string{":path", path}, [2]string{":authority", cluster.HostName()})
+ requestID := uuid.New().String()
+ // 调用 HTTP 请求
+ _, err := proxywasm.DispatchHttpCall(cluster.ClusterName(), headers, body, nil, timeout, func(numHeaders, bodySize, numTrailers int) {
+ // 获取 HTTP 响应 body 和 headers
+ respBody, err := proxywasm.GetHttpCallResponseBody(0, bodySize)
+ ...
+ respHeaders, err := proxywasm.GetHttpCallResponseHeaders()
+ ...
+ code := http.StatusBadGateway
+ var normalResponse bool
+ headers := make(http.Header)
+ for _, h := range respHeaders {
+ if h[0] == ":status" {
+ code, err = strconv.Atoi(h[1])
+ ..
+ }
+ headers.Add(h[0], h[1])
+ }
+ ...
+ // 调用自定义插件回调函数
+ callback(code, headers, respBody)
+ })
+ ...
+ return err
+}
+```
+
+## 3 easy-jwt 插件开发
+
+在实际业务场景中,可能需要独立认证授权服务,来完成每个请求的认证和授权,现在开发一个简单的 easy-jwt 插件来演示如何在 Wasm 插件进行 HTTP 调用。
+其插件核心流程如下图:
+![img](https://img.alicdn.com/imgextra/i1/O1CN01DPi2w6244OvEejP1q_!!6000000007337-0-tps-1488-682.jpg)
+
+Token Server 提供 2 个接口:
+- /api/token/auth: 认证令牌接口
+- /api/token/create: 生成令牌接口
+
+### 3.1 插件部分核心代码
+
+```golang
+package main
+...
+
+const (
+ AuthUIDHeader = "x-auth-user"
+)
+
+func main() {
+ wrapper.SetCtx(
+ // 插件名称
+ "easy-jwt",
+ // 设置自定义函数解析插件配置
+ wrapper.ParseConfigBy(parseConfig),
+ // 设置自定义函数处理请求头
+ wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
+ )
+}
+
+// 自定义插件配置
+type JwtConfig struct {
+ // HTTP Client
+ client wrapper.HttpClient
+ // 令牌服务器的完全限定域名
+ tokenServerFQDN string
+ // 令牌服务器的端口
+ tokenServerPort int
+ // HTTP请求头中包含令牌的字段名称
+ tokenFromHeaderName string
+ // 令牌前缀,如Bearer
+ tokenFromHeaderPrefix string
+ // 插件将忽略令牌验证 UR L列表
+ ignoreUrls []string
+ // 匿名令牌,用于未认证的请求
+ anonymousToken string
+ // 匿名用户ID
+ anonymousUID int
+ // 当令牌验证失败时返回的 HTTP 状态码
+ responseErrorStatusCode uint32
+ // 返回的错误信息格式
+ responseErrorBody string
+}
+
+func parseConfig(json gjson.Result, config *JwtConfig, log wrapper.Log) error {
+ log.Debugf("parseConfig()")
+ // 解析插件配置
+ config.tokenServerFQDN = json.Get("tokenServerFQDN").String()
+ config.tokenServerPort = int(json.Get("tokenServerPort").Int())
+ config.tokenFromHeaderName = json.Get("tokenFromHeaderName").String()
+ config.tokenFromHeaderPrefix = json.Get("tokenFromHeaderPrefix").String()
+ config.anonymousUID = int(json.Get("anonymousUID").Int())
+ config.anonymousToken = json.Get("anonymousToken").String()
+ config.responseErrorBody = json.Get("responseErrorBoy").String()
+ config.responseErrorStatusCode = uint32(json.Get("responseErrorStatusCode").Int())
+ config.responseErrorBody = json.Get("responseErrorBody").String()
+ config.ignoreUrls = make([]string, 0)
+ for _, item := range json.Get("ignoreUrls").Array() {
+ config.ignoreUrls = append(config.ignoreUrls, item.String())
+ }
+ // 设置 HTTP Client
+ config.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
+ FQDN: json.Get("tokenServerFQDN").String(),
+ Port: json.Get("tokenServerPort").Int(),
+ })
+ log.Debugf("parseConfig result:%+v", config)
+ return nil
+}
+
+func onHttpRequestHeaders(ctx wrapper.HttpContext, config JwtConfig, log wrapper.Log) types.Action {
+ // 首先检查请求的路径是否在 ignoreUrls 列表中,如果是,则添加匿名用户ID到请求头并继续处理请求
+ rawPath := ctx.Path()
+ path, _ := url.Parse(rawPath)
+ for _, url := range config.ignoreUrls {
+ if isPathMatch(path.Path, url) {
+ proxywasm.AddHttpRequestHeader(AuthUIDHeader, fmt.Sprintf("%d", config.anonymousUID))
+ return types.ActionContinue
+ }
+ }
+ // 如果请求头中包含令牌,插件将尝试从请求头中提取令牌
+ token, err := extractTokenFromHeader(ctx, config)
+ if err != nil {
+ log.Debugf("extractTokenFromHeader() error: %v", err)
+ body := fmt.Sprintf(config.responseErrorBody, err.Error())
+ proxywasm.SendHttpResponse(config.responseErrorStatusCode, [][2]string{{"content-type", "application/json"}}, []byte(body), -1)
+ return types.ActionContinue
+ }
+ // 如果是匿名令牌,则添加匿名用户ID到请求头并继续处理请求
+ if len(config.anonymousToken) > 0 && config.anonymousToken == token {
+ proxywasm.AddHttpRequestHeader(AuthUIDHeader, fmt.Sprintf("%d", config.anonymousUID))
+ return types.ActionContinue
+ }
+
+ authRequest, _ := json.Marshal(map[string]string{"token": token})
+ log.Debugf("call token-server with auth request:%s", string(authRequest))
+ // 插件将使用配置的HTTP客户端向令牌服务器发送POST请求,以验证令牌的有效性
+ err2 := config.client.Post(
+ "/api/token/auth",
+ [][2]string{{"content-type", "application/json"}},
+ authRequest,
+ func(statusCode int, responseHeaders http.Header, responseBody []byte) {
+ defer func() {
+ // 保证恢复请求
+ _ = proxywasm.ResumeHttpRequest()
+ }()
+
+ log.Debugf("auth response status:%d, response:%s", statusCode, string(responseBody))
+ var jsonData gjson.Result
+ jsonData = gjson.ParseBytes(responseBody)
+ if statusCode != 200 {
+ // 如果响应状态码不是200,表示验证失败,插件将直接发送错误响应给客户端。
+ message := jsonData.Get("message").String()
+ body := fmt.Sprintf(config.responseErrorBody, message)
+ proxywasm.SendHttpResponse(config.responseErrorStatusCode, [][2]string{{"content-type", "application/json"}}, []byte(body), -1)
+ } else {
+ // 如果验证成功,插件将从响应中提取用户ID,并将其添加到后续请求头中
+ uid := jsonData.Get("uid").Int()
+ proxywasm.AddHttpRequestHeader(AuthUIDHeader, fmt.Sprintf("%d", uid))
+ }
+ },
+ 2000,
+ )
+
+ if err2 != nil {
+ // 如果连接失败,则直接发送错误响应给客户端。
+ log.Debugf("call token server error:%v", err2)
+ body := fmt.Sprintf(config.responseErrorBody, err2.Error())
+ proxywasm.SendHttpResponse(config.responseErrorStatusCode, [][2]string{{"content-type", "application/json"}}, []byte(body), -1)
+ return types.ActionContinue
+ }
+ // 暂停请求处理,直到调用 proxywasm.ResumeHttpRequest() 恢复请求
+ return types.ActionPause
+}
+
+func extractTokenFromHeader(ctx wrapper.HttpContext, config JwtConfig) (string, error) {
+ ...
+}
+
+func isPathMatch(path string, url string) bool {
+ ...
+}
+```
+
+核心流程如下:
+- 初始化插件
+- 解析配置
+- onHttpRequestHeaders 处理
+ - 检查请求路径是否在 ignoreUrls 列表中
+ - 是:添加匿名 UID 到请求头,继续处理请求
+ - 否:继续
+ - 从请求头中提取令牌,检查令牌是否存在
+ - 存在:继续
+ - 不存在:返回错误,发送响应
+ - 验证令牌
+ - 如果令牌是匿名令牌,添加匿名 UID 到请求头,继续处理请求
+ - 如果令牌不是匿名令牌,调用认证服务 /api/token/auth 接口验证令牌
+ - 如果验证成功,从响应中提取 UID,添加到请求头中,继续处理请求
+ - 如果验证失败,返回错误,发送响应
+
+### 3.2 部署和验证
+
+1. 部署 YAML 如下:
+```yaml
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: higress-course
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: echo-server
+ namespace: higress-course
+spec:
+ selector:
+ app: echo-server
+ ports:
+ - protocol: TCP
+ port: 8080
+ targetPort: 3000
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: echo-server
+ namespace: higress-course
+ labels:
+ app: echo-server
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: echo-server
+ template:
+ metadata:
+ labels:
+ app: echo-server
+ spec:
+ containers:
+ - name: echo-server
+ image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-server:1.3.0
+ env:
+ - name: POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ resources:
+ requests:
+ cpu: 10m
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: echo-server
+ namespace: higress-course
+spec:
+ selector:
+ app: echo-server
+ ports:
+ - protocol: TCP
+ port: 8080
+ targetPort: 3000
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: token-server
+ namespace: higress-course
+ labels:
+ app: token-server
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: token-server
+ template:
+ metadata:
+ labels:
+ app: token-server
+ spec:
+ containers:
+ - name: token-server
+ image: registry.cn-hangzhou.aliyuncs.com/2456868764/token-server:1.0.0
+ env:
+ - name: POD_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.name
+ - name: NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ resources:
+ requests:
+ cpu: 10m
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: token-server
+ namespace: higress-course
+spec:
+ selector:
+ app: token-server
+ ports:
+ - protocol: TCP
+ port: 9090
+ targetPort: 9090
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-foo
+ namespace: higress-course
+spec:
+ ingressClassName: higress
+ rules:
+ - host: "foo.com"
+ http:
+ paths:
+ - pathType: Prefix
+ path: "/"
+ backend:
+ service:
+ name: echo-server
+ port:
+ number: 8080
+---
+apiVersion: networking.higress.io/v1
+kind: McpBridge
+metadata:
+ name: default
+ namespace: higress-system
+spec:
+ registries:
+ - name: token-server
+ domain: token-server.higress-course.svc.cluster.local
+ port: 9090
+ type: dns
+---
+apiVersion: extensions.higress.io/v1alpha1
+kind: WasmPlugin
+metadata:
+ name: easy-jwt
+ namespace: higress-system
+spec:
+ priority: 200
+ matchRules:
+ - ingress:
+ - higress-course/ingress-foo
+ config:
+ tokenServerFQDN: "token-server.dns"
+ tokenServerPort: 9090
+ tokenFromHeaderName: "Authorization"
+ tokenFromHeaderPrefix: "Bearer "
+ anonymousToken: "AnonymousToken"
+ anonymousUID: 0
+ responseErrorStatusCode: 401
+ responseErrorBody: "{\"message\":\"%s\"}"
+ url: oci://registry.cn-hangzhou.aliyuncs.com/2456868764/easy-jwt:1.0.0
+```
+2. 获取令牌
+
+获取 uid 为 100 的用户的访问令牌,其命令如下,其中 `` 是 token-server pod 名称。
+```shell
+kubectl exec -n higress-course -- curl -X POST http://127.0.0.1:9090/api/token/create -d '{"uid":100}' -H "content-type:application/json"
+
+{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVSUQiOjEwMCwiZXhwIjoxNzU0Mzg2MzQ4fQ.jncbLJqBern5DYCFvED3moiCvg6sUn5jdlllhneuHrY"}%
+```
+
+3. 请求验证
+
+```shell
+curl http://127.0.0.1/hello -X POST -d "{}" -H "host:foo.com" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVSUQiOjEwMCwiZXhwIjoxNzU0Mzg2MzQ4fQ.jncbLJqBern5DYCFvED3moiCvg6sUn5jdlllhneuHrY" -H "content-type:application/json"
+
+{
+ "path": "/hello",
+ "host": "foo.com",
+ "method": "POST",
+ "proto": "HTTP/1.1",
+ "headers": {
+ "Accept": [
+ "*/*"
+ ],
+ "Authorization": [
+ "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVSUQiOjEwMCwiZXhwIjoxNzU0Mzg2MzQ4fQ.jncbLJqBern5DYCFvED3moiCvg6sUn5jdlllhneuHrY"
+ ],
+ "Content-Length": [
+ "2"
+ ],
+ "Content-Type": [
+ "application/json"
+ ],
+ "Original-Host": [
+ "foo.com"
+ ],
+ "Req-Start-Time": [
+ "1722850461721"
+ ],
+ "User-Agent": [
+ "curl/8.1.2"
+ ],
+ "X-Auth-User": [
+ "100"
+ ],
+ "X-B3-Sampled": [
+ "0"
+ ],
+ "X-B3-Spanid": [
+ "642eab8e332d6500"
+ ],
+ "X-B3-Traceid": [
+ "d9b9e94203603997642eab8e332d6500"
+ ],
+ "X-Envoy-Attempt-Count": [
+ "1"
+ ],
+ "X-Envoy-Decorator-Operation": [
+ "echo-server.higress-course.svc.cluster.local:8080/*"
+ ],
+ "X-Envoy-Internal": [
+ "true"
+ ],
+ "X-Forwarded-For": [
+ "192.168.65.1"
+ ],
+ "X-Forwarded-Proto": [
+ "http"
+ ],
+ "X-Request-Id": [
+ "47ff21bc-c3d5-4932-8bfb-361d268d319d"
+ ]
+ },
+ "namespace": "higress-course",
+ "ingress": "",
+ "service": "",
+ "pod": "echo-server-6f4df5fcff-nksqz",
+ "body": {}
+}
+```
+可以看到请求头中包含了 `X-Auth-User` 同时值为 100 。
+
+## 4 ext-auth 插件
+
+Higress 官方提供 [ext-auth](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ext-auth) 插件,其功能更加丰富。 ext-auth 插件实现了向外部授权服务发送鉴权请求,以检查客户端请求是否得到授权。该插件实现时参考了 Envoy 原生的 [ext_authz filter](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_authz_filter),实现了原生 filter 中对接 HTTP 服务的部分能力。
+
+## 5 Envoy Cluster 不存在问题
+
+在默认情况下,Higress 控制面只下发和路由关联的服务到 Envoy Cluster 中,因此有可能在实际开发过程中,发现对应调用 HTTP 服务在 Envoy Cluster 中不存在。
+有 3 种方案去解决:
+- helm 参数 global.onlyPushRouteCluster, 默认值为 true, 只推送路由关联的 Cluster 到 Envoy Cluster 中。修改为 false 即可。
+- 创建一个新路由关联到对应的调用的 HTTP 服务。
+- 通过 McpBridge 配置,添加调用的 HTTP 服务。
+
+上面 easy-jwt 插件中调用 token-server 服务,是通过 McpBridge 配置,添加 dns 类型服务,其配置如下:
+
+```yaml
+apiVersion: networking.higress.io/v1
+kind: McpBridge
+metadata:
+ name: default
+ namespace: higress-system
+spec:
+ registries:
+ - name: token-server
+ domain: token-server.higress-course.svc.cluster.local
+ port: 9090
+ type: dns
+```
+
+## 6 HTTP 回调链问题
+
+在实际开发过程中,可能会遇到 HTTP 回调链的情况,比如在 onHttpRequestHeader 处理阶段,需要调用两个 HTTP 服务,这个时候在 onHttpRequestHeader 阶段中,要先调用第一个 HTTP 服务,在第一个 HTTP 服务的响应回调函数中,再发起第二个 HTTP 服务的调用。
+以此类推。这种情况 Redis 调用也是一样处理。 关于回调链可以参考 Higress 官方提供 [ai-agent](https://github.com/alibaba/higress/blob/main/plugins/wasm-go/extensions/ai-agent/main.go#L169) 插件功能。
+
+## 参考
+- [1][Mcp Bridge 配置说明](https://higress.io/docs/latest/user/mcp-bridge/)
+
+
+
+
+
+
diff --git a/src/content/docs/ebook/zh-cn/wasm18.md b/src/content/docs/ebook/zh-cn/wasm18.md
new file mode 100644
index 0000000000..4b690a58ba
--- /dev/null
+++ b/src/content/docs/ebook/zh-cn/wasm18.md
@@ -0,0 +1,720 @@
+---
+title: Redis 调用
+keywords: [Higress]
+---
+
+# Redis 调用
+
+本章介绍如何在插件中调用 Redis、本地开发环境搭建、以及开发基于令牌桶限流插件。
+
+## 1 Redis 调用
+
+Higress 插件的 Go SDK 中 redis_wrapper.go 封装 Redis 调用, 部分核心代码如下:
+
+```shell
+// Redis 回调函数
+type RedisResponseCallback func(response resp.Value)
+
+// Redis 调用接口
+type RedisClient interface {
+ // 初始化接口
+ Init(username, password string, timeout int64) error
+ // with this function, you can call redis as if you are using redis-cli
+ }
+ // 命令接口
+ Command(cmds []interface{}, callback RedisResponseCallback) error
+ // Lua脚本接口
+ Eval(script string, numkeys int, keys, args []interface{}, callback RedisResponseCallback) error
+
+ // 以下是 Redis 各种命令接口
+ // Key
+ Del(key string, callback RedisResponseCallback) error
+ Exists(key string, callback RedisResponseCallback) error
+ Expire(key string, ttl int, callback RedisResponseCallback) error
+ Persist(key string, callback RedisResponseCallback) error
+ ...
+}
+
+// RedisClusterClient, Redis 调用接口具体实现
+type RedisClusterClient[C Cluster] struct {
+ cluster C
+}
+
+func RedisInit(cluster Cluster, username, password string, timeout uint32) error {
+ return proxywasm.RedisInit(cluster.ClusterName(), username, password, timeout)
+}
+// 真正调用 Redis 的函数
+func RedisCall(cluster Cluster, respQuery []byte, callback RedisResponseCallback) error {
+ requestID := uuid.New().String()
+ _, err := proxywasm.DispatchRedisCall(
+ cluster.ClusterName(),
+ respQuery,
+ func(status int, responseSize int) {
+ response, err := proxywasm.GetRedisCallResponse(0, responseSize)
+ var responseValue resp.Value
+ // 获取 Redis 回调结果 responseValue
+ ...
+ if callback != nil {
+ // 调用回调函数
+ callback(responseValue)
+ }
+ })
+ ...
+ return err
+}
+```
+
+所有调用 Redis 的接口,最终通过 RedisCall 调用 Redis, 同时回调 RedisResponseCallback 回调函数。
+
+## 2 令牌桶限流
+
+常见的限流算法有固定窗口限流算法、滑动窗口限流算法、漏桶限流算法、令牌桶限流算法等。这里主要介绍令牌桶限流算法。
+令牌桶算法原理:
+- 令牌以固定的频率被添加到令牌桶中。
+- 如果令牌数量满了,超过令牌桶容量的限制,那就丢弃。
+- 系统在接受到一个用户请求时,都会先去令牌桶要一个令牌。如果拿到令牌,那么就处理这个请求的业务逻辑。
+- 如果拿不到令牌,就直接拒绝这个请求。
+
+令牌桶算法允许一定量的突发请求,因为桶可以存储一定数量的令牌,从而在短期内处理更多的请求。具体原理见下图:
+![img](https://img.alicdn.com/imgextra/i2/O1CN018T2vsi1bbGU9PeVx6_!!6000000003483-0-tps-902-922.jpg)
+
+关于 QPS 限流算法和令牌桶算法两种限流算法优缺点,可以参考:[限流算法选择](https://help.aliyun.com/document_detail/149952.html)。
+
+## 3 本地开发环境搭建
+
+### 3.1 初始化工程目录
+
+1. 新建一个工程目录文件 cluster-bucket-limit。
+
+```shell
+mkdir cluster-bucket-limit
+```
+2. 在所建目录下执行以下命令,初始化 Go 工程。
+
+```shell
+go mod init cluster-bucket-limit
+```
+更详细信息参考第十四章 Wasm 插件介绍和开发自定义插件。
+
+### 3.2 Makefile、Dockerfile、docker-compose.yaml、envoy.yaml 文件
+
+1. Makefile、Dockerfile
+
+Makefile、Dockerfile 文件参考第十四章 Wasm 插件介绍和开发自定义插件。
+
+2. docker-compose.yaml
+
+```yaml
+version: '3.9'
+services:
+ envoy:
+ image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v1.4.1
+ entrypoint: /usr/local/bin/envoy
+ # 注意这里对wasm开启了debug级别日志,正式部署时则默认info级别
+ command: -c /etc/envoy/envoy.yaml --log-level info --log-path /etc/envoy/envoy.log --component-log-level wasm:debug
+ depends_on:
+ - echo-server
+ networks:
+ - wasmtest
+ ports:
+ - "10000:10000"
+ - "9901:9901"
+ volumes:
+ - ./envoy.yaml:/etc/envoy/envoy.yaml
+ - ./build/plugin.wasm:/etc/envoy/plugin.wasm
+ - ./envoy.log:/etc/envoy/envoy.log
+ echo-server:
+ image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echo-server:1.3.0
+ networks:
+ - wasmtest
+ ports:
+ - "3000:3000"
+ redis:
+ image: registry.cn-hangzhou.aliyuncs.com/2456868764/redis:latest
+ environment:
+ - ALLOW_EMPTY_PASSWORD=yes
+ networks:
+ wasmtest:
+ ipv4_address: 172.20.0.100
+ ports:
+ - "6379:6379"
+networks:
+ wasmtest:
+ ipam:
+ config:
+ - subnet: 172.20.0.0/24
+
+```
+
+3. envoy.yaml 文件
+
+envoy.yaml 配置文件如下:
+```yaml
+admin:
+ address:
+ socket_address:
+ protocol: TCP
+ address: 0.0.0.0
+ port_value: 9901
+static_resources:
+ listeners:
+ - name: listener_0
+ address:
+ socket_address:
+ protocol: TCP
+ address: 0.0.0.0
+ port_value: 10000
+ 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
+ scheme_header_transformation:
+ scheme_to_overwrite: https
+ stat_prefix: ingress_http
+ route_config:
+ name: local_route
+ virtual_hosts:
+ - name: local_service
+ domains: ["*"]
+ routes:
+ - match:
+ prefix: "/"
+ route:
+ cluster: echo-server
+ http_filters:
+ - name: wasmdemo
+ typed_config:
+ "@type": type.googleapis.com/udpa.type.v1.TypedStruct
+ type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ value:
+ config:
+ name: wasmdemo
+ vm_config:
+ runtime: envoy.wasm.runtime.v8
+ code:
+ local:
+ filename: /etc/envoy/plugin.wasm
+ configuration:
+ "@type": "type.googleapis.com/google.protobuf.StringValue"
+ value: |-
+ {
+ "keys": [
+ "authorization"
+ ],
+ "in_header": true,
+ "limits": [
+ {
+ "name": "credential1",
+ "consumer": "Bearer credential1",
+ "rate": 2,
+ "capacity": 4
+ },
+ {
+ "name": "all",
+ "consumer": "*",
+ "rate": 1,
+ "capacity": 2
+ }
+ ],
+ "rejected_code": 429,
+ "rejected_msg": "Too Many Requests",
+ "show_limit_quota_header": true,
+ "redis":{
+ "service_name": "redis.static",
+ "service_port": 6379,
+ "timeout": 2000
+ }
+ }
+ - name: envoy.filters.http.router
+ clusters:
+ - name: echo-server
+ connect_timeout: 30s
+ type: LOGICAL_DNS
+ # Comment out the following line to test on v6 networks
+ dns_lookup_family: V4_ONLY
+ lb_policy: ROUND_ROBIN
+ load_assignment:
+ cluster_name: echo-server
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: echo-server
+ port_value: 3000
+ - name: outbound|6379||redis.static
+ connect_timeout: 30s
+ type: STATIC
+ load_assignment:
+ cluster_name: outbound|6379||redis.static
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: 172.20.0.100
+ port_value: 6379
+```
+envoy.yaml 配置文件增加了 `outbound|6379||redis.static` 集群,用于连接 Redis 服务。Redis 连接目前不支持 DNS 域名配置,只支持 IP 地址配置。
+因此这里 Redis 的 IP 地址是 `172.20.0.100`。
+
+## 3 令牌桶限流插件开发
+
+### 3.1 插件配置和配置解析
+
+插件配置和配置解析部分核心代码如下:
+```golang
+// LimitConfig 定义了限流插件的配置结构。
+type LimitConfig struct {
+ Keys []string `yaml:"keys"` // 定义了用于提取限流信息的HTTP请求头字段名称。
+ InQuery bool `yaml:"in_query,omitempty"` // 标识是否从查询参数中获取限流信息。
+ InHeader bool `yaml:"in_header,omitempty"` // 标识是否从请求头中获取限流信息。
+ Limits []LimitItem `yaml:"limits"` // 包含具体的限流规则项。
+ RejectedCode uint32 `yaml:"rejected_code"` // 请求超过阈值被拒绝时返回的 HTTP 状态码。
+ RejectedMsg string `yaml:"rejected_msg"` // 请求超过阈值被拒绝时返回的响应体。
+ ShowLimitQuotaHeader bool `yaml:"show_limit_quota_header"` // 标识是否在响应头中显示限流配额信息。
+ RedisInfo RedisInfo `yaml:"redis"` // 定义了与 Redis 交互所需的信息。
+ RedisClient wrapper.RedisClient // Redis客户端,用于执行Redis命令。
+}
+
+// LimitItem 定义了具体的限流项,包括限流名称、消费者标识、请求速率和容量。
+type LimitItem struct {
+ Name string `yaml:"name"` // 限流项的名称。
+ Consumer string `yaml:"consumer"` // 限流项关联的消费者标识,用于匹配特定的请求。
+ Rate int `yaml:"rate"` // 每秒放入桶内的令牌数量。
+ Capacity int `yaml:"capacity"` // 限流桶的最大容量。
+}
+
+// RedisInfo 定义了连接Redis所需的详细信息。
+type RedisInfo struct {
+ ServiceName string `required:"true" yaml:"service_name" json:"service_name"` // Redis 服务名或地址。
+ ServicePort int `yaml:"service_port" json:"service_port"` // Redis 服务端口。
+ Username string `yaml:"username" json:"username"` // 连接 Redis 的用户名,如果需要。
+ Password string `yaml:"password" json:"password"` // 连接 Redis 的密码,如果需要。
+ Timeout int `yaml:"timeout" json:"timeout"` // 连接 Redis 的超时时间(毫秒)。
+}
+
+// LimitContext 定义了限流上下文,存储了限流相关的信息。
+type LimitContext struct {
+ Allowed int // 表示当前请求是否被允许。
+ Count int // 限流桶当前的计数。
+ Remaining int // 限流桶剩余的容量。
+ Reset int // 限流桶重置时间(秒)。
+}
+
+// 解析配置,这里忽略插件配置解析的细节。主要显示 Redis 部分解析配置。
+func parseConfig(json gjson.Result, config *LimitConfig, log wrapper.Log) error {
+ // keys
+ names := json.Get("keys")
+ ...
+ // in_query and in_header
+ in_query := json.Get("in_query")
+ in_header := json.Get("in_header")
+ ...
+ // parse limit
+ limits := json.Get("limits")
+ ...
+ config.ShowLimitQuotaHeader = json.Get("show_limit_quota_header").Bool()
+
+ // parse redis
+ redisConfig := json.Get("redis")
+ if !redisConfig.Exists() {
+ return errors.New("missing redis in config")
+ }
+ serviceName := redisConfig.Get("service_name").String()
+ if serviceName == "" {
+ return errors.New("redis service name must not be empty")
+ }
+ servicePort := int(redisConfig.Get("service_port").Int())
+ if servicePort == 0 {
+ if strings.HasSuffix(serviceName, ".static") {
+ // use default logic port which is 80 for static service
+ servicePort = 80
+ } else {
+ servicePort = 6379
+ }
+ }
+ username := redisConfig.Get("username").String()
+ password := redisConfig.Get("password").String()
+ timeout := int(redisConfig.Get("timeout").Int())
+ if timeout == 0 {
+ timeout = 1000
+ }
+ config.RedisInfo.ServiceName = serviceName
+ config.RedisInfo.ServicePort = servicePort
+ config.RedisInfo.Username = username
+ config.RedisInfo.Password = password
+ config.RedisInfo.Timeout = timeout
+ config.RedisClient = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
+ FQDN: serviceName,
+ Port: int64(servicePort),
+ })
+ log.Debugf("parseConfig()+%+v", config)
+ return config.RedisClient.Init(username, password, int64(timeout))
+}
+```
+
+这里忽略插件配置解析的细节,主要显示 Redis 解析配置。可以看出这里需要调用 `wrapper.NewRedisClusterClient` 方法初始化 RedisClient 和 RedisClient `Init` 方法初始化 Redis 连接。
+
+### 3.2 插件限流 Lua 脚本
+令牌桶限流的 Lua 脚本如下:
+```lua
+local tokens_key = KEYS[1]
+local timestamp_key = KEYS[2]
+--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)
+
+local rate = tonumber(ARGV[1])
+local capacity = tonumber(ARGV[2])
+local now = tonumber(ARGV[3])
+local requested = tonumber(ARGV[4])
+local unit = tonumber(ARGV[5])
+
+local fill_time = capacity/rate
+local ttl = math.floor(fill_time*2*unit)
+
+--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
+--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
+--redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
+--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
+--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
+--redis.log(redis.LOG_WARNING, "ttl " .. ttl)
+
+local last_tokens = tonumber(redis.call("get", tokens_key))
+if last_tokens == nil then
+ last_tokens = capacity
+end
+--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)
+
+local last_refreshed = tonumber(redis.call("get", timestamp_key))
+if last_refreshed == nil then
+ last_refreshed = 0
+end
+--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)
+
+local delta = math.max(0, (now-last_refreshed)/unit)
+local filled_tokens = math.min(capacity, last_tokens + math.floor(delta*rate))
+local allowed = filled_tokens >= requested
+local new_tokens = filled_tokens
+local allowed_num = 0
+if allowed then
+ new_tokens = filled_tokens - requested
+ allowed_num = 1
+end
+
+--redis.log(redis.LOG_WARNING, "delta " .. delta)
+--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
+--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
+--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)
+
+if ttl > 0 then
+ redis.call("setex", tokens_key, ttl, new_tokens)
+ redis.call("setex", timestamp_key, ttl, now)
+end
+
+return { allowed_num, new_tokens, capacity, ttl}
+```
+Lua 脚本是在 Redis 中执行的,用于实现令牌桶限流算法。下面是对脚本参数和原理的分析:
+
+1. 参数解释:
+- KEYS[1] 和 KEYS[2]:这两个参数是通过 Redis 调用传递的键(keys),通常用于存储令牌桶的当前令牌数(tokens_key)和最后刷新时间(timestamp_key)。
+- ARGV[1] 到 ARGV[5]:这些参数是通过 Redis 调用传递的参数,用于配置限流策略。
+ - rate:单位时间内生成的令牌数量。
+ - capacity:令牌桶的容量,即最多可以容纳的令牌数。
+ - now:当前时间,通常以时间戳表示,以秒单位。
+ - requested:当前请求需要的令牌数。
+ - unit:令牌生成的时间单位,1 表示秒,60 表示分钟,3600 表示小时。
+2. 脚本原理:
+- 初始化变量:根据传入的参数初始化令牌桶的填充时间和 TTL(生存时间)。
+- 获取当前状态:从 Redis 中获取当前的令牌数(last_tokens)和最后刷新时间(last_refreshed)。如果不存在,则初始化为令牌桶的容量。
+- 计算令牌填充:
+ - delta:自上次刷新以来经过的单位时间。
+ - filled_tokens:根据 delta 和 rate 计算应该填充的令牌数,但不能超过桶的容量。
+- 判断是否允许请求:
+ - allowed:如果当前令牌数加上填充的令牌数大于等于请求的令牌数,则允许请求。
+- 更新令牌数:
+ - 如果请求被允许,从当前令牌数中减去请求的令牌数,更新 new_tokens。
+- 设置新的状态:
+ - 如果 TTL 大于 0,则更新 Redis 中的令牌数和时间戳,设置新的 TTL。
+- 返回结果:
+ - 返回一个包含限流结果的数组,包括是否允许请求、新的令牌数、桶容量和 TTL。
+
+### 3.3 插件限流具体实现
+
+限流主要实现在插件 `onHttpRequestHeaders` 方法中,部分核心代码如下:
+
+```golang
+// onHttpRequestHeaders 函数在处理 HTTP 请求头时被调用,用于执行限流逻辑。
+func onHttpRequestHeaders(ctx wrapper.HttpContext, config LimitConfig, log wrapper.Log) types.Action {
+ log.Debugf("onHttpRequestHeaders()")
+ // 初始化 tokens 切片,用于存储从请求头或查询参数中提取的 tokens 信息
+ var tokens []string
+
+ // 如果配置指定从请求头中获取 tokens 信息
+ if config.InHeader {
+ // 遍历配置中定义的所有键(key),尝试从请求头中获取每个键的值
+ for _, key := range config.Keys {
+ // 使用 proxywasm 库的 GetHttpRequestHeader 函数获取请求头中的值
+ value, err := proxywasm.GetHttpRequestHeader(key)
+ // 如果没有错误且值不为空,则将值添加到 tokens 切片中
+ if err == nil && value != "" {
+ tokens = append(tokens, value)
+ }
+ }
+ } else if config.InQuery {
+ // 如果配置指定从查询参数中获取 tokens 信息
+ // 获取 ":path" 请求头以获取请求 URL
+ requestUrl, _ := proxywasm.GetHttpRequestHeader(":path")
+ // 解析 URL 并获取查询参数
+ url, _ := url.Parse(requestUrl)
+ queryValues := url.Query()
+
+ // 遍历配置中定义的所有键
+ for _, key := range config.Keys {
+ // 从查询参数中获取每个键的值
+ values, ok := queryValues[key]
+ // 如果查询参数存在且有值,则将值添加到 tokens 切片中
+ if ok && len(values) > 0 {
+ tokens = append(tokens, values...)
+ }
+ }
+ }
+
+ // 如果从请求中提取了多于一个的 tokens,返回错误处理
+ if len(tokens) > 1 {
+ return deniedMultiKeyAuthData()
+ } else if len(tokens) <= 0 {
+ // 如果没有提取到 tokens,返回错误处理
+ return deniedNoKeyAuthData()
+ }
+
+ // 提取第一个 token 作为主要的令牌信息
+ limitKey := strings.Split(tokens[0], ",")[0]
+ log.Debugf("limitKey:%s", limitKey)
+
+ // 根据提取的 limitKey 查找对应的限流项
+ limitItem := findLimitItem(config, limitKey)
+ log.Debugf("limitItem:%+v", limitItem)
+
+ // 如果没有找到对应的限流项,继续处理请求
+ if limitItem.Consumer == "" {
+ return types.ActionContinue
+ }
+
+ // 构建 Redis 脚本需要的键,用于操作令牌桶
+ tokenKey := fmt.Sprintf(ClusterRateLimitFormat, limitKey, "token")
+ expireKey := fmt.Sprintf(ClusterRateLimitFormat, limitKey, "expire")
+
+ // 获取当前时间,用于计算令牌桶的填充状态
+ now := time.Now()
+ // 将当前时间转换为 Unix 时间戳(秒)
+ unixTimestamp := now.UnixNano()
+ milliseconds := unixTimestamp / 1e6
+ seconds := milliseconds / 1000
+
+ // 构建调用 Redis 脚本所需的参数
+ keys := []interface{}{tokenKey, expireKey}
+ args := []interface{}{limitItem.Rate, limitItem.Capacity, seconds, 1, 1}
+
+ // 调用 Redis 脚本执行限流逻辑
+ err := config.RedisClient.Eval(BucketTokenScript, 2, keys, args, func(response resp.Value) {
+ log.Debugf("RedisClient.Eval(),keys:%+v,args:%+v", keys, args)
+ // 检查脚本返回的结果是否包含 4 个元素
+ resultArray := response.Array()
+ if len(resultArray) != 4 {
+ log.Errorf("redis response parse error, response: %v", response)
+ proxywasm.ResumeHttpRequest()
+ return
+ }
+ // 根据脚本返回的结果创建 LimitContext 对象
+ context := LimitContext{
+ Allowed: resultArray[0].Integer(),
+ Remaining: resultArray[1].Integer(),
+ Count: resultArray[2].Integer(),
+ Reset: resultArray[3].Integer(),
+ }
+ log.Debugf("context:%+v", context)
+ // 如果请求未被允许(Allowed <= 0),触发限流逻辑
+ if context.Allowed <= 0 {
+ log.Debugf("request rejected")
+ rejected(config, context)
+ return
+ } else {
+ // 将限流上下文存储在 HttpContext 中,供后续处理使用
+ ctx.SetContext(LimitContextKey, context)
+ }
+ // 恢复 HTTP 请求处理
+ proxywasm.ResumeHttpRequest()
+ })
+ // 如果调用 Redis 脚本时出现错误,记录错误并继续处理请求
+ if err != nil {
+ log.Errorf("redis call failed: %v", err)
+ return types.ActionContinue
+ }
+ // 暂停处理当前请求头,等待 Redis 脚本调用完成
+ return types.HeaderStopAllIterationAndWatermark
+}
+
+// findLimitItem 函数用于在给定的配置中查找与特定消费者匹配的限流项。如果没有找到具体的匹配项,它将返回一个默认的 LimitItem 结构。
+func findLimitItem(config LimitConfig, key string) LimitItem {
+ // 遍历配置中的所有限流项
+ for _, limitItem := range config.Limits {
+ // 检查当前限流项的消费者字段是否与提供的 key 匹配,且消费者不是通配符"*"
+ if limitItem.Consumer == key && limitItem.Consumer != "*" {
+ // 如果找到匹配的限流项,返回这个限流项
+ return limitItem
+ }
+ }
+ // 再次遍历配置中的所有限流项,这次是为了查找通配符"*"的消费者
+ // 通配符"*"表示这个限流项适用于所有消费者
+ for _, limitItem := range config.Limits {
+ if limitItem.Consumer == "*" {
+ // 如果找到通配符限流项,返回这个限流项
+ return limitItem
+ }
+ }
+ // 则返回一个空的 LimitItem 结构,表示没有找到任何适用的限流规则
+ return LimitItem{}
+}
+```
+onHttpRequestHeaders 函数的核心逻辑可以概括为以下几个步骤:
+- 获取 Tokens:根据配置(config.InHeader 或 config.InQuery),从请求头或查询参数中提取用于限流的 tokens 信息。
+- 验证 Tokens:检查提取的 tokens 是否存在且数量合理(不能多于一个),如果不符合要求,返回相应的错误处理。
+- 查找限流项:使用提取的 tokens 查找配置中的限流规则(LimitItem),如果没有找到适用的限流规则,则允许请求继续。
+- 执行限流逻辑:如果找到限流规则,构建 Redis 脚本需要的键和参数,然后调用 Redis 脚本执行限流算法。
+- 处理 Redis 脚本结果:根据 Redis 脚本返回的结果,创建 LimitContext 对象并根据算法结果决定是否允许请求继续:
+ - 如果请求被拒绝(context.Allowed \<= 0),执行限流逻辑并通知客户端。
+ - 如果请求被允许,将 LimitContext 对象存储在 HttpContext 中,供后续处理使用。
+
+
+## 4 测试和验证
+
+1. 正常流量
+```shell
+curl -X POST -v http://127.0.0.1:10000/hello \
+ -H 'Authorization: Bearer credential1' \
+ -H 'Content-type: application/json' \
+ -H 'host:foo.com' \
+ -d '{"username":["unamexxxx"],"password":["pswdxxxx"]}'
+
+* Trying 127.0.0.1:10000...
+* Connected to 127.0.0.1 (127.0.0.1) port 10000 (#0)
+> POST /hello HTTP/1.1
+> Host:foo.com
+> User-Agent: curl/8.1.2
+> Accept: */*
+> Authorization: Bearer credential1
+> Content-type: application/json
+> Content-Length: 50
+>
+< HTTP/1.1 200 OK
+< content-type: application/json
+< x-content-type-options: nosniff
+< date: Tue, 20 Aug 2024 08:55:13 GMT
+< content-length: 692
+< req-cost-time: 42
+< req-arrive-time: 1724144113842
+< resp-start-time: 1724144113885
+< x-envoy-upstream-service-time: 8
+< x-ratelimit-limit: 4
+< x-ratelimit-remaining: 3
+< server: envoy
+<
+{
+ "path": "/hello",
+ "host": "foo.com",
+ "method": "POST",
+ "proto": "HTTP/1.1",
+ "headers": {
+ "Accept": [
+ "*/*"
+ ],
+ "Authorization": [
+ "Bearer credential1"
+ ],
+ "Content-Length": [
+ "50"
+ ],
+ "Content-Type": [
+ "application/json"
+ ],
+ "Original-Host": [
+ "foo.com"
+ ],
+ "Req-Start-Time": [
+ "1724144113842"
+ ],
+ "User-Agent": [
+ "curl/8.1.2"
+ ],
+ "X-Envoy-Expected-Rq-Timeout-Ms": [
+ "15000"
+ ],
+ "X-Forwarded-Proto": [
+ "https"
+ ],
+ "X-Request-Id": [
+ "b40e9ebb-f36c-4e22-b8fc-2559c9495f43"
+ ]
+ },
+ "namespace": "",
+ "ingress": "",
+ "service": "",
+ "pod": "",
+ "body": {
+ "password": [
+ "pswdxxxx"
+ ],
+ "username": [
+ "unamexxxx"
+ ]
+ }
+```
+可以看到请求被允许,并且返回了相应的响应。
+```shell
+< x-ratelimit-limit: 4
+< x-ratelimit-remaining: 3
+```
+
+2. 触发流控
+
+```shell
+for i in $(seq 1 10); do
+ curl -X POST -v http://127.0.0.1:10000/hello \
+ -H 'Authorization: Bearer credential1' \
+ -H 'Content-type: application/json' \
+ -H 'host:foo.com' \
+ -d '{"username":["unamexxxx"],"password":["pswdxxxx"]}'
+done
+
+> POST /hello HTTP/1.1
+> Host:foo.com
+> User-Agent: curl/8.1.2
+> Accept: */*
+> Authorization: Bearer credential1
+> Content-type: application/json
+> Content-Length: 50
+>
+< HTTP/1.1 429 Too Many Requests
+< x-ratelimit-limit: 4
+< x-ratelimit-remaining: 0
+< x-ratelimit-reset: 4
+< content-length: 17
+< content-type: text/plain
+< date: Tue, 20 Aug 2024 08:56:57 GMT
+< server: envoy
+<
+* Connection #0 to host 127.0.0.1 left intact
+Too Many Requests%
+```
+
+## 参考
+- [1] [限流算法选择](https://help.aliyun.com/document_detail/149952.html)
+
+
+
+
+
+
+
+
diff --git a/src/content/docs/ebook/zh-cn/wasm19.md b/src/content/docs/ebook/zh-cn/wasm19.md
new file mode 100644
index 0000000000..78abbbdf28
--- /dev/null
+++ b/src/content/docs/ebook/zh-cn/wasm19.md
@@ -0,0 +1,260 @@
+---
+title: Wasm 生效原理
+keywords: [Higress]
+---
+
+
+# Wasm 生效原理
+
+这一章主要介绍 Wasm 的生效原理包括全局/路由/域名/服务级别生效原理、Wasm插件的 phase & priority、以及 Wasm 插件分发的原理。
+
+## 1 测试插件链结构
+
+这里以 custom-response、transformer、key-auth、easy-logger 四个插件组成插件链为例,介绍 Wasm插件的生效原理。其插件链如下图:
+
+![img](https://img.alicdn.com/imgextra/i2/O1CN01sSmytv1DfnczmUj0j_!!6000000000244-2-tps-1830-460.png)
+
+
+## 2 全局/路由/域名/服务级生效原理
+
+以插件 custom-response 为例,其插件配置如下:
+```yaml
+apiVersion: extensions.higress.io/v1alpha1
+kind: WasmPlugin
+metadata:
+ name: custom-response
+ namespace: higress-system
+spec:
+ priority: 200
+ phase: AUTHN
+ # 配置会全局生效,但如果被下面规则匹配到,则会改为执行命中规则的配置
+ defaultConfig:
+ headers:
+ - key1=value1
+ "body": "{\"hello\":\"foo\"}"
+ matchRules:
+ # 域名级生效配置
+ - domain:
+ - foo.com
+ config:
+ headers:
+ - key2=value2
+ "body": "{\"hello\":\"foo\"}"
+ - ingress:
+ - higress-course/ingress-bar
+ # higress-course 命名空间下名为 ingress-bar 的 ingress 会应用下面这个配置
+ config:
+ headers:
+ - key3=value3
+ "body": "{\"hello\":\"bar\"}"
+ - service:
+ - echo-server.higress-course.svc.cluster.local
+ config:
+ headers:
+ - key4=value4
+ "body": "{\"hello\":\"echo server\"}"
+ url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/custom-response:1.0.0
+ imagePullPolicy: Always
+```
+
+Higress Controller 控制面会把 Higress WasmPlugin 配置转换成 Istio WasmPlugin 配置,同时通过 MCP over Xds 同步到 Istio Discovery, 然后下发到 Envoy 。
+这里可以通过 Higress Controller Debug 接口查看转换后的 Istio WasmPlugin 配置:
+
+```shell
+kubectl exec -c higress-core -n higress-system -- curl http://127.0.0.1:8888/debug/configz
+```
+以下是 custom-response 插件转换成 Istio WasmPlugin YAML 配置如下:
+```yaml
+kind: WasmPlugin
+apiVersion: extensions.istio.io/v1alpha1
+metadata:
+ name: custom-response
+ namespace: higress-system
+spec:
+ imagePullPolicy: Always
+ phase: AUTHN
+ pluginConfig:
+ _rules_:
+ - _match_domain_:
+ - foo.com
+ body: '{"hello":"foo"}'
+ headers:
+ - key2=value2
+ - _match_route_:
+ - higress-course/ingress-bar
+ body: '{"hello":"bar"}'
+ headers:
+ - key3=value3
+ - _match_service_:
+ - echo-server.higress-course.svc.cluster.local
+ body: '{"hello":"echo server"}'
+ headers:
+ - key4=value4
+ body: '{"hello":"foo"}'
+ headers:
+ - key1=value1
+ priority: 200
+ selector:
+ matchLabels:
+ higress: higress-system-higress-gateway
+ url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/custom-response:1.0.0
+```
+发现在 pluginConfig 中增加了 `_rules_` 规则列表,规则中可以指定匹配方式,并填写对应生效的配置:
+- `_match_domain_`:匹配域名生效,填写域名即可,支持通配符。
+- `_match_route_`:匹配 Ingress 生效,匹配格式为:Ingress 所在命名空间 + "/" + Ingress 名称。
+- `_match_service_`:匹配服务生效,填写服务即可,支持通配符。
+
+Higress Controller 控制面转换代码逻辑在 `pkg/ingress/config/ingress_config.go` 文件的 `convertIstioWasmPlugin` func 中实现,其实现代码逻辑比较简单。
+```golang
+func (m *IngressConfig) convertIstioWasmPlugin(obj *higressext.WasmPlugin) (*extensions.WasmPlugin, error) {
+ ...
+}
+```
+
+同时在第`十六章 Higress 插件 Go SDK 与处理流程`中介绍 `CommonPluginCtx` 插件上下文在插件启动时候解析全局/路由/域名/服务级配置,其代码逻辑在 `plugins/wasm-go/pkg/matcher/rule_matcher.go` 文件的 `ParseRuleConfig` func 中实现。
+
+```golang
+func (m *RuleMatcher[PluginConfig]) ParseRuleConfig(config gjson.Result,
+ parsePluginConfig func(gjson.Result, *PluginConfig) error,
+ parseOverrideConfig func(gjson.Result, PluginConfig, *PluginConfig) error) error {
+ ...
+}
+```
+另外在插件 `OnHttpRequestHeaders` 阶段根据当前请求的 `:authority`、`route_name`、`cluster_name` 获取对应的域名、路由、服务级和全局插件配置。其代码逻辑在 `plugins/wasm-go/pkg/matcher/rule_matcher.go` 文件的 `GetMatchConfig` func 中实现。
+```golang
+func (m RuleMatcher[PluginConfig]) GetMatchConfig() (*PluginConfig, error) {
+ host, err := proxywasm.GetHttpRequestHeader(":authority")
+ ...
+ routeName, err := proxywasm.GetProperty([]string{"route_name"})
+ ...
+ serviceName, err := proxywasm.GetProperty([]string{"cluster_name"})
+ ...
+}
+```
+这里代码逻辑相对比较简单,这里就不再赘述了,有兴趣同学可以直接看源代码。
+
+## 3 Wasm插件的 phase 和 priority
+
+Wasm 插件 phase 有 `UNSPECIFIED_PHASE`、`AUTHN`、`AUTHZ`、`STATS` 四个值,分别对应插件过滤器链的末端、Istio 认证过滤器之前、Istio 授权过滤器之前且在 Istio 认证过滤器之后、Istio 统计过滤器之前且在 Istio 授权过滤器之后。
+同时在相同 phase 情况下,priority 值越大,插件在插件链位置越靠前。 关于认证和授权相关内容可以参考 [Istio 安全](https://istio.io/latest/zh/docs/concepts/security/)官方文档。其插件链结构如下图:
+
+![img](https://img.alicdn.com/imgextra/i4/O1CN017aWyas29NFISP7P4o_!!6000000008055-2-tps-1274-1114.png)
+
+可以通过导出 Envoy 配置查看插件链结构:
+
+```shell
+kubectl exec -n higress-system -- curl http://127.0.0.1:15000/config_dump
+```
+其 Enovy 插件链结构 YAML 格式如下:
+```yaml
+name: envoy.filters.network.http_connection_manager
+typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
+ stat_prefix: outbound_0.0.0.0_80
+ http_filters:
+ - name: envoy.filters.http.cors
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
+ - name: higress-system.custom-response
+ config_discovery:
+ config_source:
+ ads: {}
+ initial_fetch_timeout: 0s
+ resource_api_version: V3
+ default_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ apply_default_config_without_warming: true
+ type_urls:
+ - type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ - type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ - name: higress-system.wasm-transformer
+ config_discovery:
+ config_source:
+ ads: {}
+ initial_fetch_timeout: 0s
+ resource_api_version: V3
+ default_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ apply_default_config_without_warming: true
+ type_urls:
+ - type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ - type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ - name: envoy.filters.http.rbac
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC
+ - name: envoy.filters.http.local_ratelimit
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
+ stat_prefix: http_local_rate_limiter
+ - name: higress-system.wasm-keyauth
+ config_discovery:
+ config_source:
+ ads: {}
+ initial_fetch_timeout: 0s
+ resource_api_version: V3
+ default_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ apply_default_config_without_warming: true
+ type_urls:
+ - type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ - type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ - name: higress-system.easy-logger
+ config_discovery:
+ config_source:
+ ads: {}
+ initial_fetch_timeout: 0s
+ resource_api_version: V3
+ default_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ apply_default_config_without_warming: true
+ type_urls:
+ - type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ - type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ - name: envoy.filters.http.fault
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault
+ - name: envoy.filters.http.router
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
+```
+
+## 4 Wasm 插件分发的原理
+
+Wasm 插件是通过 OCI 实现了 wasm 文件更新,直接热加载,不会导致任何连接中断,业务流量完全无损。其插件分发流程如下图:
+
+![img](https://img.alicdn.com/imgextra/i4/O1CN01rx9nle1TI0uWQqI3Q_!!6000000002358-0-tps-1498-1058.jpg)
+
+OCI 分发流程如下:
+
+1. 当 Higress WasmPlugin 资源更新时,Higress Core 监听到这个变化,同时把 Higress WasmPlugin 转成 Istio WasmPlugin。
+2. Higress Core 将转成 Istio WasmPlugin 通过 MCP Over Xds 推送给 Discovery。
+3. Discovery 通过 Pilot Agent 的 ADS 连接,通过 LDS 协议下发给 Plot Agent。
+4. Pilot Agent 将 LDS 响应直接代理转发给 Envoy。
+5. Envoy 根据 Listener 里插件配置,通过 ECDS (Extension Config Discovery Service) 订阅插件配置。
+6. Pilot Agent 代理 ECDS 协议请求到 Discovery, 同时拦截 ECDS 协议响应。
+7. Pilot Agent 根据 ECDS 响应里插件 OCI 配置,从 Registry Hub 下载镜像。
+8. Pilot Agent 把镜像里插件 Wasm 文件解压到本地,同时修改 ECDS 响应里插件地址到本地 Wasm 文件路径,然后把 ECDS 协议响应返回给 Envoy。
+9. Envoy 根据 ECDS 协议响应,加载本地 Wasm 文件。
+
+注意第 5 步没有直接下发插件配置。而是下发 config_discovery 配置。下面是 Envoy 导出 `custom-response` 插件配置。
+
+```yaml
+- name: higress-system.custom-response
+ config_discovery:
+ config_source:
+ ads: {}
+ initial_fetch_timeout: 0s
+ resource_api_version: V3
+ default_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+ apply_default_config_without_warming: true
+ type_urls:
+ - type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
+ - type.googleapis.com/envoy.extensions.filters.http.composite.v3.Composite
+```
+关于 ECDS 配置,可以参考 [Envoy ECDS](https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/extension/v3/config_discovery.proto)。
+
+
+## 参考
+- [1][Higress 实战:30 行代码写一个 Wasm Go插件](https://mp.weixin.qq.com/s/daYa4MSo3XelpjnIFuxhKw)
+