Node.js 体验-在Windows Azure工作者角色上托管Node.js

在我的前面的文章中我演示了如何在Windows Azure 网站(即WAWS)上开发和部署Node.js 应用程序。WAWS是Windows Azure 平台中的新功能。因为它是低成本, 同时它提供IIS 和
IISNode 组件,因此我们可以通过Git、 FTP 和WebMatrix托管Node.js 应用程序,而无需任何配置和组件的安装。

但有时我们需要使用 Windows Azure 云计算服务 (即WACS),同时在工作者角色上托管我们 的Node.js。以下是一些使用工作者角色的好处。

-WAWS 利用 IIS 和 IISNode 托管Node.js应用程序,运行在x86 WOW模式。它减少了与x64在某些情况下的性能比较。

-WACS工作者角色不需要 IIS,因此没有任何的 IIS限制,例 如 8000 并发请求限制。

- WACS给开发人员提供更多的灵活性和控制。例如,我们可以RDP 到我们的工作者角色实例的虚拟机。

-当角色运行时WACS提供的服务配置功能可以更改。

-WACS比 WAWS提供了更多的扩展能力。在 WAWS 中,每个网站我们可以有最最多3个 保留的实例,虽然在WACS中,我们可以每次预定最多 20 个实例。

-因此当使用WACS工作者角色时,在进程中由我们自己启动节点,我们可以控制输入、 输出和错误流。我们还可以控制 Node.js 的版本。

在工作者角色中运行Node.js

Node.js 可以通过拥有其可执行文件被启动。这意味着在Windows Azure中,我们可以与"node.exe"和 Node.js 的源文件拥有工作者角色,然后在运行辅助角色入口类方法中启动。

让我们在 Visual Studio 中创建一个新的 windows azure 工程并添加新的工作者角色。由于我们需要我们的工作者角色和应用程序代码执行“node.exe”,因此我们需要添加“node.exe”到我们的工程。右击工作者角色工程,添加现有项。默认情况下Node.js将被安装在“Program Files\nodejs”文件夹,因此我们需要导航到那里,然后添加“node.exe”。

然后我们需要创建 Node.js 的入口代码。在 WAWS 中入口文件必须命名"server.js",因为它由 IIS和IISNode 托管,同时 IISNode 只接受"server.js"。但是在这里作为我们控制的一切我们可以选择任何文件作为输入代码。例如,在根工程中创建一个新的 JavaScript 文件命名为"index.js"。

——由于我们创建了一个 C# Windows Azure 工程,我们不能从上下文菜单"添加新项"中创建一个 JavaScript 文件。我们必须创建一个文本文件,然后将其重命名为
JavaScript 扩展。

我们添加这两个文件后,我们应设置其"复制到输出目录"属性设置为"始终复制",或"复制如果是较新"。否则在部署时他们不会涉及到包。

将非常简单的 Node.js 代码粘贴在"index.js"中 ,如下所示。正如你见,我创建了 web 服务器监听端口 12345。

   1: var http = require("http");

   2: var port = 12345;

   3: 

   4: http.createServer(function (req, res) {

   5:    
res.writeHead(200, { "Content-Type": "text/plain"
});

   6:    
res.end("Hello World\n");

   7: }).listen(port);

   8: 

   9: console.log("Server running at port %d", port);

当工作者角色开始运行的时候,我们需要启动"node.exe"。这可以在其运行方法中进行。我找到Node.js 和JavaScript 文件名,然后创建新进程运行它。我们的工作者角色将等待进程退出。如果一切都 正常,一旦我们的web 服务器被打开,进程会在那里侦听传入的请求,不会被终止。工作者角色中的代码也会如此。

   1: public override void Run()

   2: {

   3:    
// This is a sample worker implementation. Replace with your logic.

   4:    
Trace.WriteLine("NodejsHost entry point called",
"Information");

   5: 

   6:    
// retrieve the node.exe and entry node.js source code file name.

   7:    
var node =
Environment.ExpandEnvironmentVariables(@"%RoleRoot%\approot\node.exe");

   8:    
var js = "index.js";

   9: 

  10:    
// prepare the process starting of node.exe

  11:    
var info = new ProcessStartInfo(node, js)

  12:    
{

  13:        
CreateNoWindow = false,

  14:        
ErrorDialog = true,

  15:        
WindowStyle = ProcessWindowStyle.Normal,

  16:       
 UseShellExecute = false,

  17:        
WorkingDirectory =
Environment.ExpandEnvironmentVariables(@"%RoleRoot%\approot")

  18:    
};

  19:    
Trace.WriteLine(string.Format("{0} {1}", node, js),
"Information");

  20: 

  21:    
// start the node.exe with entry code and wait for exit

  22:    
var process = Process.Start(info);

  23:    
process.WaitForExit();

  24: }

然后我们在本地运行它。在计算机仿真器 UI 工作者角色开始并执行 Node.js,然后Node.js 窗口出现。

打开浏览器,验证由工作者角色托管的网站。

下一步,让我们将它部署到 azure。但我们需要一些额外的步骤。首先,我们需要创建输入终结点。默认情况下,没有工人角色中定义终结点。所以我们将会在 Visual Studio 中打开角色属性窗口中,创建新到我们想要我们的网站使用的端口输入的 TCP 终结点。在这种情况下,我将使用 80。

——即使我们创建一个 web 服务器我们应添加工作者角色中,TCP 端点,因为 Node.js 总是侦听 TCP 而不是 HTTP。
 

然后更改"index.js",监听 80 端口上的 web 服务器。

   1: var http = require("http");

   2: var port = 80;

   3: 

   4: http.createServer(function (req, res) {

   5:    
res.writeHead(200, { "Content-Type": "text/plain"
});

   6:    
res.end("Hello World\n");

   7: }).listen(port);

   8: 

   9: console.log("Server running at port
%d", port);

然后将其发布到 Windows Azure。

在浏览器中我们可以看到我们的 Node.js 网站正在运行在WACS的worker角色上。
 

——如果我们尝试在本地仿真器 的80 端口上运行我们的Node.js 网站,我们可能会遇到错误。这是因为计算仿真程序注册80 和81 80 终结点映射。但Node.js 无法检测到此操作。所以若它试图监听80端口,将会失败,因为80端口已被占用。 

运用 NPM 模块

当我们用WAWS托管Node.js时,我们可以只安装用到的模块,然后发布或上传所有的文件到WAWS。但若用工作者角色,我们需要做些额外的工作来使模块工作。

假设我们打算在自己的应用程序中用使用 “express”模块。首先我们应该通过NPM命令下载安装这个模块。但完成后,它们只是在磁盘中,而非工作者角色工程中。如果我们立刻部署工作者角色,模块不会打包上传至Azure。因此我们需要将它们添加到工程中。在解决方案管理器中,点击 “Show all files”按钮,选择 “node_modules” 文件夹,并在上下文菜单中选择 “Include
In Project”。

但这还不够,我们也需要保证该模块下的所有文件 “Copy always” 或者 “Copy if newer”,以便它们用 “node.exe”和“index.js”上传至Azure。这是一个痛苦的步骤,因为一个模块中可能有很多文件。因此我创建了一个小工具,它能更新C#工程文件,保证它所有的项目“Copy always”。代码很简单。   1: static void Main(string[] args)

   2: {

   3:    
if (args.Length < 1)

   4:    
{

   5:        
Console.WriteLine("Usage: copyallalways [project file]");

   6:        
return;

   7:    
}

   8: 

   9:    
var proj = args[0];

  10:    
File.Copy(proj, string.Format("{0}.bak", proj));

  11: 

  12:    
var xml = new XmlDocument();

  13:    
xml.Load(proj);

  14:    
var nsManager = new XmlNamespaceManager(xml.NameTable);

  15:    
nsManager.AddNamespace("pf",
"https://schemas.microsoft.com/developer/msbuild/2003");

  16: 

  17:    
// add the output setting to copy always

  18:    
var contentNodes =
xml.SelectNodes("//pf:Project/pf:ItemGroup/pf:Content", nsManager);

  19:    
UpdateNodes(contentNodes, xml, nsManager);

  20:    
var noneNodes =
xml.SelectNodes("//pf:Project/pf:ItemGroup/pf:None", nsManager);

  21:    
UpdateNodes(noneNodes, xml, nsManager);

  22:    
xml.Save(proj);

  23: 

  24:    
// remove the namespace attributes

  25:    
var content = xml.InnerXml.Replace("<CopyToOutputDirectory
xmlns=\"\">", "<CopyToOutputDirectory>");

  26:    
xml.LoadXml(content);

  27:    
xml.Save(proj);

  28: }

  29: 

  30: static void UpdateNodes(XmlNodeList
nodes, XmlDocument xml, XmlNamespaceManager nsManager)

  31: {

  32:    
foreach (XmlNode node in nodes)

  33:    
{

  34:        
var copyToOutputDirectoryNode = node.SelectSingleNode("pf:CopyToOutputDirectory",
nsManager);

  35:        
if (copyToOutputDirectoryNode == null)

  36:        
{

  37:             var n =
xml.CreateNode(XmlNodeType.Element, "CopyToOutputDirectory", null);

  38:             n.InnerText = "Always";

  39:             node.AppendChild(n);

  40:        
}

  41:        
else

  42:        
{

  43:             if
(string.Compare(copyToOutputDirectoryNode.InnerText, "Always", true)
!= 0)

  44:             {

  45:                 copyToOutputDirectoryNode.InnerText
= "Always";

  46:             }

  47:        
}

  48:    
}

  49: }

——用此工具时请小心。我只是用作示范,所以请勿用于生产环境。

卸载工作者角色工程,运行此工具,在命令行中用工作者角色工程文件名作为参数,所有的项目会变为“Copy always”。然后重新加载工作者角色工程。

现在我们修改下 “index.js” 来使用express。

   1: var express =
require("express");

   2: var app = express();

   3: 

   4: var port = 80;

   5: 

   6: app.configure(function () {

   7: });

   8: 

   9: app.get("/", function (req,
res) {

  10:    
res.send("Hello Node.js!");

  11: });

  12: 

  13: app.get("/User/:id", function
(req, res) {

  14:    
var id = req.params.id;

  15:    
res.json({

  16:        
"id": id,

  17:        
"name": "user " + id,

  18:        
"company": "IGT"

  19:    
});

  20: });

  21: 

  22: app.listen(port);

最后发布,并到浏览器中看一下。

运用 Windows Azure SQL 数据库

我们可以从工作者角色托管的Node.js中用Windows Azure SQL数据库(以下简称 WACD) 。由于我们能够控制Node.js版本,在这里我们可以用x64版本的 “node-sqlserver”。这比起托管Node.js到WAWS上好多了,因为它只支持x86。

只需从NPM中安装 “node-sqlserver”模块,从“Build\Release” 文件夹复制 “sqlserver.node” 到“Lib” 文件夹。将它们包含到工作者角色工程,并运行我的小工具,保证它们均为 “Copy always”。最后,将 “index.js”上传,来使用WASD。

   1: var express =
require("express");

   2: var sql =
require("node-sqlserver");

   3: 

   4: var connectionString = "Driver={SQL
Server Native Client 10.0};Server=tcp:{SERVER NAME}.database.windows.net,1433;Database={DATABASE
NAME};Uid={LOGIN}@{SERVER NAME};Pwd={PASSWORD};Encrypt=yes;Connection
Timeout=30;";

   5: var port = 80;

   6: 

   7: var app = express();

   8: 

   9: app.configure(function () {

  10:    
app.use(express.bodyParser());

  11: });

  12: 

  13: app.get("/", function (req,
res) {

  14:    
sql.open(connectionString, function (err, conn) {

  15:        
if (err) {

  16:             console.log(err);

  17:             res.send(500, "Cannot open
connection.");

  18:        
}

  19:        
else {

  20:             conn.queryRaw("SELECT * FROM
[Resource]", function (err, results) {

  21:                 if (err) {

  22:                     console.log(err);

  23:                     res.send(500, "Cannot
retrieve records.");

  24:                 }

  25:                 else {

  26:                     res.json(results);

  27:                 }

  28:             });

  29:        
}

  30:    
});

  31: });

  32: 

  33: app.get("/text/:key/:culture",
function (req, res) {

  34:    
sql.open(connectionString, function (err, conn) {

  35:        
if (err) {

  36:             console.log(err);

  37:             res.send(500, "Cannot open
connection.");

  38:        
}

  39:        
else {

  40:             var key = req.params.key;

  41:             var culture = req.params.culture;

  42:             var command = "SELECT * FROM
[Resource] WHERE [Key] = '" + key + "' AND [Culture] = '" +
culture + "'";

  43:             conn.queryRaw(command, function
(err, results) {

  44:        
        if (err) {

  45:                     console.log(err);

  46:                     res.send(500, "Cannot
retrieve records.");

  47:                 }

  48:                 else {

  49:                     res.json(results);

  50:                 }

  51:             });

  52:        
}

  53:    
});

  54: });

  55: 

  56: app.get("/sproc/:key/:culture",
function (req, res) {

  57:    
sql.open(connectionString, function (err, conn) {

  58:        
if (err) {

  59:             console.log(err);

  60:             res.send(500, "Cannot open
connection.");

  61:        
}

  62:        
else {

  63:             var key = req.params.key;

  64:             var culture = req.params.culture;

  65:             var command = "EXEC GetItem
'" + key + "', '" + culture + "'";

  66:             conn.queryRaw(command, function
(err, results) {

  67:                 if (err) {

  68:                     console.log(err);

  69:                     res.send(500, "Cannot
retrieve records.");

  70:                 }

  71:                 else {

  72:                     res.json(results);

  73:                 }

  74:             });

  75:        
}

  76:    
});

  77: });

  78: 

  79: app.post("/new", function (req,
res) {

  80:    
var key = req.body.key;

  81:    
var culture = req.body.culture;

  82:    
var val = req.body.val;

  83: 

  84:    
sql.open(connectionString, function (err, conn) {

  85:        
if (err) {

  86:             console.log(err);

  87:             res.send(500, "Cannot open
connection.");

  88:        
}

  89:        
else {

  90:             var command = "INSERT INTO
[Resource] VALUES ('" + key + "', '" + culture + "',
N'" + val + "')";

  91:             conn.queryRaw(command, function
(err, results) {

  92:                 if (err) {

  93:                     console.log(err);

  94:                     res.send(500, "Cannot
retrieve records.");

  95:                 }

  96:                 else {

  97:                     res.send(200,
"Inserted Successful");

  98:                 }

  99:             });

 100:        
}

 101:    
});

 102: });

 103: 

 104: app.listen(port);

发布至Azure,我们就可以看到Node.js正通过x64版本的“node-sqlserver”与WASD工作了。
 

总结

在这篇文章里我阐述了怎样在Windows Azure Cloud Service worker role里托管我们的Node.js。通过使用worker role我们可以控制Node.js的版本和入口代码。并且使得在Node.js 应用程序启动之前可以做一些前期工作。并且它去掉了IIS和IISNode的局限性。我个人推荐使用worker role 作为 Node.js的托管。

但是如果你使用我在这里提到的方法时有一些问题。第一个是,我们需要手动地将所有的JavaScript 文件和module 文件设置为“Copy always” 或“Copy if newer”。第二个是,用这种方式我们不能获取到云计算服务的配置信息。例如,我们在worker role属性里定义了endpoint但是我们也在Node.js的硬编码里指定了监听端口。它应该做出改变使得我们的Node.js能够获取到那个endpoint。但是我可以告诉你这个方法在这里不管用。

在下一篇文章里我将描述怎样以另一种方式执行“node.exe” 和 Node.js应用程序,如此一来我们可以在Node.js 里获取到云计算服务配置。另外我将介绍怎样通过使用Windows Azure Node.js SDK从Node.js 使用Windows Azure Storage。

希望有所帮助。

Shaun

本文翻译自:https://geekswithblogs.net/shaunxu/archive/2012/09/20/node.js-adventure---host-node.js-on-windows-azure-worker-role.aspx