C/C++2019秋招面试题集合06

C/C++2019秋招面试题集合06

9.5【BIGO】正式批 - 客户端开发一面

1 项目
答:说一下你做过的项目。

2 单例模式线程安全手撕代码
答:所谓单例模式,就是一个实体对象。那么如何只能创建一个对象呢?
/*
实现单例步骤:
1 构造私有
2 设有一个私有静态本类指针
3 提供静态函数接口
*/
我们先给出单例模式的两个典型例子:懒汉模式和饿汉模式。

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;

//单例模型实例 懒汉模式 --调用时才初始化对象
class Singleton_lazy{
private:
	Singleton_lazy(){}
public:
	static Singleton_lazy*  get_Singleton_lazy(){
		if(lazy==NULL){
			lazy=new Singleton_lazy;   //只有在NULL时才能初始化对象 以后就无法再创建
		}

		return lazy;
	}
private:
	static Singleton_lazy* lazy;
};
//类外初始化静态变量
Singleton_lazy* Singleton_lazy::lazy=NULL;


//单例模型实例 饿汉模式 --初始化比main函数还快 所以叫饿汉
class Singleton_hungry{
private:
	Singleton_hungry(){
		//cout<<"我是饿汉"<<endl;
	}
public:
	static Singleton_hungry*  get_Singleton_hungry(){
		return hun;
	}

private:
	static Singleton_hungry*  hun;
};
//类外初始化静态变量
Singleton_hungry* Singleton_hungry::hun=new Singleton_hungry;

int main(){

	//懒汉模式
	Singleton_lazy *a=Singleton_lazy::get_Singleton_lazy();
	Singleton_lazy *b=Singleton_lazy::get_Singleton_lazy();

	if(a==b){
		cout<<"是单例模式"<<endl;
	}else{
		cout<<"不是单例模式"<<endl;
	}

	//饿汉模式
	Singleton_hungry *c=Singleton_hungry::get_Singleton_hungry();
	Singleton_hungry *d=Singleton_hungry::get_Singleton_hungry();
	if(c==d){
		cout<<"是单例模式"<<endl;
	}else{
		cout<<"不是单例模式"<<endl;
	}

	return 0;

	/*
	单例内存释放问题: 因为单例只有一个对象 即使在不断调用它也不会不断开辟内存 不会导致程序崩溃
	    程序结束后系统自动回收 我们平时不回收对象内存是因为会不断开辟内存 容易造成内存泄漏
	    并不是说程序结束不会回收 以前不回收的风险好大
	    但是单列模式一般不考虑内存回收问题 让他程序结束自动回收
		实在想自己回收可以内嵌一个类 将该类对象作为单例类的数据 也是程序结束回收 但没必要
	    栈是函数调用完毕就回收
	*/
	
}

下面我们来判断一下,当我们的懒汉模式遇到多线程时,两个线程同时执行if(lazy==NULL),若刚好都满足条件,那么就不符合单个对象了;而饿汉模式在多线程下,因为对象在mian函数之前就已经类外初始化完毕了,所以不管你怎么调用静态函数,它返回的都是同一个对象。 所以可以得出:
/*
1 懒汉模式碰到多线程是不安全的 因为可能同时调用时会new两个对象
2 饿汉模式碰到多线程是安全的 因为在调用时就已经确定好返回哪个对象
*/

3 观察者模式的应用场景和工作方式。
答:观察者模式:建立于对象与对象之间的关系,一个对象的变化引起其他对象的改变。前者即一个对象叫做观察目标,后者即多个对象为观察者。例如多个英雄撸一个BOSS;多辆车在等红绿灯。BOSS和红绿灯的改变都会影响英雄和车。
工作方式:一般观察者都会有一个更新接口updata,等待观察目标改变的时候,因为观察目标也不止一个,所以观察目标又可以有一个共同通知接口notify,如何通知呢,通知谁呢?所以也要用容器记录正在观察目标的观察者。 给出两个例子:
/* 步骤:
1 抽象观察者类–内有共同更新接口
2 抽象目标类
3 记忆联系成员
4 动态增加删除管理关系
*/
例子1:

/*
观察者模式:建立于对象与对象之间的关系 一个对象的变化引起其他对象的改变 前者即一个对象叫做观察目标
            后者即多个对象为观察者

以下面的例子为例:

思想步骤:  1 将英雄抽象出来 因为目标改变即boss死亡会引起各个对象变化 所以需要 一个共同的更新接口
            2 因为目标不只有一个 所以也将目标类抽象出来
			3 继承后 由于观察目标需要知道有谁在观察它即建立联系 所以需要成员容器记忆 
			4 并且需要动态的增加 删除 管理这种联系 所以抽象类需要有三种这些方法

简单来说:  1 抽象观察者类--内有共同更新接口
            2 抽象目标类
			3 记忆联系成员
			4 动态增加删除管理关系

脑海的实物图:
            1 多个英雄
			2 多个目标
			3 双方的联系
*/

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
#include<list>
using namespace std;

class AbstractHero{
public:
	virtual void Updata()=0;
};

//具体英雄类 观察者
class HeroA:public AbstractHero{
public:
	HeroA(){
		cout<<"A在撸Boss"<<endl;
	}
	virtual void Updata(){
		cout<<"A英雄停止"<<endl;
	}
};

class HeroB:public AbstractHero{
public:
	HeroB(){
		cout<<"B在撸Boss"<<endl;
	}
	virtual void Updata(){
		cout<<"B英雄停止"<<endl;
	}
};

class HeroC:public AbstractHero{
public:
	HeroC(){
		cout<<"C在撸Boss"<<endl;
	}
	virtual void Updata(){
		cout<<"C英雄停止"<<endl;
	}
};

class HeroD:public AbstractHero{
public:
	HeroD(){
		cout<<"D在撸Boss"<<endl;
	}
	virtual void Updata(){
		cout<<"D英雄停止"<<endl;
	}
};

class HeroE:public AbstractHero{
public:
	HeroE(){
		cout<<"E在撸Boss"<<endl;
	}
	virtual void Updata(){
		cout<<"E英雄停止"<<endl;
	}
};

//因为不止因为观察目标
class AbstractBoss{
public:
	//添加观察者  记得写参数
	virtual void addHero(AbstractHero* hero) = 0;
	//删除观察者
	virtual void deleteHero(AbstractHero* hero) = 0;
	//通知所有观察者
	virtual void notify() = 0; 
};

//其中一个具体观察目标
class BossA:public AbstractBoss{
public:

	//添加观察者  记得写参数
	virtual void addHero(AbstractHero* hero){
		pHeroList.push_back(hero);
	}
	//删除观察者
	virtual void deleteHero(AbstractHero* hero){
		pHeroList.remove(hero);
	}
	//通知所有观察者 --实际上就是调用Updata函数通知
	virtual void notify(){
		for(list<AbstractHero*>::iterator it=pHeroList.begin();it!=pHeroList.end();it++){
			(*it)->Updata();
		}
	}
public:
	//建立联系的容器
	list<AbstractHero*> pHeroList;
};

void test01(){

	//创建五个英雄 观察者
	AbstractHero* heroA=new HeroA;
	AbstractHero* heroB=new HeroB;
	AbstractHero* heroC=new HeroC;
	AbstractHero* heroD=new HeroD;
	AbstractHero* heroE=new HeroE;

	//创建观察目标
	AbstractBoss* bossA=new BossA;

	//添加英雄在观察者名单
	bossA->addHero(heroA);
	bossA->addHero(heroB);
	bossA->addHero(heroC);
	bossA->addHero(heroD);
	bossA->addHero(heroE);

	//从观察者名单删除C
	cout << "heroC阵亡..." << endl;
	bossA->deleteHero(heroC);

	//通知各个英雄
	cout << "Boss死了...通知其他英雄停止攻击,抢装备..." << endl;
	bossA->notify();

	//释放内存
	delete bossA;
	delete heroA;
	delete heroB;
	delete heroC;
	delete heroD;
	delete heroE;

}

int main(){

	test01();

	return 0;
}

例子2:与例子区别是,更新时需要一个信息类来更新。

#define _CRT_SECURE_NO_WARNINGS
#include<list>
#include<iostream>
#include<string>
using namespace std;
class AbstructTeam; //声明

//信息类
class Information{
public:
	Information(){
		m_gang1=NULL;
		m_gang2=NULL;
	}
	//帮派事件 打人帮派和被打帮派
	void setGangThings(AbstructTeam* beat,AbstructTeam* beaten){
		m_gang1=beat;
		m_gang2=beaten;
	}
public:
	//帮派1 2
	AbstructTeam* m_gang1;
	AbstructTeam* m_gang2;
};


//抽象门派观察者
class AbstructTeam{
public:
	virtual void Updata(Information* info)=0;
	virtual string getGangName()=0;  //因为抽象类指针访问不到成员名字 所以才加的
};

//具体帮派A ->少林
class TeamA:public AbstructTeam{
public:
	TeamA(){
		m_gangname="少林";
	}
	virtual void Updata(Information* info){
		if(this!=info->m_gang1 && this==info->m_gang2){
			cout<<"我们"<<this->getGangName()<<"被"<<info->m_gang1->getGangName()<<"打了,我们要报仇,易筋经干他"<<endl;
		}else if(this==info->m_gang1 && this!=info->m_gang2){
			cout<<this->getGangName()<<"要干死"<<info->m_gang2->getGangName()<<"这些垃圾门派,冲啊"<<endl;
		}else{
			cout<<"打啊,我们"<<this->getGangName()<<"不参与斗争,静观其变"<<endl;
		}
	}
	//返回门派名字
	virtual string getGangName(){
		return m_gangname;
	}
private:
	string m_gangname; //最后需要一个门派名来比较谁被谁打了 也可不写 我加了然后需要构造给他初始化
};

//具体帮派B --明教
class TeamB:public AbstructTeam{
public:
	TeamB(){
		m_gangname="明教";
	}
	virtual void Updata(Information* info){
		if(this!=info->m_gang1 && this==info->m_gang2){
			cout<<"我们"<<this->getGangName()<<"被"<<info->m_gang1->getGangName()<<"打了,我们要报仇,乾坤大挪移干他"<<endl;
		}else if(this==info->m_gang1 && this!=info->m_gang2){
			cout<<this->getGangName()<<"要干死"<<info->m_gang2->getGangName()<<"这些垃圾门派,冲啊"<<endl;
		}else{
			cout<<"打啊,我们"<<this->getGangName()<<"不参与斗争,静观其变"<<endl;
		}
	}
	//返回门派名字 把这个函数写进了Updata中 我曹 找了半天错误
	virtual string getGangName(){
		return m_gangname;
	}
private:
	string m_gangname;
};

//具体帮派C --武当
class TeamC:public AbstructTeam{
public:
	TeamC(){
		m_gangname="武当";
	}
	virtual void Updata(Information* info){
		if(this!=info->m_gang1 && this==info->m_gang2){
			cout<<"我们"<<this->getGangName()<<"被"<<info->m_gang1->getGangName()<<"打了,我们要报仇,太极拳干他"<<endl;
		}else if(this==info->m_gang1 && this!=info->m_gang2){
			cout<<this->getGangName()<<"要干死"<<info->m_gang2->getGangName()<<"这些垃圾门派,冲啊"<<endl;
		}else{
			cout<<"打啊,我们"<<this->getGangName()<<"不参与斗争,静观其变"<<endl;
		}
	}
	//返回门派名字
	virtual string getGangName(){
		return m_gangname;
	}
private:
	string m_gangname;
};

//抽象类观察目标  不止一个
class AbstructObject{
public:
	virtual void setInformation(Information* info)=0;
	virtual void addTeam(AbstructTeam* team)=0;
	virtual void subTeam(AbstructTeam* team)=0;
	virtual void notify()=0;
};

//具体类百晓生 知道各个门派 和 信息
class BaiXiaoShengObject:public AbstructObject{
public:
	//设置江湖信息
	virtual void setInformation(Information* info){
		m_info=info;
	}

	//增加帮派 建立观察者和目标的关系
	virtual void addTeam(AbstructTeam* team){
		if(team==NULL){
			return;
		}
		m_list.push_back(team);
	}
	//删除帮派
	virtual void subTeam(AbstructTeam* team){
		if(team==NULL){
			return;
		}
		m_list.remove(team);
	}
	//通知帮派 这里的处理类似于命令模式的服务端
	virtual void notify(){
		if(m_list.size()==0){
			return;
		}
		for (list<AbstructTeam*>::iterator it = m_list.begin(); it != m_list.end();it ++){
     //因为通知需要消息 所以更新接口需要参数信息   将信息通知各大门派
			(*it)->Updata(m_info);   
		}
	}
public:
	list<AbstructTeam*> m_list; //容器名不能与类型一样 犯了很多次了
	Information* m_info;
};



void test01(){

	//创建帮派和百晓生
	AbstructTeam* teama=new TeamA;
	AbstructTeam* teamb=new TeamB;
	AbstructTeam* teamc=new TeamC;
	AbstructObject* obj=new BaiXiaoShengObject;

	//将观察者观察目标
	obj->addTeam(teama);
	obj->addTeam(teamb);
	obj->addTeam(teamc);

	//设置江湖信息 哪两个门派打架
	Information* info=new Information;
	info->m_gang1=teamc;
	info->m_gang2=teama;
	obj->setInformation(info);   //最后要调用set我将它也定义为虚函数


	//百晓生通知各大帮派	
	obj->notify();


	delete obj;
	delete teama;
	delete teamb;
	delete teamc;
	delete info;

}

int main(){

	test01();

	return 0;
}

4 知道责任链设计模式吗?
答:责任链模式:就是客户端有一个请求,该请求在由多个对象的组成的链上传输,直到有一个对象处理请求。 责任链模式是一种对象的行为模式。所谓链,是因为在多个对象中,每一个对象对其下家的引用而连接形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织链和分配责任。
例如,有一光头程序员,辛辛苦苦了工作了一年,终于可以加薪了,向主管提交了加薪申请,主管一看不得了,自己职权不够,批不了,主管把申请上交总监,总监发现自己也批不了,申请到了总经理手中,总经理一看,小伙子口气不小了,有胆识敢申请,先来谈下心。
看代码:
/*
大概步骤:
1 请求类
2 处理对象类 (一个连接对象链的函数SetSuccessor() 和 处理请求的虚函数)
*/

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
using namespace std;

/* 
  责任链模式:当一个请求发送给某个角色处理时 该角色会判断是否能处理 能就处理掉 否则传给责任链的下一级
  例如光头程序员请求经理 总监 总经理加薪
     大概步骤:1 请求类
	           2 处理对象类 (一个连接对象链的函数SetSuccessor() 和处理请求的虚函数)

注:与命令有些类似 但命令是将多个请求封装成类 一个命令对应一个请求			   

*/
 
// 一个请求
class Request{
public:
	int m_nNumber;
};
 
// 管理者
class AbstructManager{
public:
	AbstructManager(string name){ 
		m_name = name; 
	}
	//用来设置确定下一级的处理对象
	void SetSuccessor(AbstructManager* nextObj){
		m_manager = nextObj; 
	}
	//处理请求 不够级别就传给下一级
	virtual void HandleRequest(Request* request) = 0;

protected:
	AbstructManager* m_manager;
	string m_name;
};
 
// 经理
class CommonManager : public AbstructManager{
public:
	//给管理者构造名字
	CommonManager(string name) : AbstructManager(name){}
	//总监能处理的就处理 否则传给下一级处理  可以看出链表
	virtual void HandleRequest(Request* request){
			if (request->m_nNumber>=0 && request->m_nNumber<1000){
				cout << m_name << " 处理了请求: " << request->m_nNumber << endl;
			}else{
				m_manager->HandleRequest(request);
			}
	}
};
 
 
// 总监
class Majordomo : public AbstructManager{
public:
	//给管理者构造名字
	Majordomo(string name) : AbstructManager(name){}
	//总监能处理的就处理 否则传给下一级处理  
	virtual void HandleRequest(Request* request){
			if (request->m_nNumber <= 5000){     //实际上隐含了>=1000 因为张经理只有>=1000才会传给总监
				cout << m_name << " 处理了请求: " << request->m_nNumber << endl;
			}
			else{                                //传给下一级
				m_manager->HandleRequest(request);
			}
	}
};
 
 
//总经理  
class GeneralManager: public AbstructManager{  
public:  
	//给管理者构造名字
	GeneralManager(string name):AbstructManager(name){}  
	//总经理可以处理所有请求   不必再else分给其他处理
	virtual void HandleRequest(Request* request){ 		
		cout << m_name << " 处理了请求: " << request->m_nNumber << endl;		
	}  
};
 
int main(){
    
	//先创建责任链的对象
	AbstructManager* common = new CommonManager("张经理");
	AbstructManager* major = new Majordomo("李总监");
	GeneralManager* general  = new GeneralManager("赵总");
	//将对象连接成链
	common->SetSuccessor(major);
	major->SetSuccessor(general);

	//客户端请求
	Request* rq = new Request();
 
	//张经理能处理的就处理 不能处理就给下一级总监
	rq->m_nNumber = 999;
	common->HandleRequest(rq);
	
	//张经理能处理的就处理 不能处理就给下一级总监
	rq->m_nNumber = 4999;
	common->HandleRequest(rq);
 
	//张经理能处理的就处理 不能处理就给下一级总监 最多到总经理
	rq->m_nNumber = 6999;
	common->HandleRequest(rq);
 

	delete rq;
	delete major;
	delete common;
	delete general;

	return 0;
}

理解图:
在这里插入图片描述
5 流量控制与拥塞控制?
答:1 流量控制:发送方发送数据不要过快,使得接收方能够来得及,接收所有的数据。流量控制就是利用滑动窗口协议实现的。面试时根据该协议应机回答便可。 下面看滑动窗口协议是如何实现的:
在这里插入图片描述
根据上图可以看出,我们发送方每次发送数据都要按照Ser接收端win大小来发送,当然接收方发送数据也要看发送方的win大小,但是这方面一般很少发生。 在服务端中,虚线部分窗口的大小,窗口是利用整体向右移的,右移两格说明他接收2k,又空出2k,缓冲区不断的整体移动,所以我们叫他为滑动窗口协议。

流量控制时的死锁问题:
在这里插入图片描述
如图,当B接收端确认接收并且回发窗口rwnd(即win)=0,导致发送端win也为0;然后接收端缓存有空间了,又发送一个rwnd=300窗口的包,但是该包丢失了,那么就导致A一直等该报来让发送端可以发送消息,接收端又等发送方发送消息,这样就导致死锁。
解决:
在这里插入图片描述
当B发送窗口为0的包时,该TCP连接会启动一个持续计时器,处于死锁导致计时器超时时,A会发送一个零窗口探测报文,B接收后会返回一个带有窗口确认该探测报文的包,若假设该包win仍为0;那么又会启动一个持续计时器,并且超时也会发送探测报文,B接收后又返回一个带有窗口的包,假设为win=300;那么此时死锁就被打破了。
即B发0->启动计时器->超时发探测报文。 上面就进行了两次这个步骤。
此时你可能会有两个疑惑:
1接收端窗口不是为0吗?怎么能接收零窗口探测报文?
2如果零窗口探测报文也丢失了呢?
答:实际上,当接收端窗口为0时,我们系统也会要求B接收零窗口探测报文,所以窗口为0不会影响B的接收。而零窗口报文发送时,连接也会为它启动持续计时器,只要超时,就会立即重发零窗口报文,所以也不怕该报文的丢失。

顺便给出一道例题:
在这里插入图片描述
我们分析这道题,开始发送方甲win=4000字节,发送2000字节后,收到接收方发送的第一个段的ACK,也就是说确认了0–999这一段。此时发送方窗口后移一段,变成1000-4999;但是接收端发送过来的窗口win=2000;所以发送端窗口随之改变为1000-2999。 所以发送端剩下一个待确认已发送的1000-1999的段和2000-2999未发送的段。 即发送端最大还能发送2000-2999这1000个字节的段。
总结一下这道题:只有确认了的段才会被发送端踢出窗口,已发送但未确认的仍会在窗口内,他会占一个窗口大小。
即先确认->再移动根据接收端的win修改发送端的win。

2 拥塞控制:
在某段时间,当网络中某一资源的需求超过了该资源所能提供的可用部分,网络性能就要变坏,这种情况就叫做网络拥塞。就好比你满足不了你的女朋友!!! 所以当我们对这种网络拥塞进行控制时,就叫拥塞控制。
网络资源:计算机网络中数位链路容量(即带宽)、交换结点中的缓存和处理机等等。

下面我们看看理想的拥塞控制、实际的拥塞控制、不加控制的拥塞时,网络吞吐量随输入负载的改变。
在这里插入图片描述
1)理想的控制会使输入负载即使不断增大,吞吐量都维持在水平线上;
2)实际的拥塞控制是一条不断接近水平线的曲线。
3)不加拥塞控制会使吞吐量达到顶点时,由于出现拥塞最终导致网
络崩溃而吞吐量为0,即死锁状态。

TCP的四种拥塞控制算法:
1.慢开始。
2.拥塞控制。
3.快重传。
4.快恢复。

在介绍四种控制方法前,我们先假设一些条件:
1.数据是单方向传送,而另一个方向只传送确认。
2.接收方总是有足够大的缓存空间,因而发送发发送窗口的大小由网络的拥塞程度来决定。
3.以TCP报文段的个数为讨论问题的单位,而不是以字节为单位。

并且先给出拥塞窗口概念及其维护原则、网络拥塞的依据。
拥塞窗口cwnd:发送方维护一个叫拥塞窗口的状态变量,其大小随网络资源的拥塞程度而动态的改变。

维护原则:TCP连接有一个叫慢开始门限ssthresh的变量,它将慢开始算法和拥塞避免算法隔离开。拥塞窗口一开始为cwnd=1
当cwnd<ssthresh时,采用慢开始算法。
当cwnd>ssthresh时,采用拥塞避免算法。
当cwnd=ssthresh时,两个算法都可以使用。

网络拥塞的依据:发送端发送报文后,没有按时的接收到接收方的确认报文。即发生了超时重传。

重点来了!!!

下面为了方便理解,将慢方法和拥塞避免合在一起讲。
慢开始:是指拥塞窗口一开始小,向网络资源发送较小的数据报,而不是指cwnd增长速度慢。按指数增长。
拥塞避免:并非能完全避免拥塞,是指在拥塞避免状态下由指数增长变成线性增长,使网络不那么容易出现拥塞。按线性增长。

以图理解:
在这里插入图片描述
注:传输轮次(RTT):代表传输的次数。例如发送一个包到接收方确认一个包即为1次。就是往返算1个轮次。

如图逐步分析步骤:
1 由于cwnd一开始为1,然后我们先假设慢开始门限ssthresh=16。
一开始处于慢开始算法,按照2^n来扩大拥塞窗口,当传输轮次为4时,达到慢开始门限值为16;此时改为拥塞避免算法,按照线性的增加窗口大小,即每次加1,当达到24时,发送端没有接收到确认报文,出现超时重传机制;此时认为它出现网络拥塞,TCP连接会做两件事:
1)使慢开始门限ssthresh变为出现拥塞时窗口的一半,此例即24的一半ssthresh=12。
2)使拥塞窗口重置为cwnd=1。
此后按以上步骤继续执行。以上就是慢开始和拥塞避免算法。

以上的两种方法认为当在拥塞避免状态时,发送端没接收到确认报文,等待至发送超时重传后,就认为它是网络拥塞。那么他有可能只是该确认报文丢失呢,而不是出现网络拥塞呢?按照网络拥塞将cwnd=1就会影响到网络的效率,那么如何避免呢?我们只需要将丢失的确认报文在超时重传之前,使接收端立即连续的发送3次确认报文,让发送方快速重发丢失的报文即可避免超时重传。这样就不会重置cwnd=1降低效率了。 理解不了与上面的区别的话,可以用当发生丢包时,接收端可以多次发送确认报文;而网络拥塞因为资源不足,不能回发确认报文。

重点又来了!!!

所以我们引入了快重传和快恢复。
快重传:当接收方接收的是不按顺序的报文,接收方立即连续3次发送相应的确认报文,使发送方尽快重传丢失的报文。重传是针对发送方而言。
看图:
在这里插入图片描述
如图分析步骤:当发送方发送1号数据报文段,接收方收到1号报文段后给发送方发回对1号报文段的确认,在1号报文段到达发送方之前,发送方又可以将发送窗口内的2号数据报文段发送出去,接收方收到2号报文段后给发送方发回对2号报文段的确认,在2号报文段到达发送方之前,发送方还可以将发送窗口内的3号数据报文段发送出去,
假设该报文丢失,发送方便不会发送针对该报文的确认报文给发送方,发送方还可以将发送窗口内的4号数据报文段发送出去,接收方收到后,发现这不是按序到达的报文段,因此给发送方发送针对2号报文段的重复确认,表明我现在希望收到的是3号报文段,但是我没有收到3号报文段,而收到了未按序到达的报文段,发送方还可以将发送窗口中的5号报文段发送出去,接收方收到后,发现这不是按序到达的报文段,因此给发送方发送针对2号报文段的重复确认,表明我现在希望收到的是3号报文段,但是我没有收到3号报文段,而收到了未按序到达的报文段,,发送方还可以将6号数据报文段发送出去,接收方收到后,发现这不是按序到达的报文段,因此给发送方发送针对2号报文段的重复确认,表明我现在希望收到的是3号报文段,但是我没有收到3号报文段,而收到了未按序到达的报文段。
此时,发送方收到了累计3个连续的针对2号报文段的重复确认,使得发送方在超时重传前,快重传3号报文段,接收方收到后,给发送方发回针对6号报文的确认。至此表明,序号到6为至的报文都收到了,这样就不会造成发送方对3号报文的超时重传,而是提早收到了重传。
总结来说:快重传就是当发送端丢失报文后,再发送报文结果接收端接收到的顺序不一样,就会立即连续三次回发确认报文,让你快重传丢失的报文
以上就是快重传方法。

重点又来了!!!

快恢复:当丢失个别报文,执行快重传,TCP连接知道不是网络拥塞,所以不重新执行慢开始算法;而是执行快恢复算法(看下面解释)。 执行慢开始和快恢复的区别:前者是超时重传(即网络拥塞),后者是个别报文丢失(即快重传)。
快恢复算法:发送方将慢开始门限ssthresh和拥塞窗口cwnd调整为当前窗口值的一半,此后开始执行拥塞避免算法。
也有的快恢复算法将拥塞窗口增大点,置为cwnd=ssthresh+3。原因在于发送方既然收到了三个重复的确认报文,就代表它已经发送了三个报文到接收端的缓存,因为不在接收端缓存的里话,接收端又怎么会知道接收的报文是无序的,要求它快重传呢。所以既然发送端此后发送了三个报文,并且在接收端缓存,那么就可以将拥塞窗口增大3个。
在这里插入图片描述

最重要的来了!!!

最后我们以一个图来演示这四种拥塞控制方法:
在这里插入图片描述
根据图来一步步分析:
1 cwnd=1,ssthresh设为16,开始执行慢开始算法,传输轮次RTT=4时,到达门限值16;开始执行拥塞避免算法,以线性递增,门限值为24时,出现超时重传,认为网络拥塞,使门限值为当前拥塞窗口值24的一半即ssthresh=12,cwnd=1。
2 然后重新以改变后的门限值和窗口值执行慢开始算法,到达门限值12时,改用拥塞避免算法;递增到16出现丢包快重传,执行快恢复算法后,ssthresh和拥塞窗口值各为当前窗口值的一半,即ssthresh=cwnd=8;因为拥塞窗口=门限值,所以此后执行拥塞避免算法。

最后在给大家给出一道例题理理思路。
在这里插入图片描述
答案:C
分析:当16KB出现超时重传,说明网络拥塞,则ssthresh=16*1/2=8,重新开始执行慢开始算法,cwnd=1。由于0次往返就已经是1了,所以第一轮往返发送端接收到确认报文后cwnd=2,第二轮cwnd=4,第三轮cwnd=8,第四轮后开始执行拥塞避免算法,所以cwnd=9。 我们一定要注意,他说四个轮次都得到了确认,即第四个确认报文回发给发送端前,发送端拥塞窗口就已经是8了,收到第四个确认报文后,发送端按照拥塞避免算法加1。

最后总结流量控制和拥塞控制的区别:
1 前者是基于滑动窗口协议,根据接收端的缓存空闲数来动态的改变发送端的窗口大小,是一种流量控制。
2 后者是基于四种控制算法,不考虑接收端的缓存,根据网络的拥塞程度来动态的改变发送端的的窗口大小,是一种拥塞控制。

6 tcp,udp 应用的场景?
答:tcp协议是基于安全可靠的,所以一般应用于广域网的信息传播,被加密的信息传播,文件传输等等。而udp是无连接的,实时性好,一般多数在局域网运用,例如视频会议直播。

7 讲一下红黑树?
答:

8 哪些排序算法是稳定的?
答:稳不稳定看相等的数在排序后是否交换了位置。
例如 3、8、1、3、9 若排序后第一个三排在第二个3后面则是不稳定的。
冒泡:相邻的数两两交换,不会改变。 例如从小到大排序,先确定最后一个数,3和8不换,1和8换,8和3换,8和9不换,第一轮排序后,3、1、3、8、9,接下来的轮次以此类推,前面的3和后面的3并不会改变顺序。所以冒泡是稳定的。

选择排序:选择排序是每轮找最小或最大的值。例如从小到大排序,
改变一下数组的数3 、8 、3 、1 、9。当执行第一轮后,将最小的值1与第一个数3交换后,变成1 8 3 3 9,这样就导致相等的数顺序改变了,所以选择排序是不稳定的。

插入:插入是将无序的部分插入到有序的部分,开始第一个有效部分只有一个元素。例如2 8 2 3 9,2为第一部分有序部分,8 2 3 9为无序,若第二部分的第一个数比第一部分的某个数小,则插入相应位置,否则进行下一轮比较。所以8没有比2小的,所以第一轮比较后,数组不变。第二轮开始,2 8 为有序部分,2 3 9为无序,将2插入到有序部分。步骤:2比8小,8后移一位,2再与2比较,2不比2小,将2插入,第二轮结束,结果为2 2 8 3 9。可以看出,相等的2并没有改变顺序,所以插入排序是稳定的。每次将无序部分的一个数插入到有序的指定位置,并不会改变相等数的顺序。

希尔排序:和插入一样,在无序的情况下速率非常高。希尔排序就是利用增量increasement=len/3+1来分组的,然后将分好的组进行多次插入排序。例如3 3 8 0 6 5,increasement=6/3+1=3;所以a[0]=3、a[3]=0一组。a[1]=3,a[4]=6一组,a[2]=8,a[5]=5一组。第一组排序后0 3 8 3 6 5,第二组排序后0 3 8 3 6 5 ,第三组排序后0 3 5 3 6 8。此后increasement=3/3+1=2;一直到increasement=1时退出。可以看出不需要看此后的处理,increasement=6时就出现了第一个3去到了后面的情况,所以希尔是不稳定的。

快速排序:快排比较重要,用得最多。这个更容易看出了,挖坑算法嘛,先把第一个坑挖掉并保存,从右开始找小于这个坑的数填进去;然后填进去的又相当于被挖坑了,又从左边填好坑的后一个数开始找大于坑的数填坑,最后将保存的数填进坑,在遍历两边继续挖坑填坑。例如1 3 2 8 3 7,1被挖掉保存,用后面的3填坑,结果为3 3 2 8 3 7,所以我们看到,他排序后是不稳定的。

归并排序:例如4 7 3 4 2。我们将4该数组不断按mid=len/2递归分成两部分。首先 4 7 3为左,4 2为右;再将左递归mid=3/2分成4 7和3。4 7在分后会导致递归条件不满足start<=end,函数返回并调用有序算法排序为4 7。4 7返回后与3再调用有序算法结果为3 4 7,。右边的4 2同理变成2 4。最后3 4 7与2 4调用排序算法,当我们调用有序算法判断,将4<=4等号加上去,那么相等的数就不会乱序,所以归并也是稳定的。

堆排序:不稳定。

不理解的记住总结就可以了:冒泡、插入、归并是稳定的。选择、希尔、快排、堆排序不是稳定的。
要学会自己例子,会画图分析,这种思想很重要。

9 c++ 有哪些创建线程的方法?
答:

10 用过 oracle 和 mysql 吗?
答:在做QT项目时用过mysql。

11 join 有哪几种方式?
答:

	按功能分类:
		内连接:
			等值连接
			非等值连接
			自连接
		外连接:
			左外连接
			右外连接
			全外连接
		
		交叉连接

12 讲一下左连接和右连接?
答:

13 了解过移动开发的架构实现吗?
答:

14 除了 c++ 还用过什么,java 用过吗?
答:

15 有什么想问我的?
答:最好问,让别人知道你了解过他们。 例如问有没有五险一金,怎么休假,平时上班时间是如何分配的,同事好相处吗…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值