交流群:462197261站长百科站长论坛热门标签收藏本站北冥有鱼 互联网前沿资源第一站 助力全行业互联网+
点击这里给我发消息
  • 当前位置:
  • node.js的http.createServer过程深入解析

    下面是nodejs创建一个服务器的代码。接下来我们一起分析这个过程。

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

    首先我们去到lib/http.js模块看一下这个函数的代码。

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

    只是对_http_server.js做了些封装。我们继续往下看。

    function Server(requestListener) {
     if (!(this instanceof Server)) return new Server(requestListener);
     net.Server.call(this, { allowHalfOpen: true });
     // 收到http请求时执行的回调
     if (requestListener) {
      this.on('request', requestListener);
     }
    
     this.httpAllowHalfOpen = false;
     // 建立tcp连接的回调
     this.on('connection', connectionListener);
    
     this.timeout = 2 * 60 * 1000;
     this.keepAliveTimeout = 5000;
     this._pendingResponseData = 0;
     this.maxHeadersCount = null;
    }
    util.inherits(Server, net.Server);

    发现_http_server.js也没有太多逻辑,继续看lib/net.js下的代码。

    function Server(options, connectionListener) {
     if (!(this instanceof Server))
      return new Server(options, connectionListener);
    
     EventEmitter.call(this);
     // connectionListener在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;
    }

    至此http.createServer就执行结束了,我们发现这个过程还没有涉及到很多逻辑,并且还是停留到js层面。接下来我们继续分析listen函数的过程。该函数是net模块提供的。我们只看关键的代码。

    Server.prototype.listen = function(...args) {
     // 处理入参,根据文档我们知道listen可以接收好几个参数,我们这里是只传了端口号9297
     var normalized = normalizeArgs(args);
     // normalized = [{port: 9297}, null];
     var options = normalized[0];
     var cb = normalized[1];
     // 第一次listen的时候会创建,如果非空说明已经listen过
     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);
    }

    到这我们终于看到了tcp连接的内容,每一个服务器新建一个handle并且保存他,该handle是一个TCP对象。然后执行bind和listen函数。接下来我们就看一下TCP类的代码。TCP是C++提供的类。对应的文件是tcp_wrap.cc。我们看看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); 
    }

    我们看到,new TCP的时候其实是执行libuv的uv_tcp_init函数,初始化一个uv_tcp_t的结构体。首先我们先看一下uv_tcp_t结构体的结构。

    uv_tcp_t
    uv_tcp_t
    // 初始化一个tcp流的结构体
    int uv_tcp_init(uv_loop_t* loop, uv_tcp_t* tcp) {
     // 未指定未指定协议
     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 */
     // 低八位是domain
     domain = flags & 0xFF;
     if (domain != AF_INET && domain != AF_INET6 && domain != AF_UNSPEC)
      return UV_EINVAL;
     // 除了第八位的其他位是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) {
       // 出错则把该handle移除loop队列
       QUEUE_REMOVE(&tcp->handle_queue);
       return err;
      }
     }
    
     return 0;
    }

    我们接着看uv__stream_init做了什么事情。

    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_) */
     // 初始化io观察者
     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);
     // 初始化队列,回调,需要监听的fd
     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) */
    }

    从代码可以知道,只是对uv_tcp_t结构体做了一些初始化操作。到这,new TCP的逻辑就执行完毕了。接下来就是继续分类nodejs里调用bind和listen的逻辑。nodejs的bind对应libuv的函数是uv__tcp_bind,listen对应的是uv_tcp_listen。
    先看一个bind的核心代码。

    /* Cannot set IPv6-only mode on non-IPv6 socket. */
     if ((flags & UV_TCP_IPV6ONLY) && addr->sa_family != AF_INET6)
      return UV_EINVAL;
     // 获取一个socket并且设置某些标记
     err = maybe_new_socket(tcp, addr->sa_family, 0);
     if (err)
      return err;
    
     on = 1;
     // 设置在端口可重用
     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;
     // 获取一个socket
     err = uv__socket(domain, SOCK_STREAM, 0);
     if (err < 0)
      return err;
     sockfd = err;
     // 设置选项和保存socket的文件描述符到io观察者中
     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);
      }
     }
     ...
     // 保存socket对应的文件描述符到io观察者中,libuv会在io poll阶段监听该文件描述符
     stream->io_watcher.fd = fd;
    
     return 0;
    }

    上面的一系列操作主要是新建一个socket文件描述符,设置一些flag,然后把文件描述符保存到IO观察者中,libuv在poll IO阶段会监听该文件描述符,如果有事件到来,会执行设置的回调函数,该函数是在uv__stream_init里设置的uv__stream_io。最后执行bind函数进行绑定操作。最后我们来分析一下listen函数。首先看下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);
    }

    代码中有个很重要的地方就是OnConnection函数,nodejs给listen函数设置了一个回调函数OnConnection,该函数在IO观察者里保存的文件描述符有连接到来时会被调用。OnConnection函数是在connection_wrap.cc定义的,tcp_wrapper继承了connection_wrap。下面我们先看一下uv_listen。该函数调用了uv_tcp_listen。该函数的核心代码如下。

     if (listen(tcp->io_watcher.fd, backlog))
      return UV__ERR(errno);
     // cb即OnConnection
     tcp->connection_cb = cb;
     tcp->flags |= UV_HANDLE_BOUND;
    
     // 有连接到来时的libuv层回调,覆盖了uv_stream_init时设置的值
     tcp->io_watcher.cb = uv__server_io;
     // 注册事件
     uv__io_start(tcp->loop, &tcp->io_watcher, POLLIN);

    在libuv的poll IO阶段,epoll_wait会监听到到来的连接,然后调用uv__server_io。下面是该函数的核心代码。

    // 继续注册事件,等待连接
     uv__io_start(stream->loop, &stream->io_watcher, POLLIN);
     err = uv__accept(uv__stream_fd(stream));
     // 保存连接对应的socket
     stream->accepted_fd = err;
     // 执行nodejs层回调
     stream->connection_cb(stream, 0);

    libuv会摘下一个连接,得到对应的socket。然后执行nodejs层的回调,这时候我们来看一下OnConnection的代码。

    OnConnection(uv_stream_t* handle,int status)
      if (status == 0) {
        // 新建一个uv_tcp_t结构体
        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返回0表示成功
        if (uv_accept(handle, client_handle))
         return;
        argv[1] = client_obj;
     }
     // 执行上层的回调,该回调是net.js设置的onconnection
     wrap_data->MakeCallback(env->onconnection_string(), arraysize(argv), argv);

    OnConnection新建了一个uv_tcp_t结构体。代表这个连接。然后调用uv_accept。

    int uv_accept(uv_stream_t* server, uv_stream_t* client) {
      ...
      // 新建的uv_tcp_t结构体关联accept_fd,注册读写事件
      uv__stream_open(client, server->accepted_fd, UV_HANDLE_READABLE | UV_HANDLE_WRITABLE);
      ...
    }

    最后执行nodejs的回调。

    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);
     // 触发_http_server.js里设置的connectionListener回调
     self.emit('connection', socket);
    }

    listen函数总体的逻辑就是把socket设置为可监听,然后注册事件,等待连接的到来,连接到来的时候,调用accept获取新建立的连接,tcp_wrapper.cc的回调新建一个uv_tcp_t结构体,代表新的连接,然后设置可读写事件,并且设置回调为uv__stream_io,等待数据的到来。最后执行_http_server.js设置的回调connectionListener。至此,服务器启动并且接收连接的过程就完成了。接下来就是对用户数据的读写。当用户传来数据时,处理数据的函数是uv__stream_io。后面继续解析数据的读写。

    总结

    以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对北冥有鱼的支持。

    您可能感兴趣的文章:

    • node.js中的http.createServer方法使用说明

    广而告之:
    热门推荐:
    jQuery实现字符串按指定长度加入特定内容的方法

    本文实例讲述了jQuery实现字符串按指定长度加入特定内容的方法。分享给大家供大家参考。具体分析如下: 最近的一个项目中需要将手机号码按一定长度用标识符隔开,便于阅读,网上找了一会发现没有合适的代码于是就自己手写了一个函数,可以按指定长度往字符串中插入分隔符,有需···

    基于javascipt-dom编程 table对象的使用

    排名练习:向表格添加数据,当编号遇到重复给予提示并且无法添加,而且按排名顺序添加数据 参考代码:复制代码 代码如下:<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html><head><title>ggggg</title><link rel=···

    一个简单的jQuery插件ajaxfileupload.js实现ajax上传文件例子

    jQuery插件AjaxFileUpload可以实现ajax文件上传,该插件使用非常简单,首先了解一下正确使用AjaxFileUpload插件的方法,然后再了解一些常见的错误信息和解决方法。 使用说明 需要使用jQuery库文件 和AjaxFileUpload库文件 使用实例 一,包含文件部分 复制代码 代码如下: <sc···

    JQuery Mobile实现导航栏和页脚

    导航栏由一组水平排列的链接构成,通常位于页眉或页脚内部。 默认地,导航栏中的链接会自动转换为按钮(无需 data-role="button")。 导航栏部分的代码一般放置在data-role为header的div的内。 <div data-role="header"> <a href="#" data-role="button" data-icon=···

    可以显示单图片,多图片ajax请求的ThickBox3.1类下载

    ThickBox是一个基于JQuery类库的扩展,它能在浏览器界面上显示非常棒的UI框, 它可以显示单图片,多图片,ajax请求内容或链接内容. 这是从官方上翻译过来的一个实例演示+代码调用.非常方便好用,有了这个,基本上就可以自由应用ThickBox. 以下的是ThickBox3···

    php基于数组函数实现关联表的编辑操作示例

    本文实例讲述了php基于数组函数实现关联表的编辑操作。分享给大家供大家参考,具体如下: 需求为,在创建学校时,需要添加应用,于是创建了个学校应用关联表,编辑学校并提交时,后台需要判断更新的应用是否为一开始提交的,或是有新的应用提交,有旧的应用删除,简化为数组概···

    统计某个数据表里有多少文章

    ([totaldata]"sjhy",2,0[/totaldata]) [totaldata]栏目ID,操作类型,时间范围[/totaldata] 栏目ID 也可写数据表名称 参数说明: 操作类型说明: 操作类型

    PHP 抓取网页图片并且另存为的实现代码

    下面是源代码,及其相关解释 复制代码 代码如下: <?php //URL是远程的完整图片地址,不能为空, $filename 是另存为的图片名字 //默认把图片放在以此脚本相同的目录里 function GrabImage($url, $filename=""){ //$url 为空则返回 false; if($url == ""){return false;···

    Laravel实现用户注册和登录

    Laravel身为最优雅的PHP框架,很多学习PHP的小伙伴造就对Laravel垂涎欲滴。今天就来实现你的愿望,让我们一起从零开始,利用Laravel实现Web应用最常见的注册和登录功能!所有的课程源码已放在Github上:laravel-start. Race Start ! 首先我们来明确一下我们这个课程需要的东西···

    英文:A链接标记ie下会自动补全href

    英文:A链接标记ie下会自动补全href. Whilst working on the Ajax Link Tracker and MapSurface I have come across an inconsistency in how the href attribute is retrieved using DOM Scripting. The href attribute is different to other element attributes···