如何使用Bluebird将node.js的net.connect方法转换为Promise?

4
我希望能有一个基于Promise的 node.js 函数net.connect的版本。如果连接成功,Promise应该返回socket;如果发生连接错误,则应该返回错误。此外最好还要支持可取消性,在取消时能够停止连接尝试。
我已经尝试过一次,但是还没有实现可取消性:
function connectAsync() {
    var connect_args = arguments;
    return new Promise(function (resolve, reject) {
        var socket = net.connect.apply(this, connect_args);
        socket.once('connect', function () {
            socket.removeListener('error', reject);
            resolve(socket);
        });
        socket.once('error', function (err) {
            socket.removeListener('connection', resolve);
            reject(err);
        });
    });
}

然而,对于这样简单的事情来说,似乎过于复杂了。有更好的方法吗?有人已经做过这个了吗?

1
我不建议使用 Promise 来完成这件事。 - Benjamin Gruenbaum
1
我想编写 .then(connect()).then(doThing()).then(close()) 风格的序列。这似乎是 DB 访问的标准实践,其中 db.connect 是被 Promisified 的 - 这甚至在 bluebird 示例中都有展示。为什么 net.connect 如此不同呢? - Nakedible
所以你需要其他东西来以这种方式工作,而不是仅仅依靠承诺。 - Zav
要以这种方式使用外部库,您需要编写包装器来实现您的承诺。不,示例不是必需的。 - Zav
1
如果下周你还卡住了,我会发布一个答案,同时解释为什么Bluebird不会自动支持promo sift事件发射器。 - Benjamin Gruenbaum
显示剩余5条评论
3个回答

2

总的来说,如果你直接看 - EventEmitters是一个非常复杂的抽象。

Promises代表着操作的顺序 - 可以将它们视为赋值运算符或分号。在常规同步编程中,代码看起来像这样:

try{
    var value = foo(bar);
    log(value);
} catch(e){
    // handle error
}

事情一个接一个地运行:

  1. 进入try块
  2. 使用参数bar运行foo
  3. 除非出现错误,否则记录该值
  4. 如果出现错误,则处理它。

这就像是一条长的单一操作链。Promise正是如此:

 fooAsync(bar).
 then(log).
 catch(function(){
      // handle error
 });

一个Promise是一个链式操作。您可以创建多个这样的链式操作,类似于其他形式的并发(如线程)表示执行一系列操作。代码示例如下所示:

--------------------------------+-Success------------------>

                   --Error---->// might join up

另一方面,事件发射器没有关于其触发的事件名称或类型的保证。Node EventEmitter内置了一些很酷的功能(如堆栈跟踪和error事件),但是与Promises相比,约定要弱得多 - 不同的事件发射器触发不同的事件,一个事件发射器可以像这样执行:

----Foo fired-+-Handler 1    ---- Bar fired-+      ---- Baz Fired-+-Handler 1

              --Handler 2                                         --Handler 2

这并不是一个单一的链 - 因此虽然已经有过几次尝试和讨论,但没有通用的方法来表示事件发生器中的承诺 - 它们在事件处理和事件名称上的差异太大了。

另一方面,pg.connect使用了Node风格的错误回调。因此可以很容易地将其变为Promise,这些已经被很好地规定并遵守合同。

你现在拥有的代码已经很好了,你可以将其推广到一个具有两个事件的事件发生器中。请记住,你需要编写这种样板代码一次,然后在整个代码中重复使用它:)


在你的代码中,“使用”当然是指“调用”,而不是“复制” :-) - Bergi
好的,感谢您的解释。我完全同意关于事件发射器的一般性描述。然而,即使在事件发射器中复杂地包装,"connect" Unix 调用也只有一个结果:成功或失败,因此没有理由 net.connect 不能被转换成 Promise 方法。 - Nakedible
实际上,如果您想在“connect”上使用child_process.execAsync,则会得到一个Promise - 但是net connect具有更多内置功能。 - Benjamin Gruenbaum

1
我想到的解决方案与你所拥有的几乎完全相同:
p = new Promise((resolve, reject) ->
    listener = (data) ->
        try
            check_data_format(data)
        catch err
            return reject(err)
        if is_right_data(data)
            return resolve()
    ee.on("stdout", listener)
)
return p

偶尔当事情变得更加不愉快时:

reject_f = null
resolve_f = null
p = new Promise((resolve, reject) ->
    reject_f = reject
    resolve_f = resolve
)
listener = (data) ->
    try
        check_data_format(data)
    catch err
        return reject(err)
    if is_right_data(data)
        return resolve()
ee.on("stdout", listener)

我曾就此事(请求文档)在这里 提出问题,但被重定向到了您的问题。
我得出结论,当前的 Promise 和事件发射器交集实现方式很丑陋,我只能忍受它。我还没有遇到比我们独立发明的更好的建议,如果您有,请分享。

1

您可以删除两行removeListener()。Promise 只能被解决或拒绝一次,因此您不必担心事件会再次被调用。Promise 一旦完成其状态就不会改变。

另外,我认为您需要解决几个问题:

  1. var connect_args = arguments 可能不起作用,因为 arguments 是一种奇怪的临时对象类型。通常的解决方法是复制它的内容:var connect_args = [].slice.call(arguments);

  2. 在这一行中,net.connect.apply(this, connect_args);,我不认为this是正确的值,因为此时你在 Promise 回调内部 (也许在这种情况下这并不重要)。更加技术上正确的做法可能是使用net.connect.apply(net, connect_args);,这将更直接地模拟调用net.connect(args)

至于是否明智地使用 promise,看起来您在评论中有几个不同的观点。

除了删除代码中的removeListener()行之外,我认为没有什么简化的方法。您正在创建一个承诺来响应两个不同的自定义条件,因此必须编写检测这两个条件的代码。没有其他办法。
附注:如果您不删除代码中的removeListener()行,则可能会出现错误,因为您正在设置'connect'事件,但执行removeListener('connection')。此外,我不知道为什么要将函数传递给removeListener(),因为它与建立事件处理程序时使用的函数引用不同。

2
谢谢您进行了几处更正!但是,如果我不调用removeListener,这是否意味着Promise将停留在内存中,并且只要套接字打开,就无法进行垃圾回收? - Nakedible
@Nakedible - 你提到的垃圾回收是个好观点 - 很可能是真的。在你的代码中加上一条注释,说明为什么要移除事件监听器是明智的,因为像我这样的人很容易在以后过来,认为那些行可以被删除,因为一旦承诺被兑现,它们就是不可变的,所以它们免受多次调用reject()resolve()的影响。 - jfriend00
2
var connect_args = arguments; 没有任何问题。apply 方法也可以接受 Arguments 对象。 - Bergi
如果您的 socket 持续存在且未删除 'error' 事件监听器,则下次发生任何类型的错误(不仅限于连接错误),该函数将被调用。 - Terrabits
1
@Terrabits - 这些监听器只需调用 resolve()reject(),这两个函数都会忽略多次调用的情况(一旦 Promise 被解决或拒绝,它就被锁定,无法更改),因此您不必担心这个问题。 - jfriend00

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接