使用Linux工具分析g++生成的代码

近来想用实际代码实验来验证《Effective C++》、《深度探索C++对象模型》中的知识,通过反汇编等手段查看编译器生成的代码,原本想着看能不能设置好编译参数,使得编译器可以输出书本中的中间代码,可惜的是暂时还没找到,还一度以为只能通过强行分析汇编码。经过一周的摸索,总算弄出了一个可以接受的方案,这篇博客主要说明几个命令之间生成代码的信息。

环境支持

  • Linux
  • g++
  • gdb
  • nm
  • objdump
  • c++filt
  • readelf

其中g++本身就可以通过添加-S参数就可以生成汇编码了,nm命令用来查看生成二进制文件的函数表,objdump是查看更多关于二进制文件的信息,c++filt命令使用来demanglingreadelf用于读取ELF文件信息。

目标

《深度探索C++对象模型》中的构造语义章节中提到过,如果有一个Object类,而在声明一个实例的时候:

1
Object obj;

编译器可能会生成如下的中间代码:

1
2
Object obj;
Object::Object(&obj);

而生成的函数原型是:

1
void Object::Object(Object* const this);

此实验的目的就是验证编译器确实是生成了这样的中间代码。结论是目前只有gdb能做到,在呈现成果的结果前,也试试用别的命令看看能够尝试到什么样的结果。

x86_64汇编相关的准备知识

一旦开始做这类C++相关代码分析的工作,汇编是逃不开的,不过现在也基本不用写,会读就可以了。g++生成的汇编码是AT&T格式的,64位机的条件下,每个涉及到操作数的命令中后面会带有bwlq等字母,分别代表操作一个1Byte(8 bit)、2 Byte(16bit)、4 Byte(32bit)以及8 Byte(64bit)。例如:

  • movb %al, %bl代表把ax寄存器的低8bit赋值给bx寄存器的低8bit。
  • movw %ax, %bx代表把ax中16bit的值赋值给bx寄存器。
  • movl %eax, %ebx代表把eax中32bit的值赋值予ebx寄存器。
  • movq %rax, %rbx代表把rax中64bit的值赋予rbx寄存器。

x86_64有16个64bit寄存器,分别为%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。笔者所用的Linux下的g++编译器一般会这样划分寄存器的用途:

  • %rax 作为函数返回值使用。
  • %rsp 栈指针寄存器,指向栈顶。
  • %rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数…
  • %rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改。
  • %r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值。

实验用的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
class Object
{
public:
Object()
{}
private:
};

int main(int argc, char* argv[])
{
Object obj;
return 0;
}

这里需要定义Object::Object(),因为编译器在这里不会自动合成Object类的构造函数。如果连Object类是个空类,没有函数成员,也没有数据成员,编译器会为这个类生成怎样的代码?这个问题也在《深度探索C++对象模型》中提到过,有兴趣者可以通过搜索空基类优化关键字获得相关的知识,这里就不展开细说了。

nm命令

nm命令是用于列出目标文件中的符号,在这个实验中用作输出函数签名,查找看能不能输出期望的void Object::Object(Object * const)的函数签名。

首先使用

1
$ g++ main.cpp

生成目标文件a.out。随后:

1
$ nm a.out | c++filt

可以看到终端输出(经过处理):

1
2
3
0000000000400566 T main
0000000000400588 W Object::Object()
0000000000400588 W Object::Object()

信息还不少,可惜没有找到期待的void Object::Object(Object * const)

objdump

不改动上述的a.out文件,执行:

1
$ objdump -t a.out | c++filt

也是输出了不少信息(经过处理):

1
2
3
0000000000400566 g     F .text	0000000000000022              main
0000000000400588 w F .text 000000000000000b Object::Object()
0000000000400588 w F .text 000000000000000b Object::Object()

当前只关注能不能找到void Object::Object(Object * const),所以这里也不展开介绍这个命令输出的内容。

readelf

不改动上述的a.out文件,执行:

1
$ readelf -s a.out | c++filt

也是输出了不少信息(经过处理):

1
2
3
55: 0000000000400588    11 FUNC    WEAK   DEFAULT   11 Object::Object()
60: 0000000000400588 11 FUNC WEAK DEFAULT 11 Object::Object()
63: 0000000000400566 34 FUNC GLOBAL DEFAULT 11 main

也能获得不少关于符号表中的信息,可惜没有期待的void Object::Object(Object * const)

直接查看g++生成的汇编码

还是上述的C++代码,不过不是分析编译器生成目标文件,而是通过:

1
$ g++ -S main.cpp

生成汇编文件main.s,不过需要给这个文件进行一下demangling,不然没法看:

1
$ cat main.s | c++filt 

会输出(经过处理):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Object::Object():
pushq %rbp
movq %rsp, %rbp
movq %rdi, -8(%rbp)
nop
popq %rbp
ret

main:
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
leaq -1(%rbp), %rax
movq %rax, %rdi
call Object::Object()
movl $0, %eax
leave
ret

还是看不到《对象模型》中所说的void Object::Object(Object * const)函数签名。不过注意到Object::Object():中有一句:

1
movq	%rdi, -8(%rbp)

这句汇编的意思是把%rdi寄存器中的值赋予%rbp前移8个字节的内存地址中,即复制了64bit数据。
前面提到:

1
%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数...

那是不是有什么参数传入到了这个函数过程中了呢?不过我们现在暂时没有办法得知,需要后面提到的gdb帮助下才能验证。

强力工具gdb

本实验只用到gdb的一小部分功能,之前使用搜索引擎的时候,发现都可以用gdb调试多线程程序了,命令也挺简单,可以进入某一线程中进行调试。

回到本实验上,使用gdb前编译目标文件:

1
$ g++ -g main.cpp

添加-g是为了让生成的目标文件带有调试信息。

执行:

1
$ gdb a.out

进入调试。首先使用:

1
$ (gdb) info functions

会看到输出(经过处理):

1
2
3
File main.cpp:
void Object::Object();
int main(int, char**);

还是看不到我们想要的void Object::Object(Object * const),但是使用了万能的:

1
$ (gdb) print Object::Object

看到了输出:

1
$1 = {void (Object * const)} 0x400588 <Object::Object()>

这里说明了经过输出美化的Object::Object()真正的函数签名是void Object::Object(Object * const),这个实验的基本目的就达到了。

上面提到生成的汇编码中有暗示传入参数到Object::Object()的疑点,现在就用步进模式运行程序:

1
$ (gdb) start

然后键入s或者step一步一步执行,进入了Object::Object()函数执行内部:

1
2
Object::Object (this=0x7fffffffddcf) at main.cpp:5
5 {}

注意到这个this=0x7fffffffddcf,这个是什么变量的地址值?在这里执行:

1
$ (gdb) print &obj

可惜当前Object::Object()上下文不存在这个变量,只能键入s进行到下一步回到main()中执行,得到:

1
$2 = (Object *) 0x7fffffffddcf

证明了void Object::Object(Object * const)是需要传入一个Object * const参数作为执行的上下文的。到了这里,忘了在Object::Object()上下文中检查上面提到的寄存器们了,只能用s或者n执行完这次,然后再次start并进入到Object::Object()的上下文中了,此时执行:

1
$ (gdb) info registers

得到:

1
2
3
4
5
6
rax            0x7fffffffddcf	140737488346575

rdi 0x7fffffffddcf 140737488346575

rbp 0x7fffffffdda0 0x7fffffffdda0
rsp 0x7fffffffdda0 0x7fffffffdda0

好了,根据上面所说的rdi作为存储函数参数地址的寄存器,那么其中存储的0x7fffffffddcf&obj的输出值对上了,也证明了Object::Object()真正的函数签名是void Object::Object(Object * const)

证明完了之后,上面还特意贴出了rax寄存器中的值,也是0x7fffffffddcf,上面也提到:

1
%rax 作为函数返回值使用。

那么,最终Object::Object真正函数签名是Object* Object::Object(Object * const)呢?是的,然而这个函数是返回void,只要我们使用着这个编译器,我们便无法从其提供的语法层面获得这个返回值。

这个简单的实验,也从侧面说明了C++这个语言是有多复杂,编译器做了多少的小动作,不花点心思和用点工具,使用者只能看着编译出来的黑箱运行,用点printf或者cout看看是否正确运行。

至此,这个寻找类构造函数真正的签名的实验到此为止。

Apriori算法实现

数据挖掘作为当前比较热门的领域之一,已经有较为权威的书籍指导入门了,如陈封能著,范明译的《数据挖掘导论》以及力量黑书(机械工业出版社)出版的《数据挖掘概念与技术》。

在介绍完基本的数据挖掘概念之后,第一个动手写代码实现的算法就是Apriori算法了。Apriori算法是用于挖掘关联规则的频繁项集算法。

环境支持

Apriori中的剪枝步骤

在介绍伪代码前要介绍Apriori算法中的剪枝步骤。在产生K=1,...频繁项集的过程中,一共有个候选集,不用百万级的数据,光是n大于1000的时候都可以产生组合爆炸,更别说对产生的组合进行统计。所以Apriori算法在统计候选集之前先要把产生的K候选集作一个剪枝,删除不频繁的K候选集再开始统计。

具体的做法是检验该K项集的K-1子项集是否为频繁项集,如果该K项集的所有子项集都是频繁项集,那么该K项集才有可能是频繁项集。

假设有{A,B,C,D}这个全集,在K=3{A,B,C}{A,B,D}等组合(其它组合情况省略)。设频繁阈值为1,而K-1时存在{\{A,B},{A,C\}}项集而没有{A,D}项集,那么{A,B,D}肯定不是频繁项集,因而在下一轮统计前就可以删除了。

Apriori算法伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Input:
D: 事务数据库
min_sup: 最小支持度阈值
Ouput:
L,D中的频繁项集
Procedure:
L1=find_frequent_1_itemsets(D);
for(k=2; Lk-1!=EmptySet; k++)
{
Ck=apriori_gen(Lk-1);
for each transaction t in D // 扫描D,进行计数
{
Ct=subset(Ck,t); // 得到t的子集,候选集
for each candidate c in Ct
{
c.count++;
}
}
Lk={c(Ck|c.count>=min_sup)}
}
return L union_k Lk;

Procedure apriori_gen(Lk-1: frequent(k-1)itemset)
for each itemset l1 in Lk-1
{
for each itemset l2 in Lk-1
{
if(l1[1]=l2[1])and...and(l1[k-2]=l2[k-2])and(l1[k-1]<l2[k-1]) then
{
c=l1 × l2 (Cartesian product)
if has_infrequent_subset(c, Lk-1) then
delete c; // 删除非频繁的候选
else
add c to Ck;
}
}
}
return Ck;

Procedure has_infrequent_subset(c: candidate k itemset; Lk-1: frequent(k-1)itemset)
for each (k-1) subset s of c
{
if s not in Lk-1 then
return true;
}
return false;

简单介绍了基本概念以及算法之后,一般读者都会迷迷糊糊的,这很正常,毕竟这篇博客是用来讲实现了,伪代码也因为Markdown的代码块里面不允许加载特殊html语法写得难看。建议真的想搞懂的话,就去看上述的书,然后手解模拟一下过程,一般懂了之后,实现的问题就不大了。

如上述环境支持,在这里实现的时候用的是美国国会84年的投票记录,实际上是什么数据集并没有关系,无非就是数据预处理的过程不一样了而已。针对这个数据集,笔者实现了如下代码用于加载与处理一行记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def translate_record(record):
items = []

if record[0] == 'republican':

items.append('rep')

elif record[0] == 'democrat':

items.append('demo')

if record[1] == 'y':

items.append('hci') # handicapped-infants

elif record[1] == 'n':

items.append('hci-n')

if record[2] == 'y':

items.append('wpcs') # water-project-cost-sharing

elif record[2] == 'n':

items.append('wpcs-n')

if record[3] == 'y':
items.append('aotbr') # adoption-of-the-budget-resolution

elif record[3] == 'n':
items.append('aotbr-n')

if record[4] == 'y':

items.append('pff') # physician-fee-freeze

elif record[4] == 'n':

items.append('pff-n')

if record[5] == 'y':

items.append('esa') # el-salvador-aid

elif record[5] == 'n':

items.append('esa-n')

if record[6] == 'y':
items.append('rgis') # religious-groups-in-schools

elif record[6] == 'n':
items.append('rgis-n')

if record[7] == 'y':

items.append('astb') # anti-satellite-test-ban

elif record[7] == 'n':

items.append('astb-n')

if record[8] == 'y':

items.append('atnc') # aid-to-nicaraguan-contras

elif record[8] == 'n':

items.append('atnc-n')

if record[9] == 'y':

items.append('mxm') # mx-missile

elif record[9] == 'n':

items.append('mxm-n')

if record[10] == 'y':

items.append('imm') # immigration

elif record[10] == 'n':

items.append('imm-n')

if record[11] == 'y':

items.append('scc') # synfuels-corporation-cutback

elif record[11] == 'n':

items.append('scc-n')

if record[12] == 'y':

items.append('es') # education-spending

elif record[12] == 'n':

items.append('es-n')

if record[13] == 'y':

items.append('srts') # superfund-right-to-sue

elif record[13] == 'n':

items.append('srts-n')

if record[14] == 'y':

items.append('cri') # crime

elif record[14] == 'n':

items.append('cri-n')

if record[15] == 'y':
items.append('dfe') # duty-free-exports

elif record[15] == 'n':
items.append('dfe-n')

if record[16] == 'y':
items.append('eaasa') # export-administration-act-south-africa

elif record[16] == 'n':
items.append('eaasa-n')

return items

def load_data(file_path):
src_data = open(file_path)

votes_records = []

for line in src_data:
votes_records.append(translate_record(line.strip('\n').split(',')))

src_data.close()

return votes_records

其中,load_data用于读入数据集并返回一个处理好的数据列表。

然后就是伪代码第一行,就是产生1项集,并统计1频繁项集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def find_frequent_1_itemsets(vote_records, min_sup=0):
itemsets = []

for record in vote_records:

for element in record:

if [element] not in itemsets:
itemsets.append([element])

itemsets = map(frozenset, itemsets) # 使用frozenset使得集合可以作为dict中的key

itemsets_with_count = {}

for candidate in itemsets: # 产生候选项

for record in vote_records:

if candidate.issubset(record): # 如果该候选是记录中的子集,增加计数值

if candidate in itemsets_with_count:

itemsets_with_count[candidate] += 1

else:

itemsets_with_count[candidate] = 1

qualified_itemsets = {}

for key, val in itemsets_with_count.items(): # 筛选候选项

if val >= min_sup:

qualified_itemsets[key] = val

return qualified_itemsets

注意,为了能够用dict来统计频繁项集的个数,其中的key的类型是一个集合,而一般的集合是不能来当作key的,幸好python提供了frozenset使得集合作为dictkey。不过这样的实现也有很大的性能缺陷,频繁地在普通的集合以及frozenset中转化,深度复制,性能损失可想而知。

当时实现的时候一个优化的想法就是用一个4Byte的数据结构来表示一个集合,其中的01分别代表yesno,然而数据还存在第三个状态,用扩展位来解决这个问题,可以通过拼凑的方法使得这样的数据结构可以兼容任意个选项。这样的方法通用性不好,加上这里是第一次实现,所以就还是采取frozenset这个平凡的办法来处理了。

然后在K>=2开始,需要用到的has_infrequent_set,用于检测该候选项是否存在非频繁子项集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def has_infrequent_set(un_set, frozen_set):

unfrozen_un = set(un_set)

for ele in unfrozen_un:

sub_k_1_set = unfrozen_un.copy() # 复制该候选项,并一次迭代中删除一个元素,产生子集

sub_k_1_set.remove(ele)

sub_k_1_set = frozenset(sub_k_1_set) # frozen化使之能够成为索引

if sub_k_1_set not in frozen_set: # 如果子集不在频繁项集中,则为非频繁项集

return True

return False

这里就体现出了普通的集合和frozenset的频繁转化了。

接下来是产生K(K>1)项集的apriori_gen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def apriori_gen(lk):

frozen_set = lk.keys() # 取出K-1项集

if len(frozen_set) <= 0:

return []

for ele in frozen_set: set_size = len(ele) + 1; break # 设定K的值

gen_set = []

for ele1 in frozen_set:

for ele2 in frozen_set:

if not (ele1.issubset(ele2) and ele1.issuperset(ele2)): # 如果不是同一个集合

un_set = ele1.union(ele2) # 取并集

if len(un_set) == set_size and un_set not in gen_set and not has_infrequent_set(un_set, frozen_set):
# 测试并集是否为K项集并且还未被之前的过程产生,而且没有非频繁K-1子集
gen_set.append(un_set)

return gen_set

准备好Apriori用到的主要过程之后,就是Apriori算法的主体了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def apriori(record_set, min_sup):

l1 = find_frequent_1_itemsets(record_set, min_sup) # 产生1项集

L = [l1]

k = 1

while len(L[k - 1]) > 0: # 只要K频繁项集还有,算法就不结束

ck = apriori_gen(L[k - 1]) # 从K-1项集中产生K项

count_set = {}

verified_count_set = {}

for candidate in ck: # 统计产生的K项集的支持度

count_set[candidate] = 0

for record in record_set:

if candidate.issubset(record):

count_set[candidate] += 1

for key, val in count_set.items(): # 统计符合要求的K频繁项

if val >= min_sup:

verified_count_set[key] = val

L.append(verified_count_set.copy()) # 加入到结果中,准备下一轮迭代

count_set.clear()

verified_count_set.clear()

k += 1

return L

经典的Apriori算法的主体代码实现就到这里就结束了。

算法实现并不难,难得是弄懂这个过程,所以还是看书,自己手动模拟过程更快一些。

diff命令学习笔记

只要一直使用着Linux作为主要的工作操作系统,并且经常码代码的话,总有一天会用到diff命令,前面笔者太依赖于git环境了,以致于疏忽了学习这个命令,这段时间又用到了,于是记下来。

读懂diff输出含义

在不加参数,直接对比两个文件的情况下,输出中有三种结果:

  1. a - add,即第二个文件比第一个文件多了。
  2. d - delete,即第二个文件比地一个文件少了。
  3. c - change,即第二个文件同行位置下发生了修改。

以下举例进行说明:
首先建立一个file1.txt:

1
2
3
4
5
6
7
aa
bb
cc
dd
ee
ff
gg

随后建立一个file2.txt:

1
2
3
4
5
6
7
8
aa
bb
cc
zz
dd
ee
ff
gg

使用命令:

1
$ diff file1.txt file2.txt

可以看到输出:

1
2
3a4
> zz

第一行3a4表示变化在第一个文件的第3行,变化的模式是增加(a)的内容,而a后面的4表示变动的内容发生在第二个文件的第4行,增加了>内容zz

换一个理解方法,3a4中间的a表示了模式,而过了第一个文件第3行以及第二个文件的第4行之后,内容又开始匹配了起来。

修改file2.txt:

1
2
3
4
5
6
7
aa
bb
zz
dd
ee
ff
gg

这时候也执行相同的命令,得到:

1
2
3
4
3c3
< cc
---
> zz

第一行3c3表示内容发生在第一个文件的第3行,变化的模式是更改(c)内容,c后面的3表示变化的内容在第二个文件的第3行,细节是分为两部分,---虚线上方是原来的文件变化,原来的cc出去了(<),而zz进来了(>)。

同样换一个理解方法,3c3中间的c表示了模式,而过了第一个文件第3行以及第二个文件的第3行之后,内容又开始匹配了起来。

再次修改file2.txt:

1
2
3
4
5
6
aa
bb
dd
ee
ff
gg

执行相同的diff命令:

1
2
3d2
< cc

第一行3d2表示内容发生在第一个文件的第3行,变化的模式是删除(d)内容,d后面的3表示变化的内容在第二个文件的第3行,原来的cc被删除了(<)。

同样,3d2中间的d表示了模式,而过了第一个文件第3行以及第二个文件的第2行之后,内容又开始匹配了起来。

并排输出模式

并排输出模式提供了对比源文件同行时的情况。

file1.txt内容:

1
2
3
4
5
6
7
8
aa
bb
cc
dd
ee
ff
gg
hh

file2.txt内容:

1
2
3
4
5
6
7
8
9
10
aa
bb
cc
66
99
99
dd
ez
ff
gg

执行命令:

1
$ diff file1.txt file2.txt -y

得到结果:

1
2
3
4
5
6
7
8
9
10
11
aa								aa
bb bb
cc cc
> 66
> 99
> 99
dd dd
ee | ez
ff ff
gg gg
hh <

这个模式下的输出就比较直观了,可以看到4-6行file1.txt由不存在变成file2.txt新增的三行,而第8行发生了改变,第12行的内容被删除了。

上下文模式

还是上述的file1.txtfile2.txt,执行:

1
$ diff -c file1.txt file2.txt

得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
*** file1.txt	2017-11-18 10:27:50.036977294 +0800
--- file2.txt 2017-11-18 10:27:58.026110636 +0800
***************
*** 1,8 ****
aa
bb
cc
dd
! ee
ff
gg
- hh
--- 1,10 ----
aa
bb
cc
+ 66
+ 99
+ 99
dd
! ez
ff
gg

这个-c就是指定上下文context的参数,这里由星号包围的数字表示原文件中的1到8行的内容显示,具体有第5行更改()以及第8行的删除(-)动作;然后由减号包围的数字表示变动内容发生在第二个文件的1到10行中,具体是新增了4到6行(+)以及第8行的更改()动作。

统一模式

执行:

1
$ diff -u file1.txt file2.txt

得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--- file1.txt	2017-11-18 10:27:50.036977294 +0800
+++ file2.txt 2017-11-18 10:27:58.026110636 +0800
@@ -1,8 +1,10 @@
aa
bb
cc
+66
+99
+99
dd
-ee
+ez
ff
gg
-hh

统一模式是把上下文模式分开显示前后文件合并在了一起,@@中的-1,8表示第一个文件中的1到8行,+1,10表示第二个文件的1到10行,随后显示具体的变更内容,注意-ee行和+ez行是更改操作。

diff命令也可以用于比较文件夹的不同。

产生补丁以及打补丁

使用命令:

1
$ diff -ruN file1.txt file2.txt > patch1_2.patch

产生补丁文件,随后用cat命令可以查看补丁的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--- file1.txt	2017-11-18 10:27:50.036977294 +0800
+++ file2.txt 2017-11-18 10:27:58.026110636 +0800
@@ -1,8 +1,10 @@
aa
bb
cc
+66
+99
+99
dd
-ee
+ez
ff
gg
-hh

完成对file1.txt的打补丁操作:

1
$ patch file1.txt patch1_2.patch

file1.txt的内容就会在打了补丁之后与file2.txt一致。

以上是diff命令的简单介绍以及打补丁操作。

JavaFX fxml 快速搭建入门

之前一直使用命令行或者Web技术当作用户交互界面的方式,而且刚开始学习编程的时候前人的经验都挺排斥图形界面编程的,以致于自己除了用Web页面方式之外,再也没有别的图形化编程经验。因为最近有些需要,语言要求是Java,在网上浏览之后,决定采用JavaFX而不是Swing或者awt。事实上,也有可能因为笔者Web项目的经验,而这些框架是基于MVC思想的,所以上手很快,就是得花点时间熟悉提供的工具特性。

环境支持

  1. JDK 1.8

笔者用的IDE是JetBrain IDEA Ultimate 2017.1,所以教程也是依据这个IDE提供的功能进行。

创建项目

打开IDEA,创建项目,在如图左侧选择JavaFX,一直下一步,项目名一如既往为test:

然后IDEA就会根据模板给我们创建好项目:

其中,src源码目录下会有一个sample的子目录,这个子目录在将来的工程开发即为包的名字,这个细节是后面的事了。sample目录下有三个文件:

  1. Controller.java,上面提到过,这些框架是基于MVC思想的,自然而然这个类就是项目的控制器了。当然也跟一般的web项目区别不大, JavaFX项目可以存在多个控制器。但是因为笔者不清楚JavaFX下是否有类似Spring这类IOC框架,所以控制器的实例化就需要手动操作了。
  2. Main.java,因为不是web项目,没有容器,所以JavaFX与一般项目一样,需要一个程序的入口,而这里的Main类就承担了这个责任。而且当存在启动参数设置时,尤其是需要传递到应用中,Main类则又承担了应用参数初始化的责任,这些系列将在后面演示。
  3. sample.fxml,这个就是GUI布局的文件,跟html有一些相似,却又不同,JavaFX指出两种建立GUI的方式,一种是在代码里面用代码的方式初始化GUI的布局,另一种就是用fxml的文件初始化布局。

FXML界面布局

这里介绍的是用fxml方式初始化界面布局,所谓的与html不同,是因为html提供了一些很直观的方式,例如当你放置一个tr在一个tr下面的时候,在没有css文件针对布局的时候,呈现出来的tr就一定是在tr下面,即编码的布局与显示的布局绑定了。在IDEA模板中,fxml里面的第一层是一个GridPane,将图形界面分为一个一个的格子。IDEA初始化的sample.fxml内容:

1
2
3
4
5
6
7
8
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.GridPane?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<GridPane fx:controller="sample.Controller"
xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
</GridPane>

既然是快速入门,先运行一遍项目,可以看到一个空白的窗口:

开始修改图形界面,以加入一个TextField为例,试试简单地在GridPane里面添加一句TextField:

1
2
3
4
5
6
7
8
9
10
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.GridPane?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<GridPane fx:controller="sample.Controller"
xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
<TextField/>
</GridPane>

运行得到:

似乎没有什么问题,接下来尝试在这个TextField的左边加个显示内容为Input:Label以及一个Button,注意这时候就需要给这个GridPane内的元素添加GridPane.rowIndexGridPane.columnIndex属性来确定元素在里面的位置了,不然笔者猜测可能自动将添加的元素默认为GridPane.rowIndex=0GridPane.columnIndex=0然后按照出现的顺序将前面的元素覆盖了。修改后的sample.fxml:

1
2
3
4
5
6
7
8
9
10
11
12
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.GridPane?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<GridPane fx:controller="sample.Controller"
xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
<Label text="Input:" GridPane.rowIndex="0" GridPane.columnIndex="0"/>
<TextField GridPane.rowIndex="0" GridPane.columnIndex="1"/>
<Button GridPane.rowIndex="1" GridPane.columnIndex="1" text="Button"/>
</GridPane>

然后就可以得到一个简易的界面:

由于本文目的是快速构建一个GUI,例如想将两个元素放置入一个GridPane中(使用VBox)等涉及的更多的布局需求情况暂不在此展开讨论。

获取图形界面元素中的值

前面的步骤也只是建立了一个简单的图形界面,但是程序的后台无法获得图形中的界面发生的事件以及元素中的值。这一步将介绍如何获取窗口中Button的点击事件,以及获得TextField中的值。

首先打开Controller.java,IDEA给我们创建了空的内容:

1
2
3
4
package sample;

public class Controller {
}

因为我们是想获取TextField的内容的值,以及Button的点击事件,将代码修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package sample;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;

public class Controller {

@FXML
private TextField textField;

@FXML
public void handleButtonAction(ActionEvent actionEvent)
{
System.out.println(textField.getText());
}
}

有的教程里面会提示Controller需要实现javafx.fxml.Initializable接口才能正常使用Controller,因为在本过程中没有出现该情况,因而简做提示而跳过。

注意textField这个变量名,以及在Controller中成员以及函数添加的@FXML注解,都是为了让他们能被感知到。接着修改sample.fxml:

1
2
3
4
5
6
7
8
9
10
11
12
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.GridPane?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<GridPane fx:controller="sample.Controller"
xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
<Label text="Input:" GridPane.rowIndex="0" GridPane.columnIndex="0"/>
<TextField GridPane.rowIndex="0" GridPane.columnIndex="1" fx:id="textField"/>
<Button GridPane.rowIndex="1" GridPane.columnIndex="1" text="Button" onAction="#handleButtonAction"/>
</GridPane>

注意到GridPane被IDEA模板创建的时候带有了一个fx:controller="sample.Controller",即该界面与Controller绑定,然后在TextField添加的fx:id="textField"即代表这个TextFieldController中的textField成员绑定。而Button则添加了一个onAction="#handleButtonAction"代表这个Button在发生点击事件的时候调用Controller中的handleButtonAction方法。

运行程序,在输入框中输入一些测试用的数据:

然后查看控制台输出:

证明Controller的成员成功地与图形界面中的元素成功对应。

设置传递非界面参数予Controller

前面介绍的基础内容已经足够一般的交互情况了,但是之前笔者涉及到读取程序启动参数并传入到Controller中,而且这个也可能涉及到程序的全局变量初始化问题,因而介绍一下。

假设我们需要设置Controller.textField成员初始显示的值,而且是从程序启动参数中获取,我们在Controller中添加一个String fromCmd并设置一个对应的setter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package sample;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;

public class Controller {

@FXML
private TextField textField;

private String fromCmd;

@FXML
public void handleButtonAction(ActionEvent actionEvent)
{
System.out.println(textField.getText());
}

public void setFromCmd(String fromCmd) {
this.fromCmd = fromCmd;

textField.setText(fromCmd);
}
}

注意在此类类实例不知道什么时候初始化的情况里,不要用构造函数的方法初始化一些变量,之前尝试了初始化TableView并且在初始化中设置一些值,然而因为不清楚复杂的构造过程,无法成功构建。

原本的Main.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package sample;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

@Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}


public static void main(String[] args) {
launch(args);
}
}

修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package sample;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

private static String[] appArgs;

@Override
public void start(Stage primaryStage) throws Exception{

FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("sample.fxml"));

Parent root = fxmlLoader.load();

Controller controller = fxmlLoader.getController();

if(appArgs.length > 0)
{
controller.setFromCmd(appArgs[0]);
}
else
{
controller.setFromCmd("default");
}

primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}


public static void main(String[] args) {

appArgs = args;

launch(args);
}
}

这样的话我们就可以获取到程序启动参数并输入到Controller当中,当使用者提供启动参数的时候则设置到Controller.textField中,否则设置其值为default

运行结果,不输入参数:

在IDEA中设置程序启动参数:

加入参数后的运行结果:

从前面的Main.java获取Controller的过程来看,Controller的初始化是交给了FXMLLoader的,我们无法得知其初始化的时机,而且也之能够从FXMLLoader获取得帮定的控制器。

至此,一个基于fxml的快速构建应用介绍完毕。

RHEL5.4下配置Tomcat+Apache

笔者一直为一个项目进行后期开发以及维护,因为基础服务提供者的质量太差,导致最近作为服务器的虚拟机又崩溃了。而且崩溃最严重的地方是虚拟存储服务,以致于现今需要在一台新的虚拟服务器上重新部署项目。因为上一年也经历过该情况,所以将部署过程写成博客,以防再次发生时没有部署文档参考。

环境支持

  1. RHEL 5.4
  2. gcc
  3. ssh,Windows下可选择XShell。

所需工具

  1. MySQL,笔者部署用祖传的MySQL-5.6.25-1.rhel5.x86_64.rpm-bundle.tar
  2. JDK-1.7.0_21
  3. Apache Tomcat 7.0.62
  4. Apache 2.2.34
  5. Apache Active MQ 5.7.0
  6. Tomcat Connector - JK-1.2.42

工具说明

因为这是一个实际部署的项目,所以用的工具都是一直传下来的,不能随意更换,不然很有可能导致服务器无法正常运行项目。例如笔者最近部署贪方便,使用了JDK1.8编译项目,就算能够通过设置参数让JDK1.8编译等级为1.7,但是始终无法编译成功。以下针对使用的工具进行简单说明:

  1. 这些工具版本最好是固定这个组合,别的版本组合不能确保项目能够成功部署运行,有些工具可能太老了,已经不提供官方下载了,只能通过别的方法获取,或者尝试使用近似的版本。
  2. 笔者也想全程使用ssh远程控制服务器,但是因为是通过堡垒机登录的服务器,所以涉及文件传输的时候使用scp等命令是无法正常工作的,而祖传下来的方法是在Windows下使用X-Shell登录服务器,而在服务器使用rz -be命令接收文件,sz ${文件路径}将文件发送到本地机器上。
  3. MySQL,JDK,Tomcat,Apache是常用的工具,这里不加以说明。
  4. Apache Active MQ,百度上是介绍是用来集群进行消息传递的,但是在该项目的主要提供邮件发送服务。
  5. Tomcat Connector,用于提供Apache的负载均衡时,Apache与Tomcat的连接。

初始准备

笔者将所需的工具先传输到了/home/trans/recv目录下,其中包含打包好的项目,名为Demo.war
虚拟服务器上只有一个root账户,注意操作全程是在root权限下进行。

安装MySQL

在工具所在的目录下,执行:

1
2
3
$ mkdir mysql-bundle
$ tar xvf MySQL-5.6.25-1.rhel5.x86_64.rpm-bundle.tar -C ./mysql-bundle
$ cd mysql-bundle

解压文件并进入目录。

执行命令:

1
2
$ rpm -ivh MySQL-server-5.6.25-1.rhel5.x86_64.rpm --nodeps --force
$ rpm -ivh MySQL-client-5.6.25-1.rhel5.x86_64.rpm --nodeps --force

安装MySQL服务器以及客户端,里面加了--force参数是为了跳过GPG keys检测,不然无法成功安装。

该版本的MySQL安装之后,root的默认密码不为空,执行:

1
$ cat ~/.mysql_secret

查看文本,获取初始密码。

设置MySQL root账户的初始密码:

1
$ mysqladmin -u root -p password ${新密码}

之后会询问之前的密码,输入在.mysql_secret看到的密码即可修改成功。

进入mysql建立项目所需的数据库和表,注意建库的时候使用:

1
$ CREATE DATABASE '${库名}' DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

指定库的编码为UTF-8,或者根据项目需要更改编码。

至此,MySQL的安装以及初始配置完成。

安装JDK

卸载系统自带的open JDK:

1
$ rpm -qa | grep gcj

看到的一行同时具有javagcj字样的,就是卸载目标使用yum remove命令卸载即可。
系统中也有可能存在其他的jdk,同样卸载即可。

展开JDK压缩包,并指定到:

1
$ tar zxvf jdk-7u21-linux-x64.gz -C /usr/local/Java/

添加Java的系统变量:

1
$ vim /etc/profile

添加到文件尾:

1
2
3
export JAVA_HOME=/usr/local/Java/jdk1.7.0_21
export CLASSPATH=.:$CLASSPATH:$JAVA_HOME/lib:$JAVA_HOME/jre/lib
export PATH=$PATH:$JAVA_HOME/bin:$JAVA_HOME/jre/bin

可以:

1
$ source /etc/profile

更新一下系统变量。

安装Apache

因为是一个实际部署的站点,故首先需要对本机的路由进行一些修改:

1
$ vim etc/hosts

内容修改为:

1
2
3
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
127.0.0.1 ${站点名}

后推出编辑器。

展开压缩包并进入目录:

1
2
$ tar zxvf httpd-2.2.34.tar.gz
$ cd httpd-2.2.34

创建安装目录并使用命令安装:

1
2
3
$ mkdir /usr/local/apache
$ ./configure--prefix=/usr/local/apache --enable-so --enable-proxy --enable-proxy_http=shared --enable-module=so --enable-mods-shared=all --enable-proxy-ajp=shared --enable-proxy-balancer -with-mpm=worker
$ make && make install

进入漫长的等待编译过程,中间可能会碰上aprapr-utilpcre缺失,在这该虚拟机提供的环境暂不缺失,故在这里不展开细说。

安装完毕后开始配置Apache。

1
2
$ cd /usr/local/apache/conf
$ vim httpd.conf

首先修改ServerName行为:

1
ServerName ${站点名}:${端口号}

确定DocumentRoot为:

1
DocumentRoot "/usr/local/apache/htdocs"

将以下:

1
2
3
Include conf/extra/httpd-mpm.conf
Include conf/extra/httpd-vhosts.conf
Include conf/extra/httpd-default.conf

的注释去除,启用这些配置。

修改extra/httpd-vhost.conf:

1
$ vim extra/httpd-vhost.conf

内容为:

1
2
3
4
5
6
7
8
9
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs"
ServerName ${站点名}
<Directory "/usr/local/apache/htdocs">
Options Indexes FollowSymLinks
AllowOverride None
</Directory>
</VirtualHost>

保存退出。

之后可以使用:

1
$ /usr/local/apache/bin/apachectl start

启动httpd,在别的机器上访问该站点,可以看到返回的网页,其中有字符串类似于”Hello world!”,证明站点上的apache部署成功。
使用命令:

1
$ /usr/local/apache/bin/apachectl stop

停止apache服务,进行Tomcat的安装配置。

安装Tomcat

回到存放工具压缩包的目录,执行:

1
2
$ tar zxvf apache-tomcat-7.0.62.tar.gz -C /opt/
$ cd /opt/apache-tomcat-7.0.62/

解压并开始配置tomcat。
修改conf/catalina.sh,在文件配置信息开始段添加:

1
2
3
4
5
JAVA_OPTS="-Xms1024m -Xmx4096m -XX:PermSize=256m -XX:MaxPermSize=512m"
export TOMCAT_HOME=/opt/apache-tomcat-7.0.62
export CATALINA_HOME=/opt/apache-tomcat-7.0.62
export JRE_HOME=/usr/local/Java/jdk1.7.0_21/jre
export JAVA_HOME=/usr/local/Java/jdk1.7.0_21

一个是告知tomcat,该系统的jdk的位置;第二个功能,是配置JVM的启动参数,上一年重新配置该服务器的时候,一切配置正常,从外部访问该站点却死活返回不了完整的页面,经过两三天的折腾,终于发现是tomcat启动JVM的默认内存太小,以至于部署项目以及初始化tomcat过程虚拟机的内存就爆满了,不断地触发GC,使用top命令可以看到虚拟机的CPU占用率极高,当时也没有HTTP访问。刚开始从tomcat的log里面找不到明显的异常记录,就把解决问题的方向指向了apache整合tomcat中去,配置JK模块修改了很多个版本都不行。最后还是老师兄厉害,整个部署错误就在tomcat的log的一句Memory Exceeded里面,而不是常见的一大段的异常报告文本段。
今年这一次重新部署也类似,不过有了一年的经验,找出了单句严重错误日志,是tomcat在初始化一个监听器的时候缺少了这个类的实现,还有零散的错误,也是之前提到了这个是我用JDK1.8编译项目出的问题,用祖传的JDK1.7-21编译就没问题了。

一般的教程到这里会让启动tomcat下的bin/startup.sh,然后通过本地浏览器参看tomcat是否成功配置,但是我们处于没有图形化界面的服务器中,所以这里笔者假定tomcat是能够正常启动的,或者可以先把apache的根目录设置为/opt/apache-tomcat-7.0.62/webapps,之后从能够使用浏览器的外部机器访问站点,如果能够访问到tomcat的主页的话就没问题了。

将要发布的项目的war复制到tomcat下,执行:

1
$ cp /home/trans/recv/Demo.war /opt/apache-tomcat-7.0.62/webapps/

修改conf/server.xml:

1
$ vim conf/server.xml

其中有将port值为8080以及8009Connector配置段修改为:

1
2
3
 <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" useBodyEncodingForURI="true" URIEncoding="UTF-8"/>

<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" useBodyEncodingForURI="true" URIEncoding="UTF-8" protocolHandlerClassName="org.apache.jk.server.jkCoyoteHandler"/>

因为Apache整合tomcat中用的JK模块是用AJP进行通信的,所以8009端口配置要正确,AJP的版本要和稍后配置的workers.properties中的版本一致,这里采用的1.3
继续修改server.xml中的Engine:

1
<Engine name="Catalina" defaultHost="localhost" jvmRoute="tomcat1">

这里要注意的是jvmRoute的值,这里需要稍后配置的workers.properties中的worker保持一致,否则JK模块无法正常找到该tomcat完成整合。
继续修改Host段:

1
2
3
<Host name="localhost" appBase="/usr/local/apache/htdocs/Demo" autoDeploy="true" unpackWARS="true" xmlNamespaceAware="false" xmlValidation="false">
<Context path="/" docBase="/opt/apache-tomcat-7.0.62/webapps/Demo.war" reloadable="true" debug="0" crossContext="true"/>
</Host>

这里配置了项目发布到apache中的本地位置,以及项目的存放位置等配置。

其实应该还有集群时的Cluster段的配置,当年老师兄传给我的时候说这段是不需要配的,因为项目根本不是分布式的,虽然apache那边是配置好的,但是机器只有一台,等以后有机会再配置集群的Session问题也不迟。这个项目都已经老年时期了,所以是没可能的了,所以需要配置集群的看官抱歉了,这里没有,只能自行百度了。

至此,tomcat的配置完成。

编译安装JK模块到Apache中

回到工具包的存放目录,执行:

1
2
$ tar zxvf tomcat-connectors-1.2.42-src.tar.gz
$ cd tomcat-connectors-1.2.42-src/native

解压tomcat connector并进入目录。
编译JK模块:

1
2
3
4
$ chmod 755 buildconf.sh
$ ./buildconf.sh
$ ./configure --with-apxs=/usr/local/apache/bin/apxs
$ make && make install

耐心等待过程的完成,完成后apache的modules目录下会出现一个mod_jk.so的动态库。

Apache整合Tomcat

继续打开:

1
$ vim /usr/local/apache/conf/httpd.conf

Include段添加一句:

1
Include conf/mod_jk.conf

conf目录下新建一个mod_jk.conf文件,添加如下内容:

1
2
LoadModule jk_module modules/mod_jk.so
JkWorkersFile conf/workers.properties

conf目录下新建一个workers.properties文件,添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
workers.java_home=/usr/local/Java/jdk1.7.0_21
ps=/

worker.list=controller,tomcat1

#Set properties for tomcat1
worker.tomcat1.type=ajp13
worker.tomcat1.host=localhost
worker.tomcat1.port=8009
worker.tomcat1.lbfactor=50
worker.tomcat1.cachesize=10
worker.tomcat1.cache_timeout=600
worker.tomcat1.socket_keepalive=1
worker.tomcat1.socket_timeout=300

worker.controller.type=lb
worker.controller.balance_workers=tomcat1
worker.controller.sticky_session=false

随后修改conf/extra/httpd-vhosts.conf:

1
2
3
4
5
6
7
8
9
10
<VirtualHost *:80>
DocumentRoot "/usr/local/apache/htdocs/Demo"
ServerName ${站点名}
<Directory "/usr/local/apache/htdocs/Demo">
Options Indexes FollowSymLinks
AllowOverride None
</Directory>
JkMount /* tomcat1
JkMount /*.* tomcat1
</VirtualHost>

至此,配置已经给好了,剩下的就是启动测试了。

设置tomcat和apache为系统服务

创建/etc/init.d/tomcat,添加内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# !/bin/sh
# tomcat
# chkconfig: 345 63 37
# description: tomcat.
# processname tomcat 7.0

export JAVA_HOME=/usr/local/Java/jdk1.7.0_21
export CLASSPATH=$JAVA_HOME/lib:$JAVA_HOME/jre/lib
. /etc/rc.d/init.d/functions
. /etc/sysconfig/network
case $1 in
start)
sh /opt/apache-tomcat-7.0.62/bin/startup.sh
;;
stop)
sh /opt/apache-tomcat-7.0.62/bin/shutdown.sh
;;
restart)
sh /opt/apache-tomcat-7.0.62/bin/shutdown.sh
sh /opt/apache-tomcat-7.0.62/bin/startup.sh
;;
esac
exit 0

创建/etc/init.d/tomcat,添加内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# !/bin/sh
. /etc/rc.d/init.d/functions
. /etc/sysconfig/network
case $1 in
start)
sh /usr/local/apache/bin/apachectl start
;;
stop)
sh /usr/local/apache/bin/apachectl stop
;;
restart)
sh /usr/local/apache/bin/apachectl restart
;;
esac
exit 0

赋予执行权限:

1
$ chmod a+x /etc/init.d/tomcat /etc/init.d/httpd

service感知到新的服务:

1
$ service reload

然后就可以用:

1
2
$ service httpd start
$ service tomcat start

以及restart等选项便捷启动。

老师兄传的是,反复使用上面命令启动httpd以及tomcat,直到/usr/local/apache/htdocs下生成了Demo目录,并且在该目录下有一个ROOT的目录,然后就可以从外部机器访问该站点发布的项目了。

至此,一个Apache整合Tomcat的实战就完成了。

安装ActiveMQ

因为项目用到了邮件发送,依赖于ActiveMQ的支持。不过这个安装也很简单:

1
2
3
$ tar zxvf /home/trans/recv/apache-activemq-5.7.0-bin.tar.gz -C /opt/
$ cd /opt/apache-activemq-5.7.0/
$ bin/activemq start

然后基本上就不用管了。

其他还有一些祖传的脚本,用于项目发布,以及上年经过灾害后我写来用于备份数据的脚本,可惜这一次毁得太彻底了,我都懒得再写了,就让这个项目静静地走向生命周期的结束吧。

总结

  1. 在一个真正的部署环境下,之能够查看工具提供的日志了,看日志很重要,而不是像平时的在代码写一句输出到控制台那么简单地捕捉到异常,学会看日志很重要,不然就会重现上一年我死活部署了三天,就是因为没看到那么一小条日志。
  2. 备份机制,你要假定灾害随时发生,并且都是最坏情况,幸好上一年有了经历灾害的经历,自己写了一份备份脚本,不然今年的国庆节我也不可能玩得那么放心了。

Fedora 25下开启Sublime Text 3的中文输入支持

因为之前一直没怎么使用Sublime Text 3做中文输入,没太在意这个部分,但是最近开始了写博客,总不可能一直写英文,Sublime又是我一直用来的编辑器,虽然有点麻烦,但是还是配置起来用。

经过测试,中文输入是支持了,但是我用的FZUG源的搜狗输入法并不稳定,经常会崩溃到无法关机。
后来我换了Google的拼音输入法,目前还未崩溃。

环境支持

  1. 操作系统为Fedora 25.
  2. fcitx
  3. Sublime Text 3
  4. (可选)懒人可以通过添加FZUG源,然后省去复杂的安装搜狗输入法(不推荐)步骤,细节在接下来内容介绍。

卸载iBus,安装fcitx

Fedora一般默认安装的输入法管理器是iBus,而如果要把中文输入到Sublime Text 3中,则需要fcitx,所以使用

1
$ sudo dnf remove ibus

卸载iBus,以及关闭GNOME对iBus的支持:

1
$ gsettings set org.gnome.settings-daemon.plugins.keyboard active false

安装fcitx:

1
$ sudo sudo install fcitx fcitx-configtool -y

指定输入法模块:

1
$ vim ~/.bashrc

在文件末尾添加:

1
2
3
export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS="@im=fcitx"

安装中文拼音输入法:

1
$ sudo dnf install fcitx-googlepinyin fcitx-cloudpinyin -y

注销或者重启系统,通过Ctrl+Space就可以切换输入法。
或者将鼠标指向屏幕左下方,会出现:

点击之后:

选择google输入法:

在配置中选择Addon,在底下的搜索栏搜索cloud得到Cloud Pinyin,双击设置:

修改云拼音源为baidu,这样就不会因为被墙而使用不了云拼音。

添加插件使得Sublime Text 3支持中文输入

笔者假定Sublime Text 3已经安装好,并且目录为/opt/sublime-text

下载插件源码

就这个文件就可以了。

执行命令生成动态库:

1
$ gcc -shared -o libsublime-imfix.so sublime-imfix.c `pkg-config --libs --cflags gtk+-2.0` -fPIC

注意: 如果编译报错缺失<gtk/gtk.h>,证明系统缺少gtk2.0库的支持,执行命令:

1
$ sudo dnf install gtk2-devel -y

补全依赖,再次执行编译动态库命令。

先执行:

1
$ LD_PRELOAD=./libsublime-imfix.so sublime_text

测试sublime能否支持中文输入,一般都是可以了。
通过的话,执行:

1
$ cp ./libsublime-imfix.so /opt/sublime-text/

将动态库复制到sublime的目录下。

之后修改:

1
$ sudo vim /usr/share/applications/sublime_text.desktop

将原来的:

1
Exec=/opt/sublime-text/sublime_text %F

改为:

1
Exec=bash -c "LD_PRELOAD=/opt/sublime-text/libsublime-imfix.so exec /opt/sublime-text/sublime_text %F"

至此,从GNOME启动sublime text 3可以支持fcitx的中文输入了。

(可选1):修改/usr/bin/subl:

1
2
3
#!/bin/sh
LD_PRELOAD=/opt/sublime-text/libsublime-imfix.so
/opt/sublime-text/sublime_text "$@"

(可选2):添加一行到/etc/profile:

1
LD_PRELOAD=/opt/sublime-text/libsublime-imfix.so

笔者最近将系统更新到了Fedora 27,上述的配置已经无效了,尝试了网上一些方法之后最终放弃了,更改为用iBus+VSCode方案,目前流畅使用中。

hexo+github配置

需要的环境

  1. git
  2. npm
  3. nodejs

笔者使用的是Fedora 25,所以使用dnf命令就可以把这个两个工具快速安装了,nodejs也可以通过dnf快速安装到系统中。

Github上的准备

在github上建立一个与自己用户名对应的仓库,仓库名为”${用户名}.github.io”。
在本地上新建一个目录,用于存储hexo博客的工程内容,我就建立了一个”blog”作为hexo的根目录,只需要记得根目录路径即可,很多操作直接在根目录上使用hexo命令。

安装Hexo

在前面提及的工具已经安装的情况下,使用:

1
$ sudo npm install -g hexo

安装hexo。

初始化以及配置Hexo工程

进入blog目录,执行

1
$ hexo init

初始化目录。

生成静态页面:

1
$ hexo generate

或者简化的命令参数:

1
$ hexo g

然后可以使用:

1
$ hexo server

使用浏览器访问http://localhost:4000即可看到搭建起来的本地博客。

将Hexo本地工程部署到远程站点上

在blog本地根目录下有一个名为_config.yml的配置文件,用于配置整个hexo工程。

使用:

1
$ vim _config.yml

打开, 其他平台或者编辑器的使用者请自行使用熟悉的编辑器。

到了文件末尾会有一段deploy开头的配置。
修改成如下:

1
2
3
4
deploy:
type: git
repo: ${之前在github仓库上创建的仓库的git或者http链接}
branch: master

例如我的配置:

1
2
3
4
deploy:
type: git
repo: git@github.com:CFWLoader/CFWLoader.github.io.git
branch: master

然后执行命令:

1
$ npm install hexo-deployer-git --save

保存配置。

最后,执行:

1
$ hexo deploy

将本地的修改提交到github仓库上,并会部署到站点上运行应用。

使用浏览器访问建立的仓库名则可以看到部署的博客站点,例如我的站点是http://cfwloader.github.io

至此github + hexo的基本配置结束。

部署事项

每一次编辑并发布,请按照下列命令执行:

1
2
3
$ hexo clean
$ hexo generate
$ hexo deploy

开启Hexo文章分类功能

在项目目录下,编辑scaffolds/post.md,修改内容为:

1
2
3
4
5
6
---
title: {{ title }}
date: {{ date }}
tags:
categories:
---

之后使用hexo new产生新文章时,在categories中填入文章的分类,hexo会自动产生分类栏。

因为一般我们采用的是中文,所以产生出来的页面的路径也包含中文,如果需要将分类与路径分离,可以修改_config.ymlcategory_map,有兴趣的可以自己去探索探索,这里不展开介绍了。

Hexo标签功能

Hexo自带的模板的文章头就有tags选项,之前学习搭建hexo的时候别的文章提到有多个标签的时候有两种填法。
一种是:

1
tags: [tag1, tag2, ..., tag`n`]

另一种是:

1
2
3
4
5
tags:
-tag1
-tag2
...
-tag`n`

笔者使用的hexo-cli版本是1.0.4,只有第二种方法才能正确产生期望的标签样式。故采用第二种方法。

常用命令

新建文章:

1
$ hexo new "${文章名}"

新建页面:

1
$ hexo new page "${页面名}"

生成页面:

1
$ hexo generate

或者

1
$ hexo g

部署应用到站点上:

1
$ hexo deploy

更多:

1
$ hexo help