1. 程式人生 > 實用技巧 >Nginx+OpenResty+Lua+redis 從入門到精通

Nginx+OpenResty+Lua+redis 從入門到精通

整理筆記的時候,看見資料夾中出現了這樣一個筆記,想了想決定分享了出來
從零開始學閘道器,學習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],這裡 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"
    ]
}