Kun

Kun

IT学徒、技术民工、斜杠青年,机器人爱好者、摄影爱好 PS、PR、LR、达芬奇潜在学习者


共 212 篇文章


  前端进阶(十一)-WebAssembly及微服务

WebAssembly

编译C/C++为WebAssembly

当你在用C/C++之类的语言编写模块时,你可以使用Emscripten来将它编译到WebAssembly。让我们来看看它是如何工作的。

Emscripten 环境安装

接下来,您需要通过源码自己编译一个Emscripten。运行下列命令来自动化地使用Emscripten SDK。(在你想保存Emscripten的文件夹下运行

git clone https://github.com/juj/emsdk.git
cd emsdk

# 在 Linux 或者 Mac OS X 上
./emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit
./emsdk activate --global --build=Release sdk-incoming-64bit binaryen-master-64bit
# 如果在你的macos上获得以下错误
Error: No tool or SDK found by name 'sdk-incoming-64bit'
# 请执行
./emsdk install latest
# 按照提示配置环境变量即可
./emsdk activate latest


# 在 Windows 上
emsdk install --build=Release sdk-incoming-64bit binaryen-master-64bit
emsdk activate --global --build=Release sdk-incoming-64bit binaryen-master-64bit

# 注意:Windows 版本的 Visual Studio 2017 已经被支持,但需要在 emsdk install 需要追加 --vs2017 参数。

安装过程可以会花上一点时间,是时候去休息一下。安装程序会设置所有Emscripten运行所需要的环境变量。

每当您想要使用Emscripten时,尝试从远程更新最新的emscripten代码是个很好的习惯(运行 git pull)。如果有更新,重新执行 install 和 activate 命令。这样就可以确保您使用的Emscripten一直保持最新。

现在让我们进入emsdk文件夹,输入以下命令来让你进入接下来的流程,编译一个样例C程序到asm.js或者wasm。

# on Linux or Mac OS X
source ./emsdk_env.sh

# on Windows
emsdk_env.bat

示例:编译C

创建一个c文件(hello.c)

#include <stdio.h>

int main(int argc, char ** argv) {
  printf("Hello World\n");
}

现在,转到一个已经配置过Emscripten编译环境的终端窗口中,进入刚刚保存hello.c文件的文件夹中,然后运行下列命令:

emcc hello.c -s WASM=1 -o hello.html

下面列出了我们命令中选项的细节:

  • -s WASM=1 — 指定我们想要的wasm输出形式。如果我们不指定这个选项,Emscripten默认将只会生成asm.js
  • -o hello.html — 指定这个选项将会生成HTML页面来运行我们的代码,并且会生成wasm模块,以及编译和实例化wasm模块所需要的“胶水”js代码,这样我们就可以直接在web环境中使用了。

这个时候在您的源码文件夹应该有下列文件:

  • hello.wasm 二进制的wasm模块代码
  • hello.js 一个包含了用来在原生C函数和JavaScript/wasm之间转换的胶水代码的JavaScript文件
  • hello.html 一个用来加载,编译,实例化你的wasm代码并且将它输出在浏览器显示上的一个HTML文件

现在使用一个支持 WebAssembly 的浏览器,加载生成的 hello.html

如果一切顺利,你应该可以在页面上的 Emscripten 控制台浏览器控制台 中看到 "Hello World" 的输出。

恭喜!你已经成功将 C 代码编译成 JavaScript 并且在浏览器中执行了!

自定义html模板

首先,在一个新文件夹中保存以下 C 代码到 hello2.c 中:

#include <stdio.h>

int main(int argc, char ** argv) {
    printf("Hello World\n");
}

在 emsdk 中搜索一个叫做 shell_minimal.html 的文件,然后复制它到刚刚创建的目录下的 html_template 文件夹。

mkdir html_template
cp ~/emsdk/emscripten/1.38.15/src/shell_minimal.html html_templates

现在使用你的Emscripten编译器环境的终端窗口进入你的新目录, 然后运行下面的命令:

emcc -o hello2.html hello2.c -O3 -s WASM=1 --shell-file html_template/shell_minimal.html
  1. 这次使用的选项略有不同:

    • 我们使用了 -o hello2.html ,这意味编译器将仍然输出 js 胶水代码 和 html 文件。
    • 我们还使用了 --shell-file html_template/shell_minimal.html ,这指定了您要运行的例子使用 HTML 页面模板。
  2. 下面让我们来运行这个例子。上面的命令已经生成了 hello2.html,内容和我们使用的模板非常相像,只不过多加了一些 js 胶水和加载wasm文件的代码。 在浏览器中打开它,你会看到与上一个例子相同的输出。

调用一个C语言中的函数

如果需要调用一个在 C 语言自定义的函数,你可以使用 Emscripten 中的 ccall() 函数,以及 EMSCRIPTEN_KEEPALIVE 声明 (将你的函数添加到导出函数列表中)

还是首先创建一个C语言文件

#include <stdio.h>
#include <emscripten/emscripten.h>

int main(int argc, char ** argv) {
    printf("Hello World\n");
}

#ifdef __cplusplus
extern "C" {
#endif

int EMSCRIPTEN_KEEPALIVE myFunction(int argc, char ** argv) {
  printf("我的函数已被调用\n");
}

#ifdef __cplusplus
}
#endif

默认情况下,Emscripten 生成的代码只会调用 main() 函数,其它的函数将被视为无用代码。在一个函数名之前添加 EMSCRIPTEN_KEEPALIVE 能够防止这样的事情发生。你需要导入 emscripten.h 库来使用 EMSCRIPTEN_KEEPALIVE

为了方便起见,现在将 html_template/shell_minimal.html 也添加到这一目录(但在实际开发环境中你肯定需要将其放到某一特定位置)

运行以下命令编译:(注意由于使用ccall函数,需要添加指定参数)

emcc -o hello3.html hello3.c -O3 -s WASM=1 -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']" --shell-file html_template/shell_minimal.html

如果你在浏览器中在此加载实例,你将看到和之前相同的结果。

现在我们需要运行新的 myFunction() JavaScript 函数。首先,按照以下实例添加一个button, 就在 <script type='text/javascript'> 开头标签之前。

<button class="mybutton">运行我的函数</button>
<script>
  document.querySelector('.mybutton').addEventListener('click', function(){
  alert('检查控制台');
  var result = Module.ccall('myFunction', // name of C function
                             null, // return type
                             null, // argument types
                             null); // arguments
});
</script>

以上就是如何使用 ccall() 调用导出的函数的方式。

微服务

微服务是一种应用架构,它将每个应用功能都放在自己的服务中,与其他服务隔离。这些服务是松散耦合的,可独立部署。

这种架构的出现是为了解决旧的 Web 应用开发的单体方法。在单体软件中,所有的东西都是作为一个单元构建的,所有的业务逻辑都被归入一个广泛的应用。

这种方法使更新代码库的过程变得复杂化,因为它影响到整个系统,即使是最小的代码改动也需要构建和部署整个软件的新版本。此外,哪怕你只想扩展应用的某个特定功能,却需要扩展整个应用来实现它。

微服务解决了单体系统所面临的这些挑战,它将应用从一个整体分割成几个小部分

从本质上讲,微服务架构解决了庞大、复杂应用的快速开发问题。

对于“哪个更好?”这一问题,目前还没有通用的答案。答案取决于各种情况,因为每一种情况都有其好处和缺点。

下面是一些微服务架构的优点和缺点:

优点

  • 语言不可知性:微服务并不限于特定的编程语言,每个微服务都可以用不同的语言来编写,以支持选定的通信协议。
  • 可扩展性:由于微服务和它的职责可以由开发者共同承担,所以如果有一个大的团队参与到这个项目中,应用就会变得更加易于维护。
  • 无限迭代:由于开发者不会被其他组件所束缚,所以在微服务上迭代会变得更加简单。
  • 单元测试:由于微服务是独立的应用,它的重点是特定的功能,因此,开发者可以很轻松地编写测试脚本,以验证该特定功能。

缺点

要作为一个整体来管理是很困难的:凯撒大帝有一句名言“分而治之”(divide et impera,拉丁语),即使在这里也可以大规模应用,但是要谨慎,因为过多的活动部分会变得难以管理。

  • 难以追踪:如果架构变得过于复杂,微服务之间的通信渠道会非常多,出现错误后会很难追溯并确定故障点。
  • 需要大量的专业知识:构建和部署微服务要求非常高的计划和协调方面的软技能。
  • 具有挑战性的测试:测试是一把双刃剑,因为微服务作为一个整体更难测试。集成和端到端的测试同样会有挑战。
  • 审计日志:可能更难获得和调查。

在架构方面,SaaS 微服务非常适合,因为微服务是 SaaS 应用的一个不错的选择。由于这类应用想要用户付钱买单,那么它就需要提供高可用的服务,因此将软件分成小块可以加快恢复速度。同时,SaaS 应用的发展主要是由其社区推动,所以,它也会受到很多变化的影响,而通过微服务和解耦,开发者可以获得了灵活性,这是单体架构无法提供的。

单体应用程序可能难以水平扩展,因为你必须复制整个应用程序,如果它依赖于单个数据库,这个过程将变得更加困难。另一边,微服务却可以根据单个服务进行扩展、复制或负载平衡。比如,如果你需要发送更多的电子邮件,你只需要扩展负责电子邮件功能的微服务。今天你有 10 个用户,明天你有 1000 个;SaaS 应用可以在短时间内维持大规模的增长,这就是为什么他们的架构必须要以最经济的方式进行轻松扩展的原因。

这样还可以减少资源的消耗,因此可以减少账单。所以,可以肯定地说,微服务是 SaaS 企业架构的下一个阶段。

由于服务之间彼此独立,所以与微服务的通信需要好好选择。通信协议的使用不当会造成应用的性能下降,大家必须根据自己应用的具体需求来选择通信协议。

有两种通信方式可以选择:同步通信和异步通信,这是请求 - 响应和基于事件的模式的基础。

在第一种情况下,即同步方式,客户端发送请求并等待响应。这种方法有一个缺陷,那就是它是一个阻塞模式。但是,如果你有一个读操作非常多的应用时,那就不一定了,因为你的应用更倾向从外部读取和接受信息。在这种情况下,使用同步方式可能是一个很好的选择,特别是当它涉及实时数据时。

我们的另一个选择是异步通信,这是一个非阻塞模式。如果你想要一种有弹性的微服务,那么,与同步通信相比,异步通信是一种更好的选择。在这种情况下,客户端会发送一个请求,收到请求的确认,并将其遗忘。这种方法最适用于大量写操作、无法承受数据记录丢失的应用。

下面是一些涉及微服务通信的解决方案,你可以从中选择:

  • 基于 HTTP 的 REST
  • 基于 HTTP/2 的 REST
  • WebSocket
  • TCP 套接字
  • UDP 数据包

好好考虑最适合自身需求的通信协议,因为这将使应用响应更快、效率更高。

Nodejs

在构建微服务时,有很多顶级编程语言可供选择。NodeJS 就是其中之一。那么,为什么 NodeJS 是最佳选择呢?

  • 单线程 & 异步:NodeJS 使用事件循环来执行代码,允许异步代码被执行,从而使服务器能够使用非阻塞机制来响应。
  • 事件驱动:NodeJS 使用事件驱动架构,该架构建立在软件开发的常见模式上,被称为发布 - 订阅或观察者模式,能够构建强大的应用,尤其是实时应用。
  • 快速和高度的可扩展性:运行环境建立在最强大的 JavaScript 引擎之一 V8 JavaScript Engine 之上,因此代码执行速度快,使得服务器能够同时处理多达 10000 个并发请求。
  • 易于开发:创建多个微服务会导致重复的代码。Node.js 的微服务框架很容易创建,因为它抽象了大部分的底层系统。所以用这种编程语言创建一个微服务可以像写几行代码一样简单。

实现微服务架构

我们从创建用于用户管理的微服务开始,它将使用 TCP 数据包进行通信,并负责对用户进行 CRUD 操作。我们将使用 PacketSender 对其进行测试,PacketSender 是一个免费的工具,用于发送支持 TCP 的网络数据包。

微服务的架构和作用域被进一步界定。因此,从演示的角度来看,通过 HTTP 实现一个微服务与实现 NodeJS API 没有什么不同。

同时,通过 HTTP 来使用 REST 也很容易,但如果从这个协议切换到其他协议时,会出现一些问题。这也是本文中我们将会使用 TCP 包的异步模式来与微服务通信的原因。

我们将使用 NestJS 作为应用的框架。它并非 NodeJS 微服务框架,而是一个用于构建服务器端应用的框架。但是,由于其内置了多个微服务特性,使得工作变得更加容易。

nestjs微服务

使用nestjs构建微服务框架

安装

npx @nestjs/cli new user-microservice

初始化项目

npm i --save @nestjs/microservices

让微服务启动和运行,我们需要用以下内容更新 main.ts 文件

import { INestMicroservice } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';

async function bootstrap() {
  const microservicesOptions: any = {
    transport: Transport.TCP,
    options: {
      host: '127.0.0.1',
      port: 8875,
    },
  };

  const app: INestMicroservice = await NestFactory.createMicroservice(
    AppModule,
    microservicesOptions,
  );

  app.listen(() => console.log('Microservice is listening'));
}
bootstrap();

seneca

使用express的微服务框架

安装

npm install seneca

使用

'use strict'

var Seneca = require('seneca')


// Functionality in seneca is composed into simple
// plugins that can be loaded into seneca instances.


function rejector () {
  this.add('cmd:run', (msg, done) => {
    return done(null, {tag: 'rejector'})
  })
}

function approver () {
  this.add('cmd:run', (msg, done) => {
    return done(null, {tag: 'approver'})
  })
}

function local () {
  this.add('cmd:run', function (msg, done) {
    this.prior(msg, (err, reply) => {
      return done(null, {tag: reply ? reply.tag : 'local'})
    })
  })
}


// Services can listen for messages using a variety of
// transports. In process and http are included by default.


Seneca()
  .use(approver)
  .listen({type: 'http', port: '8260', pin: 'cmd:*'})

Seneca()
  .use(rejector)
  .listen(8270)


// Load order is important, messages can be routed
// to other services or handled locally. Pins are
// basically filters over messages


function handler (err, reply) {
  console.log(err, reply)
}

Seneca()
  .use(local)
  .act('cmd:run', handler)

Seneca()
  .client({port: 8270, pin: 'cmd:run'})
  .client({port: 8260, pin: 'cmd:run'})
  .use(local)
  .act('cmd:run', handler)

Seneca()
  .client({port: 8260, pin: 'cmd:run'})
  .client({port: 8270, pin: 'cmd:run'})
  .use(local)
  .act('cmd:run', handler)


// Output
// null { tag: 'local' }
// null { tag: 'approver' }
// null { tag: 'rejector' }

此外,后续还需要安装Web插件以及用于Express的适配器

npm i --save seneca-web
npm i --save seneca-web-adapter-express

此外,还需要操作数据库的插件

npm i --save seneca-entity

https://zhuanlan.zhihu.com/p/361134769

golang

https://serviceweaver.dev/docs.html

weaver

安装

go install github.com/ServiceWeaver/weaver/cmd/weaver@latest
如果你觉得我的文章对你有帮助的话,希望可以推荐和交流一下。欢迎關注和 Star 本博客或者关注我的 Github