c++ - debug, asan

AddressSanitizer (ASan) C/C++ 程序的内存错误检测器,可以检查如下错误。

TL;TR

1
2
3
4
5
# CMAKE_CXX_FLAGS
-g3 -fno-omit-frame-pointer -fno-common -fsanitize=undefined -fsanitize=address -fsanitize-recover=address

# 环境变量
export ASAN_OPTIONS=symbolize=true:halt_on_error=false:abort_on_error=false:disable_coredump=false:unmap_shadow_on_exit=true:disable_core=false:sleep_before_dying=15:fast_unwind_on_fatal=1:log_path=asan.log

指定 Asan Library 路径

1
export LD_LIBRARY_PATH=/path/to/libasan.so.x

注意

  • 部分 IDE 集成分析工具,ASAN_OPTIONS 可能被覆盖,比如 CLion,需要在设置中设置 ASAN_OPTIONS(AddressSanitizer)。

image-20241014111225039

使用

编译选项

1
2
3
4
5
6
-fsanitize=address						# 开启内存越界检测
-fsanitize-recover=address # 内存出错后继续运行, 需配合运行选项 halt_on_error=0
-fno-stack-protector # 去使能栈溢出保护
-fno-omit-frame-pointer # 去使能栈溢出保护
-fno-var-tracking # 默认选项为-fvar-tracking,会导致运行非常慢
-g1 # 表示最小调试信息,通常debug版本用-g即-g2

示例

1
2
ASAN_CFLAGS += -fsanitize=address -fsanitize-recover=address
ASAN_CFLAGS += -fno-stack-protector -fno-omit-frame-pointer -fno-var-tracking -g1

链接选项

1
ASAN_LDFLAGS += -fsanitize=address -g1 # 如果使用gcc链接,此处可忽略

运行选项

ASAN_OPTIONS是Address-Sanitizier的运行选项环境变量。

1
2
3
4
5
6
7
8
9
10
11
halt_on_error=0/1 					# 检测内存错误后继续运行
abort_on_error=0/1 # 遇到错误后调用 abort() 而不是 _exit()
detect_leaks=0/1 # 使能内存泄露检测
malloc_context_size=15 # 内存错误发生时,显示的调用栈层数为15
log_path=asan.log # 内存检查问题日志存放文件路径
suppressions=$SUPP_FILE # 屏蔽打印某些内存错误
symbolize=0/1 # 启用符号化,将错误地址翻译成代码行号
disable_coredump=0/1 # 禁用 core dump
disable_core=0/1 # 禁用 core dump
unmap_shadow_on_exit=1
sleep_before_dying=60

更多

示例

1
2
3
4
5
6
7
# 1
export ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer
export ASAN_OPTIONS=halt_on_error=0:use_sigaltstack=0:detect_leaks=1:malloc_context_size=15:log_path=/tmp/asan.log:suppressions=$SUPP_FILE

# 2
export ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer
export ASAN_OPTIONS=symbolize=true:halt_on_error=false:abort_on_error=false:disable_coredump=false:unmap_shadow_on_exit=true:disable_core=false:sleep_before_dying=15:log_path=asan_log

cmake 链接

1
2
3
4
#asan 链接sys
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address")
SET(CMAKE_C_FLAGS_ASAN "-O2 -g -fsanitize=address -fno-omit-frame-pointer" CACHE STRING "Flags used by the C compiler during asan builds." FORCE)
SET(CMAKE_C_FLAGS "-O2 -g -fsanitize=address -fno-omit-frame-pointer -lstdc++ -lasan" CACHE STRING "Flags used by the C compiler during asan builds." FORCE)

Debug

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
==1867==ERROR: AddressSanitizer: heap-use-after-free on address 0x7f69c0e59823 at pc 0x000001f1f50c bp 0x7f69c02624e0 sp 0x7f69c02624d0
...
0x7f69c0e59823 is located 35 bytes inside of 8388608-byte region [0x7f69c0e59800,0x7f69c1659800)
freed by thread T14 here:
...
previously allocated by thread T14 here:
...
SUMMARY: AddressSanitizer: heap-use-after-free /data/zhenkai.sun/ranker/cmake-build-debug/CMakeUnzipPackages/mongo-c-driver-1.19.1/src/libbson/src/bson/bson.c:1993 in bson_init_static
Shadow bytes around the buggy address:
0x0fedb81c32b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0fedb81c32c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0fedb81c32d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0fedb81c32e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0fedb81c32f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0fedb81c3300: fd fd fd fd[fd]fd fd fd fd fd fd fd fd fd fd fd
0x0fedb81c3310: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x0fedb81c3320: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x0fedb81c3330: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x0fedb81c3340: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
0x0fedb81c3350: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==1867==ABORTING

c++ 手册

定义变量

基础操作

字符串

c++

1
2
// 查找
size_type idx = str.find(s); // 未查到: (idx == std::string::npos)

容器

顺序容器

  • [x] 可变数组 vector
  • [x] 双向链表 list
  • [ ] 单向链表 forward_list
  • [ ] 双端队列 deque
  • [ ] 固定数组 array

关联容器

  • [ ] 集合 set、multiset
  • [ ] 映射 map、multimap

容器适配器

  • [ ] 栈 stack
  • [ ] 队列 queue
  • [ ] 优先队列 priority_queue

可变容量数组 vector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <vector>
// 初始化
std::vector<T> vec;
std::vector<T> vec(5, 0); // 长度 5, 元素值
std::vector<T> vec = {1, 2, 3, 4, 5};
std::vector<T> vec(arr, arr + sizeof(arr) / sizeof(arr[0]));
std::vector<T> vec(container.begin(), container.end());
// 插入
vec.push_back(element);
vec.emplace_back(element);
vec.insert(iterator, element);
// 访问元素
vec.at(index);
vec[index];
vec.front();
vec.back();
// 删除元素
vec.pop_back();
vec.erase(iterator);
vec.erase(start_iterator, end_iterator);
// 大小和容量
vec.resize();

双向链表 list

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
#include <list>
// 初始化
std::list<T> lst;
std::list<T> lst = {1, 2, 3, 4, 5};
std::list<T> lst(container.begin(), container.end());
// 插入
lst.push_back(element);
lst.push_front(element);
lst.insert(it, element);
lst.insert(it, n, element); // 在 it 插入 n 个 element
lst.insert(it, begin, end); // 在 it 插入迭代器 [begin, end) 内元素
// 赋值 (清空元素并重置为指定元素)
assign(begin, end);
assign(n, element);
// 访问
l.begin(); // 返回迭代器
l.end(); // 返回迭代器
l.front(); // 返回元素
l.back(); // 返回元素
// 删除
erase(begin, end);
erase(it);
remove(element);
clear();
pop_back();
pop_front();

单向链表 forward_list

1
2
3
4
5
6
7
8
9
10
11
12
#include <forward_list>
// 初始化
std::forward_list<int> fl;
std::forward_list<int> fl {1, 2, 3};
std::forward_list<int> fl(n, element);
std::forward_list<int> fl(begin, end);
// 插入
fl.push_front(element);
fl.insert_after(iter, element);
// 赋值 (清空元素并重置为指定元素)
// 访问
// 删除

unordered_set

1
2
3
4
5
6
7
8
9
// 修改
insert(Key&&)
emplace(Args&&)
erase(const Key&)
merge()
// 查找
iterator find(Key)
size_type count(Key)
bool contains(Key)

基础知识

虚函数

  • 运行时动态绑定
  • 基类使用虚函数表(vtable)存储每个虚函数的地址

c++基础

[toc]

组织

文件

文件

文件名后缀 说明
.c c 源文件
.h c 同文件
.cpp c++ 源文件
.hpp c++ 头文件
.hh c++ 头文件
.hxx c++ 头文件
.h++ c++ 头文件

原则

  • 通常的,.h.c 对应 c 代码,.hpp.cpp 对应 c++ 代码
    • 除此之外的后缀,多是项目规范,保持一惯性即可
  • 保持 .h 文件是 c 兼容的代码(不包含 c++ 代码)
  • 如果想编写 c 和 c++ 的混合代码
    • 可以在 .hpp 中使用 extern "C" 来实现
  • 不要使用 .H.C 后缀,因为部分文件系统不区分大小写,比如 windows,macos 在格式化分区时也有不区分大小写的选项

输入输出

标准输入输出包含:

  • cin
  • cout
  • cerr
  • clog
    • 带缓冲区,常用于写日志数据
1
2
3
4
5
6
// 持续输入
while (cin >> num)
sum += num;

// 读取到类对象
cin >> Item;

基础

变量

初始化

C++ 支持两种初始化方式,复制初始化和直接初始化。

1
2
int val(1024);   // 直接初始化;效率更高
int val = 1024; // 复制初始化

对于内置类型来说,复制初始化和直接初始化几乎没有差别。函数体外定义的内置变量都初始化成0,函数体内定义的内置变量不进行自动初始化。变量在使用前必须被定义,且只允许被定义一次。

声明

为了让多个文件可以访问相同的变量,C++ 区分了声明和定义。声明用于向程序表明变量存在,及其类型和名字;定义用于为变量分配空间,还可对变量进行初始化。

1
2
3
4
extern int i;	 // 声明变量
int i; // 定义变量

extern int i = 1; // 有初始化的声明,可视为定义

数据类型

类型 含义 最小存储空间
bool 布尔型
char 字符 8位
wchar_t 宽字符型 16位
short 短整型 16位
int 整型 16位
long 长整型 32位
float 单精度浮点数 6位有效数字
double 双精度浮点型 10位有效数字
long double 扩展精度浮点型 10位有效数字
1
wchar_t wc = L'a'; // 字符前面加 L 表示宽字符 

类型

数字

字符串

初始化

1
string s(10, '0');

子串

1
2
str.substr(3, 5);    // [3, 8)
str.substr(5); // [5, ~)

布尔

限定

static

1
2
3
4
5
6
7
class A {
public:
static A Instance() { // 实现单例
static A _a; // static 修饰,只会创建一次 A 对象
return _a;
}
};

常量

定义

数据结构

数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 创建
int i[5];
int j[2][2];

// 初始化
int i[] = {1, 2, 3};
int j[][1] = {{1}, {2}, {3}};

// 二维数组初始化
int k[2][2] = {{1, 2}, {3}};
1 2
3 0

// 静态数组
int i[5] = {0}; // 创建静态数组, 并初始化, 没设置的元素被重置为 0
std::cout << sizeof(i) << std::endl; // 20, 5 * 4. 5 个元素, 每个占用 4 字节空间
int b[3][3] = {0}; // 多维数组

// 动态数组
int size = 3;
int *c = new int[size]; // 创建动态数组
std::cout << sizeof(c) << " - " << sizeof(*c) << std::endl; // 8 - 4. 8: 64位机器, 指针大小, 4: int 元素大小
memset(c, 0, size * sizeof(*c)); // 必须要乘 size 才能算出总的占用内存
delete[] c;

列表

1
2
3
// 初始化
vector<int> iv{1, 2, 3} // [1, 2, 3]
vector<int> iv(3, 1); // [1, 1, 1]

集合

映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 初始化
std::map<int, int> m = {
{1, 2},
{2, 3}
};

std::map<std::string, std::vector<int> > mapOfOccur = {
{ "Riti", { 3, 4, 5, 6 } },
{ "Jack", { 1, 2, 3, 5 } }
};

// 遍历
std::map<int, int> m = ...;
for (auto &entry : m) {
std::cout << entry.first << " -> " << entry.second << std::endl;
}

// 插入
map<int, string> mp;
mp.insert(pair<int,string>(1,"aaaaa"));
mp.insert(make_pair<int,string>(2,"bbbbb"));
mp.insert(map<int, string>::value_type(3,"ccccc"));
mp[4] = "ddddd";

std::map<char,int> mp;
mp.emplace('x',100);

// 查找
std::map<char,int>::iterator it = mp.find('x');
if (it != mp.end())
// exists
else
// not exists

// 删除
std::map<char,int>::iterator it = mp.find('x');
if (it != mp.end())
mp.erase(it);

语法

程序结构

注释

运算符

条件控制

循环

判断

函数

特殊成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 默认构造函数 (Default constructor)
classname ()
// 非默认函数
explicit classname(type param)
// 拷贝构造函数 (Copy constructor)
classname(const classname &other)
// 赋值构造 (Copy assignment operator)
classname& operator=(const classname &other)
// move 构造 (Move constructor)
classname(classname &&other)
// 赋值 move 构造 (Move assignment operator)
classname& operator=(classname &&other)

// 析构函数 Destructor
~classname()
Function syntax for class MyClass
Default constructor MyClass();
Copy constructor MyClass(const MyClass& other);
Move constructor MyClass(MyClass&& other) noexcept;
Copy assignment operator MyClass& operator=(const MyClass& other);
Move assignment operator MyClass& operator=(MyClass&& other) noexcept;
Destructor ~MyClass();

特性

语法糖

default

default 关键词为类的特殊默认无参函数(构造、析构、拷贝构造、拷贝赋值)提供默认行为。

1
2
3
4
5
6
7
8
9
10
11
class A
{
public:
A() = default;
A(const A&);
A& operator = (const A&);
~A() = default;
};

A::A(const X&) = default; // 拷贝构造函数
A& A::operator= (const A&) = default; // 拷贝赋值操作符

delete

default ,屏蔽默认行为。

1
2
3
4
5
6
7
class A
{
A& operator=(const A&) = delete; // assignment operator disabled
};

A a, b;
a = b; // ERROR: 拷贝赋值操作被禁用

thread_local

lambda

lambda 表达式格式如下。

1
[函数对象参数] (操作符重载函数参数) mutable 或 exception 声明 -> 返回值类型 {函数体}

函数对象参数

参数范围 参数传递方式 备注
没有函数对象参数 - -
= 表达式所有可访问局部变量(包括所在类的 this 对象) 值传递 -
& 表达式所有可访问局部变量(包括所在类的 this 对象) 引用传递 -
this 函数体内可以使用 Lambda 所在类中的成员变量 TBD -
a a 值传递 变量默认为 const,如果需要修改需为函数体添加 mutable 修饰符
&a a 引用传递
a,&b a,b a 为值传递,b 为引用传递 -
=,&a,&b 表达式所有可访问局部变量(包括所在类的 this 对象) a、b 引用传递,其他参数是值传递 -
&,a,b 表达式所有可访问局部变量(包括所在类的 this 对象) a、b 值传递,其他参数是引用传递 -

空指针处理

函数式编程

泛型编程

1
2
3
4
5
6
7
8
9
10
// 从标准输入读取T类型数据
template <typename T>
T r() {
T t;
cin >> t;
return t;
}

// 使用
int x = r<int>();
1
2
3
4
5
6
7
8
9
10
11
12
// 值交换
template <typename T>
void swapT(T& a, T& b) {
a ^= b;
b ^= a;
a ^= b;
}

// 使用
vector<int> iv{1, 2, 3};
swapT(iv[0], iv[2]);
// 1 2 3 -> 3 2 1

不限于类型。

1
2
3
4
5
6
7
8
9
template<unsigned N>
void f() {
std::cout << N << std::endl;
}

int main() {
f<10>();
return 0;
}

类型推演

重用方法

1
2
3
std::is_same<TA, TB>
typeid()
std::is_same_v<TA, TB> #include <variant>

类型读取及定义

1
2
template<auto object, class T=std::decay_t<decltype(*object)>>
int Function();

类型判断

1
2
3
4
5
6
7
8
9
#include <concepts>

template<typename Type>
concept CharTypes = std::is_same<Type, char>::value ||
std::is_same<Type, wchar_t>::value || std::is_same<Type, char8_t>::value ||
std::is_same<Type, char16_t>::value || std::is_same<Type, char32_t>::value;

template<CharTypes T>
class Some{};
1
2
3
4
5
6
7
8
#include <variant>

template<class T>
void f(T t) {
if (std::is_same_v<T, std::string>) {
// DO SOMETHING
}
}

宏定义 define

##

连接形参,忽略前后空白符。

1
2
3
4
5
6
#define Concat(a, b) a##b

int ab = 1, ax = 2, xa = 3;
std::cout << Concat(a, b) << std::endl; // output: 1
std::cout << AppendX(a) << std::endl; // output: 2
std::cout << XAppend(a) << std::endl; // output: 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// not ok
#define select(m, key) m##[#key]
// ok
#define select(m, key) (m)[#key]

//--- .
// not ok
#define select(m, key) m##.##key
#define select(m, key) m.##key
// ok
#define select(m, key) m.key


// 使用
std::map<std::string, std::string> m;
m["a"] = "0";
auto v = select(m, a);

// 在使用宏变量时,外加小括号, 比如 #define add(a, b) (a) + (b)

#@

字符化形参。

#

字符串化形参。

1
2
3
4
5
6
#define ToString(a) #a
std::cout << ToString(abc) << std::endl; // abc

// 拼接
#define ToSV(member) #member##sv
ToSV(time) // 等价于 "time"sv

Parameter pack

Parameter pack

A template parameter pack is a template parameter that accepts zero or more template arguments (non-types, types, or templates). A function parameter pack is a function parameter that accepts zero or more function arguments.

A template with at least one parameter pack is called a variadic template.

包含至少一个参数包的模板称为可变模板。

右值引用&&

C++ 11 引入右值引用主要是为了解决以下几个问题:

  1. 优化复制大对象的性能问题。

在传递一个对象时,如果使用常规的左值引用,就需要进行拷贝构造函数的调用,这会导致复制大对象的时候开销很大。而右值引用可以避免这种情况的发生。因为右值引用本身不会进行对象的拷贝操作,只是将对象所在的内存地址绑定到右值引用上,从而提高代码执行效率。

  1. 实现移动语义,支持转移资源所有权。

在C++11中,新增了std::move函数,可以将一个对象的资源所有权转移到另一个对象中,这就是移动语义。通过将对象的内部数据指针从源对象转移到目标对象,可以避免创建和销毁临时对象,从而提高代码执行效率。而实现移动语义,需要使用右值引用的特性。

总之,右值引用的引入,旨在提高C++代码的性能和效率,支持更加高效的对象传递和资源管理方式,并且为C++编程带来更多的灵活性和扩展性。

其他优化场景。

  • 函数中返回一个临时变量时,编译器会自动调用移动构造函数,并将临时变量的资源所有权移动到函数的返回值中,从而避免进行数据拷贝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 示例 1
std::string GetString() {
std::string t = "abc";
std::string res = t + "def";
std::cout << "A " << static_cast<void *>(res.data()) << std::endl;
return res; // 函数返回临时变量, 未进行对象拷贝
}

int main() {
auto r = GetString(); // 右值引用赋值给左值变量, 不进行对象拷贝
std::cout << "B " << static_cast<void *>(r.data()) << std::endl;
std::cout << "---" << std::endl;
auto r2 = std::move(GetString());
std::cout << "B " << static_cast<void *>(r2.data()) << std::endl;
return 0;
} /* output
A 0x16eeeb100
B 0x16eeeb100
---
A 0x16eeeb0c0
B 0x16eeeb0d8
*/

注意

  • 对右值调用 std::move 没有作用

其他

常量

1
2
3
// MAX / MIN
INT_MAX
INT_MIN

进阶

C++ 程序质量保障

  • 代码覆盖率(code coverage)
  • 内存检查
    • asan
    • valgrind
  • CPU Profiler

malloc

常用的 malloc 库,及实践。

  • jemalloc
  • tcmalloc
    • 性能要好于 jemalloc
  • mimalloc
    • 偶尔会 core

RAII

RAII(Resource Acquisition Is Initialization,资源获取即初始化)使用局部变量来管理资源,是 C++ 中常用的资源管理方式。

时间

  • time unit
    • std::chrono::microseconds
    • std::chrono::milliseconds
    • std::chrono::second
    • std::chrono::minutes
    • std::chrono::hours
    • std::chrono::days
    • std::chrono::months
    • std::chrono::years
  • clock
    • steady_clock 单调递增时钟
    • system_clock 系统时间时钟
    • high_resolution_clock 高精度时钟
  • time_point
  • duration
  • duration_cast
1
std::chrono::time_point<Clock,Duration>::time_point

sample

1
2
3
4
5
using namespace std::chrono_literals; // 8h, 24m, 15s
// 当前时间
std::chrono::system_clock::now(); // return time_point
// duration
std::chrono::duration(10s); // 10 秒

linux xargs

描述

功能

  • 配合管道使用,为命令传递参数

使用

1
2
3
4
5
6
$ find /sbin -perm +700 | ls -l         # NO; 这个命令是错误的
$ find /sbin -perm +700 | xargs ll # NO; 无法配合alias使用
$ find /sbin -perm +700 | xargs ls -l # OK; 这样才是正确的

# 杀死进程
$ ps aux | grep <program-name> | awk '{ print($2)}' | xargs kill

注意

  • xargs 无法获取当前shell的alias,所有无法配合alias使用

参考

Java Socket 传输文件

描述

基于Java Socket传输文件/文件夹,包括客户端、服务端,具备以下特点。

  • 一次连接,传输多文件,在传输大量小文件场景下,可节约大量连接创建时间及资源消耗
  • 支持传输文件夹,但会跳过空文件夹
  • 服务端支持多线程
  • 支持结果回显

实现

依赖

程序集成在Maven工程内,lombok非必需,仅用于提升代码整洁度,如剔除需少量改动代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

基础信息

用于传送每次请求附带参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(fluent = true)
public class FileTransferInfo {
public static final String TYPE_DELETE = "delete";
public static final String TYPE_WRITE = "write";
public static final String TYPE_CLOSE = "close";
public static final String MSG_DONE = "done";
public static final String MSG_ERROR = "error";

private String name;
private String path;
private String type;
}

服务端

服务端包含FileTransferServer.javaFileTransferServerThread.java 两个文件,用于实现多线程支持。

FileTransferServer

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
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.channels.ServerSocketChannel;

public class FileTransferServer {
private ServerSocket ss;
private final int port;
private final String wd;

/**
* @param port network port for listening
* @param wd working directory, files will be saved into this folder
*/
public FileTransferServer(int port, String wd) {
this.port = port;
this.wd = wd;
}

public void start() throws Exception {
FileUtils.forceMkdir(new File(wd));

InetSocketAddress listenAddr = new InetSocketAddress(port);
ServerSocketChannel listener = ServerSocketChannel.open();
ss = listener.socket();
ss.setReuseAddress(true);
ss.bind(listenAddr);

while (true) {
Socket socket = ss.accept();
System.out.println("Received request: " + socket);
FileTransferServerThread thread = new FileTransferServerThread(socket, wd);
thread.start();
}
}

public void close() {
if (ss != null && !ss.isClosed()) {
try {
ss.close();
} catch (Exception ignored) {
}
}
}
}

FileTransferServerThread

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
import org.apache.commons.io.FileUtils;
import com.google.gson.Gson;

import java.io.*;
import java.net.Socket;
import java.nio.file.Path;
import java.nio.file.Paths;

public class FileTransferServerThread extends Thread {
private static final Gson GSON = new Gson();
Socket socket;
String wd;

public FileTransferServerThread(Socket socket, String wd) {
this.socket = socket;
this.wd = wd;
}

@Override
public void run() {
try {
boolean loopFlag = true;
FileTransferInfo info;
DataOutputStream dot = new DataOutputStream(socket.getOutputStream());
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
DataInputStream dis = new DataInputStream(bis);

while (loopFlag) {
String msg = dis.readUTF();
info = GSON.fromJson(msg, FileTransferInfo.class);

if (FileTransferInfo.TYPE_CLOSE.equals(info.type())) {
loopFlag = false;
} else if (FileTransferInfo.TYPE_DELETE.equals(info.type())) {
Path fnl = Paths.get(wd, info.path());
FileUtils.deleteQuietly(fnl.toFile());
} else {
long length = dis.readLong();

Path fnl = Paths.get(wd, info.path(), info.name());
System.out.println("Receiving file: " + fnl.toAbsolutePath() + ", length: " + length);
FileUtils.forceMkdir(Paths.get(wd, info.path()).toFile());
FileUtils.touch(fnl.toFile());

FileOutputStream fos = new FileOutputStream(fnl.toFile());
BufferedOutputStream bos = new BufferedOutputStream(fos);
for (long j = 0; j < length; j++) {
bos.write(bis.read());
}
bos.close();
System.out.println("Saved file: " + fnl.toAbsolutePath());
}

dot.writeUTF(FileTransferInfo.MSG_DONE);
dot.flush();
}

System.out.println("Closing socket...");
dis.close();
dot.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (socket != null) {
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}

客户端

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
import java.io.*;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Paths;

import com.google.gson.Gson;

public class FileTransferClient {
private static final Gson GSON = new Gson();
Socket socket;
DataOutputStream dos;
DataInputStream dis;

public FileTransferClient(Socket socket) throws IOException {
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
this.socket = socket;
this.dos = new DataOutputStream(bos);
this.dis = new DataInputStream(socket.getInputStream());
}

public void send(String path, File file) throws IOException {
if (file.isDirectory()) {
File[] files = file.listFiles();
for (int i = 0; files != null && i < files.length; ++i) {
File f = files[i];
send(Paths.get(path, file.getName()).toString(), f);
}
} else {
System.out.println("Sending file: " + file.getAbsolutePath() + ", length: " + file.length());
dos.writeUTF(GSON.toJson(new FileTransferInfo().name(file.getName()).path(path)));
dos.writeLong(file.length());
Files.copy(file.toPath(), dos);
dos.flush();
System.out.println(dis.readUTF());
}
}

public void close() throws IOException {
FileTransferInfo info = new FileTransferInfo().type(FileTransferInfo.TYPE_CLOSE);
dos.writeUTF(GSON.toJson(info));
dos.flush();
System.out.println(dis.readUTF());
dis.close();
dos.close();
socket.close();
}

public void delete(String path) throws IOException {
FileTransferInfo info = new FileTransferInfo().path(path).type(FileTransferInfo.TYPE_DELETE);
dos.writeUTF(GSON.toJson(info));
dos.flush();
System.out.println(dis.readUTF());
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
// 启动服务端
FileTransferServer server = new FileTransferServer(2221, "/tmp");
server.start();

// 客户端传送文件
String host = "127.0.0.1";
int port = 2221;
String path = "receiver";
Socket socket = new Socket(host, port);
FileTransferClient client = new FileTransferClient(socket);
client.delete(path);
client.send(path, new File(".../Downloads/cat.jpg"));
client.close();

更多

  • 优化
    • 零拷贝
  • 代码

shell 编程

快捷键

1
2
3
4
5
6
7
8
9
10
11
12
Ctrl + R : 搜索历史命令
Ctrl + a : 指针跳到命令行开始
Ctrl + e : 指针跳到命令行结尾
Ctrl + b : 向后移动一个字符
Ctrl + f : 向前移动一个字符

Alt + right : 向右移动一个词
Alt + left : 向右移动一个词
Ctrl + X, Ctrl + X : 在当前位置和开始位置切换

Ctrl + s : 暂停终端输出
Ctrl + q : 恢复终端输出

特殊参数

1
2
3
4
5
6
7
8
9
10
11
12
13
$!  最后运行的后台线程 id
$? 最后运行的命令的结束码
$$ 脚本本身进程 id

$# 参数个数
$@ 参数列表. "$@" = "$1" "$2" "$3" "$..."
$* 参数列表. "$*" = "$1 $2 $3 ..."
$0 脚本文件名
$1~$n 参数

# 参数列表截取
${*:2} 截取第二个参数及之后
${*:2:3} 截取第二、三个参数

getopts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# opts
:前缀 忽略错误
:后缀 参数后必须有值

# example
:abc:de: 忽略参数错误,-c、-e后必须有值

while getopts ":ab:Bc:" opt; do
case $opt in
a) echo "found -a" ; a="ok" ;;
b) echo "found -b and value is: $OPTARG" ;;
*) "echo usage" ;;
esac
done

函数

1
2
3
4
5
echo() {
echo "$1"
}

echo Hello

test

test 命令通常被 [...] 替代

1
2
3
4
5
6
7
8
# [ ... ]
[ ... ] :

# if ... else ...
if [ expression ]
then
...
fi

[] 和 [[]] 的区别

  • 兼容性

    • [] 是内置命令 test 的可选项,在 Linux、Unix、POSIX 系统兼容

    • [[]] 是 Korn Shell 作为增强功能引入的,被 base、zsh 等支持,但在 POSIX 系统中不兼容

  • [[]][] 的差异

    • 比较运算符
      • 比如,[[ 1 < 2 ]],等价于 [ 1 \< 2 ]
    • 布尔运算符(-a -> &&-o -> ||
      • 比如,[[ 3 -eq 3 && 4 -eq 4 ]] 等价于 [ 3 -eq 3 -a 4 -eq 4 ]
    • 组合表达式
      • 比如,[[ 3 -eq 3 && (2 -eq 2 || 1 -eq 1) ]] 等价于 [ 3 -eq 3 -a \( 2 -eq 2 -o 1 -eq 1 \) ]
    • 模式匹配
      • 比如,[[ $name = *c* ]][] 中没有对应的匹配运算
    • 正则匹配
      • 比如,[ $name =~ ^Ali ][] 中没有对应的匹配运算
    • 单词切分([[]] 不会对变量值按空白符切分,[] 对应得需要添加 "" 来达到相似的效果)
      • 比如,name = "Wukong Sun"[[ $name ]] 等价于 [ "$name" ]

参数

字符串

1
2
3
4
5
6
-n 不为空
-z 为空
{string1} = {string2} string1 和 string2 相同
{string1} != {string2} string1 和 string2 不相同
{string1} < {string2} 基于 ASCII 码比较
{string1} > {string2} 基于 ASCII 码比较

正则匹配

1
2
3
4
"{data}" =~ ^{regex}$

# 示例
[[ "$date" =~ ^[0-9]{8}$ ]] && echo "YES"

变量

1
-v 变量是否 set

数字比较

1
2
3
4
5
6
7
{number1} -eq/-ne/-lt/-le/-gt/-ge {number2}
{number1} -eq {number2} 相等
{number1} -ne {number2} 不相等
{number1} -lt {number2} 小于
{number1} -le {number2} 小于等于
{number1} -gt {number2} 大于
{number1} -ge {number2} 大于等于

文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-a/-e 文件是否存在
-b 文件存储在块存储设备
-d 是否是文件夹
-f 文件存在且是常规文件
-h/-L 文件是符号链接
-p 文件是否是使用 mkfifo 创建的命名管道
-r 是否可读(运行命令的用户)
-s 文件存在且不为空
-S 是否是 Socket 文件
-t fd (file descriptor) 是否在终端中打开
-w 是否可写(运行命令的用户)
-x 是否可执行(运行命令的用户)
-O 运行命令的用户是否是文件 Owner
-G 文件是否被运行命令的用户 Group 拥有
{file1} -nt {file2} file1 是否比 file2 更新
{file1} -nt {file2} file1 是否比 file2 更旧
{file1} -ef {file2} file1 是否是 file2 的硬链接

多表达式

1
2
{expr1} -o {expr2} 或
{expr1} -a {expr2} 且

路径及文件

1
2
3
4
5
6
7
8
9
10
11
# 提取文件名
$(basename "/path/to/file") # file

# 提取文件路径(非绝对路径)
$(dirname "/path/to/file") # /path/to

# 获取绝对路径
$(pwd)/$(dirname "./path/to/file")

# 脚本路径
BASE=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)

数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 定义数组
ARR=(a b c)

# ARR[@]: 数组转换为字符串
echo "${ARR[@]}"
a b c

# ARR[*]
echo "${ARR[*]}"
a b c

# 数组赋值
ANOTHER=("${ARR[@]}")

# 长度
echo ${#ARR[@]}
3

# 序号
for idx in "${!ARR[@]}"; do
echo "$idx"
done

[@] VS [*]

数组的 [@][*] 都是取所有元素,但是有所差别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 定义
$ ARR=(a b c)
$ A=("${ARR[@]}")
$ B=("${ARR[*]}")

# A
$ echo ${#A[@]}
3
$ echo ${A[@]}
a b c

# B
$ echo ${#B[@]}
1
$ echo ${B[@]}
a b c

布尔

1
2
3
4
FLAG=true
if [[ $FLAG == true ]]; then
# do something
fi

字符串

比较

1
[ "$v" = "value" ]

截取

1
2
3
4
5
$ s=abcd
# 截取区间是 [left, right]
$ echo ${s:1}
bcd
$ echo ${s:1:2}

切分

IFS=';' read -ra SLICES <<< "$IN"

1
2
3
4
IFS=';' read -ra ADDR <<< "$IN" # 按 ; 分割
for i in "${ADDR[@]}"; do
# process "$i"
done

替换

1
echo ${LINE//{old}/{new}}

循环

while

语法

1
2
3
4
while command
do
Statement(s) to be executed if command is true
done

示例

1
2
3
4
5
6
7
8
9
a=0
while [ $a -lt 10 ]
do
echo $a
a=`expr $a + 1`
done

# 等待 port
while ! lsof -i:8080; do echo "wait for server ready"; sleep 1; done

数组

1
2
3
4
5
6
7
8
9
10
# 命令行
$ countries=(china us); for country in ${countries[@]}; echo $country
china
us

# 脚本
countries=(china us)
for country in ${countries[@]}; do
echo $country
done

范围

1
2
3
4
5
6
7
8
9
10
11
12
13
$ for id in {1..3}; echo $id   # 1 2 3

# 脚本
for id in {1..3}; do
echo $id
done

# seq [首数] [增量] 尾数
$ seq 1 3 # [1, 3]
$ seq 3 # [1, 3]
$ seq 1 2 5 # 1, 3, 5

$ for i in `seq 3`; do echo $i; done # 1 2 3

case…esac

1
2
3
4
5
6
7
8
9
10
11
12
word=a
case $word in
a)
echo "a $word"
;;
b)
echo "b $word"
;;
*)
echo "* $word"
;;
esac

getopts

参数

1
2
3
4
5
6
# opts
:前缀 忽略错误
:后缀 参数后必须有值

# example
:abc:de: 忽略参数错误,-c、-e后必须有值

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
usage() {
cat <<EOF
Usage: $0 [-a] [-b name] msg
EOF
}

while getopts ":ab:Bc:" opt; do
case $opt in
a) echo "found -a" ; a="hello" ;;
b) echo "found -b and value is: $OPTARG" ;;
c) echo "found -c and value is: $OPTARG" ;;
*) usage ;;
esac
done

shift $(($OPTIND - 1))

数据存储对比

数据库

KV存储

服务 备注
Redis
HBase
Pegasus

关系型数据库

服务 备注
MySQL

时序数据库

服务 备注
TDengine
InfluxDB

查询引擎

  • Impala
  • Presto

其他

OLAP & OLTP

OLTP OLAP
特性 操作处理 信息处理
面向 事务 分析
用户 操作人员,底层管理人员 决策人员,高级管理人员
功能 日程操作处理 分析决策
DB设计 面向应用 面向主题
数据 当前的,最新的,细节的,二维的,分离的 历史的,聚焦的,多维的,集成的,统一的
存取 读/写数十条记录 读上百万条数据
工作单位 简单的事务 复杂的查询
用户数 上千个 上百万个
规模 GB TB
时效 具有时效性要求 时效要求不严格
主要应用 数据库 数据仓库
优先 事务吞吐量 查询吞吐量、响应时间

待分类

  • Impala
  • Druid
  • ClickHouse
  • Hive
  • Impala
  • InfluxDB

参考

mac usage

初始化

homebrew

1
$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

node

参见 前端/node

工具

homebrew

commond line tools

1
2
3
4
5
6
7
8
9
10
# install
$ xcode-select --install

# delete
$ sudo rm -rf `xcode-select -p` # 一般会在文件夹 /Library/Developer/CommandLineTools 内

# Problems
## Can’t install the software because it is not currently available from the Software Update server.
# 手动下载
https://developer.apple.com/download/more/?=command%20line%20tools

问题

拷贝文件导致图标变灰无法访问

尝试 这里 无果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 仅适用文件夹
# 命令行输入如下内容
exitFun() {
echo "ERROR: failed, due to $1"
exit 1
}

fix() {
[ ! -d "${1}" ] && exitFun "target is not folder"
mv "${1}" "${1}_back"
mkdir "${1}"
mv "${1}_back"/* "${1}/"
mv "${1}_back"/.[^.]* "${1}/"
rmdir "${1}_back"
}

# 修复,输入 fix <文件夹目录> OR 输入 fix [拖拽文件夹至终端]
$ fix <path>

mysql/mariadb usage

[toc]

MySQL

**注 : **有些操作和操作系统有关。

CLI登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql -h localhost --port 3306 -u root -p --socket=/var/lib/mysql/mysql.sock  

#### 示例
bovenson@MBP:~/Git/notes/MySQL$ mysql -h localhost -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 14
Server version: 5.7.20 MySQL Community Server (GPL)

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

启动/停止

1
2
3
4
### Mac
sudo /usr/local/mysql/support-files/mysql.server start # 启动
sudo /usr/local/mysql/support-files/mysql.server stop # 停止
sudo /usr/local/mysql/support-files/mysql.server restart # 重启

修改密码认证规则

1
2
3
4
5
6
7
SET GLOBAL validate_password.LENGTH = 4;
SET GLOBAL validate_password.policy = 0;
SET GLOBAL validate_password.mixed_case_count = 0;
SET GLOBAL validate_password.number_count = 0;
SET GLOBAL validate_password.special_char_count = 0;
SET GLOBAL validate_password.check_user_name = 0;
FLUSH PRIVILEGES;

修改密码

SET PASSWORD

1
2
3
4
5
# set password for 用户名@localhost = password('新密码'); 
set password for root@localhost = password('123');

# 修改
ALTER USER 'user'@'localhost' IDENTIFIED BY 'pass';

系统用户

添加

1
2
CREATE USER 'myuser'@'localhost' IDENTIFIED BY 'mypass';
CREATE USER 'myuser'@'%' IDENTIFIED BY 'mypass';

赋予权限

1
2
3
GRANT ALL ON *.* TO 'myuser'@'localhost';
GRANT ALL ON *.* TO 'myuser'@'%';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;

刷新权限

1
> FLUSH PRIVILEGES;

允许远程连接

设置bind-address

1
2
3
4
5
6
7
# mysql
sudo vim /etc/mysql/mysql.conf.d/mysqld.cnf
# mariadb
/etc/mysql/mariadb.conf.d/50-server.cnf

# 修改bind-address
bind-address = 0.0.0.0

添加/修改账户

添加/修改账户允许的Host%

命令行远程登录mysql服务器

1
mysql -u root -p -h 10.154.0.43 -P 3306

设置

1
2
> USE mysql;
> UPDATE user SET Host='%' WHERE User='root' AND Host='localhost';

workbench

时间设置默认当前

默认设置为CURRENT_TIMESTAMP

添加管理账户

lower_case

错误

Can’t get hostname from your ip address

Just add below in my.ini or my.cnf.

1
2
[mysqld]
skip-name-resolve

使用

执行 SQL 文件

1
> SOURCE /path/to/file.sql;

创建数据库

1
> CREATE DATABASE 'database-name';

Pretty 查询结果

1
> select * from <table> \G

安装

CentOS

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
# 下载 rpm 源
$ wget https://repo.mysql.com//mysql57-community-release-el7-11.noarch.rpm

# 安装 rpm 源
$ yum localinstall mysql57-community-release-el7-11.noarch.rpm

# 安装 MySQL
$ yum install mysql-community-server

# 启动 MySQL
$ systemctl start mysqld

# 开机启动
$ systemctl enable mysqld
$ systemctl daemon-reload

# 获取root密码
$ cat /var/log/mysqld.log | grep password
... [Note] A temporary password is generated for root@localhost: ...

# 登录mysql
$ mysql -u root -p
# 输入获取到的临时密码

# 重置root密码
mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY '{password}';

# 更新密码
mysql > UPDATE mysql.user SET Password=PASSWORD('root') WHERE User='root' AND Host='%';

# 新建远程登录用户
mysql> CREATE USER 'root'@'%' IDENTIFIED BY '{password}';

# 授权
mysql> GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;

# 刷新权限
mysql> FLUSH PRIVILEGES;

# 防火墙放行3306端口
$ firewall-cmd --zone=public --add-port=3306/tcp --permanent
$ firewall-cmd --reload

导入导出

参数

1
2
-B, --databases : 导出创建语句
--skip-add-locks : 导出的文件中跳过添加 LOCK TABLE 语句

示例

1
2
3
$ mysqldump -uroot -p<password> -h 'host' -P port <database>  # 导出数据库
$ mysqldump -uroot -p<password> -h 'host' -P port <database> <table> # 导出表
$ mysqldump -uroot -p<password> d <database>

术语

编码

  • OOP 面向对象编程
  • AOP 面向切面编程
  • IOC 控制反转
  • DO (Data Object) 数据对象
  • DTO (Data Transfer Object) 数据传输对象
  • BO(Business Obejct) 业务对象
  • AO(Application Object) 应用对象
  • VO(View Object) 显示层对象
  • POJO (Plain Ordinary Java Object) 简单Java对象
  • DAO(Data Access Object) 数据存取对象

并行计算

  • MPP(Massively parallel Processing) 大规模并行处理

架构

  • RPS (Request Per Second) 每秒请求量
  • Ad-Hoc Query 海量实时计算/查询
  • OLAP (Online Analytical Processing) 联机分析处理
  • OLTP (Online Transaction Processing) 联机事务处理
  • LSM-tree (Log Structured-Merge Tree) 是一种分层,有序,面向磁盘的数据结构
  • WAL (Write-ahead logging) 预写式日志
  • LSN (Log Sequence Number) 日志序列号

大数据

  • BI (Business Intelligence) 智能商业
  • ETL (Extract-Transform-Load) 数据仓库技术,用于描述将数据从源端经过抽取、转换、加载到目的端的过程
  • UDF & UDAF & UDTF
    • UDF (User-Defined Function) 用户自定义函数
    • UDAF (User-Defined Aggregation Function) 用户自定义聚合函数
    • UDTF (User-Defined Table Generating Function)
  • HSAP (Hybrid Serving and Analytical Processing) 混合服务/分析处理
  • HTAP (Hybrid Transactional/Analytical Processing) 混合事务/分析处理
  • DW (Data Warehouse) 数据仓库 参考一 参考二 数据仓库分层
    • DM (Data Mart) 数据集市
    • 分层
      • ODS (Operational Data Store) 操作性数据 / 数据准备层 / 数据运营层
      • DW 数据仓库层
        • DWD (Data Warehouse Detail) 细节数据层
        • DWB (Data Warehouse Base) 数据基础层
        • DWS (Data Warehouse Service) 数据服务层
      • ADS (Apoplication Data Service) 应用数据层
  • Wide Table 宽表,业务主题相关的指标、维度、属性关联在一起的一张数据库表

广告

  • CVR(Conversion Rate) 转化率
  • CPA(Cost Per Action) 广告计费方式,指按广告投放实际效果收费
  • CPM(Cost Per Mille) 成本/千人,在投放广告领域,是一种按投放量收费的计费方式

产品

  • PV(page view) 页面浏览量,用户多次打开同一页面累计多次
  • UV(unique visitor) 独立访客,一天内访问网站去重用户数
  • IP 独立IP,一天内访问网站去重IP数
  • VV(video view) 视频播放量,统计周期内视频被播放次数
  • CV(content view) 内容播放量,是一个周期内,视频被打开且成功播放次数

口语

  • trade-offs 权衡