GVKun编程网logo

使用 Docker + WasmEdge 运行 WordPress | WebAssembly:无需容器的 Docker (下)(docker部署wordpress)

20

在这里,我们将给大家分享关于使用Docker+WasmEdge运行WordPress|WebAssembly:无需容器的Docker的知识,让您更了解下的本质,同时也会涉及到如何更有效地31.dock

在这里,我们将给大家分享关于使用 Docker + WasmEdge 运行 WordPress | WebAssembly:无需容器的 Docker 的知识,让您更了解的本质,同时也会涉及到如何更有效地31. docker swarm 通过 service 部署 wordpress、Blazor WebAssembly 中的依赖注入、认证授权、WebSocket、JS互操作等(原标题 Blazor(WebAssembly) + .NETCore 实现斗地主)、Docker swarm 实战-部署wordpress、Docker 与 WasmEdge 合作,发布 WebAssembly 支持的内容。

本文目录一览:

使用 Docker + WasmEdge 运行 WordPress | WebAssembly:无需容器的 Docker (下)(docker部署wordpress)

使用 Docker + WasmEdge 运行 WordPress | WebAssembly:无需容器的 Docker (下)(docker部署wordpress)

本文翻译自 Wasm Labs @ VMware OCTO 的 blog: WebAssembly: Docker without container。这是 Wasm Labs 在 2022 年 12 月 15 日在冬季 Docker Community All Hands 7 的关于 Docker+WebAssembly 的演讲的文字版。

上篇文章我们了解了服务端 Wasm 为什么有着重要的作用、什么是 WasmEdge 以及如何让解释型语言编写的程序在 Wasm 里运行,这篇文章,我们将通过动手示例了解在 Docker + Wasm 背景下的 Wasm container 有什么好处以及如何运行一个服务 WordPress 的 php.wasm 镜像。

动手示例

让我们开始吧! 在动手示例中,我们将使用编译为 Wasm 的 PHP 解释器。 我们会:

  • 构建一个 Wasm 容器。
  • 比较 Wasm 和原生二进制文件。
  • 比较传统容器和 Wasm 容器。
  • 展示 Wasm 的可移植性

前期准备

如果想在本地重现这些示例,你需要使用以下部分或全部内容来准备你的环境:

  • WASI SDK - 从构建 C 代码构建 WebAssembly 应用程序
  • PHP - 为了比较而运行本机 PHP 二进制文件
  • WasmEdge Runtime - 运行 WebAssembly 应用程序
  • Docker Desktop + Wasm (本文写作时,作为稳定 beta 版在 Docker Desktop4.15 版可用) - 能够运行 Wasm 容器

我们还充分运用 webassembly-language-runtimes repo,它提供了将 PHP 解释器构建为 WebAssembly 应用程序的方法。 可以像这样查看 demo 分支:

git clone --depth=1 -b php-wasmedge-demo \
   https://github.com/vmware-labs/webassembly-language-runtimes.git wlr-demo
cd wlr-demo

构建一个 Wasm 容器

第一个示例,我们将展示如何构建基于 C 的应用程序,例如 PHP 解释器。

该构建使用 WASI-SDK 工具集。 它包括一个可以构建到 wasm32-wasi 目标的 clang 编译器,以及在 WASI 之上实现基本 POSIX 系统调用接口的 wasi-libc。 使用 WASI SDK,我们可以从 PHP 的代码库中构建一个用 C 编写的 Wasm 模块,。之后,我们需要一个非常简单的基于 scratch 的 Dockerfile 来制作一个可以使用 Docker+Wasm 运行的 OCI 镜像。

构建一个 WASM 二进制码

假设你现在位于 wlr-demo 文件夹,这是前期准备工作的一部分,可以运行以下命令来构建 Wasm 二进制文件。

export WASI_SDK_ROOT=/opt/wasi-sdk/
export WASMLABS_RUNTIME=wasmedge

./wl-make.sh php/php-7.4.32/ && tree build-output/php/php-7.4.32/bin/

... ( a few minutes and hundreds of build log lines)几分钟和数百行构建日志

build-output/php/php-7.4.32/bin/
├── php-cgi-wasmedge
└── php-wasmedge

PHP 是用 autoconfmake 构建的。 所以如果你看一眼脚本 scripts/wl-build.sh ,你会注意到我们设置了所有相关变量,如 CCLDCXX 等,以使用来自 WASI_SDK 的编译器。

export WASI_SYSROOT="${WASI_SDK_ROOT}/share/wasi-sysroot"
export CC=${WASI_SDK_ROOT}/bin/clang
export LD=${WASI_SDK_ROOT}/bin/wasm-ld
export CXX=${WASI_SDK_ROOT}/bin/clang++
export NM=${WASI_SDK_ROOT}/bin/llvm-nm
export AR=${WASI_SDK_ROOT}/bin/llvm-ar
export RANLIB=${WASI_SDK_ROOT}/bin/llvm-ranlib

然后,进一步深入查看 php/php-7.4.32/wl-build.sh,可以看到像通常一样,我们使用 autoconf 构建过程。

./configure --host=wasm32-wasi host_alias=wasm32-musl-wasi \
   --target=wasm32-wasi target_alias=wasm32-musl-wasi \
   ${PHP_CONFIGURE} || exit 1
...
make -j ${MAKE_TARGETS} || exit 1

WASI 是一项正在进行的工作,许多 POSIX 调用仍然不能在它之上实现。 因此,要构建 PHP,我们必须在原始代码库之上应用多个补丁。

我们在上面看到输出二进制文件会转到 build-output/php/php-7.4.32。 在下面的示例中,我们将使用专门为 WasmEdge 构建的 php-wasmedge 二进制文件,因为它提供服务端 socket 支持,服务端 socket 支持还不是 WASI 的一部分

优化二进制码

Wasm 是一个虚拟指令集,因此任何运行时的默认行为都是即时解释这些指令。 当然,这在某些情况下可能会让速度变慢。 因此,为了通过 WasmEdge 获得两全其美的效果,你可以创建一个 AOT(提前编译)优化的二进制文件,它可以在当前机器上原生运行,但仍然可以在其他机器上进行解释。

要创建优化的二进制文件,请运行以下命令:

wasmedgec --enable-all --optimize 3 \
   build-output/php/php-7.4.32/bin/php-wasmedge \
   build-output/php/php-7.4.32/bin/php-wasmedge-aot

我们在下面的例子中使用这个 build-output/php/php-7.4.32/bin/php-wasmedge-aot 二进制码。要了解有关 WasmEdge AOT 优化二进制文件的更多信息,请查看这里。

构建 OCI 镜像

现在我们有了一个二进制文件,我们可以将它包装在一个 OCI 镜像中。 让我们看一下这个 images/php/Dockerfile.cli。 我们需要做的就是复制 Wasm 二进制文件并将其设置为 ENTRYPOINT

FROM scratch
ARG PHP_TAG=php-7.4.32
ARG PHP_BINARY=php
COPY build-output/php/${PHP_TAG}/bin/${PHP_BINARY} /php.wasm

ENTRYPOINT [ "php.wasm" ]

我们还可以在镜像添加更多内容,当 Docker 运行它时,Wasm 二进制文件可以访问这些内容。 例如,在 images/php/Dockerfile.server 中,我们还添加了一些 docroot 内容,在容器启动时由 php.wasm 提供服务。

FROM scratch
ARG PHP_TAG=php-7.4.32
ARG PHP_BINARY=php
COPY build-output/php/${PHP_TAG}/bin/${PHP_BINARY} /php.wasm
COPY images/php/docroot /docroot

ENTRYPOINT [ "php.wasm" , "-S", "0.0.0.0:8080", "-t", "/docroot"]

基于以上文件,我们可以轻松地在本地构建我们的 php-wasm 镜像。

docker build --build-arg PHP_BINARY=php-wasmedge-aot -t ghcr.io/vmware-labs/php-wasm:7.4.32-cli-aot -f images/php/Dockerfile.cli .
docker build --build-arg PHP_BINARY=php-wasmedge-aot -t ghcr.io/vmware-labs/php-wasm:7.4.32-server-aot -f images/php/Dockerfile.server .

原生 vs Wasm

现在让我们将原生 PHP 二进制文件与 Wasm 二进制文件在本地和 Docker 容器中分别进行比较。 我们将使用相同的 index.php 文件并将运行它时得到的结果与以下内容进行比较:

  • php
  • php-wasmedge-aot
  • 在传统容器中运行的 php
  • 在 Wasm 容器中运行的 php-wasmedge-aot

在下面所有的示例中,我们使用同样的 images/php/docroot/index.php 文件,让我们来看一下。简而言之,该脚本将:

  • 使用 phpversionphp_uname 展示解释器版本和它运行的平台
  • 打印脚本可以访问的所有环境变量的名称
  • 打印一条包含当前时间和日期的问候消息
  • 列出根文件夹的内容 /
<html>
<body>
<h1>Hello from PHP <?php echo phpversion() ?> running on "<?php echo php_uname()?>"</h1>

<h2>List env variable names</h2>
<?php
$php_env_vars_count = count(getenv());
echo "Running with $php_env_vars_count environment variables:\n";
foreach (getenv() as $key => $value) {
    echo  $key . " ";
}
echo "\n";
?>

<h2>Hello</h2>
<?php
$date = getdate();

$message = "Today, " . $date[''weekday''] . ", " . $date[''year''] . "-" . $date[''mon''] . "-" . $date[''mday''];
$message .= ", at " . $date[''hours''] . ":" . $date[''minutes''] . ":" . $date[''seconds''];
$message .= " we greet you with this message!\n";
echo $message;
?>

<h2>Contents of ''/''</h2>
<?php
foreach (array_diff(scandir(''/''), array(''.'', ''..'')) as $key => $value) {
    echo  $value . " ";
}
echo "\n";
?>

</body>
</html>


Native PHP 运行 index.js

我们使用本地 php 二进制码时,看到一个基于 Linux 的平台。

  • 58 个环境变量的列表,脚本可以在需要时访问
  • / 中所有文件和文件夹的列表,如果需要,脚本可以再次访问这些文件和文件夹
$ php -f images/php/docroot/index.php

<html>
<body>
<h1>Hello from PHP 7.4.3 running on "Linux alexandrov-z01 5.15.79.1-microsoft-standard-WSL2 #1 SMP Wed Nov 23 01:01:46 UTC 2022 x86_64"</h1>

<h2>List env variable names</h2>
Running with 58 environment variables:
SHELL NVM_INC WSL2_GUI_APPS_ENABLED rvm_prefix WSL_DISTRO_NAME TMUX rvm_stored_umask TMUX_PLUGIN_MANAGER_PATH MY_RUBY_HOME NAME RUBY_VERSION PWD NIX_PROFILES LOGNAME rvm_version rvm_user_install_flag MOTD_SHOWN HOME LANG WSL_INTEROP LS_COLORS WASMTIME_HOME WAYLAND_DISPLAY NIX_SSL_CERT_FILE PROMPT_COMMAND NVM_DIR rvm_bin_path GEM_PATH GEM_HOME LESSCLOSE TERM CPLUS_INCLUDE_PATH LESSOPEN USER TMUX_PANE LIBRARY_PATH rvm_loaded_flag DISPLAY SHLVL NVM_CD_FLAGS LD_LIBRARY_PATH XDG_RUNTIME_DIR PS1 WSLENV XDG_DATA_DIRS PATH DBUS_SESSION_BUS_ADDRESS C_INCLUDE_PATH NVM_BIN HOSTTYPE WASMER_CACHE_DIR IRBRC PULSE_SERVER rvm_path WASMER_DIR OLDPWD BASH_FUNC_cr-open%% _

<h2>Hello</h2>
Today, Wednesday, 2022-12-14, at 12:0:36 we greet you with this message!

<h2>Contents of ''/''</h2>
apps bin boot dev docroot etc home init lib lib32 lib64 libx32 lost+found media mnt nix opt path proc root run sbin snap srv sys tmp usr var wsl.localhost

</body>
</html>

php-aot-wasm 运行 index.js

如果我们在 WasmEdge 使用 php-aot-wasm 我们看到

  • 一个 wasi/wasm32 平台
  • 没有环境变量,因为没有明确暴露给 Wasm 应用程序
  • Wasm 应用程序未获得对 / 的明确访问权限,因此尝试列出其内容失败并出现错误

自然地,为了让 php-wasmedge-aot 能够读取 index.php 文件,我们必须明确地向 WasmEdge 声明我们想要预先打开 images/php/docroot 以便在 Wasm 应用程序的上下文中作为 /docroot 进行访问。这显而易见展示了 Wasm 除了可移植性之外的最大优势之一。 我们得到了更佳的安全性,因为除非明确说明,否则无法访问任何内容。

$ wasmedge --dir /docroot:$(pwd)/images/php/docroot \
   build-output/php/php-7.4.32/bin/php-wasmedge-aot -f /docroot/index.php


<html>
<body>
<h1>Hello from PHP 7.4.32 running on "wasi (none) 0.0.0 0.0.0 wasm32"</h1>

<h2>List env variable names</h2>
Running with 0 environment variables:


<h2>Hello</h2>
Today, Wednesday, 2022-12-14, at 10:8:46 we greet you with this message!

<h2>Contents of ''/''</h2>

Warning: scandir(/): failed to open dir: Capabilities insufficient in /docroot/index.php on line 27

Warning: scandir(): (errno 76): Capabilities insufficient in /docroot/index.php on line 27

Warning: array_diff(): Expected parameter 1 to be an array, bool given in /docroot/index.php on line 27

Warning: Invalid argument supplied for foreach() in /docroot/index.php on line 27


</body>
</html>

容器中的 PHP 运行 index.js

当我们从一个传统的容器中使用 php 时我们看到

  • 基于 Linux 的平台
  • 脚本有权访问的 14 个环境变量的列表
  • 带有当前时间和日期的问候消息
  • 包含根文件夹内容的列表 /

与在主机上使用 php 运行它相比,已经明显有区别,表现更佳。 由于 / 的环境变量和内容是 “虚拟的” 并且仅存在于容器内。

docker run --rm \
   -v $(pwd)/images/php/docroot:/docroot \
   php:7.4.32-cli \
   php -f /docroot/index.php


<html>
<body>
<h1>Hello from PHP 7.4.32 running on "Linux 227b2bc2f611 5.15.79.1-microsoft-standard-WSL2 #1 SMP Wed Nov 23 01:01:46 UTC 2022 x86_64"</h1>

<h2>List env variable names</h2>
Running with 14 environment variables:
HOSTNAME PHP_INI_DIR HOME PHP_LDFLAGS PHP_CFLAGS PHP_VERSION GPG_KEYS PHP_CPPFLAGS PHP_ASC_URL PHP_URL PATH PHPIZE_DEPS PWD PHP_SHA256

<h2>Hello</h2>
Today, Wednesday, 2022-12-14, at 10:15:35 we greet you with this message!

<h2>Contents of ''/''</h2>
bin boot dev docroot etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var

</body>
</html>

php-aot-wasm 在一个容器中运行 index.js

如果我们在 WasmEdge 使用 php-aot-wasm 我们看到

  • 一个 wasi/wasm32 平台
  • 只有 2 个基础设施环境变量,使用在 containerd 中运行的 WasmEdge shim 预先设置
  • 容器中 / 内所有文件和文件夹的列表,明确预打开以供 Wasm 应用程序访问(WasmEdge shim 中的逻辑的一部分)

注意:如果你仔细观察,会发现要从这个镜像运行一个容器,我们必须:

  • 通过 --runtime=io.containerd.wasmedge.v1 将命令行参数直接传递给 php.wasm 明确声明运行时,而不包括二进制文件本身。 拉到上面可以看到我们可以使用传统的 PHP 容器明确编写完整的命令,包括 php 二进制文件(不是必需的)。

最后一点,即使使用 Docker,Wasm 也加强了运行 index.php 的安全性,因为暴露给它的要少得多。

docker run --rm \
   --runtime=io.containerd.wasmedge.v1 \
   -v $(pwd)/images/php/docroot:/docroot \
   ghcr.io/vmware-labs/php-wasm:7.4.32-cli-aot \
   -f /docroot/index.php


<html>
<body>
<h1>Hello from PHP 7.4.32 running on "wasi (none) 0.0.0 0.0.0 wasm32"</h1>

<h2>List env variable names</h2>
Running with 2 environment variables:
PATH HOSTNAME

<h2>Hello</h2>
Today, Wednesday, 2022-12-14, at 11:33:10 we greet you with this message!

<h2>Contents of ''/''</h2>
docroot etc php.wasm

</body>
</html>

传统容器 vs Wasm 容器

我们构建并运行了一个 Wasm 二进制文件,并将其作为容器运行。 我们看到了 Wasm 和传统容器之间的输出差异以及 Wasm 带来的高级 “沙箱隔离”。我们可以轻松看到的两种容器之间的其他差异。

首先,我们将运行两个 daemon 容器,看看我们如何解释有关它们的一些统计信息。 然后我们将检查容器镜像的差异。

容器数据

让我们运行两个 daemon 容器 - 一个是从传统的 php 镜像,另一个是从 php-wasm 镜像。

docker run --rm -d \
   -p 8083:8080 -v $(pwd)/images/php/docroot:/docroot \
   php:7.4.32-cli \
   -S 0.0.0.0:8080 -t /docroot
docker run --rm -d \
   --runtime=io.containerd.wasmedge.v1 \
   -p 8082:8080 -v $(pwd)/images/php/docroot:/docroot \
   ghcr.io/vmware-labs/php-wasm:7.4.32-cli-aot 
   -S 0.0.0.0:8080 -t /docroot

但是如果我们看 docker stats,我们只看到传统容器的数据。这之后可能会变化,因为 Docker+Wasm 现在是 beta 版特性。 所以,如果真的想看看发生了什么,可以改为监视对照组。 每个传统容器都有自己的控制组,如 docker/ee44...。另一方面,Wasm 容器作为 podruntime/docker 控制组的一部分包含在内,可以间接观察它们的 CPU 或内存消耗。

$ systemd-cgtop -kP --depth=10

Control Group           Tasks    %CPU     Memory
podruntime              145      0.1      636.3M
podruntime/docker       145      0.1      636.3M
docker                  2        0.0      39.7M
docker/ee444b...        1        0.0      6.7M 

镜像大小

首先,探索镜像,我们看到 Wasm 容器镜像比传统镜像小得多。 即使是 alpine 版本的 php 容器也比 Wasm 容器大。

$ docker images


REPOSITORY                     TAG                 IMAGE ID       CREATED          SIZE
php                            7.4.32-cli          680c4ba36f1b   2 hours ago      166MB
php                            7.4.32-cli-alpine   a785f7973660   2 minutes ago    30.1MB
ghcr.io/vmware-labs/php-wasm   7.4.32-cli-aot      63460740f6d5   44 minutes ago   5.35MB

这是意料之中的,因为对于 Wasm,我们只需要在容器内添加可执行二进制文件,而对于传统容器,我们仍然需要来自运行二进制文件的操作系统的一些基本库和文件。这种大小差异对于第一次拉取镜像的速度以及进行在本地存储库中占用的空间非常有帮助。

Wasm 可移植性

Wasm 最大优势之一就是它的可移植性。 当人们想要一个可移植的应用程序时,Docker 已经提供了传统的容器作为一种选择。 然而,除了镜像特别大之外,传统容器还绑定到它们运行的平台架构。 作为程序员,相比许多人都经历过这种坎坷:针对不同的架构,必须构建支持的软件版本,并为每种架构打包对应镜像。

WebAssembly 带来了真正的可移植性。 构建一次二进制文件,就能在任何地方运行它。 作为这种可移植性的证明,我们准备了几个通过我们为 WebAssembly 构建的 PHP 解释器运行 WordPress 的示例。

当 PHP 作为独立的 Wasm 应用程序运行时,它会为 WordPress 提供服务。 它也可以在 Docker+Wasm 容器中运行。 此外,它还能在嵌入 Wasm 运行时的任何应用程序中运行。 在我们的示例中,这是 apache httpd,它可以通过 mod_wasm 使用 Wasm 应用程序作为内容处理程序。 最后,PHP.wasm 也可以在浏览器中运行。

通过 WasmEdge 服务 WordPress

我们为本次演示准备了一个紧凑的 WordPress+Sqlite 示例。 由于它是 ghcr.io/vmware-labs/php-wasm:7.4.32-server-wordpress 容器镜像的一部分,我们先将其下载到本地。

此命令将只创建一个临时容器(拉取镜像),将 WordPress 文件复制到 /tmp/wp/docroot,然后删除容器。

container_id=$(docker create ghcr.io/vmware-labs/php-wasm:7.4.32-server-wordpress) && \
   mkdir /tmp/wp && \
   docker cp $container_id:/docroot /tmp/wp/ && \
   docker rm $container_id

现在我们有了 WordPress,让我们添加服务器:

wasmedge --dir /docroot:/tmp/wp/docroot \
   build-output/php/php-7.4.32/bin/php-wasmedge-aot \
   -S 0.0.0.0:8085 -t /docroot

可以访问 http://localhost:8085 ,使用由 PHP Wasm 解释器服务的 WordPress。

通过 Docker+Wasm 服务 WordPress

自然的,有了 Docker 会容易很多。

docker run --rm --runtime=io.containerd.wasmedge.v1 \
   -p 8086:8080 -v /tmp/wp/docroot/:/docroot/ \
   ghcr.io/vmware-labs/php-wasm:7.4.32-cli-aot 
   -S 0.0.0.0:8080 -t /docroot

可以访问 http://localhost:8086 并使用由 PHP Wasm 解释器服务的 WordPress,这回是在 Docker 容器中运行。

通过 mod_wasm in Apache HTTPD 服务 WordPress

Apache HTTPD 是使用最广泛的 HTTP 服务器之一。 现在有了 mod_wasm,它还可以运行 WebAssembly 应用程序。 为了避免在本地安装和配置它,我们准备了一个容器,其中包含 Apache HTTPD、mod_wasm 和 WordPress。

docker run -p 8087:8080 projects.registry.vmware.com/wasmlabs/containers/php-mod-wasm:wordpress

可以访问 http://localhost:8087 并使用由 PHP Wasm 解释器服务的 WordPress,它由 Apache HTTPD 中的 mod_wasm 加载。

直接在浏览器中服务 WordPress

访问 https://wordpress.wasmlabs.dev 获得示例。 你将看到一个框架,其中 PHP Wasm 解释器会现场渲染 WordPress。

结论

感谢阅读本文。 需要消化的内容很多,但我们希望本文有助于理解 WebAssembly 的能力以及它如何与你现有的代码库和工具(包括 Docker)结合运行。 期待看到你使用 Wasm 编程!

如果你觉得 WasmEdge 不错,不要忘了给我们点个赞!

https://github.com/WasmEdge/WasmEdge

31. docker swarm 通过 service 部署 wordpress

31. docker swarm 通过 service 部署 wordpress

1. 创建 一个 overlay 的网络 driver

  docker network create -d overlay demo

  查看网络列表

    docker network ls

2. 创建mysql 的服务

  docker service create --name mysql --env MYSQL_ROOT_PASSWORD=root --env MYSQL_DATABASE=wordpress --network demo --mount type=volume,source=mysql-data,destination=/var/lib/mysql mysql:5.7.27

  其中

    --mount (类似与 volume)  type=volume (mount的类型为 volume),source=mysql-data (本地存储数据的位置在 mysql-data内)  ,destination=/var/lib/mysql(service 内 存储数据的位置在 /var/lib/mysql)

  查看 mysql 镜像  (发现mysql 可拓展 并且 运行在本地 docker-host 上)

    docker service ls

    docker service ps mysql

3. 创建 wordpress 的 service

  docker service create --name wordpress -p 80:80 --network demo --env WORDPRESS_DB_PASSWORD=root --env WORDPRESS_DB_HOST=mysql wordpress

  查看 wordpress 镜像  (发现 wordpress 可拓展 并且 运行在本地 docker-host 上)

    docker service ls

    docker service ps wordpress

  查看 docker-host 的 ip 地址 (wordpress 在哪台机器上 就访问 那台机器的ip 地址即可)

  访问 192.168.205.10  填写一些基本配置

4. 此时发现 使用 docker-node1 的 ip 和 docker-node2 的 ip 也能访问 wordpress

  查看 docker-node1 的网络  和 docker-node2 的网络 发现 demo 的 overlay 网络 会通过 swarm 自动同步过去  

Blazor WebAssembly 中的依赖注入、认证授权、WebSocket、JS互操作等(原标题 Blazor(WebAssembly) + .NETCore 实现斗地主)

Blazor WebAssembly 中的依赖注入、认证授权、WebSocket、JS互操作等(原标题 Blazor(WebAssembly) + .NETCore 实现斗地主)

  Blazor Wasm 最近更新到了3.2.0-preview1,支持了WebSocket同时启动类也有所修改,我就把这个文章重新修改一下。

  

  之前群里大神发了一个 html5+ .NETCore的斗地主,刚好在看Blazor WebAssembly 就尝试重写试试。

  这里主要介绍Blazor的依赖注入,认证与授权,WebSocket的使用,JS互操作等,完整实现可以查看github:https://github.com/saber-wang/FightLandlord/tree/master/src/BetGame.DDZ.WasmClient,在线演示:http://39.106.159.180:31000/

  另外强调一下Blazor WebAssembly 是纯前端框架,所有相关组件等都会下载到浏览器运行,要和MVC、Razor Pages等区分开来

  当前是基于NetCore3.1和Blazor WebAssembly 3.2.0-preview1。

  Blazor WebAssembly默认是没有安装的,在命令行执行下边的命令安装Blazor WebAssembly模板。

dotnet new -i Microsoft.AspNetCore.Blazor.Templates:3.2.0-preview1.20073.1

  

  选择Blazor应用,跟着往下就会看到Blazor WebAssembly App模板,如果看不到就在ASP.NET Core3.0和3.1之间切换一下。

  

  新建后项目结构如下。

  

  一眼看过去,大体和Razor Pages 差不多。Program.cs中创建了一个WebAssemblyHostBuilder,然后指定启动容器,代码非常简单。

  

public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);

            builder.RootComponents.Add<App>("app");

            await builder.Build().RunAsync();
        }

  

  Blazor WebAssembly 中也支持DI,注入方式与生命周期与ASP.NET Core一致,但是Scope生命周期不太一样,注册的服务的行为类似于 Singleton 服务。

  在WebAssemblyHostBuilder中有一个Services属性用来注册服务

public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);

            builder.Services.AddScoped<ApiService>();
            builder.Services.AddScoped<FunctionHelper>();
            builder.Services.AddScoped<LocalStorage>();
            builder.Services.AddScoped<CustomAuthStateProvider>();
            builder.Services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<CustomAuthStateProvider>());
            builder.Services.AddAuthorizationCore(c=> {
                c.AddPolicy("default", a => a.RequireAuthenticatedUser());
                c.DefaultPolicy = c.GetPolicy("default");
            });
            builder.Services.AddScoped(sp=>new ClientWebSocket());

            builder.RootComponents.Add<App>("app");

            WebAssemblyHttpMessageHandlerOptions.DefaultCredentials = FetchCredentialsOption.Include;
            await builder.Build().RunAsync();
        }

  

 

  默认已注入了HttpClient,IJSRuntime,NavigationManager,具体可以看官方文档介绍。

  App.razor中定义了路由和默认路由,修改添加AuthorizeRouteView和CascadingAuthenticationState以支持AuthorizeView、AuthenticationState等用于认证和获取当前的身份验证状态。

  

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <CascadingAuthenticationState>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there''s nothing at this address.</p>
            </LayoutView>
        </CascadingAuthenticationState>
    </NotFound>
</Router>

  自定义AuthenticationStateProvider并注入为AuthorizeView和CascadingAuthenticationState组件提供认证。

builder.Services.AddScoped<CustomAuthStateProvider>();
            builder.Services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<CustomAuthStateProvider>());
            //在wasm中没有默认配置,所以需要设置一下
            builder.Services.AddAuthorizationCore(c=> {
                c.AddPolicy("default", a => a.RequireAuthenticatedUser());
                c.DefaultPolicy = c.GetPolicy("default");
            });

  

CustomAuthStateProvider的实现如下。
public class CustomAuthStateProvider : AuthenticationStateProvider
    {
        ApiService _apiService;
        Player _playerCache;
        public CustomAuthStateProvider(ApiService apiService)
        {
            _apiService = apiService;
        }

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var player = _playerCache??= await _apiService.GetPlayer();

            if (player == null)
            {
                return new AuthenticationState(new ClaimsPrincipal());
            }
            else
            {
                //认证通过则提供ClaimsPrincipal
                var user = Utils.GetClaimsIdentity(player);
                return new AuthenticationState(user);
            }

        }
        /// <summary>
        /// 通知AuthorizeView等用户状态更改
        /// </summary>
        public void NotifyAuthenticationState()
        {
            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
        }
        /// <summary>
        /// 提供Player并通知AuthorizeView等用户状态更改
        /// </summary>
        public void NotifyAuthenticationState(Player player)
        {
            _playerCache = player;
            NotifyAuthenticationState();
        }
    }

  我们这个时候就可以在组件上添加AuthorizeView根据用户是否有权查看来选择性地显示 UI,该组件公开了一个 AuthenticationState 类型的 context 变量,可以使用该变量来访问有关已登录用户的信息。

  

<AuthorizeView>
    <Authorized>
     //认证通过 @context.User
    </Authorized>
    <NotAuthorized>
     //认证不通过
    </NotAuthorized>
</AuthorizeView>

   使身份验证状态作为级联参数

[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }

  获取当前用户信息

private async Task GetPlayer()
    {
        var user = await authenticationStateTask;
        if (user?.User?.Identity?.IsAuthenticated == true)
        {
            player = new Player
            {
                Balance = Convert.ToInt32(user.User.FindFirst(nameof(Player.Balance)).Value),
                GameState = user.User.FindFirst(nameof(Player.GameState)).Value,
                Id = user.User.FindFirst(nameof(Player.Id)).Value,
                IsOnline = Convert.ToBoolean(user.User.FindFirst(nameof(Player.IsOnline)).Value),
                Nick = user.User.FindFirst(nameof(Player.Nick)).Value,
                Score = Convert.ToInt32(user.User.FindFirst(nameof(Player.Score)).Value),
            };
            await ConnectWebsocket();
        }
    }

  注册用户并通知AuthorizeView状态更新

private async Task GetOrAddPlayer(MouseEventArgs e)
    {
        GetOrAddPlayering = true;
        player = await ApiService.GetOrAddPlayer(editNick);
        this.GetOrAddPlayering = false;

        if (player != null)
        {
            CustomAuthStateProvider.NotifyAuthenticationState(player);
            await ConnectWebsocket();
        }

    }

 

  JavaScript 互操作,虽然很希望完全不操作JavaScript,但目前版本的Web WebAssembly不太现实,例如弹窗、本地存储等,Blazor中操作JavaScript主要靠IJSRuntime 抽象,在重构的时候遇到最多的错误就是类型转换和索引溢出错误:)。

  从Blazor操作JavaScript比较简单,操作的JavaScript需要是公开的,这里实现从Blazor调用alert和localStorage如下

public class FunctionHelper
    {
        private readonly IJSRuntime _jsRuntime;

        public FunctionHelper(IJSRuntime jsRuntime)
        {
            _jsRuntime = jsRuntime;
        }

        public ValueTask Alert(object message)
        {
            //无返回值使用InvokeVoidAsync
            return _jsRuntime.InvokeVoidAsync("alert", message);
        }
    }
public class LocalStorage
    {
        private readonly IJSRuntime _jsRuntime;
        private readonly static JsonSerializerOptions SerializerOptions = new JsonSerializerOptions();

        public LocalStorage(IJSRuntime jsRuntime)
        {
            _jsRuntime = jsRuntime;
        }
        public ValueTask SetAsync(string key, object value)
        {

            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentException("Cannot be null or empty", nameof(key));
            }

            var json = JsonSerializer.Serialize(value, options: SerializerOptions);

            return _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, json);
        }
        public async ValueTask<T> GetAsync<T>(string key)
        {

            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentException("Cannot be null or empty", nameof(key));
            }

            //有返回值使用InvokeAsync
            var json =await _jsRuntime.InvokeAsync<string>("localStorage.getItem", key);
            if (json == null)
            {
                return default;
            }

            return JsonSerializer.Deserialize<T>(json, options: SerializerOptions);
        }
        public ValueTask DeleteAsync(string key)
        {
            return _jsRuntime.InvokeVoidAsync(
                $"localStorage.removeItem",key);
        }
    }

  从JavaScript调用C#方法则需要把C#方法使用[JSInvokable]特性标记且必须为公开的。调用C#静态方法看这里,这里主要介绍调用C#的实例方法。

  因为Blazor Wasm暂时不支持ClientWebSocket,所以我们用JavaScript互操作来实现WebSocket的链接与C#方法的回调。

  使用C#实现一个调用JavaScript的WebSocket,并使用DotNetObjectReference.Create包装一个实例传递给JavaScript方法的参数(dotnetHelper),这里直接传递了当前实例。

    [JSInvokable]
    public async Task ConnectWebsocket()
    {
        Console.WriteLine("ConnectWebsocket");
        var serviceurl = await ApiService.ConnectWebsocket();
        //TODO ConnectWebsocket
        if (!string.IsNullOrWhiteSpace(serviceurl))
            await _jsRuntime.InvokeAsync<string>("newWebSocket", serviceurl, DotNetObjectReference.Create(this));
    }

  JavaScript代码里使用参数(dotnetHelper)接收的实例调用C#方法(dotnetHelper.invokeMethodAsync(''方法名'',方法参数...))。

 

var gsocket = null;
var gsocketTimeId = null;
function newWebSocket(url, dotnetHelper)
{
    console.log(''newWebSocket'');
    if (gsocket) gsocket.close();
    gsocket = null;
    gsocket = new WebSocket(url);
    gsocket.onopen = function (e) {
        console.log(''websocket connect'');
        //调用C#的onopen();
        dotnetHelper.invokeMethodAsync(''onopen'')
    };
    gsocket.onclose = function (e) {
        console.log(''websocket disconnect'');
        dotnetHelper.invokeMethodAsync(''onclose'')
        gsocket = null;
        clearTimeout(gsocketTimeId);
        gsocketTimeId = setTimeout(function () {
            console.log(''websocket onclose ConnectWebsocket'');
            //调用C#的ConnectWebsocket();
            dotnetHelper.invokeMethodAsync(''ConnectWebsocket'');
            //_self.ConnectWebsocket.call(_self);
        }, 5000);
    };
    gsocket.onmessage = function (e) {
        try {
            console.log(''websocket onmessage'');
            var msg = JSON.parse(e.data);
            //调用C#的onmessage();
            dotnetHelper.invokeMethodAsync(''onmessage'', msg);
            //_self.onmessage.call(_self, msg);
        } catch (e) {
            console.log(e);
            return;
        }
    };
    gsocket.onerror = function (e) {
        console.log(''websocket error'');
        gsocket = null;
        clearTimeout(gsocketTimeId);
        gsocketTimeId = setTimeout(function () {
            console.log(''websocket onerror ConnectWebsocket'');
            dotnetHelper.invokeMethodAsync(''ConnectWebsocket'');
            //_self.ConnectWebsocket.call(_self);
        }, 5000);
    };
}

   Blazor中已经实现了Websocket,现在我们可以很简单的操作Websocket。

public async Task ConnectWebsocket()
    {
        Console.WriteLine("ConnectWebsocket");
        //获取Websocket链接
        var serviceurl = await ApiService.ConnectWebsocket();
        if (!string.IsNullOrWhiteSpace(serviceurl))
        {
            wsConnectState = 1010;
            //链接Websocket
            await clientWebSocket.ConnectAsync(new Uri(serviceurl),default);
            //后台接收消息
            ReceiveMessages();
            //链接Websocket时调用
            await onopen();

            //await _jsRuntime.InvokeAsync<string>("newWebSocket", serviceurl, DotNetObjectReference.Create(this));
        }
    }

  ReceiveMessages的实现如下

private async Task ReceiveMessages()
    {
        List<byte> vs = new List<byte>();
        while (true)
        {
            Memory<byte> memory = new Memory<byte>(new byte[1024]);
            var res = await clientWebSocket.ReceiveAsync(memory, default);
            var bt = memory.ToArray().Take(res.Count);
            vs.AddRange(bt);
            if (res.EndOfMessage)
            {
                if (res.MessageType == WebSocketMessageType.Close)
                {
                    onclose();
                }
                else
                {
                    var jsonDocument = JsonSerializer.Deserialize<object>(vs.ToArray());
                    vs.Clear();
                    await onmessage(jsonDocument);
                }
                //当前方法在后台执行,所以我们需要手动更新UI
                StateHasChanged();
            }
        }
    }

  

  onopen,onclose,onmessage实现如下

public async Task onopen()
    {

        Console.WriteLine("websocket connect");
        wsConnectState = 1;
        await GetDesks();
        StateHasChanged();
    }
    public void onclose()
    {
        Console.WriteLine("websocket disconnect");
        wsConnectState = 0;
    }

    public async Task onmessage(object msgobjer)
    {
        try
        {
            var jsonDocument = JsonSerializer.Deserialize<object>(msgobjer.ToString());
            if (jsonDocument is JsonElement msg)
            {
                if (msg.TryGetProperty("type", out var element) && element.ValueKind == JsonValueKind.String)
                {
                    Console.WriteLine(element.ToString());
                    if (element.GetString() == "Sitdown")
                    {
                        Console.WriteLine(msg.GetProperty("msg").GetString());
                        var deskId = msg.GetProperty("deskId").GetInt32();
                        foreach (var desk in desks)
                        {
                            if (desk.Id.Equals(deskId))
                            {
                                var pos = msg.GetProperty("pos").GetInt32();
                                Console.WriteLine(pos);
                                var player = JsonSerializer.Deserialize<Player>(msg.GetProperty("player").ToString());
                                switch (pos)
                                {
                                    case 1:
                                        desk.player1 = player;
                                        break;
                                    case 2:
                                        desk.player2 = player;
                                        break;
                                    case 3:
                                        desk.player3 = player;
                                        break;

                                }
                                break;
                            }
                        }
                    }
                    else if (element.GetString() == "Standup")
                    {
                        Console.WriteLine(msg.GetProperty("msg").GetString());
                        var deskId = msg.GetProperty("deskId").GetInt32();
                        foreach (var desk in desks)
                        {
                            if (desk.Id.Equals(deskId))
                            {
                                var pos = msg.GetProperty("pos").GetInt32();
                                Console.WriteLine(pos);
                                switch (pos)
                                {
                                    case 1:
                                        desk.player1 = null;
                                        break;
                                    case 2:
                                        desk.player2 = null;
                                        break;
                                    case 3:
                                        desk.player3 = null;
                                        break;

                                }
                                break;
                            }
                        }
                    }
                    else if (element.GetString() == "GameStarted")
                    {
                        Console.WriteLine(msg.GetProperty("msg").GetString());
                        currentChannel.msgs.Insert(0, msg);
                    }
                    else if (element.GetString() == "GameOvered")
                    {
                        Console.WriteLine(msg.GetProperty("msg").GetString());
                        currentChannel.msgs.Insert(0, msg);
                    }
                    else if (element.GetString() == "GamePlay")
                    {

                        ddzid = msg.GetProperty("ddzid").GetString();
                        ddzdata = JsonSerializer.Deserialize<GameInfo>(msg.GetProperty("data").ToString());
                        Console.WriteLine(msg.GetProperty("data").ToString());
                        stage = ddzdata.stage;
                        selectedPokers = new int?[55];
                        if (playTips.Any())
                            playTips.RemoveRange(0, playTips.Count);
                        playTipsIndex = 0;

                        if (this.stage == "游戏结束")
                        {
                            foreach (var ddz in this.ddzdata.players)
                            {
                                if (ddz.id == player.Nick)
                                {
                                    this.player.Score += ddz.score;
                                    break;
                                }
                            }

                        }

                        if (this.ddzdata.operationTimeoutSeconds > 0 && this.ddzdata.operationTimeoutSeconds < 100)
                            await this.operationTimeoutTimer();
                    }
                    else if (element.GetString() == "chanmsg")
                    {
                        currentChannel.msgs.Insert(0, msg);
                        if (currentChannel.msgs.Count > 120)
                            currentChannel.msgs.RemoveRange(100, 20);
                    }

                }
                Console.WriteLine("onmessage_end");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"onmessage_ex_{ex.Message}_{msgobjer}");
        }

    }

  在html5版中有使用setTimeout来刷新用户的等待操作时间,我们可以通过一个折中方法实现

private CancellationTokenSource timernnnxx { get; set; }

    //js setTimeout
    private async Task setTimeout(Func<Task> action, int time)
    {
        try
        {
            timernnnxx = new CancellationTokenSource();
            await Task.Delay(time, timernnnxx.Token);
            await action?.Invoke();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"setTimeout_{ex.Message}");
        }

    }
    private async Task operationTimeoutTimer()
    {
        Console.WriteLine("operationTimeoutTimer_" + this.ddzdata.operationTimeoutSeconds);
        if (timernnnxx != null)
        {
            timernnnxx.Cancel(false);
            Console.WriteLine("operationTimeoutTimer 取消");
        }
        this.ddzdata.operationTimeoutSeconds--;
        StateHasChanged();
        if (this.ddzdata.operationTimeoutSeconds > 0)
        {
            await setTimeout(this.operationTimeoutTimer, 1000);
        }
    }

  其他组件相关如数据绑定,事件处理,组件参数等等推荐直接看文档。

  浏览器下载的部分资源如下

 

Docker swarm 实战-部署wordpress

Docker swarm 实战-部署wordpress

Docker swarm 实战-部署wordpress

创建一个overlay的网络

docker network create -d overlay demo

6imq8da3vcwvj2n499k4bwdlt
docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
feea5ba8507b        bridge              bridge              local
6imq8da3vcwv        demo                overlay             swarm
84e129614ea7        docker_gwbridge     bridge              local
d1b0002bf8c8        host                host                local
dgfigrlal70j        ingress             overlay             swarm
633e169e521c        none                null                local

在swarm集群中,创建一个网络之后并不会立即同步到其他节点中,只有当该网络被某个服务使用时,才会同步过去。

创建一个mysql服务

docker service create --name mysql --env MYSQL_ROOT_PASSWORD=root --env MYSQLDATABASE=wordpress --network=demo --mount type=volume,source=mysql-data,destination=/var/lib/mysql mysql:5.7

ep1vpcjhsevqk6s8qti0m3voc
overall progress: 1 out of 1 tasks
1/1: running   [==================================================>]
verify: Service converged
docker service ls

ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
ep1vpcjhsevq        mysql               replicated          1/1                 mysql:5.7
docker service ps mysql
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE           ERROR               PORTS
rzl03fvgyjkv        mysql.1             mysql:latest        swarm-manager       Running             Running 2 minutes ago
docker ps

CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
34aae70f6c30        mysql:5.7        "docker-entrypoint.s…"   4 minutes ago       Up 4 minutes        3306/tcp            mysql.1.rzl03fvgyjkvruwfcylsprwub

创建一个wordpress服务

docker service create --name wordpress -p 80:80 --network=demo --env WORDPRESS_DB_PASSWORD=root --env WORDPRESS+DB_HOST=mysql wordpress

7w40cbn1clnd3i5zxaweyf726
overall progress: 1 out of 1 tasks
1/1: running   [==================================================>]
verify: Service converged
docker service ls

ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
ep1vpcjhsevq        mysql               replicated          1/1                 mysql:latest
7w40cbn1clnd        wordpress           replicated          1/1                 wordpress:latest    *:80->80/tcp
docker service ps wordpress

ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE           ERROR                       PORTS
yknadxszq302        wordpress.1         wordpress:latest    swarm-worker1       Running             Running 6 seconds ago

浏览器访问

wordpress.png

Docker 与 WasmEdge 合作,发布 WebAssembly 支持

Docker 与 WasmEdge 合作,发布 WebAssembly 支持

在 KubeCon NA 2022 的 Cloud native Wasm day 活动上, Docker 与 CNCF 的 WasmEdge Runtime 项目发布了 Docker+Wasm 技术预览。 只需一个命令 docker compose up, 上千万的 Docker 开发者可以立即构建、共享并运行一个完整的 Wasm 应用。

Wasm 最初是作为 Web 浏览器中的安全沙箱而开发的。近年来,它在服务器端作为 VM 和 Linux 容器 (LXC) 的安全、轻量级、快速和可移植的替代方案,有了很多应用程序。Linux 容器这一领域最初由 Docker 开创。

Docker+Wasm 中的标准 demo 应用是由 Second State 提供的。这是一个数据库驱动的 Web 应用程序,它有一个用于整个 Web 服务(微服务)的 WasmEdge “容器”,以及两个用于支持服务的 Linux 容器:一个用于 MySQL 数据库,一个用于 Niginx,为前端 UI 提供静态 HTML 页面。 这三个容器在同一个网络中并排运行并形成一个应用程序。 微服务用 Rust 编写并编译成 Wasm。 它有一个高性能(non-blocking)HTTP 服务器、一个事件处理程序(用于处理 HTTP 请求的业务逻辑)和一个 MySQL 数据库客户端。 整个“容器化”的 Wasm 微服务只有 3MB,而数据库和 Nignix 的 Linux 容器则需要好几百 MB。

Docker Compose 不仅将这些 Wasm 应用程序与侧容器一起运行,而且还将 Rust 源代码构建到 Wasm 中。 开发者甚至不需要安装 Rust 编译器工具链,因为整个构建环境也由 Docker 容器化。 Docker+Wasm 是真正的端到端解决方案。

随着 Docker 开启了引领了云原生时代的容器革命,Docker 在当今的“多运行时”世界支持 Wasm 的承诺,重要性愈发凸显。

Docker+Wasm 的发布非常有意义。 我们不再生活在单一 runtime 的世界中:有 linux 容器、Windows 容器和 Wasm 容器。 OCI 可以将它们全部打包,我应该能够使用 docker 构建和运行它们。 — Solomon Hykes, Docker 联合创始人。

Docker+Wasm 背后的技术主要来自开源社区。 例如,Docker 依赖于一个名为 runwasi 的容器 shim,最初 由 Microsoft 的 DeisLabs 创建, 来启动 WasmEdge 并执行 Wasm 程序。

深耕开源的可能远远不止 Docker 一家。 例如,红帽团队已经将 Wasm 运行时支持集成到 OCI 运行时 crun 中。 这使得整个 Kubernetes 堆栈能够无缝支持 WasmEdge 应用程序。 事实上,Liquid Reply 团队在 KubeCon 大会几天前用 WasmEdge 演示了 Podman+Wasm。

在 KubeCon 活动中展示的其他 Wasm 应用程序包括 AI 推理应用程序、基于 Dapr 的微服务和 streaming pipeline 中的数据处理函数。 现在有了 Docker+Wasm,开发者可以轻松构建、共享和运行这些应用程序。

关于使用 Docker + WasmEdge 运行 WordPress | WebAssembly:无需容器的 Docker 的介绍现已完结,谢谢您的耐心阅读,如果想了解更多关于31. docker swarm 通过 service 部署 wordpress、Blazor WebAssembly 中的依赖注入、认证授权、WebSocket、JS互操作等(原标题 Blazor(WebAssembly) + .NETCore 实现斗地主)、Docker swarm 实战-部署wordpress、Docker 与 WasmEdge 合作,发布 WebAssembly 支持的相关知识,请在本站寻找。

本文标签: