nginx+php-fpm搭建wordpress

一开始搭建的hexo博客,hexo博客有个缺点,他是用nodejs的服务器,不太稳定,服务器经常挂。所以最后还是决定用nginx+php-fpm搭建一个wordpress站点,这样网站就比较稳定。废话不多说,直接进入主题。

我是用的centos的服务器,下面的一些个命令也是centos的命令,不过其他的也相差不大,主要的是步骤正确就好。

1. 准备 LNMP 环境

安装nginx

使用yum安装nginx

yum install nginx -y

安装完之后修改配置文件 /etc/nginx/nginx.conf

ps:要是配置文件不在这个位置的,可以利用find命令和whereis命令进行查询,参考我另外两篇博文

配置文件参考下面进行更改:

这一步,只需要两处,去除对 IPv6 地址的监听,修改要监听的域名,即:

#listen [::]:80 default_server;
server_name www.***.com;

修改完了之后,就可以启动nginx,查看自己网站首页了,应该看到的是一个nginx测试页面。

启动nginx

nginx

将 Nginx 设置为开机自动启动:

chkconfig nginx on

安装php+php-fpm+php-mysql

这一步有两个选择,安装php7.0版本或是老的版本

安装老的版本比较简单:

yum install php-fpm php-mysql -y

要是想安装php7.0版本,要先添加源,再安装:

如果是centos6,那么执行以下命令

CentOS/RHEL 6.x:

rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-6.noarch.rpm
rpm -Uvh https://mirror.webtatic.com/yum/el6/latest.rpm

如果是centos7.x,那么执行以下命令

CentOS/RHEL 7.x:

rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
rpm -Uvh https://mirror.webtatic.com/yum/el7/webtatic-release.rpm

安装php和插件:

可以像上面一样,需要哪个装哪个,也可以全部安装

全部安装:

yum install php70w php70w*

安装需要的:

yum install php70w-common php70w-fpm php70w-opcache php70w-gd php70w-mysqlnd php70w-mbstring php70w-pecl-redis php70w-pecl-memcached php70w-devel

具体可以参考:帐号登录

完了之后启动php-fpm:

service php-fpm start

安装mysql

yum install mysql-server -y

行不通可参考:centos7 mysql数据库安装和配置

2. 安装wordpress

yum install wordpress -y

安装完成后,就可以在 /usr/share/wordpress 看到 WordPress 的源代码了。

由于上面的安装方法默认安装的是英文版,所以我们还要下载中文语言包,如果不需要中文安装可跳过此步骤

wget https://cn.wordpress.org/wordpress-4.8.1-zh_CN.tar.gz
tar -zxvf wordpress*
mv -f wordpress/wp-content/languages /usr/share/wordpress/wp-content/
rm -rf wordpress*

3. 配置

登录mysql,这里初次没有密码,你自己要设置一个密码

mysql -uroot

创建数据库

CREATE DATABASE wordpress;

完成退出

exit;

配置wordpress

进入wordpress目录下,根目录下有一个配置文件wp-config.php,把数据库名,用户,密码配置好就可以,数据库就是刚才建立 的数据库

define('DB_NAME', 'wordpress');
define('DB_USER', 'root');
define('DB_PASSWORD', 'MyPas$word4Word_Press');

修改这三行即可

再次配置/etc/nginx/nginx.conf文件:

完全按照下面配置,修改域名为你的域名即可

server {
listen 80 default_server;
#listen [::]:80 default_server;
server_name www.***.com;
root /usr/share/wordpress;

# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;

location / {
index index.php index.html index.htm;
try_files $uri $uri/ /index.php index.php;
}

location ~ .php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}

到这一步基本就完成了

nginx重新加载配置

nginx -s reload

访问页面进入wordpress仪表盘

关于nginx和php-fpm的修改用户和用户组的问题

修改nginx和php-fpm的用户和用户组时除了修改

nginx.conf 下的

user *****

和/etc/php-fpm.d/www.conf下的

user = ****

group = ****

修改上面之后,重启nginx和php-fpm,ps -ef|grep … 会发现应用的用户和用户组变了,但是这还能算万事大吉

还要修改一下一些目录下的权限

(以 修改的用户和用户组为nobody为例)

  1. 如 nginx的日志目录 /var/log/nginxchown -R nobody:nobody /var/log/nginx 否则不能不能读写access.logerrorlog

  2. 还有 上传文件用到的一个路径 /var/lib/nginx/tmp/chown -R nobody:nobody /var/lib/nginx 否则上传文件出错,因为保存不了上传的临时文件

3.还有的就是php-fpm的配置文件中

php_value[session.save_handler] = files

php_value[session.save_path] = /var/lib/php/session

设置的session保存的路径,如果你修改了php-fpm的用户和用户组,重启之后,你会发现什么登陆状态,验证码都保存不了,没错session没保存,因为没有修改上面保存session路径的权限

所以应该 让你修改的用户拥有/var/lib/php/session的读写权限,chown 或者chmod 反正让php-fpm的应用用户有权限访问就可以了

扩展:

“ nobody就是一个普通账户,因为默认登录shell是 ‘/sbin/nologin’,所以这个用户是无法直接登录系统的,也就是黑客很难通过漏洞连接到你的服务器来做破坏。此外这个用户的权限也给配置的很低。因此有比较高的安全性。一切都只给最低权限。这就是nobody存在的意义。”

——-摘自 https://blog.csdn.net/gjkun0202/article/details/71156205

查看linux用户列表

cat /etc/passwd|grep -v nologin|grep -v halt|grep -v shutdown|awk -F":" '{ print $1"|"$3"|"$4 }'|more

查看nobody用户所在的组,以及组内成员 groups nobody

查看所有用户组 cat /etc/group

——-摘自 https://www.cnblogs.com/jackyyou/p/5498083.html

openresty实现图片(文件)服务器

介绍

前序

该功能是利用openresty的lua脚本实现的图片(文件)保存功能,文件上传使用java代码开发的

数据定义

上传数据和文件信息不分前后,但系统只会保存最后一对信息

  • 数据格式:
{"fileDir":"文件保存的目录","fileName":"文件名"}
  • 返回结果
{"status":"是否成功","result":"返回结果","msg":"异常原因"}
enum status:["success","failed"]
  • 保存文件夹
    所保存到那个文件夹下,在nginx的perfix变量中定义

代码实现

Nginx配置

如下:

server {
    listen       80;
    server_name  localhost;
# 配置保存的文件夹
    set $prefix "/data";

    location /uploadimage {
# 配置是否每次lua更改都生效,适合调试时使用
#       lua_code_cache off;
# 配置lua脚本
        content_by_lua_file /openresty-web/luascript/luascript;
    }
# 用来配合理解传入到nginx的报文结构
    location /uploadtest{
#       lua_code_cache off;
        content_by_lua_file /openresty-web/luascript/luauploadtest;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}

lua脚本

luascript:

package.path = '/openresty-web/lualib/resty/?.lua;'
local upload = require "upload"
local cjson = require("cjson")

Result={status="success",result="",msg=""}
Result.__index=Result
function Result.conSuccess(ret)
    ret["status"]="success"
    ret["result"]="upload success"
    return ret
end

function Result.conFailed(ret,err)
    ret["status"]="failed"
    ret["msg"]=err
    ret["result"]="upload failed"
    return ret
end

function Result:new()
    local ret={}
    setmetatable({},Result)
    return ret
end

-- lua-resty-upload
local chunk_size = 4096
local form = upload:new(chunk_size)
if not form then
    ngx.say(cjson.encode(Result.conFailed(Result:new(),"plase upload right info")))
    return 
end
local file
local filelen=0
form:set_timeout(0) -- 1 sec
local filename
local prefix=ngx.var.prefix

-- 匹配文件名,当前案例用于判断是否是文件模块
function get_filename(res)
    local filename = ngx.re.match(res,'(.+)filename="(.+)"(.*)')
    if filename then 
        return filename[2]
    end
end


-- 用来开启输入流,当文件夹不存在时自动创建
function openstream(fileinfo,opt)
    local file,err=io.open(prefix..fileinfo["fileDir"],"r")
    if not file then
        local start=string.find(err,"No such file or directory")
        if start then
            local exeret=os.execute("mkdir -p "..prefix..fileinfo["fileDir"])
            if exeret ~= 0 then
                return nil,"Make directory failed"
            end
        else
            return nil,err
        end
    end
    file,err=io.open(prefix..fileinfo["fileDir"]..fileinfo["fileName"],opt)
    return file,err
end

local osfilepath
local tmpfiletbl
local hasFile=false
local loopfile=false
local fileinfostr
local fileinfo
local result=Result:new()
-- 循环读取文件和文件信息
while true do
    local typ, res, err = form:read()
    if not typ then
        break
    end
    if typ == "header" then
        if res[1] ~= "Content-Type" then
            filename = get_filename(res[2])
            if filename then
                loopfile=true
                hasFile=true
                -- 判断是否有文件信息
                -- 如果没有记录内存
                if fileinfo then
                    file,err=openstream(fileinfo,"w")
                    if not file then
                        break
                    end
                else
                    tmpfiletbl={}
                end
            else
                loopfile = false
                fileinfostr = ""
            end
        end
    end
    if loopfile then
        if typ == "body" then
            if file then
                filelen= filelen + tonumber(string.len(res))    
                file:write(res)
            else
                table.insert(tmpfiletbl,res)
            end
        elseif typ == "part_end" then
            if file then
                file:close()
                file = nil
            end
        end
    else
        if typ == "body" then
            fileinfostr=fileinfostr .. res
        elseif typ == "part_end" then
            fileinfo = cjson.decode(fileinfostr)
        end
    end
    if typ == "eof" then
        break
    end
end

if not hasFile then
    err="plase upload file"
elseif not fileinfo or not fileinfo["fileDir"] or not fileinfo["fileName"] then
    err="plase offer file info"
end

if err then
    ngx.log(ngx.ERR,err)
    Result.conFailed(result,err)
    ngx.say(cjson.encode(result))
    return 
end

-- 因为有文件信息在文件之后传送的
-- 所以需要将输入到内存中的文件信息打印到磁盘
if tmpfiletbl and table.getn(tmpfiletbl) > 0 then
    file,err=openstream(fileinfo,"w")
    if not file then
        ngx.log(ngx.ERR,err)
        Result.conFailed(result,err)
        ngx.say(cjson.encode(result))
        return 
    else
        for index,value in ipairs(tmpfiletbl)
        do
            filelen= filelen + tonumber(string.len(value)) 
            file:write(value)
        end
        file:close()
        file=nil
    end
end


Result.conSuccess(result)
ngx.say(cjson.encode(result))

luauploadtest:

local upload = require "resty.upload"
local cjson = require "cjson"

local chunk_size = 5 -- should be set to 4096 or 8192
                     -- for real-world settings

local form, err = upload:new(chunk_size)
if not form then
    ngx.log(ngx.ERR, "failed to new upload: ", err)
    ngx.exit(500)
end

form:set_timeout(1000) -- 1 sec

while true do
    local typ, res, err = form:read()
    if not typ then
        ngx.say("failed to read: ", err)
        return
    end

    ngx.say("read: ", cjson.encode({typ, res}))

    if typ == "eof" then
        break
    end
end

local typ, res, err = form:read()
ngx.say("read: ", cjson.encode({typ, res}))

luauploadtest代码是官方提供代码

Java

ImageServer:

package cn.com.cgbchina.image;

import cn.com.cgbchina.image.exception.ImageDeleteException;
import cn.com.cgbchina.image.exception.ImageUploadException;
import org.springframework.web.multipart.MultipartFile;

/**
 * Created by 11140721050130 on 16-3-22.
 */
public interface ImageServer {
    /**
     * 刪除文件
     *
     * @param fileName 文件名
     * @return 是否刪除成功
     */
    boolean delete(String fileName) throws ImageDeleteException;

    /**
     *
     * @param originalName 原始文件名
     * @param file 文件
     * @return 文件上传后的相对路径
     */
    String upload(String originalName, MultipartFile file) throws ImageUploadException;
}

LuaResult:

package cn.com.cgbchina.image.nginx;

import lombok.Getter;
import lombok.Setter;

/**
 * Comment: 用来保存返回结果,
 * 原本想放入到LuaImageServiceImpl的内部类中,
 * 但是Jackson不支持,没法反序列化
 * Created by ldaokun2006 on 2017/10/24.
 */
@Setter
@Getter
public class LuaResult{
    private LuaResultStatus status;
    private String result;
    private String msg;
    private String httpUrl;
    public LuaResult(){}

    public void setStatus(String result){
        status=LuaResultStatus.valueOf(result.toUpperCase());
    }
    public enum LuaResultStatus{
        SUCCESS,FAILED;
    }
}

ImageServerImpl:

package cn.com.cgbchina.image.nginx;

import cn.com.cgbchina.common.utils.DateHelper;
import cn.com.cgbchina.image.ImageServer;
import cn.com.cgbchina.image.exception.ImageDeleteException;
import cn.com.cgbchina.image.exception.ImageUploadException;
import com.github.kevinsawicki.http.HttpRequest;
import com.google.common.base.Splitter;
import com.spirit.util.JsonMapper;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Comment: 实现文件上传功能
 * Created by ldaokun2006 on 2017/10/16.
 */
@Service
@Slf4j
public class LuaImageServiceImpl implements ImageServer{
    // 存放nginx服务器url的,某些架构会有多个放置图片的地方
    private List<String> httpUrls;
    private ExecutorService fixedThreadPool ;
    private Integer timeout;
    private int threadSize=50;

    public LuaImageServiceImpl(String httpUrls){
        this(httpUrls,30000);
    }

    /**
     *
     * @param httpUrls 存放nginx服务器url
     * @param timeout http超时时间
     */
    public LuaImageServiceImpl(String httpUrls,int timeout){
        this.httpUrls=Splitter.on(";").splitToList(httpUrls);
        // 没啥看得,就是想让线程池的名字易懂些
        this.fixedThreadPool= new ThreadPoolExecutor(threadSize, threadSize,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(),new ThreadFactory(){
                    private final AtomicInteger poolNumber = new AtomicInteger(1);
                    private final ThreadGroup group;
                    private final AtomicInteger threadNumber = new AtomicInteger(1);
                    private final String namePrefix;

                    {
                        SecurityManager s = System.getSecurityManager();
                        group = (s != null) ? s.getThreadGroup() :
                                Thread.currentThread().getThreadGroup();
                        namePrefix = "LuaUploadPool-" +
                                poolNumber.getAndIncrement() +
                                "-thread-";
                    }

                    public Thread newThread(Runnable r) {
                        Thread t = new Thread(group, r,
                                namePrefix + threadNumber.getAndIncrement(),
                                0);
                        if (t.isDaemon())
                            t.setDaemon(false);
                        if (t.getPriority() != Thread.NORM_PRIORITY)
                            t.setPriority(Thread.NORM_PRIORITY);
                        return t;
                    }
                });
        this.timeout=timeout;
    }

    /**
     * Comment: 没必要开发删除功能
     * @param fileName 文件名
     * @return
     * @throws ImageDeleteException
     */
    @Override
    public boolean delete(String fileName) throws ImageDeleteException {
        return true;
    }

    /**
     * Commont: 用来给SpringMVC用
     * @param originalName 原始文件名
     * @param file 文件
     * @return
     * @throws ImageUploadException
     */
    @Override
    public String upload(String originalName, MultipartFile file) throws ImageUploadException {
        try {
            return this.upload(originalName,file.getInputStream());
        } catch (IOException e) {
            log.error("upload fail : " + e.getMessage(), e);
            throw new ImageUploadException("upload fail : "+e.getMessage(),e);
        }
    }

    /**
     * Commont: 上传图片核心代码
     * @param originalName 原始文件名
     * @param inputStream 要上传文件的文件流
     * @return
     * @throws ImageUploadException
     */
    private String upload(String originalName,InputStream inputStream) throws ImageUploadException {
        ByteArrayOutputStream byteOutStream = null;
        try {
            //准备数据
            byte[] tmpData=new byte[1024];
            byte[] inputData;
            byteOutStream = new ByteArrayOutputStream();
            int len=0;
            while((len=inputStream.read(tmpData,0,tmpData.length))!=-1){
                byteOutStream.write(tmpData,0,len);
            }
            inputData=byteOutStream.toByteArray();
            LuaSend sendInfo = new LuaSend(generateFileDir(),generateFileName(originalName));
            List<Future<LuaResult>> resultList=new ArrayList<>(httpUrls.size());

            //发送图片
            for(String httpUrl:httpUrls) {
                SendImg sendImg = new SendImg(httpUrl,sendInfo, inputData,this.timeout);
                resultList.add(fixedThreadPool.submit(sendImg));
            }
            for(Future<LuaResult> future:resultList) {
                // 线程池异常在这里抛出
                LuaResult resultLuaResult = future.get();
                if (LuaResult.LuaResultStatus.SUCCESS != resultLuaResult.getStatus()) {
                    throw new ImageUploadException("lua result url:"+resultLuaResult.getHttpUrl()+" msg : " + resultLuaResult.getMsg());
                }
            }

            return sendInfo.toString();
        }catch (Exception e){
            log.error("upload fail : "+e.getMessage(),e);
            throw new ImageUploadException("upload fail : "+e.getMessage(),e);
        }finally {
            try {
                if(byteOutStream!=null) {
                    byteOutStream.close();
                }
                if(inputStream!=null) {
                    inputStream.close();
                }
            } catch (IOException e) {
                throw new ImageUploadException("upload fail : "+e.getMessage(),e);
            }
        }
    }
    String separator=File.separator;
    String dateFormat=separator+"yyyy"+separator+"MM"+separator+"dd"+ separator;

    /**
     * Comment:根据时间做路径,防止某一个文件夹东西太多
     * @return 返回要保存的路径
     */
    private String generateFileDir(){
        return DateHelper.date2string(new Date(),dateFormat);
    }

    /**
     * Comment: 用UUID防止文件名重复
     * @param originalName 源文件名字
     * @return 要保存的文件名
     */
    private String generateFileName(String originalName){
        return UUID.randomUUID().toString();
    }

    /**
     * Comment: 用来发送图片的
     */
    @AllArgsConstructor
    class SendImg implements  Callable<LuaResult>{

        private String httpUrl;
        private LuaSend sendInfo;
        private byte[] inputStream;
        private Integer timeout;


        @Override
        public LuaResult call() throws Exception {
            try {
                String resultStr = HttpRequest
                        .post(httpUrl, false)
                        .part("fileInfo", JsonMapper.JSON_NON_EMPTY_MAPPER.toJson(sendInfo))
                        // 这个地方有个坑,part上传图片必须要用这个方式,
                        // 不能用没有Content-Type和fileName的
                        .part("file", sendInfo.getFileName(), "multipart/form-data; boundary=00content0boundary00", new ByteArrayInputStream(inputStream))
                        .connectTimeout(timeout).body();
                log.info("result:"+resultStr);
                LuaResult result = JsonMapper.JSON_NON_DEFAULT_MAPPER.fromJson(resultStr, LuaResult.class);
                result.setHttpUrl(httpUrl);
                return result;
            }catch(Exception e){
                throw new ImageUploadException("upload failed url:"+httpUrl+" info:"+sendInfo.toString(),e);
            }
        }
    }

    /**
     * Comment:文件数据
     */
    @Setter
    @Getter
    @AllArgsConstructor
    class LuaSend {
        // 文件目录
        private String fileDir;
        // 文件名
        private String fileName;
        @Override
        public String toString(){
            return fileDir+fileName;
        }
    }


    /**
     * Comment:测试用
     * @param args
     * @throws ImageUploadException
     * @throws FileNotFoundException
     */
    public static void main(String[] args) throws ImageUploadException, FileNotFoundException {
        LuaImageServiceImpl service=new LuaImageServiceImpl("http://192.168.99.102/uploadimage");
        try {
            System.out.println(service.upload("qqqqq", new FileInputStream("D:\shsh.txt")));
        }finally {
            service.fixedThreadPool.shutdown();
        }
    }
}

总结

可能出现的问题

  1. 上传两个图片或图片信息时系统只保留最后一个信息
  2. 图片和图片信息可以随意放置,但是这两个必须成对发送,建议先发送图片信息后发送图片,这样图片不用在lua处保存到内存中
  3. 上传大图片时会出现文件太大的提示,需要在nginx配置文件中添加client_max_body_size 100M;
  4. Http Header的Content-Type必须使用multipart/form-data;
    boundary=00content0boundary00,boundary必须存在不然不好用
  5. 传送图片HttpRequest.part上传图片必须写明Content-type和fileName,不然不好用但是Content-type不用非的用例子上的方式
  6. 图片信息必须拷贝成byte型,因为多线程使用时需要各自发送

开发中遇到的问题

  1. 传送图片HttpRequest.part上传图片必须写明Content-type,不然不好用
  2. Jackson和fastjson对于需要反序列化的类,必须有无参构造函数,并且不能是内部类
  3. lua的string.find如果没有找到,返回结果为nil
  4. CSDN的编辑器,无需功能不好用

涉及到知识

  1. HttpRequest.part用来上传Content-type:multipart/form-data;
  2. lua的使用:http://www.runoob.com/lua/lua-tutorial.html
  3. openresty的api:http://openresty.org/cn/components.html

Nginx下如何设置WordPress为多站点?

WordPress的多站点功能允许安装一个WordPress程序的情况下,实现多个站点(也就是一套程序,可以绑定多个域名或子域名)。

每个站点拥有独立的主题、插件、文章以及页面。

这样可以极大的减少了维护和更新多个WordPress安装程序的麻烦,

并且,每个站点之间又能够相互独立,互不影响。

WordPress multisite有两个方式:子目录和子域名,这里我们主要介绍子域名方式。

也就是说,在主域名的基础上,我们会创建一个子域名,例如:http://shop.awaimai.com。

同时,我们可以映射这个子域名到一个一级域名如:http://shop.com,

对于访问者来说,访问的就是独立的一级域名。

1、准备

WordPress介绍其多站点功能页面:站点网络管理页面

接着,我们准备几个域名,如下:

  • 站点一:www.awaimai.com(主域名),这是安装WordPress时用的域名
  • 站点二:blog.awaimai.com,二级域名
  • 站点三:news.com,映射的二级域名 news.awaimai.com
  • 站点四:shop.com,映射的二级域名 shop.awaimai.com

注意:WordPress安装后请勿擅自在后台修改域名,即使是把有www改成无www,或者反过来,都有可能引起 redirected you too many times. 错误,详情请看[参考资料2]

然后,登录域名服务商的解析页面,把以上域名的A记录全部设置为WordPress安装的服务器IP。

也可以在本地电脑测试,可以直接修改hosts文件,加入下面1行:

127.0.0.1 www.awaimai.com blog.awaimai.com news.com shop.com

2、Nginx配置

在Nginx配置目录下创建新建一个配置文件,如下:

$ sudo vi /etc/nginx/conf.d/awaimai.conf

内容为:

server {
    listen 80;
    server_name www.awaimai.com blog.awaimai.com news.com shop.com;

    root /usr/share/nginx/wordpress;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$args ;
    }

    location ~ /favicon.ico {
        access_log off;
        log_not_found off;
    }

    location ~ .php$ {
        try_files $uri /index.php;
        include fastcgi_params;
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }

    access_log /var/log/nginx/$host-access.log;
    error_log /var/log/nginx/wpms-error.log;
}

这里我们使用$host变量可以让Nginx为每个域名生成独立的访问日志,

如:news.com-access.log 和 shop.com-access.log。

但是error日志不能用$host变量,所以所有的错误会记录在一个文件里面。

再重启Nginx服务器:

$ nginx -s reload

3、安装WordPress

按照WordPress正常安装步骤安装WordPress。

4、启用多站点功能

用文本编辑器打开 wp-config.php 文件,在注释:/* 好了!请不要再继续编辑。请保存本文件。使用愉快! */之前加上如下一行:

/* Multisite settings */
define( 'WP_ALLOW_MULTISITE', true );

接下来我们还会编辑这个文件几次。

保存后登录WordPress后台,点击:工具 > 网络设置,选择 子域名,网络标题和网络管理员邮箱任意输入。

然后端机安装。

未分类

稍等片刻后,界面出现两个代码块,提示分别加入wp-config.php和.htaccesss文件。

这里我们用的是Nginx,所以不需要管 .htaccess 部分。

打开wp-config.php文件,还是在注释:/* 好了!请不要再继续编辑。请保存本文件。使用愉快! */之前,加上如下几行:

define('MULTISITE', true);
define('SUBDOMAIN_INSTALL', true);
define('DOMAIN_CURRENT_SITE', 'www.awaimai.com');
define('PATH_CURRENT_SITE', '/');
define('SITE_ID_CURRENT_SITE', 1);
define('BLOG_ID_CURRENT_SITE', 1);
Log out of the WordPress admin panel, and log in again.

登出WordPress后台,再登入。

打开面板左上角 我的站点 > 网络管理 > 站点。

未分类

点击 添加新的 按钮,打开添加新站点表单,依次添加 blog、news、shop三个子域名。

未分类

添加完成后,选 所有站点 ,编辑 news.awaimai.com 和 shop.awaimai.com 两个子域名,站点标题分别为新闻和商城,

把 站点地址(URL)分别改成:news.com和 shop.com。

这一步完成后,我们就可以访问blog.awaimai.com了,它已经是一个独立的站点了,拥有独立的资源了。

但是要能访问news.com和shop.com,还需继续往下看。

5、设置域名映射

打开面板左上角 我的站点 > 网络管理 > 插件。

未分类

在这里安装 WordPress MU Domain Mapping 插件,直接搜索或者下载安装都可以,然后启用。

接着复制插件目录(目录wp-content/plugins/wordpress-mu-domain-mapping)下的 sunrise.php 文件到 wp-content 目录。

打开wp-config.php文件,还是在注释:/* 好了!请不要再继续编辑。请保存本文件。使用愉快! */之前,加上如下一行:

define('SUNRISE', 'on');

保存,然后返回浏览器,在后台中打开面板左上角 我的站点 > 网络管理 > 设置。

再选择Domain Mapping,修改 Domain Options 为如下图:

未分类

然后保存。

这里的配置的功能是:重定向所有二级域名(如 news.awaimai.com )到各自的外部域名(如 news.com),包括管理页面(/wp-admin)。

接下来,我们要映射一级域名到各个站点ID。

默认在后台不显示站点ID,所以我们用一个最简单的方法让后台直接显示站点ID。

这个方法就是用WordPress的 Must-use plugin。

在 wp-content 目录下创建一个 mu-plugins 目录,再在新建的目录下创建一个名为 wpms_blogid.php 的文件,

这个PHP文件的内容为:

<?php
add_filter( 'wpmu_blogs_columns', 'do_get_id' );
add_action( 'manage_sites_custom_column', 'do_add_columns', 10, 2 );
add_action( 'manage_blogs_custom_column', 'do_add_columns', 10, 2 );

function do_add_columns( $column_name, $blog_id ) {
    if ( 'blog_id' === $column_name )
        echo $blog_id;
    return $column_name;
}

function do_get_id( $columns ) {
    $columns['blog_id'] = 'ID';
    return $columns;
}

保存后再访问后台的 站点 > 所有站点,在站点列表中就会多一列ID,下一步就会用到。

后台控制面板切换到 设置 > Domains,加入两个域名:

  • Site ID:3(以自己实际为主)
  • Domian:news.com
  • Primary:√

以及:

  • Site ID:4(以自己实际为主)
  • Domian:shop.com
  • Primary:√

如果域名是有www的,一样的操作方式。

6、结果

以上步骤完成之后,基本就OK了。

主站点域名还是不变,还是www.awaimai.com。

用 news.com 就可以访问新闻站点,

用 shop.com 就可以访问商城站点,

博客还是可以用二级域名 blog.awaimai.com 访问。

同时,这几个站点的后台也有独立的地址:

http://www.awaimai.com/wp-admin/
http://blog.awaimai.com/wp-admin/
http://news.com/wp-admin/
http://shop.com/wp-admin/

以后再安装主题和插件不能在每个站点中安装了,

都统一在网络管理(面板左上角 我的站点 > 网络管理 )中进行配置。

Nginx 配置虚拟主机

阅读本文需要安装Nginx:https://www.cnblogs.com/huangyi-427/p/9229645.html

一、什么是配置虚拟主机

就是在一台服务器启动多个网站

二、通过端口区分虚拟主机

复制一份静态页面

cd /usr/local/nginx

cp -r html html81

修改部分内容以示区分

vim /usr/local/nginx/html81/index.html

未分类

查看配置文件

more /usr/local/nginx/conf/nginx.conf
#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

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

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

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

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

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


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

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


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

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

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

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

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

}

未分类

可以通过配置多个server来配置多个虚拟主机

添加虚拟主机 将下面配置拷贝进去(与原有的server节点同级)

vim /usr/local/nginx/conf/nginx.conf
    server {
        listen       81;
        server_name  localhost;

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

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }

进入sbin目录

cd /usr/local/nginx/sbin

开机状态刷新配置文件

./nginx -s reload

未分类

未分类

三、通过域名区分虚拟主机

域名: 域名就是网站

DNS服务器: 把域名解析为IP地址 保存的就是域名和IP的映射关系

注意: 一个域名对应一个IP地址 一个IP地址可以被多个域名绑定

这里我准备了2个域名

www.hb218.cn  www.hdcpa.cn

在阿里云上购买的 只有3-5天的使用期(可以续费) 总共花了2个大洋

买好域名之后需要在阿里云控制台 -> 云解析DNS -> 配置2个域名指向同一台nginx服务器(IP)

这里赞一下 马爸爸的阿里云平台啥都有 挺方便的

复制一份显示www.hb218.cn的静态页面

cd /usr/local/nginx

cp -r html html-hb218

修改部分内容以示区分

vim /usr/local/nginx/html-hb218/index.html

未分类

复制一份显示www.hdcpa.cn的静态页面

cd /usr/local/nginx

cp -r html html-hdcpa

修改部分内容以示区分

vim /usr/local/nginx/html-hdcpa/index.html

未分类

查看配置文件

more /usr/local/nginx/conf/nginx.conf
#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

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

        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

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

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

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


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

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


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

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

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

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

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

}

未分类

可以通过配置多个server来配置多个虚拟主机

添加虚拟主机 将下面配置拷贝进去(与原有的server节点同级)

vim /usr/local/nginx/conf/nginx.conf
    server {
        listen       80;
        server_name  www.hb218.cn;

        location / {
            root   html-hb218;
            index  index.html index.htm;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }

    server {
        listen       80;
        server_name  www.hdcpa.cn;

        location / {
            root   html-hdcpa;
            index  index.html index.htm;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }

进入sbin目录

cd /usr/local/nginx/sbin

开机状态刷新配置文件

./nginx -s reload

大功告成 浏览器分别访问www.hb218.cn  www.hdcpa.cn

一个有意思的Tomcat 异常

之前有位读者留言,说了一个 Tomcat 异常的问题。
即 Tomcat 各功能正常,不影响使用,但是偶尔的在日志中会看到类似于这样的异常信息:

INFO [https-apr-8443-exec-5] org.apache.coyote.http11.Http11Processor.service Error parsing HTTP request header 
 Note: further occurrences of HTTP header parsing errors will be logged at DEBUG level. 
 java.lang.IllegalArgumentException: Invalid character (CR or LF) found in method name 
    at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:443) 
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:982) 

为啥报这个呢?明明自己没做什么操作。

顺着异常信息我们往上看,首先这个提示是解析请求头出现的错误。更细节一些是解析请求头中第一行,所谓的「Request Line」的时候出了问题。

什么是「Request Line」呢? 就是HTTP 规范中指定的,以请求方法开头 再加上请求URI 等。具体看这个规范说明

未分类

这里我们的异常信息提示我们是在解析 Method name的时候出了问题。看规范里说了「The Request-Line begins with a method token」也就是有固定的东西的,不是啥都能叫一个method name。我们熟悉的GET/POST/PUT/DELETE都是这里允许的。

我们再来看 Tomcat 的源码,是如何判断这里的 Requet Line 是不是一个包含一个合法的 method name。
顺着异常的类和方法,轻车熟路,直接就能看到了。

if (parsingRequestLinePhase == 2) { 
    // 
    // Reading the method name 
    // Method name is a token 
    // 
    boolean space = false; 
    while (!space) { 
        // Read new bytes if needed 
        if (byteBuffer.position() >= byteBuffer.limit()) { 
            if (!fill(false)) // request line parsing 
                return false; 
        } 
        // Spec says method name is a token followed by a single SP but 
        // also be tolerant of multiple SP and/or HT. 
        int pos = byteBuffer.position(); 
        byte chr = byteBuffer.get(); 
        if (chr == Constants.SP || chr == Constants.HT) { 
            space = true; 
            request.method().setBytes(byteBuffer.array(), parsingRequestLineStart, 
                    pos - parsingRequestLineStart); 
        } else if (!HttpParser.isToken(chr)) { 
            byteBuffer.position(byteBuffer.position() - 1); 
            throw new IllegalArgumentException(sm.getString("iib.invalidmethod")); 
        } 
    } 
    parsingRequestLinePhase = 3; 
} 

我们注意红色的异常就是上面产生的内容。产生这个是由于读取的byte 不是个 SP 同时下面的 isToken 也不是true导致。
那Token都有谁是怎么定义的?
这里挺有意思的,直接用一个boolean数组来存,前面我们传进来的byte,对应的是这个数组的下标。

public static boolean isToken(int c) { 
    // Fast for correct values, slower for incorrect ones 
    try { 
        return IS_TOKEN[c]; 
    } catch (ArrayIndexOutOfBoundsException ex) { 
        return false; 
    } 
} 

这里的boolean数组,初始化时有几个关联的数组一起,长度为128。

private static final boolean[] IS_CONTROL = new boolean[ARRAY_SIZE]; 
private static final boolean[] IS_SEPARATOR = new boolean[ARRAY_SIZE]; 
private static final boolean[] IS_TOKEN = new boolean[ARRAY_SIZE]; 
// Control> 0-31, 127 
if (i < 32 || i == 127) { 
    IS_CONTROL[i] = true; 
} 
// Separator 
if (    i == '(' || i == ')' || i == '<' || i == '>'  || i == '@'  || 
        i == ',' || i == ';' || i == ':' || i == '\' || i == '"' || 
        i == '/' || i == '[' || i == ']' || i == '?'  || i == '='  || 
        i == '{' || i == '}' || i == ' ' || i == 't') { 
    IS_SEPARATOR[i] = true; 
} 

// Token: Anything 0-127 that is not a control and not a separator 
if (!IS_CONTROL[i] && !IS_SEPARATOR[i] && i < 128) { 
    IS_TOKEN[i] = true; 
} 

所以这里token的定义明确了,非控制字符,非分隔符,ascii 码小于128 的都是 token。

所以问题产生原因定位了,是由于我们的请求头中传递了「非法」方法名称,导致请求不能正确处理。

我们来看一个正常的请求信息

未分类

Request Line 就是上面看到的第一行内容。 GET /a/ HTTP/1.1

那有问题的内容大概是这个样子

未分类

谁能从上面解析出来请求方法?

这时你可能会问,正常请求都好好的,你这个怎么搞的?

对。正常没问题,如果我们的Connector 是普通的此时可以响应请求,如果你一直http://localhost:port/a ,可以正常响应,此时后台收到一个https://localhost:port1/a,你要怎么响应?

要知道这两个编码大不一样。所以就出现了本文开头的问题。
如果不想走寻常路,可以自己写个Socket ,连到 Tomcat Server上,发个不合法的请求,大概也是一个样子。

那出现了这类问题怎么排查呢? 别忘了 Tomcat 提供了一系列有用的 Valve ,其中一个查看请求的叫AccessLogValve(阀门(Valve)常打开,快发请求过来 | Tomcat的AccessLogValve介绍)

在 Log里可以查看每个到达的请求来源IP,请求协议,响应状态,请求方法等。但是如果上面的异常产生时,请求方法这类有问题的内容也是拿不到的,此时的response status 是400 。但通过IP我们能看到是谁在一直请求。如果判断是非法请求后,可以再增加我们的过滤Valve,直接将其设置为Deny就OK了。

详解:配置启用NGINX状态页面

Nginx是一款免费的开源,高性能,可靠,可扩展且可完全扩展的Web服务器,负载均衡器和反向代理软件。 它有一个简单和易于理解的配置语言。 它还支持多种静态模块(自第一个版本开始就存在于Nginx中)和动态模块 (在1.9.11版本中引入 )。

Nginx中的一个重要模块是ngx_http_stub_status_module模块,它通过“ 状态页面 ”提供对基本Nginx状态信息的访问。 它显示活动客户端连接总数,接受和处理的总数,请求总数以及读取,写入和等待连接数等信息。

在大多数Linux发行版上, Nginx版本随ngx_http_stub_status_module启用。 您可以使用以下命令检查模块是否已启用。

# nginx -V 2>&1 | grep -o with-http_stub_status_module

检查Nginx状态模块

如果在终端中看到–with-http_stub_status_module作为输出,则表示状态模块已启用。 如果上述命令没有返回任何输出,则需要使用-with-http_stub_status_module作为配置参数从源代码编译NGINX ,如图所示。

# wget http://nginx.org/download/nginx-1.13.12.tar.gz
# tar xfz nginx-1.13.12.tar.gz
# cd nginx-1.13.12/
# ./configure --with-http_stub_status_module
# make
# make install

在验证模块之后,您还需要在NGINX配置文件/etc/nginx/nginx.conf中启用stub_status模块,以便为该模块设置一个本地可访问的URL(例如http://www.example.com/nginx_status )状态页面。

location /nginx_status {
stub_status;
allow 127.0.0.1;    #only allow requests from localhost
deny all;       #deny all other hosts   
}

启用Nginx状态页面

确保将127.0.0.1替换为服务器的IP地址,并确保只有您可访问此页面。

更改配置后,请确保检查nginx配置是否有任何错误,并使用以下命令重新启动nginx服务以实现最近的更改。

# nginx -t
# nginx -s reload 

检查Nginx配置

重新加载nginx服务器后,现在您可以使用curl程序访问下面的URL中的Nginx状态页面来查看您的指标。

# curl http://127.0.0.1/nginx_status
OR
# curl http://www.example.com/nginx_status

检查Nginx状态页面

重要说明 : ngx_http_stub_status_module模块已被Nginx 1.13.0版本中的ngx_http_api_module模块取代。

nginx配置location总结及rewrite规则写法

1. location正则写法

一个示例:

location  = / {
  # 精确匹配 / ,主机名后面不能带任何字符串
  [ configuration A ]
}

location  / {
  # 因为所有的地址都以 / 开头,所以这条规则将匹配到所有请求
  # 但是正则和最长字符串会优先匹配
  [ configuration B ]
}

location /documents/ {
  # 匹配任何以 /documents/ 开头的地址,匹配符合以后,还要继续往下搜索
  # 只有后面的正则表达式没有匹配到时,这一条才会采用这一条
  [ configuration C ]
}

location ~ /documents/Abc {
  # 匹配任何以 /documents/Abc 开头的地址,匹配符合以后,还要继续往下搜索
  # 只有后面的正则表达式没有匹配到时,这一条才会采用这一条
  [ configuration CC ]
}

location ^~ /images/ {
  # 匹配任何以 /images/ 开头的地址,匹配符合以后,停止往下搜索正则,采用这一条。
  [ configuration D ]
}

location ~* .(gif|jpg|jpeg)$ {
  # 匹配所有以 gif,jpg或jpeg 结尾的请求
  # 然而,所有请求 /images/ 下的图片会被 config D 处理,因为 ^~ 到达不了这一条正则
  [ configuration E ]
}

location /images/ {
  # 字符匹配到 /images/,继续往下,会发现 ^~ 存在
  [ configuration F ]
}

location /images/abc {
  # 最长字符匹配到 /images/abc,继续往下,会发现 ^~ 存在
  # F与G的放置顺序是没有关系的
  [ configuration G ]
}

location ~ /images/abc/ {
  # 只有去掉 config D 才有效:先最长匹配 config G 开头的地址,继续往下搜索,匹配到这一条正则,采用
    [ configuration H ]
}

location ~* /js/.*/.js
  • 已=开头表示精确匹配
    如 A 中只匹配根目录结尾的请求,后面不能带任何字符串。
  • ^~ 开头表示uri以某个常规字符串开头,不是正则匹配
  • ~ 开头表示区分大小写的正则匹配;
  • ~* 开头表示不区分大小写的正则匹配
  • / 通用匹配, 如果没有其它匹配,任何请求都会匹配到

顺序 no优先级:

(location =) > (location 完整路径) > (location ^~ 路径) > (location ~,~* 正则顺序) > (location 部分起始路径) > (/)

上面的匹配结果
按照上面的location写法,以下的匹配示例成立:

  • / -> config A
    精确完全匹配,即使/index.html也匹配不了

  • /downloads/download.html -> config B
    匹配B以后,往下没有任何匹配,采用B

  • /images/1.gif -> configuration D
    匹配到F,往下匹配到D,停止往下

  • /images/abc/def -> config D
    最长匹配到G,往下匹配D,停止往下
    你可以看到 任何以/images/开头的都会匹配到D并停止,FG写在这里是没有任何意义的,H是永远轮不到的,这里只是为了说明匹配顺序

  • /documents/document.html -> config C
    匹配到C,往下没有任何匹配,采用C

  • /documents/1.jpg -> configuration E
    匹配到C,往下正则匹配到E

  • /documents/Abc.jpg -> config CC
    最长匹配到C,往下正则顺序匹配到CC,不会往下到E

实际使用建议

所以实际使用中,个人觉得至少有三个匹配规则定义,如下:

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

http://tengine.taobao.org/boo…
http://nginx.org/en/docs/http…

2. Rewrite规则

rewrite功能就是,使用nginx提供的全局变量或自己设置的变量,结合正则表达式和标志位实现url重写以及重定向。rewrite只能放在server{},location{},if{}中,并且只能对域名后边的除去传递的参数外的字符串起作用,例如 http://seanlook.com/a/we/index.php?id=1&u=str 只对/a/we/index.php重写。语法rewrite regex replacement [flag];

如果相对域名或参数字符串起作用,可以使用全局变量匹配,也可以使用proxy_pass反向代理。

表明看rewrite和location功能有点像,都能实现跳转,主要区别在于rewrite是在同一域名内更改获取资源的路径,而location是对一类路径做控制访问或反向代理,可以proxy_pass到其他机器。很多情况下rewrite也会写在location里,它们的执行顺序是:

  1. 执行server块的rewrite指令
  2. 执行location匹配
  3. 执行选定的location中的rewrite指令

如果其中某步URI被重写,则重新循环执行1-3,直到找到真实存在的文件;循环超过10次,则返回500 Internal Server Error错误。

2.1 flag标志位

  • last : 相当于Apache的[L]标记,表示完成rewrite
  • break : 停止执行当前虚拟主机的后续rewrite指令集
  • redirect : 返回302临时重定向,地址栏会显示跳转后的地址
  • permanent : 返回301永久重定向,地址栏会显示跳转后的地址

因为301和302不能简单的只返回状态码,还必须有重定向的URL,这就是return指令无法返回301,302的原因了。这里 last 和 break 区别有点难以理解:

  1. last一般写在server和if中,而break一般使用在location中
  2. last不终止重写后的url匹配,即新的url会再从server走一遍匹配流程,而break终止重写后的匹配
  3. break和last都能组织继续执行后面的rewrite指令

2.2 if指令与全局变量

if判断指令
语法为if(condition){…},对给定的条件condition进行判断。如果为真,大括号内的rewrite指令将被执行,if条件(conditon)可以是如下任何内容:

  • 当表达式只是一个变量时,如果值为空或任何以0开头的字符串都会当做false
  • 直接比较变量和内容时,使用=或!=
  • ~正则表达式匹配,~*不区分大小写的匹配,!~区分大小写的不匹配

-f!-f用来判断是否存在文件
-d!-d用来判断是否存在目录
-e!-e用来判断是否存在文件或目录
-x!-x用来判断文件是否可执行

例如:

if ($http_user_agent ~ MSIE) {
    rewrite ^(.*)$ /msie/$1 break;
} //如果UA包含"MSIE",rewrite请求到/msid/目录下

if ($http_cookie ~* "id=([^;]+)(?:;|$)") {
    set $id $1;
 } //如果cookie匹配正则,设置变量$id等于正则引用部分

if ($request_method = POST) {
    return 405;
} //如果提交方法为POST,则返回状态405(Method not allowed)。return不能返回301,302

if ($slow) {
    limit_rate 10k;
} //限速,$slow可以通过 set 指令设置

if (!-f $request_filename){
    break;
    proxy_pass  http://127.0.0.1;
} //如果请求的文件名不存在,则反向代理到localhost 。这里的break也是停止rewrite检查

if ($args ~ post=140){
    rewrite ^ http://example.com/ permanent;
} //如果query string中包含"post=140",永久重定向到example.com

location ~* .(gif|jpg|png|swf|flv)$ {
    valid_referers none blocked www.jefflei.com www.leizhenfang.com;
    if ($invalid_referer) {
        return 404;
    } //防盗链
}

全局变量

下面是可以用作if判断的全局变量

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

例:http://localhost:88/test1/test2/test.php

$host:localhost
$server_port:88
$request_uri:http://localhost:88/test1/tes...
$document_uri:/test1/test2/test.php
$document_root:/var/www/html
$request_filename:/var/www/html/test1/test2/test.php

2.3 常用正则

  • . : 匹配除换行符以外的任意字符
  • ? : 重复0次或1次
  • + : 重复1次或更多次
  • * : 重复0次或更多次
  • d :匹配数字
  • ^ : 匹配字符串的开始
  • $ : 匹配字符串的介绍
  • {n} : 重复n次
  • {n,} : 重复n次或更多次
  • [c] : 匹配单个字符c
  • [a-z] : 匹配a-z小写字母的任意一个

小括号()之间匹配的内容,可以在后面通过$1来引用,$2表示的是前面第二个()里的内容。正则里面容易让人困惑的是转义特殊字符。

2.4 rewrite实例

例1:

http {
    # 定义image日志格式
    log_format imagelog '[$time_local] ' $image_file ' ' $image_type ' ' $body_bytes_sent ' ' $status;
    # 开启重写日志
    rewrite_log on;

    server {
        root /home/www;

        location / {
                # 重写规则信息
                error_log logs/rewrite.log notice;
                # 注意这里要用‘’单引号引起来,避免{}
                rewrite '^/images/([a-z]{2})/([a-z0-9]{5})/(.*).(png|jpg|gif)$' /data?file=$3.$4;
                # 注意不能在上面这条规则后面加上“last”参数,否则下面的set指令不会执行
                set $image_file $3;
                set $image_type $4;
        }

        location /data {
                # 指定针对图片的日志格式,来分析图片类型和大小
                access_log logs/images.log mian;
                root /data/images;
                # 应用前面定义的变量。判断首先文件在不在,不在再判断目录在不在,如果还不在就跳转到最后一个url里
                try_files /$arg_file /image404.html;
        }
        location = /image404.html {
                # 图片不存在返回特定的信息
                return 404 "image not foundn";
        }
}

对形如/images/ef/uh7b3/test.png的请求,重写到/data?file=test.png,于是匹配到location /data,先看/data/images/test.png文件存不存在,如果存在则正常响应,如果不存在则重写tryfiles到新的image404 location,直接返回404状态码。

例2:

rewrite ^/images/(.*)_(d+)x(d+).(png|jpg|gif)$ /resizer/$1.$4?width=$2&height=$3? last;

对形如/images/bla_500x400.jpg的文件请求,重写到/resizer/bla.jpg?width=500&height=400地址,并会继续尝试匹配location。

例3:

见 ssl部分页面加密http://seanlook.com/2015/05/28/nginx-ssl/

基于CentOS 7实现的NFS

NFS

NFS(Network FileSystem,网络文件系统),最早由Sun公司所发展出来的,主要是通过网络让不同的主机、不同的操作系统,可以彼此分享个别档案,因此我们也可以简单把NFS看成是一个文件服务器。通过NFS我们的PC可以将网络端的NFS服务器分享的目录挂载到本地端的机器当中,在本地端的机器看起来,远程主机的目录就好像是自己的一个磁盘分区一样。

未分类

NFS服务器与客户端通过随机选择小于1024以下的端口来进行数据传输,而这个端口的确定需要借助RPC(Remote Procedure Call,远程过程调用)协议的协助。RPC最主要的功能就是在指定每个NFS服务所对应的port number,并且回报给客户端,让客户端可以连结到正确的端口上去。当我们启动NFS服务时会随机取用数个端口,并主动向RPC注册,因此RPC可以知道每个端口对应的NFS,而RPC又是固定使用 port 111监听客户端的需求并回报客户端正确的端口。

未分类

  1. 客户端向服务器端的RPC(port 111)发出NFS的请求;
  2. 服务器端找到对应的已注册的NFS daemon端口后,回报给客户端;
  3. 客户端知道正确的端口后,直接与NFS daemon联机。

实现NFS

实验环境

[root@nfs ~]# lsb_release -r
Release:    7.2.1511
[root@nfs ~]# yum -y install nfs-utils
[root@nfs ~]# rpm -qi nfs-utils
Name        : nfs-utils
Epoch      : 1
Version    : 1.3.0
Release    : 0.54.el7
Architecture: x86_64
[root@nfs ~]# systemctl stop firewalld
[root@nfs ~]# setenforce 0
[root@nfs ~]# mkdir /nfs_dir
[root@nfs ~]# vim /nfs_dir/welcome
  This is NFS test file.

NFS相关配置

相关配置文件:
配置文件:/etc/exports
配置文件目录:/etc/exports.d

  NFS服务的主配置文件为/etc/exports,用于定义共享的目录以及客户端的权限,格式如下:

/PATH/TO/SOME_DIR clients1(export_options,...) clients2(export_options,...)

其中clients支持以下几种格式:

  • single host:ipv4,ipv6,FQDN
  • network:address/netmask
  • wildcards:主机名通配,例如,*.magedu.com
  • netgroups:NIS域内的主机组,@group_name
  • anonymous:使用*通配所有主机

export_options的常见参数可以分为以下两类:

  • General Options:
    • ro:客户端挂载后,其权限为只读,默认选项;
    • rw:读写权限;
    • sync:同时将数据写入到内存与硬盘中;
    • async:异步,优先将数据保存到内存,然后再写入硬盘;
    • Secure:要求请求源的端口小于1024
  • User ID Mapping:
    • root_squash:当NFS客户端使用root用户访问时,映射到NFS服务器的匿名用户;
    • no_root_squash:当NFS客户端使用root用户访问时,映射到NFS服务器的root用户;
    • all_squash:全部用户都映射为服务器端的匿名用户;
    • anonuid=UID:将客户端登录用户映射为此处指定的用户uid;
    • anongid=GID:将客户端登录用户映射为此处指定的用户gid

更多参数信息可以通过命令 man exports 查看帮助手册

配置NFS

[root@nfs ~]# vim /etc/exports
/nfs_dir 192.168.4.*(rw,sync,root_squash)
[root@nfs ~]# systemctl start nfs-server

客户端测试

#查看nfs能挂载的选项
[root@client ~]# showmount -e 192.168.4.119
Export list for 192.168.4.119:
/nfs_dir 192.168.4.*
[root@client ~]# mount -t nfs 192.168.4.119:/nfs_dir /mnt
[root@client ~]# cd /mnt/
[root@client mnt]# ls
welcome
[root@client mnt]# cat welcome 
This is NFS test file.
[root@client mnt]# touch file
touch: cannot touch ‘file’: Permission denied

可以看到无法在共享目录下创建文件,明明已经给分配了rw权限,这是因为root_squash把我们的访问权限压缩为nobody权限,自然无法对该目录进行写入操作。
对NFS的配置文件重新进行修改:

[root@nfs ~]# vim /etc/exports
/nfs_dir 192.168.4.*(rw,sync,no_root_squash)
#使用exportfs重读NFS配置,不需要重启服务
[root@nfs ~]# exportfs -rv
exporting 192.168.4.*:/nfs_dir

客户端重新测试:

[root@client mnt]# touch file
[root@client mnt]# ll
total 4
-rw-r--r--. 1 root root  0 Jun 28 14:11 file  #可以看到属主属组为root
-rw-r--r--. 1 root root 23 Jun 28 11:08 welcome
[root@client mnt]# echo 123 > file
[root@client mnt]# rm file
rm: remove regular empty file ‘file’? y  #可以删除文件
[root@client mnt]# ll
total 4
-rw-r--r--. 1 root root 23 Jun 28 11:08 welcome

很明显,将客户端访问共享文件用户映射为NFS服务器上的root是一种不安全的做法,我们可以指定客户端映射到NFS服务器的用户,配置如下:

[root@nfs ~]# useradd nfSUSEr -s /sbin/nologin 
[root@nfs ~]# id nfsuser
uid=1003(nfsuser) gid=1003(nfsuser) groups=1003(nfsuser)
[root@nfs ~]# chown -R nfsuser:nfsuser /nfs_dir/
[root@nfs ~]# vim /etc/exports
[root@nfs ~]# cat /etc/exports
/nfs_dir 192.168.4.*(rw,sync,all_squash,anonuid=1003,anongid=1003)
[root@nfs ~]# exportfs -rv
exporting 192.168.4.*:/nfs_dir

客户端进行测试:

[root@client mnt]# touch file
[root@client mnt]# ll
total 4
-rw-r--r--. 1 1003 1003  0 Jun 28 14:27 file
-rw-r--r--. 1 1003 1003 23 Jun 28 11:08 welcome
[root@client mnt]# echo 123 > file
[root@client mnt]# cat welcome 
This is NFS test file.
[root@client mnt]# rm file
rm: remove regular file ‘file’? y
[root@client mnt]# ll
total 4
-rw-r--r--. 1 1003 1003 23 Jun 28 11:08 welcome

zabbix配置

zabbix的一些常用配置

zabbix-agent

首先,安装完zabbix-web之后,想要监控系统,还需要在系统安装一个agent端:

yum -y install zabbix-agent

命令行安装即可,安装完成之后,可以在本地服务器下找到配置文件,我的是在:/etx/zabbix在:

zabbix_agentd.conf
zabbix_agentd.d

其中zabbix_agentd.d是文件夹,里面是具体的监控配置文件userparameter_mysql.conf,这个文件里面就是一系列zabbix可用的监控指标的配置,这个后面再专门写写zabbix的配置。

zabbix_get

zabbix有个工具:zabbix_get,可以用来获取监控指标,用法示例:

zabbix_get -s IP -k mysql.status[Com_insert]

上面这条命令在配置文件配置完成的情况下,可以用来获取服务器上MySQL执行的insert数量。

图像乱码问题

在第一次使用zabbix,图形显示,中文是一个个的方框,也就是乱码,这是因为zabbix的汉化不是很完全,可以手工替换字体来解决乱码问题。
首先在本地windows机器下拿到中文字体,例如楷体:simkai.ttf(文件路径:C:WindowsFonts)
然后把windows上下载完成的字体上传到zabbix服务器中,路径:/usr/share/zabbix/fonts
该目录下本来有一个系统字体,并且软链接到zabbix-web,也就是页面显示的字体,现在要做的就是用自己上传的楷体替换到系统原来的字体

[root@localhost fonts]# ll
total 11512
lrwxrwxrwx. 1 root root       33 Jun 26 15:46 graphfont.ttf -> /etc/alternatives/zabbix-web-font

首先是将系统原字体备份mv graphfont.ttf graphfont.ttf.back
然后修改配置文件/usr/share/zabbix/include/defines.inc.php
将文件中的FONT_NAME从原来的graphfont修改为simkai,一共有两处,修改完成之后页面上就可以看见乱码解决了~