運維實踐-最新Nginx二進位構建編譯lua-nginx-module動態鏈接Lua腳本訪問Redis資料庫讀取靜態資源隱式展現
設為「特別關注」每天帶你玩轉網路安全運維、應用開發、物聯網IOT學習!
希望各位看友【關注、點贊、評論、收藏、投幣】,助力每一個夢想。
本章目錄
- 0x0n 前言簡述
- 0x01 部署環境
- 0x02 使用實踐
- 0x03 擴展補充
- 0x0n 入坑出坑
- 問題1. 當編譯 Nginx 時報
checking for LuaJIT 2.x ... not found, ./configure: error: unsupported LuaJIT version; ngx_http_lua_module requires LuaJIT 2.x.
錯誤時的解決辦法。 - 問題2.在使用luajit官方主分支
LuaJIT-2.1.0-beta3
提供LuaJIT安裝部署出現nginx: [alert] detected a LuaJIT version which is not OpenResty's;
以及nginx: [alert] failed to load the 'resty.core' module
警告。
- 問題1. 當編譯 Nginx 時報
首發地址: //mp.weixin.qq.com/s/u-zb-BxG6VyaLY4EQLKlOQ
0x0n 前言簡述
為啥有此篇文章?
描述: 在進行公司的圖片存儲解決方案研究中,最開始準備使用的是FastDFS,但是經過深思熟慮,以及後期運維成本考慮還是放棄了,只能轉而使用存儲直接存放圖片文件,直接請求效率提示杠杠的,但如何最大限度保證業務安全以及減少業務對資料庫增刪改查的壓力? 在 Google 、Github一番查找後發現可以直接使用 Nginx + Lua 進行訪問數據進行獲取靜態資源資訊,而不用業務系統進行訪問資料庫直接獲取靜態資源路徑,而顯式的展現資源真實暴露給外部,非常容易被批量抓取。
其次筆者在實踐中發現當前搜索到的安裝部署Nginx+Lua可能已將不適用最新穩定版本的Nginx版本,基本上都是 1.15.x ~ 1.18.x,對於當前Nginx 1.22.0 版本來說顯然是太老了。
所以本章就該問題進行 Nginx + Lua + Redis
模組環境的安裝以及簡單的實踐,希望能幫助到各位有相同需求的Person。
基礎知識:
- Nginx: 是一個高性能的HTTP和反向代理web伺服器,同時也提供了IMAP/POP3/SMTP服務, 其三大核心功能,包含靜態資源、反向代理、api模組擴展,對於lua腳本的擴展,例如由lua-nginx-module模組,就是api模組擴展的一部分,並且nginx可以通過lua腳本直接調用redis伺服器;
- Lua: 是一種功能強大,高效,輕量級,可嵌入的腳本語言,非常容易嵌入到我們應用程式中, 它用於各種應用程式,從遊戲到Web應用程式和影像處理。
- lua-nginx-module : 該模組是 OpenResty 的核心組件,目錄是將lua的功能嵌入到Nginx http伺服器中。
- lua-resty-redis : 該模組是在 OpenResty 項目下基於 cosocket API 的 ngx_lua 的 Lua redis 客戶端驅動。
溫馨提示: 如果不是現有業務大量使用Nginx進行承載不能直接替換其它優秀的解決方案,只能一步一步來,從而轉入 OpenResty
或者 caddy
搭建能夠處理超高並發、擴展性極高的動態 Web 應用、Web 服務和動態網關。
原文地址: //blog.weiyigeek.top
知識引入
Nginx 的指令的都是安裝執行順序的嗎?
答: 既然我都這樣問了答案則是顯而易見的,這也是大多數新手頻繁遇到的一個困惑,當然也困惑了筆者,否則我也不會這樣問。
那我們下來來看這麼一個示例: (驗證此示例你可能需要先按照下一章的【0x01 部署環境】進行準備相關環境), 此時你可能會說輸出不就是WeiyiGeek
嗎?
location /sequence_demo_1 {
set $a Weiyi;
echo $a;
set $a Geek;
echo $a;
}
但如果請求該URL你會發現實時並非如此。
$ curl //demo.weiyigeek.top/sequence_demo_1
Geek
Geek
那為什麼出現了這種不合常理的現象呢?
答: 為了解釋此現象, 我們不得不介紹Nginx的請求處理的11階段,分別是post-read、server-rewrite、find-config、rewrite、post-rewrite、preaccess、access、post-access、precontent、content以及log,其中3個比較常見的按照執行時的先後順序依次是rewrite階段、access階段以及content階段。
Nginx 配置指令一般只會註冊並運行在其中的某一個處理階段,比如set
指令就是在rewrite
階段運行的,而echo
指令只會在content
階段運行, 在一次請求處理流程中rewrite
階段總是在content階段之前執行。
因此,屬於rewrite階段的配置指令(示例中的set)總是會無條件地在content階段的配置指令(示例中的echo)之前執行,即便是echo
指令出現在set
指令的前面, 上面例子中的指令按照請求處理階段的先後次序排序,實際的執行次序如下:
location /sequence_demo_1 {
# rewrite階段的配置指令,執行在前面
set $a Weiyi;
set $a Geek ;
# content階段的配置指令,執行在後面
echo $a;
echo $a;
}
所以,輸出的結果就是Weiyi Geek了。
Lua模組指令階段
各階段使用Lua模組指令
描述: 由於本章 Nginx 也是使用 OpenResty Lua 模組實現的解析Lua腳本,所以其指令我們也需要做一個簡單了解,對於後續學習有非常大的幫助。
指令語法: //github.com/openresty/lua-nginx-module#synopsis
使用Lua來構建nginx腳本就是通過一條條指令來完成的,指令常用於指定 Lua 程式碼是什麼時候執行的以及如何使用運行的結果,lua 指令分為配置指令、控制指令, 而控制指令分為兩種方式。
- lua腳本塊 :
*_by_lua_block
- lua腳本文件 :
*_by_lua_file
下圖展示了指令執行的順序:從上至下:初始化、重寫/訪問、內容處理、日誌輸出四個階段
lua-nginx-module Directives Document(Lua Nginx 模組指令文檔):
- lua_load_resty_core
- lua_capture_error_log
- lua_use_default_type
- lua_malloc_trim
- lua_code_cache
- lua_thread_cache_max_entries
- lua_regex_cache_max_entries
- lua_regex_match_limit
- lua_package_path
- lua_package_cpath
- init_by_lua
- init_by_lua_block
- init_by_lua_file
- init_worker_by_lua
- init_worker_by_lua_block
- init_worker_by_lua_file
- exit_worker_by_lua_block
- exit_worker_by_lua_file
- set_by_lua
- set_by_lua_block
- set_by_lua_file
- content_by_lua
- content_by_lua_block
- content_by_lua_file
- server_rewrite_by_lua_block
- server_rewrite_by_lua_file
- rewrite_by_lua
- rewrite_by_lua_block
- rewrite_by_lua_file
- access_by_lua
- access_by_lua_block
- access_by_lua_file
- header_filter_by_lua
- header_filter_by_lua_block
- header_filter_by_lua_file
- body_filter_by_lua
- body_filter_by_lua_block
- body_filter_by_lua_file
- log_by_lua
- log_by_lua_block
- log_by_lua_file
- balancer_by_lua_block
- balancer_by_lua_file
- lua_need_request_body
- ssl_client_hello_by_lua_block
- ssl_client_hello_by_lua_file
- ssl_certificate_by_lua_block
- ssl_certificate_by_lua_file
- ssl_session_fetch_by_lua_block
- ssl_session_fetch_by_lua_file
- ssl_session_store_by_lua_block
- ssl_session_store_by_lua_file
- lua_shared_dict
- lua_socket_connect_timeout
- lua_socket_send_timeout
- lua_socket_send_lowat
- lua_socket_read_timeout
- lua_socket_buffer_size
- lua_socket_pool_size
- lua_socket_keepalive_timeout
- lua_socket_log_errors
- lua_ssl_ciphers
- lua_ssl_crl
- lua_ssl_protocols
- lua_ssl_trusted_certificate
- lua_ssl_verify_depth
- lua_ssl_conf_command
- lua_http10_buffering
- rewrite_by_lua_no_postpone
- access_by_lua_no_postpone
- lua_transform_underscores_in_response_headers
- lua_check_client_abort
- lua_max_pending_timers
- lua_max_running_timers
- lua_sa_restart
- lua_worker_thread_vm_pool_size
值得注意的是Nginx可以提前終止請求(至少),這意味著跳過正常運行的階段,例如重寫或訪問階段。這也意味著,不管運行的後期階段(例如log_by_lua)將無法訪問通常在這些階段中設置的資訊。
400 (Bad Request)
405 (Not Allowed)
408 (Request Timeout)
413 (Request Entity Too Large)
414 (Request URI Too Large)
494 (Request Headers Too Large)
499 (Client Closed Request)
500 (Internal Server Error)
501 (Not Implemented)
好了,此處就只是先簡單點一下,在後續實踐中您在回過頭來看即可。
0x01 部署環境
安裝說明
環境描述:
# 系統資訊
$ cat /etc/issue.net
Ubuntu 20.04.3 LTS
$ uname -a
Linux weiyigeek.top 5.4.0-92-generic \#103-Ubuntu SMP Fri Nov 26 16:13:00 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
# 軟體版本
Nginx - 1.22.0 (stable 版本)
pcre - 8.45
zlib - 1.2.12
Lua - 5.4
openssl - 1.1.1q
ngx_devel_kit - v0.3.1
lua-nginx-module - v0.10.21
echo-nginx-module - v0.62
lua-resty-core - v0.1.23
lua-resty-lrucache - v0.13
lua-resty-redis - v0.29
溫馨提示: 此處使用的是 Ubuntu 20.04 作業系統, 該系統已做安全加固和內核優化符合等保2.0要求【SecOpsDev/Ubuntu-InitializeSecurity.sh at master · WeiyiGeek/SecOpsDev 】, 如你的Linux未進行相應配置環境可能與讀者有些許差異, 如需要進行(windows server、Ubuntu、CentOS)安全加固請參照如下加固腳本進行加固, 請大家瘋狂的 star 。
加固腳本地址:【 //github.com/WeiyiGeek/SecOpsDev/blob/master/OS-作業系統/Linux/Ubuntu/Ubuntu-InitializeSecurity.sh 】
為了節省大家的實踐時間,我已經把需要用到的源碼包上傳到空間中,有需要的朋友可以看一下,下載地址: [//share.weiyigeek.top/d/36158960-50338508-7c5982?p=2088](//share.weiyigeek.top/d/36158960-50338508-7c5982?p=2088)(訪問密碼:2088)
溫馨提示: 如提示證書不對,請點擊高級繼續訪問即可.
安裝部署
源程式碼編譯構建
Step 1.在 Ubuntu 20.04 LTS 系統安裝編譯所需環境.
apt install -y gcc g++ make perl net-tools
Step 2.下載 Nginx、PCRE、zlib、OpenSSL 源程式碼包,並編譯構建 PCRE、zlib、OpenSSL
.
cd /usr/local/src
# Nginx 輕量級的Web代理伺服器。
# 官網: //nginx.org/en/download.html
wget -c //nginx.org/download/nginx-1.22.0.tar.gz -O /usr/local/src/nginx-1.22.0.tar.gz
tar -zxf nginx-1.22.0.tar.gz
# PCRE – 支援正則表達式,NGINX Core 和 Rewrite 模組需要
# 官網: //pcre.org/
wget -c //nchc.dl.sourceforge.net/project/pcre/pcre/8.45/pcre-8.45.tar.bz2
tar -jxf pcre-8.45.tar.bz2 && cd pcre-8.45
./configure
make && sudo make install
# zlib – 支援標頭壓縮, NGINX Gzip 模組需要。
# 官網://www.zlib.net/
wget -c //www.zlib.net/zlib-1.2.12.tar.gz
tar -zxf zlib-1.2.12.tar.gz && cd zlib-1.2.12
./configure
make && sudo make install
# OpenSSL – 支援 HTTPS 協議, NGINX SSL 模組和其他模組需要。
# 官網: //www.openssl.org/source/
wget -c //www.openssl.org/source/openssl-1.1.1q.tar.gz
tar -zxf openssl-1.1.1q.tar.gz && cd openssl-1.1.1q
./config --prefix=/usr/local/openssl
make && sudo make install
ln -s /usr/local/openssl/bin/openssl /usr/local/bin/openssl
# lib 庫載入到系統
echo "/usr/local/openssl/lib" >> /etc/ld.so.conf.d/libc.conf
ldconfig
# 執行命令驗證系統的 OpenSSL 版本
/usr/local/bin/openssl version
OpenSSL 1.1.1q 5 Jul 2022
溫馨提示: 如./configure
未指定--prefix
參數的將會直接安裝在/usr/local
目錄下的bin、lib、share等子目錄中。
Step 3.下載編譯構建Lua解析器以及Nginx所需的開發工具包和Lua模組。
cd /usr/local/src
# ngx_devel_kit - 是Nginx開發工具包,實際上可以看做一個Nginx模組,它添加了額外的通用工具,模組開發人員可以在自己的模組中使用這些工具。
# 項目地址: //github.com/simpl/ngx_devel_kit
# 項目地址: //github.com/vision5/ngx_devel_kit
wget -c //github.com/vision5/ngx_devel_kit/archive/refs/tags/v0.3.1.tar.gz -O ngx_devel_kit-v0.3.1.tar.gz
tar -zxf ngx_devel_kit-v0.3.1.tar.gz && ls ngx_devel_kit-0.3.1
# auto config docs examples LICENSE ngx_auto_lib_core notes objs patches README_AUTO_LIB.md README.md src
# lua-nginx-module - 將Lua的強大功能嵌入到NGINX HTTP伺服器中
# 項目地址: //github.com/openresty/lua-nginx-module
wget -c //github.com/openresty/lua-nginx-module/archive/refs/tags/v0.10.21.tar.gz -O /usr/local/src/lua-nginx-module-v0.10.21.tar.gz
tar -zxf lua-nginx-module-v0.10.21.tar.gz && ls lua-nginx-module-0.10.21
# config doc dtrace misc README.markdown src t tapset util valgrind.suppress
# echo-nginx-module - 一個Nginx的輸出模組,用於將「echo」、「sleep」、「time」等功能引入Nginx的配置文件, 此模組不隨Nginx源一起分發。
# 項目地址: //github.com/openresty/echo-nginx-module
wget --no-check-certificate -c //github.com/openresty/echo-nginx-module/archive/refs/tags/v0.62.tar.gz -O /usr/local/src/echo-nginx-module-v0.62.tar.gz
tar -zxf echo-nginx-module-v0.62.tar.gz && ls echo-nginx-module-0.62
# config LICENSE README.markdown src t util valgrind.suppress
# luajit2 - lua 解析器 LuaJIT 2 OpenResty 的分支,且注意解析器的Lua版本為5.1
# 項目地址: //github.com/openresty/luajit2
wget -c //github.com/openresty/luajit2/archive/refs/tags/v2.1-20220411.tar.gz -O /usr/local/src/luajit2-v2.1-20220411.tar.gz
tar -zxvf luajit2-v2.1-20220411.tar.gz && cd luajit2-2.1-20220411
make PREFIX=/usr/local/luajit && make install PREFIX=/usr/local/luajit
ln -s /usr/local/luajit/bin/luajit /usr/local/bin/luajit
# 鏈接庫設置
echo "/usr/local/luajit/lib" >> /etc/ld.so.conf.d/libc.conf
ldconfig
# 臨時生效
export LUAJIT_LIB=/usr/local/luajit/lib
export LUAJIT_INC=/usr/local/luajit/include/luajit-2.1
/usr/local/bin/luajit -v
# LuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2017 Mike Pall. //luajit.org/
溫馨提示: 上述 lua 解析器此處採用的是 LuaJIT 官方的 OpenResty 分支, 而不是 luajit 的主分支//luajit.org/download/LuaJIT-2.1.0-beta3.tar.gz
,後面入坑出坑會解釋為啥這樣做。
Step 4.為了使Nginx可以連接到redis資料庫中執行一些列操作,此處藉助於lua-nginx-module模組下載並解壓所需的lua-resty-core、lua-resty-lrucache、lua-resty-redis。
cd /usr/local/src
# 基於 FFI 的 lua-nginx-module API
# 項目地址: //github.com/openresty/lua-resty-core
wget -c //github.com/openresty/lua-resty-core/archive/refs/tags/v0.1.23.tar.gz -O /usr/local/src/lua-resty-core.tar.gz
tar -zxvf lua-resty-core.tar.gz && ls lua-resty-core-0.1.23
# dist.ini lib Makefile README.markdown t valgrind.suppress
# 基於 LuaJIT FFI 的 Lua-land LRU Cache
# 項目地址: //github.com/openresty/lua-resty-lrucache
wget -c //github.com/openresty/lua-resty-lrucache/archive/refs/tags/v0.13.tar.gz -O /usr/local/src/lua-resty-lrucache-v0.13.tar.gz
tar -zxvf lua-resty-lrucache-v0.13.tar.gz && ls lua-resty-lrucache-0.13/
# dist.ini lib Makefile README.markdown t valgrind.suppress
# 基於 cosocket API 的 ngx_lua 的 Lua redis 客戶端驅動
# 項目地址: //github.com/openresty/lua-resty-redis
wget -c //github.com/openresty/lua-resty-redis/archive/refs/tags/v0.29.tar.gz -O /usr/local/src/lua-resty-redis-v0.29.tar.gz
tar -zxvf lua-resty-redis-v0.29.tar.gz && ls lua-resty-redis-0.29/
# 在使用時可將lua腳本放入到nginx配置目錄中。
mkdir -vp /usr/local/nginx/lua/
cp -a /usr/local/src/lua-resty-redis-0.29/lib /usr/local/nginx/lua/
# 以樹形結構顯示該目錄
$ tree /usr/local/nginx/lua/
/usr/local/nginx/lua/
├── hello.lua
└── lib
└── resty
└── redis.lua
Step 5.在上面步驟操作完畢之後,我們便可以進行nginx編譯安裝了,構建流程如下(在部落客的前面講解的Nginx系列教程就已經有詳細講述 【[Nginx進階學習之最佳配置實踐指南][//blog.weiyigeek.top/2019/9-1-124.html]】,此處就不在大篇幅累述了):
# 創建允許用戶和組,不需要家目錄不登錄bash
useradd -M -s /sbin/nologin nginx
# 創建 Nginx 所需目錄
sudo mkdir -vp /usr/local/nginx/{module,modules,lua} /var/cache/nginx/{client_temp,proxy_temp,fastcgi_temp,uwsgi_temp,scgi_temp}
cd /usr/local/src/nginx-1.22.0
# Nginx 預編譯參數設置
./configure \
--prefix=/usr/local/nginx \
--user=nginx --group=nginx \
--with-pcre=../pcre-8.45 \
--with-zlib=../zlib-1.2.12 \
--with-openssl=../openssl-1.1.1q \
--sbin-path=/usr/sbin/nginx \
--conf-path=/usr/local/nginx/nginx.conf \
--pid-path=/usr/local/nginx/nginx.pid \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--lock-path=/var/run/nginx.lock \
--modules-path=/usr/local/nginx/modules \
--http-client-body-temp-path=/var/cache/nginx/client_temp \
--http-proxy-temp-path=/var/cache/nginx/proxy_temp \
--http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
--http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
--http-scgi-temp-path=/var/cache/nginx/scgi_temp \
--with-threads \
--with-http_sub_module --with-http_v2_module \
--with-http_auth_request_module --with-http_realip_module --with-http_secure_link_module \
--with-http_gunzip_module --with-http_gzip_static_module --with-http_ssl_module \
--with-http_slice_module --with-http_stub_status_module \
--with-http_dav_module --with-http_flv_module --with-http_mp4_module \
--with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-stream_geoip_module \
--with-mail --with-mail_ssl_module \
--with-http_addition_module --with-http_random_index_module \
--with-compat --with-file-aio \
--with-cc-opt='-Os -fomit-frame-pointer -g' \
--with-ld-opt='-Wl,-rpath,/usr/local/luajit/lib,--as-needed,-O1,--sort-common' \
--add-module=/usr/local/src/ngx_devel_kit-0.3.1 \
--add-module=/usr/local/src/lua-nginx-module-0.10.21 \
--add-dynamic-module=/usr/local/src/echo-nginx-module-0.62 \
# 編譯構建安裝
make & make install
溫馨提示: 上述 ./configure
編譯配置中使用靜態鏈接庫方式來添加ngx_devel_kit-0.3.1/lua-nginx-module-0.10.21
模組, 又為了演示加入動態鏈接庫的使用方式,此處使用--add-dynamic-module
參數指定echo-nginx-module-0.62
的解壓目錄,如果使用動態連接庫的方式載入模組將會在後續實踐中展示。
構建結果:
# configure 結果
Configuration summary
# + using threads
# + using PCRE library: ../pcre-8.45
# + using OpenSSL library: ../openssl-1.1.1q
# + using zlib library: ../zlib-1.2.12
# nginx path prefix: "/usr/local/nginx"
# ....................................
# nginx http scgi temporary files: "/var/cache/nginx/scgi_temp"
# Make 構建安裝後提示lib動態鏈接庫地址。
- add LIBDIR to the 'LD_LIBRARY_PATH' environment variable during execution
- add LIBDIR to the 'LD_RUN_PATH' environment variable during linking
- use the '-Wl,-rpath -Wl,LIBDIR' linker flag # 或者在編譯是添加依賴的Lib目錄。
- have your system administrator add LIBDIR to '/etc/ld.so.conf'
/usr/local/src/nginx-1.22.0# ls objs/
# ls objs/
# addon ngx_auto_config.h
# autoconf.err ngx_auto_headers.h
# Makefile ngx_http_echo_module_modules.c
# nginx ngx_http_echo_module_modules.o
# ngx_modules.c src
# nginx.8 ngx_http_echo_module.so ngx_modules.o
Step 6.在Nginx安裝部署成功後,為了驗證Nginx + Lua安裝環境,我們需要再 nginx 主配置文件入口配置如下關鍵內容,注意下面帶有文字注釋部分。
$ grep -v "^#|^$|#" /usr/local/nginx/conf.d/nginx.conf
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
# 去除 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"';
sendfile on;
keepalive_timeout 65;
# lua 包模組依賴路徑
lua_package_path '/usr/local/src/lua-resty-core-0.1.23/lib/?.lua;/usr/local/src/lua-resty-lrucache-0.13/lib/?.lua;';
...
# 添加載入nginx家目錄下的conf.d/目錄子配置文件 (通配符)
include conf.d/*.conf;
}
然後再創建子配置目錄與demo.weiyigeek.top站點配置demo.conf文件中,添加如下server欄位內容片段。
mkdir /usr/local/nginx/conf.d
tee /usr/local/nginx/conf.d/demo.conf <<'EOF'
# https - demo.weiyigeek.top
server {
listen 80;
server_name demo.weiyigeek.top;
charset utf-8;
access_log /var/log/nginx/demo-access.log main buffer=128k flush=1m;
# 方式1.content_by_lua_block lua 片段
location /hello-lua {
default_type 'text/plain';
content_by_lua_block {
ngx.say("Hello World! Lua & Nginx .")
}
}
# 方式2.content_by_lua_file lua 腳本文件路徑
location /hello-lua-file {
default_type 'text/html';
content_by_lua_file ./lua/hello.lua;
}
# 方式3.access_by_lua 在請求訪問階段處理用於訪問控制。
location /hello-lua-access {
default_type 'text/html';
access_by_lua '
local message = "403 - Hello World! Lua & Nginx access_by_lua"
ngx.say(message)
';
}
# 方式4.content_by_lua 在內容處理階段接受請求並輸出響應。
location /hello-lua-content {
default_type 'text/html';
content_by_lua "ngx.print('Hello World!')";
}
}
EOF
溫馨提示:access_by_lua
與 content_by_lua
的區別是對於Nginx請求的不同處理階段,前者是訪問階段處理用於訪問控制(適用於http、server、location、location if
),後者內容處理器接受請求並輸出響應,適用於location、location if
Step 7.上述配置完成後為了驗證配置文件是否存在問題,可執行如下命令如果顯示 successful 表示配置沒有問題,之後就可重載 nginx 服務。
$ nginx -t
# nginx: the configuration file /usr/local/nginx/nginx.conf syntax is ok
# nginx: configuration file /usr/local/nginx/nginx.conf test is successful
$ /usr/sbin/nginx -s reload
$ ps -ef | grep "nginx"
# root 244962 1 0 16:40 ? 00:00:00 nginx: master process nginx
# nginx 245707 244962 0 21:42 ? 00:00:00 nginx: worker process
# root 245710 245523 0 21:42 pts/0 00:00:00 grep nginx
Step 8.驗證基本的Nginx+Lua環境,我們訪問上述配置文件中的域名和子目錄,訪問結果如下圖所示則表示環境OK,否則請排查錯誤或者查看是否存在下述的入坑出坑中相關問題。
curl -H "host:demo.weiyigeek.top" 10.20.172.201/hello-lua
Hello World! Lua & Nginx .
curl -H "host:demo.weiyigeek.top" 10.20.172.201/hello-lua-file
<h2> Hello world! Lua & Nginx with Hello.lua. </h2>
curl -H "host:demo.weiyigeek.top" 10.20.172.201/hello-lua-access
Hello World! Lua & Nginx access_by_lua
curl -H "host:demo.weiyigeek.top" 10.20.172.201/hello-lua-content
Hello World!
知識擴展: 編譯構建nginx後我們可通過nginx -V
命令查看曾經./configure
預編譯參數的設置。
$ nginx -V
nginx version: nginx/1.22.0
built by gcc 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)
built with OpenSSL 1.1.1q 5 Jul 2022
TLS SNI support enabled
configure arguments: --prefix=/usr/local/nginx
....
--add-module=/usr/local/src/lua-nginx-module-0.10.21 --add-dynamic-module=/usr/local/src/echo-nginx-module-0.62 -
0x02 使用實踐
1.Nginx 實踐使用 echo-nginx-module 模組之動態載入鏈接庫
描述: 從 NGINX 1.9.11 開始,您還可以將此模組編譯為動態模組,方法是在上面的 ./configure
命令行中使用 --add-dynamic-module=PATH
選項而不是--add-module=PATH
選項,然後你可以通過 load_module
指令在你的 nginx.conf
中顯式載入模組,注意必須在 events{}
片段之前.
模組語法: //github.com/openresty/echo-nginx-module#synopsis
Step 1.在Nginx.conf
文件中配置load_module
指令以動態載入 echo-nginx-module
模組。
# 方式1.絕對路徑
load_module /usr/local/nginx/modules/ngx_http_echo_module.so;
# 方式2.相對路徑
load_module ./modules/ngx_http_echo_module.so;
.....
events {
worker_connections 1024;
}
Step 2.同樣在demo.conf
文件中的進行該模組常規的使用實踐。
$ cat conf.d/demo.conf
server {
...
# 示例1.常規輸出(注意文本類型則網頁中回饋展現數據也不相同)。
location /nginx-module/echo {
default_type 'text/html';
echo -n "<b>Domain: demo.weiyigeek.top</b> <br/>";
echo "Hello World! by ngx_http_echo_module.so";
}
# 示例2.請求延時顯示以及重置時間定時器。
location /nginx-module/timed {
default_type 'text/plain';
echo "Hello World! by ngx_http_echo_module.so \r";
echo_reset_timer;
echo "1.takes about $echo_timer_elapsed sec \r";
echo_flush;
echo_sleep 2.5; # in sec
echo "2.takes about $echo_timer_elapsed sec.";
echo "End";
}
# 示例3.Body文檔前後插入數據以及在中部插嵌入反向代理網站源碼。
location /nginx-module/body {
resolver 223.6.6.6;
default_type 'text/html';
echo "Hello World! by ngx_http_echo_module.so";
echo_before_body 'Blog - ';
proxy_pass $scheme://www.weiyigeek.top:$server_port/index.html;
echo_before_body 'www.WeiyiGeek.top';
echo_after_body '[END]';
}
# 示例4.多次輸出同一個字元串以及顯示客戶端請求header與請求body主體參數
location /nginx-module/duplicate {
default_type 'text/plain';
echo_duplicate 3 "--";
echo_duplicate 1 "\rHello World! by ngx_http_echo_module.so \r\r";
# echo_duplicate 1000_000 "Hello World! by ngx_http_echo_module.so";
echo "\r";
echo_duplicate 1 $echo_client_request_headers;
echo "\r";
echo_read_request_body;
echo "\r";
echo_request_body;
echo_duplicate 3 "--";
echo;
}
# 示例5.正則匹配請求參數,注意`$arg_var`後面的var是可以自定義設置,此處為flag參數。
location ^~ /nginx-module/if {
default_type 'text/plain';
set $res default;
echo $arg_flag;
if ($arg_flag ~* '^a') {
set $res change;
echo $arg_flag, $res;
}
echo $res;
}
....
}
補充 echo_subrequest_async 非同步請求
描述: 使用 HTTP 方法、可選的 url 參數(或查詢字元串)和可選的請求主體發起非同步子請求,請求主體可以定義為字元串或包含主體的文件的路徑。
# GET /multi will yields
# querystring: foo=Foo
# method: POST
# body: hi
# content length: 2
# ///
# querystring: bar=Bar
# method: PUT
# body: hello
# content length: 5
# ///
location /multi {
echo_subrequest_async POST '/sub' -q 'foo=Foo' -b 'hi';
echo_subrequest_async PUT '/sub' -q 'bar=Bar' -b 'hello';
}
location /sub {
echo "querystring: $query_string";
echo "method: $echo_request_method";
echo "body: $echo_request_body";
echo "content length: $http_content_length";
echo '///';
}
Step 3.完成配置後重載nginx服務, 通過瀏覽器訪問上述路徑驗證模組使用與輸出,效果如下圖所示:
該模組的其它使用請參考其項目地址Readme文檔,此處演示了如何載入動態鏈接庫到nginx,並且使用鏈接庫中的模組。
2.Nginx 實踐使用 lua-resty-redis 模組連接 Redis 進行數據操作與展示
描述: 前面環境部署中已下載 ngx_lua_nginx 模組的 Redis 客戶端驅動程式Lua庫, 下面將演示如何在 Nginx 基於 ngx_lua_nginx 模組連接到Redis記憶體資料庫進行相應數據查找,好了本小節就直奔主題。
語法參考: //github.com/openresty/lua-resty-redis#synopsis
廢話不多說,實踐出真知
Step 1.在前面環境安裝中我們解壓在 ngx_lua_nginx 模組使用 Redis 客戶端驅動程式Lua庫,並將其 Lib 目錄複製到 /usr/local/nginx/lua/
目錄中,其次我也準備了Redis資料庫環境,針對安裝部署步驟就不在詳述了, 想要快速安裝的朋友可以參考我的此篇文章【[Redis記憶體資料庫環境快速搭建部署][//blog.weiyigeek.top/2022/4-24-653.html]】。
$ tree /usr/local/nginx/lua/lib/
/usr/local/nginx/lua/lib/
└── resty
└── redis.lua
# Redis 資料庫 & 為了演示數據準備兩個Key即domain/blog
192.168.1.22 6379 weiyigeek.top
/data # redis-cli
127.0.0.1:6379> auth weiyigeek.top
OK
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> set domain www.weiyigeek.top
OK
127.0.0.1:6379> set blog blog.weiyigeek.top
OK
Step 2.想要在Nginx使用該redis.lua
鏈接到資料庫,首先我們需要在nginx.conf
配置文件中加入該lua包路徑/usr/local/nginx/lua/lib/
,例如:
$ grep "lua_package_path" /usr/local/nginx/nginx.conf
lua_package_path '/usr/local/nginx/lua/lib/?.lua;/usr/local/src/lua-resty-core-0.1.23/lib/?.lua;/usr/local/src/lua-resty-lrucache-0.13/lib/?.lua;'
Step 3.此處也是在 demo.conf
中進行配置使用Redis客戶端驅動程式Lua庫,連接到Redis資料庫中, 此處為了方便演示就直接在該配置文件content_by_lua_block
程式碼塊中使用lua語法,在企業生產實踐環境中一定要將其寫入到lua文件文件中。
# /usr/local/nginx/conf.d/demo.conf
server {
...
location /redis/get {
default_type 'text/html';
set $key $arg_key;
content_by_lua_block {
-- # 引入resty.redis模組與創建實例化對象
local redis = require "resty.redis"
local client = redis:new()
local REDIS_HOST = "192.168.1.22"
local REDIS_PROT = 6379
local REDIS_AUTH = "weiyigeek.top"
-- # ngx.log(ngx.ERR, ngx.var.key)
-- # 分別設置連接、發送和讀取超時閾值(以毫秒為單位),用於後續套接字操作。
client:set_timeouts(1000, 1000, 1000)
-- # 創建鏈接對象, 連接到Redis資料庫
ngx.say("1.connect redis server..... <br>");
local ok, err = client:connect(REDIS_HOST, REDIS_PROT)
if not ok then
ngx.say("failed to connect: ", err)
return
end
-- # 認證
ngx.say("2.auth redis server..... <br>");
local res, err = client:auth(REDIS_AUTH)
if not res then
ngx.say("failed to authenticate: ", err)
return
end
-- # 獲取指定請求鍵值
ngx.say("3.get custom KV for redis server, Key = ",ngx.var.key," <br>");
local res, err = client:get(ngx.var.key)
if not res then
ngx.say("failed to get key: ", err)
return
end
if res == ngx.null then
ngx.say("key not found.")
return
end
-- # 輸出結果
ngx.say("<b style='color:red'>4.result value: ",res,"</b><br/>")
-- # 使用完畢後立即關閉銷毀Redis連接(短連接可以如此使用,如果是長鏈接建議回收該連接池對象即可)
local ok, err = client:close()
if not ok then
ngx.say("failed to close: ", err)
return
else
ngx.say("5.just close the Redis connection right away <br/>")
end
}
}
...
}
Step 5.在演示一個示例,我們可以一次性執行多個redis操作命令 lua-resty-redis 庫支援pipeline提交,下面我們演示使用 content_by_lua_file
關鍵字指定連接操作redis的lua腳本地址(/usr/local/nginx/lua/custom/nginx-redis.lua
)實踐, 該方式在線上環境中推薦使用。
# 1) 操作 redis 資料庫的 lua 腳本示例。
tee /usr/local/nginx/lua/custom/nginx-redis.lua <<'EOF'
-- # 引入resty.redis模組與創建實例化對象
local redis = require "resty.redis"
local client = redis:new()
local REDIS_HOST = "192.168.1.22"
local REDIS_PROT = 6379
local REDIS_AUTH = "weiyigeek.top"
-- # ngx.log(ngx.ERR, ngx.var.key)
-- # 分別設置連接、發送和讀取超時閾值(以毫秒為單位),用於後續套接字操作。
client:set_timeouts(1000, 1000, 1000)
-- # 驗證請求的參數是否存在
if (ngx.var.key == ngx.null and ngx.var.value == ngx.null)
then
ngx.say("Request parameters : key + value not found!")
ngx.exit(404)
end
-- # 創建鏈接對象, 連接到Redis資料庫
ngx.say("1.connect redis server..... <br>");
local ok, err = client:connect(REDIS_HOST, REDIS_PROT)
if not ok then
ngx.say("failed to connect: ", err)
return
end
-- # 認證
ngx.say("2.auth redis server..... <br>");
local res, err = client:auth(REDIS_AUTH)
if not res then
ngx.say("failed to authenticate: ", err)
return
end
-- # 使用 pipeline 通道方式進行redis 資料庫操作
client:init_pipeline()
client:set(ngx.var.key, ngx.var.value)
client:get(ngx.var.key)
client:get("domain")
local results, err = client:commit_pipeline()
if not results then
ngx.say("failed to commit the pipelined requests: ", err)
return
end
-- 結果遍歷
for i, res in ipairs(results) do
if type(res) == "table" then
if res[1] == false then
ngx.say("failed to run command ", i, ": ", res[2],"<br/>")
else
-- process the table value
ngx.say("3) 3.",i, ": ", res[2],"<br/>")
end
else
-- process the scalar value
ngx.say("<p style='color:red'>3) ",i,"---",res,"</p>")
end
end
-- 將當前 Redis 連接立即放入 ngx_lua cosocket 連接池(將其放入大小為100的連接池中,最大空閑時間為10秒)。
local ok, err = client:set_keepalive(10000, 100)
if not ok then
ngx.say("failed to set keepalive: ", err)
return
end
ngx.say("4.將當前 Redis 連接立即放入 ngx_lua cosocket 連接池<br/>")
EOF
# 2) 配置 demo.conf 文件 同樣在 server 片段中加入如下 location 片段。
server {
....
location /redis/pipeline {
default_type 'text/html';
# 獲取請求參數中key的值與value的值並存放到nginx環境變數中
set $key $arg_key;
set $value $arg_value;
# 調用並執行指定的lua腳本
content_by_lua_file ./lua/custom/nginx-redis.lua;
}
....
}
在配置完成後我們便可以重載nginx,並使用訪問瀏覽器訪問上述路徑,例如: //demo.weiyigeek.top/redis/pipeline?key=name&value=WeiyiGeek
,此處我演示的結果如下圖所示。
3.Nginx 實踐讀取Redis資料庫中圖片綁定對應鍵值並進行圖片展示
描述: 假如在這樣一個場景中,為了避免惡意用戶遍歷有規律的圖片進行下載,那如何解決這個問題呢?
方法是有得但也不限於本節實踐的案例,此處我們可以將其圖片名稱或者圖片md5值存入到Redis資料庫中作為Key,而實際的圖片路徑作為Value,在請求時我們將該md5值作為參數進行傳入,經過 Nginx 對請求參數的處理,使用前面的方式在 Lua 腳本中連接Redis,並將URL傳遞的md5參數作為key進行get查詢,並將查詢到的圖片路徑,回饋給set指令設置的變數之中,然後我們便可以通過 proxy_pass 進行代理訪問(地址欄中的url不會變化,保證實際的圖片路徑),或者也可以加上一個頭Content-Disposition
直接進行圖片下載。
不在多說廢話了,只有實踐才是王道。
實踐流程:
- Step 1.準備一個圖片目錄以及放入幾張圖片進行演示,此處你可以使用圖片名稱md5也可使用圖形文件本身md5效驗值。
$ tree /usr/local/nginx/html/
/usr/local/nginx/html/
├── 50x.html
├── images
│ ├── 1562941454569.jpeg
│ ├── 1562941454570.jpeg
│ └── 1562941454571.png
└── index.html
# 文件的MD5值
/usr/local/nginx/html/images# md5sum * | awk '{print "set "$1" "$2}'
set 6fad4c2466dc7f61fb055021ec65324d 1562941454569.jpeg
set 611877180883388de4752ded33a81165 1562941454570.jpeg
set 6636d52bfbe068177df5219edf4dd456 1562941454571.png
# 寫入KV到redis資料庫中
127.0.0.1:6379> set 6fad4c2466dc7f61fb055021ec65324d 1562941454569.jpeg
OK
127.0.0.1:6379> set 611877180883388de4752ded33a81165 1562941454570.jpeg
OK
127.0.0.1:6379> set 6636d52bfbe068177df5219edf4dd456 1562941454571.png
OK
- Step 2.在
demo.conf
文件中的server
片段中增加location
片段,其中進行如下配置:
$ vim conf.d/demo.conf
server {
......
location = /api/v2/images/get {
resolver 223.6.6.6;
set $key $arg_md5sum;
set $name "";
access_by_lua_block {
local redis = require "resty.redis"
local client = redis:new()
local REDIS_HOST = "192.168.1.22"
local REDIS_PROT = 6379
local REDIS_AUTH = "weiyigeek.top"
client:set_timeouts(1000, 1000, 1000)
local ok, err = client:connect(REDIS_HOST, REDIS_PROT)
if not ok then
ngx.say("failed to connect: ", err)
return
end
local res, err = client:auth(REDIS_AUTH)
if not res then
ngx.say("failed to authenticate: ", err)
return
end
local res, err = client:get(ngx.var.key)
if not res then
ngx.say("failed to get key: ", err)
return
end
if res == ngx.null then
ngx.say("key not found.")
return
else
-- # 關鍵點將redis中指定鍵的值賦予給nginx指定變數
ngx.var.name = res
end
local ok, err = client:set_keepalive(10000, 100)
if not ok then
ngx.say("failed to set keepalive: ", err)
return
end
}
proxy_pass $scheme://$server_name/images/$name;
}
......
}
在配置完成後我們重載 Nginx,然後利用瀏覽器進行訪問如上URL,例如//demo.weiyigeek.top/api/v2/images/get?md5sum=6636d52bfbe068177df5219edf4dd456
,執行結果如下所示:
- Step 3.如果我們想通過瀏覽器訪問上述地址就直接彈出源文件名稱進行下載的,我們則可以在
proxy_pass
片段後加上如下header
頭:add_header Content-Disposition "attachment;filename=$name";
...
proxy_pass $scheme://$server_name/images/$name;
add_header Content-Disposition "attachment;filename=$name";
...
# 重載Nginx後利用CURL訪問該URL
$ curl -I //demo.weiyigeek.top/api/v2/images/get?md5sum=6636d52bfbe068177df5219edf4dd456
HTTP/1.1 200 OK
Server: nginx/1.22.0
Date: Tue, 02 Aug 2022 02:23:12 GMT
Content-Type: image/png
Content-Length: 32641
Connection: keep-alive
Last-Modified: Wed, 23 Mar 2022 00:48:26 GMT
ETag: "623a6e5a-7f81"
Accept-Ranges: bytes
Content-Disposition: attachment;filename=1562941454571.png
- Step 4.當然,你也可使用
rewrite_by_lua_block
程式碼塊包含Lua可直接或者圖片路徑,然後使用ngx.redirect()
方法進行跳轉。
$ vim conf.d/demo.conf
server {
......
location = /api/v1/images/get {
resolver 223.6.6.6;
set $key $arg_md5sum;
rewrite_by_lua_block {
local redis = require "resty.redis"
local client = redis:new()
local REDIS_HOST = "192.168.1.22"
local REDIS_PROT = 6379
local REDIS_AUTH = "weiyigeek.top"
client:set_timeouts(1000, 1000, 1000)
local ok, err = client:connect(REDIS_HOST, REDIS_PROT)
if not ok then
ngx.say("failed to connect: ", err)
return
end
local res, err = client:auth(REDIS_AUTH)
if not res then
ngx.say("failed to authenticate: ", err)
return
end
local res, err = client:get(ngx.var.key)
if not res then
ngx.say("failed to get key: ", err)
return
end
if res == ngx.null then
ngx.say("key not found.")
else
-- # 關鍵點圖片格式化。
return ngx.redirect(string.format("%s%s","/images/",res))
end
local ok, err = client:set_keepalive(10000, 100)
if not ok then
ngx.say("failed to set keepalive: ", err)
return
end
}
# 若沒有匹配搭配到進行跳轉進行跳轉則訪問首頁
proxy_pass $scheme://$server_name/index.html;
}
......
}
好了,本章實踐就到此處了,更多的奇技淫巧盡在 [weiyigeek] 公眾號.
0x03 擴展補充
示例1.使用 ngx.location.capture() 請求內部介面
location = /auth {
internal;
retur 200 '{"status":"$auth_status"}'
}
# 此處根據業務的需求來寫正則表達式,一定要個 redis 里的 KEY 對應上
location ~/[0-9].*\.(gif|jpg|jpeg|png)$ {
set $target '';
access_by_lua '
# 使用 nginx 的內部參數 ngx.var.uri 來獲取請求的 uri 地址,如 /000001.jpg
local key = ngx.var.uri
# 根據正則匹配到 KEY ,從 redis 資料庫里獲取文件 ID (路徑和文件名)
local res = ngx.location.capture(
"/Redis", { args = { key = key } }
)
if res.status ~= 200 then
ngx.log(ngx.ERR, "Redis server returned bad status: ",res.status)
ngx.exit(res.status)
end
if not res.body then
ngx.log(ngx.ERR, "Redis returned empty body")
ngx.exit(500)
end
local parser = require "Redis.parser"
local filename, typ = parser.parse_reply(res.body)
if typ ~= parser.BULK_REPLY or not server then
ngx.log(ngx.ERR, "bad Redis response: ", res.body)
ngx.exit(500)
end
ngx.var.target = filename
';
proxy_pass //10.20.172.196/$target;
}
0x0n 入坑出坑
問題1. 當編譯 Nginx 時報checking for LuaJIT 2.x ... not found, ./configure: error: unsupported LuaJIT version; ngx_http_lua_module requires LuaJIT 2.x.
錯誤時的解決辦法。
問題描述: tell nginx’s build system where to find LuaJIT 2.1
解決辦法:
# 臨時生效
export LUAJIT_LIB=/usr/local/luajit/lib
export LUAJIT_INC=/usr/local/luajit/include/luajit-2.1
# 永久生效
tee -a /etc/profile <<'EOF'
export LUAJIT_LIB=/usr/local/luajit/lib
export LUAJIT_INC=/usr/local/luajit/include/luajit-2.1
EOF
source /etc/profile
問題2.在使用luajit官方主分支LuaJIT-2.1.0-beta3
提供LuaJIT安裝部署出現nginx: [alert] detected a LuaJIT version which is not OpenResty's;
以及nginx: [alert] failed to load the 'resty.core' module
警告。
錯誤資訊:
$ /usr/sbin/nginx
nginx: [alert] detected a LuaJIT version which is not OpenResty's; many optimizations will be disabled and performance will be compromised (see //github.com/openresty/luajit2 for OpenResty's LuaJIT or, even better, consider using the OpenResty releases from //openresty.org/en/download.html)
nginx: [alert] failed to load the 'resty.core' module (//github.com/openresty/lua-resty-core); ensure you are using an OpenResty release from //openresty.org/en/download.html (reason: module 'resty.core' not found:
no field package.preload['resty.core']
no file './resty/core.lua'
no file '/usr/local/share/luajit-2.1.0-beta3/resty/core.lua'
no file '/usr/local/share/lua/5.1/resty/core.lua'
no file '/usr/local/share/lua/5.1/resty/core/init.lua'
no file './resty/core.so'
no file '/usr/local/lib/lua/5.1/resty/core.so'
no file '/usr/local/lib/lua/5.1/loadall.so'
no file './resty.so'
no file '/usr/local/lib/lua/5.1/resty.so'
no file '/usr/local/lib/lua/5.1/loadall.so') in /usr/local/nginx/nginx.conf:117
問題原因1: 提示LuaJIT的版本不匹配OpenResty’s內核版本, 讓我不要用這個luajit版本,可以用openresty提供的luajit優化版本,或者乾脆直接用openresty,下面將安裝卸載luajit官網版本,下載openresty提供的luajit優化版本(即上面環境安裝已經實踐了,按照上面版本進行安裝一般不會存在該問題)。
# 你可能會進行 Lua 腳本解釋器的安裝 LuaJIT
//luajit.org/download.html
wget -c //luajit.org/download/LuaJIT-2.1.0-beta3.tar.gz
tar -zxf LuaJIT-2.1.0-beta3.tar.gz && cd LuaJIT-2.1.0-beta3
make && make install
ln -sf /usr/local/bin/luajit-2.1.0-beta3 /usr/local/bin/luajit
# 卸載LuaJIT官網主分支版本,然後重新安裝openresty提供的luajit優化版即可
make uninstall
make clean
問題原因2: 提示載入’resty.core’模組失敗,其解決辦法,按照//github.com/openresty/lua-nginx-module/issues/1509
上面所說, 安裝lua-resty-core
和依賴文件lua-resty-lrucache
解決問題,即我前面實踐中已經進行此部分操作,若不會操作請上翻到 【安裝部署】標題進行查看。
原文地址: //blog.weiyigeek.top/2022/7-2-676.html
本文至此完畢,更多技術文章,盡情期待下一章節!
【WeiyiGeek Blog 個人部落格 – 為了能到遠方,腳下的每一步都不能少 】
歡迎各位志同道合的朋友一起學習交流,如文章有誤請在下方留下您寶貴的經驗知識!
作者主頁: 【 //weiyigeek.top】
部落格地址: 【 //blog.weiyigeek.top 】
專欄書寫不易,如果您覺得這個專欄還不錯的,請給這篇專欄 【點個贊、投個幣、收個藏、關個注,轉個發,留個言】(人間六大情),這將對我的肯定,謝謝!。
-
echo “【點個贊】,動動你那粗壯的拇指或者芊芊玉手,親!”
-
printf(“%s”, “【投個幣】,萬水千山總是情,投個硬幣行不行,親!”)
-
fmt.Printf(“【收個藏】,閱後即焚不吃灰,親!”)
-
console.info(“【轉個發】,讓更多的志同道合的朋友一起學習交流,親!”)
-
System.out.println(“【關個注】,後續瀏覽查看不迷路喲,親!”)
-
cout << “【留個言】,文章寫得好不好、有沒有錯誤,一定要留言喲,親! ” << endl;
更多網路安全、系統運維、應用開發、物聯網實踐、網路工程、全棧文章,盡在 //blog.weiyigeek.top 之中,謝謝各位看又支援!