MoreRSS

site iconAnran758修改

Web工程师/ FE
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Anran758的 RSS 预览

Promise 与异步编程

2023-12-18 11:11:17

Promise 是 JavaScript 中的一个重要概念,与前端的工作更是息息相关。因此本文将整理一下 Promise 在日常工作中的应用。

目录

概念

MDN | 使用 Promise 中我们能学习到 Promise 的基础使用与错误处理、组合等概念,可以将 Promise 的特点概括为:

  • Promise 对象有三种状态,且状态一旦改变就不会再变。其值记录在内部属性 [[PromiseState]] 中:
    • pending: 进行中
    • fulfilled: 已成功
    • rejected: 已失败
  • 主要用于异步计算,并且可以将异步操作队列化 (链式调用),按照期望的顺序执行,返回符合预期的结果。
  • 可以在对象之间传递和操作 Promise,帮助我们处理队列
  • 链式调用的写法更简洁,可以避免回调地狱

在现实工作中,当我们使用 Promise 时更多是对请求的管理,由于不同请求或任务的异步性。因此我们会根据不同的使用场景处理 Promise 的调度。

async/await

async/await 是基于 Promise 的一种语法糖,使得异步代码的编写和理解更加像同步代码。

当一个函数前通过 async 关键字声明,那这个函数的返回值一定会返回一个 Promise,即便函数返回的值与 Promise 无关,也会隐式包装为 Promise 对象:

1
2
3
4
5
6
async function getAge() {
return 18;
}

getAge().then(age => console.log(`age: ${age}`))
// age: 18

await

await 操作符通常和 async 是配套使用的,它会等待 Promise 并拆开 Promise 包装直接取到里面的值。当它处于 await 状态时,Promise 还处于 ``,后续的代码将不会被执行,因此看起来像是 “同步” 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function delayResolve(x, timeout = 2000) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(x);
}, timeout);
});
}

async function main() {
const x = await delayResolve(2, 2000);
console.log(x);

const y = await delayResolve(1, 1000);
console.log(y);
}

main();
// 2
// 1

错误处理

async/await 的错误处理通过是通过 try..catch 来捕获错误。当然,我们也会根据实际业务的需求来 catch 我们真正需要处理的问题。

1
2
3
4
5
6
7
8
9
try {
const response = await axios.get('https://example.com/user');

// 处理响应数据
console.log('User data fetched:', response.data);
} catch (error) {
console.error('Error response:', error.response);
// 做其他错误处理
}

学习了前文的基础概念后,我们可以更近一步的探讨 Promise 的使用。

Promise 串联

Promise 串联一般是指多个 Promise 操作按顺序执行,其中每个操作的开始通常依赖于前一个操作的完成。这种串行执行的一个典型场景是在第一个异步操作完成后,其结果被用作第二个异步操作的输入,依此类推。

考虑以下场景:加载系统时,需要优先读取用户数据,同时需要用户的数据去读取用户的订单的信息,再需要两者信息生成用户报告。因此这是一个存在前后依赖的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function fetchUserInfo(userId) {
return axios.get(`/api/users/${userId}`);
}

function fetchUserOrders(userId) {
return axios.get(`/api/orders/${userId}`);
}

function generateReport(userInfo, orders) {
// 根据用户信息和订单生成报告
return {
userName: userInfo.name,
totalOrders: orders.length,
// ...其他报告数据
};
}

常规处理方法

处理串联请求无非有两种方法:

方法 1: 链式调用 .then()

在这种方法中,我们利用 .then() 的链式调用来处理每个异步任务。这种方式的优点是每个步骤都明确且连贯,但可能导致所谓的“回调地狱”,尤其是在处理多个串联的异步操作时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const userId = '12345'; // 假设已知的用户ID

fetchUserInfo(userId)
.then(response => {
const userInfo = response.data;
return fetchUserOrders(userInfo.id); // 使用用户ID获取订单
})
.then(response => {
const orders = response.data;
return generateReport(userInfo, orders); // 生成报告
})
.then(report => {
console.log('用户报告:', report);
})
.catch(error => {
console.error('在处理请求时发生错误:', error);
});

方法 2: 使用 async/await

async/await 提供了一种更加直观、类似同步的方式来处理异步操作。它使代码更易于阅读和维护,特别是在处理复杂的异步逻辑时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function getUserReport(userId) {
try {
const userInfoResponse = await fetchUserInfo(userId);
const userInfo = userInfoResponse.data;

const userOrdersResponse = await fetchUserOrders(userInfo.id);
const orders = userOrdersResponse.data;

const report = generateReport(userInfo, orders);
console.log('用户报告:', report);
} catch (error) {
console.error('在处理请求时发生错误:', error);
}
}

const userId = '12345'; // 假设已知的用户ID
getUserReport(userId);

在这个示例中,使用 async/await 使得代码的逻辑更加清晰和直观,减少了代码的嵌套深度,使错误处理变得简单。

串联自动化

以上是日常工作中最常见的需求.但这里我们还可以发散一下思维,考虑更复杂的情况:

现在有一个数组,数组内有 10 个或更多的异步函数,每个函数都依赖前一个异步函数的返回值需要做处理。在这种请求多了的特殊情况下我们手动维护会显得很冗余,因此可以通过循环来简化逻辑:

方法 1: 通过数组方法 reduce 组合

1
2
3
4
5
6
7
8
9
10
11
const processFunctions = [processStep1, processStep2, processStep3, ...];

processFunctions.reduce((previousPromise, currentFunction) => {
return previousPromise.then(result => currentFunction(result));
}, Promise.resolve(initialValue))
.then(finalResult => {
console.log('最终结果:', finalResult);
})
.catch(error => {
console.error('处理过程中发生错误:', error);
});

方法 2: 循环体和 async/await 的结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function handleSequentialTasks(tasks, initialResult) {
let result = initialResult;

try {
for (const task of tasks) {
result = await task(result);
}
console.log('最终结果:', result);
} catch (error) {
console.error('处理过程中发生错误:', error);
}
}

const tasks = [task1, task2, task3, ...];
handleSequentialTasks(tasks, initialValue);

Promise 并发

并发(Concurrency)在编程中是指多个任务在同一时间段内启动、执行,但不一定同时完成。在 JavaScript 的 Promise 中,并发通常涉及同时开始多个异步操作,并根据它们何时解决(fulfilled)或被拒绝(rejected)来进行相应的处理。

Promise 的并发会比串联的场景更复杂。Promise 对象提供了几个静态方法来处理并发情况,让开发者可以根据不同的使用场景选择合适的方法:

Promise.all(iterable)

Promise.all 静态方法接受一个 Promise 可迭代对象作为输入,当传入的数组中每个都被 resolve 后返回一个 Promise。若任意一个 Promise 被 reject 后就 reject。

1
2
3
4
5
6
7
8
9
10
11
const promise1 = fetch('https://example.com/api/data1');
const promise2 = fetch('https://example.com/api/data2');

Promise.all([promise1, promise2])
.then(([data1, data2]) => {
console.log('所有数据已加载:', data1, data2);
})
.catch(error => {
console.error('加载数据时发生错误:', error);
});

Promise.allSettled(iterable)

Promise.allSettled 方法同样接受一个 Promise 的可迭代对象。不同于 Promise.all,这个方法等待所有传入的 Promise 都被解决(无论是 fulfilled 或 rejected),然后返回一个 Promise,它解决为一个数组,每个数组元素代表对应的 Promise 的结果。这使得无论成功还是失败,你都可以得到每个 Promise 的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
Promise.allSettled([
Promise.resolve(33),
new Promise((resolve) => setTimeout(() => resolve(66), 0)),
99,
Promise.reject(new Error("an error")),
]).then((values) => console.log(values));

// [
// { status: 'fulfilled', value: 33 },
// { status: 'fulfilled', value: 66 },
// { status: 'fulfilled', value: 99 },
// { status: 'rejected', reason: Error: an error }
// ]

Promise.race(iterable)

Promise.race 方法接受一个 Promise 的可迭代对象,但与 Promise.allPromise.allSettled 不同,它不等待所有的 Promise 都被解决。相反,Promise.race 返回一个 Promise,它解决或被拒绝取决于传入的迭代对象中哪个 Promise 最先解决或被拒绝。

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
function sleep(time, value, state) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (state === "fulfill") {
return resolve(value);
} else {
return reject(new Error(value));
}
}, time);
});
}

const p1 = sleep(500, "one", "fulfill");
const p2 = sleep(100, "two", "fulfill");

Promise.race([p1, p2]).then((value) => {
console.log(value); // "two"
// Both fulfill, but p2 is faster
});

const p3 = sleep(100, "three", "fulfill");
const p4 = sleep(500, "four", "reject");

Promise.race([p3, p4]).then(
(value) => {
console.log(value); // "three"
// p3 is faster, so it fulfills
},
(error) => {
// Not called
},
);

const p5 = sleep(500, "five", "fulfill");
const p6 = sleep(100, "six", "reject");

Promise.race([p5, p6]).then(
(value) => {
// Not called
},
(error) => {
console.error(error.message); // "six"
// p6 is faster, so it rejects
},
);

Promise.any(iterable)

Promise.any 接受一个 Promise 的可迭代对象,并返回一个 Promise。它解决为迭代对象中第一个被解决的 Promise 的结果。如果所有的 Promise 都被拒绝,Promise.any 会返回一个 AggregateError 实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, "one");
});

const promise2 = new Promise((resolve, reject) => {
setTimeout(reject, 100, "two");
});

Promise.race([promise1, promise2])
.then((value) => {
console.log("succeeded with value:", value);
})
.catch((reason) => {
// Only promise1 is fulfilled, but promise2 is faster
console.error("failed with reason:", reason);
});
// failed with reason: two

控制批次

JavaScript 默认提供的并发处理函数很方便我们根据业务场景的不同来处理请求,但显然我们工作中所遇到的需求得考虑更复杂的情况,还需要进一步的封装和扩展我们的 API。

在服务器端编程,我们经常遇到需要批量处理数据的场景。例如,批量修改数据库中的用户数据。在这种情况下,由于数据库操作的性能限制或者 API 调用限制,我们不能直接一口气修改全部,因为短时间内发出太多的请求数据库也会处理不来导致应用性能下降。因此,我们需要一种方法来限制同时进行的操作任务数,以保证程序的效率和稳定性。

我们代入实际业务场景:假设有一个社区组织了一次大型户外活动,活动吸引了大量参与者进行在线报名和付费。由于突发情况(比如恶劣天气或其他不可抗力因素),活动不得不取消。这时,组织者需要对所有已付费的参与者进行退款。

活动组织者发起「解散活动」后,服务端接收到请求后当然也不能一次性全部执行退款的操作啦,毕竟一场活动说不定有上千人。因此我们需要分批次去处理。

在上述社区活动退款的例子中,服务器端处理退款请求的一个有效方法是实施分批次并发控制。这种方法不仅保护了后端服务免受过载,还确保了整个退款过程的可管理性和可靠性。

分批次处理时有以下关键问题需要考虑:

  1. 批次大小:确定每个批次中处理的退款请求数量。这个数字应基于服务器的处理能力和支付网关的限制来确定。
  2. 批次间隔:设置每个批次之间的时间间隔。这有助于避免短时间内发出过多请求,从而减轻对数据库和支付网关的压力。
  3. 错误处理:在处理退款请求时,应妥善处理可能发生的错误,并确保能够重新尝试失败的退款操作。

简易版并发控制

将所有待处理的异步任务(如退款请求)存放在一个 tasks 数组中,在调用并发请求前将 tasks 数组分割成多个小批次,每个批次包含固定数量的请求。每当前一个批次处理完后,才处理下一个批次的请求,直到所有批次的请求都被处理完毕:

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
// 假设这些是返回 Promise 的函数
const tasks = [task1, task2, task3, ...];

// 分割任务数组为批次
function splitIntoBatches(tasks, batchSize) {
let batches = [];
for (let i = 0; i < tasks.length; i += batchSize) {
batches.push(tasks.slice(i, i + batchSize));
}
return batches;
}

// 处理单个批次的函数
function processBatch(batch) {
return Promise.all(batch.map(task => task()));
}

// 控制并发的主函数
async function processTasksInBatches(tasks, batchSize) {
const batches = splitIntoBatches(tasks, batchSize);

for (const batch of batches) {
await processBatch(batch);
// 可以在这里加入日志记录或其他处理
console.log('一个批次处理完毕');
}

console.log('所有批次处理完毕');
}

// 调用主函数,假设每批次处理 10 个任务
processTasksInBatches(tasks, 10);

这种写法实现的并发简单易懂,也易于维护,在一些并发压力不大,比较简单的业务场景来看是足够了。

但如果我们将这种处理方式放在时序图上进行分析,就能发现服务器可能有能力处理更多的并发任务,而这种方法可能没有充分利用可用资源。每个批次开始前会依赖于上一个批次中请求响应时间最慢的那一个,因此我们还可以进一步考虑优化并发实现方案。

动态任务队列

在之前的 “控制批次” 方法中,我们发现固定处理批次的局限性,尤其是在并发任务数量较大时可能导致的资源利用不足。为了解决这个问题,我们可以考虑采用一种更灵活的方法:维护一个动态的任务队列来处理异步请求:

  • 任务队列:创建一个任务队列,其中包含所有待处理的异步任务。
  • 动态出队和入队:当队列中的任务完成时,它会被移出队列,同时根据当前的系统负载和任务处理能力,从待处理任务列表中拉取新的任务进入队列。
  • 并发数控制:设置一个最大并发数,确保任何时候处理中的任务数量不会超过这个限制。

我们封装一个函数,提供 concurrency 参数作为并发限制:

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
function parallelLimit(tasks, {concurrency = 10}) {
const results = [];
const executing = new Set();

let currentlyRunning = 0;
let currentIndex = 0;

return new Promise((resolve) => {
const next = () => {
if (currentIndex < tasks.length) {
// 取出记录数,准备执行
const index = currentIndex;
const task = tasks[index];

currentIndex += 1
currentlyRunning += 1;

const resultPromise = task().then((result) => {
// 任务执行完毕,更新运行数、保存结果
currentlyRunning -= 1;
results[index] = result;
executing.delete(resultPromise);

// 开启下一个任务
next();
});

executing.add(resultPromise);

// 当前运行的任务数小于限制并且还有任务未开始时,继续添加任务
if (currentlyRunning < concurrency && currentIndex < tasks.length) {
next();
}
} else if (currentlyRunning === 0) {
// 所有任务都已完成
resolve(results);
}
};

// 初始化
for (let i = 0; i < Math.min(concurrency, tasks.length); i += 1) {
next();
}
});
}

该函数会在初始阶段会按照并发数先同步执行指定任务数,若某个任务执行完毕后,在执行完毕的回调中会唤醒下一个任务,直至任务队列执行完毕。

以下添加一些测试数据用于测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
function asyncTask(id) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`任务 ${id} 完成`);
resolve(`结果 ${id}`);
}, Math.random() * 2000);
});
}
const taskArray = Array.from({ length: 10 }, (_, i) => () => asyncTask(i + 1));

parallelLimit(taskArray, {concurrency: 3}).then((results) => {
console.log('所有任务完成:', results);
});

第三方库

在实际的项目开发中,特别是面临复杂的并发处理需求时,我们更多会考虑使用成熟的第三库来处理业务问题,它们具备更完善的测试用例来检验边界情况。

处理并发的热门库有 RxJSp-mapasync.js

  • RxJS 是一个以响应式编程为核心的库,竟然搭配 Angular 在网页端搭配使用,提供了丰富的操作符和方法来处理异步事件和数据流。
  • p-mapasync.js 包的体积更小,更适合在服务端中使用。p-map 专注于提供并发控制功能,而 async.js 提供包括并发控制、队列管理等广泛的异步处理模式,功能会更全。

笔者在 Node.js 环境下只需要处理并发问题,故用的 p-map 会更多一些。下面简要介绍 p-map 的使用:

1
2
3
4
5
6
7
8
9
import pMap from 'p-map';

const list = Array.from({ length: 10 }, (_, i) => i)

pMap(list, asyncTask, { concurrency: 3 })
.then((results) => {
console.log('所有任务完成:', results);
});

p-map源码实现很精简,建议想深入复习并发的同学去阅读其底层代码的实现作为参考思路。

总结

在本文中,我们首先回顾了 Promise 的基本概念及其在 JavaScript 异步编程中的常用方法。通过这个基础,我们能够更好地理解如何有效地处理和组织异步代码。

随后,我们深入到并发处理的实际应用场景,探讨了如何根据具体需求选择合适的并发实现策略。我们讨论了从简单的批次控制到更复杂的动态任务队列的不同方法,展示了在不同场景下优化异步任务处理的多种可能性。

但值得注意的是,我们自行实现的并发控制工具在没有做足测试用例测试时,可能不适合直接应用于生产环境。在实际的项目开发中,选择成熟且持续维护的第三方库往往是更安全和高效的选择。比如笔者选择的 p-map 稳定性和可靠性相比上文简单实现的版本将会更好。


参考资料

数据结构实践

2022-05-18 23:18:13

本篇将根据自考实践要求对「数据结构」一科进行简要的复习,代码实现使用 C++ 语言实现。

实践

已知 Q 是一个非空队列,S 是一个空栈。编写算法,仅用队列和栈的 ADT 函数和少量工作变量,将队列 Q 的所有元素逆置。

栈的基本 ADT 函数有:

  1. 置空栈。函数原型为: void MakeEmpty(SqStack s);
  2. 元素e入栈。函数原型为: void Push(SqStack s,ElemType e);
  3. 出栈,返回栈顶元素。函数原型为: ElemType pop(SqStack s);
  4. 判断栈是否为空。函数原型为: int isEmpty(SqStack s);

队列的基本ADT函数有:

  1. 元素e入队。函数原型为:void enQueue(Queue q,ElemType e);
  2. 出队,返回队头元素。函数原型为:ElemType deQueue(Queue q);(3)(3)判断队是否为空。函数原型为:int isEmpty(Queue q);

题目要求:

  1. 编程实现队列和栈的ADT函数
  2. 仅用队列和栈的ADT函数和少量工作变量,编写将队列Q的所有元素逆置的函数
  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
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
// 栈的基本 ADT 函数有:

// 1. 置空栈。函数原型为: `void MakeEmpty(SqStack s);`
// 2. 元素e入栈。函数原型为: `void Push(SqStack s,ElemType e);`
// 3. 出栈,返回栈顶元素。函数原型为: `ElemType pop(SqStack s);`
// 4. 判断栈是否为空。函数原型为: `int isEmpty(SqStack s);`
#include <iostream>

using namespace std;

#define StackSize 10
typedef int ElemType;

// 栈结构
class SqStack {
private:
ElemType data[StackSize];
int top;
public:
SqStack(): top(-1) {}

// 1. 置空栈
void makeEmpty() {
this->top = -1;
}

// 2. 元素e入栈
void push(ElemType e) {
if (this->isFull()) {
std::cout << "栈满" << std::endl;
return;
}

this->data[++this->top] = e;
}

// 3. 出栈,返回栈顶元素
ElemType pop() {
if (this->isEmpty()) {
std::cout << "栈空" << std::endl;
return -1;
}

return this->data[this->top--];
}

// 4. 判断栈是否为空
bool isEmpty() {
return this->top == -1;
}

// 5. 栈满
int isFull() {
return this->top == StackSize;
}
};

// 队列的基本ADT函数有:

// (1)元素e入队。函数原型为:void enQueue(Queue q,ElemType e);
// (2)出队,返回队头元素。函数原型为:ElemType deQueue(Queue q);(
// (3)判断队是否为空。函数原型为:int isEmpty(Queue q);
#define QueueSize 10

// 队列结构
class Queue {
private:
ElemType data[QueueSize];
int front, real;
public:
Queue(): front(0), real(0) {}

// 队列是否已满
int isQueueFull() {
return (this->real + 1) % QueueSize == this->front;
}

// 元素e入队
void enQueue(ElemType e) {
if (isQueueFull()) {
std::cout << "队列满" << std::endl;
return;
}

this->data[this->real] = e;
// 循环意义下的 +1
this->real = (this->real + 1) % QueueSize;
}

// 出队列
ElemType deQueue() {
if (this->isEmpty()) {
std::cout << "队列空" << std::endl;
return -1;
}

ElemType e = this->data[this->front];
this->front = (this->front + 1) % QueueSize;

return e;
}

// 判断队列是否为空
int isEmpty() {
return this->front == this->real;
}
};

// 队列元素倒序, 这里注意要用 & 取引用才有副作用
void reverseQueue(Queue &q) {
SqStack s;
int val;

while (!q.isEmpty()) {
val = q.deQueue();
s.push(val);
}

while (!s.isEmpty()) {
val = s.pop();
q.enQueue(val);
}
}

// (1) 编程实现队列和栈的ADT函数
// (2) 仅用队列和栈的ADT函数和少量工作变量,编写将队列Q的所有元素逆置的函数。
// (3) 测试该函数
int main() {
cout << "准备测试 stack 数据结构" << endl;
SqStack s;

cout << "[stack] 1. test SqStack.push" << endl;
int testData1[] = {109, 108, 107};
for (int i = 0; i < 3; i++) {
s.push(testData1[i]);
}

cout << "[stack] 2. test SqStack.isEmpty: " << (s.isEmpty() ? "" : "非") << "空栈" << endl;
cout << "[stack] 3. test SqStack.pop: " << s.pop() << endl;
cout << "[stack] 4. test SqStack.makeEmpty" << endl;
s.makeEmpty();

cout << "[stack] 5. check stack now is empty: " << (s.isEmpty() ? "" : "非") << "空栈" << endl;
cout << "============================================" << endl;

cout << "准备测试 queue 数据结构" << endl;
Queue q;

cout << "[queue] 1. test SqStack.push" << endl;
for (int i = 0; i < 3; i++) {
q.enQueue(testData1[i]);
}

cout << "[queue] 2. test Queue.isEmpty: " << (q.isEmpty() ? "空队列" : "非空队列") << endl;
while (!q.isEmpty()) {
cout << "[queue] 3. test Queue.pop: " << q.deQueue() << endl;
}

cout << "[queue] 4. check queue now is empty: " << (q.isEmpty() ? "空队列" : "非空队列") << endl;
cout << endl << endl;
cout << "============================================" << endl;

cout << "仅用队列和栈的ADT函数和少量工作变量,编写将队列Q的所有元素逆置的函数。" << endl;
const int reverseTestData[] = {11,12,13,14,15};
for (int i = 0; i < 5; i++) {

q.enQueue(reverseTestData[i]);
}
reverseQueue(q);

while(!q.isEmpty()) {
cout << "reverseQueue deQueue: " << q.deQueue() << endl;
}

return 0;
}

排序

选择排序

基本思想: 每一趟在待排序的记录中选出关键字最小的记录,依次存放在已排好序的记录序列的最后,直到全部排序完为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

#include <iostream>
using namespace std;

void SelectSort(int arr[], int n) {
int k;
for (int i = 0; i < n; i++) {
k = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[k]) {
k = j;
}
}

if (k != i) {
swap(arr[i], arr[k]);
int temp = arr[i];
arr[i] = arr[k];
arr[k] = temp;
}
}
}

int main() {
int arr[] = {4, 3, 2, 9, 8, 6, 7, 1, 5, 10};
int n = sizeof(arr) / sizeof(arr[0]);
cout << sizeof(arr[0]) << "\n";

SelectSort(arr, n);

for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
}

插入排序

基本思想: 每次将一个待排序的记录按其关键字的大小插入到前面已经排序好的文件中的适当位置,直到全部记录插入完位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void InsertSort(int arr[], int n) {
int i, j, tmp;
// 对顺序表做直接插入排序
for(i = 1; i < n; i++) {
// 当前值比上一个值小,则交换位置
if (arr[i] < arr[i - 1]) {

tmp = arr[i];
// 对有序区逐项向后 diff,寻找合适的插入位置
for(j = i - 1; j >= 0 && tmp < arr[j]; j--) {
arr[j + 1] = arr[j];
}
arr[j + 1] = tmp;
}
}
}

冒泡排序

冒泡排序的基本思想是:通过相邻元素之间的比较和交换,使娇小的元素逐渐从底部移向顶部,就像水底下气泡一样逐渐向上冒泡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void BubbleSort(int *arr, int n) {
int i, j, flag, temp;
for (i = 0; i < n; i++) {
flag = 0;

// 从右向左对比
for (j = n - 1; j >= i; j--) {
if (arr[j] < arr[j - 1]) {
swap(arr[j], arr[j - 1]);
// temp = arr[j];
// arr[j] = arr[j - 1];
// arr[j - 1] = temp;
flag = 1;
}
}

if (flag == 0) return;
}
}

MySQL 实践

2022-05-16 11:25:00

由于自考的实践考核要求有需要用到 mysql 进行考核,故记录一下在 mac 环境下试手的笔记。

初始环境

首先在 mysql 官网中下载你想要的版本。可以直接下载 dmg 安装包,按照安装指示一步一步安装,并设置 mysql 的密码。

下载完毕后,一般情况下直接通过命令行使用 mysql 命令会找不到对应的命令:

1
2
➜  ~ mysql -v
zsh: command not found: mysql

因此需要对当前的命令行工具配置对应的环境变量,比如笔者使用的是 zsh,则打开 ~/.zshrc 文件添加以下配置:

1
export PATH=${PATH}:/usr/local/mysql/bin/

若使用 bash 的用户同理,直接在 ~/.bashrc 添加相同代码。添加完毕后通过 source 命令重新加载对应的环境变量: source ~/.zshrc

接着就可以在命令行直接使用 mysql 了。输入 mysql -u root -p 登录 mysql,密码是在安装阶段时设置的密码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  ~ mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 13
Server version: 8.0.29 MySQL Community Server - GPL

Copyright (c) 2000, 2022, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

数据库操作

DATABASE 可以不区分大小写,但只能要么全小写,要么全大写。一般会将这些参数用大写写出。

创建数据库

1
2
3
-- 还可以通过 DEFAULT CHARACTER SET 选项设置默认的编码集
mysql> CREATE DATABASE DANNY_DATABASE;
Query OK, 1 row affected (0.01 sec)

查看现有的数据库

1
2
3
4
5
6
7
8
9
10
11
mysql> SHOW DATABASES;
+----------------------------+
| Database |
+----------------------------+
| information_schema |
| DANNY_DATABASE |
| mysql |
| performance_schema |
| sys |
+----------------------------+
6 rows in set (0.00 sec)

切换到指定数据库

1
mysql> USE DANNY_DATABASE

数据库的查看与删除

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
-- 创建数据库: 准备稍后移除的数据库
mysql> CREATE DATABASE DANNY_DATABASE_WAIT_DELETE;
Query OK, 1 row affected (0.01 sec)

mysql> SHOW DATABASES;
+----------------------------+
| Database |
+----------------------------+
| information_schema |
| DANNY_DATABASE |
| DANNY_DATABASE_WAIT_DELETE |
| mysql |
| performance_schema |
| sys |
+----------------------------+
6 rows in set (0.00 sec)

-- 删除数据库
mysql> DROP DATABASE DANNY_DATABASE_WAIT_DELETE;
Query OK, 0 rows affected (0.02 sec)

mysql> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| information_schema |
| DANNY_DATABASE |
| mysql |
| performance_schema |
| sys |
+--------------------+
5 rows in set (0.00 sec)

查看当前使用的数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 未选择的情况下
mysql> SELECT DATABASE();
+----------------+
| DATABASE() |
+----------------+
| null |
+----------------+
1 row in set (0.00 sec)

-- 切换指定数据库
use DANNY_DATABASE;

mysql> SELECT DATABASE();
+----------------+
| DATABASE() |
+----------------+
| danny_database |
+----------------+
1 row in set (0.00 sec)

数据表操作

创建数据表

1
2
3
4
5
6
7
8
9
10
-- 创建名为 customers 的数据表
mysql> CREATE TABLE IF NOT EXISTS customers(
-> cust_id INT NOT NULL AUTO_INCREMENT,
-> cust_name CHAR(50) NOT NULL,
-> cust_sex CHAR(1) NOT NULL DEFAULT 0,
-> cust_address CHAR(50) NULL,
-> cust_contact CHAR(50) NULL,
-> PRIMARY KEY(cust_id)
-> );
Query OK, 0 rows affected (0.11 sec)

其中 IF NOT EXISTS 参数是可选的,它的意思为若 customers 表不存在则创建它。

查看数据表与表列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- 查看当前用户在当前数据库中可以访问的数据表
mysql> SHOW TABLES;
+--------------------------+
| Tables_in_danny_database |
+--------------------------+
| customers |
+--------------------------+
1 rows in set (0.00 sec)

-- 查看指定数据表中列的信息
-- DESC customers; 等价于如下命令
mysql> SHOW COLUMNS from customers;
+--------------+-------------+------+-----+-----------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+-------------+------+-----+-----------+----------------+
| cust_id | int(11) | NO | PRI | NULL | auto_increment |
| cust_name | char(50) | NO | | NULL | |
| cust_sex | char(1) | NO | | 0 | |
| cust_address | char(50) | YES | | NULL | |
| cust_contact | char(50) | YES | | NULL | |
+--------------+-------------+------+-----+-----------+----------------+
5 rows in set (0.00 sec)

删除数据表

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
-- 添加一个数据表用于演示删除
mysql> CREATE TABLE IF NOT EXISTS customers_1(
-> cust_id INT NOT NULL AUTO_INCREMENT,
-> cust_name CHAR(50) NOT NULL,
-> cust_sex CHAR(1) NOT NULL DEFAULT 0,
-> cust_address CHAR(50) NULL,
-> cust_contact CHAR(50) NULL,
-> PRIMARY KEY(cust_id)
-> );
Query OK, 0 rows affected (0.11 sec)

-- 查看当前的数据表
mysql> SHOW tables;
+--------------------------+
| Tables_in_danny_database |
+--------------------------+
| customers |
| customers_1 |
+--------------------------+
2 rows in set (0.00 sec)

-- 删除指定数据表
mysql> DROP TABLES customers_1;
Query OK, 0 rows affected (0.02 sec)

mysql> SHOW tables;
+--------------------------+
| Tables_in_danny_database |
+--------------------------+
| customers |
+--------------------------+
1 row in set (0.00 sec)

数据表添加新列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 插入新列
mysql> alter TABLE customers
-> ADD COLUMN cust_city char(10) NOT NULL DEFAULT 'guangzhou' AFTER cust_sex;
Query OK, 0 rows affected (0.06 sec)
Records: 0 Duplicates: 0 Warnings: 0

-- 确认表列状态
mysql> SHOW COLUMNS from customers;
+--------------+-------------+------+-----+-----------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+-------------+------+-----+-----------+----------------+
| cust_id | int(11) | NO | PRI | NULL | auto_increment |
| cust_name | char(50) | NO | | NULL | |
| cust_sex | char(1) | NO | | 0 | |
| cust_city | char(10) | NO | | guangzhou | |
| cust_address | char(50) | YES | | NULL | |
| cust_contact | char(50) | YES | | NULL | |
+--------------+-------------+------+-----+-----------+----------------+
6 rows in set (0.00 sec)

数据表修改表列

修改整列: 将列名 cust_sex 修改 sex,并修改默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> alter TABLE customers
-> CHANGE COLUMN cust_sex sex char(1) NULL DEFAULT 'M';
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> SHOW COLUMNS from customers;
+--------------+-------------+------+-----+-----------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+-------------+------+-----+-----------+----------------+
| cust_id | int(11) | NO | PRI | NULL | auto_increment |
| cust_name | char(50) | YES | | NULL | |
| sex | char(1) | YES | | M | |
| cust_city | char(10) | NO | | guangzhou | |
| cust_address | char(50) | YES | | NULL | |
| cust_contact | char(50) | YES | | NULL | |
+--------------+-------------+------+-----+-----------+----------------+
6 rows in set (0.00 sec)

仅修改列的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> ALTER TABLE customers
-> MODIFY COLUMN cust_address varchar(50);
Query OK, 0 rows affected (0.06 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> show COLUMNS from customers;
+--------------+-------------+------+-----+-----------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+-------------+------+-----+-----------+----------------+
| cust_id | int(11) | NO | PRI | NULL | auto_increment |
| cust_name | char(50) | YES | | NULL | |
| sex | char(1) | YES | | M | |
| cust_city | char(10) | NO | | guangzhou | |
| cust_address | varchar(50) | YES | | NULL | |
| cust_contact | char(50) | YES | | NULL | |
+--------------+-------------+------+-----+-----------+----------------+
6 rows in set (0.00 sec)

修改指定列的指定字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> ALTER TABLE customers
-> ALTER COLUMN cust_city SET DEFAULT 'shenzhen';
Query OK, 0 rows affected (0.03 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> SHOW COLUMNS from customers;
+--------------+-------------+------+-----+----------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+-------------+------+-----+----------+----------------+
| cust_id | int(11) | NO | PRI | NULL | auto_increment |
| cust_name | char(50) | YES | | NULL | |
| sex | char(1) | YES | | M | |
| cust_city | char(10) | NO | | shenzhen | |
| cust_address | varchar(50) | YES | | NULL | |
| cust_contact | char(50) | YES | | NULL | |
+--------------+-------------+------+-----+----------+----------------+
6 rows in set (0.00 sec)

移除数据表列: 移除 cust_contact 数据表项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> ALTER TABLE danny_database.customers
-> DROP COLUMN cust_contact;
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> SHOW COLUMNS from customers;
+--------------+-------------+------+-----+----------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+-------------+------+-----+----------+----------------+
| cust_id | int(11) | NO | PRI | NULL | auto_increment |
| cust_name | char(50) | YES | | NULL | |
| sex | char(1) | YES | | M | |
| cust_city | char(10) | NO | | shenzhen | |
| cust_address | varchar(50) | YES | | NULL | |
+--------------+-------------+------+-----+----------+----------------+
5 rows in set (0.00 sec)

数据项操作

添加数据

默认情况下在命令行中 mysql 是不能直接插入中文的,这个跟字符集有关。可输入下面命令修改数据库或表的字符集:

1
2
3
4
5
6

-- 设置名为 danny_database 的数据库字符集
ALTER DATABASE danny_database character SET utf8;

-- 设置名为 customers 的数据库表字符集 (Tip: 若数据库已经被设置为 utf8, 则无需再设置表的字符集)
ALTER TABLE customers convert to character SET utf8;

为数据表插入数据,显式设置字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql> INSERT INTO danny_database.customers(cust_id, cust_name, sex, cust_address)
-> VALUES(901, '张三', DEFAULT, '广州市');
Query OK, 1 row affected (0.02 sec)

mysql> INSERT INTO danny_database.customers(cust_id, cust_name, sex, cust_address)
-> VALUES(0, '李四', DEFAULT, '广州市');
Query OK, 1 row affected (0.01 sec)

mysql> select * from customers;
+---------+-----------+------+-----------+--------------+
| cust_id | cust_name | sex | cust_city | cust_address |
+---------+-----------+------+-----------+--------------+
| 901 | 张三 | M | shenzhen | 广州市 |
| 902 | 李四 | M | shenzhen | 广州市 |
+---------+-----------+------+-----------+--------------+
2 rows in set (0.00 sec)

由于 cust_id 是自增的,因此可以将此字段的值设置为 0 或 NULL 会自动自增。上例 “李四” 的 cust_id 在创建后就被自增为 902。

还可以通过 SET 语句设置部分值:

1
2
mysql> INSERT INTO danny_database.customers SET cust_name='王五', cust_address='武汉市', sex=DEFAULT;
Query OK, 1 row affected (0.00 sec)

查询数据

可通过 SELECT 语句查询数据:

1
2
3
4
5
6
7
8
9
mysql> SELECT * FROM customers;
+---------+-----------+------+-----------+--------------+
| cust_id | cust_name | sex | cust_city | cust_address |
+---------+-----------+------+-----------+--------------+
| 901 | 张三 | M | shenzhen | 广州市 |
| 902 | 李四 | M | shenzhen | 广州市 |
| 903 | 王五 | M | shenzhen | 武汉市 |
+---------+-----------+------+-----------+--------------+
3 rows in set (0.00 sec)

仅展示指定字段:

1
2
3
4
5
6
7
8
+---------+-----------+------+
| cust_id | cust_name | sex |
+---------+-----------+------+
| 901 | 张三 | M |
| 902 | 李四 | M |
| 903 | 王五 | M |
+---------+-----------+------+
3 rows in set (0.00 sec)

通过 WHERE 子句设置查询条件,筛选出符合查询条件的数据:

1
2
3
4
5
6
7
8
9
mysql> SELECT cust_id,cust_name,cust_address FROM customers
-> WHERE cust_address="广州市";
+---------+-----------+--------------+
| cust_id | cust_name | cust_address |
+---------+-----------+--------------+
| 901 | 张三 | 广州市 |
| 902 | 李四 | 广州市 |
+---------+-----------+--------------+
2 rows in set (0.00 sec)

删除数据

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
-- 添加几项测试数据
mysql> INSERT INTO danny_database.customers(cust_id, cust_name, sex, cust_address)
-> VALUES(1, 'test1', DEFAULT, '深圳市');
Query OK, 1 row affected (0.02 sec)

mysql> select * from customers;
+---------+-----------+------+-----------+--------------+
| cust_id | cust_name | sex | cust_city | cust_address |
+---------+-----------+------+-----------+--------------+
| 1 | test1 | M | shenzhen | 深圳市 |
| 901 | 张三 | M | shenzhen | 广州市 |
| 902 | 李四 | M | shenzhen | 广州市 |
| 903 | 王五 | M | shenzhen | 武汉市 |
+---------+-----------+------+-----------+--------------+
4 rows in set (0.00 sec)

-- 删除表数据
mysql> DELETE FROM customers
-> WHERE cust_id=1;
Query OK, 1 row affected (0.02 sec)


mysql> select * from customers;
+---------+-----------+------+-----------+--------------+
| cust_id | cust_name | sex | cust_city | cust_address |
+---------+-----------+------+-----------+--------------+
| 901 | 张三 | M | shenzhen | 广州市 |
| 902 | 李四 | M | shenzhen | 广州市 |
| 903 | 王五 | M | shenzhen | 武汉市 |
+---------+-----------+------+-----------+--------------+

更新数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 更新数据
mysql> UPDATE customers SET cust_address="深圳市" WHERE cust_name="李四";
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

mysql> SELECT * FROM customers;
+---------+-----------+------+-----------+--------------+
| cust_id | cust_name | sex | cust_city | cust_address |
+---------+-----------+------+-----------+--------------+
| 901 | 张三 | M | shenzhen | 广州市 |
| 902 | 李四 | M | shenzhen | 深圳市 |
| 903 | 王五 | M | shenzhen | 武汉市 |
+---------+-----------+------+-----------+--------------+
3 rows in set (0.00 sec)

实践

以一个 eShop 的需求为例做个简单的测试吧。

创建 eshop 数据库

在 MySQL 中创建一个名为 eshop 的数据库,选择字符集为 utf8mb4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> CREATE DATABASE IF NOT EXISTS eshop DEFAULT CHARACTER SET utf8mb4;
Query OK, 1 row affected (0.01 sec)

mysql> SHOW DATABASES;
+--------------------+
| Database |
+--------------------+
| information_schema |
| DANNY_DATABASE |
| eshop |
| mysql |
| performance_schema |
| sys |
+--------------------+
6 rows in set (0.01 sec)

-- 切换数据库
mysql> use eshop;
Database changed

创建数据表及相关记录

相关表信息如下

表名:用户(t_user)

字段名 类型 大小
用户ID (id) 自增类型
姓名 (user_name) 文本 50,非空
联系电话 (phone_no) 文本 20,非空

表名:商品(product)

字段名 类型 大小
商品ID(id) 自增类型
商品名称(product_name) 文本 50,非空
价格(price) 数值类型 (整数位9位,小数位2位),非空

表名:购物车 (shopping_cart)

字段名 类型 大小
用户id(user_id) 整数 非空,主键,参考用户表主键
商品id(product_id) 整数 非空,主键,参考商品表主键
商品数量(quantity) 整数 非空
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
-- 用户表
mysql> CREATE TABLE IF NOT EXISTS t_user(
-> `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
-> `user_name` CHAR(50) NOT NULL,
-> `phone_no` CHAR(20) NOT NULL
-> );
Query OK, 0 rows affected (0.06 sec)

-- 商品表
mysql> CREATE TABLE IF NOT EXISTS product(
-> `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
-> `product_name` CHAR(50) NOT NULL,
-> `price` DOUBLE(9, 2)
-> );
Query OK, 0 rows affected (0.06 sec)

-- 购物车
mysql> CREATE TABLE IF NOT EXISTS shopping_cart(
-> `user_id` INT NOT NULL,
-> `product_id` INT NOT NULL,
-> `quantity` INT NOT NULL,
-> PRIMARY KEY(`user_id`, `product_id`)
-> );
Query OK, 0 rows affected (0.05 sec)

-- 查看数据表
mysql> show tables;
+-----------------+
| Tables_in_eshop |
+-----------------+
| product |
| shopping_cart |
| t_user |
+-----------------+
3 rows in set (0.00 sec)

录入用户数据

用户信息

1
2
3
4
1;张三; 13333333333;
2;李四; 13666666666
3;王五; 13888888888
4;赵六; 13999999999

商品信息

1
2
3
1; C++程序设计教程; 45.5
2; 数据结构; 33.7
3; 操作系统; 51

购物车

1
2
3
4
1; 1; 5
1; 2; 3
2; 3; 6
2; 4; 8

录入数据:

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
-- 插入用户表数据
mysql> INSERT INTO t_user
-> (id, user_name, phone_no)
-> VALUES
-> (1, '张三', '13333333333'),
-> (2, '李四', '13666666666'),
-> (3, '王五', '13888888888'),
-> (4, '赵六', '13999999999');
Query OK, 4 rows affected (0.02 sec)
Records: 4 Duplicates: 0 Warnings: 0

mysql> SELECT * FROM t_user;
+----+-----------+-------------+
| id | user_name | phone_no |
+----+-----------+-------------+
| 1 | 张三 | 13333333333 |
| 2 | 李四 | 13666666666 |
| 3 | 王五 | 13888888888 |
| 4 | 赵六 | 13999999999 |
+----+-----------+-------------+
4 rows in set (0.00 sec)

-- 插入「商品信息」
mysql> INSERT INTO product
-> (id, product_name, price)
-> VALUES
-> (1, 'C++程序设计教程', 45.5),
-> (2, '数据结构', 33.7),
-> (3, '操作系统', 51);
Query OK, 3 rows affected (0.01 sec)
Records: 3 Duplicates: 0 Warnings: 0

mysql> SELECT * FROM product;
+----+-----------------------+-------+
| id | product_name | price |
+----+-----------------------+-------+
| 1 | C++程序设计教程 | 45.50 |
| 2 | 数据结构 | 33.70 |
| 3 | 操作系统 | 51.00 |
+----+-----------------------+-------+
3 rows in set (0.00 sec)

-- 插入购物车
mysql> INSERT INTO shopping_cart
-> (user_id, product_id, quantity)
-> VALUES
-> (1, 1, 5),
-> (1, 2, 3),
-> (2, 3, 6),
-> (2, 4, 8);
Query OK, 4 rows affected (0.01 sec)
Records: 4 Duplicates: 0 Warnings: 0

mysql> SELECT * FROM shopping_cart;
+---------+------------+----------+
| user_id | product_id | quantity |
+---------+------------+----------+
| 1 | 1 | 5 |
| 1 | 2 | 3 |
| 2 | 3 | 6 |
| 2 | 4 | 8 |
+---------+------------+----------+
4 rows in set (0.00 sec)

数据的查询与更新

使用 SQL 语句列出「张三」购买商品清单信息,以购买数量升序排列:

1
2
3
4
5
6
7
8
9
10
11
mysql> SELECT u.user_name, p.product_name, u.phone_no, p.price, s.quantity FROM t_user u, product p, shopping_cart s
-> WHERE u.user_name="张三" AND u.id = s.user_id AND p.id = s.product_id
-> ORDER BY quantity asc
-> LIMIT 100;
+-----------+-----------------------+-------------+-------+----------+
| user_name | product_name | phone_no | price | quantity |
+-----------+-----------------------+-------------+-------+----------+
| 张三 | 数据结构 | 13333333333 | 33.70 | 3 |
| 张三 | C++程序设计教程 | 13333333333 | 45.50 | 5 |
+-----------+-----------------------+-------------+-------+----------+
2 rows in set (0.01 sec)

使用 SQL 语句选出李四购买商品的总价:

1
2
3
4
5
6
7
8
9
mysql> SELECT u.user_name, p.product_name, p.price, s.quantity, p.price*s.quantity AS total_price FROM t_user u, product p, shopping_cart s
-> WHERE u.user_name="李四" AND u.id = s.user_id AND p.id = s.product_id
-> LIMIT 100;
+-----------+--------------+-------+----------+-------------+
| user_name | product_name | price | quantity | total_price |
+-----------+--------------+-------+----------+-------------+
| 李四 | 操作系统 | 51.00 | 6 | 306.00 |
+-----------+--------------+-------+----------+-------------+
1 row in set (0.00 sec)

使用 SQL 语句列出购买数量排前两位的商品名称:

1
2
3
4
5
6
7
8
9
10
11
mysql> SELECT p.product_name, p.price, s.quantity FROM product p, shopping_cart s
-> WHERE p.id = s.product_id
-> ORDER BY quantity desc
-> LIMIT 2;
+-----------------------+-------+----------+
| product_name | price | quantity |
+-----------------------+-------+----------+
| 操作系统 | 51.00 | 6 |
| C++程序设计教程 | 45.50 | 5 |
+-----------------------+-------+----------+
2 rows in set (0.00 sec)

忘记密码

若忘记数据库密码后可通过 mysqld_safe 来修改密码:

  1. 在系统偏好设置中关闭 mysql 服务

  2. 打开终端,输入命令:

    1
    2
    ➜  ~ cd /usr/local/mysql/bin
    ➜ ~ sudo su
  3. 命令行变成以 sh-3.2# 开头后继续输入命令:

    1
    2
    3
    4
    sh-3.2# ./mysqld_safe --skip-grant-tables &

    mysqld_safe Logging to '/usr/local/mysql/data/DannydeMBP.err'.
    mysqld_safe Starting mysqld daemon with databases from /usr/local/mysql/data
  4. 新开个命令行窗口,进入 mysql:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    ➜  ~ /usr/local/mysql/bin/mysql

    Enter password:
    Welcome to the MySQL monitor. Commands end with ; or \g.
    Your MySQL connection id is 30
    Server version: 5.7.31

    Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

    Oracle is a registered trademark of Oracle Corporation and/or its
    affiliates. Other names may be trademarks of their respective
    owners.

    Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

    mysql>
    mysql> use mysql

    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A

    Database changed
  5. 更新密码

    1
    2
    3
    4
    mysql> update user set authentication_string=password('admin') where Host='localhost' and User='root';

    Query OK, 1 row affected, 1 warning (0.01 sec)
    Rows matched: 1 Changed: 1 Warnings: 1
  6. 输入 exit 命令退出 mysql,查出 mysqld_safe 进程号并杀掉:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    mysql> exit
    Bye

    ➜ ~ ps -ax | grep mysql
    8553 ttys004 0:00.03 /bin/sh ./mysqld_safe --skip-grant-tables
    8623 ttys004 0:00.92 /usr/local/mysql-5.7.31-macos10.14-x86_64/bin/mysqld --basedir=/usr/local/mysql-5.7.31-macos10.14-x86_64 --datadir=/usr/local/mysql-5.7.31-macos10.14-x86_64/data --plugin-dir=/usr/local/mysql-5.7.31-macos10.14-x86_64/lib/plugin --user=mysql --skip-grant-tables --log-error=host-3-187.can.danny1.network.err --pid-file=host-3-187.can.danny1.network.pid

    # 杀掉 mysql 的进程
    ➜ ~ kill -9 8553
    ➜ ~ kill -9 8623
  7. 此时返回系统偏好设置中看到 mysql 被关闭后就算正确退出了。接着继续输入 mysql -u root -p 命令连接数据库,再输入刚才修改的密码即可。


参考资料

组件通信: EventBus 的原理解析与应用

2022-01-22 10:40:49

在开发复杂的单页面应用时,我们经常会遇到一个问题:如何高效地在组件或模块之间进行通信?这里,EventBus(事件总线)就派上了用场。简单来说,EventBus 是一种设计模式,它允许不同组件或模块之间通过事件来通信,而无需直接引用彼此。

EventBus 是传统的组件通信解决方案,下面我们将讲解 EventBus 跨组件通信的原理、实现方式以及该如何使用。

原理解析

EventBus 的核心在于提供一个中央机制,允许不同的组件或模块相互通信,而不必直接引用对方。它是一种典型的发布-订阅(pub-sub)模式,这是一种广泛使用的设计模式,用于解耦发送者和接收者。

在这个模式中,EventBus 充当了一个中介的角色:它允许组件订阅那些它们感兴趣的事件,并在这些事件发生时接收通知。同样,当某个事件发生时,比如用户的一个动作或者数据的变化,EventBus 负责将这一消息广播给所有订阅了该事件的组件。

它基于三个核心操作:注册事件(on(event, callback))、触发事件(emit(event, ...args))、以及移除事件(off(event, callback))。因此,EventBus 的基本代码可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
class EventBus {
on(event, callback) {
// 注册事件监听器
}

emit(event, ...args) {
// 触发事件
}

off(event, callback) {
// 移除事件监听器
}
}

显然,我们需要有一个私有变量来储存用户的函数,此时为类添加 events 属性。events 属性是一个对象映射,其中每个属性表示一个事件名称,对应的值是一个回调函数的数组,这个数组存储了所有订阅了该事件的回调函数。

1
2
3
4
class EventBus {
private events: Record<string, Function[]> = {};
// ...
}

当用户执行订阅事件 on 时,回调函数会被添加到相应事件名称的数组中。这样,同一个事件可以被不同组件或模块订阅,而每个订阅者的回调函数都会被正确地保存在事件队列中。最后,当触发事件 emit 时,事件队列中的每个回调函数都会被执行,实现了事件的触发和通知功能。若已经没有订阅需求,则可以通过 off 移除已经订阅的事件。

代码实现

接下来我们按照前文所述完善我们的代码实现:

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
class EventBus {
// 事件存储对象,用于保存不同事件的回调函数
private events: Record<string, Function[]> = {};

/**
* 注册事件监听器
* @param eventName - 事件名称
* @param callback - 回调函数,当事件触发时执行
* @returns this - 返回 EventBus 实例,支持链式调用
*/
public on(eventName: string, callback: Function): this {
// 检查回调函数是否为函数类型
if (typeof callback !== "function") {
throw new Error("EventBus 'on' method expects a callback function.");
}

// 如果事件不存在,创建一个空数组用于存储回调函数
if (!this.events[eventName]) {
this.events[eventName] = [];
}

// 将回调函数添加到事件的回调函数列表中
this.events[eventName].push(callback);

// 支持链式调用
return this;
}

/**
* 触发事件
* @param eventName - 要触发的事件名称
* @param args - 传递给回调函数的参数
* @returns this - 返回 EventBus 实例,支持链式调用
*/
public emit(eventName: string, ...args: any[]): this {
// 获取事件对应的回调函数列表
const callbacks = this.events[eventName];
if (callbacks) {
// 遍历执行每个回调函数,并传递参数
callbacks.forEach((callback) => callback(...args));
}

// 支持链式调用
return this;
}

/**
* 移除事件监听器
* @param event - 要移除的事件名称或事件名称数组
* @param callback - 要移除的回调函数(可选)
* @returns this - 返回 EventBus 实例,支持链式调用
*/
public off(event?: string | string[], callback?: Function): this {
// 清空所有事件监听器
if (!event || (Array.isArray(event) && !event.length)) {
this.events = {};
return this;
}

// 处理事件数组
if (Array.isArray(event)) {
event.forEach((e) => this.off(e, callback));
return this;
}

// 如果没有提供回调函数,则删除该事件的所有监听器
if (!callback) {
delete this.events[event];
return this;
}

// 移除特定的回调函数
const callbacks = this.events[event];
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}

// 支持链式调用
return this;
}
}

当涉及到一次性的事件监听需求时,我们可以进一步扩展 EventBus,以支持一次性事件监听。允许用户在某个事件触发后,自动移除事件监听器,以确保回调函数只执行一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
class EventBus {
// other code ...
public once(eventName: string, callback: Function): this {
const onceWrapper = (...args: any[]) => {
this.off(eventName, onceWrapper);
callback(...args);
};

this.on(eventName, onceWrapper);

return this;
}
}

使用方式

我们将类的封装到 event-bus.ts 中,通过模块的来管理:

1
2
3
export class EventBus {
// ...
}

我们现在已经封装好了一个类,若我们像使用则需要实例化。此处再文件内直接实例化一个类:

1
2
// 创建 EventBus 实例并导出
export const eventBus = new EventBus();

这样使用时可以提供两种方式:

  1. 引入已经实例化的 eventBus

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import { eventBus } from './event-bus';

    // 订阅事件
    eventBus.on('eventName', callback);

    // 触发事件
    eventBus.emit('eventName', data);

    // 移除事件
    eventBus.off('eventName', callback);
  2. 需要多个独立的事件总线实例时,或者希望在不同模块或组件之间使用不同的事件总线时,可以选择额外实例化 eventBus。这样做的目的可能是为了隔离命名的冲突、组件与模块逻辑隔离等原因。

    1
    2
    3
    4
    5
    6
    // events.ts
    import { EventBus } from './event-bus';

    // 创建独立的事件总线实例
    export const eventBusA = new EventBus();
    export const eventBusB = new EventBus();
    1
    2
    3
    4
    5
    6
    7
    8
    9
    import {eventBusA, eventBusB} from './events'

    // 在不同模块或组件中使用不同的事件总线
    eventBusA.on('eventA', callbackA);
    eventBusB.on('eventB', callbackB);

    // 触发不同事件总线上的事件
    eventBusA.emit('eventA', dataA);
    eventBusB.emit('eventB', dataB);

以下是 CodeSandbox 的演示代码:

总结

在本文中,我们深入探讨了 EventBus 的原理,了解了它是如何工作的。我们学习了它的核心操作。除了本文所提及的实现方式,有时候在生产项目中,为了确保代码的可靠性,我们可以考虑使用成熟的第三方库,例如 mitttiny-emitter

这些库已经经过广泛的测试和使用,可以提供稳定和可靠的 EventBus 功能。

Redux 食用指南

2021-10-11 12:08:31

Redux 是一个强大的状态管理框架,被广泛用于管理应用程序的状态。它的设计理念是让状态的更新可预测和透明。本文将简要探讨 Redux 的核心机制和实际应用。

在 Redux 中,有一个状态对象负责应用程序的整个状态.Redux store 是应用程序状态的唯一真实来源

如果应用程序想要更新状态,只能通过 Redux store 执行,单向数据流可以更轻松地对应用程序中的状态进行监测管理。

Redux store 是一个保存和管理应用程序状态的 state,使用 Redux 对象中的 createStore() 来创建一个 redux store,此方法将 reducer 函数作为必需参数.

1
2
3
const reducer = (state = 5) => state;

const store = Redux.createStore(reducer);

获取数据

Redux store 对象提供了几种允许你与之交互的方法,可以使用 getState() 方法检索 Redux store 对象中保存的当前的 state

1
2
3
4
5
6
const store = Redux.createStore(
(state = 5) => state
);

// 更改此行下方的代码
const currentState = store.getState();

更新状态

由于 Redux 是一个状态管理框架,因此更新状态是其核心任务之一。在 Redux 中,所有状态更新都由 dispatch action 触发,action 只是一个 JavaScript 对象,其中包含有关已发生的 action 事件的信息。

Redux store 接收这些 action 对象,然后更新相应的状态。action 对象中必须要带有 type 属性,reducer 才能根据 type 进行区分处理。
action 除了 type 属性外,还可以附带数据给 reducer 做相应的处理,这个数据是可选的。

我们可以将 Redux action 视为信使,将有关应用程序中发生的事件信息提供给 Redux store,然后 store 根据发生的 action 进行状态的更新。

reducer

reducer 将 state 和 action 作为参数,并且它总是返回一个新的 state。这是 reducer 的唯一的作用,它不应有任何其他的作用。比如它不应调用 API 接口,也不应存在任何潜在的副作用。reducer 只是一个接受状态和动作,然后返回新状态的纯函数

在 reducer 中一般通过 switch 进行判断 action 的类型,做不同的处理。

订阅事件

store.subscribe() 可以订阅 store 的数据变化,它接收一个回调函数作为参数。当 store 数据更新时会调用该回调函数。

模块划分

当应用程序的状态开始变得越来越复杂时,将状态划分为多个部分可能是个更好的选择。我们可以考虑将不同的模块进行划分,Login 作为一个模块,Account 作为另一个模块。

但对 state 进行模块划分也不能破坏 redux 中将数据存入简单 state 的原则。因此可以生成多个 reducer, 再将它们合并到 root reducer 中。

redux 提供了 combineReducers() 函数对 reducer 进行合并。它接收一个对象作为参数,对象中的 key/value 别分对应着 module name 和相对应的 reducer 函数。

1
2
3
4
5
6
7

const rootReducer = Redux.combineReducers({
counter: counterReducer,
auth: authReducer
})

const store = Redux.createStore(rootReducer);

异步

redux 本身是不能直接处理异步操作,因此需要引入中间件来处理这些问题。在 createStore 时,还可以传入第二个可选参数,这个参数就是传递给 redux 的中间件函数。

Redux 提供了 applyMiddleware() 来创建一个中间件,一般处理 redux 异步的中间件有 redux-thunkredux-saga 等。

redux-thunk

redux-thunk 允许 action 创建函数返回一个函数而不是一个 action 对象。这个返回的函数接收 dispatchgetState 作为参数,允许直接进行异步操作和状态的分发。

例如,一个异步获取数据的 thunk 可能如下所示:

1
2
3
4
5
6
7
8
9
function fetchData() {
return (dispatch, getState) => {
// 异步操作
fetch('some-api-url')
.then(response => response.json())
.then(data => dispatch({ type: 'FETCH_DATA_SUCCESS', payload: data }))
.catch(error => dispatch({ type: 'FETCH_DATA_ERROR', error }));
};
}

redux-saga

redux-saga 是一个更高级的中间件,它使用 ES6 的 Generator 函数来让你以同步的方式写异步代码。saga 监听发起的 action,并决定基于这些 action 执行哪些副作用(如异步获取数据、访问浏览器缓存等)。

一个简单的 saga 可能如下所示:

1
2
3
4
5
6
7
8
function* fetchDataSaga(action) {
try {
const data = yield call(fetch, 'some-api-url');
yield put({ type: 'FETCH_DATA_SUCCESS', payload: data });
} catch (error) {
yield put({ type: 'FETCH_DATA_ERROR', error });
}
}

React 与 Redux

在 React 应用中,Redux 被用来跨组件共享状态。使用 react-redux 库可以方便地将 Redux 集成到 React 应用中。

Provider 组件

Providerreact-redux 提供的一个组件,它使 Redux store 对 React 应用中的所有组件可用。通常,我们在应用的最顶层包裹 Provider 并传入 store:

1
2
3
4
5
6
7
8
import { Provider } from 'react-redux';
import { store } from './store';

const App = () => (
<Provider store={store}>
<MyRootComponent />
</Provider>
);

connect 函数

connect 是一个高阶函数,用于将 React 组件连接到 Redux store。它接受两个参数:mapStateToPropsmapDispatchToProps,分别用于从 store 中读取状态和向 store 发起 actions。

1
2
3
4
5
6
7
8
9
10
11
import { connect } from 'react-redux';

const mapStateToProps = state => ({
items: state.items
});

const mapDispatchToProps = dispatch => ({
fetchData: () => dispatch(fetchData())
});

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);

总结

Redux 提供了一种统一、可预测的方式来管理应用程序的状态。通过使用 actions, reducers 和 store,开发者可以以一种高度解耦的方式来管理状态和 UI。

当结合异步处理和 React 集成时,Redux 成为了一个强大的工具,能够提升大型应用程序的开发和维护效率。

计算机网络原理笔记

2021-08-15 12:56:15

计算机网络原理学习笔记。

目录

下面目录定位有些使用不了。若需要跳转到具体段落应使用侧边栏文章定位


计算类速览

速率与带宽

  1. 传输时延:链路发送到结束所用的时间

    1
    dt = L(分组长度) / R(链路带宽)
  2. 传播时延: 从发送端到接收端传输所需的时间

    1
    dp = D(链路长度) / V(信号传播速度)
  3. 时延带宽(乘)积:传播时延和链路带宽的乘积

    1
    G = dp(传播时延) * R(链路带宽)

TCP 报文段确认号

题目一般是主机 A 会发送两个 TCP 报文段给主机 B,其实有字节序号分别为 n1 和 n2。

  1. 算携带了多少字节: 字节数=n2-n1
  2. 接收到第一个报文段返回的确认号是 n2
  3. 如果主机 B 接收到第二个报文段后的确认号是 n3, 问第二个携带多少字节: 字节数=n3-n2
  4. 如果第一段丢失了,第二段到达了,主机 B 返回的确认号是: n1, 即要求主机 B 重传之前没有接受到的数据

汉明距离

两个等长码字之间的,对应位不同的位数,成为两个码字的汉明距离。汉明距离是两个码字进行按位异或后 1 的个数。

1
2
3
4
5
6
01100101
10011101
-------- 进行异或(^)
11111000
--------
5个1,汉明距离 = 5

循环冗余码

多项式 G(x)=x4 + x3 + 1,对位串 101100111101 进行 CRC 编码,结果为:

G(x)=x4 + x3 + 1 对应的比特位为 11001,则在待编位串后面添加 0000.

位串除 11001


计算机网络概述

常见应用的端口号

应用 端口号
TCP/FTP 21
SMTP 25
HTTP 80
POP3 服务器 110

TCP/IP、OSI 参考模型

OSI模型 单位
7. 应用层 报文
4. 传输层 数据报/报文段
3. 网络层 分组/包
2. 数据链路层
1. 物理层 比特流

简述OSI参数模型物理层的主要功能及该层协议规定的四个特性。

物理层的主要功能是实现比特流的透明传输,为数据链路层提供数据传输服务。

物理层协议规定的特性包括:

  1. 机械特性
  2. 电气特性
  3. 功能特性
  4. 规程特性

应用层

网络协议的三要素包括

  • 语法: 定义实体之间交换信息的格式与结构
  • 语义: 定义实体之间交换的信息中需要发送哪些控制信息,这些信息的具体含义,以及针对不同含义的控制信息,接收信息端应如何响应。
  • 时序: 定义实体之间交换信息的顺序以及如何匹配或适应彼此的速度

简述典型的HTTP请求方法及其作用

  1. GET: 读取由 URL 所标识的信息
  2. POST: 给服务器添加信息
  3. HEAD: 请求读取由 URL 所标识的信息首部,无需在相应报文中包含对象
  4. OPTION: 请求一些选项的信息
  5. PUT: 在指明的 URL 下存储一个文档

简述 POP3 协议交互过程

POP3 是邮件读取协议,可用于接收邮件。

  1. 授权阶段: 用户代理需要向邮件服务器发送用户名和口令,服务器鉴别用户身份,授权访问邮箱。
  2. 事务处理阶段: 用户代理向邮件服务器发送 POP3 命令,实现邮件读取,为邮件做删除编辑、取消邮件删除标记以及获取邮件的统计信息等操作。
  3. 更新阶段: 客户发出来 quit 命令,结束 POP3 回话,服务器删除哪些被标记为删除的邮件。

传输层

传输层核心任务:为应用进程之间提供端到端的逻辑通信服务。

TCP/IP

核心层: 传输层
网络互联层核心协议: IP 协议

简述传输层所实现的功能

实现的功能:

  1. 传输层寻址
  2. 对应用层报文进行分段和重组
  3. 对报文进行差错检测
  4. 实现进程间端到端的可靠数据传输控制
  5. 面向应用层实现复用与分解
  6. 流量控制
  7. 拥塞控制

简述传输层实现可靠数据传输的主要措施

不可靠传输信道在数据传输中可能发生:

  1. 比特差错
  2. 乱序
  3. 数据丢失
  1. 差错控制: 利用差错编码实现数据报传输过程中的比特差检测(甚至是纠正)。
  2. 确认: 「接收方」向「发送方」反馈接受状态
  3. 重传: 「发送方」重新发送「接收方」没有正确接收到的数据
  4. 序号: 确保数据按序提交
  5. 计时器: 解决数据丢失问题

简述保证网络传输可靠性的确认与重传机制的概念

  • 确认是指数据分组接受节点再收到每个分组后,要求想发送节点会送正确接受分组的确认信息。
  • 在规定时间内,如果发送节点没有接收到「接收方」返回的确认信息,就认为该数据分组发送失败,发送节点会重传该数据分组。

简述差错控制的概念以及差错控制的基本方法

差错控制就是通过差错编码技术实现对信息传输的检测,并通过某种机制进行差错纠正和处理。

差错检测的基本方法有:

  1. 检错重发
  2. 检错丢弃
  3. 前向纠错
  4. 反馈校验

简述TCP所提供的面向连接服务

在生成报文开始传送之前,TCP 客户和服务器相互交换传输层的控制信息,完成握手。在客户进程与服务器进程的套接字之间建立一条逻辑的 TCP 连接。

简述为 UDP 套接字分配端口号的两种方法

  1. 传输层自动分配: 创建一个 UDP 套接字时,传输层自动为该套接字分配一个端口号,该端口号当前未被该主机任何其他 UDP 套接字使用。
  2. 手动绑定: 在创建 UDP 套接字后,通过调用 bind 函数来绑定一个特定的端口号。

简述 UDP 提供的服务的主要特征

  1. 应用进程更容易控制发送什么数据以及什么时候发送。
  2. 无需建立连接
  3. 无连接状态
  4. 首部开销小,仅有8字节的开销

网络层

网络层提供的功能有:

  1. 连接建立
  2. 路由
  3. 转发

简述虚电路的概念及其构成要素

虚电路是源主机到目的主机的一条路径上建立的一条网络层逻辑连接,成为虚电路。

comment: 因为是逻辑连接,不是真实的电路连接,故称为虚电路

一条虚电路由 3 个要素组成:

  1. 从源主机到目的主机之间的一条路径
  2. 该路径上每条链路各有一个虚电路标记(VCID)
  3. 该路径上每台分组交互机的转发表记录虚电路标识的接续关系

虚电路交换和数据交换的主要差别

  • 虚电路网络通常由网络完成顺序控制、差错控制和流量控制等功能,向端系统提供无差错数据传送服务,而端系统则可以很简单。
  • 数据报网络的顺序控制、差错控制和流量控制等功能需要由端系统完成,网络实现的功能很简单,比如基本的路由与转发功能。

电路交换的特点和优缺点

电路交换的特点是有连接的,在通信时需要先建立电路连接,在通讯过程中独占一个信道,在通讯结束后需要拆除电路连接。

优点: 实时性高,时延和时延抖动都较小
缺点: 对于突发性数据传输,信道利用率低,且传输速率单一。

简述永久虚电路与交换虚电路的区别

永久虚电路是一种提前建立、长期使用的虚电路,虚电路的建立时间开销基本上可以忽略。
交换虚电路是根据通信需要而临时建立的虚电路,通信结束后立即拆除,虚电路的建立和拆除时间有时相对影响较大。

简述路由器输入端口接受与处理数据的过程

输入端口接受信号,还原数据链路层帧,提取 IP 数据报,根据 IP 数据报的目的 IP 地址检索路由表,决策将数据报交换到哪个输出端口


数据链路层与局域网

数据链路层提供的服务有:

  1. 组帧
  2. 链路接入
  3. 可靠交付
  4. 差错控制

帧的组成

HDLC: 帧组成:

  1. 管理帧
  2. 信息帧
  3. 无序号帧

IEEE 802.11 帧:

  1. 管理帧
  2. 控制帧
  3. 数据帧

PPP (point to point protocol) 数据帧结构:

  1. 标志(01111110)
  2. 地址(11111111)
  3. 控制(00000011)
  4. 协议
  5. 信息
  6. 校验和
  7. 标志(01111110)

==== 多路访问控制协议 ====

非坚持 csma 的基本原理

  • 若通信站有数据发送,先监听信道,若发现信道空闲,则立即发送数据(与 1-坚持 CSMA 第一步一致)
  • 若发现信道忙,则等待一个随机时间,然后再重新监听信道,尝试发送数据。
  • 若发送数据时产生冲突,则等待一个随机时间,然后重新开始监听信道,尝试发送数据。

这是个做事不太着急的协议。将上面文绉绉的描述用通俗的话来理解是:它在寝室中想要去洗澡

  1. 它会先看看有没有人在用浴室,没人在用就直接去洗澡
  2. 去洗澡时发现有人也想用了,它会礼让给其他人。自个再晚一段时间再看看还有没有人用,没人用就自个用了
  3. 如果已经有人在用浴室了,那又晚点再看看

1-坚持 csma 的基本原理

  • 若通信站有数据发送,先监听信道,若发现信道空闲,则立即发送数据(与 非坚持 CSMA 第一步一致)
  • 若发现信道忙,则继续监听信道,直至发现信道空闲,然后立即发送数据。

通俗话理解: 顾名思义,坚持不懈。如果浴室有人用了,我就守在门口。有人出来我就立马进去。

==== 局域网 ====

简述地址解析协议 ARP 的作用和基本思想

ARP 用于根据本网内目的主机默认网关的 IP 地址获取其 MAC 地址。

基本思想是: 在每一台主机中设置专用内存区域作为 ARP 高速缓存区域,储存该主机所在局域网中其他主机和路由器(默认网关)的 IP 地址与 MAC 地址之间的映射,并且要经常更新这个映射表。

ARP 在局域网中通过广播 ARP 查询报文的方式,来询问某目的站的 IP 地址对应的 MAC 地址,即知道本网内某主机的 IP 地址就能知道它的 MAC 地址。

简述虚拟局域网(VLAN)的概念以及划分方法

虚拟局域网是一种基于交换机的逻辑分隔广播域的局域网应用形式。划分方法主要有 3 种:

  1. 基于交换机端口划分
  2. 基于 MAC 地址划分
  3. 基于上层协议或地址划分

物理层

简述 CMI 码的编码规则,并画出二进制比特序列 1011010011 的 CMI 码信号波形

CMI 码的编码规则是将信息码的 0 编码为双极不归零码的 01,信息码的 1 交替编码为双极不归零码的 11 和 00。

米勒码的编码规则

P229

  1. 信息码的 1 编码为「双极非归零码」的 01 或 10(占半格)
  2. 信息码连 1 时,后面的 1 要换编码
  3. 信息码的 0 编码为 00 或 11,中间码元不跳变(占一格)
  4. 单个 0 时不跳变
  5. 多个 0 时,间隔跳变
  6. (备注): 有两极

无线与移动网络

简述 4 个 IEEE 802.11 标准具有的共同特征

  1. 都使用相同介质访问协议 CSMA/CA。
  2. 链路层帧使用相同的帧格式
  3. 都具有降低传输速率以传输更远距离的能力
  4. 都支持“基础设施模式”和“自组织模式”两种模式

简答题

每个 AS 可以通过 BGP(边界网关协议) 实现哪些功能

AS: Autonomous system, 自治系统

  1. 从相邻 AS 获取某子网的可达性信息。
  2. 向本 AS 内部的所有路由器传播跨 AS 的某子网可达性信息。
  3. 基于某子网可达性信息和 AS 策略,觉得到达该子网的最佳路由

简述数字签名应满足的要求

  1. 接收方能够确认或证实发送方的签名,但不能伪造
  2. 发送发发送签名给接受方后,就不能否认他所签发的信息
  3. 接收方对已收到的签名信息不能再否认,既有收报认证
  4. 第三者可以确认收发双方之间的消息传送,但不能伪造这一过

基础计算

十进制转二进制

十进制转二进制主要的方法是除2取余,逆序排列法

可以写一个简单的 js 函数打印每次计算的结果。例如将整数 251 转为二进制的过程是:

点击展开详细代码
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
function convertToBinary(n, buffer = []) {
const a = Math.floor(n / 2)
const b = n % 2;

buffer.push(b);
console.log(`${n} / 2 = ${a}...${b}`)

if (a === 0) {
const result = buffer.reverse().join('');
console.log(`将每次取到的余数逆转排序后,最终转换后的二进制是: ${result}`)

return result;
}

return convertToBinary(a, buffer);
}

convertToBinary(521);

/**
* 521 / 2 = 260...1
* 260 / 2 = 130...0
* 130 / 2 = 65...0
* 65 / 2 = 32...1
* 32 / 2 = 16...0
* 16 / 2 = 8...0
* 8 / 2 = 4...0
* 4 / 2 = 2...0
* 2 / 2 = 1...0
* 1 / 2 = 0...1
* 将每次取到的余数逆转排序后,最终转换后的二进制是: 1000001001
*/

// js 的 toString 方法还可以将数值转为指定进制
var fn = (n, base = 2) => n.toString(base);

fn(521); // "1000001001"

2^n 速查表

2 的 N 次方速查表
次方
2^1 2
2^2 4
2^3 8
2^4 16
2^5 32
2^6 64
2^7 128
2^8 256
2^9 512
2^10 1024
2^11 2048
2^12 4096
2^13 8192
2^14 16384
2^15 32768
2^16 65536
2^17 131072
2^18 262144
2^19 524288
2^20 1048576

子网掩码速览

类别 子网掩码十进制 子网掩码二进制
A 255.0.0.0 11111111 00000000 00000000 00000000
B 255.255.0.0 11111111 11111111 00000000 00000000
C 255.255.255.0 11111111 11111111 11111111 00000000

通过 IP 地址与子网掩码推算出其他信息

1
2
1. 子网地址: 主机 IP 地址 & 子网掩码
2. 广播地址: 子网地址 | 子网掩码反码