ichuan.net

自信打不死的心态活到老

在 pm2 的 cluster mode 中使用 80 端口的变通方法

pm2 是一个 nodejs 进程管理器,和 forever 类似,但是相比有以下优点:

  1. 内置负载均衡,充分利用多核
  2. 不间断刷新代码
  3. 内置内存和 CPU 监控

最近做的项目中发现一台 expressjs 开发的 API 服务器其它资源都占满,但 CPU 却有大部分闲置。项目使用 forever 部署,于是想切换到 pm2 利用其余 CPU 资源。

切换过去重新启动后,发现总是报错,后来查询发现这是 pm2 的一个局限:其负载均衡是基于 nodejs 的 cluster 模块开发,不支持应用绑定 0 到 1024 间的端口。而我的 API 服务以前是 forever 绑定到 80 端口,由于 forever 使用 fork 方式,所以没有问题。

有两种解决方法,一种是继续使用 pm2,但是使用 fork 模式,这样就和 forever 一样了,但缺失了其那几大优势;另一种是我想到的变通方法:

  1. 修改程序,让其监听在非特权端口,比如 56789:

    //app.set('port', 80);
    app.set('port', 56789);
    
  2. 用 pm2 启动

  3. 用 iptables 把 80 端口的流量导向 56789 端口

    sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-ports 56789
    

完了后访问下 80 端口,可以看到正确的页面了。问题解决。

120行代码的“web shell”

工作是无聊的,为工作而写代码的过程是枯燥、令人气馁的。但工作又是必须的,该做的工作是要做的。我的权衡之策就是在条件允许的情况下,把一些新的方式、新的技术用到工作代码中,这样在编码的过程中会学到新知识并应用起来,编码的过程也不会都是些无聊的体力劳动了。查阅文档和研究 bug、寻求较完美的解决方案,这些经历都会使工作变得有趣起来,最终的结果也会让人充满成就感。

最近一个项目需要这样一个功能:把后台的不定期的输出反馈到 web 界面上。我首先想到了最简单是在前端用 ajax 轮询,不过这样做就太土了。于是想到了用 websocket 实现后台实时推送到前台。正好这个项目前台是用 express 来做的(之前一直用 django,这次也是图新鲜第一次用 nodejs),自然而然可以用 socket.io 来做 websocket。

socket.io 解决的问题是推送。在 websocket 出现前 http 推送主要用的是 comet 技术:

comet

当 websocket 不可用时,socket.io 中可以利用 flash socket, iframe, jsonp 等方法实现准实时的客户端服务器双向通信。

下面以做一个 "web-shell" 应用为例演示 socket.io 功能和用法。美化后的 html 代码和 js 代码总和大约 120 行。

此 "web-shell" 非彼 "web-shell",我们要做的是一个基于 web 的 shell,最终截图如下:

enter image description here

因为在浏览器可以输入命令发给服务器,服务器有输出了可以实时推送给客户端,所以使用 socket.io 可以实现。

首先新建个目录,安装 socket.io:

mkdir web-shell && cd web-shell
npm init
npm install socket.io

然后按照预想的样子编写 index.html 如下:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <script src="/socket.io/socket.io.js"></script>
    <style>
      /* 篇幅原因省去了 css 代码,可以在尾部下载 */
    </style>
  </head>
  <body>
    <pre id="console"></pre>
    <input id="cmd" />
    <script>
      // 这块留出来写控制流程
    </script>
  </body>
</html>

然后参考 socket.io 的示例代码,编写服务端的 app.js:

var app = require('http').createServer(handler)
  , io = require('socket.io').listen(app)
  , fs = require('fs')
  , spawn = require('child_process').spawn;

app.listen(8080);

function handler (req, res) {
  fs.readFile(__dirname + '/index.html', function (err, data) {
    res.writeHead(200);
    res.end(data);
  });
}

io.sockets.on('connection', function (socket) {
  var shell = spawn('bash')
    , output = function (msg) {
        socket.emit('output', msg.toString());
      };

  shell.stdout.on('data', output);
  shell.stderr.on('data', output);
  shell.on('close', function () {
    output('Exit');
    socket.disconnect(true);
  });

  socket.on('input', function (data) {
    shell.stdin.write(data);
  });

  socket.on('disconnect', function () {
    shell.kill('SIGKILL');
  });
});

io.sockets.on('connection' 这一行是客户端建立连接后会执行的逻辑。对每个客户端,创建一个新的子进程(spawn('bash')),并且在客户端发来命令时(socket.on('input'))把命令传到 bash 的标准输入;在 bash 进程有输出时(shell.stdout.on('data')shell.stderr.on('data'))把输出反馈给客户端(socket.emit('output'))。最后一点,在 bash 进程退出时(shell.on('close'))断开客户端连接。

再看客户端逻辑,把如下代码加入 index.html 的 <script></script> 标记间:

var elConsole = document.getElementById('console')
  , elCmd = document.getElementById('cmd')
  , socket = io.connect(location.host);

var escapeHtml = function (html) {
  return html.replace(/&/g, '&amp;')
             .replace(/>/g, '&gt;')
             .replace(/</g, '&lt;');
};

var output = function (data) {
  elConsole.innerHTML += escapeHtml(data);
  elConsole.scrollTop = 9e5;
};

socket.on('output', output);

socket.on('connect', function () {
  output('连接成功\n');
  socket.emit('input', 'cat /etc/iss*\n');
});

socket.on('disconnect', function () {
  output('\n连接已断开\n');
});

elCmd.addEventListener('keypress', function (e) {
  if (e.keyCode === 13) {
    var data = elCmd.value + '\n';
    output('$ ' + data);
    socket.emit('input', data);
    elCmd.value = '';
  }
});

首先创建连接(io.connect(location.host)),然后在连接后先发送一个查看发行版信息的命令(cat /etc/iss*\n)。监听输入控件的 keypress 事件,在用户敲回车时把输入发送给服务端。在服务端有消息发回时更新展示控件(socket.on('output'))。

完整代码见 https://github.com/ichuan/yc-lib/tree/master/node/web-shell这里有下载。

还有改进空间,这里是个加入了命令历史记录和自动清屏功能的版本。当我想做 tab 补全功能时发现,bash 的有些输出在 tty,而不在 stdout 和 stderr,所以有些输出是没法在网页端看到的,例如 tab 补全,ssh 登录时的输密码提示。ssh 登录的输入也是在 tty,所以只有加入了公钥自动登录后,才可以在网页端 ssh 到其它机器。