这里有三个词很关键,我们来拆解一下,分别是是高性能、反向代理和web服务器;首先这个web服务器自不用多说,像我们熟知的Apache、IIS、Tomcat等都是web服务器;然后是高性能,一个服务器的性能自然是网站开发者最为关心的,那么服务器的性能如何来进行衡量呢?一般可以通过CPU和内存的使用量来进行衡量。经过笔者简单的并发测试,在20000个并发链接时,CPU和内存占用也非常低,CPU仅占5%,内存占用也才2MB不到。

  我们可以通过一个web压力测试工具Apache Bench,对Nginx进行简单的压力测试;通过在命令行ab -n 20000 -c 10000 [url],我们对Nginx的首页发起请求总数为20000,并发数为10000的请求测试,测试结果如下:

压力测试结果

  我们看到总的请求时间(Time taken for tests)是25秒,平均每个请求耗时(Time per request)1.25毫秒,在这么高的并发量下面,服务器响应性能还是挺不错的。

  然后是反向代理,与之对应的就是正向代理,这两者的区别也是面试中经常被问到的。我们先来看一下什么是正向代理,一个正向代理最典型的例子就是我们常用的“梯子”。

表情包

  我们直接访问Google,是访问不到的,但是如果我们使用了代理服务器,那么通过访问代理服务器就可以浏览Google,这里的代理服务器就属于正向代理;通过正向代理我们可以访问原来无法访问的资源。

正向代理

  那么什么是反向代理呢?反向代理最典型的例子就是我们的Nginx服务器了;比如我们在访问某个网站时,由代理服务器去目标服务器获取数据后返回给客户端,这样就能够隐藏真实服务器的IP地址,只对外开放代理服务器,以防止外网对内网服务器的恶性攻击。

反向代理

  理解了上面两个典型的案例,相信大家对正向反向代理也了解了,我们总结一下:

  • 正向代理,代理客户端,服务端不知道实际发起请求的客户端。
  • 反向代理,代理服务端,客户端不知道实际提供服务的服务端。

安装配置

  Nginx安装程序分为Linux版和Windows版,Windows版本的Nginx下载解压后就可以直接运行了,而Linux版本的需要make、configure等命令编译安装,好处是可以方便灵活的编译不同的模块到Nginx;网上也有很多的安装教程,这里就不再赘述了,可以从官网下载适合自己的版本,下载好后我们来看一下他的目录结构:

├── conf            #所有配置文件的目录
    ├── nginx.conf  #主配置文件
    ├── mime.types  #媒体类型控制文件
├── contrib         #存放一些实用工具
├── docs            #文档资料
├── html            #默认解析的静态文件目录
├── logs            #日志目录
├── sbin            #启动运行程序
复制代码

  我们经常用到的就是conf目录和html目录;而在根目录可以运行常用的一些命令对Nginx进行操作控制:

nginx -s reopen 	#重启Nginx
nginx -s reload 	#重新加载Nginx配置文件,然后以优雅的方式重启Nginx
nginx -s stop   	#强制停止Nginx服务
nginx -s quit   	#优雅地停止Nginx服务(即处理完所有请求后再停止服务)
nginx -h 		    #打开帮助信息
nginx -v 		    #显示版本信息并退出
nginx -V		    #显示版本和配置选项信息,然后退出
nginx -t		    #检测配置文件是否有语法错误,然后退出
nginx -T	 	    #检测配置文件是否有语法错误,转储并退出
nginx -q 	  	    #在检测配置文件期间屏蔽非错误信息
nginx -p prefix   	#设置前缀路径(默认是:/usr/share/nginx/)
nginx -c filename	#设置配置文件(默认是:/etc/nginx/nginx.conf)
nginx -g directives #设置配置文件外的全局指令
killall nginx		#杀死所有nginx进程
复制代码

  我们看前四个命令会发现,这四个命令可以分为两种,重启和停止Nginx,不过一种是强制的方式,另一种是优雅的方式;强制的方式就是让Nginx立即停止当前处理的所有请求,丢弃链接,停止工作;而优雅的方式是允许Nginx将当前正在处理的请求处理完成,但是不再接收新的请求,所有处理完成后再停止工作。

  我们再来看一下主要配置文件nginx.conf的基本结构:

# nginx进程数,建议设置为等于CPU总核心数
worker_processes  1;
# 进程文件
pid        logs/nginx.pid;
# 单个进程最大连接数
events {
    worker_connections  1024;
}
http {
    # 文件扩展名与类型映射表
    include       mime.types;
    # 默认文件类型
    default_type  application/octet-stream;
    # 开启gzip压缩
    gzip  on;
    sendfile        on;
    keepalive_timeout  65;
    server {
        # 监听端口
        listen       80;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
    }
}
复制代码

  配置文件中主要可以分为以下几个块:

  • 全局模块:从配置文件开始到events块之间的内容,此处的配置影响nginx服务器整体的运⾏,⽐如worker进程的数量、错误⽇志的位置等
  • events:配置影响nginx服务器或与用户的网络连接。
  • http:可以嵌套多个server,配置代理,缓存,日志定义等绝大多数功能和第三方模块的配置。
  • server:配置虚拟主机的相关参数,一个http中可以有多个server。
  • location:配置请求的路由,以及各种页面的处理情况。

  很多时候,我们不会将所有的配置全都写在一个主配置文件,因为这样会显得冗长,也不知道每个模块是做什么用的;而是会根据项目来拆分多个配置文件,每个配置文件彼此独立,互不干扰,然后在主配置文件中引入;我们在conf目录下新建一个projects目录,然后可以新建多个.conf配置文件:

# /conf/projects/home.conf
server {
    listen       8080;
    server_name  localhost;
    location / {
        root   html;
        index  index.html index.htm;
    }
}
server {
    listen       8081;
    server_name  localhost;
    location / {
        root   html;
        index  index.html index.htm;
    }
}
复制代码

  然后在主配置nginx.conf中将projects目录下的所有配置文件引入:

http {
    include       mime.types;
    default_type  application/octet-stream;
    gzip  on;
    sendfile        on;
    keepalive_timeout  65;
    ## 引入projects目录下所有的配置文件
    include       projects/*.conf;
}
复制代码

  这样我们可以直接在projects目录下新增.conf后缀的配置文件,而不用修改主配置文件;但是我们修改完还不能确定是否会有错误,可以通过命令对配置文件进行检测:

nginx -t
#nginx: the configuration file nginx/conf/nginx.conf syntax is ok
#nginx: configuration file nginx/conf/nginx.conf test is successful
复制代码

  通过检测发现没有任何报错,就可以优雅的重启服务器了:

nginx -s reload
复制代码

静态服务器

  作为一个web服务器,最重要的就是能够对静态资源提供访问服务,我们的Nginx服务器可以用来托管一些静态的资源,比如js、css、图片等,访问某一特定的静态资源路径时会转发到本地目录文件上;那么我们就来看Nginx是如何一步一步的通过域名配置、URI配置以及目录配置来命中请求的。

Nginx首页

server_name配置

  在上面的配置中,我们主要是将server_name设置为localhost,但是这样仅能让局域网内的主机访问到;我们想要让广域网上的其他主机访问,可以将server_name匹配域名,它的参数值可以是以下几种:

  • 精确的域名,如www.my.com
  • 通配符名称,但通配符只能用在由三段字符串组成的名称的首段或尾段,如*.my.com或者www.my.*
  • 正则表达式,使用波浪号~作为正则表达式字符串的开始标记,如~^www\d+\.my\.com$
  • ip地址

  在上面正则表达式中,^表示以www开头,紧跟一个或多个数字(\d+),然后跟上域名my.com,最后以$结尾;因此上面的表达式可以匹配的域名比如www1.my.com,但是www.my.com就不行。

  正则表达式还支持字符串捕获功能,即将正则表达式匹配成功的名称中的一部分字符串截取出来,放在变量中供后面使用;比如将server_name进行如下设置:

server {
  listen       80;
  server_name  ~^(.+)?\.my\.com$;
  location / {
    root   /usr/share/nginx/html/$1;
    index  index.html index.htm;
  }
}
复制代码

  这样,通过二级域名home.my.com到达Nginx时,被server_name正则表达式捕获,将其中的home字符串存入$1变量中,我们在/usr/share/nginx/html/home目录下的静态资源就能通过home.my.com域名来访问了;我们服务器的目录就可以是这样的:

/usr/share/nginx/html/
    |- home
        |- index.html
    |- blog
        |- index.html
    |- mail
        |- index.html
    |- photo
        |- index.html
复制代码

  这样就只需要一个server块来完成多个站点的配置。

  nginx允许一个虚拟主机有多个域名,因此我们可以给server_name同时配置多个域名,多个之间以空格分隔:

server {
  listen       80;
  server_name  a.com b.com c.com;
  # ...其他配置
}
复制代码

  由于server_name支持以上三种配置方式,如果出现多个server块同时匹配了相同的域名,那么这个请求交给哪个server呢?因此优先级顺序如下:

  1. 精确匹配server_name
  2. 通配符在开始时匹配server_name
  3. 通配符在结尾时匹配server_name
  4. 正则表达式匹配server_name

  如果我们想让局域网内的设备访问nginx,可以将server_name设置ip地址的方式:

server {
  listen       80;
  server_name  localhost 192.168.1.101;
}
复制代码

  如果还不能访问,可以查看下是否是防火墙的原因,在防火墙允许通过的应用中将Nginx勾选(没有找到Nginx可以点击允许其他应用进行新增):

Windows防火墙

  有时候我们还会见到将server_name设置为_(下划线),意味着server_name为空,即匹配全部的主机;我们可以配置host,将a.com、b.com和c.com都指向本机,然后配置nginx:

server {
  listen       80;
  server_name  _;
  location / {
    root   html;
    index  index.html index.htm;
  }
}
复制代码

  这样我们不仅可以通过域名a.com、b.com、c.com来访问,也能通过ip的方式。

location配置

  location用于匹配不同的URI请求,它的语法如下:

location [ = | ~ | ~* | ^~ ] uri { ... }
location @/name/ { … }
复制代码

  这里的uri就是待匹配的请求字符串,可以是不含正则的字符串,比如/home,称为标准URI;也可以是包含正则的字符串,比如\.html$(表示以.html结尾),称为正则URI。而方括号中的四种匹配符都是可选的,用来改变请求字符串与URI的匹配方式,我们来看下四种匹配符的解释:

匹配符 解释
不填 location后没有参数,直接跟着标准URI,表示前缀匹配,代表跟请求中的URI从头开始匹配
= 用于标准URI前,要求请求字符串与其精准匹配,成功则立即处理,nginx停止搜索其他匹配
^~ 用于标准URI前,要求一旦匹配就会立即处理,不再去匹配其他正则URI,一般用来匹配目录
~ 用于正则URI前,表示URI包含正则表达式,区分大小写
~* 用于正则URI前,表示URI包含正则表达式,不区分大小写
@ 定义一个命名的location,@定义的location名字一般用在内部定向

  我们来看下每种匹配规则能匹配的url,首先不填代表的话表示前缀匹配,如果我们有多个相似的前缀匹配:

location /pre/fix {
  # ...
}
location /pre {
  # ...
}
复制代码

  对于请求/pre/fix/home,根据最大匹配原则,匹配第一个location。

  然后是=,要求路径完全匹配:

location = /abc {
  # ...
}

# /abc    匹配
# /abcde  不匹配
# /abc/   不匹配,带有结尾的/
# /cde/abc不匹配
复制代码

  其次是^~最佳匹配,它的优先级高于正则表达式:

location ^~ /login {
  # ...
}

# /login      匹配
# /loginss    匹配
# /login/     匹配
# /home/login 不匹配
复制代码

  接着是~正则表达式匹配,它区分大小写匹配(注意:windows版本nginx不区分):

location ~ \.(gif|jpg|png|js|css)$ {
  # ...
}
# /bg.png     匹配
# /bg.PNG     不匹配 
# /bg.png?a=1 匹配
# /bg.jpeg    不匹配
复制代码

  ~*同样也是正则匹配,只不过它不区分大小写,这里就不再演示。

  如果我们的URI匹配到了多个location,其并不完全按照在配置文件中出现的顺序来进行匹配,URI会按照如下规则进行匹配:

  1. = 精确匹配会第一个被处理。如果发现精确匹配,nginx停止搜索其他匹配。
  2. 普通字符匹配,正则表达式规则和长的块规则将被优先和查询匹配,也就是说如果该项匹配还需去看有没有正则表达式匹配和更长的匹配。
  3. ^~ 则只匹配该规则,nginx停止搜索其他匹配,否则nginx会继续处理其他location指令。
  4. 最后匹配理带有~~*的指令,如果找到相应的匹配,则nginx停止搜索其他匹配;当没有正则表达式或者没有正则表达式被匹配的情况下,那么匹配程度最高的逐字匹配指令会被使用。

请求目录配置

  在location匹配URI后,就需要在服务器指定的目录中寻找请求资源,而rootalias就是用来指定目录的两种指令,两者主要的区别在于如何解析location后面的路径;我们首先来看下root的用法,假如我们需要将/data/下面的所有路径转发到html/roottest下面:

location /data/ {
  root html/roottest;
}
复制代码

  当location接收到/data/index.html的请求时,会在html/roottest/data/目录下找到index.html文件并进行相应,root会将root路径和location路径进行拼接。

  而alias指令则改变location接收到的请求路径,假如我们需要将/data1/下面的所有路径转发到html/aliastest下面:

location /data1/ {
  alias html/aliastes/;
}
复制代码

  当location接收到/data1/index.html的请求时,会在html/aliastes/目录下查找index.html文件并响应。

需要注意的是:alias指令后面的路径必须以/结束,否则会找不到文件,而root则可有可无。

访问权限控制

  针对一些静态资源,我们可能会设置一些用户访问权限,比如和js一起打包产出的.map文件,会对源码进行映射;但是我们想让它只能针对公司的ip进行开放,对外网的ip禁止访问,这时就需要用到allowdeny命令了。

  假如局域网还有两个设备,我们只能让这两个设备的ip通过访问:

location / {
  alias html/aliastes/;
  allow 192.168.1.102;
  allow 192.168.1.103;
  deny all;
}
复制代码

  deny和allow指令是由ngx_http_access_module模块提供,Windows版本的Nginx并不包含该模块。

  还可以对前端的.map文件进行访问权限控制,打包后的map文件一般会放在服务器上,但是如果能对所有人开放,别人就能查看到对应源码;因此我们可以控制只有公司的ip才有访问权限:

try_files

  前端在配置路由时经常会用到history路由模式,因此后台就需要映射对应的路由到index.html;但是如果我们给每个路由都配置一个location就会比较繁琐,因此可以通过try_files指令来进行尝试解析;try_files的语法规则如下:

# 格式1:
try_files file ... uri;  
# 格式2:
try_files file ... =code;
复制代码

  假设我们打包出来的单页面位于/html/my/index.html,我们想要将/login、/regisrer等路由指向index.html,我们可以配置try_files:

server {
    listen       8080;
    server_name  localhost;
    location / {
        try_files $uri /my/index.html;
    }
}
复制代码

  对于多页面的应用,假设我们的页面都放在/html/pages/目录下,我们想要访问/login时响应/html/pages/login.html页面,可以通过$uri

server {
    listen       8080;
    server_name  localhost;
    location / {
        index  index.html index.htm;
        root html/pages;
        try_files $uri /$uri.html $uri/index.html /index.html;
    }
}
复制代码

  这里我们设置root目录为html/pages,当我们访问/login路由时,这里的$uri就是/login,try_files会去尝试在根目录下找/login.html;如果找不到就尝试/login/index.html,最后找不到则会默认返回index.html。