MyException - 我的异常网
当前位置:我的异常网» Web前端 » 浅析egg-cluster多进程管理模块(1)

浅析egg-cluster多进程管理模块(1)

www.MyException.Cn  网友分享于:2013-08-22  浏览:0次
浅析egg-cluster多进程管理模块(一)

浅析egg-cluster多进程管理模块(一)

nodejs/cluster集群模块

cluster模块采用Master-Worker模式,以主进程操控子进程的方式启动多个http或https服务器。主要需解决两个问题:

  • 多个工作进程的http或https服务监听同一个端口;

  • http或https服务器与cluster内部tcp服务间的通信。

大致实现思路是: - 在工作进程http或https服务的listen方法执行时,启动主进程内部的tcp服务,置空工作进程http或https服务的listen方法;

  • 主进程tcp服务"connection"事件时,促使工作进程将主进程服务发出的net#Server交由工作进程的http或https服务处理。

多进程启动服务

按DavidCai1993在《通过源码解析 Node.js 中 cluster 模块的主要功能实现》这篇文章中的表述,当主进程调用cluster.fork方法执行子进程脚本时,node在net模块的实现中筛分了net#Server实例listen方法的执行场景,以策略模式对不同场景予以不同处理。

cluster.fork执行场景下,当主进程的tcp服务尚未创建时,则予以创建,用以实际处理监听端口的职能;同时worker进程下启动的http/https服务,其监听端口行为也变得无效。其余场景仍采用net#Server实例listen方法原有的执行逻辑。

node@8.3.0中的表现是,根据net#Server实例listen方法的参数筛分场景,后续执行逻辑相同。个中情形又不同于DavidCai1993的说法。

node@8.3.0版本中,通过环境变量NODE_UNIQUE_ID决定cluster模块实际调用哪份脚本文件。NODE_UNIQUE_ID初始值为0,require("cluster")模块将调用lib/internal/cluster/master.js脚本,cluster.fork将使NODE_UNIQUE_ID自增1,并创建工作进程;随后在其他脚本文件中require("cluster")模块,因NODE_UNIQUE_ID为真值,将调用lib/internal/cluster/child.js脚本。如使用者require("cluster")模块fork工作进程时,调用的是master脚本,node内置的net模块require("cluster")时,调用的是child脚本。

const childOrMaster = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'master';
module.exports = require(`internal/cluster/${childOrMaster}`);

 

同时,master脚本的isMaster属性为真值,cluster._getServer方法也不存在;child脚本的isMaster属性为否值,存在cluster._getServer方法。对于cluster.fork工作进程中创建的net#Server实例,在其执行listen方法时,将调用cluster._getServer方法。 

// node@8.3.0 lib/net.js
function listenInCluster(server, address, port, addressType, backlog, fd, exclusive) {
  exclusive = !!exclusive;

  if (!cluster) cluster = require('cluster');

  if (cluster.isMaster || exclusive) {
    // Will create a new handle
    // _listen2 sets up the listened handle, it is still named like this
    // to avoid breaking code that wraps this method
    server._listen2(address, port, addressType, backlog, fd);
    return;
  }

  const serverQuery = {
    address: address,
    port: port,
    addressType: addressType,
    fd: fd,
    flags: 0
  };

  // Get the master's server handle, and listen on it
  cluster._getServer(server, serverQuery, listenOnMasterHandle);

  function listenOnMasterHandle(err, handle) {
    // EADDRINUSE may not be reported until we call listen(). To complicate
    // matters, a failed bind() followed by listen() will implicitly bind to
    // a random port. Ergo, check that the socket is bound to the expected
    // port before calling listen().
    //
    // FIXME(bnoordhuis) Doesn't work for pipe handles, they don't have a
    // getsockname() method. Non-issue for now, the cluster module doesn't
    // really support pipes anyway.
    if (err === 0 && port > 0 && handle.getsockname) {
      var out = {};
      err = handle.getsockname(out);
      if (err === 0 && port !== out.port)
        err = uv.UV_EADDRINUSE;
    }

    if (err) {
      var ex = exceptionWithHostPort(err, 'bind', address, port);
      return server.emit('error', ex);
    }

    // Reuse master's server handle
    server._handle = handle;
    // _listen2 sets up the listened handle, it is still named like this
    // to avoid breaking code that wraps this method
    server._listen2(address, port, addressType, backlog, fd);
  }
}

 

lib/internal/cluster/child.js脚本中,cluster.getServer方法将促使工作进程send消息,该消息通过lib/internal/utils.js模块转化为"internalMessage"消息。内部消息的判断凭据是子对象发送的message消息对象含有cmd属性,且该属性以"NODE"前缀起始。

// node@8.3.0 lib/internal/cluster/child.js
// cb回调为listenInCluster函数中的listenOnMasterHandle函数
cluster._getServer = function(obj, options, cb) {
  const indexesKey = [options.address,
                      options.port,
                      options.addressType,
                      options.fd ].join(':');

  // ...

  const message = util._extend({
    act: 'queryServer',
    index: indexes[indexesKey],
    data: null
  }, options);

  // ...

  // 工作进程发送消息,次参作为回调添加到lib/internal/utils.js模块callbacks回调队列中
  send(message, (reply, handle) => {
    if (typeof obj._setServerData === 'function')
      obj._setServerData(reply.data);

    if (handle)
      shared(reply, handle, indexesKey, cb);  // Shared listen socket.
    else
      rr(reply, indexesKey, cb);              // Round-robin.
  });

  // ...
};

// ...

function send(message, cb) {
  return sendHelper(process, message, null, cb);
}

// node@8.3.0 lib/internal/cluster/utils.js
// 将工作进程发送的消息转化为内部消息
// 将cb添加到callbacks中
var seq = 0;
function sendHelper(proc, message, handle, cb) {
  if (!proc.connected)
    return false;

  // Mark message as internal. See INTERNAL_PREFIX in lib/child_process.js
  message = util._extend({ cmd: 'NODE_CLUSTER' }, message);

  if (typeof cb === 'function')
    callbacks[seq] = cb;

  message.seq = seq;
  seq += 1;
  return proc.send(message, handle);
}

而在lib/internal/cluster/master.js脚本中,cluster.fork方法执行时,将促成新创建的工作进程订阅内部消息,借由通过onmessage函数处理该内部消息。又因lib/internal/cluster/child.js模块发送的内部消息,其act属性为"queryServer",所以在onmessage函数处理过程中,最终将调用queryServer函数。 

// node@8.3.0 lib/internal/cluster/master.js
cluster.fork = function(env) {
  cluster.setupMaster();
  const id = ++ids;
  const workerProcess = createWorkerProcess(id, env);
  const worker = new Worker({
    id: id,
    process: workerProcess
  });

  // ...

  worker.process.on('internalMessage', internal(worker, onmessage));
  // ...
};

function onmessage(message, handle) {
  const worker = this;

  // ...

  else if (message.act === 'queryServer')
    queryServer(worker, message);

  // ...
}

// node@8.3.0 lib/internal/cluster/utils.js
// 以cb回调即onmessage函数处理消息和句柄
function internal(worker, cb) {
  return function onInternalMessage(message, handle) {
    if (message.cmd !== 'NODE_CLUSTER')
      return;

    var fn = cb;

    // ...

    fn.apply(worker, arguments);
  };
}

 

关于queryServer函数,它的工作内容即是在首次fork工作进程、且在该工作进程中调用net#Server实例的listen方法时,创建cluster模块内部的tcp服务。因工作进程发送的内部消息中,其启动服务的配置项address、port、addressType、fd均由用户配置,所以内部Tcp服务只会创建一个。该tcp服务在lib/internal/cluster/round_robin_handle.js模块中通过实例化RoundRobinHandle 改造函数时创建。随后再次fork工作进程时,queryServer函数将重用缓存的RoundRobinHandle 实例,而不是创建新的tcp服务。 又因内部tcp服务在master进程中创建,而master进程的'NODE_UNIQUE_ID'环境变量为否值,该net#Server实例执行listen方法时,将以创建Tcp实例的方式赋值net#Server实例的_handle属性,再通过该Tcp实例完成端口监听。

// node@8.3.0 lib/internal/cluster/master.js
function queryServer(worker, message) {
  // Stop processing if worker already disconnecting
  if (worker.exitedAfterDisconnect)
    return;

  const args = [message.address,
                message.port,
                message.addressType,
                message.fd,
                message.index];
  const key = args.join(':');
  var handle = handles[key];

  if (handle === undefined) {
    var constructor = RoundRobinHandle;
    // UDP is exempt from round-robin connection balancing for what should
    // be obvious reasons: it's connectionless. There is nothing to send to
    // the workers except raw datagrams and that's pointless.
    if (schedulingPolicy !== SCHED_RR ||
        message.addressType === 'udp4' ||
        message.addressType === 'udp6') {
      constructor = SharedHandle;
    }

    handles[key] = handle = new constructor(key,
                                            message.address,
                                            message.port,
                                            message.addressType,
                                            message.fd,
                                            message.flags);
  }

  // ...

}

// node@8.3.0 lib/internal/cluster/round_robin_handle.js
function RoundRobinHandle(key, address, port, addressType, fd) {
  this.key = key;
  this.all = {};
  this.free = [];
  this.handles = [];
  this.handle = null;
  this.server = net.createServer(assert.fail);

  if (fd >= 0)
    this.server.listen({ fd });
  else if (port >= 0)
    this.server.listen(port, address);
  else
    this.server.listen(address);  // UNIX socket path.

  // ...

}

// node@8.3.0 lib/net.js
// 主进程启动内部tcp服务时,fd为undefined
function listenInCluster(server, address, port, addressType, backlog, fd, exclusive) {
  exclusive = !!exclusive;

  if (!cluster) cluster = require('cluster');

  // cluster内部tcp服务由master进程启动
  if (cluster.isMaster || exclusive) {
    // Will create a new handle
    // _listen2 sets up the listened handle, it is still named like this
    // to avoid breaking code that wraps this method
    server._listen2(address, port, addressType, backlog, fd);
    return;
  }

  // ...

}

Server.prototype._listen2 = setupListenHandle;  // legacy alias

// 启动tcp服务
function setupListenHandle(address, port, addressType, backlog, fd) {
  debug('setupListenHandle', address, port, addressType, backlog, fd);

  // If there is not yet a handle, we need to create one and bind.
  // In the case of a server sent via IPC, we don't need to do this.
  if (this._handle) {
    debug('setupListenHandle: have a handle already');
  } else {
    debug('setupListenHandle: create a handle');

    // ...

    if (rval === null)
      rval = createServerHandle(address, port, addressType, fd);

    if (typeof rval === 'number') {
      var error = exceptionWithHostPort(rval, 'listen', address, port);
      process.nextTick(emitErrorNT, this, error);
      return;
    }
    this._handle = rval;
  }

  this[async_id_symbol] = getNewAsyncId(this._handle);
  this._handle.onconnection = onconnection;
  this._handle.owner = this;

  // Use a backlog of 512 entries. We pass 511 to the listen() call because
  // the kernel does: backlogsize = roundup_pow_of_two(backlogsize + 1);
  // which will thus give us a backlog of 512 entries.
  var err = this._handle.listen(backlog || 511);

  // ...
}

// 创建Tcp实例
function createServerHandle(address, port, addressType, fd) {
  var err = 0;
  // assign handle in listen, and clean up if bind or listen fails
  var handle;

  var isTCP = false;
  if (typeof fd === 'number' && fd >= 0) {
    // ...
  } else if (port === -1 && addressType === -1) {
    // ...
  } else {
    handle = new TCP();
    isTCP = true;
  }

  // ...

  return handle;
}

每次cluster.fork工作进程,并调用net#Server实例的listen方法时,都会执行RoundRobinHandle.add方法,促使工作进程发送{ack:message.seq}内部消息。基于前述lib/internal/cluster/master.js同样一套侦听内部消息的逻辑,工作进程在侦听{ack}消息过程,将调用lib/internal/cluster/utils.js模块中存储的回调函数。其中,消息对象的ack属性,用于指定将调用哪个回调函数。该回调函数即cluster._getServer方法调用send方法时传入的次参,从而间接调用rr函数。 

// node@8.3.0 lib/internal/cluster/master.js
function queryServer(worker, message) {

  // ...

  // Set custom server data
  handle.add(worker, (errno, reply, handle) => {
    reply = util._extend({
      errno: errno,
      key: key,
      ack: message.seq,
      data: handles[key].data
    }, reply);

    if (errno)
      delete handles[key];  // Gives other workers a chance to retry.

    send(worker, reply, handle);
  });
}

// node@8.3.0 lib/internal/cluster/round_robin_handle.js
RoundRobinHandle.prototype.add = function(worker, send) {
  assert(worker.id in this.all === false);
  this.all[worker.id] = worker;

  const done = () => {
    if (this.handle.getsockname) {
      const out = {};
      this.handle.getsockname(out);
      // TODO(bnoordhuis) Check err.
      send(null, { sockname: out }, null);
    } else {
      send(null, null, null);  // UNIX socket.
    }

    this.handoff(worker);  // In case there are connections pending.
  };

  if (this.server === null)
    return done();

  // Still busy binding.
  this.server.once('listening', done);
  // ...
};

// node@8.3.0 lib/internal/cluster/utils.js
// 工作进程发送{ack}消息时,将触发执行utils模块中存储的回调函数
function internal(worker, cb) {
  return function onInternalMessage(message, handle) {
    // ...

    if (message.ack !== undefined && callbacks[message.ack] !== undefined) {
      fn = callbacks[message.ack];
      delete callbacks[message.ack];
    }

    fn.apply(worker, arguments);
  };
}

// node@8.3.0 lib/internal/cluster/child.js
cluster._getServer = function(obj, options, cb) {

  // ...

  send(message, (reply, handle) => {
    if (typeof obj._setServerData === 'function')
      obj._setServerData(reply.data);

    if (handle)
      shared(reply, handle, indexesKey, cb);  // Shared listen socket.
    else
      rr(reply, indexesKey, cb);              // Round-robin.
  });

  // ...
};

 

rr函数执行过程中,将构建handle={listen,close}对象,并作为次参传入lib/net.js模块中listenOnMasterHandle函数里,并调用listenOnMasterHandle函数。随着listenOnMasterHandle函数的执行,工作进程创建的net#Server实例将添加_handle属性,等到程序调用net#Server实例的_listen2方法时,因为存在_handle属性,该方法内不执行任何有意义的脚本,即不触发net#Server实例原listen方法端口监听动作。

// node@8.3.0 lib/internal/cluster/child.js
// 参数cb为lib/net.js模块中的listenOnMasterHandle函数
function rr(message, indexesKey, cb) {
  if (message.errno)
    return cb(message.errno, null);

  var key = message.key;

  function listen(backlog) {
    // TODO(bnoordhuis) Send a message to the master that tells it to
    // update the backlog size. The actual backlog should probably be
    // the largest requested size by any worker.
    return 0;
  }

  function close() {
    // lib/net.js treats server._handle.close() as effectively synchronous.
    // That means there is a time window between the call to close() and
    // the ack by the master process in which we can still receive handles.
    // onconnection() below handles that by sending those handles back to
    // the master.
    if (key === undefined)
      return;

    send({ act: 'close', key });
    delete handles[key];
    delete indexes[indexesKey];
    key = undefined;
  }

  function getsockname(out) {
    if (key)
      util._extend(out, message.sockname);

    return 0;
  }

  // Faux handle. Mimics a TCPWrap with just enough fidelity to get away
  // with it. Fools net.Server into thinking that it's backed by a real
  // handle. Use a noop function for ref() and unref() because the control
  // channel is going to keep the worker alive anyway.
  const handle = { close, listen, ref: noop, unref: noop };

  if (message.sockname) {
    handle.getsockname = getsockname;  // TCP handles only.
  }

  assert(handles[key] === undefined);
  handles[key] = handle;
  cb(0, handle);
}

// node@8.3.0 lib/net.js
function listenOnMasterHandle(err, handle) {

  // ...

  // Reuse master's server handle
  server._handle = handle;
  // _listen2 sets up the listened handle, it is still named like this
  // to avoid breaking code that wraps this method
  server._listen2(address, port, addressType, backlog, fd);
}

Server.prototype._listen2 = setupListenHandle;  // legacy alias

function setupListenHandle(address, port, addressType, backlog, fd) {
  debug('setupListenHandle', address, port, addressType, backlog, fd);

  // If there is not yet a handle, we need to create one and bind.
  // In the case of a server sent via IPC, we don't need to do this.
  if (this._handle) {
    debug('setupListenHandle: have a handle already');
  }

  // ...

}

 

随后工作进程启动的http或https服务将侦测到"listening"事件,触发工作进程发送{act:"listening"}内部消息,基于前述lib/internal/cluster/master.js同样一套侦听内部消息的逻辑,通过onmessage函数将驱动执行listening函数,触发"linstening"事件,以使开发者订阅"listening"消息。

// node@8.3.0 lib/internal/cluster/child.js
cluster._getServer = function(obj, options, cb) {

  // ...

  obj.once('listening', () => {
    cluster.worker.state = 'listening';
    const address = obj.address();
    message.act = 'listening';
    message.port = address && address.port || options.port;

    // send消息通过lib/internal/cluster/utils.js模块转化为内部消息
    send(message);
  });
};

// node@8.3.0 lib/internal/cluster/master.js
function onmessage(message, handle) {
  const worker = this;

  // ...

  else if (message.act === 'listening')
    listening(worker, message);

  // ...
}

function listening(worker, message) {
  const info = {
    addressType: message.addressType,
    address: message.address,
    port: message.port,
    fd: message.fd
  };

  worker.state = 'listening';
  worker.emit('listening', info);
  cluster.emit('listening', worker, info);
}

服务间的通信

nodejs/cluster模块实现中,主进程内部创建的tcp服务,当其有"connection"事件触发时(即客户端发起请求时),将执行RoundRobinHandle实例的distribute方法,取出空闲的工作进程,并促使该进程发送{act:'newconn'}内部消息。该消息在lib/internal/cluster/child.js模块中得到订阅。工作进程监听到该消息时,将通过onconnection函数执行该进程内的http或https服务的onconnection方法,传入的次参为主进程tcp服务所提供的net#Server实例(即主进程tcp.onconnenction方法的次参),为工作进程中的http或https服务建立新的tcp流;与此同时,工作进程将发送{ack:message.seq}内部消息,执行reply=>{}回调,以调用主进程tcp服务net#Server实例的close方法,促使该服务不再接受新的connection。

// node@8.3.0 lib/internal/cluster/round_robin_handle.js
function RoundRobinHandle(key, address, port, addressType, fd) {

  // ...

  this.server.once('listening', () => {
    this.handle = this.server._handle;// lib/net.js模块中构建的Tcp实例

    // 监听"connection"事件
    this.handle.onconnection = (err, handle) => this.distribute(err, handle);
    this.server._handle = null;
    this.server = null;
  });
}

RoundRobinHandle.prototype.distribute = function(err, handle) {
  this.handles.push(handle);
  const worker = this.free.shift();

  if (worker)
    this.handoff(worker);
};

RoundRobinHandle.prototype.handoff = function(worker) {
  if (worker.id in this.all === false) {
    return;  // Worker is closing (or has closed) the server.
  }

  const handle = this.handles.shift();

  if (handle === undefined) {
    this.free.push(worker);  // Add to ready queue again.
    return;
  }

  const message = { act: 'newconn', key: this.key };

  // 尾参作为工作进程发送{ack:message.seq}内部消息时待执行的函数
  sendHelper(worker.process, message, handle, (reply) => {
    // 工作进程启动http或https服务时,关停tcp服务"connection"事件发出的net#Server
    if (reply.accepted)
      handle.close();
    else
      this.distribute(0, handle);  // Worker is shutting down. Send to another.

    // 嵌套调用handoff方法,若主进程tcp服务没有新的请求,将worker添加this.free中
    this.handoff(worker);
  });
};

// node@8.3.0 lib/internal/cluster/child.js
cluster._setupWorker = function() {

  // ...

  function onmessage(message, handle) {
    if (message.act === 'newconn')
      onconnection(message, handle);
    else if (message.act === 'disconnect')
      _disconnect.call(worker, true);
  }
};

// Round-robin connection.
function onconnection(message, handle) {
  const key = message.key;
  const server = handles[key];
  const accepted = server !== undefined;

  send({ ack: message.seq, accepted });

  if (accepted)
    server.onconnection(0, handle);
}
 

 

简易流程回顾

cluster.fork过程

  1. cluster.fork工作进程执行脚本

  2. 工作进程执行httpServer.listen

  3. 转交listenInCluster函数处理,执行cluster._getServer

    • 工作进程发送{act:"queryServer"},由订阅该消息触发执行queryServer函数,将rr函数添加到回调队列中

    • 实例化RoundRobinHandle,创建内部tcp服务

      • 调用RoundRobinHandle实例的add方法

      • 工作进程发送{ack:message.seq}内部消息,由订阅该消息触发执行rr函数,构建handle={listen,close}对象

      • 内部tcp服务监听listening事件,将connection交给工作进程转发给httpServer

    • 执行listenInCluster函数中的子函数listenOnMasterHandle

      • 为工作进程启动的httpServer添加_handle属性,由于_handle属性为真,置空httpServer._listen2

请求处理过程

  1. 接受新的请求,内部tcp服务调用tcpServer._handle.onconnection方法

  2. 调用RoundRobinHandle实例的distribute方法,取出空闲的进程

  3. 调用RoundRobinHandle实例的handoff方法,工作进程发送{act:'newconn'}内部消息,将handoff方法添加回调队列中

  4. 执行订阅{act:'newconn'}内部消息的绑定函数onconnection

  5. 工作进程的httpServer服务根据主进程的net#Server实例,发起新的tcp流,处理请求

  6. 工作进程发送{ack:message.seq}内部消息,由订阅该消息触发执行handoff方法,调用net#Server实例close方法,不在接受新的请求

  7. 嵌套调用handoff方法,将工作进程置为空闲

使用案列,来自《解读Nodejs多核处理模块cluster》

var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log("master start...");

  // Fork workers.
  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('listening',function(worker,address){
    console.log('listening: worker ' + worker.process.pid +', Address: '+address.address+":"+address.port);
  });

  cluster.on('exit', function(worker, code, signal) {
    console.log('worker ' + worker.process.pid + ' died');
  });
} else {
  http.createServer(function(req, res) {
    console.log('worker'+cluster.worker.id);
    res.writeHead(200);
    res.end("hello world\n");
  }).listen(8080,"127.0.0.1");
}
 

可借鉴处

复杂事件系统的设计,可由send发送的消息args影响订阅函数on的职能。

按nodejs/cluster模块,send函数的次参将作为钩子函数缓存起来,等到发送{ack}类消息时,再取出相应的回调函数执行;send函数发送的消息对象若含有cmd属性,且前缀为"NODE_",将视为内部消息。

文章评论

编程语言是女人
编程语言是女人
程序员和编码员之间的区别
程序员和编码员之间的区别
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
旅行,写作,编程
旅行,写作,编程
我的丈夫是个程序员
我的丈夫是个程序员
Java 与 .NET 的平台发展之争
Java 与 .NET 的平台发展之争
那些争议最大的编程观点
那些争议最大的编程观点
我跳槽是因为他们的显示器更大
我跳槽是因为他们的显示器更大
程序员必看的十大电影
程序员必看的十大电影
10个调试和排错的小建议
10个调试和排错的小建议
如何区分一个程序员是“老手“还是“新手“?
如何区分一个程序员是“老手“还是“新手“?
中美印日四国程序员比较
中美印日四国程序员比较
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
2013年中国软件开发者薪资调查报告
2013年中国软件开发者薪资调查报告
十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
一个程序员的时间管理
一个程序员的时间管理
亲爱的项目经理,我恨你
亲爱的项目经理,我恨你
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
看13位CEO、创始人和高管如何提高工作效率
看13位CEO、创始人和高管如何提高工作效率
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
科技史上最臭名昭著的13大罪犯
科技史上最臭名昭著的13大罪犯
程序员都该阅读的书
程序员都该阅读的书
每天工作4小时的程序员
每天工作4小时的程序员
Java程序员必看电影
Java程序员必看电影
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
鲜为人知的编程真相
鲜为人知的编程真相
程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
程序员周末都喜欢做什么?
程序员周末都喜欢做什么?
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
10个帮程序员减压放松的网站
10个帮程序员减压放松的网站
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
那些性感的让人尖叫的程序员
那些性感的让人尖叫的程序员
代码女神横空出世
代码女神横空出世
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
如何成为一名黑客
如何成为一名黑客
做程序猿的老婆应该注意的一些事情
做程序猿的老婆应该注意的一些事情
程序员的鄙视链
程序员的鄙视链
2013年美国开发者薪资调查报告
2013年美国开发者薪资调查报告
总结2014中国互联网十大段子
总结2014中国互联网十大段子
我是如何打败拖延症的
我是如何打败拖延症的
为什么程序员都是夜猫子
为什么程序员都是夜猫子
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
程序员应该关注的一些事儿
程序员应该关注的一些事儿
不懂技术不要对懂技术的人说这很容易实现
不懂技术不要对懂技术的人说这很容易实现
 程序员的样子
程序员的样子
老程序员的下场
老程序员的下场
程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有