24 April 2011

之前也说了,开始玩儿nodejs,然后拿cheater下手。做得差不多了,总结一下。会有几篇单独的文章,省得看起来乱。

nodejs的基本思想就是异步,这对传统编程模型是彻底的颠覆。刚开始很不习惯,死得很惨,程序怎么都不按我想的跑。一个简单的例子就是http的request,调用不会等待response而是立即返回,然后通过回调函数处理response。

对于这种异步的模型,通常有两种办法,一是回调函数+事件传递,二是直接用回调函数处理所有事情。简单说明一下,第一种在回调函数中不做什么事情,只是简单地发出一个事件,由事件的listener处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var cheater = function () {
    this.on('login', function (retryTimes) {
        this.once('login_ok', function () { // note that "once" is used, not "on"
            this.emit('heartbeat', , );
        });
 
        this.once('login_nok', function () {
            var that = this;
 
            if (retryTimes < CheaterConfig.loginMaxRetryTimes) {
                timeoutId = setTimeout(function () {
                    that.emit('login', retryTimes + 1);
                }, CheaterConfig.loginRetryInterval * 1000);
            }
        });
 
        client.login(); // send out http request, later login_ok or login_nok event will be emitted
    });
}

这里没有给client.login()注册任何回调函数,client.login()自己在调用https.request时注册了一个回调函数,这个函数仅仅用来发事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CheaterClient.prototype.login = function () {
    var fs = require('fs');
    var options = {
        cert: fs.readFileSync(CheaterConfig.cert),
        host: CheaterConfig.loginHost,
        port: CheaterConfig.loginPort,
        path: CheaterConfig.loginUrl,
        method: 'POST'
    };
 
    var request = https.request(options, function (response) {
        response.setEncoding('ascii');
        response.on('data', function (data) {
            if (data is valid) {
                this.cheater.emit('login_ok'); // everything is fine
            } else {
                this.cheater.emit('login_nok'); // oops, bad things just happened
        });
    });
    request.write(CheaterConfig.loginInfo());
    request.end();
};

值得注意的是在注册login_ok和login_nok的时候并不是使用“on”,而是用了“once”,原因就在于为了不把那个retryTimes传来传去,这里用了closure,如果用“on”的话,每次login事件被捕获的时候都会注册login_ok和login_nok的listener,然后就……杯具了。

对于第二种方案就不多说了,比较容易理解,但是个人感觉代码会比较混乱,所以我没有使用。

还有一点值得一提,也是折磨我许久。看下面这段代码:

1
2
3
4
5
6
7
8
Cheater.prototype.heartbeat = function (livingTime, failedTimes) {
    if (livingTime < 3600 / CheaterConfig.heartbeatInterval * CheaterConfig.hoursToLive) {
        setTimeout(function () {
            this.heartbeat(livingTime + 1, );
            this.doSomething();
        }, CheaterConfig.heartbeatInterval * 1000);
    }
};

先梳理一下这段代码都干了什么。login之后开始做heartbeat,隔一段时间一次,然后过了几个小时之后logout。看起来很美好,heartbeat递归调用,直到某次调用返回之后doSomething()。

但是,杯具就发生了,doSomething被提前调用了!我们来看看为什么。

下面是函数调用桟(比较土,凑合着看吧):

cheater.heartbeat(0, 0);
--> setTimeout(function, interval);
    --> heartbeat(1, 0);
        --> setTimeout(function, interval); // 这里异步出现了,线程在这一点上留下一个continuation,然后这次递归调用结束了!
    --> doSomething(); // 并不像我们所想的,doSomething()被提前调用了,在逻辑上的递归还没有结束之前!

这个问题害得我调了很久,看来惯性思维确实很难改变。



blog comments powered by Disqus