前端进阶(十一)-WebAssembly及微服务
当你在用C/C++之类的语言编写模块时,你可以使用Emscripten来将它编译到WebAssembly。让我们来看看它是如何工作的。
接下来,您需要通过源码自己编译一个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文件(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
这次使用的选项略有不同:
-o hello2.html
,这意味编译器将仍然输出 js 胶水代码 和 html 文件。--shell-file html_template/shell_minimal.html
,这指定了您要运行的例子使用 HTML 页面模板。调用一个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 企业架构的下一个阶段。
由于服务之间彼此独立,所以与微服务的通信需要好好选择。通信协议的使用不当会造成应用的性能下降,大家必须根据自己应用的具体需求来选择通信协议。
有两种通信方式可以选择:同步通信和异步通信,这是请求 - 响应和基于事件的模式的基础。
在第一种情况下,即同步方式,客户端发送请求并等待响应。这种方法有一个缺陷,那就是它是一个阻塞模式。但是,如果你有一个读操作非常多的应用时,那就不一定了,因为你的应用更倾向从外部读取和接受信息。在这种情况下,使用同步方式可能是一个很好的选择,特别是当它涉及实时数据时。
我们的另一个选择是异步通信,这是一个非阻塞模式。如果你想要一种有弹性的微服务,那么,与同步通信相比,异步通信是一种更好的选择。在这种情况下,客户端会发送一个请求,收到请求的确认,并将其遗忘。这种方法最适用于大量写操作、无法承受数据记录丢失的应用。
下面是一些涉及微服务通信的解决方案,你可以从中选择:
好好考虑最适合自身需求的通信协议,因为这将使应用响应更快、效率更高。
在构建微服务时,有很多顶级编程语言可供选择。NodeJS 就是其中之一。那么,为什么 NodeJS 是最佳选择呢?
实现微服务架构
我们从创建用于用户管理的微服务开始,它将使用 TCP 数据包进行通信,并负责对用户进行 CRUD 操作。我们将使用 PacketSender 对其进行测试,PacketSender 是一个免费的工具,用于发送支持 TCP 的网络数据包。
微服务的架构和作用域被进一步界定。因此,从演示的角度来看,通过 HTTP 实现一个微服务与实现 NodeJS API 没有什么不同。
同时,通过 HTTP 来使用 REST 也很容易,但如果从这个协议切换到其他协议时,会出现一些问题。这也是本文中我们将会使用 TCP 包的异步模式来与微服务通信的原因。
我们将使用 NestJS 作为应用的框架。它并非 NodeJS 微服务框架,而是一个用于构建服务器端应用的框架。但是,由于其内置了多个微服务特性,使得工作变得更加容易。
使用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();
使用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
https://serviceweaver.dev/docs.html
安装
go install github.com/ServiceWeaver/weaver/cmd/weaver@latest