前情提醒,在本次学习中,我使用的代码将会是llfc大佬的代码,因为实在是懒的敲,这段博客可以视为是我个人的一个笔记。具体的可以去看llfc大佬的个人博客喵

终端节点

​ 在网络编程中,联系端对端的基础单位是一个套接字,对于asio来说,其实现了一层自己的框架,或者说规则。其对于使用其来开发的网络编程中进行了一系列的封装。在这其中,终端该概念,就是基于套接字进行的封装。但是需要注意的是,终端节点本身并不是一个套接字,只有对其进行进一步的包装,其才能成为一个合格的套接字。

​ 我们来看到对应的创建终端节点的代码。正如在之前的tinyweb去创建一个套接字的前置工作一样,我们这里也需要对于终端节点的构造进行一些前置准备。简单来说,为了生成一个终端节点,我们需要先去包装一个ip地址以及对应的窗口。类比对应的getaddrinfo系统调用即使前俩个参数的信息。那么对于ip地址的构建,其又存在着什么规则呢,请看看下面源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int client_end_point()
{
std::string raw_ip_address = "127.4.8.1";
unsigned short port_num=3333;
boost::system::error_code ec;
asio::ip::address ip_address = asio::ip::make_address(raw_ip_address,ec);
if(ec.value()!=0)
{
std::cout<<"Failed to parse the IP adderss,Error code = "<<ec.value()<<".Message is "<<ec.message();
return ec.value();
}

asio::ip::tcp::endpoint ep(ip_address,port_num);
}

​ 这里以客户端的终端节点创建为例,在创建节点前,我们需要优先知道所需要连接的服务器端的ip地址。有了这个ip地址,再配合我们自己声明的一个用于储存错误代码的boost::system::error_code成员,我们可以进行make_address函数的调用。该函数位于asio::ip域下,使用一个ip地址和一个错误储存码来构建一个在asio中合格意义的ip地址。

​ 前面的传入ip地址只是一个字符串,直接使用其来进行通信具有太多的风险,使用类来对其进行包装能使得程序更加健壮。

​ 对于第二个参数,顾名思义,其是一个错误储存码,储存着在该次创建过程中的状态信息,当且仅当在函数正常返回时返回0.对于这种简单的错误处理,之后将不再进行赘述。

​ 在创建完一个封装完毕的ip地址之后,我们可以使用其来构建我们的ip地址。在asio中,相对于之前直接使用c系统调用来进行创建的最大不同点是,我们在这里不必再对其进行一些属性信息在传递参数上的设置。更简单的。C++实现的asio通过类作用域来进行一个区分,这样既显式的指出了我们在创建一个终端节点时的繁琐,而且大大提高了使用的连接的可读性。

1
asio::ip::tcp::endpoint ep(ip_address,port_num);

​ 在该类对象创建中,通过传入一个ip地址和对应的端口号,我们可以对其进行一个绑定,由于这是一个客户端,所以这里指定的其实就是对应的服务器端的服务提供位置。除此之外,还可以看到,在该类成员构造之前,我们使用了一系列的类作用域限定符。通过ip::tcp::endpoint这种层次上的逻辑,我们不难推断出在该终端节点中我们使用的协议以及该成员变量的用处。

​ 接下来来看到对应与客户端节点的服务器端节点的创建,正如一般的网络编程的服务器端设置比客户端设置简单一样。对应的服务器端由于使用的端口地址被固定为了本地地址,所以这里的终端节点的创建也更加简单。但是,这是基于简单的实现的。在asio或者更加现代的网络库中,对于机器监听的设置更加灵活。对于一个进程所请求的监听设置,其可能不再局限于一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int  server_end_point(){
// Step 1. Here we assume that the server application has
//already obtained the protocol port number.
unsigned short port_num = 3333;

// Step 2. Create special object of asio::ip::address class
// that specifies all IP-addresses available on the host. Note
// that here we assume that server works over IPv6 protocol.
asio::ip::address ip_address = asio::ip::address_v6::any();

// Step 3.
asio::ip::tcp::endpoint ep(ip_address, port_num);

// Step 4. The endpoint is created and can be used to
// specify the IP addresses and a port number on which
// the server application wants to listen for incoming
// connections.
return 0;
}

​ 在这里,我们需要对于一个计算机上的ip地址再进行一下分析。在很多情况下,我们其实都认为一个计算机存在一个自己唯一的ip地址。事实上也确实如此,但是!这是相对于整个互联网网络来说的。对于一个计算机本身,其还存在着一系列的用于本身的测试等作用的地址,就比如我们很熟悉的本地回环地址localhost。这个回环地址与计算机在公网上的地址是不一样的。

​ 回到我们这里的服务器端的地址的构建,这里其实本质上是将所有的本地使用的地址都绑定到了对应的类结构上了。这个可以去查看对应的asio::ip::address源码去了解。对应的,一但使用any进行对应的服务器ip地址的构建,其将能够接受来自本机上所有不同属性的请求。就比如,来自本地的IPV6协议网络请求,来自局域网和公网的网络请求等。但是,这并不意味着使用any绑定后该进程能够拦截所有来自外部的请求。对应的请求还是只能传输给对应的端口。这里也就是不必关心出现问题的原因。

​ 在创建出对应的IP地址之后,接下来的终端节点创建工作与客户端无异,这里就直接跳过了。

数据库

​ 在这一个分类中,将记录我的数据库学习过程,主要是个人在学习15-445网课的思考,敬请期待吧,没什么好说的,让我们开始吧。之后有什么要补充的将会在这篇文章中进行补充(如果没忘的话)。

Read more »

模版

​ 模版是C++中一个相对重要的特性了,直接来吧

类模版

​ 由于这种东西还是得上手去敲,所以我们在这里对很多内容将会进行简化。

​ 模版这种东西最直观的作用就是进行方法与所使用的数据类型之间的解耦,使得在设计方法时能够更加专注于特定功能的方法的构建而不必要去关注所使用的数据类型。

Read more »

该篇是在对于std标准库的迭代器初步接触时所写,只是一些基本的常识以及对于部分类知识的回顾

迭代器

初步了解

​ 迭代器本身其实并没有什么特殊的,我们需要先从它存在的意义开始了解,对于一个迭代器,其的存在意义就是为一系列的容器提供一个通用的接口。通过这个接口,我们能够实现一系列的操作。

Read more »

高精度

加法

​ 高精度的加法主要是逐位的加,在大于10时向前进位即可,需要注意的就是需要一开始进行位的补齐,以方便后续的运算。

​ 主要就是以下几个要点:

  • 对于进行运算的俩个字符串的位进行补齐,统一之后的位计算
  • 对于每个位进行简单加法运算的时候需要注意进位的时机
  • 对于运算后可能的额外位别给忘了
Read more »

数据库环境构建

​ 对于一个普通的java项目,想要实现对于数据库操作的支持,及连接数据库以及对于数据库的增删查改等。需要一下几个操作(可能存在的操作)

Read more »

中介者模式

定义

​ 用一个中介对象来封装一系列的对象交互。中介者使得各对象不需要显示的相互引用,从而是其耦合松散,而且可以独立地改变他们的交互。

Read more »

迭代器模式

定义

​ 提供一个方法顺序·访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。

Read more »

命令模式

定义

将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请 求排队或者记录请求日志,可以提供命令的撤销和恢复功能

粗略理解

​ 奇怪,我对于这个命令模式就是有点疑惑,我就是不知道它到底是一个什么情况。让我简单先敲一遍,兴许就能理解问题了。

​ 先举一种简单的现实生活中的命令模式来进行理解。就是餐馆的一个点餐系统。在现实生活中,你需要去思考你到底要选择那一道菜,然后你需要去考虑把你选择的这些个菜单送给服务员,然后服务员会将这些菜送给后厨,然后用户就只需要等待。

​ 但是我们有没有考虑过没有服务员这种情况呢,如果没有服务员,我们就需要去直接与后厨进行对接。这想想都不现实对吧。在程序设计中,这其实也是一种哲学。当我们程序设计得在用户实现一个功能直接调用底层的api时,这种设计是极其糟糕的。当然我这句话的描述有点问题,主要就是这里的用户是指使用设计好的程序的用户,而不是程序设计阶段的。

​ 这种没有服务员的情况其实就是一种用户请求与具体实现之间的强耦合。再举一个甲乙方的例子来看。当甲方下发一个需求的时候,具体的乙方一般是不会看到这个甲方来直接去找到对应的员工来实现对应的功能的。相反,我们通常是把这个命令发送给乙方的对接部门,然后通过一些处理,这些命令会被分包并且下发给对应的实现部门。

​ 我好像有点理解了。所谓的命令模式,要实现的就是一个命令发出者与命令实现之间的一个解耦。这时就需要我们去进行在中间的一层代理,在点餐系统中这是服务员。在项目对接中这个可能是项目经理。但是不变的是,在这种对接中,我们解开了命令发出者和命令执行者之间的强耦合。

​ 通过这种解耦,客户将不用去后厨直接找到对应的厨师进行下单。只需要更具餐馆的规范去进行我们需求的发出,服务员负责添加这些需求到对应的列表中,而服务员接下来还负责将这些个需求按一定顺序发送给对应的后厨部门进行完成,这种分层式的设计使得用户不再需要去了解底层复杂的东西,只需要去了解餐馆提供的简单的菜单即可,其他的逻辑由下面的命令转发者和命令执行者进行操作。

​ 到这里我其实明白了之前的误解。在理解命令模式中,不要在一开始就想得太多。类似与服务员这种消息转发者能够处理一些非常简单的消息转发,就比如用户想要那道菜,服务员就通知后厨做那道菜。除此之外,用户可能还会点套餐,这是一套复杂的命令,可能需要多个处理动作配合使用,这个也是服务员这个中间层需要做的事,但是一开始我们并不需要去拘泥在这里,可以从简单的地方出发。

​ 还有,当我们需要添加菜单时,我们至少也需要去为对应的后厨添加对应的处理动作,这也是我之前存在疑惑的地方,不过现在也解开了一点了。

总的来说,我现在感觉命令模式的核心在于:命令的处理与转发,通过引入一个具有转发行为的类来进行解耦

在最简单的命令模式中,这个转发可以是一个不做任何处理的转发,其他复杂情况再说,现在先从简单的入手。

代码实例

​ 在我看来,理解命令模式中,去看代码甚至比看UML类图更加重要。至少我第一次看的视乎类图根本看不懂它在说什么。

命令

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
// Receiver: 后厨
class Kitchen {
public:
void prepareDish(const std::string& dish) {
std::cout << "Kitchen is preparing: " << dish << std::endl;
}
};

// Command: 命令接口
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
};

// ConcreteCommand: 具体命令类
class PrepareDishCommand : public Command {
private:
Kitchen* kitchen;
std::string dish;
public:
PrepareDishCommand(Kitchen* k, const std::string& d) : kitchen(k), dish(d) {}
void execute() override {
kitchen->prepareDish(dish);
}
};

​ 这里的几个类就是命令模式中的基本单位:命令的基本构成。在一个具体的命令中,需要包含这个命令具体的执行部门,所以这里保留了一个后厨的一个指针,用于进行执行部门的指定。我猜想,如果在后期需要对这个后厨部门进行划分,会将这个执行部门进行派生,至于继承还是聚合组合,那不是我们现在考虑的问题。

转发者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Invoker: 服务员
class Waiter {
private:
std::vector<std::shared_ptr<Command>> orders;
public:
void takeOrder(std::shared_ptr<Command> command) {
orders.push_back(command);
}
void sendOrders() {
for (const auto& order : orders) {
order->execute();
}
orders.clear();
}
};

​ 可以看到,这里的转发者维护了一个命令队列,所有的客户给出的命令将会储存在这个队列中。我们还可以观察到这里的命令实际执行情况,其实就是一次程序控制流的转移,将当前的执行权限交给命令类进行自我的执行来实现对应的功能。

​ 这里当然还可以进行一系列的扩展,就比如进行消息的撤销等操作,可以自行添加。

​ 总的来说,这里的转发类重要的是这里的命令处理方法。通过储存客户端发出的所有命令,在适当的时机通过自身设定的命令执行顺序来进行对应的执行。而这里的执行,就跟很多设计模式一样,实际上就是进行一次程序控制流的转移,将程序控制流从当前的中转类转移到对应的命令处理程序中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Client: 客户端
int main() {
// 创建接收者
Kitchen kitchen;

// 创建具体命令
auto steakCommand = std::make_shared<PrepareDishCommand>(&kitchen, "Steak");
auto pastaCommand = std::make_shared<PrepareDishCommand>(&kitchen, "Pasta");

// 创建调用者
Waiter waiter;
waiter.takeOrder(steakCommand);
waiter.takeOrder(pastaCommand);

// 服务员发送订单
waiter.sendOrders();

return 0;
}

​ 这里其实就没有什么好说的,需要注意的是,在命令模式中,我们需要在程序一开始进行所有的命令的初始化,之后我们所有发出的命令本质上其实就是保留了一份对应的指针。当然,你可以在每次调用时都进行副本的一次创建,不给你也应该知道这种模式的坏处。

下面给出一个不是很美的UML类图,我自己都没去看()

UML类图

img