Edge.js:让.NET 和 Node.js 代码比翼齐飞

Tomasz Janczuk Explains Edge.js

阅读数:9299 2013 年 9 月 16 日

通过 Edge.js 项目,你可以在一个进程中同时运行 Node.js 和.NET 代码。在本文中,我将会论述这个项目背后的动机,并描述 Edge.js 提供的基本机制。随后将探讨一些 Edge.js 应用场景,它在这些场景中可以为你开发 Node.js 程序提供帮助。

为何要使用 Edge.js?

虽然许多应用程序只能用 Node.js 编写,不过有些情况下又需要综合 Node.js 和.NET 两者的优点。基于以下几个理由,你想要在程序中使用.NET 和 Node.js:.NET 框架和 NuGet 包提供了一个丰富的功能生态系统,它很好地补充了 Node.js 和 NPM 模块;可能你希望在 Node.js 程序中重用某些现成的.NET 组件;也可能想使用多线程 CLR 运行 CPU 密集型的计算,而这绝非是单线程的 Node.js 所擅长的;又或者你可能优先选择使用.NET 框架和 C#而不是使用 C/C++ 编写原生的 Node.js 扩展来访问那些尚未通过 Node.js 暴露的操作系统机制。

一旦你决定在程序中使用 Node.js 和.NET,那么你必须将 Node.js 和.NET 的组件用进程壁垒将两者分离开来,并建立某种形式的进程间通信的机制,比如说 HTTP:

Edge.js 提供另一种类似的组建异构系统的方式。它允许你在单一进程中同时运行 Node.js 和.NET 代码,并且提供了 V8 和 CLR 之间的互操作机制。

使用 Edge.js 可以在一个进程中运行 Node.js 和.NET,而不用将其分割为两个进程,这样有两个主要的好处:更好的性能和更低的复杂性。

某个场景的性能测试显示,从 Node.js 向 C#发出的进程内 Edge.js 请求比两个进程间通过 HTTP 发送的相同请求快 32 倍。与两个进程和进程间的通信信道相比,只处理一个单独的进程,明显降低了你需要解决的部署和维护的复杂性。

.NET 欢迎 Node.js

接下来我将用一个基础实例讲解 Edge.js 的关键概念,这个例子是从 Node.js 向 C#发送请求。

第 1 行引入事先从 NPM 安装的 edge 模块。Edge.js 是一个原生的 Node.js 组件。Edge.js 的特殊之处在于,它被加载的时候便在 node.exe 进程内部开始代管 CLR。

edge 模块暴露了一个名为 func 的单函数。在高层次上,该函数以 CLR 代码为参数,然后返回一个 JavaScript 函数作为 CLR 代码的代理。func 函数接受多种格式的 CLR 代码,从源代码,文件名,到预编译的 CLR 都可以。在上面的 3-8 行中,程序指定了一个异步的 Lambda 表达式作为 C#文本代码。Edge.js 提取出那段代码并将其编译为内存中的 CLR 程序集。然后它围绕着第 3 行的 CLR 代码(分配给 hello 变量的)创建并返回了一个 JavaScript 代理函数。需要注意的是,这个编译过程在每次调用 edge.func 函数时都会执行一次并将结果缓存。此外,如果你用同样的字符串变量调用 edge.func 函数两次,那么就会从缓存中获得相同的 Func<object,Task<object>> 实例。

Edge.js 创建的 hello 函数是 C#代码的代理函数,它在第 10 行由标准的 Node.js 异步模式调用。这个函数接收一个单独参数(Node.js 字符串),并且还有一个接收错误和返回结果的回调函数。输入的参数在第 4 行被传递到 C#异步 Lambda 表达式中,这个表达式在第 6 行将传入值附加到“.NET welcomes”字符串之后。当调用第 10 行的 JavaScript 回调函数的时候,这个 C#中新构造的字符串被 Edge.js 作为 result 参数传递进去。JavaScript 回调函数则将其打印在控制台上:“.NET welcomes Node.js”。

Edge.js 提供了一套进程内 Node.js 和.NET 代码之间规范的互操作模型。它不允许 JavaScript 直接调用任何 CLR 函数。CLR 函数必须是一个 Func<object,Task<object>> 委托。这种机制为 Node.js 和.NET 互相传递数据提供了足够的灵活性。同时,它需要.NET 代码异步执行,以便于和单线程的 Node.js 代码自然地集成在一起。这是 Func<object,Task<object>> 委托如何映射于 Node.js 异步模型概念:

互操作模式并不禁止你访问.NET framework 的任何部分,但是它往往会要求你额外编写一个适配器层以暴露所需的.NET 功能如同 Func<object,Task<object>> 委托。这个适配器层要求你明确地定位.NET 中的阻塞 APIs 的问题所在,它可能将这些运算运行在 CLR 线程池中以避免阻塞 Node.js 事件循环。

数据和功能

虽然 Edge.js 仅仅允许你在 Node.js 和.NET 之间传递一个参数,但是这个参数可能是个复杂类型的。当从 Node.js 请求.NET 代码的时候,Edge.js 可以封送(marshal)所有标准的 JavaScript 类型:从基类型到对象和数组。当从.NET 向 Node.js 传递数据的时候,Edge.js 不但可以封送所有的基本 CLR 类型,而且还可以处理 CLR 对象实例、列表、集合和字典类型。从概念上讲,你可以认为在 V8 和 CLR 之间的数据传递就像是在一个环境中将数据序列化为 JSON,而在另一个环境中对 JSON 进行反序列化。但是,Edge.js 并没有在进程中进行实际的 JSON 序列化过程。相反,它直接在内存中进行 V8 和 CLR 类型系统之间的数据封送,而省略了字符串型中间代码,这个过程远比 JSON 序列化和反序列化更加高效。

Edge.js 通过值进行数据封送,所以当执行过程跨越 V8/CLR 边界时,它会在 V8 或者 CLR 的堆中另外创建一份数据拷贝。这个规则有一处显著的例外:与通过值进行数据封送不同,Edge.js 通过引用来封送函数。让我们通过下面这个例子来说明这个强有力的概念:

在这个例子中,Node.js 调用 addAndMultiplyBy2 的 C#中运行的函数。这个函数获取两个数字,而后返回它们总和的 2 倍。鉴于这个例子的目的,我们假设 C#知道如何做加法但是却并不清楚如何做乘法。C#代码在计算和之后需要回调至 JavaScript 以进行乘法运算。

为了实现这个场景,Node.js 应用程序在第 18-20 行定义一个 multiplyBy2 函数,并在第 23 行调用 addAndMultiplyBy2 函数时将其随同两个运算对象传递至 C#代码。注意 multiplyBy2 函数是如何满足 Edge.js 规范的互操作模式的。这使得 Edge.js 可以在给 multiplyBy2 这个 JavaScript 函数创建.NET 代理,就像是.NET 中的 Func<object,Task<object>> 委托。这个 JavaScript 函数代理接下来被 C#代码在第 10 行调用,用于对第 8-9 行中得到的和执行乘法运算。

遵守规范的互操作模式的函数也可以从.NET 被封送到 Node.js。能够在 V8 和 CLR 中双向封送函数是很强有力的概念,尤其是当掺杂着闭包的时候更是如此。请看下面这个例子:

在第 1-7 行,Edge.js 创造了一个 JavaScript 函数 createCounter,这个是 C# Lambda 表达式的代理。第 9 行中传给 createCounter 函数的的参数在第 3 行被强制转化为一个 C#的本地变量。第 4-5 行的代码比较有趣:C#异步 Lambda 表达式的结果是一个 Func<object,Task<object>> 型的委托实例,它(第 5 行)的实现包含了第 3 行在闭包中定义的本地变量。当 Edge.js 将这个 Func<object,Task<object>> 实例封送为 JavaScript 函数回传给 Node.js,并将其分配给第 9 行的 counter 变量的时候,这个 JavaScript 的 counter 函数有效的涵盖了 CLR 状态下的闭包。这点在第 10-11 行得到了充分的证明。这两行两次调用 counter 函数,结果返回的是一个不断增加的值。这是由于每次调用第 5 行实现的 Func<object,Task<object>> 都会使得第 3 行的本地变量的数值增加。

在 V8 和 CLR 之间封送函数的能力加上闭包的概念是个很强有力的机制。这样.NET 代码就能够暴露 CLR 对象的功能给 Node.js。第三行的本地变量在最后的例子中是一个Person 类的实例。

让我们一起动手

我们来看几个实际的例子以便了解如何在 Node.js 应用程序中使用 Edge.js。

Node.js 是单线程的架构。如果要保持响应性,那么应用程序中就不能执行阻塞的代码。大部分 Node.js 程序都是在进程外执行 CPU 密集型的运算。外部进程通常使用的技术并不是 Node.js。Edge.js 使得这种场景非常容易实现。它允许你的 Node.js 程序在 Node.js 进程内部的 CLR 线程池中执行 CPU 密集型的逻辑运算。当 CPU 密集型的计算在 CLR 线程池的线程中运行时,V8 线程上的 Node.js 程序仍然是可响应的。一旦 CPU 密集型操作结束,Edge.js 同步线程就在 V8 线程上执行 JavaScript 回调函数。请看这个使用.NET 功能转换图片格式的例子:

convertImageToJpg 函数使用了.NET 中的 System.Drawing 的功能将 PNG 图片转换为 JPG 格式。这是计算密集型的操作,因此第 6 行创建的 C#实现(implementation)调用了 Task.Run 在 CLR 线程池中运行这个转换。当计算执行的时候,进程中的单例(singleton)V8 线程可以处理后续的事件。C#代码随第 6 行的 await 关键字而等待图片转换的完成。只有在图片转换完成之后,convertImageToJpg 在 V8 线程上执行第 14-15 行 JavaScript 回调代码,整个函数才算完成。

另一个让 Edge.js 大显身手的例子是在 MS SQL 中读取数据。现在 Node.js 开发者还没有什么读取 MS SQL 数据的方法可以比.NET Framework 中的 ADO.NET 更加完善和成熟。Edge.js 提供给你一个简单的在 Node.js 程序中利用 ADO.NET 的方法。请看下这个 Node.js 程序:

在第 1 行中,Edge.js 通过编译 sql.csx 文件中的 ADO.NET 代码创建了 sql 函数。这个 sql 函数接受一个 T-SQL 命令构成的字符串,并使用 ADO.NET 异步执行它,然后将结果返回给 Node.js。sql.csx 文件用 C#编写了不到 100 行的 ADO.NET 代码,它支持对 MS SQL 数据库执行 CRUD 四种操作:

在 sql.csx 文件中的实现(implementation)使用异步 ADO.NET 的 API 来访问 MS SQL 数据并执行 Node.js 传给它的 T-SQL 命令。

上面的两个例子仅仅代表了 Edge.js 帮你编写 Node.js 程序的一小部分场景。更多的例子可以参见Edge.js 的GitHub 站点。

路线图

Edge.js 是一个遵循 Apache 2.0 协议的开源项目。它目前的开发很活跃,欢迎前来贡献代码。你可以用你的时间和经验来检查工作项目列表。

尽管本文中所有的例子都是使用 C#写的,Edge.js 支持在 Node.js 程序中运行任何 CLR 语言的代码。目前的扩展提供了对脚本语言 F# Python PowerShell 的支持。通过语言扩展模型你能很容易的添加其他CLR 语言的编译器。

Edge.js 目前需要.NET Framework 环境,因此只能运行在 Windows 上。但是对 Mono 的支持也在积极的开发中,不久就可以在 MacOS 和 *nix 上运行 Edge.js 程序了。

关于作者

Tomasz Janczuk 是微软的一名软件工程师。他目前主要关注 Node.js 和 Windows Azure。在此之前他从事.NET Framework 和网络服务(web services)方面的工作。业余时间里,他在太平洋等地参加了很多户外活动。你可以在 Twitter 上关注他, @tjanczuk ,也可以访问他的GitHub 页面或者阅读他的博客以获得更多的资讯。

查看英文原文: Run .NET and Node.js code in-process with Edge.js


感谢侯伯薇对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论