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

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


  • 首页

  • 归档

  • 搜索

【koa 源码阅读(一)】手把手教你搞懂 koa 中间件(洋

发表于 2021-11-26

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

我是小十七_,今天和大家一起阅读 koa 中间件源码,通俗易懂,包教包会~

  • 翻译自:itnext.io/how-koa-mid…

Koa 的中间件不同于 Express,Koa 使用洋葱模型原理。它的源码只包含四个文件,对于初读源码的同学非常友好,今天我们只看主文件 - application.js,它已经包含了中间件是如何工作的核心逻辑。

image.png

前置准备

首先 clone koa 源码

1
2
js复制代码git clone git@github.com:koajs/koa.git
npm install

然后我们在项目的根目录添加一个 index.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
js复制代码// index.js
// 包括 koa 的入口文件
const Koa = require('./lib/application.js');
const app = new Koa();
const debug = require('debug')('koa');
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(6);
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// 时间日志记录
app.use(async (ctx, next) => {
console.log(2);
const start = Date.now();
await next();
console.log(5);
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
app.use(async (ctx, next) => {
console.log(3);
ctx.body = 'Hello World';
await next();
console.log(4);
});

app.listen(3000);

运行下面的命令来启动服务器:

1
js复制代码node index.js

接着访问 http://localhost:3000,你会看到 1, 2, 3, 4, 5, 6 输出。这称为洋葱模型(中间件)

洋葱模型的工作原理

让我们一起阅读 koa 的核心代码,看看中间件是如何工作的。在 index.js 中,我们这样使用中间件:

1
2
3
4
ini复制代码const app = new Koa();
app.use(// middleware);
app.use(// middleware);
app.listen(3000);

让我们来看看 application.js,它在源代码的 lib 目录下,这里是与中间件相关的代码,我把代码进行了简化,保留了中间件的核心逻辑,并在代码中添加了一些注释。

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
js复制代码module.exports = class Application extends Emitter {

constructor() {
super();
this.proxy = false;
// Step 0:初始化 middleware 中间件数组
this.middleware = [];
}

use(fn) {
// Step 1: 向数组里 push 中间件
this.middleware.push(fn);
return this;
}

listen(...args) {
debug('listen');
// Step 2: 调用 this.callback() 去组合所有的中间件
const server = http.createServer(this.callback());
return server.listen(...args);
}

callback() {
// Step 3: 最重要的部分 - compose 函数, 它把所有
// 中间件组合成一个大的函数,函数返回一个 promise,我们后面会继续讨论这个函数
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};

return handleRequest;
}

handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
// Step 4: Resolve 这个 promise
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
}

我们把代码简化为只有关于 compose 函数的部分的伪代码:

1
2
3
4
5
6
7
8
9
10
11
js复制代码 listen(...args) {
const server = http.createServer(this.callback());
}
callback() {
// compose 函数
const fn = compose(this.middleware);
return this.handleRequest(ctx, fn);
}
handleRequest(ctx, fnMiddleware) {
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

从上面的代码可以猜测到:compose 函数,执行后返回了一个函数(这里叫 fn),fn 函数执行后,返回的是一个 promise。

关于 compose 函数

更多关于 compose 函数的信息,我们可以看一下 koa-compose 包的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
js复制代码module.exports = compose
function compose (middleware) {
// 这里跳过类型检测的代码
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}

和上面猜测的一样,我们简称 compose 返回的函数叫 fn,所有中间件都被 compose 传递到 了 fn 函数中,它返回了 dispatch(0),也就是立即执行了 dispatch 函数并返回了一个 promise。在了解 dispatch 函数的内容之前,我们先要了解 promise 的语法。

关于 Promise

通常我们会像这样使用 promise:

1
2
3
4
5
6
7
js复制代码const promise = new Promise(function(resolve, reject) {
if (success){
resolve(value);
} else {
reject(error);
}
});

在 Koa 中,它是这样使用的:

1
2
3
4
5
6
7
8
js复制代码let testPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('test success');
}, 1000);
});
Promise.resolve(testPromise).then(function (value) {
console.log(value); // "test success"
});

因此,我们知道在 compose 函数中,它返回一个 promise。

回到 Koa - compose 中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
js复制代码module.exports = compose
function compose (middleware) {
// 这里跳过类型检测的代码
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}

dispatch 是一个递归函数,它将循环所有中间件。其中我们简化一下递归的部分:

1
2
js复制代码let fn = middleware[i]
fn(context, dispatch.bind(null, i + 1))

这里 fn 是当前的中间件函数,执行了 fn,参数分别是 context 和 dispatch.bind(null, i + 1)(也就是我们传给中间价的 next),中间件执行这个函数,也就递归执行了 dispatch 函数,具体看下面的分析:

在我们的测试文件 index.js 中,我们有 3 个中间件,所有 3 个中间件都会在 await next() 之前执行这些代码;

1
2
3
4
5
6
7
8
js复制代码app.use(async (ctx, next) => {
console.log(2);
const start = Date.now();
await next(); // <- 停在这儿并且等待下一个中间件执行
console.log(5);
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});

我们可以看一下 index.js 中这三个中间件的执行顺序:

  • 执行 dispatch(0) 时,会执行 Promise.resolve(fn(context, dispatch.bind(null, 0 + 1)))
  • 第一个中间件内容将运行直到 await next()
  • next() = dispatch.bind(null, 0 + 1),这是第二个中间件
  • 第二个中间件将运行直到 await next()
  • next() = dispatch.bind(null, 1 + 1),这是第三个中间件
  • 第三个中间件将一直运行到 await next()
  • next() = dispatch.bind(null, 2 + 1),没有第四个中间件,会立即通过 if (!fn) return Promise.resolve() 返回,第三个中间件中的 await next() 被解析,剩余执行第三个中间件中的代码。
  • 第二个中间件中的 await next() 被解析,第二个中间件中的剩余代码被执行。
  • 第一个中间件中的 await next() 被解析,第一个中间件中的剩余代码被执行。

为什么使用洋葱模型?

如果我们在中间件中有 async/await,编码会更简单。当我们想为 api 请求编写一个时间记录器时,通过添加这个中间件可以非常容易:

1
2
3
4
5
6
js复制代码app.use(async (ctx, next) => {
const start = Date.now();
await next(); // 你的 API 逻辑
const ms = Date.now() - start;
console.log('API response time:' + ms);
});

本文转载自: 掘金

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

小酌数据一口----数据类型 数据类型 数据存储

发表于 2021-11-26

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

数据类型

1.内置类型

char 字符数据类型

short 短整型

int 整型

long 长整型

long long 更长的整型

float 单精度浮点型

double 双精度浮点型

整型家族

char

有符号还是无符号取决于编译器,大部分编译器下是signed char

unsigned char

signed char

short

unsigned short【int】

signed short【int】

int

unsigned int

signed int

long

unsigned long【int】

signed long【int】

浮点型家族

float

double

2.构造类型

数组类型

int a = 10 去掉名字就是类型所以a的类型就是int

int arr 【10】 = {0}去掉名字arr 所以这个数组类型是int 【10】数组类型是自定义的所以它也是构造类型

结构体类型

枚举类型

联合类型

3.指针类型

int* 整型指针

float* 浮点型指针

char* 字符型指针

void* 空类型指针

4.空类型

void

表示空类型 通常用在函数的返回值类型,函数的参数,指针类型

数据存储

整形存储

char

image-20210813111047103

image-20210813132832428

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
c复制代码#include<stdio.h>

int main()
{
//char的范围-128~127
char a = 128;
//00000000000000000000000010000000原反补相同
//10000000
//11111111111111111111111110000000
//4,294,967,168

char b = -127;
//10000000000000000000000001111111原
//11111111111111111111111110000000反
//11111111111111111111111110000001补
//10000001
//11111111111111111111111110000001
//4,294,967,169‬

printf("%u\n", a);
printf("%u\n", b);
return 0;
}

打印无符号整形 %u

打印有符号整形 %d

int

整形最大值INT_MAX,最小值INT_MIN定义在limits.h头文件里面

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
c复制代码#include<stdio.h>

int main()
{
int i = -20;
//10000000 00000000 00000000 00010100
//11111111 11111111 11111111 11101011
//11111111 11111111 11111111 11101100 补码
unsigned int j = 10;
//00000000 00000000 00000000 00001010 原反补
int a = 0;
unsigned int b = 0;
a = i + j;
printf("%d\n", a);
//11111111 11111111 11111111 11101100
//00000000 00000000 00000000 00001010
//相加之后
//11111111 11111111 11111111 11110110
//11111111 11111111 11111111 11110101
//10000000 00000000 00000000 00001010 -10
b = i + j;
printf("%u\n",b);

return 0;
}

同样的数据存储,由于看待的视角不同,所以打印出来的结果就不同

image-20210813123848411

short

image-20210813133812748

image-20210813134228967

浮点型存储

float

浮点型大小限制头文件在float.h头文件里面

image-20210813201244908

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c复制代码#include<stdio.h>

int main()
{
int n = 9;
float* pf = (float*)&n;//站在浮点型角度去访问这个空间,那为什么会不一样呢,说明浮点型数据存储方式和整形存储方式是不一样的
printf("%d\n", n);
printf("%f\n", *pf);

*pf = 9.0;
printf("%d\n", n);
printf("%f\n", *pf);
return 0;
}

根据国际标准IEEE(电子和电子工程协会)754,任意一个二进制浮点数V可以表示成下面形式:

1
2
3
4
c复制代码(-1)^S*M*2^E
(-1)^S表示符号位,当s=0,v为正数;当s=1,v为负数
M表示有效数字,大于等于1,小于2
2^E表示指数位

上面那个是什么鬼玩意是不是觉得懵逼了,下面我们举个例子

image-20210813224424760

1
2
3
4
c复制代码所以9.0的二进制表现形式可以表示为
10010.0
1.001*2^5
因此(-1)^0*1.001*2^5

IEEE754规定:

对于32位的浮点数,最高位是符号位s,接着的8位是指数E,剩下的23位为有效数字M

image-20210813220921441

对于64位的浮点数,最高的一位是符号位,接着的11位是指数位,剩下来的52位是有效数字M

image-20210813220946333

IEEE 754对有效数字M和指数E,有一些特别规定

因为1<=M<2,也就是说,M可以写成1.xxxxx的形式,其中xxxxxx是小数部分

IEEE 754规定,在计算机内部保存M时默认这个数的第一位总是1,所以可以被省略,只保留后面的xxxxxx就好。比如1.01就保存01,等读到他的时候把1加上去就好,这样可以节省一位有效数字

至于E的话

E作为一个无符号整数(unsigned int)8位范围0~255,11位范围0-2047,但实际上科学计数法中的E是可以位负数的,所以IEEE 754规定,存入内存时的E必须再加上一个中间数,8位就加127,11位就加1023,(什么意思呢 就是我们求的真实的E加上一个中间数再存到内存中)

image-20210813221029000

这些仅仅是存进去的那我们是如何取出来的呢?

E在内存中有三种方式可以取出

E不全为0不全为1

这时浮点数就采用下面的规则,E的计算值减去127(或1023),得到真实值,再将有效数字M前面加上第一位1

E全为0

这时浮点数的指数E直接等于1-127(或1-1023)为真实值

有效数字M不再加上第一位的1,而是还原为0.xxxxxxxxx的小数,这样做是为了表示正负0,以及接近于0的很小数字

E全为1

如果有效数字M全为0,表示正负无穷大(正负取决于s)

所以那个题目就好解了

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
c复制代码#include<stdio.h>

int main()
{
//float b = 1.5f;
//(-1)^0*1.1*2^0
//0+127
//0 0111 1111 1000 0000........
//整合一下0011 1111 1100 0000 .........
//3f c0 00 00
int n = 9;
//00000000 00000000 00000000 00001001
float* pf = (float*)&n;
//换个视角看内存
//他会认为E是全0 所以是无限小 所以打印出来的是0.000000
printf("%d\n", n);
printf("%f\n", *pf);

*pf = 9.0;
//1001.0
//1.001*2^3
//3+127 = 130
//0 1000 0010 0010 000 ........
//0100 0001 0001 0000 0000 0000 0000 0000
//如果是整形视角的话就是1,091,567,616
printf("%d\n", n);
printf("%f\n", *pf);
return 0;
}

image-20210813223341105

本文转载自: 掘金

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

qsort库函数排序 qsort库函数排序

发表于 2021-11-26

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

qsort库函数排序

quick sort快速排序原函数解释

image-20210818230558340

用qsort

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
c复制代码#include<stdio.h>
#include<stdlib.h>

struct Stu
{
char name[20];
int age;
};

int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}

int cmp_float(const void* e1, const void* e2)
{
return (int)(*(float*)e1 - *(float*)e2);
}

//结构体排序有不同排序方法,每个人的标准不同,有人是名字,有人是姓名
int cmp_stu_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}

int cmp_stu_name(const void* e1, const void* e2)
{
//名字比较就是比较字符串
//字符串比较不能直接用<>=来比较,应该用strcmp来比较

return strcmp(((struct Stu*)e1)->name , ((struct Stu*)e2)->name);
}

void test_int()
{
int arr[10] = { 0,5,6,3,5,9,6,5,3,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%2d ", arr[i]);
}
printf("\n");
}
void test_float()
{
float arr[10] = { 0.0,5.0,6.0,3.0,5.1,9.0,6.3,5.6,3.3,9.5 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_float);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%f ", arr[i]);
}
printf("\n");
}
void test_stu_age()
{
struct Stu s[3] = { {"zhuangsan",20},{"lisi",21},{"wangwu",10} };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_stu_age);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%s ", (&s[i])->name);
printf("%d ", (&s[i])->age);
}
printf("\n");
}
void test_stu_name()
{
struct Stu s[3] = { {"zhuangsan",20},{"lisi",21},{"wangwu",10} };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_stu_name);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%s ", (&s[i])->name);
printf("%d ", (&s[i])->age);
}
printf("\n");
}
int main()
{
test_int();
test_float();
test_stu_age();
test_stu_name();
return 0;
}

image-20210818225357880

一般到了这里肯定想自己写个qsort,因此我们仿写一个为BubbleSort

image-20210831222802853

漂亮之处void万能类型和char*,字宽width的联合出击

image-20210831222830598

image-20210831223052692

用BubbleSort

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
C复制代码#include<stdio.h>
struct Stu
{
char name[20];
int age;
};

//自己写一个万能交换函数
void Swap(char* buf1, char* buf2,int width)
{
size_t i = 0;
for (i = 0; i < width; i++)
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}

//自己写一个冒泡函数,使用回调函数实现一个通用的冒泡排序函数
voidBubbleSort(void* base, int num, size_t width, int (*cmp)(const void* e1, const void* e2))
{
int i = 0;
//趟数
for (i = 0; i < num - 1; i++)
{
//比较的对数
int j = 0;
for (j = 0; j < num - 1 - i; j++)
{
if (cmp((char*)base+j*width,(char*)base+(j+1)*width) > 0)
{
//交换
Swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
}
}
}
}

int cmp_int(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}

int cmp_float(const void* e1, const void* e2)
{
return (int)(*(float*)e1 - *(float*)e2);
}

int cmp_stu_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int cmp_stu_name(const void* e1, const void* e2)
{
//名字比较就是比较字符串
//字符串比较不能直接用<>=来比较,应该用strcmp来比较

return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
void test_int()
{
int arr[10] = { 0,5,6,3,5,9,6,5,3,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
BubbleSort(arr, sz, sizeof(arr[0]), cmp_int);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%2d ", arr[i]);
}
printf("\n");
}
void test_float()
{
float arr[10] = { 0.0,5.0,6.0,3.0,5.1,9.0,6.3,5.6,3.3,9.5 };
int sz = sizeof(arr) / sizeof(arr[0]);
BubbleSort(arr, sz, sizeof(arr[0]), cmp_float);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%f ", arr[i]);
}
printf("\n");
}
void test_stu_age()
{
struct Stu s[3] = { {"zhuangsan",20},{"lisi",21},{"wangwu",10} };
int sz = sizeof(s) / sizeof(s[0]);
BubbleSort(s, sz, sizeof(s[0]), cmp_stu_age);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%s ", (&s[i])->name);
printf("%d ", (&s[i])->age);
}
printf("\n");
}

void test_stu_name()
{
struct Stu s[3] = { {"zhuangsan",20},{"lisi",21},{"wangwu",10} };
int sz = sizeof(s) / sizeof(s[0]);
BubbleSort(s, sz, sizeof(s[0]), cmp_stu_name);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%s ", (&s[i])->name);
printf("%d ", (&s[i])->age);
}
printf("\n");
}
int main()
{
test_int();
test_float();
test_stu_age();
test_stu_name();
return 0;
}

image-20210831223655636

本文转载自: 掘金

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

Rust - 元组结构体和单元结构体 元组结构体 单元结构体

发表于 2021-11-26

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

元组结构体

除了定义常规的结构体之外,也可以定义与元组类似的结构体,称为 元组结构体(tuple structs),元组结构体的主要特征是没有字段名,只有字段类型,当我们想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了,比如定义颜色的RGB和坐标。要定义元组结构体,以 struct 关键字和结构体名开头并后跟元组中的类型。例如,下面是两个分别叫做 Color 和 Point 元组结构体的定义和用法:

1
2
3
4
5
rust复制代码struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

需要注意 black 和 origin 值的类型不同,因为它们是不同的元组结构体的实例。你定义的每一个结构体有其自己的类型,即使结构体中的字段有着相同的类型。例如,一个获取 Color 类型参数的函数不能接受 Point 作为参数,即便这两个类型都由三个 i32 值组成。在其他方面,元组结构体实例类似于元组:可以将其解构为单独的部分,也可以使用 . 后跟索引来访问单独的值,等等。

单元结构体

我们也可以定义一个没有任何字段的结构体!它们被称为 类单元结构体(unit-like structs)因为它们类似于 (),即 unit 类型。类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用。trait我们会在后面的文章进行介绍。

结构体数据的所有权

1
2
3
4
5
rust复制代码struct UserInfo {
name: String,
age: i32,
sex: bool, // true表示男 false表示女
}

在上面代码示例中,使用了自身拥有所有权的 String 类型而不是 &str 字符串 slice 类型。原因就是想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也是有效的。这里会提到一个 生命周期(lifetimes)的概念,后续文章也会介绍哦,生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果尝试在结构体中存储一个引用而不指定生命周期将会引发程序编译失败:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
rust复制代码struct User {
username: &str,
email: &str,
sign_in_count: u64,
active: bool,
}

fn main() {
let user1 = User {
email: "someone@example.com",
username: "someusername123",
active: true,
sign_in_count: 1,
};
}

报错信息如下所示:

1
2
3
4
5
6
7
8
9
10
11
lua复制代码error[E0106]: missing lifetime specifier
-->
|
2 | username: &str,
| ^ expected lifetime parameter

error[E0106]: missing lifetime specifier
-->
|
3 | email: &str,
| ^ expected lifetime parameter

我们会在后续的文章中介绍如何修复这个问题以便在结构体中存储引用。

结语

文章首发于微信公众号程序媛小庄,同步于掘金。

码字不易,转载请说明出处,走过路过的小伙伴们伸出可爱的小指头点个赞再走吧(╹▽╹)

本文转载自: 掘金

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

Rust - 初识结构体 定义并实例化结构体 基于已有实例创

发表于 2021-11-26

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

struct,或者 *structure,是一个自定义数据类型,允许命名和包装多个相关的值,从而形成一个有意义的组合。如果你熟悉一门面向对象语言,*struct 就像对象中的数据属性。

定义并实例化结构体

结构体和之前介绍过的元组类似,和元组一样,结构体的每一部分可以是不同类型。但不同于元组,结构体需要命名各部分数据以便能清楚的表明其值的意义。由于有了这些名字,结构体比元组更灵活:不需要依赖顺序来指定或访问实例中的值。

定义结构体需要使用struct关键字并给结构体提供一个名字,类似于变量名结构体的名字需要描述它所组合的数据的意义。然后在大括号中,定义每一部分数据的名字和类型,名字和类型之间用:分隔,我们称为 字段(field),不同字段之间用,隔开。如下代码示例:

1
2
3
4
5
6
rust复制代码// 定义结构体
struct UserInfo {
name: String,
age: i32,
sex: bool, // true表示男 false表示女
}

一旦定义了结构体,就可以通过为每个字段指定具体值来创建这个结构体的 实例,也就是实例化操作。

创建一个实例需要以结构体的名字开头,接着在大括号中使用 key: value 键-值对的形式提供字段,其中 key 是字段的名字,value 是需要存储在字段中的数据值。实例中字段的顺序不需要和它们在结构体中声明的顺序一致。换句话说,结构体的定义就像一个类型的通用模板,而实例则会在这个模板中放入特定数据来创建这个类型的值。比如下面的代码示例:

1
2
3
4
5
6
7
rust复制代码// 结构体实例化

let userinfo = UserInfo {
name: String::from("rust"),
age: 18,
sex: true,
};

如果要从结构体中获取某个特定的值,可以使用点号。比如我们只想要用户的名字,可以使用userinfo.name,这个语法和python中的面向对象就非常类似了。

如果结构体的实例是可变的,并且想要修改结构体中的值,也可以使用点号给对应的字段重新赋值:

1
2
3
4
5
6
7
rust复制代码let userinfo = UserInfo {
name: String::from("rust"),
age: 18,
sex: true,
};
// 修改实例的值
userinfo.age = 10;

需要注意的是整个实例必须是可变的;Rust 并不允许只将某个字段标记为可变。另外需要注意同其他任何表达式一样,我们可以在函数体的最后一个表达式中构造一个结构体的新实例,来隐式地返回这个实例。如下代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
rust复制代码fn main() {
let name = String::from("rust");
let age = 20;
let userinfo = build_userinfo(name, age);
println!("user info name is {}", userinfo.name)
}

struct UserInfo {
name: String,
age: i32,
sex: bool, // true表示男 false表示女
}

fn build_userinfo(name: String, age: i32) ->UserInfo {
UserInfo {
name: name,
age: age,
sex: true,
} // 注意不能加分号哦,加上就会变成语句,返回值为空元组
}

为函数参数起与结构体字段相同的名字是可以理解的,但是重复 email 和 username 字段名称与变量显得有些啰嗦。如果结构体有更多字段,开发人员就要哭了~~因此Rust提供了更加方便的简写语法。就是当函数参数和结构体的字段名称相同时,只需要写字段名即可,而不是字段名: 函数参数名:

1
2
3
4
5
6
7
rust复制代码fn build_userinfo(name: String, age: i32) ->UserInfo {
UserInfo {
name,
age,
sex: true,
} // 注意不能加分号哦,加上就会变成语句,返回值为空元组
}

基于已有实例创建新的实例

基于已有实例创建新的实例在一些特定情况下可以帮助我们减少代码,可以通过结构体更新语法(struct update syntax)实现。下面是一段示例代码,在不使用更新语法时,如何在 userinfo1 中创建一个新 UserInfo 实例。

1
2
3
4
5
rust复制代码let userinfo1  = UserInfo {
name: String::from("rust"),
age: user info.age,
sex: userinfo.sex,
};

下面是使用结构体更新语法的代码示例,可以通过更少的代码实现相同的效果,..语法指定了剩余未显式设置值的字段应有与给定实例对应字段相同的值。

1
2
3
4
rust复制代码let userinfo1  = UserInfo {
name: String::from("rust"),
..userinfo
};

结语

文章首发于微信公众号程序媛小庄,同步于掘金。

码字不易,转载请说明出处,走过路过的小伙伴们伸出可爱的小指头点个赞再走吧(╹▽╹)

本文转载自: 掘金

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

Linux启动tomcat命令行关闭后服务会停止

发表于 2021-11-26

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

​
最近因为公司项目需要迁移到云服务器上、自己一个开发还要兼顾运维的事情真是太难了、由于不熟悉、遇到一些各种各样的小坑、在此记录一下、不足之处还请指正。

问题:Linux启动tomcat命令行关闭后服务会停止?

一般Linux下tomcat服务的启动都是直接切换到 apache-tomcat-8.5.73/bin/目录下执行 ./startup.sh 命令。这样是能成功启动、但是ctrl c退出命令行之后服务会停止。

解决方法:通过执行 nohup ./startup.sh & 命令来启动服务

1
2
3
csharp复制代码[root@ecs-d8ce ~]# cd /data/liuzhuren/apache-tomcat-8.5.73/bin/
[root@ecs-d8ce bin]# nohup ./startup.sh &
[1] 34925

这样问题就成功解决问题了。

扩展知识:

1.nohup

用途:不挂断地运行命令。

语法:nohup Command [ Arg … ] [ & ]

  无论是否将 nohup 命令的输出重定向到终端,最后输出都将附加到当前目录的 nohup.out 文件中里面。

  如果当前目录的 nohup.out 文件不可以写,需要输出重定向到 $HOME/nohup.out 文件中里面。

  如果没有文件能创建或打开以用于追加,那么 Command 参数指定的命令不可调用。

退出状态:该命令返回下列出口值:   

  126 可以查找但不能调用 Command 参数指定的命令。   

  127 nohup 命令发生错误或不能查找由 Command 参数指定的命令。   

  否则,nohup 命令的退出状态是 Command 参数指定命令的退出状态。

2.&

用途:在后台运行

一般两个一起用

1
2
3
csharp复制代码[root@ecs-d8ce ~]# cd /data/liuzhuren/apache-tomcat-8.5.73/bin/
[root@ecs-d8ce bin]# nohup ./startup.sh &
[1] 34925

这样就算在后台运行tomcat、即使退出命令行或断开连接也是可以的。

​大家点赞、收藏、关注、评论啦 、 打卡 文章 更新 105/ 365天

本文转载自: 掘金

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

TCP/IP五层网络协议

发表于 2021-11-26

本文正在参与 “网络协议必知必会”征文活动

C/S & B/S机构

互联网通信软件有两种模式,分别是C/S架构和B/S架构。

C/S架构指的是Client-Server,分别有一个客户端软件和一个服务端软件,比如说计算机上安装的QQ、WeChat、Youku等都属于C/S架构的网络通信软件,需要安装客户端软件。

B/S架构指的是Brower-Server,B/S架 构本质上也是C/S架构,B指的就是浏览器,在这种架构中浏览器就是客户端,比如访问百度,百度就是服务端 ,访问优酷网页端,优酷网页端就是服务端。

计算机之间进行通信从根本上来说还是基于计算机硬件,计算机硬件之间通过物理介质进行通信。

网络通信介绍

网络存在的意义就是跨地域传输数据,也称为通信;互联网的本质就是一系列的网络协议,这些协议规定了计算机如何接入Internet以及接入Internet的标准。比如说中国人和中国人打电话说的是中文,美国人和美国人打电话说的是英文。

因此可以说网络 = 物理链接介质 + 互联网通信协议。

网络通信协议

互联网的核心就是由一堆协议组成,目前来讲世界上通用的语言是英语,如果将计算机比作人的话,互联网协议就是计算机界的英语,如果所有计算机都遵守互联网协议,那么所有的计算机都可以按照统一的标准进行收发信息从而完成通信。这些协议规定了计算机如何接入Internet以及接入Internet的标准。

互联网协议按照功能不同分为osi七层协议或者tcp/ip五层协议或者tcp/ip四层协议。

image-20210606153337950

OSI七层协议是国际标准化组织ISO提出的,但由于是市场原因和该协议本身的缺点(协议实现过于复杂等)导致起初市场上大规模使用的并不是该协议,而是因特网。所以有这样一个现象:得到最广泛应用的不是法律上的国际标准OSI,而是非国际标准TCP/IP五层协议。这样TCP/IP五层成为了事实上的国际标准。

TCP/IP五层协议

将应用层、表示层、会话层合并为应用层,接下来我们就从TCP/IP五层协议的角度介绍每层的功能,理解互联网通信原理。

用户感知到的只有最上面一层协议即应用层,自上而下每层都依赖下一层,因此从最下面一层-物理层还是介绍,每层都有特定的协议,越往上越靠近用户,越往下越靠近硬件。

物理层

计算机之间想要一起happy,就必须接入Internet,就是计算机之间必须完成组网。

物理层的主要功能就是发送电信号,一组物理层的数据称为位,单纯的电信号没有任何数据意义,必须对电信号进行分组才能使这些电信号具有数据意义。

问题就是如何对这些电信号进行分组呢?这就是数据链路层的作用。

数据链路层

由于计算机之间通信的底层是基于电信号的,电信号只有两种状态0和1,但是单纯的电信号没有任何意义,必须对这些电信号进行分组,每组表示什么意义。因此数据链路层的主要作用就是定义电信号的分组方式。

以太网协议

对电信号的分组方式以前是不统一的,后来形成统一的标准后,就是现在的以太网协议,因此,以太网协议规定了数据链路层对电信号的分组方式。

以太网协议规定,一组电信号构成一个数据包,叫做帧,每一个数据帧分成报头head和数据data两部分。

head - 固定包含18个字节 data - 46~1500字节
发送者/源地址:6个字节接收者/目标地址:6个字节数据类型:6个字节 电信号数据包的具体内容

每个数据帧包含的数据最短为64字节,最长为1518字节,如果超过最大限制就分片发送。

mac地址

报头中发送者和接收者的地址又是如何来的呢?以太网协议规定接入Internet的设备都必须具备网卡,发送者和接收者的地址就是指网卡的地址,即mac地址。每块网卡在出场的时候就会被刻上世界上唯一的mac地址。

广播

有了mac地址标识不同的计算机,同一网络内的两台计算机就可以通信了(一台计算机通过ARP协议获取另一台计算机的mac地址),以太网协议采用最原始的方式—广播进行通信,理论上只要有数据链路层就可以实现全世界的网络通信,但是这是非常不合理的,因为全世界的计算机不可能存在于同一个广播域。以太网协议广播的这种通信方式,会产生一个问题,就是发送者发送一个数据包时,在同一个网络中的其他计算机也可以收到这个数据包,数据量太大。

为了解决以太网协议的这种广播靠吼产生的问题,网络层就有了解决方案 — 将计算机放到不同的广播域中。

网络层

通过以太网协议,理论上来说世界上的计算机都可以进行彼此通信,但是世界上的互联网是由一个个彼此隔离的小的局域网组成的,如果所有的通信都采用以太网协议的这种广播进行信息发送,那么一台计算机发送的数据包全世界都能收得到,这已经不只是效率的问题了。因此必须找到一种方式来区分哪些计算机属于同一个广播域,在同一个广播域中的计算机可以通过广播的方式进行数据包的传输。

网络层引入了一套新的地址来区分不同的广播域(子网),这套地址就是网络IP地址。

IP协议

IP协议规定了网络地址,该地址称为IP地址,目前多数还是使用的v4版本ipv4,范围是0.0.0.0~255.255.255.255,一个IP地址通常携程四段十进制数比如112.162.78.49.

IP协议同时规定,一组数据称为一个数据包,数据包分成两部分,头和数据,头包含源IP地址和目标IP地址,数据是传输层发送过来的数据。

IP地址分为两部分,一部分是网络部分,用来标识局域网(子网),另一部分是主机部分,用来标识主机,单纯的ip地址段只是标识了IP地址的种类,从网络部分或主机部分都无法辨识一个ip所处的子网。比如112.162.78.49和112.162.78.94并不能说明这两个IP地址属于同一个局域网。

一个合法的ipv4地址组成部分应当是:IP地址 /子网掩码地址。

子网掩码

子网掩码是表示局域网(子网)特征的一个参数,在形式上等同于IP地址,是一个32位二进制数字,网络部分全部为1,主机部分全部为0。比如,IP地址为172.16.10.1,如果已知网络部分是前24位,主机部分是后8位,那么该网络的子网掩码就是:

11111111.11111111.11111111.00000000,写成十进制就是255.255.255.0

子网掩码的功能就是用来判断任意两个IP地址是否处于同一个子网(局域网),判断两个IP 地址是否属于同一个局域网的方法就是将两个IP地址与子网掩码分别进行与运算(两个数位都为1,运算结果为1,否则为0)。如果比较结果相同,如果相同就表明它们在同一个子网中,否则不在。 判断两个计算机是否属于同一个局域网,比如:

1
2
3
4
5
6
7
8
9
10
11
python复制代码# IP地址和子网掩码配合到一起会计算出一个广播域地址/子网地址/局域网地址
# 计算方式:将IP地址和子网掩码地址转换为二级制 --- 按位与运算
计算机1
172.16.10.1: 10101100.00010000.00001010.00000001
255.255.255.0 11111111.11111111.11111111.00000000
计算结果: 172.16.10.0: 10101100.00010000.00001010.00000000

计算机2
172.16.10.2: 10101100.00010000.00001010.00000010
255.255.255.0 11111111.11111111.11111111.00000000
计算结果: 172.16.10.0: 10101100.00010000.00001010.00000000

如果两个计算机处于同一个局域网,直接靠广播的方式进行数据包的传输,如果不属于同一个局域网,需要通过网关,将数据包输出到公网,根据IP协议传输给另一个网络地址。

ARP协议

计算机通信基本靠吼,即广播的方式,所有上层的包到最后都要封装上以太网头,然后通过以太网协议发送,在谈及以太网协议时候,我们了解到通信是基于mac的广播方式实现,计算机在发包时,获取自身的mac是容易的,如何获取目标主机的mac,就需要通过ARP协议。

ARP协议是通过获取目标计算机的IP地址,从而获取获取其mac地址。

传输层

网络层的IP地址可以区分计算机属于哪个局域网,数据链路层的mac地址可以标识独一无二的计算机,那么如何对计算机上的应用程序进行标识呢?因为计算机发送的数据可能是由不同的应用程序发送,比如QQ、youku,标识计算机上的应用程序就需要靠端口,端口是应用程序与网卡关联的编号,端口的范围是0-65535,0-1023位系统占用端口。

传输层的功能就是建立端口到端口的通信。传输层有两种协议分别是TCP协议和UDP协议。

TCP协议

TCP协议也被称为好人协议,是可靠传输的,TCP数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常TCP数据包的长度不会超过IP数据包的长度,以确保单个TCP数据包不必再分割。

基于TCP协议的通信,客户端与服务端必须建立一个双向通信的通道,客户端和服务端在建立双向连接时需要进行三次握手,断开连接时则需要四次挥手。

UDP协议

UDP协议是不可靠传输的,发送数据之前不需要建立连接,同理发送数据结束也没有连接可以释放,这极大减少了资源开销,但是UDP协议不保证数据的可靠交付,只负责将数据尽可能快的发出去。

应用层

用户使用的都是应用程序,都工作在应用层,互联网是开放的,大家都可以开发自己的应用程序,数据多种多样,就必须规定好应用层的数据的组织形式,因此应用层的功能就是规定应用程序的数据格式。

TCP协议可以为各种各样的程序传递数据,比如邮件、文件等,因此就必须有不同的协议来规定邮件、网页、文件等数据的格式,这些应用程序的协议就构成了应用层,这些协议就包括HTTP协议、SMTP、FTP等协议。

网络通信流程

下面这张图片很好地总结了这几层协议数据传输的封包和解包过程:

img

计算机想要实现网络通信,每台计算机需要具备四种要素:

  • 本机IP地址
  • 子网掩码
  • 网关的IP地址
  • DNS的IP地址

DNS域名解析

在互联网中其实没有类似于www.baidu.com这种域名的概念,而应该是IP地址,比如百度的IP地址是112.112.112.0,在浏览器中应该输入此IP地址,也可以访问百度,但是如今网站的数量数不胜数,如果要记住这么多网站的IP地址实在是一个非常大的挑战,所以DNS就出出现了。

这里并不对DNS进行详细的介绍,如果有想要详细的了解DNS的小伙伴,可以自行搜索相关知识,这对我们正常编程影响不大。

这里只需要知道,DNS的主要作用就是将IP地址解析为域名,比如将112.112.112.0解析为www.baidu.com。

网络通信流程

我们知道基于网络通信的软件有两种:BS架构、CS架构。不同之处在于BS架构没有客户端软件,它依赖浏览器(特殊的客户端)。

在网络通信流程方面的不同之处在于,BS架构的软件在客户端(浏览器网址输入栏)多一个DNS域名解析过程。

在浏览器的网址输入栏上输入一个网址,按回车键到获取页面响应信息发生了以下几件事情:

在浏览器的url输入框中输入想访问的地址.

(1) 在发送http请求之前,需要进行域名解析得到目标服务器的IP地址,查询请求会先找到本地 DNS 服务器来查询是否包含 IP 地址,如果本地 DNS 无法查询到目标 IP 地址,就会向根域名服务器发起一个 DNS 查询用户需要访问的IP地址。

(2) 浏览器和目标服务器建立TCP连接。

(3) 建立连接之后浏览器会向目标服务器发起http请求,请求数据包。

(4) 如果目标服务器是一个简单的页面,就会直接返回。对于需要的重定向的页面,浏览器在获取了重定向响应后,找到重定向地址,重复第一步操作,然后浏览器重新发送请求,携带新的 URL,目标服务器返回数据。

结语

文章首发于微信公众号程序媛小庄,同步于掘金、知乎。

码字不易,转载请说明出处,走过路过的小伙伴们伸出可爱的小指头点个赞再走吧(╹▽╹)

网络协议

本文转载自: 掘金

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

经典的多线程同步问题!详细解析多线程中的生产者和消费者问题

发表于 2021-11-26

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

生产者消费者模型

  • 生产者消费者模型包括生产者,消费者,仓库,产品:
    • 生产者在仓库未满时才能生产,仓库满时停止生产
    • 消费者在仓库有产品时才能消费,仓库空时等待
    • 当消费者发现仓库为空,没有产品可以消费时,通知生产者生产
    • 生产者在生产出可以消费的产品时,通知等待的消费者消费

生产者消费者实现

  • 生产者消费者示例
  • 线程池中可以实现生产者消费者模型
  • 这里通过简单的wait() 和notify() 方式实现生产者消费者模型
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
java复制代码class Depot {
/**
* 仓库容量
*/
private int capacity;
/**
* 仓库的实际容量
*/
private int size = 0;

public Depot(int capacity) {
this.capacity = capacity;
this.size = 0;
}

/**
* 生产方法
*
* @param val 需要生产的数量
*/
public synchronized void produce(int val) {
try {
// want表示想要生产的数量.因为可能想要生产的数量太多,需要进行多次生产
int want = val;
while (want > 0) {
// 如果库存已满时,等待消费者消费产品
while (size >= capacity) {
wait();
}
/*
* 获取实际生产的数量,即新增的产品数量
* - 如果库存和想要生产的数量之和大于仓库容量时,则实际生产数量等于总的容量减去当前容量
* - 如果库存和想要生产的数量之和小于等于仓库容量,则实际生产数量等于想要生产的数量
*/
int increment = (size + want) > capacity ? (capacity - size) : want;
size += increment;
want -= increment;
System.out.printf("%sproduce(%3d) --> want=%3d, increment=%3d, size=%3d\n", Thread.currentThread().getName(), val, want, increment, size);
// 生产完成通知消费者消费
notifyAll();
}
} catch (InterruptedException e) {
// 不需要进行处理
}
}

/**
* 消费方法
*
* @param val 消费的数量
*/
public synchronized void consume(int val) {
try {
// want表示客户消费的数量.因为想要消费的数量太多,需要进行多次消费
int want = val;
while (want > 0) {
// 如果库存为空时,等待生产者生产产品
while (size <= 0) {
wait();
}
/*
* 获取实际消费的数量,即减少的数量
* - 如果库存的数量小于想要消费的数量,则实际消费数量等于库存量
* - 如果库存的数量等于或者大于想要消费的数量,则实际消费数量等于想要消费的数量
*/
int decrease = (size < want) ? size : want;
size -= decrease;
want -= decrease;
System.out.printf("%s consume(%3d) <-- want=%3d, decrease=%3d, size=%3d\n");
// 消费完成通知生产者生产
notifyAll();
}
} catch (InterruptedException e) {
// 不需要进行处理
}
}
}

class Producer {
private Depot depot;

public Producer(Depot depot) {
this.depot = depot;
}

/**
* 新建一个生产者线程,用于生产产品
*
* @param val 需要生产的数量
*/
public void produce(final int val) {
new Thread () {
public void run() {
depot.produce(val);
}
}.start();
}
}

class Consumer {
private Depot depot;

public Consumer(Depot depot) {
this.depot = depot;
}

/**
* 新建一个消费者,用于消费产品
*
* @param val 需要消费的数量
*/
public void consume(final int val) {
new Thread() {
public void run() {
depot.consume(val);
}
}.start();
}
}

class ProducerConsumerTest {
public static void main(String[] args) {
Depot depot = new Depot(100);
Producer producer = new Producer(depot);
Consumer consumer = new Consumer(depot);

producer.produce(60);
producer.produce(120);
consumer.consume(90);
consumer.consume(150);
producer.produce(110);
}
}
1
2
3
4
5
6
7
8
9
console复制代码Thread-0 produce( 60) --> want=  0, increment= 60, size= 60
Thread-4 produce(110) --> want= 70, increment= 40, size=100
Thread-2 consume( 90) <-- want= 0, decrease= 90, size= 10
Thread-3 consume(150) <-- want=140, decrease= 10, size= 0
Thread-1 produce(120) --> want= 20, increment=100, size=100
Thread-3 consume(150) <-- want= 40, decrease=100, size= 0
Thread-4 produce(110) --> want= 0, increment= 70, size= 70
Thread-3 consume(150) <-- want= 0, decrease= 40, size= 30
Thread-1 produce(120) --> want= 0, increment= 20, size= 50
  • Producer是生产者类,与仓库depot关联. 当调用生产者的produce() 方法时,会新建一个线程并向仓库depot中生产产品
  • Consumer是消费者类,与仓库depot关联. 当调用消费者的consume() 方法时,会新建一个线程并消费仓库depot中的商品
  • Depot是仓库类,仓库类Depot中记录仓库的容量capacity, 以及仓库中当前产品的数目size :
    • 仓库类Depot的生产方法produce() 和消费方法consume() 方法都是synchronized方法,进入synchronized方法体,意味着这个线程获取了该仓库对象的同步锁
    • 意味着,同一时间生产者和消费者线程只能有一个能运行. 通过同步锁,实现了对仓库的互斥访问
    • produce()生产方法:
      • 当仓库满时,生产者线程等待,需要等待消费者消费产品后,生产者线程才能生产
      • 生产者线程生产完产品之后,会通过notifyAll() 方法唤醒同步锁上的所有线程,包括消费者线程,这样就可以通知消费者进行消费
    • consume()消费方法:
      • 当仓库为空时,消费者线程等待,需要等待生产者生产产品之后,消费者线程才能消费
      • 消费者线程消费完产品之后,会通过notifyAll() 方法唤醒同步锁上的所有线程,包括生产者线程,这样就可以通知生产者进行生产

本文转载自: 掘金

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

Maven使用教程,建议小白(二) Maven核心概念 Ma

发表于 2021-11-26

Maven核心概念

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

关于作者

  • 作者介绍

🍓 博客主页:作者主页

🍓 简介:JAVA领域优质创作者🥇、一名在校大三学生🎓、在校期间参加各种省赛、国赛,斩获一系列荣誉🏆

🍓 关注我:关注我学习资料、文档下载统统都有,每日定时更新文章,励志做一名JAVA资深程序猿👨‍💻


Maven核心概念

1、坐标

什么是坐标?

坐标在平面几何中坐标(x,y)可以标识平面中唯一的点。

Maven坐标主要组成(以下四个标识决定了Maven的唯一性)

–groupId:定义当前Maven项目隶属项目

  • groupld :定义当前Maven项目隶属的实际项目。首先, Maven项目和实际项目不-定是一-对一 -的关系。比如SpringFrameWork这一实际项目,其对应的Maven项目会有很多,如spring-core spring-contex等。这是由于Maven中模块的概念,因此,一个实际项目往往会被划分成很多模块。其次,groupld不应该对应项目隶属的组织或公司。原因很简单, -个组织下会有很多实际项目,如果groupld只定义到组织级别,而后面我们会看到 , aifactd只能定义Maven项目(模块) , 那么实际项目这个层次将难以定义。最后, groupld的表示方式与Java包名的表达方式类似,通常与域名反向一对应。

–artifactId:定义实际项目中的一个模块

  • artifactld :该元素定义当前实际项目中的一-个Maven项目(模块) , 推荐的做法是使用实际项目名称作为artifactld的前缀。比如上例中的my-app。

–version:定义当前项目的当前版本

  • version :该元素定义Maven项目当前的版本

–packaging:定义该项目的打包方式

  • packaging :定义Maven项目打包的方式,首先,打包方式通常与所生成构件的文件扩展名对应,如上例中的packaging为jar,最终的文件名为my-app-0.1-SNAPSHOT.jar。也可以打包成war, ear等。当不定义packaging的时候 , Maven会使用默认值jar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.rjxy.maven</groupId>
<artifactId>Hello</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Hello</name>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

Maven为什么使用坐标?

  • Maven世界拥有大量构建,我们需要找一个用来唯一标识一个构建的统一规范
  • 拥有了统一规范,就可以把查找工作交给机器

2、 依赖的范围

依赖声明主要包含如下元素:

1
2
3
4
5
6
7
8
xml复制代码<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
<scope>test</scope>
</dependency>
</dependencies>

其中依赖范围scope 用来控制依赖和编译,测试,运行的classpath的关系. 主要的是三种依赖关系如下:

  1. compile: 默认编译依赖范围。对于编译,测试,运行三种classpath都有效
  2. test:测试依赖范围。只对于测试classpath有效
  3. provided:已提供依赖范围。对于编译,测试的classpath都有效,但对于运行无效。因为由容器已经提供,例如servlet-api(Tomcat)
  4. runtime:运行时提供。例如:jdbc驱动

image-20210924130351620

3、仓库管理

什么是Maven仓库?

用来统一存储所有Maven共享构建的位置就是仓库

Maven仓库布局

  • 根据Maven坐标定义每个构建在仓库中唯一存储路径

image-20210924135028633

仓库的分类

  • 本地仓库
    ~/.m2/repository/(默认的仓库目录)
    每个用户只有一个本地仓库(需要从中央仓库中下载资源存到本地仓库)
  • 远程仓库
+ 中央仓库:Maven默认的远程仓库 [repo1.maven.org/maven2](http://repo1.maven.org/maven2)
+ 私服:是一种特殊的远程仓库,它是架设在局域网内的仓库


![image-20210924135412539](https://gitee.com/songjianzaina/juejin_p7/raw/master/img/e31bed022e683a06707bd58b402063bdfb22af9a51e25786b77d60316baeff2a)
+ 镜像:用来替代中央仓库,速度一般比中央仓库快

4、 生命周期

何为生命周期?

  • Maven生命周期就是为了对所有的构建过程进行抽象和统一
  • 包括项目清理,初始化,编译,打包,测试,部署等几乎所有构建步骤

Maven三大生命周期

  • clean:清理项目的
  • default:构建项目的
  • site:生成项目站点的

生命周期Maven有三套相互独立的生命周期,请注意这里说的是“三套”,而且“相互独立”,这三套生命周期分别是:

  • Clean Lifecycle 在进行真正的构建之前进行一些清理工作。
  • Default Lifecycle 构建的核心部分,编译,测试,打包,部署等等。
  • Site Lifecycle 生成项目报告,站点,发布站点。

注意:

  • 再次强调一下它们是相互独立的,你可以仅仅调用clean来清理工作目录,仅仅调用site来生成站点。当然你也可以直接运行 mvn clean install site 运行所有这三套生命周期。

Clean生命周期包含了三个阶段:

  • pre-clean 执行一些需要在clean之前完成的工作
  • clean 移除所有上一次构建生成的文件
  • post-clean 执行一些需要在clean之后立刻完成的工作

clean生命周期每套生命周期都由一组阶段(Phase)组成,我们平时在命令行输入的命令总会对应于一个特定的阶段。比如,运行mvn clean ,这个的clean是Clean生命周期的一个阶段。有Clean生命周期,也有clean阶段。

mvn clean 中的clean就是上面的clean,在一个生命周期中,运行某个阶段的时候,它之前的所有阶段都会被运行,也就是说,mvn clean 等同于 mvn pre-clean clean ,如果我们运行 mvn post-clean ,那么 pre-clean,clean 都会被运行。这是Maven很重要的一个规则,可以大大简化命令行的输入。

Site生命周期

Site生命周期pre-site 执行一些需要在生成站点文档之前完成的工作;site 生成项目的站点文档 ;post-site 执行一些需要在生成站点文档之后完成的工作,并且为部署做准备;site-deploy 将生成的站点文档部署到特定的服务器上; 这里经常用到的是site阶段和site-deploy阶段,用以生成和发布Maven站点,这可是Maven相当强大的功能,Manager比较喜欢,文档及统计数据自动生成,很好看。

Default生命周期

Default生命周期Default生命周期是Maven生命周期中最重要的一个,绝大部分工作都发生在这个生命周期中。这里,只解释一些比较重要和常用的阶段:

img

运行任何一个阶段的时候,它前面的所有阶段都会被运行,这也就是为什么我们运行mvn install 的时候,代码会被编译,测试,打包。此外,Maven的插件机制是完全依赖Maven的生命周期的,因此理解生命周期至关重要。

5、Maven插件

  • 插件目标
    Maven的核心仅仅定义了抽象的生命周期,具体的任务都是交由插件完成
  • 每个插件都能实现多个功能,每个功能就是一个插件目标
  • Maven的生命周期与插件目标相互绑定,以完成某个具体的构建任务例如compile就是插件maven-compiler-plugin的一个插件目标。例如:mvn compile或者全写 mvn compiler :compile

image-20210924152722238

后语

厂长写博客目的初衷很简单,希望大家在学习的过程中少走弯路,多学一些东西,对自己有帮助的留下你的赞赞👍或者关注➕都是对我最大的支持,你的关注和点赞给厂长每天更文的动力。

对文章其中一部分不理解,都可以评论区回复我,我们来一起讨论,共同学习,一起进步!

微信(z613500)或者 qq(1016942589) 详细交流。

本文转载自: 掘金

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

操作系统学习笔记(十四)~进程同步单元测试 前言

发表于 2021-11-26

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

前言

Hello!小伙伴!

非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您指出~

自我介绍 ଘ(੭ˊᵕˋ)੭

昵称:海轰

标签:程序猿|C++选手|学生

简介:因C语言结识编程,随后转入计算机专业,有幸拿过一些国奖、省奖…已保研。目前正在学习C++/Linux/Python

学习经验:扎实基础 + 多做笔记 + 多敲代码 + 多思考 + 学好英语!

1、有界缓冲问题中,“counter++”的伪机器语言为:

1
2
3
4
5
cpp复制代码(S0)register1 = counter

(S1)register1 = register1 + 1

(S2)counter = register1

当两个生产者并发执行“counter++”时,有()中不同的运行结果。B
A.3
B.2
C.1
D.4

解释:假设counter初值为5,那么正确结果就是7,但是并发执行是,可能两次的S1一起执行,也就是counter就加了1,(都是register1+1,resgister值没有变,所以相当于就+1),错误结果就可能为6。那为什么没有5、9…呐? 这里可以这样思考:register必定会+1,不可能一次都不会加,而对多也只能加两次,所以最多就是7。

2、存在忙等的信号量是()。B
A.记录型信号量
B.整型信号量
C.AND信号量
D.二值信号量

解释:这里是存在忙等,而记录型信号量则是去除忙等。
在这里插入图片描述)在这里插入图片描述
在这里插入图片描述

3、在生产者消费者问题中,消费者执行Wait(full)后阻塞的原因是()。B
A.full=0
B.full<1
C.full>1
D.full=1

解释:在这里插入图片描述
4、读者优先的读者写者问题中,写者可以进入写的前提条件是()。C
A.其它
B.没有读者在读
C.既没有读者在读,也没有写者在写
D.没有写者在写

5、系统中有10个并发进程涉及某个相同的变量A,则变量A的相关临界区最少有()个。A
A.10
B.20
C.1
D.2

6、进程P1对信号量S执wait(S)操作,则信号量S的值应()。B
A.不变
B.减1
C.加1
D.不定

解释:在这里插入图片描述

7、有m个进程共享同一临界资源,若使用信号量机制实现对这一临界资源的互斥访问,则信号量的变化范围是()。A
A.-(m-1)1
B.0
m
C.01
D.0
(m-1)

解释:最多一个进程运行,所以最大值为1,而最多可以(m-1)个进程等待,所以最小为-(m-1)。【负数的绝对值表示等待进程的个数】

8、设两个进程共用一个临界资源的互斥信号量mutex,当mutex=-1时表示()。B
A.没有一个进程进入临界区
B.一个进程进入了临界区,另一个进程等待
C.两个进程都进入临界区
D.两个进程都在等待

解释:|-1|=1,说明有一个进程在等待

9、若有10个进程共享同一程序段,而且每次最多允许5个进程进入该程序段,则互斥信号量的变化范围是()。B
A.110
B.(-5)
5
C.510
D.(-5)
1

10、当信号量的值等于0时,以下描述错误的是()。B
A.再有进程申请信号量将阻塞
B.该信号量的值不能比0更小
C.目前没有信号量可用
D.申请该信号量的进程无法进入临界区

11、在Hoare管程中,有关条件变量x的操作signal()的描述正确的是()。A、B
A.x的条件队列非空时,唤醒该条件队列的第一个等待进程,执行该操作进程进入紧急队列
B.x的条件队列空时是空操作,执行该操作进程继续运行
C.x的条件队列空时是空操作,执行该操作进程进入紧急队列
D.x的条件队列非空时,唤醒该条件队列的第一个等待进程,执行该操作进程继续运行

解释:
在这里插入图片描述

12、在读者写者问题中,读者可以进入缓冲区读的条件是()。A、B
A.有写者等,但有其它读者在读
B.无读者、写者在读写
C.有写者写
D.有读者在等待

13、临界区使用的准则包括()。B、C、D
A.同步
B.有限等待
C.互斥
D.有空让进

解释:在这里插入图片描述

14、有关信号量S的描述,正确的是()。B、D
A.S初值不能为0
B.除了初始化,只能通过执行P、V操作来访问S
C.S的值不为负
D.S必须置一次且只能置一次初值

解释:在这里插入图片描述

15、在生产者消费者问题中,消费者调用wait(mutex)(mutex是互斥信号量)阻塞的条件是()。B、D
A.缓冲区空
B.有消费者进入在从缓冲区读产品
C.缓冲区满
D.有生产者进入在往缓冲区存放产品

16、二值信号量的值区间为0-1。×

17、一次允许多个进程使用的资源称为共享资源。√

18、同步操作时,一个信号量的P、V操作一般处于同一进程。×

解释:P(W)是测试是否为第一个读者,而V(W)则是测试是否为最后一个读者,每一个读者属于不同的进程,故P、V操作不在一个进程中
在这里插入图片描述

19、整型信号量是一个整数,如果一个进程要申请信号量时发现该信号量大于等于0则表示可以获得信号量,小于0则表示无法获得信号量。×

20、记录型信号量增加了一个等待队列,当一个进程无法获得一个信号量时,马上释放CPU并把自己转换为等待状态,加入该信号量的等待队列,从而消除忙等。√

本文转载自: 掘金

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

1…174175176…956

开发者博客

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