设为首页 收藏本站
查看: 1651|回复: 0

[经验分享] nginx源码分析之网络初始化

[复制链接]
累计签到:1 天
连续签到:1 天
发表于 2014-6-17 10:22:01 | 显示全部楼层 |阅读模式
nginx作为一个高性能的HTTP服务器,网络的处理是其核心,了解网络的初始化有助于加深对nginx网络处理的了解,本文主要通过nginx的源代码来分析其网络初始化。
从配置文件中读取初始化信息

  与网络有关的配置命令主要有两个:listen和sever_name。首先先了解这两个命令的用法。
listen

  listen命令设置nginx监听地址,nginx从这里接受请求。对于IP协议,这个地址就是address和port;对于UNIX域套接字协议,这个地址就是path。 一条listen指令只能指定一个address或者port。 address也可以是主机名。 比如:

1 listen 127.0.0.1:8000;
2 listen 127.0.0.1;
3 listen 8000;
4 listen *:8000;
5 listen localhost:8000;

  IPv6地址用方括号来表示:

1 listen [::]:8000;
2 listen [fe80::1];

  UNIX域套接字则使用“unix:”前缀:

1 listen unix:/var/run/nginx.sock;

  如果只定义了address,nginx将使用80端口。在没有定义listen指令的情况下,如果以超级用户权限运行nginx,它将监听*:80,否则他将监听*:8000。如果listen指令携带default_server参数,当前虚拟主机将成为指定address:port的默认虚拟主机。 如果任何listen指令都没有携带default_server参数,那么第一个监听address:port的虚拟主机将被作为这个地址的默认虚拟主机。之所以会有默认虚拟主机,是由于同一个address:port可能会隶属于很多个虚拟主机,而区分这些虚拟主机则是用server_name指定各个虚拟主机的主机名。

  可以为listen指令定义若干额外的参数,这些参数用于套接字相关的系统调用。 这些参数可以在任何listen指令中指定,但是对于每个address:port,只能定义一次。具体参数看以到nginx帮助文档中查到,这里就不再说明了。

  更详细的介绍:http://nginx.org/cn/docs/http/ngx_http_core_module.html#listen
server_name

  listen指令描述虚拟主机接受连接的地址和端口,用server_name指令列出虚拟主机的所有主机名。

  设置虚拟主机名,比如:

1 server {
2     server_name example.com www.example.com;
3 }

  第一个名字成为虚拟主机的首要主机名。

  主机名中可以含有星号(“*”),以替代名字的开始部分或结尾部分:

1 server {
2     server_name example.com *.example.com www.example.*;
3 }

  也可以在主机名中使用正则表达式,就是在名字前面补一个波浪线(“~”):

1 server {
2     server_name www.example.com ~^www\d+\.example\.com$;
3 }

  更详细的介绍:http://nginx.org/cn/docs/http/server_names.html

  了解了这两个命令的用法后,下面来看下源码中处理这两个命令的函数ngx_http_core_listen和ngx_http_core_server_name,这两个函数都在ngx_http_core_module.c文件中定义,也就是说这两个命令属于模块ngx_http_core_module。
static char *
ngx_http_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ...
    cscf->listen = 1;
    value = cf->args->elts;
    ngx_memzero(&u, sizeof(ngx_url_t));

    u.url = value[1];
    u.listen = 1;
    u.default_port = 80;
    //解析listen命令后面的参数,ip:port
    if (ngx_parse_url(cf->pool, &u) != NGX_OK) {
        if (u.err) {
            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                               "%s in \"%V\" of the \"listen\" directive",
                               u.err, &u.url);
        }
        return NGX_CONF_ERROR;
    }
    //根据上面解析的参数初始化ngx_http_listen_opt_t结构
    ngx_memzero(&lsopt, sizeof(ngx_http_listen_opt_t));
    ngx_memcpy(&lsopt.u.sockaddr, u.sockaddr, u.socklen);
    lsopt.socklen = u.socklen;
    ...
    //解析其它参数,如default_server,bind等,并通过这些参数设置lsopt
    ...
    //将解析到的虚拟主机的地址信息加入到监听列表中
      if (ngx_http_add_listen(cf, cscf, &lsopt) == NGX_OK) {
        return NGX_CONF_OK;
    }
    return NGX_CONF_ERROR;
}
从ngx_http_core_listen函数代码可以看出,ngx_http_add_listen函数是其主要的部分,下面看下ngx_http_add_listen:
ngx_int_t
ngx_http_add_listen(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
    ngx_http_listen_opt_t *lsopt)
{
    ...
    //获取ngx_http_core_module的main配置结构
    cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
    if (cmcf->ports == NULL) {  //初始化ports数组
        cmcf->ports = ngx_array_create(cf->temp_pool, 2,
                                       sizeof(ngx_http_conf_port_t));
        if (cmcf->ports == NULL) {
            return NGX_ERROR;
        }
    }
    sa = &lsopt->u.sockaddr;
    ...  //解析协议
    port = cmcf->ports->elts;  //查看已经注册的port,是否新加入地址信息中的port已经存在了
    for (i = 0; i < cmcf->ports->nelts; i++) {
        if (p != port.port || sa->sa_family != port.family) {
            continue;
        }
        //port已经存在了,将地址信息加入到这个port的地址列表中
        return ngx_http_add_addresses(cf, cscf, &port, lsopt);
    }
    //port不存在,将新的port加入到ports数组中
    port = ngx_array_push(cmcf->ports);
    if (port == NULL) {
        return NGX_ERROR;
    }
    port->family = sa->sa_family;
    port->port = p;
    port->addrs.elts = NULL;
    return ngx_http_add_address(cf, cscf, port, lsopt);  //将地址信息加入到对应port的地址列表中,一个port可以对应过个地址
}
ngx_http_add_listen中调用了ngx_http_add_addresses和ngx_http_add_address函数,先看下ngx_http_add_addresses:
static ngx_int_t
ngx_http_add_addresses(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
    ngx_http_conf_port_t *port, ngx_http_listen_opt_t *lsopt)
{
    ...
    sa = &lsopt->u.sockaddr;
    ...
    p = lsopt->u.sockaddr_data + off;
    addr = port->addrs.elts;
    for (i = 0; i < port->addrs.nelts; i++) {
        if (ngx_memcmp(p, addr.opt.u.sockaddr_data + off, len) != 0) {
            continue;
        }
        //新加入的地址已经在地址列表中存在了,将新的虚拟主机信息加入到这个地址的虚拟主机列表中
        if (ngx_http_add_server(cf, cscf, &addr) != NGX_OK) {
            return NGX_ERROR;
        }
        default_server = addr.opt.default_server;
        if (lsopt->set) {  //新的虚拟主机信息中设置了其它参数

            if (addr.opt.set) {
                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                        "duplicate listen options for %s", addr.opt.addr);
                return NGX_ERROR;
            }

            addr.opt = *lsopt;
        }
        if (lsopt->default_server) {  //新的虚拟主机被设置为默认主机

            if (default_server) {
                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                        "a duplicate default server for %s", addr.opt.addr);
                return NGX_ERROR;
            }
            default_server = 1;
            addr.default_server = cscf;
        }
        addr.opt.default_server = default_server;
        ...
        return NGX_OK;
    }
    //添加新地址信息到port的地址列表中
    return ngx_http_add_address(cf, cscf, port, lsopt);
}
 ngx_http_add_addresses函数中如果address:port都已经存在了,则调用ngx_http_add_server将新的虚拟主机的配置加入到address:port对应的虚拟主机列表中,由于一个address:port是可以对应多个虚拟主机的。如果address:port不存在,则调用ngx_http_add_address,将新的address加入到port地址列表中。下面看下ngx_http_add_address函数:
ngx_http_add_address(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
    ngx_http_conf_port_t *port, ngx_http_listen_opt_t *lsopt)
{
    ngx_http_conf_addr_t  *addr;
    //初始化port地址列表
    if (port->addrs.elts == NULL) {
        if (ngx_array_init(&port->addrs, cf->temp_pool, 4,
                           sizeof(ngx_http_conf_addr_t))
            != NGX_OK)
        {
            return NGX_ERROR;
        }
    }
    ...
    //将新地址加入到地址列表中
    addr = ngx_array_push(&port->addrs);
    if (addr == NULL) {
        return NGX_ERROR;
    }
    addr->opt = *lsopt;
    addr->hash.buckets = NULL;
    addr->hash.size = 0;
    addr->wc_head = NULL;
    addr->wc_tail = NULL;
    addr->default_server = cscf;
    addr->servers.elts = NULL;
    //将新的虚拟主机信息加入到这个地址的虚拟主机列表中
    return ngx_http_add_server(cf, cscf, addr);
}
 这个函数代码很简单,初始化地址列表,并调用ngx_http_add_server将新的虚拟主机的配置加入到address:port对应的虚拟主机列表中。下面再看下ngx_http_add_server函数:
static ngx_int_t
ngx_http_add_server(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
    ngx_http_conf_addr_t *addr)
{
    ...
    server = ngx_array_push(&addr->servers);  //添加新的虚拟主机配置
    if (server == NULL) {
        return NGX_ERROR;
    }
    *server = cscf;
    return NGX_OK;
}
 上面的对listen指令的处理函数基本分析完了,接下来再分析server_name指令对应的函数ngx_http_core_server_name:
static char *
ngx_http_core_server_name(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ...
    value = cf->args->elts;
    for (i = 1; i < cf->args->nelts; i++) {
        ...
        sn = ngx_array_push(&cscf->server_names);
        if (sn == NULL) {
            return NGX_CONF_ERROR;
        }
        ...
        sn->name = value;
    ...
}
这个函数主要是把server_name命令后面各个主机名放到当前虚拟主机配置的server_names数组中。

  分析到这里,已经将配置文件中所有虚拟主机配置信息都读取到ngx_http_core_module模块的配置信息的ports中。在http命令的处理函数ngx_http_block最后调用了函数ngx_http_optimize_servers对上面的配置信息做了优化,下面具体来看下这个函数:
static ngx_int_t
ngx_http_optimize_servers(ngx_conf_t *cf, ngx_http_core_main_conf_t *cmcf,
    ngx_array_t *ports)
{
    ...
    port = ports->elts;
    for (p = 0; p < ports->nelts; p++) {

        ngx_sort(port[p].addrs.elts, (size_t) port[p].addrs.nelts,
                 sizeof(ngx_http_conf_addr_t), ngx_http_cmp_conf_addrs);
        addr = port[p].addrs.elts;
        for (a = 0; a < port[p].addrs.nelts; a++) {
        ...
            if (ngx_http_server_names(cf, cmcf, &addr[a]) != NGX_OK) {
                return NGX_ERROR;
            }
        }
        if (ngx_http_init_listening(cf, &port[p]) != NGX_OK) {
            return NGX_ERROR;
        }
    }
    return NGX_OK;
}
 这个函数先对每个address:port调用ngx_http_server_names函数,然后对每个port调用ngx_http_init_listening函数。下面看看ngx_http_server_names函数:
static ngx_int_t
ngx_http_server_names(ngx_conf_t *cf, ngx_http_core_main_conf_t *cmcf,
    ngx_http_conf_addr_t *addr)
{
    ...
    if (ngx_hash_keys_array_init(&ha, NGX_HASH_LARGE) != NGX_OK) {
        goto failed;
    }
    cscfp = addr->servers.elts;
    for (s = 0; s < addr->servers.nelts; s++) {
        //每个server_name后面会带有多个域名
        name = cscfp->server_names.elts;
        for (n = 0; n < cscfp->server_names.nelts; n++) {
            ...
            rc = ngx_hash_add_key(&ha, &name[n].name, name[n].server,
                                  NGX_HASH_WILDCARD_KEY);
            ...
            }
        }
    }
    ...
    if (ha.keys.nelts) {  //无通配
        hash.hash = &addr->hash;  //非通配hash
        hash.temp_pool = NULL;

        if (ngx_hash_init(&hash, ha.keys.elts, ha.keys.nelts) != NGX_OK) {
            goto failed;
        }
    }
    if (ha.dns_wc_head.nelts) {  //前缀通配
        ngx_qsort(ha.dns_wc_head.elts, (size_t) ha.dns_wc_head.nelts,
                  sizeof(ngx_hash_key_t), ngx_http_cmp_dns_wildcards);

        hash.hash = NULL;  //使用通配hash
        hash.temp_pool = ha.temp_pool;
        if (ngx_hash_wildcard_init(&hash, ha.dns_wc_head.elts,
                                   ha.dns_wc_head.nelts)
        ...
        addr->wc_head = (ngx_hash_wildcard_t *) hash.hash;
    }
    if (ha.dns_wc_tail.nelts) {  //后缀通配
        ngx_qsort(ha.dns_wc_tail.elts, (size_t) ha.dns_wc_tail.nelts,
                  sizeof(ngx_hash_key_t), ngx_http_cmp_dns_wildcards);
        hash.hash = NULL;  //使用通配hash
        hash.temp_pool = ha.temp_pool;
        if (ngx_hash_wildcard_init(&hash, ha.dns_wc_tail.elts,
                                   ha.dns_wc_tail.nelts)
        ...
        addr->wc_tail = (ngx_hash_wildcard_t *) hash.hash;
    }
    ...
}
上面代码主要就是将每个address:port对应的所有域名与域名所在的虚拟主机配置信息建立hash映射,这样就可以通过域名快速找到域名所在的虚拟主机配置信息。有关nginx的hash可以参考nginx源码分析之hash的实现这篇文章。下面再看下ngx_http_init_listening函数:
static ngx_int_t
ngx_http_init_listening(ngx_conf_t *cf, ngx_http_conf_port_t *port)
{
    ...
    addr = port->addrs.elts;
    last = port->addrs.nelts;
    if (addr[last - 1].opt.wildcard) {
        addr[last - 1].opt.bind = 1;
        bind_wildcard = 1;

    } else {
        bind_wildcard = 0;
    }
    i = 0;
    while (i < last) {  //last代表的是address:port的个数
        //忽略隐式绑定
        if (bind_wildcard && !addr.opt.bind) {
            i++;
            continue;
        }
        //这个函数里面将会创建,并且初始化listen结构,这个listen已经存放在cycle结构的listen数组中
        ls = ngx_http_add_listening(cf, &addr);
        if (ls == NULL) {
            return NGX_ERROR;
        }
        hport = ngx_pcalloc(cf->pool, sizeof(ngx_http_port_t));
        ...
        ls->servers = hport;
        if (i == last - 1) {  //将*:port和没有显式bind的address:port放在同一个listen中
            hport->naddrs = last;
        } else {
            hport->naddrs = 1;
            i = 0;  //i重新赋值为0
        }
        switch (ls->sockaddr->sa_family) {
        ...
        default: /* AF_INET */
            if (ngx_http_add_addrs(cf, hport, addr) != NGX_OK) {  //初始化虚拟主机相关的地址,设置hash等等.
                return NGX_ERROR;
            }
            break;
        }
        addr++;
        last--;
    }
    return NGX_OK;
}
 这个函数就是遍历某个端口port对应的所有address,如果所有address中不包含通配符,则对所有的address:port调用ngx_http_add_listening分配一个listen结构和ngx_http_port_t结构,并初始化它们。如果存在address包含通配符,则如果address:port需要bind,分配一个listen结构和ngx_http_port_t结构,并初始化它们,对所有address:port不需要bind的,它们和包含通配符*:port共同使用一个listen结构和ngx_http_port_t结构,并且listen结构中包含的地址是*:port,所以最好bind的地址是*:port。所有的listen都会存放在全局变量ngx_cycle的listening数组中,这样后面就可以利用这些address:port信息建立每个套接字了。
建立监听套接字

  建立监听套接字是在ngx_open_listening_sockets中完成,这个函数是在ngx_init_cycle中被调用的。
ngx_int_t
ngx_open_listening_sockets(ngx_cycle_t *cycle)
{
    ...
    reuseaddr = 1;
    ...
    log = cycle->log;
    //尝试5次
    for (tries = 5; tries; tries--) {
        failed = 0;
        ls = cycle->listening.elts;
        for (i = 0; i < cycle->listening.nelts; i++) {
            ...
            //创建socket
            s = ngx_socket(ls.sockaddr->sa_family, ls.type, 0);
            ...  //设置socket
            //绑定socket
            if (bind(s, ls.sockaddr, ls.socklen) == -1) {
                ...
            }
            ...
            //设置socket为监听套接字
            if (listen(s, ls.backlog) == -1) {
                ...
            }
            ls.listen = 1;
            ls.fd = s;
        }

        if (!failed) {
            break;
        }
    }
    ...
}
这个函数就是遍历listening数组,为每个listen结构创建监听套接字。到目前为止,所有的网络初始化部分就基本完成了,然后就是根据这些监听套接字来获取客户端的连接请求,并处理这些请求了。怎样获取客户端连接和nginx的进程模型和事件处理有关,进程模型和事件处理后面再贴文章分析。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-20727-1-1.html 上篇帖子: Nginx配置文件nginx.conf中文详解 下篇帖子: nginx tomcat集群配置实现无痛重启服务教程 网络
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表