vlambda博客
学习文章列表

PipyJS - 函数式网络编程语言

这篇文章主要介绍 Pipy[1] 以及强大的 PipyJS - 没有垃圾回收开销、轻量级的函数式 JavaScript 解释引擎。

Pipy 概览

Pipy 是一个开源的[2]、轻量级、高性能、模块化、可编程的云原生网络流处理器,适用于边缘路由、负载均衡和代理解决方案、API 网关、静态 HTTP 服务器、服务网格边车、策略即代码等应用场景。

技术术语有点多,我们继续深入了解一下。

轻量级

Pipy 的可执行文件只有 10MB 左右,没有任何外部依赖,仅需很少的内存就可以运行。

高性能

Pipy 是使用 C++ 并基于异步 I/O 库 Asio 开发的。

模块化

Pipy 的核心使用了模块化设计,提供了大量可复用的小模块(过滤器)。这些过滤器可以组装成管道,网络数据流经这些管道并被处理。

流处理器

Pipy 将网络流数据字节抽象成事件,以事件驱动的方式来操作网络流。提供了事件驱动管道的抽象,管道使用输入流、执行用户定义的转换并输出流。

可编程

Pipy 对外屏蔽了底层的细节,提供了类似拼图的编程方式来实现业务目标。Pipy 是通过强大的 PipyJS 实现的,也是接下来我们要详细介绍的。

为什么使用 JavaScript

就像标题介绍的,PipyJS 是基于 JavaScript 的。Pipy 决定使用类似 JavaScript 脚本语言是有原因的:

JavaScript 可能是世界上最广泛使用的语言[3]它的语法有 C 和 Java 的影子,因此对大多数程序员不陌生Pipy 作为流处理器,意味着有大量的数据转换和重组的工作。通过 JavaScript 提供的很多强大的语法糖可以轻松搞定,比如展开语法[4]解构赋值[5]、以及数组的 map()[6]reduce()[7] 等操作JSON 作为网络上最广泛使用的数据格式,JavaScript 提供了原生支持可以轻松操作最后也是最最重要的,JavaScript 包含了“函数式编程”范式

Pipy 就像是一些彼此相连的管道一样。每个管道由一系列的过滤器组成,每个过滤器就像接收输入并返回特定结果的函数。对于这种设计模式,假如你的目标是保持连贯和简单,函数式编程语言是最适合 Pipy 的。

PipyJS 介绍

Pipy 通过自研的 PipyJS 组件来解析 JavaScript,该组件是 Pipy 代码库的一部分,但不依赖它。

PipyJS 是一个小且可嵌入的 JavaScript 引擎,专为高性能而设计,没有垃圾回收开销。它支持 ECMAScript 标准的子集,并且在某些方面进行了大胆的扩展。Pipy 拥抱纯函数[8]式 JavaScript,在 PipyJS 中,万物皆表达式

数据类型

与标准 JavaScript 一样, PipyJS 支持 4 种基本类型和结构数据的 对象 类型。

基本类型Undefined:变量未初始化时的唯一值 undefinde布尔类型 Boolean:true 和 false数字类型 Number:64 位双精度浮点数,如 1230xabc3.14159261e6字符串类型 String:Unicode 字符序列。结构化数据类型Null 对象:唯一值 null用户定义的 POD(plain old data) ,如 { x: 1.2, y: 300 }内置对象:比如 ArrayRegExp函数:比如 (a, b) => a * a + b * b

操作符

PipyJS 支持所有的标准 JavaScript 操作符,包括一些在 ES2020 才引入的,比如 可选链操作符[9] 和 空值合并运算符[10]

全局变量

全局变量在 PipyJS 中也叫做 上下文变量。这里的上下文概念等同于高并发网络编程中的连接,每个连接都有彼此独立的状态。在 PipyJS 中,为方便起见这些隔离的状态保存在全局变量中,这也是为什么有时我们称其上下文变量。这也很好理解为什么 PipyJS 全局变量与标准 JavaScript 不同,简单说就是在不同的连接中值不相同。这点上,有点像线程本地存储[11]

全局变量通过内置的函数 pipy() 来定义,它通常是脚本中第一个调用的函数。

pipy({
  _myGlobalVariable: undefined
})

为了方便起见,全局变量使用下划线作为变量名的首字符,尽管不是语言强制的。

全局变量的作用范围是单个文件或者模块,多个模块间可通过 export() 和 import() 来共享。

// file A
pipy().export('namespace-1', {
  __myGlobalVariable: undefined
})

// file B
pipy().import({
  __myGlobalVariable: 'namespace-1'
})

为了方便起见,导出的全局变量名使用双下划线开头,但不强制。

本地变量

在 PipyJS 中,我们使用嵌套在局部变量函数范围内的函数参数作为本地变量。

void ((
  x, y, z, // declare local variables as function arguments
) => (
  x = 0,
  y = 0,
  z = 0 // initialize and use the variables in the function body
))() // Don't miss the () to invoke the function right away!

假如想要上面的表达式执行后返回结果,则需要删除开头的 void

分支

在 PipyJS 中,万物皆表达式。不存在代码块或者流程控制,不能写 if 或者 for 语句。但这并不是说就不能写分支和循环了,而只是换了种形式:函数式

简单分支可以使用逻辑运算符 &&

res.status === 200 && (_result = 'OK'console.log('Success.'))

// That's equivalent to:
if (res.status === 200) {
  _result = 'OK';
  console.log('Success.');
}

注意:上面是 PipyJS 分支语法,下面是标准 JavaScript 语法(下同)。

多选择分支可以将逻辑运算符 && 和 || 组合使用。

(res.status === 200) && (
  _result = 'OK'
) ||
(res.status === 404) && (
  _result = 'Not found'
) || (
  _result = ''
)

// That's equivalent to:
if (res.status === 200) {
  _result = 'OK';
else if (res.status === 404) {
  _result = 'Not found';
else {
  _result = '';
}

循环

可以使用 Array.forEach()[12] 来实现简单有界的循环。

new Array(100).fill(0).forEach(
  (_, i) => (
    console.log(i)
  )
)

// That's equivalent to:
for (let i = 0; i < 100; i++) {
  console.log(i);
}

或者,对于通用条件循环,可以使用内置的函数 repeat()

void ((
  n, i
) => (
  n = i = 1,
  repeat(
    () => (
      n *= i,
      i += 1,
      i <= 10
    )
  )
))()

// That's equivalent to:
let n = 1, i = 1;
while (i <= 10) {
  n *= i;
  i += 1;
}

派生 ECMAScript

PipyJS 专为速度设计,其结构对于编写高性能网络流处理逻辑至关重要。下面重点介绍 PipyJS 与标准 ECMAScript 的不同或者未实现的:

面向对象编程(OOP)结构 - 没有用户定义类型或者会构造器,没有原型系统控制流关键字break , case , catch , continue , debugger , default , do , else , finally , function , for , if , return , switch , throw , try , while , with , yield , class , import , export , extends , static , super类型系统BigInt and SymbolStrings 内部存储为 UTF-8,在脚本中可以作为 UTF-32 来访问。比如 "😀".length 在标准 JavaScript 中是 2,但在 PipyJS 中是 1变量 - 没有用来声明变量的关键词 var 或者 let

尽管 PipyJS 派生自标准 ECMAScript,但是通过函数式机制来填补空白。

如何使用

现在你应该对 PipyJS 有了一定的了解,接下来我们看下如何编写代码并运行。

首先下载[13]对应平台的二进制文件,将下面的代码保存为 ”hello.js“。

pipy()

.listen(8080)
  .serveHTTP(
    new Message('Hi, there!\n')
  )

然后使用刚刚下载的二进制文件执行该脚本。

pipy hello.js

2022-02-23 14:14:33.315 [INF] [config]
2022-02-23 14:14:33.316 [INF] [config] Module /hello.js
2022-02-23 14:14:33.316 [INF] [config] ================
2022-02-23 14:14:33.316 [INF] [config]
2022-02-23 14:14:33.316 [INF] [config]  [Listen on 8080 at 0.0.0.0]
2022-02-23 14:14:33.316 [INF] [config]  ----->|
2022-02-23 14:14:33.316 [INF] [config]        |
2022-02-23 14:14:33.316 [INF] [config]       serveHTTP
2022-02-23 14:14:33.316 [INF] [config]        |
2022-02-23 14:14:33.316 [INF] [config]  <-----|
2022-02-23 14:14:33.316 [INF] [config]
2022-02-23 14:14:33.316 [INF] [listener] Listening on port 8080 at 0.0.0.0

这段脚本会监听 8080 端口,并返回信息 Hi, there!

curl http://localhost:8080
Hi, there!

总结

Pipy[14] 是有 Flomesh[15] 开源的、极其快速且轻量级的网络流量处理器,适用于多种场景。Pipy 正由全职的提交者和贡献者积极地开发和维护中,尽管还还是早期的版本,但已经过多个商业客户生产环境的实战测试。它的创建者和维护者 Flomesh.cn[16] 提供了以 Pipy 为核心的商业级解决方案。

这篇文章对 Pipy 进行了简短的概述和高阶的介绍。在 GitHub 页面[17]上可以找到教程和文档,也可以通过 Pipy 的管理控制台界面来访问。Pipy 社区非常欢迎贡献开发、尝试特定场景、或者提供简介和反馈。

引用链接

[1] Pipy: https://github.com/flomesh-io/pipy
[2] 开源的: https://github.com/flomesh-io/pipy
[3] 世界上最广泛使用的语言: https://insights.stackoverflow.com/survey/2020#technology-programming-scripting-and-markup-languages-professional-developers
[4] 展开语法: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Spread_syntax
[5] 解构赋值: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
[6] map(): https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/map
[7] reduce(): https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
[8] 纯函数: https://en.wikipedia.org/wiki/Functional_programming
[9] 可选链操作符: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Optional_chaining
[10] 空值合并运算符: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator
[11] 线程本地存储: https://en.wikipedia.org/wiki/Thread-local_storage
[12] Array.forEach(): https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
[13] 下载: https://github.com/flomesh-io/pipy/releases
[14] Pipy: https://github.com/flomesh-io/pipy
[15] Flomesh: https://flomesh.cn/
[16] Flomesh.cn: https://flomesh.cn/
[17] GitHub 页面: https://github.com/flomesh-io/pipy