http.createServer process analysis of nodejs

Keywords: Javascript socket github Programming

github:https://github.com/theanarkh/read-nodejs-code

Here is the code for nodejs to create a server. Let's analyze the process together.

var http = require('http');
http.createServer(function (request, response) {
    response.end('Hello World\n');
}).listen(9297);

First, let's go to the lib/http.js module to see the code of this function.

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

It just encapsulates http server.js. Let's keep looking down.

function Server(requestListener) {
  if (!(this instanceof Server)) return new Server(requestListener);
  net.Server.call(this, { allowHalfOpen: true });
  // Callback when http request is received
  if (requestListener) {
    this.on('request', requestListener);
  }

  this.httpAllowHalfOpen = false;
  // Callback for establishing tcp connection
  this.on('connection', connectionListener);

  this.timeout = 2 * 60 * 1000;
  this.keepAliveTimeout = 5000;
  this._pendingResponseData = 0;
  this.maxHeadersCount = null;
}
util.inherits(Server, net.Server);

It is found that there is not much logic in http server.js. Continue to look at the code under lib/net.js.

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

  EventEmitter.call(this);
  // connectionListener has been processed in http.js
  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 errors.TypeError('ERR_INVALID_ARG_TYPE',
                               'options',
                               'Object',
                               options);
  }

  this._connections = 0;
  ......
  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;
}

At this point, the execution of http.createServer is over. We find that this process does not involve a lot of logic, and still stays at the js level. Next, we continue to analyze the process of the listen function. This function is provided by net module. We only look at the key code.

Server.prototype.listen = function(...args) {
  // According to the document, we know that listen can receive several parameters. Here we only pass the port number 9297
  var normalized = normalizeArgs(args);
  //  normalized = [{port: 9297}, null];
  var options = normalized[0];
  var cb = normalized[1];
  // The first time you listen, it will be created. If it is not empty, it means that you have listened
  if (this._handle) {
    throw new errors.Error('ERR_SERVER_ALREADY_LISTEN');
  }
  ......
  listenInCluster(this, null, options.port | 0, 4,
                      backlog, undefined, options.exclusive);
}
function listenInCluster() {
    ...
    server._listen2(address, port, addressType, backlog, fd);
}

_listen2 = setupListenHandle = function() {
    ......
    this._handle = createServerHandle(...);
    this._handle.listen(backlog || 511);
}
function createServerHandle() {
    handle = new TCP(TCPConstants.SERVER);
    handle.bind(address, port);
}

At last, we see the content of the TCP connection. Each server creates a new handle and saves it. The handle is a TCP object. Then execute the bind and listen functions. Let's take a look at the code of the TCP class. TCP is a class provided by C + +. The corresponding file is tcp_wrap.cc. Let's see what happens when we go to new TCP.


void TCPWrap::New(const FunctionCallbackInfo<Value>& args) {
  // This constructor should not be exposed to public javascript.
  // Therefore we assert that we are not trying to call this as a
  // normal function.
  CHECK(args.IsConstructCall());
  CHECK(args[0]->IsInt32());
  Environment* env = Environment::GetCurrent(args);

  int type_value = args[0].As<Int32>()->Value();
  TCPWrap::SocketType type = static_cast<TCPWrap::SocketType>(type_value);

  ProviderType provider;
  switch (type) {
    case SOCKET:
      provider = PROVIDER_TCPWRAP;
      break;
    case SERVER:
      provider = PROVIDER_TCPSERVERWRAP;
      break;
    default:
      UNREACHABLE();
  }

  new TCPWrap(env, args.This(), provider);
}


TCPWrap::TCPWrap(Environment* env, Local<Object> object, ProviderType provider)
    : ConnectionWrap(env, object, provider) {
  int r = uv_tcp_init(env->event_loop(), &handle_);
  CHECK_EQ(r, 0);  
}

We can see that the new TCP is actually to execute the UV TCP init function of libuv and initialize a UV TCP init structure. First, let's take a look at the structure of the uv-u tcp-t structure.

// Initializing the structure of a tcp stream
int uv_tcp_init(uv_loop_t* loop, uv_tcp_t* tcp) {
  // No agreement specified
  return uv_tcp_init_ex(loop, tcp, AF_UNSPEC);
}

int uv_tcp_init_ex(uv_loop_t* loop, uv_tcp_t* tcp, unsigned int flags) {
  int domain;

  /* Use the lower 8 bits for the domain */
  // The lower octet is domain
  domain = flags & 0xFF;
  if (domain != AF_INET && domain != AF_INET6 && domain != AF_UNSPEC)
    return UV_EINVAL;
  // Except for the eighth, the other bits are flags
  if (flags & ~0xFF)
    return UV_EINVAL;

  uv__stream_init(loop, (uv_stream_t*)tcp, UV_TCP);

  /* If anything fails beyond this point we need to remove the handle from
   * the handle queue, since it was added by uv__handle_init in uv_stream_init.
   */

  if (domain != AF_UNSPEC) {
    int err = maybe_new_socket(tcp, domain, 0);
    if (err) {
      // If there is an error, remove the handle from the loop queue
      QUEUE_REMOVE(&tcp->handle_queue);
      return err;
    }
  }

  return 0;
}

We then look at what UV stream init did.

void uv__stream_init(uv_loop_t* loop,
                     uv_stream_t* stream,
                     uv_handle_type type) {
  int err;

  uv__handle_init(loop, (uv_handle_t*)stream, type);
  stream->read_cb = NULL;
  stream->alloc_cb = NULL;
  stream->close_cb = NULL;
  stream->connection_cb = NULL;
  stream->connect_req = NULL;
  stream->shutdown_req = NULL;
  stream->accepted_fd = -1;
  stream->queued_fds = NULL;
  stream->delayed_error = 0;
  QUEUE_INIT(&stream->write_queue);
  QUEUE_INIT(&stream->write_completed_queue);
  stream->write_queue_size = 0;

  if (loop->emfile_fd == -1) {
    err = uv__open_cloexec("/dev/null", O_RDONLY);
    if (err < 0)
        /* In the rare case that "/dev/null" isn't mounted open "/"
         * instead.
         */
        err = uv__open_cloexec("/", O_RDONLY);
    if (err >= 0)
      loop->emfile_fd = err;
  }

#if defined(__APPLE__)
  stream->select = NULL;
#endif /* defined(__APPLE_) */
  // Initialize io observer
  uv__io_init(&stream->io_watcher, uv__stream_io, -1);
}

void uv__io_init(uv__io_t* w, uv__io_cb cb, int fd) {
  assert(cb != NULL);
  assert(fd >= -1);
  // Initialization queue, callback, fd to be monitored
  QUEUE_INIT(&w->pending_queue);
  QUEUE_INIT(&w->watcher_queue);
  w->cb = cb;
  w->fd = fd;
  w->events = 0;
  w->pevents = 0;

#if defined(UV_HAVE_KQUEUE)
  w->rcount = 0;
  w->wcount = 0;
#endif /* defined(UV_HAVE_KQUEUE) */
}

It can be seen from the code that only some initialization operations have been done to the UV ﹐ TCP ﹐ T structure. At this point, the logic of new TCP is finished. The next step is to continue to classify the logic of calling bind and listen in nodejs. The bind function of nodejs corresponds to libuv, which is UV TCP bind, and the listen function corresponds to UV TCP listen.
First look at the core code of a bind.

/* Cannot set IPv6-only mode on non-IPv6 socket. */
  if ((flags & UV_TCP_IPV6ONLY) && addr->sa_family != AF_INET6)
    return UV_EINVAL;
  // Get a socket and set some flags
  err = maybe_new_socket(tcp, addr->sa_family, 0);
  if (err)
    return err;

  on = 1;
  // Set reusable on port
  if (setsockopt(tcp->io_watcher.fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)))
    return UV__ERR(errno);
  bind(tcp->io_watcher.fd, addr, addrlen) && errno != EADDRINUSE
static int maybe_new_socket(uv_tcp_t* handle, int domain, unsigned long flags) {
  struct sockaddr_storage saddr;
  socklen_t slen;

  if (domain == AF_UNSPEC) {
    handle->flags |= flags;
    return 0;
  }
  return new_socket(handle, domain, flags);
}
static int new_socket(uv_tcp_t* handle, int domain, unsigned long flags) {
  struct sockaddr_storage saddr;
  socklen_t slen;
  int sockfd;
  int err;
  // Get a socket
  err = uv__socket(domain, SOCK_STREAM, 0);
  if (err < 0)
    return err;
  sockfd = err;
  // Set options and save socket file descriptor to io observer
  err = uv__stream_open((uv_stream_t*) handle, sockfd, flags);
  if (err) {
    uv__close(sockfd);
    return err;
  }
  ...
  return 0;
}

int uv__stream_open(uv_stream_t* stream, int fd, int flags) {
  if (!(stream->io_watcher.fd == -1 || stream->io_watcher.fd == fd))
    return UV_EBUSY;

  assert(fd >= 0);
  stream->flags |= flags;

  if (stream->type == UV_TCP) {
    if ((stream->flags & UV_HANDLE_TCP_NODELAY) && uv__tcp_nodelay(fd, 1))
      return UV__ERR(errno);

    /* TODO Use delay the user passed in. */
    if ((stream->flags & UV_HANDLE_TCP_KEEPALIVE) &&
        uv__tcp_keepalive(fd, 1, 60)) {
      return UV__ERR(errno);
    }
  }
  ...
  // Save the file descriptor corresponding to socket to IO observer, and libuv will listen to the file descriptor in io poll stage
  stream->io_watcher.fd = fd;

  return 0;
}

The above series of operations are mainly to create a new socket file descriptor, set some flag s, and then save the file descriptor to the IO observer. libuv will listen to the file descriptor in the poll IO stage. If there is an event coming, it will execute the set callback function, which is set in UV stream init. Finally, execute the bind function to bind. Finally, let's analyze the listen function. First, look at the code of tcp_wrapper.cc.

void TCPWrap::Listen(const FunctionCallbackInfo<Value>& args) {
  TCPWrap* wrap;
  ASSIGN_OR_RETURN_UNWRAP(&wrap,
                          args.Holder(),
                          args.GetReturnValue().Set(UV_EBADF));
  int backlog = args[0]->Int32Value();
  int err = uv_listen(reinterpret_cast<uv_stream_t*>(&wrap->handle_),
                      backlog,
                      OnConnection);
  args.GetReturnValue().Set(err);
}

A very important part of the code is the OnConnection function. nodejs sets a callback function OnConnection for the listen function, which will be called when the file descriptors saved in the IO observer are connected. The OnConnection function is defined in connection ﹣ wrap.cc, and TCP ﹣ wrapper inherits connection ﹣ wrap. Let's take a look at UV listen. This function calls UV < TCP < listen. The core code of this function is as follows.

  if (listen(tcp->io_watcher.fd, backlog))
    return UV__ERR(errno);
  // cb is OnConnection
  tcp->connection_cb = cb;
  tcp->flags |= UV_HANDLE_BOUND;

  // There is a libuv layer callback when the connection arrives, which overrides the value set when UV stream init
  tcp->io_watcher.cb = uv__server_io;
  // Registration event
  uv__io_start(tcp->loop, &tcp->io_watcher, POLLIN);

In the poll IO stage of libuv, epoll_wait listens to the incoming connection and then calls uv__server_io. Here is the core code of the function.

// Continue to register events, wait for connection
  uv__io_start(stream->loop, &stream->io_watcher, POLLIN);
  err = uv__accept(uv__stream_fd(stream));
  // Save the socket corresponding to the connection
  stream->accepted_fd = err;
  // Execute nodejs layer callback
  stream->connection_cb(stream, 0);

libuv will remove a connection and get the corresponding socket. Then execute the callback of nodejs layer. At this time, let's take a look at the code of OnConnection.

OnConnection(uv_stream_t* handle,int status)
    if (status == 0) {
        // A new UV TCP T structure
        Local<Object> client_obj = WrapType::Instantiate(env, wrap_data, WrapType::SOCKET);
        WrapType* wrap;
        ASSIGN_OR_RETURN_UNWRAP(&wrap, client_obj);
        uv_stream_t* client_handle = reinterpret_cast<uv_stream_t*>(&wrap->handle_);
        // UV accept returns 0 to indicate success
        if (uv_accept(handle, client_handle))
          return;
        argv[1] = client_obj;
  }
  // Execute the upper callback, which is the onconnection set by net.js
  wrap_data->MakeCallback(env->onconnection_string(), arraysize(argv), argv);

OnConnection creates a new UV TCP structure. Represents this connection. Then call uv_accept.


int uv_accept(uv_stream_t* server, uv_stream_t* client) {
    ...
    // The new UV TCP structure is associated with accept FD to register read and write events
    uv__stream_open(client, server->accepted_fd, UV_HANDLE_READABLE | UV_HANDLE_WRITABLE);
    ...
}

Finally, the nodejs callback is executed.

function onconnection(err, clientHandle) {
  var handle = this;
  var self = handle.owner;
  if (err) {
    self.emit('error', errnoException(err, 'accept'));
    return;
  }
  if (self.maxConnections && self._connections >= self.maxConnections) {
    clientHandle.close();
    return;
  }
  var socket = new Socket({
    handle: clientHandle,
    allowHalfOpen: self.allowHalfOpen,
    pauseOnCreate: self.pauseOnConnect
  });
  socket.readable = socket.writable = true;
  self._connections++;
  socket.server = self;
  socket._server = self;
  DTRACE_NET_SERVER_CONNECTION(socket);
  LTTNG_NET_SERVER_CONNECTION(socket);
  COUNTER_NET_SERVER_CONNECTION(socket);
  // Trigger the connectionListener callback set in http server.js
  self.emit('connection', socket);
}

The overall logic of the listen function is to set the socket to be listenable, then register the event and wait for the connection to arrive. When the connection arrives, call accept to obtain the newly established connection. The callback of TCP wrapper.cc creates a new UV TCP structure to represent the new connection, and then sets the read-write event and the callback to UV stream IO to wait for the data to arrive. Finally, execute the onconnection set by net.js. At this point, the server starts and receives the connection. The next step is to read and write user data. When the user transmits data, the function to process the data is UV stream io. We will continue to analyze the reading and writing of data later.

Students who do technology are welcome to pay attention to public programming acrobatics. Share technology, exchange technology.

Posted by Morbius on Mon, 11 Nov 2019 23:11:45 -0800