工作是无聊的,为工作而写代码的过程是枯燥、令人气馁的。但工作又是必须的,该做的工作是要做的。我的权衡之策就是在条件允许的情况下,把一些新的方式、新的技术用到工作代码中,这样在编码的过程中会学到新知识并应用起来,编码的过程也不会都是些无聊的体力劳动了。查阅文档和研究 bug、寻求较完美的解决方案,这些经历都会使工作变得有趣起来,最终的结果也会让人充满成就感。
最近一个项目需要这样一个功能:把后台的不定期的输出反馈到 web 界面上。我首先想到了最简单是在前端用 ajax 轮询,不过这样做就太土了。于是想到了用 websocket 实现后台实时推送到前台。正好这个项目前台是用 express 来做的(之前一直用 django,这次也是图新鲜第一次用 nodejs),自然而然可以用 socket.io 来做 websocket。
socket.io 解决的问题是推送。在 websocket 出现前 http 推送主要用的是 comet 技术:

当 websocket 不可用时,socket.io 中可以利用 flash socket, iframe, jsonp 等方法实现准实时的客户端服务器双向通信。
下面以做一个 "web-shell" 应用为例演示 socket.io 功能和用法。美化后的 html 代码和 js 代码总和大约 120 行。
此 "web-shell" 非彼 "web-shell",我们要做的是一个基于 web 的 shell,最终截图如下:

因为在浏览器可以输入命令发给服务器,服务器有输出了可以实时推送给客户端,所以使用 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, '&')
.replace(/>/g, '>')
.replace(/</g, '<');
};
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 到其它机器。