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

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


  • 首页

  • 归档

  • 搜索

小码农教你堆排序 堆排序

发表于 2021-11-21

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

堆排序

升序

一种非常正常的想法 空间复杂度O(N)

把数组中的元素全都push到小堆中,然后再取堆顶元素重新给数组,就可以达到升序的效果了

堆升序函数HeapSort

image-20211109215923477

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c复制代码//升序
void HeapSort(HPDataType* a,int n)
{
HP hp;
HeapInit(&hp);
int i = 0;
//建立一个小堆
for (i = 0; i < n; i++)
{
HeapPush(&hp, a[i]);
}
//然后把堆顶的元素重新放到数组里面
for (i = 0; i < n; i++)
{
a[i] = HeapTop(&hp);
//用完pop掉
HeapPop(&hp);
}
HeapDestroy(&hp);
}

堆排序测试函数

image-20211109220129388

1
2
3
4
5
6
7
8
9
10
11
12
13
c复制代码void testHeapSort()
{
int a[] = { 40,2,0,12,5,454,2323 };
//堆排序
HeapSort(a, sizeof(a) / sizeof(a[0]));
int i = 0;
for (i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
//把数组里面的元素取出来
printf("%d ",a[i]);
}
printf("\n");
}

建堆(向上向下为建堆)

向上调整(建大堆)

上面做法一点毛病都没有,但是有要求了,空间复杂度为O(1) 也就是我们不可以在用Heap了

(这里的插入不是真正的插入,因为这些数据原本就在里面,我们就是在调堆,类似插入)

image-20211110004419158

image-20211110004843299

看到上面打印的结果我们看到建的是小堆,但是不好的是最小的在下标为0的位置,再次找次小的,从下标为一的位置再构建,这是不行的,因为会破坏结构,所以我们要重头建大堆,然后收尾交换

image-20211112004113756

1
2
3
4
5
6
7
8
9
10
11
c复制代码//真正玩法
void HeapSort(HPDataType* a, int n)
{
assert(a && n>=0);
//把数组a构建成堆
int i = 0;
for (i = 0; i < n; i++)
{
AdjustUp(a,n,i);
}
}

交换排序&&再向上调整

image-20211112011348827

堆排序代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
c复制代码//真正玩法
void HeapSort(HPDataType* a, int n)
{
assert(a && n>=0);
//把数组a构建成堆
int i = 0;
//向上调整
for (i = 0; i < n; i++)
{
AdjustUp(a,i);
}
//根与最后一个数交换并每次都找到次大的数
int tail = n - 1;
while (tail)
{
Swap(&a[0], &a[tail]);//根与最后一个数交换
tail--;
for (i = 0; i <= tail; i++)
{
AdjustUp(a, i);
}
}
}
堆排序测试
1
2
3
4
5
6
7
8
9
10
11
12
13
c复制代码void testHeapSort()
{
int a[] = { 40,2,50,12,5,454,2323,};
//堆排序
HeapSort(a, sizeof(a) / sizeof(a[0]));
int i = 0;
for (i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
//把数组里面的元素取出来
printf("%d ",a[i]);
}
printf("\n");
}

向下调整

排升序 构建小堆

image-20211110204752959

排升序 构建大堆

有种就是这样玩的

image-20211110234106150

堆排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
c复制代码//真正玩法
void HeapSort(HPDataType* a, int n)
{
assert(a && n>=0);
//把数组a构建成堆
int i = 0;
////向上调整
//for (i = 0; i < n; i++)
//{
// AdjustUp(a,n,i);
//}
//向下调整
for (i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
//根与最后一个数交换并每次都找到次大的数
int tail = n - 1;
for (i = tail; i > 0; i--)
{
Swap(&a[0],&a[i]);//根与最后一个数交换
AdjustDown(a, i, 0);//向下调整每次都找到次大的数
}
}
测试堆排序
1
2
3
4
5
6
7
8
9
10
11
12
13
c复制代码void testHeapSort()
{
int a[] = { 40,2,50,12,5,454,232,30,3,10 };
//堆排序
HeapSort(a, sizeof(a) / sizeof(a[0]));
int i = 0;
for (i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
//把数组里面的元素取出来
printf("%d ",a[i]);
}
printf("\n");
}

降序

向上调整 (建小堆)

image-20211112012044528

向下调整(建小堆)

image-20211112012356035

建堆的时间复杂度

3.2.3 建堆时间复杂度因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果):

image-20211111230047286

image-20211111230800529

所以时间复杂度是O(n)

本文转载自: 掘金

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

FPGA ZYNQ Linux内核分离编译

发表于 2021-11-21

1 开发模式

image.png
image.png

  • 编写 linux 驱动的时候,经常改动的要素有设备树文件、linux 内核、根文件系统,当然如果改动 PL 的话还需改动 bit 文件。因而我们将这些要素独立出来,从而方便修改变更。也就是说我们将 bit 文件从原先的 BOOT.BIN 文件独立出来,将 image.ub 文件分开为内核 zImage和设备树dtb。另外将根文件系统放到 SD 卡的

2 BOOT.BIN 剥离出bitStream文件

  • 先在 Ubuntu 主机终端中选一个合适的路径以创建出厂镜像的 Petalinux 工程,然后在终端中输入如下命令:
1
2
3
4
arduino复制代码    source /opt/pkg/petalinux/2018.3/settings.sh //设置 petalinux 工作环境
petalinux-create -t project --template zynq -n ALIENTEK-ZYNQ //创建 Petalinux 工程
cd ALIENTEK-ZYNQ //进入到 petalinux 工程目录下
petalinux-config --get-hw-description ../hdf/Navigator_7010.sdk/ //导入 hdf 文件

image.png

  • 进入“Subsystem AUTO Hardware Settings”子菜单下的“Advanced bootable imagesstorage Settings”菜单中,移动到“dtb image settings”选项,并将 image storage media 设置为 primary sd

image.png

  • 进入到“Image Packaging Configuration”菜单下的“Root filesystem type (INITRAMFS)”子菜单下
    image.png
  • 配置完成后,编译 uboot,及生成 BOOT.BIN 文件
1
2
css复制代码    petalinux-build -c u-boot
petalinux-package --boot --fsbl --u-boot --force

image.png

3 image.ub剥离出ZImage/设备树/根文件

  • 生成设备树文件,在 Petalinux 工 程 中 执 行 编 译 uboot 后 , 会 在 工 程 的components/plnx_workspace/device-tree/device-tree/目录下生成设备树文件,红框圈出的是我们需要用到的设备树文件,skeleton.dtsi 文件我们一般不用。
    image.png
  • 内核源码,用新xilinx 官方 2018.3 版本( 这个版本是 x xilinx 设定的版本,其 x linux 版本为 4.14.0)的内核源码
    image.png

3.1 修改设备树文件

  • 将前面生成的设备树文件(生成的设备树文件在Petalinux 工程components/plnx_workspace/device-tree/device-tree/目录下)pcw.dtsi、pl.dtsi、system-top.dts 以及 zynq-7000.dtsi 四个文件直接拷贝到内核源码目录下的 arch/arm/boot/dts 目录中。

3.2 编译内核

1
ini复制代码make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j10

image.png

  • 编译完成之后会在 arch/arm/boot/目录下生成一个名为 zImage 的内核镜像文件

3.3 编译设备树

  • 在内核里边我们需要单独编译出设备树的 dtb 文件,前面已经将我们所需要的设备树文件拷贝到内核的 arch/arm/boot/dts 目录下了,接下来执行下面这条命令编译 system-top.dtb文件:
1
ini复制代码make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- system-top.dtb -j10
  • 编译成功之后会在 arch/arm/boot/dts 目录下生成 system-top.dtb 文件
  • 为了方便、好看,笔者将 system-top.dtb 文件进行了重命名 system.dtb
    image.png

3.4 编译 rootfs

1
arduino复制代码petalinux-config -c rootfs
  • 进入“Image Features —>”菜单下,使能“debug-tweaks”,如下图所示
    image.png
  • 等待其编译完成,完成之后产生的根文件系统压缩包在 images/linux 目录下
    image.png

4 制作SD启动卡

image.png

4.1 拷贝镜像到FAT分区

  • 将前面过程当中生成的各种镜像文件拷贝到 SD 启动卡的 FAT 分区,包括 zImage(内核镜像,内核源码目录 arch/arm/boot/zImage)、 system-top.dtb(内核设备树 dtb 文件,内核源码目录 arch/arm/boot/dts/system-top.dtb)、 systemt.bit(pl 端 bitstream 文件,Petalinux工程目录下的 images/linux/system.bit)。
  • 将BOOT.BIN文件拷贝到 FAT 分区
    image.png

4.2 将根文件系统解压到EXT4

1
bash复制代码sudo tar -xzf rootfs.tar.gz -C /media/linux/rootfs

image.png

5启动开发板

  • 在U-Boot 启动 2 秒倒计时之前,按回车或者是空格键停止启动,进入到 U-Boot 的命令行
    模式,因为现在不能直接启动,我们需要对 U-Boot 环境变量进行修改,在 U-Boot 命令行下执
    行下面这些命令设置环境变量,
    image.png

image.png

1
2
3
4
5
6
7
8
9
10
dart复制代码env default -a
setenv bitstream_load_address 0x100000
setenv bitstream_image system.bit
setenv bitstream_size 0x300000
setenv kernel_img zImage
setenv dtbnetstart 0x2000000
setenv netstart 0x2080000
setenv default_bootcmd 'if mmcinfo; then run uenvboot; echo Copying Linux from SD to RAM... && load mmc
0 ${bitstream_load_address} ${bitstream_image} && fpga loadb 0 ${bitstream_load_address} ${bitstream_size}
&& run cp_kernel2ram && run cp_dtb2ram && bootz ${netstart} - ${dtbnetstart}; fi'
  • 保存完成后执行 boot 命令启动内核或者执行 reset 重启开发板:

image.png

本文转载自: 掘金

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

若依系统分页工具学习-PageHelper篇九

发表于 2021-11-21

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

在昨日文章《若依系统分页工具学习-PageHelper篇八》中,我们在AbstractHelperDialect.beforePage方法中发现了一个变量Page,并且知道了PageHelper是通过ThreadLocal<Page>来将其作为线程局部变量来巧妙的传递的。

今天我们接着看在通过beforePage方法判断需要分页后,程序又执行了哪些逻辑?

接着看ExecutorUtil类的pageQuery方法:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码    public static <E> List<E> pageQuery(Dialect dialect, Executor executor, MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql, CacheKey cacheKey) throws SQLException {
//判断是否需要进行分页查询
if (dialect.beforePage(ms, parameter, rowBounds)) {
//生成分页的缓存 key
CacheKey pageKey = cacheKey;
//处理参数对象
parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
// ....其他代码暂时不贴

}

下一行代码注释为生成分页的缓存key,之间将参数cacheKey赋值给局部变量pageKey,不多阐述。

紧接着下一行:

1
2
java复制代码//处理参数对象
parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);

这一行代码调用dialect处理参数方法处理parameter并且将返回值重新赋值给parameter参数。

我们来细细的看一下其中的参数处理逻辑。

通过前几篇文章,我们知道MysqlDialect的类层次结构:

Dialect -> AbstractDialect -> AbstractHelperDialect -> MySqlDialect

我们在最近的一层发现AbstractHelperDialect中有其实现:

1
2
3
4
5
java复制代码@Override
public Object processParameterObject(MappedStatement ms, Object parameterObject, BoundSql boundSql, CacheKey pageKey) {
//处理参数
// 具体代码....
}

我们逐行查看其处理逻辑:

1
2
3
4
5
6
java复制代码//处理参数
Page page = getLocalPage();
//如果只是 order by 就不必处理参数
if (page.isOrderByOnly()) {
return parameterObject;
}

Page参数不多叙述了,在文章《PageHelper篇八》中已经详细阐述。

这里有一个奇怪的点或者也可能是代码风格,通过检索orderByOnly我没有发现在什么情况下会设置其为false的代码逻辑,换句话说,目前orderByOnly默认值为false,而调用setOrderByOnly的情况只有setOrderByOnly(true)

接着往下看:

1
2
3
4
5
6
7
8
9
10
11
java复制代码Map<String, Object> paramMap = null;
if (parameterObject == null) {
paramMap = new HashMap<String, Object>();
} else if (parameterObject instanceof Map) {
//解决不可变Map的情况
paramMap = new HashMap<String, Object>();
paramMap.putAll((Map) parameterObject);
} else {
/// 其他情况代码.....省略
}
return processPageParameter(ms, paramMap, page, boundSql, pageKey);

前面两种if处理参数为空以及参数为map的情况。

下面代码涉及到多个参数相关的类,暂时不多解释。我们最关心的仍然是sql修改。

本文转载自: 掘金

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

Python使用Redis 前言 环境准备 开始实践

发表于 2021-11-21

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

前言

前面我们都是使用 Redis 客户端对 Redis 进行使用的,但是实际工作中,我们大多数情况下都是通过代码来使用 Redis 的,由于小编对 Python 比较熟悉,所以我们今天就一起来学习下如何使用 Python 来操作 Redis。

环境准备

  • Redis 首先需要安装好。
  • Python 安装好(建议使用 Python3)。
  • Redis 的 Python 库安装好(pip install redis)。

开始实践

小试牛刀

例:我们计划通过 Python 连接到 Redis。然后写入一个 kv,最后将查询到的 v 打印出来。

直接连接

1
2
3
4
5
6
7
8
9
python复制代码#!/usr/bin/python3

import redis # 导入redis模块

r = redis.Redis(host='localhost', port=6379, password="pwd@321", decode_responses=True) # host是redis主机,password为认证密码,redis默认端口是6379
r.set('name', 'phyger-from-python-redis') # key是"name" value是"phyger-from-python-redis" 将键值对存入redis缓存
print(r['name']) # 第一种:取出键name对应的值
print(r.get('name')) # 第二种:取出键name对应的值
print(type(r.get('name')))

执行结果

服务端查看客户端列表

其中的 get 为连接池最后一个执行的命令。

连接池

通常情况下,需要连接 redis 时,会创建一个连接,基于这个连接进行 redis 操作,操作完成后去释放。正常情况下,这是没有问题的,但是并发量较高的情况下,频繁的连接创建和释放对性能会有较高的影响,于是连接池发挥作用。

连接池的原理:预先创建多个连接,当进行 redis 操作时,直接获取已经创建好的连接进行操作。完成后,不会释放这个连接,而是让其返回连接池,用于后续 redis 操作!这样避免连续创建和释放,从而提高了性能!

1
2
3
4
5
6
7
8
9
10
python复制代码#!/usr/bin/python3

import redis,time # 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

pool = redis.ConnectionPool(host='localhost', port=6379, password="pwd@321", decode_responses=True) # host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379
r = redis.Redis(connection_pool=pool)
r.set('name', 'phyger-from-python-redis')
print(r['name'])
print(r.get('name')) # 取出键name对应的值
print(type(r.get('name')))

执行结果

你会发现,在实际使用中直连和使用连接池的效果是一样的,只是在高并发的时候会有明显的区别。

基操实践

对于众多的 Redis 命令,我们在此以 SET 命令为例进行展示。

格式: set(name, value, ex=None, px=None, nx=False, xx=False)

在 redis-py 中 set 命令的参数:

参数名 释义
ex <int>过期时间(m)
px <int>过期时间(ms)
nx <bool>如果为真,则只有 name 不存在时,当前 set 操作才执行
xx <bool>如果为真,则只有 name 存在时,当前 set 操作才执行

ex

我们计划创建一个 kv 并且设置其 ex 为 3,期待 3 秒后此 k 的 v 会变为 None。

1
2
3
4
5
6
7
8
9
10
11
python复制代码#!/usr/bin/python3

import redis,time # 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

pool = redis.ConnectionPool(host='localhost', port=6379, password="pwd@321", decode_responses=True) # host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379
r = redis.Redis(connection_pool=pool)
r.set('name', 'phyger-from-python-redis',ex=3)
print(r['name']) # 应当有v
time.sleep(3)
print(r.get('name')) # 应当无v
print(type(r.get('name')))

3秒过期

nx

由于 px 的单位太短,我们就不做演示,效果和 ex 相同。

我们计划去重复 set 前面已经 set 过的 name,不出意外的话,在 nx 为真时,我们将会 set 失败。但是人如果 set 不存在的 name1,则会成功。

1
2
3
4
5
6
7
8
9
10
11
python复制代码#!/usr/bin/python3

import redis,time # 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

pool = redis.ConnectionPool(host='localhost', port=6379, password="pwd@321", decode_responses=True) # host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379
r = redis.Redis(connection_pool=pool)
r.set('name', 'phyger-0',nx=3) # set失败
print(r['name']) # 应当不生效
r.set('name1', 'phyger-1',nx=3) # set成功
print(r.get('name1')) # 应当生效
print(type(r.get('name')))

只有不存在的k才会被set

如上,你会发现 name 的 set 未生效,因为 name 已经存在于数据库中。而 name1 的 set 已经生效,因为 name1 是之前在数据库中不存在的。

xx

我们计划去重复 set 前面已经 set 过的 name,不出意外的话,在 nx 为真时,我们将会 set 成功。但是人如果 set 不存在的 name2,则会失败。

1
2
3
4
5
6
7
8
9
10
11
python复制代码#!/usr/bin/python3

import redis,time # 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

pool = redis.ConnectionPool(host='localhost', port=6379, password="pwd@321", decode_responses=True) # host是redis主机,需要redis服务端和客户端都起着 redis默认端口是6379
r = redis.Redis(connection_pool=pool)
r.set('name', 'phyger-0',xx=3) # set失败
print(r['name']) # 应当变了
r.set('name2', 'phyger-1',xx=3) # set成功
print(r.get('name2')) # 应当没有set成功
print(type(r.get('name')))

只有存在的k才会被set

以上,就是今天全部的内容,更多信息建议参考 redis 官方文档。

本文转载自: 掘金

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

一个 Java 对象的诞生

发表于 2021-11-21

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

  1. 一个对象的诞生

当 JVM 通过程序计数器的行号读取到一条 new 指令的时候,首先会去方法区寻找这个类是否被加载、解析、初始化过,如果没有的话需要先进行类的加载。

类被加载之后需要在 Java 堆中分配内存,如果 Java 堆中的内存是非常规整的,记录着使用和未使用区域的分界线,那么内存分配仅仅是将分界线向没有使用的内存方向移动一段距离就可以了。这种方式称为”指针碰撞”。如果内存并不是规整的,那么虚拟机就需要单独维护一个列表,用来记录哪些内存是可用的。在分配的时候通过这个列表找到足够大的一块内存分配给该对象。这种方式称为”空闲列表”。

这里还需要注意一个并发的问题。如果 A 对象正在分配内存,指针位置还没有来得及修改,B 对象又从原来的位置开始分配内存。

为了解决这个问题,有两种方案。一种是将内存分配进行同步处理。保证内存分配的原子性。另外一种解决方式是根据线程的不同,将分配内存的动作划分到不同的区域。每个线程都在自己的内存区域申请内存。这个区域称为本地线程分配缓冲(TLAB)。当这部分区域的内存不够时再进行同步申请新的区域。

内存分配完成后,虚拟机将内存初始化为 0 值,一些属性的默认值都是在这一步赋值的。比如 int 类型、boolean 类型的默认值等。

然后需要对对象进行必要的设置。比如说对象的年龄、对象的哈希值、对象的类的信息等。这部分数据保存在对象头中

  1. 对象的访问

对象创建结束后,有两种方式来访问我们创建的对象。使用句柄和直接指针。

2.1 使用句柄访问。

如果使用句柄访问的话虚拟机会多划出一块内存用来做句柄池。Java 虚拟机栈中记录的就是对象的句柄地址。如下图所示:

20190321-3

2.2 直接指针访问

使用直接指针访问时,Java 虚拟机的栈中记录的是对象在 Java 堆中的直接地址。如下图所示:

20190321-4

这两种方式各有优点,使用句柄访问时在对象移动的时候(GC时)只需要改变句柄的值就可以了,而 Java 栈中的引用地址可以不用改变。

直接指针方式最大的好处就是速度快,它减少了一次指针定位的时间开销。

  1. 参考

  • 深入理解Java虚拟机(第2版)

本文转载自: 掘金

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

爬虫工程师也应该了解的 NodeJs 基础(三) - Exp

发表于 2021-11-21

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

什么是 Express ?

Express 是一个基于 NodeJS 的 Web Server 开发框架,能够帮助我们快速的搭建 Web 服务器

为什么需要 Express ?

1、不使用框架,使用原生的 NodeJS 开发 Web 服务器,我们需要处理很多繁琐且没有技术含量的内容,例如:获取路由,处理路由等等

2、 不使用框架,使用原生的 NodeJS 开发 Web 服务器,需要解析 get、post 参数解析,使用 Express 可以使用现成的插件实现上面的功能,只要关心核心的业务逻辑即可

3、在 js 逆向中,使用的 execjs 经常会出现一些未知的 bug,导致我们调用出现错误,如果使用 express 运行本地 web 接口可以大大的减少工作量,完成加密算法的调度

如何使用 Express ?

不会的建议回顾一下上一篇文章

juejin.cn/post/703265…

手动安装

1
bash复制代码npm install express

简单使用

1
2
3
4
5
6
7
8
js复制代码const express = require("express")
const app = express()
app.get('/',function(req,res){
res.send('hello,express')
})
app.listen(3000,()=>{
console.log("监听端口3000成功")
})

返回静态资源

1
2
3
4
5
6
7
8
9
10
11
js复制代码const express = require("express")
const path = require("path")
const app = express()
app.get('/',function(req,res){
res.send('hello,express')
})
// 这里的 pathname 是存放静态资源的路径
app.use(express.static(path.join(__dirname,'pathname')));
app.listen(3000,()=>{
console.log("监听端口3000成功")
})

获取 get 请求参数

1
2
3
4
5
6
7
8
js复制代码const express = require("express")
const app = express()
app.get('/',function(req,res){
res.send(req.query)
})
app.listen(3000,()=>{
console.log("监听端口3000成功")
})

获取 get 请求参数测试结果

获取 post 请求参数

安装

1
bash复制代码npm install body-parser

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码const express = require("express");
const bodyParser = require('body-parser');

const app = express();
// app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended : false}));

app.post('/',function(req,res){
res.send(req.body['name'])
});

app.listen(3000,()=>{
console.log("监听端口3000成功")
});

获取 post 请求参数截图

Express 在 Js 逆向中的应用

通过上面的两个例子已经可以学会关于 express 是如何处理请求参数的了,现在就把它应用到 Js 逆向中

在之前我们处理 Js 加密使用的是 python的 execjs

这个包已经很久没有更新了,经常会出现一些未知的bug,所以我们今天就要放弃execjs使用express来处理加密的 js

直接上一段之前文章的测试代码

先是 Python + execjs 版本

Python + execjs 版本:

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
python复制代码import requests
import execjs
# 用 postman 直接生成的,勿喷
url = "https://xd.newrank.cn/xdnphb/nr/cloud/douyin/rank/hotAccountAllRankList"

# 这里的 crack_xd.js 就是 js 加密逻辑
with open('crack_xd.js',"r") as f:
js_data = f.read()
js_data = execjs.compile(js_data)

params = js_data.call("get_params","/xdnphb/nr/cloud/douyin/rank/hotAccountAllRankList")
print(params)
payload = "{\"date\":\"2020-08-16\",\"date_type\":\"days\",\"type\":\"娱乐\",\"start\":1,\"size\":20}"
headers = {
'authority': "xd.newrank.cn",
'pragma': "no-cache",
'cache-control': "no-cache,no-cache",
'accept': "application/json, text/plain, */*",
'user-agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
'content-type': "application/json;charset=UTF-8",
'origin': "https://xd.newrank.cn",
'sec-fetch-site': "same-origin",
'sec-fetch-mode': "cors",
'sec-fetch-dest': "empty",
'referer': "https://xd.newrank.cn/data/tiktok/rank/overall",
'accept-language': "zh-CN,zh;q=0.9,en;q=0.8",
'cookie': "Hm_lvt_e20c9ff085f402c8cfc53a441378ca86=1597660797; Hm_lpvt_e20c9ff085f402c8cfc53a441378ca86=1597660859; token=62621CBEE73B4CF98CAA79A77958EA9D",
'Postman-Token': "30dbcaa8-0e0e-44f0-b3ad-b6ddf9c90921"
}

response = requests.request("POST", url, data=payload.encode(), headers=headers, params=params)
print(response.text)

可以看到我们需要先把加密的代码抠出来,然后再使用 execjs 调用这个加密的方法,这个过程中要处理加密的算法的编码问题,以及调度过程中不同语言的编码转换,还有一些调用的 bug

如果使用 Python + express,就很简单了

Python + express 版本:

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
python复制代码import requests
import execjs

url = "https://xd.newrank.cn/xdnphb/nr/cloud/douyin/rank/hotAccountAllRankList"

def get_params():
params_url = "http://localhost:3000"
headers = {
'Connection': "keep-alive",
'Accept-Language': "zh-CN,zh;q=0.9,en;q=0.8",
'Content-Type': "application/x-www-form-urlencoded",
'cache-control': "no-cache",
}

payload = "callback=u_params('/xdnphb/nr/cloud/douyin/rank/hotAccountAllRankList')"
response = requests.post(params_url, headers=headers, data=payload)

return response.text


params = get_params()

payload = "{\"date\":\"2020-08-16\",\"date_type\":\"days\",\"type\":\"娱乐\",\"start\":1,\"size\":20}"
headers = {
'authority': "xd.newrank.cn",
'pragma': "no-cache",
'cache-control': "no-cache,no-cache",
'accept': "application/json, text/plain, */*",
'user-agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
'content-type': "application/json;charset=UTF-8",
'origin': "https://xd.newrank.cn",
'sec-fetch-site': "same-origin",
'sec-fetch-mode': "cors",
'sec-fetch-dest': "empty",
'referer': "https://xd.newrank.cn/data/tiktok/rank/overall",
'accept-language': "zh-CN,zh;q=0.9,en;q=0.8",
'cookie': "token=62621CBEE73B4CF98CAA79A77958EA9D; Hm_lvt_e20c9ff085f402c8cfc53a441378ca86=1597660797,1598777678; Hm_lpvt_e20c9ff085f402c8cfc53a441378ca86=1598777678; _uab_collina=159877767862449280501573",
'Postman-Token': "30dbcaa8-0e0e-44f0-b3ad-b6ddf9c90921"
}

response = requests.request("POST", url, data=payload.encode(), headers=headers, params=params)

print(response.text)

express 代码样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
js复制代码const express = require("express");
const bodyParser = require('body-parser');
const app = express();

app.use(bodyParser.urlencoded({extended : false}));
/*
中间省略加密的逻辑代码
*/
app.post('/',function(req,res){
let params = req.body['callback'];
console.log(params)
let value = eval(params);
res.send(value)
});

app.listen(3000,()=>{
console.log("监听端口3000成功")
});

直接使用 python 访问已经搭建好的接口就行了

代码运行结果

以上就是这次的全部内容了,咱们下次再会 ~

本文转载自: 掘金

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

AEJoy —— AE 插件开发中的 效果 UI 与 事件

发表于 2021-11-21

正文

效果 UI 与 事件

效果可以在两个区域提供自定义 UI :

  • 效果控制窗口(自定义 ECW UI)
  • 合成或层窗口(自定义 Comp UI)。

使用自定义 UI 的效果应该设置 PF_OutFlag_CUSTOM_UI (【从全局选择器的】PF_Cmd_GLOBAL_SETUP 期间从 PF_OutFlags 中) ,并处理 PF_Cmd_EVENT 选择器。

自定义 ECW UI 允许效果使用自定义控件提供参数,该控件既可以与标准参数类型一起使用,也可以与任意数据参数一起使用。

具有自定义 UI 的参数在添加参数时应该设置 PF_PUI_CONTROL(来自参数 UI 标志)。

自定义 Comp UI 允许效果在合成或图层窗口中直接操作视频。

当效果被选择时,窗口可以直接在视频上覆盖自定义控件,并可以处理用户与这些控件的交互,以更快速和自然地调整参数。

效果应该通过调用 PF_REGISTER_UI 来注册自己来接收事件。

After Effects 可以将事件发送到效果 ,以进行用户界面处理和参数管理,并将效果集成到其中心消息队列中。

在发送许多事件以响应用户输入的同时,After Effects 也向管理任意数据参数的效果发送事件。

事件的类型在 PF_EventExtra->e_type 中指定,各种事件如下所述。

Effect UI & Events

Effects can provide custom UI in two areas: (1) the Effect Controls Window (custom ECW UI), and (2) the Composition or Layer Windows (Custom Comp UI).

Effects that use custom UI should set PF_OutFlag_CUSTOM_UI (from PF_OutFlags during PF_Cmd_GLOBAL_SETUP (from Global Selectors), and handle the PF_Cmd_EVENT selector.

Custom ECW UI allows an effect to provide a parameter with a customized control, which can be used either with standard parameter types or Arbitrary Data Parameters.

Parameters that have a custom UI should set PF_PUI_CONTROL (from Parameter UI Flags) when adding the parameter.

Custom Comp UI allows an effect to provide direct manipulation of the video in the Composition or Layer Windows.

When the effect is selected, the Window can overlay custom controls directly on the video, and can handle user interaction with those controls, to adjust parameters more quickly and naturally.

Effects should register themselves to receive events by calling PF_REGISTER_UI.

After Effects can send events to effects for user interface handling and parameter management, integrating effects into its central message queue.

While many events are sent in response to user input, After Effects also sends events to effects which manage arbitrary data parameters.

The type of event is specified in PF_EventExtra->e_type and the various events are described below.

事件

时间类型 指示
PF_Event_NEW_CONTEXT 用户为事件创建了一个新的上下文(可能是通过打开一个窗口)。允许插件使用上下文句柄在上下文中存储状态信息。PF_EventUnion包含有效的上下文和类型,但其他一切都应该被忽略。
PF_Event_ACTIVATE 用户激活了一个新的上下文(可能是通过将一个窗口带到前景)。PF_EventUnion是空的。
PF_Event_DO_CLICK 用户在效果的 UI 内单击。PF_EventUnion包含一个 PF_DoClickEventInfo 。处理鼠标点击和响应,传递拖动信息; 请参阅示例代码)。注意:从 7.0 开始,不要阻塞直到鼠标抬起; 改为依赖于 PF_Event_DRAG 。
PF_Event_DRAG 也是一个点击事件,PF_EventUnion包含一个 PF_DoClickEventInfo 。请求从 PF_Event_DO_CLICK 返回 send_drag == TRUE 。这样 After Effects 就可以从用户的更改中看到新的数据。
PF_Event_DRAW 绘制事件! PF_EventUnion 包含一个 PF_DrawEventInfo。
PF_Event_DEACTIVATE 用户已经停用了一个上下文(可能是通过将另一个窗口带到前景)。PF_EventUnion 是空的。
PF_Event_CLOSE_CONTEXT 上下文已被用户关闭。PF_EventUnion 将为空。
PF_Event_IDLE 一个上下文是开放的,但没有发生任何事情。PF_EventUnion 是空的。
PF_Event_ADJUST_CURSOR 鼠标位于插件的 UI 上方。通过改变 PF_AdjustCursorEventInfo 中的 PF_CursorType 来设置光标。使用特定于操作系统的调用来实现自定义游标; 告诉 After Effects 你已经通过设置 PF_CursorType 为 PF_Cursor_CUSTOM 完成了。尽可能使用 After Effects 游标来保持接口的连续性。
PF_Event_KEYDOWN 按键事件。PF_EventUnion包含一个 PF_KeyDownEvent。
PF_Event_MOUSE_EXITED CS6 新引入的。通知鼠标不再是在一个特定的视图上(仅层或复合)。

Events

Event Indicates
PF_Event_NEW_CONTEXT The user created a new context (probably by opening a window) for events.The plug-in is allowed to store state information inside the context using the context handle.PF_EventUnion contains valid context and type, but everything else should be ignored.
PF_Event_ACTIVATE The user activated a new context (probably by bringing a window into the foreground). PF_EventUnion is empty.
PF_Event_DO_CLICK The user clicked within the effect’s UI. PF_EventUnion contains a PF_DoClickEventInfo.Handle the mouse click and respond, passing along drag info; see sample code), within a context.NOTE: As of 7.0, do not block until mouse-up; instead, rely on PF_Event_DRAG.
PF_Event_DRAG Also a Click Event, PF_EventUnion contains a PF_DoClickEventInfo.Request this by returning send_drag == TRUE from PF_Event_DO_CLICK.Do this so After Effects can see new data from the user’s changes.
PF_Event_DRAW Draw! PF_EventUnion contains a PF_DrawEventInfo.
PF_Event_DEACTIVATE The user has deactivated a context (probably by bringing another window into the foreground). PF_EventUnion is empty.
PF_Event_CLOSE_CONTEXT A context has been closed by the user. PF_EventUnion will be empty.
PF_Event_IDLE A context is open but nothing is happening. PF_EventUnion is empty.
PF_Event_ADJUST_CURSOR The mouse is over the plug-in’s UI. Set the cursor by changing the PF_CursorType in the PF_AdjustCursorEventInfo.Use OS-specific calls to implement a custom cursor; tell After Effects you’ve done so by setting PF_CursorType to PF_Cursor_CUSTOM.Use an After Effects cursor whenever possible to preserve interface continuity.
PF_Event_KEYDOWN Keystroke. PF_EventUnion contains a PF_KeyDownEvent.
PF_Event_MOUSE_EXITED New in CS6. Notification that the mouse is no longer over a specific view (layer or comp only).

本文转载自: 掘金

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

Go 字符串比较

发表于 2021-11-21

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

golang 字符串比较

字符串比较, 可以直接使用 == 进行比较, 也可用用 strings.Compare 比较

go 中字符串比较有三种方式:

  • == 比较
  • strings.Compare 比较
  • strings.EquslFold 比较
1
2
3
4
5
6
7
8
9
10
less复制代码
#### 代码示例
```go
fmt.Println("go"=="go")
fmt.Println("GO"=="go")

fmt.Println(strings.Compare("GO","go"))
fmt.Println(strings.Compare("go","go"))

fmt.Println(strings.EqualFold("GO","go"))

上述代码执行结果如下:

1
2
3
4
5
go复制代码true
false
-1
0
true

Compare 和 EqualFold 区别

  • EqualFold 是比较UTF-8编码在小写的条件下是否相等,不区分大小写
1
go复制代码// EqualFold reports whether s and t, interpreted as UTF-8 strings, // are equal under Unicode case-folding. func EqualFold(s, t string) bool
  • 要注意的是 Compare 函数是区分大小写的, == 速度执行更快
1
go复制代码// Compare is included only for symmetry with package bytes. // It is usually clearer and always faster to use the built-in // string comparison operators ==, <, >, and so on. func Compare(a, b string) int

忽略大小写比较

有时候要忽略大小写比较, 可以使用strings.EqualFold 字符串比较是否相等

源码实现

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
go复制代码// EqualFold reports whether s and t, interpreted as UTF-8 strings,
// are equal under Unicode case-folding, which is a more general
// form of case-insensitivity.
func EqualFold(s, t string) bool {
for s != "" && t != "" {
// Extract first rune from each string.
var sr, tr rune
if s[0] < utf8.RuneSelf {
sr, s = rune(s[0]), s[1:]
} else {
r, size := utf8.DecodeRuneInString(s)
sr, s = r, s[size:]
}
if t[0] < utf8.RuneSelf {
tr, t = rune(t[0]), t[1:]
} else {
r, size := utf8.DecodeRuneInString(t)
tr, t = r, t[size:]
}

// If they match, keep going; if not, return false.

// Easy case.
if tr == sr {
continue
}

// Make sr < tr to simplify what follows.
if tr < sr {
tr, sr = sr, tr
}
// Fast check for ASCII.
if tr < utf8.RuneSelf {
// ASCII only, sr/tr must be upper/lower case
if 'A' <= sr && sr <= 'Z' && tr == sr+'a'-'A' {
continue
}
return false
}

// General case. SimpleFold(x) returns the next equivalent rune > x
// or wraps around to smaller values.
r := unicode.SimpleFold(sr)
for r != sr && r < tr {
r = unicode.SimpleFold(r)
}
if r == tr {
continue
}
return false
}

// One string is empty. Are both?
return s == t
}

通过源码可看到 if 'A' <= sr && sr <= 'Z' && tr == sr+'a'-'A' 可以看到不区分大小写的实现。

看个完整测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码// Golang program to illustrate the
// strings.EqualFold() Function
package main

// importing fmt and strings
import (
"fmt"
"strings"
)

// calling main method
func main() {
// case insensitive comparing and returns true.
fmt.Println(strings.EqualFold("Geeks", "Geeks"))

// case insensitive comparing and returns true.
fmt.Println(strings.EqualFold("computerscience", "computerscience"))
}

执行结构

1
2
go复制代码true
true

欢迎关注工作号:程序员财富自由之路

参考资料

  • www.geeksforgeeks.org/strings-equ…

本文转载自: 掘金

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

linux(CentOS)下安装mongodb 前言 配置y

发表于 2021-11-21

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

前言

在mongodb的官网中有linux各个系统的安装配置说明

docs.mongodb.com/master/admi…

本文以CentOS(RedHat)为例

docs.mongodb.com/master/tuto…

结合官方说明,并加以补充。

配置yum

首先需要配置mongo的yum,这样才能用yum进行安装

创建 /etc/yum.repos.d/mongodb-org-3.2.repo,并添加内容

1
2
3
4
5
6
7
8
9
10
11
ini复制代码[mongodb-org-3.2]

name=MongoDB Repository

baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/3.2/x86_64/

gpgcheck=1

enabled=1

gpgkey=https://www.mongodb.org/static/pgp/server-3.2.asc

安装mongo

配置好yum后,就可以通过yum按照mongo了

1
复制代码sudo yum install -y mongodb-org

这时如果出现Could not resolve host: repo.mongodb.org; Unknown error这样的错误,是因为访问国外的网站不稳定,重试即可。

配置防火墙

还需要禁用SELinux和配置防火墙,否则无法访问

如果没有 semanage需要先安装

1
复制代码yum -y install policycoreutils-python

然后配置防火墙开放27017端口

1
css复制代码semanage port -a -t mongod_port_t -p tcp 27017

最后关闭SELinux,修改/etc/selinux/config文件

1
ini复制代码SELINUX=disabled

启动Mongo

安装完成后自动生成mongo的配置文件/etc/mongod.conf,在其中可以查看数据库地址和日志地址。默认数据库路路径/var/lib/mongo,默认日子路径/var/log/mongodb/mongod.log

我们也可以自己重新配置,mongod.conf内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码#processManagement:
#   fork: true
net:
   bindIp: 127.0.0.1
   port: 27017

storage:
   dbPath: /mnt/mongo

systemLog:
   destination: file
   path: "/mnt/mongodb/mongodb.log"
   logAppend: true

storage:
   journal:
      enabled: true

修改dbPath和systemLog下的path即可。

然后启动Mongo服务

sudo service mongod start

也可以直接通过mongo命令启动,如下:

1
javascript复制代码/usr/bin/mongod -dbpath=/mnt/mongo -logpath=/mnt/mongodb/mongodb.log -logappend -port=27017 -fork --maxConns=20000 --bind_ip=127.0.0.1 --wiredTigerCacheSizeGB=0.2

注意:这条命令后面加上--bind_ip=127.0.0.1,这样可以禁止外网访问,如果不加默认外网是可以访问的,这样如果没有启动账号密码验证,很容易被攻击。但是如果想外网访问,就不能加,那样就最好启动账号密码验证预防攻击。

连接数据库

在终端中执行mongo,如果进入数据库,即表示安装启动成功。

本文转载自: 掘金

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

记一次提升18倍的性能优化

发表于 2021-11-21

背景

最近负责的一个自研的 Dubbo 注册中心经常收到 CPU 使用率的告警,于是进行了一波优化,效果还不错,于是打算分享下思考、优化过程,希望对大家有一些帮助。

自研 Dubbo 注册中心是个什么东西,我画个简图大家稍微感受一下就好,看不懂也没关系,不影响后续的理解。

  • Consumer 和 Provider 的服务发现请求(注册、注销、订阅)都发给 Agent,由它全权代理
  • Registry 和 Agent 保持 Grpc 长链接,长链接的目的主要是 Provider 方有变更时,能及时推送给相应的 Consumer。为了保证数据的正确性,做了推拉结合的机制,Agent 会每隔一段时间去 Registry 拉取订阅的服务列表
  • Agent 和业务服务部署在同一台机器上,类似 Service Mesh 的思路,尽量减少对业务的入侵,这样就能快速的迭代了

回到今天的重点,这个注册中心最近 CPU 使用率长期处于中高水位,偶尔有应用发布,推送量大时,CPU 甚至会被打满。

以前没感觉到,是因为接入的应用不多,最近几个月应用越接越多,慢慢就达到了告警阈值。

寻找优化点

由于这项目是 Go 写的(不懂 Go 的朋友也没关系,本文重点在算法的优化,不在工具的使用上), 找到哪里耗 CPU 还是挺简单的:打开 pprof 即可,去线上采集一段时间即可。

具体怎么操作可以参考我之前的这篇文章,今天文章中用到的知识和工具,这篇文章都能找到。

CPU profile 截了部分图,其他的不太重要,可以看到消耗 CPU 多的是 AssembleCategoryProviders方法,与其直接关联的是

  • 2个 redis 相关的方法
  • 1个叫assembleUrlWeight的方法

稍微解释下,AssembleCategoryProviders 方法是构造返回 Dubbo provider 的 url,由于会在返回 url 时对其做一些处理(比如调整权重等),会涉及到对这个 Dubbo url 的解析。又由于推拉结合的模式,线上服务使用方越多,这个处理的 QPS 就越大,所以它占用了大部分 CPU 一点也不奇怪。

这两个 redis 操作可能是序列化占用了 CPU,更大头在 assembleUrlWeight,有点琢磨不透。

接下来我们就分析下 assembleUrlWeight 如何优化,因为他占用 CPU 最多,优化效果肯定最好。

下面是 assembleUrlWeight 的伪代码:

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
go复制代码func AssembleUrlWeight(rawurl string, lidcWeight int) string {
u, err := url.Parse(rawurl)
if err != nil {
return rawurl
}

values, err := url.ParseQuery(u.RawQuery)
if err != nil {
return rawurl
}

if values.Get("lidc_weight") != "" {
return rawurl
}

endpointWeight := 100
if values.Get("weight") != "" {
endpointWeight, err = strconv.Atoi(values.Get("weight"))
if err != nil {
endpointWeight = 100
}
}

values.Set("weight", strconv.Itoa(lidcWeight*endpointWeight))

u.RawQuery = values.Encode()
return u.String()
}

传参 rawurl 是 Dubbo provider 的url,lidcWeight 是机房权重。根据配置的机房权重,将 url 中的 weight 进行重新计算,实现多机房流量按权重的分配。

这个过程涉及到 url 参数的解析,再进行 weight 的计算,最后再还原为一个 url

Dubbo 的 url 结构和普通 url 结构一致,其特点是参数可能比较多,没有 #后面的片段部分。

CPU 主要就消耗在这两次解析和最后的还原中,我们看这两次解析的目的就是为了拿到 url 中的 lidc_weight 和 weight 参数。

url.Parse 和 url.ParseQuery 都是 Go 官方提供的库,各个语言也都有实现,其核心是解析 url 为一个对象,方便地获取 url 的各个部分。

如果了解信息熵这个概念,其实你就大概知道这里面一定是可以优化的。Shannon(香农) 借鉴了热力学的概念,把信息中排除了冗余后的平均信息量称为信息熵。

url.Parse 和 url.ParseQuery 在这个场景下解析肯定存在冗余,冗余意味着 CPU 在做多余的事情。

因为一个 Dubbo url 参数通常是很多的,我们只需要拿这两个参数,而 url.Parse 解析了所有的参数。

举个例子,给定一个数组,求其中的最大值,如果先对数组进行排序,再取最大值显然是存在冗余操作的。

排序后的数组不仅能取最大值,还能取第二大值、第三大值…最小值,信息存在冗余了,所以先排序肯定不是求最大值的最优解。

优化

优化获取 url 参数性能

第一想法是,不要解析全部 url,只拿相应的参数,这就很像我们写的算法题,比如获取 weight 参数,它只可能是这两种情况(不存在 #,所以简单很多):

  • dubbo://127.0.0.1:20880/org.newboo.basic.MyDemoService?weight=100&…
  • dubbo://127.0.0.1:20880/org.newboo.basic.MyDemoService?xx=yy&weight=100&…

要么是 &weight=,要么是 ?weight=,结束要么是&,要么直接到字符串尾,代码就很好写了,先手写个解析参数的算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码func GetUrlQueryParam(u string, key string) (string, error) {
sb := strings.Builder{}
sb.WriteString(key)
sb.WriteString("=")
index := strings.Index(u, sb.String())
if (index == -1) || (index+len(key)+1 > len(u)) {
return "", UrlParamNotExist
}

var value = strings.Builder{}
for i := index + len(key) + 1; i < len(u); i++ {
if i+1 > len(u) {
break
}
if u[i:i+1] == "&" {
break
}
value.WriteString(u[i : i+1])
}
return value.String(), nil
}

原先获取参数的方法可以摘出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码func getParamByUrlParse(ur string, key string) string {
u, err := url.Parse(ur)
if err != nil {
return ""
}

values, err := url.ParseQuery(u.RawQuery)
if err != nil {
return ""
}

return values.Get(key)
}

先对这两个函数进行 benchmark:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码func BenchmarkGetQueryParam(b *testing.B) {
for i := 0; i < b.N; i++ {
getParamByUrlParse(u, "anyhost")
getParamByUrlParse(u, "version")
getParamByUrlParse(u, "not_exist")
}
}

func BenchmarkGetQueryParamNew(b *testing.B) {
for i := 0; i < b.N; i++ {
GetUrlQueryParam(u, "anyhost")
GetUrlQueryParam(u, "version")
GetUrlQueryParam(u, "not_exist")
}
}

Benchmark 结果如下:

1
2
3
4
5
6
bash复制代码BenchmarkGetQueryParam-4          103412              9708 ns/op
BenchmarkGetQueryParam-4 111794 9685 ns/op
BenchmarkGetQueryParam-4 115699 9818 ns/op
BenchmarkGetQueryParamNew-4 2961254 409 ns/op
BenchmarkGetQueryParamNew-4 2944274 406 ns/op
BenchmarkGetQueryParamNew-4 2895690 405 ns/op

可以看到性能大概提升了20多倍

新写的这个方法,有两个小细节,第一是返回值中区分了参数是否存在,这个后面会用到;第二是字符串的操作用到了 strings.Builder,这也是实际测试的结果,使用 +或者 fmt.Springf 性能都没这个好,感兴趣可以测试下看看。

优化 url 写入参数性能

计算出 weight 后再把 weight 写入 url 中,这里直接给出优化后的代码:

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
go复制代码func AssembleUrlWeightNew(rawurl string, lidcWeight int) string {
if lidcWeight == 1 {
return rawurl
}

lidcWeightStr, err1 := GetUrlQueryParam(rawurl, "lidc_weight")
if err1 == nil && lidcWeightStr != "" {
return rawurl
}

var err error
endpointWeight := 100
weightStr, err2 := GetUrlQueryParam(rawurl, "weight")
if weightStr != "" {
endpointWeight, err = strconv.Atoi(weightStr)
if err != nil {
endpointWeight = 100
}
}

if err2 != nil { // url中不存在weight
finUrl := strings.Builder{}
finUrl.WriteString(rawurl)
if strings.Contains(rawurl, "?") {
finUrl.WriteString("&weight=")
finUrl.WriteString(strconv.Itoa(lidcWeight * endpointWeight))
return finUrl.String()
} else {
finUrl.WriteString("?weight=")
finUrl.WriteString(strconv.Itoa(lidcWeight * endpointWeight))
return finUrl.String()
}
} else { // url中存在weight
oldWeightStr := strings.Builder{}
oldWeightStr.WriteString("weight=")
oldWeightStr.WriteString(weightStr)

newWeightStr := strings.Builder{}
newWeightStr.WriteString("weight=")
newWeightStr.WriteString(strconv.Itoa(lidcWeight * endpointWeight))
return strings.ReplaceAll(rawurl, oldWeightStr.String(), newWeightStr.String())
}
}

主要就是分为 url 中是否存在 weight 两种情况来讨论:

  • url 本身不存在 weight 参数,则直接在 url 后拼接一个 weight 参数,当然要注意是否存在 ?
  • url 本身存在 weight 参数,则直接进行字符串替换

细心的你肯定又发现了,当 lidcWeight = 1 时,直接返回,因为 lidcWeight = 1 时,后面的计算其实都不起作用(Dubbo 权重默认为100),索性别操作,省点 CPU。

全部优化完,总体做一下 benchmark:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码func BenchmarkAssembleUrlWeight(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, ut := range []string{u, u1, u2, u3} {
AssembleUrlWeight(ut, 60)
}
}
}

func BenchmarkAssembleUrlWeightNew(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, ut := range []string{u, u1, u2, u3} {
AssembleUrlWeightNew(ut, 60)
}
}
}

结果如下:

1
2
3
4
5
6
bash复制代码BenchmarkAssembleUrlWeight-4               34275             33289 ns/op
BenchmarkAssembleUrlWeight-4 36646 32432 ns/op
BenchmarkAssembleUrlWeight-4 36702 32740 ns/op
BenchmarkAssembleUrlWeightNew-4 573684 1851 ns/op
BenchmarkAssembleUrlWeightNew-4 646952 1832 ns/op
BenchmarkAssembleUrlWeightNew-4 563392 1896 ns/op

大概提升 18 倍性能,而且这可能还是比较差的情况,如果传入 lidcWeight = 1,效果更好。

效果

优化完,对改动方法写了相应的单元测试,确认没问题后,上线进行观察,CPU Idle(空闲率) 提升了10%以上

最后

其实本文展示的是一个 Go 程序非常常规的性能优化,也是相对来说比较简单,看完后,大家可能还有疑问:

  • 为什么要在推送和拉取的时候去解析 url 呢?不能事先算好存起来吗?
  • 为什么只优化了这点,其他的点是否也可以优化呢?

针对第一个问题,其实这是个历史问题,当你接手系统时他就是这样,如果程序出问题,你去改整个机制,可能周期比较长,而且容易出问题

第二个问题,其实刚也顺带回答了,这样优化,改动最小,收益最大,别的点没这么好改,短期来说,拿收益最重要。当然我们后续也打算对这个系统进行重构,但重构之前,这样优化,足以解决问题。


搜索关注微信公众号”捉虫大师”,后端技术分享,架构设计、性能优化、源码阅读、问题排查、踩坑实践。也欢迎加我个人微信MrRoshi,围观朋友圈。

本文转载自: 掘金

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

1…253254255…956

开发者博客

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