Nginx是企业内网的对外入口,它常常同时对接许多应用,因此,Nginx上会同时监听多个端口、为多个域名提供服务。然而,匹配多级域名并不简单,Nginx为此准备了字符串精确匹配、前缀通配符、后缀通配符、正则表达式,当它们同时出现时,弄清楚HTTP请求会被哪个server{ }下的指令处理,就成了一件困难的事。
这是因为基于域名规范,请求匹配server{ }配置块时,并不会按照它们在nginx.conf文件中的出现顺序作为选择依据。而且对于不支持Host头部、没有域名的HTTP/1.0请求和无法匹配到合适server{ }的异常请求,我们都要区别对待。
另外,为了加快匹配速度,Nginx将字符串域名、前缀通配符、后缀通配符都放在了哈希表中,该设计充分使用了CPU的批量载入主存功能。如果不了解这些流程,既有可能导致请求没有被正确的server{ }块处理,也有可能降低了原本非常高效地哈希表查询性能。
本文将沿着Nginx处理HTTP请求的流程,介绍一个请求是如何根据listen、server_name等配置关联到server{ }块的。我们将从TCP连接的建立、Nginx从哪些字段取出域名、域名是怎样与server_name匹配的,讲清楚Nginx如何为请求找到处理它的server{ }块。在实际运维中,大部分问题都是由于请求匹配指令错误造成的,搞清楚这一匹配流程,对我们掌握Nginx非常重要。
listen指令对server{ }块的第1次关联
为了让一台服务器可以处理访问多个域名的不同请求,我们用“虚拟主机”来定义一种域名的处理方式,在Nginx中这对应着一个server{ }块。因此,HTTP请求到达时,Nginx首先要找到处理它的server{ }配置块。
请求关联server{ }块时主要依据listen和server_name这两个指令,其中listen指令发生在TCP连接建立完成时,它对server{ }块进行首次匹配,等到接收HTTP请求头部时,server_name再进行第二次匹配,这样就可以决定请求由哪个server{ }块中的指令处理。我们先来看listen指令是如何匹配请求的。
Nginx启动时创建socket并监听listen指令告知的端口(包括绑定IP地址)。当运行在TCP协议之上的HTTP请求到达服务器时,操作系统首先收到了TCP三次握手请求。我们知道,TCP这种传输层协议是由内核实现的,因此,由内核完成TCP的三次握手后,就会通过“读事件”经由Linux的Epoll通知到Nginx的worker进程以及具体监听的socket。
比如,我们在nginx.conf中配置了以下两个server:
server {
listen 192.168.1.5:80;
}
server {
listen 127.0.0.1:80;
}
如果是本机进程发来的HTTP请求(在Linux中可以用curl或者telnet发起请求),它的IP报文头部目的IP地址就是127.0.0.1,而TCP报文头部的目的端口就是80。这样,Linux内核就找到了相应的socket,进而通过epoll_wait函数唤醒Nginx进程,而Nginx也就找到了对应的listen指令以及其所属的server{ }块。
版本号 | 头部长度 | 服务类型 | 总长度 | |||||||
标识 | 标志位 | 分片偏移 | ||||||||
TTL生存时间 | 上层协议 | 首部校验和 | ||||||||
源IP地址 | ||||||||||
127.0.0.1 | ||||||||||
源端口号 | 80 | |||||||||
序列号 | ||||||||||
确认序列号 | ||||||||||
首部长度 | 保留位 | U | A | P | R | S | F | 窗口大小 | ||
校验和 | 紧急指针 | |||||||||
你可能注意到,有些server{ }块没有listen指令也可以正常的工作。这是因为Nginx认为每个server{}都应该监听TCP端口,当你没有显式的配置listen指令时,Nginx会默认帮你打开80端口。