环境搭建概述
下面开始手工搭建 PHP 的开发环境、部署环境
测试环境
系统 : Debian GNU/Linux 13 (trixie)
内核 : linux-image-6.12.41+deb13-amd64
============================================================================
LNMPP 环境目录
============================================================================
├─ /server ------------------------------------------------ 服务目录
| ├─ sqlite ----------------------------------- SQLite3基目录
| | └─ ...
| |
| ├─ redis ------------------------------------ redis基目录
| | ├─ redis.conf ------------------ redis配置文件
| | ├─ tls ------------------------- redis的tls相关文件存放目录(考虑权限安全)
| | └─ ...
| |
| ├─ mysql ------------------------------------ mysql基目录
| | └─ ...
| |
| ├─ data ------------------------------------- mysql数据目录
| | └─ ...
| |
| ├─ postgres --------------------------------- pgsql基目录
| | ├─ tls ------------------------- pgsql的tls相关文件存放目录(考虑权限安全)
| | └─ ...
| |
| ├─ pgData ----------------------------------- pgsql数据目录
| | ├─ postgresql.conf ------------- pgsql服务器配置文件
| | ├─ pg_hba.conf ----------------- pgsql客户访问限制配置文件
| | └─ ...
| |
| ├─ php -------------------------------------- PHP 目录
| | ├─ tools ----------------------- PHP通用工具存放目录
| | ├─ 84 -------------------------- PHP8.4基目录
| | └─ ...
| |
| ├─ nginx ------------------------------------ nginx基目录
| | ├─ conf ------------------------ nginx配置目录
| | └─ ...
| |
| ├─ default ---------------------------------- 缺省站点路径
| | ├─ index.php ------------------- 缺省站点php首页
| | └─ index.html ------------------ 缺省站点html页面
| |
| ├─ sites ------------------------------------ 虚拟主机配置文件目录
| | ├─ tls ------------------------- tls证书相关
| | ├─ available ------------------- 所有站点配置的仓库
| | └─ enabled --------------------- 当前启用配置的快捷方式
| |
| ├─ etc -------------------------------------- 服务器相关日志文件目录
| | ├─ mysql ----------------------- mysql
| | | ├─ init.sql ------- mysql初始化sql
| | | ├─ my.cnf --------- mysql配置文件
| | | └─ ...
| | |
| | ├─ redis ----------------------- redis
| | | ├─ config --------- 配置目录
| | | | ├─ custom 自定义模块配置目录
| | | | ├─ source.conf 源配置文件
| | | | └─ ...
| | | ├─ tls ------------ tls证书相关
| | | └─ ...
| | |
| | ├─ nginx ----------------------- nginx
| | | ├─ custom --------- 自定义配置目录
| | | ├─ main.nginx ----- 主配置文件
| | | └─ ...
| | |
| | ├─ postgres -------------------- postgres
| | | ├─ tls ------------ tls证书相关
| | | └─ ...
| | |
| |
| ├─ logs ------------------------------------- 日志根目录
| | ├─ redis --------------------------------- redis日志目录
| | | ├─ redis.log ---------------- 日志文件
| | | └─ ...
| | |
| | ├─ postgres ------------------------------ pgsql日志目录
| | | ├─ wal_archive -------------- 预写式日志存放目录
| | | └─ ...
| | |
| | ├─ mysql --------------------------------- mysql日志目录
| | | ├─ error.log ---------------- 错误日志
| | | ├─ binlog --------- 二进制日志存放目录
| | | | ├─ binlog.index
| | | | └─...
| | | |
| | | └─ ...
| | |
| | ├─ php ----------------------------------- php日志目录
| | | ├─ error-84.log ------------- php-8.4错误日志
| | | └─ ...
| | |
| | ├─ nginx ----------------------- nginx日志目录
| | | ├─ error --------- 错误日志存放目录
| | | | ├─ all.log 缺省错误日志
| | | | └─...
| | | ├─ access --------- 访问日志存放目录
| | | | ├─ all.log 缺省访问日志
| | | | └─...
| | | |
| | |
| |
├─ ├─ ...
| |
|
└─ /www --------------------------------------------------- 站点基目录
脚本文件
我们准备了几个 bash 脚本文件:
#!/usr/bin/env bash
# 创建 SQLite3 用户
groupadd -g 2001 sqlite
useradd -c 'sqlite main user' \
-g sqlite -u 2001 -s /sbin/nologin -m sqlite
cp -r /root/{.oh-my-zsh,.zshrc} /home/sqlite
chown sqlite:sqlite -R /home/sqlite/{.oh-my-zsh,.zshrc}
# 创建 redis 用户
groupadd -g 2002 redis
useradd -c 'redis service main process user' \
-g redis -u 2002 -s /sbin/nologin -m redis
cp -r /root/{.oh-my-zsh,.zshrc} /home/redis
chown redis:redis -R /home/redis/{.oh-my-zsh,.zshrc}
# 创建 postgres 用户
groupadd -g 2003 postgres
useradd -c 'postgres service main process user' \
-g postgres -u 2003 -s /sbin/nologin -m postgres
cp -r /root/{.oh-my-zsh,.zshrc} /home/postgres
chown postgres:postgres -R /home/postgres/{.oh-my-zsh,.zshrc}
# 创建 MySQL 用户
groupadd -g 2005 mysql
useradd -c 'mysql service main process user' \
-g mysql -u 2005 -s /sbin/nologin -m mysql
cp -r /root/{.oh-my-zsh,.zshrc} /home/mysql
chown mysql:mysql -R /home/mysql/{.oh-my-zsh,.zshrc}
# 创建 php-fpm 用户
groupadd -g 2006 php-fpm
useradd -c 'php-fpm service main process user' \
-g php-fpm -u 2006 -s /sbin/nologin -m php-fpm
cp -r /root/{.oh-my-zsh,.zshrc} /home/php-fpm
chown php-fpm:php-fpm -R /home/php-fpm/{.oh-my-zsh,.zshrc}
# 创建 nginx 用户
groupadd -g 2007 nginx
useradd -c 'nginx service main process user' \
-g nginx -u 2007 -s /sbin/nologin -m nginx
cp -r /root/{.oh-my-zsh,.zshrc} /home/nginx
chown nginx:nginx -R /home/nginx/{.oh-my-zsh,.zshrc}
# 新版本开始使用tcp/ip转发,并不需要考虑socket文件转发相关的权限问题
# php-fpm 主进程非特权用户时,需要考虑如下问题:
# nginx 如果是通过 sock 文件代理转发给 php-fpm,
# php-fpm 主进程创建 sock 文件时需要确保 nginx 子进程用户有读写 sock 文件的权限
# 方式1:采用 sock 文件权限 php-fpm:nginx 660 (nginx 权限较少,php-fpm 权限较多)
# usermod -G nginx php-fpm
# 方式2:采用 sock 文件权限 php-fpm:php-fpm 660 (nginx 权限较多,php-fpm 权限较少)
# usermod -G php-fpm nginx
# php编译pgsql扩展,使用指定Postgres安装目录时,需提供读取libpg目录的权限
# php使用到SQLite3的pkgconfig时,需提供读取SQLite3头和库文件的权限
usermod -a -G postgres,sqlite php-fpm
# 部署环境注释,开发环境取消注释,开发用户追加附属组,其中emad指开发用户
# - 部署环境不需要开发用户,可直接使用 nginx 用户作为 ftp、ssh 等上传工具的用户
usermod -a -G emad nginx
usermod -a -G emad php-fpm
usermod -a -G sqlite,redis,postgres,mysql,php-fpm,nginx emad
#!/usr/bin/env bash
# mkdir.bash
func_create(){
mkdir $1
}
server_array=(
"/www"
"/server"
"/server/default"
"/server/logs"
"/server/etc"
"/server/sqlite"
"/server/redis"
"/server/redis/rdbData"
"/server/logs/redis"
"/server/etc/redis"
"/server/etc/redis/tls"
"/server/etc/redis/config"
"/server/etc/redis/config/custom"
"/server/postgres"
"/server/pgData"
"/server/logs/postgres"
"/server/logs/postgres/wal_archive"
"/server/etc/postgres"
"/server/etc/postgres/tls"
"/server/mysql"
"/server/data"
"/server/logs/mysql"
"/server/logs/mysql/binlog"
"/server/etc/mysql"
"/server/php"
"/server/php/84"
"/server/php/tools"
"/server/logs/php"
"/server/nginx"
"/server/logs/nginx"
"/server/logs/nginx/access"
"/server/logs/nginx/error"
"/server/etc/nginx"
"/server/etc/nginx/custom"
"/server/sites"
"/server/sites/tls"
"/server/sites/available"
"/server/sites/enabled"
)
echo "-----开始创建server目录-----"
for((i=0;i<${#server_array[*]};i++));
do
echo ${server_array[i]}
func_create ${server_array[i]}
done
echo "-----server目录创建结束 -----"
#!/usr/bin/env bash
func_chown_sqlite(){
chown sqlite:sqlite -R $1
find $1 -type f -exec chmod 640 {} \;
find $1 -type d -exec chmod 750 {} \;
}
func_chown_redis(){
chown redis:redis -R $1
find $1 -type f -exec chmod 640 {} \;
find $1 -type d -exec chmod 750 {} \;
}
func_chown_postgres(){
chown postgres:postgres -R $1
find $1 -type f -exec chmod 640 {} \;
find $1 -type d -exec chmod 750 {} \;
}
func_chown_mysql(){
chown mysql:mysql -R $1
find $1 -type f -exec chmod 640 {} \;
find $1 -type d -exec chmod 750 {} \;
}
func_chown_phpFpm(){
chown php-fpm:php-fpm -R $1
find $1 -type f -exec chmod 640 {} \;
find $1 -type d -exec chmod 750 {} \;
}
func_chown_nginx(){
chown nginx:nginx -R $1
find $1 -type f -exec chmod 640 {} \;
find $1 -type d -exec chmod 750 {} \;
}
func_chown_www(){
chown emad:emad -R $1
find $1 -type f -exec chmod 640 {} \;
find $1 -type d -exec chmod 750 {} \;
}
chown_sqlite_array=(
"/server/sqlite"
);
chown_redis_array=(
"/server/redis"
"/server/redis/rdbData"
"/server/logs/redis"
"/server/etc/redis"
);
chown_postgres_array=(
"/server/postgres"
"/server/pgData"
"/server/logs/postgres"
"/server/etc/postgres"
);
chown_mysql_array=(
"/server/mysql"
"/server/data"
"/server/logs/mysql"
"/server/etc/mysql"
);
chown_phpFpm_array=(
"/server/php"
"/server/logs/php"
);
chown_nginx_array=(
"/server/nginx"
"/server/logs/nginx"
"/server/etc/nginx"
"/server/sites"
);
chown_www_array=(
"/www"
);
echo "-----开始设置 sqlite 用户权限目录-----"
for((i=0;i<${#chown_sqlite_array[*]};i++));
do
echo ${chown_sqlite_array[i]}
func_chown_sqlite ${chown_sqlite_array[i]}
done
echo "-----sqlite 用户权限目录设置结束-----"
echo "-----开始设置 redis 用户权限目录-----"
for((i=0;i<${#chown_redis_array[*]};i++));
do
echo ${chown_redis_array[i]}
func_chown_redis ${chown_redis_array[i]}
done
echo "-----redis 用户权限目录设置结束-----"
echo "-----开始设置 postgres 用户权限目录-----"
for((i=0;i<${#chown_postgres_array[*]};i++));
do
echo ${chown_postgres_array[i]}
func_chown_postgres ${chown_postgres_array[i]}
done
echo "-----postgres 用户权限目录设置结束-----"
echo "-----开始设置 mysql 用户权限目录-----"
for((i=0;i<${#chown_mysql_array[*]};i++));
do
echo ${chown_mysql_array[i]}
func_chown_mysql ${chown_mysql_array[i]}
done
echo "-----mysql 用户权限目录设置结束-----"
echo "-----开始设置 php-fpm 用户权限目录-----"
for((i=0;i<${#chown_phpFpm_array[*]};i++));
do
echo ${chown_phpFpm_array[i]}
func_chown_phpFpm ${chown_phpFpm_array[i]}
done
echo "-----php-fpm 用户权限目录设置结束-----"
echo "-----开始设置nginx用户权限目录-----"
for((i=0;i<${#chown_nginx_array[*]};i++));
do
echo ${chown_nginx_array[i]}
func_chown_nginx ${chown_nginx_array[i]}
done
echo "-----nginx用户权限目录设置结束-----"
echo "-----开始设置 开发者 用户权限目录-----"
for((i=0;i<${#chown_www_array[*]};i++));
do
echo ${chown_www_array[i]}
func_chown_www ${chown_www_array[i]}
done
echo "-----开发者 用户权限目录设置结束-----"
#!/usr/bin/env bash
# 获取当前脚本的路径
SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
for i in `ls *.tar.gz`;
do
echo "gz解压" $i
tar -xzf $i
done
for i in `ls *.tar.bz2`;
do
echo "bz2解压" $i
tar -xjf $i
done
for i in `ls *.tar.xz`;
do
echo "xz解压" $i
tar -xJf $i
done
if [ -d "${SCRIPT_PATH}/php_ext" ]; then
EXT_DIR="${SCRIPT_PATH}/php_ext"
cd ${EXT_DIR}
for i in `ls *.tgz`;
do
echo "tgz解压" $i
tar -xzf $i
done
cd ${EXT_DIR}
rm package.xml
fi
#!/usr/bin/env bash
printf "\033c"
echo_cyan(){
printf '\033[1;36m%b\033[0m\n' "$@"
}
echo_green(){
printf '\033[1;32m%b\033[0m\n' "$@"
}
echo_red(){
printf '\033[1;31m%b\033[0m\n' "$@"
}
echo_yellow(){
printf '\033[1;33m%b\033[0m\n' "$@"
}
echo_cyan "删除/var/cache/apt/archives/下所有包文件"
apt clean
echo_cyan "仅删除过期的缓存文件"
apt autoclean
echo_cyan "删除不再被依赖的软件包"
apt autoremove --purge
echo_cyan "立即清空日志"
rm -rf /var/log/*
echo_cyan "是否清理zsh_history文件(1清理/默认不清理):"
read num
if [[ "$num" = "1" ]]; then
echo_yellow "开始清理终端历史文件文件"
rm /home/{sqlite,redis,postgres,mysqld-84,php-fpm,nginx,emad}/.{zsh,bash}_history
rm /home/{sqlite,redis,postgres,mysqld-84,php-fpm,nginx,emad}/.{z,viminfo}
rm /home/{sqlite,redis,postgres,mysqld-84,php-fpm,nginx,emad}/.zcompdump-*
rm /root/.{zsh,bash}_history
rm /root/.{z,viminfo}
rm /root/.zcompdump-*
echo_yellow "清理终端历史文件结束"
else
echo_yellow "不清理 .zsh_history 文件"
fi
echo_red "警告⚠️:请谨慎执行此脚本!!!"
echo_yellow "清理日志需停止服务"
echo_cyan "是否清理lnmpp日志(1清理/默认不清理):"
read num1
if [ "$num1" = "1" ]; then
echo_green "先停止服务"
systemctl stop {redis,postgres,mysqld-84,php84-fpm,nginx}.service
echo_green "开始清理redis日志"
find /server/logs/redis/ -type f -exec rm {} \;
echo_green "开始清理postgres日志"
find /server/logs/postgres/ -type f -exec rm {} \;
echo_green "开始清理mysql错误日志"
find /server/logs/mysql/ -type f -exec rm {} \;
echo_green "开始清理php日志"
find /server/logs/php/ -type f -exec rm {} \;
echo_green "开始清理nginx错误日志"
find /server/logs/nginx/error/ -type f -exec rm {} \;
echo_green "开始清理nginx访问日志"
find /server/logs/nginx/access/ -type f -exec rm {} \;
find /server/nginx/logs/ -type f -exec rm {} \;
echo_green "清理lnmpp日志完成"
else
echo_yellow "不清理lnmpp日志"
fi
echo_red "警告⚠️:请谨慎执行此脚本!!!"
echo_cyan "是否清理二进制日志(1清理/默认不清理):"
read num2
if [ "$num2" = "1" ]; then
echo_green "先停止服务"
systemctl stop {redis,postgres,mysqld-84,php84-fpm,nginx}.service
echo_green "开始清理PostgreSQL预写式日志"
rm /server/logs/postgres/wal_archive/*
echo_green "开始清理MySQL二进制日志"
rm /server/logs/mysql/binlog/*
echo_green "清理lnpp日志完成"
else
echo_yellow "不清理数据库二进制日志"
fi
echo_cyan "是否清理Redis本地存储(1清理/默认不清理):"
read num3
if [[ "$num3" = "1" ]]; then
echo_yellow "开始清理Redis本地存储"
find /server/redis/rdbData/ -type f -exec rm {} \;
find /server/redis/rdbData/appendonlydir/ -type f -exec rm {} \;
echo_yellow "清理Redis本地存储结束"
else
echo_yellow "不清理Redis本地存储"
fi
echo_cyan "是否启动服务(1启动/默认不启动):"
read num2
if [ "$num2" = "1" ]; then
systemctl start {redis,postgres,mysqld-84,php84-fpm,nginx}.service
echo_green "服务已重新启动"
else
echo_yellow "未重启服务,请手动启动"
fi
#!/usr/bin/env bash
printf "\033c"
echo_cyan(){
printf '\033[1;36m%b\033[0m\n' "$@"
}
echo_green(){
printf '\033[1;32m%b\033[0m\n' "$@"
}
echo_red(){
printf '\033[1;31m%b\033[0m\n' "$@"
}
echo_yellow(){
printf '\033[1;33m%b\033[0m\n' "$@"
}
echo_cyan "删除/var/cache/apt/archives/下所有包文件"
apt clean
echo_cyan "仅删除过期的缓存文件"
apt autoclean
echo_cyan "删除不再被依赖的软件包"
apt autoremove --purge
echo_cyan "立即清空日志"
rm -rf /var/log/*
echo_cyan "是否清理终端历史文件(1清理/默认不清理):"
read num
if [[ "$num" = "1" ]]; then
echo_yellow "开始清理终端历史文件"
rm /home/emad/.{zsh,bash}_history
rm /home/emad/.{z,viminfo}
rm /home/emad/.zcompdump-*
rm /root/.{zsh,bash}_history
rm /root/.{z,viminfo}
rm /root/.zcompdump-*
echo_yellow "清理终端历史文件结束"
else
echo_yellow "不清理终端历史文件"
fi
包列表
1. sqlite-autoconf-3500400.tar.gz
2. redis-8.2.1.tar.gz
3. mysql-8.4.6.tar.gz
4. postgresql-17.6.tar.bz2
5. php-8.4.12.tar.xz
- 动态扩展
- xdebug-3.4.5.tgz
- apcu-5.1.27.tgz
- mongodb-2.1.1.tgz
- redis-6.2.0.tgz
- yaml-2.2.5.tgz
6. nginx-1.28.0.tar.gz
- openssl-3.5.2.tar.gz
- pcre2-10.45.tar.bz2
- zlib-1.3.1.tar.xz
| package | url |
| -------------- | ---------------------------------------------------- |
| SQLite3 | https://www.sqlite.org/ |
| Redis | https://download.redis.io/redis-stable.tar.gz |
| PostgreSQL | https://www.postgresql.org/ |
| MySQL | https://www.mysql.com/ |
| PHP | https://www.php.net/ |
| PHP extend | http://pecl.php.net/ |
| Nginx | http://nginx.org/ |
| zlib | http://www.zlib.net/ |
| openssl | https://openssl-library.org/ |
| openssl-1.1.1w | https://www.openssl.org/source/openssl-1.1.1w.tar.gz |
| pcre2 | https://github.com/PCRE2Project/pcre2 |
| icu4c | https://github.com/unicode-org/icu/releases/ |
请求生命周期
Nginx 请求生命周期
在 Nginx 中,处理一次请求的整个生命周期涉及到 master 进程和 worker 进程的协同工作。以下是详细介绍每个阶段中 master 进程和 worker 进程的作用:
1. 初始化阶段
- master 进程:负责读取和验证 Nginx 配置文件,初始化工作环境。
- worker 进程:由 master 进程根据配置文件中定义的 worker_processes 参数创建,数量通常与服务器的 CPU 核数一致,以充分利用多核特性。
2. 监听和接受连接阶段
- master 进程:在初始化完成后,master 进程会监听配置中指定的端口。
- worker 进程:负责接受来自客户端的连接请求。由于 worker 进程与 CPU 核心绑定,可以高效地处理并发连接。
3. 处理请求阶段
- master 进程:不直接参与请求的处理,主要负责监控和管理 worker 进程。
- worker 进程:每个 worker 进程只有一个线程,它们独立处理请求,执行如解析请求头、查找配置、重写 URI、访问权限检查等任务,并将结果返回给客户端。
4. 发送响应阶段
- master 进程:继续监控 worker 进程的状态,确保系统稳定运行。
- worker 进程:负责将响应数据发送回客户端,并记录访问日志。
5. 关闭连接阶段
- master 进程:不直接参与连接的关闭。
- worker 进程:在完成响应后,负责关闭与客户端的连接。
6. 重载和退出阶段
- master 进程:处理信号如 SIGTERM 来平滑退出或重新加载配置文件,创建新的 worker 进程来替换旧的 worker 进程,从而不影响正在处理的请求。
- worker 进程:在收到退出信号后,完成当前请求的处理,然后退出。
总结:
- 客户端发起的所有请求均由 worker 进程处理,而 master 进程不直接参与处理这些请求。
- 也就是说,客户端发起请求只使用了 worker 进程用户权限,不会涉及到 master 进程用户权限
问题:浏览器是否具备 Nginx worker 进程用户的权限?
浏览器作为客户端,不具备 nginx worker 进程用户的任何权限,浏览器只是通过建立的 TCP 连接来接收 Nginx worker 进程发送的内容
PHP-FPM 请求生命周期
php-fpm 处理一次请求的生命周期包括请求接收、请求处理和请求结束三个阶段:
1. 请求接收阶段
当 php-fpm 的 master 进程接收到一个来自 Web 服务器(如 Nginx)的请求时,它会加锁以避免多个 worker 进程同时处理同一个请求,这在 Linux 中称为"惊群问题"。
Master 进程随后会指派一个可用的 worker 进程来处理这个请求。如果没有可用的 worker 进程,将返回错误,这也是在使用 Nginx 配合 php-fpm 时可能遇到 502 错误的一个原因。
2. 请求处理阶段
被指派的 worker 进程开始处理请求。这个过程中,worker 进程会执行 PHP 脚本并生成响应结果。如果处理过程超时,可能会返回 504 错误。
Worker 进程内部嵌入了 PHP 解释器,是 PHP 代码实际执行的地方。
3. 请求结束阶段
一旦 worker 进程完成了请求处理,它会将结果返回给 Web 服务器,从而完成整个请求的处理周期。
用户说明
在用户脚本中我们创建了多个用户,其中 Nginx
和 PHP-FPM
进程和用户关系比较复杂:
| 用户名 | 说明 |
| -------- | --------------- |
| emad | 开发者用户 |
| sqlite | SQLite3 用户 |
| redis | Redis 用户 |
| postgres | PostgreSQL 用户 |
| mysql | MySQL 用户 |
| php-fpm | PHP-FPM 用户 |
| nginx | Nginx 用户 |
| process | user |
| -------------- | ------- |
| Nginx master | nginx |
| Nginx worker | nginx |
| PHP-FPM master | php-fpm |
| PHP-FPM pool | php-fpm |
> Nginx 主进程:
master 进程用户需要有 worker 进程用户的全部权限,master 进程用户类型:
1. 特权用户(root):worker 进程可以指定为其它非特权用户;
2. 非特权用户:worker 进程跟 master 进程是同一个用户。
> Nginx 工作进程:
- worker 进程负责处理实际的用户请求;
- 代理转发和接收代理响应都是由 worker 进程处理。
> Nginx 配置文件 `user` 指令限制说明:
- 主进程是特权用户(root):`user` 指令是有意义,用于指定工作进程用户和用户组;
- 主进程是非特权用户:`user` 指令没有意义,会被 Nginx 程序忽略掉。
> PHP-FPM 主进程:
- master 进程负责管理 pool 进程;
- master 进程创建和管理 pool 进程的 sock 文件;
- master 进程需要有 pool 进程用户的全部权限,master 进程用户类型:
1. 特权用户(root):pool 进程可以指定为其它非特权用户
2. 非特权用户:pool 进程用户跟 master 进程用户相同
> PHP-FPM 工作池进程:
- pool 进程独立地处理请求,执行 PHP 脚本代码;
- pool 进程处理完 PHP 代码后,会直接将结果返回给客户端。
> 具体流程如下:
1. Nginx master 进程:当有新的请求到来时,
master 进程会将其分配给一个 worker 进程来处理。
2. Nginx worker 进程:如果请求是静态资源,则直接返回给客户端;如果请求是 PHP 文件,
则通过 `PHP-FPM pool` 进程的 sock 文件将请求转发给 PHP-FPM 进行处理。
3. PHP-FPM master 进程:当收到 `PHP-FPM pool` 进程的 sock 文件传递的请求时,
master 进程会将其分配给一个 pool 进程来处理。
4. PHP-FPM pool 进程:pool 进程执行和处理 PHP 代码,
并将返回结果发送给对应的 sock 文件
5. 返回结果:Nginx 的 worker 进程,接收 sock 文件的响应内容,
再将处理后的动态内容返回给客户端。
> nginx 站点代理转发 php 请求时:
- Nginx 主进程用户不需要对 PHP-FPM 的 socket 文件拥有任何权限,
处理请求的是工作进程;
- Nginx 工作进程用户需要对 PHP-FPM 的 socket 文件具有 `读+写` 权限;
1. `r` 权限:Nginx 工作进程需要读取 socket 文件以发送请求到 PHP-FPM。
2. `w` 权限:Nginx 工作进程也需要写入权限,以便接收来自 PHP-FPM 的响应。
- PHP-FPM 主进程用户需要对 sock 文件具有全部权限;
1. 创建/删除 pool 进程的 sock 文件。
2. 监听指定的端口或 Unix 套接字文件,以便接收来自 Web 服务器(如 Nginx)的请求。
> PHP-FPM 的套接字文件:
- pool 进程的 sock 文件监听用户: `php-fpm`;
- pool 进程的 sock 文件监听用户组: `nginx`;
- pool 进程的 sock 文件监听权限: `0660`;
- 用户 php-fpm 的附属用户组增加 `nginx`。
> 使用 tcp/ip 的方式建立代理转发通道时,nginx 站点代理转发 php 请求时:
- Nginx 的主进程和工作进程均不需要拥有 php-fpm 相关的权限;
- PHP-FPM 主进程和工作进程同样不需要拥有 nginx 相关的权限。
用户职责
用户 nginx
是 Nginx worker 进程的 Unix 用户用户 php-fpm
是 PHP-FPM 子进程的 Unix 用户用户 emad
是开发者操作项目资源、文件的用户
用户权限
1. 对静态文件需提供 `读` 的权限
2. 对 php-fpm 的 `unix socket` 文件提供了读写的权限
客户端(如浏览器)使用 nginx 用户浏览网站:
1. 加载静态文件;
2. php-fpm 的 `unix socket` 文件传输
- 使用 socket 转发,nginx 用户可作为 FPM 的监听用户,
如:监听 socket、连接 web 服务器,权限设为 `660`
- 使用 `TCP/IP` 转发是,PHP-FPM 无需监听用户
1. FPM 进程运行的 Unix 用户,对 php 脚本、php 所需的配置文件需要 `读` 的权限;
2. 当 php 需要操作文件或目录时,需要提供 `读+写` 权限:
- 如:框架中记录运行时日志、缓存的 runtime 目录,就需要 `读+写` 全新
3. 除此以外,php-fpm 用户通常不需要其他权限
- 开发环境:需要对 php 文件、静态文件有 `读+写` 的权限;
- 部署环境:平时可以不提供任何权限,因为该用户与服务没有关联;
- 部署环境:对需要变动的文件,需要具有`读+写`的权限;
> 说明:emad 泛指开发者账户,你可以取其它名字
提示
新版 lnmpp 环境将采用 TCP/IP
代理转发方式,所以 socket 文件权限不需要过多考虑
用户及用户组
1. 设置站点基目录权限
chown root:root /www
chmod 755 /www
chown emad:emad /www
chmod 750 -R /www
2. tp 站点权限案例
- 对于php文件,php-fpm 需要读取权限
- 对于页面文件,nginx 需要读取权限
- 对于上传文件,php-fpm 需要读写权限,nginx需要读取权限
- 对于缓存目录,php-fpm 需要读写权限
- 对于入口文件,php-fpm 需要读取权限, nginx不需要任何权限(直接走代理转发)
- 如果是开发环境,开发用户对所有文件都应该拥有读写权限
chown php-fpm:php-fpm -R /www/tp
find /www/tp -type f -exec chmod 440 {} \;
find /www/tp -type d -exec chmod 550 {} \;
# 部分目录需确保nginx可以访问和进入
chmod php-fpm:nginx -R /www/tp /www/tp/public /www/tp/public/static \
/www/tp/public/static/upload
# 部分文件需确保nginx可以访问
chmod 440 /www/tp/public/{favicon.ico,robots.txt}
# 缓存和上传目录需要写入权限
chmod 750 /www/tp/public/static/upload /www/tp/runtime
chown emad:emad -R /www/tp
find /www/tp -type f -exec chmod 640 {} \;
find /www/tp -type d -exec chmod 750 {} \;
chmod 770 /www/tp/public/static/upload
chmod 770 /www/tp/runtime
3. laravel 站点权限案例
- 对于php文件,php-fpm 需要读取权限
- 对于页面文件,nginx 需要读取权限
- 对于上传文件,php-fpm需要读写权限,nginx需要读取权限
- 对于缓存目录,php-fpm需要读写权限
- 对于入口文件,php-fpm需要读取权限, nginx不需要任何权限(直接走代理转发)
- 如果是开发环境,开发用户对所有文件都应该拥有读写权限
chown php-fpm:php-fpm -R /www/laravel
find /www/laravel -type f -exec chmod 440 {} \;
find /www/laravel -type d -exec chmod 550 {} \;
# 部分目录需确保nginx可以访问和进入
chmod php-fpm:nginx -R /www/laravel /www/laravel/public \
/www/laravel/public/static /www/laravel/public/static/upload
# 部分文件需确保nginx可以访问
chmod 440 /www/laravel/public/{favicon.ico,robots.txt}
# 缓存和上传目录需要写入权限
chmod 750 /www/laravel/public/static/upload
find /www/laravel/storage/ -type d -exec chmod 750 {} \;
chown emad:emad -R /www/laravel
find /www/laravel -type f -exec chmod 640 {} \;
find /www/laravel -type d -exec chmod 750 {} \;
# php读写 nginx读
chmod 770 /www/laravel/public/static/upload
# php读写
find /www/laravel/storage/* -type d -exec chmod 770 {} \;
umask 权限
# ~/.profile
# 第9行 umask 022处新建一行
umask 027 # 创建的文件权限是 640 目录权限是 750
# /etc/profile
# 第9行 umask 022 处新建一行
# 即使客户端上传了木马上来,也没得执行
umask 022 # 创建的文件权限是 644 目录权限是 755
# 提示:
# - php-fpm 用户的进程通常不会进入终端,所以只能在系统级别的初始化文件里设置
# - 但是这样一来其它用户的权限也会跟着改变,需要慎重处理
# - 建议使用php自身来限制上传文件的权限
⚠️注意
bash/zsh 配置文件开头需要载入 ~/.profile
:
# ~/.(bashrc|zshrc)
source ~/.profile
编译器选择
PostgreSQL 推荐使用 CLANG+LLVM
编译套件,--with-llvm
启用 JIT 支持,能提升查询性能;其余软件优先使用 GCC
编译套件。
系统级优化
⚠️ 需理解的是:
系统级的配置仅代表这台服务器最大允许的值,软件的配置文件通常也会提供对应值来做更精准的限制。
内核管理
服务器中如果存在多个主要的服务,优化内核就需要兼顾到各种服务的特性,这里通过修改 sysctl 配置文件(/etc/sysctl.d/*.conf
)来实现:
1. 虚拟内存限制
控制进程是否允许使用虚拟内存
- 0:进程只能使用物理内存(默认值)
- 1:进程可以使用比物理内存更多的虚拟内存
echo "vm.overcommit_memory = 1" > /etc/sysctl.d/overcommit_memory.conf
2. 全连接队列长度
TCP 全连接队列(Accept 队列)最大长度,即已完成三次握手但未被应用层 accept() 的连接数
- debian13 默认为 4096,通常足够
- 超过 65535 需确认内核是否支持
echo "net.core.somaxconn = 4096" > /etc/sysctl.d/somaxconn.conf
3. 半连接队列长度
TCP 半连接队列(SYN 队列)最大长度,即处于 SYN_RECV 状态的未完成握手连接数
- debian13 默认为 512,存在高并发服务建议设为 4096
- 增大值会占用更多内存
echo "net.ipv4.tcp_max_syn_backlog = 4096" > /etc/sysctl.d/tcp_max_syn_backlog.conf
⚠️ 注意:
在 Debian 13 中,sysctl 的配置文件路径发生了显著变化,从传统的单一文件 /etc/sysctl.conf
转向了模块化的分散配置目录。
以下是 Debian13 具体的配置文件路径及其优先级规则:
路径 | 优先级 | 说明 |
---|---|---|
/etc/sysctl.d/*.conf | 1 | 优先级最高 |
/run/sysctl.d/*.conf | 2 | 重启失效 |
/usr/local/lib/sysctl.d/*.conf | 3 | 第三方软件配置 |
/usr/lib/sysctl.d/*.conf | 4 | 系统默认配置 |
/lib/sysctl.d/*.conf | 5 | 兼容旧版 |
sysctl --system # 加载所有配置文件
sysctl -a # 检查所有生效参数
sysctl vm.overcommit_memory # 查看特定参数
资源管理
限制资源的目的是为了防止单个用户或进程过度消耗系统资源(如 CPU、内存、文件打开数等),从而保障系统的稳定性和安全性。
通过修改 /etc/security/limits.conf
配置文件,可以对用户或用户组在系统上使用资源的最大值进行限制,具体说明如下:
1. 配置文件
limits 主要分主配置文件和模块化配置文件,这里推荐使用模块化配置文件:
配置文件 | 说明 |
---|---|
/etc/security/limits.conf | 资源限制主配置文件 |
/etc/security/limits.d/*.conf | 模块化管理配置文件 |
2. 案例
# 针对 postgres 用户
echo "postgres soft nofile 65535
postgres hard nofile 65535" > /etc/security/limits.d/postgres.conf
# 针对 redis 用户
echo "redis soft nofile 65535
redis hard nofile 65535" > /etc/security/limits.d/redis.conf
echo "
# 针对 postgres 用户
postgres soft nofile 65535
postgres hard nofile 65535
# 针对 redis 用户
echo "redis soft nofile 65535
redis hard nofile 65535
" >> /etc/security/limits.conf
日志管理
Linux/Unix 系统使用 Logrotate
来管理日志文件。
echo "/server/logs/redis/redis-server.log {
monthly
maxsize 100M
missingok
rotate 12
compress
delaycompress
dateext
dateformat -%Y%m%d.%s
dateyesterday
create 0640 redis redis
sharedscripts
postrotate
if [ -f /run/redis/process.pid ]; then
/usr/bin/kill -USR1 \$(/bin/cat /run/redis/process.pid)
fi
endscript
}" > /etc/logrotate.d/redis
echo "
/server/logs/nginx/access/*.log {
daily
maxsize 100M
missingok
rotate 30
compress
delaycompress
dateext
dateformat -%Y%m%d.%s
notifempty
create 0640 nginx nginx
sharedscripts
postrotate
if [ -f /run/nginx/process.pid ]; then
/usr/bin/kill -USR1 \$(/bin/cat /run/nginx/process.pid)
fi
endscript
}
/server/logs/nginx/error/*.log {
monthly
maxsize 100M
missingok
rotate 12
compress
delaycompress
dateext
dateformat -%Y%m%d.%s
notifempty
create 0640 nginx nginx
sharedscripts
postrotate
if [ -f /run/nginx/process.pid ]; then
/usr/bin/kill -USR1 \$(/bin/cat /run/nginx/process.pid)
fi
endscript
}
" > /etc/logrotate.d/nginx
echo "/server/logs/redis/redis-server.log {
# 轮转周期 daily/weekly/monthly/yearly 对应 每天/每周/每月/每年
# 每天轮转(切割)一次
daily
# 若日志大小超过100M,即使未到周期也轮转
maxsize 100M
# 日志文件不存在时忽略错误,继续执行
missingok
# 保留30个备份
rotate 30
# 压缩旧日志以节省空间(使用gzip)
compress
# 延迟压缩:本次轮转的日志在下一次轮转时才压缩
delaycompress
# 使用日期作为轮转文件的后缀
dateext
# 可选:自定义日期格式,例如 -20250901.1714900000
dateformat -%Y%m%d.%s
# 可选:使用前一天的日期而非轮转执行日期
dateyesterday
# 可选:使用前一天的日期而非轮转执行日期
# dateyesterday
# 复制当前日志文件后清空原文件,避免需重启Redis或发送信号(与 create 二选一)
# copytruncate
# 轮转后创建的新日志文件权限和属主(用户和组请根据实际情况调整)
create 0640 redis redis
# 所有日志文件处理完后,只执行一次postrotate脚本
sharedscripts
# 轮转后执行的脚本
postrotate
# 如果使用 copytruncate 无须发送信号,但有可能丢失极小量的日志
# 通过pid文件向Redis进程发送USR1信号,使其重新打开日志文件
if [ -f /run/redis/process.pid ]; then
# 这里是写入文件,需注意“$”符号转义
/usr/bin/kill -USR1 \$(/bin/cat /run/redis/process.pid)
fi
endscript
}" > /etc/logrotate.d/redis
常用指令 | 描述 |
---|---|
logrotate -vf /etc/logrotate.conf | 强制立即轮转所有配置文件(-v 显示详细过程) |
logrotate -vf /etc/logrotate.d/your_config | 强制立即轮转指定的配置文件(-v 显示详细过程) |
logrotate -d /etc/logrotate.d/your_config | 模拟轮转过程并显示详细信息 |
grep logrotate /var/log/syslog | 查看 logrotate 自身的执行记录和可能出现的错误信息 |
⚠️ copytruncate/create
两者区别
copytruncate :
- 复制当前日志文件后清空原文件,避免需重启 Redis 或发送信号
- 对于不支持主动重新打开日志的程序非常有用
- 但理论上在复制和清空之间可能有极小量的日志丢失
create :
- 确保日志不丢失,
- 需要配置
Logrotate
脚本,在轮转后对程序发送信号,重新打开日志
手动安装依赖
通常有两种情况需要手动安装依赖项:
- 在较新的系统发行版上安装较旧的软件包;
- 在较旧的系统发行版上安装较新的软件包。
⚠️ 警告
我们推荐使用发行版自带的依赖库解决编译依赖问题;
因为手动安装的依赖会对系统资源造成浪费,处理不当也会对系统自身的依赖链造成混淆;
只有在必要且管理员知道自己在做什么的情况下,才能手动安装依赖项解决问题;
对于正式环境我们有以下几点建议:
1. 发行版必须为正式发行版;
2. 发行版尽可能是长期支持,可以延长漏洞修复周期;
3. 对应软件版本在此发行版做过权威测试,如:官方机构。
1. 使用长期支持的稳定版;
2. 上线存在中型及以上项目时,坚决不做版本升级;
3. 软件包不追求最新,以对项目框架友好为优先。
1. 新项目使用官方还在维护的长期支持版;
2. 新旧项目使用的框架要相似,最好只做版本迭代,可减少维护成本;
3. 使用开发人员熟悉的框架,再好的框架不会用也白瞎。
编译安装 OpenSSL
如果对 openssl 依赖库版本有特殊需求,需自行编译安装特定版本
# 作为公共依赖库,推荐以root用户安装它
mkdir /server/openssl-1.1.1w
cd /root/openssl-1.1.1w/
./config --prefix=/server/openssl-1.1.1w \
--openssldir=/server/openssl-1.1.1w \
no-shared \
zlib
# 作为公共依赖库,推荐以root用户安装它
mkdir /server/openssl-3.0.17
cd /root/openssl-3.0.17/
./config --prefix=/server/openssl-3.0.17 \
--openssldir=/server/openssl-3.0.17 \
no-shared \
zlib
make -j4 > make.log
make test > make-test.log
make install
# 设置新的 PKG_CONFIG_PATH,排除系统默认的 OpenSSL 库路径
export PKG_CONFIG_PATH=/server/openssl-1.1.1w/lib/pkgconfig:$PKG_CONFIG_PATH
# 使用下面指令检查,是否正确替换
pkg-config --path openssl,libssl,libcrypto
# 成功替换展示:
/server/openssl-1.1.1w/lib/pkgconfig/openssl.pc
/server/openssl-1.1.1w/lib/pkgconfig/libssl.pc
/server/openssl-1.1.1w/lib/pkgconfig/libcrypto.pc
# 未成功替换展示:
/usr/lib/x86_64-linux-gnu/pkgconfig/openssl.pc
/usr/lib/x86_64-linux-gnu/pkgconfig/libssl.pc
/usr/lib/x86_64-linux-gnu/pkgconfig/libcrypto.pc
# 设置新的 PKG_CONFIG_PATH,排除系统默认的 OpenSSL 库路径
export PKG_CONFIG_PATH=/server/openssl-3.0.17/lib64/pkgconfig:$PKG_CONFIG_PATH
# 使用下面指令检查,是否正确替换
pkg-config --path openssl,libssl,libcrypto
# 成功替换展示:
/server/openssl-3.0.17/lib/pkgconfig/openssl.pc
/server/openssl-3.0.17/lib/pkgconfig/libssl.pc
/server/openssl-3.0.17/lib/pkgconfig/libcrypto.pc
# 未成功替换展示:
/usr/lib/x86_64-linux-gnu/pkgconfig/openssl.pc
/usr/lib/x86_64-linux-gnu/pkgconfig/libssl.pc
/usr/lib/x86_64-linux-gnu/pkgconfig/libcrypto.pc
编译安装 ICU4C
Debian/Ubuntu 系统仓库中对 ICU4C
库的命名约定为 libicu
。
系统自带的 libicu 版本如果不能满足编译环境需求,就需要通过手动编译特定版本来满足需求。
mkdir /server/icu4c-72_1
wget https://github.com/unicode-org/icu/releases/download/release-72-1/icu4c-72_1-src.tgz
tar - xzf icu4c-72_1-src.tgz
cd icu/source/
./configure --prefix=/server/icu4c-72.1 \
--enable-static
make -j4 > make.log
make check > make-check.log
make install
# 设置新的 PKG_CONFIG_PATH,排除系统默认的 icu 库路径
export PKG_CONFIG_PATH=/server/icu4c-72.1/lib/pkgconfig:$PKG_CONFIG_PATH
# 使用下面指令检查,是否正确替换
pkg-config --path icu-i18n, icu-io, icu-uc
# 成功替换展示:
/server/icu4c-72.1/lib/pkgconfig/icu-i18n.pc
/server/icu4c-72.1/lib/pkgconfig/icu-io.pc
/server/icu4c-72.1/lib/pkgconfig/icu-uc.pc
# 未成功替换展示:
/usr/lib/x86_64-linux-gnu/pkgconfig/icu-i18n.pc
/usr/lib/x86_64-linux-gnu/pkgconfig/icu-io.pc
/usr/lib/x86_64-linux-gnu/pkgconfig/icu-uc.pc
附录一、输出重定向
在 Linux 系统中,输出重定向是一个非常重要的功能,它允许你将命令的输出内容(包括标准输出和标准错误输出)重定向到文件、其他命令或丢弃,而不是默认显示在终端上。掌握输出重定向可以帮助你更好地控制命令的输出,记录日志,调试程序等。
基本概念
在 Linux 中,每个命令通常有以下三种输出流:
流类型 | 文件描述符 | 含义 | 默认输出位置 | 英文别名 |
---|---|---|---|---|
标准输出 | 1 | 正常输出结果 | 终端 | Standard Output,stdout |
标准错误输出 | 2 | 错误或警告信息 | 终端 | Standard Error,stderr |
标准输入 | 0 | 输入来源 | 键盘输入 | Standard Input,stdin |
输出重定向主要涉及 stdout
(1)和 stderr
(2),通过重定向符号可以将这些输出流重定向到文件或其他地方。
常用输出重定向符号
符号 | 含义 | 示例 |
---|---|---|
> | 将 stdout 重定向到文件(覆盖) | command > file |
>> | 将 stdout 重定向到文件(追加) | command >> file |
2> | 将 stderr 重定向到文件(覆盖) | command 2> file |
2>> | 将 stderr 重定向到文件(追加) | command 2>> file |
&> | 将 stdout 和 stderr 重定向到文件(覆盖) | command &> file |
&>> | 将 stdout 和 stderr 重定向到文件(追加) | command &>> file |
2>&1 | 将 stderr 重定向到 stdout(覆盖) | command 2>&1 file |
| | 管道符号,传递信息给另 1 个命令 | ls -l / | grep 'server' |
tee | 将数据同时写入到文件和终端 | command | tee file |
案例展示
# 终端输出错误和警告,文件记录正常信息
cmake \
-DWITH_DEBUG=ON \
-DCMAKE_INSTALL_PREFIX=/server/mysql \
-DWITH_SYSTEMD=ON \
-DFORCE_COLORED_OUTPUT=ON \
-DWITH_MYSQLX=OFF \
-DWITH_UNIT_TESTS=OFF \
-DINSTALL_MYSQLTESTDIR= \
.. > stdout.log
# 终端输出正常信息,文件记录错误和警告
cmake \
-DWITH_DEBUG=ON \
-DCMAKE_INSTALL_PREFIX=/server/mysql \
-DWITH_SYSTEMD=ON \
-DFORCE_COLORED_OUTPUT=ON \
-DWITH_MYSQLX=OFF \
-DWITH_UNIT_TESTS=OFF \
-DINSTALL_MYSQLTESTDIR= \
.. 2> stdout.log
附录二、预构建包一键安装脚本
- Postgres 默认有个超级管理员用户 `admin` 密码 `1`
- MySQL 默认有个本地用户 `admin@localhost` 密码 `1`
- MySQL 默认有个局域网用户 `admin@'192.168.%.%'` 密码 `1`
- Redis 默认设置了全局密码 `1`
#!/usr/bin/env bash
printf "\033c"
echo_cyan(){
printf '\033[1;36m%b\033[0m\n' "$@"
}
echo_green(){
printf '\033[1;32m%b\033[0m\n' "$@"
}
echo_red(){
printf '\033[1;31m%b\033[0m\n' "$@"
}
echo_yellow(){
printf '\033[1;33m%b\033[0m\n' "$@"
}
#系统更新到最新
upgradeOS(){
echo_yellow "=================================================================="
echo_red "请使用纯净版操作系统,否则可能会造成系统破坏和数据丢失!!!"
echo_green "操作前请将系统更新至最新,指令如下:"
echo_yellow "=================================================================="
apt update
apt full-upgrade -y
echo_red "条件允许,建议重启系统"
}
#清空原先数据
cleanOldData(){
echo_yellow "=================================================================="
echo_green "清理旧数据"
echo_yellow "=================================================================="
echo_cyan "清理systemctl单元"
systemctl disable --now {redis,postgres,mysqld-84,php84-fpm,nginx}.service
rm /lib/systemd/system/{redis,postgres,mysqld-84,php84-fpm,nginx}.service
systemctl daemon-reload
echo_cyan "清理旧目录 /server,/www 如果有重要数据请先备份"
rm -rf /server /www
echo_cyan "删除旧用户 sqlite,redis,postgres,mysql,php-fpm,nginx 如果有重要数据请先备份"
userdel -r sqlite
userdel -r redis
userdel -r postgres
userdel -r mysql
userdel -r php-fpm
userdel -r nginx
groupdel sqlite
groupdel redis
groupdel postgres
groupdel mysql
groupdel php-fpm
groupdel nginx
}
#创建单个用户
createSingleUser(){
userName=$1
isSupportZsh=$2
echo_green "创建 $userName 用户"
groupadd $userName
useradd -c "$userName service main process user" -g $userName -s /sbin/nologin -m $userName
if [[ "$isSupportZsh" == '1' ]]; then
cp -r /root/{.oh-my-zsh,.zshrc} /home/$userName
chown $userName:$userName -R /home/$userName/{.oh-my-zsh,.zshrc}
fi
}
#创建用户
createUser(){
echo_yellow "=================================================================="
echo_green "创建sqlite,redis,postgres,mysql,php-fpm,nginx的进程用户"
echo_yellow "=================================================================="
echo_red "必须root用户安装并配置成功zsh,才允许支持zsh"
zshState=0
echo_cyan "是否支持启用zsh(1支持,默认不支持):"
read zshState
createSingleUser 'sqlite' $zshState
createSingleUser 'redis' $zshState
createSingleUser 'postgres' $zshState
createSingleUser 'mysql' $zshState
createSingleUser 'php-fpm' $zshState
createSingleUser 'nginx' $zshState
echo ' '
echo_yellow "=================================================================="
echo_green "处理php-fpm的socket文件授权问题"
echo_yellow "当 php-fpm 主进程非特权用户时,需要考虑socket文件权限问题:"
echo_yellow "nginx 如果是通过 sock 文件代理转发给 php-fpm,php-fpm 主进程创建\n sock 文件时需要确保 nginx 子进程用户有读写 sock 文件的权限"
echo_yellow " "
echo_yellow "方式1:采用 sock 文件权限 php-fpm:nginx 660 \n(nginx 权限较少,php-fpm 权限较多)"
echo_cyan "usermod -a -G nginx php-fpm"
echo_yellow " "
echo_yellow "方式2:采用 sock 文件权限 php-fpm:php-fpm 660 \n(nginx 权限较多,php-fpm 权限较少)"
echo_cyan "usermod -a -G php-fpm nginx"
echo_yellow " "
echo_green "此版本使用的是tcp转发,并不需要考虑socket文件转发相关的权限问题"
echo_yellow "=================================================================="
echo ' '
echo_yellow "=================================================================="
echo_green "php编译pgsql扩展,使用指定Postgres安装目录时,需要提供读取libpq相关权限:"
echo_green "php编译sqlite3扩展,使用指定sqlite3自带的pkgconfig时,需要提供读取对应目录的权限:"
echo_cyan "usermod -a -G postgres,sqlite php-fpm"
echo_green "如果使用 apt install libpq-dev libsqlite3-dev -y 依赖包则不需要"
echo_yellow " "
echo_green "此版本使用指定Postgres安装目录以及自己编译的SQLite3"
echo_yellow "=================================================================="
usermod -a -G postgres,sqlite php-fpm
}
#开发用户追加权限
devUserPower(){
devUserName=$1
echo_yellow "=================================================================="
echo_green "开发用户追加权限"
echo_yellow "web/php文件所属用户都是开发用户,所以nginx和php-fpm用户需要追加开发组"
echo_yellow " "
echo_red "部署环境请注释此函数,开发环境需要开启"
echo_red "部署环境不需要开发用户,可直接使用 nginx 用户作为 ftp、ssh 等上传工具的用户"
echo_yellow "=================================================================="
usermod -a -G $devUserName nginx
usermod -a -G $devUserName php-fpm
usermod -a -G sqlite,redis,postgres,mysql,php-fpm,nginx $devUserName
}
#安装依赖包
installPackage(){
echo_yellow "=================================================================="
echo_green "安装依赖"
echo_green "确保 SQLite3/Redis/PostgreSQL/MySQL/PHP/Nginx 必备依赖项"
echo_green "debian12 发行版,如因依赖导致部分功能异常,自行安装相应依赖包即可"
echo_red "注意1:该lnmpp包不兼容其他发行版,因为极有可能因为依赖问题,导致整个环境无法使用"
echo_red "注意2:部分依赖包在部署阶段可能没用,但由于没对单个功能测试,只能选择安装全部依赖"
echo_yellow "=================================================================="
apt install -y build-essential autoconf libtool pkg-config tcl libssl-dev \
clang libicu-dev liblz4-dev libzstd-dev libbison-dev bison flex \
libreadline-dev zlib1g-dev libpam0g-dev libxslt1-dev libxslt1-dev uuid-dev \
libsystemd-dev cmake libtirpc-dev libcurl4-openssl-dev libpng-dev \
libavif-dev libwebp-dev libjpeg-dev libxpm-dev libfreetype-dev libonig-dev \
libcapstone-dev libsodium-dev libzip-dev libyaml-dev libgd-dev libgeoip-dev
}
#安装预构建包
InstallBuild(){
echo_yellow "=================================================================="
echo_green "解压lnmpp预构建包\n含两个目录"
echo_yellow " "
echo_cyan "/server"
echo_cyan "/www"
echo_yellow " "
echo_green "预先编译成功的lnmpp解压到服务器目录下"
echo_yellow "=================================================================="
tar -xJf ./lnmpp.tar.xz -C /
}
#重置Redis数字证书
resetRedisCertificate(){
redisTlsPath=/server/redis/tls
redisTlsScriptPath=/server/redis/gen-test-certs.sh
rm -rf $redisTlsPath
echo_yellow "=================================================================="
echo_green "创建一键生成redis数字证书脚本"
echo_yellow " "
echo_cyan "注意: 不能向其他用户开放权限"
echo_cyan "开发环境: 目录 750/ 文件 640"
echo_cyan "部署环境: 目录 700/ 文件 600"
echo_yellow " "
echo_yellow "=================================================================="
echo_cyan "[+] Create Redis certs script..."
echo "#\!/bin/bash
generate_cert() {
local name=\$1
local cn=\"\$2\"
local opts=\"\$3\"
local keyfile=$redisTlsPath/\${name}.key
local certfile=$redisTlsPath/\${name}.crt
[ -f \$keyfile ] || openssl genrsa -out \$keyfile 2048
openssl req \\
-new -sha256 \\
-subj \"/O=Redis Test/CN=\$cn\" \\
-key \$keyfile | \\
openssl x509 \\
-req -sha256 \\
-CA $redisTlsPath/ca.crt \\
-CAkey $redisTlsPath/ca.key \\
-CAserial $redisTlsPath/ca.txt \\
-CAcreateserial \\
-days 365 \\
\$opts \\
-out \$certfile
}
mkdir $redisTlsPath
[ -f $redisTlsPath/ca.key ] || openssl genrsa -out $redisTlsPath/ca.key 4096
openssl req \\
-x509 -new -nodes -sha256 \\
-key $redisTlsPath/ca.key \\
-days 3650 \\
-subj '/O=Redis Test/CN=Certificate Authority' \\
-out $redisTlsPath/ca.crt
cat > $redisTlsPath/openssl.cnf <<_END_
[ server_cert ]
keyUsage = digitalSignature, keyEncipherment
nsCertType = server
[ client_cert ]
keyUsage = digitalSignature, keyEncipherment
nsCertType = client
_END_
generate_cert server \"Server-only\" \"-extfile $redisTlsPath/openssl.cnf -extensions server_cert\"
generate_cert client \"Client-only\" \"-extfile $redisTlsPath/openssl.cnf -extensions client_cert\"
generate_cert redis \"Generic-cert\"
[ -f $redisTlsPath/redis.dh ] || openssl dhparam -out $redisTlsPath/redis.dh 2048
" > $redisTlsScriptPath
echo_cyan "[+] run Redis certs script..."
chmod +x $redisTlsScriptPath
$redisTlsScriptPath
echo_cyan "tls证书重置完成,是否删除一键生成Redis证书脚本(1删除/默认不删除):"
read isDeleteRedisTlsScript
if [[ "$isDeleteRedisTlsScript" == '1' ]]; then
rm $redisTlsScriptPath
fi
}
#重置PostgreSQL数字证书
resetPgsqlCertificate(){
pgsqlTlsPath=/server/postgres/tls
pgsqlTlsScriptPath=/server/postgres/gen-test-certs.sh
rm -rf $pgsqlTlsPath
echo_yellow "=================================================================="
echo_green "创建一键生成PostgreSQL数字证书脚本"
echo_yellow " "
echo_cyan "注意: 不能向其他用户开放权限"
echo_cyan "开发环境: 目录 750/ 文件 640"
echo_cyan "部署环境: 目录 700/ 文件 600"
echo_yellow " "
echo_yellow "=================================================================="
echo_cyan "[+] Create PostgreSQL certs script..."
echo "#\!/bin/bash
generate_cert() {
local name=\$1
local cn=\"\$2\"
local opts=\"\$3\"
local keyfile=$pgsqlTlsPath/\${name}.key
local certfile=$pgsqlTlsPath/\${name}.crt
[ -f \$keyfile ] || openssl genrsa -out \$keyfile 2048
openssl req \\
-new -sha256 \\
-subj \"/O=PostgreSQL Test/CN=\$cn\" \\
-key \$keyfile | \\
openssl x509 \\
-req -sha256 \\
-CA $pgsqlTlsPath/root.crt \\
-CAkey $pgsqlTlsPath/root.key \\
-CAserial $pgsqlTlsPath/root.txt \\
-CAcreateserial \\
-days 365 \\
\$opts \\
-out \$certfile
}
mkdir $pgsqlTlsPath
[ -f $pgsqlTlsPath/root.key ] || openssl genrsa -out $pgsqlTlsPath/root.key 4096
openssl req \\
-x509 -new -nodes -sha256 \\
-key $pgsqlTlsPath/root.key \\
-days 3650 \\
-subj '/O=PostgreSQL Test/CN=Certificate Authority' \\
-out $pgsqlTlsPath/root.crt
cat > $pgsqlTlsPath/openssl.cnf <<_END_
[ server_cert ]
keyUsage = digitalSignature, keyEncipherment
nsCertType = server
[ client_cert ]
keyUsage = digitalSignature, keyEncipherment
nsCertType = client
_END_
generate_cert server \"Server-only\" \"-extfile $pgsqlTlsPath/openssl.cnf -extensions server_cert\"
generate_cert client \"Client-only\" \"-extfile $pgsqlTlsPath/openssl.cnf -extensions client_cert\"
generate_cert client-admin \"admin\" \"-extfile $pgsqlTlsPath/openssl.cnf -extensions client_cert\"
generate_cert client-emad \"emad\" \"-extfile $pgsqlTlsPath/openssl.cnf -extensions client_cert\"
generate_cert pgsql \"Generic-cert\"
[ -f $pgsqlTlsPath/pgsql.dh ] || openssl dhparam -out $pgsqlTlsPath/pgsql.dh 2048
" > $pgsqlTlsScriptPath
echo_cyan "[+] run PostgreSQL certs script..."
chmod +x $pgsqlTlsScriptPath
$pgsqlTlsScriptPath
echo_cyan "tls证书重置完成,是否删除一键生成PostgreSQL证书脚本(1删除/默认不删除):"
read isDeletePgsqlTlsScript
if [[ "$isDeletePgsqlTlsScript" == '1' ]]; then
rm $pgsqlTlsScriptPath
fi
}
#修改文件权限
modFilePower(){
echo_yellow "=================================================================="
echo_green "文件权限"
echo_green "通常来讲压缩包里含的权限是正确的,这里重新执行一遍,更加稳妥"
echo_yellow "=================================================================="
echo_green "/server 目录权限"
chown root:root -R /server
chmod 755 /server
find /server/default -type f -exec chmod 644 {} \;
find /server/default -type d -exec chmod 755 {} \;
echo_green "/www 目录权限"
echo_red "开发环境使用emad用户(nginx/php-fpm 需加入 emad用户组)"
echo_red "部署环境通常是www用户(nginx/php-fpm 需加入 www用户组)"
chmod 750 /www
echo_green "sqlite3文件权限"
chown sqlite:sqlite -R /server/sqlite
find /server/sqlite -type f -exec chmod 640 {} \;
find /server/sqlite -type d -exec chmod 750 {} \;
chmod 750 -R /server/sqlite/bin
echo_green "redis文件权限"
chown redis:redis -R /server/redis /server/logs/redis /server/etc/redis
find /server/redis /server/logs/redis /server/etc/redis -type f -exec chmod 640 {} \;
find /server/redis /server/logs/redis /server/etc/redis -type d -exec chmod 750 {} \;
chmod 750 -R /server/redis/bin
find /server/etc/redis/tls -type f -exec chmod 600 {} \;
find /server/etc/redis/tls -type d -exec chmod 700 {} \;
echo_green "postgres文件权限"
chown postgres:postgres -R /server/postgres /server/pgData /server/logs/postgres /server/etc/postgres
find /server/postgres /server/pgData /server/logs/postgres /server/etc/postgres -type f -exec chmod 640 {} \;
find /server/postgres /server/pgData /server/logs/postgres /server/etc/postgres -type d -exec chmod 750 {} \;
chmod 750 -R /server/postgres/bin
find /server/etc/postgres/tls -type f -exec chmod 600 {} \;
find /server/etc/postgres/tls -type d -exec chmod 700 {} \;
echo_green "MySQL文件权限"
chown mysql:mysql -R /server/mysql /server/data /server/logs/mysql /server/etc/mysql
find /server/mysql /server/logs/mysql /server/etc/mysql -type f -exec chmod 640 {} \;
find /server/mysql /server/logs/mysql /server/etc/mysql -type d -exec chmod 750 {} \;
chmod 700 /server/data
chmod 750 -R /server/mysql/bin
echo_green "php文件权限"
chown php-fpm:php-fpm -R /server/php /server/logs/php
find /server/php /server/logs/php -type f -exec chmod 640 {} \;
find /server/php /server/logs/php -type d -exec chmod 750 {} \;
chmod 640 /server/php/84/lib/php/extensions/no-debug-non-zts-*/*
chmod 750 -R /server/php/84/{bin,sbin}
chmod 750 /server/php/tools/{composer,php-cs-fixer-v3}.phar
echo_green "nginx文件权限"
chown nginx:nginx -R /server/{nginx,sites}
chown nginx:nginx -R /server/{etc,logs}/nginx
find /server/{nginx,sites} -type f -exec chmod 640 {} \;
find /server/{nginx,sites} -type d -exec chmod 750 {} \;
find /server/etc/nginx -type f -exec chmod 640 {} \;
find /server/etc/nginx -type d -exec chmod 750 {} \;
chmod 750 /server/logs/nginx
chmod 750 -R /server/nginx/sbin
echo_green "为nginx启用CAP_NET_BIND_SERVICE能力"
echo_red "注:每次修改nginx执行文件权限,都需要重新启用该能力"
setcap cap_net_bind_service=+eip /server/nginx/sbin/nginx
}
#安装systemctl单元
InstallSystemctlUnit(){
echo_yellow "=================================================================="
echo_green "加入systemctl守护进程\n含systemctl unit文件"
echo_yellow " "
echo_cyan "/lib/systemd/system/{redis,postgres,mysqld-84,php84-fpm,nginx}.service"
echo_yellow " "
echo_green "支持开启自动启动服务,非常规终止进程会自动启动服务"
echo_yellow "=================================================================="
echo_cyan "[+] Create redis service..."
echo "[Unit]
Description=redis-8.2.x
After=network.target
[Service]
Type=forking
User=redis
Group=redis
RuntimeDirectory=redis
RuntimeDirectoryMode=0750
ExecStart=/server/redis/bin/redis-server /server/redis/redis.conf
ExecReload=/bin/kill -s HUP \$MAINPID
ExecStop=/bin/kill -s QUIT \$MAINPID
Restart=on-failure
PrivateTmp=true
[Install]
WantedBy=multi-user.target
" > /usr/lib/systemd/system/redis.service
echo_cyan "[+] Create postgres service..."
echo "[Unit]
Description=PostgreSQL database server
Documentation=man:postgres(1)
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
User=postgres
Group=postgres
RuntimeDirectory=postgres
RuntimeDirectoryMode=0750
ExecStart=/server/postgres/bin/postgres -D /server/pgData
ExecReload=/bin/kill -HUP \$MAINPID
KillMode=mixed
KillSignal=SIGINT
TimeoutSec=infinity
[Install]
WantedBy=multi-user.target
" > /lib/systemd/system/postgres.service
echo_cyan "[+] Create MySQL service..."
echo "[Unit]
Description=MySQL Server 8.4.x
Documentation=man:mysqld(8)
After=network-online.target
Wants=network-online.target
After=syslog.target
[Service]
User=mysql
Group=mysql
Type=notify
TimeoutSec=0
ExecStart=/server/mysql/bin/mysqld --defaults-file=/server/etc/mysql/my.cnf
LimitNOFILE=10000
Restart=on-failure
RestartPreventExitStatus=1
PrivateTmp=false
RuntimeDirectory=mysql
RuntimeDirectoryMode=0750
[Install]
WantedBy=multi-user.target
" > /lib/systemd/system/mysqld-84.service
echo_cyan "[+] Create php84-fpm service..."
echo "[Unit]
Description=The PHP 8.4 FastCGI Process Manager
After=network.target
[Service]
Type=notify
User=php-fpm
Group=php-fpm
RuntimeDirectory=php84-fpm
RuntimeDirectoryMode=0750
ExecStart=/server/php/84/sbin/php-fpm --nodaemonize --fpm-config /server/php/84/etc/php-fpm.conf
ExecReload=/bin/kill -USR2 \$MAINPID
PrivateTmp=true
ProtectSystem=full
PrivateDevices=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectControlGroups=true
RestrictRealtime=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_NETLINK AF_UNIX
RestrictNamespaces=true
[Install]
WantedBy=multi-user.target
" > /lib/systemd/system/php84-fpm.service
echo_cyan "[+] Create nginx service..."
echo "[Unit]
Description=nginx-1.28.x
After=network.target
[Service]
Type=forking
User=nginx
Group=nginx
RuntimeDirectory=nginx
RuntimeDirectoryMode=0750
ExecStartPre=/server/nginx/sbin/nginx -t
ExecStart=/server/nginx/sbin/nginx -c /server/nginx/conf/nginx.conf
ExecReload=/server/nginx/sbin/nginx -s reload
ExecStop=/server/nginx/sbin/nginx -s quit
Restart=on-failure
PrivateTmp=true
[Install]
WantedBy=multi-user.target
" > /lib/systemd/system/nginx.service
echo_green "Registered Service..."
systemctl daemon-reload
systemctl enable --now {redis,postgres,mysqld-84,php84-fpm,nginx}.service
}
#日志管理
LogManagement(){
echo_yellow "=================================================================="
echo_green "Redis/Nginx 使用 Logrotate 来管理日志文件"
echo_yellow "=================================================================="
echo_cyan "[+] 创建 redis 的 Logrotate 脚本..."
echo "/server/logs/redis/redis-server.log {
monthly
maxsize 100M
missingok
rotate 12
compress
delaycompress
dateext
dateformat -%Y%m%d.%s
dateyesterday
create 0640 redis redis
sharedscripts
postrotate
if [ -f /run/redis/process.pid ]; then
/usr/bin/kill -USR1 \$(/bin/cat /run/redis/process.pid)
fi
endscript
}" > /etc/logrotate.d/redis
echo_cyan "[+] 创建 nginx 的 Logrotate 脚本..."
echo "
/server/logs/nginx/access/*.log {
daily
maxsize 100M
missingok
rotate 30
compress
delaycompress
dateext
dateformat -%Y%m%d.%s
notifempty
create 0640 nginx nginx
sharedscripts
postrotate
if [ -f /run/nginx/process.pid ]; then
/usr/bin/kill -USR1 \$(/bin/cat /run/nginx/process.pid)
fi
endscript
}
/server/logs/nginx/error/*.log {
monthly
maxsize 100M
missingok
rotate 12
compress
delaycompress
dateext
dateformat -%Y%m%d.%s
notifempty
create 0640 nginx nginx
sharedscripts
postrotate
if [ -f /run/nginx/process.pid ]; then
/usr/bin/kill -USR1 \$(/bin/cat /run/nginx/process.pid)
fi
endscript
}
" > /etc/logrotate.d/nginx
}
#日志管理
LogManagement(){
echo_yellow "=================================================================="
echo_green "系统级优化"
echo_yellow "=================================================================="
echo_cyan "[+] 内核管理..."
echo "vm.overcommit_memory = 1" > /etc/sysctl.d/overcommit_memory.conf
echo "net.core.somaxconn = 4096" > /etc/sysctl.d/somaxconn.conf
echo "net.ipv4.tcp_max_syn_backlog = 4096" > /etc/sysctl.d/tcp_max_syn_backlog.conf
echo_cyan "[+] postgres用户资源管理..."
echo "postgres soft nofile 65535
postgres hard nofile 65535" > /etc/security/limits.d/postgres.conf
echo_cyan "[+] redis用户资源管理..."
echo "redis soft nofile 65535
redis hard nofile 65535" > /etc/security/limits.d/redis.conf
}
#PHP工具软链接到 /usr/local/bin
LogManagement(){
echo_yellow "=================================================================="
echo_green "PHP工具软链接到 /usr/local/bin"
echo_green "含:composer和php-cs-fixer"
echo_yellow "=================================================================="
ln -s /server/php/tools/composer.phar /usr/local/bin/composer
ln -s /server/php/tools/php-cs-fixer-v3.phar /usr/local/bin/php-cs-fixer
}
#安装nginx站点管理工具
InstallSiteManagement(){
echo_yellow "=================================================================="
echo_green "安装nginx站点管理工具"
echo_yellow "=================================================================="
echo_cyan "[+] 创建nginx站点管理工具..."
echo "#\!/usr/bin/env bash
# ==============================================
# nginxctl - Nginx 站点管理工具
# 功能: 启用/禁用 Nginx 站点配置
# 作者: 地上马
# ==============================================
# 定义路径
AVAILABLE_DIR=\"/server/sites/available\"
ENABLED_DIR=\"/server/sites/enabled\"
# 颜色定义
RED='\\\033[0;31m'
GREEN='\\\033[0;32m'
YELLOW='\\\033[1;33m'
BLUE='\\\033[0;34m'
NC='\\\033[0m' # No Color
# 打印带格式的消息函数
print_success() {
echo -e \"\${GREEN}[SUCCESS] \$1\${NC}\"
}
print_error() {
echo -e \"\${RED}[ERROR] \$1\${NC}\"
}
print_info() {
echo -e \"\${BLUE}[INFO] \$1\${NC}\"
}
print_warning() {
echo -e \"\${YELLOW}[WARNING] \$1\${NC}\"
}
# 检查必要目录是否存在
check_directories() {
if [ ! -d \"\$AVAILABLE_DIR\" ]; then
print_error \"目录不存在: \$AVAILABLE_DIR\"
exit 1
fi
if [ ! -d \"\$ENABLED_DIR\" ]; then
print_error \"目录不存在: \$ENABLED_DIR\"
exit 1
fi
}
# 执行启用操作
enable_site() {
local SITE_NAME=\$1
local CONF_FILE=\"\${SITE_NAME}.nginx\"
local AVAILABLE_FILE=\"\${AVAILABLE_DIR}/\${CONF_FILE}\"
local ENABLED_FILE=\"\${ENABLED_DIR}/\${CONF_FILE}\"
# 检查可用配置文件是否存在
if [ ! -f \"\$AVAILABLE_FILE\" ]; then
print_error \"配置文件 \${AVAILABLE_FILE} 不存在\"
exit 1
fi
# 检查是否已启用
if [ -L \"\$ENABLED_FILE\" ]; then
print_warning \"站点 \${SITE_NAME} 已启用,无需重复操作\"
exit 0
fi
# 创建软链接
if ! ln -s \"\$AVAILABLE_FILE\" \"\$ENABLED_FILE\"; then
print_error \"创建软链接失败\"
exit 1
fi
print_success \"已启用站点 \${SITE_NAME}\"
# 测试Nginx配置
print_info \"测试Nginx配置...\"
if ! nginx -t; then
print_error \"Nginx 配置测试失败,请检查配置\"
exit 1
fi
# 重载nginx
print_info \"重载Nginx配置...\"
if ! systemctl reload nginx; then
print_error \"重载Nginx失败\"
exit 1
fi
print_success \"Nginx已成功重载\"
}
# 执行禁用操作
disable_site() {
local SITE_NAME=\$1
local CONF_FILE=\"\${SITE_NAME}.nginx\"
local ENABLED_FILE=\"\${ENABLED_DIR}/\${CONF_FILE}\"
# 检查是否已启用
if [ ! -L \"\$ENABLED_FILE\" ]; then
print_warning \"站点 \${SITE_NAME} 未启用,无需操作\"
exit 0
fi
# 删除软链接
if ! rm \"\$ENABLED_FILE\"; then
print_error \"删除软链接失败\"
exit 1
fi
print_success \"已禁用站点 \${SITE_NAME}\"
# 测试Nginx配置
print_info \"测试Nginx配置...\"
if ! nginx -t; then
print_error \"Nginx 配置测试失败,请检查配置\"
exit 1
fi
# 重载nginx
print_info \"重载Nginx配置...\"
if ! systemctl reload nginx; then
print_error \"重载Nginx失败\"
exit 1
fi
print_success \"Nginx已成功重载\"
}
# 显示帮助信息
show_help() {
echo -e \"\${BLUE}nginxctl - Nginx站点管理工具\${NC}\"
echo \"\"
echo -e \"\${BLUE}用法:\${NC}\"
echo -e \" nginxctl enable <site_name> \${GREEN}启用指定站点\${NC}\"
echo -e \" nginxctl disable <site_name> \${RED}禁用指定站点\${NC}\"
echo -e \" nginxctl help \${BLUE}显示帮助信息\${NC}\"
echo \"\"
echo -e \"\${BLUE}示例:\${NC}\"
echo -e \" \${GREEN}nginxctl enable demo\${NC}\"
echo -e \" \${RED}nginxctl disable demo\${NC}\"
echo \"\"
echo -e \"\${BLUE}说明:\${NC}\"
echo -e \" 站点配置文件应位于 \${AVAILABLE_DIR}/ 目录中\"
echo -e \" 启用后会在 \${ENABLED_DIR}/ 创建软链接\"
}
# 主程序逻辑
main() {
# 检查必要目录是否存在
check_directories
# 检查参数数量
if [ \$# -lt 1 ]; then
show_help
exit 1
fi
ACTION=\$1
SITE_NAME=\$2
# 根据输入参数执行相应操作
case \"\$ACTION\" in
enable)
if [ \$# -ne 2 ]; then
print_error \"启用站点需要指定站点名称\"
echo \"用法: nginxctl enable <site_name>\"
exit 1
fi
enable_site \"\$SITE_NAME\"
;;
disable)
if [ \$# -ne 2 ]; then
print_error \"禁用站点需要指定站点名称\"
echo \"用法: nginxctl disable <site_name>\"
exit 1
fi
disable_site \"\$SITE_NAME\"
;;
help|--help|-h)
show_help
;;
*)
print_error \"无效操作 '\${ACTION}'\"
echo \"可用操作: enable, disable\"
echo \"使用 'nginxctl help' 查看完整帮助\"
exit 1
;;
esac
}
# 执行主程序
main \"\$@\"" > /usr/local/bin/nginxctl
echo_cyan "站点管理工具授权..."
chmod 755 /usr/local/bin/nginxctl
}
echo_cyan "解压脚本同级目录下需存在源码压缩包 lnmpp.tar.xz"
echo_cyan "是否退出(1退出/默认继续):"
read isExit
if [[ "$isExit" == '1' ]]; then
exit 0
fi
#系统更新到最新
upgradeOS
echo_cyan "是否重启操作系统(1重启/默认不重启):"
read num
if [[ "$num" == '1' ]]; then
echo_cyan "停止向下执行,并重启系统"
sync;sync;sync;reboot
else
#清理旧数据
cleanOldData
echo ' '
#创建用户
createUser
echo ' '
#开发用户追加权限,部署环境请注释掉
echo_red "部署环境通常不需要授权"
echo_cyan "输入开发用户名,为其授权(为空不授权):"
read userName
if [[ -n "$userName" ]]; then
devUserPower $userName
fi
echo ' '
#安装依赖包
installPackage
echo ' '
#解压lnmpp预构建包到指定目录
InstallBuild
echo ' '
#是否重新生成tls证书
echo_cyan "是否重置数字证书(1重置/默认不重置):"
read isResetCertificate
if [[ "$isResetCertificate" == '1' ]]; then
resetRedisCertificate
resetPgsqlCertificate
fi
echo ' '
#修改文件权限
modFilePower
echo ' '
#安装systemctl单元
InstallSystemctlUnit
#日志管理
LogManagement
#系统级调优
SystemdOptimize
#安装nginx站点管理工具
InstallSiteManagement
echo ' '
echo_yellow "=================================================================="
echo_green "lnmpp安装完成"
echo_yellow " - Postgres 默认有个超级管理员用户 admin 密码 1"
echo_yellow " - MySQL 默认有个本地用户 admin@localhost 密码 1"
echo_yellow " - MySQL 默认有个局域网用户 admin@'192.168.%.%' 密码 1"
echo_yellow " - Redis 默认设置了全局密码 1"
echo_yellow "=================================================================="
echo ' '
echo_yellow "=================================================================="
echo_green "systemctl 常用指令"
echo_yellow "重载 systemctl"
echo_yellow "systemctl daemon-reload"
echo_yellow "启用并开启服务"
echo_yellow "systemctl enable --now {redis,postgres,mysqld-84,php84-fpm,nginx}.service"
echo_yellow "禁用并禁止服务"
echo_yellow "systemctl disable --now {redis,postgres,mysqld-84,php84-fpm,nginx}.service"
echo_yellow "开启"
echo_yellow "systemctl start {redis,postgres,mysqld-84,php84-fpm,nginx}.service"
echo_yellow "停止"
echo_yellow "systemctl stop {redis,postgres,mysqld-84,php84-fpm,nginx}.service"
echo_yellow "查看状态"
echo_yellow "systemctl status {redis,postgres,mysqld-84,php84-fpm,nginx}.service"
echo_yellow "重新加载配置(部分服务器不支持重载配置文件)"
echo_yellow "systemctl reload nginx"
echo_yellow "使用nginx站点管理工具"
echo_yellow "启用站点: nginxctl enable <site_name>"
echo_yellow "禁用站点: nginxctl disable <site_name>"
echo_yellow "获取帮助: nginxctl help"
echo_yellow "=================================================================="
echo ' '
echo_yellow "=================================================================="
echo_red "条件允许,建议重启系统!!!"
echo_yellow "=================================================================="
fi
附录三、PHP 探针
<?php
declare(strict_types=1);
/**
* 超高性能 PHP 探针 - 专为 PHP 8.4+ 深度优化
*/
// 设置字符编码和时区
header('Content-Type: text/html; charset=UTF-8');
header('X-Powered-By: PHP-Probe/3.0');
date_default_timezone_set('Asia/Shanghai');
// 定义探针版本和常量
define('PROBE_VERSION', '3.0');
define('PROBE_START_TIME', hrtime(true));
// 预定义颜色常量
const COLOR_SUCCESS = '#28a745';
const COLOR_DANGER = '#dc3545';
const COLOR_WARNING = '#ffc107';
const COLOR_INFO = '#17a2b8';
const COLOR_PRIMARY = '#2c3e50';
const COLOR_SECONDARY = '#4ca1af';
// 使用枚举定义探针状态 (PHP 8.1+)
enum ProbeStatus {
case SUCCESS;
case ERROR;
case WARNING;
case INFO;
}
// 使用 readonly 类存储配置 (PHP 8.2+)
readonly class ProbeConfig {
public function __construct(
public string $timezone = 'Asia/Shanghai',
public string $version = '3.0',
public int $benchmarkIterations = 500000
) {}
}
final class PHPProbe {
private static ?ProbeConfig $config = null;
private static array $cache = [];
public static function init(ProbeConfig $config): void {
self::$config = $config;
}
// 获取客户端IP (使用 match 表达式)
public static function getClientIP(): string {
return match(true) {
!empty($_SERVER['HTTP_X_FORWARDED_FOR']) => $_SERVER['HTTP_X_FORWARDED_FOR'],
!empty($_SERVER['HTTP_CLIENT_IP']) => $_SERVER['HTTP_CLIENT_IP'],
default => $_SERVER['REMOTE_ADDR'] ?? 'Unknown'
};
}
// 格式化字节大小
public static function formatBytes(int $bytes, int $precision = 2): string {
static $units = ['B', 'KB', 'MB', 'GB', 'TB'];
if ($bytes <= 0) return '0 B';
$base = log($bytes) / log(1024);
$pow = min((int)$base, count($units) - 1);
$bytes /= (1024 ** $pow);
return round($bytes, $precision) . ' ' . $units[$pow];
}
// 检测扩展支持 (使用枚举返回状态)
public static function checkExtension(string $ext): ProbeStatus {
return extension_loaded($ext) ? ProbeStatus::SUCCESS : ProbeStatus::ERROR;
}
// 获取状态HTML
public static function getStatusHTML(ProbeStatus $status, string $text): string {
$class = match($status) {
ProbeStatus::SUCCESS => 'success',
ProbeStatus::ERROR => 'danger',
ProbeStatus::WARNING => 'warning',
ProbeStatus::INFO => 'info'
};
$icon = match($status) {
ProbeStatus::SUCCESS => '✓',
ProbeStatus::ERROR => '✗',
ProbeStatus::WARNING => '⚠',
ProbeStatus::INFO => 'ℹ'
};
return sprintf('<span class="%s">%s %s</span>', $class, $icon, $text);
}
// 高性能基准测试
public static function benchmark(): string {
$start = hrtime(true);
// 优化的计算测试 - 使用更有效的算法
$result = 0;
$iterations = self::$config->benchmarkIterations;
for ($i = 0; $i < $iterations; $i++) {
$result += $i ** 0.5;
}
$elapsed = (hrtime(true) - $start) / 1e6;
return number_format($elapsed, 2) . ' ms';
}
// 获取系统信息 (使用静态缓存)
public static function getSystemInfo(): array {
if (!empty(self::$cache['system_info'])) {
return self::$cache['system_info'];
}
$info = [
'服务器时间' => date('Y-m-d H:i:s'),
'服务器域名' => $_SERVER['SERVER_NAME'] ?? 'N/A',
'服务器IP' => $_SERVER['SERVER_ADDR'] ?? gethostbyname($_SERVER['SERVER_NAME'] ?? 'localhost'),
'服务器端口' => $_SERVER['SERVER_PORT'] ?? 'N/A',
'服务器操作系统' => php_uname('s') . ' ' . php_uname('r'),
'服务器架构' => php_uname('m'),
'Web服务器' => $_SERVER['SERVER_SOFTWARE'] ?? 'N/A',
'客户端IP' => self::getClientIP(),
'请求时间' => date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME'] ?? time()),
'运行用户' => get_current_user() ?: (function_exists('posix_getpwuid') ? posix_getpwuid(posix_geteuid())['name'] ?? 'Unknown' : 'Unknown'),
];
// 尝试获取更多系统信息
if (function_exists('sys_getloadavg')) {
$load = sys_getloadavg();
$info['系统负载'] = implode(', ', array_map(fn($v) => number_format($v, 2), $load));
}
return self::$cache['system_info'] = $info;
}
// 获取PHP配置信息
public static function getPHPInfo(): array {
if (!empty(self::$cache['php_info'])) {
return self::$cache['php_info'];
}
$info = [
'PHP版本' => PHP_VERSION,
'PHP运行方式' => php_sapi_name(),
'Zend引擎版本' => zend_version(),
'PHP安装路径' => PHP_BINDIR,
'PHP配置文件' => php_ini_loaded_file() ?: '未找到',
'当前包含路径' => get_include_path(),
'最大内存限制' => ini_get('memory_limit'),
'最大执行时间' => ini_get('max_execution_time') . '秒',
'最大上传文件大小' => ini_get('upload_max_filesize'),
'最大POST数据大小' => ini_get('post_max_size'),
'时区设置' => date_default_timezone_get(),
'OPCache状态' => extension_loaded('Zend OPcache') && ini_get('opcache.enable') ? '启用' : '禁用',
'JIT状态' => ini_get('opcache.jit') ?: '禁用',
'PHP接口类型' => PHP_SAPI,
'PHP整数大小' => PHP_INT_SIZE * 8 . '位',
'PHP最大整数值' => PHP_INT_MAX,
];
return self::$cache['php_info'] = $info;
}
// 获取性能信息
public static function getPerformanceInfo(): array {
if (!empty(self::$cache['performance_info'])) {
return self::$cache['performance_info'];
}
$info = [
'探针执行时间' => number_format((hrtime(true) - PROBE_START_TIME) / 1e6, 2) . ' ms',
'CPU性能测试' => self::benchmark(),
'内存使用峰值' => self::formatBytes(memory_get_peak_usage(true)),
'当前内存使用' => self::formatBytes(memory_get_usage(true)),
'真实内存使用' => self::formatBytes(memory_get_usage(false)),
'PHP进程ID' => getmypid(),
'内存限制使用率' => self::getMemoryUsagePercent() . '%',
];
return self::$cache['performance_info'] = $info;
}
// 获取数据库支持信息
public static function getDatabaseInfo(): array {
if (!empty(self::$cache['database_info'])) {
return self::$cache['database_info'];
}
$info = [];
// PDO支持
$info['PDO支持'] = extension_loaded('pdo')
? self::getStatusHTML(ProbeStatus::SUCCESS, '已启用')
: self::getStatusHTML(ProbeStatus::ERROR, '未启用');
// PDO驱动
if (extension_loaded('pdo')) {
$drivers = PDO::getAvailableDrivers();
$info['PDO驱动'] = $drivers ? implode(', ', $drivers) : '无';
}
// MySQLi支持
$info['MySQLi支持'] = extension_loaded('mysqli')
? self::getStatusHTML(ProbeStatus::SUCCESS, '已启用')
: self::getStatusHTML(ProbeStatus::ERROR, '未启用');
return self::$cache['database_info'] = $info;
}
// 获取PHP 8.4+ 特性支持信息
public static function getPHP84Features(): array {
if (!empty(self::$cache['php84_features'])) {
return self::$cache['php84_features'];
}
$features = [];
// 检查新特性支持
$features['JIT编译器'] = ini_get('opcache.jit') ? '启用' : '禁用';
$features['纤程(Fiber)支持'] = class_exists('Fiber') ? '支持' : '不支持';
$features['枚举支持'] = function_exists('enum_exists') ? '支持' : '不支持';
$features['属性注解'] = PHP_VERSION_ID >= 80000 ? '支持' : '不支持';
$features['匹配表达式'] = PHP_VERSION_ID >= 80000 ? '支持' : '不支持';
$features['空安全操作符'] = PHP_VERSION_ID >= 80000 ? '支持' : '不支持';
$features['命名参数'] = PHP_VERSION_ID >= 80000 ? '支持' : '不支持';
$features['联合类型'] = PHP_VERSION_ID >= 80000 ? '支持' : '不支持';
return self::$cache['php84_features'] = $features;
}
// 获取扩展支持信息
public static function getExtensionsInfo(): array {
if (!empty(self::$cache['extensions_info'])) {
return self::$cache['extensions_info'];
}
$extensions = [
'GD库支持' => 'gd',
'OpenSSL支持' => 'openssl',
'JSON支持' => 'json',
'MBString支持' => 'mbstring',
'CURL支持' => 'curl',
'ZIP支持' => 'zip',
'XML支持' => 'xml',
'Redis支持' => 'redis',
'Memcached支持' => 'memcached',
'MongoDB支持' => 'mongodb',
'PDO支持' => 'pdo',
'MySQLi支持' => 'mysqli',
'SQLite支持' => 'sqlite3',
'PostgreSQL支持' => 'pgsql',
'ODBC支持' => 'odbc',
'SOAP支持' => 'soap',
'GD图像处理' => 'gd',
'IMAP支持' => 'imap',
'LDAP支持' => 'ldap',
'FTP支持' => 'ftp',
'Socket支持' => 'sockets',
'Exif支持' => 'exif',
'Gettext支持' => 'gettext',
'Iconv支持' => 'iconv',
'Intl支持' => 'intl',
'PCRE支持' => 'pcre',
'PDO MySQL支持' => 'pdo_mysql',
'PDO SQLite支持' => 'pdo_sqlite',
'PDO PostgreSQL支持' => 'pdo_pgsql',
'PDO ODBC支持' => 'pdo_odbc',
'APCu支持' => 'apcu',
'XDebug支持' => 'xdebug',
'Zend OPcache支持' => 'Zend OPcache',
];
$result = [];
foreach ($extensions as $name => $ext) {
$result[$name] = self::checkExtension($ext) === ProbeStatus::SUCCESS
? self::getStatusHTML(ProbeStatus::SUCCESS, '支持')
: self::getStatusHTML(ProbeStatus::ERROR, '不支持');
}
return self::$cache['extensions_info'] = $result;
}
// 计算内存使用百分比
private static function getMemoryUsagePercent(): float {
$limit = ini_get('memory_limit');
$usage = memory_get_usage(true);
if ($limit == -1) return 0; // 无限制
$limitBytes = self::convertToBytes($limit);
return round(($usage / $limitBytes) * 100, 2);
}
// 将内存限制字符串转换为字节
private static function convertToBytes(string $value): int {
$unit = strtolower(substr($value, -1));
$bytes = (int)substr($value, 0, -1);
return match($unit) {
'g' => $bytes * 1024 * 1024 * 1024,
'm' => $bytes * 1024 * 1024,
'k' => $bytes * 1024,
default => (int)$value
};
}
// 获取所有已加载扩展
public static function getLoadedExtensions(): array {
if (!empty(self::$cache['loaded_extensions'])) {
return self::$cache['loaded_extensions'];
}
$extensions = get_loaded_extensions();
sort($extensions);
return self::$cache['loaded_extensions'] = $extensions;
}
// 获取扩展信息统计
public static function getExtensionStats(): array {
$extensions = self::getLoadedExtensions();
return [
'总扩展数' => count($extensions),
'Zend扩展' => count(array_filter($extensions, fn($ext) => extension_loaded($ext) && str_starts_with($ext, 'Zend'))),
'PHP核心扩展' => count(array_filter($extensions, fn($ext) => extension_loaded($ext) && in_array($ext, ['standard', 'Core', 'date', 'pcre', 'reflection', 'SPL']))),
];
}
// 清理缓存
public static function clearCache(): void {
self::$cache = [];
}
}
// 初始化配置
PHPProbe::init(new ProbeConfig());
try {
// 收集所有信息
$systemInfo = PHPProbe::getSystemInfo();
$phpInfo = PHPProbe::getPHPInfo();
$performanceInfo = PHPProbe::getPerformanceInfo();
$databaseInfo = PHPProbe::getDatabaseInfo();
$php84Features = PHPProbe::getPHP84Features();
$extensionsInfo = PHPProbe::getExtensionsInfo();
$loadedExtensions = PHPProbe::getLoadedExtensions();
$extensionStats = PHPProbe::getExtensionStats();
// 计算总扩展数
$totalExtensions = count($loadedExtensions);
// 先处理phpinfo的请求
if (isset($_GET['phpinfo'])) {
$phpinfoType = (int)$_GET['phpinfo'];
match ($phpinfoType) {
1 => phpinfo(),
2 => phpinfo(INFO_CONFIGURATION | INFO_MODULES),
default => header('Location: ' . $_SERVER['PHP_SELF']),
};
exit(0);
}
} catch (Throwable $e) {
// 错误处理
header('HTTP/1.1 500 Internal Server Error');
echo "<h1>探针错误</h1><p>{$e->getMessage()}</p>";
exit;
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>超高性能 PHP 探针 v<?= PROBE_VERSION ?> - PHP <?= PHP_VERSION ?></title>
<style>
:root {
--primary: <?= COLOR_PRIMARY ?>;
--secondary: <?= COLOR_SECONDARY ?>;
--success: <?= COLOR_SUCCESS ?>;
--danger: <?= COLOR_DANGER ?>;
--warning: <?= COLOR_WARNING ?>;
--info: <?= COLOR_INFO ?>;
--light: #f8f9fa;
--dark: #343a40;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: #fff;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
header {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
padding: 25px 30px;
text-align: center;
position: relative;
overflow: hidden;
}
header::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
transform: rotate(30deg);
}
h1 {
font-size: 2.8rem;
margin-bottom: 10px;
font-weight: 700;
position: relative;
}
.version {
font-size: 1.3rem;
opacity: 0.9;
margin-bottom: 15px;
}
.php-version {
display: inline-block;
background: rgba(255, 255, 255, 0.2);
padding: 8px 20px;
border-radius: 25px;
font-weight: bold;
backdrop-filter: blur(5px);
position: relative;
}
.content {
padding: 25px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 25px;
margin-bottom: 30px;
}
.card {
background: #fff;
border-radius: 12px;
padding: 20px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.card h2 {
color: var(--primary);
border-bottom: 2px solid var(--secondary);
padding-bottom: 15px;
margin-bottom: 20px;
font-size: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.toggle {
background: var(--secondary);
color: white;
border: none;
padding: 6px 12px;
border-radius: 5px;
cursor: pointer;
font-size: 0.85rem;
transition: background 0.3s;
}
.toggle:hover {
background: var(--primary);
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
th {
background-color: var(--light);
font-weight: 600;
width: 35%;
}
tr:last-child td {
border-bottom: none;
}
tr:hover {
background-color: rgba(0, 0, 0, 0.01);
}
.success { color: var(--success); }
.danger { color: var(--danger); }
.warning { color: var(--warning); }
.info { color: var(--info); }
.extensions-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
margin-top: 15px;
}
.extension-item {
background: var(--light);
padding: 10px 15px;
border-radius: 6px;
font-size: 0.9rem;
transition: background 0.3s;
}
.extension-item:hover {
background: #e9ecef;
}
footer {
text-align: center;
padding: 25px;
background: var(--primary);
color: white;
font-size: 0.95rem;
}
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr;
}
h1 {
font-size: 2.2rem;
}
.content {
padding: 15px;
}
th, td {
padding: 10px;
display: block;
width: 100%;
}
th {
background: none;
font-weight: 700;
padding-bottom: 5px;
}
tr {
display: block;
margin-bottom: 15px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding-bottom: 15px;
}
tr:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
}
.badge {
display: inline-block;
padding: 3px 8px;
border-radius: 10px;
font-size: 0.75rem;
font-weight: 600;
margin-left: 8px;
}
.badge-success {
background: var(--success);
color: white;
}
.badge-danger {
background: var(--danger);
color: white;
}
.badge-info {
background: var(--info);
color: white;
}
.extensions-check-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 15px;
margin-top: 15px;
}
.extension-check-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-radius: 6px;
background: var(--light);
}
.extension-name {
font-weight: 500;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>高性能 PHP 探针</h1>
<div class="version">版本 v<?= PROBE_VERSION ?> - 专为 PHP 8.4+ 优化</div>
<div class="php-version">PHP <?= PHP_VERSION ?></div>
</header>
<div class="content">
<!-- 系统信息 -->
<div class="card">
<h2>
系统信息
<button class="toggle" onclick="toggleSection('system-info')">显示/隐藏</button>
</h2>
<div id="system-info" style="display: none;">
<table>
<?php foreach ($systemInfo as $key => $value): ?>
<tr>
<th><?= $key ?></th>
<td><?= htmlspecialchars((string)$value) ?></td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div>
<!-- PHP配置 -->
<div class="card">
<h2>
PHP配置信息
<button class="toggle" onclick="toggleSection('php-info')">显示/隐藏</button>
</h2>
<div id="php-info" style="display: none;">
<table>
<?php foreach ($phpInfo as $key => $value): ?>
<tr>
<th><?= $key ?></th>
<td><?= htmlspecialchars((string)$value) ?></td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div>
<!-- 性能信息 -->
<div class="card">
<h2>
性能信息
<button class="toggle" onclick="toggleSection('performance-info')">显示/隐藏</button>
</h2>
<div id="performance-info" style="display: none;">
<table>
<?php foreach ($performanceInfo as $key => $value): ?>
<tr>
<th><?= $key ?></th>
<td><?= $value ?></td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div>
<div class="grid">
<!-- 数据库支持 -->
<div class="card">
<h2>数据库支持</h2>
<table>
<?php foreach ($databaseInfo as $key => $value): ?>
<tr>
<th><?= $key ?></th>
<td><?= $value ?></td>
</tr>
<?php endforeach; ?>
</table>
</div>
<!-- PHP 8.4+ 特性 -->
<div class="card">
<h2>PHP 8.4+ 特性</h2>
<table>
<?php foreach ($php84Features as $key => $value): ?>
<tr>
<th><?= $key ?></th>
<td>
<?= $value ?>
<?php if (strpos($value, '支持') !== false): ?>
<span class="badge badge-success">NEW</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div>
<!-- 扩展支持 -->
<div class="card">
<h2>
扩展支持检测
<button class="toggle" onclick="toggleSection('extensions-info')">显示/隐藏</button>
</h2>
<div id="extensions-info" style="display: none;">
<div class="extensions-check-grid">
<?php foreach ($extensionsInfo as $key => $value): ?>
<div class="extension-check-item">
<span class="extension-name"><?= $key ?></span>
<span><?= $value ?></span>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- 已加载扩展 -->
<div class="card">
<h2>
已加载扩展 <span class="badge badge-info"><?= $totalExtensions ?></span>
<button class="toggle" onclick="toggleSection('loaded-extensions')">显示/隐藏</button>
</h2>
<div id="loaded-extensions" style="display: none;">
<div class="extensions-grid">
<?php foreach ($loadedExtensions as $ext): ?>
<div class="extension-item"><?= $ext ?></div>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- 更多信息 -->
<div class="card">
<h2>更多信息</h2>
<p>
<a href="?phpinfo=1" target="_blank" class="btn">查看完整的 phpinfo()</a> |
<a href="?phpinfo=2" target="_blank" class="btn">查看配置信息</a>
</p>
</div>
</div>
<footer>
高性能 PHP 探针 v<?= PROBE_VERSION ?> - 生成时间: <?= date('Y-m-d H:i:s') ?> (北京时间)
</footer>
</div>
<script>
// 切换章节显示/隐藏
function toggleSection(id) {
const element = document.getElementById(id);
if (element.style.display === 'none') {
element.style.display = 'block';
} else {
element.style.display = 'none';
}
}
// 添加打印功能
function printProbe() {
window.print();
}
// 添加复制功能
function copyProbeInfo() {
const content = document.documentElement.outerHTML;
navigator.clipboard.writeText(content)
.then(() => alert('探针信息已复制到剪贴板'))
.catch(err => console.error('复制失败:', err));
}
</script>
</body>
</html>