您的位置:首页 > 数据库 > Redis

Nginx+OpenResty+Lua+redis 从入门到精通

2020-12-13 22:30 2221 查看

整理笔记的时候,看见文件夹中出现了这样一个笔记,想了想决定分享了出来 从零开始学网关,学习nginx,学习openResty,学习lua,如果觉得有用的,请点个赞

1. nginx下载和安装

1、Nginx下载:nginx-1.13.0.tar.gz,下载到:/opt/softwares/

$ wget http://nginx.org/download/nginx-1.13.0.tar.gz

2、Nginx解压安装:

$ tar -zxvf nginx-1.13.0.tar.gz -C ./

3、预先安装

$ yum -y install gcc gcc-c++ ncurses-devel perl pcre pcre-devel zlib gzip zlib-devel

4、Nginx编译

$ ./configure --prefix=/usr/local/nginx

5、安装Nginx:

安装命令:make & make install

6、查看安装路径

$ cd /usr/local/nginx
$ ll
conf 存放配置文件
html 网页文件
logs 存放日志
sbin   shell启动、停止等脚本

7、启动nginx

$ cd sbin
$ ./nginx

8、浏览器,访问ip地址,默认80端口

9、停止nginx

$ ps -ef | grep nginx

执行命令:$ kill –INT 进程号
$ kill -INT 3844

$ ./nginx -s stop

10、重新读取配置文件

$ nginx -s reload

11、检查配置文件是否正确

$ ./nginx -t

问题报错:[error] invalid PID number "" in "/usr/local/nginx/logs/nginx.pid"
解决方案:
/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
-c的命令是指定配置文件位置

12.服务器上 把nginx加载配置环境

vim /etc/profile
PATH=$PATH:/usr/local/nginx/sbin
export PATH

13.nginx配置服务器启动就运行

/usr/lib/systemd/system 添加 nginx.service

[Unit]
Description=nginx - high performance web server
Documentation=http://nginx.org/en/docs/
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStartPre=/usr/local/nginx/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf
ExecStart=/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target

2.正向代理、反向代理描述

正向代理

用户要访问服务器C,但因为网络原因无法访问;但服务器A可以访问服务器C。这样用户可以把服务器A设置为正向代理服务器。由服务器A去请求服务器C,然后服务器A把数据返回会用户。

反向代理

用户需要访问一些服务器应用,但对方不想把服务器应用地址暴露给用户,这样可以确保安全。那用户如果访问呢?可以通过反向代理服务器,用户只需要知道反向代理服务器地址就可以,最后由反向代理服务器去访问服务器的应用

总结:正向代理与反向代理的区别

1)正向代理 是需要 在用户的电脑上 配置正向代理服务器的;而反向代理不需要,因为用户是直接访问的反向代理服务器
2)正向代理的应用场景是 用户是知道目标服务器的地址,如:www.google.com,但不能直接访问,那么就需要在用户电脑配置一个正向代理服务器,用户再次访问的地址www.google.com。
而反向代理的应用场景是 用户本来就不知道 目标服务器的地址;而是由平台方提供一个反向代理服务器的地址,用户直接访问反向代理服务器的地址就行 www.a.com
不管目标服务器有多少,用户不需要关心,只要访问反向代理服务器就ok;由反向代理服务器去解析访问目标服务器
3)反向代理 极大的保护了应用的安全性,而且此结构可以很好的搭建负载均衡

3.nginx命令,信号控制

一)nginx命令

1)nginx启动
指令:nginx程序   -c   nginx配置文件
/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf

2)nginx重启
cd /usr/local/nginx/sbin
重启
./nginx -s reload
进入nginx可执行程序的目录
cd /usr/local/nginx/sbin/
./nginx -s reload
nginx: [error] invalid PID number "" in "/usr/local/nginx/logs/nginx.pid"
重启是建立在nginx服务需要启动

3)nginx停止
./nginx -s stop
./nginx -s quit

quit 是一个优雅的关闭方式,Nginx在退出前完成已经接受的连接请求
stop 是快速关闭,不管有没有正在处理的请求。

4)重新打开日志
nginx -s reopen

5)nginx检查配置文件
第一种
进入nginx可执行程序的目录
nginx -t

第二种
/usr/local/nginx/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf

二)nginx的信号控制

Nginx支持2种进程模型 Single 和 Master-Worker
Single是单进程,一般不适用,
Master-Worker 是主进程和工作进程模型运行,主进程对工作进程管理。
Nginx允许我们通过信号来控制主进程,用信号的方式可以达到不影响现有连接的目的。

信号类型

INT,TERM		快速关闭信号
QUIT			从容关闭信号
HUP				从容重启信号,一般用于修改配置文件后,重启
USR1			重读日志,一般用于日志的切割
USR2			平滑升级信号
WINCH			从容关闭旧进程

具体语法:
kill  -信号选项  nginx的主进程号
例:
kill -INT 26661
kill -HUP 4873

1)nginx停止 ps -ef | grep nginx 获得进程号

第1种从容“优雅”停止
kill -QUIT master进程号
Nginx服务可以正常地处理完当前所有请求再停止服务

步骤:首先会关闭监听端口,停止接收新的连接,然后把当前正在处理的连接全部处理完,最后再退出进程。

第2种快速停止
kill -TERM master进程号
kill -INT master进程号
快速停止服务时,worker进程与master进程在收到信号后会立刻跳出循环,退出进程。

第3种强制停止
pkill -9 nginx
系统强杀nginx进程

2)重启nginx kill -HUP master进程号

4.nginx平滑升级

把服务器从低版本升级为高版本,强行停止服务器,会影响正在运行的进程。
平滑升级不会停掉正在进行中的进程,这些进程会继续处理请求。但不会再接受新请求,这些老的进程在处理完请求之后 会停止。
此平滑升级过程中,新开的进程会被处理。

一)平滑升级

进入nginx可执行程序的目录
cd /usr/local/nginx/sbin/
./nginx -V  #查看nginx版本

1)下载高版本nginx http://nginx.org/download/	nginx-1.13.1.tar.gz
执行指令
./configure
make    #不能执行 make install
cd objs #此目录下 有高版本的nginx
备份低版本的nginx
cp nginx nginx.old
执行强制覆盖
cp -rfp objs/nginx /usr/local/nginx/sbin

测试一下新复制过来文件生效情况:
/usr/local/nginx/sbin/nginx -t
ps -ef | grep nginx

2)执行信号平滑升级

kill -USR2 `cat /usr/local/nginx/logs/nginx.pid`  更新配置文件
给nginx发送USR2信号后,nginx会将logs/nginx.pid文件重命名为nginx.pid.oldbin,然后用新的可执行文件启动一个新的nginx主进程和对应的工作进程,并新建一个新的nginx.pid保存新的主进程号

ps -ef | grep nginx
ll logs/

3)kill -WINCH 旧的主进程号

旧的主进程号收到WINCH信号后,将旧进程号管理的旧的工作进程优雅的关闭。即一段时间后旧的工作进程全部关闭,只有新的工作进程在处理请求连接。这时,依然可以恢复到旧的进程服务,因为旧的进程的监听socket还未停止。

处理完后,工作进程会自动关闭
ps -ef | grep nginx

4)优雅的关闭

kill -QUIT `cat /usr/local/nginx/logs/nginx.pid.oldbin`
给旧的主进程发送QUIT信号后,旧的主进程退出,并移除logs/nginx.pid.oldbin文件,nginx的升级完成。

查看
./nginx -V
已经平滑升级成功

二)中途停止升级,回滚到旧的nginx

在步骤(3)时,如果想回到旧的nginx不再升级

(1)给旧的主进程号发送HUP命令,此时nginx不重新读取配置文件的情况下重新启动旧主进程的工作进程。
kill -HUP 9944 --旧主进程号
重启工作进程

(2)优雅的关闭新的主进程
kill -QUIT 10012  --新主进程号

5.nginx配置文件说明

以哪个用户,运行nginx应用
nobody是个低权限用户,为了安全
user nobody

nginx进程数 启动进程,通常设置成 cpu的核数
查看cpu核数
cat /proc/cpuinfo
worker_processes  1;

全局错误日志
nginx的error_log类型如下(从左到右:debug最详细 crit最少):
[ debug | info | notice | warn | error | crit ]
例如:error_log logs/nginx_error.log  crit;
解释:日志文件存储在nginx安装目录下的 logs/nginx_error.log ,错误类型为 crit ,也就是记录最少错误信息;
error_log  logs/error.log;
error_log  logs/notice.log  notice;
error_log  logs/info.log  info;

PID文件,记录当前启动的nginx的进程ID
pid        logs/nginx.pid;

worker_rlimit_nofile 这个参数表示worker进程最多能打开的文件句柄数,基于liunx系统ulimit设置
查看系统文件句柄数最大值:ulimit -n
Linux一切皆文件,所有请求过来最终目的访问文件,所以该参数值设置等同于liunx系统ulimit设置为优
可以通过linux命令设置  最大的文件句柄数65535

worker_rlimit_nofile 65535;

工作模式及连接数上限

events {
#网络模型高效(相当于建立索引查找结果),nginx配置应该启用该参数
#但是仅用于linux2.6以上内核,可以大大提高nginx的性能
use   epoll;
#该参数表示设置一个worker进程最多开启多少线程数
#优化设置应该等同于worker_rlimit_nofile设置值,表明一个线程处理一个http请求,同时可以处理一个文件数,各个模块之间协调合作不等待。
worker_connections  65535;
}

设定http服务器,利用它的反向代理功能提供负载均衡支持

http {
#设定mime类型,类型由mime.type文件定义
#MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型。
#是设定某种扩展名的文件用一种应用程序来#打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开
include       /etc/nginx/mime.types;
default_type  application/octet-stream;

#设定日志格式
log_format  main  '[$remote_addr] - [$remote_user] [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log    /var/log/nginx/access.log;

#sendfile 开启高效文件传输模式,sendfile指令指定nginx是否调用sendfile函数来输出文件,对于普通应用设为 on,如果用来进行下载等应用磁盘IO重负载应用,可设置为off,以平衡磁盘与网络I/O处理速度,降低系统的负载。
#注意:如果图片显示不正常把这个改成off。
sendfile        on;
tcp_nopush     on; #防止网络阻塞
tcp_nodelay        on; #防止网络阻塞

#连接超时时间
#keepalive_timeout  0;
keepalive_timeout  65; #长连接超时时间,单位是秒

#开启gzip压缩
gzip  on;
gzip_disable "MSIE [1-6]\."; # IE6及以下禁止压缩
gzip_min_length 1k; #最小压缩文件大小
gzip_buffers 4 16k; #压缩缓冲区
gzip_http_version 1.0; #压缩版本(默认1.1,前端如果是squid2.5请使用1.0)
gzip_comp_level 2; #压缩等级
gzip_types text/plain application/x-javascript text/css application/xml; #压缩类型
gzip_vary on; #给CDN和代理服务器使用,针对相同url,可以根据头信息返回压缩和非压缩副本

#设定请求缓冲
client_header_buffer_size    1k;   #上传文件大小限制
large_client_header_buffers  4 4k;  #设定请求缓存

#设定负载均衡的服务器列表
upstream mysvr {
#weigth参数表示权值,权值越高被分配到的几率越大
server 192.168.8.1x:3128 weight=5;
server 192.168.8.2x:80  weight=1;
server 192.168.8.3x:80  weight=6;
}

upstream mysvr2 {
#weigth参数表示权值,权值越高被分配到的几率越大
server 192.168.8.x:80  weight=1;
server 192.168.8.x:80  weight=6;
}

#虚拟主机的配置
server {
#侦听80端口
listen       80;
#设置编码
#charset koi8-r;

#定义使用www.xx.com访问 域名可以有多个,用空格隔开
server_name  www.xx.com;

#设定本虚拟主机的访问日志
access_log  logs/www.xx.com.access.log  main;

#默认请求
location / {
root   /root;      #定义服务器的默认网站根目录位置
index index.php index.html index.htm;   #定义首页索引文件的名称

proxy_pass  http://mysvr ;#请求转向mysvr 定义的服务器列表

client_max_body_size 10m;    #允许客户端请求的最大单文件字节数
client_body_buffer_size 128k;  #缓冲区代理缓冲用户端请求的最大字节数,

#以下是一些反向代理的配置可删除.

proxy_redirect off;

#后端的Web服务器可以通过X-Forwarded-For获取用户真实IP
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 90;  #nginx跟后端服务器连接超时时间(代理连接超时)
proxy_send_timeout 90;     #后端服务器数据回传时间(代理发送超时)
proxy_read_timeout 90;     #连接成功后,后端服务器响应时间(代理接收超时)
proxy_buffer_size 4k;      #设置代理服务器(nginx)保存用户头信息的缓冲区大小
proxy_buffers 4 32k;       #proxy_buffers缓冲区,网页平均在32k以下的话,这样设置
#高负荷下缓冲大小(proxy_buffers*2)
proxy_busy_buffers_size 64k;
#设定缓存文件夹大小,大于这个值,将从upstream服务器传
proxy_temp_file_write_size 64k;

}

# 定义错误提示页面
error_page   500 502 503 504 /50x.html;
location = /50x.html {
root   /root;
}

#本地动静分离反向代理配置
#所有jsp的页面均交由tomcat或resin处理
location ~ .(jsp|jspx|do)?$ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:8080;
}

#静态文件,nginx自己处理
location ~ ^/(images|javascript|js|css|flash|media|static)/ {
root /var/www/virtual/htdocs;
#过期30天,静态文件不怎么更新,过期可以设大一点,如果频繁更新,则可以设置得小一点。
expires 30d;
}

#设定查看Nginx状态的地址
location /NginxStatus {
stub_status            on;
access_log              on;
auth_basic              "NginxStatus";
auth_basic_user_file  conf/htpasswd;
#htpasswd文件的内容可以用apache提供的htpasswd工具来产生。
}
#禁止访问 .htxxx 文件
location ~ /\.ht {
deny all;
}

}
}

Nginx基本配置 Nginx的主配置文件是:nginx.conf,nginx.conf主要组成如下:

全局区   有一个工作子进程,一般设置为CPU数 * 核数
worker_processes  1;
events {
# 一般是配置nginx进程与连接的特性
# 如1个word能同时允许多少连接,一个子进程最大允许连接1024个连接
worker_connections  1024;
}

配置HTTP服务器配置段 http { # 配置虚拟主机段 server { # 定位,把特殊的路径或文件再次定位。 location {

}
}
server {

}
}

------------------配置完整文件---------------------

#user  nobody; 基于什么样的用户权限执行 nobody是个低权限用户,为了安全
worker_processes  1; #进程数 启动进程,通常设置成 cpu的核数 查看cpu核数 cat /proc/cpuinfo

#error_log  logs/error.log;  			#错误日记输出
#error_log  logs/error.log  notice;	#日记等级
#error_log  logs/error.log  info;   	#日记等级

#pid        logs/nginx.pid;	          #进程id目录

# worker_rlimit_nofile
#这个参数表示worker进程最多能打开的文件句柄数,基于liunx系统ulimit设置
#查看系统的最大值,命令  ulimit -n
#Linux一切皆文件,所有请求过来最终目的访问文件,所以该参数值设置等同于liunx系统ulimit设置为优
#可以通过linux命令设置  最大的文件句柄数65535

events {
#网络模型高效(相当于建立索引查找结果),nginx配置应该启用该参数
#但是仅用于linux2.6以上内核,可以大大提高nginx的性能
use   epoll;
#该参数表示设置一个worker进程最多开启多少线程数
#优化设置应该等同于worker_rlimit_nofile设置值,表明一个线程处理一个http请求,同时可以处理一个文件数,各个模块之间协调合作不等待。
worker_connections  1024;
}

http {

#设定mime类型,类型由mime.type文件定义
#MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型。
#是设定某种扩展名的文件用一种应用程序来#打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开
include       mime.types;
default_type  application/octet-stream;

#设置日记格式
#log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
#                  '$status $body_bytes_sent "$http_referer" '
#                  '"$http_user_agent" "$http_x_forwarded_for"';

#access_log  logs/access.log  main;

#sendfile 开启高效文件传输模式,sendfile指令指定nginx是否调用sendfile函数来输出文件,对于普通应用设为 on,
#如果用来进行下载等应用磁盘IO重负载应用,可设置为off,以平衡磁盘与网络I/O处理速度,降低系统的负载。
#注意:如果图片显示不正常把这个改成off。
sendfile        on;

#防止网络阻塞
#tcp_nopush     on;
#防止网络阻塞
#tcp_nodelay    on;

#连接超时时间
#keepalive_timeout  0;
keepalive_timeout  65;

#开启gzip压缩
#gzip  on;
#gzip_disable "MSIE [1-6]\."; # IE6及以下禁止压缩
#gzip_min_length 1k;          #最小压缩文件大小
#gzip_buffers 4 16k;          #压缩缓冲区
#gzip_http_version 1.0;       #压缩版本(默认1.1,前端如果是squid2.5请使用1.0)
#gzip_comp_level 2;           #压缩等级
#gzip_types text/plain application/x-javascript text/css application/xml; #压缩类型
#gzip_vary on;                #给CDN和代理服务器使用,针对相同url,可以根据头信息返回压缩和非压缩副本

#设定请求缓冲
#client_header_buffer_size    1k;   #上传文件大小限制
#large_client_header_buffers  4 4k;  #设定请求缓存

#设定负载均衡的服务器列表
#upstream mysvr {
#weigth参数表示权值,权值越高被分配到的几率越大
#server 127.0.0.1:3128   weight=5;
#server 127.0.0.1.2x:80  weight=1;
#server 127.0.0.1.3x:80  weight=6;
#}

#upstream mysvr2 {
#weigth参数表示权值,权值越高被分配到的几率越大
#server 192.168.8.2:80  weight=1;
#server 192.168.8.3:80  weight=6;
#}

#虚拟主机的配置
server {
#侦听80端口
listen       80;

#设置编码
#charset koi8-r;

#定义使用www.xx.com访问 域名可以有多个,用空格隔开
server_name  localhost;

#设定本虚拟主机的访问日志
#access_log  logs/host.access.log  main;

#默认请求
location / {
root   html;          #定义服务器的默认网站根目录位置
index  index.html index.htm;  #定义首页索引文件的名称

#proxy_pass  http://mysvr ; #请求转向mysvr 定义的服务器列表

# client_max_body_size 10m;    #允许客户端请求的最大单文件字节数
# client_body_buffer_size 128k;  #缓冲区代理缓冲用户端请求的最大字节数,

#以下是一些反向代理的配置可删除.

# proxy_redirect off;

#后端的Web服务器可以通过X-Forwarded-For获取用户真实IP
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_connect_timeout 90;  #nginx跟后端服务器连接超时时间(代理连接超时)
# proxy_send_timeout 90;     #后端服务器数据回传时间(代理发送超时)
# proxy_read_timeout 90;     #连接成功后,后端服务器响应时间(代理接收超时)
# proxy_buffer_size 4k;      #设置代理服务器(nginx)保存用户头信息的缓冲区大小
# proxy_buffers 4 32k;       #proxy_buffers缓冲区,网页平均在32k以下的话,这样设置

#高负荷下缓冲大小(proxy_buffers*2)
# proxy_busy_buffers_size 64k;

#设定缓存文件夹大小,大于这个值,将从upstream服务器传
# proxy_temp_file_write_size 64k;
}

#error_page  404              /404.html;

# redirect server error pages to the static page /50x.html
#
# 定义错误提示页面
error_page   500 502 503 504  /50x.html;
location = /50x.html {
root   html;
}

#本地动静分离反向代理配置
#所有jsp的页面均交由tomcat或resin处理
#location ~ .(jsp|jspx|do)?$ {
#	proxy_set_header Host $host;
#	proxy_set_header X-Real-IP $remote_addr;
#	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#	proxy_pass http://127.0.0.1:8080;
#}

#静态文件,nginx自己处理
#location ~ ^/(images|javascript|js|css|flash|media|static)/ {
#	root /var/www/virtual/htdocs;
#	#过期30天,静态文件不怎么更新,过期可以设大一点,如果频繁更新,则可以设置得小一点。
#	expires 30d;
#}

#设定查看Nginx状态的地址
#location /NginxStatus {
#	stub_status             on;
#	access_log              on;
#	auth_basic              "NginxStatus";
#	auth_basic_user_file    conf/htpasswd;
#	#htpasswd文件的内容可以用apache提供的htpasswd工具来产生。
#}

#禁止访问 .htxxx 文件
#location ~ /\.ht {
#	deny all;
#}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
#    proxy_pass   http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
#    root           html;
#    fastcgi_pass   127.0.0.1:9000;
#    fastcgi_index  index.php;
#    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
#    include        fastcgi_params;
#}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
#    deny  all;
#}
}

# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
#    listen       8000;
#    listen       somename:8080;
#    server_name  somename  alias  another.alias;

#    location / {
#        root   html;
#        index  index.html index.htm;
#    }
#}

# HTTPS server
#
#server {
#    listen       443 ssl;
#    server_name  localhost;

#    ssl_certificate      cert.pem;
#    ssl_certificate_key  cert.key;

#    ssl_session_cache    shared:SSL:1m;
#    ssl_session_timeout  5m;

#    ssl_ciphers  HIGH:!aNULL:!MD5;
#    ssl_prefer_server_ciphers  on;

#    location / {
#        root   html;
#        index  index.html index.htm;
#    }
#}

}

6.nginx配置连接数

worker_processes

表示开启nginx的worker进程的个数,nginx启动会开两种进程,master进程用来管理调度,worker进程用来处理请求;

上面表示两种设置方法,比如

方法一:worker_processes auto;
  表示设置服务器cpu核数匹配开启nginx开启的worker进程数
  查看cpu核数:cat /proc/cpuinfo

方法二:nginx设置cpu亲和力

  worker_processes 8;

  worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000;

  00000001表示启用第一个CPU内核,00000010表示启用第二个CPU内核,以此类推

worker_cpu_affinity

表示开启八个进程,第一个进程对应着第一个CPU内核,第二个进程对应着第二个CPU内核,以此类推。
这种设置方法更高效,因将每个cpu核提供给固定的worker进程服务,减少cpu上下文切换带来的资源浪费

如果服务器cpu有限,比如

2核CPU,开启2个进程,设置如下
worker_processes     2;
worker_cpu_affinity 01 10;

比如:4核CPU,开启4个进程,设置如下
worker_processes     4;
worker_cpu_affinity 0001 0010 0100 1000;

8核cpu ,worker_processes=8

1个worker进程 能够最大打开的文件数(线程数)

worker_connections=65535
(参考worker_rlimit_nofile  ---->  linux  ulimit -n)

最大的客户端连接数 max_clients = (多少个工作进程数)worker_processes * (1个工作线程的处理线程数)worker_connections 8*65535

nginx作为http服务器

请求模型   client <---> nginx
max_clients = worker_processes * worker_connections/2

nginx作为反向代理服务器的时候

请求模型   client <---> nginx  <----> web server
max_clients = worker_processes * worker_connections/4

为什么除以2:该公式基于http 1.1协议,一次请求大多数浏览器发送两次连接,并不是request和response响应占用两个线程(很多人也是这么认为,实际情况:请求是双向的,连接是没有方向的,由上面的图可以看出来)
为什么除以4:因nginx作为方向代理,客户端和nginx建立连接,nginx和后端服务器也要建立连接

由此,我们可以计算nginx作为http服务器最大并发量(作为反向代理服务器自己类推),可以为压测和线上环境的优化提供一些理论依据:

单位时间(keepalive_timeout)内nginx最大并发量C

C=worker_processes * worker_connections/2=8*65535/2
而每秒的并发量CS
CS=worker_processes * worker_connections/(2*65)   65:keepalive_timeout(长连接超时时间)

7.nginx虚拟主机

1)虚拟主机

虚拟主机使用的是特殊的软硬件技术,它把一台运行在因特网上的服务器主机分成一台台“虚拟”的主机,每台虚拟主机都可以是一个独立的网站,
可以具有独立的域名,具有完整的Intemet服务器功能(WWW、FTP、Email等),同一台主机上的虚拟主机之间是完全独立的。
从网站访问者来看,每一台虚拟主机和一台独立的主机完全一样。

利用虚拟主机,不用为每个要运行的网站提供一台单独的Nginx服务器或单独运行一组Nginx进程。
虚拟主机提供了在同一台服务器、同一组Nginx进程上运行多个网站的功能。

2)配置虚拟主机

我们先配置在一个nginx中配置一个虚拟主机,编辑nginx.conf配置文件,在http模块中,配置server模块,一个server模块就针对一个虚拟主机。
我们模拟一个独立的网站,此网站域名访问为www.server1.com;网站的根目录放到nginx目录下html/server1目录,我们创建一个首页index.html到server1中,编辑index.html
<!DOCTYPE html>
<html>
<head>
<title>server1 首页</title>
</head>
<body>
<h1>server1 首页</h1>
</body>
</html>

下面我们回到nginx.conf配置文件中,配置server模块

server {
listen 80; #监听80端口
server_name www.server1.com; #虚拟主机名,可以为域名或ip地址
location / { #默认请求路由,以后文章中会重点介绍
root html/server1; #网站的根目录
index index.html index.htm; #默认首页文件名
}
}
配置完成之后,重启nginx
因为www.server1.com是模拟的,需要在访问的客户端配置一下域名映射
打开浏览器,访问www.server1.com

3)第一个虚拟主机配置完成,我们再配置一个server2,与server1配置类似,先给server2网站创建一个根目录,nginx目录下html/server2目录,编辑index.html

<!DOCTYPE html>
<html>
<head>
<title>server2</title>
</head>
<body>
<h1>server2 首页</h1>
</body>
</html>

再编辑nginx.conf,再增加个server模块,监听还是80端口,但服务名改为www.server2.com 服务器上如果有域名,可以绑定域名后操作

server {
listen 80; #监听80端口
server_name www.server2.com; #虚拟主机名,可以为域名或ip地址
location / { #默认请求路由,以后文章中会重点介绍
root html/server2; #网站的根目录
index index.html index.htm; #默认首页文件名
}
}

重启nginx,不要忘了把hosts再增加个域名映射 打开浏览器访问www.server2.com,运行结果 监听的端口 listen 和 server_name 组合起来是唯一的

nginx -t nginx -s reload

8.nginx的日记与及切割

1)日志文件格式配置

nginx服务器在运行的时候,会有各种操作,操作的信息会记录到日志文件中,日志文件的记录是有格式的。那我们如何设置日志文件的格式呢?
使用log_format指令进行配置文件格式
nginx的log_format有很多可选的参数用于指示服务器的活动状态,默认的是
log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '    '$status $body_bytes_sent "$http_referer" '    '"$http_user_agent" "$http_x_forwarded_for"';

192.168.31.247 - - [11/Mar/2018:16:26:43 +0800] "GET / HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3350.0 Safari/537.36" "-"

示例

参数                      说明
$remote_addr             客户端地址                                    211.28.65.253
$remote_user             客户端用户名称                                --
$time_local              访问时间和时区                                18/Jul/2012:17:00:01 +0800
$request                 请求的URI和HTTP协议                           "GET /article-10000.html HTTP/1.1"
$http_host               请求地址,即浏览器中你输入的地址(IP或域名)     www.wang.com 192.168.100.100
$status                  HTTP请求状态                                  200
$upstream_status         upstream状态                                  200
$body_bytes_sent         发送给客户端文件内容大小                        1547
$http_referer            url跳转来源                                   https://www.baidu.com/
$http_user_agent         用户终端浏览器等信息                           "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; SV1; GTB7.0; .NET4.0C;
$ssl_protocol            SSL协议版本                                   TLSv1
$ssl_cipher              交换数据中的算法                               RC4-SHA
$upstream_addr           后台upstream的地址,即真正提供服务的主机地址     10.10.10.100:80
$request_time            整个请求的总时间                               0.205
$upstream_response_time  请求过程中,upstream响应时间                    0.002
$http_x_forwarded_for    是反向代理服务器转发客户端地址的参数

假设将Nginx服务器作为Web服务器,位于负载均衡设备、Squid、Nginx反向代理之后,不能获取到客户端的真实IP地址了。
原因是经过反向代理后,由于在客户端和Web服务器之间增加了中间层,因此Web服务器无法直接拿到客户端的IP。
通过$remote_addr变量拿到的将是反向代理服务器的IP地址。
但是,反向代理服务器在转发请求的HTTP头信息中,可以增加X-Forwarded-For信息,用以记录原有的客户端IP地址和原来客户端请求的服务器地址。
这时候,要用log_format指令设置日志格式,让日志记录X-Forearded-For信息中的IP地址,即客户的真实IP。
日志文件路径配置

2)access_log指令

语法
access_log path [format [buffer=size [flush=time]]];
access_log path format gzip[=level] [buffer=size] [flush=time];
access_log off;

默认值
access_log logs/access.log combined;

配置段
gzip压缩等级。
buffer设置内存缓存区大小。
flush保存在缓存区中的最长时间。
不记录日志:access_log off;
使用默认combined格式记录日志:access_log logs/access.log 或 access_log logs/access.log combined;
值得注意的是,Nginx进程设置的用户和组必须对日志路径有创建文件的权限,否则,会报错。
此外,对于每一条日志记录,都将是先打开文件,再写入日志,然后关闭。可以使用open_log_file_cache来设置日志文件缓存(默认是off)。

3)日志文件手动切割 server1.log
改为----> server1-2018-03-11.log
改为---> server1-2018-03-12.log ...

通过mv命令 把当前log文件重命令
再用信号控制指令 发送重读日志指令  产生了新的日志log文件

nginx日志默认情况下统统写入到一个文件中,文件会变的越来越大,非常不方便查看分析。
以日期来作为日志的切割是比较好的,通常我们是以每日来做统计的。下面来说说nginx日志切割。

我们先手动完成日志文件切割
到logs目录中,先备份日志文件,在重新生成日志文件
mv access.log access_20180124.log

kill -USR1 pid进程号
#向 Nginx 主进程发送 USR1 信号。USR1 信号是重新打开日志文件
#如果 不发送USR1 指令 会导致原本地址对象指向已经改了名字的文件,存储的文件仍然是同一个,只是名字改了

4)系统自动切割

利用sh脚本的方式执行刚才的手动操作,在每天凌晨执行一个计划任务 调用sh脚本,就完成的系统自动切割日志文件

编写脚本,在nginx目录下logs目录,touch cutlog.sh脚本

#!/bin/bash
LOGS_PATH=/usr/local/nginx/logs
YESTERDAY=$(date -d "yesterday" +%Y-%m-%d)
mv ${LOGS_PATH}/access.log ${LOGS_PATH}/access_${YESTERDAY}.log
kill -USR1 $(cat /usr/local/nginx/logs/nginx.pid)   # 向 Nginx 主进程发送 USR1 信号。USR1 信号是重新打开日志文件

注意:执行 sed -i 's/\r$//' cutlog.sh

原因
这个文件在Windows下编辑过,在Windows下每一行结尾是\n\r,而Linux下则是\n

sed -i 's/\r$//' cutlog.sh   替换命令
这个命令 会把cutlog.sh 中的行尾的\r替换为空白

设置定时任务

vi  /etc/crontab
0 0 * * * root /usr/local/nginx/logs/cutlog.sh
表示配置一个定时任务,定时每天00:00以root身份执行脚本/usr/local/nginx/logs/cutlog.sh,实现定时自动分割Nginx日志

手动指定测试
sh cutlog.sh

9.location详解

nginx的location配置详解

1)语法规则

location [=|~|~*|^~] /uri/ { … }
指令                  前缀              uri
location          [=|~|~*|^~]          /uri

路由匹配规则,正则匹配,正则表达式

2)location区分普通匹配和正则匹配

用前缀 “~” 和 “~*”修饰的为正则匹配
~   前缀表示区分大小写的正则匹配
~*  前缀表示不区分大小写的正则匹配

除上面修饰的前缀(“=” 和 “^~”,或没有前缀修饰)都为普通匹配
=   前缀表示精确匹配
^~  前缀表示uri以某个常规字符串开头,可以理解为url的普通匹配

location作用于server模块,且支持多个location模块

server {
.........
location /p {
root   html/p;
index  index.html index.htm;
}
location = /50x.html {
root   html;
}
location / {
root   html/server1;
index  index.html index.htm;
}
}
在多个location情况下,是按照什么原则进行匹配的呢?

3)匹配的原则

普通匹配:优先原则---->最大前缀匹配原则; 顺序无关
server {
location /prefix/ {
#规则A
}
location /prefix/mid/ {
#规则B
}
}

请求url为:/prefix/mid/t.html
此请求匹配的是 规则B,是以最大的匹配原则进行的,跟顺序无关

正则匹配:为顺序匹配,优先原则:谁在前面 就匹配谁;顺序相关
server {
location ~ \.(gif|jpg|png|js|css)$ {
#规则C
}
location ~* \.png$ {
#规则D
}
}
请求http://localhost/1.png,匹配的是规则C,因为规则C在前面,即叫做顺序匹配

如果location有普通匹配也有正则匹配,那匹配的原则为

匹配模式及顺序

带前缀普通匹配 最优先,=前缀优先级最高

location = /uri    
=开头表示精确匹配,只有完全匹配上才能生效。
location ^~ /uri   
^~ 开头对URL路径进行前缀匹配,并且在正则之前。

正则匹配
location ~ pattern  ~开头表示区分大小写的正则匹配。
location ~* pattern  ~*开头表示不区分大小写的正则匹配。

不带前缀匹配
location /uri     
不带任何修饰符,也表示前缀匹配,但是在正则匹配之后。

location /      
通用匹配,任何未匹配到其它location的请求都会匹配到,相当于switch中default。

匹配顺序

首先匹配 =,其次匹配^~, 其次是按文件中顺序的正则匹配,不带前缀普通匹配,最后是交给 / 通用匹配。
当有匹配成功时候,停止匹配,按当前匹配规则处理请求。

location = / {
return 200 '规则A';
}
location = /login {
return 200 '规则B';
}
location ^~ /static/ {
return 200 '规则C';
}
location ~ \.(gif|jpg|png|js|css)$ {
return 200 '规则D';
}
location ~* \.js$ {
return 200 '规则E';
}
location / {
return 200 '规则F';
}

那么产生的效果如下:
访问根目录/, 比如http://localhost/ 将匹配规则A
访问 http://localhost/login 将匹配规则B,
http://localhost/register 则匹配规则F
http://localhost/static/a.html 将匹配规则C
http://localhost/a.css, 匹配规则D
http://localhost/b.js则优先匹配到 规则D,不会匹配到规则E
http://localhost/static/c.js 则优先匹配到 规则C
http://localhost/a.JS 则匹配规则E, 而不会匹配规则D,因为规则E不区分大小写。
访问 http://localhost/category/id/1111 则最终匹配到规则F,因为以上规则都不匹配,

4)在实际场景中,通常至少有三个匹配规则定义

#直接匹配网站根,通过域名访问网站首页比较频繁,使用这个会加速处理。
#这里是直接转发给后端应用服务器了,也可以是一个静态首页
# 第一个必选规则
location = / {
.....
}
# 第二个必选规则是处理静态文件请求,这是nginx作为http服务器的强项
# 有两种配置模式,目录匹配或后缀匹配,任选其一或搭配使用
location ^~ /static {
root /webroot/static/;
}
location ~* \.(gif|jpg|jpeg|png|css|js|ico)$ {
root /webroot/res/;
}
#第三个规则就是通用规则,用来转发动态请求到后端应用服务器
#非静态文件请求就默认是动态请求,自己根据实际把握
#毕竟目前的一些框架的流行,带.php,.jsp后缀的情况很少了
location / {
.....
}

10.nginx负载均衡

当一台服务器单位时间内访问量很大的时候,服务器压力就会很大,当达到这台服务器的极限,就会崩溃;怎么解决?可以通过nginx的反向代理设置,添加几台同样功能的服务器 分担压力。

nginx实现负载均衡原理,用户访问首先访问到nginx服务器,然后nginx服务器再从应用服务器集群中选择压力比较小的服务器,然后将该访问请求引向该服务器。如应用服务器集群中某一台服务器崩溃,那么从待选择服务器列表中将该服务器删除,也就是说一个服务器崩溃了,那么nginx服务器不会把请求引向到该服务器。

upstream mypro {
server 192.168.5.140:8080;
server 192.168.5.141:8080;
xxxxx
xxxx
}

server {
listen 80;
server_name xxxx;
location / {
proxy_pass http://mypro;
}
}

1)负载均衡方案

随机轮询
upstream mypro {
server 192.168.5.140:8080;
server 192.168.5.141:8080;
}

权重
upstream mypro {
server 192.168.5.140:8080 weight=5;
server 192.168.5.141:8080 weight=10;
}

ip_hash
upstream mypro {
ip_hash;
server 192.168.5.140:8080;
server 192.168.5.141:8080;
}

server {
listen       80;
server_name  192.168.5.138;
location / {
proxy_pass http://mypro;
}
}

11.nginx的echo模块

查看nginx安装的现有模块指令
/usr/local/nginx/sbin/nginx -V (大写的V)
nginx version: nginx/1.13.2
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-16) (GCC)
configure arguments:

nginx -V

built by gcc 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC)
configure arguments: --prefix=/usr/local/nginx --pid-path=/var/run/nginx/nginx.pid --lock-path=/var/lock/nginx.lock --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --with-http_gzip_static_module --http-client-body-temp-path=/var/temp/nginx/client --http-proxy-temp-path=/var/temp/nginx/proxy --http-fastcgi-temp-path=/var/temp/nginx/fastcgi --http-uwsgi-temp-path=/var/temp/nginx/uwsgi --http-scgi-temp-path=/var/temp/nginx/scgi

1、下载需要的echo模块

https://github.com/openresty/echo-nginx-module/tags

wget https://github.com/openresty/echo-nginx-module/archive/v0.61.tar.gz
tar -zxvf v0.61.tar.gz
mv echo-nginx-module-0.61/ ../nginx/nginx-tools/echo-nginx-module-0.61

2、重新编译nginx,安装echo-nginx模块

安装echo模块(文件夹名echo-nginx-module-0.61)
./configure --add-module=/usr/local/nginx/nginx-tools/echo-nginx-module-0.61
make #开始编译,但别安装 (make install会直接覆盖安装)

./configure \
--pid-path=/var/run/nginx/nginx.pid \
--lock-path=/var/lock/nginx.lock \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--with-http_gzip_static_module \
--http-client-body-temp-path=/var/temp/nginx/client \
--http-proxy-temp-path=/var/temp/nginx/proxy \
--http-fastcgi-temp-path=/var/temp/nginx/fastcgi \
--http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
--http-scgi-temp-path=/var/temp/nginx/scgi \
--add-module=/usr/local/nginx/nginx-tools/echo-nginx-module-0.61

编译 make

3、平滑升级 nginx

注意先备份一下之前老的,手动安装一下。
mv /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old
cp -f objs/nginx /usr/local/nginx/sbin/nginx

这里是平滑升级,如是全新安装请执行:make install
make upgrade
make clean (清除编译产生的文件,可以忽略)

location /module1 {
# 上面默认设置为流 请求会变成文件下载
default_type text/plain;
echo 'hello world';
}

location /module {
# 实际上访问/set
echo_exec /set;
}

location /set {
# 上面默认设置为流 请求会变成文件下载
default_type text/plain;

set $foo 'hello world111';   #自定义变量
echo "$request_uri";      #显示nginx全局变量的内容
echo $foo;
}
结果
/module
hello world111

lua模块

可以在nginx服务中执行lua脚本

nginx全局变量

$args :                     #这个变量等于请求行中的参数,同$query_string
$content_length :    #请求头中的Content-length字段。
$content_type :       #请求头中的Content-Type字段。
$document_root :   #当前请求在root指令中指定的值。
$host :                     #请求主机头字段,否则为服务器名称。
$http_user_agent :  #客户端agent信息
$http_cookie :          #客户端cookie信息
$limit_rate :              #这个变量可以限制连接速率。
$request_method :   #客户端请求的动作,通常为GET或POST。
$remote_addr :         #客户端的IP地址。
$remote_port :          #客户端的端口。
$remote_user :         #已经经过Auth Basic Module验证的用户名。
$request_filename : #当前请求的文件路径,由root或alias指令与URI请求生成。
$scheme :                #HTTP方法(如http,https)。
$server_protocol :    #请求使用的协议,通常是HTTP/1.0或HTTP/1.1。
$server_addr :         #服务器地址,在完成一次系统调用后可以确定这个值。
$server_name :       #服务器名称。
$server_port :          #请求到达服务器的端口号。
$request_uri :          #包含请求参数的原始URI,不包含主机名,如:”/foo/bar.php?arg=baz”。
$uri :                        #不带请求参数的当前URI,$uri不包含主机名,如”/foo/bar.html”。
$document_uri :      #与$uri相同

12.nginx的echo模块

一、Nginx优点

十几年前,互联网没有今天这么火,
软件外包开发,信息化建设,帮助企业做无纸化办公,收银系统,工厂erp,c/s架构偏多
互联网05年;;;b/s架构

Nginx设计为一个主进程多个工作进程的工作模式,每个进程是单线程来处理多个连接,而且每个工作进程采用了非阻塞I/O来处理多个连接,从而减少了线程上下文切换,从而实现了公认的高性能、高并发;因此在生成环境中会通过把CPU绑定给Nginx工作进程从而提升其性能;另外因为单线程工作模式的特点,内存占用就非常少了。

Nginx更改配置重启速度非常快,可以毫秒级,而且支持不停止Nginx进行升级Nginx版本、动态重载Nginx配置。

Nginx模块也是非常多,功能也很强劲,不仅可以作为http负载均衡,Nginx发布1.9.0版本还支持TCP负载均衡,还可以很容易的实现内容缓存、web服务器、反向代理、访问控制等功能。

nginx模块:rewrite 经常用到的

二、什么是ngx_lua

ngx_lua是Nginx的一个模块,将Lua嵌入到Nginx中,从而可以使用Lua来编写脚本,这样就可以使用Lua编写应用脚本,部署到Nginx中运行,即Nginx变成了一个Web容器;这样开发人员就可以使用Lua语言开发高性能Web应用了。

Lua是一种轻量级、可嵌入式的脚本语言,这样可以非常容易的嵌入到其他语言中使用。另外Lua提供了协程并发,即以同步调用的方式进行异步执行,从而实现并发,比起回调机制的并发来说代码更容易编写和理解,排查问题也会容易。Lua还提供了闭包机制,函数可以作为First Class Value 进行参数传递,另外其实现了标记清除垃圾收集。

因为Lua的小巧轻量级,可以在Nginx中嵌入Lua VM,请求的时候创建一个VM,请求结束的时候回收VM。

ngx_lua模块的原理

ngx_lua将Lua嵌入Nginx,能够让Nginx运行Lua脚本,而且高并发、非堵塞的处理各种请求。
Lua内建协程。这样就能够非常好的将异步回调转换成顺序调用的形式。
ngx_lua在Lua中进行的IO操作都会托付给Nginx的事件模型。从而实现非堵塞调用。
开发人员能够採用串行的方式编敲代码,ngx_lua会自己主动的在进行堵塞的IO操作时中断。
保存上下文;然后将IO操作托付给Nginx事件处理机制。
在IO操作完毕后,ngx_lua会恢复上下文,程序继续运行,这些操作都是对用户程序透明的。

每一个NginxWorker进程持有一个Lua解释器或者LuaJIT实例,被这个Worker处理的全部请求共享这个实例。

每一个请求的Context会被Lua轻量级的协程切割,从而保证各个请求是独立的。
ngx_lua採用“one-coroutine-per-request”的处理模型。
对于每一个用户请求,ngx_lua会唤醒一个协程用于执行用户代码处理请求,当请求处理完毕这个协程会被销毁。

每一个协程都有一个独立的全局环境(变量空间),继承于全局共享的、仅仅读的“comman data”。
被用户代码注入全局空间的不论什么变量都不会影响其它请求的处理。
而且这些变量在请求处理完毕后会被释放,这样就保证全部的用户代码都执行在一个“sandbox”(沙箱),这个沙箱与请求具有同样的生命周期。
得益于Lua协程的支持。ngx_lua在处理10000个并发请求时仅仅须要非常少的内存。
依据測试,ngx_lua处理每一个请求仅仅须要2KB的内存,假设使用LuaJIT则会更少。
所以ngx_lua非常适合用于实现可扩展的、高并发的服务。

三、ngx_lua安装

echo模块

ngx_lua安装能够通过下载模块源代码,编译Nginx。可是推荐採用openresty。
Openresty就是一个打包程序,包括大量的第三方Nginx模块,比方HttpLuaModule,HttpRedis2Module,HttpEchoModule等。省去下载模块。而且安装很方便。

OpenResty将Nginx核心、LuaJIT、许多有用的Lua库和Nginx第三方模块打包在一起;这样开发人员只需要安装OpenResty,不需要了解Nginx核心和写复杂的C/C++模块就可以,只需要使用Lua语言进行Web应用开发了。

OpenResty提供了一些常用的ngx_lua开发模块

lua-resty-memcached
lua-resty-mysql
lua-resty-redis
lua-resty-dns
lua-resty-limit-traffic
lua-resty-template
nginx + lua 就可以开发出 一些系统。龙果学院中 有一门课程 就专门应用了这个技术

这些模块涉及到如mysql数据库、redis、限流、模块渲染等常用功能组件;
另外也有很多第三方的ngx_lua组件供我们使用,对于大部分应用场景来说现在生态环境中的组件已经足够多了;如果不满足需求也可以自己去写来完成自己的需求。

openresty.org/cn官网

应用场景

应用的公司:奇虎360、京东、百度、魅族、知乎、优酷、新浪这些互联网公司都在使用。
业务场景: WAF、有做 CDN 调度、广告系统、消息推送系统,API server 网关

13.openresty安装

1)下载安装

centos系统
yum install readline-devel pcre pcre-devel openssl openssl-devel gcc curl GeoIP-devel

下载源码包

https://github.com/openresty/openresty/releases/tag/v1.15.8.2
选择最新版本v1.15.8.2

解压安装

tar -xzvf openresty-1.15.8.2.tar.gz

cd openresty-1.15.8.2

选择模块

./configure --help  可以看见默认的 openresty安装路径

./configure \
--with-luajit \
--with-pcre \
--with-http_gzip_static_module \
--with-http_realip_module \
--with-http_geoip_module \
--with-http_ssl_module  \
--with-http_stub_status_module

--with-http_gzip_static_module #静态文件压缩
--with-http_stub_status_module #监控nginx状态
--with-http_realip_module #通过这个模块允许我们改变客户端请求头中客户端IP地址值(例如X-Real-IP 或 X-Forwarded-For),意义在于能够使得后台服务器记录原始客户端的IP地址
--with-pcre #设置PCRE库(pcre pcre-devel)
--with-http_ssl_module #使用https协议模块。(openssl openssl-devel)
--with-http_geoip_module #增加了根据ip获得城市信息,经纬度等模块 (GeoIP-devel)

make && make install

2)安装成功后,默认会在/usr/local/openresty/

luajit 是采用C语言写的Lua代码的解释器 ----just in time   即时解析
lualib 是编辑好的lua类库
nginx,其实我们openResty就是nginx,只是做了一些模块化工作;所以启动openResty就是启动nginx,我们可以到 cd nginx/sbin/,直接运行  ./nginx

3)设置环境变量

vi /etc/profile
export NGINX_HOME=/usr/local/openresty/nginx
export PATH=$PATH:$NGINX_HOME/sbin
source /etc/profile ##生效

14.openresty的helloWord

1)ngx_lua模块的hello world

编辑nginx下conf配置文件nginx.conf
vi nginx.conf
在server模块加上
location /helloworld {
default_type text/html;
content_by_lua 'ngx.say("hello world")';
}

检查配置文件是否正确
/usr/local/openresty/nginx/sbin/nginx -t -c /usr/local/openresty/nginx/conf/nginx.conf
nginx -t

重启nginx
nginx -s reload
访问http://111.230.33.133/helloworld  输出 hello world

2)nginx的内部变量

名称      说明
$arg_name 请求中的name参数
$args 请求中的参数
$binary_remote_addr 远程地址的二进制表示
$body_bytes_sent 已发送的消息体字节数
$content_length HTTP请求信息里的"Content-Length"
$content_type 请求信息里的"Content-Type"
$document_root 针对当前请求的根路径设置值
$document_uri 与$uri相同; 比如 /test2/test.php
$host 请求信息中的"Host",如果请求中没有Host行,则等于设置的服务器名
$hostname 机器名使用 gethostname系统调用的值
$http_cookie cookie 信息
$http_referer 引用地址
$http_user_agent 客户端代理信息
$http_via 最后一个访问服务器的Ip地址。
$http_x_forwarded_for 相当于网络访问路径
$is_args 如果请求行带有参数,返回“?”,否则返回空字符串
$limit_rate 对连接速率的限制
$nginx_version 当前运行的nginx版本号
$pid worker进程的PID
$query_string 与$args相同
$realpath_root 按root指令或alias指令算出的当前请求的绝对路径。其中的符号链接都会解析成真是文件路径
$remote_addr 客户端IP地址
$remote_port 客户端端口号
$remote_user 客户端用户名,认证用
$request 用户请求
$request_body 这个变量(0.7.58+)包含请求的主要信息。在使用proxy_pass或fastcgi_pass指令的location中比较有意义
$request_body_file 客户端请求主体信息的临时文件名
$request_completion 如果请求成功,设为"OK";如果请求未完成或者不是一系列请求中最后一部分则设为空
$request_filename 当前请求的文件路径名,比如/opt/nginx/www/test.php
$request_method 请求的方法,比如"GET"、"POST"等
$request_uri 请求的URI,带参数; 比如http://localhost:88/test1/
$scheme 所用的协议,比如http或者是https
$server_addr 服务器地址,如果没有用listen指明服务器地址,使用这个变量将发起一次系统调用以取得地址(造成资源浪费)
$server_name 请求到达的服务器名
$server_port 请求到达的服务器端口号
$server_protocol 请求的协议版本,"HTTP/1.0"或"HTTP/1.1"
$uri 请求的URI,可能和最初的值有不同,比如经过重定向之类的

写个配置文件,测试一下$uri变量
location /1test_url {
echo "url:$uri";
}

location /2test_url {
echo "url:$uri";
echo "full url : $host$request_uri";
}

重启nginx,测试$args变量

location /3test_url {
echo "url:$uri----args:$args";
}

测试$arg_name变量

location /4test_url {
echo "url:$uri----args:$args--------arg_name:$arg_name";
}

说明一下,$arg_name表示取名为name的参数,如果想取其他名称的参数可以对应的取该参数名

location /5test_url {
echo "url:$uri --- args:$args --- arg_name:$arg_name <br/>";
echo "arg_user:$arg_user --- arg_age:$arg_age<br/>";
echo "arg_test:$arg_test";
}

test参数名不存在,则为空。

自定义变量

location /1test_def {
set $name "rainbow";
echo $name;
}
set 设置的变量 为局部变量,其他请求无法获取此$name的值

location /2test_def {
set $name "rainbow";
echo_exec /3test_def;  ##内部跳转,可以传递变量
}
location /3test_def{
echo $name;
}

15.openresty的helloWord

一)lua介绍

1993 年在巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro in Brazil)诞生了一门编程语言,发明者是该校的三位研究人员,他们给这门语言取了个浪漫的名字——Lua,在葡萄牙语里代表美丽的月亮。事实证明她没有糟蹋这个优美的单词,Lua 语言正如它名字所预示的那样成长为一门简洁、优雅且富有乐趣的语言。
Lua 从一开始就是作为一门方便嵌入(其它应用程序)并可扩展的轻量级脚本语言来设计的,因此她一直遵从着简单、小巧、可移植、快速的原则,官方实现完全采用 ANSI C 编写,能以 C 程序库的形式嵌入到宿主程序中。LuaJIT 2 和标准 Lua 5.1 解释器采用的是著名的 MIT 许可协议。正由于上述特点,所以 Lua 在游戏开发、机器人控制、分布式应用、图像处理、生物信息学等各种各样的领域中得到了越来越广泛的应用。其中尤以游戏开发为最,许多著名的游戏

Lua 和 LuaJIT 的区别

Lua 非常高效,它运行得比许多其它脚本(如 Perl、Python、Ruby)都快,这点在第三方的独立测评中得到了证实。尽管如此,仍然会有人不满足,他们总觉得“嗯,还不够快!”。LuaJIT 就是一个为了再榨出一些速度的尝试,它利用即时编译(Just-in Time)技术把 Lua 代码编译成本地机器码后交由 CPU 直接执行。LuaJIT 2 的测评报告表明,在数值运算、循环与函数调用、协程切换、字符串操作等许多方面它的加速效果都很显著。凭借着 FFI 特性,LuaJIT 2 在那些需要频繁地调用外部 C/C++ 代码的场景,也要比标准 Lua 解释器快很多。目前 LuaJIT 2 已经支持包括 i386、x86_64、ARM、PowerPC 以及 MIPS 等多种不同的体系结构。
LuaJIT 是采用 C 和汇编语言编写的 Lua 解释器与即时编译器。LuaJIT 被设计成全兼容标准的 Lua 5.1 语言,同时可选地支持 Lua 5.2 和 Lua 5.3 中的一些不破坏向后兼容性的有用特性。因此,标准 Lua 语言的代码可以不加修改地运行在 LuaJIT 之上。LuaJIT 和标准 Lua 解释器的一大区别是,LuaJIT 的执行速度,即使是其汇编编写的 Lua 解释器,也要比标准 Lua 5.1 解释器快很多,可以说是一个高效的 Lua 实现。另一个区别是,LuaJIT 支持比标准 Lua 5.1 语言更多的基本原语和特性,因此功能上也要更加强大。

2)应用场景 在很多时候,我们可以将Lua直接嵌入到我们的应用程序中,如游戏、监控服务器等。 这样的应用方式对于程序的最终用户而言是完全透明的,但是对于程序本身,其扩展性将会得到极大的增强。

将Lua视为一种独立的脚本语言,通过它来帮助我们完成一些软件产品的辅助性工具的开发。比如在我们之前的数据分析产品中,我们通过编写Lua脚本,将每个用户不同格式的数据重新格式化为我们的软件平台能够读取的格式,之后再将格式化的后的数据加载到数据库中,或者是写入我们的分析引擎可以识别的数据分析文件中。这其中Lua仅仅用于文件格式的规格化过程,至于此后的操作,都是通过Lua调用我们的C语言导出函数来完成的。

将Lua应用于应用程序的动态配置部分。比如移动智能设备或嵌入式设备,它们的显示分辨率在很多情况下都是非标准的,如果我们为每一款设备都维护一套相关的配置信息,这无疑会加大我们程序的维护开销,如果我们将这段动态配置逻辑交由Lua脚本完成,那么这对于程序配置的灵活性而言,将会得到很大的提高。甚至可以是这样,运行在移动终端设备上的应用程序,在启动主窗体之前先和服务器建立连接,在服务器确认设备的各种参数后,再将和该设备显示相关的Lua脚本发送给设备客户端,这样客户端在得到Lua脚本之后,就可以立刻执行它以得到最新的动态配置信息。

3)主要优势

高效性
  作为一种脚本语言,Lua的高效是众所周知的,因此在实际应用中,很多大型程序都会考虑将代码中易变的部分用Lua来编写。这不但没有明显降低系统的运行效率,反而使程序的稳定性和可扩展性得到了显著的提升。

可移植性
  在官方网站中提供了基于多种平台的发布包,如Linux/Unix、Windows、Symbian和Pocket PC等。

可嵌入性
  在语言设计之初,Lua就被准确的定位为嵌入式脚本语言,因此Lua的设计者们为Lua提供了与其他编程语言之间的良好交互体验,这特别体现在和C/C++之间的交互上。对于其他语言,如Java和C#,也可以将Lua作为其嵌入式脚本引擎,并在代码中进行直接的交互。

简单强大
  尽管是过程化脚本语言,但由于Lua的设计者们为Lua提供了meta-mechanisms机制,这不仅使Lua具备了一些基本的面向对象特征,如对象和继承,而且仍然保持了过程化语言所具有的语法简单的特征。

小巧轻便
  在最新版本(5.2.0)的Lua中,仅仅包含了大约20000行的C语言代码,编译后的库文件大小约为240K左右,因此这对于很多资源有限的平台有着极强的吸引力。

免费开源
  MIT Licence可以让Lua被免费的用于各种商业程序中。

在openresry里面创建一个lua目录,用来存放lua脚本,里面可再分文件夹
mkdir lua

16.lua基本语法一

一)注释

单行注释
两个减号是单行注释:   --注释内容

多行注释
--[[
多行注释
多行注释
--]]

二)基本类型

Lua中有8个基本类型分别为:
nil(空) -----> java null(空)
boolean(布尔)、
number(数字) 双精度浮点数    ---> java int double float
string(字符串)
table(表) ----> 类似 java map
function(函数)、
userdata(自定义的类型)、
thread(线程/协程)
使用type函数测试给定变量或者值的类型

三)变量

1)变量命名

大小写区分命名规则
Lua 标示符用于定义一个变量,函数获取其他用户定义的项。标示符以一个字母 A 到 Z 或 a 到 z 或下划线 _ 开头后加上0个或多个字母,下划线,数字(0到9)。

_temp

一般约定,以下划线开头连接一串大写字母的名字(比如 _VERSION)被保留用于 Lua 内部全局变量。

关键词:

and	break	do	else
elseif	end	false true	for
function	if	in	local
nil	not	or
return	then	 repeat until
while

变量名字,它的大小写是相关的。也就是说,A和a是两个不同的变量
定义一个变量的方法就是赋值。"="操作就是用来赋值的。

2)全局变量

在默认情况下,变量总是认为是全局的。除非,你在前面加上"local"。这一点要特别注意,因为你可能想在函数里使用局部变量,却忘了用local来说明.

全局变量不需要声明,给一个变量赋值后即创建了这个全局变量,访问一个没有初始化的全局变量也不会出错,只不过得到的结果是:nil。

> print(b)
nil
> b=10
> print(b)
10
>

如果你想删除一个全局变量,只需要将变量赋值为nil。
b = nil
print(b)      --> nil

这样变量b就好像从没被使用过一样。换句话说, 当且仅当一个变量不等于nil时,这个变量即存在。

3)局部变量

变量名称 前加修饰符 local

四)nil类型

print(type(a))
nil

对于全局变量和 table,nil 还有一个"删除"作用,给全局变量或者 table 表里的变量赋一个 nil 值,等同于把它们删掉,执行下面代码就知:

tab1 = { key1 = "val1", key2 = "val2" }
for k, v in pairs(tab1) do
print(k .. " - " .. v)
end
print('---------')
tab1.key1 = nil
for k, v in pairs(tab1) do
print(k .. " - " .. v)
end

放到文件 table_nil.lua文件中
去到目录 /usr/local/openresty/luajit/bin
使用 luajit 文件 进行编译

或者直接执行
/usr/local/openresty/luajit/bin/luajit table_nil.lua

判断nil类型 作比较时应该加上双引号 "

type(X) ---> 返回的类型 其实是string
> type(X)==nil
false
> type(X)=="nil"
true

17.lua基本语法二

一)boolean(布尔)

布尔类型,可选值 true/false;

Lua 中 nil 和 false 为“假”,其它所有值均为“真”。比如 0 和空字符串就是“真”;

local a = true
local b = 0
local c = nil

if a then
print("a")        -->output:a
else
print("not a")    --这个没有执行
end

if b then
print("b")        -->output:b
else
print("not b")    --这个没有执行
end

if c then
print("c")        --这个没有执行
else
print("not c")    -->output:not c
end

二)number(数字)

Number 类型用于表示实数,和 C/C++ 里面的 double 类型很类似。可以使用数学函数 math.floor(向下取整)和 math.ceil(向上取整)进行取整操作。

local count = 10
local order = 3.99
local score = 98.01
print(math.floor(order))   -->output:3
print(math.ceil(score))    -->output:99

三)string(字符串)

Lua 中有三种方式表示字符串:
1、使用一对匹配的单引号。例:'hello'。
2、使用一对匹配的双引号。例:"abclua"。

local str1 = 'hello world'
local str2 = "hello lua"

print(str1)    -->output:hello world
print(str2)    -->output:hello lua

转义字符
string =  hello\',\"\",\\n,\\t

3、字符串还可以用一种长括号(即[[ ]])括起来的方式定义。

整个词法分析过程将不受分行限制,不处理任何转义符。
在 Lua 实现中,Lua 字符串一般都会经历一个“内化”(intern)的过程,即两个完全一样的 Lua 字符串在 Lua 虚拟机中只会存储一份。每一个 Lua 字符串在创建时都会插入到 Lua 虚拟机内部的一个全局的哈希表中。
另外,Lua 的字符串是不可改变的值,不能像在 c 语言中那样直接修改字符串的某个字符,而是根据修改要求来创建一个新的字符串。Lua 也不能通过下标来访问字符串的某个字符。

它支持一些转义字 符,列表如下
\a  响铃
\b  退格 (back space)
\f  提供表格(form feed)
\n  换行(newline)
\r  回车(carriage return)
\t  水平tab(horizontal tab)
\v  垂直tab(vertical tab)
\\  反斜杠(backslash)
\"  双引号(double quote)
\'  单引号(single quote)

定义:"add\name",'hello' 字符串

local str3 = [["add\name",'hello']]
print(str3)    -->output:"add\name",'hello'

4、字符串连接使用的是 ..

print("a" .. 'b')
ab

local str3 = [["add\name","hellow"]]
print(str3)   -->  "add\name","hellow"
local str4 = " wew"
print(str3 .. str4)  ---> "add\name","hellow" wew

5、字符串与number类型转换

print(tonumber("10") == 10)
print(tostring(10) == "10")

6、使用 # 来计算字符串的长度,放在字符串前面

print(#"this is string")

注意:string不可修改

local a= "this is a";
a = "this is new a ";
local b= "this is new a ";

1)在 Lua 实现中,Lua 字符串一般都会经历一个“内化”(intern)的过程,即两个完全一样的 Lua 字符串在 Lua 虚拟机中只会存储一份。每一个 Lua 字符串在创建时都会插入到 Lua 虚拟机内部的一个全局的哈希表中。

2)Lua 的字符串是不可改变的值,不能像在c语言中那样直接修改字符串的某个字符,而是根据修改要求来创建一个新的字符串。Lua 也不能通过下标来访问字符串的某个字符。
Lua的字符串和其它对象都是自动内存管理机制所管理的对象,不需要担心字符串的内存分配和释放

提供了效率,安全
s='123456789';
s1 = s;
s='abcdwff';

print(s);  --> abcdwff
print(s1); --> 123456789

说明指向的地址是不可改的
--------------------
s='123456789'

local s1 = string.sub(s, 2, 5)
print(s)   --> 123456789
print(s1)  --> 2345

18.lua基本语法三

一)function (函数)

有名函数
optional_function_scope function function_name( argument1, argument2, argument3..., argumentn)
function_body
return result_params_comma_separated
end

optional_function_scope: 该参数是可选的制定函数是全局函数还是局部函数,未设置该参数默认为全局函数,如果你需要设置函数为局部函数需要使用关键字 local。
function 函数定义关键字
function_name: 指定函数名称。
argument1, argument2, argument3..., argumentn: 函数参数,多个参数以逗号隔开,函数也可以不带参数。
function_body: 函数体,函数中需要执行的代码语句块。
result_params_comma_separated: 函数返回值,Lua语言函数可以返回多个值,每个值以逗号隔开
end:函数定义结束关键字

1)函数实例

--[[ 函数返回两个值的最大值 --]]
(可以加local) function max(num1, num2)

if (num1 > num2) then
result = num1;
else
result = num2;
end

return result;
end
-- 调用函数
print("两值比较最大值为 ",max(10,4))
print("两值比较最大值为 ",max(5,6))

匿名函数

optional_function_scope function_name = function (argument1, argument2, argument3..., argumentn)
function_body
return result_params_comma_separated
end

有名函数的定义本质上是匿名函数对变量的赋值

function foo()
end
等价于
foo = function ()
end

类似地,
local function foo()
end
等价于
local foo = function ()
end

local function 与 function 区别

1 使用function声明的函数为全局函数,在被引用时可以不会因为声明的顺序而找不到
2 使用local function声明的函数为局部函数,在引用的时候必须要在声明的函数后面

function test()
test2()
test1()
end

local function test1()
print("hello test1")
end

function test2()
print("hello test2")
end

test()

-----------------

local function test1()
print("hello test1")
end

function test()
test2()
test1()
end

function test2()
print("hello test2")
end

test()

函数参数

  1. 将函数作为参数传递给函数

    local myprint = function(param) print("这是打印函数 - ##",param,"##") end

    local function add(num1,num2,functionPrint) result = num1 + num2

    functionPrint(result)

    end

    add(2,5,myprint)

2)传参数,lua参数可变

local function foo(a,b,c,d)
print(a,b,c,d)
end

a、若参数个数大于形参个数,从左向右,多余的实参被忽略
b、若实参个数小于形参个数,从左向右,没有被初始化的形参被初始化为nil
c、Lua还支持变长参数。用...表示。此时访问参数也要用...,如:
function average(...)
result = 0
local arg={...}
for i,v in ipairs(arg) do
result = result + v
end
print("总共传入 " .. #arg .. " 个数")
return result/#arg
end

print("平均值为",average(1,2,3,4,5,6))

3)返回值

Lua函数允许返回多个值,返回多个值时,中间用逗号隔开

函数返回值的规则:
  1)若返回值个数大于接收变量的个数,多余的返回值会被忽略掉;
  2)若返回值个数小于参数个数,从左向右,没有被返回值初始化的变量会被初始化为nil

local function init()
return 1,"lua";
end

local x,y,z = init();

print(x,y,z);

注意:

1)当一个函数返回一个以上的返回值,且函数调用不是一个列表表达式的 最后一个元素,那么函数只返回第一个返回值

local function init()
return 1,"lua";
end

local x,y,z = 2,init();  -- 2 1 lua
local x,y,z = init(),2;  -- 1 2 nil

print(x,y,z);

2)如果你确保只取函数返回值的第一个值,可以使用括号运算符 ()
local function init()
return 1,"lua";
end

local x,y,z = 2,(init());

print(x,y,z);

19.lua基本语法四

一)table (表)

Table 类型实现了一种抽象的“关联数组”。
即可用作数组,也可以用作map。
lua中没有数组和map,都是用table这个类型

--数组
java   int[] intArr = new int[]{1,2,3,4,5,6};
intArr[0]
intArr[1]

--map
HashMap map
map.add(key,value)

-- 初始化表
mytable = {}

-- 指定值
mytable[1]= "Lua"
mytable[2]= "Lua2"
mytalbe["k1"] = v1;

-- 移除引用
mytable = nil
-- lua 垃圾回收会释放内存

lua类似数组的table ,索引值从1开始,,而不是0
mytable={1,2,3,4,5}
mytalbe[1]

mytable={"a","b","hello","world"}
mytable1 = {key1 = "v1",k2="v2",k3="v3"}
mytable2 = {"a",key1 = "v1","b",k2="v2",k3="v3","hello","world"}

print(mytable[1],mytable[2],mytable[3],mytable[4]);
print("------------------")

print(mytable1["key1"],mytable1["k2"],mytable1["k3"]);
--> v1      v2      v3

print("------------------")
print(mytable2[1],mytable2["key1"],mytable2[2],mytable2["k2"],mytable2[3],mytable2[4]);
--> a       v1      b       v2      hello   world

talbe   key可以为 number 或字符串,,也可以是其他类型
table 是内存地址 赋值给变量

二)table进行赋值给变量,其实是把内存地址给了变量,变量只是引用了内存地址

local mytable1 = {"a",key1 = "v1","b",k2="v2",k3="v3","hello","world"}

local mytable2 =  mytable1

mytable2[1] = "aa"

print(mytable2[1])  --> aa

print(mytable1[1])  --> aa

mytable2 = nil  --移除的是引用

print("-------------")
print(mytable1[1])  --> aa

20.lua运算符

一)算术运算符

+	加法
-	减法
*	乘法
/	除法
%	取余
^	乘幂
-	负号

print(1 + 2)       -->打印 3
print(5 / 10)      -->打印 0.5。 这是Lua不同于c语言的
print(5.0 / 10)    -->打印 0.5。 浮点数相除的结果是浮点数
-- print(10 / 0)   -->注意除数不能为0,计算的结果会出错
print(2 ^ 10)      -->打印 1024。 求2的10次方

local num = 1357
print(num % 2)       -->打印 1
print((num % 2) == 1) -->打印 true。 判断num是否为奇数
print((num % 5) == 0)  -->打印 false。判断num是否能被5整数

二)关系运算符

==	等于,检测两个值是否相等,相等返回 true,否则返回 false
~=	不等于,检测两个值是否相等,相等返回 false,否则返回 true	不想java !=,,~=
>	大于,如果左边的值大于右边的值,返回 true,否则返回 false
<	小于,如果左边的值大于右边的值,返回 false,否则返回 true
>=	大于等于,如果左边的值大于等于右边的值,返回 true,否则返回 false
<=	小于等于, 如果左边的值小于等于右边的值,返回 true,否则返回 false

print(1 < 2)    -->打印 true
print(1 == 2)   -->打印 false
print(1 ~= 2)   -->打印 true
local a, b = true, false
print(a == b)  -->打印 false

注意:table, userdate 和函数;判断的是引用地址的判断

local a = { x = 1, y = 0}
local b = { x = 1, y = 0}
if a == b then
print("a==b")
else
print("a~=b")
end

三)逻辑运算符

and	逻辑与操作符。(A and B) 若 A 为 假,则返回 A,否则返回 B
or	逻辑或操作符。(A or B) 若 A 为 真,则返回 A,否则返回 B
not	逻辑非操作符。与逻辑运算结果相反,如果条件为 true,逻辑非为 false。 永远只返回 true 或者 false

local c = nil
local d = 0
local e = 100

print(c and d)  -->打印 nil
print(c and e)  -->打印 nil
print(d and e)  -->打印 100

print(c or d)   -->打印 0
print(c or e)   -->打印 100
print(d or e)   -->打印 0

print(not c)    -->打印 true
print(not d)    -->打印 false

短路取值 原理
and  与,,,if(a and b and c)

a and b and c
a为真,b为真,c为真  返回c
a为真,b为假,c为真  返回b
a为假 bc不管bc为什么值  返回a

一路去判断变量值是否为真,一旦遇到某个变量为假,就立刻短路返回 返回值就是 假的变量值

a or b or c
a为真,bc    返回a
a为假,b为真,不管c是什么值 返回b

四)其他运算符

..	连接两个字符串 a..b  输出结果为 ab"。

一元运算符,返回字符串或表的长度。 #"Hello" 返回 5

很多字符串连接 我们如果采用..这个运算符 ,性能是很低
推荐使用 table 和 table.concat() 来进行很多字符串的拼接

五)优先级

^
not   # -
*   /   %
+   -
..
< > <=  >=  ==  ~=
and
or

local a, b = 1, 2
local x, y = 3, 4
local i = 10
local res = 0
res = a + i < b/2 + 1  -->等价于res =  (a + i) < ((b/2) + 1)
res = 5 + x^2*8        -->等价于res =  5 + ((x^2) * 8)
res = a < y and y <=x  -->等价于res =  (a < y) and (y <= x)

20.lua控制结构一

一)条件 - 控制结构 if-else

if-else 是我们熟知的一种控制结构。
Lua 跟其他语言一样,提供了 if-else 的控制结构。

1)单个 if 分支 型

if 条件 then
--body
end
条件为真 ,执行if中的body

x = 10
if x > 0 then
print("分支一")
end

x = 10
if (x > 0) then
print("分支一")
end

运行输出:分支一

2)两个分支 if-else 型

if 条件 then
--条件为真 执行此body
else
--条件为假 执行此body
end

x = 10
if x > 0 then
print("分支一")
else
print("分支二")
end

运行输出:分支一

3)多个分支 if-elseif-else 型

if 条件一 then
--条件为真 执行此body
elseif  条件二  then
.....
elseif  条件三  then
.....
else
--条件为假 执行此body
end

score = 90
if score == 100 then
print("分支一")
elseif score >= 60 then
print("分支二")
--此处可以添加多个elseif
else
print("分支三")
end
运行输出:分支二

与 C 语言的不同之处是 else 与 if 是连在一起的,若将 else 与 if 写成 "else if" 则相当于在 else 里嵌套另一个 if 语句,如下代码:
score = 0
if score == 100 then
print("分支一")
elseif score >= 60 then
print("分支二")
else
if score > 0 then
print("分支三")
else
print("分支四")
end --与上一示例代码不同的是,此处要添加一个end
end
运行输出:分支四

二)循环 - while 型控制结构

Lua 跟其他常见语言一样,提供了 while 控制结构,语法上也没有什么特别的。但是没有提供 do-while 型的控制结构,但是提供了功能相当的 repeat。
while 型控制结构语法如下,当表达式值为假(即 false 或 nil)时结束循环。也可以使用 break 语言提前跳出循环。

while 条件表达式 do
--body
end

示例代码,求 1 + 2 + 3 + 4 + 5 的结果

x = 1
sum = 0

while x <= 5 do
sum = sum + x
x = x + 1
end
print(sum)  -->output 15

continue继续执行,lua是没有这个概念
break 终端循环,lua是有的

值得一提的是,Lua 并没有像许多其他语言那样提供类似 continue 这样的控制语句用来立即进入下一个循环迭代(如果有的话)。
因此,我们需要仔细地安排循环体里的分支,以避免这样的需求。
没有提供 continue,却也提供了另外一个标准控制语句 break,可以跳出当前循环。
例如我们遍历 table,查找值为 11 的数组下标索引:

local t = {1, 3, 5, 8, 11, 18, 21}
local i = 1
while i < #t do
if 11 == t[i] then
print("index[" .. i .. "] have right value[11]")
break
end
i = i + 1;
end

三)循环 - repeat 控制结构

repeat  ---重复执行
--body
until 条件       条件为真时就结束

1)until的条件表达式  为真 就结束
2)repeat until 控制结构 ,他至少会执行一遍

Lua 中的 repeat 控制结构类似于其他语言(如:C++ 语言)中的 do-while,但是控制方式是刚好相反的。
执行 repeat 循环体后,直到 until 的条件为真时才结束,而其他语言(如:C++ 语言)的 do-while 则是当条件为假时就结束循环。

以下代码将会形成死循环:
x = 10
repeat
print(x)
until false

该代码将导致死循环,因为until的条件一直为假,循环不会结束
除此之外,repeat 与其他语言的 do-while 基本是一样的。同样,Lua 中的 repeat 也可以在使用 break 退出。

21.lua控制结构二

四)for 控制结构

for 语句有两种形式:数字 for 和范型 for。

1)数字型 for 的语法

for var = begin, finish, step do
--body
end

关于数字 for 需要关注以下几点:
1.var 从 begin 变化到 finish,每次变化都以 step 作为步长递增 var
2.begin、finish、step 三个表达式只会在循环开始时执行一次
3.第三个表达式 step 是可选的,默认为 1
4.控制变量 var 的作用域仅在 for 循环内,需要在外面控制,则需将值赋给一个新的变量 5.循环过程中不要改变控制变量的值,那样会带来不可预知的影响

for i = 1, 5 do
print(i)
end

-- output
1
2
3
4
5

for i = 1, 10, 2 do
print(i)
end

-- output
1
3
5
7
9

for i = 10, 1, -1 do
print(i)
end

-- output
10
9
8
7
6
5
4
3
2
1

如果不想给循环设置上限的话,可以使用常量 math.huge

for i = 1, math.huge do
if (0.3*i^3 - 20*i^2 - 500 >=0) then
print(i)
break
end
end

2)for 泛型

对lua的table类型进行遍历

泛型 for 循环通过一个迭代器(iterator)函数来遍历所有值

-- 打印数组a的所有值
local a = {"a", "b", "c", "d"}
for i, v in ipairs(a) do
print("index:", i, " value:", v)
end

-- output:、
index:  1  value: a
index:  2  value: b
index:  3  value: c
index:  4  value: d

Lua 的基础库提供了 ipairs,这是一个用于遍历数组的迭代器函数。
在每次循环中,i 会被赋予一个索引值,同时 v 被赋予一个对应于该索引的数组元素值。

下面是另一个类似的示例,演示了如何遍历一个 table 中所有的 key
-- 打印table t中所有的key
for k in pairs(t) do
print(k)
end

pairs是可以把数组类型和哈希类型索引值,都会迭代出来

对于泛型 for 的使用,再来看一个更具体的示例。假设有这样一个 table,它的内容是一周中每天的名称
local days = {
"Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"
}

k v ===》 v  ,k

现在要将一个名称转换成它在一周中的位置。为此,需要根据给定的名称来搜索这个 table。然而 在 Lua 中,通常更有效的方法是创建一个“逆向 table”。
例如这个逆向 table 叫 revDays,它以 一周中每天的名称作为索引,位置数字作为值:
local revDays = {
["Sunday"] = 1,
["Monday"] = 2,
["Tuesday"] = 3,
["Wednesday"] = 4,
["Thursday"] = 5,
["Friday"] = 6,
["Saturday"] = 7
}

接下来,要找出一个名称所对应的需要,只需用名字来索引这个 reverse table 即可
local x = "Tuesday"
print(revDays[x])  -->3

当然,不必手动声明这个逆向 table,而是通过原来的 table 自动地构造出这个逆向 table
local days = {
"Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday","Sunday"
}

local revDays = {}
for k, v in pairs(days) do
revDays[v] = k
end

-- print value
for k,v in pairs(revDays) do
print("k:", k, " v:", v)
end

-- output:
k:  Tuesday   v: 2
k:  Monday    v: 1
k:  Sunday    v: 7
k:  Thursday  v: 4
k:  Friday    v: 5
k:  Wednesday v: 3
k:  Saturday  v: 6

这个循环会为每个元素进行赋值,其中变量 k 为 key(1、2、...),变量 v 为 value("Sunday"、"Monday"、...)。
值得一提的是,在 LuaJIT 2.1 中,ipairs() 内建函数是可以被 JIT 编译的,而 pairs() 则只能被解释执行。因此在性能敏感的场景,应当合理安排数据结构,避免对哈希表进行遍历。
事实上,即使未来 pairs 可以被 JIT 编译,哈希表的遍历本身也不会有数组遍历那么高效,毕竟哈希表就不是为遍历而设计的数据结构。

五)break,return 关键字

1)break

语句 break 用来终止 while、repeat 和 for 三种循环的执行,并跳出当前循环体, 继续执行当前循环之后的语句。下面举一个 while 循环中的 break 的例子来说明:

-- 计算最小的x,使从1到x的所有数相加和大于100
sum = 0
i = 1
while true do
sum = sum + i
if sum > 100 then
break
end
i = i + 1
end
print("The result is " .. i)  -->output:The result is 14
在实际应用中,break 经常用于嵌套循环中。

2)return

return 主要用于从函数中返回结果,或者用于简单的结束一个函数的执行。
return 只能写在语句块的最后,一旦执行了 return语句,该语句之后的所有语句都不会再执行。

执行return方法,如果实在主函数体里面,不在语句块中;执行return  且没有返回值,之后的语句照样会执行

若要写在函数中间,则只能写在一个显式的语句块内,参见示例代码:

local function add(x, y)
return x + y
end

local function is_positive(x)
if x > 0 then
return x .. " is > 0"
else
return x .. " is not > 0"
end

print("function end!")
end

--由于return只出现在前面显式的语句块,所以此语句不注释也不会报错
--,但是不会被执行,此处不会产生输出
sum = add(10, 20)
print("The sum is " .. sum)  -->output:The sum is 30
answer = is_positive(-10)
print(answer)                -->output:-10 is is not > 0

有时候,为了调试方便,我们可以想在某个函数的中间提前 return,以进行控制流的短路。
此时我们可以将 return 放在一个 do ... end 代码块中,例如:
local function add(x, y)
print(1)
return
print(2)
end

--return 不放在语句块中,return 也没有返回值,不注释该语句,不会报错; 但会执行return之后的业务
local function add(x, y)
print(1)
do return end
print(2)
end

22.lua的正则表达式

元字符		描述		表达式实例	匹配的字符串
字符
普通字符		除去%.[]()^$*+-?的字符,匹配字符本身	Kana	Kana
.			匹配任意字符	Ka.a	Kana,Ka2a
%			转义字符,改变后一个字符的原有意思。当后面的接的是特殊字符时,将还原特殊字符的原意。%和一些特定的字母组合构成了lua的预定义字符集。%和数字1~9组合表示之前捕获的分组,就是重复了一遍的意思	K%wna	Kana,K9na
%%na%%	%na%
(a)n%1	ana
[...]		字符集(字符类)。匹配一个包含于集合内的字符。[...]中的特殊字符将还原其原意,但有下面几种特殊情况	[a%%]na	%na,ana
1. %],%-,%^作为整体表示字符']','-','^'	[%a]na	wna,bna,Bna
2. 预定义字符集作为一个整体表示对应字符集	[%%a]na	%na,ana
[...-...]	-表示ascii码在它前一个字符到它后一个字符之间的所有字符	[a-z]na	ana,bna…..zna
[^...]		不在...中的字符集合。	[^0-9]na	Kna
[^^0-9]na	Kna

重复(数量词)
*	表示前一个字符出现0次或多次	[0-9]*	2009,2,23
[a-z]*9*	na
+	表示前一个字符出现1次或1次以上	n+[0-9]+	n2009,nn99
-	匹配前一字符0次或多次
?	表示前一个字符出现0次或1次	n?[0-9]+	2009

"元字符+和*是贪婪的,总是进行最长的匹配,而-则是吝啬的,总是进行最短匹配,注意元字符-可以匹配0次。例子:
待匹配的字符串:<font>a</font><font>b</font>
模式串(1):<font>.+</font>此时将匹配整个字符串,贪婪模式下,正则引擎即使发现了第一个匹配,也不会停止,因此效率相对较低。
模式串(2):<font>.-</font>此时将依次匹配<font>a</font>、<font>b</font>,最短匹配模式下,一旦正则引擎发现第一个匹配就停止动作,不会继续匹配"

预定义字符集
%s	空白字符(比如空格,tab啊)	an[%s]?9	an 9
%p	标点符号	an[%p]9	an.9
%c	控制字符 例如\n
%w	字母数字[a-zA-Z0-9]	[%w]+	Kana9
%a	字母[a-zA-Z]	[%a]*	Kana
%l	小写字母[a-z]	-
%u	大写字母[A-Z]	-
%d	数字[0-9]	-
%x	16进制数[0-9a-fA-F]	-
%z	ascii码是0的字符	-

分组
(...)	表达式中用小括号包围的子字符串为一个分组,分组从左到右(以左括号的位置),组序号从1开始递增。	ab(%d+)	ab233
(%d+)%1	123123 1212 12341234  就是重复了一遍的意思

边界匹配(属于零宽断言)
^	匹配字符串开头	^(%a)%w*	abc123
$	匹配字符串结尾	%w*(%d)$	abc123

23.lua的String操作

string的相关操作

1)string.upper(s)

接收一个字符串 s,返回一个把所有小写字母变成大写字母的字符串。
print(string.upper("Hello Lua"))  -->output  HELLO LUA

2)string.lower(s)

接收一个字符串 s,返回一个把所有大写字母变成小写字母的字符串。
print(string.lower("Hello Lua"))  -->output   hello lua

3)string.len(s)

接收一个字符串,返回它的长度
print(string.len("hello lua")) -->output  9
使用此函数是不推荐的。推荐使用 # 运算符来获取 Lua 字符串的长度。
print(#("hello lua")) -->output  9
由于 Lua 字符串的长度是专门存放的,并不需要像 C 字符串那样即时计算
因此获取字符串长度的操作总是 O(1) 的时间复杂度。

4)string.find(s, p [, init [, plain]]) --查找子字符串

在 s 字符串中第一次匹配 p 字符串。若匹配成功,则返回 p 字符串中出现的开始位置和结束位置;
若匹配失败,则返回 nil。

第三个参数 init 默认为 1,并且可以为负整数,
当 init 为负数时,表示从后往前数的字符个数;再从此索引处开始向后匹配字符串 p 。

第四个参数默认为 false,当其为 true 时,关闭模式匹配;只会把 p 看成一个字符串对待。

local find = string.find
print(find("abc cba", "ab"))
print(find("abc cba", "ab", 2))
print(find("abc cba", "ba", -1))
print(find("abc cba", "ba", -3))

-->output  开始到结束两个下标
1   2
nil
nil
6   7

模式匹配--lua正则表达式

local s = "am+df"
print(string.find(s, "m+", 1, false))    -- 2    2、
其中字符 + 在 Lua 正则表达式中的意思是匹配在它之前的那个字符一次或者多次,
也就是说 m+ 在正则表达式里会去匹配 m, mm, mmm ……。

print(string.find(s, "m+", 1, true))    -- 2    3
plain为true,关闭了模式匹配,p参数也就是"m+",当做了是个普通字符串,不进行模式匹配

5)string.format(formatstring, ...) --格式化输出

按照格式化参数 formatstring,返回后面 ... 内容的格式化版本。
编写格式化字符串的规则与标准 c 语言中 printf 函数的规则基本相同:
它由常规文本和指示组成,这些指示控制了每个参数应放到格式化结果的什么位置,及如何放入它们

一个指示由字符 % 加上一个字母组成,这些字母指定了如何格式化参数,
例如 d 用于十进制数、x 用于十六进制数、o 用于八进制数、f 用于浮点数、s 用于字符串等。
在字符 % 和字母之间可以再指定一些其他选项,用于控制格式的细节。

print(string.format("%.4f", 3.1415926))     -- 保留4位小数
print(string.format("%d %x %o", 31, 31, 31))-- 十进制数31转换成不同进制

d = 29; m = 7; y = 2015                     -- 一行包含几个语句,用;分开
print(string.format("%s %02d/%02d/%d", "today is:", d, m, y))

-->output
3.1416
31 1f 37
today is: 29/07/2015

6)整型数字 与 字符互换

Lua 字符串总是由字节构成的。下标是从 1 开始的,这不同于像 C 和 Perl

string.byte(s [, i [, j ]])
返回字符 s[i]、s[i + 1]、s[i + 2]、······、s[j] 所对应的 ASCII 码。
i 的默认值为 1,即第一个字节;j 的默认值为 i
print(string.byte("abc", 1, 3))
print(string.byte("abc", 3)) -- 缺少第三个参数,第三个参数默认与第二个相同,此时为 3
print(string.byte("abc"))    -- 缺少第二个和第三个参数,此时这两个参数都默认为 1

-->output
97  98  99
99
97
由于 string.byte 只返回整数,而并不像 string.sub 等函数那样(尝试)创建新的 Lua 字符串,
因此使用 string.byte 来进行字符串相关的扫描和分析是最为高效的,尤其是在被 LuaJIT 2 所 JIT 编译之后。

string.char (...)

接收 0 个或更多的整数(整数范围:0~255),返回这些整数所对应的 ASCII 码字符组成的字符串。
当参数为空时,默认是一个 0。
print(string.char(96, 97, 98))
print(string.char())        -- 参数为空,默认是一个0,
-- 你可以用string.byte(string.char())测试一下
print(string.char(65, 66))

--> output
`ab

AB

如果你只是想对字符串中的单个字节进行检查,使用 string.char 函数通常会更为高效。

7)string.match(s, p [, init])--匹配子字符串

在字符串 s 中匹配(模式)字符串 p,若匹配成功,则返回目标字符串中与模式匹配的子串;否则返回 nil。
第三个参数 init 默认为 1,并且可以为负整数,
当 init 为负数时,表示从后往前数的字符个数,在此索引处开始向后匹配字符串 p。

print(string.match("hello lua", "lua"))
print(string.match("lua lua", "lua", 2))  --匹配后面那个lua
print(string.match("lua lua", "hello"))
print(string.match("today is 27/7/2015", "%d+/%d+/%d+"))

-->output
lua
lua
nil
27/7/2015

string.match 目前并不能被 JIT 编译,应 尽量 使用 ngx_lua 模块提供的 ngx.re.match 等接口。

8)string.gmatch(s, p) --匹配多个字符串

返回一个迭代器函数,通过这个迭代器函数可以遍历到在字符串 s 中出现模式串 p 的所有地方。
s = "hello world from Lua"
for w in string.gmatch(s, "%a+") do  --匹配最长连续且只含字母的字符串
print(w)
end

-->output
hello
world
from
Lua

t = {}
s = "from=world, to=Lua"
for k, v in string.gmatch(s, "(%a+)=(%a+)") do  --匹配两个最长连续且只含字母的
t[k] = v                                    --字符串,它们之间用等号连接
end
for k, v in pairs(t) do
print (k,v)
end

-->output
to      Lua
from    world

此函数目前并不能被 LuaJIT 所 JIT 编译,而只能被解释执行。应 尽量 使用 ngx_lua 模块提供的 ngx.re.gmatch 等接口。

9)string.rep(s, n) --字符串拷贝

返回字符串 s 的 n 次拷贝。
print(string.rep("abc", 3)) --拷贝3次"abc"

-->output  abcabcabc

10)string.sub(s, i [, j]) --截取子字符串

返回字符串 s 中,索引 i 到索引 j 之间的子字符串。当 j 缺省时,默认为 -1,也就是字符串 s 的最后位置。
i 可以为负数。当索引 i 在字符串 s 的位置在索引 j 的后面时,将返回一个空字符串。
print(string.sub("Hello Lua", 4, 7))
print(string.sub("Hello Lua", 2))
print(string.sub("Hello Lua", 2, 1))    --看到返回什么了吗
print(string.sub("Hello Lua", -3, -1))

-->output
lo L
ello Lua

Lua

11)string.gsub(s, p, r [, n]) --替换子字符串

将目标字符串 s 中所有的子串 p 替换成字符串r。可选参数n,表示限制替换次数。
返回值有两个,第一个是被替换后的字符串,第二个是替换了多少次。
print(string.gsub("Lua Lua Lua", "Lua", "hello"))
print(string.gsub("Lua Lua Lua", "Lua", "hello", 2)) --指明第四个参数

-->output
hello hello hello   3
hello hello Lua     2
此函数不能为 LuaJIT 所 JIT 编译,而只能被解释执行。一般我们推荐使用 ngx_lua 模块提供的 ngx.re.gsub 函数。

12)string.reverse (s) --反转

接收一个字符串 s,返回这个字符串的反转。
print(string.reverse("Hello Lua"))  --> output: auL olleH

24.lua的table操作

Lua中table内部实际采用哈希表和数组分别保存键值对、普通值;下标从1开始

不推荐混合使用这两种赋值方式。

local color={first="red", "blue", third="green", "yellow"}

print(color["first"]) --> output: red print(color[1]) --> output: blue print(color["third"]) --> output: green print(color[2]) --> output: yellow print(color[3]) --> output: nil

一)table.getn 获取长度

相关于取长度操作符写作一元操作 #。

字符串的长度是它的字节数(就是以一个字符一个字节计算的字符串长度)。 对于常规的数组,里面从 1 到 n 放着一些非空的值的时候,它的长度就精确的为 n,即最后一个值的下标。 local tblTest1 = { 1, a = 2, 3 } print("Test1 " .. table.getn(tblTest1)) 此table的长度为2,可是明明是三个数值啊,这里要说明一下,getn 只能执行数值型的table,即数值索引的值. 忽略哈希值类型的键值对map,即不包含字符索引值

local tblTest1 = { b=1, a = 2, 3 } print("Test1 " .. table.getn(tblTest1)) #输出长度为1

local tblTest1 = { b=1, a = 2, c=3 } print("Test1 " .. table.getn(tblTest1)) #输出长度为0

local tblTest1 = { 1, 2, 3 } print("Test1 " .. table.getn(tblTest1)) #输出长度为3

在有个地方说明一下 --此table 虽然有[3]=6,也是数组型,但是不连续的,缺少了2的索引,所以输出长度为1

local tblTest1 = { [3] = 6,c = 1, a = 2, [1]=3 } print("Test1 " .. table.getn(tblTest1))

--此table 数值索引是连续 到2,所以输出长度为2 local tblTest1 = { [2] = 6,c = 1, a = 2, 3 } print("Test1 " .. table.getn(tblTest1))

============================= 获取table长度,不区分数组和键值对

function table_length(t) local leng=0 for k, v in pairs(t) do leng=leng+1 end return leng; end

================= 特殊说明:

如果数组有一个“空洞”(就是说,nil 值被夹在非空值之间),那么 #t 可能是指向任何一个是 nil 值的前一个位置的下标(就是说,任何一个 nil 值都有可能被当成数组的结束)。 这也就说明对于有“空洞”的情况,table 的长度存在一定的 不可确定性。

关于nil的特别说明 local tblTest2 = { 1,nil,2} 对一个table中有nil值 取长度,会有很多不确定性,不同的luajit版本输出的结果也不一样

不要在 Lua 的 table 中使用 nil 值,如果一个元素要删除,直接 remove,不要用 nil 去代替。

二)table.concat (table [, sep [, i [, j ] ] ])

对于元素是 string 或者 number 类型的表 table, 返回 table[i]..sep..table[i+1] ··· sep..table[j] 连接成的字符串。填充字符串 sep 默认为空白字符串。 起始索引位置 i 默认为 1,结束索引位置 j 默认是 table 的长度。如果 i 大于 j,返回一个空字符串。

local a = {1, 3, 5, "hello" } print(table.concat(a)) -- output: 135hello print(table.concat(a, "|")) -- output: 1|3|5|hello print(table.concat(a, " ", 2, 4)) -- output: 3 5 hello print(table.concat(a, " ", 4, 2)) -- output:

在介绍string字符串那边有个字符串拼接,是用.. 这个符号进行的 local str = "a" .. "b" .. "c".......................... 推荐用concat

三)table.insert (table, [pos ,] value)

在(数组型)表 table 的 pos 索引位置插入 value,其它元素向后移动到空的地方。 pos 的默认值是表的长度加一,即默认是插在表的最后。 local a = {1, 8} --a[1] = 1,a[2] = 8 table.insert(a, 1, 3) --在表索引为1处插入3 print(a[1], a[2], a[3]) table.insert(a, 10) --在表的最后插入10 print(a[1], a[2], a[3], a[4])

-->output 3 1 8 3 1 8 10

四)table.remove (table [, pos])

在表 table 中删除索引为 pos(pos 只能是 number型)的元素,并返回这个被删除的元素, 它后面所有元素的索引值都会减一。pos 的默认值是表的长度,即默认是删除表的最后一个元素。 local a = { 1, 2, 3, 4} print(table.remove(a, 1)) --删除速索引为1的元素 print(a[1], a[2], a[3], a[4])

print(table.remove(a)) --删除最后一个元素 print(a[1], a[2], a[3], a[4])

-->output 1 2 3 4 nil 4 2 3 nil nil

五)table.sort (table [, comp])

local a = { 1, 7, 3, 4, 25} table.sort(a) --默认从小到大排序

print(a[1], a[2], a[3], a[4], a[5]) -->output 1 3 4 7 25

按照给定的比较函数 comp 给表 table 排序,也就是从 table[1] 到 table
,这里 n 表示 table 的长度。 比较函数有两个参数,如果希望第一个参数排在第二个的前面,就应该返回 true,否则返回 false。 如果比较函数 comp 没有给出,默认从小到大排序。

local function compare(x, y) --从大到小排序 return x > y --如果第一个参数大于第二个就返回true,否则返回false end

table.sort(a, compare) --使用比较函数进行排序 print(a[1], a[2], a[3], a[4], a[5])

-->output 25 7 4 3 1

六)table.maxn (table)

返回(数组型)表 table 的最大索引编号;如果此表没有正的索引编号,返回 0。 local a = a[-1] = 10 print(table.maxn(a)) a[5] = 10 print(table.maxn(a))

-->output 0 5

七)table 判断是否为空

大家在使用 Lua 的时候,一定会遇到不少和 nil 有关的坑吧。 有时候不小心引用了一个没有赋值的变量,这时它的值默认为 nil。如果对一个 nil 进行索引的话,会导致异常。 如下: local person = {name = "Bob", sex = "M"} -- do something person = nil -- do something print(person.name)

报错person为nil了 然而,在实际的工程代码中,我们很难这么轻易地发现我们引用了 nil 变量。 因此,在很多情况下我们在访问一些 table 型变量时,需要先判断该变量是否为 nil,例如将上面的代码改成:

local person = {name = "Bob", sex = "M"} -- do something person = nil -- do something if person ~= nil then print(person.name) else print("person 为空") end

对于简单类型的变量,我们可以用 if (var == nil) then 这样的简单句子来判断。 我们如果要判断table类型的对象是否为空,那如何判断呢?

我们思考一下,判断table是否为空有两种情况:

第一种table对象为nil; 第二种table对象为,代表没有键值对,但不为nil。

那么我们一般的判断逻辑就应该是 table == nil 或者 table的长度为0 就表示为空 下面我们看看以下例子: local a = local b = {name = "Bob", sex = "Male"} local c = {"Male", "Female"} local d = nil

if a == nil then print("a == nil") end

if b == nil then print("b == nil") end

if c == nil then print("c == nil") end

if d== nil then print("d == nil") end

if next(a) == nil then print("next(a) == nil") end

if next(b) == nil then print("next(b) == nil") end

if next(c) == nil then print("next(c) == nil") end

以上有几个注意点,涉及到table类型的长度 (#a) --"#"表示为获取table类型的长度,类似table.getn() 因为a为,所以长度为0.

我们再看(#b) ,依然输出的是0,但b是有值的啊。

我们再看(#c),输出的是2,这个是怎么回事。这里就是之前在table类型的课程中已经介绍的获取table的长度, 只是获取的是 数组型的长度,不包含map型的。

我们再往下看 if a == nil then 在判断 a是否为nil,明显a不为nil if next(a) == nil then中的next是什么意思呢?

next (table [, index]) 功能:允许程序遍历表中的每一个字段,返回下一索引和该索引的值。 参数:table:要遍历的表    index:要返回的索引的前一索中的号,当index为nil[]时,将返回第一个索引的值, 当索引号为最后一个索引或表为空时将返回nil next(a) 就是返回第一个索引的值,a的第一个索引是没有值的,那么next(a) 就为nil 所以next方法经常用来判断 table中是否有值。 下面的语句相信大家就能看懂了。 综合以上代码,我们判断table是否为空,就不能简单的判断table长度是否为0,而是判断索引值。

所以要判断table是否为空应该按照以下进行判断

function isTableEmpty(t) return t == nil or next(t) == nil end

八)ipairs和pairs的区别

为了看出两者的区别,首先定义一个table: a={"Hello","World";a=1,b=2,z=3,x=10,y=20;"Good","Bye"}
for i, v in ipairs(a) do print(v) end 输出的结果是: Hello World Good Bye 可见ipairs并不会输出table中存储的键值对,会跳过键值对,然后按顺序输出table中的值。 再使用pairs对其进行遍历: for i, v in pairs(a) do print(v) end 输出的结果是: Hello World Good
Bye
1
10
2
20
3
可见pairs会输出table中的值和键值对,并且在输出的过程中先按顺序输出值,再乱序输出键值对。 这是因为table在存储值的时候是按照顺序的,但是在存储键值对的时候是按照键的哈希值存储的, 并不会按照键的字母顺序或是数字顺序存储。 对于a来说,如果执行print(a[3]),输出的结果也会是Good。也就是说table并不会给键值对一个索引值。

也就是说ipairs只是按照索引值顺序,打印出了table中有索引值的数据,没有索引值的不管。 而pairs是先按照数组索引值打印,打印完成后再按照哈希键值对的键的哈希值打印它的值。

LuaJIT 2.1 新增加的 table.new 和 table.clear 函数是非常有用的。 前者主要用来预分配 Lua table 空间,后者主要用来高效的释放 table 空间,并且它们都是可以被 JIT 编译的。

#25、lua变量

一)全局-局部变量

全局变量是指:这个变量在没有被同名局部变量覆盖的时候,所有代码块都是可见的。

局部变量是指:该变量只在被申明的代码块中可见,并且可以覆盖同名全局变量或者外层局部变量。

Lua 中的局部变量要用 local 关键字来显式定义,不使用 local 显式定义的变量就是全局变量:
g_var = 1         -- 全局变量
local l_var = 2   -- 局部变量

1)局部变量作用域

局部变量的生命周期是有限的,它的作用域仅限于声明它的块(block)。
一个块是一个控制结构的执行体、或者是一个函数的执行体再或者是一个程序块(chunk)。
我们可以通过下面这个例子来理解一下局部变量作用域的问题:

x = 10          -- 全局变量
local i = 1         -- 程序块中的局部变量 i

while i <=x do      -- 判断条件的x为全局变量的x
local x = i * 2   -- while 循环体中的局部变量 x
print(x)          -- output: 2, 4, 6, 8, ...
i = i + 1
end

print("while循环结束");

if i > 20 then
local x                 -- then 中的局部变量 x
x = 20
print(x + 2)  -- 如果i > 20 将会打印 22,此处的 x 是局部变量
else
print(x)          -- 打印 10,这里 x 是全局变量
end

print(x)            -- 打印 10

2)使用局部变量的好处

局部变量可以避免因为命名问题污染了全局环境
local 变量的访问比全局变量更快
由于局部变量出了作用域之后生命周期结束,这样可以被垃圾回收器及时释放
在生产环境中,我们应该尽可能用 局部变量。

3)全局变量,其实本质上也是一个table,它把我们创建的全局变量都保存到一个table里了。

而这个table的名字是:_G

-- 定义一个全局变量
gName = "我是个全局变量";
-- 用三种方式输出变量的值
print(gName);
print(_G["gName"]);
print(_G.gName);

二)虚变量

当一个方法返回多个值时,有些返回值有时候用不到,要是声明很多变量来一一接收,显然不太合适(不是不能)。
Lua 提供了一个虚变量,以单个下划线(“_”)来命名,用它来丢弃不需要的数值,仅仅起到占位的作用。

local start, finish = string.find("hello", "he") --start 值为起始下标,finish
--值为结束下标
print ( start, finish )                          --输出 1   2

local start = string.find("hello", "he")      -- start值为起始下标
print ( start )                               -- 输出 1

local _,finish = string.find("hello", "he")   --采用虚变量(即下划线),接收起
--始下标值,然后丢弃,finish接收
--结束下标值
print ( finish )                              --输出 2

代码倒数第二行,定义了一个用 local 修饰的 虚变量(即 单个下划线)。使用这个虚变量接收 string.find() 第一个返回值,静默丢掉,这样就直接得到第二个返回值了。

虚变量不仅仅可以被用在返回值,还可以用在迭代等。

local t = {1, 3, 5}

print("all  data:")
for i,v in ipairs(t) do
print(i,v)
end

print("")
print("part data:")
for _,v in ipairs(t) do
print(v)
end

执行结果:
# luajit test.lua
all  data:
1   1
2   3
3   5

part data:
1
3
5

#26、lua时间操作

在 Lua 中,函数 time、date 和 difftime 提供了所有的日期和时间功能。
在 OpenResty 的世界里,不推荐使用这里的标准时间函数,
因为这些函数通常会引发不止一个昂贵的系统调用,同时无法为 LuaJIT JIT 编译,对性能造成较大影响。
推荐使用 ngx_lua 模块提供的带缓存的时间接口,
如 ngx.today, ngx.time, ngx.utctime, ngx.localtime, ngx.now, ngx.http_time,以及 ngx.cookie_time 等。

一)os.time ([table])

它会返回当前的时间和日期的时间戳(精确到秒),如赋值table,表示此table指定日期的时间戳

字段名称 		取值范围
year 			四位数字
month 			1--12
day 			1--31
hour 			0--23
min 			0--59
sec 			0--59
isdst 			boolean(true表示夏令时)

对于 time 函数,如果参数为 table,那么 table 中必须含有 year、month、day 字段。
其他字缺省时段默认为中午(12:00:00)。
print(os.time())
a = { year = 2018, month = 1, day = 30, hour = 0, min = 0, sec = 0 }
print(os.time(a))

时间戳的是以计算机最小时间和指定时间之间相差的秒数,计算机最小时间为1970-1-1 00:00:00(美国时区),
针对中国时区就是1970-1-1 08:00:00
a = { year = 1970, month = 1, day = 1, hour = 8, min = 0, sec = 1 }
print(os.time(a))
输出的就是1秒

二)os.difftime (t2, t1)

返回 t1 到 t2 的时间差,单位为秒。

local day1 = { year = 2018, month = 1, day = 30 }
local t1 = os.time(day1)
local day2 = { year = 2018, month = 1, day = 31 }
local t2 = os.time(day2)
print(os.difftime(t2, t1))

--->output:86400

三)os.date ([format [, time]])

把一个表示日期和时间的数值,转换成更高级的表现形式。
格式字符 			含义
%a 					一星期中天数的简写(例如:Wed)
%A 					一星期中天数的全称(例如:Wednesday)
%b 					月份的简写(例如:Sep)
%B 					月份的全称(例如:September)
%c 					日期和时间(例如:07/30/15 16:57:24)
%d 					一个月中的第几天[01 ~ 31]
%H 					24小时制中的小时数[00 ~ 23]
%I 					12小时制中的小时数[01 ~ 12]
%j 					一年中的第几天[001 ~ 366]
%M 					分钟数[00 ~ 59]
%m 					月份数[01 ~ 12]
%p 					“上午(am)”或“下午(pm)”
%S 					秒数[00 ~ 59]
%w 					一星期中的第几天[0 ~ 6 = 星期天 ~ 星期六]
%x 					日期(例如:07/30/15)
%X 					时间(例如:16:57:24)
%y 					两位数的年份[00 ~ 99]
%Y 					完整的年份(例如:2015)
%% 					字符'%'

print(os.date("today is %A, in %B"))
print(os.date("now is %x %X"))
print(os.date("%Y-%m-%d %H:%M:%S"))

-->output
today is Thursday, in July
now is 07/30/15 17:39:22
2018-03-29 22:36:05

---------------------------

t = os.date("*t", os.time());
for i, v in pairs(t) do
print(i, v);
end

yday    120  --一年中的第几天,一月一日为1
month   4
sec     9
min     9
hour    16
day     30
year    2018
isdst   false  --是否夏令时
wday    2   --一周第几天  星期日为1

#27、lua模块

从lua5.1开始,Lua 加入了标准的模块管理机制,Lua 的模块是由变量、函数等已知元素组成的 table,
因此创建一个模块很简单,就是创建一个 table,然后把需要导出的常量、函数放入其中,最后返回这个 table 就行。

一)模块定义

模块的文件名 和 模块定义引用名称要一致

-- 文件名为 model.lua
-- 定义一个名为 model 的模块
model = {}

-- 定义一个常量
model.constant = "这是一个常量"

-- 定义一个函数
function model.func1()
print("这是一个公有函数")
end

local function func2()
print("这是一个私有函数!")
end

function model.func3()
func2()
end

return model

二)require 函数 Lua提供了一个名为require的函数用来加载模块。要加载一个模块,只需要简单地调用就可以了。例如:

require("<模块名>") 或者 require "<模块名>"

执行 require 后会返回一个由模块常量或函数组成的 table,并且还会定义一个包含该 table 的全局变量。

-- test_model.lua 文件 -- model 模块为上文提到 model.lua (目前直接这样引用会报错,看下面require加载机制)

require("model")

print(model.constant)

model.func3()

另一种写法,给加载的模块定义一个别名变量,方便调用

local m = require("model")

print(m.constant)

m.func3()

以上代码执行结果为:

这是一个常量 这是一个私有函数!

如:模块定义的model,为local修饰为局部变量,那只能采用local m = require("model") 引用

三)require 加载机制

我们使用require命令时,系统需要知道引入哪个路径下的model.lua文件。

require 用于搜索 Lua 文件的路径是存放在全局变量 package.path 中,

当 Lua 启动后,会以环境变量 LUA_PATH 的值来初始这个环境变量。 如果没有找到该环境变量,则使用一个编译时定义的默认路径来初始化。

lua文件的路径存放在全局变量package.path中,默认的package.path的值为 print(package.path) ./?.lua;/usr/local/openresty/luajit/share/luajit-2.1.0-beta3/?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/openresty/luajit/share/lua/5.1/?.lua;/usr/local/openresty/luajit/share/lua/5.1/?/init.lua

我们运行require("model");相当于把model替换上面的?号,lua就会在那些目录下面寻找model.lua如果找不到就报错。 所以我们就知道为什么会报错了。

那我们如何解决,我这里介绍常用的解决方案,编辑环境变量LUA_PATH 在当前用户根目录下打开 .profile 文件(没有则创建,打开 .bashrc 文件也可以), 例如把 "~/lua/" 路径加入 LUA_PATH 环境变量里:

vim /etc/profile

#LUA_PATH export LUA_PATH="/usr/local/openresty/lua/study/?.lua;;"

文件路径以 ";" 号分隔,最后的 2 个 ";;" 表示新加的路径后面加上原来的默认路径。

接着,更新环境变量参数,使之立即生效。

source /etc/profile

这时假设 package.path 的值是:

/usr/local/lua/?.lua;./?.lua;/usr/local/openresty/luajit/share/luajit-2.1.0-beta3/?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/openresty/luajit/share/lua/5.1/?.lua;/usr/local/openresty/luajit/share/lua/5.1/?/init.lua

那么调用 require("model") 时就会尝试打开以下文件目录去搜索目标。

#28、lua元表

举个例子,在 Lua table 中我们可以访问对应的key来得到value值,但是却无法对两个 table 进行操作。

那如何计算两个table的相加操作a+b?

local t1 = {1,2,3}
local t2 = {4,5,6}

local t3 = t1 + t2   ---->  {1,2,3,4,5,6}

类似java的一些操作重载

这种类似的需求,lua 提供了元表(Metatable)和元方法,允许我们改变table的行为,每个行为关联了对应的元方法。

1)setmetatable(table,metatable)

对指定table设置元表(metatable),
如果元表(metatable)中存在__metatable键值,setmetatable会失败 。

2)getmetatable(table)

返回对象的元表(metatable)。

mytable = {}                          -- 普通表
mymetatable = {}                      -- 元表
setmetatable(mytable,mymetatable)     -- 把 mymetatable 设为 mytable 的元表

等价于:
mytable = setmetatable({},{})

返回对象元表:
getmetatable(mytable)                 -- 返回mymetatable

元方法的命名都是以 __ 两个下划线开头。

一)__index 元方法

对表读取索引一个元方法

这是 metatable 最常用的键。

当你通过键来访问 table 的时候,如果这个键没有值,那么Lua就会寻找该table的metatable(假定有metatable)
中的__index元方法。如果__index是一个表,Lua会在表格中查找相应的键。

local t = {}              -- 普通表t为空
local other = {foo = 2}   -- 元表 中有foo值
setmetatable(t,{__index = other})     -- 把 other 设为 t 的元表__index
print(t.foo);  ---输出 2
print(t.bar);  ---输出nil

t.foo为什么会输出2,就是因为我们重写了__index索引的重载,lua在执行中如果t中没有foo,
就会在他的元表中__index中去找,__index等于other,就输出2。

----------------

如果我们把t设置一下foo的值为3,在看看结果
local t = {foo = 3 }                  -- 普通表t为空
local other = {foo = 2}               -- 元表 中有foo值
setmetatable(t,{__index = other})     -- 把 other 设为 t 的元表__index
print(t.foo);  ---输出 3
print(t.bar);  ---输出nil

---------------------------------------------

如果__index赋值为一个函数的话,Lua就会调用那个函数,table和键会作为参数传递给函数。
__index 元方法查看表中元素是否存在,如果不存在,返回结果为 nil;如果存在则由 __index 返回结果。

local t = {key1 = "value1" }
local function metatable(mytable,key)
if key == "key2" then
return "metatablevalue"
else
return nil
end
end
setmetatable(t,{__index = metatable})
print(t.key1);
print(t.key2);
print(t.key3);

分析:

print(t.key1);  ---这个输出value1 ,是因为t表中有此key
print(t.key2);  ---这个输出metatablevalue,是因为t表中没有此key,就会调用t的元表中的元方法__index,
---这是__index是个函数,就会执行这个函数,传t表和key值这两个参数到此函数,
---函数体中判断有此key2 就输出metatablevalue;

print(t.key3);  ---这个输出nil,是因为t表没有,元方法__index函数中 对key3返回nil值

--------------------------------------

总结:lua对表索引取值的步骤

Lua查找一个表元素时的规则,其实就是如下3个步骤:
1.在表中查找,如果找到,返回该元素,找不到则继续步骤2
2.判断该表是否有元表,如果没有元表,返回nil,有元表则继续步骤3。
3.判断元表有没有__index元方法,如果__index方法为nil,则返回nil;
如果__index方法是一个表,则重复1、2、3;
如果__index方法是一个函数,则执行该函数,得到返回值。

二)__newindex 元方法

__newindex 元方法用来对表更新,__index则用来对表访问 。

当你给表进行索引进行赋值,但此索引不存在;lua会查找是否有元表,有元表就会查找__newindex 元方法是否存在:
如果存在则调用__newindex的值进行执行操作,但不会对原表进行赋值操作。

以下实例演示了 __newindex 元方法的应用:

mymetatable = {}
mytable = setmetatable({key1 = "value1"}, { __newindex = mymetatable })

print("mytable.key1=",mytable.key1)   --value1
mytable.newkey = "新值2"

print("mytable.newkey=",mytable.newkey)  --nil
print("mymetatable.newkey=",mymetatable.newkey) --"新值2"

mytable.key1 = "新值1"
print("mytable.key1=",mytable.key1)  -- 新值1
print("mymetatable.key1=",mymetatable.key1)  --nil

以上实例中表设置了元方法 __newindex
在对新索引键(newkey)赋值时(mytable.newkey = "新值2"),会调用元方法,而对mytable原表不进行赋值。
而对mytable已存在的索引键(key1),则会进行赋值,而不调用元方法 __newindex。

----------------------------

如果我们要对原来的table进行赋值,那我们就可以用rawset;;__newindex函数会传三个参数,
mytable = setmetatable({key1 = "value1"}, {
__newindex = function(t,k,v)    ---第一个参数为table,第二个参数为key,第三个参数为value
rawset(t,k,v);
end
})
mytable.key1 = "new value"
mytable.key2 = 4
print("mytable.key1=",mytable.key1)
print("mytable.key2=",mytable.key2)

key2原本是不在mytable表中的,通过元方法__newindex中函数使用了rawset,就可以对原table进行赋值。

三)为表添加操作符“+”

我们这里定义“+”这元方法,把它定义为两个table相连
如
t1={1,2,3}
t2={4,5,6}
t1 + t2 相加的结果,我们想得到的是 {1,2,3,4,5,6}
那我们如何写元表?
“+”对应的元方法为__add

local function add(mytable,newtable)
local num = table.maxn(newtable)
for i = 1, num do
table.insert(mytable,newtable[i])
end
return mytable
end

local t1 = {1,2,3}
local t2 = {4,5,6}

setmetatable(t1,{__add = add})

t1 = t1 + t2

for k,v in ipairs(t1) do
print("key=",k," value=",v)
end

这样我们就实现了两个table相加

以下是我们的操作符对应关系
模式                    描述
__add                 对应的运算符 '+'.
__sub                 对应的运算符 '-'.
__mul                 对应的运算符 '*'.
__div                 对应的运算符 '/'.
__mod                 对应的运算符 '%'.
__unm                 对应的运算符 '-'.
__concat              对应的运算符 '..'.
__eq                  对应的运算符 '=='.
__lt                  对应的运算符 '<'.
__le                  对应的运算符 '<='.

四)__call元方法

__call元方法在 Lua 调用一个值时调用。以下实例演示了计算两个表中所有值相加的和:

类似的 t();类似函数调用

local function call(mytable,newtable)
local sum = 0
local i
for i = 1, table.maxn(mytable) do
sum = sum + mytable[i]
end
for i = 1, table.maxn(newtable) do
sum = sum + newtable[i]
end
return sum
end
local t1 = {1,2,3}
local t2 = {4,5,6}
setmetatable(t1,{__call = call})

local sum = t1(t2)
print(sum)

五)__tostring 元方法

__tostring 元方法用于修改表的输出行为。以下实例我们自定义了表的输出内容,把表中所有的元素相加输出:
local t1 = {1,2,3}

setmetatable(t1,{
__tostring = function(mytable)
local sum = 0
for k, v in pairs(mytable) do
sum = sum + v
end
return "all value sum =" .. sum

end
})
print(t1)    ----print方法会调用table的tostring元方法

到此我们的元表 和 元方法 就讲完了,这个是需要大家自己动手去测试体验的。要有领悟能力

六)点号与冒号操作符的区别

local str = "abcde"
print("case 1:", str:sub(1, 2))
print("case 2:", str.sub(str, 1, 2))

执行结果
case 1: ab
case 2: ab

冒号操作会带入一个 self 参数,用来代表 自己。而点号操作,只是 内容 的展开。
在函数定义时,使用冒号将默认接收一个 self 参数,而使用点号则需要显式传入 self 参数。
obj = { x = 20 }
function obj:fun1()
print(self.x)
end
等价于
obj = { x = 20 }
function obj.fun1(self)
print(self.x)
end
注意:冒号的操作,只有当变量是类对象时才需要。

#29、lua面向对象

面向对象编程(Object Oriented Programming,OOP)是一种非常流行的计算机编程架构。
java,c++,.net等都支持面向对象

面向对象特征
1) 封装:指能够把一个实体的信息、功能、响应都装入一个单独的对象中的特性。
2) 继承:继承的方法允许在不改动原程序的基础上对其进行扩充,这样使得原功能得以保存,
而新功能也得以扩展。这有利于减少重复编码,提高软件的开发效率。
3) 多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在运行时,
可以通过指向基类的指针,来调用实现派生类中的方法。
4) 抽象:抽象(Abstraction)是简化复杂的现实问题的途径,它可以为具体问题找到最恰当的类定义,
并且可以在最恰当的继承级别解释问题。

一)Lua 中面向对象

对象由属性和方法组成,lua中的面向对象是用table来描述对象的属性,function表示方法;
LUA中的类可以通过table + function模拟出来。

一个简单实例
以下简单的类代表矩形类,包含了二个属性:length 和 width;getArea方法用获取面积大小

新建rect.lua脚本

local rect = {length = 0, width = 0}

-- 派生类的方法 new
function rect:new (length,width)
local o = {
--设定各个项的值
length = length or 0,
width = width or 0
}
setmetatable(o, {__index = self})
return o
end

-- 派生类的方法 getArea
function rect:getArea ()
return self.length * self.width
end

return rect

-----------------test.lua------------------------

引用模块
local rect = require("rect")

创建对象
创建对象是为类的实例分配内存的过程。每个类都有属于自己的内存并共享公共数据。

local rect1 = rect:new(10,20)
local rect2 = rect:new(5,6)

访问属性  ----》 rect1.length
访问成员函数  ----》rect1:getArea()

print("长度1:",rect1.length);
print("面积1:",rect1:getArea());  --- 点号 和 冒号 调用前面课程已经介绍
print("长度2:",rect2.length);
print("面积2:",rect2:getArea());

二)Lua继承

-------------------shape基类---------------------------

local shape = {name = ""}

-- 创建实体对象方法 new
function shape:new (name)
local o = {
name = name or "shape"
}
setmetatable(o, {__index = self})
return o
end

-- 获取周长的方法 getPerimeter
function shape:getPerimeter ()
print("getPerimeter in shape");
return 0
end

-- 获取面积的方法 getArea
function shape:getArea ()
print("getArea in shape");
return 0
end

function shape:getVum ()
print("getVum in shape");
return 0
end

return shape

----------------------triangle继承类----------------------------

local shape = require("shape")

local triangle = {}

-- 派生类的方法 new
function triangle:new (name,a1,a2,a3)
local obj = shape:new(name);

--1)当方法在子类中查询不到时,再去父类中去查找。
local super_mt = getmetatable(obj);
setmetatable(self, super_mt);

--2)把父类的元表 赋值super对象
obj.super = setmetatable({}, super_mt)

--3)属性赋值
obj.a1 = a1 or 0;
obj.a2 = a2 or 0;
obj.a3 = a3 or 0;

setmetatable(obj, { __index = self })

return obj;
end

-- 派生类的方法 getPerimeter
function triangle:getPerimeter()
print("getPerimeter in triangle");
return (self.a1 + self.a2 + self.a3);
end

-- 派生类的方法 getHalfPerimeter
function triangle:getHalfPerimeter()
print("getHalfPerimeter in triangle");
return (self.a1 + self.a2 + self.a3) / 2
end

return triangle

------------------test-----------------------

local rect = require("rect")
--local shape = require("shape")
local triangle = require("triangle")

local rect1 = rect:new(10,20)
local rect2 = rect:new(5,6)

--local shape1 = shape:new();

local triangle1 = triangle:new("t1",1,2,3)
local triangle2 = triangle:new("t2",6,7,8)

print("长度1:",rect1.length);
print("面积1:",rect1:getArea());
print("===============");
print("长度2:",rect2.length);
print("面积2:",rect2:getArea());
print("===============");
--print("shape1 getPerimeter:",shape1:getPerimeter())
print("===============");
----覆盖了shape的方法
print("t1 getPerimeter:",triangle1:getPerimeter())
print("t1 getHalfPerimeter:",triangle1:getHalfPerimeter())

print("t2 getPerimeter:",triangle2:getPerimeter())
print("t2 getHalfPerimeter:",triangle2:getHalfPerimeter())

---- 从shape继承的getVum方法
print("t1 getVum:",triangle1:getVum())
print("t2 getVum:",triangle2:getVum())
print("===============");
print("t2 super getPerimeter:",triangle2.super:getPerimeter())

#30、openresty中使用lua

openresty 引入 lua

一)openresty中nginx引入lua方式

1)xxx_by_lua --->字符串编写方式 2) xxx_by_lua_block ---->代码块方式 3) xxx_by_lua_file ---->直接引用一个lua脚本文件

我们案例中使用内容处理阶段,用content_by_lua演示

-----------------编辑nginx.conf-----------------------

第一种:content_by_lua

location /testlua { content_by_lua "ngx.say('hello world')"; }

输出了hello world

content_by_lua 方式,参数为字符串,编写不是太方便。

第二种:content_by_lua_block location /testlua { content_by_lua_block { ngx.say("hello world"); } }

content_by_lua_block 表示内部为lua块,里面可以应用lua语句

第三种:content_by_lua_file

location /testlua { content_by_lua_file /usr/local/lua/test.lua; }

content_by_lua_file 就是引用外部lua文件

vi test.lua

ngx.say("hello world");

二)openresty使用lua打印输出案例

location /testsay { content_by_lua_block { --写响应头
ngx.header.a = "1"
ngx.header.b = "2" --输出响应
ngx.say("a", "b", "
")
ngx.print("c", "d", "
")
--200状态码退出
return ngx.exit(200) } }

ngx.header:输出响应头; ngx.print:输出响应内容体; ngx.say:通ngx.print,但是会最后输出一个换行符; ngx.exit:指定状态码退出。

三)介绍一下openresty使用lua常用的api

1)ngx.var : 获取Nginx变量 和 内置变量

nginx内置的变量

$arg_name 请求中的name参数 $args 请求中的参数 $binary_remote_addr 远程地址的二进制表示 $body_bytes_sent 已发送的消息体字节数 $content_length HTTP请求信息里的"Content-Length" $content_type 请求信息里的"Content-Type" $document_root 针对当前请求的根路径设置值 $document_uri 与$uri相同; 比如 /test2/test.php $host 请求信息中的"Host",如果请求中没有Host行,则等于设置的服务器名 $hostname 机器名使用 gethostname系统调用的值 $http_cookie cookie 信息 $http_referer 引用地址 $http_user_agent 客户端代理信息 $http_via 最后一个访问服务器的Ip地址。 $http_x_forwarded_for 相当于网络访问路径 $is_args 如果请求行带有参数,返回“?”,否则返回空字符串 $limit_rate 对连接速率的限制 $nginx_version 当前运行的nginx版本号 $pid worker进程的PID $query_string 与$args相同 $realpath_root 按root指令或alias指令算出的当前请求的绝对路径。其中的符号链接都会解析成真是文件路径 $remote_addr 客户端IP地址 $remote_port 客户端端口号 $remote_user 客户端用户名,认证用 $request 用户请求 $request_body 这个变量(0.7.58+)包含请求的主要信息。在使用proxy_pass或fastcgi_pass指令的location中比较有意义 $request_body_file 客户端请求主体信息的临时文件名 $request_completion 如果请求成功,设为"OK";如果请求未完成或者不是一系列请求中最后一部分则设为空 $request_filename 当前请求的文件路径名,比如/opt/nginx/www/test.php $request_method 请求的方法,比如"GET"、"POST"等 $request_uri 请求的URI,带参数; 比如http://localhost:88/test1/ $scheme 所用的协议,比如http或者是https $server_addr 服务器地址,如果没有用listen指明服务器地址,使用这个变量将发起一次系统调用以取得地址(造成资源浪费) $server_name 请求到达的服务器名 $server_port 请求到达的服务器端口号 $server_protocol 请求的协议版本,"HTTP/1.0"或"HTTP/1.1" $uri 请求的URI,可能和最初的值有不同,比如经过重定向之类的

ngx.var.xxx

location /var { set $c 3;

#处理业务
content_by_lua_block {
local a = tonumber(ngx.var.arg_a) or 0
local b = tonumber(ngx.var.arg_b) or 0
local c = tonumber(ngx.var.c) or 0
ngx.say("sum:", a + b + c )
}

}

注意:ngx.var.c 此变量必须提前声明; 另外对于nginx location中使用正则捕获的捕获组可以使用ngx.var[捕获组数字]获取;

location ~ ^/var/([0-9]+) { content_by_lua_block { ngx.say("var[1]:", ngx.var[1] ) } }

2)ngx.req请求模块的常用api

ngx.req.get_headers:获取请求头, 获取带中划线的请求头时请使用如headers.user_agent这种方式;如果一个请求头有多个值,则返回的是table;

-----------test.lua-------------------

local headers = ngx.req.get_headers()
ngx.say("headers begin===", "
")
ngx.say("Host : ", headers["Host"], "
")
ngx.say("headers['user-agent'] : ", headers["user-agent"], "
")
ngx.say("headers.user_agent : ", headers.user_agent, "
") ngx.say("-------------遍历headers-----------", "
") for k,v in pairs(headers) do
if type(v) == "table" then
ngx.say(k, " : ", table.concat(v, ","), "
")
else
ngx.say(k, " : ", v, "
")
end
end
ngx.say("headers end=", "
")
ngx.say("
")

3)获取请求参数 ngx.req.get_uri_args:获取url请求参数,其用法和get_headers类似; ngx.req.get_post_args:获取post请求内容体,其用法和get_headers类似, 但是必须提前调用ngx.req.read_body()来读取body体 (也可以选择在nginx配置文件使用lua_need_request_body on;开启读取body体, 但是官方不推荐);

ngx.req.get_body_data:为解析的请求body体内容字符串。

---------------test.lua---------------

--get请求uri参数
ngx.say("uri get args begin=======", "
")
local uri_args = ngx.req.get_uri_args()
for k, v in pairs(uri_args) do
if type(v) == "table" then
ngx.say(k, " : ", table.concat(v, ", "), "
")
else
ngx.say(k, ": ", v, "
")
end
end
ngx.say("uri get args end=======", "
")

--post请求参数
ngx.req.read_body()
ngx.say("post args begin===", "
")
local post_args = ngx.req.get_post_args()
for k, v in pairs(post_args) do
if type(v) == "table" then
ngx.say(k, " : ", table.concat(v, ", "), "
")
else
ngx.say(k, ": ", v, "
")
end
end
ngx.say("post args end=====", "
")

  1. ngx.req其他常用的api --请求的http协议版本
    ngx.say("ngx.req.http_version : ", ngx.req.http_version(), "
    ")
    --请求方法
    ngx.say("ngx.req.get_method : ", ngx.req.get_method(), "
    ")
    --原始的请求头内容
    ngx.say("ngx.req.raw_header : ", ngx.req.raw_header(), "
    ")
    --请求的body内容体
    ngx.say("ngx.req.get_body_data() : ", ngx.req.get_body_data(), "
    ")
    ngx.say("
    ")

ngx.req.raw_header()这个函数返回值为字符串

5)编码解码

ngx.escape_uri/ngx.unescape_uri : uri编码解码;

ngx.encode_args/ngx.decode_args:参数编码解码;

ngx.encode_base64/ngx.decode_base64:BASE64编码解码;

-------test.lua

--未经解码的请求uri
local request_uri = ngx.var.request_uri;
ngx.say("request_uri : ", request_uri, "
");

--编码 local escape_uri = ngx.escape_uri(request_uri) ngx.say("escape_uri : ", escape_uri, "
");

--解码
ngx.say("decode request_uri : ", ngx.unescape_uri(escape_uri), "
");

--参数编码 local request_uri = ngx.var.request_uri; local question_pos, _ = string.find(request_uri, '?') if question_pos>0 then local uri = string.sub(request_uri, 1, question_pos-1) ngx.say("uri sub=",string.sub(request_uri, question_pos+1),"
");

--对字符串进行解码 local args = ngx.decode_args(string.sub(request_uri, question_pos+1))

for k,v in pairs(args) do ngx.say("k=",k,",v=", v, "
"); end

if args and args.userId then args.userId = args.userId + 10000 ngx.say("args+10000 : ", uri .. '?' .. ngx.encode_args(args), "
"); end end

6)md5加密api --MD5
ngx.say("ngx.md5 : ", ngx.md5("123"), "
")

7)nginx获取时间

之前介绍的os.time()会涉及系统调用,性能比较差,推荐使用nginx中的时间api

ngx.time() --返回秒级精度的时间戳 ngx.now() --返回毫秒级精度的时间戳

就是通过这两种方式获取到的只是nginx缓存起来的时间戳,不是实时的。 所以有时候会出现一些比较奇怪的现象,比如下面代码:

local t1 = ngx.now() for i=1,1000000 do end local t2 = ngx.now() print(t1, ",", t2) -- t1和t2的值是一样的,why? ngx.exit(200)

正常来说,t2应该大于t1才对,但由于nginx没有及时更新(缓存的)时间戳,所以导致t2和t1获取到的时间戳是一样的。 那么怎样才能强迫nginx更新缓存呢?调用多一个ngx.update_time()函数即可:

local t1 = ngx.now() for i=1,1000000 do end ngx.update_time() local t2 = ngx.now() print(t1, ",", t2) ngx.exit(200)

8)ngx.re模块中正则表达式相关的api

ngx.re.match ngx.re.sub ngx.re.gsub ngx.re.find ngx.re.gmatch

我们这里只简单的介绍 ngx.re.match,详细用法可以自行去网上学习

ngx.re.match 只有第一次匹配的结果被返回,如果没有匹配,则返回nil;或者匹配过程中出现错误时, 也会返回nil,此时错误信息会被保存在err中。

当匹配的字符串找到时,一个Lua table captures会被返回, captures[0]中保存的就是匹配到的字串, captures[1]保存的是用括号括起来的第一个子模式(捕获分组)的结果, captures[2]保存的是第二个子模式(捕获分组)的结果,依次类似。

local m, err = ngx.re.match("hello, 1234", "[0-9]+") if m then ngx.say(m[0]) else if err then ngx.log(ngx.ERR, "error: ", err) return end

ngx.say("match not found") end

上面例子中,匹配的字符串是1234,因此m[0] == "1234",

local m, err = ngx.re.match("hello, 1234", "([0-9])[0-9]+") ngx.say(m[0],"
") ngx.say(m[1])

备注:有没有注意到,我们每次修改都要重启nginx,这样太过于麻烦,我们可以用 content_by_lua_file 引入外部lua,这样的话 只要修改外部的lua,就可以了,不需要重启nginx了。 注意需要把lua_code_cache 设置为off

语法:lua_code_cache on | off 默认: on 适用上下文:http、server、location、location if 这个指令是指定是否开启lua的代码编译缓存,开发时可以设置为off,以便lua文件实时生效, 如果是生产线上,为了性能,建议开启。 最终nginx.conf修改为

以后我们只要修改test.lua 文件就可以了。

9)标准日志输出

ngx.log(log_level, ...)

日志输出级别

ngx.STDERR -- 标准输出 ngx.EMERG -- 紧急报错 ngx.ALERT -- 报警 ngx.CRIT -- 严重,系统故障,触发运维告警系统 ngx.ERR -- 错误,业务不可恢复性错误 ngx.WARN -- 告警,业务中可忽略错误 ngx.NOTICE -- 提醒,业务比较重要信息 ngx.INFO -- 信息,业务琐碎日志信息,包含不同情况判断等 ngx.DEBUG -- 调试

#user nobody; worker_processes 1;

error_log logs/error.log error; # 日志级别 #pid logs/nginx.pid;

events { worker_connections 1024; }

http { server { listen 80; location / { content_by_lua_block { local num = 55 local str = "string" local obj ngx.log(ngx.ERR, "num:", num) ngx.log(ngx.INFO, " string:", str) print([[i am print]]) ngx.log(ngx.ERR, " object:", obj) } } } }

日志输出级别使用的 error,只有等于或大于这个级别的日志才会输出

ngx.DEBUG ngx.WARN

对于应用开发,一般使用 ngx.INFO 到 ngx.CRIT 就够了。生产中错误日志开启到 error 级别就够了

10)重定向 ngx.redirect

-----重定向

location = /bar { content_by_lua_block { ngx.say([[I am bar]]) } }

location = /foo { rewrite_by_lua_block { return ngx.redirect('/bar'); } }

11)不同阶段共享变量

ngx.ctx 全局共享变量

在 OpenResty 的体系中,可以通过共享内存的方式完成不同工作进程的数据共享,
本地内存方式 去让不同的工作进程共享数据

openresty有不同处理阶段,后面的课程会介绍。在不同的处理阶段,如何共享数据

可以通过 Lua 模块方式完成单个进程内不同请求的数据共享。如何完成单个请求内不同阶段的数据共享呢?

ngx.ctx 表就是为了解决这类问题而设计的。参考下面例子:

location /test {
rewrite_by_lua_block {
ngx.ctx.foo = 76
}
access_by_lua_block {
ngx.ctx.foo = ngx.ctx.foo + 3
}
content_by_lua_block {
ngx.say(ngx.ctx.foo)
}
}

ngx.ctx.xxxxx

首先 ngx.ctx 是一个表,所以我们可以对他添加、修改。它用来存储基于请求的 Lua 环境数据,
其生存周期与当前请求相同 (类似 Nginx 变量)。它有一个最重要的特性:
单个请求内的 rewrite (重写),access (访问),和 content (内容) 等各处理阶段是保持一致的。

额外注意,每个请求,包括子请求,都有一份自己的 ngx.ctx 表。例如:

location /sub {
content_by_lua_block {
ngx.say("sub pre: ", ngx.ctx.blah)
ngx.ctx.blah = 32
ngx.say("sub post: ", ngx.ctx.blah)
}
}

location /main {
content_by_lua_block {
ngx.ctx.blah = 73
ngx.say("main pre: ", ngx.ctx.blah)
local res = ngx.location.capture("/sub")
ngx.print(res.body)
ngx.say("main post: ", ngx.ctx.blah)
}
}

ngx.ctx 表查询需要相对昂贵的元方法调用,这比通过用户自己的函数参数直接传递基于请求的数据要慢得多。 所以不要为了节约用户函数参数而滥用此 API,因为它可能对性能有明显影响。

由于 ngx.ctx 保存的是指定请求资源,所以这个变量是不能直接共享给其他请求使用的。

更多api使用 https://www.nginx.com/resources/wiki/modules/lua/#nginx-api-for-lua

操作指令  说明
ngx.arg 指令参数,如跟在content_by_lua_file后面的参数
ngx.var 变量,ngx.var.VARIABLE引用某个变量
ngx.ctx 请求的lua上下文
ngx.header  响应头,ngx.header.HEADER引用某个头
ngx.status  响应码

API 说明
ngx.log 输出到error.log
print 等价于 ngx.log(ngx.NOTICE, ...)
ngx.send_headers  发送响应头
ngx.headers_sent  响应头是否已发送
ngx.resp.get_headers  获取响应头
ngx.timer.at  注册定时器事件
ngx.is_subrequest 当前请求是否是子请求
ngx.location.capture  发布一个子请求
ngx.location.capture_multi  发布多个子请求
ngx.exec
ngx.redirect
ngx.print 输出响应
ngx.say 输出响应,自动添加'n'
ngx.flush 刷新响应
ngx.exit  结束请求
ngx.eof
ngx.sleep 无阻塞的休眠(使用定时器实现)
ngx.get_phase
ngx.on_abort  注册client断开请求时的回调函数
ndk.set_var.DIRECTIVE
ngx.req.start_time  请求的开始时间
ngx.req.http_version  请求的HTTP版本号
ngx.req.raw_header  请求头(包括请求行)
ngx.req.get_method  请求方法
ngx.req.set_method  请求方法重载
ngx.req.set_uri 请求URL重写
ngx.req.set_uri_args
ngx.req.get_uri_args  获取请求参数
ngx.req.get_post_args 获取请求表单
ngx.req.get_headers 获取请求头
ngx.req.set_header
ngx.req.clear_header
ngx.req.read_body 读取请求体
ngx.req.discard_body  扔掉请求体
ngx.req.get_body_data
ngx.req.get_body_file
ngx.req.set_body_data
ngx.req.set_body_file
ngx.req.init_body
ngx.req.append_body
ngx.req.finish_body
ngx.req.socket
ngx.escape_uri  字符串的url编码
ngx.unescape_uri  字符串url解码
ngx.encode_args 将table编码为一个参数字符串
ngx.decode_args 将参数字符串编码为一个table
ngx.encode_base64 字符串的base64编码
ngx.decode_base64 字符串的base64解码
ngx.crc32_short 字符串的crs32_short哈希
ngx.crc32_long  字符串的crs32_long哈希
ngx.hmac_sha1 字符串的hmac_sha1哈希
ngx.md5 返回16进制MD5
ngx.md5_bin 返回2进制MD5
ngx.sha1_bin  返回2进制sha1哈希值
ngx.quote_sql_str SQL语句转义
ngx.today 返回当前日期
ngx.time  返回UNIX时间戳
ngx.now 返回当前时间
ngx.update_time 刷新时间后再返回
ngx.localtime
ngx.utctime
ngx.cookie_time 返回的时间可用于cookie值
ngx.http_time 返回的时间可用于HTTP头
ngx.parse_http_time 解析HTTP头的时间
ngx.re.match
ngx.re.find
ngx.re.gmatch
ngx.re.sub
ngx.re.gsub
ngx.shared.DICT
ngx.shared.DICT.get
ngx.shared.DICT.get_stale
ngx.shared.DICT.set
ngx.shared.DICT.safe_set
ngx.shared.DICT.add
ngx.shared.DICT.safe_add
ngx.shared.DICT.replace
ngx.shared.DICT.delete
ngx.shared.DICT.incr
ngx.shared.DICT.flush_all
ngx.shared.DICT.flush_expired
ngx.shared.DICT.get_keys
ngx.socket.udp
udpsock:setpeername
udpsock:send
udpsock:receive
udpsock:close
udpsock:settimeout
ngx.socket.tcp
tcpsock:connect
tcpsock:sslhandshake
tcpsock:send
tcpsock:receive
tcpsock:receiveuntil
tcpsock:close
tcpsock:settimeout
tcpsock:setoption
tcpsock:setkeepalive
tcpsock:getreusedtimes
ngx.socket.connect
ngx.thread.spawn
ngx.thread.wait
ngx.thread.kill
coroutine.create
coroutine.resume
coroutine.yield
coroutine.wrap
coroutine.running
coroutine.status
ngx.config.debug  编译时是否有 --with-debug选项
ngx.config.prefix 编译时的 --prefix选项
ngx.config.nginx_version  返回nginx版本号
ngx.config.nginx_configure  返回编译时 ./configure的命令行选项
ngx.config.ngx_lua_version  返回ngx_lua模块版本号
ngx.worker.exiting  当前worker进程是否正在关闭(如reload、shutdown期间)
ngx.worker.pid  返回当前worker进程的pid

常量说明
ngx.OK (0)
ngx.ERROR (-1)
ngx.AGAIN (-2)
ngx.DONE (-4)
ngx.DECLINED (-5)
ngx.nil

HTTP 请求方式
ngx.HTTP_GET
ngx.HTTP_HEAD
ngx.HTTP_PUT
ngx.HTTP_POST
ngx.HTTP_DELETE
ngx.HTTP_OPTIONS
ngx.HTTP_MKCOL
ngx.HTTP_COPY
ngx.HTTP_MOVE
ngx.HTTP_PROPFIND
ngx.HTTP_PROPPATCH
ngx.HTTP_LOCK
ngx.HTTP_UNLOCK
ngx.HTTP_PATCH
ngx.HTTP_TRACE

HTTP 返回状态
ngx.HTTP_OK (200)
ngx.HTTP_CREATED (201)
ngx.HTTP_SPECIAL_RESPONSE (300)
ngx.HTTP_MOVED_PERMANENTLY (301)
ngx.HTTP_MOVED_TEMPORARILY (302)
ngx.HTTP_SEE_OTHER (303)
ngx.HTTP_NOT_MODIFIED (304)
ngx.HTTP_BAD_REQUEST (400)
ngx.HTTP_UNAUTHORIZED (401)
ngx.HTTP_FORBIDDEN (403)
ngx.HTTP_NOT_FOUND (404)
ngx.HTTP_NOT_ALLOWED (405)
ngx.HTTP_GONE (410)
ngx.HTTP_INTERNAL_SERVER_ERROR (500)
ngx.HTTP_METHOD_NOT_IMPLEMENTED (501)
ngx.HTTP_SERVICE_UNAVAILABLE (503)
ngx.HTTP_GATEWAY_TIMEOUT (504)

#31、openresty中使用json模块

web开发过程中,经常用的数据结构为json,openresty中封装了json模块,我们看如何使用

一)如何引入cjson模块,需要使用require

local json = require("cjson")

json.encode 将表格数据编码为 JSON 字符串
格式:
jsonString = json.encode(表格对象)
用法示例:

table 包含哈希键值对 和 数组键值对

-------------------test.lua--------------

1)table包含哈希键值对时,数组键值将被转换为字符串键值

local json = require("cjson")
local t = {1,3,name="张三",age="19",address={"地址1","地址2"},sex="女"}
ngx.say(json.encode(t));
ngx.say("<br/>");
----{"1":1,"2":3,"sex":"女","age":"19","address":["地址1","地址2"],"name":"张三"}

local str = json.encode({a=1,[5]=3})
ngx.say(str); ----- {"a":1,"5":3}
ngx.say("<br/>");

2)table所有键为数组型键值对时,会当作数组看待,空位将转化为null

local str = json.encode({[3]=1,[5]=2,[6]="3",[7]=4})
ngx.say(str); ---- [null,null,1,null,2,"3",4]
ngx.say("<br/>");

local str = json.encode({[3]=2,[5]=3})
ngx.say(str); ---- [null,null,2,null,3]
ngx.say("<br/>");
json.decode 将JSON 字符串解码为表格对象
格式:
table = json.decode(string)
用法示例:

local str  = [[ {"a":"v","b":2,"c":{"c1":1,"c2":2},"d":[10,11],"1":100} ]]
local t    = json.decode(str)
ngx.say(" --> ", type(t))

-------------

local str = [[ {"a":1,"b":null} ]]
local t    = json.decode(str)
ngx.say(t.a, "<br/>")
ngx.say(t.b == nil, "<br/>")
ngx.say(t.b == json.null, "<br/>")

----> 1
----> false
----> true

注意:null将会转换为json.null

二)异常处理

local json = require("cjson")
local str  = [[ {"key:"value"} ]]---少了一个双引号

local t    = json.decode(str)
ngx.say(" --> ", type(t))
执行请求,看看效果,执行报了--500 Internal Server Error
是因为decode方法报错了导致

实际情况我们希望的结果不是报错,而是返回一个友好的结果,如返回个nil

使用pcall命令
如果需要在 Lua 中处理错误,必须使用函数 pcall(protected call)来包装需要执行的代码。
pcall 接收一个函数和要传递给后者的参数,并执行,执行结果:有错误、无错误;
返回值 true 或者或 false, errorinfo。
pcall 以一种"保护模式"来调用第一个参数(函数),因此 pcall 可以捕获函数执行中的任何错误。

第一个方案:重新包装一个 json decode编码

local json = require("cjson")

local function _json_decode(str)
return json.decode(str)
end

function json_decode( str )
local ok, t = pcall(_json_decode, str)
if not ok then
return nil
end

return t
end

local str  = [[ {"key:"value"} ]]---少了一个双引号

local t    = json_decode(str)
ngx.say(t)

执行效果,没有系统错误,返回了nil

第二个方案:引入cjson.safe 模块接口,该接口兼容 cjson 模块,并且在解析错误时不抛出异常,而是返回 nil。

local json = require("cjson.safe")
local str  = [[ {"key:"value"} ]]

local t    = json.decode(str)
if t then
ngx.say(" --> ", type(t))
else
ngx.say("t is nil")
end

三)空table返回object还是array

测试一下,编码空table   {}
local json = require("cjson")
ngx.say("value --> ", json.encode({}))
输出 value --> {}

{}是个object;对于java的开发人员来说就不对了,空数组table,应该是[]

这个是因为对于 Lua 本身,是把数组和哈希键值对融合到一起了,所以他是无法区分空数组和空字典的。

要达到目标把 encode_empty_table_as_object 设置为 false

local json = require("cjson")
json.encode_empty_table_as_object(false)
ngx.say("value --> ", json.encode({}))
输出 value --> []

#32、openresty中使用redis模块

在一些高并发的场景中,我们常常会用到缓存技术,现在我们常用的分布式缓存redis是最知名的,
我们这里介绍一下如何操作redis。
操作redis,我们需要引入redis模块 require "resty.redis";
我们现在做个可以操作redis进行赋值,读值的案例

一)连接redis服务器

---定义 redis关闭连接的方法
local function close_redis(red)
if not red then
return
end
local ok, err = red:close()
if not ok then
ngx.say("close redis error : ", err)
end
end

local redis = require "resty.redis"  --引入redis模块
local red = redis:new()  --创建一个对象,注意是用冒号调用的
--设置超时(毫秒)
red:set_timeout(1000)
--建立连接
local ip = "192.168.31.247"
local port = 6379
local ok, err = red:connect(ip, port)
if not ok then
ngx.say("connect to redis error : ", err)
return close_redis(red)
end
--调用API设置key
ok, err = red:set("msg", "hello world")
if not ok then
ngx.say("set msg error : ", err)
return close_redis(red)
end
--调用API获取key值
local resp, err = red:get("msg")
if not resp then
ngx.say("get msg error : ", err)
return close_redis(red)
end

ngx.say("msg : ", resp)
close_redis(red)

请求结果   msg : hello world

--------------------------------
注意:得到的数据为空处理 ,redis返回的空 为null,所以不能用nil判断,而要用ngx.null判断
if resp == ngx.null then
resp = ''  --比如默认值
end

--------------连接授权的redis-----------------
在redis.conf配置文件 配置认证密码
requirepass 123456

注意:windows 启动redis时,配置redis.windows.conf;并且不能直接 双击redis-server.exe,
如果双击启动,默认不会找此目录下的配置文件;需要指定配置文件
解决方案:
1)cmd窗口中 运行 redis-server.exe redis.windows.conf
2)新建一个bat批处理文件  文件内容 redis-server.exe redis.windows.conf

连接报错set msg error : NOAUTH Authentication required.因为认证出错
在red:connect成功后,调用red:auth认证密码

ok, err = red:auth("123456")
if not ok then
ngx.say("failed to auth: ", err)
return close_redis(red)
end

=======================================================

二)redis连接池

redis的连接是tcp连接,建立TCP连接需要三次握手,而释放TCP连接需要四次握手,而这些往返时延仅需要一次,
以后应该复用TCP连接,此时就可以考虑使用连接池,即连接池可以复用连接。
我们需要把close_redis函数改造一下

local function close_redis(red)
if not red then
return
end
--释放连接(连接池实现)
local pool_max_idle_time = 10000 --毫秒
local pool_size = 100 --连接池大小
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx.say("set keepalive error : ", err)
end
end
即设置空闲连接超时时间防止连接一直占用不释放;设置连接池大小来复用连接。
注意:
1、连接池是每Worker进程的,而不是每Server的;
2、当连接超过最大连接池大小时,会按照LRU算法回收空闲连接为新连接使用;
3、连接池中的空闲连接出现异常时会自动被移除;
4、连接池是通过ip和port标识的,即相同的ip和port会使用同一个连接池(即使是不同类型的客户端);
5、连接池第一次set_keepalive时连接池大小就确定下了,不会再变更;

---------------------------------------------------

注意:我们如何知道,redis连接对象是从连接池中获取的,还是新创建的连接呢??

使用 red:get_reused_times --->得到此连接被使用的次数

如果当前连接不是从内建连接池中获取的,该方法总是返回 0 ,也就是说,该连接还没有被使用过。

如果连接来自连接池,那么返回值永远都是非零。所以这个方法可以用来确认当前连接是否来自池子。

=============================================
连接优化

采用连接池,连接带认证的redis

---定义 redis关闭连接的方法
local function close_redis(red)
if not red then
return
end
--释放连接(连接池实现)
local pool_max_idle_time = 10000 --毫秒
local pool_size = 100 --连接池大小
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx.say("set keepalive error : ", err)
end
end

local redis = require "resty.redis"  --引入redis模块
local red = redis:new()  --创建一个对象,注意是用冒号调用的
--设置超时(毫秒)
red:set_timeout(1000)
--建立连接
local ip = "192.168.31.247"
local port = 6379
local ok, err = red:connect(ip, port)
if not ok then
ngx.say("connect to redis error : ", err)
return close_redis(red)
end

local count, err = red:get_reused_times()
if 0 == count then ----新建连接,需要认证密码
ok, err = red:auth("123456")
if not ok then
ngx.say("failed to auth: ", err)
return
end
elseif err then  ----从连接池中获取连接,无需再次认证密码
ngx.say("failed to get reused times: ", err)
return
end

--调用API设置key
ok, err = red:set("msg", "hello world333333333")
if not ok then
ngx.say("set msg error : ", err)
return close_redis(red)
end
--调用API获取key值
local resp, err = red:get("msg")
if not resp then
ngx.say("get msg error : ", err)
return close_redis(red)
end

ngx.say("msg : ", resp)
close_redis(red)

=======================================

非常注意:连接池使用过程中,业务代码有select方法,会导致数据错乱

ok, err = red:select(1)  --->选择db
if not ok then
ngx.say("failed to select db: ", err)
return
end

如:
A业务使用了db1,所以使用了 select(1);

B业务使用默认的db0,select(0)遗漏

但A,B业务共用了连接池,很有可能 B业务拿到的 A业务使用的连接,而此连接操作的数据库db1;
而B业务中代码没有指定select数据库,所以B业务操作数据到了db1中;导致数据错乱

切记!!!

#33、openresty中封装redis操作

在关于web+lua+openresty开发中,项目中会大量操作redis,

重复创建连接-->数据操作-->关闭连接(或放到连接池)这个完整的链路调用完毕,
甚至还要考虑不同的 return 情况做不同处理,就很快发现代码中有大量的重复

推荐一个二次封装的类库
local redis_c = require "resty.redis"

local ok, new_tab = pcall(require, "table.new")
if not ok or type(new_tab) ~= "function" then
new_tab = function (narr, nrec) return {} end
end

local _M = new_tab(0, 155)
_M._VERSION = '0.01'

local commands = {
"append",            "auth",              "bgrewriteaof",
"bgsave",            "bitcount",          "bitop",
"blpop",             "brpop",
"brpoplpush",        "client",            "config",
"dbsize",
"debug",             "decr",              "decrby",
"del",               "discard",           "dump",
"echo",
"eval",              "exec",              "exists",
"expire",            "expireat",          "flushall",
"flushdb",           "get",               "getbit",
"getrange",          "getset",            "hdel",
"hexists",           "hget",              "hgetall",
"hincrby",           "hincrbyfloat",      "hkeys",
"hlen",
"hmget",              "hmset",      "hscan",
"hset",
"hsetnx",            "hvals",             "incr",
"incrby",            "incrbyfloat",       "info",
"keys",
"lastsave",          "lindex",            "linsert",
"llen",              "lpop",              "lpush",
"lpushx",            "lrange",            "lrem",
"lset",              "ltrim",             "mget",
"migrate",
"monitor",           "move",              "mset",
"msetnx",            "multi",             "object",
"persist",           "pexpire",           "pexpireat",
"ping",              "psetex",            "psubscribe",
"pttl",
"publish",      --[[ "punsubscribe", ]]   "pubsub",
"quit",
"randomkey",         "rename",            "renamenx",
"restore",
"rpop",              "rpoplpush",         "rpush",
"rpushx",            "sadd",              "save",
"scan",              "scard",             "script",
"sdiff",             "sdiffstore",
"select",            "set",               "setbit",
"setex",             "setnx",             "setrange",
"shutdown",          "sinter",            "sinterstore",
"sismember",         "slaveof",           "slowlog",
"smembers",          "smove",             "sort",
"spop",              "srandmember",       "srem",
"sscan",
"strlen",       --[[ "subscribe",  ]]     "sunion",
"sunionstore",       "sync",              "time",
"ttl",
"type",         --[[ "unsubscribe", ]]    "unwatch",
"watch",             "zadd",              "zcard",
"zcount",            "zincrby",           "zinterstore",
"zrange",            "zrangebyscore",     "zrank",
"zrem",              "zremrangebyrank",   "zremrangebyscore",
"zrevrange",         "zrevrangebyscore",  "zrevrank",
"zscan",
"zscore",            "zunionstore",       "evalsha"
}

local mt = { __index = _M }

local function is_redis_null( res )
if type(res) == "table" then
for k,v in pairs(res) do
if v ~= ngx.null then
return false
end
end
return true
elseif res == ngx.null then
return true
elseif res == nil then
return true
end

return false
end

function _M.close_redis(self, redis)
if not redis then
return
end
--释放连接(连接池实现)
local pool_max_idle_time = self.pool_max_idle_time --最大空闲时间 毫秒
local pool_size = self.pool_size --连接池大小

local ok, err = redis:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx.say("set keepalive error : ", err)
end
end

-- change connect address as you need
function _M.connect_mod( self, redis )
redis:set_timeout(self.timeout)

local ok, err = redis:connect(self.ip, self.port)
if not ok then
ngx.say("connect to redis error : ", err)
return self:close_redis(redis)
end

if self.password then ----密码认证
local count, err = redis:get_reused_times()
if 0 == count then ----新建连接,需要认证密码
ok, err = redis:auth(self.password)
if not ok then
ngx.say("failed to auth: ", err)
return
end
elseif err then  ----从连接池中获取连接,无需再次认证密码
ngx.say("failed to get reused times: ", err)
return
end
end

return ok,err;
end

function _M.init_pipeline( self )
self._reqs = {}
end

function _M.commit_pipeline( self )
local reqs = self._reqs

if nil == reqs or 0 == #reqs then
return {}, "no pipeline"
else
self._reqs = nil
end

local redis, err = redis_c:new()
if not redis then
return nil, err
end

local ok, err = self:connect_mod(redis)
if not ok then
return {}, err
end

redis:init_pipeline()
for _, vals in ipairs(reqs) do
local fun = redis[vals[1]]
table.remove(vals , 1)

fun(redis, unpack(vals))
end

local results, err = redis:commit_pipeline()
if not results or err then
return {}, err
end

if is_redis_null(results) then
results = {}
ngx.log(ngx.WARN, "is null")
end
-- table.remove (results , 1)

--self.set_keepalive_mod(redis)
self:close_redis(redis)

for i,value in ipairs(results) do
if is_redis_null(value) then
results[i] = nil
end
end

return results, err
end

local function do_command(self, cmd, ... )
if self._reqs then
table.insert(self._reqs, {cmd, ...})
return
end

local redis, err = redis_c:new()
if not redis then
return nil, err
end

local ok, err = self:connect_mod(redis)
if not ok or err then
return nil, err
end

redis:select(self.db_index)

local fun = redis[cmd]
local result, err = fun(redis, ...)
if not result or err then
-- ngx.log(ngx.ERR, "pipeline result:", result, " err:", err)
return nil, err
end

if is_redis_null(result) then
result = nil
end

--self.set_keepalive_mod(redis)
self:close_redis(redis)

return result, err
end

for i = 1, #commands do
local cmd = commands[i]
_M[cmd] =
function (self, ...)
return do_command(self, cmd, ...)
end
end

function _M.new(self, opts)
opts = opts or {}
local timeout = (opts.timeout and opts.timeout * 1000) or 1000
local db_index= opts.db_index or 0
local ip = opts.ip or '127.0.0.1'
local port = opts.port or 6379
local password = opts.password
local pool_max_idle_time = opts.pool_max_idle_time or 60000
local pool_size = opts.pool_size or 100

return setmetatable({
timeout = timeout,
db_index = db_index,
ip = ip,
port = port,
password = password,
pool_max_idle_time = pool_max_idle_time,
pool_size = pool_size,
_reqs = nil }, mt)
end

return _M

调用案例

local redis = require "resty.redis_iresty"

local opts = {
ip = "192.168.31.247",
port = "6379",
password = "123456",
db_index = 1
}

local red = redis:new(opts)

local ok, err = red:set("dog", "an animal11111")
if not ok then
ngx.say("failed to set dog: ", err)
return
end

ngx.say("set result: ", ok)

管道

redis.set
redis.get

red:init_pipeline()
red:set("cat", "Marry")
red:set("horse", "Bob")
red:get("cat")
red:get("horse")
local results, err = red:commit_pipeline()
if not results then
ngx.say("failed to commit the pipelined requests: ", err)
return
end

for i, res in ipairs(results) do
ngx.say(res,"<br/>");
end

不需要重复造轮子,只要这个轮子 稳定,高效

#34、openresty中使用mysql

Mysql客户端
在我们应用中最长打交道的就是数据库了,尤其mysql数据库,那openresty lua如何操作mysql呢?
默认安装OpenResty时已经自带了该模块。

我们编写个案例,操作mysql数据库,编辑test.lua
---定义关闭mysql的连接
local function close_db(db)
if not db then
return
end
db:close()
end

local mysql = require("resty.mysql") ---引入mysql模块
--创建实例
local db, err = mysql:new()
if not db then
ngx.say("new mysql error : ", err)
return
end
--设置超时时间(毫秒)
db:set_timeout(1000)
---连接属性定义
local props = {
host = "192.168.31.247",
port = 3306,
database = "test",
user = "root",
password = "123456",
charset = "utf8"
}

local res, err, errno, sqlstate = db:connect(props)

if not res then
ngx.say("connect to mysql error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
return close_db(db)
end

ngx.say("===========删除表user========", "<br/>")

--我们对数据库进行crud,统一的操作方法 query
--不同于其他语言 insert update delete select
--删除表
local drop_table_sql = "drop table if exists user"
res, err, errno, sqlstate = db:query(drop_table_sql)
if not res then
ngx.say("drop table error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
return close_db(db)
end

ngx.say("===========创建表user========", "<br/>")
--创建表
local create_table_sql = "create table user(id int primary key auto_increment, ch varchar(100))"
res, err, errno, sqlstate = db:query(create_table_sql)
if not res then
ngx.say("create table error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
return close_db(db)
end

ngx.say("===========插入数据user========", "<br/>")
--插入
local insert_sql = "insert into user (ch) values('hello')"
res, err, errno, sqlstate = db:query(insert_sql)
if not res then
ngx.say("insert error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
return close_db(db)
end

res, err, errno, sqlstate = db:query(insert_sql)

ngx.say("insert rows : ", res.affected_rows, " , id : ", res.insert_id, "<br/>")

ngx.say("===========更新表user========", "<br/>")
--更新
local update_sql = "update user set ch = 'hello2' where id =" .. res.insert_id
res, err, errno, sqlstate = db:query(update_sql)
if not res then
ngx.say("update error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
return close_db(db)
end

ngx.say("update rows : ", res.affected_rows, "<br/>")

ngx.say("===========查询user========", "<br/>")

--查询
local select_sql = "select id, ch from user"
res, err, errno, sqlstate = db:query(select_sql)
if not res then
ngx.say("select error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
return close_db(db)
end
----查询成功后,res为表类型,结构类型如下
----{{id=1,name="n1"},{id=2,name="n2"}}

for i, row in ipairs(res) do
for name, value in pairs(row) do
ngx.say("select row ", i, " : ", name, " = ", value, "<br/>")
end
end

ngx.say("<br/>")

ngx.say("===========查询user=根据ch参数=======", "<br/>")

--防止sql注入
local ch_param = ngx.req.get_uri_args()["ch"] or ''
--使用ngx.quote_sql_str防止sql注入
local query_sql = "select id, ch from user where ch = " .. ngx.quote_sql_str(ch_param)
res, err, errno, sqlstate = db:query(query_sql)
if not res then
ngx.say("select error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
return close_db(db)
end

for i, row in ipairs(res) do
for name, value in pairs(row) do
ngx.say("select row ", i, " : ", name, " = ", value, "<br/>")
end
end

ngx.say("===========删除user========", "<br/>")
--删除
local delete_sql = "delete from user"
res, err, errno, sqlstate = db:query(delete_sql)
if not res then
ngx.say("delete error : ", err, " , errno : ", errno, " , sqlstate : ", sqlstate)
return close_db(db)
end

ngx.say("delete rows : ", res.affected_rows, "<br/>")

ngx.say("===========关闭db========", "<br/>")

close_db(db)

======================================

对于新增/修改/删除会返回如下格式的响应:
{
insert_id = 0,     ----insert_id是在使用自增序列时产生的id。
server_status = 2,
warning_count = 1,
affected_rows = 32,   ----affected_rows表示操作影响的行数
message = nil
}

对于查询会返回如下格式的响应:
{
{ id= 1, ch= "hello"},
{ id= 2, ch= "hello2"}
}
null将返回ngx.null。

访问请求http://192.168.31.150/lua?ch=hello
输出结果
===========删除表user========
===========创建表user========
===========插入数据user========
insert rows : 1 , id : 2
===========更新表user========
update rows : 1
===========查询user========
select row 1 : ch = hello
select row 1 : id = 1
select row 2 : ch = hello2
select row 2 : id = 2

===========查询user=根据ch参数=======
select row 1 : ch = hello
select row 1 : id = 1
===========删除user========
delete rows : 2
===========关闭db========

注意点:

客户端目前还没有提供预编译SQL支持(即占位符替换位置变量),
这样在入参时记得使用ngx.quote_sql_str进行字符串转义,防止sql注入;

==========================================

连接池和之前Redis客户端完全一样。
local function close_db(db)
if not db then
return
end
--释放连接(连接池实现)
local pool_max_idle_time = 10000 --毫秒
local pool_size = 100 --连接池大小
local ok, err = db:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx.say("set keepalive error : ", err)
end
end

更多资料 https://github.com/openresty/lua-resty-mysql
sqlstate https://blog.csdn.net/tercel99/article/details/1520094

#35、lua发起http请求(nginx配置外部访问404,内部请求 internal)

有些场景是需要nginx在进行请求转发,什么含义呢?

用户浏览器请求url访问到nginx服务器,但此请求业务需要再次请求其他业务;
如用户请求订单服务获取订单详情,可订单详情中需要返回商品信息,也就需要再请求商品服务获取商品信息;
这样就需要nginx需要有发起http请求的能力,而不是让用户浏览器在请求商品访问。

nginx服务发起http请求区分内部请求 和 外部请求

图解

下面我们就介绍一下,openResty中如何发起http请求?

一)内部请求

1)capture请求方法

res = ngx.location.capture(uri,{
options?
});

options可以传参数和设置请求方式

local res = ngx.location.capture("/product",{
method = ngx.HTTP_GET,   #请求方式
args = {a=1,b=2},  #get方式传参数
body = "c=3&d=4" #post方式传参数
});

res.status --->保存子请求的响应状态码
res.header --->用一个标准 Lua 表储子请求响应的所有头信息。如果是“多值”响应头,
--->这些值将使用 Lua (数组) 表顺序存储。

res.body   --->保存子请求的响应体数据,它可能被截断。
--->用户需要检测 res.truncated (截断) 布尔值标记来判断 res.body 是否包含截断的数据。
--->这种数据截断的原因只可能是因为子请求发生了不可恢复的错误,
--->例如远端在发送响应体时过早中断了连接,或子请求在接收远端响应体时超时。

res.truncated --->是否截断

-----------------------------------------

编辑nginx.conf配置文件,配置一下路由,定义有个两个服务请求 商品服务请求和订单服务请求

location /product {  #商品服务请求
echo "商品请求";
}

location /order {  #订单服务请求
content_by_lua_block {
local res = ngx.location.capture("/product");
ngx.say(res.status)
ngx.say(res.body)
}
}

ngx.location.capture 方法就是发起http的请求,但是它只能请求 内部服务,不能直接请求外部服务

输出结果,http状态为200,返回了 商品服务中的内容

------------------------------------------------

这边有一种情况,这样的定义,用户用浏览器直接请求商品服务也照样请求

可很多时候我们会要求商品请求 是不对外暴露的,也就是用户无法直接访问商品服务请求。
那我们只要在内部请求那边加上一个关键字,internal 就可以了
location /product {  #商品服务请求
internal;
echo "商品请求";
}
这样直接访问就报404错误了

-----------------post 请求-----------------

location /product {  #商品服务请求
content_by_lua_block {
ngx.req.read_body();
local args = ngx.req.get_post_args()
ngx.print(tonumber(args.a) + tonumber(args.b))
}
}

location /order {  #订单服务请求
content_by_lua_block {
local res = ngx.location.capture("/product",{
method = ngx.HTTP_POST,
args = {a=1,b=2},
body = "a=3&b=4"
});
ngx.say(res.status)
ngx.say(res.body)
}
}

2)capture_multi 并发请求
再以上基础上面增加需求,要获得用户信息
正常逻辑: order request ---> product request ---> user request ----> end
提高性能的方式:   order request ---> product request
---> user request      ----> end

语法:res1,res2, ... = ngx.location.capture_multi({
{uri, options?},
{uri, options?},
...
})

-----并发调用
location = /sum {
internal;
content_by_lua_block {
ngx.sleep(0.1)
local args = ngx.req.get_uri_args()
ngx.print(tonumber(args.a) + tonumber(args.b))
}
}

location = /subduction {
internal;
content_by_lua_block {
ngx.sleep(0.1)
local args = ngx.req.get_uri_args()
ngx.print(tonumber(args.a) - tonumber(args.b))
}
}

location = /app/test_multi {
content_by_lua_block {
local start_time = ngx.now()
local res1, res2 = ngx.location.capture_multi( {
{"/sum", {args={a=3, b=8}}},
{"/subduction", {args={a=3, b=8}}}
})
ngx.say("status:", res1.status, " response:", res1.body)
ngx.say("status:", res2.status, " response:", res2.body)
ngx.say("time used:", ngx.now() - start_time)
}
}

location = /app/test_queue {
content_by_lua_block {
local start_time = ngx.now()
local res1 = ngx.location.capture("/sum", {
args={a=3, b=8}
})
local res2 = ngx.location.capture("/subduction", {
args={a=3, b=8}
})
ngx.say("status:", res1.status, " response:", res1.body)
ngx.say("status:", res2.status, " response:", res2.body)
ngx.say("time used:", ngx.now() - start_time)
}
}

==============================================

二)外部请求

如何发起外部请求呢?
因为ngx.location.capture不能直接发起外部请求,我们需要通过内部请求中用反向代理请求发起外部请求

location /product {
internal;
proxy_pass "https://s.taobao.com/search?q=iphone";
}

location /order {
content_by_lua_block {
local res = ngx.location.capture("/product");
ngx.say(res.status)
ngx.say(res.body)
}
}

在商品服务那边用的proxy_pass 请求外部http请求,这样就达到了请求外部http的目的。

请求返回了200,表示请求成功了;

但发现都是乱码,这个是什么原因呢?
一开始想到的是字符编码的问题,需要把虚拟主机的server模块配置一个字符编码
charset UTF-8;   设置为utf-8。重启nginx重新访问
还是乱码,这是为什么呢,编码不是改了吗?这个是因为taobao这个web服务器加了gzip压缩,
他返回给我们的结果是经过压缩的,我们再接受过来的时候需要解压才行,那怎么办?
我们可以让taobao服务返回不需要压缩的数据吗?  我们可以在请求外部链接那边设置
proxy_set_header Accept-Encoding   ' ';#让后端不要返回压缩(gzip或deflate)的内容
最终
location /product {
internal;
proxy_set_header Accept-Encoding ' ';
proxy_pass "https://s.taobao.com/search?q=iphone";
}
重启nginx,再次访问,结果输出

==========================================================

以上我们介绍了 内部访问 和  外部访问

三)动态变量

刚才我们请求外部请求,是写死了q=iphone,那我们用capture传参

location /product {
internal;
resolver 8.8.8.8;
proxy_set_header Accept-Encoding ' ';
proxy_pass "https://s.taobao.com/search?q=$arg_q";
}

location /order {
content_by_lua_block {
local get_args = ngx.req.get_uri_args();
local res = ngx.location.capture("/product",{
method = ngx.HTTP_GET,
args = {q=get_args["q"]}
});
ngx.say(res.status)
ngx.say(res.body)
}
}

注意:在proxy_pass 中使用变量,需要使用resolver指令解析变量中的域名

# google 域名解析
resolver 8.8.8.8;

这样我们请求传q参数的值,随便由用户决定查询什么值。
我们这边就发现了请求外部服务的时候发现比较复杂,我们可以借用第三方的库 resty.http ,
从可实现外部请求,而且使用很方便,下篇文章我们就介绍resty.http模块

#36、openresty中使用http模块

OpenResty默认没有提供Http客户端,需要使用第三方提供
我们可以从github上搜索相应的客户端,比如https://github.com/pintsized/lua-resty-http

只要将 lua-resty-http/lib/resty/ 目录下的 http.lua 和 http_headers.lua
两个文件拷贝到 /usr/local/openresty/lualib/resty 目录下即可
(假设你的 OpenResty 安装目录为 /usr/local/openresty)

cd /usr/local/openresty/lualib/resty
wget https://raw.githubusercontent.com/pintsized/lua-resty-http/master/lib/resty/http_headers.lua
wget https://raw.githubusercontent.com/pintsized/lua-resty-http/master/lib/resty/http.lua

local res, err = httpc:request_uri(uri, {
method = "POST/GET",  ---请求方式
query = str,  ---get方式传参数
body = str,	 ---post方式传参数
path = "url" ----路径
headers = {  ---header参数
["Content-Type"] = "application/json",
}
})

--------------------------------------------

编写个模拟请求淘宝的查询

--引入http模块
local http = require("resty.http")
--创建http客户端实例
local httpc = http.new()
--request_uri函数请求淘宝
local resp, err = httpc:request_uri("https://s.taobao.com", {
method = "GET",    		---请求方式
query = "q=iphone&b=2",   ---get方式传参数
body = "c=3&d=4",  		---post方式传参数
path = "/search", 		----路径
headers = { 			---header参数
["User-Agent"] = "Mozilla/5.0 (Windows NT 6.1; WOW64) ",
["token"] = "1234456"
}
})

if not resp then
ngx.say("request error :", err)
return
end

--获取返回的状态码
ngx.status = resp.status

if ngx.status ~= 200 then
ngx.log(ngx.WARN,"非200状态,ngx.status:"..ngx.status)
return resStr
end

--获取遍历返回的头信息
for k, v in pairs(resp.headers) do
if type(v) == "table" then
ngx.log(ngx.WARN,"table:"..k, ": ", table.concat(v, ", "))
else
ngx.log(ngx.WARN,"one:"..k, ": ", v)
end
end

--响应体
ngx.say(resp.body)

httpc:close()

-------------------------------------------------------
发现报错
request error :no resolver defined to resolve "s.taobao.com"

此错误是因为要配置DNS解析器resolver 8.8.8.8,否则域名是无法解析的。
在nginx.conf配置文件中 http模块加上resolver 8.8.8.8; Google提供的免费DNS服务器的IP地址
配置好后,重启nginx

---------------------------------------------------------

访问https错误,因为我们访问的https,需要配置ssl证书
在nginx配置文件中,server虚拟主机模块设置

lua_ssl_verify_depth 2;
lua_ssl_trusted_certificate "/etc/ssl/certs/ca-bundle.crt";

--------------------------------------------------------

http模块应用场景很多,这里只简单介绍了一下http模块的使用

还有很多openresty模块,可以参考 https://github.com/bungle/awesome-resty

#37、openresty中使用全局缓存

Nginx全局内存---本地缓存

使用过如Java的朋友可能知道如Ehcache等这种进程内本地缓存。
Nginx是一个Master进程多个Worker进程的工作方式,因此我们可能需要在多个Worker进程中共享数据。

使用ngx.shared.DICT来实现全局内存共享。

一)首先在nginx.conf的http部分分配内存大小

语法:lua_shared_dict <name> <size>

该命令主要是定义一块名为name的共享内存空间,内存大小为size。
通过该命令定义的共享内存对象对于Nginx中所有worker进程都是可见的

注意:当Nginx通过reload命令重启时,共享内存字典项会从新获取它的内容 (即共享内存保留)
当Nginx退出时,字典项的值将会丢失。(即共享内存丢失)

http {

lua_shared_dict dogs 10m;
... ...
}

二)通过ngx.shared.DICT接口获取共享内存字典项对象

语法:dict = ngx.shared.DICT
dict = ngx.shared[name_var]
其中,DICT和name_var表示的名称是一致的,比如上面例子中,
dogs = ngx.shared.dogs 就是dict = ngx.shared.DICT的表达形式;
也可以通过下面的方式达到同样的目的:
dogs = ngx.shared["dogs"]

三)对象操作方法

1)获取 ngx.shared.DICT.get

语法:value, flags = ngx.shared.DICT:get(key)
获取共享内存上key对应的值。如果key不存在,或者key已经过期,将会返回nil;
如果出现错误,那么将会返回nil以及错误信息。
local dogs = ngx.shared.dogs
local value, flags = dogs:get("Marry")  ---(冒号点号)等价于 dogs.get(dogs, "Marry")

返回列表中的flags,是在ngx.shared.DICT.set方法中设置的值,默认值为0.
如果设置的flags为0,那么在这里flags的值将不会被返回。

2)获取包含过期的key ngx.shared.DICT.get_stale

语法:value, flags, stale = ngx.shared.DICT:get_stale(key)

与get方法类似,区别在于该方法对于过期的key也会返回,
第三个返回参数表明返回的key的值是否已经过期,true表示过期,false表示没有过期。

3)设置 ngx.shared.DICT.set

语法:success, err, forcible = ngx.shared.DICT:set(key, value, exptime?, flags?)

“无条件”地往共享内存上插入key-value对,这里讲的“无条件”指的是不管待插入的共享内存上是否已经存在相同的key。
三个返回值的含义:
success:成功插入为true,插入失败为false
err:操作失败时的错误信息,可能类似"no memory"
forcible:true表明通过强制删除(LRU算法)共享内存上其他字典项来实现插入,
false表明没有删除共享内存上的字典项来实现插入。

第三个参数exptime表明key的有效期时间,单位是秒(s),默认值为0,表明永远不会过期。
第四个参数flags是一个用户标志值,会在调用get方法时同时获取得到。

local dogs = ngx.shared.dogs
local succ, err, forcible = dogs:set("Marry", "it is a nice cat!")

4)安全设置 ngx.shared.DICT.safe_set

语法:ok, err = ngx.shared.DICT:safe_set(key, value, exptime?, flags?)

与set方法类似,区别在于不会在共享内存用完的情况下,通过强制删除(LRU算法)的方法实现插入。
如果内存不足,会直接返回nil和err信息"no memory"

注意:set和safe_set共同点是:如果待插入的key已经存在,那么key对应的原来的值会被新的value覆盖!

5)增加 ngx.shared.DICT.add

语法:success, err, forcible = ngx.shared.DICT:add(key, value, exptime?, flags?)

与set方法类似,与set方法区别在于不会插入重复的键(可以简单认为add方法是set方法的一个子方法),
如果待插入的key已经存在,将会返回nil和和err="exists"

6)安全增加 ngx.shared.DICT.safe_add

语法:ok, err = ngx.shared.DICT:safe_add(key, value, exptime?, flags?)

与safe_set方法类似,区别在于不会插入重复的键(可以简单认为safe_add方法是safe_set方法的一个子方法),
如果待插入的key已经存在,将会返回nil和err="exists"

7)替换 ngx.shared.DICT.replace

语法:success, err, forcible = ngx.shared.DICT:replace(key, value, exptime?, flags?)

与set方法类似,区别在于只对已经存在的key进行操作(可以简单认为replace方法是set方法的一个子方法),
如果待插入的key在字典上不存在,将会返回nil和错误信息"not found"

8)删除 ngx.shared.DICT.delete

语法:ngx.shared.DICT:delete(key)

无条件删除指定的key-value对,其等价于

ngx.shared.DICT:set(key, nil)

9)自增 ngx.shared.DICT.incr

语法:newval, err = ngx.shared.DICT:incr(key, value)

对key对应的值进行增量操作,增量值是value,其中value的值可以是一个正数,0,也可以是一个负数。
value必须是一个Lua类型中的number类型,否则将会返回nil和"not a number";
key必须是一个已经存在于共享内存中的key,否则将会返回nil和"not found".

10)清除 ngx.shared.DICT.flush_all

语法:ngx.shared.DICT:flush_all()

清除字典上的所有字段,但不会真正释放掉字段所占用的内存,而仅仅是将每个字段标志为过期。

11)清除过期内存 ngx.shared.DICT.flush_expired

语法:flushed = ngx.shared.DICT:flush_expired(max_count?)

清除字典上过期的字段,max_count表明上限值,如果为0或者没有给出,表明需要清除所有过期的字段,
返回值flushed是实际删除掉的过期字段的数目。

注意:与flush_all方法的区别在于,该方法将会释放掉过期字段所占用的内存。

12)获取keys  ngx.shared.DICT.get_keys

语法:keys = ngx.shared.DICT:get_keys(max_count?)

从字典上获取字段列表,个数为max_count,如果为0或没有给出,表明不限定个数。默认值是1024个

注意:强烈建议在调用该方法时,指定一个max_count参数,因为在keys数量很大的情况下,
如果不指定max_count的值,可能会导致字典被锁定,从而阻塞试图访问字典的worker进程。

-----------------案例---------------------

--1、获取全局共享内存变量
local shared_data = ngx.shared.shared_data

--2、获取字典值
local i = shared_data:get("i")
if not i then
i = 1
--3、赋值
shared_data:set("i", i)
ngx.say("set i ", i, "<br/>")
end
--递增
i = shared_data:incr("i", 1)
ngx.say("i=", i, "<br/>")

#38、openresty执行流程

#39、openresty执行流程详解

#40、nginx-lua-redis实现访问频率控制

一)需求背景

在高并发场景下为了防止某个访问ip访问的频率过高,有时候会需要控制用户的访问频次
在openresty中,可以找到:
set_by_lua,rewrite_by_lua,access_by_lua,content_by_lua等方法。
那么访问控制应该是,access阶段。
我们用Nginx+Lua+Redis来做访问限制主要是考虑到高并发环境下快速访问控制的需求。

二)设计方案

我们用redis的key表示用户,value表示用户的请求频次,再利用过期时间实现单位时间;

现在我们要求10秒内只能访问10次frequency请求,超过返回403

1)首先为nginx.conf配置文件,nginx.conf部分内容如下:

location /frequency {
access_by_lua_file /usr/local/lua/access_by_limit_frequency.lua;
echo "访问成功";
}

2)编辑access_by_limit_frequency.lua

local function close_redis(red)
if not red then
return
end
--释放连接(连接池实现)
local pool_max_idle_time = 10000 --毫秒
local pool_size = 100 --连接池大小
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx.say("set keepalive error : ", err)
end
end

local function errlog(...)
ngx.log(ngx.ERR, "redis: ", ...)
end

local redis = require "resty.redis"  --引入redis模块
local red = redis:new()  --创建一个对象,注意是用冒号调用的

--设置超时(毫秒)
red:set_timeout(1000)
--建立连接
local ip = "192.168.31.247"
local port = 6379
local ok, err = red:connect(ip, port)
if not ok then
close_redis(red)
errlog("Cannot connect");
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end

local key = "limit:frequency:login:"..ngx.var.remote_addr

--得到此客户端IP的频次
local resp, err = red:get(key)
if not resp then
close_redis(red)
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) --redis 获取值失败
end

if resp == ngx.null then
red:set(key, 1) -- 单位时间 第一次访问
red:expire(key, 10) --10秒时间 过期
end

if type(resp) == "string" then
if tonumber(resp) > 10 then -- 超过10次
close_redis(red)
return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
end
end

--调用API设置key
ok, err = red:incr(key)
if not ok then
close_redis(red)
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) --redis 报错
end

close_redis(red)

请求地址:/frequency

10秒内 超出10次 ,返回403

10秒后,又可以访问了

如果我们想整个网站 都加上这个限制条件,那只要把
access_by_lua_file /usr/local/lua/access_by_limit_frequency.lua;
这个配置,放在server部分,让所有的location 适用就行了

#41、通过 Lua + Redis 实现动态封禁 IP

一)需求背景 为了封禁某些爬虫或者恶意用户对服务器的请求,我们需要建立一个动态的 IP 黑名单。 对于黑名单之内的 IP ,拒绝提供服务。

二)设计方案 实现 IP 黑名单的功能有很多途径: 1、在操作系统层面,配置 iptables,拒绝指定 IP 的网络请求; 2、在 Web Server 层面,通过 Nginx 自身的 deny 选项 或者 lua 插件 配置 IP 黑名单; 3、在应用层面,在请求服务之前检查一遍客户端 IP 是否在黑名单。

为了方便管理和共享,我们通过 Nginx+Lua+Redis 的架构实现 IP 黑名单的功能

如图

配置nginx.conf
在http部分,配置本地缓存,来缓存redis中的数据,避免每次都请求redis

lua_shared_dict shared_ip_blacklist 1m; #定义ip_blacklist 本地缓存变量

location /ipblacklist {
access_by_lua_file /usr/local/lua/access_by_limit_ip.lua;
echo "ipblacklist";
}

local function close_redis(red)
if not red then
return
end
--释放连接(连接池实现)
local pool_max_idle_time = 10000 --毫秒
local pool_size = 100 --连接池大小
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx.say("set keepalive error : ", err)
end
end

local function errlog(...)
ngx.log(ngx.ERR, "redis: ", ...)
end

local function duglog(...)
ngx.log(ngx.DEBUG, "redis: ", ...)
end

local function getIp()
local myIP = ngx.req.get_headers()["X-Real-IP"]
if myIP == nil then
myIP = ngx.req.get_headers()["x_forwarded_for"]
end
if myIP == nil then
myIP = ngx.var.remote_addr
end
return myIP;
end

local key = "limit:ip:blacklist"
local ip = getIp();
local shared_ip_blacklist = ngx.shared.shared_ip_blacklist

--获得本地缓存的最新刷新时间
local last_update_time = shared_ip_blacklist:get("last_update_time");

if last_update_time ~= nil then
local dif_time = ngx.now() - last_update_time
if dif_time < 60 then --缓存1分钟,没有过期
if shared_ip_blacklist:get(ip) then
return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
end
return
end
end

local redis = require "resty.redis"  --引入redis模块
local red = redis:new()  --创建一个对象,注意是用冒号调用的

--设置超时(毫秒)
red:set_timeout(1000)
--建立连接
local ip = "192.168.5.202"
local port = 6379
local ok, err = red:connect(ip, port)
if not ok then
close_redis(red)
errlog("limit ip cannot connect redis");
else
local ip_blacklist, err = red:smembers(key);

if err then
errlog("limit ip smembers");
else
--刷新本地缓存,重新设置
shared_ip_blacklist:flush_all();

--同步redis黑名单 到 本地缓存
for i,bip in ipairs(ip_blacklist) do
--本地缓存redis中的黑名单
shared_ip_blacklist:set(bip,true);
end
--设置本地缓存的最新更新时间
shared_ip_blacklist:set("last_update_time",ngx.now());
end
end

if shared_ip_blacklist:get(ip) then
return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
end

用户redis客户端设置 sadd limit:ip:blacklist 192.168.31.247

#42、nginx实现接口签名安全认证

一)需求背景 现在app客户端请求后台服务是非常常用的请求方式,在我们写开放api接口时如何保证数据的安全, 我们先看看有哪些安全性的问题

请求来源(身份)是否合法?
请求参数被篡改?
请求的唯一性(不可复制)

二)为了保证数据在通信时的安全性,我们可以采用参数签名的方式来进行相关验证。 案例: 我们通过给某 [移动端(app)] 写 [后台接口(api)] 的案例进行分析:
客户端: 以下简称app 后台接口:以下简称api

我们通过app查询产品列表这个操作来进行分析:
app中点击查询按钮==》调用api进行查询==》返回查询结果==>显示在app中

一、不进行验证的方式 api查询接口:/getproducts?参数 app调用:http://api.test.com/getproducts?参数1=value1....... 如上,这种方式简单粗暴,通过调用getproducts方法即可获取产品列表信息了,但是 这样的方式会存在很严重的安全性问题, 没有进行任何的验证,大家都可以通过这个方法获取到产品列表,导致产品信息泄露。 那么,如何验证调用者身份呢?如何防止参数被篡改呢?

二、MD5参数签名的方式

我们对api查询产品接口进行优化:

1.给app客户端分配对应的key=1、secret秘钥

2.Sign签名,调用API 时需要对请求参数进行签名验证,签名方式如下:

a. 按照请求参数名称将所有请求参数按照字母先后顺序排序得到:keyvaluekeyvalue...keyvalue
字符串如:将arong=1,mrong=2,crong=3 排序为:arong=1, crong=3,mrong=2  然后将参数名和参数值进行拼接
得到参数字符串:arong1crong3mrong2。
b. 将secret加在参数字符串的头部后进行MD5加密 ,加密后的字符串需大写。即得到签名Sign

新api接口代码:
app调用:http://api.test.com/getproducts?key=app_key&sign=BCC7C71CF93F9CDBDB88671B701D8A35&参数1=value1&参数2=value2.......
注:secret 仅作加密使用, 为了保证数据安全请不要在请求参数中使用。

如上,优化后的请求多了key和sign参数,这样请求的时候就需要合法的key和正确签名sign才可以获取产品数据。

这样就解决了身份验证和防止参数篡改问题,如果请求参数被人拿走,没事,他们永远也拿不到secret,因为secret是不传递的。
再也无法伪造合法的请求。

http://api.test.com/getproducts?a=1&c=world&b=hello

http://api.test.com/getproducts?a=1&c=world&b=hello&key=1&sign=BCC7C71CF93F9CDBDB88671B701D8A35

客户端的算法 要和 我们服务器端的算法是一致的

“a=1&b=hello&c=world&key=1”
和秘钥进行拼接
secret=123456

“a=1&b=hello&c=world&123456”  =》md5 加密   ===》字符串sign= BCC7C71CF93F9CDBDB88671B701D8A35

-----------------------------------

http://api.test.com/getproducts?a=1&c=world&b=hello&key=2&sign=BCC7C71CF93F9CDBDB88671B701D8A35

key去判断 是否客户端身份是合法
参数是否被篡改   服务器这边 也去生成一个sign签名,算法和客户端一致
a=2&c=world&b=hello  ==>"a=2&b=hello&c=world" =>secret=123456==>"a=2&b=hello&c=world&123456" ==>md5
===》服务器生成的sign ===》如果和客户端传过来的sign一致,就代表合法===》验证参数是否被篡改

三、不可复制

第二种方案就够了吗?我们会发现,如果我获取了你完整的链接,一直使用你的key和sign和一样的参数不就可以正常获取数据了...-_-!是的,仅仅是如上的优化是不够的

请求的唯一性:
为了防止别人重复使用请求参数问题,我们需要保证请求的唯一性,就是对应请求只能使用一次,
这样就算别人拿走了请求的完整链接也是无效的。

唯一性的实现:在如上的请求参数中,我们加入时间戳 timestamp(yyyyMMddHHmmss),同样,时间戳作为请求参数之一,
也加入sign算法中进行加密。

新的api接口:
app调用:
http://api.test.com/getproducts?key=app_key&sign=BCC7C71CF93F9CDBDB88671B701D8A35&timestamp=201803261407&参数1=value1&参数2=value2.......

http://api.test.com/getproducts?a=1&c=world&b=hello

http://api.test.com/getproducts?a=1&c=world&b=hello&key=1&sign=BCC7C71CF93F9CDBDB88671B701D8A35&time=201801232

time是客户端发起请求的那一时刻,传过来的

客户端的算法 要和 我们服务器端的算法是一致的

“a=1&b=hello&c=world&time=201801232”
和秘钥进行拼接
secret=123456

“a=1&b=hello&c=world&time=201801232&123456”  =》md5 加密   ===》字符串sign= BCC7C71CF93F9CDBDB88671B701D8A35

---------------------------------

key=1 是否身份验证合法
time=客户端在调用这个接口那一刻传的时间
服务器去处理这个接口请求的当前时间  相减,如果这个大于10s;;;这个链接应该是被人家截取
如果小于10s,表示正常请求

如上,我们通过timestamp时间戳用来验证请求是否过期。这样就算被人拿走完整的请求链接也是无效的。

Sign签名安全性分析:
通过上面的案例,我们可以看出,安全的关键在于参与签名的secret,整个过程中secret是不参与通信的,
所以只要保证secret不泄露,请求就不会被伪造。

总结
上述的Sign签名的方式能够在一定程度上防止信息被篡改和伪造,保障通信的安全,这里使用的是MD5进行加密,
当然实际使用中大家可以根据实际需求进行自定义签名算法,比如:RSA,SHA等。

-----------------------------------------
编辑nginx.conf
location /sign {
access_by_lua_file /usr/local/lua/access_by_sign.lua;
echo "sign验证成功";
}

==============================编辑access_by_sign.lua

--判断table是否为空
local function isTableEmpty(t)
return t == nil or next(t) == nil
end

--两个table合并
local function union(table1,table2)
for k, v in pairs(table2) do
table1[k] = v
end
return table1
end

--检验请求的sign签名是否正确
--params:传入的参数值组成的table
--secret:项目secret,根据key找到secret
local function signcheck(params,secret)
--判断参数是否为空,为空报异常
if isTableEmpty(params) then
local mess="参数为空"
ngx.log(ngx.ERR, mess)
return false,mess
end

if secret == nil then
local mess="私钥为空"
ngx.log(ngx.ERR, mess)
return false,mess
end

local key = params["key"]; --平台分配给某客户端类型的keyID
if key == nil then
local mess="key值为空"
ngx.log(ngx.ERR, mess)
return false,mess
end

--判断是否有签名参数
local sign = params["sign"]
if sign == nil then
local mess="签名参数为空"
ngx.log(ngx.ERR, mess)
return false,mess
end

--是否存在时间戳的参数
local timestamp = params["time"]
if timestamp == nil then
local mess="时间戳参数为空"
ngx.log(ngx.ERR, mess)
return false,mess
end

--时间戳有没有过期,10秒过期
local now_mill = ngx.now() * 1000 --毫秒级
if now_mill - timestamp > 10000 then
local mess="链接过期"
ngx.log(ngx.ERR, mess)
return false,mess
end

local keys, tmp = {}, {}

--提出所有的键名并按字符顺序排序
for k, _ in pairs(params) do
if k ~= "sign" then --去除掉
keys[#keys+1]= k
end
end
table.sort(keys)
--根据排序好的键名依次读取值并拼接字符串成key=value&key=value
for _, k in pairs(keys) do
if type(params[k]) == "string" or type(params[k]) == "number" then
tmp[#tmp+1] = k .. "=" .. tostring(params[k])
end
end

--将salt添加到最后,计算正确的签名sign值并与传入的sign签名对比,
local signchar = table.concat(tmp, "&") .."&"..secret
local rightsign = ngx.md5(signchar);
if sign ~= rightsign then
--如果签名错误返回错误信息并记录日志,
local mess="sign error: sign,"..sign .. " right sign:" ..rightsign.. " sign_char:" .. signchar
ngx.log(ngx.ERR, mess)
return false,mess
end
return true
end

local params = {}

local get_args = ngx.req.get_uri_args();
ngx.req.read_body()
local post_args = ngx.req.get_post_args();

union(params,get_args)

union(params,post_args)

local secret = "123456"  --根据keyID到后台服务获取secret

local checkResult,mess = signcheck(params,secret)

if not checkResult then
ngx.say(mess);
return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
end

============================================================================

java代码,模仿请求
package com.rainbow.sign;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SignApplication {

public static void main(String[] args) throws IOException {
SpringApplication.run(SignApplication.class, args);

HashMap<String,String> params = new HashMap<String,String>();

params.put("key", "1");
params.put("a", "1");
params.put("c", "w");
params.put("b", "2");

long time = new Date().getTime();

params.put("time", "" + time);

System.out.println(time);

String sign = getSignature(params,"123456");

System.out.println(sign);

params.put("sign", sign);

String resp = HttpUtil.doGet("http://192.168.31.150/sign",params);

System.out.println(resp);
}

/**
* 签名生成算法
* @param HashMap<String,String> params 请求参数集,所有参数必须已转换为字符串类型
* @param String secret 签名密钥
* @return 签名
* @throws IOException
*/
public static String getSignature(HashMap<String,String> params, String secret) throws IOException
{
// 先将参数以其参数名的字典序升序进行排序
Map<String, String> sortedParams = new TreeMap<String, String>(params);
Set<Entry<String, String>> entrys = sortedParams.entrySet();

// 遍历排序后的字典,将所有参数按"key=value"格式拼接在一起
StringBuilder basestring = new StringBuilder();
for (Entry<String, String> param : entrys) {
if(basestring.length() != 0){
basestring.append("&");
}
basestring.append(param.getKey()).append("=").append(param.getValue());
}
basestring.append("&");
basestring.append(secret);

System.out.println("basestring="+basestring);

// 使用MD5对待签名串求签
byte[] bytes = null;
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
bytes = md5.digest(basestring.toString().getBytes("UTF-8"));
} catch (GeneralSecurityException ex) {
throw new IOException(ex);
}

String strSign = new String(bytes);
System.out.println("strSign="+strSign);
// 将MD5输出的二进制结果转换为小写的十六进制
StringBuilder sign = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] & 0xFF);
if (hex.length() == 1) {
sign.append("0");
}
sign.append(hex);
}
return sign.toString();
}
}

#43、nginx实现网关一之网关框架介绍

#44、nginx实现网关二之网关主入口设计

#45、nginx实现网关三之可配置插件设计

#46、nginx实现网关四之加载插件

rainbow根据配置文件加载插件

编写加载配置文件的方法

一)首先我们先要编写读取配置文件内容的类库

---------------io.lua--------------------

---
-- :P some origin code is from https://github.com/Mashape/kong/blob/master/kong/tools/io.lua
-- modified by sumory.wu

local stringy = require("rainbow.common.stringy")

local _M = {}

---
-- Checks existence of a file.
-- @param path path/file to check
-- @return `true` if found, `false` + error message otherwise
function _M.file_exists(path)
local f, err = io.open(path, "r")
if f ~= nil then
io.close(f)
return true
else
return false, err
end
end

---
-- Execute an OS command and catch the output.
-- @param command OS command to execute
-- @return string containing command output (both stdout and stderr)
-- @return exitcode
function _M.os_execute(command, preserve_output)
local n = os.tmpname() -- get a temporary file name to store output
local f = os.tmpname() -- get a temporary file name to store script
_M.write_to_file(f, command)
local exit_code = os.execute("/bin/bash "..f.." > "..n.." 2>&1")
local result = _M.read_file(n)
os.remove(n)
os.remove(f)
return preserve_output and result or string.gsub(string.gsub(result, "^"..f..":[%s%w]+:%s*", ""), "[%\r%\n]", ""), exit_code / 256
end

---
-- Check existence of a command.
-- @param cmd command being searched for
-- @return `true` of found, `false` otherwise
function _M.cmd_exists(cmd)
local _, code = _M.os_execute("hash "..cmd)
return code == 0
end

--- Kill a process by PID.
-- Kills the process and waits until it's terminated
-- @param pid_file the file containing the pid to kill
-- @param signal the signal to use
-- @return `os_execute` results, see os_execute.
function _M.kill_process_by_pid_file(pid_file, signal)
if _M.file_exists(pid_file) then
local pid = stringy.strip(_M.read_file(pid_file))
return _M.os_execute("while kill -0 "..pid.." >/dev/null 2>&1; do kill "..(signal and "-"..tostring(signal).." " or "")..pid.."; sleep 0.1; done")
end
end

--- Read file contents.
-- @param path filepath to read
-- @return file contents as string, or `nil` if not succesful
function _M.read_file(path)
local contents
local file = io.open(path, "rb")
if file then
contents = file:read("*all")
file:close()
end
return contents
end

--- Write file contents.
-- @param path filepath to write to
-- @return `true` upon success, or `false` + error message on failure
function _M.write_to_file(path, value)
local file, err = io.open(path, "w")
if err then
return false, err
end

file:write(value)
file:close()
return true
end

--- Get the filesize.
-- @param path path to file to check
-- @return size of file, or `nil` on failure
function _M.file_size(path)
local size
local file = io.open(path, "rb")
if file then
size = file:seek("end")
file:close()
end
return size
end

return _M

二)编写一个针对字符串的工具类

-------------------stringy.lua-----------------------

local string_gsub = string.gsub
local string_find = string.find
local table_insert = table.insert

local _M = {}

function _M.trim_all(str)
if not str or str == "" then return "" end
local result = string_gsub(str, " ", "")
return result
end

function _M.strip(str)
if not str or str == "" then return "" end
local result = string_gsub(str, "^ *", "")
result = string_gsub(result, "( *)$", "")
return result
end

function _M.split(str, delimiter)
if not str or str == "" then return {} end
if not delimiter or delimiter == "" then return { str } end

local result = {}
for match in (str .. delimiter):gmatch("(.-)" .. delimiter) do
table_insert(result, match)
end
return result
end

function _M.startswith(str, substr)
if str == nil or substr == nil then
return false
end
if string_find(str, substr) ~= 1 then
return false
else
return true
end
end

function _M.endswith(str, substr)
if str == nil or substr == nil then
return false
end
local str_reverse = string.reverse(str)
local substr_reverse = string.reverse(substr)
if string.find(str_reverse, substr_reverse) ~= 1 then
return false
else
return true
end
end

return _M

三)编写一个字符串与json对象编码的工具类

-----------------------json.lua-----------------------------

local cjson = require("cjson.safe")

local _M = {}

function _M.encode(data, empty_table_as_object)
if not data then return nil end

if cjson.encode_empty_table_as_object then
-- empty table default is arrya
cjson.encode_empty_table_as_object(empty_table_as_object or false)
end

if require("ffi").os ~= "Windows" then
cjson.encode_sparse_array(true)
end

return cjson.encode(data)
end

function _M.decode(data)
if not data then return nil end

return cjson.decode(data)
end

return _M

四)编写配置文件加载类库

-------------------config_loader.lua---------------------

local json = require("rainbow.common.json")
local IO = require("rainbow.common.io")

local _M = {}

function _M.load(config_path)
config_path = config_path or "/etc/rainbow/rainbow.conf"
local config_contents = IO.read_file(config_path)

if not config_contents then
ngx.log(ngx.ERR, "No configuration file at: ", config_path)
os.exit(1)
end

local config = json.decode(config_contents)
return config, config_path
end

return _M

五)改造主入口rainbow.lua

-------------------rainbow.lua------------------------

local utils = require("rainbow.common.utils")
local config_loader = require("rainbow.common.config_loader")

local function load_node_plugins(config)
utils.debug_log("===========load_node_plugins============");
local plugins = config.plugins --插件列表
local sorted_plugins = {} --按照优先级的插件集合
for _, v in ipairs(plugins) do
local loaded, plugin_handler = utils.load_module_if_exists("rainbow.plugins." .. v .. ".handler")
if not loaded then
utils.warn_log("The following plugin is not installed or has no handler: " .. v)
else
utils.debug_log("Loading plugin: " .. v)
table.insert(sorted_plugins, {
name = v,
handler = plugin_handler(), --插件
})
end
end
--表按照优先级排序
table.sort(sorted_plugins, function(a, b)
local priority_a = a.handler.PRIORITY or 0
local priority_b = b.handler.PRIORITY or 0
return priority_a > priority_b
end)

return sorted_plugins
end

local rainbow = {}

function rainbow.init(options)
options = options or {}
local config
local status, err = pcall(function()
--rainbow的配置文件路径
local conf_file_path = options.config
utils.debug_log("Loading rainbow conf : " .. conf_file_path)
config = config_loader.load(conf_file_path)
--加载配置的插件
loaded_plugins = load_node_plugins(config)
end)

if not status or err then
utils.error_log("Startup error: " .. err)
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end

ngx.log(ngx.DEBUG, "===========rainbow.init============");
end

六)更改rainbow-nginx.conf配置文件

init_by_lua_block {

local rainbow = require("rainbow.rainbow")

local config_file = "/usr/local/lua/rainbow/conf/rainbow.conf"

rainbow.init({
config = config_file
})

context = {
rainbow = rainbow
}
}

启动nginx,查看nginx日志

2018/04/28 18:33:15 [notice] 1565#0: signal 1 (SIGHUP) received from 2585, reconfiguring
2018/04/28 18:33:15 [notice] 1565#0: reconfiguring
2018/04/28 18:33:15 [debug] 1565#0: [lua] utils.lua:6: debug_log(): Loading rainbow conf : /usr/local/lua/rainbow/conf/rainbow.conf
2018/04/28 18:33:15 [debug] 1565#0: [lua] utils.lua:6: debug_log(): ===========load_node_plugins============
2018/04/28 18:33:15 [debug] 1565#0: [lua] utils.lua:6: debug_log(): Loading plugin: sign_auth
2018/04/28 18:33:15 [debug] 1565#0: [lua] utils.lua:6: debug_log(): BasePlugin executing plugin "sign_auth-plugin": new
2018/04/28 18:33:15 [debug] 1565#0: [lua] utils.lua:6: debug_log(): ===========SignAuthHandler.new============
2018/04/28 18:33:15 [debug] 1565#0: [lua] rainbow.lua:50: init(): ===========rainbow.init============
2018/04/28 18:33:15 [notice] 1565#0: using the "epoll" event method
2018/04/28 18:33:15 [notice] 1565#0: start worker processes
2018/04/28 18:33:15 [notice] 1565#0: start worker process 2586
2018/04/28 18:33:15 [debug] 2586#0: *20 [lua] rainbow.lua:54: init_worker(): ===========rainbow.init_worker============
2018/04/28 18:33:15 [notice] 2583#0: gracefully shutting down
2018/04/28 18:33:15 [notice] 2583#0: exiting
2018/04/28 18:33:15 [notice] 2583#0: exit
2018/04/28 18:33:15 [notice] 1565#0: signal 17 (SIGCHLD) received from 2583
2018/04/28 18:33:15 [notice] 1565#0: worker process 2583 exited with code 0
2018/04/28 18:33:15 [notice] 1565#0: signal 29 (SIGIO) received

到此我们就实现了插件的加载管理,可以动态的配置添加插件

#47、nginx实现网关五之实现签名验证插件

我们这节就重点实现一个签名验证,就是把我们之前的实现的签名验证转变为插件,集成到网关设计中

一)针对sign_auth插件目录下的handler.lua进行改造

---------------------handler.lua-----------------------

local utils = require("rainbow.common.utils")
local BasePlugin = require("rainbow.plugins.base_plugin")
local redis = require "resty.redis"  --引入redis模块

local function close_redis(red)
if not red then
return
end
--释放连接(连接池实现)
local pool_max_idle_time = 10000 --毫秒
local pool_size = 100 --连接池大小
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
utils.error_log("set keepalive error : "..err)
end
end

--检验请求的sign签名是否正确
--params:传入的参数值组成的table
--secret:项目secret,根据appid找到secret
local function signcheck(params,secret)
--判断参数是否为空,为空报异常
if utils.isTableEmpty(params) then
local mess="params table is empty"
utils.error_log(mess)
return false,mess
end

--判断是否有签名参数
local sign = params["sign"]
if sign == nil then
local mess="params sign is nil"
utils.error_log(mess)
return false,mess
end

--是否存在时间戳的参数
local timestamp = params["time"]
if timestamp == nil then
local mess="params timestamp is nil"
utils.error_log(mess)
return false,mess
end
--时间戳有没有过期,10秒过期
local now_mill = ngx.now() * 1000 --毫秒级
if now_mill - timestamp > 10000 then
local mess="params timestamp is 过期"
utils.error_log(mess)
return false,mess
end

local keys, tmp = {}, {}

--提出所有的键名并按字符顺序排序
for k, _ in pairs(params) do
if k ~= "sign" then --去除掉
keys[#keys+1]= k
end
end
table.sort(keys)
--根据排序好的键名依次读取值并拼接字符串成key=value&key=value
for _, k in pairs(keys) do
if type(params[k]) == "string" or type(params[k]) == "number" then
tmp[#tmp+1] = k .. "=" .. tostring(params[k])
end
end
--将salt添加到最后,计算正确的签名sign值并与传入的sign签名对比,
local signchar = table.concat(tmp, "&") .."&"..secret
local rightsign = ngx.md5(signchar);
if sign ~= rightsign then
--如果签名错误返回错误信息并记录日志,
local mess="sign error: sign,"..sign .. " right sign:" ..rightsign.. " sign_char:" .. signchar
utils.error_log(mess)
return false,mess
end
return true
end

local SignAuthHandler = BasePlugin:extend()
SignAuthHandler.PRIORITY = 0

function SignAuthHandler:new()
SignAuthHandler.super.new(self, "sign_auth-plugin")
utils.debug_log("===========SignAuthHandler.new============");
end

function SignAuthHandler:access()
SignAuthHandler.super.access(self)
utils.debug_log("===========SignAuthHandler.access============");
local params = {}

local get_args = ngx.req.get_uri_args();

local appid = get_args["appid"];

if appid == nil then
ngx.say("appid is empty,非法请求");
return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
end

ngx.req.read_body()
local post_args = ngx.req.get_post_args();

utils.union(params,get_args)
params = utils.union(params,post_args)

local red = redis:new()  --创建一个对象,注意是用冒号调用的

--设置超时(毫秒)
red:set_timeout(1000)
--建立连接
local host = "192.168.31.247"
local port = 6379
local ok, err = red:connect(host, port)
if not ok then
close_redis(red)
utils.error_log("Cannot connect");
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
end

--得到此appid对应的secret
local resp, err = red:hget("apphash",appid)
if not resp or (resp == ngx.null) then
close_redis(red)
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) --redis 获取值失败
end
--resp存放着就是appid对应的secret
local checkResult,mess = signcheck(params,resp)

if not checkResult then
ngx.say(mess);
return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
end
end

return SignAuthHandler;

二)在主入口rainbow.lua中access阶段进行改造

-----------------------rainbow.lua---------------------

function rainbow.access()
ngx.log(ngx.DEBUG, "===========rainbow.access============");
for _, plugin in ipairs(loaded_plugins) do
ngx.log(ngx.DEBUG, "==rainbow.access name==" .. plugin.name);
plugin.handler:access()
end
end

三)在utils工具类加入对table的操作

-----------------------utils.lua---------------------

--判断table是否为空
function _M.isTableEmpty(t)
return t == nil or next(t) == nil
end

--两个table合并
function _M.union(table1,table2)
for k, v in pairs(table2) do
table1[k] = v
end
return table1
end

启动nginx,利用之前编写的java代码,模拟请求

#48、nginx实现网关六之实现黑名单插件

背景
为了封禁某些爬虫或者恶意用户对服务器的请求,我们需要建立一个动态的 IP 黑名单。对于黑名单之内的 IP ,拒绝提供服务。

一)在plugins目录下新建limit_ip目录,代表插件名称;并在其目录下新建handler.lua

拷贝之前二次封装的redis类库
-----------------------------redis.lua---------------------
local redis_c = require "resty.redis"

local ok, new_tab = pcall(require, "table.new")
if not ok or type(new_tab) ~= "function" then
new_tab = function (narr, nrec) return {} end
end

local _M = new_tab(0, 155)
_M._VERSION = '0.01'

local commands = {
"append",            "auth",              "bgrewriteaof",
"bgsave",            "bitcount",          "bitop",
"blpop",             "brpop",
"brpoplpush",        "client",            "config",
"dbsize",
"debug",             "decr",              "decrby",
"del",               "discard",           "dump",
"echo",
"eval",              "exec",              "exists",
"expire",            "expireat",          "flushall",
"flushdb",           "get",               "getbit",
"getrange",          "getset",            "hdel",
"hexists",           "hget",              "hgetall",
"hincrby",           "hincrbyfloat",      "hkeys",
"hlen",
"hmget",              "hmset",      "hscan",
"hset",
"hsetnx",            "hvals",             "incr",
"incrby",            "incrbyfloat",       "info",
"keys",
"lastsave",          "lindex",            "linsert",
"llen",              "lpop",              "lpush",
"lpushx",            "lrange",            "lrem",
"lset",              "ltrim",             "mget",
"migrate",
"monitor",           "move",              "mset",
"msetnx",            "multi",             "object",
"persist",           "pexpire",           "pexpireat",
"ping",              "psetex",            "psubscribe",
"pttl",
"publish",      --[[ "punsubscribe", ]]   "pubsub",
"quit",
"randomkey",         "rename",            "renamenx",
"restore",
"rpop",              "rpoplpush",         "rpush",
"rpushx",            "sadd",              "save",
"scan",              "scard",             "script",
"sdiff",             "sdiffstore",
"select",            "set",               "setbit",
"setex",             "setnx",             "setrange",
"shutdown",          "sinter",            "sinterstore",
"sismember",         "slaveof",           "slowlog",
"smembers",          "smove",             "sort",
"spop",              "srandmember",       "srem",
"sscan",
"strlen",       --[[ "subscribe",  ]]     "sunion",
"sunionstore",       "sync",              "time",
"ttl",
"type",         --[[ "unsubscribe", ]]    "unwatch",
"watch",             "zadd",              "zcard",
"zcount",            "zincrby",           "zinterstore",
"zrange",            "zrangebyscore",     "zrank",
"zrem",              "zremrangebyrank",   "zremrangebyscore",
"zrevrange",         "zrevrangebyscore",  "zrevrank",
"zscan",
"zscore",            "zunionstore",       "evalsha"
}

local mt = { __index = _M }

local function is_redis_null( res )
if type(res) == "table" then
for k,v in pairs(res) do
if v ~= ngx.null then
return false
end
end
return true
elseif res == ngx.null then
return true
elseif res == nil then
return true
end

return false
end

function _M.close_redis(self, redis)
if not redis then
return
end
--释放连接(连接池实现)
local pool_max_idle_time = self.pool_max_idle_time --最大空闲时间 毫秒
local pool_size = self.pool_size --连接池大小

local ok, err = redis:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx.say("set keepalive error : ", err)
end
end

-- change connect address as you need
function _M.connect_mod( self, redis )
redis:set_timeout(self.timeout)

local ok, err = redis:connect(self.ip, self.port)
if not ok then
ngx.say("connect to redis error : ", err)
return self:close_redis(redis)
end

if self.password ~= "" then ----密码认证

local count, err = redis:get_reused_times()
if 0 == count then ----新建连接,需要认证密码
ok, err = redis:auth(self.password)
if not ok then
ngx.say("failed to auth: ", err)
return
end
elseif err then  ----从连接池中获取连接,无需再次认证密码
ngx.say("failed to get reused times: ", err)
return
end
end

return ok,err;
end

function _M.init_pipeline( self )
self._reqs = {}
end

function _M.commit_pipeline( self )
local reqs = self._reqs

if nil == reqs or 0 == #reqs then
return {}, "no pipeline"
else
self._reqs = nil
end

local redis, err = redis_c:new()
if not redis then
return nil, err
end

local ok, err = self:connect_mod(redis)
if not ok then
return {}, err
end

redis:init_pipeline()
for _, vals in ipairs(reqs) do
local fun = redis[vals[1]]
table.remove(vals , 1)

fun(redis, unpack(vals))
end

local results, err = redis:commit_pipeline()
if not results or err then
return {}, err
end

if is_redis_null(results) then
results = {}
ngx.log(ngx.WARN, "is null")
end
-- table.remove (results , 1)

--self.set_keepalive_mod(redis)
self:close_redis(redis)

for i,value in ipairs(results) do
if is_redis_null(value) then
results[i] = nil
end
end

return results, err
end

local function do_command(self, cmd, ... )
if self._reqs then
table.insert(self._reqs, {cmd, ...})
return
end

local redis, err = redis_c:new()
if not redis then
return nil, err
end

local ok, err = self:connect_mod(redis)
if not ok or err then
return nil, err
end

redis:select(self.db_index)

local fun = redis[cmd]
local result, err = fun(redis, ...)
if not result or err then
-- ngx.log(ngx.ERR, "pipeline result:", result, " err:", err)
return nil, err
end

if is_redis_null(result) then
result = nil
end

--self.set_keepalive_mod(redis)
self:close_redis(redis)

return result, err
end

for i = 1, #commands do
local cmd = commands[i]
_M[cmd] =
function (self, ...)
return do_command(self, cmd, ...)
end
end

function _M.new(self, opts)
opts = opts or {}
local timeout = (opts.timeout and opts.timeout * 1000) or 1000
local db_index= opts.db_index or 0
local ip = opts.ip or '127.0.0.1'
local port = opts.port or 6379
local password = opts.password or ""
local pool_max_idle_time = opts.pool_max_idle_time or 60000
local pool_size = opts.pool_size or 100

return setmetatable({
timeout = timeout,
db_index = db_index,
ip = ip,
port = port,
password = password,
pool_max_idle_time = pool_max_idle_time,
pool_size = pool_size,
_reqs = nil }, mt)
end

return _M

-----------------------handler.lua-------------------------

local utils = require("rainbow.common.utils")
local redis = require("rainbow.lib.redis")  --引入redis模块
local BasePlugin = require("rainbow.plugins.base_plugin")

local opts = {
ip = "192.168.31.247",
port = "6379",
password = "123456",
db_index = 0
}

local LimitIpHandler = BasePlugin:extend()
LimitIpHandler.PRIORITY = 2

function LimitIpHandler:new()
LimitIpHandler.super.new(self, "limit_ip-plugin")
utils.debug_log("===========LimitIpHandler.new============");
end

function LimitIpHandler:access()
LimitIpHandler.super.access(self)
utils.debug_log("===========LimitIpHandler.access============");

local key = "limit:ip:blacklist";
local user_ip = utils.get_ip();
local shared_ip_blacklist = ngx.shared.shared_ip_blacklist;

--获得本地缓存的最新刷新时间
local last_update_time = shared_ip_blacklist:get("last_update_time");

if last_update_time ~= nil then
local dif_time = ngx.now() - last_update_time
if dif_time < 60 then --缓存1分钟,没有过期
if shared_ip_blacklist:get(user_ip) then
return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
end
end
end

local red = redis:new(opts)  --创建一个对象,注意是用冒号调用的

local ip_blacklist, err = red:smembers(key);
if err then
utils.error_log("limit ip smembers");
else
--刷新本地缓存,重新设置
shared_ip_blacklist:flush_all();

if ip_blacklist ~= nil then
for i,bip in ipairs(ip_blacklist) do
--本地缓存redis中的黑名单
shared_ip_blacklist:set(bip,true);
end
end

--设置本地缓存的最新更新时间
shared_ip_blacklist:set("last_update_time",ngx.now());
end

if shared_ip_blacklist:get(ip) then
return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
end

end

return LimitIpHandler;

-------------------------utils.lua-----------------------------

function _M.get_ip()
local myIP = ngx.req.get_headers()["X-Real-IP"]
if myIP == nil then
myIP = ngx.req.get_headers()["x_forwarded_for"]
end
if myIP == nil then
myIP = ngx.var.remote_addr
end
return myIP;
end

#49、nginx实现网关七之实现限制访问频率插件

设计限制访问频率的插件

一)在plugins目录下新建limit_frequency目录,代表插件名称;并在其目录下新建handler.lua

---------------------handler.lua-----------------------
local utils = require("rainbow.common.utils")
local redis = require("rainbow.lib.redis")  --引入redis模块
local BasePlugin = require("rainbow.plugins.base_plugin")

local opts = {
ip = "192.168.31.247",
port = "6379",
db_index = 0
}

local LimitFrequencyHandler = BasePlugin:extend()
LimitFrequencyHandler.PRIORITY = 1

function LimitFrequencyHandler:new()
LimitFrequencyHandler.super.new(self, "LimitFrequency-plugin")
utils.debug_log("===========LimitFrequencyHandler.new============");
end

function LimitFrequencyHandler:access()
LimitFrequencyHandler.super.access(self)
utils.debug_log("===========LimitFrequencyHandler.access============");

local user_ip = utils.get_ip();

local key = "limit:frequency:"..user_ip;

local red = redis:new(opts)  --创建一个对象,注意是用冒号调用的

--得到此客户端IP的频次
local resp, err = red:get(key)

if err then
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) --redis 获取值失败
end

if resp == nil then
utils.debug_log("===========key set ============");
local result,err = red:set(key, 1) -- 单位时间 第一次访问
utils.debug_log("===========key expire ============");
result,err = red:expire(key, 10) --10秒时间 过期
end

if type(resp) == "string" then
if tonumber(resp) > 10 then -- 超过10次
return ngx.exit(ngx.HTTP_FORBIDDEN) --直接返回403
end
end

--调用API设置key
local ok, err = red:incr(key)
if err then
return ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR) --redis 报错
end

end

return LimitFrequencyHandler;

二)在主入口rainbow.lua中access阶段进行改造

-----------------------rainbow.conf---------------------

{
"plugins": [
"limit_ip",
"limit_frequency"
]
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: