老司机种菜


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 公益404

  • 搜索

Android逆向相关

发表于 2019-04-23 | 分类于 Android

安装apktool和dex2jar,jd-gui homebrew安装:

1
2
brew install apktool
brew install dex2jar

安装结果:

1
2
/usr/local/Cellar/apktool/2.4.0: 4 files, 15.6MB, built in 3 minutes 39 seconds
/usr/local/Cellar/dex2jar/2.0: 34 files, 2.4MB, built in 1 minute 12 seconds

JD-GUI去http://jd.benow.ca/下载 dmg可能不支持最新版本的mac用不了,打开就报错

反编译流程

  1. 执行脚本apktool d xxx.apk 注:xxx.apk为你要反编译的apk
  2. 在你执行命令的目录下会生成xxx文件夹 到文件夹中查看res资源文件即可
  3. 将apk文件后缀名直接改为.zip并解压。得到其中的classes.dex文件,它就是java源代码经过编译再通过dx工具打包而成的。
  4. 将classes.dex文件复制到dex2jar所在的文件夹
  5. 终端cd到dex2jar目录,运行 sh dex2jar.sh classes.dex
  6. 生成 classes_dex2jar.jar
  7. 用jd-gui打开就可以看到源代码

自动化反编译 shell脚本

1
apktool d $1 && mv $1 $1.zip && unzip $1.zip "*.dex" -d $1_dex/ && cd $1_dex/ && d2j-dex2jar *.dex

解压apk:

1
2
3
4
5
#解压apk,获取代码中的classs.dex
unzip apk-debug.apk -d apk-debug_code

#将.dex文件转换成.jar文件
sh dex2jar-2.0/d2j-dex2jar.sh apk-debug_code/classes.dex

apktool wrappter apktool.jar dex2jar jd-gui http://t.cn/R56KIjj

CPP小技巧

发表于 2019-04-21 | 分类于 language

内置宏

标准C语言预处理要求定义某些对象宏,每个预定义宏的名称一两个下划线字符开头和结尾,这些预定义宏不能被取消定义(#undef)或由编程人员重新定义。下面预定义宏:

  • __LINE__ 当前程序行的行号,表示为十进制整型常量
  • __FILE__ 当前源文件名,表示字符串型常量
  • __DATE__ 转换的日历日期,表示为Mmm dd yyyy 形式的字符串常量,Mmm是由asctime产生的。
  • __TIME__ 转换的时间,表示”hh:mm:ss”形式的字符串型常量,是有asctime产生的。(asctime貌似是指的一个函数)
  • __STDC__ 编辑器为ISO兼容实现时位十进制整型常量
  • __STDC_VERSION__ 如何实现复合C89整部1,则这个宏的值为19940SL;如果实现符合C99,则这个宏的值为199901L;否则数值是未定义
  • __STDC_EOBTED__ (C99)实现为宿主实现时为1,实现为独立实现为0
  • __STDC_IEC_559__ (C99)浮点数实现复合IBC 60559标准时定义为1,否者数值是未定义
  • __STDC_IEC_559_COMPLEX__ (C99)复数运算实现复合IBC 60559标准时定义为1,否者数值是未定义
  • __STDC_ISO_10646__ (C99)定义为长整型常量,yyyymmL表示wchar_t值复合ISO 10646标准及其指定年月的修订补充,否则数值未定义

C++中还定义了 __cplusplus, C语言中的__FILE__、__LINE__和__DATE__等都在头文件#include<stdio.h>中

__attribute__机制

GNU C的一大特色就是__attribute__机制.__attribute__可以设置函数属性(Function Attribute),变量属性(Variable Attribute)和类型属性(Type Attribute)

其位置约束为: 放与生命的尾部”;”之前 __attribute__书写特征为:__attribute__前后都有两个下划线,并且后面会紧跟一对圆括号,括号里面是相应的__attribute__参数. __attribute__语法格式为:`attribute((attribute-list))

一, 函数属性(Function Attribute)

函数属性可以帮助开发者把一些特性添加到函数声明中,从而可以使编译器在错误检查方面的功能更强大。attribute机制也很容易同非GNU应用程序做到兼容之功效。GNU CC需要使用 –Wall编译器来击活该功能,这是控制警告信息的一个很好的方式。

下面介绍几个常见的属性参数:

__attribute__ format 该属性可以给被声明的函数加上类似printf或者scanf的特征,它可以使编译器检查函数声明和函数实际调用参数之间的格式化字符串是否匹配。该功能十分有用,尤其是处理一些很难发现的bug。

format的语法格式为:format (archetype, string-index, first-to-check)

format属性告诉编译器,按照 printf, scanf, strftime或strfmon的参数表格式规则对该函数的参数进行检查。“archetype”指定是哪种风格;“string-index”指定传 入函数的第几个参数是格式化字符串;“first-to-check”指定从函数的第几个参数开始按上述规则进行检查。 具体使用格式如下:

1
2
__attribute__((format(printf,m,n)))
__attribute__((format(scanf,m,n)))

其中参数m与n的含义为:

  • m:第几个参数为格式化字符串(format string);
  • n:参数集合中的第一个,即参数“…”里的第一个参数在函数参数总数排在第几.这里需要注意,有时函数参数里还有“隐身”的,如C++的类成员函数的第一个参数实际上是”隐身”的”this”指针; 在使用上,__attribute__((format(printf,m,n)))是常用的,而另一种却很少见到。

下面举例说明,其中myprint为自己定义的一个带有可变参数的函数,其功能类似于printf:

1
2
3
4
5
// m = 1, n = 2...如果在这里myprint()为类成员函数,则gcc编译后会提示"format argument is not a pointer"的警告
extern void myprint(const char *format,...) __attribute__((format(printf,1,2)));

// m = 2, n = 3
extern void myprint(short num,const char *format,...) __attribute__((format(printf,2,3)));

最经典的应用可以去看linux源码里的函数device_create(…)和class_device_create(…), 做linux驱动开发的小伙伴应该对这两个函数不陌生.

__attribute__ noreturn 该属性通知编译器函数从不返回值。当遇到函数需要返回值却还没运行到返回值处就已退出来的情况,该属性可以避免出现错误信息。

C库函数中的abort()和exit()的声明格式就采用了这种格式:

1
2
extern void exit(int)  __attribute__((noreturn));
extern void abort(void) __attribute__((noreturn));

例如下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extern void myexit(int);

int testFunc(void)
{
printf("-- Enter %s --", __func__);

myexit(0);

// 其实函数运行不到这里
printf("-- Exit %s --", __func__);
}

void myexit(int i)
{
exit(i);
}

编译时会报”control reaches end of non-void function”的警告, 但若将”extern void myexit(int);”改为”extern void myexit(int) attribute((noreturn));” 就不会再报警告了. attribute constructor/destructor 若函数被设定为constructor属性,则该函数会在 main()函数执行之前被自动的执行。类似的,若函数被设定为destructor属性,则该函数会在main()函数执行之后或者exit()被调用后被自动的执行。拥有此类属性的函数经常隐式的用在程序的初始化数据方面。

这两个属性还没有在面向对象C中实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__attribute__((constructor)) void before_main() {
printf("--- %s\n", __func__);
}

__attribute__((destructor)) void after_main() {
printf("--- %s\n", __func__);
}

int main(int argc, char **argv) {
printf("--- %s\n", __func__);

exit(0);

printf("--- %s, exit ?\n", __func__);

return 0;
}

其执行结果为:

1
2
3
--- before_main
--- main
--- after_main

弱引用符号__attribute__((weak))

情况是这样的,碰到一个棘手的问题:我们不确定外部模块是否提供一个函数func,但是我们不得不用这个函数,即自己模块的代码必须用到func函数:

1
2
3
4
5
6
7
8
extern int func(void);
...................
int a = func();
if( a > .....)
{
..........
}
............

我们不知道func函数是否被定义了,这会导致2个结果:

  1. 外部存在这个函数func,并且EXPORT_SYMBOL(func),那么在我自己的模块使用这个函数func,正确。
  2. 外部其实不存在这个函数,那么我们使用func,程序直接崩溃。

所以这个时候,__attribute__((weak)) 派上了用场。 在自己的模块中定义:

1
2
3
4
int  __attribute__((weak))  func(......)
{
return 0;
}

将本模块的func转成弱符号类型,如果遇到强符号类型(即外部模块定义了func),那么我们在本模块执行的func将会是外部模块定义的func。如果外部模块没有定义,那么,将会调用这个弱符号,也就是在本地定义的func,直接返回了一个1(返回值视具体情况而定),相当于增加了一个默认函数。

原理:连接器发现同时存在弱符号和强符号,有限选择强符号,如果发现不存在强符号,只存在弱符号,则选择弱符号。如果都不存在:静态链接,恭喜,编译时报错,动态链接:对不起,系统无法启动。

TIPS weak属性只会在静态库(.o .a )中生效,动态库(.so .ko(内核模块))中不会生效。

举个例子: strong.c //生成libstrong.so

1
2
3
4
5
6
#include <stdio.h>

void real_func()
{
printf("int real func\n");
}

weak.c //生成libweak.so

1
2
3
4
5
6
7
8
#include <stdio.h>
void real_func() __attribute__((weak));


void real_func()
{
printf("fake func\n");
}

main.c //

1
2
3
4
5
6
7
#include <stdio.h>
extern void real_func();

void main()
{
real_func();
}

如果

1
gcc main.c -lstrong -lweak

那么输出结果”real func”。

如果

1
gcc main.c -lweak -lstrong

那么输出结果为”fake func”。

可见,对于动态库,weak属性毫无作用,且main中调用哪个real_func(),取决于链接的顺序。 如果将strong.c 和 weak.c编译成.a或者.o

1
gcc main.c strong.o weak.o

或者

1
gcc main.c strong.o weak.o

那么输出结果都是”real func”。

所以,如果在so中使用weak属性,那么任何不符合预期的情况,都是可能出现的。

官方解释:https://sourceware.org/bugzilla/show_bug.cgi?id=3946

alias属性可以给函数名取一个外号。使用方法如下:

1
2
void func(void);
void alias_func(void) __attribute__((alias("func"))); 需要注意c++的符号修饰机制!

这样的意思就是函数func的别名或外号是alias_func,那么就是调用alias_func()和func()的效果是一样的,有兴趣的话可以自己写代码验证。这时需要主意func函数必须是要有定义的,否则会编译报错的。

__attribute__((weakref))为弱引用,请注意引用与定义的区别。weakref就是申明某个引用为弱引用,弱引用时如果需引用符号不存在也不会链接出错,而是将需要引用的符号定义为WEAK属性及0地址(跟前面的WEAK属性很相似吧)。 weakref的用法有点特别,必须要配合alias使用及必须是static定义。attribute((weak(“target”)))相当于attribute((weakref,alias(“target”))),以下看个实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
** weakref_test.c
*/

/* 申明func_alias函数func的带弱引用的别名 */
void func(void)
{
  printf("func:%s in\n", __FUNC__);
}

static void func_alias(void) __attribute__((weakref,alias("func")));

int main()
{
  func_alias(); /* 相当于调用func */
}

编译运行,会发现实际运行的就是func函数。func_alias相当于是func的一个带有weakref属性的另一份申明,可以这样理解:void *func = func;void *func_alias = func(“weakref”)。 注意到前面alias属性如果func不存在时申明alias会出错,通过weakref方法,可以让func未定义就可以编译通过,使用static void alias_func(void) __attribute__((weakref,alias("func")))时即使func未定义也能链接通过,只不过func或alias_func的地址为0,可以去掉func的实现,验证一下即可。

宏参数的字符串化和宏参数的连接

在宏定义中,有时还会用到#和##两个符号,它们能够对宏参数进行操作。

# 的用法

#用来将宏参数转换为字符串,也就是在宏参数的开头和末尾添加引号。例如有如下宏定义:

1
#define STR(s) #s

那么:

1
2
printf("%s", STR(c.biancheng.net));
printf("%s", STR("c.biancheng.net"));

分别被展开为:

1
2
printf("%s", "c.biancheng.net");
printf("%s", "\"c.biancheng.net\"");

以发现,即使给宏参数“传递”的数据中包含引号,使用#仍然会在两头添加新的引号,而原来的引号会被转义。

##的用法

##称为连接符,用来将宏参数或其他的串连接起来。例如有如下的宏定义:

1
2
#define CON1(a, b) a##e##b
#define CON2(a, b) a##b##00

那么:

1
2
printf("%f\n", CON1(8.5, 2));
printf("%d\n", CON2(12, 34));

将被展开为:

1
2
printf("%f\n", 8.5e2);
printf("%d\n", 123400);

`extern “C”的作用

extern “C”的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern “C”后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern “C”就是其中的一个策略。

这个功能主要用在下面的情况:

  • C++代码调用C语言代码
  • 在C++的头文件中使用
  • 在多个人协同开发时,可能有的人比较擅长C语言,而有的人擅长C++,这样的情况下也会有用到

看一个简单的例子: 有moduleA、moduleB两个模块,B调用A中的代码,其中A是用C语言实现的,而B是利用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
//moduleA头文件
#ifndef __MODULE_A_H //对于模块A来说,这个宏是为了防止头文件的重复引用
#define __MODULE_A_H
int fun(int, int);
#endif

//moduleA实现文件moduleA.C //模块A的实现部分并没有改变
#include"moduleA"
int fun(int a, int b)
{
return a+b;
}

//moduleB头文件
#idndef __MODULE_B_H //很明显这一部分也是为了防止重复引用
#define __MODULE_B_H
#ifdef __cplusplus //而这一部分就是告诉编译器,如果定义了__cplusplus(即如果是cpp文件,因为cpp文件默认定义了该宏),则采用C语言方式进行编译
extern "C"{
#include"moduleA.h"
#endif
… //其他代码

#ifdef __cplusplus
}
#endif
#endif

//moduleB实现文件 moduleB.cpp //B模块的实现也没有改变,只是头文件的设计变化了
#include"moduleB.h"
int main()
{
  cout<<fun(2,3)<<endl;
}

补充介绍: 由于C、C++编译器对函数的编译处理是不完全相同的,尤其对于C++来说,支持函数的重载,编译后的函数一般是以函数名和形参类型来命名的。例如函数void fun(int, int),编译后的可能是_fun_int_int(不同编译器可能不同,但都采用了类似的机制,用函数名和参数类型来命名编译后的函数名);而C语言没有类似的重载机制,一般是利用函数名来指明编译后的函数名的,对应上面的函数可能会是_fun这样的名字。

看下面的一个面试题:为什么标准头文件都有类似的结构?

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef __INCvxWorksh /*防止该头文件被重复引用*/
#define __INCvxWorksh
#ifdef __cplusplus//告诉编译器,这部分代码按C语言的格式进行编译,而不是C++的
extern "C"{
#endif

/*…*/

#ifdef __cplusplus
}
#endif
#endif /*end of __INCvxWorksh*/

分析:

  • 显然,头文件中编译宏”#ifndef __INCvxWorksh 、#define __INCvxWorksh、#endif”的作用是为了防止该头文件被重复引用
  • 那么以下代码的作用是什么呢?
    1
    2
    3
    4
    5
    6
    #ifdef __cplusplus (其中__cplusplus是cpp中自定义的一个宏!!!)
    extern "C"{
    #endif
    #ifdef __cplusplus
    }
    #endif

extern “C”包含双重含义,从字面上可以知道,首先,被它修饰的目标是”extern”的;其次,被它修饰的目标代码是”C”的。

  • 被extern “C”限定的函数或变量是extern类型的. extern是C/C++语言中表明函数和全局变量的作用范围的关键字,该关键字告诉编译器,其申明的函数和变量可以在本模块或其他模块中使用。记住,语句:extern int a; 仅仅是一个变量的声明,其并不是在定义变量a,也并未为a分配空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出错。 通常来说,在模块的头文件中对本模块提供给其他模块引用的函数和全局变量以关键字extern生命。例如,如果模块B要引用模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但并不会报错;它会在链接阶段从模块A编译生成的目标代码中找到该函数。 extern对应的关键字是static,static表明变量或者函数只能在本模块中使用,因此,被static修饰的变量或者函数不可能被extern C修饰。
  • 被extern “C”修饰的变量和函数是按照C语言方式进行编译和链接的:这点很重要!!!! 上面也提到过,由于C++支持函数重载,而C语言不支持,因此函数被C++编译后在符号库中的名字是与C语言不同的;C++编译后的函数需要加上参数的类型才能唯一标定重载后的函数,而加上extern “C”后,是为了向编译器指明这段代码按照C语言的方式进行编译

未加extern “C”声明时的链接方式:

1
2
3
4
5
6
//模块A头文件 moduleA.h
#idndef _MODULE_A_H
#define _MODULE_A_H

int foo(int x, int y);
#endif

在模块B中调用该函数:

1
2
3
//模块B实现文件 moduleB.cpp
#include"moduleA.h"
foo(2,3);

实际上,在链接阶段,链接器会从模块A生成的目标文件moduleA.obj中找_foo_int_int这样的符号,显然这是不可能找到的,因为foo()函数被编译成了_foo的符号,因此会出现链接错误。

extern “C”的使用要点总结

  1. 可以是如下的单一语句

    1
    extern "C" double sqrt(double);
  2. 可以是复合语句, 相当于复合语句中的声明都加了extern “C”

    1
    2
    3
    4
    5
    extern "C"
    {
    double sqrt(double);
    int min(int, int);
    }
  3. 可以包含头文件,相当于头文件中的声明都加了extern “C”

    1
    2
    3
    4
    extern "C"
    {
    #include <cmath>
    }
  4. 不可以将extern “C” 添加在函数内部 如果函数有多个声明,可以都加extern “C”, 也可以只出现在第一次声明中,后面的声明会接受第一个链接指示符的规则。 除extern “C”, 还有extern “FORTRAN” 等。

C++11对匿名函数的支持

C++11提供了对匿名函数的支持,称为Lambda函数(也叫Lambda表达式). Lambda表达式具体形式如下:

1
[capture](parameters)->return-type{body}

  如果没有参数,空的圆括号()可以省略.返回值也可以省略,如果函数体只由一条return语句组成或返回类型为void的话.形如:

1
[capture](parameters){body}

下面举了几个Lambda函数的例子:

1
2
3
4
5
6
[](int x, int y) { return x + y; } // 隐式返回类型
[](int& x) { ++x; } // 没有return语句 -> lambda 函数的返回类型是'void'
[]() { ++global_x; } // 没有参数,仅访问某个全局变量
[]{ ++global_x; } // 与上一个相同,省略了()
```    
可以像下面这样显示指定返回类型:

[](int x, int y) -> int { int z = x + y; return z; }

1
在这个例子中创建了一个临时变量z来存储中间值. 和普通函数一样,这个中间值不会保存到下次调用. 什么也不返回的Lambda函数可以省略返回类型, 而不需要使用 -> void 形式.Lambda函数可以引用在它之外声明的变量. 这些变量的集合叫做一个闭包. 闭包被定义在Lambda表达式声明中的方括号[]内. 这个机制允许这些变量被按值或按引用捕获.下面这些例子就是:

[] //未定义变量.试图在Lambda内使用任何外部变量都是错误的. [x, &y] //x 按值捕获, y 按引用捕获. [&] //用到的任何外部变量都隐式按引用捕获 [=] //用到的任何外部变量都隐式按值捕获 [&, x] //x显式地按值捕获. 其它变量按引用捕获 [=, &z] //z按引用捕获. 其它变量按值捕获

1
接下来的两个例子演示了Lambda表达式的用法.

std::vector some_list; int total = 0; for (int i=0;i<5;++i) some_list.push_back(i); std::for_each(begin(some_list), end(some_list), [&total](int x) { total += x; });

1
此例计算list中所有元素的总和. 变量total被存为lambda函数闭包的一部分. 因为它是栈变量(局部变量)total的引用,所以可以改变它的值.

std::vector some_list; int total = 0; int value = 5; std::for_each(begin(some_list), end(some_list), [&, value, this](int x) { total += x * value * this->some_func(); }); ```

cmake语法入门

发表于 2019-04-21 | 分类于 language

CMake 的基本操作

CMake 编译可执行文件

一个打印 hello world 的 cpp 文件,通过 CMake 将它编译成可执行文件。

在 cpp 的同一目录下创建 CMakeLists.txt 文件,内容如下:

1
2
3
4
5
6
# 指定 CMake 使用版本
cmake_minimum_required(VERSION 3.9)
# 工程名
project(HelloCMake)
# 编译可执行文件
add_executable(HelloCMake main.cpp )

其中,通过 cmake_minimum_required 方法指定 CMake 使用版本,通过 project 指定工程名。 而 add_executable 就是指定最后编译的可执行文件名称和需要编译的 cpp 文件,如果工程很大,有多个 cpp 文件,那么都要把它们添加进来。 定义了 CMake 文件之后,就可以开始编译构建了。 CMake 在构建工程时会生成许多临时文件,避免让这些临时文件污染代码,一般会把它们放到一个单独的目录中。 操作步骤如下:

1
2
3
4
5
6
# 在 cpp 目录下创建 build 目录
mkdir build
# 调用 cmake 命令生成 makefile 文件
cmake ..
# 编译
make

在 build 目录中可以找到最终生成的可执行文件。

这就是 CMake 的一个简单操作,将 cpp 编译成可执行文件,但在 Android 中,大多数场景都是把 cpp 编译成库文件。

CMake 编译静态库和动态库

同样还是一个 cpp 文件和一个 CMake 文件,cpp 文件内容为打印字符串的函数:

1
2
3
4
#include <iostream>
void print() {
std::cout << "hello lib" << std::endl;
}

同时,CMake 文件也要做相应更改:

1
2
3
4
5
cmake_minimum_required(VERSION 3.12)
# 指定编译的库和文件,SHARED 编译动态库
add_library(share_lib SHARED lib.cpp)
# STATIC 编译静态库
# add_library(share_lib STATIC lib.cpp)

通过 add_library 指定要编译的库的名称,以及动态库还是静态库,还有要编译的文件。

最后同样地执行构建,在 build 目录下可以看到生成的库文件。

到这里,就基本可以使用 CMake 来构建 C/C++ 工程了。

CMake 基本语法

熟悉了上面的基本操作之后,就必然会遇到以下的问题了:

如果要参与编译的 C/C++ 文件很多,难道每个都要手动添加嘛? 可以把编译好的可执行文件或者库自动放到指定位置嘛? 可以把编译好的库指定版本号嘛?

带着这些问题,还是要继续深入学习 CMake 的相关语法,最好的学习材料就是 官网文档 了。 为了避免直接看官方文档时一头雾水,这里列举一些常用的语法命令。

PROJECT(工程名字)

这条指令会自动创建两个变量:

  • <projectname>_BINARY_DIR(二进制文件保存路径)
  • <projectname>_SOURCE_DIR(源代码路径)

cmake系统也帮助我们预定义了PROJECT_BINARY_DIR和PROJECT_SOURCE_DIR其值与上述对应相等

注释与大小写

在前面就已经用到了 CMake 注释了,每一行的开头 # 代表注释。

另外,CMake 的所有语法指令是不区分大小写的。

变量定义与消息打印

通过 set 来定义变量:

1
2
# 变量名为 var,值为 hello
set(var hello)

当需要引用变量时,在变量名外面加上 ${} 符合来引用变量

1
2
# 引用 var 变量
${var}

还可以通过 message 在命令行中输出打印内容。

1
2
set(var hello) 
message(${var})

在IF等语句中,是直接使用变量名而不通过${}取值。

数学和字符串操作

数学操作

CMake 中通过 math 来实现数学操作。

1
2
# math 使用,EXPR 为大小
math(EXPR <output-variable> <math-expression>)
1
2
3
math(EXPR var "1+1")
# 输出结果为 2
message(${var})

math 支持 +, -, *, /, %, |, &, ^, ~, <<, >>等操作,和 C 语言中大致相同。

字符串操作

CMake 通过 string 来实现字符串的操作,这波操作有很多,包括将字符串全部大写、全部小写、求字符串长度、查找与替换等操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
set(var "this is  string")
set(sub "this")
set(sub1 "that")
# 字符串的查找,结果保存在 result 变量中
string(FIND ${var} ${sub1} result )
# 找到了输出 0 ,否则为 -1
message(${result})

# 将字符串全部大写
string(TOUPPER ${var} result)
message(${result})

# 求字符串的长度
string(LENGTH ${var} num)
message(${num})

另外,通过空白或者分隔符号可以表示字符串序列。

1
2
set(foo this is a list) // 实际内容为字符串序列
message(${foo})

当字符串中需要用到空白或者分隔符时,再用双括号””表示为同一个字符串内容。

1
2
set(foo "this is a list") // 实际内容为一个字符串
message(${foo})

文件操作

Make 中通过 file 来实现文件操作,包括文件读写、下载文件、文件重命名等。

1
2
3
4
5
6
7
8
9
# 文件重命名
file(RENAME "test.txt" "new.txt")

# 文件下载
# 把文件 URL 设定为变量
set(var "http://img.zcool.cn/community/0117e2571b8b246ac72538120dd8a4.jpg")

# 使用 DOWNLOAD 下载
file(DOWNLOAD ${var} "/Users/glumes/CLionProjects/HelloCMake/image.jpg")

在文件的操作中,还有两个很重要的指令 GLOB 和 GLOB_RECURSE 。

1
2
3
4
# GLOB 的使用
file(GLOB ROOT_SOURCE *.cpp)
# GLOB_RECURSE 的使用
file(GLOB_RECURSE CORE_SOURCE ./detail/*.cpp)

其中,GLOB 指令会将所有匹配 *.cpp 表达式的文件组成一个列表,并保存在 ROOT_SOURCE 变量中。 而 GLOB_RECURSE 指令和 GLOB 类似,但是它会遍历匹配目录的所有文件以及子目录下面的文件。 使用 GLOB 和 GLOB_RECURSE 有好处,就是当添加需要编译的文件时,不用再一个一个手动添加了,同一目录下的内容都被包含在对应变量中了,但也有弊端,就是新建了文件,但是 CMake 并没有改变,导致在编译时也会重新产生构建文件,要解决这个问题,就是动一动 CMake,让编译器检测到它有改变就好了。

INSTALL指令

安装的需要有两种,一种是从代码编译后直接make install安装,一种是打包时的指定目录安装。 这里需要引入一个新的cmake 指令 INSTALL和一个非常有用的变量CMAKE_INSTALL_PREFIX。 CMAKE_INSTALL_PREFIX变量类似于configure脚本的 –prefix,常见的使用方法看起来是这个样子: cmake -DCMAKE_INSTALL_PREFIX=/usr .

INSTALL指令用于定义安装规则,安装的内容可以包括目标二进制、动态库、静态库以及文件、目录、脚本等。INSTALL指令包含了各种安装类型,我们需要一个个分开解释:

目标文件的安装
1
2
3
4
5
6
7
8
9
INSTALL(TARGETS targets...
[[ARCHIVE|LIBRARY|RUNTIME]
[DESTINATION <dir>]
[PERMISSIONS permissions...]
[CONFIGURATIONS
[Debug|Release|...]]
[COMPONENT <component>]
[OPTIONAL]
] [...])

参数中的TARGETS后面跟的就是我们通过ADD_EXECUTABLE或者ADD_LIBRARY定义的目标文件,可能是可执行二进制、动态库、静态库。目标类型也就相对应的有三种,ARCHIVE特指静态库,LIBRARY特指动态库,RUNTIME特指可执行目标二进制。 DESTINATION定义了安装的路径,如果路径以/开头,那么指的是绝对路径,这时候CMAKE_INSTALL_PREFIX其实就无效了。如果你希望使用CMAKE_INSTALL_PREFIX来定义安装路径,就要写成相对路径,即不要以/开头,那么安装后的路径就是 ${CMAKE_INSTALL_PREFIX}/<DESTINATION定义的路径> 举个简单的例子:

1
2
3
4
5
INSTALL(TARGETS myrun mylib mystaticlib
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION libstatic
)

上面的例子会将:

  • 可执行二进制myrun安装到${CMAKE_INSTALL_PREFIX}/bin目录
  • 动态库libmylib安装到${CMAKE_INSTALL_PREFIX}/lib目录
  • 静态库libmystaticlib安装到${CMAKE_INSTALL_PREFIX}/libstatic目录 特别注意的是你不需要关心TARGETS具体生成的路径,只需要写上TARGETS名称就可以 了。
    普通文件的安装
    1
    2
    3
    4
    5
    INSTALL(FILES files... DESTINATION <dir>
    [PERMISSIONS permissions...]
    [CONFIGURATIONS [Debug|Release|...]]
    [COMPONENT <component>]
    [RENAME <name>] [OPTIONAL])

可用于安装一般文件,并可以指定访问权限,文件名是此指令所在路径下的相对路径。 如果默认不定义权限PERMISSIONS,安装后的权限为,OWNER_WRITE,OWNER_READ, GROUP_READ,和WORLD_READ,即644权限。

非目标文件的可执行程序安装(比如脚本之类)
1
2
3
4
5
INSTALL(PROGRAMS files... DESTINATION <dir>
[PERMISSIONS permissions...]
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>]
[RENAME <name>] [OPTIONAL])

跟上面的FILES指令使用方法一样,唯一的不同是安装后权限为: OWNER_EXECUTE, GROUP_EXECUTE, 和WORLD_EXECUTE,即755权限

目录的安装
1
2
3
4
5
6
7
8
INSTALL(DIRECTORY dirs... DESTINATION <dir>
[FILE_PERMISSIONS permissions...]
[DIRECTORY_PERMISSIONS permissions...]
[USE_SOURCE_PERMISSIONS]
[CONFIGURATIONS [Debug|Release|...]]
[COMPONENT <component>]
[[PATTERN <pattern> | REGEX <regex>]
[EXCLUDE] [PERMISSIONS permissions...]] [...])

这里主要介绍其中的DIRECTORY、PATTERN以及PERMISSIONS参数。

  • DIRECTORY后面连接的是所在Source目录的相对路径,但务必注意: abc和abc/有很大的区别。 abc意味着abc这个目录会安装在目标路径下; abc/意味着abc这个目录的内容会被安装在目标路径下; 如果目录名不以/结尾,那么这个目录将被安装为目标路径下的abc,如果目录名以/结尾, 代表将这个目录中的内容安装到目标路径,但不包括这个目录本身。
  • PATTERN用于使用正则表达式进行过滤,
  • PERMISSIONS用于指定PATTERN过滤后的文件权限。

我们来看一个例子:

1
2
3
4
5
INSTALL(DIRECTORY icons scripts/ DESTINATION share/myproj
PATTERN "CVS" EXCLUDE
PATTERN "scripts/*"
PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ
GROUP_EXECUTE GROUP_READ)

这条指令的执行结果是: 将icons目录安装到 <prefix>/share/myproj,将scripts/中的内容安装到 <prefix>/share/myproj不包含目录名为CVS的目录,对于scripts/*文件指定权限为 OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ.

安装时cmake脚本的执行
1
2
3
4
INSTALL([[SCRIPT <file>] [CODE <code>]] [...])
SCRIPT参数用于在安装时调用cmake脚本文件(也就是<abc>.cmake文件)
CODE参数用于执行CMAKE指令,必须以双引号括起来。比如:
INSTALL(CODE "MESSAGE(\"Sample install message.\")")

把相对路径转换成绝对路径

1
2
3
get_filename_component(<VAR> <FileName>
<COMP> [BASE_DIR <BASE_DIR>]
[CACHE])

Set to the absolute path of , where is one of:

  • ABSOLUTE = Full path to file REALPATH = Full path to existing file with symlinks resolved
  • If the provided is a relative path, it is evaluated relative to the given base directory . If no base directory is provided, the default base directory will be CMAKE_CURRENT_SOURCE_DIR.
  • Paths are returned with forward slashes and have no trailing slashes. If the optional CACHE argument is specified, the result variable is added to the cache.

To convert an absolute path to a file into a relative paths, you might use the file command:

1
file(RELATIVE_PATH <variable> <directory> <file>)

Compute the relative path from a <directory> to a <file> and store it in the <variable>.

1
file(RELATIVE_PATH buildDirRelFilePath "${CMAKE_BINARY_DIR}" "${myFile}")

预定义的常量

在 CMake 中有许多预定义的常量,使用好这些常量能起到事半功倍的效果。

  • CMAKE_BINARY_DIR

  • PROJECT_BINARY_DIR

  • < projectname >_BINARY_DIR 这三个变量指代的内容是一致的,如果是in-source编译,指得就是工程顶层目录;如果是out-of-source编译,指的是工程编译发生的目录。PROJECT_BINARY_DIR跟其它指令稍有区别,目前可以认为它们是一致的。

  • CMAKE_SOURCE_DIR

  • PROJECT_SOURCE_DIR

  • < projectname >_SOURCE_DIR 这三个变量指代的内容是一致的,不论采用何种编译方式,都是工程顶层目录。也就是在in-source编译时,他跟CMAKE_BINARY_DIR等变量一致。PROJECT_SOURCE_DIR跟其它指令稍有区别,目前可以认为它们是一致的。 (out-of-source build与in-source build相对,指是否在CMakeLists.txt所在目录进行编译。)

  • CMAKE_CURRENT_SOURCE_DIR : 前处理的CMakeLists.txt所在的路径,比如上面我们提到的src子目录。

  • CMAKE_CURRRENT_BINARY_DIR 如果是in-source编译,它跟CMAKE_CURRENT_SOURCE_DIR一致;如果是out-of-source编译,指的是target编译目录。使用ADD_SUBDIRECTORY(src bin)可以更改这个变量的值。使用SET(EXECUTABLE_OUTPUT_PATH <新路径>)并不会对这个变量造成影响,它仅仅修改了最终目标文件存放的路径。

  • CMAKE_CURRENT_LIST_FILE : 输出调用这个变量的CMakeLists.txt的完整路径

  • CMAKE_CURRENT_LIST_LINE : 输出这个变量所在的行

  • CMAKE_MODULE_PATH : 这个变量用来定义自己的cmake模块所在的路径。如果工程比较复杂,有可能会自己编写一些cmake模块,这些cmake模块是随工程发布的,为了让cmake在处理CMakeLists.txt时找到这些模块,你需要通过SET指令将cmake模块路径设置一下。比如SET(CMAKE_MODULE_PATH,${PROJECT_SOURCE_DIR}/cmake) 这时候就可以通过INCLUDE指令来调用自己的模块了。

  • EXECUTABLE_OUTPUT_PATH : 新定义最终结果的存放目录

  • LIBRARY_OUTPUT_PATH : 新定义最终结果的存放目录

  • PROJECT_SOURCE_DIR : 指当前工程的路径

  • PROJECT_NAME : 返回通过PROJECT指令定义的项目名称

比如,在 add_library 中需要指定 cpp 文件的路径,以 CMAKE_CURRENT_SOURCE_DIR 为基准,指定 cpp 相对它的路径就好了。

1
2
3
4
5
6
7
8
# 利用预定义的常量来指定文件路径
add_library( # Sets the name of the library.
openglutil
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${CMAKE_CURRENT_SOURCE_DIR}/opengl_util.cpp
)

平台相关的常量

CMake 能够用来在 Window、Linux、Mac 平台下进行编译,在它的内部也定义了和这些平台相关的变量。

列举一些常见的:

  • WIN32:如果编译的目标系统是 Window,那么 WIN32 为 True 。
  • UNIX:如果编译的目标系统是 Unix 或者类 Unix 也就是 Linux ,那么 UNIX 为 True 。
  • MSVC:如果编译器是 Window 上的 Visual C++ 之类的,那么 MSVC 为 True 。
  • ANDROID:如果目标系统是 Android ,那么 ANDROID 为 1 。
  • APPLE:如果目标系统是 APPLE ,那么 APPLE 为 1 。

有了这些常量做区分,就可以在一份 CMake 文件中编写不同平台的编译选项。

1
2
3
4
5
if(WIN32){
# do something
}elseif(UNIX){
# do something
}

toolChain脚本中设置的几个重要变量

  1. CMAKE_SYSTEM_NAME: 即你目标机target所在的操作系统名称,比如ARM或者Linux你就需要写”Linux”,如果Windows平台你就写”Windows”,如果你的嵌入式平台没有相关OS你即需要写成”Generic”,只有当CMAKE_SYSTEM_NAME这个变量被设置了,CMake才认为此时正在交叉编译,它会额外设置一个变量CMAKE_CROSSCOMPILING为TRUE.
  2. CMAKE_C_COMPILER: 顾名思义,即C语言编译器,这里可以将变量设置成完整路径或者文件名,设置成完整路径有一个好处就是CMake会去这个路径下去寻找编译相关的其他工具比如linker,binutils等,如果你写的文件名带有arm-elf等等前缀,CMake会识别到并且去寻找相关的交叉编译器。
  3. CMAKE_CXX_COMPILER: 同上,此时代表的是C++编译器。
  4. CMAKE_FIND_ROOT_PATH: 指定了一个或者多个优先于其他搜索路径的搜索路径。比如你设置了/opt/arm/,所有的Find_xxx.cmake都会优先根据这个路径下的/usr/lib,/lib等进行查找,然后才会去你自己的/usr/lib和/lib进行查找,如果你有一些库是不被包含在/opt/arm里面的,你也可以显示指定多个值给CMAKE_FIND_ROOT_PATH,比如set(CMAKE_FIND_ROOT_PATH /opt/arm /opt/inst)该变量能够有效地重新定位在给定位置下进行搜索的根路径。该变量默认为空。当使用交叉编译时,该变量十分有用:用该变量指向目标环境的根目录,然后CMake将会在那里查找。
  5. CMAKE_FIND_ROOT_PATH_MODE_PROGRAM: 对FIND_PROGRAM()起作用,有三种取值,NEVER,ONLY,BOTH,第一个表示不在你CMAKE_FIND_ROOT_PATH下进行查找,第二个表示只在这个路径下查找,第三个表示先查找这个路径,再查找全局路径,对于这个变量来说,一般都是调用宿主机的程序,所以一般都设置成NEVER
  6. CMAKE_FIND_ROOT_PATH_MODE_LIBRARY: 对FIND_LIBRARY()起作用,表示在链接的时候的库的相关选项,因此这里需要设置成ONLY来保证我们的库是在交叉环境中找的.
  7. CMAKE_FIND_ROOT_PATH_MODE_INCLUDE: 对FIND_PATH()和FIND_FILE()起作用,一般来说也是ONLY,如果你想改变,一般也是在相关的FIND命令中增加option来改变局部设置,有NO_CMAKE_FIND_ROOT_PATH,ONLY_CMAKE_FIND_ROOT_PATH,BOTH_CMAKE_FIND_ROOT_PATH
  8. BOOST_ROOT:对于需要boost库的用户来说,相关的boost库路径配置也需要设置,因此这里的路径即ARM下的boost路径,里面有include和lib。
  9. QT_QMAKE_EXECUTABLE: 对于Qt用户来说,需要更改相关的qmake命令切换成嵌入式版本,因此这里需要指定成相应的qmake路径(指定到qmake本身)

系统信息

  1. CMAKE_MAJOR_VERSION,CMAKE主版本号,比如2.4.6中的2
  2. CMAKE_MINOR_VERSION,CMAKE次版本号,比如2.4.6中的4
  3. CMAKE_PATCH_VERSION,CMAKE补丁等级,比如2.4.6 中的6
  4. CMAKE_SYSTEM,系统名称,比如Linux-2.6.22
  5. CMAKE_SYSTEM_NAME,不包含版本的系统名,比如Linux
  6. CMAKE_SYSTEM_VERSION,系统版本,比如2.6.22
  7. CMAKE_SYSTEM_PROCESSOR,处理器名称,比如i686.
  8. UNIX,在所有的类UNIX平台为TRUE,包括OS X和cygwin
  9. WIN32,在所有的win32平台为TRUE,包括cygwin

主要的开关选项:

  1. CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS,用来控制IF ELSE语句的书写方式
  2. BUILD_SHARED_LIBS 这个开关用来控制默认的库编译方式,如果不进行设置,使用ADD_LIBRARY并没有指定库类型的情况下,默认编译生成的库都是静态库。如果SET(BUILD_SHARED_LIBS ON)后,默认生成的为动态库。
  3. CMAKE_C_FLAGS 设置C编译选项,也可以通过指令ADD_DEFINITIONS()添加。
  4. CMAKE_CXX_FLAGS 设置C++编译选项,也可以通过指令ADD_DEFINITIONS()添加。

函数、宏、流程控制和选项 等命令

具体参考cmake-commands ,这里面包括了很多重要且常见的指令。

简单示例 CMake 中的函数操作:

1
2
3
4
5
6
7
function(add a b)
message("this is function call")
math(EXPR num "${a} + ${b}" )
message("result is ${aa}")
endfunction()

add(1 2)

其中,function 为定义函数,第一个参数为函数名称,后面为函数参数。

在调用函数时,参数之间用空格隔开,不要用逗号。

宏的使用与函数使用有点类似:

1
2
3
4
5
6
7
macro(del a b)
message("this is macro call")
math(EXPR num "${a} - ${b}")
message("num is ${num}")
endmacro()

del(1 2)

在流程控制方面,CMake 也提供了 if、else 这样的操作:

1
2
3
4
5
6
7
8
set(num 0)
if (1 AND ${num})
message("and operation")
elseif (1 OR ${num})
message("or operation")
else ()
message("not reach")
endif ()

其中,CMake 提供了 AND、OR、NOT、LESS、EQUAL 等等这样的操作来对数据进行判断,比如 AND 就是要求两边同为 True 才行。

另外 CMake 还提供了循环迭代的操作:

1
2
3
4
set(stringList this is string list)
foreach (str ${stringList})
message("str is ${str}")
endforeach ()

CMake 还提供了一个 option 指令。

可以通过它来给 CMake 定义一些全局选项:

1
2
3
4
5
6
7
option(ENABLE_SHARED "Build shared libraries" TRUE)

if(ENABLE_SHARED)
# do something
else()
# do something
endif()

可能会觉得 option 无非就是一个 True or False 的标志位,可以用变量来代替,但使用变量的话,还得添加 ${} 来表示变量,而使用 option 直接引用名称就好了。

CMake 阅读实践

明白了上述的 CMake 语法以及从官网去查找陌生的指令意思,就基本上可以看懂大部分的 CMake 文件了。 这里举两个开源库的例子:

  1. github.com/g-truc/glm
  • glm 是一个用来实现矩阵计算的,在 OpenGL 的开发中会用到。
  • CMakeLists.txt 地址在 这里
  1. github.com/libjpeg-tur
  • libjpeg-turbo 是用来进行图片压缩的,在 Android 底层就是用的它。
  • CMakeLists.txt 地址在 这里

这两个例子中大量用到了前面所讲的内容,可以试着读一读增加熟练度。

为编译的库设置属性

接下来再回到用 CMake 编译动态库的话题上,毕竟 Android NDK 开发也主要是用来编译库了,当编译完 so 之后,我们可以对它做一些操作。

通过 set_target_properties 来给编译的库设定相关属性内容,函数原型如下:

1
2
3
set_target_properties(target1 target2 ...
PROPERTIES prop1 value1
prop2 value2 ...)

比如,要将编译的库改个名称:

1
set_target_properties(native-lib PROPERTIES OUTPUT_NAME "testlib" )

更多的属性内容可以参考 官方文档。

不过,这里面有一些属性设定无效,在 Android Studio 上试了无效,在 CLion 上反而可以,当然也可能是我使用姿势不对。

比如,实现动态库的版本号:

1
set_target_properties(native-lib PROPERTIES VERSION 1.2 SOVERSION 1 )

对于已经编译好的动态库,想要把它导入进来,也需要用到一个属性。

比如编译的 FFmpeg 动态库,

1
2
3
4
5
# 使用 IMPORTED 表示导入库
add_library(avcodec-57_lib SHARED IMPORTED)
# 使用 IMPORTED_LOCATION 属性指定库的路径
set_target_properties(avcodec-57_lib PROPERTIES IMPORTED_LOCATION
${CMAKE_CURRENT_SOURCE_DIR}/src/main/jniLibs/armeabi/libavcodec-57.so )

链接到其他的库

如果编译了多个库,并且想库与库之间进行链接,那么就要通过 target_link_libraries 。

1
2
3
4
target_link_libraries( native-lib
glm
turbojpeg
log )

在 Android 底层也提供了一些 so 库供上层链接使用,也要通过上面的方式来链接,比如最常见的就是 log 库打印日志。 如果要链接自己编译的多个库文件,首先要保证每个库的代码都对应一个 CMakeLists.txt 文件,这个 CMakeLists.txt 文件指定当前要编译的库的信息。 然后在当前库的 CMakeLists.txt 文件中通过 ADD_SUBDIRECTORY 将其他库的目录添加进来,这样才能够链接到。

1
2
ADD_SUBDIRECTORY(src/main/cpp/turbojpeg)
ADD_SUBDIRECTORY(src/main/cpp/glm)

ADD_SUBDIRECTORY(src_dir [binary_dir] [EXCLUDE_FROM_ALL]) 向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制的存放位置 EXCLUDE_FROM_ALL含义:将这个目录从编译过程中排除 SET(EXECUTABLE_OUTPUT_PATH${PROJECT_BINARY_DIR}/bin)更改生成的可执行文件路径 SET(LIBRARY_OUTPUT_PATH${PROJECT_BINARY_DIR}/lib)更改生成的库文件路径

添加头文件

在使用的时候有一个容易忽略的步骤就是添加头文件,通过 include_directories 指令把头文件目录包含进来。 这样就可以直接使用 #include "header.h" 的方式包含头文件,而不用 #include "path/path/header.h" 这样添加路径的方式来包含。

区分debug、release版本

建立debug/release两目录,分别在其中执行cmake -D CMAKE_BUILD_TYPE=Debug(或Release),需要编译不同版本时进入不同目录执行make即可:

1
2
Debug版会使用参数-g;
Release版使用-O3–DNDEBUG

另一种设置方法——例如DEBUG版设置编译参数DDEBUG

1
2
3
IF(DEBUG_mode)
add_definitions(-DDEBUG)
ENDIF()

在执行cmake时增加参数即可,例如cmake -D DEBUG_mode=ON

设置条件编译

例如debug版设置编译选项DEBUG,并且更改不应改变CMakelist.txt 使用option command,eg:

1
2
3
4
option(DEBUG_mode"ON for debug or OFF for release" ON)
IF(DEBUG_mode)
add_definitions(-DDEBUG)
ENDIF()

使其生效的方法:首先cmake生成makefile,然后make edit_cache编辑编译选项;Linux下会打开一个文本框,可以更改,改完后再make生成目标文件——emacs不支持make edit_cache; 局限:这种方法不能直接设置生成的makefile,而是必须使用命令在make前设置参数;对于debug、release版本,相当于需要两个目录,分别先cmake一次,然后分别makeedit_cache一次; 期望的效果:在执行cmake时直接通过参数指定一个开关项,生成相应的makefile。

gerrit工具介绍

发表于 2019-04-04 | 分类于 工具

Gerrit的gerrit query命令就是要查询Gerrit的changes数据库。

默认,查询结果是根据changes的更新时间,由近及远排序。

对于有多个patch set的change,默认查询结果只包含最后的patch set。

如果查询结果有很大,则默认只返回有限个查询结果,可以设置limit:参数指定查询结果包含的changes数量。

1. gerrit query命令用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
ssh -p <port> <host> gerrit query
[--format {TEXT | JSON}]
[--current-patch-set]
[--patch-sets | --all-approvals]
[--files]
[--comments]
[--commit-message]
[--dependencies]
[--submit-records]
[--all-reviewers]
[--start <n> | -S <n>]
<query>
[limit:<n>]

常见选项说明:

  • –format=TEXT,默认
  • –format=JSON
  • –current-patch-set,给出当前patch set的信息
  • –patch-sets,给出所有patch set的信息
  • –commit-message,给出change的完整commit message
  • –all-reviewers,给出所有reviewer的name和email <query>说明:
  • status:open等价于status:pending, is:open, is:pending
  • owner:self等价于is:owner
  • reviewer:self等价于is:reviewer
  • project:bbauto/bba或p:bbauto/bba
  • projects:bb
  • branch:develop或branch:refs/heads/develop
  • change:2311176或change:I03369813660369e983b56dcabe4cb48839be4de0
  • commit:4e8ea8d43ab22273e4949348e1e7316f88cd54e3
  • ref:refs/changes/76/2311176/1
  • message:my_commit_message
  • is:visible
  • label:Code-Review=2或label:Code-Review=+2或label:Code-Review+2
  • label:Verified+1 补充:
  • 属性值除了bare words (数字、大小写字母和@-_.)之外,必须使用””或{}包含
  • 多个属性之间默认为and关系,还有or关系,或者取反not/-

查询Gerrit指定状态的patch set,并保存到文件。采用分段查看首先查看最近500条的,再查看最近500-1000条的。

alias gerrit="ssh -p 29418 gerrit.yourdomain.com gerrit" gerrit review <id> --abandon 放弃某个commit(id是commit id不是change id)

ssh -p 29418 192.168.1.127 gerrit review --submit a6b548272aa754857b4 提交某个commit 循环放弃脚本:

1
2
3
4
5
6
7
8
9
#!/bin/bash
alias gerrit="ssh -p 29418 gerrit.lianjia.com gerrit"

for c in $(gerrit query project:mars --start 500 --current-patch-set| grep "revision");do
if [ $c != 'revision:' ];then
gerrit review $c --abandon;
echo $c
fi
done

2.参考文献:

https://gerrit-documentation.storage.googleapis.com/Documentation/2.13.7/cmd-query.html https://gerrit-documentation.storage.googleapis.com/Documentation/2.13.7/json.html https://gerrit-documentation.storage.googleapis.com/Documentation/2.13.7/user-search.html

3.gerrit 不经代码审核直接push进库的方法

需求:

gerrit 代码审核将代码入库权限收起,可以有效控制代码质量.

但同时弊端也是明显的: 对于频繁改动的项目(比如新项目),每段代码都审核会明显拖慢工作效率.

这时可以给某个组配置不经审核直接push权限.

方法:

管理员账号,到 projects -> access 页面下配置 reference 权限. 其他 reference 的权限配置依旧, 给如下 reference 增加权限: refs/for/refs/heads/master 配置 submit 权限给某个具体的组即可.

该组用户需要忽略审核时,执行如下命令: git push origin HEAD:refs/for/master%submit 就是在原有 push 命令基础上加上 %submit 这个后缀即可.

tips-android-sqlite

发表于 2019-03-27
1
2
3
4
5
6
7
8
9
10
11
08-30 20:27:36.751 E/CursorWindow(  760): Could not allocate CursorWindow '/data/data/com.android.providers.media/databases/external.db' of size 2097152 due to error -12.
08-30 20:27:36.771 E/JavaBinder( 760): *** Uncaught remote exception! (Exceptions are not yet supported across processes.)
08-30 20:27:36.771 E/JavaBinder( 760): android.database.CursorWindowAllocationException: Cursor window allocation of 2048 kb failed. # Open Cursors=781 (# cursors opened by pid 3105=781)
08-30 20:27:36.771 E/JavaBinder( 760): at android.database.CursorWindow.<init>(CursorWindow.java:104)
08-30 20:27:36.771 E/JavaBinder( 760): at android.database.AbstractWindowedCursor.clearOrCreateWindow(AbstractWindowedCursor.java:198)
08-30 20:27:36.771 E/JavaBinder( 760): at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:162)
08-30 20:27:36.771 E/JavaBinder( 760): at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:156)
08-30 20:27:36.771 E/JavaBinder( 760): at android.database.CursorToBulkCursorAdaptor.count(CursorToBulkCursorAdaptor.java:184)
08-30 20:27:36.771 E/JavaBinder( 760): at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:117)
08-30 20:27:36.771 E/JavaBinder( 760): at android.os.Binder.execTransact(Binder.java:338)
08-30 20:27:36.771 E/JavaBinder( 760): at dalvik.system.NativeStart.run(Native Method)

错误原因 CursorWindow缓存数据达到最大限制(2M不同的机器和SQLite版本其值可能不同)后,仍有查询结果集需要缓存,在申请内存分配时申请失败发生了OOM内存溢出;SQLite查询出的数据集cursor,都由native层的CursorWindow进行数据管理,包括内存空间的申请和数据的填充。CursorWindow实际上是共享内存的抽象,以实现跨进程,跨应用数据共享(ContentProvider作为数据通道,也支持跨进程,跨应用的数据访问) 在ContentProvider端透过SQLiteDatabase的封装查询到的数据集保存在CursorWindow所指向的共享内存中,然后通过Binder把这片共享内存传递到ContentResolver端,即查询端。这样客户就可以通过Cursor来访问这块共享内存中的数据集了。 解决办法 保证CursorWindow不会达到最大限制):

  1. 只查询需要的字段;根据UI显示需要,或实际需要查询的字段进行查询,尽量不会表查询
  2. 二进制文件不要存在数据库中;数据库仅适用于保存一些较短文字,整数,布尔,浮点数等一些,易于查询和操作的轻量级的数据,目的也是在于快速搜索和查询。对于像图片,较长的文字(如文章)等大数据,最好直接以文件形式存储在硬盘中,然后在数据库保存它们的访问路径
  3. 对于大数据量的查询采用分段查询方式;无论表中的一条记录数据量如何的小,当条数达到5000级或者万级或者更多的时候,还是会达到最大的限制
  4. 正确的关闭Cursor,释放CursorWindow中不用的资源(需手动调用释放native中的资源,类似3.0之前的Bitmap需要手动释放。调用close的必要性:

从源码看ANDROID中SQLITE是怎么通过CURSORWINDOW读DB的

Sqlite性能优化

(1)编译SQL语句 Sqlite想要执行操作,需要将程序中的sql语句编译成对应的SQLiteStatement,比如select * from record这一句,被执行100次就需要编译100次。对于批量处理插入或者更新的操作,我们可以使用显示编译来做到重用SQLiteStatement。

想要做到重用SQLiteStatement也比较简单,基本如下:

编译sql语句获得SQLiteStatement对象,参数使用?代替 在循环中对SQLiteStatement对象进行具体数据绑定,bind方法中的index从1开始,不是0

如下向person表中插入100条数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void insertBatchPreCompile() {
long start = SystemClock.currentThreadTimeMillis();
String sql = "insert into " + TAB_PERSON + " values (?,'test','1');";
SQLiteStatement sqLiteStatement = getReadableDatabase().compileStatement(sql);
int count = 0;
while (count < 100) {
count++;
sqLiteStatement.clearBindings();
sqLiteStatement.bindLong(1, count);
sqLiteStatement.executeInsert();
}
Log.e(TAG, "insert recompile use time " + (SystemClock.currentThreadTimeMillis() - start));
}

(2)显示使用事务 在Android中,无论是使用SQLiteDatabase的insert,delete等方法还是execSQL都开启了事务,来确保每一次操作都具有原子性,使得结果要么是操作之后的正确结果,要么是操作之前的结果。

然而事务的实现是依赖于名为rollback journal文件,借助这个临时文件来完成原子操作和回滚功能。既然属于文件,就符合Unix的文件范型(Open-Read/Write- Close),因而对于批量的修改操作会出现反复打开文件读写再关闭的操作。然而好在,我们可以显式使用事务,将批量的数据库更新带来的journal文件打开关闭降低到1次。

具体的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void insertWithTransaction() {
long start = SystemClock.currentThreadTimeMillis();
int count = 0;
try {
getWritableDatabase().beginTransaction();
while (count++ < 100) {
insert(count, "test", 1);
}
getWritableDatabase().setTransactionSuccessful();
} catch (Exception e) {
e.printStackTrace();
} finally {
getWritableDatabase().endTransaction();
}
Log.e(TAG, "insert traceaction use time " + (SystemClock.currentThreadTimeMillis() - start));
}

使用这两种方式分别优化,对比效果如下:

从图中可以看到在插入100条的情况下,使用预编译的方式可以稍微提升性能,但是使用事务,能够使性能提升大概8倍,所以可以看出频繁的IO操作还是比较耗时的。同时使用两种方式进行优化,可以提升17倍,优化效果非常明显。

(3)建立索引 a.索引的概念 索引,使用索引可快速访问数据库表中的特定信息。索引是对数据库表中一列或多列的值进行排序的一种结构。

在关系数据库中,索引是一种与表有关的数据库结构,它可以使对应于表的SQL语句执行得更快。索引的作用相当于图书的目录,可以根据目录中的页码快速找到所需的内容。当表中有大量记录时,若要对表进行查询,第一种搜索信息方式是全表搜索,是将所有记录一一取出,和查询条件进行一一对比,然后返回满足条件的记录,这样做会消耗大量数据库系统时间,并造成大量磁盘I/O操作;第二种就是在表中建立索引,然后在索引中找到符合查询条件的索引值,最后通过保存在索引中的ROWID(相当于页码)快速找到表中对应的记录。 索引是一个单独的、物理的数据库结构,它是某个表中一列或若干列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。

索引提供指向存储在表的指定列中的数据值的指针,然后根据您指定的排序顺序对这些指针排序。数据库使用索引的方式与您使用书籍中的索引的方式很相似:它搜索索引以找到特定值,然后顺指针找到包含该值的行。

b.建立索引 创建索引的基本语法:

CREATE INDEX index_name ON table_name; 1 创建单列索引

CREATE INDEX index_name ON table_name; 1 c.索引的利弊 毋庸置疑,索引加速了我们检索数据表的速度。然而正如西方谚语 “There are two sides of a coin”,索引亦有缺点:

对于增加,更新和删除来说,使用了索引会变慢,比如你想要删除字

列表内容典中的一个字,那么你同时也需要删除这个字在拼音索引和部首索引中的信息。 建立索引会增加数据库的大小,比如字典中的拼音索引和部首索引实际上是会增加字典的页数,让字典变厚的。 为数据量比较小的表建立索引,往往会事倍功半。 所以使用索引需要考虑实际情况进行利弊权衡,对于查询操作量级较大,业务对要求查询要求较高的,还是推荐使用索引的。

(4)查询数据优化 按需获取列信息

db.query(TableDefine.TABLE_RECORD, null, null, null, null, null, null) ; 1 其中上面方法的第二个参数类型为String[],意思是返回结果参考的colum信息,传递null表明需要获取全部的column数据。如果我们不需要所有列的信息,最好指定一下需要的列。

提前获取索引 例如下面的代码,我们可以把获取列索引的代码cursor.getColumnIndex(TableDefine.COLUMN_INSERT_TIME)放到循环外,这样不需要每次获取。

1
2
3
4
5
6
 private void badQueryWithLoop(SQLiteDatabase db) {
Cursor cursor = db.query(TableDefine.TABLE_RECORD, new String[]{TableDefine.COLUMN_INSERT_TIME}, null, null, null, null, null) ;
while (cursor.moveToNext()) {
long insertTime = cursor.getLong(cursor.getColumnIndex(TableDefine.COLUMN_INSERT_TIME));
}
}

(5)ContentValues的容量调整 SQLiteDatabase提供了方便的ContentValues简化了我们处理列名与值的映射,ContentValues内部采用了 HashMap来存储Key-Value数据,ContentValues的初始容量是8,如果当添加的数据超过8之前,则会进行双倍扩容操作,因此建议对ContentValues填入的内容进行估量,设置合理的初始化容量,减少不必要的内部扩容操作。

(6)及时关闭Cursor (7)耗时异步化 数据库的操作,属于本地IO,通常比较耗时,如果处理不好,很容易导致ANR,因此建议将这些耗时操作放入异步线程中处理

Android 性能优化之数据库优化(一)

tips-android-file

发表于 2019-03-27

文件句柄泄露

1
2
3
4
5
6
7
8
9
10
11
10-27 00:35:32.141  7437  7437 E AndroidRuntime: FATAL EXCEPTION: main

10-27 00:35:32.141 7437 7437 E AndroidRuntime: Process: com.Android56, PID: 7437

10-27 00:35:32.141 7437 7437 E AndroidRuntime: java.lang.RuntimeException: Could not read input channel file descriptors from parcel.

10-27 00:35:32.141 7437 7437 E AndroidRuntime: at android.view.InputChannel.nativeReadFromParcel(Native Method)

10-27 00:35:32.141 7437 7437 E AndroidRuntime: at android.view.InputChannel.readFromParcel(InputChannel.java:148)

10-27 00:35:32.141 7437 7437 E AndroidRuntime: at android.view.InputChannel$1.createFromParcel(InputChannel.java:39)

这里有一句Could not read input channel file descriptors from parcel,然后我们在这句话的上面又发现一个有价值的信息。

1
Caused by: java.io.IOException: Too many open files

通过网上搜索,基本判断这是一个文件句柄泄漏的问题。 那么我们该如何查找文件句柄泄漏的地方呢。 首先我们需要做到监控文件句柄数,由于android是linux的内核,所以,系统为每一个进程都有一个文件句柄的目录。 我们先通过ps命令,获取到我们app的进程id。 然后找到一个root过的手机,或者使用andorid模拟器,然后用adb连接到手机,通过shell命令进入到/proc/进程id/fd这个目录。由于linux关于系统的管理都是用文件方式,所以这个文件夹下面就是所有被打开的句柄。 我们可以在app运行的过程中,不断的进入到这个目录中,然后用ls -l 命令列出所有的文件句柄,这样就能看到文件句柄有哪些是增长的。然后再根据不同类型的文件句柄,初步定位是什么在泄漏。 在排查的过程中,为了方便获取某个进程的句柄数,我写了一个简单的shell脚本。有需要的同学可以拿去使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
echo '查询进程占用文件句柄数'
set `adb shell ps |grep com.Android56 |grep -v channel |grep -v Daemon`
pidnum=$2
index=0
while true
do
index=$[index+1]
echo '##################'
echo '第'$index'次查询'
echo '总句柄'
adb shell ls -l /proc/$pidnum/fd |grep "" -c
echo 'anon句柄'
adb shell ls -l /proc/$pidnum/fd |grep anon -c
echo '##################'
sleep 2
done

markdown语法

发表于 2019-03-26 | 分类于 工具

Markdown Preview Enhanced 内部支持 mermaid, PlantUML, WaveDrom, GraphViz,Vega & Vega-lite,Ditaa 图像渲染。 你也可以通过使用 Code Chunk 来渲染 TikZ, Python Matplotlib, Plotly 等图像。

基本流程图

Markdown常用的元素有以下几种:

  • start
  • end
  • operation
  • condition
  • inputoutput
  • subroutine

以简单的登录场景为例,流程图代码

1
2
3
4
5
6
7
8
9
10
11
start=>start: 开始
loginInfo=>inputoutput: 登录数据
verifyLogin=>subroutine: 登录验证
isSuccess=>condition: 验证成功?
respondSuccess=>operation: 响应成功
responseFailure=>operation: 响应失败
end=>end: 结束

start->loginInfo->verifyLogin->isSuccess
isSuccess(yes)->respondSuccess->end
isSuccess(no)->responseFailure->end

注意:冒号和名称之间需要有一个空格。

方向调整 绘制流程图有时会出现比较一言难尽的情况

1
2
3
4
5
6
7
8
9
10
11
start=>start: start
operation1=>operation: operation1
isSuccess=>condition: success?
operation2=>operation: operation2
operation3=>operation: operation3
operation4=>operation: operation4
end=>end: 结束

start->operation1->isSuccess
isSuccess(yes)->operation2->end
isSuccess(no)->operation3->operation4->operation1

这种情况下可以使用left、right和bottom关键字来调整线条的位置使流程图更加清晰,例如此处给operation4元素添加right关键字,就可以分离重叠的线条。

1
2
3
4
5
6
7
8
9
10
11
start=>start: start
operation1=>operation: operation1
isSuccess=>condition: success?
operation2=>operation: operation2
operation3=>operation: operation3
operation4=>operation: operation4
end=>end: 结束

start->operation1->isSuccess
isSuccess(yes)->operation2->end
isSuccess(no)->operation3->operation4(right)->operation1

如果给condition元素添加这些关键字的话会调整整个分支的方向:

1
2
3
4
5
6
7
8
9
10
11
start=>start: start
operation1=>operation: operation1
isSuccess=>condition: success?
operation2=>operation: operation2
operation3=>operation: operation3
operation4=>operation: operation4
end=>end: 结束

start->operation1->isSuccess
isSuccess(yes)->operation2->end
isSuccess(no,left)->operation3->operation4(left)->operation1

状态标记 Markdown会使用不同的颜色来标记状态,状态主要有以下几种:

  • past
  • current
  • future
  • approved
  • rejected
  • invalid

以软件生命周期的一部分为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
start=>start: 开始|past
requirementAnalysis=>operation: 需求分析|past
design=>operation: 软件设计|past
coding=>operation: 编码|past
selfTestingPased=>condition: 自测通过?|approved
debug=>operation: debug|invalid
submitTestingPased=>condition: 提测通过?|rejected
modifyBug=>operation: 修bug|current
deploy=>operation: 部署|future
end=>end: 结束|future

start->requirementAnalysis->design->coding->selfTestingPased
selfTestingPased(no)->debug(right)->selfTestingPased
selfTestingPased(yes)->submitTestingPased
submitTestingPased(yes)->deploy->end
submitTestingPased(no)->modifyBug(right)->submitTestingPased

箭头高亮 可以通过高亮某些箭头来标记出主流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
start=>start: 开始
loginInfo=>inputoutput: 登录数据
verifyLogin=>subroutine: 登录验证
isSuccess=>condition: 验证成功?
respondSuccess=>operation: 响应成功
responseFailure=>operation: 响应失败
end=>end: 结束

start->loginInfo->verifyLogin->isSuccess
isSuccess(yes)->respondSuccess->end
isSuccess(no)->responseFailure->end

start@>loginInfo({"stroke":"Red"})@>verifyLogin({"stroke":"Red"})@>isSuccess({"stroke":"Red"})@>respondSuccess({"stroke":"Red"})@>end({"stroke":"Red","stroke-width":6,"arrow-end":"classic-wide-long"})

流程图语法详解

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
## 操作块(格式为:变量=>操作块: 备注名)
st=> start: 开始
e=>end: 结束
#普通操作块 opration
op1=>opration: 第一个操作块
op2=>opration: 第二个操作块
#判断块 condition
cond1=>condition: 第一个判断
cond2=>condition: 第二个判断

#输入输出块 inputoutput[平行四边形]
io1=>inputoutput: 输入输出块1
io2=>inputoutput: 输入输出块2
#子任务块
sub1=>subroutine: 子任务1
sub2=>subroutine: 子任务2

## 判断和位置控制
#判断流程控制
cond1(yes)->op1 #yes 的时候回到 op1
cond1(no)->e #no 的时候 去结束

#位置指定
cond1(no)->op2(right)->op1 #控制 op2 位置置于右边,再由op2 返回 op1 (好像不能向左)
#还可以这样 cond1(no,right)
cond1(yes)->e

## 流程控制
#分着写
st->op1
op1->e

#合着写
st->op1->e

#判断也是一样:
st->cond
cond(yes)->io
cond(no)->op1

时序图

注意:其实时序图使用platuml也可以画的很美观 platuml-sequence

  • sequence代码块中的内容将会被js-sequence-diagrams渲染
  • 支持两个主题simple(default)和hand
1
2
3
4
5
6
bgbiao-> bianbian: good morning
note left of bgbiao: man
bianbian -> bgbiao: eat something
note right of bianbian: woman

note over bgbiao: test

Mermaid

Mermaid可以用来渲染流程图和时序图

  • mermaid代码块中的内容将会被渲染mermaid图像
  • mermaid-docs图像文档

注意:{code_block=true}会影藏图像 Mermaid 是一个用于画流程图、状态图、时序图、甘特图的库,使用 JS 进行本地渲染,广泛集成于许多 Markdown 编辑器中。

定义节点 |表述|说明| |—|—| |id[文字]| 矩形节点| |id(文字)| 圆角矩形节点| |id((文字)) |圆形节点| |id>文字]| 旗帜状节点| |id{文字}| 菱形节点| 定义连线 |表述| 说明| |—|—| |>| 添加尾部箭头| |-| 不添加尾部箭头| |– |单线| |–text–| 单线上加文字| |==| 粗线| |==text==| 粗线加文字| |-.-| 虚线| |-.text.-| 虚线加文字|

流程图方向有下面几个值

  • TB 从上到下
  • BT 从下到上
  • RL 从右到左
  • LR 从左到右
  • TD 同TB

子流程图 格式

1
2
3
subgraph title
graph definition
end

示例:

1
2
3
4
5
6
7
8
9
10
11
graph TB
c1-->a2
subgraph one
a1-->a2
end
subgraph two
b1-->b2
end
subgraph three
c1-->c2
end

自定义样式

语法:style id 具体样式

1
2
3
4
graph LR
id1(Start)-->id2(Stop)
style id1 fill:#f9f,stroke:#333,stroke-width:4px,fill-opacity:0.5
style id2 fill:#ccf,stroke:#f66,stroke-width:2px,stroke-dasharray: 10,5

plantuml创建各种图形

  • 可以安装graphviz来辅助生成各种图形
  • puml或plantuml代码中的内容将会被PlantUML渲染 注意:也可以为图像的容器添加属性,例如居中{align=”center”}

使用graphviz来绘制各种图形

使用Viz.js来渲染dot语言图形。

  • viz或者dot代码块中的内容将会被Viz.js渲染
  • 可以通过{engine=”…”}来选择不同的渲染引擎。引擎circo,dot,neato,osage,或者twopi.

注意:默认的dot是二叉树,twopi是依赖树,两种常用的类型

vega和vega-lite的支持的静态图像

  • vega代码块中的内容会被vega渲染
  • vega-lite代码中的内容会被vega-lite渲染
  • 支持JSON和YAML的数据源输入 vega-lite-docs vega-docs
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    {
    "$schema": "https://vega.github.io/schema/vega-lite/v2.json",
    "description": "A simple bar chart with embedded data.",
    "data": {
    "values": [
    {"a": "A","b": 28}, {"a": "B","b": 55}, {"a": "C","b": 43},
    {"a": "D","b": 91}, {"a": "E","b": 81}, {"a": "F","b": 53},
    {"a": "G","b": 19}, {"a": "H","b": 87}, {"a": "I","b": 52}
    ]
    },
    "mark": "bar",
    "encoding": {
    "x": {"field": "a", "type": "ordinal"},
    "y": {"field": "b", "type": "quantitative"}
    }
    }

https://shd101wyy.github.io/markdown-preview-enhanced/#/zh-cn/diagrams

hexo中支持流程图

  1. hexo-filter-mermaid-diagrams: brew install yarn 进入到博客的根目录,一定要在博客的根目录执行yarn add hexo-filter-mermaid-diagrams 执行完成后在博客根目录下的node_modules种看下有没有hexo-filter-mermaid-diagrams这个插件文件夹,如果没有,说明没安装成功,安装成功后执行下一步,打开博客根目录下面的_config.yml文件,在底部插入以下代码:
    1
    2
    3
    4
    5
    6
    # mermaid chart
    mermaid: ## mermaid url https://github.com/knsv/mermaid
    enable: true # default true
    version: "7.1.2" # default v7.1.2
    options: # find more api options from https://github.com/knsv/mermaid/blob/master/src/mermaidAPI.js
    #startOnload: true // default true

完成上一步操作之后打开主题目录的themes/next/layout/_partials/footer.swig,这里因为我用的是next主题,其他主题应该大同小异,在footer.swig文件最后加上以下代码:

1
2
3
4
5
6
7
8
{% if theme.mermaid.enable %}
<script src='https://unpkg.com/mermaid@{{ theme.mermaid.version }}/dist/mermaid.min.js'></script>
<script>
if (window.mermaid) {
mermaid.initialize({theme: 'forest'});
}
</script>
{% endif %}

添加完成后,回到博客根目录的_config.yml,把external_link的值改为false,默认的为true,这一步绝大多数教程中都没有写 Hexo中引入Mermaid流程图:点击查看 hexo-filter-mermaid-diagrams插件开发杂谈:点击查看 插件官方位置:点击查看 插件官方使用教程:点击查看

  1. hexo-filter-flowchart npm install –save hexo-filter-flowchart
  2. hexo-filter-sequence npm install –save hexo-filter-sequence

tips-net-socket

发表于 2019-03-13
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
FCNTL(2)                    BSD System Calls Manual                   FCNTL(2)

NAME
fcntl -- file control

SYNOPSIS
#include <fcntl.h>

int
fcntl(int fildes, int cmd, ...);

DESCRIPTION
fcntl() provides for control over descriptors. The argument fildes is a descriptor to be operated on
by cmd as follows:

F_DUPFD Return a new descriptor as follows:

o Lowest numbered available descriptor greater than or equal to arg.
o Same object references as the original descriptor.
o New descriptor shares the same file offset if the object was a file.
o Same access mode (read, write or read/write).
o Same file status flags (i.e., both file descriptors share the same file
status flags).
o The close-on-exec flag associated with the new file descriptor is cleared
so that the descriptor remains open across an execv(2) system call.

F_DUPFD_CLOEXEC Like F_DUPFD, except that the close-on-exec flag associated with the new file
descriptor is set.

F_GETFD Get the flags associated with the file descriptor fildes, as described below (arg
is ignored).

F_SETFD Set the file descriptor flags to arg.

F_GETFL Get descriptor status flags, as described below (arg is ignored).

F_SETFL Set descriptor status flags to arg.

F_GETOWN Get the process ID or process group currently receiving SIGIO and SIGURG signals;
process groups are returned as negative values (arg is ignored).

F_SETOWN Set the process or process group to receive SIGIO and SIGURG signals; process
groups are specified by supplying arg as negative, otherwise arg is interpreted as
a process ID.

F_GETPATH Get the path of the file descriptor Fildes. The argument must be a buffer of size
MAXPATHLEN or greater.

F_PREALLOCATE Preallocate file storage space. Note: upon success, the space that is allocated
can be the size requested, larger than the size requested, or (if the
F_ALLOCATEALL flag is not provided) smaller than the space requested.

F_PUNCHHOLE Deallocate a region and replace it with a hole. Subsequent reads of the affected
region will return bytes of zeros that are usually not backed by physical blocks.
This will not change the actual file size. Holes must be aligned to file system
block boundaries. This will fail on file systems that do not support this inter-
face.

F_SETSIZE Truncate a file without zeroing space. The calling process must have root privi-
leges.

F_RDADVISE Issue an advisory read async with no copy to user.

F_RDAHEAD Turn read ahead off/on. A zero value in arg disables read ahead. A non-zero
value in arg turns read ahead on.

F_READBOOTSTRAP Read bootstrap from disk.

F_WRITEBOOTSTRAP Write bootstrap on disk. The calling process must have root privileges.

F_NOCACHE Turns data caching off/on. A non-zero value in arg turns data caching off. A
value of zero in arg turns data caching on.

F_LOG2PHYS Get disk device information. Currently this only returns the disk device address
that corresponds to the current file offset. Note that the system may return -1 as
the disk device address if the file is not backed by physical blocks. This is sub-
ject to change.

F_LOG2PHYS_EXT Variant of F_LOG2PHYS that uses the passed in file offset and length.

F_FULLFSYNC Does the same thing as fsync(2) then asks the drive to flush all buffered data to
the permanent storage device (arg is ignored). This is currently implemented on
HFS, MS-DOS (FAT), and Universal Disk Format (UDF) file systems. The operation
may take quite a while to complete. Certain FireWire drives have also been known
to ignore the request to flush their buffered data.

F_SETNOSIGPIPE Determines whether a SIGPIPE signal will be generated when a write fails on a pipe
or socket for which there is no reader. If arg is non-zero, SIGPIPE generation is
disabled for descriptor fildes, while an arg of zero enables it (the default).

F_GETNOSIGPIPE Returns whether a SIGPIPE signal will be generated when a write fails on a pipe or
socket for which there is no reader. The semantics of the return value match
those of the arg of F_SETNOSIGPIPE.

The flags for the F_GETFD and F_SETFD commands are as follows:

FD_CLOEXEC Close-on-exec; the given file descriptor will be automatically closed in the suc-
cessor process image when one of the execv(2) or posix_spawn(2) family of system
calls is invoked.

The flags for the F_GETFL and F_SETFL commands are as follows:

O_NONBLOCK Non-blocking I/O; if no data is available to a read call, or if a write operation
would block, the read or write call returns -1 with the error EAGAIN.

O_APPEND Force each write to append at the end of file; corresponds to the O_APPEND flag of
open(2).

O_ASYNC Enable the SIGIO signal to be sent to the process group when I/O is possible,
e.g., upon availability of data to be read.

Several commands are available for doing advisory file locking; they all operate on the following
structure:

struct flock {
off_t l_start; /* starting offset */
off_t l_len; /* len = 0 means until end of file */
pid_t l_pid; /* lock owner */
short l_type; /* lock type: read/write, etc. */
short l_whence; /* type of l_start */
};

The commands available for advisory record locking are as follows:

F_GETLK Get the first lock that blocks the lock description pointed to by the third argument, arg,
taken as a pointer to a struct flock (see above). The information retrieved overwrites
the information passed to fcntl in the flock structure. If no lock is found that would
prevent this lock from being created, the structure is left unchanged by this function
call except for the lock type which is set to F_UNLCK.

F_SETLK Set or clear a file segment lock according to the lock description pointed to by the third
argument, arg, taken as a pointer to a struct flock (see above). F_SETLK is used to
establish shared (or read) locks (F_RDLCK) or exclusive (or write) locks, (F_WRLCK), as
well as remove either type of lock (F_UNLCK). If a shared or exclusive lock cannot be
set, fcntl returns immediately with EAGAIN.

F_SETLKW This command is the same as F_SETLK except that if a shared or exclusive lock is blocked
by other locks, the process waits until the request can be satisfied. If a signal that is
to be caught is received while fcntl is waiting for a region, the fcntl will be inter-
rupted if the signal handler has not specified the SA_RESTART (see sigaction(2)).

When a shared lock has been set on a segment of a file, other processes can set shared locks on that
segment or a portion of it. A shared lock prevents any other process from setting an exclusive lock
on any portion of the protected area. A request for a shared lock fails if the file descriptor was
not opened with read access.

An exclusive lock prevents any other process from setting a shared lock or an exclusive lock on any
portion of the protected area. A request for an exclusive lock fails if the file was not opened with
write access.

The value of l_whence is SEEK_SET, SEEK_CUR, or SEEK_END to indicate that the relative offset,
l_start bytes, will be measured from the start of the file, current position, or end of the file,
respectively. The value of l_len is the number of consecutive bytes to be locked. If l_len is nega-
tive, the result is undefined. The l_pid field is only used with F_GETLK to return the process ID of
the process holding a blocking lock. After a successful F_GETLK request, the value of l_whence is
SEEK_SET.

Locks may start and extend beyond the current end of a file, but may not start or extend before the
beginning of the file. A lock is set to extend to the largest possible value of the file offset for
that file if l_len is set to zero. If l_whence and l_start point to the beginning of the file, and
l_len is zero, the entire file is locked. If an application wishes only to do entire file locking,
the flock(2) system call is much more efficient.

There is at most one type of lock set for each byte in the file. Before a successful return from an
F_SETLK or an F_SETLKW request when the calling process has previously existing locks on bytes in the
region specified by the request, the previous lock type for each byte in the specified region is
replaced by the new lock type. As specified above under the descriptions of shared locks and exclu-
sive locks, an F_SETLK or an F_SETLKW request fails or blocks respectively when another process has
existing locks on bytes in the specified region and the type of any of those locks conflicts with the
type specified in the request.

This interface follows the completely stupid semantics of System V and IEEE Std 1003.1-1988
(``POSIX.1'') that require that all locks associated with a file for a given process are removed when
any file descriptor for that file is closed by that process. This semantic means that applications
must be aware of any files that a subroutine library may access. For example if an application for
updating the password file locks the password file database while making the update, and then calls
getpwname(3) to retrieve a record, the lock will be lost because getpwname(3) opens, reads, and
closes the password database. The database close will release all locks that the process has associ-
ated with the database, even if the library routine never requested a lock on the database. Another
minor semantic problem with this interface is that locks are not inherited by a child process created
using the fork(2) function. The flock(2) interface has much more rational last close semantics and
allows locks to be inherited by child processes. Flock(2) is recommended for applications that want
to ensure the integrity of their locks when using library routines or wish to pass locks to their
children. Note that flock(2) and fcntl(2) locks may be safely used concurrently.

All locks associated with a file for a given process are removed when the process terminates.

A potential for deadlock occurs if a process controlling a locked region is put to sleep by attempt-
ing to lock the locked region of another process. This implementation detects that sleeping until a
locked region is unlocked would cause a deadlock and fails with an EDEADLK error.







The F_PREALLOCATE command operates on the following structure:

typedef struct fstore {
u_int32_t fst_flags; /* IN: flags word */
int fst_posmode; /* IN: indicates offset field */
off_t fst_offset; /* IN: start of the region */
off_t fst_length; /* IN: size of the region */
off_t fst_bytesalloc; /* OUT: number of bytes allocated */
} fstore_t;

The flags (fst_flags) for the F_PREALLOCATE command are as follows:

F_ALLOCATECONTIG Allocate contiguous space.

F_ALLOCATEALL Allocate all requested space or no space at all.

The position modes (fst_posmode) for the F_PREALLOCATE command indicate how to use the offset field.
The modes are as follows:

F_PEOFPOSMODE Allocate from the physical end of file.

F_VOLPOSMODE Allocate from the volume offset.

The F_PUNCHHOLE command operates on the following structure:

typedef struct fpunchhole {
u_int32_t fp_flags; /* unused */
u_int32_t reserved; /* (to maintain 8-byte alignment) */
off_t fp_offset; /* IN: start of the region */
off_t fp_length; /* IN: size of the region */
} fpunchhole_t;

The F_RDADVISE command operates on the following structure which holds information passed from the
user to the system:

struct radvisory {
off_t ra_offset; /* offset into the file */
int ra_count; /* size of the read */
};

The F_READBOOTSTRAP and F_WRITEBOOTSTRAP commands operate on the following structure.

typedef struct fbootstraptransfer {
off_t fbt_offset; /* IN: offset to start read/write */
size_t fbt_length; /* IN: number of bytes to transfer */
void *fbt_buffer; /* IN: buffer to be read/written */
} fbootstraptransfer_t;

The F_LOG2PHYS command operates on the following structure:

struct log2phys {
u_int32_t l2p_flags; /* unused so far */
off_t l2p_contigbytes; /* unused so far */
off_t l2p_devoffset; /* bytes into device */
};

The F_LOG2PHYS_EXT command operates on the same structure as F_LOG2PHYS but treats it as an in/out:

struct log2phys {
u_int32_t l2p_flags; /* unused so far */
off_t l2p_contigbytes; /* IN: number of bytes to be queried;
OUT: number of contiguous bytes allocated at this position */
off_t l2p_devoffset; /* IN: bytes into file;
OUT: bytes into device */
};

If fildes is a socket, then the F_SETNOSIGPIPE and F_GETNOSIGPIPE commands are directly analogous,
and fully interoperate with the SO_NOSIGPIPE option of setsockopt(2) and getsockopt(2) respectively.

RETURN VALUES
Upon successful completion, the value returned depends on cmd as follows:

F_DUPFD A new file descriptor.

F_GETFD Value of flag (only the low-order bit is defined).

F_GETFL Value of flags.

F_GETOWN Value of file descriptor owner.

other Value other than -1.

Otherwise, a value of -1 is returned and errno is set to indicate the error.

ERRORS
The fcntl() system call will fail if:

[EAGAIN] The argument cmd is F_SETLK, the type of lock (l_type) is a shared lock (F_RDLCK)
or exclusive lock (F_WRLCK), and the segment of a file to be locked is already
exclusive-locked by another process; or the type is an exclusive lock and some
portion of the segment of a file to be locked is already shared-locked or exclu-
sive-locked by another process.

[EACCESS] The argument cmd is either F_SETSIZE or F_WRITEBOOTSTRAP and the calling process
does not have root privileges.

[EBADF] Fildes is not a valid open file descriptor.

The argument cmd is F_SETLK or F_SETLKW, the type of lock (l_type) is a shared
lock (F_RDLCK), and fildes is not a valid file descriptor open for reading.

The argument cmd is F_SETLK or F_SETLKW, the type of lock (l_type) is an exclusive
lock (F_WRLCK), and fildes is not a valid file descriptor open for writing.

The argument cmd is F_PREALLOCATE and the calling process does not have file write
permission.

The argument cmd is F_LOG2PHYS or F_LOG2PHYS_EXT and fildes is not a valid file
descriptor open for reading.

[EDEADLK] The argument cmd is F_SETLKW, and a deadlock condition was detected.

[EINTR] The argument cmd is F_SETLKW, and the function was interrupted by a signal.

[EINVAL] Cmd is F_DUPFD and arg is negative or greater than the maximum allowable number
(see getdtablesize(2)).

The argument cmd is F_GETLK, F_SETLK, or F_SETLKW and the data to which arg points
is not valid, or fildes refers to a file that does not support locking.

The argument cmd is F_PREALLOCATE and the fst_posmode is not a valid mode, or when
F_PEOFPOSMODE is set and fst_offset is a non-zero value, or when F_VOLPOSMODE is
set and fst_offset is a negative or zero value.

The argument cmd is F_PUNCHHOLE and either fp_offset or fp_length are negative, or
both fp_offset and fp_length are not multiples of the file system block size.

The argument cmd is either F_READBOOTSTRAP or F_WRITEBOOTSTRAP and the operation
was attempted on a non-HFS disk type.

[EMFILE] Cmd is F_DUPFD and the maximum allowed number of file descriptors are currently
open.

[EMFILE] The argument cmd is F_DUPED and the maximum number of file descriptors permitted
for the process are already in use, or no file descriptors greater than or equal
to arg are available.

[ENOLCK] The argument cmd is F_SETLK or F_SETLKW, and satisfying the lock or unlock request
would result in the number of locked regions in the system exceeding a system-
imposed limit.

[ENOSPC] The argument cmd is F_PREALLOCATE and either there is no space available on the
volume containing fildes or fst_flags contains F_ALLOCATEALL and there is not
enough space available on the volume containing fildes to satisfy the entire
request.

The argument cmd is F_PUNCHHOLE and there is not enough space available on the
volume containing fildes to satisfy the request. As an example, a filesystem that
supports cloned files may return this error if punching a hole requires the cre-
ation of a clone and there is not enough space available to do so.

[EOVERFLOW] A return value would overflow its representation. For example, cmd is F_GETLK,
F_SETLK, or F_SETLKW and the smallest (or, if l_len is non-zero, the largest) off-
set of a byte in the requested segment will not fit in an object of type off_t.

[EPERM] The argument cmd is F_PUNCHHOLE and the calling process does not have file write
permission.

[ESRCH] Cmd is F_SETOWN and the process ID given as argument is not in use.

SEE ALSO
close(2), execve(2), flock(2), getdtablesize(2), open(2), pipe(2), socket(2), setsockopt(2),
sigaction(3)

HISTORY
The fcntl() function call appeared in 4.2BSD.

4.2 Berkeley Distribution August 24, 2017 4.2 Berkeley Distribution
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

GETSOCKOPT(2) BSD System Calls Manual GETSOCKOPT(2)

NAME
getsockopt, setsockopt -- get and set options on sockets

SYNOPSIS
#include <sys/socket.h>

int
getsockopt(int socket, int level, int option_name, void *restrict option_value,
socklen_t *restrict option_len);

int
setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);

DESCRIPTION
getsockopt() and setsockopt() manipulate the options associated with a socket. Options may exist at
multiple protocol levels; they are always present at the uppermost ``socket'' level.

When manipulating socket options the level at which the option resides and the name of the option
must be specified. To manipulate options at the socket level, level is specified as SOL_SOCKET. To
manipulate options at any other level the protocol number of the appropriate protocol controlling the
option is supplied. For example, to indicate that an option is to be interpreted by the TCP proto-
col, level should be set to the protocol number of TCP; see getprotoent(3).

The parameters option_value and option_len are used to access option values for setsockopt(). For
getsockopt() they identify a buffer in which the value for the requested option(s) are to be
returned. For getsockopt(), option_len is a value-result parameter, initially containing the size of
the buffer pointed to by option_value, and modified on return to indicate the actual size of the
value returned. If no option value is to be supplied or returned, option_value may be NULL.

option_name and any specified options are passed uninterpreted to the appropriate protocol module for
interpretation. The include file <sys/socket.h> contains definitions for socket level options,
described below. Options at other protocol levels vary in format and name; consult the appropriate
entries in section 4 of the manual.

Most socket-level options utilize an int parameter for option_value. For setsockopt(), the parameter
should be non-zero to enable a boolean option, or zero if the option is to be disabled. SO_LINGER
uses a struct linger parameter, defined in <sys/socket.h>, which specifies the desired state of the
option and the linger interval (see below). SO_SNDTIMEO and SO_RCVTIMEO use a struct timeval parame-
ter, defined in <sys/time.h>.

The following options are recognized at the socket level. Except as noted, each may be examined with
getsockopt() and set with setsockopt().

SO_DEBUG enables recording of debugging information
SO_REUSEADDR enables local address reuse
SO_REUSEPORT enables duplicate address and port bindings
SO_KEEPALIVE enables keep connections alive
SO_DONTROUTE enables routing bypass for outgoing messages
SO_LINGER linger on close if data present
SO_BROADCAST enables permission to transmit broadcast messages
SO_OOBINLINE enables reception of out-of-band data in band
SO_SNDBUF set buffer size for output
SO_RCVBUF set buffer size for input
SO_SNDLOWAT set minimum count for output
SO_RCVLOWAT set minimum count for input
SO_SNDTIMEO set timeout value for output
SO_RCVTIMEO set timeout value for input
SO_TYPE get the type of the socket (get only)
SO_ERROR get and clear error on the socket (get only)
SO_NOSIGPIPE do not generate SIGPIPE, instead return EPIPE
SO_NREAD number of bytes to be read (get only)
SO_NWRITE number of bytes written not yet sent by the protocol (get only)
SO_LINGER_SEC linger on close if data present with timeout in seconds

SO_DEBUG enables debugging in the underlying protocol modules.

SO_REUSEADDR indicates that the rules used in validating addresses supplied in a bind(2) call should
allow reuse of local addresses.

SO_REUSEPORT allows completely duplicate bindings by multiple processes if they all set SO_REUSEPORT
before binding the port. This option permits multiple instances of a program to each receive UDP/IP
multicast or broadcast datagrams destined for the bound port.

SO_KEEPALIVE enables the periodic transmission of messages on a connected socket. Should the con-
nected party fail to respond to these messages, the connection is considered broken and processes
using the socket are notified via a SIGPIPE signal when attempting to send data.

SO_DONTROUTE indicates that outgoing messages should bypass the standard routing facilities.
Instead, messages are directed to the appropriate network interface according to the network portion
of the destination address.

SO_LINGER controls the action taken when unsent messages are queued on socket and a close(2) is per-
formed. If the socket promises reliable delivery of data and SO_LINGER is set, the system will block
the process on the close attempt until it is able to transmit the data or until it decides it is
unable to deliver the information (a timeout period, termed the linger interval, is specified in the
setsockopt() call when SO_LINGER is requested). If SO_LINGER is disabled and a close is issued, the
system will process the close in a manner that allows the process to continue as quickly as possible.

SO_LINGER_SEC is the same option as SO_LINGER except the linger time is in seconds for SO_LINGER_SEC.

The option SO_BROADCAST requests permission to send broadcast datagrams on the socket. Broadcast was
a privileged operation in earlier versions of the system.

With protocols that support out-of-band data, the SO_OOBINLINE option requests that out-of-band data
be placed in the normal data input queue as received; it will then be accessible with recv or read
calls without the MSG_OOB flag. Some protocols always behave as if this option is set.

SO_SNDBUF and SO_RCVBUF are options to adjust the normal buffer sizes allocated for output and input
buffers, respectively. The buffer size may be increased for high-volume connections, or may be
decreased to limit the possible backlog of incoming data. The system places an absolute limit on
these values.

SO_SNDLOWAT is an option to set the minimum count for output operations. Most output operations
process all of the data supplied by the call, delivering data to the protocol for transmission and
blocking as necessary for flow control. Nonblocking output operations will process as much data as
permitted (subject to flow control) without blocking, but will process no data if flow control does
not allow the smaller of the low-water mark value or the entire request to be processed. A select(2)
operation testing the ability to write to a socket will return true only if the low-water mark amount
could be processed. The default value for SO_SNDLOWAT is set to a convenient size for network effi-
ciency, often 2048.

SO_RCVLOWAT is an option to set the minimum count for input operations. In general, receive calls
will block until any (non-zero) amount of data is received, then return with the smaller of the
amount available or the amount requested. The default value for SO_RCVLOWAT is 1. If SO_RCVLOWAT is
set to a larger value, blocking receive calls normally wait until they have received the smaller of
the low-water mark value or the requested amount. Receive calls may still return less than the low-
water mark if an error occurs, a signal is caught, or the type of data next in the receive queue is
different than that returned.

SO_SNDTIMEO is an option to set a timeout value for output operations. It accepts a struct timeval
parameter with the number of seconds and microseconds used to limit waits for output operations to
complete. If a send operation has blocked for this much time, it returns with a partial count or
with the error EWOULDBLOCK if no data were sent. In the current implementation, this timer is
restarted each time additional data are delivered to the protocol, implying that the limit applies to
output portions ranging in size from the low-water mark to the high-water mark for output.

SO_RCVTIMEO is an option to set a timeout value for input operations. It accepts a struct timeval
parameter with the number of seconds and microseconds used to limit waits for input operations to
complete. In the current implementation, this timer is restarted each time additional data are
received by the protocol, and thus the limit is in effect an inactivity timer. If a receive opera-
tion has been blocked for this much time without receiving additional data, it returns with a short
count or with the error EWOULDBLOCK if no data were received. The struct timeval parameter must rep-
resent a positive time interval; otherwise, setsockopt() returns with the error EDOM.

SO_NOSIGPIPE is an option that prevents SIGPIPE from being raised when a write fails on a socket to
which there is no reader; instead, the write to the socket returns with the error EPIPE when there is
no reader.

Finally, SO_TYPE, SO_ERROR, SO_NREAD, and SO_NWRITE are options used only with getsockopt().

SO_TYPE returns the type of the socket, such as SOCK_STREAM; it is useful for servers that inherit
sockets on startup.

SO_ERROR returns any pending error on the socket and clears the error status. It may be used to
check for asynchronous errors on connected datagram sockets or for other asynchronous errors.

SO_NREAD returns the amount of data in the input buffer that is available to be received. For data-
gram oriented sockets, SO_NREAD returns the size of the first packet -- this differs from the ioctl()
command FIONREAD that returns the total amount of data available.

SO_NWRITE returns the amount of data in the output buffer not yet sent by the protocol.

RETURN VALUES
Upon successful completion, the value 0 is returned; otherwise the value -1 is returned and the
global variable errno is set to indicate the error.

ERRORS
The getsockopt() and setsockopt() system calls will succeed unless:

[EBADF] The argument socket is not a valid file descriptor.

[EFAULT] The address pointed to by option_value is not in a valid part of the process
address space. For getsockopt(), this error may also be returned if option_len is
not in a valid part of the process address space.

[EINVAL] The option is invalid at the level indicated.

[ENOBUFS] Insufficient system resources available for the call to complete.

[ENOMEM] Insufficient memory available for the system call to complete.

[ENOPROTOOPT] The option is unknown at the level indicated.

[ENOTSOCK] The argument socket is not a socket (e.g., a plain file).

The setsockopt() system call will succeed unless:

[EDOM] The argument option_value is out of bounds.

[EISCONN] socket is already connected and a specified option cannot be set while this is the
case.

[EINVAL] The socket has been shut down.

LEGACY SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>

The include file <sys/types.h> is necessary.

SEE ALSO
socket(2), bind(2), ioctl(2), getprotoent(3), protocols(5)

BUGS
Several of the socket options should be handled at lower levels of the system.

HISTORY
The getsockopt() system call appeared in 4.2BSD.

4.3-Reno Berkeley Distribution April 19, 1994 4.3-Reno Berkeley Distribution

Android微信智能心跳方案

发表于 2019-03-12

前言:在13年11月中旬时,因为基础组件组人手紧张,Leo安排我和春哥去广州轮岗支援。刚到广州的时候,Ray让我和春哥对Line和WhatsApp的心跳机制进行分析。我和春哥抓包测试了差不多两个多礼拜,在我们基本上摸清了Line和WhatsApp的心跳机制后,Ray才告诉我们真正的任务——对微信的固定心跳进行优化,并告诉我们这不是一件容易的事情。于是我和春哥开始构思第一个方案,我们开始想用统计的方法来解决问题,当我们拿着第一个方案和Ray讨论时,发现不能优雅应对Ray的所有提问:1、测试环境的准确性,失败到底是因为网络的特性导致还是因为用户当前的环境变化导致的暂时失败。2、临界值界定,如果方案选中的心跳值是临界值,我们该怎么办。Ray和组件组同事在网络方面有极其丰富的经验,虽然他没有给我们指出明确的方向,但提出的问题帮助我们更快的补齐需要面对的核心问题。这两个问题让我和春哥意识到如果能很好的解决,就可以给出一个比较好的心跳方案。第一个问题我和春哥开始就意识到,第二个问题我们确实在一开始时疏忽了。但直接解决这两个问题确实不容易,这着实让我和春哥迷茫了几天,有两三天在纺园我都没怎么睡着,因为想不到更好的方法。直到有一天思路发生了一些转变,既然最优解比较复杂,为什么不绕过去,使用有损服务理念找次优解呢。让复杂的事情简单化,好了,想到这里突然有一种拨开云雾的感觉。

思路对了,方案就可以做到简单并且可靠,大家可以看到最终的方案是比较简单的,并且效果还挺好的。在方案描述之前大概讲一下减低问题复杂度的方法:

  1. 延迟心跳测试法:这是测试结果准确的前提保障,我们认为长连接建立后连续三次成功的短心跳就可以很大程度的保证下一次心跳环境是正常的。
  2. 成功一次认定,失败连续累积认定:成功是绝对的,连续失败多次才可能是失败。
  3. 临界值避免:我们使用比计算出的心跳稍微小一点的值做为稳定心跳避免临界值。
  4. 动态调整:即使在一次完整的智能心跳计算过程中,我们没有找到最好的值,我们还有机会来进行校正。

当我和春哥想出第二个简单易行的方案后,我们心里就很有底了,去找Ray讨论,Ray听完后一次通过,然后Ray约了Harvey,给Harvey讲完后,Harvey说听起来可以,可以试试。

然后就开始动手,分析竞品加确定方案花了差不多两个月。写心跳的主要代码,只花了一天时间,我记得那天是年会后的一天。回过头来再看这个方案花费的时间还是值得的,后来灰度的统计数据显示,70%用户都可以达到我们的心跳上限。

搞完智能心跳后一段时间在广州没事干,我就跟Ray商量,Ray让我去测试下WebView的性能瓶颈。然后我跟周斯基一起来做这件事,搞完了安卓客户端WebView性能瓶颈测试后,因为怀孕的老婆一个人在深圳,领导就安排我先回深圳了。春哥坚守着把GCM部分完成后才回深圳。

等我们的心跳版本正式发布后,一年前我在公司km上分享了智能心跳方案,吸引不少做push的同事加入了讨论,感觉这方面的交流还是很有必要的。

好了,废话了很多,下面分享一下微信的智能心跳方案细节。由于字数比较多,建议大家使用PC版微信查看。

1.主要目标

本方案的主要目标是,在尽量不影响用户收消息及时性的前提下,根据网络类型自适应的找出保活信令TCP连接的尽可能大的心跳间隔,从而达到减少安卓微信因心跳引起的空中信道资源消耗,减少心跳Server的负载,以及减少部分因心跳引起的耗电。

主要方法是参考WhatsApp和Line中有价值的做法,结合影响TCP连接寿命的因素,实现Android微信后台自适应心跳算法,同时使用GCM作为辅助通道增加新消息通知的可靠性。

2. WhatsApp、Line、微信的Push策略分析

2.1 WhatsApp

在不支持GCM的设备上,采用和微信类似的长连接+心跳策略,WIFI和手机网络下的心跳间隔都为4分45秒,心跳5次后,主动断开连接再重连。

在支持GCM的设备上,主要靠GCM来激活WhatsApp,WhatsApp启动后,会建立一个与服务器的长连接,直接通过此长连接发送Push消息,这个长连接10分钟无消息就会主动断掉,且这十分钟内不做心跳,断掉后WhatsApp客户端和它的服务器不再有连接。当有消息时候,服务器发现没有长连接会发送GCM消息,手机收到GCM消息后,会重新建立长连接来收取消息,10分钟无消息会再断开,如此循环。

2.2 Line

从测试中发现Line在国内、台湾、美国使用了不同的策略。

2.2.1美国(使用GCM):

启动时,会保持7分钟心跳(CDMA2000网络)维持长连接半小时,之后主动断开长连接。当有消息时,服务器会发送GCM消息,Line客户端接收到GCM消息后,重新建立长连接,并再次用心跳维持半个小时。

2.2.2国内(不使用GCM):

在国内,同样帐号在相同网络,不同的手机上测出了两种策略:

  • 长连接+心跳策略(在Galaxy S3上使用),心跳间隔WIFI下是3分20秒,手机网络是7分钟。
  • 轮询策略(在红米和Nexus S上使用),如图2-1所示。与心跳策略的主要区别用红色标出,客户端在长连接建立后也会定时发送请求,Server会回复并且同时关闭长连接。客户端等待轮询间隔T1后再次建立TCP连接。Line会根据手机的活跃状态动态调整T1,调整范围是从最小1分到最大到2小时半。而长连接存活时间T2比较固定,在WIFI下4分钟,手机网络7分钟。如果在T2时收到新消息会延长T2的时间。 图2-1 Line在国内的轮询策略

2.2.3台湾(不使用GCM):

从IBG同事win和guang提供的测试数据中看到,台湾使用的策略跟国内的轮询策略类似。

2.3 微信

微信没有使用GCM,自己维护TCP长连接,使用固定心跳。

2.4心跳典型值

WhatsApp Line GCM
WIFI 4分45秒 3分20秒 15分钟
手机网络 4分45秒 7分钟 28分钟

2.5Line、WhatsApp、微信Push策略的优点

  1. 微信:当前心跳间隔比竞品短,所以微信在新消息提醒上会最及时。
  2. 使用GCM:Line和WhatsApp使用GCM策略的最大优点就是省电,以及减轻系统负荷(减少后台应用数目)。
  3. Line:Line的轮询策略,优点是当Line处于活跃状态时,及时收消息。当Line处于不活跃状态时,省电。

2.6Line、WhatsApp微信Push策略的不足

  1. 微信当前心跳频率相对竞品较大,在耗电、耗流量,占用信令通道等方面有所影响。
  2. Line的轮询策略,导致的问题是消息可能会延迟接收,测试发现最大延迟间隔到2.5小时。
  3. WhatsApp和Line使用Push拉起一个定时长连接策略,缺点是要依赖Google的Push服务,如果Google的Push服务不稳定,消息也会延迟接收。
  4. 在国内的移动和联通2G网络下,由于运营商的策略,GCM长连接频繁断连,WhatsApp的Push消息很不及时,体验非常差。

3. GCM研究

3.1 GCM特点

  1. Android2.2以下的手机不支持GCM,2.2到3.0需要安装Google Store并设置Google帐号,4.04及以上版本不需要设置帐号也能支持。
  2. GCM只传递数据(可以传递小于4kb的数据),对这些数据的处理可以全部由开发者控制。
  3. Android应用不需要运行就可以接收消息(通过Android广播)。
  4. GCM不保证发送的消息的顺序,也不保证消息一定能够推送到手机。

3.2 GCM心跳策略以及存在的问题

  1. 用心跳保活长连接,心跳间隔为WIFI下15分钟,数据网络下28分钟。
  2. Google可以改变所有Android设备的心跳间隔值(目前还未改变过)。
  3. GCM由于心跳间隔固定,并且较长,所以在NAT aging-time设置较小的网络(如联通2G,或有些WIFI环境下)会导致TCP长连接在下一次心跳前被网关释放。造成Push延迟接收。

3.3 GCM的可用性及稳定性

目前测试发现GCM在国内可用性不高,原因有:

  1. Android很多被手机厂商定制化,厂商可能会去掉GCM服务。
  2. Android2.2到3.0之间需要安装Google Store并设置Google帐号。
  3. 由于国内2G和移动3G的NAT超时时间都小于GCM心跳时间(28分钟),TCP长连接必然无法保活,每次都要等28分钟心跳失败重连后才能收到Push。
  4. 某些运营商可能限制了5228端口,移动3G/2G下,发现几乎无法连接上GCM服务器,也就无法获得GCM通知,WhatsApp放后台10分钟后,经常很长时间都收不到Push消息。

在美国3G网络下抓包的24小时,GCM的连接极其稳定,24小时内GCM长连接未曾断过,在台湾3G网络下抓包14个小时,GCM连接也只断过一次。WhatsApp用户在此类地区网络下客户端可以获得很及时的Push通知。

在中国电信3G下抓包,大部分时间GCM连接都比较稳定,只会因为偶尔的DHCP造成断连现象,由于频率很低(平均数小时才发生一次),对Push体验的影响不大。

3.4 GCM Server类型

GCM提供两种Server模型:

  1. HTTP Server : 使用同步接口发送HTTP请求,一次请求可以发给最多1000个设备。
  2. XMPP Server :使用异步接口发送请求,只支持对单个设备(或同一个用户的多个关联设备发送),发送请求并发数须小于1000,支持设备到云端Server发送数据。需要Google将我们的发送Server加入白名单。

4.微信可能的改进点探讨

微信Push的优化主要有几个优化点:

  1. 公共Push通道
  2. 使用GCM Push作为辅助通道
  3. 自适应心跳间隔优化

4.1 公共Push通道

由于GCM在国内的可靠性很低,现在国内Android上的Push基本上是各自为政,很多软件都自己实现Push。导致手机被经常性的唤醒,耗电耗流量严重。

市面上已经有很多第三方的公共推送服务,大家可以选择一个适合自己应用的推送服务。腾讯也有信鸽和维纳斯组件,大家在选择方案的时候可以对比下。

最终因为我们国内外使用一套方案,并且是辅助公道,所以我们选择使用GCM。

4.2 使用GCM Push作为辅助通道

当前使用GCM的成本不大,可以使用GCM作为辅助通道来增加新消息的及时性。

使用GCM作为辅助通道,在支持GCM的设备上微信上传自己的注册GCM ID给微信Server。

微信Server在发现长连接失效的情况下,可以使用GCM 作为辅助通道通知客户端有新消息,客户端收到push通知后做一次sync。

只利用GCM来激活微信,不传递消息的具体数据,要控制给同一设备发送GCM通知的时间间隔(如五分钟)。

4.3 自适应心跳间隔优化

4.3.1影响TCP连接寿命的因素

在Android下,不管是GCM,还是微信,都是通过TCP长连接来进行Push消息的,TCP长连接存活,消息Push就及时,所以要对影响TCP连接寿命的因素进行研究。

1、NAT超时

大部分移动无线网络运营商都在链路一段时间没有数据通讯时,会淘汰 NAT 表中的对应项,造成链路中断(NAT超时的更多描述见附录6.1)。NAT超时是影响TCP连接寿命的一个重要因素(尤其是国内),所以客户端自动测算NAT超时时间,来动态调整心跳间隔,是一个重要的优化点。

2、DHCP的租期(lease time)

目前测试发现安卓系统对DHCP的处理有Bug,DHCP租期到了不会主动续约并且会继续使用过期IP,这个问题会造成TCP长连接偶然的断连。(租期问题的具体描述见附录6.2)。

3、网络状态变化

手机网络和WIFI网络切换、网络断开和连上等情况有网络状态的变化,也会使长连接变为无效连接,需要监听响应的网络状态变化事件,重新建立Push长连接。

4.3.2 心跳范围选择

1、前后台区分处理:为了保证微信收消息及时性的体验,当微信处于前台活跃状态时,使用固定心跳。

微信进入后台(或者前台关屏)时,先用几次最小心跳维持长链接。然后进入后台自适应心跳计算。这样做的目的是尽量选择用户不活跃的时间段,来减少心跳计算可能产生的消息不及时收取影响。

2、后台自适应心跳选择区间:

可根据自身产品的特点选择合适的心跳范围。

4.3.3 状态转换图

tips-net-mars-heartbeat-2019312164921

4.3.4自适应心跳算法描述

1、按网络类型区分计算:

因为每个网络的NAT时间可能不一致。所以需要区分计算,数据网络按subType做关键字,WIFI按WIFI名做关键字。 对稳定的网络,因为NAT老化时间的存在,在自适应计算态的时候,暂设计以下步骤在当前心跳区间逼近出最大可用的心跳。

a) 变量说明:
  • [MinHeart,MaxHeart]——心跳可选区间。
  • successHeart——当前成功心跳,初始为MinHeart
  • curHeart——当前心跳初始值为successHeart
  • heartStep——心跳增加步长
  • successStep——稳定期后的探测步长
b) 最大值探测步骤:

图4-1 自适应心跳计算流程

自适应心跳计算流程如图4-1所示,经过该流程,会找到必然使心跳失败的curHeart(或者MaxHeart),为了保险起见,我们选择比前一个成功值稍微小一点的值作为后台稳定期的心跳间隔。

影响手机网络测试的因素太多,为了尽量保证测试结果的可靠性,我们使用延迟心跳测试法。在我们重新建立TCP连接后,先使用 短心跳连续成功三次,我们才认为网络相对稳定,可以使用curHeart进行一次心跳测试。图4-2显示了一次有效心跳测试过程。图4-3显示了在没有达到稳定网络环境时,我们会一直使用固定短心跳直到满足三次连续短心跳成功。

使用延迟心跳测试的好处是,可以剔除偶然失败,和网络变化较大的情况(如地铁),使测试结果相对可靠(五次延迟测试确定结论)。同时在网络波动较大的情况,使用短心跳,保证收取消息相对及时。

c) 运行时的动态调整策略(已经按测算心跳稳定值后)

NAT超时值算出来后,在维持心跳的过程中的策略

  1. 无网络、网络时好时坏、偶然失败、NAT超时变小:在后台稳定期发生心跳发生失败后,我们使用延迟心跳测试法测试五次。如果有一次成功,则保持当前心跳值不变;如果五次测试全失败,重新计算合理心跳值。该过程如图4-4所示,有一点需要注意,每个新建的长连接需要先用短心跳成功维持3次后才用successHeart进行心跳。 图4-2 后台稳定态动态调整心跳策略
  2. NAT超时变大:以周为周期,每周三将后台稳定态调至自适应计算态,使用心跳延迟法往后探测心跳间隔。
  3. successHeart是NAT超时临界值:因为我们现在选择的是一个比successHeart稍小的值作为稳定值,所以在计算过程中可以避开临界值。当运营商在我们后台稳定期将NAT超时调整为我们当前计算值,那么由于我们每周会去向下探索,所以下一周探测时也可以及时调整正确。

4.3.5 冗余Sync和心跳

在用户的一些主动操作以及联网状态改变时,增加冗余Sync和心跳,确保及时收到消息。

  1. 当用户点亮屏幕的时候,做一次心跳。
  2. 当微信切换到前台时,做一次Sync。
  3. 联网时重建信令TCP,做一次Sync

5. 可能存在的风险及预防措施

5.1 DHCP租期因素

  1. 问题:根据目前的测试结果显示,安卓不续约到期的IP Bug,会导致TCP连接在不确定的时间点失效,从而会导致一次心跳失败。
  2. 预防:统计后台稳定期的心跳成功率,上报给后台。后台可以按地区分网络监控这个指标的波动,并且后台可以根据不同的波动,动态调整某区域特定网络下可选的心跳区间。

5.2 其他影响TCP寿命的因素

是否有遗漏的因素?欢迎各位联系我反馈。

6 附录

6.1 附录A——NAT超时介绍

因为 IP v4 的 IP 量有限,运营商分配给手机终端的 IP 是运营商内网的 IP,手机要连接 Internet,就需要通过运营商的网关做一个网络地址转换(Network Address Translation,NAT)。简单的说运营商的网关需要维护一个外网 IP、端口到内网 IP、端口的对应关系,以确保内网的手机可以跟 Internet 的服务器通讯。 NAT 功能由图中的 GGSN 模块实现

大部分移动无线网络运营商都在链路一段时间没有数据通讯时,会淘汰 NAT 表中的对应项,造成链路中断。下表列出一些已测试过的网络的NAT超时时间(更多数据由于测试条件所限没有测到):

地区/网络 NAT超时时间
中国移动3G和2G 5分钟
中国联通2G 5分钟
中国电信3G 大于28分钟
美国3G 大于28分钟
台湾3G 大于28分钟

长连接心跳间隔必须要小于NAT超时时间(aging-time),如果超过aging-time不做心跳,TCP长连接链路就会中断,Server就无法发送Push给手机,只能等到客户端下次心跳失败后,重建连接才能取到消息。

6.2 附录B——安卓DHCP的租期(lease time)问题

目前测试发现安卓系统对DHCP的处理有Bug:

  1. DHCP租期到了不会主动续约并且会继续使用过期IP,详细描述见http://www.net.princeton.edu/android/android-stops-renewing-lease-keeps-using-IP-address-11236.html。这个问题导致的问题表象是,在超过租期的某个时间点(没有规律)会导致IP过期,老的TCP连接不能正常收发数据。并且系统没有网络变化事件,只有等应用判断主动建立新的TCP连接才引起安卓设备重新向DHCP Server申请IP租用。
  2. 未到租期的一半时间,安卓设备重新向DHCP Server申请IP租用。从目前测试结果来看,这种现象恢复的比较快。
  3. 移动2G/3G,联通2G没有抓到DHCP。
  4. 美国3G下抓取24小时,没有抓到DHCP。

转自Android微信智能心跳方案

微信终端跨平台组件 Mars 系列(三)连接超时与IP&Port排序

发表于 2019-03-12

前言

Mars 是微信官方的终端基础组件,是一个使用 C++ 编写的业务无关、跨平台的基础组件。目前在微信 Android、iOS、Windows、Mac、WP 等多个平台中使用。Mars 主要包括以下几个独立的部分:

  • COMM:基础库,包括socket、线程、消息队列、协程等基础工具;
  • XLOG:通用日志模块,充分考虑移动终端的特点,提供高性能、高可用、安全性、容错性的日志功能;(详情点击:高性能日志模块xlog)
  • SDT:网络诊断模块;
  • STN:信令传输网络模块,负责终端与服务器的小数据信令通道。包含了微信终端在移动网络上的大量优化经验与成果,经历了微信海量用户的考验。

Mars 系列开始,将为大家介绍 STN(信令传输网络模块)。由于 STN 的复杂性,该模块将被分解为多个篇章进行介绍。本文主要介绍微信中关于 socket 连接及 IP&Port 选择的思考与设计。

你需要知道的TCP连接

TCP 协议应该是目前使用的最广泛的传输层协议,它提供了可靠的端到端的传输,为应用的设计节省了大量的工作。TCP 建立连接的”三次握手”与连接终止的“四次挥手”也广为人知。在这简单的 connect 调用中,还能做怎样的思考与设计呢?

1
int connect(int sockfd, const struct *addr, socklen_t addrlen)

连接的超时重传

超时与重传是 TCP 协议最核心的部分,在不稳定的移动网络中,超时重传的设计尤为重要。在连接建立的过程中,由于网络本身的不可靠特性,不可避免的需要重传的机制来保障可靠服务。在《TCP/IP详解 卷1》的描述中,在大多数 BSD 实现中,若主动 connect 方没有收到 SYN 的回应,会在第6秒发送第2个 SYN 进行重试,第3个 SYN 则是与第2个间隔24秒。在第75秒还没有收到回应,则 connect 调用返回 ETIMEOUT。

这就意味着,在不能立刻确认失败(例如 unreachable 等)的情况下,需要75秒的时间,才能获得结果。如果真相并不是用户的网络不可用,而是某台服务器故障、繁忙、网络不稳定等因素,那75秒的时间只能尝试1个 IP&Port 资源,对于大多数移动应用而言,是不可接受的。我们需要更积极的超时重传机制!!!

然而,我们并不能修改 TCP 的协议栈,我们只能在应用层进行干预,设计应用层的超时机制。说干就干,这个时候你是否已经在构思新的、应用层的连接超时重传机制了呢?应用层的超时重传,典型做法就是提前结束 connect 的阻塞调用,使用新的 IP&Port 资源进行 connect 重试。但是,我们应该选择怎样的连接超时值呢?4秒?10秒?20秒?30秒?不同的应用场景会有不同的选择。我们来看一下常见的几种场景:

  • 连不通 or 网络不可用等
  • 服务器繁忙 or 中间路由故障等
  • 基站繁忙 or 连接信号弱 or 丢包率高等

在第一种场景中,连接超时设置不会带来什么区别。在第二种场景中,部分服务器资源或路由不可用,我们希望连接超时能稍微短一些,使得我们能尽快的发现故障,并且通过更换 IP&Port 的方式获得可用资源或路由路径。而第三种场景则是在移动网络中经常遇到的弱网络的场景。在这种场景中,我们更换 IP&Port 资源也是无效的,因此希望连接超时能相对长一些,进行更多的TCP层的重传。(当然,也不是超时越长越好,后面的分析可以看到很多等待时长是效果低微的)

不同的场景对连接超时有不同的需求,然而,我们在程序中并没有很好的方法来区分这些场景。在进行连接超时这个阈值的选择前,我们先来看看,当前主流的 android、iOS 操作系统的连接设计。android 的 TCP 层连接超时重传如下图所示(测试机型为 nexus5,android 4.4)。超时间隔依次为(1,2,4,8,16),第5次重试后32秒返回 ETIMEOUT,总用时63秒。超时设置符合 Linux 的常规设置。 tips-net-mars-ip-sort-2019312162610

但在不同的机型中,偶尔会出现差异性。如下图 android 抓包(三星 android 4.4)。 tips-net-mars-ip-sort-2019312162636

iOS 的 connect 超时重传如下图所示。超时间隔依次为(1,1,1,1,1,2,4,8,16,32),总共是67s。 tips-net-mars-ip-sort-2019312162711

经过 tcpdump 的调研分析后,我们发现:

  1. 在 iOS 系统中对 connect 的超时重传进行了一定的修改,在 connect 初期使用更积极的策略,以适应移动网络的不稳定特征。而在 android 系统中,connect 超时重传则使用了较为“懒惰”、适用于有线网络的超时重传间隔;
  2. 不管什么平台,连接总超时时长都需要1分钟左右,这个时长在大多数移动应用中,都是不符合用户体验要求的;
  3. 连接的初始阶段,TCP 超时重传会更积极一些,越到后面,重传间隔越大。

因此,在实际的连接超时设置上,我们根据不同的系统特征,结合应用能接受的“用户体验”范围,可以设置不同的连接超时间隔。例如在 iOS 系统中,由于采用了较为积极的超时间隔,我们可以将 connect 调用的超时设置为10s。在10s内,iOS 会自动进行6次的重发。在 android 系统中,系统会在第7秒发起第3次重发,之后需要在第15秒才会重发。在不同的用户体验要求下,应用可以将 connect 的调用超时设置为不同的值。例如也可以设置为10s(意味着给第3次重发3s的等待时间),从而避免无效的等待时长。同时通过更换 IP&Port 后,重新调用 connect 操作的方式,来获得更积极的重发策略,更快的查找到可用的 IP&Port 组合。

连接的终止

“四次挥手”的连接终止协议已经口熟能详。过程如下图所示。需要关注的是,图中主动关闭的一方会进入 TIME_WAIT 状态,在此状态中通常将停留2倍的 MSL 时长。MSL 时长在不同的操作系统中有不同的设置,通常在30秒到60秒。TIME_WAIT 的数量太多会导致耗尽主动关闭方的 socket 端口和句柄,导致无法再发起新的连接,进而严重影响主动关闭方的并发性能。虽然在实际的使用中,可以通过 tcp_tw_recycle,tcp_tw_reuse,tcp_max_tw_buckets 等方式缓解该问题,但也会带来一些副作用。最好的解决方案是在协议的设计上,尽量的由终端来发起关闭的操作,避免服务器的大量 TIME_WAIT 状态。例如,使用长连接避免频繁的关闭;在短连接的协议设计上,务必加上终止标记(例如 http 头部加上 content-length )使得可以由终端来发起关闭的操作。 tips-net-mars-ip-sort-2019312162922

串行连接 VS 并发连接 VS 复合连接

在上述的连接超时策略中,我们选择10秒的连接超时。这就意味着我们需要10秒的时间来确认一个 IP&Port 组合的 connect 超时。当我们有多个 IP&Port 资源时,遍历的效率偏低。那我们是否能设置 connect 的超时为更短呢?例如4秒。我们知道移动互联网具有不稳定的特征,超时时间设置过短,会导致在弱网络的情况下,connect 总是失败,导致不可用。串行连接的策略在超时选择上,由于需要兼顾高性能与高可用的设计目标,使得该策略是一个相对“慢”的连接策略。

与此相应,我们会想到并发连接的策略。并发连接,同时发起对N个 IP&Port 的连接调用,可以让我们第一时间发现可用的连接,并且还顺带发现了 connect 最快的 IP&Port 配置。并发连接可以一举解决了“高性能”、“高可用”的设计目标,看起来很完美。然而,这个时候,服务端的同学“跳”起来了。在并发连接的策略下,服务器需要提供的连接能力是串行连接的N倍,对服务器连接资源是极大的浪费。同时,并发连接是否会引起连接资源的竞争,从而影响网络正常用户的常规体验,也是个未知的因素。

让我们来回顾串行连接与并行连接的优缺点。

串行连接

  • 资源占用少
  • 无服务器负载问题
  • 超时选择困难
  • 最慢可用

并行连接

  • 网络资源竞争
  • 服务器负载高
  • 最快可用

那么,有没有一种策略,能同时满足高性能、高可用、低负载的目标呢?在微信的连接设计中,我们使用了”复合连接“的策略。如下图所示。 tips-net-mars-ip-sort-2019312163133

初始阶段,应用发起对 IP1 &Port1 的 connect 调用。在第4秒的时候,如果第一个 connect 还没有返回,则发起对 IP2 &Port2 的 connect 调用。以此类推,直至发起了5组 IP&Port 的 connect 调用。

  • 对比串行连接与并行连接,复合连接有以下特点:
  • 常规情况下,服务器负载与串行连接策略相同,实现了低负载的目标;
  • 异常情况下,每4s发起新(IP,Port)组合的 connect 调用,使得应用可以快速的查找可用 IP&Port,实现高性能的目标;
  • 在超时时间的选择上,复合方式的“并发”已经实现了高性能、低负载的目标,因此在超时时间的选择上可以相对宽松,以保障高可用为重。

综合对比,复合连接能够维持低资源消耗的情况下,能同时实现低负载、高性能、高可用的目标。

微信 IP&Port 排序算法的演进

在建立连接的调用中,除了超时时间的设置外,IP&Port是连接的最重要参数。IP&Port 的排序、选择对于 connect 的性能也是有着重大的影响。本节主要讨论在已知 IP 列表、Port 列表的情况下,如何排序、组合的问题,而不讨论如何获得就近接入等问题。

IP&Port 的组成

在微信中,IP有多种来源类型。优先级从上而下分别为:

  • WXDNS IP 自建的DNS服务获得的IP列表
  • DNS IP 通过常规的 DNS 解析获得的 IP 列表
  • Auth IP 动态下发的保底列表
  • Hardcode IP 最终保底IP列表

WXDNS IP 是通过微信自建的 DNS 服务获得的IP列表,自建 DNS 对防劫持、有效期控制等有重要作用。DNS IP 则是通过常规的 DNS 解析获得的 IP 列表。Auth IP 是微信动态下发的保底IP列表。而Hardcode IP 则是最终的保底IP列表。总体而言,分为常规IP列表、保底IP列表两个类别。WXDNS IP、DNS IP 为常规列表,Auth IP,Hardcode IP 为保底列表。同时,在组成实际使用的 IP&Port 列表时,由于 WXDNS 与 DNS 的功能近似,因此通常只出现其中一种类型的IP列表。Auth IP 与Hardcode IP 的功能近似,也是同时只能出现两者中的一种类型。 在 Port 的选择上,微信服务在常规情况下提供2个端口,预防端口被封锁的情况。特别情况下,可以通过配置下发进行端口更新。

IP&Port排序算法(一):随机组合排序算法

每个TCP连接都是以 IP&Port 的组合为唯一标识。在 IP&Port 的选择上,我们初步归纳为2个目标:

  • 高可用:尽快的找到可用的 IP&Port 资源
  • 高性能:优先使用质量好的 IP&Port
  • 负载均衡:IP的排序算法不带任何偏向因子,避免造成人为的负载不均衡

在微信早期的排序选择上,我们使用了一种随机组合的排序算法。即将 WXDNS or DNS IP 列表与 Port 列表进行组合,组合后的结果进行随机排序。在随机排序的结果列表中,使用下述步骤进行排序:

  1. 选取IP1+Port1;
  2. 选取IP2+Port2,尽量使得IP1与IP2不相等,Port1与Port2不相等;
  3. 选取IP3+Port3,尽量使得IP3与IP1、IP2都不相等,Port3与Port1、Port2都不相等;
  4. 以此类推,形成常规列表。

同理,使用 Auth IP or Hardcode IP 列表与 Port 列表的组合,我们按照相同算法生成另外一份保底列表,并将保底列表排序在常规列表的后面,从而组成完整的 IP&Port 列表。随机组合排序的算法有着以下的特点:

  • 高性能:每一次尝试都尽量使用完全不同的资源,使得能最快的发现可用资源;
  • 初始随机,从而避免列表顺序的固化;
  • 保底列表在最后,形成最后的保护屏障;
  • 在不同的网络下,维护着不同的资源列表。

在使用中,如果发现 IP&Port 访问失败,则在列表中 ban 掉该资源。这里有个小优化,即当 IP1&Port1 的上一次访问成功时,需要连续失败2次才 ban 该资源。目的是为了减小偶然的网络抖动造成的影响。

随机组合排序算法的设计初衷,是为了以最快的速度尝试不同的资源组合,从而快速寻找到可用的资源。然而,在微信的实际使用中,却发现这种算法存在着诸多的问题。例如:

  • 网络不可用或网络较大波动情况下,列表被ban的速度较快;
  • Auth IP or Hardcode IP 列表太容易被访问到:随着常规资源陆续被ban,保底资源总是会被访问到,造成对保底资源的访问量大。保底资源是为了微信服务这不符合保底资源的设计初衷。
  • 当引入复合连接策略后,IP资源不足。这是因为 ban 的策略简单粗暴的丢弃失败的 IP,导致 IP 资源越来越少;
  • 每次缓存超时或列表轮空后,对于新列表没有经验信息可用

在随机组合排序算法的基础上,为了解决遇到的新问题,微信使用了新的“以史为鉴”的算法。

IP&Port 排序算法(二):以史为鉴

由于复合连接的引入,在每次复合连接的尝试中,微信可以伪“并发”的对N个 IP&Port 进行 connect(微信中目前N=5)。简单的ban丢弃的策略会使得 IP 资源越来越少。 针对这个特点,我们对IP&Port算法进行了以下修改:

  • 初始资源列表分为两类列表:常规列表,保底列表,分别使用方案(一)随机组合排序算法生成初始顺序;
  • 对每次复合连接使用的列表,规定5个资源的组成是4个常规资源+1个保底资源,并且保底资源在最后(完全无法获取常规资源的情况除外)。这种资源组成方式一方面解决了“保底资源太容易被访问到”的问题,一方面也保障了保底资源的作用;
  • 在不同网络中,分别记录每个 IP&Port 的使用情况,并根据使用记录进行评分、排序;
  • 区分连续记录:对每个 IP&Port 的更新,10秒内的连续成功或失败,不进行使用情况的记录。这种处理方式一方面是为了避免网络不可用或网络出现较大波动时,IP资源被过快的错误标记;一方面也避免失败历史被快速的覆盖;
  • 最近的8条使用记录中,如果有超过3条失败记录,且最新一次失败记录时间为10分钟内,则本次排序ban该记录。这种处理方式的目的是避免历史分数较高的 IP&Port 在突然出现故障时很难被排序算法排除的问题;
  • 无历史的记录使用随机评分排序。

通过上述方法,我们保证了保底资源不会被轻易访问到,解决了列表被快速标记的问题,同时也保证了历史记录好的资源在出现故障时也能被快速替换。

IP&Port 排序算法(三):遗忘历史

“以史为鉴”的方案在微信中使用了一段时间,看起来运行良好。直至某一天,微信的部分服务集群出现了故障。虽然微信客户端快速的切换到可用的服务器资源,但当故障服务器恢复后,微信客户端却迟迟没有分流到已恢复服务的集群,导致部分微信服务器负载过高,而部分微信服务器却负载较低的情况。通过分析,发现“以史为鉴”的排序方案存在着一些问题:

  • 初始阶段排在前面的资源容易获得较多的成功记录,从而分数始终维持在较高的水平;
  • 出灾情况下,故障机器由于有失败记录,使得很难获得“被原谅”的机会,从而也很难更新使用历史;
  • 采用了无历史记录随机评分,破坏了原有的“相邻记录尽量不相同”的随机性设计; 因此,好的 IP&Port 排序算法,不仅应该快速的发现可用的资源,使得在出灾情况下能快速的响应,同时,也应该具备一定的“遗忘性”、“容灾性”,使得灾情恢复后能较快的发现“灾情恢复”这一事实,并且进行重排序,使得服务器资源得到更合理的使用。在综合考虑“以史为鉴”和“遗忘历史”后,新的 方案具有以下特征:
  • 内存历史、文件历史双层记录历史:反映资源使用的近期情况及历史情况;
  • 初始化状态:每次进程重启或网络切换后,从文件历史中“压缩”出内存历史作为初始状态;
  • 旁路检测:额外更新历史的渠道,更有助于挑选高性能的资源,并且帮助“灾情恢复”的资源获得使用的机会;
  • 文件历史的遗忘性:文件历史每24小时强制刷新,避免高分数的记录长期“占有”队列;
  • 无历史、有历史的混合排序。

具体实现查看 Mars 源代码中的 simple_ipport_sort。

总结

连接是信令传输的前提,一个简单的连接操作蕴含着不少的优化空间。在连接超时的选择上,我们要兼顾性能与可用性,过短的连接超时可能导致弱网络下的低可用性,但过长的连接超时又影响用户体验。在 STN 中,我们结合系统本身的 TCP 连接重传特性,进行了相应的设计考量。即使如此,串行的连接方案仍然不能满足高性能的需求。并发连接的方案获得高性能的同时,也带来了服务器负载剧增的损失。综合考虑下,STN 使用了“复合连接”的方案,获得高性能的同时,也保证通常情况下的服务器低负载。

IP&Port 是连接的最重要资源,IP&Port 的排序选择是连接过程的重要部分。在微信的实际使用中,我们依次使用了“随机组合”、“以史为鉴”、“遗忘历史”三种方案,综合的考虑了查找性能、移动互联网的不稳定性、容灾及容灾恢复等。

连接超时、连接策略及 IP&Port 排序是连接的是三个重要组成部分,相关的方案也随着微信实践在不断的发展中。相信在不同的应用场景中,我们可能会遇到更多的不同问题及需求。随着Mars的开源,也能有机会参考、吸收其他应用中的实战经验,使得网络优化持续的深入。

转自微信终端跨平台组件 Mars 系列(三)连接超时与IP&Port排序

1234…19
轻口味

轻口味

190 日志
27 分类
63 标签
RSS
GitHub 微博 豆瓣 知乎
友情链接
  • SRS
© 2015 - 2019 轻口味
京ICP备17018543号
本站访客数 人次 本站总访问量 次