Node.js 域

2022-02-26 10:31 更新
穩(wěn)定性: 2 - 不穩(wěn)定

Node.js域包含了能把不同的IO操作看成單獨組的方法。如果任何一個注冊到域的事件或者回調(diào)觸發(fā)error事件,或者拋出一個異常,則域就會接收到通知,而不是在process.on('uncaughtException')處理程序中丟失錯誤的上下文,也不會使程序立即以錯誤代碼退出。

警告:不要忽視錯誤!

你不能將域錯誤處理程序看做錯誤發(fā)生時就關閉進程的一個替代方案。

根據(jù)JavaScript中拋出異常的工作原理,基本上沒有方法可以安全的“回到原先離開的位置”,在不泄露引用,或者不造成一些其他未定義的狀態(tài)下。

響應拋出錯誤最安全的方法就是關閉進程。一個正常的服務器會可能有很多活躍的連接,因為某個錯誤就關閉所有連接顯然是不合理的。

比較好的方法是給觸發(fā)錯誤的請求發(fā)送錯誤響應,讓其他連接正常工作時,停止監(jiān)聽觸發(fā)錯誤的人的新請求。

按這種方法,和集群(cluster)模塊可以協(xié)同工作,當某個進程遇到錯誤時,主進程可以復制一個新的進程。對于Node程序,終端代理或者注冊的服務,可以留意錯誤并做出反應。

舉例來說,下面的代碼就不是好辦法:

javascript
// XXX WARNING!  BAD IDEA!

var d = require('domain').create();
d.on('error', function(er) {
  // The error won't crash the process, but what it does is worse!
  // Though we've prevented abrupt process restarting, we are leaking
  // resources like crazy if this ever happens.
  // This is no better than process.on('uncaughtException')!
  console.log('error, but oh well', er.message);
});
d.run(function() {
  require('http').createServer(function(req, res) {
    handleRequest(req, res);
  }).listen(PORT);
});

通過使用域的上下文,并將程序切為多個工作進程,我們能夠更合理的響應,處理錯誤更安全:

javascript
// 好一些的做法!

var cluster = require('cluster');
var PORT = +process.env.PORT || 1337;

if (cluster.isMaster) {
  // In real life, you'd probably use more than just 2 workers,
  // and perhaps not put the master and worker in the same file.
  //
  // You can also of course get a bit fancier about logging, and
  // implement whatever custom logic you need to prevent DoS
  // attacks and other bad behavior.
  //
  // See the options in the cluster documentation.
  //
  // The important thing is that the master does very little,
  // increasing our resilience to unexpected errors.

  cluster.fork();
  cluster.fork();

  cluster.on('disconnect', function(worker) {
    console.error('disconnect!');
    cluster.fork();
  });

} else {
  // the worker
  //
  // This is where we put our bugs!

  var domain = require('domain');

  // See the cluster documentation for more details about using
  // worker processes to serve requests.  How it works, caveats, etc.

  var server = require('http').createServer(function(req, res) {
    var d = domain.create();
    d.on('error', function(er) {
      console.error('error', er.stack);

      // Note: we're in dangerous territory!
      // By definition, something unexpected occurred,
      // which we probably didn't want.
      // Anything can happen now!  Be very careful!

      try {
        // make sure we close down within 30 seconds
        var killtimer = setTimeout(function() {
          process.exit(1);
        }, 30000);
        // But don't keep the process open just for that!
        killtimer.unref();

        // stop taking new requests.
        server.close();

        // Let the master know we're dead.  This will trigger a
        // 'disconnect' in the cluster master, and then it will fork
        // a new worker.
        cluster.worker.disconnect();

        // try to send an error to the request that triggered the problem
        res.statusCode = 500;
        res.setHeader('content-type', 'text/plain');
        res.end('Oops, there was a problem!\n');
      } catch (er2) {
        // oh well, not much we can do at this point.
        console.error('Error sending 500!', er2.stack);
      }
    });

    // Because req and res were created before this domain existed,
    // we need to explicitly add them.
    // See the explanation of implicit vs explicit binding below.
    d.add(req);
    d.add(res);

    // Now run the handler function in the domain.
    d.run(function() {
      handleRequest(req, res);
    });
  });
  server.listen(PORT);
}

// This part isn't important.  Just an example routing thing.
// You'd put your fancy application logic here.
function handleRequest(req, res) {
  switch(req.url) {
    case '/error':
      // We do some async stuff, and then...
      setTimeout(function() {
        // Whoops!
        flerb.bark();
      });
      break;
    default:
      res.end('ok');
  }
}

錯誤對象的附加內(nèi)容

任何時候一個錯誤被路由傳到一個域的時,會添加幾個字段。

  • error.domain 第一個處理錯誤的域
  • error.domainEmitter 用這個錯誤對象觸發(fā)'error'事件的事件分發(fā)器
  • error.domainBound 綁定到domain的回調(diào)函數(shù),第一個參數(shù)是error。
  • error.domainThrown boolean值,表明是拋出錯誤,分發(fā),或者傳遞給綁定的回到函數(shù)。

隱式綁定

新分發(fā)的對象(包括流對象(Stream objects),請求(requests),響應(responses)等)會隱式的綁定到當前正在使用的域中。

另外,傳遞給底層事件循環(huán)(比如fs.open或其他接收回調(diào)的方法)的回調(diào)函數(shù)將會自動的綁定到這個域。如果他們拋出異常,域會捕捉到錯誤信息。

為了避免過度使用內(nèi)存,域對象不會象隱式的添加為有效域的子對象。如果這樣做的話,很容易影響到請求和響應對象的垃圾回收。

如果你想將域對象作為子對象嵌入到父域里,就必須顯式的添加它們。

隱式綁定路由拋出的錯誤和'error'事件,但是不會注冊事件分發(fā)器到域,所以domain.dispose()不會關閉事件分發(fā)器。隱式綁定僅需注意拋出的錯誤和 'error'事件。

顯式綁定

有時候正在使用的域并不是某個事件分發(fā)器的域?;蛘哒f,事件分發(fā)器可能在某個域里創(chuàng)建,但是被綁定到另外一個域里。

例如,HTTP服務器使用正一個域對象,但我們希望可以每一個請求使用一個不同的域。

這可以通過顯式綁定來實現(xiàn)。

例如:

// create a top-level domain for the server
var serverDomain = domain.create();

serverDomain.run(function() {
  // server is created in the scope of serverDomain
  http.createServer(function(req, res) {
    // req and res are also created in the scope of serverDomain
    // however, we'd prefer to have a separate domain for each request.
    // create it first thing, and add req and res to it.
    var reqd = domain.create();
    reqd.add(req);
    reqd.add(res);
    reqd.on('error', function(er) {
      console.error('Error', er, req.url);
      try {
        res.writeHead(500);
        res.end('Error occurred, sorry.');
      } catch (er) {
        console.error('Error sending 500', er, req.url);
      }
    });
  }).listen(1337);
});

domain.create()

  • return: {Domain}

用于返回一個新的域對象。

Class: Domain

這個類封裝了將錯誤和沒有捕捉到的異常到有效對象功能。

域是EventEmitter的子類. 監(jiān)聽它的error事件來處理捕捉到的錯誤。

domain.run(fn)

  • fn {Function}

在域的上下文運行提供的函數(shù),隱式的綁定了所有的事件分發(fā)器,計時器和底層請求。

這是使用域的基本方法。

例如:

var d = domain.create();
d.on('error', function(er) {
  console.error('Caught error!', er);
});
d.run(function() {
  process.nextTick(function() {
    setTimeout(function() { // simulating some various async stuff
      fs.open('non-existent file', 'r', function(er, fd) {
        if (er) throw er;
        // proceed...
      });
    }, 100);
  });
});

這個例子里程序不會崩潰,而會觸發(fā)d.on('error')。

domain.members

  • {Array}

顯式添加到域里的計時器和事件分發(fā)器數(shù)組。

domain.add(emitter)

  • emitter {EventEmitter | Timer} 添加到域里的計時器和事件分發(fā)器

顯式地將一個分發(fā)器添加到域。如果分發(fā)器調(diào)用的事件處理函數(shù)拋出錯誤,或者分發(fā)器遇到error事件,將會導向域的error事件,和隱式綁定一樣。

對于setIntervalsetTimeout返回的計時器同樣適用。如果這些回調(diào)函數(shù)拋出錯誤,將會被域的'error'處理器捕捉到。

如果計時器或分發(fā)器已經(jīng)綁定到域,那它將會從上一個域移除,綁定到當前域。

domain.remove(emitter)

  • emitter {EventEmitter | Timer} 要移除的分發(fā)器或計時器

與domain.add(emitter)函數(shù)恰恰相反,這個函數(shù)將分發(fā)器移除出域。

domain.bind(callback)

  • callback {Function} 回調(diào)函數(shù)
  • return: {Function}被綁定的函數(shù)

返回的函數(shù)是一個對于所提供的回調(diào)函數(shù)的包裝函數(shù)。當調(diào)用這個返回的函數(shù)被時,所有被拋出的錯誤都會被導向到這個域的error事件。

Example

var d = domain.create();

function readSomeFile(filename, cb) {
  fs.readFile(filename, 'utf8', d.bind(function(er, data) {
    // if this throws, it will also be passed to the domain
    return cb(er, data ? JSON.parse(data) : null);
  }));
}

d.on('error', function(er) {
  // an error occurred somewhere.
  // if we throw it now, it will crash the program
  // with the normal line number and stack message.
});

domain.intercept(callback)

  • callback {Function} 回調(diào)函數(shù)
  • return: {Function} 被攔截的函數(shù)

domain.bind(callback)類似。除了捕捉被拋出的錯誤外,它還會攔截Error對象作為參數(shù)傳遞到這個函數(shù)。

這種方式下,常見的if (er) return callback(er);模式,能被一個地方一個錯誤處理替換。

Example

var d = domain.create();

function readSomeFile(filename, cb) {
  fs.readFile(filename, 'utf8', d.intercept(function(data) {
    // note, the first argument is never passed to the
    // callback since it is assumed to be the 'Error' argument
    // and thus intercepted by the domain.

    // if this throws, it will also be passed to the domain
    // so the error-handling logic can be moved to the 'error'
    // event on the domain instead of being repeated throughout
    // the program.
    return cb(null, JSON.parse(data));
  }));
}

d.on('error', function(er) {
  // an error occurred somewhere.
  // if we throw it now, it will crash the program
  // with the normal line number and stack message.
});

domain.enter()

這個函數(shù)就像run,bindintercept的管道系統(tǒng),它設置有效域。它設定了域的domain.activeprocess.domain,還隱式的將域推到域模塊管理的域棧(關于域棧的細節(jié)詳見domain.exit())。enter函數(shù)的調(diào)用,分隔了異步調(diào)用鏈以及綁定到一個域的I/O操作的結束或中斷。

調(diào)用enter僅改變活動的域,而不改變域本身。在一個單獨的域里可以調(diào)用任意多次Enterexit。

domain.exit()

exit函數(shù)退出當前域,并從域的棧里移除。每當程序的執(zhí)行流程要切換到不同的異步調(diào)用鏈的時候,要保證退出當前域。調(diào)用exit函數(shù),分隔了異步調(diào)用鏈,和綁定到一個域的I/O操作的結束或中斷。

如果有多個嵌套的域綁定到當前的上下文,exit函數(shù)將會退出所有嵌套。

調(diào)用exit僅改變活躍域,不會改變自身域。在一個單獨的域里可以調(diào)用任意多次Enterexit。

如果在這個域名下exit已經(jīng)被設置,exit將不退出域返回。

domain.dispose()

穩(wěn)定性: 0 - 拋棄。通過域里設置的錯誤事件來顯示的消除失敗的 IO 操作。

調(diào)用dispos后,通過run,bind或intercept綁定到域的回調(diào)函數(shù)不再使用這個域,并且分發(fā)dispose事件。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號