一 简介
Nginx 模块在定义服务时,一般是通过配置server里面的listen端口来完成。根据不同的listen语法,可以实现下了几种功能。
1. 最简单的,可以通过不同ip和port,对应某一个服务。
server1 {
listen 1.2.3.4:2121;
}
上面的配置,可以让所有到1.2.3.4端口是2121的连接进行server1规定的服务。
2. 通过指定port 范围,实现多个port对应一个服务。
server2 {
listen 1.2.3.4:2121-2123;
}
上面的配置就可以实现所有到ip地址1.2.3.4端口从2121到2123的连接都指向同样的服务server2。
3. 如果Nginx所在的服务器有多个ip 地址,可以通过不同的ip和相同的port组合实现不同的服务。
server3 {
listen 1.2.3.4:2121;
}
server4{
listen 2.3.4.5:2121;
}
当客户端连接 1.2.3.4:2121 时,提供server3的服务。连接2.3.4.5:2121时,提供server4的服务。
4. 如果Nginx所在的服务器有多个ip 地址,通过配置监听所有ip地址上相同的某些port。
server5 {
listen 2121;
}
server6{
listen *:2121;
}
上述两种配置的功能是一样的,通过省略ip地址或者用*表示ip地址,可以达到只要连接到达任何一个ip地址的端口2121都可以进行server5或者server6的目的。
5. 如果Nginx所在的服务器有多个ip 地址,可以通过如下配置,针对同一个port,让特定ip的连接进行某个服务,剩余ip的连接进 行另外的服务。
server7 {
listen *:2121;
}
server8{
listen 1.2.3.4:2121;
}
连接到1.2.3.4:2121的连接进行server8的服务。连接别的ip地址的2121port进行server7的服务。
二 实现原理
数据结构
在进行代码分析之前,让我们先来看看与stream模块的listening port相关的数据结构是如何组织的。用户配置的server listen属性,最终需要生成ngx_listen_t结构。
图一 stream 模块监听端口相关数据结构
数据转换
从配置listen到生成ngx_listen_t的流程如下。其中需要经过两次数据结构的转换。
- 在配置解析阶段,server配置的listen port生成的ngx_stream_listen_t结构被存放到下图的右上角的红色线框中。也就是stream模块对应的ngx_stream_core_main_conf_t结构中的listen字段所对应的链表。
- 然后把链表ngx_stream_listen_t转换成ngx_stream_conf_port_t结构。通过这个转换可以把所有端口相同的ngx_stream_listen_t合并成一个ngx_stream_conf_port_t结构。并且把这些具有相同端口的ngx_stream_listen_t结构对应的地址存放到生成的那个ngx_stream_conf_port_t结构的地址数组中.
- 最后通过ngx_stream_optimize_servers函数,把ngx_stream_conf_port_t结构转化成ngx_listen_t结构。转换的原则是:如果没有通配监听(listen *:2121或者listen 2121),每一对唯一的host address和port生成一个ngx_listen_t。相反,如果有通配监听,所有相同的port (2121)对应的所有的地址,只生成一个ngx_listen_t结构,同时把所有的对应的port的地址存放到ngx_listen_t结构的addrs数组中。
配置举例
server {
listen *:2121;
proxy_timeout 65534;
proxy_pass v*nftp1;
alg ftp;
}
server {
listen 10.250.64.103:2121;
proxy_timeout 65534;
proxy_pass v*nftp;
alg ftp;
}
server {
listen 60.60.60.77:2121;
proxy_timeout 65534;
proxy_pass v*nftp;
alg ftp;
}
上述例子会在上述转换过程中,会生成3个ngx_stream_listen_t,1个 ngx_stream_conf_port_t,因为有通配符的存在只会1 个ngx_listen_t.
server {
listen 10.250.64.103:2121;
proxy_timeout 65534;
proxy_pass v*nftp;
alg ftp;
}
server {
listen 60.60.60.77:2121;
proxy_timeout 65534;
proxy_pass v*nftp;
alg ftp;
}
上述例子会在上述转换过程中,会生成2个ngx_stream_listen_t,1个 ngx_stream_conf_port_t,因为没有通配符所以生成2 个ngx_listen_t。
server {
listen 60.60.60.77:2122;
proxy_timeout 65534;
proxy_pass v*nftp;
alg ftp;
}
server {
listen 60.60.60.77:2121;
proxy_timeout 65534;
proxy_pass v*nftp;
alg ftp;
}
上述例子会在上述转换过程中,会生成2个ngx_stream_listen_t,2个 ngx_stream_conf_port_t, 2个ngx_listen_t.
server {
listen *:2121;
proxy_timeout 65534;
proxy_pass v*nftp;
alg ftp;
}
server {
listen 10.250.64.103:2121-2123;
proxy_timeout 65534;
proxy_pass v*nftp;
alg ftp;
}
server {
listen 60.60.60.77:2121;
proxy_timeout 65534;
proxy_pass v*nftp;
alg ftp;
}
上述例子会在上述转换过程中,会生成5个ngx_stream_listen_t,3个 ngx_stream_conf_port_t, 3个ngx_listen_t.
三 代码流程
配置平面
在配置解析过程中,对应于stream模块配置解析入口是ngx_stream_block.在此函数中,对于stream server中对应的每一个listen字段,相应的解析函数是ngx_stream_core_listen。
函数的大提流程是:
- 调用ngx_parse_url -> ngx_parse_inet_url 生成了ngx_url_t数据结构。在ngx_url_t这个结构中,会存放listen定义的所有的port和可能的地址。如果语法是 listen 2121. 则会生成一个port,和一个INADDR_ANY地址。如果语法是listen *:2121,也会生成一个port,和一个INADDR_ANY地址。如果语法是listen 1.2.3.4:2121,生成一个port,和一个1.2.3.4的地址。如果语法是listen 1.2.3.4:2121-2123 则生成3个port和一个1.2.3.4的地址。如果语法是listen hostname:2121-2123则会生成3个port并且根据对hostname的域名解析得到的地址个数生成相应的地址数量。最后通过ngx_inet_add_addr函数把总数为port_num * addr_num数量的addr存放到ngx_url_t结构中。
- 生成上述的结构ngx_url_t后,根据结构中地址的个数 (port_num * addr_num)生成相应数量的ngx_stream_listen_t添加到ngx_stream_core_main_conf_t结构中的listen成员中。同时解析比如reuse,sndbuf,rcvbuf等属性。
- 通过函数ngx_stream_add_ports把上述的listen成员中的ngx_stream_listen_t数据按照相同的port number生成一个ngx_stream_conf_port_t的原则把所有的ngx_stream_listen_t结构转换成ngx_stream_conf_port_t结构。对于原来ngx_stream_listen_t中port number相同但是地址不相同的情况把所有的地址都组合起来存放到同一个ngx_stream_conf_port_t结构中的addr数组中。
- 通过函数ngx_stream_optimize_servers把上述的ngx_stream_conf_port_t结构定义的port转变成ngx_listening_t结构。转换的规则是:对于有通配匹配的情况(list *:2121或者listen 2121)的端口,如果有几个server对应的port number是一样的,则会生成唯一的一个ngx_listen_t结构,然后把几个server对应的地址存放到ngx_listen_t的地址数组中。对于没有通配匹配的端口,则会根据唯一的地址和端口对生成一个唯一的ngx_listen_t结构。每一个结构中只包含一个addr_t结构。
数据平面
当连接初次建立时,(对于udp协议路径是ngx_event_recvmsg,对于tcp协议路径是ngx_event_accept),ngx_stream_init_connection函数得到调用。此时如果对应connection上的ngx_listen_t对应的addr有多个。(针对listen *情况),根据连接的Nginx侧的连接信息来确定使用的server是哪一个。通过这个server来连接upstream结构和server的context.
四 结语
为了支持监听端口的通配匹配,Nginx做了很多额外的工作进行了数据结构的两次转化。但是也没有想到更好的办法可以省略这两次转换。
另外,这篇文章更多是帮助自己理解代码而写的,希望对大家也有帮助。