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

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


  • 首页

  • 归档

  • 搜索

Koa如何链接mongodb数据库

发表于 2021-08-13
先建立一个koa的文件夹
1
csharp复制代码npm init -y    // 初始化项目
安装所需的插件
1
2
3
4
5
arduino复制代码npm install koa -S   // 安装 koa
npm install koa-router -S // 安装 koa-router
npm install mongodb // 安装 mongodb
npm install mongoose // 安装 mongoose
npm install koa-body // 安装 koa-body

刚刚面那种是分开安装的,你也可以一次性安装完 :

1
css复制代码npm i koa  koa-router mongodb mongoose koa-body -S

安装完后如下:image.png

一、开始写入

建立app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码const Koa = require('koa');
const Router = require('koa-router');
const koaBody = require("koa-body"); // 进入 koa-body
//koa、router实例化
const app = new Koa();
const router = new Router();
app.use(koaBody());
router.get('/',ctx =>{
ctx.body = 'hello world'
})
app.use(router.routes()).use(router.allowedMethods());
//建立端口号3000,不一定非要是3000 有点端口号会被占用
app.listen(3000,()=>{
console.log("服务已开启")
})

在 package.json 里面添加

1
json复制代码"dev": "nodemon app.js"

运行 npm run dev 效果如下:

image.png

访问页面,这是页面是:
若是如下图所示,你的项目已经启动了image.png

开始建立目录 效果如下: (个人习惯提前建立好需要的文件和文件夹)

不一定全要 user.js 可以换成自己喜欢的名字。但是引入的时候也要注意

image.png

二、连接远程免费的mongodb数据库

提前在 vue.config.js 配置 字符串里面的东西是在 MongoDBCompass 上复制下来的 格式 必须是

image.png)image.png在 app.jss 上进行添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ini复制代码const Koa = require("koa");
const Router = require("koa-router");
const koaBody = require("koa-body");
+ const mongoose = require("mongoose");
+ const { connectionStr } = require("./vue.config.js");
+ mongoose.connect(connectionStr, (err) => {
+ if (err) console.log("mongonDB连接失败了");
+ console.log("mongonDB连接成功了");
+ });
//koa实例化
const app = new Koa();
const router = new Router();
// 总路由添加前缀/api,总地址变为http://localhost:3000/api
router.prefix("/api");
router.get("/", async (ctx) => {
ctx.body = "hello World";
});
app.use(koaBody());
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000, () => {
console.log("服务启动了");
});

你会看到:

这样就已经拦截成功了image.png)(我直接拆分写了)

一、在 routes/user.js 文件夹里写

1
2
3
4
5
6
7
8
9
10
11
arduino复制代码const Router = require('koa-router');
const router = new Router();
// 从 controllers/user引入的
const { find, findById, create,
update, delete: del, } = require('../controllers/user');
router.get('/', find); // 总数据
router.post('/', create); // 添加
router.get('/:id', findById);// 查找
router.patch('/:id', update); // 修改
router.delete('/:id', del); // 删除
module.exports = router;

二、在 controllers/user.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
ini复制代码const User = require("../models/users.js");

class UsersCtl {
async find(ctx) {
ctx.body = await User.find();
}
async findById(ctx) {
const user = await User.findById(ctx.params.id);
if (!user) {
ctx.throw(404, "用户不存在");
}
ctx.body = user;
}
async create(ctx) {
const user = await new User(ctx.request.body).save();
ctx.body = user;
}
async update(ctx) {
const user = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body);
const users = await User.findById(ctx.params.id);
if (!users) {
ctx.throw(404, "用户不存在");
}
ctx.body = users;
}
async delete(ctx) {
const user = await User.findByIdAndRemove(ctx.params.id);
if (!user) {
ctx.throw(404, "用户不存在");
}
ctx.status = 204;
}
}
module.exports = new UsersCtl();

三、在 models/user.js 文件夹里写

1
2
3
4
5
6
7
8
9
10
11
12
arduino复制代码const mongoose = require("mongoose");
const { Schema, model } = mongoose;
// 里面写一些需要的字段
// required: true 为必填项
// 是数字时 default: 0 默认为0
// 是字符串时 default: "" 默认为空
const userSchema = new Schema({
name: { type: String, required: true },
age: { type: Number, default: 0 },
state:{ type: String, default: "" }
});
module.exports = model("User", userSchema, "users");

四、最后别忘了在 全局 app.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
ini复制代码const Koa = require("koa");
const Router = require("koa-router");
const koaBody = require("koa-body");
const mongoose = require("mongoose");
+ const user = require("./routes/user.js");
const { connectionStr } = require("./vue.config.js");
mongoose.connect(connectionStr, (err) => {

if (err) console.log("mongonDB连接失败了");
console.log("mongonDB连接成功了");
});
//koa实例化
const app = new Koa();
const router = new Router();
// 总路由添加前缀/api,总地址变为http://localhost:3000/api
router.prefix("/api");

router.get("/", async (ctx) => {
ctx.body = "hello World";
});
app.use(koaBody());

// 子路由添加前缀/users,最后访问地址变为http://localhost:3000/api/users/user
+ router.use("/users", user.routes());
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000, () => {
console.log("服务启动了");
});

五、还剩最后一个文件夹没有说,那就是data文件夹 这里面是放数据的 默认为空数组 [] 不然会报错。

我建议大家在 Postman 上测试一下接口,看看能不能正常的使用,有错及时更改。 请求的方式不同 返回的结果就不同 //

我的添加效果:image.png

查找:他是直接子啊后面拼接的 id

image.png

修改:

image.png

剩下的就不一一展示了

谢谢观看!!!!

本文转载自: 掘金

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

10分钟了解express和koa中间件机制和错误处理机制

发表于 2021-08-12

一、前言

大家可能都知道koa是express核心原班人马写的,那么他们为什么要在express后再造一个koa的轮子呢? 今天就给大家带来一些分析。希望能够起到一个抛砖引玉的作用。

其实,这个题目也可以这么问, express有什么缺点? koa解决了一些express的什么问题?
这也在一些面试题中会这么问。所以,为了实现自己的理想(money), 志同道合的同志们可以随我分析一下了。

我想先从express的一个非常重要的特征开始说起,那就是 中间件。 中间件贯穿了express的始终,我们在express中比较常用到应用级的中间件,比如:

1
2
3
4
5
6
javascript复制代码    const app = require('express')();

app.use((req, res, next) => {
// 做一些事情。。。
next();
})

再比如我们更常用到的路由级中间件。 我为什么要叫它是路由级的呢? 因为它的内部也同样维护着一个next

1
2
3
javascript复制代码    app.get('/', (req, res, next) => {
res.send('something content');
})

这里中间件我不详细展开。 后面有我对中间件的详细解析,欢迎大家围观。

那么我们可以看到,其中会有个关键的next, 它在express内部做的是从栈中获取下一个中间件的关键。

那么重点来了, 我们开始研究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
javascript复制代码    const Express = require('express');
const app = new Express();
const sleep = () => new Promise(resolve => setTimeout(function(){resolve(1)}, 2000));
const port = 8210;
function f1(req, res, next) {
console.log('this is function f1....');
next();
console.log('f1 fn executed done');
}

function f2(req, res, next) {
console.log('this is function f2....');
next();
console.log('f2 fn executed done');
}

async function f3(req, res) {
console.log('f3 send to client');
res.send('Send To Client Done');
}
app.use(f1);
app.use(f2);
app.use(f3);
app.get('/', f3)
app.listen(port, () => console.log(`Example app listening on port ${port}!`))

理想下的返回,和真正的返回,目前是没有问题的。

1
2
3
4
5
javascript复制代码    this is function f1....
this is function f2....
f3 send to client
f1 fn executed done
f2 fn executed done

好的,那么再继续下一个例子。 在下一个例子中,其它都是没有变化的,只有一个地方:

1
2
3
4
5
6
javascript复制代码    const sleep = () => new Promise(resolve => setTimeout(function(){resolve()}, 1000))
async function f3(req, res) {
await sleep();
console.log('f3 send to client');
res.send('Send To Client Done');
}

这时你认为的返回值顺序是什么样的呢?

可能会认为跟上面的没有变化,因为我们增加await了,照道理应该等待await执行完了,再去执行下面的代码。 其实结果并不是。返回的结果是:

1
2
3
4
5
javascript复制代码    this is function f1....
this is function f2....
f1 fn executed done
f2 fn executed done
f3 send to client

发生了什么?? 大家可能有点吃惊。但是,如果深入到express的源码中去一探究竟,问题原因也就显而易见了。

具体源码我在这一篇中就不详细分析了,直接说出结论:

因为express中的中间件调用不是Promise 所以就算我们加了async await 也不管用。

那么koa中是怎么使用的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
javascript复制代码const Koa = require('koa');
const app = new Koa();
const sleep = () => new Promise(resolve => setTimeout(function(){resolve()}, 1000))
app.use(async (ctx, next) => {
console.log('middleware 1 start');
await next();
console.log('middleware 1 end');
});
app.use(async (ctx, next) => {
await sleep();
console.log('middleware 2 start');
await next();
console.log('middleware 2 end');
});

app.use(async (ctx, next) => {
console.log('middleware 3 start')
ctx.body = 'test middleware executed';
})

不出所料, 实现的顺序是:

1
2
3
4
5
html复制代码middleware 1 start
middleware 2 start
middleware 3 start
middleware 2 end
middleware 1 end

原因是: koa 内部使用了Promise,所以能够控制顺序的执行。

综合上面的例子,我们知道了express中中间件使用的时候,如果不清楚原理,是容易踩坑的。
而koa通过使用async 和 await next() 实现洋葱模型,即:通过next,到下一个中间件,只要下面的中间件执行完成后,才一层层的再执行上面的中间件,直到全部完成。

3.错误逻辑捕获

3.1 express的错误捕获逻辑

同样,先看express在错误逻辑的捕获上有什么特点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
javascript复制代码app.use((req, res, next) => {
// c 没有定义
const a = c;
});

// 错误处理中间件
app.use((err, req, res, next) => {
if(error) {
console.log(err.message);
}
next()
})

process.on("uncaughtException", (err) => {
console.log("uncaughtException message is::", err);
})

再看一个异步的处理:

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
javascript复制代码app.use((req, res, next) => {
// c 没有定义
try {
setTimeout(() => {
const a = c;
next()
}, 0)
} catch(e) {
console.log('异步错误,能catch到么??')
}


});

app.use((err, req, res, next) => {
if(error) {
console.log('这里会执行么??', err.message);
}
next()
})


process.on("uncaughtException", (err) => {
console.log("uncaughtException message is::", err);
})

可以先猜一下同步和异步的会不会有所区别?

答案是: 有很大的区别!!

具体分开来看:

  • 同步的时候, 不会触发 uncaughtException, 而进入了错误处理的中间件。
  • 异步的时候,不会触发错误处理中间件, 而会触发 uncaughtException

这中间发生了什么?

3-1、同步逻辑错误获取的底层逻辑

逻辑是: express内部对同步发生的错误进行了拦截,所以,不会传到负责兜底的node事件 uncaughtException ,如果发生了错误,则直接绕过其它中间件,进入错误处理中间件。
那么,这里会有一个很容易被忽略的点, 那就是,即使没有错误处理中间件做兜底,也不会进入node的
uncaughtException, 这时, 会直接报 500错误。

3-2 异步逻辑错误获取的底层逻辑

还是因为express的实现并没有把Promise考虑进去, 它的中间件执行是同步顺序执行的。 所以如果有异步的,那么错误处理中间件实际是兜不住的,所以,express对这种中间件中的异步处理错误无能为力。

从上面的异步触发例子来看, 除了错误处理中间件没有触发,我们当中的try catch也没有触发。这是一个大家可能都会踩到的坑。 这里其实是与javascript的运行机制相关了。具体原因见本篇 # 异步队列进行try catch时的问题

所以要想去catch 当前的错误,那么就需要用 async await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
javascript复制代码app.use(async (req, res, next) => {
try {
await (() => new Promise((resolve, reject) => {
http.get('http://www.example.com/testapi/123', res => {
reject('假设错误了');
}).on('error', (e) => {
throw new Error(e);
})
}))();
} catch(e) {
console.log('异步错误,能catch到么??')
}


});

这样,我们的catch不仅可以获取到, uncaughtException也可以获取到。

3.2 koa的错误获取逻辑

总体上是跟express差不多,因为js的底层处理还是一致的。但还是使用上有所差异。

上面也提过洋葱模型,特点是最开始的中间件,在最后才执行完毕,所以,在koa上,可以把错误处理中间件放到中间件逻辑最前面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
javascript复制代码const http = require('http');
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next)=>{
try {
await next();
} catch (error) {
// 响应用户
ctx.status = 500;
ctx.body = '进入默认错误中间件';
// ctx.app.emit('error', error); // 触发应用层级错误事件
}
});

app.use(async (ctx, next) => {
await (() => new Promise((resolve, reject) => {
http.get('http://www.example.com/testapi/123', res => {
reject('假设错误了');
}).on('error', (e) => {
throw new Error(e);
})
}))();
await next();
})

上面的代码, reject出的错误信息,会被最上面的错误处理中间件捕获。

总结来说,js的底层机制是一样的, 只是使用方法和细节点上不一样,大家在用的时候注意一下,应该就能很快掌握啦~~

本文转载自: 掘金

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

庖丁解牛:图解redis几种常见数据结构 string的替身

发表于 2021-08-12

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

string的替身-sds

内存申请和释放

我们知道内存都是一小块一小块拼在一起的,当我们要给一个变量赋值的时候,得先申请一块容的下变量的内存。

image.png
假设我们现在要var db=redis,前提我们得先malloc(5)个字节的空间,当我们申请到了5个空间后,我们就可以给db赋值了。

image.png
现在情况有变,我要把db=redis 改成 db=memcache,这时我们可以发现还需要3个空间,所以我们只要申请3个空间?计算机可没这么智能,这时我还是得申请8个空间malloc(8)。

image.png
当我们的db变量变成了memcache后,还得回收原来redis空间。

image.png

总结:一整套下来,我们大概的流程是 先malloc(5)给redis,然后malloc(8)给memcache,最后还要free(5)回收redis,总共两次申请内存。当我们的变量变了n次,那么对应的就要申请n次 同时释放n-1次

登场

由于上面的流程导致不停的malloc和free,于是redis的开发者发明了sds,当我们执行 set key value的时候, key对应一个sds结构, value也对应一个sds结构。它的结构大致如下:

1
2
3
4
5
c复制代码struct sdshdr {
int len;
int free;
char buf[];
};

image.png

  • free:sds还剩多少空间
  • len:字符串长度
  • buf:真正存储的值
    看个例子:

多分配空间

image.png
假设现在有个key=redis,且此时它正好用完空间free=0,len=5。

每个字符串的结尾都是\0结束的,这样就知道读到哪该停止了,所以sds真正的占用空间,是字符串占用的空间加上1个字节。
现在执行set key memcache,我们知道key已经没有空间了,于是要去申请更多的内存,但是这里并不是只要申请8个字节的空间,而是申请16个字节的空间,并且此时free=8 和 len=8。这就是redis sds的策略,当空间不够时,申请空间时会是扩容后的2倍,但是当扩容后的空间大于1M后,那么会固定过申请1M的额外空间,这一点是需要注意的。

现在又执行set key memcacheGood,发现free还有8个字节的空间,说明空间够用,那么就不用去申请空间了,sds的好处就体现出来了。

保留多余空间

image.png
当memcacheGood又改成memcache时,多出来的4个字节,redis也不会把它还回去,而是会保存起来。

总结

redis sds就是通过这种冗余空间来减少内存的申请和释放过程,从而提升速度,但是缺点就是耗内存。

压缩列表-ziplist

我们知道对于一个数组来说,它们在内存中是连续的,这种设计可以很好的利用cpu缓存,访问快速。但是数组有一个缺点:每个元素的大小都是相同的,即使一些元素只需要很小的空间。

image.png
例如1,2,3,4其实用一个字节int8表示就行了,但是999很明显就不够用了,至少int16。于是因为999,1,2,3,4也得用int16类型,这样就造成了空间浪费。

但是如果每个元素都用最短长度,那么cpu就不知道怎么读了(每次读几个字节?):
image.png
这样好办,我们可以为每个元素加一个长度,这样就知道每次读取多少长度了。

image.png
于是redis的压缩列表(ziplist)出现了。压缩列表是一种为节约内存而开发的顺序型数据结构,压缩列表被用作列表键和哈希键的底层实现之一,压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值,当列表或者哈希的元素数量不多且元素是小整数值或者短字符串,那么底层就会用ziplist来存储。ziplist结构如下:

image.png
其中每个entry都是由previous_entry_length、encoding、content三个组成。

  • previous_entry_length:前一个节点的长度,可以是1个字节或者5个字节,如果前一个节点长度小于254字节,就为1,否则为5。通过previous_entry_length可以知道前一个节点的地址(当前地址减去previous_entry_length)。
  • encoding:节点的encoding属性记录了节点的content属性所保存数据的类型以及长度。
  1. encoding是一字节、两字节或者五字节长时,值的最高位为00、01或者10的是字节数组编码:这种编码表示节点的content属性保存着字节数组,数组的长度由编码除去最高两位之后的其他位记录;
  2. encoding是一字节长时,值的最高位以11开头的是整数编码:这种编码表示节点的content属性保存着整数值,整数值的类型和长度由编码除去最高两位之后的其他位记录。
  • content:节点的值。

image.png

  1. 高位00表示 content是一个字节数据,后6位010011等于11,表示content的长度
  2. 11表示是个数字

总结

压缩列表是一种为节约内存而开发的顺序型数据结构,压缩列表被用作列表键和哈希键的底层实现之一,压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值,当列表或者哈希的元素数量不多且元素是小整数值或者短字符串底层就会使用ziplist。

hash表

在redis中不仅仅是数据类型为hash的才用到hash结构,redis本身所有的k、v就是一个大hash。例如我们经常用的set key value,key就是hash的键,value就是hash的值。

image.png
当添加一个新key的时候,会根据hash函数先算出应该落在哪个桶中,当发现要存放桶中已经有元素了,那么这就是hash冲突,对于hash冲突,redis也是用链表的方式来解决的,但是redis解决hash冲突的链表并不存在指向表尾的指针,那么如果将新加的元素添加在表尾,获取它的时间复杂度将是O(N),一般新添加的元素在接下来将会被访问的概率还是比较大的,于是对于冲突,新加的元素会被添加到表头。

当hash表元素越来越多,会出现冲突越来越多,那么查询效率会降低。当hash表元素越来越少,会出现hash表中有的桶没数据,造成浪费。这就涉及到hash表的扩容和缩容。

image.png

  1. 0号桶元素太多,这样当查找kn的时候,时间复杂度将趋近O(N)
  2. 0和2号桶是空桶,那么它们就是多余的
    对redis本身来说,其实它在一开始就有两个hash表(h1和h2),只不过只有h1是对外工作的,h2是随时准备rehash的。在没有发生rehash的时候,h2是空的。当发生rehash的时候,h2的空间大小取决于h1中键值对的数量。

rehash的依赖:负载因子=哈希表已保存节点数量/哈希表大小

  • 扩容:当服务此时没有执行bgsave或bgrewriteaof时,负载因子大于等于1,就扩容。当服务此时正在进行bgsave或bgrewriteaof时,负载因子大于等于5,就扩容。且h2的大小等于第一个大于等于h1键值对数量2倍的2^n。
  • 缩容:当负载因子小于0.1时就开始缩容,且h2的大小等于第一个大于等于h1键值对数量的2^n。
  • 触发时机:serverCron周期性检测或每次新增k、v的时候,重新计算将h1的键值对慢慢迁移到h2上,当迁移完毕后,h1就是空的了,这时会把h2变成h1,h1变成h2。*

为啥执行bgsave或bgrewriteaof的时候,要提高负载因子?
我们知道bgsave或bgrewriteaof本身是一个耗时的工作,不能卡主线程,所以redis的做法是fork一个子进程来做,子进程这时不是复制一份新的数据内存,而是和父进程共享一份内存,这就是现代操作系统支持的cow(copy-on-write)机制,这种机制的好处就是节约内存,既然大家都一样的,为什么要复制内存?杜绝浪费。但是当主进程在fork后,发生了写的操作,这时候子进程该怎么办?复制整块内存?当然也不是,计算机的内存也是页式存储,内存是由一块一块的页组成,当主进程发生了写的时候,我们只需要把变更的那一页复制一下,其余页不变是不是就可以了,这样可以达到节约内存的目的。cow主要依赖页异常中断来做的,当父进程fork子进程后,会把所有的页设置成只读,那么当新的写到来时就会触发页异常中断,这时候就会对这个页进行单独的复制。hash扩容的时候,肯定是会发生写的,这样就会造成大量的页复制,所以要提高负载因子,尽量不同时操作。

渐进式的rehash
当redis的k、v比较少的时候,可能一次性rehash没什么问题。但是当redis包含大量的k、v时,cpu的计算量就上去了,那么此时一次性rehash是不现实的。为了避免rehash对服务本身造成影响,rehash并不是一次性完成的而是多次的执行rehash的。

  • hash的更新、查找、删除可能会在两个hash表中都执行一次,比如查找的时候,先查找h1,如果没找到,那么会接着找h2。对于新增的操作,会直接在h2上操作。
  • serverCron执行的时候,每次会给永久的kv分配1ms的时间来rehash,给带过期时间的kv也分配1ms的时间来rehash。
  • rehash过程移动的是指针,所以移动1kb和1mb的value是没什么区别的*

image.png

总结

hash是redis中用的比较多的数据结构,渐进式rehash是redis的一种高性能体现。

跳跃表

redis的有序集合zset,在某些场景下非常好用,比如学生成绩排序,最近活跃用户等等。它的底层实现就是跳跃表,跳跃表的好处就是可以加速访问某些节点。

image.png

  1. 当没有跳跃表的时候,我们要访问7的话,必须是1->2->3->4->5->6->7,一共7步
  2. 当我们在1、4、7上再加一层,那么访问7的话,可以是1->4->7,一共3步
    跳跃表的实现就是和上图的2一样的,每个节点有不同的层,通过层来加速访问其他节点,实现了跳跃。层越高,理论访问的越快,但是占用的空间越大,典型的空间换时间。

跳跃表的特点

image.png
我们看看跳跃表是如何找到20这个节点的,首先从最高层开始检索,20大于1继续向后找,20大于12,但是20小于38,于是从12往下找,发现右边还是38,于是12接着往下找,发现右边就是20,结束。

  • 每个节点至少都有1层
  • 每层都是一个有序链表
  • 最底层的有序链表保存所有数据
  • 如果一个节点出现在第x层,那么x-1层也会出现
  • 每个节点包含两个指针,一个指向右边,一个指向下边

redis的跳跃表
我们来看看redis的zset结构:
image.png

  1. header:指向表头节点,redis zset在初始化的时候会创建一个层数为 32,分数为 0,没有 value值的跳跃表头节点
  2. tail指向表尾节点
  3. level:层高,除表头节点外,最高的那一层
  4. length:数量,除表头节点外,节点的数量
  5. 跨度:可以用于表示一个节点在集合中的排位
  6. 后退指针:每个节点都有一个后退指针
  7. 分值:即权重,决定排序
  8. 成员:指向每个节点对象的指针,对象保存的是一个SDS值

总结

每当新加一个节点的时候,redis会随机一个层高。相比链表查询的时间O(N),跳跃表最坏的时间复杂度是O(N),支持平均的时间复杂度O(logN)。

本文转载自: 掘金

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

AES的256带偏移量、128不带偏移量的加解密算法(PKC

发表于 2021-08-12

AES的加解密:

AES 256加密支持

  • java中的AES 256算法遇到 Illegal key size or default parameters错的解决办法
    解决方法:

stackoverflow.com/questions/6…
JDK8 jar包下载地址:
www.oracle.com/technetwork…
JDK7 jar包下载地址:
www.oracle.com/technetwork…
JDK6 jar包下载地址:
www.oracle.com/technetwork…

1
2
3
4
5
6
7
8
ini复制代码1.在 jdk安装目录中(%JAVA_HOME%\jre\lib\ext)添加 jar 包 bcprov-jdk16-1.46.jar。
2.在 jdk安装目录下( %JAVA_HOME%\jre\lib\security )修改 java.security 文件,将第74行(我的是在74行)的
security.provider.7=com.sun.security.sasl.Provider 替换为security.provider.7=org.bouncycastle.jce.provider.BouncyCastleProvider
3.替换( %JAVA_HOME%\jre\lib\security )下面的local_policy.jar和US_export_policy.jar
备注:
JDK1.8.0_151无需去官网下载 local_policy.jar US_export_policy.jar这个jar包,只需要修改Java\jdk1.8.0_151\jre\lib\security这目录下的java.security文件配置即可。
从Java 1.8.0_151和1.8.0_152开始,为JVM启用 无限制强度管辖策略 有了一种新的更简单的方法。如果不启用此功能,则不能使用AES-256。
crypto.policy=unlimited
  • 测试代码使用jdk1.8.0_131 ,详细代码如下:

AES 256位 带偏移量 加解密算法

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
java复制代码import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* AES加密工具类:256位 不带偏移量
*
* @author lilinshen
* @date 2021/03/31
*/
public class AesUtils {

//偏移量
public static final String VIPARA = "f%Z4F+qtFh624970";
//编码方式
public static final String CODE_TYPE = "UTF-8";
//填充类型(注:CBC)
public static final String AES_TYPE = "AES/CBC/PKCS7Padding";
//私钥,AES固定格式为128/192/256 bits.即:16/24/32bytes
private static final String AES_KEY = "AG+BwcnekYZy$9f7X#b2zdB93brfFMmz";

/**
* 加密带偏移量
*/
public static String encrypt(String cleartext) {
try {
IvParameterSpec zeroIv = new IvParameterSpec(VIPARA.getBytes());
SecretKeySpec key = new SecretKeySpec(AES_KEY.getBytes(), "AES");
Cipher cipher = Cipher.getInstance(AES_TYPE);
cipher.init(Cipher.ENCRYPT_MODE, key, zeroIv);
byte[] encryptedData = cipher.doFinal(cleartext.getBytes(CODE_TYPE));
return new BASE64Encoder().encode(encryptedData);
} catch (Exception e) {
e.printStackTrace();
return "";
}
}

/**
* 解密带偏移量
*/
public static String decrypt(String encrypted) {
try {
byte[] byteMi = new BASE64Decoder().decodeBuffer(encrypted);
IvParameterSpec zeroIv = new IvParameterSpec(VIPARA.getBytes());
SecretKeySpec key = new SecretKeySpec(AES_KEY.getBytes(), "AES");
Cipher cipher = Cipher.getInstance(AES_TYPE);
cipher.init(Cipher.DECRYPT_MODE, key, zeroIv);
byte[] decryptedData = cipher.doFinal(byteMi);
return new String(decryptedData, CODE_TYPE);
} catch (Exception e) {
e.printStackTrace();
return "";
}
}

/**
* 测试
*
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
String content = "{\"vid\":73160,\"road\":\"default1\",\"timestamp\":1585815671868,\"devicetype\":\"pc\",\"encode_sign\":\"a069831103819c3c76a23fa084747bac\"}";
System.out.println(encryptPy(content));
System.out.println(decryptPy(encryptPy(content)));
}
}

AES 128位 不带偏移量 加解密算法

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
swift复制代码import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* AES加密工具类:128位 不带偏移量
*
* @author lilinshen
* @date 2021/03/31
*/
public class AesUtils {
//编码方式
public static final String CODE_TYPE = "UTF-8";
//填充类型(注:ECB)
public static final String AES_TYPE = "AES/ECB/PKCS7Padding";
//私钥,AES固定格式为128/192/256 bits.即:16/24/32bytes
private static final String AES_KEY = "46cc793c53dc451b";

/**
* 加密不带偏移量
*/
public static String encrypt(String cleartext) {
try {
SecretKeySpec sKey = new SecretKeySpec(AES_KEY.getBytes(), "AES");
Cipher cipher = Cipher.getInstance(AES_TYPE);
cipher.init(Cipher.ENCRYPT_MODE, sKey);
byte[] decrypted = cipher.doFinal(cleartext.getBytes(CODE_TYPE));
return Base64.encodeBase64String(decrypted);
} catch (Exception e) {
e.printStackTrace();
return "";
}
}

/**
* 解密不带偏移量
*/
public static String decrypt(String content) {
try {
byte[] sourceBytes = Base64.decodeBase64(content);
Cipher cipher = Cipher.getInstance(AES_TYPE);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(AES_KEY.getBytes(), "AES"));
byte[] decoded = cipher.doFinal(sourceBytes);
return new String(decoded, CODE_TYPE);
} catch (Exception e) {
e.printStackTrace();
return "";
}
}

/**
* 测试
*
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
String content = "{\"vid\":73160,\"road\":\"default1\",\"timestamp\":1585815671868,\"devicetype\":\"pc\",\"encode_sign\":\"a069831103819c3c76a23fa084747bac\"}";
System.out.println(encrypt(content));
System.out.println(decrypt(encrypt(content)));
}
}

本文转载自: 掘金

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

Window下配置Redis和Elasticsearch

发表于 2021-08-12

Window下Redis和Elasticsearch的配置

(一)Window下Redis的配置

1.Redis的Window最新版下载

下载地址:
github.com/microsoftar…
“.msi”是安装版的redis,需要安装才能用
“.zip”是解压版的redis,解压之后就能直接使用(建议下载这个)

  • 在这里插入图片描述
  • 解压版下载解压之后的目录结构如下图所示:
  • 在这里插入图片描述
2.Redis的客户端工具最新版下载

redis-desktop-manager-2019.5.0.exe

Win 64位
链接:pan.baidu.com/s/1fomRJxf1…
提取码:1vp4

源码编译学习:
kany.me/2019/10/10/…

如果上面链接失效,可以用下面链接下载,都是一样的:
链接:pan.baidu.com/s/1qfybYj7v…
提取码:g78y

  • 下载之后直接安装即可:
  • 在这里插入图片描述
3.Redis的配置说明

1.一般情况下,直接启动redis-server.exe就可以启动redis了。redis的默认配置是不需要连接密码的,绑定的ip是127.0.0.1
对应的配置:bind 127.0.0.1,# requirepass foobared

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

2.如果我们想要配置,可以通过公司内网的ip访问redis,那就需要配置绑定的ip。

  • 在这里插入图片描述

从上图可以看出,我配置了绑定ip192.168.4.124,但是直接启动redis-server.exe的时候,使用客户端功能,还是提示连接不上。

  • 在这里插入图片描述

从这个redis-server.exe启动图,可以看出,直接启动”redis-server.exe”,默认情况下是不会以”redis.windows.conf”这个配置文件启动的。所以导致配置不生效。

那这样的话,就需要在启动redis的时候,指定redis的配置文件。

  • 在这里插入图片描述

这样的话,就可以通过ip访问到redis了。

(二)Window下Elasticsearch的配置

1.Elasticsearch的Window最新版下载

下载链接:(可以选择自己需要的版本)
www.elastic.co/cn/download…

本人是选择了一个Elasticsearch5.x的版本。下载解压后的文件目录结构如下图:

  • 在这里插入图片描述
2.Elasticsearch的配置说明

一般ElasticSearch的默认”JVM堆大小”是2g,比较大,所以这里可以设置小一点,本人设置为700M
修改文件:elasticsearch-5.6.10\config\jvm.options

  • 在这里插入图片描述

直接启动:elasticsearch-5.6.10\bin\elasticsearch.bat文件即可。

  • 在这里插入图片描述

在浏览器输入:localhost:9200验证,可以显示如下图,表示启动成功了。

  • 在这里插入图片描述

如果想配置可以通过公司内网ip访问到Elasticsearch,需要配置network配置。
修改文件:elasticsearch-5.6.10\config\elasticsearch.yml
network.host修改为”0.0.0.0”

  • 在这里插入图片描述

启动之后。通过ip和localhost都可以访问到Elasticsearch的9200端口。

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

本文转载自: 掘金

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

加密、摘要、签名、证书,一次说明白! 前言 目录 1 概述

发表于 2021-08-12

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 GitHub · Android-NoteBook 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式 & 入群方式在 GitHub)

前言

  • 数据安全传输,是一个非常宽泛的话题。HTTPS、文件签名、应用签名等场景背后,其实解决的就是如何安全地传输数据的问题;
  • 在这篇文章里,我将带你理解 数据安全传输的基本模型,以及加密、摘要、签名和数字证书的概念与作用。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

目录


  1. 概述

1.1 CIA 三要素

在讨论今天的主题之前,有必要先搞清楚到底什么是安全?在计算机领域,所谓的 “安全” 其实是指 “信息安全”,它的基本含义可以概括为三个要素,简称 CIA 三要素:

  • 1、机密性(Confidentiality): 指确保数据在传输和存储过程中的私密性。主要的手段是加密、权限管理和敏感信息脱敏;
  • 2、完整性(Integrity): 指确保数据内容完成,没有被篡改。主要手段是数字签名;
  • 3、可用性(Avaliability): 指确保服务保持可用状态。例如能够承受 Dos 等网络攻击。

1.2 非安全信道的风险

数据需要通过信道进行传输,狭义上的信道是指报纸、有线网络、无线电波等通信媒介,而广义上说,信道可以理解为数据从分发到接收的整个过程。在非安全信道中,主要会面临以下三个安全风险:

  • 1、窃听风险: 现代计算机网络建立在 TCP/IP 协议族提供传输能力上,数据在传输线路上的每个环节都可能被窃听,从而导致敏感数据泄露;
  • 2、篡改风险: 数据在传输过程中可能被篡改,例如中间人攻击。攻击者可以和通信双方分别建立独立的连接,使得通信双方误以为它们正在进行一个私密连接,但察觉不到数据被篡改;
  • 3、伪装风险: 攻击者可以伪装成合法的身份。

1.3 如何实现传输安全?

我们今天的主题 “加密 + 签名 + 证书”,本质上就是在非安全信道中实现数据安全传输的解决方案。要实现数据安全传输,其实就是要高效地解决非安全信道中的三个风险:

  • 1、加密 —— 防窃听 : 将明文转换为密文,只有期望的接收方有能力将密文解密为明文,即使密文被攻击者窃取也无法理解数据的内容;
  • 2、验证完整性 —— 防止篡改: 对原始数据计算摘要,并将数据和摘要一起交付给通信对方。接收方收到后也对数据计算摘要,并比较是否和接受的摘要一致,借此判断接收的数据是否被篡改。不过,因为收到的摘要也可能被篡改,所以需要使用更安全的手段:数字签名;
  • 3、认证数据来源 —— 防止伪装: 数字签名能够验证数据完整性,同时也能认证数据来源,防止伪装。

  1. 加密 —— 防窃听

2.1 什么是加密?

加密(Encryption)是将明文(Plaintext)转换为密文(Ciphertext)的过程,只有期望的接收方有能力将密文解密为明文,即使密文被攻击者窃取也无法理解数据的内容。

在古典密码时期,数据保密性取决于算法的保密性,一旦加密算法被破解或泄露,就失去了数据的安全性。而进入现代密码时期,柯克霍夫原则成为加密系统的基本设计原则:数据的安全性基于密钥而不是算法的保密。

根据柯克霍夫原则,现代的保密通信模型是基于密钥的保密模型模型。在这个模型中,加密和解密使用相同密钥,就是 对称加密密码体制;反之,加密和解密使用是不同密钥,就是 非对称密码体制。

2.2 对称加密和非对称加密的区别?

  • 1、密钥管理: 对称加密算法中需要将密钥发送给通信对方,存在密钥泄漏风险;非对称加密公钥是公开的,私钥是保密的,防止了私钥外传;
  • 2、密钥功能: 公钥加密的数据,只可使用私钥对其解密。反之,私钥加密的数据,只可使用公钥对其解密(注意:公钥加密的数据无法使用公钥解密,因为公钥是公开的,如果公钥可以解密的话,就失去了加密的安全性);
  • 3、计算性能: 非对称加密算法的计算效率低,因此实际中往往采用两种算法结合的复合算法:先使用非对称加密建立安全信道传输对称密钥,再使用该密钥进行对称加密;
  • 4、认证功能: 非对称加密算法中,私钥只有一方持有,具备认证性和抗抵赖性(第 3 节 数字签名算法 应用了此特性)。

考虑到性能的因素,在 HTTPS 协议中采用的是 “对称加密” 和 “非对称加密” 结合的 “混合加密” 方案:在建立通信时,采用非对称加密的方式来协商 “会话密钥”,在通信过程中基于该密钥进行对称加密通信。

2.3 数据加密标准 —— DES

1977 年,数据加密标准(Data Encryption Standard, DES)成为美国联邦信息处理标准,并逐渐成为事实标准,很多主流的对称加密算法都是从 DES 算法发展过来的。

DES 算法的主要缺点是加密强度和计算性能较差

  • 1、密钥长度太短: DES 算法密钥长度只有 56 bit,理论最大加密长度为 256。随着计算机算力的提高,用穷举法可以在较短时间破解密钥;
  • 2、不能对抗差分和线性密码分析;
  • 3、计算性能较差: 增加 DES 密钥长度,加解密的计算开销呈指数增长。

2.4 高级加密标准 —— AES

高级加密标准(Advanced Encryption Standard, AES),又称 Rijndael [rain-dahl]加密法,是目前最流行的对称加密算法之一。

相对于 DES 算法,AES 算法的主要优点如下:

  • 1、密钥长度更大: 密钥长度最小为 12 bit,最大为 256 bit。用穷举法难以破解;
  • 2、使用 WTS 设计策略,可对抗差分和线性密码分析;
  • 3、计算性能高: 计算和内存开销低,适用于受限设备。

2.5 RSA 算法

1977 年,麻省理工学院的三位教授 Rivest、Shamir 和 Adleman 共同提出了 RSA 加密算法,其中 RSA 分别是他们姓氏的首字母。RSA 是经典的非对称加密算法,同时也是经典的数字签名算法。

RSA 算法的安全性依赖于一个数学难题 —— “大数因式分解”:两个大素数相乘非常容易,但对一个极大整数做因式分解的复杂度极高。如果存在某种快速因素分解的算法,那么 RSA 算法的可靠性将会大大折扣。RSA 算法存在一个系统性风险:“不支持前向加密”。在 RSA 算法中,服务端公钥是相对固定的,一但服务端私钥被破解,则之前所有发送过得加密数据都会被破解。

关于 RSA 算法的原理解析,可参考:浅析 RSA 算法。

2.6 DH 算法

DH 算法的安全性依赖于一个数学难题 —— “离散对数”:已知对数计算出真数非常简单,但已知真数求对数的复杂度极高。 如果存在某种求对数的算法,那么 DH 算法的可靠性将会大大折扣。

目前,DH 算法有多种实现,主要区别如下:

  • static DH 算法:一方的私钥是静态的(通常是服务器私钥固定),不具备前向安全性;
  • DHE 算法:双方的私钥都在密钥交换节点随机生成,具备前向安全性;
  • ECDHE 算法:利用 ECC 椭圆曲线特性,可以用更少的计算量计算公钥和私钥。

关于 DH 算法的原理解析,可参考:这 HTTPS,真滴牛逼!

2.7 安全系统中为什么要使用随机数?

在 RSA 算法生成密钥对的过程中,我们需要随机生成两个大素数。事实上,除了在 RSA 算法中,很多安全系统中都需要一个随机数,为什么呢?—— 关键在于随机数不可预测性,可以提高破解和报文重放攻击难度。

2.8 计算机如何生成随机数?

随机数是计算机安全领域中非常重要的一个点,很多场景中都需要一个随机数来生成随机事件,比如密钥的生成、文件名、sessionId/orderId/token 等。现代的随机数生成模型依然采用的是 1946 年冯·诺依曼设计的随机数模型:

1、输入任意一个数作为 “种子”,通过随机数算法得到一个随机数;
2、将生成的随机数作为新的种子,代入下一轮计算;
3、重复 1、2 步骤,就可以生成多个具有统计意义的随机数。

然而,通过这种模型生成的随机数并不是绝对随机的。只要取样范围足够大,随机结果一定会陷入循环,因此这种模型生成的随机数只能称为 “伪随机数”,而随机结果陷入循环的周期称为 “随机周期”。

要得到真正意义的随机数,需要硬件层面支持。1999 年 Intel 在其 i810 芯片组上集成了世界上第一款真随机数生成器,它的方案是将电路的热噪声(分子的不规则运动)作为数据来源,缺点是效率太低,因此目前计算机中采用的随机数依旧是软件实现的伪随机数。虽然软件无法做到真随机,但可以提高生成器的随机性。比如采用更强壮的随机算法(Java#SecurityRandom)、采用更复杂的种子(系统时间、鼠标位置、网络速度、硬盘读写速度)、扩大随机数的取值范围、组合多个随机算法等。


  1. 数字签名 —— 验证完整性 & 认证数据来源

3.1 什么是数字签名?

数字签名(Digital Signature)也叫作数字指纹(Digital Fingerprint),它是消息摘要算法和非对称加密算法的结合体,能够验证数据的完整性,并且认证数据的来源。

数据签名算法的模型分为两个主要阶段:

  • 1、签名: 先计算数据的 [摘要],再使用私钥对 [摘要] 进行加密生成 [签名],将 [数据 + 签名] 一并发送给接收方;
  • 2、验证: 先使用相同的摘要算法计算接收数据的 [摘要],再使用预先得到的公钥解密 [签名],对比 [解密的签名] 和 [计算的摘要] 是否一致。若一致,则说明数据没有被篡改。

提示: 接收方如何安全地预先得到发送方的公钥,见 第 4 节。

3.2 为什么数字签名可以验证完整性?

验证完整性主要依赖于消息摘要算法的特性,摘要算法的原理是根据一定的运算规则提取原始数据中的信息,被提取的信息就是原始数据的消息摘要,也称为数据指纹。著名的摘要算法有 MD5 算法和 SHA 系列算法。

摘要算法具有以下特点:

  • 一致性: 相同数据多次计算的摘要是相同的,不同的数据(在不考虑碰撞时)的摘要是不同的;
  • 不可逆性: 只能正向提取原始数据的摘要,无法从摘要反推出原始数据;
  • 高效性: 摘要的生成过程高效快速;

摘要算法的模型分为两个主要步骤:

  • 生成摘要: 先计算数据的 [摘要],再将 [数据 + 摘要] 一并发送给接收方;
  • 验证摘要: 使用相同的摘要算法计算接收数据的 [摘要],对比 [收到的摘要] 与 [计算的摘要]是否一致。若一致,则说明数据是完整的。

需要注意的是,单纯依靠摘要算法不能严格地验证数据完整性。因为在非安全信道中,数据和摘要都存在篡改风险,攻击者在篡改数据时也可以篡改摘要。因此,摘要算法需要配合加密算法才能严格验证完整性。

3.3 为什么数字签名可以认证数据来源?

这是因为签名时引入了发送方的私有信息(私钥),只有 ”合法的发送方“ 才能产生其他人无法伪造的一段数字签名(加密字符串),这个数字签名就证明了数据的真实来源。当接收方采用 ”合法途径“ 获得发送方的公有信息是(公钥),并且成功验证数字签名,那么正说明数据来自 ”合法的接收方“。

另外,在签名时引入发送方私有信息,在验证时使用发送方公有信息,这正好符合 “非对称加密” 的特点。因此签名时引入的私有信息正是私钥,验证时使用的公有信息正式公钥。

3.4 摘要算法存在碰撞,是不是不安全?

摘要算法(散列算法)本质上是一种 压缩映射,因此一定存在不同原始数据映射到同一个散列值的情况,这就是发生了碰撞(散列冲突,Hash Collision)。事实上,MD5、SHA-1 等散列算法已经陆续被找到快速散列碰撞的的方法,攻击者可以构造内存篡改但摘要一致的文件从而绕过检查。但不代表完全不安全,因为篡改为有价值的伪造内容还有困难??

3.5 为什么摘要中需要加盐?

为了提高安全性,在对原始数据生成摘要之前,我们往往会先向原始数据中加盐,再生成摘要。为什么要这么做呢?

这是为了避免 “彩虹表(Rainbow tables)” 攻击,提高简单数据的安全性。因为摘要算法有一致性的特点,相同数据多次计算的摘要是相同的。利用这个特性,攻击者可以预先生成一系列简单数据的摘要,并存储 “摘要 - 数据” 的映射,这个映射关系就是彩虹表。在获取到数据摘要后,如果发现摘要存在彩虹表中,就可以轻易地反推出原始数据。

用户在设置密码时,也要避免使用 123456 这种简单密码,因为容易被彩虹表攻击破解。为了提高安全性,在传输手机号、密码等敏感信息的过程中,往往会在原始密码中加盐。

3.6 可以先使用私钥对原数据签名,再对签名进行摘要吗?

不可以,主要有两个原因:

  • 1、可行性: 接收方需要通过摘要验证数据完整性,然而接收方无法对数据进行签名,因此无法验证数据摘要一致性;
  • 2、时间效率: 对原始数据进行签名(加密)时间太长,而摘要算法本身是压缩映射,可以缩短签名消耗的时间。

  1. 数字证书 —— 安全地发放公钥

在 第 3 节 中,我们提到接收方需要使用发送方的公钥来验证数据真实性。那么,接收方怎样才能安全地获得发送方公钥呢?这就需要数字证书来保证。

4.1 什么是数字证书?

数字签名和数字证书总是成对出现,二者不可分离。数字签名主要用来验证数据完整性和认证数据来源,而数字证书主要用来安全地发放公钥。 数字证书主要包含三个部分:用户的信息、用户的公钥和 CA 对该证书实体信息的签名。

数字证书的模型主要分为两个步骤:

  • 1、颁发证书:
+ 1.1 申请者将签名算法、公钥、有效时间等信息发送给 CA 机构;
+ 1.2 CA 机构验证申请者身份后,将申请者发送的信息打成一个实体,并计算摘要;
+ 1.3 CA 机构使用自己的私钥对摘要进行加密,生成证书签名(Certificate Signature);
+ 1.4 CA 机构将证书签名添加在数字证书上,构成完整的数字生出。
  • 2、验证证书
+ 2.1 验证方使用相同的摘要算法计算证书实体的摘要;
+ 2.2 使用 CA 机构的公钥(浏览器和操作系统中集成了 CA 的公钥信息)解密证书签名;
+ 2.3 对比解密后的数据与计算的摘要是否一致,如果一致则是可信任的证书。

4.2 什么是证书颁发机构 CA?

证书颁发机构(certifcation authroity, CA)是负责数字证书的审批、颁发、归档和吊销等功能的机构,具有权威性。CA 机构分为 “根 CA” 和 “中间 CA”,原则上要避免根 CA 机构直接颁发最终实体证书,而需要由中间 CA 机构颁发最终实体证书。这是为了避免证书失效的影响范围,一旦根证书失效或被伪造,那么整个证书链都有问题。

4.3 什么是证书链?

证书链是多个数字证书建立的的证书验证链条。数字证书主要包含三个部分:用户信息、用户密钥以及 CA 机构对该证书实体的签名。为了验证证书实体的合法性,需要获得颁发该证书的 CA 机构公钥,这个公钥就存在于上一级证书中。因此,为了验证证书的合法性,就需要沿着证书链向上追溯直到根证书为止。

根证书是自签名证书,用户下载根证书就表示信任该根证书所有签发的证书。在操作系统或浏览器中,已经内置了一部分受信任的根证书。

4.4 数字证书的标准

数字证书主要包含三个部分:用户的信息、用户的公钥和 CA 对该证书实体信息的签名。目前的数字证书采用的是公钥基础设施(PKI)制定的 X.509 标准,目前已经有 3 个版本,其中比较常见的是 X.509 第三版的标准。主要格式如下:

字段 描述
版本 (Version) 证书的版本信息
序列号 (Serial Number) 证书的唯一标识
签名算法标识 (Hash) 证书签名采用的算法
有效期 (Validity) 证书有效期的开始日期和结束日期
持有者信息 (Subject) 证书的持有者
公钥 (Subject Public Key Info) 持有者构建的公共密钥
颁布者信息 (Issuer) 证书颁布者
签名 (Certificate Signature) 颁布者对证书实体的签名

  1. 总结

看到这里,你应该已经建立起数据安全传输的基本认知。大多数情况,我们是在讨论 HTTPS 协议时才会遇到加密、摘要、签名和证书等概念。事实上,这些概念不止于 HTTPS,但凡涉及到数据在非安全信道中流转时,就需要应用这些工具来实现数据安全传输。

后面,我会写一些文章,在更多具体场景中讨论数据安全传输,请关注~ 更多文章:

  • 在构建 Android Apk 时,需要进行应用签名。看完这篇文章后,你现在能说清楚应用签名的作用吗?具体分析:他山之石,可以攻玉!一篇文章看懂 v1/v2/v3 签名机制

参考资料

  • HTTPS 权威指南 —— 伊万 · 里斯蒂奇 著
  • 趣谈网络协议 · HTTPS 协议 —— 刘超 著,极客时间出品
  • 图解 HTTP(第 7、8 章)—— 上野宜 著
  • Java 加密与解密的艺术 —— 梁栋 著
  • 图解网络 —— 小林coding 著
  • VasDolly 实现原理 —— 腾讯 VasDolly 技术团队 著
  • HTTP 权威指南(第 12、13 章) —— [美] David Gourley,Brian Totty 等著

创作不易,你的「三连」是丑丑最大的动力,我们下次见!

本文转载自: 掘金

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

【Spring Boot 快速入门】六、Spring Boo

发表于 2021-08-12

这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

收录专栏

Spring Boot 快速入门

Java全栈架构师

前言

  相信大部分开发人员都遇到过,在java中对象出现大量的属性生成构造器、getter/setter、equals、hashcode、toString方法,显得很冗长也没有太多技术含量,一旦修改属性,就容易出现忘记修改对应方法的失误。那么有没有比较好的方法去简化这些冗余的低效的代码呢,现在给大家介绍Lombok。

初始

  Lombok 是一种 Java 实用工具,可用来帮助开发人员消除 Java 的冗长,尤其是对于简单的 Java 对象(POJO)。它通过注释实现这一目的。通过在开发环境中实现 Lombok,开发人员可以节省构建诸如 hashCode() 和 equals() 这样的方法以及以往用来分类各种 accessor 和 mutator 的大量时间。

Lombok 缺点

  • 消除冗余低效的代码
  • 注释实现快速开发

Lombok 缺点

  • 强行安装,如果团队中一个人使用了Lombok插件,所有人必须安装。
  • 代码可读性,可调试性低,开发过程中缺少响应的方法,在编译阶段才生成。
  • 有未知的风险:开发者对其产生过度依赖,容易产生意想不到的结果。
  • 影响升级:对于代码有很强的侵入性,对JDK的升级和框架的升级,
  • 破坏封装性,代码耦合度增加,对项目有一定的干扰。

Lombok注解

注解 描述
@NonNull 在方法或者构造方法前进行参数非空检查
@Cleanup 自动资源管理,安全的调用close方法
@Getter 成员变量生成对应的set方法
@Setter 成员变量生成对应的get方法
@ToString 生成toString,equals和hashcode方法
@EqualsAndHashCode 生成toString、equals、hashcode和canEqual方法
@NoArgsConstructor 为类产生无参的构造方法
@RequiredArgsConstructor 类中所有带有@NonNull注解的或者带有final修饰的成员变量生成对应的构造方法
@AllArgsConstructor 为类产生包含所有参数的构造方法
@Data 注在类上,提供类的get、set、equals、hashCode、canEqual、toString方法
@Value 和@Data类似,定义为private final修饰,并且不会生成set方法
@SneakyThrows 捕获异常并在catch中用Lombok.sneakyThrow(e)把异常抛出
@Synchronized 和synchronized关键字相同
@Log 注解用在类上,直接进行日志记录

Lombok安装

设置

  选择file目录点击settings。
图片.png
  选择plugins,搜索lombok。
图片.png
选择安装即可。

添加依赖

1
2
3
4
5
6
7
8
js复制代码      <!-- lombok start -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
<!-- lombok end -->

更多快速入门参考:
【快速开发】Lombok 快速入门

结语

    这样Lombok与Spring Boot集成成功啦。更多的测试大家可以深入研究一下Lombok相关信息,相信一定会有新大陆发现的。

    作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

    好了,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

推荐阅读:

我的第一个Spring Boot项目启动啦!

周末建立了Spring Boot专栏,欢迎学习交流

Spring Boot集成MyBatis,可以连接数据库啦!

【Spring Boot 快速入门】四、Spring Boot集成JUnit

【Spring Boot 快速入门】五、Spring Boot集成Swagger UI

本文转载自: 掘金

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

超硬核详细学习系列第九天——深入浅出IO的知识点,值得你学习

发表于 2021-08-12

“这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战”

茫茫人海千千万万,感谢这一秒你看到这里。希望我的文章对你的有所帮助!

愿你在未来的日子,保持热爱,奔赴山海!

I/O高级流

昨天我们对高级流中的转换流学习,而在IO流的整个大体系中,他还有一些高级流等待着我们来解锁。

所以话不多说,今天我们先来学习其中一种高级流——打印流

打印流

不知道大家有没有注意,我们平时我们在控制台打印输出,是调用print()方法和println()方法完成的,其实它们都来自于java.io.PrintStream类中,而这个类也是一个IO流,能够方便地打印各种数据类型的值,是一种便捷的输出方式。

分类

打印流只有输出流,分为:

  • 字节打印流:printStream。
  • 字符打印流:printWriter。

两者具体使用方法中,基本类似。

打印流的特点

  • 只操作目的地,不操作数据源。
  • 可以操作任意类型的数据。
  • 如果启用了自动刷新,在调用println()方法的时候,能够换行并刷新。
  • 可以直接操作文本文件。

PrintStream

1. 构造方法

  • public PrintStream(String fileName): 使用指定的文件名创建一个新的打印流。

构造举例,代码如下:

1
ini复制代码PrintStream ps = new PrintStream("e:\demo\ps.txt");

2. 打印到文件

我们也经常看见System.out.println()这个打印到控制台就是printStream类型的,只不过它的流向是系统规定的,打印在控制台上。不过,既然是流对象,我们就可以玩另一个功能,将数据输出到指定文本文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
csharp复制代码public class PrintStreamDemo {
   public static void main(String[] args) throws FileNotFoundException {
       // 调用系统的打印流,控制台直接输出97
       System.out.println(97);
​
       //创建打印流
       PrintStream ps = new PrintStream("e:\demo\ps.txt");
​
       // 设置系统的打印流流向,输出到ps.txt
       System.setOut(ps);
       // 调用系统的打印流,ps.txt中输出97
       System.out.println(97);
  }
}

程序运行结果可以看到控制台只有打印一个97,那剩下一个97打印到哪里了呢,查看ps.txt文件,可以看到97打印到了文件中。

3. 复制文件案例

1
2
3
4
5
6
7
8
9
10
11
12
arduino复制代码public class PrintStreamDemo2 {
   public static void main(String[] args) throws IOException {
       BufferedReader br = new BufferedReader(new FileReader("e:\demo\ps.txt"));
       PrintStream ps = new PrintStream("e:\democopy\psCopy.txt");
       String line;
       while ((line = br.readLine()) != null) {
           ps.println(line);
      }
       br.close();
       ps.close();
  }
}

程序执行结果:

PrintWriter

1. 构造方法

  • public PrintWriter(String fileName): 使用指定的文件名创建一个新的打印流。

构造举例,代码如下:

1
ini复制代码PrintWriter pw = new PrintWriter("e:\demo\pw.txt");

2. 复制文件案例

1
2
3
4
5
6
7
8
9
10
11
12
13
arduino复制代码public class PrintWriterDemo {
   public static void main(String[] args) throws IOException {
​
       BufferedReader br = new BufferedReader(new FileReader("e:\demo\ps.txt"));
       PrintWriter pw = new PrintWriter("e:\democopy\pwCopy.txt");
       String line;
       while ((line = br.readLine()) != null) {
           pw.println(line);
      }
       br.close();
       pw.close();
  }
}

程序执行结果:

标准输出流的本质

  • 输出语句的原理和如何使用字符流输出数据

直接看一段代码先:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csharp复制代码public class SystemOutDemo {
   public static void main(String[] args) {
       //在System类下有out对象,可以获取PrintStream输出流对象。
       //public final static PrintStream out = null;
       System.out.println("helloworld");
​
       //输出流对象
       PrintStream ps = System.out;
       ps.println("helloworld");
  }
}
​
程序执行结果:
都打印在了控制台上:
helloworld
helloworld
  • 本质:

在System类下有一个public final static PrintStream out = null这个静态对象,可以返回一个printStream对象。

所以输出语句其本质就是IO流操作,把数据输出到控制台上。

总结

相信各位看官都对IO流中高级流中的打印流类有了一定了解,期待等待下一章的高级流——序列化教学吧!

当然还有很多流等着下次一起看吧!欢迎期待下一章的到来!

学到这里,今天的世界打烊了,晚安!虽然这篇文章完结了,但是我还在,永不完结。我会努力保持写文章。来日方长,何惧车遥马慢!

感谢各位看到这里!愿你韶华不负,青春无悔!

注: 如果文章有任何错误和建议,请各位大佬尽情留言!如果这篇文章对你也有所帮助,希望可爱亲切的您给个三连关注下,非常感谢啦!

本文转载自: 掘金

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

一文彻底弄懂零拷贝原理

发表于 2021-08-12

这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

零拷贝

零拷贝(Zero-Copy)是一种 I/O 操作优化技术,可以快速高效地将数据从文件系统移动到网络接口,而不需要将其从内核空间复制到用户空间。其在 FTP 或者 HTTP 等协议中可以显著地提升性能。但是需要注意的是,并不是所有的操作系统都支持这一特性,目前只有在使用 NIO 和 Epoll 传输时才可使用该特性。

需要注意,它不能用于实现了数据加密或者压缩的文件系统上,只有传输文件的原始内容。这类原始内容也包括加密了的文件内容。

传统I/O操作存在的性能问题

如果服务端要提供文件传输的功能,我们能想到的最简单的方式是:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。

传统 I/O 的工作方式是,数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。

代码通常如下,一般会需要两个系统调用:

1
2
lua复制代码read(file, tmp_buf, len);
write(socket, tmp_buf, len);

代码很简单,虽然就两行代码,但是这里面发生了不少的事情。

首先,期间共发生了 4 次用户态与内核态的上下文切换,因为发生了两次系统调用,一次是 read() ,一次是 write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。

上下文切换到成本并不小,一次切换需要耗时几十纳秒到几微秒,虽然时间看上去很短,但是在高并发的场景下,这类时间容易被累积和放大,从而影响系统的性能。

其次,还发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的,下面说一下这个过程:

  • 第一次拷贝,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝的过程是通过 DMA 搬运的。
  • 第二次拷贝,把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们应用程序就可以使用这部分数据了,这个拷贝到过程是由 CPU 完成的。
  • 第三次拷贝,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然还是由 CPU 搬运的。
  • 第四次拷贝,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程又是由 DMA 搬运的。

这种简单又传统的文件传输方式,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能。

所以,要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。

零拷贝技术原理

零拷贝主要是用来解决操作系统在处理 I/O 操作时,频繁复制数据的问题。关于零拷贝主要技术有 mmap+write、sendfile和splice等几种方式。

虚拟内存

在了解零拷贝技术之前,先了解虚拟内存的概念。

所有现代操作系统都使用虚拟内存,使用虚拟地址取代物理地址,主要有以下几点好处:

  • 多个虚拟内存可以指向同一个物理地址。
  • 虚拟内存空间可以远远大于物理内存空间。

利用上述的第一条特性可以优化,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样在 I/O 操作时就不需要来回复制了。

如下图展示了虚拟内存的原理。

image-20210812181924274

mmap/write 方式

使用mmap/write方式替换原来的传统I/O方式,就是利用了虚拟内存的特性。下图展示了mmap/write原理:

image-20210812201839908

整个流程的核心区别就是,把数据读取到内核缓冲区后,应用程序进行写入操作时,直接把内核的Read Buffer的数据复制到Socket Buffer以便写入,这次内核之间的复制也是需要CPU的参与的。

上述流程就是少了一个 CPU COPY,提升了 I/O 的速度。不过发现上下文的切换还是4次并没有减少,这是因为还是要应用程序发起write操作。

那能不能减少上下文切换呢?这就需要sendfile方式来进一步优化了。

sendfile 方式

从 Linux 2.1 版本开始,Linux 引入了 sendfile来简化操作。sendfile方式可以替换上面的mmap/write方式来进一步优化。

sendfile将以下操作:

1
2
java复制代码  mmap();
write();

替换为:

1
java复制代码 sendfile();

这样就减少了上下文切换,因为少了一个应用程序发起write操作,直接发起sendfile操作。

下图展示了sendfile原理:

image-20210812201905046

sendfile方式只有三次数据复制(其中只有一次 CPU COPY)以及2次上下文切换。

那能不能把 CPU COPY 减少到没有呢?这样需要带有 scatter/gather的sendfile方式了。

带有 scatter/gather 的 sendfile方式

Linux 2.4 内核进行了优化,提供了带有 scatter/gather 的 sendfile 操作,这个操作可以把最后一次 CPU COPY 去除。其原理就是在内核空间 Read BUffer 和 Socket Buffer 不做数据复制,而是将 Read Buffer 的内存地址、偏移量记录到相应的 Socket Buffer 中,这样就不需要复制。其本质和虚拟内存的解决方法思路一致,就是内存地址的记录。

下图展示了scatter/gather 的 sendfile 的原理:

image-20210812201922193

scatter/gather 的 sendfile 只有两次数据复制(都是 DMA COPY)及 2 次上下文切换。CUP COPY 已经完全没有。不过这一种收集复制功能是需要硬件及驱动程序支持的。

splice 方式

splice 调用和sendfile 非常相似,用户应用程序必须拥有两个已经打开的文件描述符,一个表示输入设备,一个表示输出设备。与sendfile不同的是,splice允许任意两个文件互相连接,而并不只是文件与socket进行数据传输。对于从一个文件描述符发送数据到socket这种特例来说,一直都是使用sendfile系统调用,而splice一直以来就只是一种机制,它并不仅限于sendfile的功能。也就是说 sendfile 是 splice 的一个子集。

在 Linux 2.6.17 版本引入了 splice,而在 Linux 2.6.23 版本中, sendfile 机制的实现已经没有了,但是其 API 及相应的功能还在,只不过 API 及相应的功能是利用了 splice 机制来实现的。

和 sendfile 不同的是,splice 不需要硬件支持。

总结

无论是传统的 I/O 方式,还是引入了零拷贝之后,2 次 DMA copy是都少不了的。因为两次 DMA 都是依赖硬件完成的。所以,所谓的零拷贝,都是为了减少 CPU copy 及减少了上下文的切换。

下图展示了各种零拷贝技术的对比图:

CPU拷贝 DMA拷贝 系统调用 上下文切换
传统方法 2 2 read/write 4
内存映射 1 2 mmap/write 4
sendfile 1 2 sendfile 2
scatter/gather copy 0 2 sendfile 2
splice 0 2 splice 0

结尾

我是一个正在被打击还在努力前进的码农。如果文章对你有帮助,记得点赞、关注哟,谢谢!

本文转载自: 掘金

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

七夕节马上要到了,前端工程师,后端工程师,算法工程师都怎么哄

发表于 2021-08-12

这篇文章的前提是,你得有个女朋友,没有就先收藏着吧!

七夕节的来源是梁山伯与祝英台的美丽传说,化成了一对蝴蝶~
美丽的神话!虽然现在一般是过214的情人节了,但是不得不说,古老的传统的文化遗产,还是要继承啊~

在互联网公司中,主要的程序员品种包括:前端工程师,后端工程师,算法工程师。

对于具体的职业职能划分还不是很清楚的,我们简单的介绍一下不同程序员岗位的职责:

前端程序员:绘制UI界面,与设计和产品经理进行需求的对接,绘制特定的前端界面推向用户

后端程序员:接收前端json字符串,与数据库对接,将json推向前端进行显示

算法工程师:进行特定的规则映射,优化函数的算法模型,改进提高映射准确率。

七夕节到了,怎么结合自身的的专业技能,哄女朋友开心呢?

前端工程师:我先来,画个动态的晚霞页面!

1.定义样式风格:

1
2
3
4
5
6
7
8
9
js复制代码.star {
width: 2px;
height: 2px;
background: #f7f7b6;
position: absolute;
left: 0;
top: 0;
backface-visibility: hidden;
}

2.定义动画特性

1
2
3
4
5
6
7
8
9
js复制代码@keyframes rotate {
0% {
transform: perspective(400px) rotateZ(20deg) rotateX(-40deg) rotateY(0);
}

100% {
transform: perspective(400px) rotateZ(20deg) rotateX(-40deg) rotateY(-360deg);
}
}

3.定义星空样式数据

1
2
3
4
5
6
7
8
js复制代码export default {
data() {
return {
starsCount: 800, //星星数量
distance: 900, //间距
}
}
}

4.定义星星运行速度与规则:

1
2
3
4
5
6
7
8
9
10
11
js复制代码starNodes.forEach((item) => {
let speed = 0.2 + Math.random() * 1;
let thisDistance = this.distance + Math.random() * 300;
item.style.transformOrigin = `0 0 ${thisDistance}px`;
item.style.transform =
`
translate3d(0,0,-${thisDistance}px)
rotateY(${Math.random() * 360}deg)
rotateX(${Math.random() * -50}deg)
scale(${speed},${speed})`;
});

前端预览效果图:

截屏2021-08-12 上午9.53.34.png

后端工程师看后,先点了点头,然后表示不服,画页面太肤浅了,我开发一个接口,定时在女朋友生日的时候发送祝福邮件吧!

1.导入pom.xml 文件

1
2
3
4
5
js复制代码        <!-- mail邮件服务启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

2.application-dev.properties内部增加配置链接

1
2
3
4
5
6
7
8
js复制代码#QQ\u90AE\u7BB1\u90AE\u4EF6\u53D1\u9001\u670D\u52A1\u914D\u7F6E
spring.mail.host=smtp.qq.com
spring.mail.port=587

## qq邮箱
spring.mail.username=#yourname#@qq.com
## 这里填邮箱的授权码
spring.mail.password=#yourpassword#

3.配置邮件发送工具类
MailUtils.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
js复制代码@Component
public class MailUtils {
@Autowired
private JavaMailSenderImpl mailSender;

@Value("${spring.mail.username}")
private String mailfrom;

// 发送简单邮件
public void sendSimpleEmail(String mailto, String title, String content) {
// 定制邮件发送内容
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(mailfrom);
message.setTo(mailto);
message.setSubject(title);
message.setText(content);
// 发送邮件
mailSender.send(message);
}
}

4.测试使用定时注解进行注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
js复制代码@Component
class DemoApplicationTests {

@Autowired
private MailUtils mailUtils;

/**
* 定时邮件发送任务,每月1日中午12点整发送邮件
*/
@Scheduled(cron = "0 0 12 1 * ?")
void sendmail(){
// 定制邮件内容
StringBuffer content = new StringBuffer();
content.append("HelloWorld");
//分别是接收者邮箱,标题,内容
mailUtils.sendSimpleEmail("123456789@qq.com","自定义标题",content.toString());
}
}

@scheduled注解 使用方法:
cron:秒,分,时,天,月,年,* 号表示 所有的时间均匹配

5.工程进行打包,部署在服务器的容器中运行即可。

算法工程师,又开发接口,又画页面,我就训练一个自动写诗机器人把!

1.定义神经网络RNN结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码def neural_network(model = 'gru', rnn_size = 128, num_layers = 2):
cell = tf.contrib.rnn.BasicRNNCell(rnn_size, state_is_tuple = True)
cell = tf.contrib.rnn.MultiRNNCell([cell] * num_layers, state_is_tuple = True)
initial_state = cell.zero_state(batch_size, tf.float32)
with tf.variable_scope('rnnlm'):
softmax_w = tf.get_variable("softmax_w", [rnn_size, len(words)])
softmax_b = tf.get_variable("softmax_b", [len(words)])
embedding = tf.get_variable("embedding", [len(words), rnn_size])
inputs = tf.nn.embedding_lookup(embedding, input_data)
outputs, last_state = tf.nn.dynamic_rnn(cell, inputs, initial_state = initial_state, scope = 'rnnlm')
output = tf.reshape(outputs, [-1, rnn_size])
logits = tf.matmul(output, softmax_w) + softmax_b
probs = tf.nn.softmax(logits)
return logits, last_state, probs, cell, initial_state

2.定义模型训练方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
js复制代码def train_neural_network():
logits, last_state, _, _, _ = neural_network()
targets = tf.reshape(output_targets, [-1])
loss = tf.contrib.legacy_seq2seq.sequence_loss_by_example([logits], [targets], \
[tf.ones_like(targets, dtype = tf.float32)], len(words))
cost = tf.reduce_mean(loss)
learning_rate = tf.Variable(0.0, trainable = False)
tvars = tf.trainable_variables()
grads, _ = tf.clip_by_global_norm(tf.gradients(cost, tvars), 5)
#optimizer = tf.train.GradientDescentOptimizer(learning_rate)
optimizer = tf.train.AdamOptimizer(learning_rate)
train_op = optimizer.apply_gradients(zip(grads, tvars))

Session_config = tf.ConfigProto(allow_soft_placement = True)
Session_config.gpu_options.allow_growth = True

trainds = DataSet(len(poetrys_vector))

with tf.Session(config = Session_config) as sess:
sess.run(tf.global_variables_initializer())

saver = tf.train.Saver(tf.global_variables())
last_epoch = load_model(sess, saver, 'model/')

for epoch in range(last_epoch + 1, 100):
sess.run(tf.assign(learning_rate, 0.002 * (0.97 ** epoch)))
#sess.run(tf.assign(learning_rate, 0.01))

all_loss = 0.0
for batche in range(n_chunk):
x,y = trainds.next_batch(batch_size)
train_loss, _, _ = sess.run([cost, last_state, train_op], feed_dict={input_data: x, output_targets: y})

all_loss = all_loss + train_loss

if batche % 50 == 1:
print(epoch, batche, 0.002 * (0.97 ** epoch),train_loss)

saver.save(sess, 'model/poetry.module', global_step = epoch)
print (epoch,' Loss: ', all_loss * 1.0 / n_chunk)

3.数据集预处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
js复制代码poetry_file ='data/poetry.txt'
# 诗集
poetrys = []
with open(poetry_file, "r", encoding = 'utf-8') as f:
for line in f:
try:
#line = line.decode('UTF-8')
line = line.strip(u'\n')
title, content = line.strip(u' ').split(u':')
content = content.replace(u' ',u'')
if u'_' in content or u'(' in content or u'(' in content or u'《' in content or u'[' in content:
continue
if len(content) < 5 or len(content) > 79:
continue
content = u'[' + content + u']'
poetrys.append(content)
except Exception as e:
pass

poetry.txt文件中存放这唐诗的数据集,用来训练模型

4.测试一下训练后的模型效果:

藏头诗创作:“七夕快乐”

模型运算的结果

截屏2021-08-12 上午9.43.07.png

哈哈哈,各种节日都是程序员的表(zhuang)演(bi) 时间,不过这些都是锦上添花,只有实实在在,真心,才会天长地久啊~

提前祝各位情侣七夕节快乐!

我是千与千寻,我们下期见~

本文转载自: 掘金

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

1…566567568…956

开发者博客

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