微信公众号:郑尔多斯
关注可了解更多的Nginx
知识。任何问题或建议,请公众号留言;
关注公众号,有趣有内涵的文章第一时间送达!
前言
本篇文章详细介绍一下listen
指令的解析,以及socket
的创建过程。
listen
的内容太多了,并且牵涉到后面的很多地方,所以只能再一次的回到这里仔细的学习listen
指令的解析过程。首先要参考listen
的nginx
官方文档,知道listen的用法,然后才能学习源码。
listen配置
我们先从源文件中找到listen
的配置项,如下:
{
ngx_string("listen"),
NGX_HTTP_SRV_CONF|NGX_CONF_1MORE,
ngx_http_core_listen,
NGX_HTTP_SRV_CONF_OFFSET,
0,
NULL
}
我们从配置文件中可以看到,listen
指令的解析函数为ngx_http_core_listen
,我们下面分析一下这个函数。
使用到的结构体
ngx_url_ttypedef struct {
union {
struct sockaddr sockaddr;
struct sockaddr_in sockaddr_in;
#if (NGX_HAVE_INET6)
struct sockaddr_in6 sockaddr_in6;
#endif
#if (NGX_HAVE_UNIX_DOMAIN)
struct sockaddr_un sockaddr_un;
#endif
u_char sockaddr_data[NGX_SOCKADDRLEN];
} u;
socklen_t socklen;
unsigned set:1;
unsigned default_server:1;
unsigned bind:1;
unsigned wildcard:1;
#if (NGX_HTTP_SSL)
unsigned ssl:1;
#endif
#if (NGX_HAVE_INET6 && defined IPV6_V6ONLY)
unsigned ipv6only:2;
#endif
int backlog;
int rcvbuf;
int sndbuf;
#if (NGX_HAVE_SETFIB)
int setfib;
#endif
#if (NGX_HAVE_DEFERRED_ACCEPT && defined SO_ACCEPTFILTER)
char *accept_filter;
#endif
#if (NGX_HAVE_DEFERRED_ACCEPT && defined TCP_DEFER_ACCEPT)
ngx_uint_t deferred_accept;
#endif
u_char addr[NGX_SOCKADDR_STRLEN + 1];
} ngx_http_listen_opt_t;
上面的几个数据结构是我们分析listen
指令时常用到的,这里简单的说明一下他们的作用。
-
ngx_url_t
结构体是用来保存解析address:port
的内容。 -
ngx_http_listen_opt_t
结构体是用来保存listen
指令后面所配置的选项。为后面创建socket
做准备。
解析地址
我们阅读ngx_http_core_listen
的源码就会发现,解析listen
指令的第一步是调用ngx_parse_url()
来解析address:port
部分。这其实是ngx_http_core_listen
函数最重要的一部分,我们先来分析一下这个函数。
首先,我们必须要知道listen
指令配置的address:port
的格式,我们这里只分析ipv4
格式,我们从nginx
文档中可以查到:
Sets the address and port for IP, or the path for a UNIX-domain socket on which the server will accept requests. Both address and port, or only address or only port can be specified. An address may also be a hostname, for example:
listen 127.0.0.1:8000;
listen 127.0.0.1;
listen 8000;
listen *:8000;
listen localhost:8000;
If only address is given, the port 80 is used.
If the directive is not present then either *:80 is used if nginx runs with the superuser privileges, or *:8000 otherwise.
通过上面的文档我们可以知道,address:port
可以有一下几种格式:
listen 8080
listen *:8080
listen 127.0.0.1:8080
listen localhost:8080
listen 127.0.0.1;
如果我们没有指定port
,那么默认是80
端口。
如果我们在server
中没有配置listen
指令,那么会有两种情况:
- 当前启动
nginx
的是普通用户,那么默认为*:80
- 当前启动
nginx
的是root
用户,那么默认为*:8000
那么nginx
是如何解析address:port
的呢?这就是下面的ngx_parse_url()
函数的功能了。
u.url = value[1];
u.listen = 1;
u.default_port = 80;
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_parse_url()
函数之前,会先进行一些简单的赋值,
// address:port部分
u.url = value[1];
// 表示当前server显式的设置了listen指令
u.listen = 1;
// 设置一个默认的端口号
u.default_port = 80;
由于我们使用的是ipv4
地址,所以最终调用的是ngx_parse_inet_url()
,如下:
static ngx_int_t
ngx_parse_inet_url(ngx_pool_t *pool, ngx_url_t *u)
{
u_char *p, *host, *port, *last, *uri, *args;
size_t len;
ngx_int_t n;
struct hostent *h;
struct sockaddr_in *sin;
u->socklen = sizeof(struct sockaddr_in);
sin = (struct sockaddr_in *) &u->sockaddr;
sin->sin_family = AF_INET;
u->family = AF_INET;
host = u->url.data;
last = host + u->url.len;
port = ngx_strlchr(host, last, ':');// 判断是否有端口号
uri = ngx_strlchr(host, last, '/');// 判断是否有path
args = ngx_strlchr(host, last, '?');// 判断是否有QueryString
if (args) {
/*我们的listen指令后面的address:port没有args,不会执行这里*/
}
if (uri) {
/*我们的listen指令后面的address:port没有uri,不会执行这里*/
}
if (port) {//如果有端口号
port++;//port向后移动一位,表示从此位置开始是端口号
len = last - port;//端口号的长度
n = ngx_atoi(port, len);// 把端口号转换为数字
u->port = (in_port_t) n;
sin->sin_port = htons((in_port_t) n);
u->port_text.len = len;
u->port_text.data = port;
last = port - 1;// 此时last指向了address的最后
} else {
// 如果我们没有冒号,这时候有两种情况,
// ① 我们没有指定端口号,如 listen 127.0.0.1
// ② 我们指定了端口号,但是没有指定address,如 listen 8080
if (uri == NULL) {
// 我们在server中显式的使用了listen指令
if (u->listen) {
/* test value as port only */
// 这句话注释的很明显,nginx首先将它作为一个port
// 进行转换,如果成功,那么就认为这是一个port
n = ngx_atoi(host, last - host);
if (n != NGX_ERROR) {
//对于上面的第①种情况,由于无法将 127.0.0.1
// 转换为一个正确的端口号,
// 所以就不会执行下面的if语句,而是执行
// u->noport = 1 , 表示我们没有指定端口号
u->port = (in_port_t) n;
sin->sin_port = htons((in_port_t) n);
u->port_text.len = last - host;
u->port_text.data = host;
u->wildcard = 1;
return NGX_OK;
}
}
}
// 对于上述的第①种情况,会执行到这里,表示我们没有指定端口号
u->no_port = 1;
}
// 如果执行到这里,说明listen后面没有端口号,只有address
// len 表示address的长度,
// 比如 127.0.0.1 或者 localhost的长度
len = last - host;
if (len == 0) {
u->err = "no host";
return NGX_ERROR;
}
if (len == 1 && *host == '*') {
len = 0;
}
u->host.len = len;
u->host.data = host;
if (u->no_resolve) {
return NGX_OK;
}
if (len) {
// 对于 listen * 的情况,上面的代码会把len设置为0,所以不会执行这里
// 这里会首先尝试把address转换为ip形式,如果转换不成功,
// 那么就会调用gethostbyname()进行DNS地址解析
// 比如 127.0.0.1这种形式就可以通过 ngx_inet_addr()进行转换,
// 这时就不会调用gethostbyname()进行DNS解析
// 但是对于 localhost 这种情况,只能进行DNS地址解析
sin->sin_addr.s_addr = ngx_inet_addr(host, len);
if (sin->sin_addr.s_addr == INADDR_NONE) {
p = ngx_alloc(++len, pool->log);
(void) ngx_cpystrn(p, host, len);
h = gethostbyname((const char *) p);
ngx_free(p);
if (h == NULL || h->h_addr_list[0] == NULL) {
u->err = "host not found";
return NGX_ERROR;
}
sin->sin_addr.s_addr = *(in_addr_t *) (h->h_addr_list[0]);
}
if (sin->sin_addr.s_addr == INADDR_ANY) {
u->wildcard = 1;
}
} else {// address和port都忽略的时候
sin->sin_addr.s_addr = INADDR_ANY;
u->wildcard = 1;
}
if (u->no_port) {
// 如果没有指定端口号,那么会使用默认的80端口
// 从这里也可以看出来,ngx_url_t 的 default_port 字段就是用来保存默认端口的
// 如果我们没有指定一个明确的端口号,那么就会使用这个默认的端口,默认是 80
u->port = u->default_port;
sin->sin_port = htons(u->default_port);
}
if (u->listen) {
return NGX_OK;
}
//因为我们的先决条件是 u->listen = 1,所以下面的语句不会被执行
if (ngx_inet_resolve_host(pool, u) != NGX_OK) {
return NGX_ERROR;
}
return NGX_OK;
}
结合图片以及代码中的注释,我们简单的分析一下nginx对listen指令中 address:port
的解析方法:
address:port
的格式组合格式有好几种
address
: 可以没有该字段,可以为IP地址,可以为域名
port
: 可以不设置该字段,可以为一个固定的端口
所以组合形式就有 3 * 2 = 6
中。
nginx
对于他们的解析有固定的格式,如下:
如果address
没有设置,那么 u->wildcard = 1
,表示这时一个通配的地址匹配
如果address
设置为一个ip格式,那么监听的地址就是这个ip地址
如果address
是一个域名格式,那么就会对该域名进行DNS地址解析,获取监听的IP地址
如果端口号为空,那么就会使用默认的80端口。
address:port
解析完之后,我们可以从 listen
指令的处理函数 ngx_http_core_listen()
中看到,只有 u.sockaddr
, u.socklen
,以及 u.wildcard
三个字段被ngx_http_listen_opt_t
结构体用到了,ngx_url_t
的其他字段都是作为 ngx_parse_url()
函数的辅助字段使用。我们在后续的分析过程中,可以结合上面的图片进行学习
内部布局总结
根据上面的分析,我们对常见的几种address:port
格式的内存布局进行了总结,如下:
listen 8080
listen *:8080
listen 127.0.0.1:8080
listen localhost:8080
对应的图片如下:
图1 图2 图3 图4
喜欢本文的朋友们,欢迎长按下图关注订阅号郑尔多斯,更多精彩内容第一时间送达
郑尔多斯
网友评论