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

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


  • 首页

  • 归档

  • 搜索

顺序栈和链栈以及他们的应用

发表于 2021-10-29

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

一、什么是栈

  • 只允许在一端插入和删除的线性表
  • 允许插入和删除的一端称为栈顶 (top),另一端称 为栈底(base)
  • 特点:后进先出 (LIFO, Last In, First Out)

在这里插入图片描述

二、顺序栈

2.1 顺序栈的顺序存储表示

1
2
3
4
5
6
7
cpp复制代码#define  STACK_INIT_SIZE  100
#define STACKINCREMENT 10
typedef struct {
SElemType *base;
SElemType *top;
int stacksize;
} SqStack;

2.2 顺序栈的初始化

1
2
3
4
5
6
7
8
9
cpp复制代码Status InitStack (SqStack &S)
{// 构造一个空栈S
S.base=(SElemType*)malloc(STACK_INIT_SIZE*
sizeof(SElemType));
if (!S.base) exit (OVERFLOW); //存储分配失败
S.top = S.base;
S.stacksize = STACK_INIT_SIZE;
return OK;
}

2.3 返回顺序栈顶元素

1
2
3
4
5
6
cpp复制代码Status GetTop (SqStack S, SElemType &e) {
// 若栈不空, 用e返回栈顶元素,并返回 //OK; 否则返回ERROR
if (S.top = = S.base) return ERROR;
e = *(S.top-1);
return OK;
}//GetTop

2.4 压栈

1
2
3
4
5
6
7
8
9
10
11
12
cpp复制代码Status Push (SqStack &S, SElemType e) {
if (S.top - S.base >= S.stacksize) {//栈满,追加存储空间
S.base = (SElemType *) realloc ( S.base,
(S.stacksize + STACKINCREMENT) *
sizeof (SElemType));
if (!S.base) exit (OVERFLOW); //存储分配失败
S.top = S.base + S.stacksize;
S.stacksize += STACKINCREMENT;
}
*S.top++ = e;
return OK;
}

2.5 出栈

1
2
3
4
5
6
7
8
cpp复制代码Status Pop (SqStack &S, SElemType &e) {
// 若栈不空,则删除S的栈顶元素,
// 用e返回其值,并返回OK;
// 否则返回ERROR
if (S.top = = S.base) return ERROR;
e = *--S.top;
return OK;
}

三、链栈

1
2
3
4
cpp复制代码typedef   struct LinkNode 
{ SElemType data;
struct LinkNode *link;
}LinkNode, *LinkStack;

四、栈的应用

4.1 数制转换

1
ini复制代码算法基于原理:	  N = (N div d)×d + N mod d

例如:(1348)10 = (2504)8 ,其运算过程如下:

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
cpp复制代码void conversion () {
InitStack(S);
scanf ("%d",&N);
while (N) {
Push(S, N % 8);
N = N/8;
}
while (!StackEmpty(S)) {
Pop(S,e);
printf ( "%d", e );
}
} // conversion

4.2 括号匹配的检验

1
2
3
4
5
复制代码假设在表达式中
([]())或[([ ][ ])]
等为正确的格式,
[( ])或([( ))或 (()])
均为不正确的格式。

算法思想:

  1. 凡出现左括弧,则进栈;
  2. 凡出现右括弧,首先检查栈是否空?
    • 若栈空,则表明该“右括弧”多余,
    • 否则和栈顶元素比较,
      • 若相匹配,则“左括弧出栈” ,
      • 否则表明不匹配。
  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
30
31
32
33
34
35
36
37
38
39
cpp复制代码public boolean isValid(String s) {
Stack stack = new Stack();
char string[] = s.toCharArray();
int index = 0;//指向栈判断到哪一位了
if(string.length==0)
return true;
while(index<string.length)
{
if(string[index]==' ')
{
index++;
continue;
}
char ithis = string[index];
if(string[index]=='('||string[index]=='['||string[index]=='{')
stack.push(string[index]);
else
{
if(stack.empty()==true)
{
return false;
}
char temp = (char)stack.pop();
if(string[index]==')'&&temp!='(' ||string[index]==']'&&temp!='[' ||string[index]=='}'&&temp!='{')
{
return false;
}
}
index++;
}
if(stack.empty()==true)
return true;
else
return false;




}

本文转载自: 掘金

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

【实操】线上CPU爆满,一次提桶跑路的经历

发表于 2021-10-29

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言

在最近的一次项目更新后,系统会在不确定的场景下出现超级卡顿的情况,打开服务器会发现CPU已经卡爆,重启数次之后依旧会出现卡顿的现象,再不找原因很有可能就要提桶跑路了。

image.png

这个项目是一个比较成熟的产品了,现在基本就是根据客户需求进行小打小闹的修改,所以就让两个刚毕业不久的同学负责,卡顿的情况出现好几次,服务器反复重启n次之后他们和我说了这个问题,经过我的一通小操作,成功拿下这两个小迷弟,有没有需要的,打包送走。

接下来就通过下面这个小示例给大家演示一下这个定位的过程。

测试代码

首先新建一个demo,用于演示这种CPU爆满的情况,代码也非常简单,就是一个while死循环,无限打印一段日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kotlin复制代码import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author :huage
* @date :Created in 2021/10/25
* @description: 测试范例
*/
@Slf4j
@RestController
public class DemoController2 {
   
   @RequestMapping("test")
   public void testHashMap(){
       while (true){
           log.info("花哥你最棒");
      }
  }
​
}

打包部署

用idea打包也是非常方便,直接点击package就能搞定,这里花哥使用的是jar包方式打包。

打包阶段,花哥分享两个常用脚本(启动/停止脚本),想动手部署的小伙伴肯定可以用到。

  • 启动脚本
1
2
3
4
5
bash复制代码export NAME=jar名称.jar
export CLASSPATH=.:jar包路径/:${NAME}
export JAVA_OPS="-server -DappName=程序名称(对应停止脚本中名称)"
echo ${CLASSPATH}
nohup java ${JAVA_OPS} -cp ${CLASSPATH} org.springframework.boot.loader.JarLauncher >/dev/null 2>&1 &
  • 停止脚本
1
perl复制代码ps -ef | grep -v grep | grep java | grep 启动脚本中DappName名称 | awk '{print "kill -9 "$2}' | sh

image.png

  • 启动项目

上述步骤全部完成后,使用./run.sh就可以完成项目的启动,在logs中能够看到启动日志。

确定进程

成功启动后,使用命令top -c查看系统资源的使用情况,不断使用大写P可以进行排序,找到占据CPU最大的进程。从下图中能够看到,当前项目只占据0.7%的cpu,

image.png

  • 调用test接口

如果调用完死循环test接口呢,调用完测试接口后,项目直接占据了cpu的107.3%,简直卡到爆炸,命令都快不听使唤了,当然也可能是花哥的服务器太辣鸡了。

image.png

打开日志,我们可以看到日志在不断刷【花哥你最棒】,因为方便测试,这里使用循环打印日志的方式,在现实项目中,有很多很多隐晦的问题并不会体现在日志中,只能通过其他的手段来确定问题的来源。

执行结果6.gif

查找线程

通过top -c命令可以确定出现问题的进程,接下来就要找到这个进程中是哪个线程出现问题,这个确定也是比较简单的,只需要输入命令:top -Hp PID。比如在本例中输入top -Hp 9828。

image.png

9877这个线程暂用87.5%的CPU资源,那就可以确定该线程是有问题的,到目前为止,我们完成了问题线程的定位,接下来是就是要确定该线程在代码中哪个位置了。

定位代码

在jvm中进程快照中线程是以16进制显示,所以我们需要将【9877】这个PID转换成16进制,使用浏览器中在线转换工具,或者命令【printf “%x\n” 9877】都可以转换。

printf “%x\n” PID

image.png

本例中转换后的16进制为【2698】,接下来使用jstack命令便可以定位到具体的问题代码,如下图中,最终定位到DemoController的testHashMap方法。

jstack 进程PID | grep 线程16进制 -c 显示行数

image.png

总结

定位线上环境问题在工作中非常有用,比如有用户反馈一个问题,你老大丢了一些服务器配置给你,让你定位一下,如果你都不知道该看啥,又不好意思问,是不是也会很尴尬….当然除了工作中的需要,这类问题在面试中也经常会被问到,流利的回答可以让面试官觉得你这小伙不是背面试题的,而是真正的用过,工资也能多拿两百块钱吧。

本文转载自: 掘金

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

Redis的事务,Go+lua用起来真香

发表于 2021-10-29

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

Redis是应对高并发的常用工具,在常用缓存技巧中讲过相关技巧。但有些业务场景,使用Redis会遇到问题,如电商里的秒杀、扣减库存等。

拿减库存举例,一般需要两步:

  • 先扣减库存,获取扣减后的库存值V
  • 如果V小于0,说明库存不够,需要将扣减的值再加回去;如果V大于等于0,则执行后续操作

但这两步是分开的,很可能扣减时成功,但增加回去时失败,导致库存不一致。

另一种方案是:

  • t1时刻,先查询库存,判断是否够用
  • t2时刻,再减库存

但这两步也是分开的,而且t1和t2有时间差,t2时刻扣减库存时,真正的库存和t1时刻已经不一致了。

Redis有没有像MySQL原子性一样的能力,来解决这个问题呢?

事务

要解决扣减库存的问题,可以借助Redis的事务能力。

基本介绍

Redis的基本事务(basic transaction)需要用到MULTI命令和EXEC命令,这种事务可以让一个客户端在不被其他客户端打断的情况下执行多个命令。

和关系数据库那种可以在执行的过程中进行回滚(rollback)的事务不同,在Redis里面,被MULTI命令和EXEC命令包围的所有命令会一个接一个地执行,直到所有命令都执行完毕为止。当一个事务执行完毕之后,Redis才会处理其他客户端的命令。

Redis事务在执行的中途遇到错误,不会回滚,而是继续执行后续命令;

  • 还未执行exec就报错:如果事务中出现语法错误,则事务会成功回滚,整个事务中的命令都不会提交
  • 成功执行exec后才报错:如果事务中出现的不是语法错误,而是执行错误,不会触发回滚,该事务中仅有该错误命令不会提交,其他命令依旧会继续提交

另外,Redis里遇到有查询的情况穿插在事务中间,不会返回结果,如下所示,只有执行exec才能返回查询结果:

1
2
3
4
5
6
7
8
9
shell复制代码127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set a aaa
QUEUED
127.0.0.1:6379(TX)> get a
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) "aaa"

对于事务,做个简单的总结:

  1. Redis事务作为一个整体被执行,执行期间不会被其它客户端打断
  2. Redis事务在执行的中途遇到错误,不会回滚,而是继续执行后续命令
  3. Redis里遇到有查询的情况穿插在事务中间,不会返回结果

Redis事务执行的时候不会被其它客户端打断,所以多个命令可以进行打包,当做一个命令执行。但Redis的原生命令无法提供根据Redis查询结果执行相关动作的功能,这时我们就可以用lua脚本了。

lua

Redis2.6之后新增的功能,我们可以在Redis中通过lua脚本操作Redis。

脚本会将多个命令和操作当成一个命令在Redis中执行,也就是说该脚本在执行的过程中,不会被任何其他脚本或命令打断干扰。

正是因此这种原子性,lua脚本才可以代替multi和exec的事务功能。同时也是因此,在lua脚本中不宜进行过大的开销操作,避免影响后续的其他请求的正常执行。

使用lua脚本的好处:

  • lua脚本是作为一个整体执行的,所以中间不会被其他命令插入
  • 可以把多条命令一次性打包,所以可以有效减少网络开销
  • lua脚本可以常驻在redis内存中,所以在使用的时候,可以直接拿来复用,也减少了代码量

lua脚本在Redis里的样子如下图所示:

图片

实例


场景

秒杀系统里画过秒杀的流程图,其中秒杀库存相关代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码 //加库存
goodsKey := getGoodsKey(p.AppLocal, tag, tid, goods_id)
cacheQty, err := app.Global().RedisStore.Incr(goodsKey)
if err != nil {
return nil, 11008, fmt.Errorf(SERVICE_BUSY)
}
//和本次秒杀总数量进行比较
if cacheQty > int64(goodsInfo.Qty) {
//不符合条件,则减去库存
_, err := app.Global().RedisStore.Decr(goodsKey)
if err != nil {
ctx.Warn("%s seckill %s ,incr the cnt but not decr success in count process, err:%s", userId, goods_id, err.Error())
}
return nil, 11009, fmt.Errorf(NOT_SUCCESS)
}

可以看出,先增加库存,然后和本次秒杀总数量进行比较,如果超出范围,需减去库存。这种分开的操作,增加了失败的可能性。扣减商品库存,也是类似的逻辑。

使用lua

在Go语言里用lua脚本实现扣减库存操作。我们可以先查询,后比较,最后扣减,脚本将操作打包,代码为:

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
go复制代码package main

import (
"fmt"
"github.com/go-redis/redis"
)

var Client *redis.Client

func init() {
Client = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "111111", // no password set
DB: 0, // use default DB
})
}

func useLua() {
Client.FlushAll()
//设置初始值
Client.Set("stock", "10", 0)
//编写脚本 - 检查数值,是否够用,够用再减,否则返回减掉后的结果
var luaScript = redis.NewScript(`
local value = redis.call("Get", KEYS[1])
print("当前值为 " .. value);
if( value - KEYS[2] >= 0 ) then
local leftStock = redis.call("DecrBy" , KEYS[1],KEYS[2])
print("剩余值为" .. leftStock );
return leftStock
else
print("数量不够,无法扣减");
return value - KEYS[2]
end
return -1
`)
//执行脚本
n, err := luaScript.Run(Client, []string{"stock", "6"}).Result()
if err != nil {
panic(err)
}
fmt.Println("结果", n, err)
}


func main() {
useLua()
}

客户端将lua加载到Redis,脚本被Redis执行:

图片

如果将扣减值设置为25,执行之后,观察到返回值为负数,但再次查询,值未变

➜ myproject go run main.go

当前值为 10

数量不够,无法扣减

结果 -15

➜ myproject redis-cli -p 6379 -a 111111

127.0.0.1:6379> get stock

“10”

如果将扣减值设置为6,扣减成功,执行后查看结果

➜ myproject go run main.go

当前值为 10

剩余值为4

结果 4

➜ myproject redis-cli -p 6379 -a 111111

127.0.0.1:6379> get stock

“4”

优化lua使用

当脚本会被多次执行时,可考虑使用ScriptLoad和EvalSha代替RUN节省带宽。

  • 先用命令ScriptLoad将脚本缓存到Redis,Redis返回一个sha1的标识符
  • 命令EvalSha基于sha1执行脚本

这种方案只有标识符sha1通过网络传输,而不需传输lua代码块,节省流量,流程如下图所示:

图片

具体代码为:

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复制代码package main

import (
"fmt"
"github.com/go-redis/redis"
)

var Client *redis.Client
var script string = `
local value = redis.call("Get", KEYS[1])
print("当前值为 " .. value);
if( value - KEYS[2] >= 0 ) then
local leftStock = redis.call("DecrBy" , KEYS[1],KEYS[2])
print("剩余值为" .. leftStock );
return leftStock
else
print("数量不够,无法扣减");
return value - KEYS[2]
end
return -1
`
var luaHash string

func init() {
Client = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "111111", // no password set
DB: 0, // use default DB
})
luaHash, _ = Client.ScriptLoad(script).Result() //返回的脚本会产生一个sha1哈希值,下次用的时候可以直接使用这个值
}

func useLuaHash() {
n, err := Client.EvalSha(luaHash, []string{"stock", "6"}).Result()
if err != nil {
panic(err)
}
fmt.Println("结果", n, err)
}

func main() {
useLuaHash()
}

定制

如果大家觉得在Go中自己写lua不优雅,我们可选择定制方案。

找Redis的运维同学,让运维人员通过lua定制命令,将这些命令常驻Redis内存, 实现复用效果。使用这些定制的命令与使用Redis自身提供的命令没有区别。

当然,前提是运维同学同意做。

问题

虽然使用lua利用Redis事务能力,保证执行过程中不会插入其他命令,但无法解决所有问题:

  • 不确定的返回结果:如请求超时了,要不要归还库存?
  • 这种情况一般不要归还,因为很可能没扣减成功,超卖容易造成资损,少卖问题要小一些
  • 归还失败:扣减成功,但后续操作失败,仍要归还库存,始终有归还失败的可能
  • 这种情况需要重试,而且必须是确定类型的错误才可重试,例如超时就不可重试,同时要做好记录

这也是要求SLA要高,要常记日志的原因,无形中能解决很多问题。

搭建

记录一下在Mac机上搭建Redis的过程,大家可以安装一下Redis,执行Go+lua的代码。

安装

Redis搭建比较简单,使用如下命令即可安装完毕

1
复制代码brew install redis

密码

为了安全,可以设置密码。打开 redis.conf 文件,然后按 command + f 进行搜索:#requirepass foobared 修改为:requirepass 你的密码

服务端启动

执行如下命令,在服务端启动redis

1
bash复制代码/usr/local/bin/redis-server /usr/local/etc/redis.conf

执行redis-server命令时,如果没有redis.conf文件,则按照默认配置启动,这种情况下登录无需auth

命令行登录

如果设置了auth,命令行登录如下:

1
css复制代码redis-cli -p 端口 -a 密码

如果未设置auth,直接执行

1
复制代码redis-cli

总结

有些问题无法百分之百解决,我们要接受这个事实。但并不意味着妥协,我们要能发现问题,确定解决方案,即使是手动解决的方案。有一个前提,解决频率要足够低,这时就没必要把关注点放在100%自动化解决上,产出比特别低的事尽量少做。

资料

  1. 在golang中使用lua脚本
  2. redis登录及设置密码
  3. 为什么说Redis是单线程的以及Redis为什么这么快!
  4. go-redis 事务提交
  5. Golang使用lua脚本实现redis原子操作
  6. Redis事务与Lua
  7. Redis事务的分析及改进
  8. Redis系列(九)、Redis的“事务”及Lua脚本操作

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客为:shidawuhen.github.io/

往期文章回顾:

  1. 设计模式
  2. 招聘
  3. 思考
  4. 存储
  5. 算法系列
  6. 读书笔记
  7. 小工具
  8. 架构
  9. 网络
  10. Go语言

本文转载自: 掘金

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

万万没想到,我的Docker居然被挖矿了!

发表于 2021-10-29

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。


事情是这样的,前段时间的周末刚在外面坐下准备吃个饭,突然收到一条短信:

什么情况?我一个小小的个人服务器都能被挖矿?回想了一下,也没想到之前做了什么能引起被挖矿的操作啊,直到两个小时后,又收到一条短信报警:

端口?提到端口我想起来了,自己前一天因为要用idea连接服务器上的docker,开启了一个2375的端口。至于具体操作,就是简单修改了一下配置文件/usr/lib/systemd/system/docker.service/usr/lib/systemd/system/docker.servicevv,修改了下面的内容:

1
shell复制代码ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

在这句后面添加了一句:

1
shell复制代码-H tcp://0.0.0.0:2375

这样,就开启了一个2375端口,通过这个端口,可以在idea中配置docker,并在打包的同时进行镜像的上传:

图片

通过这个端口,我们就可以直接对远程的docker daemon进行操作了。

紧急处理

既然找到了问题所在,那我们就得立马处理这个问题了。回到家,马上在安全配置里关闭了2375端口的外网访问:

图片

再看一下后台的报警信息:

使用kill命令杀死这个进程:

图片

好了,这下进程也杀死了,外网的端口访问也关闭了,应该没事了吧?没想到晚上7点多,又连着报了两条警告信息:

使用ps指令看一下13102进程,没有查到。查一下masscan进程,能够找到,这个masscan是一个端口扫描工具,能够根据IP地址的范围和端口号,快速进行端口扫描:

图片

顺便查询了下父进程的pid,居然使用了portainer,居然连docker的图形化管理界面都帮我安装上了。需要注意的是在杀死masscan进程前,一定要杀死父进程,否则masscan进程会不断重启。

再看一下第二个报警的32452进程,能够找到docker-cache进程,使用kill杀死后不会进行重启。

图片

停止运行的容器并删除:

图片

删除挖矿image:

图片

修改配置文件,删掉2375端口的tcp连接,然后重启docker:

1
2
shell复制代码systemctl daemon-reload
systemctl start docker

入侵原理

到这,先总结一下为什么能够通过2375端口入侵宿主主机?

  • docker对user namespace没有做隔离,也就是说,容器内部的root用户就是宿主机的root用户,一旦挂载目录,就可以在容器内部以宿主机的root用户身份对挂载的文件系统随意修改了
  • docker服务拥有很高的执行权利(相当于root),并且在docker用户组下的普通用户不需要任何其他验证就可以执行docker run等命令
  • 而暴露的docker remote API端口如果没有启动ssl验证的话,任何能连通到这台docker宿主机的的机器都可以随意操作这台docker宿主机的docker daemon

漏洞修复

那么应该如何修复这个漏洞呢,通过查阅资料,docker本身提供了加密的远程管理端口2376,配合CA证书,就能提供TLS连接了。

首先要准备5个证书和秘钥文件,分别是ca.pem、server-cert.pem、server-key.pem、client-cert.pem和client-key.pem。其中,server-cert.pem中限制了能够访问Docker主机的客户端列表。

启动docker deamon时,需要设置-H、–tls、–tlscacert=ca.pem、–tlscert=server-cert.pem和–tlskey=server-key.pem。此时,只有客户端列表中的主机能够访问docker主机。

1.生成CA私钥ca-key.pem,使用该私钥对CA证书签名

1
shell复制代码openssl genrsa -out ~/docker/ca-key.pem 4096

2.使用CA私钥生成自签名CA证书ca.pem。生成证书时,通过-days 365设置证书的有效期。单位为天,默认情况下为30天

1
shell复制代码openssl req -x509 -sha256 -batch -subj '/C=CN/ST=Sichuan/L=Chengdu/O=Ghostcloud Co.,Ltd/OU=Laboratory/CN=www.ghostcloud.cn' -new -days 365 -key ~/docker/ca-key.pem -out ~/docker/ca.pem

3.生成服务器私钥server-key.pem和CSR(Certificate Signing Request)server-csr.pem

1
2
shell复制代码openssl genrsa -out ~/docker/server-key.pem 4096
openssl req -subj '/CN=DockerDaemon' -sha256 -new -key ~/docker/server-key.pem -out ~/docker/server-csr.pem

4.使用CA证书生成服务器证书server-cert.pem。TLS连接时,需要限制客户端的IP列表或者域名列表。只有在列表中的客户端才能通过客户端证书访问docker daemon

1
2
shell复制代码echo subjectAltName = IP:127.0.0.1,IP:192.168.1.100 > ~/docker/allow.list
openssl x509 -req -days 365 -sha256 -in ~/docker/server-csr.pem -CA ~/docker/ca.pem -CAkey ~/docker/ca-key.pem -CAcreateserial -out ~/docker/server-cert.pem -extfile ~/docker/allow.list

5.生成客户端私钥client-key.pem和CSRclient-csr.pem

1
2
shell复制代码openssl genrsa -out ~/docker/client-key.pem 4096
openssl req -subj '/CN=DockerClient' -new -key ~/docker/client-key.pem -out ~/docker/client-csr.pem

6.使用CA证书生成客户端证书client-cert.pem。需要加入extendedKeyUsage选项

1
2
shell复制代码echo extendedKeyUsage = clientAuth > ~/docker/options.list
openssl x509 -req -days 365 -sha256 -in ~/docker/client-csr.pem -CA ~/docker/ca.pem -CAkey ~/docker/ca-key.pem -CAcreateserial -out ~/docker/client-cert.pem -extfile ~/docker/options.list

7.成功生成了需要的证书和秘钥,可以删除临时文件

1
shell复制代码rm -f ~/docker/server-csr.pem ~/docker/client-csr.pem ~/docker/allow.list ~/docker/options.list

8.为了保证证书和私钥的安全,需要修改文件的访问权限

1
2
shell复制代码chmod 0444 ~/docker/ca.pem ~/docker/server-cert.pem ~/docker/client-cert.pem
chmod 0400 ~/docker/ca-key.pem ~/docker/server-key.pem ~/docker/client-key.pem

9.重启docker daemon,加入ca.pem、server-cert.pem和server-key.pem。-H=0.0.0.0:2376表示docker daemon监听在2376端口

1
shell复制代码docker daemon --tlsverify --tlscacert=~/docker/ca.pem --tlscert=~/docker/server-cert.pem --tlskey=~/docker/server-key.pem -H=0.0.0.0:2376

10.在客户端,运行docker命令时,加入ca.pem、client-cert.pem和client-key.pem

1
2
shell复制代码docker --tlsverify --tlscacert=~/docker/ca.pem --tlscert=~/docker/client-cert.pem --tlskey=~/docker/client-key.pem -H=tcp://127.0.0.1:2376 info
Containers: 41 Running: 16 Paused: 0 Stopped: 25 Images: 821 Server Version: 1.10.3

这样,就可以安全的远程控制docker主机了。

最后

如果觉得对您有所帮助,小伙伴们可以点赞、转发一下,非常感谢

公众号码农参上,加个好友,做个点赞之交啊

本文转载自: 掘金

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

记一次整合SpringSecurity和JWT实现认证和授权

发表于 2021-10-29

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

前言

前不久做过整合SpringSecurity和JWT实现登录认证和授权的功能,在整合过程中遇到了一些问题,在此记录一下。

1、Postman工具如何添加token请求头?

解决方式:在Headers里面添加请求参数,主要要把前面的选择框选上
在这里插入图片描述

2、若knife4j 在线文档调试没有请求头部设置?

解决方式:
在 文档管理-》个性化设置 开启动态请求参数,刷新即可
在这里插入图片描述

3、Spring security 配置的AccessDeniedHandler无效?

问题说明:当验证权限失败时抛出AccessDeniedException异常 不允许访问,而我明明配置了SimpleAccessDeniedHandler 来处理异常并返回提示信息。检查一看是由于配置了全局异常,由于GlobalExceptionHandler 全局异常处理器会比 AccessDeniedHandler 先捕获 AccessDeniedException异常,因此当配置了 GlobalExceptionHandler 后,会发现AccessDeniedHandler 失效了

解决方式:直接在全局异常处理GlobalExceptionHandler里添加AccessDeniedHandler 异常的处理
在这里插入图片描述

4、调用mybaits里的xml文件中的方法报文件找不到?

问题说明:由于mybatis里的xml文件放在了java目录下,未放在resources目录下。所以编译的时候是不会打包的。

解决方式:先在pom文件中添加
在这里插入图片描述
再在配置文件里在加上xml文件的路径
在这里插入图片描述

本文转载自: 掘金

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

iphone13到底香不香,真的这么难抢?python+se

发表于 2021-10-29

先水俩图

iphone12/13抢购自动化测试脚本

请添加图片描述
在这里插入图片描述

说在前面

本文核心内容

  1. iphone13 有货通知 NodeJs脚本
  2. iphone13 抢购的python+selenium自动化测试脚本
  3. iphone12 预约抢购的python+selenium自动化测试脚本

起因

首先说明本菜狗不是什么专业评测,起因只是因为刷新闻时,看到铺天盖地的新闻:13有多香,高刷有多好,供货多紧张,根本抢不到等。作为一个好奇心重的菜狗,就只是想单纯的看看这确有其事还是故弄玄虚的噱头!
在这里插入图片描述

走起

  1. 登录苹果官网,映入眼帘的大苹果

看起来是挺不错,但是价格也很感人

在这里插入图片描述2.去看看有没有货,现货怎么取,快递啥时候到

看似确实货源紧张,都快排到圣诞节了,而且现货也没有,只能在线购买,可能网上说的是真的?

在这里插入图片描述
在这里插入图片描述
3.一探究竟

选了一下我要取货,怎么刷都刷不出来,表面看起来确实是没货啊!

在这里插入图片描述

实操代码

为了解决这个抢不到或者无法及时得到有货通知的问题,采用以下解决方法!

1.iphone13 有货通知 NodeJs脚本

当有货时发送邮件通知,通过手速进行抢购

js代码
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
javascript复制代码/*
* @Author: JavaDog
* @Date: 2021-09-19 21:22:49
* @LastEditTime: 2021-10-12 14:55:49
* @Description: iphone13 pro 银色 256g/512g
* @FilePath: apple13.js
*/
var https = require("https");
var http = require("http");
var querystring = require("querystring");
var request = require("request");
// 是否有货地址,可以替换成你的自己想要的型号
let url = [
"https://www.apple.com.cn/shop/fulfillment-messages?pl=true&mt=compact&parts.0=MLTG3CH/A&searchNearby=true&store=R557",
"https://www.apple.com.cn/shop/fulfillment-messages?pl=true&mt=compact&parts.0=MLTC3CH/A&searchNearby=true&store=R557",
];
// 循环定时器
setInterval(() => {
url.forEach((item) => {
https.get(item, function (response) {
var body = [];
response.on("data", function (chunk) {
body.push(chunk);
});
response.on("end", function () {
body = Buffer.concat(body);
let json = JSON.parse(body.toString());
let content = json.body.content;
let pickupMessage = content.pickupMessage;
let store = pickupMessage.stores[0];
let partsAvailability = store.partsAvailability;
let obj =
partsAvailability["MLTG3CH/A"] ||
partsAvailability["MLTC3CH/A"] ||
partsAvailability["MLTJ3CH/A"] ||
partsAvailability["MLTE3CH/A"];
let pickupDisplay = obj.pickupDisplay;
let storePickupProductTitle = obj.storePickupProductTitle;
let pickupSearchQuote = obj.pickupSearchQuote;
// 重要参数拼接
let result =
storePickupProductTitle +
" " +
pickupDisplay +
" " +
pickupSearchQuote +
" " +
new Date();

if (pickupDisplay != "unavailable" || pickupDisplay == "available") {
// 有货
console.log(result);
// 此处注意,请改成自己的邮件发送服务器,或者其他通知平台,本人的是自己的阿里云邮箱
mail(result, storePickupProductTitle);
} else {
// 无货
console.log(result);
}
});
});
});
}, 1000);

// 有货邮件提醒
function mail(noSimilarModelsText, storePickupProductTitle) {
var options = {
headers: {
"Content-Type": "application/json",
"admin-authorization": "dfaf86e4e45f4906a6ef8d180efba1c0",
},
url: "https://blog.javadog.net/api/admin/mails/test",
method: "POST",
json: true,
body: {
to: "862422627@qq.com",
subject: storePickupProductTitle,
content: noSimilarModelsText,
},
};

function callback(error, response, data) {
console.log("----info------", data);
}
request(options, callback);
}
启动

node apple.js

启动时,使用命令 node apple.js 即可,前置条件需要已安装node,如未安装,请参考 Node.js 安装配置,启动后详见下图。如需放置服务器启动,请参考pm2应用进程管理器。

在这里插入图片描述

结论

经过放置服务器24小时不间断的测试得出:每天还是有少量的现货,但只在早上9-12点之间,每天现货量大约在1-5个左右(青岛地区测试),秒杀时间基本在半分钟之内,否则立刻无货
在这里插入图片描述
在这里插入图片描述

2.iphone12 抢购python+selenium自动化测试脚本【只针对谷歌浏览器】

通过python+selenium自动化,让代码帮我们处理。
此处用iphone 12作为流程演示,因为12基本无需抢购,流程可以跑通
😂本菜狗之前没有接触过python,写的垃圾请大佬勿喷!

前置条件

了解python+selenium,可可以参考 Python+Selenium基础入门及实践

注意

如果没有chromedriver.exe 请按照谷歌浏览器版本匹配规则进行选择!本人谷歌版本94.0.4606.81 32位点击下载本人所用的 chromedriver.exe
在这里插入图片描述
切记chromedriver.exe依赖如果不是32位的话可能会报如下错误

WebDriverException:Message:unknown error:cannot find Chrome binary

在这里插入图片描述

python代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
python复制代码from selenium import webdriver
from datetime import datetime
from selenium.webdriver.support.select import Select
import time

# iphone12 自动化测试
print("iphone12 自动化测试开始")

# 访问测试的url定义
url = "https://www.apple.com.cn/shop/buy-iphone/iphone-12"

# 1. 创建浏览器对象 这里的Chrome中的变量是chromedriver的驱动地址
driver = webdriver.Chrome()

# 2. 跳转到apple官网
driver.get(url)

# 3. 隐式等待 设置 防止预售的网络的阻塞
driver.implicitly_wait(10)

# 4.1 开始选择规格【此处我选择了-12 mini】
element_sku = driver.find_element_by_name('dimensionScreensize')
driver.implicitly_wait(10)
element_sku.click()

# 4.2 选择颜色【此处我选择了-白色】
element_color = driver.find_element_by_xpath(
'//*[@value="white"]')
driver.execute_script("arguments[0].click();", element_color)
driver.implicitly_wait(10)

# 4.3 选择内存【此处我选择了-256g】
element_memory = driver.find_element_by_xpath(
'//*[@value="256gb"]')
driver.execute_script("arguments[0].click();", element_memory)
driver.implicitly_wait(10)

# 4.4 你是否有智能手机要折抵 【此处我选择了-没有旧机折扣】
element_old = driver.find_element_by_xpath('//*[@id="noTradeIn"]')
driver.execute_script("arguments[0].click();", element_old)
driver.implicitly_wait(10)

# 4.5 Applecare 【此处我选择了-无Applecare】
element_care = driver.find_element_by_id('applecareplus_58_noapplecare_label')
driver.execute_script("arguments[0].click();", element_care)
driver.implicitly_wait(10)

# 4.6 添加到购物袋
element_car = driver.find_element_by_xpath(
'//*[@value="add-to-cart"]')
driver.execute_script("arguments[0].click();", element_car)
driver.implicitly_wait(10)

# 5 页面跳转查看购物袋
element_check = driver.find_element_by_xpath(
'//*[@value="proceed"]')
driver.execute_script("arguments[0].click();", element_check)
driver.implicitly_wait(10)

# 6 结账
element_check_out = driver.find_element_by_xpath(
'//*[@id="shoppingCart.actions.navCheckout"]')
driver.execute_script("arguments[0].click();", element_check_out)
driver.implicitly_wait(10)

# 7.1 输入用户名
element_username = driver.find_element_by_id(
'signIn.customerLogin.appleId')
element_username.send_keys('appleId账号')
driver.implicitly_wait(10)

# 7.2 输入密码
element_password = driver.find_element_by_id(
'signIn.customerLogin.password')
element_password.send_keys('密码')
driver.implicitly_wait(10)

# 7.3 点击登录
element_login = driver.find_element_by_id(
'signin-submit-button')
element_login.click()
driver.implicitly_wait(10)

# 8.1 你希望如何收到订单商品 【此处我选择了-我要取货】
element_want_order = driver.find_element_by_id(
'fulfillmentOptionButtonGroup1')
driver.execute_script("arguments[0].click();", element_want_order)
driver.implicitly_wait(10)

# 8.2 点击显示此地附近的零售店
element_selectdistrict = driver.find_element_by_xpath(
'//*[@aria-describedby="rs-fulfillment-storelocator-error"]')
driver.execute_script("arguments[0].click();", element_selectdistrict)
driver.implicitly_wait(10)

# 8.3 点击山东
element_provice = driver.find_element_by_xpath(
'//*[@value="山东"]')
driver.execute_script("arguments[0].click();", element_provice)
driver.implicitly_wait(10)

# 8.4 点击青岛
element_city = driver.find_element_by_xpath(
'//*[@value="青岛"]')
driver.execute_script("arguments[0].click();", element_city)
driver.implicitly_wait(10)

# 8.5 点击市南
element_area = driver.find_element_by_xpath(
'//*[@value="市南区"]')
driver.execute_script("arguments[0].click();", element_area)
driver.implicitly_wait(20)

# 8.6 选择取货零售店 【此处我选择了-Apple 青岛万象城】
element_pickupTab = driver.find_element_by_id(
'checkout.fulfillment.pickupTab.pickup.storeLocator-R557')
driver.execute_script("arguments[0].click();", element_pickupTab)
driver.implicitly_wait(20)

# 8.7 选择取货时间 【根据时间自己定】
element_pickup_time = driver.find_element_by_xpath(
'//*[@value="11"]')
driver.execute_script("arguments[0].click();", element_pickup_time)
driver.implicitly_wait(10)

# 8.8 选择取货时间段 【此处我选择了-默认第一个时间段】
element_time_quantum = driver.find_element_by_xpath(
'//*[@aria-labelledby="timeWindows_label"]')
Select(element_time_quantum).select_by_index(1)
driver.implicitly_wait(15)

# 8.9 继续填写取货详情
element_checkout = driver.find_element_by_id(
'rs-checkout-continue-button-bottom')
driver.implicitly_wait(15)
driver.execute_script("arguments[0].click();", element_checkout)
element_checkout.click()
driver.implicitly_wait(10)

# 9.1 请填写收件人手机号码
element_Phone = driver.find_element_by_name('fullDaytimePhone')
element_Phone.send_keys('手机号')
driver.implicitly_wait(10)

# 9.2 请填写收件人身份证
element_nationalId = driver.find_element_by_name('nationalId')
element_nationalId.send_keys('身份证')
driver.implicitly_wait(10)

# 9.3 检查订单
element_checkoutPay = driver.find_element_by_id(
'rs-checkout-continue-button-bottom')
driver.execute_script("arguments[0].click();", element_checkoutPay)
driver.implicitly_wait(10)

# 10 立即下单 【此处我选择了-微信支付】
element_billingOptions = driver.find_element_by_id(
'checkout.billing.billingOptions.options.2')
driver.execute_script("arguments[0].click();", element_billingOptions)
driver.implicitly_wait(10)

# 11.1 确定
element_orderPay = driver.find_element_by_id(
'rs-checkout-continue-button-bottom')
driver.execute_script("arguments[0].click();", element_orderPay)
driver.implicitly_wait(15)

# 12 确认订单
element_endPay = driver.find_element_by_xpath(
'//*[@aria-describedby="rs-checkout-continuedisclaimer-bottom"]')
driver.execute_script("arguments[0].click();", element_endPay)
driver.implicitly_wait(15)

# 11 退出浏览器
time.sleep(10)
# driver.quit()

print("iphone12 自动化测试结束")
启动

python apple-12.py

全自动操作过程动图

在这里插入图片描述

Tip

其中账号密码相关需要替换成自己的,并且苹果官网会有卡顿延时,及403等异常,属于正常情况。

3.iphone13 预约抢购的python+selenium自动化测试脚本【只针对谷歌浏览器】

前置条件同iphone 12
Tips

因iphone13 无货,代码中加入了一个while判断,重复拉取省市区进行有无货的重新加载

python 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
python复制代码from selenium import webdriver
from datetime import datetime
from selenium.webdriver.support.select import Select
import pdb
import time

# iphone13 自动化测试
print("iphone13 自动化测试开始")

# 访问测试的url定义
url = "https://www.apple.com.cn/shop/buy-iphone/iphone-13-pro"

# 1. 创建浏览器对象 这里的Chrome中的变量是chromedriver的驱动地址
driver = webdriver.Chrome()

# 2. 跳转到apple官网
driver.get(url)

# 3. 隐式等待 设置 防止预售的网络的阻塞
driver.implicitly_wait(10)

# 4. 开始选择规格【此处我选择了-13 pro】
element_sku = driver.find_element_by_name('dimensionScreensize')
driver.implicitly_wait(10)
element_sku.click()

# 4.2 选择颜色【此处我选择了-银色】
element_color = driver.find_element_by_xpath(
'//*[@value="silver"]')
driver.execute_script("arguments[0].click();", element_color)
driver.implicitly_wait(10)

# 4.3 选择内存【此处我选择了-256g】
element_memory = driver.find_element_by_xpath(
'//*[@value="256gb"]')
driver.execute_script("arguments[0].click();", element_memory)
driver.implicitly_wait(10)

# 4.4 你是否有智能手机要折抵 【此处我选择了-没有旧机折扣】
element_old = driver.find_element_by_xpath('//*[@id="noTradeIn"]')
driver.execute_script("arguments[0].click();", element_old)
driver.implicitly_wait(10)

# 4.5 Applecare 【此处我选择了-无Applecare】
element_care = driver.find_element_by_id('iphonexs_ac_iup_noapplecare_label')
driver.execute_script("arguments[0].click();", element_care)
driver.implicitly_wait(10)

# 4.6 添加到购物袋
element_car = driver.find_element_by_xpath(
'//*[@value="add-to-cart"]')
driver.execute_script("arguments[0].click();", element_car)
driver.implicitly_wait(10)

# 5 页面跳转查看购物袋
element_check = driver.find_element_by_xpath(
'//*[@id="root"]/div[2]/div/div/div[2]/div/form/button')
driver.execute_script("arguments[0].click();", element_check)
driver.implicitly_wait(10)

# 6 结账
element_check_out = driver.find_element_by_xpath(
'//*[@id="shoppingCart.actions.checkout"]')
driver.execute_script("arguments[0].click();", element_check_out)
driver.implicitly_wait(10)

# 7.1 输入用户名
element_username = driver.find_element_by_id(
'signIn.customerLogin.appleId')
element_username.send_keys('苹果appleId')
driver.implicitly_wait(10)

# 7.2 输入密码
element_password = driver.find_element_by_id(
'signIn.customerLogin.password')
element_password.send_keys('密码')
driver.implicitly_wait(10)

# 7.3 点击登录
element_login = driver.find_element_by_id(
'signin-submit-button')
element_login.click()
driver.implicitly_wait(10)

# 8.1 你希望如何收到订单商品 【此处我选择了-我要取货】
element_want_order = driver.find_element_by_id(
'fulfillmentOptionButtonGroup1')
driver.execute_script("arguments[0].click();", element_want_order)
driver.implicitly_wait(10)

# 8.2 点击显示此地附近的零售店
selectdistrict = driver.find_element_by_xpath(
'//*[@aria-describedby="rs-fulfillment-storelocator-error"]')
driver.execute_script("arguments[0].click();", selectdistrict)
driver.implicitly_wait(10)

# 8.3 点击山东
element_provice = driver.find_element_by_xpath(
'//*[@value="山东"]')
driver.execute_script("arguments[0].click();", element_provice)
driver.implicitly_wait(10)

# 8.4 点击青岛
element_city = driver.find_element_by_xpath(
'//*[@value="青岛"]')
driver.execute_script("arguments[0].click();", element_city)
driver.implicitly_wait(10)

# 8.5 点击市南
element_area = driver.find_element_by_xpath(
'//*[@value="市南区"]')
driver.execute_script("arguments[0].click();", element_area)
driver.implicitly_wait(20)

# 因为无货需要判断元素是否可以点击
isOK = driver.find_element_by_id(
'checkout.fulfillment.pickupTab.pickup.storeLocator-R557').is_enabled()
isOKFlag = bool(1 - isOK)
#print("准备isOKFlag " + str(isOKFlag))

# while循环查看是否有货
while isOKFlag:
try:
# 重新调用省市区
#print("进来了isOKFlag " + str(isOKFlag))
driver.implicitly_wait(20)
selectdistrict = driver.find_element_by_xpath(
'//*[@aria-describedby="rs-fulfillment-storelocator-error"]')
driver.execute_script("arguments[0].click();", selectdistrict)

driver.implicitly_wait(20)
provice = driver.find_element_by_xpath(
'//*[@value="山东"]')
driver.execute_script("arguments[0].click();", provice)

city = driver.find_element_by_xpath(
'//*[@value="青岛"]')
driver.execute_script("arguments[0].click();", city)

area = driver.find_element_by_xpath(
'//*[@value="市南区"]')
driver.execute_script("arguments[0].click();", area)

driver.implicitly_wait(20)
isOK = driver.find_element_by_id(
'checkout.fulfillment.pickupTab.pickup.storeLocator-R557').is_enabled()
isOKFlag = bool(1 - isOK)
#print("最后了isOK " + str(isOKFlag))
except:
print("异常了 ")
break

# tips: 经验证,苹果官网如果在付款页面之前实体店无货,若有货后在结算页面也无法选择实体店

# 8.6 选择取货零售店 【此处我选择了-Apple 青岛万象城】
element_pickupTab = driver.find_element_by_id(
'checkout.fulfillment.pickupTab.pickup.storeLocator-R557')
driver.execute_script("arguments[0].click();", element_pickupTab)
driver.implicitly_wait(20)

# 8.7 选择取货时间 【根据时间自己定】
element_pickup_time = driver.find_element_by_xpath(
'//*[@value="11"]')
driver.execute_script("arguments[0].click();", element_pickup_time)
driver.implicitly_wait(10)

# 8.8 选择取货时间段 【此处我选择了-默认第一个时间段】
element_time_quantum = driver.find_element_by_xpath(
'//*[@aria-labelledby="timeWindows_label"]')
Select(element_time_quantum).select_by_index(1)
driver.implicitly_wait(15)

# 8.9 继续填写取货详情
element_checkout = driver.find_element_by_id(
'rs-checkout-continue-button-bottom')
driver.implicitly_wait(15)
driver.execute_script("arguments[0].click();", element_checkout)
element_checkout.click()
driver.implicitly_wait(20)

# 9.1 请填写收件人手机号码
element_Phone = driver.find_element_by_name('fullDaytimePhone')
element_Phone.send_keys('电话')
driver.implicitly_wait(10)

# 9.2 请填写收件人身份证
element_nationalId = driver.find_element_by_name('nationalId')
element_nationalId.send_keys('身份证号')
driver.implicitly_wait(10)

# 9.3 检查订单
element_checkoutPay = driver.find_element_by_id(
'rs-checkout-continue-button-bottom')
driver.execute_script("arguments[0].click();", element_checkoutPay)
driver.implicitly_wait(10)

# 10 立即下单 【此处我选择了-微信支付】
element_billingOptions = driver.find_element_by_id(
'checkout.billing.billingOptions.options.2')
driver.execute_script("arguments[0].click();", element_billingOptions)
driver.implicitly_wait(10)

# 11.1 确定
element_orderPay = driver.find_element_by_id(
'rs-checkout-continue-button-bottom')
driver.execute_script("arguments[0].click();", element_orderPay)
driver.implicitly_wait(15)

# 12 确认订单
element_endPay = driver.find_element_by_xpath(
'//*[@aria-describedby="rs-checkout-continuedisclaimer-bottom"]')
driver.execute_script("arguments[0].click();", element_endPay)
driver.implicitly_wait(15)

# 11 退出浏览器
time.sleep(10)
# driver.quit()

print("iphone13 自动化测试结束")
启动

python apple-13.py

全自动操作过程动图

在这里插入图片描述

脚本成果

在这里插入图片描述

写在最后

以上订单均为测试所用,本菜狗并没有购买iphone,也希望大家还是多多支持国货,没有针对,没有道德绑架,只是希望我们国家产物越来越优秀❤️

我是JavaDog,谢谢博友耐心看完, 抽空来我狗窝🐕瞅瞅呗 blog.javadog.net

本文转载自: 掘金

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

【爬虫系列】WebSocket握手验证请求 前言 一、Web

发表于 2021-10-29

前言

查了下最新的反爬虫方式,看到一个WebSocket握手验证反爬虫,还没有遇到过,找了个网站试一试~
最新反爬方式:blog.csdn.net/qq_26079939…


一、WebSocket是什么?

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

二、WebSocket握手验证反爬虫

1.目标网站

乐鱼体育:live.611.com/zq

2.网站分析

1.建立Socket链接的地址,其中9394adf88ece4ff08f9ac6e82949f3a1参数是变值

在这里插入图片描述
在这里插入图片描述

2.通过如下方式获取token

1
2
3
4
5
6
7
8
9
python复制代码def getToken():
url = "https://live.611.com/Live/GetToken"
response = requests.get(url)
if response.status_code == 200:
data = json.loads(response.text)
token = data["Data"]
return token
else:
print("请求错误")

3.从下面的图可以看出,绿色的箭头是客户端发送给服务器的数据,红色箭头是服务器响应的数据

在这里插入图片描述

3.获取数据

Python 库中用于连接 WebSocket 的有很多,但是易用、稳定的有 websocket-client(非异步)、websockets(异步)、aiowebsocket(异步),以下用websocket-client和websockets方式实现。

websocket-client方法:

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
python复制代码import requests
import websocket
import json
import time


def getToken(): # 获取token参数
url = "https://live.611.com/Live/GetToken"
response = requests.get(url)
if response.status_code == 200:
data = json.loads(response.text)
token = data["Data"]
return token
else:
print("请求错误")


def get_message(): # 需要发送的数据
timestamp = int(time.time()) * 1000
info = {'chrome': 'true', 'version': '80.0.3987.122', 'webkit': 'true'}
message1 = {
"command": "RegisterInfo",
"action": "Web",
"ids": [],
"UserInfo": {
"Version": str([timestamp]) + json.dumps(info),
"Url": "https://live.611.com/zq"
}
}
message2 = {
"command": "JoinGroup",
"action": "SoccerLiveOdd",
"ids": []
}
message3 = {
"command": "JoinGroup",
"action": "SoccerLive",
"ids": []
}
return json.dumps(message1), json.dumps(message2), json.dumps(message3)


def Download(token,message1,message2,message3):
uri = "wss://push.611.com:6119/{}".format(token)
ws = websocket.create_connection(uri, timeout=10)
ws.send(message1)
ws.send(message2)
ws.send(message3)
while True:
result = ws.recv()
print(result)

if __name__ == '__main__':
token = getToken() # 获取token字符串
message1, message2, message3 = get_message() # 构造请求信息
Download(token,message1, message2,message3) # 抓取数据

运行结果在这里插入图片描述

websockets方法

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
python复制代码import asyncio
import logging
import time,json,requests
from aiowebsocket.converses import AioWebSocket


def getToken():
url = "https://live.611.com/Live/GetToken"
response = requests.get(url)
if response.status_code == 200:
data = json.loads(response.text)
token = data["Data"]
return token
else:
print("请求错误")


def get_message(): # 需要发送的数据
timestamp = int(time.time()) * 1000
info = {'chrome': 'true', 'version': '80.0.3987.122', 'webkit': 'true'}
message1 = {
"command": "RegisterInfo",
"action": "Web",
"ids": [],
"UserInfo": {
"Version": str([timestamp]) + json.dumps(info),
"Url": "https://live.611.com/zq"
}
}
message2 = {
"command": "JoinGroup",
"action": "SoccerLiveOdd",
"ids": []
}
message3 = {
"command": "JoinGroup",
"action": "SoccerLive",
"ids": []
}
return message1, message2, message3

async def startup():
token = getToken() # 获取token字符串
uri = "wss://push.611.com:6119/{}".format(token)
message1, message2,message3 = get_message() # 构造请求信息
async with AioWebSocket(uri) as aws:
converse = aws.manipulator
await converse.send(json.dumps(message1))
await converse.send(json.dumps(message2))
await converse.send(json.dumps(message3))
while True:
mes = await converse.receive()
if mes:
msg = json.loads(str(mes, encoding="utf-8"))
print(msg)


if __name__ == '__main__':
try:
asyncio.get_event_loop().run_until_complete(startup())
except KeyboardInterrupt as exc:
logging.info('Quit.')

运行结果

在这里插入图片描述


三、 总结

Web 领域中,用于实现数据’实时’更新的手段有轮询和 WebSocket 这两种。轮询指的是客户端按照一定时间间隔(如 1 秒)访问服务端接口,从而达到 ‘实时’ 的效果,虽然看起来数据像是实时更新的,但实际上它有一定的时间间隔,并不是真正的实时更新。轮询通常采用 拉 模式,由客户端主动从服务端拉取数据。

WebSocket 采用的是 推 模式,由服务端主动将数据推送给客户端,这种方式是真正的实时更新。

服务器端创建 socket 服务后监听客户端,使用 while True 的方式读取客户端发送的消息

然后对服务器端发送的握手请求进验证,如果验证通过,则返回状态码为 101 的响应头,否则返回状态码为 403 的响应头

本文转载自: 掘金

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

别再稀里糊涂的使用ls命令了,带你重新认识linux查看文件

发表于 2021-10-29

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

欢迎评论指正文中的不足之处!

linux极简小知识:40、文件管理中最重要的ls命令,及文件类型、文件权限标识【附带了解pwd命令】

本篇是 linux极简小知识 系列的第40篇,介绍真实有用的linux知识,欢迎阅读查看!

ls 用于查看linux中的文件信息。之所以说它是最重要的命令,原因在于在整个 Unix(或linux)的哲学中,核心就是“一切皆文件!”

驱动、终端、网卡、显示器、硬件设备、软件程序等,在linux中,都是文件。知道了如何查看文件,就知道了如何深入了解linux。

比如,打印或查看当前的工作目录:

1
2
sh复制代码[root_test@VM_0_15_centos test]$ pwd
/home/root_test/test

pwd 命令

pwd (print work directory),直译就是打印工作目录,即显示当前的目录、显示当前命令行操作的位置!

ls 命令

ls (list files)列出当前目录下的文件,用于查看指定目录下都有哪些文件及文件信息。

可以查看到文件的名称、类型、权限、所属用户和组、修改时间等各种信息。

ls 查看当前目录下的文件

1
2
sh复制代码[root_test@VM_0_15_centos ~]$ ls
axel-2.4-9.el7.x86_64.rpm test Y6kkd1d

ls 查看指定目录下的文件

ls 后跟着一个目录,即表示查看这个目录下的文件。

比如,查看根目录下的文件 ls /。

查看 /etc/my.cnf.d 目录中的文件。

1
2
sh复制代码[root_test@VM_0_15_centos ~]$ ls /etc/my.cnf.d
hostname xzdiff xzgrep

注,ls 列出来的文件,不同的颜色,表示不同的权限!

ls查看多个目录或文件

ls后面可以通过空格跟着多个文件或目录。

如下,查看当前目录、根目录、root用户目录、my.txt文件。

1
2
3
4
5
6
7
8
9
10
sh复制代码[root_test@VM_0_15_centos ~]$ ls ./ / /root test/my.txt
test/my.txt

/:
bin data etc lib lost+found mnt proc run srv sys usr
boot dev home lib64 media opt root sbin swap_file tmp var

./:
axel-2.4-9.el7.x86_64.rpm test Y6kkd1d
ls: cannot open directory /root: Permission denied

ls -l 查看长列表形式的文件

ls -l 中 -l 表示长列表格式(long listing format)。

使用该命令,可以以列表且内容更详细的形式查看文件。

1
2
3
4
5
sh复制代码$ ls -l
total 64
-rw-rw-r-- 1 root_test root_test 54908 Apr 16 2014 axel-2.4-9.el7.x86_64.rpm
drwxrwxr-x 2 root_test root_test 4096 Oct 26 21:52 test
-rw-rw-r-- 1 root_test root_test 0 Sep 3 17:40 Y6kkd1d

ls -l 长列表格式详解

通过执行命令 ls -l 显示的长列表格式的文件信息,包含着文件有关的更多内容。

我们以下面两行为例:

1
2
3
4
sh复制代码-rw-rw-r-- 1 root_test root_test 54908 Apr 16  2014 axel-2.4-9.el7.x86_64.rpm


drwxrwxr-x 2 root_test root_test 4096 Oct 26 21:52 test

格式详解

长列表格式的输出在每一行中列出了单个文件或目录。不仅有文件名,还包括文件的类型(属性)、大小、权限等详细信息:

  • 第一行显示了在目录中文件占用的总块数(文件系统块数 the number of file system blocks)。表示的是目录下所有文件的大小。
  • 之后的每一行显示该文件的长格式信息。

长格式信息一个分为7大列,10小项。

file mode【文件模式】 number of links【链接数】 owner name【所属用户名】 group name【所属组名】 number of bytes in the file【文件字节数】 The date of last modification【组后修改的日期】 filename【文件名】
File Type[文件类型] 所有者权限 所属组的权限 其他用户权限 链接数 所有者 所属群组 文件的字节大小 最后修改时间 文件名
- rw- rw- r– 1 root_test root_test 54908 Apr 16 2014 axel-2.4-9.el7.x86_64.rpm
d rwx rwx r-x 2 root_test root_test 4096 Oct 26 21:52 test

image.png

  1. 第一列共10位,第1位表示文档类型,d表示目录,-表示普通文件。后9位,依次对应三种身份所拥有的权限,身份顺序为:owner、group、others,权限顺序为:readable、writable、excutable,即可读、可写、可执行权限。如:-rwxr-xr–的含义为,当前文档是一个普通文件,所属用户可读、可写、可执行,同一个群组下的用户,可读、可执行,其他人只有可读取的权限。
  2. 第二列表示链接数,表示有多少个文件链接到inode号码。它表示的是硬链接数。

硬链接数并不表示实际的文件数。

硬链接(hard link, 也称链接)是指通过索引节点来进行的链接。

在Linux的文件系统中,保存在磁盘分区中的文件不管是什么类型都会给它分配一个编号,这个编号被称为索引节点编号(inode number)。

【关于inode参见最后一部分附:关于 inode 和 ls -li的介绍】

  1. 第三列表示拥有者,即文件所属用户
  2. 第四列表示所属用户组。
  3. 第五列表示文档容量大小,单位字节。
  4. 第六列表示文档最后修改时间,注意不是文档的创建时间!包括缩写的月(abbreviated month)、日、小时、分钟等。
  5. 第七列表示文档名称。以点 . 开头的是隐藏文件

关于第一行的文件块数和-k选项的问题

第一行显示的文件大小,默认的单位是 512 byte,除非指定 -k,指定选项 -k 将以 1024 byte(1kB blocks) 为单位。

该内容参考自 What does the first line “Total” indicate while executing the ls -l command in Linux? [closed],并且 POSIX文档 中也是这么说的。

可实际测试,在加 -k 和不加 -k 选项的情况下,Total显示的数字是一样的,具体原因未找到相关资料,不知是否是用法不对。

不加 -k 选项为 64:

1
2
3
4
5
sh复制代码$ ls -l
total 64
-rw-rw-r-- 1 root_test root_test 54908 Apr 16 2014 axel-2.4-9.el7.x86_64.rpm
drwxrwxr-x 2 root_test root_test 4096 Oct 26 21:52 test
-rw-rw-r-- 1 root_test root_test 0 Sep 3 17:40 Y6kkd1d

加 -k 选项仍为 64:

1
2
3
4
5
sh复制代码$ ls -lk
total 64
-rw-rw-r-- 1 root_test root_test 54908 Apr 16 2014 axel-2.4-9.el7.x86_64.rpm
drwxrwxr-x 2 root_test root_test 4096 Oct 26 21:52 test
-rw-rw-r-- 1 root_test root_test 0 Sep 3 17:40 Y6kkd1d

使用 --block-size 选项设置第一行显示的块大小

要查看不同单位的块大小的显示,可以使用 --block-size 选项。

ls -l --block-size=M 将会以近似 MiB 的文件大小显示。如果先要使用 MB (10^6 bytes) 而不是 MiB (2^20 bytes) 单位,可以使用 --block-size=MB 代替。

实际测试 --block-size=B 和--block-size=MB 看不出什么区别。

1
2
3
4
5
6
7
8
9
10
sh复制代码$ ls -l --block-size=M
total 1M
-rw-rw-r-- 1 root_test root_test 1M Apr 16 2014 axel-2.4-9.el7.x86_64.rpm
drwxrwxr-x 2 root_test root_test 1M Oct 26 21:52 test
-rw-rw-r-- 1 root_test root_test 0M Sep 3 17:40 Y6kkd1d
$ ls -l --block-size=k
total 64K
-rw-rw-r-- 1 root_test root_test 54K Apr 16 2014 axel-2.4-9.el7.x86_64.rpm
drwxrwxr-x 2 root_test root_test 4K Oct 26 21:52 test
-rw-rw-r-- 1 root_test root_test 0K Sep 3 17:40 Y6kkd1d

而且,还可以使用任何你想要的给定的数字单位,如 --block-size=1M、--block-size=2k。

1
2
3
4
5
sh复制代码$ ls -l --block-size=2k
total 32
-rw-rw-r-- 1 root_test root_test 27 Apr 16 2014 axel-2.4-9.el7.x86_64.rpm
drwxrwxr-x 2 root_test root_test 2 Oct 26 21:52 test
-rw-rw-r-- 1 root_test root_test 0 Sep 3 17:40 Y6kkd1d

关于 -h 选项(human readable)

ls -lh 可以获得长格式列表和 人类可读(human readable) 的文件大小。

1
2
3
4
5
sh复制代码$ ls -lh
total 64K
-rw-rw-r-- 1 root_test root_test 54K Apr 16 2014 axel-2.4-9.el7.x86_64.rpm
drwxrwxr-x 2 root_test root_test 4.0K Oct 26 21:52 test
-rw-rw-r-- 1 root_test root_test 0 Sep 3 17:40 Y6kkd1d

ls -s 查看磁盘使用情况

1
2
3
sh复制代码$ ls -s
total 64
60 axel-2.4-9.el7.x86_64.rpm 4 test 0 Y6kkd1d

ls -a 显示隐藏文件和文件夹,即指定目录下的所有文件

Linux中,以 . 开头的文件或文件名就是隐藏文件。

默认 ls 查看文件时,并不会显示隐藏文件。可通过 -a (--all)选项查看所有文件。

1
2
3
4
5
6
sh复制代码$ ls --all
. axel-2.4-9.el7.x86_64.rpm .bash_logout .bashrc .config .pki .viminfo
.. .bash_history .bash_profile .cache .mozilla test Y6kkd1d

$ ls
axel-2.4-9.el7.x86_64.rpm test Y6kkd1d

结合 -l 查看,ls -al:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sh复制代码$ ls -al
total 116
drwx------ 7 root_test root_test 4096 Oct 26 21:52 .
drwxr-xr-x. 7 root root 4096 Apr 11 2018 ..
-rw-rw-r-- 1 root_test root_test 54908 Apr 16 2014 axel-2.4-9.el7.x86_64.rpm
-rw------- 1 root_test root_test 10633 Oct 28 14:04 .bash_history
-rw-r--r-- 1 root_test root_test 18 Dec 7 2016 .bash_logout
-rw-r--r-- 1 root_test root_test 193 Dec 7 2016 .bash_profile
-rw-r--r-- 1 root_test root_test 231 Dec 7 2016 .bashrc
drwxrwxr-x 3 root_test root_test 4096 Sep 2 11:36 .cache
drwxrwxr-x 3 root_test root_test 4096 Sep 2 11:36 .config
drwxr-xr-x 4 root_test root_test 4096 Jun 21 2019 .mozilla
drwxrw---- 3 root_test root_test 4096 Sep 2 11:41 .pki
drwxrwxr-x 2 root_test root_test 4096 Oct 26 21:52 test
-rw------- 1 root_test root_test 1985 Sep 3 09:44 .viminfo
-rw-rw-r-- 1 root_test root_test 0 Sep 3 17:40 Y6kkd1d

ls -lr

通常不会直接使用 ls -r 而是要结合 -l 使用。

-r 表示逆向排序显示;默认是以文件名逆序显示的。

1
2
3
4
5
sh复制代码$ ls -r
Y6kkd1d test axel-2.4-9.el7.x86_64.rpm

$ ls
axel-2.4-9.el7.x86_64.rpm test Y6kkd1d

结合 -l:

1
2
3
4
5
sh复制代码$ ls -l -r
total 64
-rw-rw-r-- 1 root_test root_test 0 Sep 3 17:40 Y6kkd1d
drwxrwxr-x 2 root_test root_test 4096 Oct 26 21:52 test
-rw-rw-r-- 1 root_test root_test 54908 Apr 16 2014 axel-2.4-9.el7.x86_64.rpm

-r -t 可以实现按时间进行逆序显示(ls -l -r -t):

1
2
3
4
5
sh复制代码$ ls -lrt
total 64
-rw-rw-r-- 1 root_test root_test 54908 Apr 16 2014 axel-2.4-9.el7.x86_64.rpm
-rw-rw-r-- 1 root_test root_test 0 Sep 3 17:40 Y6kkd1d
drwxrwxr-x 2 root_test root_test 4096 Oct 26 21:52 test

以上所有命令都可以合并简写,如:ls -lr,ls -lrt。

ls -R 递归显示文件(所有的文件)

-R 用于递归显示文件夹中的文件。

1
2
3
4
5
6
sh复制代码$ ls -R
.:
axel-2.4-9.el7.x86_64.rpm test Y6kkd1d

./test:
my.txt

命令组合使用:

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
sh复制代码$ ls -alrtR
.:
total 116
-rw-rw-r-- 1 root_test root_test 54908 Apr 16 2014 axel-2.4-9.el7.x86_64.rpm
-rw-r--r-- 1 root_test root_test 231 Dec 7 2016 .bashrc
-rw-r--r-- 1 root_test root_test 193 Dec 7 2016 .bash_profile
-rw-r--r-- 1 root_test root_test 18 Dec 7 2016 .bash_logout
drwxr-xr-x. 7 root root 4096 Apr 11 2018 ..
drwxr-xr-x 4 root_test root_test 4096 Jun 21 2019 .mozilla
drwxrwxr-x 3 root_test root_test 4096 Sep 2 11:36 .cache
drwxrwxr-x 3 root_test root_test 4096 Sep 2 11:36 .config
drwxrw---- 3 root_test root_test 4096 Sep 2 11:41 .pki
-rw------- 1 root_test root_test 1985 Sep 3 09:44 .viminfo
-rw-rw-r-- 1 root_test root_test 0 Sep 3 17:40 Y6kkd1d
drwx------ 7 root_test root_test 4096 Oct 26 21:52 .
drwxrwxr-x 2 root_test root_test 4096 Oct 26 21:52 test
-rw------- 1 root_test root_test 10777 Oct 28 14:10 .bash_history

./.mozilla:
total 16
drwxr-xr-x 2 root_test root_test 4096 Jun 10 2014 plugins
drwxr-xr-x 2 root_test root_test 4096 Jun 10 2014 extensions
drwxr-xr-x 4 root_test root_test 4096 Jun 21 2019 .
drwx------ 7 root_test root_test 4096 Oct 26 21:52 ..

./.mozilla/plugins:
total 8
drwxr-xr-x 2 root_test root_test 4096 Jun 10 2014 .
drwxr-xr-x 4 root_test root_test 4096 Jun 21 2019 ..

./.mozilla/extensions:
total 8
drwxr-xr-x 2 root_test root_test 4096 Jun 10 2014 .
drwxr-xr-x 4 root_test root_test 4096 Jun 21 2019 ..

./.cache:
total 12
drwxrwxr-x 3 root_test root_test 4096 Sep 2 11:36 .
drwx------ 7 root_test root_test 4096 Oct 26 21:52 ..
drwxrwxr-x 2 root_test root_test 4096 Oct 28 10:17 abrt

./.cache/abrt:
total 12
drwxrwxr-x 3 root_test root_test 4096 Sep 2 11:36 ..
-rw------- 1 root_test root_test 11 Oct 28 10:17 lastnotification
drwxrwxr-x 2 root_test root_test 4096 Oct 28 10:17 .

./.config:
total 12
drwxrwxr-x 2 root_test root_test 4096 Sep 2 11:36 abrt
drwxrwxr-x 3 root_test root_test 4096 Sep 2 11:36 .
drwx------ 7 root_test root_test 4096 Oct 26 21:52 ..

./.config/abrt:
total 8
drwxrwxr-x 3 root_test root_test 4096 Sep 2 11:36 ..
drwxrwxr-x 2 root_test root_test 4096 Sep 2 11:36 .

./.pki:
total 12
drwxrw---- 2 root_test root_test 4096 Sep 2 11:41 nssdb
drwxrw---- 3 root_test root_test 4096 Sep 2 11:41 .
drwx------ 7 root_test root_test 4096 Oct 26 21:52 ..

./.pki/nssdb:
total 8
drwxrw---- 3 root_test root_test 4096 Sep 2 11:41 ..
drwxrw---- 2 root_test root_test 4096 Sep 2 11:41 .

./test:
total 8
drwx------ 7 root_test root_test 4096 Oct 26 21:52 ..
-rw-rw-r-- 1 root_test root_test 0 Oct 26 21:52 my.txt
drwxrwxr-x 2 root_test root_test 4096 Oct 26 21:52 .

文件类型标志(File type flag)

  • -(regular file) 普通文件
  • d(directory) 目录文件
  • b(block) 块特殊⽂件,块设备文件。最常见的块设备是硬盘,但也存在许多其他块设备,如软盘驱动器、蓝光阅读器和闪存。
  • c(character) 字符特殊⽂件,字符设备文件,多表示一次性读取设备,典型的字符设备是终端(终端分多种,有物理的也有虚拟的)和键盘。
  • l(linkage) 链接文件,符号链接文件
  • f(FIFO-pipe) 管道文件,命名管道p,也简称 p
  • s(socket) 套接字⽂件,本地套接字

区分块设备和字符设备最简单的方法是看数据访问的方式。

能随机访问获取数据的是块设备,必须按字节顺序访问的是字符设备。

推荐参考 搞懂Linux下的几种文件类型。

文件权限的表示

  • r = 4,读。
  • w = 2,写。
  • x = 1,执行。

对于目录的可执行权限(x),表示的是可进入目录。

rwx权限的组合,可以通过其对应的数字相加的值来表示。

比如,可读可执行(rx=5=4+1)、可读可写(rw=6=4+2)、可读可写可执行(rwx=7=4+2+1)。

这也是常常看到的 chmod 命令中 chmod 755、chmod 774、chmod 775、chmod 777 数字所表示的含义。

有时会看到 chmod 4755 四个数字表示的形式。与 chmod 755 的区别在于开头多了一位,此处为4,表示其他用户执行文件时,具有与所有者相同的权限。(未进一步确认)

linux命令chmod命令设置权限的777,775,774

附:关于 inode 和 ls -li

inode 中文意思是“索引节点”。

每个存储设备或存储设备的分区(存储设备是硬盘、软盘、U盘等等)被格式化为文件系统后,应该有两部份,一部份是inode,另一部份是Block,Block是用来存储数据用的。而inode,则是用来存储这些数据的信息,这些信息包括文件大小、属主、归属的用户组、读写权限等。

inode为每个文件进行信息索引,所以就有了inode的数值。操作系统根据指令,能通过inode值最快的找到相对应的文件。

当用 ls 查看目录或文件时,如果加上 -i 参数,就会显示出 inode 节点号码。

1
2
3
4
5
sh复制代码$ ls -li
total 64
492775 -rw-rw-r-- 1 root_test root_test 54908 Apr 16 2014 axel-2.4-9.el7.x86_64.rpm
499731 drwxrwxr-x 2 root_test root_test 4096 Oct 26 21:52 test
492981 -rw-rw-r-- 1 root_test root_test 0 Sep 3 17:40 Y6kkd1d

test 的 inode 值为 499731;Y6kkd1d 的 inode 值是 492981。

此部分参考自 linux每日命令(26):Linux文件属性详解。

参考

  • Linux实战技能100讲
  • How do I make ls show file sizes in megabytes?
  • Linux ls 命令

本文转载自: 掘金

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

无需 Dockerfile 的镜像构建:BuildPack

发表于 2021-10-29

过去的工作中,我们使用微服务、容器化以及服务编排构建了技术平台。为了提升开发团队的研发效率,我们同时还提供了 CICD 平台,用来将代码快速的部署到 Openshift(企业级的 Kubernetes) 集群。

部署的第一步就是应用程序的容器化,持续集成的交付物从以往的 jar 包、webpack 等变成了容器镜像。容器化将软件代码和所需的所有组件(库、框架、运行环境)打包到一起,进而可以在任何环境任何基础架构上一致地运行,并与其他应用“隔离”。

我们的代码需要从源码到编译到最终可运行的镜像,甚至部署,这一切在 CICD 的流水线中完成。最初,我们在每个代码仓库中都加入了三个文件,也通过项目生成器(类似 Spring Initializer)在新项目中注入:

  • Jenkinsfile.groovy:用来定义 Jenkins 的 Pipeline,针对不同的语言还会有多种版本
  • Manifest YAML:用于定义 Kubernetes 资源,也就是工作负载及其运行的相关描述
  • Dockerfile:用于构建对象

这个三个文件也需要在工作中不断的演进,起初项目较少(十几个)的时候我们基础团队还可以去各个代码仓库去维护升级。随着项目爆发式的增长,维护的成本越来越高。我们对 CICD 平台进行了迭代,将“Jenkinsfile.groovy”和 “manifest YAML”从项目中移出,变更较少的 Dockerfile 就保留了下来。

随着平台的演进,我们需要考虑将这唯一的“钉子户” Dockerfile 与代码解耦,必要的时候也需要对 Dockerfile 进行升级。因此调研了一下 buildpacks,就有了今天的这篇文章。

什么是 Dockerfile

Docker 通过读取 Dockerfile 中的说明自动构建镜像。Dockerfile 是一个文本文件,包含了由 Docker 可以执行用于构建镜像的指令。我们拿之前用于测试 Tekton 的 Java 项目的 Dockerfile 为例:

1
2
3
4
5
6
dockerfile复制代码FROM openjdk:8-jdk-alpine

RUN mkdir /app
WORKDIR /app
COPY target/*.jar /app/app.jar
ENTRYPOINT ["sh", "-c", "java -Xmx128m -Xms64m -jar app.jar"]

镜像分层

你可能会听过 Docker 镜像包含了多个层。每个层与 Dockerfile 中的每个命令对应,比如 RUN、COPY、ADD。某些特定的指令会创建一个新的层,在镜像构建过程中,假如某些层没有发生变化,就会从缓存中获取。

在下面的 Buildpack 中也同样通过镜像分层和 cache 来加速镜像的构建。

什么是 Buildpack

BuildPack 是一个程序,它能将源代码转换成容器镜像的并可以在任意云环境中运行。通常 buildpack 封装了单一语言的生态工具链。适用于 Java、Ruby、Go、NodeJs、Python 等。

buildpacks.io

Builder 是什么?

一些 buildpacks 按顺序组合之后就是 builder,除了 buildpacks, builder 中还加入了 生命周期 和 stack 容器镜像。

stack 容器镜像由两个镜像组成:用于运行 buildpack 的镜像 build image,以及构建应用镜像的基础镜像 run image。如上图,就是 builder 中的运行环境。

Buildpack 的工作方式

how buildpack works

每个 buildpack 运行时都包含了两个阶段:

phases

1. 检测阶段

通过检查源代码中的某些特定文件/数据,来判断当前 buildpack 是否适用。如果适用,就会进入构建阶段;否则就会退出。比如:

  • Java maven 的 buildpack 会检查源码中是否有 pom.xml
  • Python 的 buildpack 会检查源码中是否有 requirements.txt 或者 setup.py 文件
  • Node buildpack 会查找 package-lock.json 文件。

2. 构建阶段

在构建阶段会进行如下操作:

  1. 设置构建环境和运行时环境
  2. 下载依赖并编译源码(假如需要的话)
  3. 设置正确的 entrypoint 和启动脚本。

比如:

  • Java maven buildpack 在检查到有 pom.xml 文件之后,会执行 mvn clean install -DskipTests
  • Python buildpack 检查到有 requrements.txt 之后,会执行 pip install -r requrements.txt
  • Node build pack 检查到有 package-lock.json 后执行 npm install

BuildPack 上手

那到底如何在没有 Dockerfile 的情况下使用 builderpack 构建镜像的。看了上面这些,大家基本上也都能了解到这个核心就在 buildpack 的编写和使用的。

其实现在有很多开源的 buildpack 可以用,没有特定定制的情况下无需自己手动编写。比如下面的几个大厂开源并维护的 Buildpacks:

  • Heroku Buildpacks
  • Google Buildpacks
  • Paketo

但是正式详细介绍开源的 buildpacks 之前,我们还是通过自己创建 buildpack 的方式来深入了解 Buildpacks 的工作方式。测试项目呢,我们还是用测试 Tekton 的 Java 项目。

下面所有的内容都提交到了 Github 上,可以访问:github.com/addozhang/b… 获取相关代码。

最终的目录buildpacks-sample 结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
arduino复制代码├── builders
│   └── builder.toml
├── buildpacks
│   └── buildpack-maven
│   ├── bin
│   │   ├── build
│   │   └── detect
│   └── buildpack.toml
└── stacks
├── build
│   └── Dockerfile
├── build.sh
└── run
└── Dockerfile

创建 buildpack

1
2
3
4
5
shell复制代码pack buildpack new examples/maven \
--api 0.5 \
--path buildpack-maven \
--version 0.0.1 \
--stacks io.buildpacks.samples.stacks.bionic

看下生成的 buildpack-maven 目录:

1
2
3
4
5
python复制代码buildpack-maven
├── bin
│   ├── build
│   └── detect
└── buildpack.toml

各个文件中都是默认的初试数据,并没有什么用处。需要添加些内容:

bin/detect:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
shell复制代码#!/usr/bin/env bash

if [[ ! -f pom.xml ]]; then
exit 100
fi

plan_path=$2

cat >> "${plan_path}" <<EOL
[[provides]]
name = "jdk"
[[requires]]
name = "jdk"
EOL

bin/build:

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
shell复制代码#!/usr/bin/env bash

set -euo pipefail

layers_dir="$1"
env_dir="$2/env"
plan_path="$3"

m2_layer_dir="${layers_dir}/maven_m2"
if [[ ! -d ${m2_layer_dir} ]]; then
mkdir -p ${m2_layer_dir}
echo "cache = true" > ${m2_layer_dir}.toml
fi
ln -s ${m2_layer_dir} $HOME/.m2

echo "---> Running Maven"
mvn clean install -B -DskipTests

target_dir="target"
for jar_file in $(find "$target_dir" -maxdepth 1 -name "*.jar" -type f); do
cat >> "${layers_dir}/launch.toml" <<EOL
[[processes]]
type = "web"
command = "java -jar ${jar_file}"
EOL
break;
done

buildpack.toml:

1
2
3
4
5
6
7
8
shell复制代码api = "0.5"

[buildpack]
id = "examples/maven"
version = "0.0.1"

[[stacks]]
id = "com.atbug.buildpacks.example.stacks.maven"

创建 stack

构建 Maven 项目,首选需要 Java 和 Maven 的环境,我们使用 maven:3.5.4-jdk-8-slim 作为 build image 的 base 镜像。应用的运行时需要 Java 环境即可,因此使用 openjdk:8-jdk-slim作为 run image 的 base 镜像。

在 stacks 目录中分别创建 build 和 run 两个目录:

build/Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
dockerfile复制代码FROM maven:3.5.4-jdk-8-slim

ARG cnb_uid=1000
ARG cnb_gid=1000
ARG stack_id

ENV CNB_STACK_ID=${stack_id}
LABEL io.buildpacks.stack.id=${stack_id}

ENV CNB_USER_ID=${cnb_uid}
ENV CNB_GROUP_ID=${cnb_gid}

# Install packages that we want to make available at both build and run time
RUN apt-get update && \
apt-get install -y xz-utils ca-certificates && \
rm -rf /var/lib/apt/lists/*

# Create user and group
RUN groupadd cnb --gid ${cnb_gid} && \
useradd --uid ${cnb_uid} --gid ${cnb_gid} -m -s /bin/bash cnb

USER ${CNB_USER_ID}:${CNB_GROUP_ID}

run/Dockerfile

1
2
3
4
5
6
7
8
dockerfile复制代码FROM openjdk:8-jdk-slim

ARG stack_id
ARG cnb_uid=1000
ARG cnb_gid=1000
LABEL io.buildpacks.stack.id="${stack_id}"

USER ${cnb_uid}:${cnb_gid}

然后使用如下命令构建出两个镜像:

1
2
3
4
shell复制代码export STACK_ID=com.atbug.buildpacks.example.stacks.maven

docker build --build-arg stack_id=${STACK_ID} -t addozhang/samples-buildpacks-stack-build:latest ./build
docker build --build-arg stack_id=${STACK_ID} -t addozhang/samples-buildpacks-stack-run:latest ./run

创建 Builder

有了 buildpack 和 stack 之后就是创建 Builder 了,首先创建 builder.toml 文件,并添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
toml复制代码[[buildpacks]]
id = "examples/maven"
version = "0.0.1"
uri = "../buildpacks/buildpack-maven"

[[order]]
[[order.group]]
id = "examples/maven"
version = "0.0.1"

[stack]
id = "com.atbug.buildpacks.example.stacks.maven"
run-image = "addozhang/samples-buildpacks-stack-run:latest"
build-image = "addozhang/samples-buildpacks-stack-build:latest"

然后执行命令,注意这里我们使用了 --pull-policy if-not-present 参数,就不需要将 stack 的两个镜像推送到镜像仓库了:

1
shell复制代码pack builder create example-builder:latest --config ./builder.toml --pull-policy if-not-present

测试

有了 builder 之后,我们就可以使用创建好的 builder 来构建镜像了。

这里同样加上了 --pull-policy if-not-present 参数来使用本地的 builder 镜像:

1
2
shell复制代码# 目录 buildpacks-sample  与 tekton-test 同级,并在 buildpacks-sample  中执行如下命令
pack build addozhang/tekton-test --builder example-builder:latest --pull-policy if-not-present --path ../tekton-test

如果看到类似如下内容,就说明镜像构建成功了(第一次构建镜像由于需要下载 maven 依赖耗时可能会比较久,后续就会很快,可以执行两次验证下):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
shell复制代码...
===> EXPORTING
[exporter] Adding 1/1 app layer(s)
[exporter] Reusing layer 'launcher'
[exporter] Reusing layer 'config'
[exporter] Reusing layer 'process-types'
[exporter] Adding label 'io.buildpacks.lifecycle.metadata'
[exporter] Adding label 'io.buildpacks.build.metadata'
[exporter] Adding label 'io.buildpacks.project.metadata'
[exporter] Setting default process type 'web'
[exporter] Saving addozhang/tekton-test...
[exporter] *** Images (0d5ac1158bc0):
[exporter] addozhang/tekton-test
[exporter] Adding cache layer 'examples/maven:maven_m2'
Successfully built image addozhang/tekton-test

启动容器,会看到 spring boot 应用正常启动:

1
2
3
4
5
6
7
8
9
10
shell复制代码docker run --rm addozhang/tekton-test:latest
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.2.3.RELEASE)

...

总结

其实现在有很多开源的 buildpack 可以用,没有特定定制的情况下无需自己手动编写。比如下面的几个大厂开源并维护的 Buildpacks:

  • Heroku Buildpacks
  • Google Buildpacks
  • Paketo

上面几个 buildpacks 库内容比较全面,实现上会有些许不同。比如 Heroku 的执行阶段使用 Shell 脚本,而 Paketo 使用 Golang。后者的扩展性较强,由 Cloud Foundry 基金会支持,并拥有由 VMware 赞助的全职核心开发团队。这些小型模块化的 buildpack,可以通过组合扩展使用不同的场景。

当然还是那句话,自己上手写一个会更容易理解 Buildpack 的工作方式。

本文转载自: 掘金

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

初学者也能看懂的 Vue2 源码中那些实用的基础工具函数

发表于 2021-10-29

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

  1. 前言

大家好,我是若川。欢迎关注我的公众号若川视野,最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 参与,已进行两个多月,大家一起交流学习,共同进步,很多人都表示收获颇丰。

想学源码,极力推荐之前我写的《学习源码整体架构系列》 包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4、koa-compose、vue 3.2 发布、vue-this、create-vue、玩具vite等10余篇源码文章。

本文仓库 vue-analysis,求个star^_^

最近组织了源码共读活动,大家一起学习源码。于是搜寻各种值得我们学习,且代码行数不多的源码。

之前写了 Vue3 相关的两篇文章。

  • 初学者也能看懂的 Vue3 源码中那些实用的基础工具函数

这篇写了如何学习 JavaScript 基础知识,推荐了很多书籍和学习资料,还有我的一些经验分享。

  • Vue 3.2 发布了,那尤雨溪是怎么发布 Vue.js 的?

参加源码共读的读者反馈,TA 其实还是用着 Vue2。能不能写篇 Vue2 基础工具函数。作为一名知识博主卑微号主,本着学我所学,为我所用,帮助他人的宗旨,于是写上了这篇文章。算是 Vue3 工具函数的姐妹篇。

阅读本文,你将学到:

1
2
3
4
5
js复制代码1. Vue2 源码 shared 模块中的几十个实用工具函数
2. 如何学习源码中优秀代码和思想,投入到自己的项目中
3. 如何学习 JavaScript 基础知识,会推荐很多学习资料
4. 我的一些经验分享
5. 等等
  1. 环境准备

2.1 读开源项目 贡献指南

打开 vue 仓库,
开源项目一般都能在 README.md 或者 .github/contributing.md 找到贡献指南。

而贡献指南写了很多关于参与项目开发的信息。比如怎么跑起来,项目目录结构是怎样的。怎么投入开发,需要哪些知识储备等。

我们可以在 项目目录结构 描述中,找到shared模块。

shared: contains utilities shared across the entire codebase.

README.md 和 contributing.md 一般都是英文的。可能会难倒一部分人。其实看不懂,完全可以可以借助划词翻译,整页翻译和谷歌翻译等翻译工具。再把英文加入后续学习计划。

本文就是讲 shared 模块,对应的文件路径是:vue/vue/src/shared。

也可以用github1s访问,速度更快。github1s vue/vue/src/shared

2.2 为了降低文章难度,直接学习打包后的源码

源代码的代码vue/vue/src/shared,使用了Flow 类型,可能不太好理解。

为了降低文章难度,我们直接学习源码仓库中的打包后的 dist/vue.js 14行到379行。

当然,前面可能比较啰嗦。我可以直接讲 3. 工具函数。但通过我上文的介绍,即使是初学者,都能看懂一些开源项目源码,也许就会有一定的成就感。
另外,面试问到被类似的问题或者笔试题时,你说看Vue2源码学到的,面试官绝对对你刮目相看。

  1. 工具函数

打包后的 vue.js 14行到379行,接下来就是解释其中的这些方法。

3.1 emptyObject

1
2
3
4
5
6
7
js复制代码/*!
* Vue.js v2.6.14
* (c) 2014-2021 Evan You
* Released under the MIT License.
*/
/* */
var emptyObject = Object.freeze({});

冻结对象。第一层无法修改。对象中也有判断是否冻结的方法。

1
js复制代码Object.isFrozen(emptyObject); // true

关于对象 API 推荐看之前我的文章 JavaScript 对象所有API解析

还可以看阮一峰老师的ES6 入门书籍 reflect

3.2 isUndef 是否是未定义

1
2
3
4
5
js复制代码// These helpers produce better VM code in JS engines due to their
// explicitness and function inlining.
function isUndef (v) {
return v === undefined || v === null
}

3.3 isDef 是否是已经定义

JavaScript中假值有六个。

1
2
3
4
5
6
js复制代码false
null
undefined
0
'' (空字符串)
NaN

为了判断准确,Vue2 源码中封装了isDef、 isTrue、isFalse函数来准确判断。

见名知意。

1
2
3
js复制代码function isDef (v) {
return v !== undefined && v !== null
}

3.4 isTrue 是否是 true

见名知意。

1
2
3
js复制代码function isTrue (v) {
return v === true
}

3.5 isFalse 是否是 false

见名知意。

1
2
3
js复制代码function isFalse (v) {
return v === false
}

3.6 isPrimitive 判断值是否是原始值

判断是否是字符串、或者数字、或者 symbol、或者布尔值。

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码/**
* Check if value is primitive.
*/
function isPrimitive (value) {
return (
typeof value === 'string' ||
typeof value === 'number' ||
// $flow-disable-line
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}

3.7 isObject 判断是对象

因为 typeof null 是 ‘object’。数组等用这个函数判断也是 true

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码/**
* Quick object check - this is primarily used to tell
* Objects from primitive values when we know the value
* is a JSON-compliant type.
*/
function isObject (obj) {
return obj !== null && typeof obj === 'object'
}

// 例子:
isObject([]) // true
// 有时不需要严格区分数组和对象

3.8 toRawType 转换成原始类型

Object.prototype.toString() 方法返回一个表示该对象的字符串。

mdn

ecma 规范,说明了这些类型。

ecma 规范

ECMAScript5.1 中文版

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码/**
* Get the raw type string of a value, e.g., [object Object].
*/
var _toString = Object.prototype.toString;

function toRawType (value) {
return _toString.call(value).slice(8, -1)
}

// 例子:
toRawType('') // 'String'
toRawType() // 'Undefined'

3.9 isPlainObject 是否是纯对象

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码/**
* Strict object type check. Only returns true
* for plain JavaScript objects.
*/
function isPlainObject (obj) {
return _toString.call(obj) === '[object Object]'
}

// 上文 isObject([]) 也是 true
// 这个就是判断对象是纯对象的方法。
// 例子:
isPlainObject([]) // false
isPlainObject({}) // true

3.10 isRegExp 是否是正则表达式

1
2
3
4
5
6
7
js复制代码function isRegExp (v) {
return _toString.call(v) === '[object RegExp]'
}

// 例子:
// 判断是不是正则表达式
isRegExp(/ruochuan/) // true

3.11 isValidArrayIndex 是否是可用的数组索引值

数组可用的索引值是 0 (‘0’)、1 (‘1’) 、2 (‘2’) …

1
2
3
4
5
6
7
js复制代码/**
* Check if val is a valid array index.
*/
function isValidArrayIndex (val) {
var n = parseFloat(String(val));
return n >= 0 && Math.floor(n) === n && isFinite(val)
}

该全局 isFinite() 函数用来判断被传入的参数值是否为一个有限数值(finite number)。在必要情况下,参数会首先转为一个数值。

isFinite mdn

1
2
3
4
5
6
7
8
js复制代码isFinite(Infinity);  // false
isFinite(NaN); // false
isFinite(-Infinity); // false

isFinite(0); // true
isFinite(2e64); // true, 在更强壮的Number.isFinite(null)中将会得到false

isFinite('0'); // true, 在更强壮的Number.isFinite('0')中将会得到false

3.12 isPromise 判断是否是 promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码function isPromise (val) {
return (
isDef(val) &&
typeof val.then === 'function' &&
typeof val.catch === 'function'
)
}

// 例子:
// 判断是不是Promise对象
const p1 = new Promise(function(resolve, reject){
resolve('若川');
});
isPromise(p1); // true

这里用 isDef 判断其实相对 isObject 来判断 来说有点不严谨。但是够用。

3.13 toString 转字符串

转换成字符串。是数组或者对象并且对象的 toString 方法是 Object.prototype.toString,用 JSON.stringify 转换。

1
2
3
4
5
6
7
8
9
10
js复制代码/**
* Convert a value to a string that is actually rendered.
*/
function toString (val) {
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2)
: String(val)
}

3.14 toNumber 转数字

转换成数字。如果转换失败依旧返回原始字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码/**
* Convert an input value to a number for persistence.
* If the conversion fails, return original string.
*/
function toNumber (val) {
var n = parseFloat(val);
return isNaN(n) ? val : n
}

toNumber('a') // 'a'
toNumber('1') // 1
toNumber('1a') // 1
toNumber('a1') // 'a1'

3.15 makeMap 生成一个 map (对象)

传入一个以逗号分隔的字符串,生成一个 map(键值对),并且返回一个函数检测 key 值在不在这个 map 中。第二个参数是小写选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码/**
* Make a map and return a function for checking if a key
* is in that map.
*/
function makeMap (
str,
expectsLowerCase
) {
var map = Object.create(null);
var list = str.split(',');
for (var i = 0; i < list.length; i++) {
map[list[i]] = true;
}
return expectsLowerCase
? function (val) { return map[val.toLowerCase()]; }
: function (val) { return map[val]; }
}

// Object.create(null) 没有原型链的空对象

3.16 isBuiltInTag 是否是内置的 tag

1
2
3
4
5
6
7
8
9
10
js复制代码/**
* Check if a tag is a built-in tag.
*/
var isBuiltInTag = makeMap('slot,component', true);

// 返回的函数,第二个参数不区分大小写
isBuiltInTag('slot') // true
isBuiltInTag('component') // true
isBuiltInTag('Slot') // true
isBuiltInTag('Component') // true

3.17 isReservedAttribute 是否是保留的属性

1
2
3
4
5
6
7
8
9
10
11
js复制代码/**
* Check if an attribute is a reserved attribute.
*/
var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');

isReservedAttribute('key') // true
isReservedAttribute('ref') // true
isReservedAttribute('slot') // true
isReservedAttribute('slot-scope') // true
isReservedAttribute('is') // true
isReservedAttribute('IS') // undefined

3.18 remove 移除数组中的中一项

1
2
3
4
5
6
7
8
9
10
11
js复制代码/**
* Remove an item from an array.
*/
function remove (arr, item) {
if (arr.length) {
var index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1)
}
}
}

splice 其实是一个很耗性能的方法。删除数组中的一项,其他元素都要移动位置。

引申:axios InterceptorManager 拦截器源码 中,拦截器用数组存储的。但实际移除拦截器时,只是把拦截器置为 null 。而不是用splice移除。最后执行时为 null 的不执行,同样效果。axios 拦截器这个场景下,不得不说为性能做到了很好的考虑。因为拦截器是用户自定义的,理论上可以有无数个,所以做性能考虑是必要的。

看如下 axios 拦截器代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
js复制代码// 代码有删减
// 声明
this.handlers = [];

// 移除
if (this.handlers[id]) {
this.handlers[id] = null;
}

// 执行
if (h !== null) {
fn(h);
}

3.19 hasOwn 检测是否是自己的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
js复制代码/**
* Check whether an object has the property.
*/
var hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
return hasOwnProperty.call(obj, key)
}

// 例子:

// 特别提醒:__proto__ 是浏览器实现的原型写法,后面还会用到
// 现在已经有提供好几个原型相关的API
// Object.getPrototypeOf
// Object.setPrototypeOf
// Object.isPrototypeOf

// .call 则是函数里 this 显示指定以为第一个参数,并执行函数。

hasOwn({__proto__: { a: 1 }}, 'a') // false
hasOwn({ a: undefined }, 'a') // true
hasOwn({}, 'a') // false
hasOwn({}, 'hasOwnProperty') // false
hasOwn({}, 'toString') // false
// 是自己的本身拥有的属性,不是通过原型链向上查找的。

3.20 cached 缓存

利用闭包特性,缓存数据

1
2
3
4
5
6
7
8
9
10
js复制代码/**
* Create a cached version of a pure function.
*/
function cached (fn) {
var cache = Object.create(null);
return (function cachedFn (str) {
var hit = cache[str];
return hit || (cache[str] = fn(str))
})
}

系统学习正则推荐老姚:《JavaScript 正则表达式迷你书》问世了!,看过的都说好。所以本文不会很详细的描述正则相关知识点。

3.21 camelize 连字符转小驼峰

连字符 - 转驼峰 on-click => onClick

1
2
3
4
5
6
7
js复制代码/**
* Camelize a hyphen-delimited string.
*/
var camelizeRE = /-(\w)/g;
var camelize = cached(function (str) {
return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
});

3.22 capitalize 首字母转大写

首字母转大写

1
2
3
4
5
6
js复制代码/**
* Capitalize a string.
*/
var capitalize = cached(function (str) {
return str.charAt(0).toUpperCase() + str.slice(1)
});

3.23 hyphenate 小驼峰转连字符

onClick => on-click

1
2
3
4
5
6
7
js复制代码/**
* Hyphenate a camelCase string.
*/
var hyphenateRE = /\B([A-Z])/g;
var hyphenate = cached(function (str) {
return str.replace(hyphenateRE, '-$1').toLowerCase()
});

3.24 polyfillBind bind 的垫片

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
js复制代码/**
* Simple bind polyfill for environments that do not support it,
* e.g., PhantomJS 1.x. Technically, we don't need this anymore
* since native bind is now performant enough in most browsers.
* But removing it would mean breaking code that was able to run in
* PhantomJS 1.x, so this must be kept for backward compatibility.
*/

/* istanbul ignore next */
function polyfillBind (fn, ctx) {
function boundFn (a) {
var l = arguments.length;
return l
? l > 1
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}

boundFn._length = fn.length;
return boundFn
}

function nativeBind (fn, ctx) {
return fn.bind(ctx)
}

var bind = Function.prototype.bind
? nativeBind
: polyfillBind;

简单来说就是兼容了老版本浏览器不支持原生的 bind 函数。同时兼容写法,对参数的多少做出了判断,使用call和apply实现,据说参数多适合用 apply,少用 call 性能更好。

如果对于call、apply、bind的用法和实现不熟悉,可以查看我在面试官问系列中写的面试官问:能否模拟实现JS的call和apply方法
面试官问:能否模拟实现JS的bind方法

3.25 toArray 把类数组转成真正的数组

把类数组转换成数组,支持从哪个位置开始,默认从 0 开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
js复制代码/**
* Convert an Array-like object to a real Array.
*/
function toArray (list, start) {
start = start || 0;
var i = list.length - start;
var ret = new Array(i);
while (i--) {
ret[i] = list[i + start];
}
return ret
}

// 例子:
function fn(){
var arr1 = toArray(arguments);
console.log(arr1); // [1, 2, 3, 4, 5]
var arr2 = toArray(arguments, 2);
console.log(arr2); // [3, 4, 5]
}
fn(1,2,3,4,5);

3.26 extend 合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js复制代码/**
* Mix properties into target object.
*/
function extend (to, _from) {
for (var key in _from) {
to[key] = _from[key];
}
return to
}

// 例子:
const data = { name: '若川' };
const data2 = extend(data, { mp: '若川视野', name: '是若川啊' });
console.log(data); // { name: "是若川啊", mp: "若川视野" }
console.log(data2); // { name: "是若川啊", mp: "若川视野" }
console.log(data === data2); // true

3.27 toObject 转对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js复制代码/**
* Merge an Array of Objects into a single Object.
*/
function toObject (arr) {
var res = {};
for (var i = 0; i < arr.length; i++) {
if (arr[i]) {
extend(res, arr[i]);
}
}
return res
}

// 数组转对象
toObject(['若川', '若川视野'])
// {0: '若', 1: '川', 2: '视', 3: '野'}

3.28 noop 空函数

1
2
3
4
5
6
7
8
9
js复制代码/* eslint-disable no-unused-vars */
/**
* Perform no operation.
* Stubbing args to make Flow happy without leaving useless transpiled code
* with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
*/
function noop (a, b, c) {}

// 初始化赋值

3.29 no 一直返回 false

1
2
3
4
5
js复制代码/**
* Always return false.
*/
var no = function (a, b, c) { return false; };
/* eslint-enable no-unused-vars */

3.30 identity 返回参数本身

1
2
3
4
js复制代码/**
* Return the same value.
*/
var identity = function (_) { return _; };

3.31 genStaticKeys 生成静态属性

1
2
3
4
5
6
7
8
js复制代码/**
* Generate a string containing static keys from compiler modules.
*/
function genStaticKeys (modules) {
return modules.reduce(function (keys, m) {
return keys.concat(m.staticKeys || [])
}, []).join(',')
}

3.32 looseEqual 宽松相等

由于数组、对象等是引用类型,所以两个内容看起来相等,严格相等都是不相等。

1
2
3
4
js复制代码var a = {};
var b = {};
a === b; // false
a == b; // false

所以该函数是对数组、日期、对象进行递归比对。如果内容完全相等则宽松相等。

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
js复制代码/**
* Check if two values are loosely equal - that is,
* if they are plain objects, do they have the same shape?
*/
function looseEqual (a, b) {
if (a === b) { return true }
var isObjectA = isObject(a);
var isObjectB = isObject(b);
if (isObjectA && isObjectB) {
try {
var isArrayA = Array.isArray(a);
var isArrayB = Array.isArray(b);
if (isArrayA && isArrayB) {
return a.length === b.length && a.every(function (e, i) {
return looseEqual(e, b[i])
})
} else if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime()
} else if (!isArrayA && !isArrayB) {
var keysA = Object.keys(a);
var keysB = Object.keys(b);
return keysA.length === keysB.length && keysA.every(function (key) {
return looseEqual(a[key], b[key])
})
} else {
/* istanbul ignore next */
return false
}
} catch (e) {
/* istanbul ignore next */
return false
}
} else if (!isObjectA && !isObjectB) {
return String(a) === String(b)
} else {
return false
}
}

3.33 looseIndexOf 宽松的 indexOf

该函数实现的是宽松相等。原生的 indexOf 是严格相等。

1
2
3
4
5
6
7
8
9
10
11
js复制代码/**
* Return the first index at which a loosely equal value can be
* found in the array (if value is a plain object, the array must
* contain an object of the same shape), or -1 if it is not present.
*/
function looseIndexOf (arr, val) {
for (var i = 0; i < arr.length; i++) {
if (looseEqual(arr[i], val)) { return i }
}
return -1
}

3.34 once 确保函数只执行一次

利用闭包特性,存储状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
js复制代码/**
* Ensure a function is called only once.
*/
function once (fn) {
var called = false;
return function () {
if (!called) {
called = true;
fn.apply(this, arguments);
}
}
}


const fn1 = once(function(){
console.log('哎嘿,无论你怎么调用,我只执行一次');
});

fn1(); // '哎嘿,无论你怎么调用,我只执行一次'
fn1(); // 不输出
fn1(); // 不输出
fn1(); // 不输出

3.35 生命周期等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
js复制代码var SSR_ATTR = 'data-server-rendered';

var ASSET_TYPES = [
'component',
'directive',
'filter'
];

[Vue 生命周期](https://cn.vuejs.org/v2/api/#%E9%80%89%E9%A1%B9-%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E9%92%A9%E5%AD%90)

var LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
];
  1. 最后推荐一些文章和书籍

这部分和Vue3工具函数文章一样,值得推荐,所以复制到这里。

先推荐我认为不错的JavaScript API的几篇文章和几本值得读的书。

JavaScript字符串所有API全解密

【深度长文】JavaScript数组所有API全解密

正则表达式前端使用手册

老姚:《JavaScript 正则表达式迷你书》问世了!

老姚浅谈:怎么学JavaScript?

JavaScript 对象所有API解析 lxchuan12.gitee.io/js-object-a…

MDN JavaScript

《JavaScript高级程序设计》第4版

《JavaScript 权威指南》第7版

《JavaScript面向对象编程2》 面向对象讲的很详细。

阮一峰老师:《ES6 入门教程》

《现代 JavaScript 教程》

《你不知道的JavaScript》上中卷

《JavaScript 设计模式与开发实践》

我也是从小白看不懂书经历过来的。到现在写文章分享。

我看书的方法:多本书同时看,看相同类似的章节,比如函数。看完这本可能没懂,看下一本,几本书看下来基本就懂了,一遍没看懂,再看几遍,可以避免遗忘,巩固相关章节。当然,刚开始看书很难受,看不进。这些书大部分在微信读书都有,如果习惯看纸质书,那可以买来看。

这时可以看些视频和动手练习一些简单的项目。

比如:可以自己注册一个github账号,分章节小节,抄写书中的代码,提交到github,练习了才会更有感觉。

再比如 freeCodeCamp 中文在线学习网站 网站。看书是系统学习非常好的方法。后来我就是看源码较多,写文章分享出来给大家。

  1. 总结

本文通过查看 Vue2 源码中 shared 模块打包后的 dist/vue.js 14行到379行。源码也不是那么难,至少很多能看懂,比如工具函数。难可能是难在:不知道应用场景。

Vue2 工具函数命名很规范,比如:is 判断,to 转换,has 是否有,让开发者一眼就能看出函数语意。

这些函数也非常单一,基本一个函数只做一件事。

建议读者朋友把不熟悉的函数,动手写写,有助于巩固基础知识,查漏补缺。

最后可以持续关注我@若川。欢迎加我微信 ruochuan12 交流,参与 源码共读 活动,大家一起学习源码,共同进步。

关于 && 交流群

最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 参与,长期交流学习。

作者:常以若川为名混迹于江湖。欢迎加我微信ruochuan12。前端路上 | 所知甚少,唯善学。

关注公众号若川视野,每周一起学源码,学会看源码,进阶高级前端。

若川的博客

segmentfault若川视野专栏,开通了若川视野专栏,欢迎关注~

掘金专栏,欢迎关注~

知乎若川视野专栏,开通了若川视野专栏,欢迎关注~

github blog,求个star^_^~

本文转载自: 掘金

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

1…457458459…956

开发者博客

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