WordPress文章页添加面包屑导航

前言

浏览网站的时候经常会发现许多网站页面上有个如下图所示的面包屑导航,非常漂亮而且有利于帮助用户快速学习和了解网站内容和组织方式,从而形成很好的位置感。DUX主题4.0版本开始增加了这么个功能,之前有网友也像我询问过这个功能,这两天刚好有时间就简单整理了下分享出来,希望大家喜欢。

未分类

额,出于好奇,还是先了解一下为什么这个导航叫面包屑导航吧。以下内容来自百度百科。

汉赛尔与格莱特是一对兄妹,为樵夫的前妻所生,在后母的逼迫下被父亲抛弃。兄妹俩前后经历了两次遗弃,第一次,汉赛尔沿途用石子做记号,兄妹俩重新回到了家中。第二次被遗弃,汉赛尔用面包屑做记号,却被鸟儿啄食干净,兄妹俩在森林中迷了路。不天的寻路,让他们饥饿难忍,腿脚无力,来到了一个用面包做屋项,糖果做窗户的小屋。饥饿让他们忘记了疲惫,及潜在的危险,啃起了屋子。结果在巫婆的诱骗下,哥哥被锁屋中,妹妹被迫做劳力,就在巫婆要吃掉哥哥之时,妹妹借向巫婆学习添柴之机。将巫婆推入炉中,兄妹俩带着巫婆的财宝,回到了家中。后母己经去世,兄妹俩和父亲一起过上了幸福的生活。

正文

对于面包屑导航网上有很多实现的方法,很多代码也非常细心的提供了对主页、标签页,搜索页、独立页等的支持,但是个人感觉一般也就文章页上真正能用的上,其他页面添加这个功能后多少显得累赘,故这里只提供文章页面添加此功能的方法,其他页面的添加可参照此法进行。

生成函数

将以下代码添加到主题的 functions.php 文件中去:

//面包屑导航生成函数
function qgg_breadcrumbs(){
    if( !is_single() ) return false;

    $categorys = get_the_category();
    $category = $categorys[0]; 
    return '当前位置:<a href="'.get_bloginfo('url').'">'.get_bloginfo('name').'</a> <small>></small> '.get_category_parents($category->term_id, true, ' <small>></small> ').get_the_title();
}

前端显示

将以下代码添加到主题的 single.php 文件中去:

<!-- 面包屑导航前端显示代码 -->
<div class="breadcrumbs">
    <div class="container"><?php echo qgg_breadcrumbs() ?></div>
</div>

样式美化

将以下代码添加到主题的主样式表里面去,DUX主题是 main.css文件,其他主题可能是 style.css 文件,具体请咨询主题作者。

.breadcrumbs{padding: 15px 0;font-size: 12px;line-height: 1;text-align: left;background-color: #fff;margin-bottom: 15px;margin-top: -15px;border-bottom: 1px solid #EBEBEB; box-shadow: 0 1px 3px rgba(0,0,0,.04);color: #999;}
.breadcrumbs small{font-size: 12px;font-family: serif;color: #bbb;margin: 0 2px;font-weight: bold;}
.breadcrumbs a{color: #999;}
.breadcrumbs a:hover{color: #666;}
@media (max-width:640px){
 .breadcrumbs{margin-top: 1px;margin-bottom: 0;padding: 10px 15px;border-bottom: none;margin-bottom: 1px;}
}

样式代码直接复制的 DUX 主题的,具体样式大家可自行调整。

将WordPress上面的网站迁移至另外一台VPS

站长们可能会遇到一个问题,之前的VPS配置可能不够用了,所以需要换一个VPS(比如:搬瓦工VPS)。
  
一、打包源网站

打包一下源网站的数据,这个操作应该很简单,相信大家都会做。比如你的网站目录是/data/www/bwh.idcspy.com,那么我们打包一下这个目录:

zip -r oldtang.com.zip /data/www/bwh.idcspy.com

这样我们就在当前目录下生成了一个 bwh.idcspy.com.zip 的压缩文件,里面包含了网站的所有文件。

二、打包数据库

进入 phpmyadmin,然后选择对应的MySQL 数据库,导出即可,另存为 .sql 文件就行。
  
三、迁移数据

迁移也很简单,我们将 .zip 文件放到网站目录下,然后我们从新的 VPS 上直接 wget 这个文件即可。

wget http://bwh.idcspy.com/bwh.idcspy.com.zip

然后解压到新的 VPS 上对应的目录下

unzip bwh.idcspy.com.zip /data/www/bwh.idcspy.com

接下来导入数据库,进入新的 VPS 的 phpmyadmin 里面,选择对应的数据库,导入.sql 文件。

四、更改解析

将域名的解析从原来的搬瓦工VPS 指向新的搬瓦工VPS 的 IP 地址就可以了,然后等待解析生效。

WordPress 教程:如何通过 PHP 代码修改表结构和索引

我们在 WordPress 创建表之后,由于业务的需求,可能需要对表结构进行修改,比如增加多一些字段,或者删除一些无用的字段,也有可能因为优化的问题需要对某个表结构增加索引。我们怎么操作呢?

修改表结构

比如我们需要给表 $table 增加一个 field1 字段。

首先我们判断一下,$table 是否已有该字段:

$wpdb->query("SHOW COLUMNS FROM `{$table}` WHERE field='field1'")

如果不含,我们增加:

if(!$wpdb->query("SHOW COLUMNS FROM `{$table}` WHERE field='field1'")){
    $wpdb->query("ALTER TABLE `{$table}` ADD COLUMN `field1` var(15) NOT NULL");
}

修改表索引

比如我们需要给表 $table 增加一个 field1_idx 索引。

首先我们判断一下,$table 是否已有该索引:

$wpdb->query("SHOW INDEX FROM `{$table}` WHERE Key_name='field1_idx'")

如果没有,我们增加:

if(!$wpdb->query("SHOW INDEX FROM `{$table}` WHERE Key_name='field1_idx'")){
    $wpdb->query("ALTER TABLE `{$table}` ADD KEY `field1_idx` (`field1`);");
}

WordPress基础配置文件wp-config.php详解

在安装WordPress博客程序的时候我们常常会碰到下面的问题:当我们按照安装步骤一步步填写好数据库名、用户名、密码/数据库主机以及表前缀的时候,WordPress突然给我们来一句“抱歉,我不能写入wp-config.php文件。”,这就很尴尬了不是。那么wp-config.php文件到底是什么呢,为什么我们安装个博客程序还要写入这么个文件呢?wp-config.php是wordpress的基础配置文件,有了它我们才可以正常的访问数据库,并针对自己的情况进行一些常规的配置。你的服务器之所以无法写入wp-config.php文件,多半是由于文件夹权限不足,一般情况下改为777就可以了,当然你也可以将程序自带的wp-config-sample.php下载下来,等配置完成后重命名为wp-config.php上传至服务器亦可。下面我们就来详细了解一下wp-config.php的一些基本配置。

未分类

告诉你一个小秘密:一定不要使用如Microsoft Word类型的文字处理程序来编辑WordPress文件哦!建议使用Notepad++进行编辑。

配置MySQL数据库参数

/** WordPress数据库的名称 */
define('DB_NAME', 'database_name_here');

/** MySQL数据库用户名 */
define('DB_USER', 'username_here');

/** MySQL数据库密码 */
define('DB_PASSWORD', 'password_here');

/** MySQL主机 */
define('DB_HOST', 'localhost');

/** 创建数据表时默认的文字编码 */
define('DB_CHARSET', 'utf8');

/** 数据库整理类型。如不确定请勿更改 */
define('DB_COLLATE', '');

首先是DB_NAME的配置,你需要将“database_name_here”替换成你自己的数据库名称。记住,是你的数据库名称而不是你登陆数据库的用户名,一般情况下,如果你选用的是虚拟主机,主机提供商会赠送给你一个数据库(阿里云的虚拟主机数据库可以在/主机管理控制台/数据库信息中查看);如果你选用的是云服务器而又没额外购买数据库的话,那么就需要你自己配置数据库了,此时的数据库名称也就是你自己配置的数据库名称了。

然后是DB_USERDB_PASSWORD了,这两个参数毫无疑问就是你登陆数据库管理工具的用户名和密码了,你只需要将对应的“username_here”与“password_here”替换成自己的用户名和密码就可以了。(阿里云虚拟主机的用户名对应的是数据库信息中的数据库账号一项,ECS服务器的用户名一般是root,对于其他公司的服务器与虚拟主机的数据库信息是什么样的,蝈蝈暂时还没用过,等用过以后再给大家分享一下吧)。

接着是DB_HOST参数设置了,如果你用的是虚拟主机,并且主机提供商赠送了数据库的话,那么将“localhost”替换成对应的数据库地址链接就可以了;如果你用的是服务器,并且是自己在服务器上安装的数据库,一般“localhost”不用更改或者改成“127.0.0.1”就可以了。

#如果你的主机为数据库使用备用端口的话,就需要修改wp-config.php中的DB_HOST值以反映出主机备用端口设置。#

对于localhost

define('DB_HOST', 'localhost:3307');

对于其他

define('DB_HOST', 'mysql.example.com:4454');

如果你实在不清楚自己数据库的地址,有一种更好的方法就是自动检测数据库服务器值:

define('DB_HOST', $_ENV{DATABASE_SERVER});

在WordPress 中,DB_CHARSET可用,以允许数据库字符集的标识(如TIS620 Thai,tis620)在定义MySQL数据库表时被使用。WordPress默认值的utf8,UTF-8支持多国语言,一般保持默认就好了,如果你不了解字符集,随意更改可是会崩盘的哦。DB_COLLATE参数是用来给数据库排序的(即字符集的排序次序)。一般保持留空(null),这样数据库排序才能被MySQL字段分配,这是基于DB_CHARSET所指定的数据库字符集之上的。

未分类

安全密钥与盐

define('AUTH_KEY',        'md9~vb7*_Va _IA8sZYnR)RSGKsB An,.9 uV|V5XLkz=t2 2|}Xf/a)j=%<T_Tg');
define('SECURE_AUTH_KEY', '+,:[):wzxAJx&|2piW.~#?][4fE]NR9-Ffth)6wxHjObks8%3%ZMAPCXvKf9?y|#');
define('LOGGED_IN_KEY',   'yqRwG$FI7|MvfgJIw,fs`uUxE<9$Tu#|qAF2duH|]mF;M[ JEJ|aVODk;~QXupEi');
define('NONCE_KEY',       '4]Dxj4muyYD>_kio1:g%38F9U |i6>v**wOM||UiCzv$`7ou]%)jj%Du%kH#XGEJ');

那么什么是密钥与盐呢?简单点说就是为了保证我们的用户账户安全,通过一段无序的字符来增加密码强度、抗暴力破解的安全机制。WordPress提供了AUTH_KEY、SECURE_AUTH_KEY、LOGGED_IN_KEY和NONCE_KEY这4中密钥进行加密,,它们能够保证用户cookies中的信息得到更好的加密,你可以使用wordpress提供的在线密钥生成器生成一段密钥,你也可以自己设置自己的密钥,总之你自要保证自己的密钥够长够复杂就好。如果你觉得这还不够安全,可以另外再加盐,WordPress提供了AUTH_SALT、SECURE_AUTH_SALT、LOGGED_IN_SALT、NONCE_SALT这4组参数供你设置。需要注意的是,密钥与盐尽量不要经常重新设定,因为你每次设定都会改变cookie,这样会使得当前登陆中的用户自动重新登陆。

数据库表格前缀

$table_prefix  = 'wp_';

数据库表格前缀是什么呢?它更像是你数据库表的标记,是被放在你的数据库表格前面部分的设定值。WordPress提供$table_prefix函数使你能自定义自己数据库表的前缀,那么自定义数据库表格的前缀有什么用处呢?

  1. 自定义数据库表前缀可以减轻你站点受攻击的可能性,总体上提高你的网站安全性;

  2. 如果你使用的是虚拟主机,同时你只有一个赠送的数据库,但是你想要建立多个WordPress站点,如果所有的站点都使用默认的数据表前缀“wp_”,那么当你想要编辑某个站点的数据库表的时候是不是很头疼不知道怎么区分那个表那行数据对应的是那个站点?这时候自定义数据库表前缀的好处就体现出来了,它可以让你轻松管理你的数据库;

开发者调试

define('WP_DEBUG', false);

WP_DEBUG是用来查看WordPress错误并处理的,默认为停用(false),主要是网站上线前开发调试用的。你可以通过在wp-config.php中将其布尔值改为true来开启它也可以通过我爱水煮鱼开发的插件WPJAM来从网站后台开启它,不过,如果不用的话建议关闭就好,毕竟开启后生成的错误文件很大,如果忘记处理会很快占满你的主机空间的。

另外,如果你想要修改WordPress的一些内置JavaScript你可以在wp-config.php中添加以下代码:

define('SCRIPT_DEBUG', true);

zh_CN本地化设置

define('WP_ZH_CN_ICP_NUM', true);

这个主要是针对备案网站显示备案号的设置,如果你的网站备案了,你可以在后台设置/常规中填写上自己的备案号,这地方填写的备案号只针对一些默认的主题有用,很多主题都集成了备案信息的选项,你需要在主题的选项中填写才能在网站前台显示你的备案号。

WordPress目录的绝对路径

if ( !defined('ABSPATH') )
    define('ABSPATH', dirname(__FILE__) . '/');
require_once(ABSPATH . 'wp-settings.php');

WordPress预定义了ABSPATH,在WordPress的主题以及插件中,如果你获得WordPress根目录的话,就可以用ABSPATH而不用每一次都用dirname(__FILE__)。wp-config.php的最后一行代码是用来提取WordPress变量与包含内容文件的。说到这里就有必要提一下WordPress的文件调用关系了,一般情况下WordPress的文件调用顺序为:

index.php > wp-blog-header.php > wp-load.php > wp-config.php > wp-settings.php > 其他文件

好了,关于WordPress基础配置文件wp-cnfig.php的整理就先进行到这里,后续我会逐步补充完善这篇日志,希望帮助自己学习的同时能够帮助到同样热爱WordPress的你。

Wireshark的基本使用——过滤器

前言

网络上关于Wireshark的教程已有不少,博主就简单介绍一下Wireshark分析数据包时最重要的技巧之一的过滤器。。一次性嗅探到的数据包有很多,想要高效地提取出你想要的数据包或者对某个数据包中某个字段值的分析等,必不可少的就是过滤。过滤器分为捕捉过滤器(CaptureFilters)和显示过滤器(DisplayFilters)。

捕捉过滤器

用于决定将什么样的信息记录在捕捉结果中,需要在开始捕捉前设置。

语法

未分类

  • Protocol(协议)

可能的值: ether, fddi, ip, arp, rarp, decnet, tcp and udp等。如果没有特别指明是什么协议,则默认使用所有支持的协议

  • Direction(方向)

可能的值: src, dst, src and dst, src or dst。如果没有特别指明来源或目的地,则默认使用 “src or dst” 作为关键字
比如:”host 10.2.2.2″与”src or dst host 10.2.2.2″是一样的

  • Host(s)

可能的值: net, port, host, portrange。如果没有指定此值,则默认使用”host”关键字

比如:”src 10.1.1.1″与”src host 10.1.1.1″相同

  • Logical Operations(逻辑运算)

可能的值:not, and, or

否(“not”)具有最高的优先级。或(“or”)和与(“and”)具有相同的优先级,运算时从左至右进行。
例如,
“not tcp port 3128 and tcp port 23″与”(not tcp port 3128) and tcp port 23″相同。
“not tcp port 3128 and tcp port 23″与”not (tcp port 3128 and tcp port 23)”不同。

捕获过滤器例子

显示目的TCP端口为3128的封包

tcp dst port 3128

显示来源IP地址为10.1.1.1的封包

ip src host 10.1.1.1

显示目的或来源IP地址为10.1.2.3的封包

host 10.1.2.3

显示来源为UDP或TCP,并且端口号在2000至2500范围内的封包

src portrange 2000-2500

显示除了icmp以外的所有封包

not icmp

显示来源IP地址为10.7.2.12,但目的地不是10.200.0.0/16的封包

src host 10.7.2.12 and not dst net 10.200.0.0/16

显示源IP为10.4.1.12或源网络为10.6.0.0/16,目的TCP 端口号在200至10000之间,并且目的位于网络 10.0.0.0/8内所有封包

(src host 10.4.1.12 or scr net 10.6.0.0/16) and tcp dst portrange 200-10000 and dst net 10.0.0.0/8

捕获广播流量

broadcast

显示过滤器

在捕捉的结果中进行详细查找时使用,可以在得到捕捉结果后随意修改显示过滤器表达式。

未分类

语法

未分类

  • Protocol(协议)

位于OSI模型第2至7层的协议,如:IP、TCP、DNS等

  • String1,String2(可选项)

协议的子类

  • Comparison Operator(比较运算符)

可以使用6种比较运算符

未分类

  • Logical Operations(逻辑运算符)

未分类

逻辑异或是一种排除性的或。当其被用在过滤器的两个条件之间时,只有当且仅当其中的一个条件满足时,这样的结果才会被显示在屏幕上

例如:”tcp.dstport 80 xor tcp.dstport 1025″

只有当目的TCP端口为80或者来源于端口1025(但又不能同时满足这两点)时,这样的数据包才会被显示

显示过滤器例子

显示SNMP或DNS或ICMP封包

snmp || dns || icmp

显示来源或目的IP地址为10.1.1.1的封包

ip.addr == 10.1.1.1

显示来源不为10.1.2.3或目的不为10.4.5.6的包

ip.src != 10.1.2.3 or ip.dst != 10.4.5.6

显示来源或目的TCP端口号为25的封包

tcp.port == 25

显示目的TCP端口号为25的封包

tcp.dstport == 25

显示包含TCP标志的封包

tcp.flags

显示包含TCP SYN标志的封包

tcp.flags.syn == 1

排除arp流量

!arp

文本管理流量(telnet或ftp)

tcp.port == 23 || tcp.port == 21

两种过滤器的区别

可以看出这两种过滤器在使用和功能上有着区别:

  • 捕捉过滤器是数据经过的第一层过滤器,它用于控制捕捉数据的数量,以避免产生过大的日志文件。而显示过滤器是在对捕获结果进行筛选时使用,允许我们在日志文件中迅速准确地找到所需要的记录。

  • 捕捉过滤器必须在捕捉数据包之前设置,若是捕捉的数据包不是我们想要的,需要重新设置捕捉过滤器并重新捕捉数据包。显示过滤器的功能比捕捉过滤器更为强大,可以重复设置表达式对捕获的结果重复筛选而不需要重新捕获数据包。

跟踪TCP流

处理TCP协议时,想查看TCP流中的应用层数据,”Following TCP streams”功能将会很有用。
如查看ftp流的命令控制或者数据控制信息。或者仅仅只需要一个显示过滤来显示某个TCP流的包。这些都可以通过Wireshark的”Following TCP streams”功能来实现。
Follow Tcp Stream会装入一个过滤来选择你已经选择的TCP流的所有包。

未分类

未分类

小结

对于Wireshark的使用推荐:

  • 官网的用户手册:https://www.wireshark.org/docs/wsug_html_chunked/

  • github上的一站式学习 Wireshark 中文全教程:https://github.com/justjavac/free-programming-books-zh_CN/issues/233

参考:https://openmaniak.com/cn/wireshark_filters.php

搭建 Keepalived + Nginx + Tomcat 的高可用负载均衡架构

1、概述

初期的互联网企业由于业务量较小,所以一般单机部署,实现单点访问即可满足业务的需求,这也是最简单的部署方式,但是随着业务的不断扩大,系统的访问量逐渐的上升,单机部署的模式已无法承载现有的业务量,需要进行服务集群化部署,本文主要介绍服务端 Tomcat 多实例部署,以及如何保证 web 服务的高可用方案。

  • Nginx 是一个高性能的 HTTP 反向代理服务器
  • Keepalived 是一个基于 VRRP 协议来实现的 LVS 服务高可用方案,可以利用其来避免服务的单点故障
  • Tomcat 是一个免费的开放源代码的 Web 应用服务器,属于轻量级应用服务器。

2、Nginx 的高可用负载均衡架构

如下图:为典型的 Tomcat 服务多实例部署的架构图

未分类

  1. 用户通过域名请求到 DNS,由 DNS 解析域名后返回对应的 IP 地址,该 IP 及为 Keepalived 映射服务器的虚拟 IP

  2. 通过该虚拟 IP 访问到对应的负载均衡器(Nginx),这里 Nginx 部署两个,然后通过 Keepalived 来保证 NG 的高可用,正常情况下由 Keepalived-M 将虚拟 IP 映射转发至 Nginx-M,如果 Nginx-M 出现故障,此时 Keepalived 会切换至 Keepalived-S 开始工作,从而保证了 NG 的单点故障问题。

  3. 通过 Nginx 负载均衡器,将请求路由到对应的 Tomcat 服务。

3、搭建 Keepalived + Nginx + Tomcat 的高可用负载均衡架构

3.1 需要准备的软件

(1)apache-tomcat-8.5.16.tar.gz

(2)nginx-1.12.2.tar.gz

(3)keepalived-1.3.9.tar.gz

3.2 服务器准备

两台服务器如:192.168.10.11,192.168.10.12

3.3 安装需要的依赖包

yum -y install gcc gcc-c++ automake pcre pcre-devel zlib zlib-devel open openssl-devel

3.4 安装

3.4.1 安装 Tomcat

(1)分别在两台服务器中安装 Tomcat,解压 apache-tomcat-8.5.16.tar.gz 及可完成安装。

3.4.2 安装 Nginx

(1)解压安装包:tar -zxvf nginx-1.12.2.tar.gz

(2)进入到 nginx-1.12.2 目录:cd nginx-1.12.2

(3)编译:

./configure --with-http_stub_status_module --with-http_ssl_module  --prefix=/usr/local/nginx
make && sudo make install

3.4.3 安装 Keepalived

(1)解压安装包:tar -zxvf keepalived-1.3.9.tar.gz

(2)进入到 keepalived-1.3.9 目录:cd keepalived-1.3.9

(3)执行编译:

./configure --prefix=/usr/local/keepalived --sysconf=/etc
make && sudo make install

3.5 配置

3.5.1 分别配置两台服务器的 Nginx

(1)分别修改两台服务器 nginx 配置文件,vi /usr/local/nginx/conf/nginx.conf

(2)内容如下:

#nginx进程数
worker_processes  1;

#单个进程最大连接数
events {
    worker_connections  1024;
}

#http服务器配置
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    #长连接超时时间,单位是秒
    keepalive_timeout  65;
    #upstream负载均衡配置,配置路由到tomcat的服务地址以及权重
    upstream localhost{
       server 192.168.10.11:8080 weight=2;
       server 192.168.10.12:8080 weight=2;
    }

    #虚拟主机的配置
    server {
        #监听端口
        listen       80;
         #域名可以有多个,用空格隔开
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
            #nginx跟后端服务器连接超时时间(代理连接超时)
            proxy_connect_timeout 3;
            #后端服务器数据回传时间(代理发送超时)
            proxy_send_timeout 30;
            #连接成功后,后端服务器响应时间(代理接收超时)
            proxy_read_timeout 30;
            proxy_pass http://localhost;
        }

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

3.5.2 主 Keepalived 配置

(1)修改 11 服务器的 keepalived 配置文件,vi /etc/keepalived/keepalived.conf

(2)内容如下:

! Configuration File for keepalived
#全局配置
global_defs {
   #keepalived切换的时候,发消息到指定的email,可配置多个email
   notification_email {
     [email protected]
     [email protected]
   }
   #通知邮件从哪个地址发出
   notification_email_from [email protected]
   #通知邮件的smtp地址
   smtp_server smtp.exmail.qq.com
   #连接smtp服务器的超时时间,单位秒
   smtp_connect_timeout 30
   #Keepalived的机器标识,一个网络内保持唯一
   router_id nginx-master
}

#执行脚本配置
vrrp_script chk_nginx {
    #脚本所在路径
    script "/home/project/keepalived/check_nginx.sh"
    #脚本执行间隔时间,秒
    interval 2
    #优先级
    weight 2
}
#keepalived实例配置
vrrp_instance VI_1 {
    #指定实例的初始状态,MASTER或BACKUP两种状态,并且需要大写
    state MASTER
    #实例绑定的网卡
    interface ens33
    #虚拟路由标识,是一个数字,整个VRRP内唯一,如果keepalived配置了主备,需要相同
    virtual_router_id 51
    #优先级,数值愈大,优先级越高
    priority 100
    #MASTER与BACKUP之间同步检查的时间间隔,单位为秒
    advert_int 1
    #通信验证
    authentication {
        auth_type PASS
        auth_pass feinik
    }
    #追踪外围脚本
    track_script {
        #这里配置vrrp_script的名称
        chk_nginx
    }
    #虚拟ip配置,可配置多个
    virtual_ipaddress {
        192.168.10.200
    }
}

3.5.3 备 Keepalived 配置

(1)修改 12 服务器的 keepalived 配置文件,vi /etc/keepalived/keepalived.conf

(2)内容如下:

! Configuration File for keepalived
#全局配置
global_defs {
   #keepalived切换的时候,发消息到指定的email,可配置多个email
   notification_email {
     [email protected]
     [email protected]
   }
   #通知邮件从哪个地址发出
   notification_email_from [email protected]
   #通知邮件的smtp地址
   smtp_server smtp.exmail.qq.com
   #连接smtp服务器的超时时间,单位秒
   smtp_connect_timeout 30
   #Keepalived的机器标识,一个网络内保持唯一
   router_id nginx-master
}

#执行脚本配置
vrrp_script chk_nginx {
    #脚本所在路径
    script "/home/project/keepalived/check_nginx.sh"
    #脚本执行间隔时间,秒
    interval 2
    #优先级
    weight 2
}
#keepalived实例配置
vrrp_instance VI_1 {
    #指定实例的初始状态,MASTER或BACKUP两种状态,并且需要大写
    state BACKUP
    #实例绑定的网卡
    interface ens33
    #虚拟路由标识,是一个数字,整个VRRP内唯一,如果keepalived配置了主备,需要相同
    virtual_router_id 51
    #优先级,数值愈大,优先级越高
    priority 99
    #MASTER与BACKUP之间同步检查的时间间隔,单位为秒
    advert_int 1
    #通信验证
    authentication {
        auth_type PASS
        auth_pass feinik
    }
    #追踪外围脚本
    track_script {
        #这里配置vrrp_script的名称
        chk_nginx
    }
    #虚拟ip配置,可配置多个
    virtual_ipaddress {
        192.168.10.200
    }
}

3.5.4 Nginx 状态检查脚本创建

(1)新建 Nginx 的状态检查脚本:check_nginx.sh

(2)内容如下:

#!/bin/sh
NGINX=/usr/common/nginx/sbin/nginx
PORT=80
nmap localhost -p $PORT | grep "$PORT/tcp open"
#echo $?
if [ $? -ne 0 ];then
    $NGINX -s stop
    #这里再次尝试启动NG
    $NGINX
    sleep 5
    nmap localhost -p $PORT | grep "$PORT/tcp open"
    [ $? -ne 0 ] && cd /usr/common/keepalived/sbin && pkill keepalived
    echo "stoped"
fi

4、运行测试

(1)为了更直观的查看到 keepalived 切换的效果,将 11 服务器中的 nginx 的 upstream 服务只配置 11 的 tomcat 服务地址,12 服务器中的 upstream 服务只配置 12 的 tomcat 服务地址,这样只需要观察将 11 服务器中的 nginx 关闭看使用虚拟 ip 是否可以访问到 12 服务器的 tomcat。

(2)分别启动两个服务器中的 tomcat、nginx、keepalived,访问虚拟 ip:192.168.10.200,可以查看到访问的是主 keepalived 服务器的 tomcat

未分类

3)关闭 11 服务器的 nginx,nginx -s stop,再次访问虚拟 ip,如下:说明主 keepalived 通过配置的脚本检测到了本服务的 nginx 服务挂掉了,所以立马切换至了备的 keepalived,这时 12 服务器的 keepalived 升为了主,所以就访问到了 12 服务器的 tomcat。

未分类

关于Tomcat中前台传值乱码的解决方式

在java SSM开发过程中,有时遇到前台传值到后台出现中文乱码,这是因为Tomcat默认西文编码,应更改编码,具体方法如下:

1、找到Tomcat文件夹下conf/server.xml

2、打开server.xml并找到以下内容

<Connector port="8888" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443"/>

3、在上段内容中添加

URIEncoding="UTF-8

变成:

<Connector port="8888" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" URIEncoding="UTF-8"/>

Tomcat生命周期

Lifecycle接口

Lifecycle接口统一管理Tomcat生命周期。一共做了4件事:

  • 定义13个string类型常量,用于LifecycleEvent时间的type属性中,用于区分组件发出的LifecycleEvent事件时的状态。

  • 定义三个管理监听器的方法,addLifecycleListener、findLifecycleListeners、removeLifecycleListener。

  • 定义4个生命周期的方法,init、start、stop、destory,用于执行生命周期的各个阶段的操作。

  • 定义了获取当前状态的两个方法,getState、getStateName、用于获取当前的状态。

public interface Lifecycle {
    // 13个状态常量值
    public static final String BEFORE_INIT_EVENT = "before_init";
    public static final String AFTER_INIT_EVENT = "after_init";
    public static final String START_EVENT = "start";
    public static final String BEFORE_START_EVENT = "before_start";
    public static final String AFTER_START_EVENT = "after_start";
    public static final String STOP_EVENT = "stop";
    public static final String BEFORE_STOP_EVENT = "before_stop";
    public static final String AFTER_STOP_EVENT = "after_stop";
    public static final String AFTER_DESTROY_EVENT = "after_destroy";
    public static final String BEFORE_DESTROY_EVENT = "before_destroy";
    public static final String PERIODIC_EVENT = "periodic";
    public static final String CONFIGURE_START_EVENT = "configure_start";
    public static final String CONFIGURE_STOP_EVENT = "configure_stop";
    // 3个监听器方法
    public void addLifecycleListener(LifecycleListener listener);
    public LifecycleListener[] findLifecycleListeners();
    public void removeLifecycleListener(LifecycleListener listener);
    // 4个生命周期方法
    public void init() throws LifecycleException;
    public void start() throws LifecycleException;
    public void stop() throws LifecycleException;
    public void destroy() throws LifecycleException;
    // 2个当前状态方法
    public LifecycleState getState();
    public String getStateName();

生命周期的状态转化

Tomcat中的事件触发是通过这些状态来判定的。

未分类

生命周期事件监听机制

事件监听器需要三个参与者:

  • 事件对象:用于封装事件的信息,在事件监听器接口的同一方法中作为参数使用,继承自java.util.EventObject类。
  • 事件源:触发事件的源头,不同事件源触发不同事件类型。
  • 事件监听器:负责监听事件源发出的事件。实现 java.util.EventListener 接口。

LifecyleBase类

LifecycleBase 类是Lifecycle 接口的默认实现,所有实现了生命周期的组件都直接或者间接的继承自LifecycleBase。

生命周期方法

@Override
public final synchronized void init() throws LifecycleException {
    // 通过设置不同的启动状态
    try {
        setStateInternal(LifecycleState.INITIALIZING, null, false);
        initInternal();
        setStateInternal(LifecycleState.INITIALIZED, null, false);
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        setStateInternal(LifecycleState.FAILED, null, false);
        throw new LifecycleException(
            sm.getString("lifecycleBase.initFail",toString()), t);
    }
}

// 不同状态时,触发不同事件
private synchronized void setStateInternal(LifecycleState state,
                                           Object data, boolean check) {
    this.state = state;
    String lifecycleEvent = state.getLifecycleEvent();
    if (lifecycleEvent != null) {
        fireLifecycleEvent(lifecycleEvent, data);
    }
}

// 调用注册的监听器的 lifecycleEvent 方法
protected void fireLifecycleEvent(String type, Object data) {
    LifecycleEvent event = new LifecycleEvent(this, type, data);
    for (LifecycleListener listener : lifecycleListeners) {
        listener.lifecycleEvent(event);
    }
}


@Override
public final synchronized void start() throws LifecycleException {
    // 验证生命周期状态
    if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
        LifecycleState.STARTED.equals(state)) {
        return;
    }

    // 不同生命周期执行不同方法
    if (state.equals(LifecycleState.NEW)) {
        init();
    } else if (state.equals(LifecycleState.FAILED)) {
        stop();
    } else if (!state.equals(LifecycleState.INITIALIZED) &&
               !state.equals(LifecycleState.STOPPED)) {
        invalidTransition(Lifecycle.BEFORE_START_EVENT);
    }

    // 设置生命周期状态 STARTING_PREP, 并调用 startInternal方法。
    try {
        setStateInternal(LifecycleState.STARTING_PREP, null, false);
        startInternal();
        if (state.equals(LifecycleState.FAILED)) {
            stop();
        } else if (!state.equals(LifecycleState.STARTING)) {
            invalidTransition(Lifecycle.AFTER_START_EVENT);
        } else {
            setStateInternal(LifecycleState.STARTED, null, false);
        }
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        setStateInternal(LifecycleState.FAILED, null, false);
    }
}

监听管理方法

// 由 standardContext#startInternal来注入监听器
private final List<LifecycleListener> lifecycleListeners = 
                    new CopyOnWriteArrayList<>();

@Override
public void addLifecycleListener(LifecycleListener listener) {
    lifecycleListeners.add(listener);
}

@Override
public LifecycleListener[] findLifecycleListeners() {
    return lifecycleListeners.toArray(new LifecycleListener[0]);
}

@Override
public void removeLifecycleListener(LifecycleListener listener) {
    lifecycleListeners.remove(listener);
}

状态管理方法

@Override
public LifecycleState getState() {
    return state;
}

@Override
public String getStateName() {
    return getState().toString();
}

监听机制

事件监听器需要三个参与者:

  • 事件对象—用于封装事件的信息,在事件监听器接口的统一方法中作为参数,一般继承 java.util.EventObjecct类。
  • 事件源—触发事件对的源头,不同事件源触发不同事件。
  • 事件监听器—负责监听事件源发出的事件,发生事件时,事件源调用事件监听器的统一方法处理。监听器一般实现java.util.EventListener接口。
public final class LifecycleEvent extends java.util.EventObject {
    public LifecycleEvent(Lifecycle lifecycle, String type, Object data) {
        super(lifecycle);
        this.type = type;
        this.data = data;
    }
}
public interface LifecycleListener {
    public void lifecycleEvent(LifecycleEvent event);
}
public class HostConfig implements LifecycleListener {
    @Override
    public void lifecycleEvent(LifecycleEvent event) {
        try {
            host = (Host) event.getLifecycle();
            if (host instanceof StandardHost) {
                setCopyXML(((StandardHost) host).isCopyXML());
                setDeployXML(((StandardHost) host).isDeployXML());
                setUnpackWARs(((StandardHost) host).isUnpackWARs());
                setContextClass(((StandardHost) host).getContextClass());
            }
        } catch (ClassCastException e) {
            return;
        }

        // Process the event that has occurred
        if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
            check();
        } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
            beforeStart();
        } else if (event.getType().equals(Lifecycle.START_EVENT)) {
            start();
        } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
            stop();
        }
    }
}
// LifecycleBase.startInternal
this.state = state;
String lifecycleEvent = state.getLifecycleEvent();
if (lifecycleEvent != null) {
    fireLifecycleEvent(lifecycleEvent, data);
}

protected void fireLifecycleEvent(String type, Object data) {
    LifecycleEvent event = new LifecycleEvent(this, type, data);
    for (LifecycleListener listener : lifecycleListeners) {
        listener.lifecycleEvent(event);
    }
}

Tomcat重复请求,log4j2日志重复显示问题排查

前提:

公司项目用的是nginx+tomcat+java,线上环境和线下环境版本配置都是一样,采用的是jenkins自动化部署。

问题:

前段时间突然发现浏览器访问一个页面,后端日志会出现重复的两条日志记录一模一样,请求时间都是一样的,经过测试发现所有的请求都会有两条重复的日志记录。如下图:

未分类

排查:

1、一开始认为是前端重复请求了,查看nginx访问日志,发现刷新一次页面的确nginx会出现两条请求记录,一次是OPTIONS请求,一次是GET请求,但是监控tomcat日志,发现刷新一次页面有4次请求,两次完全相同(包括时间)的OPTIONS和两次完全相同的GET请求。

2、排除前端页面问题以后,开始怀疑是nginx重复请求导致,于是直接通过IP+tomcat端口访问,发现还是会有完全一模一样的两条日志。然后通过IP+端口测试其他几个后端api,都会出现一样的问题。

3、排查nginx和前端以后,把问题锁定在了tomcat环境上面,于是开始一步一步排查:

a、对比了线上和线下的tomcat版本、java版本以及diff对比了线上和线下的实例配置文件,都是完全一样的。

b、百度搜索以后,有网友说重复请求问题是因为在tomcat的默认的server.xml里面,错误的配置了Host或者Context标签导致两个对象同时持有日志文件,导致重复加载。

①如果新增了Host,那Host/appBase的值是不能和其他Host/appBase的值一样的。比如appBase都是webapps,那么两个Host会导致webapps下面的工程被加载两遍。

②如果配置了Context标签那么就一定要配置name属性值为:工程名(/web-sys),或者配置path值为工程名(/web-sys),否则就会引起重复加载工程的问题。

③标签,允许我们把多个域名配置在一个Host下面,例如:xxx.com、xxx.cn、xxx.com.cn,都可以配置在一个Host下面

需要配置成下面这样:

<Host name="www.xxx.com" appBase="mywebapps" autoDeploy="true" unpackWARs="true">

<Alias>www.xxx.cn</Alias> 

<Alias>www.xxx.com.cn</Alias> 

<Context docBase="web-sys" path="" name="/web-sys"/>

</Host>

我对比了我的server.xml文件,于是添加了name属性,然后删除了默认的ROOT目录,进行了一番测试,结果还是会出现显示两条重复日志的问题。

4、经过上面的排查以后,开始有点迷茫,于是把线上的实例war包直接拷贝到了本地环境,然后修改了数据库信息,经测试还是会有同样的问题。此时瞬间又找到了方向,应该是war包封装过程中的问题。

5、于是去查看jenkins 线上和线下构建过程,发现配置文件有个redis参数不一样、log4j2.xml日志级别不一样,swagger开启状态不一样,然后马上把新建一个实例,把jenkins构建过程改成和本地测试环境一样,重新构建部署到生产环境,访问测试终于正常了。

6、这次终于找到了原因,于是依次对上面几个不同的参数一个一个修改发布测试,最后发现是因为log4j2.xml里面少了additivity=”false”这个属性导致。(一旦一个日志输出到一个Logger,这个Logger的additivity设置为false,那么这个日志不会再继续向父Logger进行传递,忽略其他Logger的additivity的设置。)

7、最后再去排查为何会少一个属性,才发现是jenkins构建过程中,使用sed命令替换时,使用了.*通配符去替换日志等级level=”DEBUG”,导致把additivity=”false”替换为了空。

 <Logger name="cn.amd5.community" level="DEBUG" additivity="false">

            <AppenderRef ref="Console"/>

            <AppenderRef ref="DailyLogFile"/>

</Logger>

加上additivity=”false”以后,重启实例,测试正常,如下图:

未分类