系列文章目录
提示:本文是本系列教程的第4篇,前期工作请参考以下3篇 :
使用TicToc学习OMNeT++——第 0 部分 - 简介
使用TicToc学习OMNeT++——第 1 部分 - 入门
使用TicToc学习OMNeT++——第 2 部分 - 运行仿真
提示:本人的第4篇文章,写得不好,请读者多多包涵
文章目录
- 系列文章目录
- 前言
- 一、原文翻译
- Part 3 - Enhancing the 2-node TicToc 第 3 部分 - 增强 2 节点 TicToc
- 3.1 Adding icons 3.1 添加图标
- 3.2 Adding logging 3.2 添加日志记录
- 3.3 Adding state variables 3.3 添加状态变量
- 3.4 Adding parameters 3.4 添加参数
- 3.5 Using NED inheritance 3.5 使用 NED 继承
- 3.6 Modeling processing delay 3.6 建模处理延迟
- 3.7 Random numbers and parameters 3.7 随机数和参数
- 3.8 Timeout, cancelling timers 3.8 超时,取消计时器
- 3.9 Retransmitting the same message 3.9 重新传输相同的消息
- 二、补充内容
- 总结
前言
提示:这里说明了本文要记录的大概内容:
本文基于原版英文教程的第3部分:Part 3 - Enhancing the 2-node TicToc进行翻译和论述,并结合仿真手册,用户指南和AI问答以厘清基本概念。
提示:以下是本篇文章正文内容
一、原文翻译
原文经过本人的人工润色,主要对一些复杂嵌套的英文语句调整语序,修改一些术语翻译错误,然后是修正句内的修饰关系。此外,原文翻译中还补充了一些自己实操过程中的截图。
Part 3 - Enhancing the 2-node TicToc 第 3 部分 - 增强 2 节点 TicToc
3.1 Adding icons 3.1 添加图标
Here we make the model look a bit prettier in the GUI. We assign the block/routing
icon (the file images/block/routing.png
), and paint it cyan for tic
and yellow for toc
. This is achieved by adding display strings to the NED file. The i=
tag in the display string specifies the icon.
在这里,我们使模型在 GUI 中看起来更漂亮一些。我们分配 block/routing
图标(文件 images/block/routing.png
),并将 tic
涂成青色,将 toc
涂成黄色。这是通过向 NED 文件添加显示字符串来实现的。显示字符串中的 i=
标记指定图标。
提示:原来block/ruoting中间的斜线是目录和文件之间的分隔符,之前一直没想明白😂
simple Txc2
{
parameters:
@display("i=block/routing"); // add a default icon 添加一个默认图标
gates:
input in;
output out;
}
//
// Make the two module look a bit different with colorization effect. 使用着色效果使两个模块看起来有点不同。
// Use cyan for `tic', and yellow for `toc'. 青色用于“tic”,黄色用于“toc”。
//
network Tictoc2
{
submodules:
tic:Txc2{
parameters:
@display("i=,cyan"); // do not change the icon (first arg of i=) just colorize it 不要更改图标(第一个参数的 i=),仅将其着色即可
}
toc:Txc2{
parameters:
@display("i=,gold"); // here too 这里也是
}
connections:
tic.out --> { delay = 100ms; } --> toc.in;
tic.in <-- { delay = 100ms; } <-- toc.out;
}
You can see the result here:
您可以在此处查看结果:
3.2 Adding logging 3.2 添加日志记录
We also modify the C++ code. We add log statements to Txc1
so that it prints what it is doing. OMNeT++ provides a sophisticated logging facility with log levels, log channels, filtering, etc. that are useful for large and complex models, but in this model we’ll use its simplest form EV
:
我们还修改了 C++ 代码。我们添加日志语句到 Txc1
,以便它打印它正在做的事情。OMNeT++ 提供了一个复杂的日志记录工具,其中包含日志级别、日志通道、过滤等,这些功能对大型和复杂的模型很有用,但在这个模型中,我们将使用其最简单的形式 EV
:
EV << "Sending initial message\n";
and 和
EV << "Received message'" << msg->getName() << "',sending it out again\n";
When you run the simulation in the OMNeT++ runtime environment, the following output will appear in the log window:
在 OMNeT++ 运行时环境中运行模拟时,日志窗口中将显示以下输出:
提示:如果找不到Qtenv的日志窗口,可以按住下方的横向滚动条的下边缘,然后向上拉出来,我也是今天才发现😂
You can also open separate output windows for tic and toc by right-clicking on their icons and choosing Component log from the menu. 您还可以通过右键单击 tic 和 toc 的图标并从菜单中选择 Component log 来打开单独的输出窗口。
This feature will be useful when you have a large model (“fast scrolling logs syndrome”) and you’re interested only in the log messages of specific module.
当您有一个大型模型(“快速滚动日志综合症”)并且您只对特定模块的日志消息感兴趣时,此功能将很有用。
Sources: tictoc2.ned, txc2.cc, omnetpp.ini
源(代码):tictoc2.ned, txc2.cc, omnetpp.ini
3.3 Adding state variables 3.3 添加状态变量
In this step we add a counter to the module, and delete the message after ten exchanges.
在此步骤中,我们向模块添加一个计数器,并在十次交换后删除消息。
We add the counter as a class member:
我们将计数器添加为类成员:
class Txc3 : public cSimpleModule
{
private:
int counter; // Note the counter here // 注意这里的计数器 , 注意这会使得 tic 和 toc 模块实例各有一个只属于自身的计数器 counter!!!
protected:
virtual void initialize() override;
virtual void handleMessage(cMessage *msg) override;
};
We set the variable to 10 in initialize()
and decrement in handleMessage()
, that is, on every message arrival. After it reaches zero, the simulation will run out of events and terminate.
我们将变量在 initialize()
中设置为10和在 handleMessage()
中递减,即,在每次消息到达时(减1)。当它达到零时,模拟将耗尽事件并终止。
Note the 请注意
// The WATCH() statement below will let you examine the variable under 下面的WATCH()语句将让您检查
// Tkenv. After doing a few steps in the simulation, double-click either Tkenv下的变量。在模拟中完成一些步骤后,双击
// `tic' or `toc', select the Contents tab in the dialog that pops up, 'tic' 或 'toc', 在弹出的对话框中选择内容选项卡,
// and you'll find "counter" in the list. //你会在列表中找到"counter"。
WATCH(counter);
line in the source: this makes it possible to see the counter value in the graphical runtime environment.
源代码中的行:这样就可以在图形运行时环境中查看计数器值。
If you click on tic
’s icon, the inspector window in the bottom left corner of the main window will display details about tic
. Make sure that Children mode is selected from the toolbar at the top. The inspector now displays the counter variable.
如果单击 tic
的图标,主窗口左下角的检查器窗口将显示有关 tic
的详细信息。确保从顶部的工具栏中选择了子女模式。检查器现在显示计数器变量。
As you continue running the simulation, you can follow as the counter keeps decrementing until it reaches zero.
当您继续运行模拟时,您可以跟随计数器不断递减,直到它达到零。
提示:仔细分析就会发现,toc的计数器会先达到0,同时tic的计数器为1
Sources: tictoc3.ned, txc3.cc, omnetpp.ini
源(代码):tictoc3.ned, txc3.cc, omnetpp.ini
3.4 Adding parameters 3.4 添加参数
In this step you’ll learn how to add input parameters to the simulation: we’ll turn the “magic number” 10 into a parameter and add a boolean parameter to decide whether the module should send out the first message in its initialization code (whether this is a tic
or a toc
module).
在此步骤中,您将学习如何添加输入参数到模拟中:我们将“幻数”10 转换为参数并添加布尔参数,以决定模块是否应在其初始化代码中发送第一条消息(无论这是模块 tic
还是模块 toc
)。
Module parameters have to be declared in the NED file. The data type can be numeric, string, bool, or xml (the latter is for easy access to XML config files), among others.
模块参数必须在 NED 文件中声明。数据类型可以是数字、字符串、布尔值或 xml(后者用于轻松访问 XML 配置文件)等。
simple Txc4
{
parameters:
bool sendMsgOnInit = default(false); // whether the module should send out a message on initialization 模块是否应该在初始化时发出消息
int limit = default(2); // another parameter with a default value 另一个具有默认值的参数
@display("i=block/routing");
gates:
input in;
We also have to modify the C++ code to read the parameter in initialize()
, and assign it to the counter.
我们还必须修改 C++ 代码以读取 initialize()
中的参数,并将其赋值给计数器。
counter = par("limit"); // par函数用于读取ned文件中的 参数/parameter
We can use the second parameter to decide whether to send initial message:
我们可以使用第二个参数(难道不是第一个吗?)来决定是否发送初始消息:
if(par("sendMsgOnInit").boolValue() == true){ // 单双引号定界的字符串是有区别的,会被函数视为不同类型的参数!!!
Now, we can assign the parameters in the NED file or from omnetpp.ini
. Assignments in the NED file take precedence. You can define default values for parameters if you use the default(...)
syntax in the NED file. In this case you can either set the value of the parameter in omnetpp.ini
or use the default value provided by the NED file.
现在,我们可以在 NED 文件中或从 omnetpp.ini
中赋值参数。NED 文件中的赋值优先。如果使用 NED 文件中的 default(...)
语法,则可以定义参数的 缺省/默认 值。在这种情况下,您可以在 omnetpp.ini
中设置参数的值,也可以使用 NED 文件提供的默认值。
Here, we assign one parameter in the NED file:
在这里,我们在 NED 文件中赋值一个参数:
network Tictoc4
{
submodules:
tic:Txc4{
parameters:
sendMsgOnInit = true;
@display("i=,cyan");
}
toc:Txc4{
parameters:
sendMsgOnInit = false;
@display("i=,gold");
}
connections:
and the other in omnetpp.ini:
另一个在 omnetpp.ini 中:
Tictoc4.toc.limit = 5
Note that because omnetpp.ini supports wildcards, and parameters assigned from NED files take precedence over the ones in omnetpp.ini, we could have used
请注意,由于 omnetpp.ini 支持通配符,并且从 NED 文件赋值的参数优先于 omnetpp.ini 中的参数,因此我们可以使用
Tictoc4.t*c.limit = 5
或
Tictoc4.*.limit = 5
或甚至
**.limit = 5
with the same effect. (The difference between * and ** is that * will not match a dot and ** will.)
具有相同的效果。( * 和 ** 之间的区别在于, * 不会匹配点但 ** 会。)
In the graphical runtime environment, you can inspect module parameters either in the object tree on the left-hand side of the main window, or in the Parameters page of the module inspector (information is shown in the bottom left corner of the main window after clicking on a module).
在图形运行时环境中,您可以在主窗口左侧的对象树中或模块检查器的“参数”页面中检查模块参数(单击模块后,信息显示在主窗口的左下角)。
The module with the smaller limit will delete the message and thereby conclude the simulation.
限制较小的模块将删除消息,从而结束模拟。
Sources: tictoc4.ned, txc4.cc, omnetpp.ini
源(代码):tictoc4.ned, txc4.cc, omnetpp.ini
3.5 Using NED inheritance 3.5 使用 NED 继承
If we take a closer look at the NED file we will realize that tic
and toc
differs only in their parameter values and their display string. We can create a new simple module type by inheriting from an other one and specifying or overriding some of its parameters. In our case we will derive two simple module types (Tic
and Toc
). Later we can use these types when defining the submodules in the network.
如果我们仔细查看 NED 文件,我们将意识到, tic
和 toc
仅在它们的参数值和显示字符串上有所不同。我们可以通过从另一个模块类型继承并指定或覆盖其某些参数来创建一个新的简单模块类型。在我们的例子中,我们将派生两个简单的模块类型(Tic
和 Toc
)。稍后,我们可以在定义网络中的子模块时使用这些类型。
Deriving from an existing simple module is easy. Here is the base module:
从现有的简单模块派生是容易的。下面是基础模块:
simple Txc5
{
parameters:
bool sendMsgOnInit = default(false);
int limit = default(2);
@display("i=block/routing");
gates:
input in;
output out;
}
And here is the derived module. We just simply specify the parameter values and add some display properties.
这是派生模块。我们只是简单地指定参数值并添加一些显示属性。
simple Tic5 extends Txc5
{
@display("i=,cyan");
sendMsgOnInit = true; // Tic modules should send a message on init Tic模块应该 在 init上(在初始化时) 发送消息
}
The Toc module looks similar, but with different parameter values.
该 Toc 模块看起来相似,但参数值不同。
simple Toc5 extends Txc5
{
@display("i=,gold");
sendMsgOnInit = false; // Toc modules should NOT send a message on init Toc模块不应 在 init上(在初始化时) 发送消息
}
Note 注意 |
---|
The C++ implementation is inherited from the base simple module (Txc5). |
C++ 实现继承自基本简单模块 ( Txc5 )。 |
Once we created the new simple modules, we can use them as submodule types in our network:
一旦我们创建了新的简单模块,我们就可以将它们用作网络中的子模块类型:
network Tictoc5
{
submodules:
tic:Tic5; // the limit parameter is still unbound here. We will get it from the ini file 此处limit参数仍未绑定。我们将从ini文件中获取它
toc:Toc5;
connections:
As you can see, the network definition is much shorter and simpler now. Inheritance allows you to use common types in your network and avoid redundant definitions and parameter settings.
正如你所见,现在的网络定义要短得多,也简单得多。继承允许您在网络中使用通用类型,并避免冗余的定义和参数设置。
Sources: tictoc5.ned, txc5.cc, omnetpp.ini
源(代码):tictoc5.ned, txc5.cc, omnetpp.ini
3.6 Modeling processing delay 3.6 建模处理延迟
In the previous models, tic
and toc
immediately sent back the received message. Here we’ll add some timing: tic
and toc
will hold the message for 1 simulated second before sending it back. In OMNeT++ such timing is achieved by the module sending a message to itself. Such messages are called self-messages (but only because of the way they are used, otherwise they are ordinary message objects).
在以前的模型中, tic
和 toc
立即发回收到的消息。在这里,我们将添加一些计间: tic
和 toc
将在发回消息之前保留消息 1 仿真秒。在 OMNeT++ 中,这种计时是通过模块向自身发送消息来实现的。此类消息称为自消息(但只是因为它们的使用方式,否则它们是普通的消息对象)。
We added two cMessage * variables, event
and tictocMsg
to the class, to remember the message we use for timing and message whose processing delay we are simulating.
我们在类中添加了两个 cMessage * 变量 event
和 tictocMsg
,以记住我们用于计时的消息和我们正在模拟其处理延迟的消息。
class Txc6 : public cSimpleModule
{
private:
cMessage *event; // pointer to the event object which we'll use for timing // 指向我们将用于计时的事件对象的指针
cMessage *tictocMsg; // variable to remember the message until we send it back // 用来记住消息的变量,直到我们将它发送回来
public:
We “send” the self-messages with the scheduleAt() function, specifying when it should be delivered back to the module.
我们使用 scheduleAt() 函数“发送”自消息,指定何时应将其传递回模块。
scheduleAt(simTime()+1.0, event); // simTime应该是用于提取当前模拟时间的函数
In handleMessage()
now we have to differentiate whether a new message has arrived via the input gate or the self-message came back (timer expired). Here we are using
现在 handleMessage()
,我们必须区分新消息是通过输入门到达还是自消息返回(计时器已到期)。在这里,我们使用
if(msg == event){
but we could have written
但我们本来(也)可以写
if (msg->isSelfMessage())
We have left out the counter, to keep the source code small.
我们省略了计数器,以保持源代码的小巧。
While running the simulation you will see the following log output:
运行模拟时,您将看到以下日志输出:
Sources: tictoc6.ned, txc6.cc, omnetpp.ini
源(代码):tictoc6.ned, txc6.cc, omnetpp.ini
3.7 Random numbers and parameters 3.7 随机数和参数
In this step we’ll introduce random numbers. We change the delay from 1s to a random value which can be set from the NED file or from omnetpp.ini. Module parameters are able to return random variables; however, to make use of this feature we have to read the parameter in handleMessage()
every time we use it.
在此步骤中,我们将引入随机数。我们将延迟从 1 秒更改为随机值,该值可以从 NED 文件或 omnetpp.ini 中设置。模块参数能够返回随机变量;但是,要使用此功能,我们必须 handleMessage()
在每次使用它时读取参数。
// The "delayTime" module parameter can be set to values like 可以将"delayTime"模块参数设置为如下值
// "exponential(5)" (tictoc7.ned, omnetpp.ini), and then here "指数分布(5)" (tictoc7.ned,omnetpp.ini),然后这里
// we'll get a different delay every time. 我们每次都会得到不同的延迟。
simtime_t delay = par("delayTime");
EV << "Message arrived, starting to wait " << delay << " secs...\n";
tictocMsg = msg;
In addition, we’ll “lose” (delete) the packet with a small (hardcoded) probability.
此外,我们将以很小的(硬编码)概率“丢失”(删除)数据包
// "Lose" the message with 0.1 probability: 有0.1的概率"丢失"消息:
if(uniform(0, 1) < 0.1){ // uniform(0,1) 生成一个在[0, 1]上服从均匀分布的随机变量的值
EV << "\"Losing\" message\n";
delete msg;
}
We’ll assign the parameters in omnetpp.ini:
我们将在omnetpp.ini中分配参数:
# argument to exponential() is the mean; truncnormal() returns values from # 指数分布函数 exponential() 的参数是平均值;truncnormal()返回值来自
# the normal distribution truncated to nonnegative values # 截断为非负值的正态分布
Tictoc7.tic.delayTime = exponential(3s)
Tictoc7.toc.delayTime = truncnormal(3s, 1s)
You can try that no matter how many times you re-run the simulation (or restart it, Simulate -> Rebuild network menu item), you’ll get exactly the same results. This is because OMNeT++ uses a deterministic algorithm (by default the Mersenne Twister RNG) to generate random numbers, and initializes it to the same seed. This is important for reproducible simulations. You can experiment with different seeds if you add the following lines to omnetpp.ini:
您可以尝试,无论重新运行模拟多少次(或重新启动模拟,模拟 ->重建网络菜单项),您都会得到完全相同的结果。这是因为 OMNeT++ 使用确定性算法(默认为 Mersenne Twister RNG)来生成随机数,并将其初始化为相同的种子。这对于可重复的模拟非常重要。如果将以下行添加到omnetpp.ini,则可以试验不同的种子:
[General]
seed-0-mt=532569 # or any other 32-bit value
From the syntax you have probably guessed that OMNeT++ supports more than one RNGs. That’s right, however, all models in this tutorial use RNG 0.
从语法中,您可能已经猜到OMNeT++支持多个RNG。没错,但是,本教程中的所有模型都使用 RNG 0。
Exercise 锻炼 |
---|
Try other distributions as well. |
也可以尝试其他分布。 |
Sources: tictoc7.ned, txc7.cc, omnetpp.ini |
Sources: tictoc7.ned, txc7.cc, omnetpp.ini
源(代码):tictoc7.ned, txc7.cc, omnetpp.ini
3.8 Timeout, cancelling timers 3.8 超时,取消计时器
In order to get one step closer to modelling networking protocols, let us transform our model into a stop-and-wait simulation. This time we’ll have separate classes for tic
and toc
. The basic scenario is similar to the previous ones: tic
and toc
will be tossing a message to one another. However, toc
will “lose” the message with some nonzero probability, and in that case tic
will have to resend it.
为了更接近网络协议建模,让我们将模型转换为停止和等待模拟。这一次,我们将为 tic
和 toc
提供单独的类。基本场景与前面的场景类似: tic
和 toc
将相互抛出消息。但是, toc
将以一些非零概率“丢失”消息,在这种情况下 tic
将不得不重新发送它。
Here’s toc
’s code:
这是 toc
代码:
void Toc8::handleMessage(cMessage *msg)
{
if(uniform(0,1) < 0.1) {
EV << "\"Losing\" message.\n";
bubble("Message lost"); // making animation more informative... 使动画具有更多信息…
delete msg;
}
else {
EV << "Sending back same message as acknowledgement.\n"; // 发回同样的消息作为确认
send(msg, "out");
}
}
Thanks to the bubble()
call in the code, toc
will display a callout whenever it drops the message.
由于代码中的 bubble()
调用, toc
都会显示标注,每当它删除消息时。
So, tic
will start a timer whenever it sends the message. When the timer expires, we’ll assume the message was lost and send another one. If toc
’s reply arrives, the timer has to be cancelled. The timer will be (what else?) a self-message.
因此, ‘tic’ 每当它发送消息时都会启动一个计时器。当计时器到期时,我们将假设消息已丢失并发送另一条消息。如果 toc
收到回复,则必须取消计时器。计时器将是(还能是什么?)自消息。
scheduleAt(simTime()+timeout, timeoutEvent); // 通过安排发出超时事件消息,开始超时计时。
Cancelling the timer will be done with the cancelEvent()
call. Note that this does not prevent us from being able to reuse the same timeout message over and over.
取消计时器将通过 cancelEvent()
的调用 完成。请注意,这并不妨碍我们一遍又一遍地重复使用相同的超时消息。
cancelEvent(timeoutEvent); // 超时事件自消息不会再到达,也就是其从事件队列中被删除?
You can read Tic’s full source in txc8.cc
您可以在 txc8.cc 中阅读 Tic 的完整来源
Sources: tictoc8.ned, txc8.cc, omnetpp.ini
源(代码):tictoc8.ned, txc8.cc, omnetpp.ini
3.9 Retransmitting the same message 3.9 重新传输相同的消息
In this step we refine the previous model. There we just created another packet if we needed to retransmit. This is OK because the packet didn’t contain much, but in real life it’s usually more practical to keep a copy of the original packet so that we can re-send it without the need to build it again. Keeping a pointer to the sent message - so we can send it again - might seem easier, but when the message is destroyed at the other node the pointer becomes invalid.
在此步骤中,我们优化了以前的模型。在那里,如果需要重新传输,我们只是创建了另一个数据包。这没关系,因为数据包包含的内容不多,但在现实生活中,保留原始数据包的副本通常更实用,这样我们就可以重新发送它而无需再次构建它。保留指向已发送消息的指针(以便我们可以再次发送它)似乎更容易,但是当消息在另一个节点上被销毁时,指针将变得无效。
What we do here is keep the original packet and send only copies of it. We delete the original when toc
’s acknowledgement arrives. To make it easier to visually verify the model, we’ll include a message sequence number in the message names.
我们在这里所做的是保留原始数据包并仅发送其副本。当toc
的确认到达时,我们会删除原始文件。为了便于直观地验证模型,我们将在消息名称中包含消息序列号。
In order to avoid handleMessage()
growing too large, we’ll put the corresponding code into two new functions, generateNewMessage()
and sendCopyOf()
and call them from handleMessage()
.
为了避免 handleMessage()
变得太大,我们将相应的代码放入两个新函数中, generateNewMessage()
和 sendCopyOf()
从 handleMessage()
中调用它们。
The functions: 函数:
cMessage *Tic9::generateNewMessage()
{
// Generate a message with a different name every time. 每次生成不同名称的消息
char msgname[20];
sprintf(msgname, "tic-%d", ++seq); // 向字符串中打印
cMessage *msg = new cMessage(msgname);
return msg;
}
void Tic9::sendCopyOf(cMessage *msg)
{
// Duplicate message and send the copy. 复制消息并发送副本
cMessage *copy = (cMessage*)msg->dup();
send(copy, "out");
}
Sources: tictoc9.ned, txc9.cc, omnetpp.ini
源(代码):tictoc9.ned, txc9.cc, omnetpp.ini
二、补充内容
对于教程中涉及到的概念,本人查阅仿真手册得到的相关内容。以及询问AI得到的回答。
1. NED语言中的 volatile 关键字
根据3.7节 tictoc7.ned 中的 txc7简单模块类型的 parameters段:
volatile double delayTime @unit(s); // delay before sending back message 发送回消息前的延迟
因此这个参数的值是在运行时也会不断改变的,因为每次访问,都会重新调用一次其存储的(返回随机值的)表达式。
根据仿真手册的3.6.4节的第1段
Volatile parameters are those marked with the volatile modifier keyword. Normally, expressions assigned to parameters are evaluated once, and the resulting values are stored in the parameters. In contrast, a volatile parameter holds the expression itself, and it is evaluated every time the parameter is read. Therefore, if the expression contains a stochastic or changing component, such as normal(0,1) (a random value from the unit normal distribution) or simTime() (the current simulation time), reading the parameter may yield a different value every time.
易变参数是用 volatile 修饰符关键字标记的参数。通常,赋值参数的表达式(只)计算一次,结果值存储在参数中。相反,易失性参数保存表达式本身,并且每次读取参数时都会对其进行计算。因此,如果表达式包含随机或变化分量,例如 normal(0,1) (来自单位正态分布的随机值) 或 simTime() (当前模拟时间),则读取参数每次都可能产生不同的值。
总结
提示:这里对文章进行总结:
终于完成了第4篇,目测也是最长的一篇😤(真不容易😔)。目前实在没想到更多的补充内容😭。也许这篇也并不会有什么大的用处,但也是一种自我学习的过程,也是对自己的学习心得的一种记录,最后如果能对读者有点用处的话,那就再好不过了😂。