Node source code: http module

http module

The HTTP module is located in / lib/http.js. Let's take a look at the core method of the module, createServer.

function createServer(opts, requestListener) {
  return new Server(opts, requestListener);
}

The function of createServer is to create a Server class, which is located in / lib/_http_server.js

function Server(options, requestListener) {
  if (!(this instanceof Server)) return new Server(options, requestListener);

  if (typeof options === 'function') {
    requestListener = options;
    options = {};
  } else if (options == null || typeof options === 'object') {
    options = { ...options };
  } else {
    throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
  }

  // options allows two parameters to be passed, IncomingMessage and ServerResponse
  // IncomingMessage is the information requested by the client and ServerResponse is the data responded by the server.
  // IncomingMessage and ServerResponse allow some custom operations after inheritance
  // IncomingMessage is mainly responsible for serializing HTTP message and taking out and serializing the content of message header.
  // ServerResponse provides some interfaces for quick response clients, and sets the interface for response message header content.
  this[kIncomingMessage] = options.IncomingMessage || IncomingMessage;
  this[kServerResponse] = options.ServerResponse || ServerResponse;

  // The Server class is still inherited from the net.Server class
  net.Server.call(this, { allowHalfOpen: true });

  if (requestListener) {
    // After triggering the request event, call the requestListener function
    this.on('request', requestListener);
  }

  // Similar option to this. Too lazy to write my own docs.
  // http://www.squid-cache.org/Doc/config/half_closed_clients/
  // http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
  this.httpAllowHalfOpen = false;

  this.on('connection', connectionListener);

  // Timeout, disabled by default
  this.timeout = 0;
  // When the connection is inactive for a certain period of time, disconnect the TCP connection, and reestablish the connection with three handshakes.
  this.keepAliveTimeout = 5000;
  // Maximum response headers, unlimited by default
  this.maxHeadersCount = null;
  this.headersTimeout = 40 * 1000; // 40 seconds
}

We need to go further down to see what the net.Server class looks like. The file is located in / lib/net.js.

function Server(options, connectionListener) {
  if (!(this instanceof Server))
    return new Server(options, connectionListener);

  // Inherited from EventEmitter class, which belongs to event class
  EventEmitter.call(this);

  if (typeof options === 'function') {
    connectionListener = options;
    options = {};
    this.on('connection', connectionListener);
  } else if (options == null || typeof options === 'object') {
    options = { ...options };

    if (typeof connectionListener === 'function') {
      this.on('connection', connectionListener);
    }
  } else {
    throw new ERR_INVALID_ARG_TYPE('options', 'Object', options);
  }

  this._connections = 0;

  Object.defineProperty(this, 'connections', {
    get: deprecate(() => {

      if (this._usingWorkers) {
        return null;
      }
      return this._connections;
    }, 'Server.connections property is deprecated. ' +
       'Use Server.getConnections method instead.', 'DEP0020'),
    set: deprecate((val) => (this._connections = val),
                   'Server.connections property is deprecated.',
                   'DEP0020'),
    configurable: true, enumerable: false
  });

  this[async_id_symbol] = -1;
  this._handle = null;
  this._usingWorkers = false;
  this._workers = [];
  this._unref = false;

  this.allowHalfOpen = options.allowHalfOpen || false;
  this.pauseOnConnect = !!options.pauseOnConnect;
}
Object.setPrototypeOf(Server.prototype, EventEmitter.prototype);
Object.setPrototypeOf(Server, EventEmitter);

From the code, we can see that the net.Server class itself is an event distribution center, and its specific implementation is composed of multiple internal methods. Let's first look at one of our most commonly used methods, listen.

Server.prototype.listen = function(...args) {
  const normalized = normalizeArgs(args);
  // After serialization, options contains port and host fields (if filled in)
  var options = normalized[0];
  const cb = normalized[1];

  if (this._handle) {
    throw new ERR_SERVER_ALREADY_LISTEN();
  }

  if (cb !== null) {
    // A one-time event is bound to the callback function, which is called when the listening event is triggered
    this.once('listening', cb);
  }
  
  // ... 

  var backlog;
  if (typeof options.port === 'number' || typeof options.port === 'string') {
    if (!isLegalPort(options.port)) {
      throw new ERR_SOCKET_BAD_PORT(options.port);
    }
    backlog = options.backlog || backlogFromArgs;
    // start TCP server listening on host:port
    if (options.host) {
      // If the Host is localhost and the port is 3000, then the lookupAndListen method is called.
      lookupAndListen(this, options.port | 0, options.host, backlog,
                      options.exclusive, flags);
    } else { // Undefined host, listens on unspecified address
      // Default addressType 4 will be used to search for master server
      listenInCluster(this, null, options.port | 0, 4,
                      backlog, undefined, options.exclusive);
    }
    return this;
  }

  // ...
};

function lookupAndListen(self, port, address, backlog, exclusive, flags) {
  if (dns === undefined) dns = require('dns');
  // In order to get an ip address, dns module is used to parse the host.
  dns.lookup(address, function doListen(err, ip, addressType) {
    if (err) {
      self.emit('error', err);
    } else {
      addressType = ip ? addressType : 4;
      // After getting the ip address, I executed listenInCluster.
      listenInCluster(self, ip, port, addressType,
                      backlog, undefined, exclusive, flags);
    }
  });
}

function listenInCluster(server, address, port, addressType,
                         backlog, fd, exclusive, flags) {
  exclusive = !!exclusive;

  // Cluster is a cluster module in Node, which can create subprocesses of shared server port.
  // The role of listen is to enable a process to listen to http requests
  if (cluster === undefined) 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
    // This method is the final one
    server._listen2(address, port, addressType, backlog, fd, flags);
    return;
  }

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

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

  function listenOnMasterHandle(err, handle) {
    err = checkBindError(err, port, handle);

    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, flags);
  }
}

// Server. _listen2points to a setupListenHandle method, and setupListenHandle finally points to the createServerHandle method.
// Returns handle if it can be created, or error code if it can't
function createServerHandle(address, port, addressType, fd, flags) {
  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) {
    try {
      handle = createHandle(fd, true);
    } catch (e) {
      // Not a fd we can listen on.  This will trigger an error.
      debug('listen invalid fd=%d:', fd, e.message);
      return UV_EINVAL;
    }

    err = handle.open(fd);
    if (err)
      return err;

    assert(!address && !port);
  } else if (port === -1 && addressType === -1) {
    handle = new Pipe(PipeConstants.SERVER);
    if (process.platform === 'win32') {
      var instances = parseInt(process.env.NODE_PENDING_PIPE_INSTANCES);
      if (!Number.isNaN(instances)) {
        handle.setPendingInstances(instances);
      }
    }
  } else {
    // Finally, a TCP service is started. When the TCP port receives data, it will be pushed to the HTTP process.
    // The specific implementation may be that TCP internally sends out emit for event notification, and at the same time converts the byte stream to the message format (protocol specification) acceptable to HTTP protocol.
    handle = new TCP(TCPConstants.SERVER);
    isTCP = true;
  }

  if (address || port || isTCP) {
    debug('bind to', address || 'any');
    if (!address) {
      // Try binding to ipv6 first
      err = handle.bind6(DEFAULT_IPV6_ADDR, port, flags);
      if (err) {
        handle.close();
        // Fallback to ipv4
        return createServerHandle(DEFAULT_IPV4_ADDR, port);
      }
    } else if (addressType === 6) {
      err = handle.bind6(address, port, flags);
    } else {
      // In this step, bind the host and port.
      err = handle.bind(address, port);
    }
  }
  
  return handle;
}

From the above we can see that the calling process of http.createServer is as follows:

  • http.createServer actually returns a Server class, which is essentially inherited from EventEmitter and used to receive and distribute events to notify external calls. At this time, the process is not started.
  • http.createServer().listen(). At the bottom, the built-in module TCP of Node is called to start a TCP process and passively open the waiting connection.
  • When the TCP port receives the connection, it receives the data from the sender, and then passes the received byte stream to the application layer. After that, it decomposes the message format according to the http protocol specification through the http ﹣ parser module, and notifies the Server class in the form of request notification.
  • When the Server class receives the event notification, it uses the IncomingMessage class to analyze the received data, serializes the HTTP message, takes out and serializes the content of the message header to the object format of Javascript, and uses the ServerResponse class to provide some interfaces that respond to the client quickly, sets the interfaces that respond to the content of the message header, and then As two parameters (req, RES) = > {} in the callback parameter of listen, the callback function is triggered.

Keywords: node.js DNS Javascript

Added by orion2004 on Thu, 24 Oct 2019 11:12:06 +0300