单例模式

单例模式

1.何为单例模式

​ 单例模式的理念是,应用程序中只能有一个特定组件的实例。保证一个类只有一个实例,并提供一个该实例的全局访问点。

2.单例模式的意义和使用场景

​ 有的类需要保证只有一个实例才能确保其逻辑正确性以及保持良好的效率。

3.单例模式的实现

​ 只需要将单例模式的构造函数、拷贝构造函数、移动构造函数、拷贝赋值函数删除,再提供一个返回唯一实例的指针的函数就可以了。

4.多线程下的单例模式

单例模式的两种实现方式

​ 1.饿汉式:唯一实例在定义时就初始化,这种方式是线程安全的。但是它会使程序启动事件增加,浪费资源。

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton {
private:
static Singleton* instance;
Singleton() {} // 将构造函数设为私有,禁止外部实例化

public:
static Singleton* getInstance() {
return instance;
}
};

Singleton* Singleton::instance = new Singleton(); // 在定义静态成员变量时直接初始化

​ 2.懒汉式:延迟初始化的方式,即在首次使用时才创建单例对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Singleton {
private:
static Singleton* instance;
Singleton() {} // 将构造函数设为私有,禁止外部实例化

public:
static Singleton* getInstance() {
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
};

Singleton* Singleton::instance = nullptr; // 初始化静态成员变量

​ 这种形式的创建存在线程安全问题,有两种解决方案。一是在 getInstance()函数的第一行上锁,但这种方式十分浪费资源,在高并发环境下不可取。

​ 第二种是使用双重校验锁。这种双重校验锁在c++11前会出现问题,因为编译器优化可能会分配内存后直接将地址返回给指针。导致在一段时间内,指针指向未完全初始化的内存。而其他线程可能会将这个未初始化完成的内存返回。

​ C++11标准对静态局部变量的初始化进行了明确规定:静态局部变量在第一次访问时进行初始化,且只有一个线程能够执行初始化过程。这确保了静态局部变量的线程安全性。

1
2
3
4
5
6
7
8
static Singleton* getInstance() {
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mtx); // 加锁
if (instance == nullptr) { // 双重检查
instance = new Singleton();
}
}
return instance;

原型模式

原型模式

1.何为原型模式

​ 使用原型实例来制定创建对象的种类,然后拷贝这些原型来创造新的对象。

2.原型模式的意义和使用场景

​ 在生产中,存在一些比较负责的类,它们经常会发生改变,用原型克隆的方式来创建这样的类的对象。它可是使我们灵活地动态创建某些拥有稳定接口的新对象。所需的工作仅仅是注册一个新对象(原型),然后在任何需要的地方进行克隆。

3.如何使用

​ 在抽象基类中定义一个clone接口,然后每当有新类继承于该抽象类,就去实现相应的clone接口。当需要创建该类对象时,传入原型(即一个已经创建好的该类对象),然后调用clone函数创建

工厂方法

设计模式—工厂方法

1.何为工厂方法

​ 定义一个用来创建对象的接口,让子类来决定实例化哪一个类。工厂方法将一个类的实例化延迟到子类。

2.工厂方法的意义和使用场景

​ 工厂方法隔离了类对象的使用者和具体类型之间的耦合关系。面对一个经常变化的类型,紧耦合(new)会导致软件脆弱。它体现了单一责任原则和开闭原则。

​ 工厂模式解决“单个对象”的需求变化,缺点在于要求创建方法/参数相同。

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
		//分割器抽象类(稳定)
class ISplitter
{
public:
virtual void split() = 0;
virtual ~ISplitter(){}

};
//分割器工厂抽象类(稳定)
class SplitterFactory
{
public:
virtual ISplitter* create() = 0;
virtual ~SplitterFactory(){}

};
//文本分割器(变化)
class textSplitter :public ISplitter
{
virtual void split() {};
};
class textSplitterFactory :public SplitterFactory
{
virtual ISplitter* create() {};
};
//工厂生产对象的过程
class MainForm
{
SplitterFactory* myfactory;
MainForm(SplitterFactory* factory)
{
myfactory = factory;
}
void fun()
{
ISplitter* splitter = myfactory->create(); //实现了多态new,根据工厂的不同创建不同的splitter
splitter->split();
}
};

4.工厂方法的其他作用

​ 1.可以在创建对象的时候加入对参数的验证,当参数不满足条件时拒绝创建对象

​ 2.可以在工厂中创建一个vector,来跟踪记录已经创建好的对象。

5.抽象工厂

​ 抽象工厂是提供一个接口,把一系列相关的、相互依赖的对象的创建交给工厂来做,而无需指定具体的类。

​ 如一个数据库对象需要连接、命令、数据结果三个对象,使用一个工厂将相匹配的三个对象一起创建。这样即实现了解耦,又避免了用工厂方法一个一个创建对象可能造成的不匹配问题。(如创建了mysql连接,却又创建了redis命令来搭配使用),抽象工厂将这三个对象的创建封装在一起,保证了正确性。

TCP粘包问题

TCP粘包问题

1.何为TCP粘包

​ 因为TCP是面向流,没有边界,而操作系统在发送TCP数据时,会通过缓冲区来进行优化,例如缓冲区为1024个字节大小。

如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题。

2.如何解决

  • 发送端将每个包都封装成固定的长度,比如100字节大小。如果不足100字节可通过补0或空等进行填充到指定长度;
  • 发送端在每个包的末尾使用固定的分隔符,例如\r\n。如果发生拆包需等待多个包发送过来之后再找到其中的\r\n进行合并;例如,FTP协议;
  • 将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息;
  • 通过自定义协议进行粘包和拆包的处理

TCP拥塞控制

TCP拥塞控制

1.拥塞控制是什么?

​ 拥塞控制是TCP连接的每一方都要进行的行为,它通过算法预测网络线路上的拥塞情况,根据拥塞情况来调整和指导数据的发送。

2.为什么要有拥塞控制?

​ 要讲拥塞控制,首先要讲明白拥塞。所谓拥塞,是路由器因无法处理高速率到达的流量而被迫丢弃数据信息的现象。这里要特别注意区别拥塞控制和窗口管理的区别,窗口管理是基于TCP连接双方的,双方根据各自应用层处理数据的速率来协调发送窗口和接受窗口。而拥塞控制是基于整个网络的,它的目的是尽可能地缓解网络上出现的拥塞现象。

3.如何进行拥塞控制

​ 拥塞控制的核心是:当检测到网络拥塞时,减缓数据的发送速率。在发送端引人一个窗日控制变量,确保发送窗口大小不超过接收端接收能力和网络传输能力,即TCP发送端的发送速率等于接收速率和传输速率两者中较小值。这个窗口称为拥塞窗口(cwnd)

​ SMSS:TCP报文段的最大长度(仅指数据部分)

1.慢启动

​ 当一个新的TCP连接建立或检测到由重传超时(RTO)导致的丢包时,需要执行慢启动。何为慢启动?慢启动实际上是对拥塞窗口的大小控制算法,它先将cwnd设置成一个较小值(通常为最大段大小和最大传输单元中的较小值),每当收到一个ACK包,表明网络情况允许当前发送速率,cwnd就可以尝试进行扩大。因此,在接收到一个数据段的ACK后,通常cwnd值会增加到2,接着会发送两个数据

段。如果成功收到相应的新的ACK, cwnd会由2变4,由4变8,以此类推,直到cwnd == 接收窗口。

​ 前所述, cwnd会随着RTT呈指数增长。因此,最终cwnd会增至很大,大量数据包的发送将导致网络瘫痪(TCP吞吐量与,RTT成正比)。当发生上述情况时, cwnd大幅度减小(减至原值一半)。这是TCP由慢启动阶段至拥塞避免阶段的转折点,与cwnd和慢启动阂sIowstart threshold,ssthresh)相关。

2.拥塞避免

​ 为了得到更多的传输资源而不致影响其他连接传输, TCP实现了拥塞避免算法。一旦确立慢启动阈值, TCP会进人拥塞避免阶段, cwnd每次的增长值近似于成功传输的数据段大小。将指数增长转为线性增长。

慢启动阈值的设定

​ 慢启动闹值的初始值可任意设定(如awnd或更大),这会使得TCP总是以慢启动状态开始传输。当有重传情况发生,无论是超时重传还是快速重传, ssthresh=maX (当前发送窗口大小/2, 2*SMSS)

3.快速恢复

​ 快速恢复机制一般和快速重传机制同时使用。

​ 当发送端收到第三个重复确认的报文时,会更新ssthresh的值,然后立即重传丢失的报文段,并且设置:cwnd = ssthresh+3*SMSS,进入拥塞避免阶段。当收到一个重复确认的报文时,设置cwnd = cwnd +SMSS。此时发送端可以发送新的TCP报文(如果新的cwnd允许)当收到新数据的确认时,设置cwnd=ssthresh。进入拥塞避免阶段。这里的新数据表示新的报文,而不是丢失的报文。

在旧的tcp拥塞控制算法中,快速重传之后会进入慢启动阶段,而新的tcp拥塞控制算法在快速重传之后进入快速恢复阶段。
20200429142015139

TCP重传机制

TCP重传机制

​ 为了保证数据的正确性,TCP协议会重传丢失的数据包。TCP通过接收端返回的一系列确认信息来判断是否丢包。TCP有两套独立机制进行重传:超时重传和快速重传。

1.超时重传

​ 当发送端发送数据包后,会启动一个定时器,如果在一定时间阈值内没能收到接收端的ACK确认包,就会进行重传。

超时时间的确定

​ RTT(Round Trip Time):往返时延,也就是数据包从发出去到收到对应 ACK 的时间。RTT 是针对连接的,每一个连接都有各 自独立的 RTT。

​ RTO(Retransmission Time Out):重传超时,也就是超时时间。

SRTT <- (1 - α)·SRTT + α·RTT //求 SRTT 的加权平均

rttvar <- (1 - h)·rttvar + h·(|RTT - SRTT |) //计算 SRTT 与真实值的差距(称之为绝对误差|Err|),同样用到加权平均

RTO = SRTT + 4·rttvar //估算出来的新的 RTO,rttvar 的系数 4 是调参调出来的

​ 这个算法的整体思想就是结合平均值(就是基本方法)和平均偏差来进行估算,

2.快速重传

​ 当发现接收到失序数据包后,TCP会立刻生成确认信息,来表明出现了丢包现象。如接收端收到序号为101的段后,接收端期望收到102段,但收到了103段,此时接收端会立刻向发送端发送确认102段的ACK信息,直到102段被接受。重复ACk也可能在另一种情况下出现,即当网络中出现失序分组时,若接收端收到当前期盼序列号的后续分组时,当前期盼的段可能丢失,也可能仅为延迟到达。通常我们无法得知是哪种情况,因此TCP等待一定数目的重复ACK.,决定数据是否丢失并触发快速重传。当重复ACK累计到阈值的时候,发送端立即重传。

带选择确认的重传(SACK)

​ 发送端通过重传来填补丢失的数据段,但同时也要保证不重传已经正确接收到的数据,只需要重传接收端没收到的段。

​ 接收端向发送端指明SACK数据,指导发送端重传数据。

​ SACK选项结构:

sack

​ 其中Kind表示SACK选项被启用,Length是SACK信息总长度,L和R指明了接收端已经接受到的块,每个ACK段中的SACK信息最多包含三个块。

​ 第一个SACK块内包含的是最近接收到的报文段的序列号范围。由于SACK选项的空间有限,应尽可能确保向TCP发送端提供最新信息。其余的SACK块包含的内容也按照接收的先后依次排列。

sack1

3.为什么选择在传输层将数据分成多个段,而不是等到网络层再去分片?

​ 如果网络层分片,那么如果出现丢包现象,那么只能去重传整个数据包,在传输层分段的话,可以只重传丢失的部分,提高效率

a20d4103c3164557a8b4121c82857dcf

tcp连接的建立与断开

TCP连接的建立与断开

1.连接的建立—–3次握手

​ 1.发起连接的客户端会向服务器发送一个SYN报文段(在SYN段置位的tcp报文),并指定端口号和客户端序列号(INC)。

​ 2.服务器收到SYN报文段后,会向客户端回发一个SYN报文段,并指定服务器序列号(INS),ACK为INC+1

​ 3.客户端收到后,返回ACK = INS+1的报文段确认,至此三次握手完成

​ ![屏幕截图 2023-11-04 162752](D:\myboke\source\images\屏幕截图 2023-11-04 162752.png)

​ 三次握手的目的不仅在于让通信双方了解一个连接正在建立,还在于利用数据包的选项来承载特殊的信息,交换初始序列号

2.连接的断开—–4次挥手

​ 1.连接的主动关闭者发送一个FIN报文段来发起关闭,指明接收者希望看到的自已当前的序列号K,同时携带ACK来确认最近一次收到的数据。

​ 2.连接的被动关闭者收到FIN报文段后将K+1作为ACK值来确认收到FIN段。此时,上层的应用程序会被告知连接的另一端已经提出了关闭的请求。通常,这将导致应用程序发起自已的关闭操作。

​ 3.连接的被动关闭者变为主动关闭者,发送一个FIN报文,以L为序列号

​ 4..为了完成连接的关闭,关闭发起者最后发送一个报文段,包含一个ACK用于确认上一个FIN,如果出现FIN丢失的情况,那么发送方将重新传输,直到接收到一个ACK确认为止。

连接的半关闭

​ 首先发送的两个报文与正常关闭一致,接受到半关闭的一方扔能发送数据,直到数据发送完毕。它将会发送一个FIN来关闭本方的连接,同时向发起半关闭的应用程序发出一个文件尾指示。

tcpsicihuishou

3.TCP状态转移

TCPztzy

​ TIME_WAIT状态:TIME_WAIT状态也称为2MSL等待状态。在该状态中, TCP将会等待两倍于最大段生存期。

TIME_WAIT状态存在的意义

​ MSL:最大段生存期,它代表任何报文段在被丢弃前在网络中被允许存在的最长时间。

​ 1.确保连接被可靠的关闭:让TCP重新发送最终的ACK以避免出现丢失的情况。重新发送最终的ACK并不是因为TCP重传了ACK. (它们并不消耗序列号也不会被TCP重传),而是因为通信另一方重传了它的FIN (它消耗一个序列号)。事实上, TCP总是重传FIN, 直到它收到一个最终的ACK 。

​ 2.TIME_WAIT状态能够防止新的连接将前一个连接的延迟报文段解释成自身数据的状况。因为一个TCP报文的能存在的最长时 间是1个MSL,所以等待一个MSL就可以保证网络中不会再有本次连接的报文了

c++右值引用

右值引用

1.左值和右值

​ 左值:指一个指向特定内存的具有名称的值,它有一个相对稳定的地址并有一段较长的声明周期

​ 右值:不指向稳定内存地址的匿名值,它的生命周期很短。(如函数返回时构建的临时对象)

常见的左值和右值

​ 前置++:前置++的实现是将对象自增,然后返回这个对象本身,所以前置++的返回是左值。

​ 后置++:后置++的实现创建一个临时对象,在对传入对象自增,最后返回临时对象,所以后置++返回右值。

​ 字符串字面量(常量):字符串字面量是左值。因为编译器会将它存储到数据段中,程序加载的时候会为它开辟固定的内 存。

2.右值引用

​ 顾名思义,右值引用是一种引用右值切只能应用于右值的方法。它可以延长临时对象的声明周期,目的是减少对象的复制,避免不必要的拷贝,提升性能。它的主要意义是支持移动语义完美转发

​ 注:常量左值引用能引用右值。

​ 例:1.用右值引用接受函数的返回值,这样函数直接将return时产生的临时对象返回给右值引用,避免了一次拷贝构造函数的执 行。

​ 2.在使用一个临时对象创建一个对象时,我们可以使用移动构造函数。这时,资源将从一个临时对象(右值)“移动”到新对 象,而不是创建新资源的拷贝。减少了不必要的拷贝操作。

RVO介绍

​ RVO是一种编译器优化技术,用于消除不必要的临时对象拷贝,从而提高性能。它主要针对函数返回局部对象的情况,通过 优化,可以避免创建临时对象并执行拷贝构造函数。

​ RVO的思想:在函数调用栈上直接构造返回值,而不是先创建一个局部对象,再拷贝到调用者的栈空间。

3.移动语义

​ 移动语义帮助我们将临时对象的内存转移到另一个对象中,来避免内存数据的复制。原理是在类中创建一个移动构造函数,它以右值引用作为形参,函数中不进行资源的拷贝,而是进行资源所有权的转让。对于右值,编译器会优先使用移动构造函数去构造目标对象。

4.将亡值和std::move()

​ 将亡值表示资源可以被重用的对象和位域,通常这是因为它们接近生命周期的末尾,也坑你是经过右值引用的转换产生的。

​ 将左值转换成右值引用代码

1
static_cast<classname&&>(xxxx)

​ 这个操作的意义是让左值使用移动语义,std::move()实质上与上述代码等价。

c++基础之编译过程

从C++文件到可执行文件经过了哪几个过程

​ 预编译、编译、汇编、链接

1.预编译

​ 处理#开头的指令,比如拷贝#include包含的文件代码,#define宏定义的替换,条件编译等

   就是为编译做的预备工作的阶段

   主要处理#开始的预编译指令

   预编译指令指示了在程序正式编译前就由编译器进行的操作,可以放在程序中的任何位置。

2.编译

​ 编译器对预编译后的c++文件进行词法分析、语法分析、语义分析和优化,翻译成文本文件xxx.s,包含了一系列汇编语言

3.汇编

​ 汇编器将xxx.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件xxx.o中,xxx.o是一个二进制文件。

4.链接

​ 链接(linking)是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(run time),也就是由应用程序来执行。链接是由叫链接器(linker)的程序自动执行的。

链接的过程做了什么?

​ 1.符号解析:目标文件和库中包含很多符号(函数、变量名),它们在编译过程中被分配到了唯一的标识符。链接器需要解析这些符号,找到它们在其他文件中的定义。

​ 2.地址分配:链接器为每个目标文件和库分配内存地址空间,并确定它们在最终可执行文件中的布局。连接器还需要处理重定位,即根据实际内存地址更新代码中的地址引用。

​ 3.库依赖管理:连接器处理程序对库的依赖关系,确保所有需要的库都被包含在可执行文件中。链接器还需要处理库之间的依赖关系,确保它们按正确顺序链接。

链接存在的意义

​ 链接使得分离编译成为可能,我们不用将一个大型的应用程序组织成一个巨大的源文件,而是可以把它分解成为更小、更好管理的模块,可以独立的修改和编译这些模块。当我们改变这些模块中的一个时,只需要简单的重新编译它,并重新链接应用,而不必重新编译其他文件。