c++后端相关(加深记忆,抄的github和小林coding)

文章目录

C++

面向对象和面向过程语言的区别

面向过程是分析出解决问题所需的步骤,然后用函数把这些步骤一步步实现,使用时候一个个依次调用即可;
面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为完成一个步骤,而是为描叙某个事物在解决问题的步骤中的行为;

宏定义#define和const常量区别

宏定义相当于字符替换,而const是常量声明;
宏定义是预处理器处理,而const是编译器处理;
宏定义五类型安全检查,而const有类型安全检查;
宏定义不分配内存,const要分配内存;
宏定义存在代码段,const存在数据段;

const

const修饰变量:限定变量为不可修改;
const修饰指针:指针常量和指向常量的指针;
const修饰成员函数,主要目的是防止成员函数修改成员变量的值,即该成员函数并不能修改成员变量;

static

  1. static修饰普通变量(修改变量的存储区域和生命周期,变量存在静态区)
  2. 修饰普通函数,表明函数作用范围,在定义该函数的文件内才能使用;
  3. 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员;
  4. 修饰成员函数,修饰成员函数使得不需要生成对象就可访问该函数,但static函数内不能访问非静态成员;

static和const可同时修饰成员函数吗?

不可以。C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系。因此不能同时用它们。

inline函数

  1. 虚函数可以是内联函数,内联(编译器内联)是可以修饰虚函数的,但是当虚函数表现多态性(运行期)的时候不能内联。
  2. inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。

volatile

  1. volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化;
  2. 每次访问时都必须从内存中取出值;
  3. 指针可以是 volatile;

sizeof()

sizeof 对数组,得到整个数组所占空间大小;
sizeof 对指针,得到指针本身所占空间大小;

位域

类可以将其(非静态)数据成员定义为位域(bit-field),在一个位域中含有一定数量的二进制位。当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。

C++ 中 struct 和 class

总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
本质区别:默认访问控制(struct是共有的public class是私有的private)

深浅拷贝

数据成员中无指针时,可用浅拷贝。有指针时,若没有自定义拷贝构造函数,会调用默认拷贝构造函数,这样就会调用两次析构函数,第一次析构delete了内存,第二次就指针悬挂了,所以得采用深拷贝。

两者主要区别在于深拷贝在堆内存中另外申请空间来存储数据,也就解决了指针悬挂的问题。

union共同体与struct结构体的区别

结构体struct:把不同类型的数据组合成一个整体。struct里每个成员都有自己独立的地址;
共同体union:各成员共享一段内存空间, 一个union变量的长度等于各成员中最长的长度,以达到节省空间的目的;

explicit(显式)关键字

修饰构造和转换函数时,可防止隐式转换

friend 友元类和友元函数

可以访问私有成员、友元关系不可传递且具有单向性;

Using

尽量少使用 using 指示污染命名空间,多使用using声明std::cin、std::cout.

::范围解析运算符

全局作用域符 ::name
类作用域符 class::name
命名空间作用域符 namespace:: name

decltype

用于检查实体的声明类型或表达式的类型及值分类

左/右值引用

左值:可放在等号左边的、能够取地址、具名的值;(变量名、前置自增/减、赋值运算符、返回左值引用的函数调用、解引用)

右值:只能放在等号右边的、不能取地址、不具名的值;(纯右值:字面值、返回非引用类型的函数调用、后置自增/减、算术/逻辑/比较表达式
将亡值:(移动语义),将亡值用来触发移动构造或移动赋值构造进行资源转移,然后调用析构函数)

左值引用:常规引用,一般表示对象的身份;
右值引用:必须绑定到右值(一个临时对象、将要销毁的对象)的引用,一般表示对象的值。右值引用可实现移动语义和精准传递,主要可用于 1. 消除两个对象交互式不必要的对象拷贝,节省运算存储资源; 2. 更简洁明确的定义泛型函数;

左值引用是对左值的引用,右值引用是对右值的引用
const左值引用能指向右值:局限是不能修改这个值
右值引用通过std::move() (将左值转换为右值,也可以触发移动语义) 可以指向左值
声明出来的左值引用或右值引用都是左值

左值引用避免对象拷贝
右值引用可实现移动语义( 解决对象赋值的问题,对象赋值时避免资源的重新分配 ,通过触发移动构造和移动拷贝构造来实现的 )、完美转发( 不仅能准确地转发参数的值,还能保证其左右属性不变。实现:万能引用 )

成员初始化列

好处是少了一次调用默认构造函数的过程。

initializer_list 列表初始化

面向对象三大特征

  1. 封装:客观事物封装成抽象的类,关键字public(任意实体访问)、protected(只允许被子类及本类的成员函数访问)、private(只允许被本类的成员函数、友元类或友元函数访问)

  2. 继承: 基类---->派生类

  3. 多态:多种状态,可将多态定义为消息以多种形式显示的能力,可简单概括为”一个接口,多种方法“,即用同一个接口,但效果不同,有两种形式多态,即静态多态( 本质上就是模板的具现化 )和动态多态( 虚函数 )。

  4. C++多态分类及实现:
    4.1 重载多态(编译期):函数重载、运算符重载
    4.2 子类型多态(运行期):虚函数
    4.3 参数多态性(编译器):类模板、函数模板

静态多态:函数重载

class A{
public:
	void do(int a);
	void do(int a,int b);
};

动态多态:虚函数(virtual修饰成员函数)、动态绑定(使用基类的引用或指针调用一个虚函数时将发生动态绑定)
注: 可将派生类的对象赋值给基类的指针或引用,反之不行。普通函数(非成员函数)、静态函数、构造函数(调构造时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针) 都不能是虚函数(内联函数不能是表现多态时的虚函数)。

虚函数

虚函数是实现多态的一个技术之一,派生类的指针指向基类对象的地址(派生类对象的地址赋给基类指针)称为向上转型,c++允许隐式向上转型。将子类指向父类,向下转换则必须强制类型转换。我们可以用父类的指针指向其子类的对象,然后通过父类的指针调用实际子类的成员函数。如果子类重写了该成员函数就会调用子类的成员函数,没有声明重写就调用基类的成员函数。这种技术可以让父类的指针有“多种形态”

虚函数工作原理

c++没有强制规定虚函数的实现方式。编译器中主要用虚表指针(vptr)和虚函数表(vtbl)来实现的。当调用一个对象对应的函数时,通过对象内存中的vptr找到一个虚函数表vtbl,虚函数表内部是一个函数指针数组,记录的是该类各个虚函数的首地址。然后调用对象所拥有的函数。

虚函数表和虚函数指针位置:

构造函数可以设置为虚否?
不可,因为虚函数调用得通过”虚函数表“来进行,而虚函数表需要在对象实例化之后才能够进行调用。构造对象过程中,尚未给”虚函数表“分配内存,所以这个调用违背先实例化后调用准则。

友元函数不是虚函数,因为友元函数不是类成员,只有类成员才能是虚函数
静态成员函数不能是虚: 1. 因为static不属于任何类对象或类实例;2.静态成员函数没有隐藏的this指针,虚函数调用刚好需要this指针。在有虚函数的类实例中,this指针调用vptr指针,vptr找到vtable(虚函数列表),通过虚函数列表找到需要调用的虚函数地址。总体来说虚函数调用关系是:this指针->vptr->vtable ->virtual虚函数。

虚析构函数

如果类是父类,则必须声明为虚析构函数。基类声明一个虚析构函数,为了确保释放派生对象时,按照正确的顺序调用析构函数。如果析构函数不是虚的,那么编译器只会调用对应指针类型的虚构函数。切记,是指针类型的,不是指针指向类型的!! 而其他类的析构函数就不会被调用。
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。若基类有虚析构函数,那delete 释放内存的时候,先调用子类虚析构函数,再调用基类虚析构函数,防止内存泄漏。

纯虚函数----抽象基类

纯虚函数是一种特殊的虚函数,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。

虚函数在子类里面可以不重写;但纯虚函数必须在子类实现才可以实例化子类。
带纯虚函数的类叫抽象类,这种类不能直接生成对象(抽象类无法实例化对象),而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
虚基类是虚继承中的基类。

虚函数指针、虚函数表

虚函数指针:在含有虚函数类的对象中,指向虚函数表,在运行时确定;
虚函数表:在程序只读数据段,存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。

虚继承

虚继承用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)

虚继承、虚函数

相同之处:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)
不同之处:

  1. 虚继承
    虚基类依旧存在继承类中,只占用存储空间
    虚基类表存储的是虚基类相对直接继承类的偏移
  2. 虚函数
    虚函数不占用存储空间
    虚函数表存储的是虚函数地址

类模板、成员模板、虚函数

  1. 类模板中可以使用虚函数;
  2. 一个类(无论是普通类还是类模板)的成员模板(本身是模板的成员函数)不能是虚函数;
    抽象类:含有纯虚函数的类
    接口类:仅含有纯虚函数的抽象类

C++11关键字

noexcept:告诉编译器指定某个函数不抛异常;
oveerride: 告诉编译器要重写父类的方法;
final:该关键字用来修饰类,当用final修饰后,该类不允许被继承,在 C++ 11 中 final 关键字要写在类名的后面;
=default 、 =delete 、using等

new / delete、 malloc / free区别和联系

new / new[]:完成两件事,先底层调用 malloc 分配了内存,然后调用构造函数(创建对象)。
delete/delete[]:也完成两件事,先调用析构函数(清理资源),然后底层调用 free 释放空间。
new在申请内存时会自动计算所需字节数,所以不需要指定内存块大小,而 malloc 则需我们自己输入申请内存空间的字节数。
new操作符分配对象会经历三个步骤:

  1. 调用operator new函数分配一块足够大的原始的空间;
  2. 编译器运行相应的构造函数以构造对象,并传入初值;
  3. 构造完对象后,就返回一个指向该对象的指针;

new和malloc区别:malloc时c语言标准库函数,new时c++的运算符。malloc分配失败返回空指针,new分配失败默认抛出异常。malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针
malloc/fre存在的必要性: 对一些非内部数据类型来说,只用malloc/free无法满足要求,对象在创建同时要自动执行构造函数,对象消亡时自动执行析构函数,而malloc/ free不在编译器的控制权限内,不能自动执行构造函数和析构函数。所以c++中需要可完成动态内存分配和初始化工作的运算符new和完成清理和释放内存工作的运算符delete。但c程序只能用这俩,所以依然存在malloc/free。

free回收的内存是立即返还给操作系统吗?

不是的,回收的内存首先被ptmalloc使用双链接表保存起来,当用户下次申请内存的时候先尝试从这些内存中寻找,可避免频繁系统调用,ptmalloc也可避免过多内存碎片

this指针相关问题

  1. this指针是一个隐含于每一个非静态成员函数中的特殊指针,指向正在被该成员函数操作的那个对象。
  2. this指针在成员函数开始执行前构造,在成员执行结束后清除,this指针会因为某种情况有不同的存放位置,例如栈、寄存器、全局变量等。this是个右值,不能取得this地址 ( 不能&this );this指针只有在成员函数中才有定义,不能通过对象使用this指针,在成员函数里,是可以知道this指针的位置的(可通过&this获得),也可直接使用它。
  3. 在成员函数中调用delete this,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。
  4. 在类析构函数中调用delete this 会导致堆栈溢出,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存”。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。
  5. 要让delete this 合法,必须保证 this 对象是通过 new分配的,且delete this后面没人使用了,成员函数delete this后面没有调用this了。

如何定义一个只能在堆上(栈上)生成对象的类?

  1. 析构函数设为私有,类生成对象就只能定义在堆上,因为c++是静态绑定语言,为类对象分配栈空间时,会先检查类析构函数的访问性,若析构函数不能访问就不能在栈上创建对象;【只能在堆上也就意味着不能再栈上,在栈上是编译器分配内存空间,构造函数来构造栈对象。在栈上当对象周期完成,编译器会调用析构函数来释放栈对象所占的空间,也就是说编译器管理了对象的整个生命周期。编译器在调用构造函数为类的对象分配空间时,会先检查析构函数的访问性,不光是析构函数,编译器会检查所有非静态函数的访问性。因此,如果类的析构函数是私有的,编译器不会为对象在栈上分配内存空间。】
  2. 将new和delete重载为私有,类生成对象就只能定义在栈上,因为若要在堆上生成对象,使用new关键词操作需要两阶段:第一阶段,使用 new 在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将 new 操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象;【只有使用new运算符才会在堆上创建对象。设为私有即可。动态建立类对象,是使用new运算符将对象建立在堆空间中。这个过程分为两步,第一步是执行operator new()函数,在堆空间中搜索合适的内存并进行分配;第二步是调用构造函数构造对象,初始化这片内存空间(这种方法,间接调用类的构造函数),但是operator new()函数用于分配内存,无法提供构造功能,所以不能将构造函数设为私有;】

C++所有构造函数

类对象被创建时,编译器为对象分配内存空间,并自动调用构造函数,由构造函数完成成员的初始化工作。因此构造函数的的作用是初始化对象的成员函数;
默认构造函数:若无自定义构造函数,编译器会自动默认生成一个无参构造函数
一般构造函数:
拷贝构造函数:此函数的函数参数为对象本身的引用,用于根据一个已存在的对象复制出一个新的该类的对象,一般在函数中会将已存在的对象的数据成员的值一一复制到新创建的对象中。若没有写拷贝构造函数系统会默认创建一个拷贝构造函数,但类中有指针成员时,最好自己定义且在函数中执行深拷贝
移动构造函数:有时会遇到这样的情况,用对象a初始化对象b后对象a就不在使用了,但是对象a的空间还在(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那为啥不能直接使用a的空间呢?这样就避免新的空间的分配,大大降低了构造成本。这就是移动构造函数设计的初衷。拷贝构造函数中,对于指针,一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。
赋值构造函数:=运算符的重载,类似拷贝构造函数,将=右边的类对象赋值给类对象左边的对象,不属于构造函数,=两边的对象必须都要被创建。

智能指针

指针管理存在困难:资源释放指针没有置空(野指针、指针悬挂(多个指针指向同一块内存,一个已经释放了,但其他指针还在傻傻等待着) )、没有释放资源产生内存泄漏、重复释放资源引发coredump;

智能指针种类:

  1. share_ptr: 共享所有权,多个指针可以指向一个相同的对象,当最后一个share_ptr离开作用域时候才会释放掉内存。原理:在其内部有一个共享引用计数器来自动管理,计数器实际上就是指向该资源指针的个数,复制一个ptr时,引用计数会-1,引用计数为0时,delete内存。

  2. wrak_ptr : weak指针的出现是为了解决shared指针循环引用造成的内存泄漏的问题。而weak_ptr不会增加引用计数,因此将循环引用的一方修改为弱引用,可以避免内存泄露。允许共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何 weak_ptr 都会自动成空。可打破环状引用。

  3. unique_ptr : 它拥有对持有对象的唯一所有权,采用独占式拥有,意味着可以确保一个对象和其相应的资源同一时间只被一个 pointer 拥有。即两个unique_ptr不能同时指向同一个对象。主要体现在:1. unique_ptr不能被复制到另外一个unique_pt; 2. unique_ptr所持有的对象只能通过转移语义将所有权转移到另外一个unique_ptr;

强制类型转换运算符

static_cast(非多态类型转换,安全性不如dynamic_cast)
void*和其它类型指针之间的转换、子类对象的指针转换成父类对象的指针

dynamic_cast(多态类型转换,安全性好)
用于执行 ”安全地向下转型“,他是唯一一个在运行时处理的

进行下行转换时,dynamic_cast安全的,如果下行转换不安全的话其会返回空指针,这样在进行操作的时候可以预先判断。而使用static_cast下行转换存在不安全的情况也可以转换成功,但是直接使用转换后的对象进行操作容易造成错误。

运行时类型信息 (RTTI)

typeid

type_info

栈和堆

栈由系统自动分配(分配速度快,不会由碎片),堆是自己申请和释放的(速度慢,有碎片)
且栈顶和栈底时预设好的,大小固定,堆向高地址扩展,是不连续的内存区域,大小可灵活调整;
内存管理机制:
:系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆结点,删 除空闲结点链表中的该结点,并将该结点空间分配给程序(大多数系统会在这块内存空间首地址记录本次分配的大小,这样delete才能正确释放本内存空间,另外系统会将多余的部分重新放入空闲链表中);
:只要栈的剩余空间大于所申请空间,系统为程序提供内存,否则报异常提示栈溢出。(这一块理解一下链表和队列的区别,不连续空间和连续空间的区别,应该就比较好理解这两种机制的区别了);

STL

array,底层数据结构是数组,无序可重复且支持访问的;
vector, 底层数据结构是数组,
deque, 底层数据结构是双端队列
forward_list, 单向链表
list, 双向链表
stack,deque / list
queue, deque / list
priority_queue,底层数据结构是vector+max-heap
set,multiset, map,multimap 底层数据结构都是红黑树
unordered_set,unordered_multiset,unordered_map, unordered_multimap 底层数据结构是哈希表

数据结构

堆是一种完全二叉树形式,可分为最大值堆和最小值堆
最大值堆:子节点均小于父节点,根节点是树中最大的节点
最大值堆:子节点均大于父节点,根节点是树中最小的节点

哈希冲突处理方法

1.链式地址法:key相同的用单链表连接;
2.开放定址法:发生冲突时,若哈希表未被装满,可把这个值存放到冲突位置种的下一个空位置中;

AVL树

由于AVL树在每次插入和删除操作时都要进行旋转操作来保持平衡,因此相比于其他平衡树结构,如红黑树,AVL树更加平衡,但也更消耗空间和时间。任何一个节点的左子支高度与右子支高度只差绝对值不超过1。

红黑树

比起AVL树,红黑树的平衡调整操作简单有效,更适合对插入和删除操作频繁的场景。
红黑树主要特征:每个节点上增加一个属性表示节点颜色,可以红色或黑色。
性质

  1. 节点是红色或黑色。
  2. 根节点是黑色。
  3. 所有叶子节点(NIL节点)都是黑色。
  4. 如果一个节点是红色的,则其两个子节点都是黑色的。也就是说,红色节点之间不能有直接相连。
  5. 从任意一个节点到叶子节点,经过的黑色节点数量都相同,即黑高度相同。

B树和B+树

区别

  1. B+树中只有叶子节点会带有指向记录的指针(ROWID),而B树则所有节点都带有,在内部节点出现的索引项不会再出现在叶子节点中;
  2. B+树中所有叶子节点都是通过指针连接在一起,而B树不会;

B树有点

对于在内部节点的数据,可直接得到,不必根据叶子节点来定位。

B+树优点

  1. 非叶子节点不会带上 ROWID,一个块中可以容纳更多的索引项,一是可以降低树的高度。二是一个内部节点可以定位更多的叶子节点;
  2. 叶子节点之间通过指针来连接,范围扫描将十分简单,而对于B树来说,则需要在叶子节点和内部节点不停的往返移动;

红黑树、B、B+树

  1. 红黑树的深度比较大,而 B 树和 B+ 树的深度则相对要小一些;
  2. B+ 树则将数据都保存在叶子节点,并通过链表形式将他们连接在一起;

稳定/不稳定 排序算法

稳定:插入排序、冒泡排序、归并排序
不稳定:希尔排序、直接选择排序、堆排序、快速排序;

冒泡排序

比较相邻元素,如果第一个比第二个大就进行交换,对每一队相邻元素做同样的工作 (排序算法稳定,时间复杂度 O(n^2) ,空间复杂度O(1))

#include<iostream>
#include<vector>
using namespace std;

int main() {
	int n,temp;
	cin >> n;
	vector<int>a(n);
	for (int i = 0; i < n; i++) {
		cin >> a[i];
	}

	for (int i = 0; i < n - 1; i++) {
		for (int j = 0; j < n - i - 1; j++) {
			if (a[j] < a[j + 1]) {
				temp = a[j + 1];
				a[j + 1] = a[j];
				a[j] = temp;
			}
		}
	}
	cout << "冒泡排序结果:" << endl;
	for (int i = 0; i < n; i++) {
		
		cout << a[i] << endl;
	}
}

插入排序

每一趟将一个待排序记录按其关键字的大小插入到已排好序的一组记录的适当位置上,直到所有待排序记录全部插入为止。(稳定,时间:O(n^2))

#include<iostream>
#include<vector>

using namespace std;

int main() {
	int n,temp,j;
	cin >> n;
	vector<int>a(n);
	for (int i = 0; i < n; i++) {
		cin >> a[i];
	}

	for (int i = 1; i < n; i++) {  // i从1开始遍历
		temp = a[i];  //把这个值先存起来
		// 看前面的值有没有比当前值大,有的话就逐个互换位置
		for (j = i - 1; j >= 0 && temp < a[j]; j--) { 
			a[j + 1] = a[j];
		}
		a[j + 1] = temp;
	}
	cout << "插入排序结果:" << endl;
	for (int i = 0; i < n; i++) {
		
		cout << a[i] << " ";
	}
}

直接选择排序

每次从待排序列中找出最大或最小的元素,顺序放在待排序的数列的最前面,直到全部待排序的数据元素排完。(不稳定,时间O(n^2))

#include<iostream>
#include<vector>
using namespace std;

int main() {
	int n,temp,min;
	cin >> n;
	vector<int>a(n);
	for (int i = 0; i < n; i++) {
		cin >> a[i];
	}
	for (int i = 0; i < n; i++) {
		min = i;  // 从头遍历的时候,先假设最大值索引为 本次值i
		for (int j = i + 1; j < n; j++) { // 然后从i后面的值开始遍历比大小
			if (a[min] > a[j]) min = j; // 找到最小值的话就返回其索引
		}
		if (min != i) swap(a[i], a[min]); //根据索引交换相应元素
	}	

	cout << "选择排序结果:" << endl;
	for (int i = 0; i < n; i++) {
		
		cout << a[i] << " ";
	}
	return 0;
}

归并排序

通过分开治理,先将分开的部分变得有序,再来进行合并,称为归并排序;
二路归并: 通过将待排序列分成两部分进行归并排序;(<=是稳定的,时间复杂度O(NlogN));

#include<iostream>
using namespace std;

int a[8] = { 36,25,48,12,25,43,20,28 };
int b[8];
// 总体来说就是两两合并,先是两个元素合并,然后是两个组合并
void gsort(int left, int right) {
	if (left == right) return;
	int mid = (left + right) >> 1;
	gsort(left, mid);  // 分为左半区
	gsort(mid + 1, right); // 右半区
	int i = left, j = mid + 1, k = left;
	while (i <= mid && j <= right) {  // 两个大块逐元素对比
		if (a[i] <= a[j]) {
			b[k] = a[i];
			i++;
		}
		else {
			b[k] = a[j];
			j++;
		}
		k++;
	}
	while (i <= mid) b[k++] = a[i++];  // 合并左半区剩余元素
	while (j <= right) b[k++] = a[j++];  //合并右半区剩余元素
	for (int i = left; i <= right; i++) a[i] = b[i];  // 临时数组中合并后元素复制回原来的数组
}

int main() {
	int n=8;

	gsort(0,n-1);
	cout << "归并排序结果:" << endl;
	for (int i = 0; i < n; i++) {
		cout << a[i] << endl;
	}
	return 0;
}

根据实验,直接从b输出是不行的,a[i]=b[i]这行代码是必须要有的;

快速排序

分治思想,随机选一个基准元素,通过一趟排序将要排序的数据分割成独立的两部分,一部分全小于基准元素,一部分全大于基准元素,再按此方法递归对这两部分数据进行快速排序。(不稳定,时间复杂度O(NlogN))

#include<iostream>
using namespace std;

int a[8] = { 12,45,56,23,65,1,23,1 };
int n = 8;

void qsort(int left, int right) {
	int i = left, j = right;
	int mid = a[(left + right) >> 1];  // 取出中间元素
	do {
		while (a[i] < mid) i++; // i到1时就不满足条件退出
		while (a[j] > mid) j--; // j到n-1时不满足直接退出
		if (i <= j) {
			swap(a[i], a[j]); // 然后给大于mid的左边和小于mid的右边换一下
			i++, j--;  // 同时
		}
	} while (i <= j);

	if (j > left) qsort(left, j);  // 保证左边有序
	if (i < right) qsort(i, right); // 保证右边有序
}

int main() {
	qsort(0, n - 1);
	cout << "快速排序结果:" << endl;
	for (int i = 0; i < n; i++) {
		cout << a[i] << endl;
	}
	return 0;
}

堆排序

构造大顶堆,然后取出元素(直接把堆顶元素放到末尾,下次构建不考虑就相当于取出了),然后继续构造。(不稳定,时间复杂度O(NlogN))

设计模式

  1. 单一职责:一个类、接口或方法只负责一个职责;
  2. 开放封闭:程序拓展时,不能修改原有代码;
  3. 依赖倒置:针对接口编程,依赖于抽象类或接口而不依赖于具体实现类;
  4. 接口隔离:将不同功能定义在不同接口中实现接口隔离;
  5. 里氏替换:任何基类可出现的地方,子类一定可出现;
  6. 迪米特:
  7. 合成复用:
    设计模式分类:创建型模式(工厂方法模式、抽象工厂模式、单例模式)、结构型、行为型模式;
    简单工厂模式:因为不符合开闭原则,所以不属于GoF23种设计模式之一;
    单例模式:系统中只能有一个A类的对象。全局只有一个对象
    单例——懒汉模式 实现如下:
class Singleton {
private:
	static Singleton* instance;  // 静态成员变量,保存单例对象的指针
	Singleton(){} //私有构造函数 防止外部代码直接实例化对象
public:
	//获取单例的实例
	static Singleton* getInstance() {   // getInstance是获取单例的实例的静态方法
		if (instance == nullptr) {  
			instance = new Singleton(); // 为空就创建一个新的Singleton对象
		}
		return instance;  // 不为空就返回已存在的对象
	}
};
// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;

懒汉模式的好处是只有在需要使用单例对象时才会创建该对象,从而节省了系统资源。但缺点是第一次访问时需要创建对象,可能会导致一定的延迟,并且在多线程环境中需要特殊处理以保证线程安全。


class Singleton {
private:
	static Singleton* instance;
	//私有构造函数,防止外部创建对象
	Singleton(){}
public:
	//获取单例的实例
	static Singleton* getInstance() {
		return instance;
	}
};
// 初始化静态成员变量
Singleton* Singleton::instance = new Singleton();

饿汉模式的好处是在程序启动时就创建了单例对象,可以避免在程序运行过程中再次创建对象导致的延迟和线程安全问题。但缺点是如果该对象很大或者初始化需要消耗大量资源,会导致程序启动变慢。

在多线程环境中,使用懒汉模式实现的单例模式可能会存在线程安全问题,因为多个线程可能同时通过getInstance()方法来创建对象。为了解决这个问题,可以采用双重检查锁定(Double-Checked Locking)机制来保证线程安全。

#include<iostream>
#include<mutex>
using namespace std;

class Singleton{
private:
	static Singleton* instance;
	static mutex mtx;
	//私有构造函数  防止外部创建对象
	Singleton(){}
public:
	//获取单例的实现
	static Singleton* getInstance() {  // 双重检查锁定机制
		if (instance == nullptr) {
			unique_lock<mutex> lock(mtx); // 若为null 则进入临界区(加锁)
			if (instance == nullptr) {  // 再次检查是否为空  这样可避免多个线程同时创建对象
				instance = new Singleton();
			}
			lock.unlock(); //解锁  当第一个线程退出临界区(解锁)后,会继续执行 并返回已存在的实例。
		}
		return instance;
	}
};
// 初始化静态成员变量
Singleton* Singleton::instance = nullptr;
mutex Singleton::mtx;   // 使用互斥锁(std::mutex)来保证临界区的互斥访问,可以保证在多线程环境下的线程安全性。

int main() {
	thread t1([]() {
		Singleton* s1 = Singleton::getInstance();
		cout << "Thread 1 - Singleton address:" << s1 << endl;
		cout << endl;
		});
	thread t2([]() {
		Singleton* s2 = Singleton::getInstance();
		cout << "Thread 2 - Singleton address:" << s2 << endl;
		cout << endl;
		});
	t1.join();
	t2.join();

	return 0;
}

操作系统

进程与线程区别

进程是资源分配的基本单位,线程是运算调度的最小单位;

进程线程
资源开销创建、销毁、上下文切换开销高线程开销小
通信与同步管道、消息队列、共享内存线程共享相同的内存空间,可直接访问共享数据,所以通信更方便
安全性一个进程崩溃不会直接影响其他进程稳定性线程共享相同的内存空间,一个线程错误可能影响整个进程稳定性

进程调度算法

方法实现
先来先服务调度算法按照进程到达的先后顺序进⾏调度,即最早到达的进程先执⾏,直到完成或阻塞
最短作业优先优先选择运行时间最短的进程来运行
高响应比优先调度算法综合考虑等待时间和服务时间的比率,选择具有最高翔硬币的进程来执行
时间片轮转调度算法将CPU时间划分为时间片,每个进程在一个时间片内运行,然后切换到下一个进程
最高优先级调度为每个进程分配一个优先级,优先级高的进程先执行。可能导致低优先级进程长时间等待,引发饥饿问题
多级反馈队列
最短剩余时间优先每次选择剩余执行时间最短的进程来执行
最大吞吐量调度旨在最大化单位时间内完成的进程数量

进程间通信方式

方式具体作用
有名管道可实现任意关系的进程间通信
无名管道只能在父子进程间使用
信号量一个计数器,可用来控制多个线程对共享资源的访问
信号比较复杂的通信方式,用于通知接收进程某个事件已发生
消息队列消息的链表,存放在内核中(任意进程间通信)
共享内存映射一段能被其他进程访问的内存,这段内存有一个进程创建,丹多个进程都可访问
Socket套接字用于不同计算机的进程通信,支持TCP/IP网络通信的基本操作单元
作单元;

线程间通信

锁机制
互斥锁/量:提供了以排他方式防止数据结构被并发修改的方法;
读写锁:允许多个线程同时读共享数据,而对写操作是互斥的;
自旋锁:类似于互斥锁,都是为了保护共享资源,互斥锁是资源被占用,申请者进入睡眠状态,而自旋锁则循环检测保持者是否已经释放锁;
条件变量:以原子的方式阻塞进程直到某个特定条件为真为止,对条件的测试是在互斥锁保护下进行的。(始终与互斥锁一起使用);
信号量机制:
信号机制:类似进程间的信号处理;
屏障:
进程之间私有和共享的资源
私有:地址空间、堆、栈、全局变量、寄存器;
共享:代码段、公共数据、进程目录、进程ID;

多进程可靠性高,创建销毁,切换速度慢,内存资源占用大。多线程创建销毁和切换速度快,内存、资源占用小,但可靠性差;

死锁

两个或多个进程在争夺系统资源时,由于互相等待对⽅释放资源⽽⽆法继续执⾏的状态。当系统资源不足、资源分配不当、进程运行推进顺序不合适时就会产生死锁;

条件具体说明
互斥条件一个进程占用了某个资源时,其他进程无法同时占用该资源
请求保持一个线程因为请求资源而阻塞的时候,不会释放自己的资源
不可剥夺资源不能被强制性地从一个进程中剥夺,只能由持有者自愿释放
环路等待多个进程间形成一个循环等待资源的链,每个进程都在等待下一个进程所占有的资源

破坏上面任意条件就可破环死锁

条件具体方案
破环请求保持条件一次性申请所有资源
破环不可剥夺条件占用部分资源的线程进一步申请其他资源时,若申请不到可主动释放它占有的资源
破坏环路等待条件靠按序申请资源来预防。让所有进程按照相同的顺序请求资源,释放资源则反序释放

满足 1.互斥、2.请求和保持、3.不可剥夺、4.环路 就可死锁;

虚拟内存?

虚拟内存在每一个进程创建和加载的过程中,会分配一个连续虚拟地址空间,不是真实存在的,而是通过映射与实际地址空间对应,这样就可使每个进程看起来有自己独立的连续地址空间,每个程序都认为它拥有足够的内存来运行。
需要虚拟内存的原因如下:

原因具体说明
内存扩展虚拟内存是得每个程序都可以使用比实际可用内存更多的内存,从而允许运行更大的程序或处理更多的数据
内存隔离虚拟内存提供了进程之间的内存隔离,每个进程都有自己的虚拟地址空间,因此一个进程无法直接访问另一个进程的内存
物理内存管理虚拟内存允许操作系统动态的将数据和程序的部分加载到物理内存中,以满足当前正在运行的进程的需求。物理内存不足时,操作系统可以将不常用的数据或程序暂时移到硬盘上,释放内存便于其他进程使用
页面交换当物理内存不⾜时,操作系统可以将⼀部分数据从物理内存写⼊到硬盘的虚拟内存中,这个过程被称为⻚⾯交换
内存映射文件虚拟内存还可以⽤于将⽂件映射到内存中,这使得⽂件的读取和写⼊可以像访问内存⼀样⾼效

内存分段和分页?作用?

**内存分段:**是将⼀个程序的内存空间分为不同的逻辑段 segments ,每个段代表程序的⼀个功能模块或数据类型,如代码段、数据段、堆栈段等。每个段都有其⾃⼰的⼤⼩和权限;
**内存分⻚:**是把整个虚拟和物理内存空间分成固定⼤⼩的⻚(如4KB)。这样⼀个连续并且尺⼨固定的内存空间,我们叫⻚ Page;

具体作用有:逻辑隔离、内存保护、虚拟内存、内存共享、内存管理;

分页管理:内存空间利用率高,不会产生外部碎片,只会有少量的页内碎片,但不方便按照逻辑模块实现信息的共享和保护;
分段管理:很方便按照逻辑模块实现信息 的共享和保护,但是如果段长过大,为其分配很大的连续空间会很不方便,段式管理会产生外部碎片;

用户态和和心态

  1. 用户态:用户态下,进程或程序只能访问受限的资源和执行受限的指令集,不能直接访问操作系统的核心部分,也不能直接访问硬件资源;
  2. 和心态:核心态时操作系统的特权级别,允许进程或程序执行特权指令和访问操作系统的核心部分。核心态下,进程可以直接访问硬件资源,执行系统调用,管理内存,文件系统等操作。

页面置换算法,如LRU(最近最少使用),FIFO(先进先出)

  1. 最佳置换算法:这是一个非常理想且神奇的算法,该算法根据未来的页面访问情况,选择最长时间内不会被访问到的页面进行置换。这就存在问题了,未来访问啥页面?操作系统咋知道?当然不知道,所以此算法通常不会实现
  2. 先进先出算法:最先进入内存的页面最先被置换出去。存在一个问题,随着分配给进程的空闲页面数增加,缺页情况也会增加。我们通常认为若一个进程经常发生缺页,那就应该给他多分配一点内存,然而FIFO时,反而可能导致更多缺页情况出现;
  3. 最近最久未使用 (LRU) 算法:基于页面的使用历史,通过选择最长时间未被使用的页面进行置换。核心思想:最近被访问的页面可能在未来再次被访问,而最长时间未被访问的页面可能是最不常用的,因此将其置换出去可以腾出空间给新的页面。

进程同步与互斥,以及解决问题方法?

进程同步:多个并发执行的进程之间协调和管理他们的执行顺序,以确保他们按照一定的顺序或时间间隔执行。
互斥:在某一时刻只允许一个进程访问某个共享资源。当一个进程正在使用共享资源时,其他进程不能同时访问该资源。
常见解决进程同步和互斥的方法:使用信号量和PV操作,PV操作是一种对信号量进行增加或者减少的操作,他们可用来控制进程之间的同步或者互斥。
以下方法也可解决进程同步和互斥问题:

方法具体实现
临界区可能引发互斥问题的代码称为临界区,每个进程进入临界区前必须获取一个锁,退出后释放该锁,可确保同一时间只有一个进程可以进入临界区
互斥锁一种同步机制,进程访问资源前获取互斥锁,用完后释放锁,只有获得所的进程才能访问共享资源
条件变量用于在进程之间传递信息,一遍他们在特定条件下等待或唤醒。通常与互斥锁一起使用,以确保等待和唤醒的操作在正确的时机执行

中段和异常?有啥区别?

中断和异常是两种不同的事件,它们都会导致CPU暂停当前的程序执⾏,转⽽去执⾏⼀个特定的处理
程序。但中断是由外部设备或其他处理器产生的,通常是异步的,且可以被屏蔽或禁止(CPU可以不鸟中断);异常是由CPU内部产生的,通常是同步的,且不可以被屏蔽或禁止(CPU必须立即响应,随时待命处理)

中断作用中断产生
外设异步通知CPU外设
CPU之间发送消息CPU
处理CPU异常CPU异常( 陷阱、故障、中止 )
实现系统调用中断指令

几种典型的锁?

解释
互斥锁用于实现互斥访问共享资源,任何时刻只有一个线程可以持有互斥锁,其他线程必须等待直到锁被释放
自旋锁一种基于忙等待的锁,即线程在尝试获取锁时会不断轮询,直到锁被释放
读写锁允许多个线程同时读共享资源,只允许一个线程进行写操作
悲观锁认为多线程同时修改共享资源的概率比较高,所以访问共享资源时要上锁
乐观锁先啥也不管,修改了共享资源再说,若出现同时修改的情况,再放弃本次操作

线程同步方式

为保证线程之间的互不干扰采用的一种机制,叫线程同步机制
互斥锁、条件变量、读写锁、信号量

计算机网络

物理层:通过媒介传输比特,确定机械及电器规范
数据链路层:将比特组装城帧和点到点的传递(帧Frame)
网络层:IP协议为计算机网络相互连接进行通信而设计的协议;
ARP(地址解析协议)、ICMP(网际控制报文协议)、IGMP(网际组管理协议)
VPN虚拟专用网、NAT网络地址转换
路由表:网络ID、子网掩码、下一跳地址/接口
运输层:TCP、UDP
TCP是面向连接的智能点对点通信,面向字节流(可能出现粘包问题)、可靠交互。TCP通过确认和超时重传、数据合理分片和排序、流量控制、拥塞控制、数据校验等机制保证可靠传输;
UDP是无连接的,尽最大努力交付的说明他不太可靠,面向报文的(不会出现粘包),没有拥塞控制,支持一对一、一对多、多对多交互通信的;

输入URL到页面展示到底发生了什么?

  1. 浏览器收到用户请求,先检查浏览器缓存中是否有该资源,没有进入下一步网络请求;
  2. 进行DNS解析,获取请求域名的IP地址,HTTPS还需建立TLS连接。DNS解析(本地浏览器缓存 -> 本地Host文件 路由器缓存 -> DNS服务器 ->根DNS服务器顺序查找域名对应IP)
  3. 浏览器与服务器IP建立TCP连接。然后构建请求行、请求头等信息。向服务器构建请求信息;
  4. 服务器收到信息后生成响应数据
  5. 浏览器解析响应头
  6. 浏览器解析HTML文件,渲染,完成页面展示

TCP、UDP概念,特点区别和对应的使用场景?

TCPUDP
可靠性可靠不可靠
连接性面向连接无连接
报文面向字节流面向报文
效率
双工性全双工一对一、一对多、多对一、多对多
流量控制滑动窗口
拥塞控制慢启动、拥塞避免、快重传、快恢复
传输速度
应用场景要求通信数据可靠场景(如⽹⻚浏览、⽂件传输、邮件传输、远程登录、数据库操作等)⽤于要求通信速度⾼场景(如域名转换、视频直播、实时游戏等)

HTTP请求常见状态码和字段

常见状态码:
1xx : 提示信息,协议处理的中间状态;
2xx : 请求成功;
3xx : 请求重定向;
4xx : 请求错误;
5xx : 服务器错误;(504:网关超时,服务器作为网关或代理,但没有及时从上游服务器收到请求)

HTTP常见字段:
Host字段:客户端请求发送时,用来指定服务器的域名
Content-length字段:服务器返回数据时,带有该字段,表示回应的数据长度
Connection字段:用于客户端要求服务器使用HTTP长连接时使用;
Content-Type字段:服务器返回时告诉客户端本次数据的 格式
Content-Encoding字段:服务器返回的数据使用了什么压缩格式

常见请求方式?GET和POST请求区别?

GETPOST
作用从服务端获取资源向服务端提交数据
参数传递方式一般写在URL中,且只接受ASCII字符一般放在请求体中,对数据类型无限制
安全性参数直接暴露在URL中不安全,不能传递敏感信息安全
参数长度限制数据量较小,不能大于2KB数据量较大,一般默认为不受限制
参数长度限制HTTP协议没有Body和URL的长度限制,对URL限制的大多是浏览器和服务器的原因
编码方式URL编码多种编码方式
缓存机制请求被浏览器Cache,请求参数被完整保留在浏览器历史记录里,产生的URL地址可被保存为书签,在浏览器退出时是无害的POST不会被主动cache,参数不会被保留,不可保存为书签,浏览器回退时POST会再次提交请求
时间消耗产生一个TCP数据包产生两个TCP数据包
发送数据浏览器把header和data一并发送出去,服务器响应200浏览器先发header,服务器响应100continue,浏览器再发送data,服务器响应200ok
幂等因为只读操作,无论操作多少次,服务器数据都是安全的因为是新增或提交数据操作,会修改服务器上资源,所以不安全,不是幂等的

强缓存和协商缓存?

缓存:减少不必要网络传输、节约带宽;更快加载页面;减少服务器负载,避免服务过载情况出现;
Cache-Control强缓存
last-modified
If-Modified-Since
if-None-Match
Etag什么的

HTTP1.0和HTTP1.1区别?

HTTP1.0HTTP1.1
长连接默认短连接,每次请求都需建立一个TCP连接支持长连接,每个TCP连接上可传送多个HTTP请求和响应,默认开始Connection : Keep-Alive
缓存主要使⽤ If-Modified-Since/Expires 来做为缓存判断的标准引⼊了更多的缓存控制策略例如 Entity tag / If-None-Match 等更多可供选择的缓存头来控制缓存策略
管道化使请求能够”并行“传输,但响应必须按照请求发出的顺序依次返回
增加Host字段一个服务器能够用来创建多个Web站点
状态码新增了24个错误状态响应码
带宽优化存在一些浪费带宽现象,如客户端仅需某个对象一部分,服务器将整个对象送过来,且不支持断点续传功能在请求头引入了range头域,允许只请求资源的某个部分,返回码时206

HTTP2.0 和 HTTP1.1区别?

HTTP1.1HTTP2.0
二进制分帧在应用层(HTTP2.0)和传输层(TCP or UDP)间加入一个二进制分帧层,从而突破HTTP1.1的性能限制
多路复用允许同时通过单⼀的 HTTP/2 连接发起多重的请求-响应消息,这个强⼤的功能则是基于“⼆进制分帧”的特性
首部压缩不⽀持 header 数据的压缩使⽤ HPACK 算法对 header的数据进⾏压缩,这样数据体积⼩了,在⽹络上传输就会更快。⾼效的压缩算法可以很⼤的压缩 header ,减少发送包的数量从⽽降低延迟。
服务端推送服务器可对客户端的一个请求发送多个响应,即服务器可额外的向客户端推送资源,而无需客户端明确的请求

HTTPS工作原理?(https怎么建立连接的)

  1. 首先,客户端向服务端发送请求报文,请求与服务端建立连接;
  2. 服务端产生一对公私钥,将自己的公钥发给CA机构,CA机构也有一对公私钥,它使用自己的私钥将服务端发送的公钥加密,产生一个CA数字证书,并发送给客户端;
  3. 客户端将服务端发送过来的数字证书进行解析(浏览器跟CA有合作,所以浏览器本来就保存了大部分CA的密钥,用于对服务端的数字证书解密),验证此数字证书是否合法,不合法就发送警告,合法就取出服务端生成的公钥;
  4. 客户端取出公钥并生成一个随机码key ( 对称加密中的密钥 ),随机码加密后发给服务端,作为接下来的对称加密的密钥;
  5. 服务端接收到随机码key后,使用自己的私钥对他进行解密,获得随机码key
  6. 服务端使用key对传输数据加密后发给客户端;
  7. 客户端使用自己生成的随机码key解密服务端发送过来的数据,之后客户端和服务端通过对称加密传输数据,随机码key作为传输的密钥;

HTTPS与HTTP的区别?

HTTPHTTPS
传输明文传输通过SSL\TLS加密传输
端口80443
CA证书需要
连接连接简单、无状态SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP安全

DNS是啥?查询过程?

DNS域名管理系统是用户使用浏览器访问网址后使用的第一个重要协议,DNS解决的是域名和IP地址映射问题;

  1. 首先用户在浏览器输入URL后,会先查询浏览器缓存是否有该域名对应的IP地址;
  2. 浏览器缓存中没有就去计算机本地Host文件中查询是否有对应缓存,没有则会向本地的DNS解析器发送一个DNS查询请求;
  3. 若本地DNS解析器没有缓存该域名的解析记录,会向根DNS服务器发出查询请求。根DNS服务器可告诉本地DNS解析器应该向哪个顶级域(.com / .net / .org )的DNS服务器继续查询;
  4. 本地DNS解析器接着向指定的顶级域DNS服务器发出查询请求。顶级域可告诉本地DNS解析器应前往哪个权威DNS服务器查询下一步信息;
  5. 本地DNS解析器最后向权威服务器 (负责存储特定域名和IP地址映射)发送查询请求。然后会查到对应IP地址;
  6. 本地DNS收到IP地址后返回给浏览器,换回将域名解析结果缓存在本地,便于下次快速响应。

HTTP多个TCP连接咋实现的?

多TCP连接靠某些服务器对Connection: keep-alive 的 Header进⾏了⽀持,就是完成此HTTP请求后,不断开HTTP请求使用的TCP连接。这种操作的好处是连接可被重新使用,之后发送HTTP请求时不需要重新建立TCP连接,且维持连接的SSL开销也能避免;

TCP 的 Keepalive 和 HTTP 的 Keep-Alive 是⼀个东⻄吗?

HTTP的Keep-Alive是由应用层(用户态)实现的,称为HTTP长连接( 同一个TCP发送接收多个HTTP 请求/应答 避免了连接建立和释放的开销);HTTP短连接每次建立链接都只能请求一次资源,都要经历建立TCP->请求资源->响应资源->释放连接;
TCP的KeepAlive是由 TCP 层(内核态) 实现的,称为 TCP 保活机制:TCP有一个定时任务做倒计时,超时后触发任务,内容是发送一个探测报文给对端,用来判断对端是否存活;

TCP通过啥保证可靠性的?

方法名具体实现
数据块大小控制应用数据被分割成TCP认为最合适发送的数据块,再传输给网络层,数据块被称为报文段或段
序列号TCP给每个数据包指定序列号,接收⽅根据序列号对数据包进⾏排序,并根据序列号对数据包去重
校验和TCP将保持它⾸部和数据的校验和,⽬的是检测数据在传输过程中的任何变化,若收到报文校验和有错,TCP丢弃此报文段
流量控制TCP连接的每⼀⽅都有固定⼤⼩的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收⽅来不及处理发送⽅的数据,能提示发送⽅降低发送的速率,防⽌包丢失。TCP⽤滑动窗⼝实现流量控制
拥塞控制网络拥塞时,减少数据发送;慢启动、拥塞避免、超时重传、快速重传和快速恢复等
确认应答通过ARQ协议实现。基本原理是每发完⼀个分组就停⽌发送,等待对⽅确认。如果没收到确认,会重发数据包,直到确认后再发下⼀个分组
超时重传TCP发出⼀个数据段后,它启动⼀个定时器,等待⽬的端确认收到这个报⽂段。如果不能及时收到⼀个确认,将重发这个报⽂段

拥塞控制咋实现的?

方法名具体实现
慢启动在连接刚开始时,发送方会逐渐增加发送窗口大小,从而以指数增长的速度增加发送的数据量
拥塞避免慢启动阶段过后,发送方进入拥塞避免阶段,这个阶段发送方逐渐增加发送窗口的大小,但增加速率较慢,避免过快导致网络拥塞
超时重传若发送方在超时时间内未收到确认,会认为数据包丢失,并重传这些数据包。这是拥塞窗口的最后手段,用于检测和处理网络中的丢包或拥塞情况
快速重传和快速恢复发送方发送的数据包丢失或网络出现拥塞时,接收方会发送重复确认(ACK)通知发送方有数据包丢失。发送方受到一定数量的重复确认时,会立即重传丢失的数据包而不是等待超时
拥塞窗口调整发送方根据网络的拥塞程度动态调整发送窗口大小,通过检测网络延迟和丢包情况确定合适的发送速率

Cookie和Session是什么?有什么区别?

两者都用于管理用户的状态和身份,Cookie 通过在客户端记录信息确定⽤户身份, Session 通过在服务器端记录信息确定⽤户身份。

  1. Cookie
    Cookie 是存储在⽤户浏览器中的⼩型⽂本⽂件,⽤于在⽤户和服务器之间传递数据。通常,服务器会将⼀个或多个 Cookie 发送到⽤户浏览器,然后浏览器将这些 Cookie 存储在本地。服务器在接收到来⾃客户端浏览器的请求之后,就能够通过分析存放于请求头的Cookie得到客户端特有的信息,从⽽动态⽣成与该客户端相对应的内容。
  2. Session
    客户端浏览器访问服务器的时候,服务器把客户端浏览器以某种形式记录在服务器上。Session 主要⽤于维护⽤户登录状态、存储⽤户的临时数据和上下⽂信息等。
CookieSession
存储位置用户浏览器服务器
数据容量较小,一般为几KB无固定限制,取决于服务器的配置和资源
安全性可被用户读取和篡改难以被访问和修改,所以更安全
传输方式每次HTTP请求中都会被自动发送到服务器通常通过Cookie或URL参数传递

TCP粘包问题

TCP是一个基于字节流的传输服务,意味着TCP所传输的数据是没有边界的,所以可能会出现两个数据包黏在一起的情况;
解决方案:发送定长包;包头上加上包体长度,给他说明一下具体消息长度;在数据包之间设置边界,例如添加特殊符号\r\n标记之类的(FTP协议就这么做的。问题是如果正文中也有\r\n那也会误判消息边界);使用更加复杂的应用层协议;

TCP流量控制

流量控制是让发送方发送速率不要太快,要让接收方来得及接收;
主要方法是利用可变窗口进行流量控制

TCP拥塞控制

拥塞控制就是防止过多的数据注入到网络中,这样可使网络中的路由器或链路不至过载;
方法:慢启动、拥塞避免、快重传、快恢复;

TCP三次握手

  1. 客户端发送SYN给服务器,说明客户端请求建立连接;
  2. 服务端收到客户端发的SYN,并回复SYN+ACK给客户端(同意建立连接);
  3. 客户端收到服务端的SYN+ACK后,回复ACK给服务端(表示客户端收到了服务端发的同意报文);
  4. 服务端收到客户端的ACK,连接建立,可以传输数据;
    可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的;

为啥需要三次握手

  1. 因为信道不可靠,TCP想建立可靠地传输,三次通信是理论上的最小值(UDP不需要建立可靠传输,因此UDP不需要三次握手);
  2. 双方都需要确认对方收到了自己发送的序列号,确认过程最少要进行三次通信;
  3. 为防止已失效的连接请求报文段突然又传送到了服务端,而产生错误(要是只有两次,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,从而造成资源浪费);

TCP四次挥手

  1. 客户端发送⼀个 FIN 报⽂给服务端,表示⾃⼰要断开数据传送,报⽂中会指定⼀个序列号 (seq=x) 。然后,客户端进⼊ FIN-WAIT-1 状态;
  2. 服务端收到 FIN 报⽂后,回复 ACK 报⽂给客户端,且把客户端的序列号值 +1 ,作为ACK +1 报⽂的序列号 (seq=x+1) 。然后,服务端进⼊ CLOSE-WAIT (seq=x+1) 状态,客户端进⼊ FIN-WAIT-2 状态;
  3. 服务端也要断开连接时,发送 FIN 报⽂给客户端,且指定⼀个序列号 (seq=y+1) ,随后服务端进⼊ LAST-ACK 状态;
  4. 客户端收到FIN报⽂后,发出ACK报⽂进⾏应答,并把服务端的序列号值+1作为ACK报⽂序列号 (seq=y+2) 。此时客户端进⼊TIME-WAIT状态。服务端在收到客户端的 ACK报⽂后进⼊ CLOSE 状态。如果客户端等待 2MSL 没有收到回复,才关闭连接。

为啥建立连接得三次,释放连接得四次?

TCP是全双工模式,客户端请求关闭连接后,客户端向服务端的连接关闭(一二次挥手),服务端继续传输之前没传完的数据给客户端(数据传输),服务端向客户端的连接关闭(三四次挥手),TCP释放连接时服务器的ACK和FIN是分开发送的(中间隔着数据传输),而TC建立连接时服务器的ACK和SYN是一起发送的(第二次握手)。

TCP 是全双⼯通信,可以双向传输数据。任何⼀⽅都可以在数据传送结束后发出连接释放的通知,待对⽅确认后进⼊半关闭状态。 当另⼀⽅也没有数据再发送的时候,则发出连接释放通知,对⽅确认后才会完全关闭了 TCP 连接。总结:两次握⼿可以释放⼀端到另⼀端的 TCP 连接,完全释放连接⼀共需要四次握手。

为啥连接时ACK和SYN可一起发送,释放时分开发送嘞?

因为客户端请求释放连接时,服务器可能还有数据需要传输给客户端,因此服务端要先响应客户端FIN请求(服务端发送ACK),然后传输数据,完成后,服务端再提出FIN请求(服务端发送FIN);而连接时则没有中间的数据传输;

为啥客户端释放最后需要TIME_WAIT等2MSL嘞?

  1. 为保证客户端发送的最后一个ACK报文能够到达服务端,若未成功到达,则服务端超时重传FIN+ACK报文段,客户端再重传ACK,并重新计时;
  2. 防止已失效的连接请求报文段出现在本连接中。持续2MSL可使本连接持续的时间内所产生的所有报文段都从网络中消失,这样可使下次连接中不会出现旧的连接报文段;

应用层

DNS域名系统:作为将域名和IP地址相互映射的一个分布式数据库,能使人更方便的访问互联网。DNS使用TCP和UDP端口53;
域名:
FTP(文件传输协议):用于在网络上进行文件传输的一套标准协议,使用客户/服务器模式,使用TCP数据报,提供交互式访问,双向传输。
TELNET:
HTTP:
GET:请求指定的页面信息,并返回实体主体;
POST:向指定资源提交数据进行处理请求(例如提交表单或上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和已有资源的修改;

状态码:
1xx : 通知信息,请求收到了或正在进行处理
2xx : 成功,如接收或知道了
3xx : 重定向,要完成请求还必须采取进一步的行动
4xx : 客户差错,如请求中有错误的语法或不能完成
5xx : 服务器差错,如服务器失效无法完成请求

其他协议:
SMTP(简单邮件传输协议):传输Email的标准,一个相对简单的基于文本的协议;
DHCP(动态主机设置协议):一个局域网的网络协议,使用UDP协议工作;

网络编程

Socket

首先服务端先建立socket()接口,然后bind(),然后listen(),accept()等待客户端建立连接,TCP客户端也得先建立socket()接口,然后connect()建立连接,然后客户端的werite()函数写入数据到服务端,服务端利用read()函数读取数据,服务端处理完请求后利用write()函数写入响应给客户端,客户端利用read()函数读取响应的信息,完事后客户端可以close()关闭连接,服务端收到请求后也开始关闭连接。over

三次握手

  1. 当客户端调用 connect 时,触发了连接请求,向服务器发送了 SYN J 包,这时 connect 进入阻塞状态;
  2. 服务器监听到连接请求,即收到 SYN J 包,调用 accept 函数接收请求并向客户端发送 SYN K ,ACK J+1,这时 accept 进入阻塞状态;
  3. 客户端收到服务器的 SYN K ,ACK J+1 之后,这时 connect 返回,并对 SYN K 进行确认;
  4. 服务器收到 ACK K+1 时,accept 返回(第三次握手可携带数据,返回连接socket),三次握手完毕连接建立;

四次挥手

  1. 某应用进程首先调用 close 主动关闭连接,这时 TCP 发送一个 FIN M;
  2. 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认,它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
  3. 一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的 TCP 也发送一个 FIN N;
  4. 接收到这个 FIN 的源发送端 TCP 对它进行确认;
    四次挥手主要是保证每个方向上都有一个FIN和ACK;

MySQL

执行一条SQL语句,期间发生了啥?

  1. 连接器:建立连接、管理连接、校验用户身份;
  2. 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行;
  3. 解析SQL,通过解析器堆SQL查询语句进行词法、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型
  4. 执行SQL:预处理阶段、优化阶段、执行阶段
    (小林的图,主打一个清晰明了,看官们领略一下吧!)
    在这里插入图片描述

索引

索引就是数据的目录,MySQL存储引擎有 MyISAM InnoDB Memory;

索引分类

按照数据结构来分(B+Tree索引、HASH索引、Full-Text索引)

B+树索引:所有数据存储在叶⼦节点,复杂度为O(logn),适合范围查询
哈希索引:适合等值查询,检索效率⾼,⼀次到位

B+Tree索引类型是MySQL存储引擎采用最多的索引类型

创建的主键索引和二级索引默认使用的是B+Tree索引,B+Tree是一种多叉树,叶子节点存放数据,非叶子节点只存放索引,且每个节点里的数据按照主键顺序存放的。每层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,且每个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表
每读取一个节点当作一次I/O操作,如整个查询过程共经历了3个节点,就进行了3次I/O操作。B+Tree存储千万级的数据只需要3-4层高度就可满足,意味着从千万级的表查询目标数据最多需要3-4次磁盘I/O,所以B+Tree相比于B树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况下,查询一个数据的磁盘I/O依然维持在3-4次

二级索引查询商品数据过程:

主键索引B+Tree和二级索引B+Tree区别:

  1. 主键索引的B+Tree的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的B+Tree的叶子节点里;
  2. 二级索引的叶子节点存放的是主键值

若使用二级索引查询商品,先检二级索引中的B+Tree的索引值,找到对应的叶子节点,然后获取主键值,再通过主键索引中的B+Tree树查询到对应的叶子节点,然后获取整行数据(此过程叫回表,得差两个B+Tree才能查到数据)。但当查询的数据只能在二级索引的B+Tree的叶子节点里查询到,这时就不用再主键索引,这种在二级索引的B+Tree就能查询到结果的过程就叫做覆盖索引,也就是只需要查一个B+Tree就能找到数据;

为啥InnoDB选择B+Tree作为索引的数据结构?
  1. B+Tree vs B Tree
    1)B+只在叶子节点存储数据,而B树在非叶子节点也存数据,所以B+Tree单个节点的数据量更小,相同磁盘I/O次数下查的节点更多;
    2)B+采用双链表链接,适合基于范围的顺序查找,而B树无法做到;
  2. B+Tree vs 二叉树
    B+搜索复杂度为O(logdN),因为是多叉树,d代表节点允许的最大子结点个数为d个,实际应用中d>100,就可保证数据达到千万级别时,B+Tree高度依然维持在3~4层左右。而二叉树每个父节点的子结点个数只能是2个,意味着其搜索复杂度为O(logN),因此二叉树检索到目标数据所经历的磁盘I/O次数要更多;
  3. B+Tree vs Hash
    Hash做等值查询时效率很快,搜索复杂度O(1),但Hash表不适合做范围查询,这也是B+Tree笔Hash表索引有更广泛使用场景的原因;

按物理存储分类(聚簇索引(主键索引),二级索引(辅助索引))

在查询时使用了二级索引,如果查询的数据能在二级索引里查询的到,那么就不需要回表,这个过程就是覆盖索引。如果查询的数据不在二级索引里,就会先检索二级索引,找到对应的叶子节点,获取到主键值后,然后再检索主键索引,就能查询到数据了,这个过程就是回表。

按字段特性分类(主键索引、唯一索引、普通索引、前缀索引)

  1. 主键索引
    建立在主键字段上的索引,一张表最多只有一个主键索引,索引列的值不允许有空值;创建主键索引方式如下:
CREATR TABLE table_name(
	...
	PRIMARY KEY(index_column_1) USING BTREE
);
  1. 唯一索引
    建立在UNIQUE字段上的索引,一张表可有多个唯一索引,索引列的值必须唯一,允许有空值
  2. 普通索引
    普通索引就是建立在普通字段上的索引,既不要求字段为主键,也不要求字段为 UNIQUE
  3. 前缀索引
    指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,使用前缀索引目的为了减少索引占用的存储空间,提升查询效率。

按字段个数分类(单列索引、联合索引 (复合索引) )

建立在单列上的索引称为单列索引,如主键索引,建在多列上的索引称为联合索引

联合索引

将多个字段组合成一个索引,就是联合索引;
联合索引非叶子节点用两个字段的值作为B+Tree的key值,使用联合索引存在最左匹配原则(不遵循,联合索引就会失效);比如创建一个联合索引(a,b,c),要是不遵循最左匹配,那b,c是全局无序,局部相对有序的的话,是无法利用到索引的。(利用索引的前提是索引里的Key是有序的)

联合索引 范围查询

联合索引中并不是查询过程使用了联合索引查询,就代表联合索引中的所有字段都用到了联合索引进行索引查询;
也可能存在部分字段用到联合索引的B+树,部分字段没有用到联合索引B+Tree的情况。最左匹配原则会一直享有匹配直到遇到 范围查询 就会停止匹配。

select * from t_table where a > 1 and b = 2

符合a>1条件的二级索引记录的范围里,b字段的值是无序的。条查询语句只有 a 字段用到了联合索引进行索引查询,而 b 字段并没有使用到联合索引;

select * from t_table where a >= 1 and b = 2

在符合 a>= 1 条件的二级索引记录的范围里,b 字段的值是「无序」的,但是对于符合 a = 1 的二级索引记录的范围里,b 字段的值是「有序」的(因为对于联合索引,是先按照 a 字段的值排序,然后在 a 字段的值相同的情况下,再按照 b 字段的值进行排序。这条查询语句 a 和 b 字段都用到了联合索引进行索引查询

SELECT * FROM t_table WHERE a BETWEEN 2 AND 8 AND b = 2

在 MySQL 中,BETWEEN 包含了 value1 和 value2 边界值,类似于 >= and =<。而有的数据库则不包含 value1 和 value2 边界值(类似于 > and <)。这条查询语句 a 和 b 字段都用到了联合索引进行索引查询

SELECT * FROM t_user WHERE name like 'j%' and age = 22

然在符合前缀为 ‘j’ 的 name 字段的二级索引记录的范围里,age 字段的值是「无序」的,但是对于符合 name = j 的二级索引记录的范围里,age字段的值是「有序」的(因为对于联合索引,是先按照 name 字段的值排序,然后在 name 字段的值相同的情况下,再按照 age 字段的值进行排序。这条查询语句 a 和 b 字段都用到了联合索引进行索引查询。

综上:联合索引的最左匹配原则,在遇到范围查询(如 >、<)的时候,就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。注意,对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配。

索引下推
索引下推优化(index condition pushdown), 可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

索引区分度
实际开发工作中建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的 SQL 使用到。

如何通过索引提高查询效率?( 联合索引进行排序 )

select * from order where status = 1 order by create_time asc

利用索引的有序性,在 status 和 create_time 列建立联合索引,这样根据 status 筛选后的数据就是按照 create_time 排好序的,避免在文件排序,提高了查询效率。

啥时候适用索引?
  1. 字段有唯一性限制的,比如商品编码;
  2. 经常用于WHERE查询条件的字段,这样能够提高整个表的查询速度,如果查询条件不是一个字段,可以建立联合索引;
  3. 常用于 GROUP BY 和 ORDER BY 的字段,这样在查询时候就不需要再去做一次排序了,因为我们都已经知道了建立索引之后在 B+Tree 中的记录都是排序好的;
啥时候不用创建索引?
  1. WHERE 条件,GROUP BY,ORDER BY 里用不到的字段,索引的价值是快速定位,如果起不到定位的字段通常是不需要创建索引的,因为索引是会占用物理空间的;
  2. 字段中存在大量重复数据,不需要创建索引,比如性别字段,只有男女,如果数据库表中,男女的记录分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为 MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描;
  3. 表数据太少的时候,不需要创建索引;
  4. 常更新的字段不用创建索引,比如不要对电商项目的用户余额建立索引,因为索引字段频繁修改,由于要维护 B+Tree的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能的;

索引优化方法

前缀优化

前缀索引顾名思义就是使用某个字段中字符串的前几个字符建立索引。为了减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。限制:

  1. order by就无法使用前缀索引;
  2. 无法把前缀索引用作覆盖索引
覆盖索引优化

覆盖索引是指 SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作.使用覆盖索引的好处就是,不需要查询出包含整行记录的所有信息,也就减少了大量的 I/O 操作。

主键索引最好是自增的

建表的时候,都会默认将主键索引设置为自增的

  1. 如果我们使用自增主键,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。因为每次插入一条新记录,都是追加操作,不需要重新移动数据,因此这种插入数据的方法效率非常高;
  2. 如果我们使用非自增主键,由于每次插入主键的索引值都是随机的,因此每次插入新的数据时,就可能会插入到现有数据页中间的某个位置,这将不得不移动其它数据来满足新数据的插入,甚至需要从一个页面复制数据到另外一个页面,我们通常将这种情况称为页分裂。页分裂还有可能会造成大量的内存碎片,导致索引结构不紧凑,从而影响查询效率;

主键字段长度越小,意味着二级索引的叶子节点越小(二级索引的叶子节点存放的数据是主键值),这样二级索引占用的空间也就越小。

防止索引失效

常见索引失效场景:

  1. 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx%这两种方式都会造成索引失效;
  2. 当我们在查询条件中对索引列做了计算、函数、类型转换操作,这些情况下都会造成索引失效;
  3. 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效;
  4. 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效;
    通过执行计划显示的数据判断查询语句是否使用了索引。type数据扫描类型:
    要尽量避免全表扫描(All)和全索引扫描(index);

InnoDB如何存储数据的

为什么MySQL采用B+树作为索引

  1. MySQL 是会将数据持久化在硬盘,而存储功能是由 MySQL 存储引擎实现的,所以讨论 MySQL 使用哪种数据结构作为索引,实际上是在讨论存储引使用哪种数据结构作为索引,InnoDB 是 MySQL 默认的存储引擎,它就是采用了 B+ 树作为索引的数据结构。

  2. 要设计一个 MySQL 的索引数据结构,不仅仅考虑数据结构增删改的时间复杂度,更重要的是要考虑磁盘 I/0 的操作次数。因为索引和记录都是存放在硬盘,硬盘是一个非常慢的存储设备,我们在查询数据的时候,最好能在尽可能少的磁盘 I/0 的操作次数内完成。

  3. 二分查找树虽然是一个天然的二分结构,能很好的利用二分查找快速定位数据,但是它存在一种极端的情况,每当插入的元素都是树内最大的元素,就会导致二分查找树退化成一个链表,此时查询复杂度就会从 O(logn)降低为 O(n)。

  4. 为了解决二分查找树退化成链表的问题,就出现了自平衡二叉树,保证了查询操作的时间复杂度就会一直维持在 O(logn) 。但是它本质上还是一个二叉树,每个节点只能有 2 个子节点,随着元素的增多,树的高度会越来越高。

  5. 而树的高度决定于磁盘 I/O 操作的次数,因为树是存储在磁盘中的,访问每个节点,都对应一次磁盘 I/O 操作,也就是说树的高度就等于每次查询数据时磁盘 IO 操作的次数,所以树的高度越高,就会影响查询性能。

重点来了重点来了!!!
B 树和 B+ 都是通过多叉树的方式,会将树的高度变矮,所以这两个数据结构非常适合检索存于磁盘中的数据。
但是 MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构,原因有:

  1. B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少
  2. B+ 树有大量的冗余节点(所有非叶子节点都是冗余索引),这些冗余索引让 B+ 树在插入、删除的效率都更高,比如删除根节点的时候,不会像 B 树那样会发生复杂的树的变化;
  3. B+ 树叶子节点之间用链表连接了起来,有利于范围查询,而 B 树要实现范围查询,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。

大体来说就是: 以很小的时间代价查找到、插入和删除效率高、适用于范围查询

MySQL单表不要超过2000w行,靠谱吗?

  1. MySQL 的表数据是以页的形式存放的,页在磁盘中不一定是连续的;
  2. 页的空间是 16K, 并不是所有的空间都是用来存放数据的,会有一些固定的信息,如页头,页尾,页码,校验码等等;
  3. 在 B+ 树中,叶子节点和非叶子节点的数据结构是一样的,区别在于叶子节点存放的是实际的行数据,而非叶子节点存放的是主键和页号;
  4. 索引结构不会影响单表最大行数,2000W 也只是推荐值,超过了这个值可能会导致 B + 树层级更高,影响查询性能;

六种索引失效场景

  1. 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx%这两种方式都会造成索引失效;
  2. 当我们在查询条件中对索引列使用函数,就会导致索引失效。
  3. 当我们在查询条件中对索引列进行表达式计算,也是无法走索引的。
  4. MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。如果字符串是索引列,而条件语句中的输入参数是数字的话,那么索引列会发生隐式类型转换,由于隐式类型转换是通过 CAST 函数实现的,等同于对索引列使用了函数,所以就会导致索引失效。
  5. 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。
  6. 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。

MySQL 使用 like “%x“,索引一定会失效吗?

  1. 使用左模糊匹配 (like “%xx”) 并不一定会走全表扫描,关键还看数据表中的字段。若数据库表中的字段只有主键+二级索引,那么即使使用了左模糊匹配,也不会走全表扫描(type=all),而是走全扫描二级索引树(type=index);
  2. 联合索引要遵循最左匹配才能走索引,但是如果数据库表中的字段都是索引的话,即使查询过程中没有遵循最左匹配原则,也是走全扫描二级索引树(type=index);

count(*) 和 count(1) 有什么区别?哪个性能最好?

在这里插入图片描述
count()是一个聚合函数,函数的参数不仅可以是字段名,也可以是其他任意表达式,该函数作用是统计符合查询条件的记录中,函数指定的参数不为 NULL 的记录有多少个。
count(*) 其实等于 count(0),也就是说,当你使用 count() 时,MySQL 会将 * 参数转化为参数 0 来处理。所以,count() 执行过程跟 count(1) 执行过程基本一样的,性能没有什么差异。

  1. count(1)、count(*)、count(主键字段)在执行时,若表里存在二级索引,优化器就会选择二级索引进行扫描;
  2. 若要执行 count(1)、 count(*)、 count(主键字段) 时,尽量在数据表上建立二级索引,这样优化器会自动采用 key_len最小的二级索引进行扫描,相比于扫描主键索引效率会高一些;
  3. 不要使用 count(字段) 来统计记录个数,因为它的效率是最差的,会采用全表扫描的方式来统计。如果你非要统计表中该字段不为 NULL 的记录个数,建议给这个字段建立一个二级索引。

如何优化 count(*)?

  1. 近似值;
  2. 额外表保存计数值;

事务

事务实在MySQL引擎层实现的,其四大特性是?

原子性:一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态;
一致性:是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。
隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的;
持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失;

脏读:如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象;
不可重复读:在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象;
幻读:在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象;

隔离级别

以下主要是隔离性
多个事务并发执行的时候,会引发脏读、不可重复读、幻读这些问题,为避免这些问题,SQL提出四种隔离级别,分别是读未提交、读已提交、可重复读、串行化,从左往右隔离级别顺序递增,隔离级别越高性能越差,InnoDB引擎默认隔离级别是可重复读

  1. 解决脏读,隔离级别升级到读已提交以上隔离级别,解决不可重复读,隔离界别升级到可重复读以上隔离级别;
  2. 对于幻读,不建议将隔离级别升级为串行化,因为会导致数据库并发时性能很差。InnoDB引擎默认隔离级别虽然是可重复读,但他很大程度上避免幻读现象(并不是完全解决),具体解决方案有下面两种:
    2.1 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题;
    2.2 针对当前读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
  3. 对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建

Read View 的时机不同:
3.1 读提交隔离级别是在每个 select 都会生成一个新的 Read View,也意味着事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务;
3.2 可重复读隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View,这样就保证了在事务期间读到的数据都是事务启动前的记录;

上面两个隔离级别实现是通过「事务的 Read View 里的字段」和「记录中的两个隐藏列」的比对,来控制并发事务访问同一个记录时的行为,这就叫 MVCC(多版本并发控制)

在可重复读隔离级别中,普通的 select 语句就是基于 MVCC 实现的快照读,也就是不会加锁的。而 select … for update 语句就不是快照读了,而是当前读了,也就是每次读都是拿到最新版本的数据,但是它会对读到的记录加上 next-key lock 锁。

MVCC

mysql可重复读隔离级别没有彻底解决幻读

举例了两个发生幻读场景的例子:

  1. 对于快照读, MVCC 并不能完全避免幻读现象。因为当事务 A 更新了一条事务 B 插入的记录,那么事务 A 前后两次查询的记录条目就不一样了,所以就发生幻读;
  2. 对于当前读,如果事务开启后,并没有执行当前读,而是先快照读,然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读;
    综上,MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select … for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录。

MySQL有哪些锁?

全局锁

全局锁主要应用于做全库逻辑备份, 这样在备份数据库期间,不会因为数据或表结构的更新而出现备份文件的数据与预期不一样。加上全局锁,意味着整个数据库都是只读状态。若数据库里有很多数据,备份就会花费很多的时间,关键是备份期间,业务只能读数据,而不能更新数据,这样会造成业务停滞。若数据库引擎支持的事务支持可重复读的隔离级别

表级锁

1.表锁
表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。当会话退出后,也会释放所有表锁。不过尽量避免在使用 InnoDB 引擎的表使用表锁,因为表锁的颗粒度太大,会影响并发性能,InnoDB 牛逼的地方在于实现了颗粒度更细的行级锁。

  1. 元数据锁(MDL)
    当我们对数据库表进行操作时,会自动给这个表加上 MDL:对一张表进行 CRUD 操作时,加的是 MDL 读锁
    对一张表做结构变更操作的时候,加的是 MDL 写锁;MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。

  2. 意向锁
    在使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前,需要先在表级别加上一个「意向共享锁」;
    在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」;
    也就是,当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。
    意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables … read)和独占表锁(lock tables … write)发生冲突。意向锁的目的是为了快速判断表里是否有记录被加锁。

  3. AUTO-INC锁
    AUTO-INC 锁是特殊的表锁机制,锁不是再一个事务提交后才释放,而是再执行完插入语句后就会立即释放。
    在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为被 AUTO_INCREMENT 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉

行级锁
  1. Record Lock ( 记录锁 )
    锁住的是一条记录。而且记录锁是有 S 锁和 X 锁(排他锁和共享锁)之分的 ( SS兼容 SX不兼容 XX不兼容 )

  2. Gap Lock( 间隙锁 )
    只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但并无区别,间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的;

  3. Next-Key Lock ( 临建锁 )
    Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的;

  4. 插入意向锁
    一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。若有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态;
    如果说间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点。因而从这个角度来说,插入意向锁确实是一种特殊的间隙锁。

MySQL怎么加锁的?(行级锁加锁规则)

唯一索引等值查询
  1. 当查询的记录是「存在」的,在索引树上定位到这一条记录后,将该记录的索引中的 next-key lock 会退化成「记录锁」;
  2. 查询的记录是「不存在」的,在索引树找到第一条大于该查询记录的记录后,将该记录的索引中的 next-key lock 会退化成「间隙锁」;
非唯一索引等值查询
  1. 当查询的记录「存在」时,由于不是唯一索引,所以肯定存在索引值相同的记录,于是非唯一索引等值查询的过程是一个扫描的过程,直到扫描到第一个不符合条件的二级索引记录就停止扫描,然后在扫描的过程中,对扫描到的二级索引记录加的是 next-key 锁,而对于第一个不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。同时,在符合查询条件的记录的主键索引上加记录锁;
  2. 当查询的记录「不存在」时,扫描到第一条不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。因为不存在满足查询条件的记录,所以不会对主键索引加锁。
上述两者区别

非唯一索引和主键索引范围查询加锁规则不同点在于某些情况下,唯一索引的next-key lock退化为间隙锁或记录锁,而非唯一索引的next-key lock不会退化。

注意

在线上在执行 update、delete、select … for update 等具有加锁性质的语句,一定要检查语句是否走了索引,如果是全表扫描的话,会对每一个索引加 next-key 锁,相当于把整个表锁住了,这是挺严重的问题。

Mysql死锁怎么办?

死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。在数据库层面,有两种策略通过打破循环等待条件解除死锁:

  1. 设置事务等待锁的超时时间:当一个事务的等待时间超过该值后,就对这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了;
  2. 开启主动死锁检测:主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行;

Buffer Pool 缓冲池

Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。Buffer Pool 以页为单位缓冲数据,InnoDB通过三种链表来管理缓存页,Free List (空闲页链表)管理空闲页;Flush List (脏页链表)管理脏页;
LRU List,管理脏页+干净页,将最近且经常查询的数据缓存在其中,而不常查询的数据就淘汰出去:
InnoDB 对 LRU 做了一些优化,我们熟悉的 LRU 算法通常是将最近查询的数据放到 LRU 链表的头部,而 InnoDB 做 2 点优化:

  1. 将 LRU 链表 分为young 和 old 两个区域,加入缓冲池的页,优先插入 old 区域;页被访问时,才进入 young 区域,目的是为了解决预读失效的问题;
  2. 当**「页被访问」且「 old 区域停留时间超过 innodb_old_blocks_time 阈值(默认为1秒)」**时,才会将页插入到 young 区域,否则还是插入到 old 区域,目的是为了解决批量数据访问,大量热数据淘汰的问题。

日志

undo log(回滚日志):是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC;
redo log(重做日志):是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复;
binlog (归档日志):是 Server 层生成的日志,主要用于数据备份和主从复制;

2023年某公司某部门面经

1. c++内存管理

c++内存分区:从高到底分别为:
栈区:函数内局部变量的存储单元可在栈上创建,函数执行结束后,这些存储单元被自动释放;
堆:由new分配的内存块,手动去释放内存,一个new对应一个delete
自由存储区:堆是操作系统维护的一块内存,那自由存储区就是new和delete动态分配和释放对象的抽象概念;
全局/静态存储区:全局变量和静态变量被分配到同一块内存中
常量存储区:存放的常量,不可修改
代码区:存放函数体的二进制代码

2. 什么时候出现内存泄漏

内存泄漏指的是堆内存泄露,动态分配的内存没有被释放。1. 使用new malloc分配内存后,没有使用delete和free进行内存释放;2. 对已释放的内存进行访问或操作,释放已经释放的内存;3 循环引用,两个或多个对象之间相互持有对方的引用,导致无法正确释放他们之间的内存;

避免及解决的方法:计数法,用new时+1,delete时-1; 一定要将基类的析构函数声明为虚函数;保证new和malloc成对出现;
智能指针啥的 也能解决问题好像

3. 类解决什么问题

类是一种面向对象编程的主要概念,提供一种封装数据以及相关行为的方式,类可以将数据和其操作相关的函数封装在一起,类可将对象的属性和行为组织成一个逻辑单元,类可将实现细节隐藏在类的内部,只提供必要的公共接口,类可通过继承关系建立继承链,避免重复编写相同代码。通过多态,派生类的对象可表现出与基类不同的行为,实现更灵活的代码结构和动态绑定能力。

4. 怎么实现多态

同一事物表现出不同事物的能力,向不同对象发送同一消息,不同对象在接受时会产生不同的行为 ( 重载实现编译时多态,虚函数实现运行时多态 )
函数重载:同一个类中定义函数名相同,但参数列表不同的函数;
函数重写:也称为虚函数重写,指的是在派生类中重新定义(覆盖)基类中已有的虚函数;

虚函数:用父类指针指向子类的对象,然后通过父类指针调用实际子类的成员函数,如果子类重写了该成员函数就会调用子类的成员函数,没有声明重写调用基类的成员函数。
虚函数工作原理:主要通过虚表指针和虚函数表实现,调用对象的函数时,对象内存中的虚表指针找到一个虚函数表,虚函数表内部是一个函数指针数组,记录的时虚函数的首地址,然后调用对象拥有的函数。

5. vector和链表区别,删除链表元素怎么写

vector底层:数组,内存连续; 链表底层:双向链表,内存不用连续
vector: 顺序内存,支持随机访问,但插入删除性能差; 链表:随机访问性能差,但插入删除性能好;
vector:一次性分配好内存,不够时才进行翻倍扩容; list:每次插入新节点都会进行内存申请;

删除链表元素:
找到待删除元素的前一个结点,将这个节点的next指针指向待删除结点的下一个节点,跳过待删除结点。释放待删除结点的内存空间;

6. 红黑树

一种自平衡的二叉搜索树,在插入和删除节点时通过特定的旋转和涂色操作来维持平衡 用于c++中map和set等
特性:
节点是红色或者黑色、根节点是黑色、所有叶子节点(NULL节点)都是黑色、若一个节点是红色,那两个子节点都是黑色的、从任意一个节点到叶子节点,经过黑色节点的数量都相同,即黑高度相同;

7.遍历树

按照一定顺序访问书的每个节点,常用树的遍历方式包括 深度优先遍历、广度优先遍历
深度优先: 前序遍历、中序遍历、后序遍历
广度优先:从根节点开始,逐层访问树上的每个节点,先访问根节点,然后从左到右一次访问同一层每个节点

8.快排

随机选择一个基准元素,通过一趟排序将要排序的数据分割成独立的两部分,一部分全小于基准元素,一部分全大于基准元素,然后递归对这两部分数据进行快速排序


#include<iostream>
using namespace std;
int a[8] = { 12,43,1,90,0,65,4,56 };
int n = 8;

void qsort(int left, int right) {
	int i = left, j = right;
	int mid = a[(left + right) / 2];
	do{
		while (a[i] < mid) i++;
		while (a[j] > mid) j--;
		if (i <= j) {
			swap(a[i], a[j]);
			i++, j--;  // 交换后还得继续遍历两边有没有其他元素符合条件的
		}
	} while (i <= j);

	if (j > left) qsort(left, j);

	if (i < right) qsort(i, right);
}
int main() {
	qsort(0, n - 1);
	for (int i = 0; i < n; i++) {
		cout << a[i] << endl;
	}
	return 0;
}

9.了解那些网络协议

TCP/IP协议:用户互联网通信的基础协议,包括IP ICMP(在IP网络上传递错误报告、控制消息的协议) UDP等
HTTP:用于客户端和服务器之间传输超文本;
HTTPS:基于HTTPS的安全通信协议,使用SSL/TLS来加密和保护数据传输;
FTP:在客户端和服务器之间传输文件;
SMTP:在邮件传输代理之间传递电子邮件;
DNS:域名解析;

10.拥塞控制怎么实现的

慢启动:连接刚开始,发送方会逐渐增加发送窗口大小,从而以指数增长速度增加发送数据量;
拥塞避免:慢启动后,发送方进入拥塞避免阶段,这个阶段发送方逐渐增加发送窗口的大小,但增加速率慢,避免过快导致网络拥塞;
超时重传:发送方超时时间内未收到确认,认为数据包丢失,并重传这些数据包;
快速重传和快恢复:快速恢复是拥塞发生后慢启动的优化,其首要目的仍然是降低 cwnd 来减缓拥塞;
拥塞窗口调整:发送方根据网络的拥塞程度动态调整发送窗口大小;

11.滑动窗口,窗口大小怎么确定

TCP头里有一个字段叫window,也就是窗口大小。这个字段是接收端告诉发送端自己还有多少缓冲区可以接受数据,于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来;所以通常窗口的大小是由接收方的窗口大小来决定的。

13.TCP 、UDP 应用场景

TCP是面向连接的字节流,低效率,全双工的通信,传输速度慢,而UDP是不可靠的,无连接的面向报文的高效的通信协议,不仅支持点对点,还支持一对多,多对一,多对多。TCP由流量控制,拥塞控制等措施保证可靠性,而UDP没有这些操作,所以此协议是尽最大可能交付,不保证可靠性;
应用场景:
TCP要求通信数据可靠场景(网页浏览、文件传输、邮件传输、远程登录、数据库操作)
UDP要求通信速度高场景(视频直播、实时游戏等)

14.HTTPS怎么实现的

HTTPS在HTTP于TCP层之间加入了SSL/TLS协议,即可保证安全性;
HTTPS通过混合加密方式实现信息的机密性,解决了窃听的风险;摘要算法实现了完整性,可以为数据生成独一无二的指纹,解决了数据被更改的可能性,将服务器公钥放入到数字证书中,解决了被冒充的风险;
SSL/TLS协议基本流程:
1)客户端向服务器索要并验证服务器的公钥;
2)双方协商生产会话密钥
3)双方采用会话密钥进行加密通信

https实现基础步骤:
客户端发起https连接请求、服务器准备证书、客户端验证证书、握手过程、数据传输;

15.SSL握手

TLS握手阶段涉及四次通信,使用不同的密钥交换算法握手流程也不一样,常用密钥交换算法有两种:RSA算法、ECDHE算法;
TLS协议建立详细流程:
1)客户端向服务器发起加密通信请求 ( clienthello请求 ),客户端主要向服务器发送 (1)客户端支持的TLS版本 (2)客户端生产的随机数(后面用于生成 会话密钥 的条件之一)(3)客户端支持的密码套件 ( RSA加密算法);

2)服务器收到客户端请求后,向客户端发出响应(severhello),服务端回应的内容有(1)确认TLS协议版本,若浏览器不支持,就关闭加密通信。(2)服务器生产的随机数(后面用于生产会话密钥条件之一)。(3)确认的密码套件列表(RSA加密算法)。(4)服务器的数字证书

3)客户端收到服务器的回应后,先通过浏览器或者操作系统中的CA公钥,确认服务器的数字证书的真实性。没问题,客户端就从数字证书中取出服务器公钥,使用它加密报文,并给服务端发送(1)一个随机数,该随机数被服务器公钥加密通信(2)加密算法改变通知,表示后面的信息都用会话密钥加密通信(3)客户端握手结束通知。

4)服务器收到客户端的第三个随机数后,通过协商的加密算法计算出本次通信的 会话密钥,向客户端发送(1)加密通信算法该表通知,随后用会话密钥加密通信 (2)服务器握手结束通知。

CA证书签发过程

  1. 首先CA把持有者公钥、用途、颁发者、有效时间等信息打成一个包,然后对这些信息进行 Hash 计算,得到一个 Hash 值;
  2. 然后 CA 会使用自己的私钥将该 Hash 值加密,生成 Certificate Signature,也就是 CA 对证书做了签名;
  3. 最后将 Certificate Signature 添加在文件证书上,形成数字证书;

客户端 校验 服务端的 数字证书 过程

  1. 首先客户端会使用同样的 Hash 算法获取该证书的 Hash 值 H1;
  2. 通常浏览器和操作系统中集成了 CA 的公钥信息,浏览器收到证书后可以使用 CA 的公钥解密 Certificate Signature 内容,得到一个 Hash 值 H2 ;
  3. 最后比较 H1 和 H2,如果值相同,则为可信赖的证书,否则则认为证书不可信;

16.进程和线程区别

进程的创建、销毁、上下文切换开销都比线程 大。进程的通信与同步方式有 管道、消息队列、共享内存等,而线程因为共享相同的内存空间,可直接访问共享数据,所以通信更加方便。一个进程的崩溃不会影响其他进程稳定性,但线程因为共享内存空间,所以一个线程错误会影响整个进程的稳定性;

17.进程间通信方式

有名管道/无名管道:任意关系/父子关系进程间通信;
信号量:一个计数器,可用之多个线程对共享资源的访问;
信号:用于通知接收进程某个时间已发生;
消息队列:消息的链表,存放在内核中;
共享内存:映射一段能被其他进程访问的内存;
socket套接字:用于不同计算机进程通信,支持TCP/IP网路通信的基本操作单元;

18.共享内存在什么时候使用,注意的地方

在多进程通信时候使用;
数据共享时候使用:多进程可能需要共享相同的数据;

注意: 同步机制、内存管理、数据保护、安全性;

19.程序崩溃怎么定位

日志和错误信息、调试器、内存调试工具(Valgrind)、核心转储文件;

20.线程同步

互斥锁、读写锁、条件变量、信号量、原子操作

21.mysql查询,符合条件的个数怎么查效率高

使用索引、COUNT()、避免不需要的列和排序;

22.写题 回文子串的个数

// 回文子串
int countsub(string s) {
	int n = s.size();
	vector<vector<bool>>dp(n, vector<bool>(n, false));
	int result = 0;
	for (int i = n - 1; i >= 0; i--) {
		for (int j = i; j < n; j++) {
			if (s[i] == s[j]) {
				if (j - i <= 1) {
					result++;
					dp[i][j] = true;
				}
				else if (dp[i + 1][j - 1]) {
					result++;
					dp[i][j] = true;
				}
			}
		}
	}

	return result;
}

// 双指针法
int extend(const string& s, int i, int j, int n) {
	int res = 0;
	while (i >= 0 && j < n && s[i] == s[j]) {
		i--;
		j++;
		res++;
	}
	return res;
}
int countsub1(string s) {
	int result = 0;
	for (int i = 0; i < s.size(); i++) {
		result += extend(s, i, i, s.size());
		result += extend(s, i, i + 1, s.size());
	}
	return result;
}

int main() {
	string s = "caabaa";
	cout << "动规:";
	cout << countsub(s) << endl;
	cout << "双指针:";
	cout << countsub1(s) << endl;
}

23.了解下项目,课外学习的知识,解决了什么问题,怎么解决的

webserver开启讲述,课外学了opencv解决图像分帧问题

24.笔试复盘

25.手撕 很像leetcode原题,笔试第2道相似

26.学没学过linux操作系统

了解过一下基础的命令 ls:列出当前目录下的文件和文件夹; cd:更改当前工作目录;
mkdir:创建新的目录 rm:删除文件 cp:复制文件或目录 mv : 移动或重命名文件或目录
cat: 显示文件的内容 touch:创建新文件或更新文件的访问和修改时间
grep: 在文件中搜索指定的模式 chmod:修改文件或目录的权限 ps aux 显示当前正在运行的进程状态
ifconfig : 显示和配置网络接口信息 ping : 测试与指定主机连接

27.堆和栈

栈内存的分配和释放时自动的,堆是由程序员手动进行分配和释放的;
栈大小通常较小,由编译器预先分配的固定空间,而堆的大小通常较大,受限于操作系统的虚拟内存大小;
栈上分配和释放速度比堆上块;
栈上变量具有局部作用,生命周期随函数的执行自动开始和结束,堆上分配的内存可以在多个函数之间共享,程序员释放前一直存在;
栈(直接访问)上内存访问速度比堆(通过指针访问)块

28.数组和链表区别

1)存储方式:数组在内存中按照顺序存储元素结构、内存中连续分布,通过索引直接访问和修改元素。链表使用指针将不同节点链接在一起,节点可在内存中离散分布,通过指针连接进行访问;
2)大小调整:数组大小创建时确定且固定不变,列表的大小可以动态调整,在需要时可以添加或删除节点,不需要重新分配和复制整个数据。
3)插入和删除:数组的元素在内存中是连续存储的,插入和删除操作可能涉及到大量的数据移动;对于列表,由于节点之间通过指针链接,插入和删除节点只需要修改指针的指向,具有较低的时间复杂度。
4)随机访问性能:数组的连续存储方式,可以通过索引直接访问元素,具有O(1)的随机访问性能。而对于列表,需要从头节点开始顺序访问链表,具有O(n)的访问性能;
5)内存分配:数组需要一段连续的内存空间来存储所有元素,因此,在创建数组时需要首先分配足够的内存。而链表可以根据需要动态增长,每个节点可以独立分配内存;

29.数组和列表区别

  1. 大小
    数组在创建时必须指定其大小,而列表的大小可以动态增加或减少。这是因为列表在内存中是动态分配的,而数组则是在创建时分配固定的内存空间。因此,如果程序员需要存储可变数量的元素,应该使用列表而不是数组。
  2. 内存管理
    列表和数组在内存管理方面也有很大的不同。数组在创建时需要分配固定大小的内存空间,而这个空间在整个程序的生命周期中都会被保留。这意味着即使数组中只有几个元素,也会占用同样大小的内存空间。而列表则可以根据需要动态分配和释放内存,这使得它们更加灵活和有效。
  3. 访问元素
    在数组中,元素可以通过索引值进行访问。由于数组中的元素都是相同的类型,因此可以使用简单的算术运算来计算要访问的元素的地址。这使得数组元素的访问速度非常快。而在列表中,元素也是通过索引值进行访问,但是由于列表中的元素可以是任何类型,因此访问元素的速度可能会慢一些。
  4. 功能
    列表通常具有更多的功能,例如添加、删除、排序和搜索元素。这些操作在数组中也是可行的,但通常需要更多的编程工作。此外,列表还可以包含其他列表或数组,从而为程序员提供更多的灵活性和控制力。

二面

1. 给了个字符串数组,一个字符串字典,如果字典中出现了该字符,就表示能学会,一个字符只能用一次,计算能够学会的字符串长度和,eg:[“cat",“arr”,“hat”,"re] ,“cattahg”,只能学会cat和hat

2.什么场景用多态

  1. 简化代码的逻辑,多态可将一组具有相同接口或继承关系的对象视为共同类型,从而简化代码逻辑,如在一个汽车类中,各种具体的汽车可以继承同一个基类,实现各自的启动、加速、停止等方法。

  2. 多态可以提高代码的可读性、可维护性和可扩展性,通过将不同的对象视为同一类型,实现统一的接口和行为。多态在面向对象编程中的应用非常广泛,特别是在需要处理不同类型的对象集合时,能够带来很多优势

  3. 多态的最典型应用是在继承关系中,当多个子类继承自同一个父类时,可以使用多态来实现统一的接口,提高代码的可读性和可维护性。

  4. 在需要处理多种类型对象的情况下,可以使用多态来统一对这些对象进行处理。例如,可以定义一个通用的函数或方法,接受基类(父类)指针或引用作为参数,然后根据对象的实际类型执行不同的操作。

  5. 设计模式中,多态也被广泛应用。例如,策略模式和工厂模式等都利用了多态的特性来实现不同算法或对象的动态切换。

  6. 当需要扩展程序功能时,通过添加新的子类并重写父类的方法,可以使用多态来保持原有代码的兼容性,而无需修改现有代码。

3.问项目

webserver走起

4.快排

选择一个基准元素,然后让左边小于基准元素,右边大于基准元素,然后递归左右两边直到排序完成;

// 快排走一波
int a[8] = { 23,12,67,34,0,77,12,0 };
int n = 8;

void qsort(int left,int right) {
	int i = left, j = right;
	int mid = a[(left + right) / 2];
	do {
		while (a[i] < mid) i++;
		while (a[j] > mid) j--;
		if (i<=j) { //这一步是必须要有滴
			swap(a[i], a[j]);
			i++, j--;
		}
	} while (i <= j);
	if (left < j)qsort(left,j);  //保证左边有序
	if (i < right) qsort(i, right); //保证右边有序
}

int main() {
	qsort(0, n - 1);
	for (int i = 0; i < n; i++) {
		cout << a[i] << endl;
	}
	return 0;
}

5.找到链表中间的节点

#include <iostream>
struct ListNode {
    int val;
    ListNode* next;

    ListNode(int x) : val(x), next(nullptr) {}
};
// 快慢指针直接拿下
int findMiddleNode(ListNode* head) {
    ListNode* slow = head;
    ListNode* fast = head;

    while (fast != nullptr && fast->next != nullptr) {
        slow = slow->next;
        fast = fast->next->next;
    }

    return slow->val;
}

int main() {
    // 创建一个链表: 1 -> 2 -> 3 -> 4 -> 5 -> nullptr
    ListNode* head = new ListNode(1);
    head->next = new ListNode(2);
    head->next->next = new ListNode(3);
    head->next->next->next = new ListNode(4);
    head->next->next->next->next = new ListNode(5);

    int middleNode = findMiddleNode(head);
    std::cout << "Middle Node Value: " << middleNode << std::endl;

    return 0;
}

6.linux命令

7.项目中做了什么优化

8.怎么调试程序

利用调试工具、打印调试信息、缩小问题范围、搜索互联网资源和文档

9.遇到程序死机的问题,什么情况,怎么解决

死循环、内存泄露、资源竞争、无限递归、外部原因(硬件故障、操作系统错误等)
解决: 使用调试工具、添加日志输出、使用断点、隔离问题

10.多线程需要注意什么

线程安全性、合适的通信机制(线程间通信)、死锁与饥饿(设置合适的资源分配策略)、资源回收与内存管理、并发性(使用合适的同步机制避免出现意外的并发问题)

11.什么情况下会死锁

互斥、占有且等待、不可强占用、循环等待;只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立

预防和解决死锁的方法包括:

避免循环等待:通过规定资源的获取顺序,破坏循环等待条件,避免死锁的发生。
使用资源分级:按照固定的层次顺序获取资源,在申请资源时遵循固定的顺序
引入超时机制:在申请资源时设置超时参数,一段时间内未获取到资源则放弃申请,避免长时间等待。
资源预分配:根据需求预先分配所需的资源,避免在运行时竞争资源,降低死锁风险。
死锁检测和恢复:周期性地检测是否出现死锁,如果发现死锁,则采取相应的恢复策略,如终止某个进程来解除死锁。

12.实习遇到?比较难得问题,怎么解决的

表情识别项目走起

13.科研项目介绍

表情识别走起

14.平衡二叉树,什么时候用平衡二叉树

AVL树,是一种二叉树的特殊形式,其左右子树的高度差不超过1;

  1. 频繁的查找、插入和删除操作:平衡二叉树具有较快的查找、插入和删除操作的平均时间复杂度,即O(logn)。对于需要频繁进行这些操作的应用,平衡二叉树可以提供更高效的性能。
  2. 对数据的动态性要求较高:当数据集合频繁发生变化时,如插入和删除操作频繁的场景,平衡二叉树可以通过自平衡的特性,保持树的结构平衡,避免树的高度过大,保持操作的效率。
  3. 对数据的有序性要求:平衡二叉树在存储有序数据时具有优势。由于平衡二叉树的特性,可以保持左子树小于等于当前节点,右子树大于等于当前节点,从而在查找、范围查询等操作中提供高效的性能。

平衡二叉树适用于较为平衡的数据分布情况。在数据分布极端不平衡的情况下,例如插入数据有序或逆序排列,平衡二叉树可能会失去平衡,造成性能下降

15.锁,读写锁,什么时候用读写锁

读写锁(Read-Write Lock)是一种在多线程环境中使用的同步机制,它允许多个线程同时读数据,而对写操作进行互斥。

  1. 高读取频率:当存在大量读取操作,并且读取操作不会修改共享数据时,可以使用读写锁。读写锁允许多个线程同时读取数据,提高了并发性和吞吐量。

  2. 低写入频率:如果写入操作相对较少,而读取操作较频繁,读写锁可以提供更好的性能。只有在没有写入操作时,才会允许多个线程同时读取数据,减少了互斥的开销。

  3. 读操作耗时较长:如果读取操作需要花费较长的时间,为了避免写操作长时间被阻塞,可以使用读写锁。读写锁允许多个线程同时读取数据,提高了并发性和响应性。

16.往地址栏输入一个地址,会发生什么

DNS解析:浏览器会提取出输入的地址,并发送域名(例如"www.example.com")到DNS服务器进行解析。DNS服务器将域名解析为对应的IP地址(例如"192.0.2.123"),以便浏览器能够向服务器发送请求。

建立连接:浏览器通过TCP/IP协议与服务器建立连接。这个过程中,浏览器会将请求发送给服务器的IP地址,建立起客户端与服务器之间的通信通道。

发送HTTP请求:浏览器发送HTTP请求给服务器。这个请求中包含了请求的方法(如GET、POST)、路径(URL)、请求头(headers)等信息。

服务器处理请求:服务器接收到浏览器发送的请求后,根据请求进行相应的处理。如果请求的是静态资源(如HTML文件、图片、CSS文件等),服务器会直接将资源返回给浏览器;如果请求的是动态资源(如通过脚本语言生成的内容),服务器会执行相应的处理逻辑,生成内容后再发送给浏览器。

接收响应:浏览器接收到服务器返回的响应数据,其中包括HTTP状态码、响应头(headers)、响应内容等信息。

渲染页面:浏览器根据接收到的响应数据,将其解析并渲染为用户可见的页面。这个过程中,浏览器会按照规则解析HTML、CSS和JavaScript,加载显示页面的内容。

断开连接:一旦页面渲染完成,浏览器与服务器之间的连接将会断开。如果在同一网站上浏览其他页面,浏览器可能会重用已经建立的TCP连接来提高性能。

17.ip寻址的过程

  1. 确定目标IP地址:在进行IP寻址之前,需要明确寻址的目标IP地址,该地址标识了要进行通信的目标主机或网络。

  2. 确定子网掩码:在IPv4网络中,每个IP地址通常与一个子网掩码(Subnet Mask)配对使用,用于确定主机所属的网络范围。子网掩码与目标IP地址进行按位“与”运算,可以获得主机所在的网络地址。

  3. 路由查找:主机根据自身配置的路由表进行路由查找,确定到达目标IP地址所需经过的下一跳路由器。路由表中包含了网络前缀和对应的下一跳路由器的信息。

  4. ARP解析:如果下一跳路由器的MAC地址未知,主机需要进行ARP(Address Resolution Protocol)解析,以获取目标IP地址对应的MAC地址。主机会广播一个ARP请求,请求目标IP地址的MAC地址,并等待目标主机的响应。

  5. 数据传输:一旦获得了目标IP地址的MAC地址,主机将网络数据包封装成数据帧,并通过物理介质(如以太网)向目标主机发送数据包。数据包在网络链路中依照路由表中的指引进行传输,经过多个路由器直至到达目标网络。

  6. 目标主机接收与处理:目标主机接收到数据包后,根据目标IP地址和端口,进行数据处理。其中,目标IP地址与主机的IP地址进行比较,确定该数据包是否为自己应该接收的。

18.熟悉的数据结构,觉得自己的编程能力

19.手撕,输入一个字符串,给一个计算结果,简单题,四则运算

力扣224基本计算器

给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。
注意:不允许使用任何将字符串作为数学表达式计算的内置函数

示例 1:
输入:s = “1 + 1”
输出:2

示例 2:
输入:s = " 2-1 + 2 "
输出:3

示例 3:
输入:s = “(1+(4+5+2)-3)+(6+8)”
输出:23

#include<iostream>
#include<stack>

int caclue(string s) {
	stack<int>st;
	int n = s.size(), sign = 1, sum = 0,i=0,ret=0;
	st.push(1);
	while (i < n) {
		if (s[i] == ' ') {
			i++;
		}
		else if (s[i] == '+') {
			sign = st.top();
			i++;
		}
		else if (s[i] == '-') {
			sign = -st.top();
			i++;
		}
		else if (s[i] == '(') {
			st.push(sign);
			i++;
		}
		else if (s[i] == ')') {
			st.pop();
			i++;
		}
		else {
			long num = 0;
			while (i < n && s[i] >= '0' && s[i] <= '9') {
				num += num * 10 + s[i] - '0';
				i++;
			}
			ret += sign * num;
		}
	}
	return ret;
}

int main() {
	string s = "(1+(4+5+2)-3)+(6+8)";
	cout << caclue(s) << endl;
	return 0;
}

力扣227 基本计算器II

给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。
整数除法仅保留整数部分。你可以假设给定的表达式总是有效的。注意:不允许使用任何将字符串作为数学表达式计算的内置函数。

示例 1:
输入:s = “3+2*2”
输出:7

示例 2:
输入:s = " 3/2 "
输出:1

示例 3:
输入:s = " 3+5 / 2 "
输出:5

#include<stack>
#include<numeric>

int caclue(string& s) {
	int n = s.length();
	char sign = '+';
	vector<int>st;
	long num = 0;
	for (int i = 0; i < n; i++) {
		if (isdigit(s[i])) num = num * 10 + s[i] - '0';
		if (!isdigit(s[i]) && s[i] != ' ' || i == n - 1) {
		// if (s[i] == '+' || s[i]=='-' || s[i]=='*' || s[i]=='/') {
			switch (sign) {
			case '+':
				st.push_back(num);
				break;
			case '-':
				st.push_back(-num);
				break;
			case '*':
				st.back() *= num;
				break;
			case '/':
				st.back() /= num;

			}
			num = 0;
			sign = s[i];
		}
	}
	return accumulate(st.begin(), st.end(), 0);
}

int main() {
	string s = " 2 + 3 * 5 -32 / 4 -2 +12";
	int res = caclue(s);
	cout << res << endl;
}

20.手撕复盘,觉得自己今天的发挥怎么样

21.反问

2023年9月13日笔试题

快递中转站

快递公司有一个业务要求,所有当天下发到快递中转站的快递,最迟在第二天送达用户手中。

假设已经知道接下来n天每天下发到快递中转站的快递重量。快递中转站负责人需要使用快递运输车运输给用户,每一辆运输车最大只能装k重量的快递。

每天可以出车多次,也可以不出车,也不要求运输车装满。当天下发到快递中转站的快递,最晚留到第二天就要运输走送给用户。

快递中转站负责人希望出车次数最少,完成接下来n天的快递运输。

输入:
输入第一行包含两个整数n(1<= n<=200000),k(1<=k<=100000000)
第二行包含n个整数ai,表示第i天下发到快递中转站的快递重量。

输出:
输出最少需要的出车次数。
输入:
3 2
3 2 1
输出:3

解释:
第一天的快递出车一次送走2个重量,留1个重量到第二天
第二天送走第一天留下的1个重量和当前的1个重量,留1个重量到第三天送走。

#include<iostream>
#include<vector>

using ll = long long;
int main() {
	int n, m;
	cin >> n >> m;
	vector<ll>weight(n+1);
	for (int i = 0; i < n; i++) {
		cin >> weight[i];
	}
	ll total = 0;
	for (int i = 1; i <= n; i++) {
		ll de = weight[i - 1] / m;
		if (weight[i - 1] % m) {
			de += 1;
		}
		ll res = de * m - weight[i - 1];
		total += de;
		weight[i] = max(weight[i] - res, 0LL);
	}
	cout << total << endl;
}

互通设备集

局一局域网内的设备可以相互发现,具备直连路由的两个设备可以互通。假定设备A和B互通,B和C互通,那么可以将B作为中心设备,通过多跳路由策略使设备A和C互通。这样,A、B、C三个设备就组成了一个互通设备集。其中,互通设备集包括以下几种情况:

直接互通的多个设备
通过多跳路由第略间接互通的多个设备
没有任何互通关系的单个设备现给出某一局域网内的设备总数以及具备直接互通关系的设备,请计算该局域网内的互通设备集有多少个?

输入:

第一行: 某一局域网内的设备总数M,32位有符号整数表示。1<= M<=200
第二行:具备直接互通关系的数量N,32位有符号整数表示。0<= N<200

第三行到第N+2行: 每行两个有符号32位整数,分别表示具备直接互通关系的两个设备的编号,用空格隔开。每个设备具有唯一的编号,0<设备编号< M

输出:
互通设备集的数量,32位有符号整数表示。

输入:
3
2
0 1
0 2
输出:1

解释:
编号0和1以及编号0和2的设备直接互通,编号1和2的设备可通过编号0的设备建立互通关系,互通设备集可合并为1个。

#define MAXN 40001

int fa[MAXN];
void init(int n) {
	for (int i = 0; i < n; i++) {
		fa[i] = i;
	}
}

int find(int i) {
	if (fa[i] == i) return i;
	else {
		return find(fa[i]);
	}
}

void unnion(int i, int j) {
	int i_fa = find(i);
	int j_fa = find(j);
	fa[i_fa] = j_fa;
}

int main() {
	int m, n;
	cin >> m >> n;
	init(m);
	int dev1, dev2;
	vector<vector<int>> connection(m, vector<int>(m, 0));
	for (int i = 0; i < n; i++) {
		cin >> dev1 >> dev2;
		connection[dev1][dev2] = 1;
		connection[dev2][dev1] = 1;
	}

	for (int i = 0; i < m; i++) {
		for (int j = 0; j < i; j++) {
			if (connection[i][j]) unnion(i, j);
		}
	}
	int ans = 0;
	for (int i = 0; i < n; i++) {
		if (find(i) == i) ans += 1;
	}
	cout << ans << endl;
	return 0;
}

三、三面

1.实习做了什么,获得了什么

实践技能:实习是一个将学习应用到实际工作中的机会。通过实习,您将有机会运用学到的理论知识,在真实的工作环境中进行实践,并提升相关技能。这包括专业技能,如编程、数据分析等,以及软技能,如沟通、团队合作等。
自我认知和成长:实习是一个自我认知和成长的机会。通过实践和面对挑战,您将能够更好地认识自己的优势和不足,并有机会发展和提升自己的能力。实习经历可以帮助您建立自信和适应性,为将来的职业生涯打下坚实的基础。

2.遇到的难题怎么解决的

3.对公司的了解
我觉得能进公司的人都是很优秀的人 跟优秀的人在一块共事我觉得很开心 自己也会变得优秀,公司是个很大的平台 我们无论是往哪个方向走都可以提供一个良好的平台 无论是做技术还是人力资源等等 当然我目前是想走技术路线的

核心网:华为云核心网是华为云服务的重要组成部分,它构建了强大的网络基础架构,为用户提供高性能、可靠的云计算环境。该网络架构采用了先进的技术,并结合了多种功能和服务,使华为云核心网成为行业的领先者之一。

核心网(Core Network)位于计算机网络中的网络层(Network Layer),也可以看作是TCP/IP协议栈中的网络层。网络层负责处理数据包的路由选择和转发,为不同主机之间提供可靠性和连接性。

核心网是一个高度复杂的网络系统,用于连接不同类型的网络和设备,例如运营商的网络、企业内部网络以及互联网等。它扮演着数据传输、流量调度和信令传递等重要角色,为用户提供诸如互联网接入、通信服务、数据传输等多种功能。

在TCP/IP协议中,核心网使用IP协议进行数据包的传输与路由选择。它通过路由器和交换机等设备来实现数据的转发和交换,并借助路由协议进行路由表的维护和更新。核心网的设计和运营需要考虑大规模网络的管理、安全性、性能优化等方面的问题,以满足用户对网络速度、可靠性和服务质量的要求。

华为云核心网提供了稳定的网络连接和传输功能,使得数据能够在不同的数据中心之间高效、安全地传输。它还具备强大的网络安全和隔离机制,保护用户的数据和资源免受恶意攻击和非法访问的威胁。
另外,华为云核心网基于全球骨干网络,通过网络优化和调度,实现了全球范围内的高速、低延迟的云计算连接。用户可以享受到稳定可靠的网络服务,实现多地区的业务需求。
华为云核心网还提供了丰富的功能和服务,包括虚拟专用云、弹性IP、私有网络互连和防火墙等,为用户构建和管理复杂的网络架构提供了综合解决方案。

总之,华为云核心网凭借其先进的技术、强大的网络连接和传输能力,以及多样化的功能和服务,为用户提供了出色的云计算环境。它在提供高性能、高可靠性的云服务方面发挥着重要的作用,并对用户的业务发展起到了积极的推动作用。

4.哪门课学的好,怎么学的,如果回到过去,你要怎么学

6.进入公司要学习新业务,会遇到挑战你怎么看

学习曲线:学习新业务肯定会有一个学习曲线,尤其是在进入一个陌生行业或领域时。您可能需要学习新的专业术语、业务流程和相关技能。这可能需要花费一些时间和努力,但随着经验的积累,您将逐渐适应并掌握新的业务。

挑战意味着成长:挑战是成长的机会。面对新业务的挑战,您需要思考解决问题的方法和策略。这有助于培养您的解决问题的能力、创新能力和适应能力。通过克服挑战,您将不断提升自己,并获得更多的经验和技能。

学习机会:学习新业务可以给您带来新的学习机会。您将有机会了解不同的行业和领域,并接触到新的知识和技术。这将增加您的知识广度,并为将来可能面临的机会和挑战做好准备。

请教和合作:在学习新业务的过程中,不要害怕向同事、上级或其他专业人士寻求帮助和指导。他们可能拥有更丰富的经验和知识,愿意与您分享并提供支持。积极与他人合作和交流,可以加速您的学习进程,并加深对新业务的理解。

自信和积极态度:面对挑战,保持自信和积极的态度非常重要。相信自己的能力并相信自己可以学会新的业务。与此同时,保持开放的心态,接受新的观点和方法,愿意不断学习和改进。

7.解决了什么难题最能体现出你的技术实力
我这边只能展现我解决问题的能力

8.职业规划

1年:
掌握核心编程语言和技术:在第一年,您应该专注于深入学习和掌握核心的编程语言和技术,如Java、Python、C++等,以及相关的开发框架和工具。同时,了解软件开发的基本流程和团队协作方式。
参与项目开发:争取参与不同的项目开发,并积极投入到实际的软件开发任务中。通过实践中的经验,熟悉项目开发的流程、沟通和协作技巧,并提升自己的编程能力和问题解决能力。
建立技术基础:在这一年中,建立起坚实的技术基础,包括掌握常用的开发工具、版本控制系统和调试技巧。此外,关注行业的最新发展和趋势,了解新兴技术和领域。

3年:
深入技术领域:在前三年,您可以选择在特定的技术领域进行深入研究和学习,如移动端开发、云计算、大数据、人工智能等。通过专注于一个领域,成为该领域的专家,并具备解决复杂问题和设计系统架构的能力。
担任技术角色:根据您的兴趣和能力,争取在团队中担任技术角色,如技术负责人、架构师或项目经理。这将使您有机会领导团队、参与决策,并在项目中发挥更大的影响力。
探索新技术和方法:继续保持对新技术和方法的开放态度,参与行业的培训、研讨会和社区活动,与其他专业人士交流和分享经验。通过不断学习和实践,扩展自己的技术视野,并应用到实际的项目中。

5年:
带领团队:在五年时间内,您可以朝着带领团队的方向发展。作为资深软件开发人员,您可以带领和指导新人,分享经验和知识,推动团队的整体成长和发展。同时,积极参与项目管理和决策,推动业务发展和产品创新。
拓展领域和市场:五年的经验使您得以更好地了解市场需求和趋势。您可以考虑在不同的业务领域进行尝试,拓展自己的技术广度和业务深度。通过与客户和合作伙伴交流合作,拓展业务网络和资源。
深造与终身学习:在职业的五年时间内,持续深造和终身学习是非常重要的。关注领域内的最新技术和趋势,参与相关的证书考试和培训课程,不断提升自己的技术素养和管理能力。同时,积极参与行业组织和社区,扩展人脉和学习机会。

9.为什么不去读博?

读博给我的反馈没有工业界来的直接 读博是为了一个未知的结果 一个网络模型去调研 实验等等 完事还得写好故事 把论文很好的呈现出来 到最后发出来是要经过一个很长的时间沉淀的 而工业界不一样了 反馈很积极 那我在这段时间里拿下这个项目 两三个月是吧 ok直接项目结束 客户需求解决 ,自我价值得到了体现 我觉得很有意义 技术也提升了 我觉得我适合工业界。

10.介绍项目,科研项目主要实现的功能
表情识别走起

11.为了进入公司,做了哪些努力
自我学习和技能提升:您可以讨论您主动学习和提升自己的努力。这可能包括通过自学、在线课程、培训、参加相关研讨会和工作坊等方式来扩展自己的知识和技能。强调您的自我驱动力和对持续学习的承诺。

项目经验和实践:描述您在个人项目、学校项目或志愿者工作中积累的实践经验。说明您如何主动参与不同类型的项目,如开发个人应用、参与团队项目、为开源项目进行贡献等。强调您在这些项目中取得的成果和能力的增长。

网络和行业参与:提及您积极参与行业相关的社群、论坛和网络平台。这可以包括参与技术社区、在开源项目中建立联系、参加行业活动和会议等。强调您通过这些参与展示了您对行业和技术的热情,并与其他领域专业人士建立了联系。

实习和兼职经验:如果您有相关的实习或兼职经验,强调您在这些机会中学到的知识和技能,以及如何与团队合作和应对工作挑战。描述您如何将这些经验应用到实际工作中,并为您进入新公司带来的价值。

深入了解公司和行业:突出您对目标公司和行业的研究和了解。分享您在准备面试过程中所进行的调查和学习,包括研究公司的产品和服务、行业趋势和竞争情况等。这表明您对行业有浓厚的兴趣,并认识到加入这个特定公司对您的职业目标有何重要性。

13.设计模式有了解吗
单例模式:

工厂模式:

14.看过哪些书?课程和非课程的?

15.我觉得你不喜欢软件开发
我觉得我喜欢,因为编程的逻辑、创造力和问题解决能力会让我有很大思维能力上的提升,会对一件事情的看法到一个相对微观的地步,并且通过编程可以解决现实世界的问题(最基本的,抛开业务不谈,我们处理一个文件、表格、都可以直接代码实现)。
因为软件开发可以与科技紧密联系 紧跟时代潮流 我们是科技公司 那有什么新鲜的科技玩意 身处这个行业我觉得会及时接触到这些东西 这是个科技不断迭代更新的时代,我们身处这个行业就不会落后于时代。让我觉得我在跟世界一块进步。开发行业提供了广泛的学习资源和发展机会,我非常乐于接受新的技术和工具,愿意不断学习和提升自己的技能。

16.加班看法
网上说的我是不太认同的。 只有亲身经历体验了 我相信自己的眼光 而且有项目也可以体现出一个公司的活力, 一直是在发展的。年轻就应该多提升自己。跳出舒适区才能更快成长,而且每个人根据自身周围环境,自身情况,对加班的看法也是不一样的。

运动 音乐 无压力,热爱读书 生活 民族企业 加班没啥 良性竞争 跑步中 跳出舒适区,年轻 多锻炼自己的能力,实现自我价值等等

优缺点:争强好胜心,过于谨慎,慢热。

反问:
1)您也见过很多的应届生,您根据您的经验,以过来人的角度, 跟您聊天也比较轻松加愉快,能不能给我这个毕业后初入职场提一些建议,就假设我已经进了咋们公司了,随便一个角度,技术、工作汇报等等各方面都行,您觉得重要的点。
2)公司这边支持提前过来实习吗?

Webserver项目总结(各方面总结,全,很他么全!!)

项目描述

此项目是在学习计算机网络和操作系统过程中开发的一个运行在Linux系统下的轻量级Web服务器,一个Web Server就是一个服务器软件(程序),或者是运行这个服务器软件的硬件(计算机)。其主要功能是通过HTTP协议与客户端(通常是浏览器(Browser))进行通信,来接收,存储,处理来自客户端的HTTP请求,并对其请求做出HTTP响应,返回给客户端其请求的内容(文件、网页等)或返回一个Error信息。

项目由IO多路复用模块、定时器模块、线程池模块组成。实现了浏览器访问服务器,获取服务器资源的功能。
项目的框架采用的Preactor 的事件处理模式。
在主线程里通过IO多路复用监听多个文件描述符上的事件。主线程负责连接的建立和断开,同时将读写和逻辑处理任务加入线程池里的任务队列,由线程池里的工作线程负责完成相应操作实现任务的并行处理。此外,通过定时器来清除不活跃的连接减少高并发场景下不必要的系统资源的占用【文件描述符的占用、维护一个TCP连接所需要的资源】,通过及时释放资源来确保服务器在高并发场景下也能够稳定可靠。对于到达的HTTP报文,采用了有限状态机和正则表达式进行解析,
资源的响应则通过 集中写 和 内存映射 的方式进行传输。通过构建线程池完成多个读写任务和逻辑处理任务的并行处理。

通过这个项⽬,我学习了两种Linux下的⾼性能⽹络模式,熟悉了Linux环境下的编程。

用户如何与Web服务器进行通信?

通常用户使用Web浏览器与相应服务器进行通信。在浏览器中键入“域名”或“IP地址:端口号”,浏览器则先将你的域名解析成相应的IP地址或者直接根据你的IP地址向对应的Web服务器发送一个HTTP请求。这一过程首先要通过TCP协议的三次握手建立与目标Web服务器的连接,然后HTTP协议生成针对目标Web服务器的HTTP请求报文,通过TCP、IP等协议发送到目标Web服务器上。

服务器编程基本框架?

I/O 处理单元是服务器管理客户连接的模块。它通常要完成以下工作:等待并接受新的客户连接,接收
客户数据,将服务器响应数据返回给客户端。但是数据的收发不一定在 I/O 处理单元中执行,也可能在
逻辑单元中执行,具体在何处执行取决于事件处理模式。
一个逻辑单元通常是一个进程或线程。它分析并处理客户数据,然后将结果传递给 I/O 处理单元或者直
接发送给客户端(具体使用哪种方式取决于事件处理模式)。服务器通常拥有多个逻辑单元,以实现对
多个客户任务的并发处理。网络存储单元可以是数缓存和文件,但不是必须的。
请求队列是各单元之间的通信方式的抽象。I/O 处理单元接收到客户请求时,需要以某种方式通知一个
逻辑单元来处理该请求。同样,多个逻辑单元同时访问一个存储单元时,也需要采用某种机制来协调处
理竞态条件。请求队列通常被实现为池的一部分。

项目流程

首先是服务器的一个参数初始化操作。通过构造WebServer这个对象传递参数进行服务器相关参数的设定,主要参数有设置定时器超时时间、设置Epoll触发模式、设置线程池的线程数量。然后是通过设定的参数对服务器的各个模块进行初始化。主要有线程池、IO复用、HTTP对象、阻塞队列等模块。

线程池采用RAII手法,在构造时创建线程,析构时唤醒所有睡眠的线程使其退出循环。IO复用是对epoll函数调用的一个封装。HTTP对象主要设置文件存放的相关路径。缓冲区和阻塞队列主要完成指定大小的参数设定。

服务器各个模块初始化完成之后就是主线程里IO复用监听事件的循环。监听事件有新连接到达、读事件和写事件和异常事件。根据不同的事件进行一个任务处理。

当新连接到达的时候,通过调用accept取出新连接(ET模式下需要循环操作),将新连接的文件描述符用来初始化HTTP连接(套接字地址和文件描述符绑定到一个HTTP对象),完成绑定定时器的初始化,同时添加监听读事件,设置其文件描述符为非阻塞。

当有异常事件发生的时候,关闭该连接同时移除监听事件和定时器。
当触发读事件的时候,调整定时器的定时时间,将读任务放入线程池的任务队列当中去。这个时候线程池对象里的多个线程,处于一个睡眠或者竞争任务并执行的过程,任务加入到任务队列当中去时会发送一个唤醒信号,如果有睡眠的线程则会被唤醒,进入循环里探测任务队列是否为空,取出任务并执行或者队列为空继续睡眠。

线程执行读任务函数主要是完成一个非阻塞读取调用直到读完,将数据缓存在用户缓冲区中,接着执行一个消息解析的操作,根据HTTP解析是否成功的判断来决定重新注册写事件还是读事件。如果解析失败那么重新注册读事件等待下次读取更多数据直到一个完整的HTTP请求。如果是解析成功的话就制作响应报文并且注册写事件,等待内核缓冲区可写触发事件时,将其写入内核缓冲区。

这部分的重点是逻辑处理的过程,也就是HTTP解析和HTTP报文的制作?
解析采用的是状态机和正则表达式,每次都读取以\r\n结尾的一段字符串,通过状态机来判定获取的字符串是属于HTTP请求的哪一部分,再跳转到相应的函数进行解析,如果读取的字符串没有以\r\n结尾则认为此次数据获取不完整,返回解析失败重新注册读事件。如果解析完成之后则根据解析过程中保存的相应信息,制作响应报文,通过 集中写 将资源文件和响应报文分别发送回客户端。

Web服务器如何接收客户端发来的HTTP请求报文呢?

web服务器通过socket监听来自用户的请求;
很多用户会尝试去connect这个webserver上正在监听的port,而监听到的这些连接会排队等待被accept,由于用户连接请求时随机到达的异步事件,每当监听socket,监听到新的客户连接并放入监听队列,我们都需要告诉我们的web服务器有新连接过来,accept这个连接,并分配一个逻辑单元来处理这个用户请求,而且我们在处理这个请求的同时,还需要继续监听其他客户的请求并分配其另一逻辑单元来处理(并发,同时处理多个事件,后面会提到使用线程池实现并发),服务器通过epoll这种IO复用技术来实现对监听socket和连接socket的同时监听。

有两种事件处理模式?

服务器程序通常需要处理三类事件:I/O事件,信号及定时事件。

同步IO:在一个线程中,cpu执行代码速度很快,然后一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待IO操作完成,才能继续进行下一步操作,这种称为同步IO;
异步IO:当代码执行一个耗时的IO操作时,只发出IO指令,并不等待IO结果,然后去执行其他代码,一段时间后,当IO返回结果时,在通知CPU进行处理。

reactor模式

要求主线程只负责监听 文件描述符上是否有事件发生(可读、写),若有则立即通知工作线程,将socket可读可写事件放入请求队列,交给工作线程处理,此外主线程不做任何操作,什么读写数据、接受新的连接、处理客户请求啥的都在工作线程中完成。以同步IO(epoll_wait为例)实现的reactor模式工作流程:

1)主线程往内核事件表中注册socket上的读就绪事件,然后主线程调用epoll_wait等待socket有数据可读;
2)socket上有数据可读时,epoll_wait通知主线程,主线程将可读事件放入请求队列,睡在请求队列上的某个工作线程被唤醒,从socket读取数据,然后处理请求;

3)往epoll内核事件表中注册该socket上的写就绪事件,主线程调用epoll_wait等待socket可写;
4)socket上有数据可写时,epoll_wait通知主线程,将可写事件放入请求队列,睡在请求队列上的某个工作线程被唤醒,往socket上写入服务器处理客户请求的结果。

proactor模式

将所有的I/O操作都交给主线程和内核来处理(进行读、写),工作线程仅负责处理逻辑,如主线程读完成后,选择一个工作线程来处理客户请求。使用异步IO模型(aio_read, aio_write为例),实现proactor模式的工作流程是:

1)主线程调用aio_read函数往内核注册 socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及读完成时如何通知应用程序;
2)主线程处理其他逻辑 ( 因为是异步嘛,所以主线程肯定忙其他事情去了!!)
3)当socket上的数据被读入用户缓冲区后,内核向应用程序发送一个信号,通知应用程序数据可用了,然后信号处理函数选一个工作线程来处理这个请求;

4)工作线程处理完客户请求后,调用aio_write函数向内核注册 socket上写完成事件,并告诉内核用户写缓冲区位置,以及写操作完成时如何通知应用程序;
5)主线程继续处理其他逻辑 ( 因为是异步嘛,所以主线程肯定忙其他事情去了!!)
6)当用户缓冲区的数据被写入socket之后,内核将向应用程序发送一个信号,然后信号处理函数选一个工作线程来善后。

同步IO模拟proactor模式

通常使用同步I/O模型(epoll_wait)实现reactor,异步I/O(aio_read和aio_write)实现proactor。此项目中使用同步IO模拟proactor事件处理模式。原理是:主线程执行数据读写操作,读写完成后主线程向工作线程通知这一“完成事件”,从工作线程角度看,他们就直接得到了数据读写结果,接下来只要做对读写结果进行逻辑处理。

1)主线程往epoll内核事件表中注册socket上的读就绪事件,然后调用epoll_wait等待socket上有数据可读;
2)有数据可读时epoll_wait通知主线程,主线程从socket循环读取数据,读完后将数据封装成一个请求对象插入请求队列。睡在请求队列上的工作线程被唤醒,处理请求;
3)然后往epoll内核事件表中注册socket上的写就绪事件,主线程调用epoll_wait等待socket可写,可写时epoll_wait通知主线程,主线程往socket上写入服务器处理客户请求的结果;

Linux下有三种IO复用方式:epoll(ET+LT) select poll(LT)

  1. select和poll,所有文件描述符都是在用户态被加入其文件描述符集合的,每次调用都需将整个集合拷贝到内核态,epoll则将整个文件描述符集合维护在内核态,每次添加文件描述符的时候都需执行一个系统调用,所以在有很多短期活跃连接的情况下,epoll可能慢于select和poll,;

  2. select使用线性表描述文件描述符集合,文件描述符有上限;poll使用链表来描述;epoll底层通过红黑树来描述文件描述符集合,并且维护一个ready list,将事件表中已经就绪的事件添加到这里,在使用epoll_wait调用时,仅观察这个list中有没有数据即可。

  3. select和poll的最大开销来自内核判断是否有文件描述符就绪这一过程:每次执行select或poll调用时,会遍历整个文件描述符集合去判断各个文件描述符是否有活动;epoll则不需要去以这种方式检查,当有活动产生时,会自动触发epoll回调函数通知epoll文件描述符,然后内核将这些就绪的文件描述符放到之前提到的ready list中等待epoll_wait调用后被处理。

ET和LT

ET模式时,必须保证该文件描述符是非阻塞的(确保在没有数据可读时,该文件描述符不会一直阻塞);且每次调用read和write的时候都必须等到它们返回EWOULDBLOCK(确保所有数据都已读完或写完)。

LT模式下只要内核缓冲区还有数据可读便会提醒(哪怕已经提醒过,针对同一事件可以多次提醒);
ET模式下,每一次事件到来只通知一次(针对一个事件只提醒一次而不是提醒多次),没有及时读取完,该部分剩余数据直到下次触发,才能被读取(有可能永远也读不到,如果没有再次触发文件描述符上的该事件);

为什么ET模式下一定要设置非阻塞?

因为ET模式下是无限循环读,直到出现错误为EAGAIN或者EWOULDBLOCK,这两个错误表示socket为空,不用再读了,然后就停止循环了,如果是阻塞,循环读在socket为空的时候就会阻塞到那里,主线程的read()函数一旦阻塞住,当再有其他监听事件过来就没办法读了,给其他事情造成了影响,所以必须要设置为非阻塞。

LT用于并发量小的情况:LT通知用户后,会一直保留fd,随fd增多,就绪链表越大,每次都要从头开始遍历找到对应的fd,并发量越大效率越低,而ET会将fd从就绪链表中删除;

Get和Post区别

Get是请求获取数据,不对服务器产生影响,所以是安全(不会破环资源)幂等的(多次执行相同操作,结果相同)
Post是向服务器提交数据,会对服务器产生影响,是不安全不幂等的。

get方法只产生一个TCP数据包,浏览器把请求头和请求数据一并发送出去,服务器响应200ok,而post会产生两个TCP数据包,浏览器会先将请求头 发给服务器,服务器响应100continue后,再发送请求数据,服务器响应200ok(返回数据);
GET请求参数会被保存在浏览器历史记录里,参数在URL中,而post通过请求体传递参数,参数不会被保留

GET /562f25980001b1b106000338.jpg HTTP/1.1
Host:img.mukewang.com
User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Accept:image/webp,image/,/*;q=0.8
Referer:http://www.imooc.com/
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8
空行
请求数据为空

POST / HTTP1.1
Host:www.wrox.com
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive
空行
name=Professional%20Ajax&publisher=Wiley

Web服务器如何处理以及响应接收到的HTTP请求报文呢? 线程池

利用线程池并发处理用户请求,主线程负责读写,工作线程(线程池中的线程)负责处理逻辑(HTTP请求报文解析等等),通过之前的代码,将listenfd上到达的连接 通过accept接收,并返回一个新的socket文件描述符connfd用于和用户通信,对用户请求返回响应,同时将这个connfd注册到内核事件表中,等用户发来请求报文。
过程是:通过epoll_wait发现这个connfd上有可读事件了(EPOLLIN),主线程就将这个HTTP的请求报文读进这个连接socket的读缓存中users[sockfd].read(),然后将该任务对象(指针)插入线程池的请求队列中pool->append(users + sockfd),线程池的实现还需依靠锁机制以及信号量机制来实现线程同步,保证操作的原子性。

线程同步机制:临界区 互斥量 信号量 事件
线程池中的工作线程是一直等待吗?
阻塞等待的模式下为了能够处理高并发的问题,将线程池中的工作线程都设置为阻塞等待在请求队列是否不为空的条件上

你的线程池工作线程处理完一个任务后的状态是什么?
这里要分两种情况考虑(1) 当处理完任务后如果请求队列为空时,则这个线程重新回到阻塞等待的状态(2) 当处理完任务后如果请求队列不为空时,那么这个线程将处于与其他线程竞争资源的状态,谁获得锁谁就获得了处理事件的资格

若同时1000个客户端进行访问请求,线程数不多,怎么能及时响应处理每一个呢?

问服务器如何处理高并发的问题对子线程循环调用来解决高并发的问题的。

通过子线程的run调用函数进行while循环,让每一个线程池中的线程永远都不会终止,他处理完当前任务就去处理下一个,没有任务就一直阻塞在那里等待。这样就能达到服务器高并发的要求

若一个客户请求需占用线程很久的时间,会不会影响接下来的客户请求呢,有什么好的策略呢?
会影响接下来的客户请求,因为线程池内线程的数量是有限的,如果客户请求占用线程时间过久的话会影响到处理请求的效率,当请求处理过慢时会造成后续接受的请求只能在请求队列中等待被处理,从而影响接下来的客户请求。

应对策略:我们可以为线程处理请求对象设置处理超时时间, 超过时间先发送信号告知线程处理超时,然后设定一个时间间隔再次检测,若此时这个请求还占用线程则直接将其断开连接。

线程数目:CPU是4核的,若是CPU密集型任务(如视频剪辑等),线程池中线程数量也设置为4,若是IO密集型任务,一般多于CPU核数。

主线程选择哪个子线程来服务?

主线程使用某种算法来主动选择子线程。最简单、最常用的算法是随机算法和 Round Robin(轮流选取)算法,但更优秀、更智能的算法将使任务在各个工作线程中更均匀地分配,从而减轻服务器的整体压力。

主线程和所有子线程通过一个共享的工作队列来同步,子线程都睡眠在该工作队列上。当有新的任务到来时,主线程将任务添加到工作队列中。这将唤醒正在等待任务的子线程,不过只有一个子线程将获得新任务的”接管权“,它可以从工作队列中取出任务并执行之,而其他子线程将继续睡眠在工作队列上。

生成HTTP响应返回给用户

通过以上操作,我们已经对读到的请求做好了处理,然后也对目标文件的属性作了分析,若目标文件存在、对所有用户可读且不是目录时,则使用 mmap将其映射 到内存地址m_file_address处,并告诉调用者获取文件成功FILE_REQUEST。 接下来要做的就是根据读取结果对用户做出响应了,也就是到了process_write(read_ret);这一步,该函数根据process_read()的返回结果来判断应该返回给用户什么响应,我们最常见的就是404错误了,说明客户请求的文件不存在,除此之外还有其他类型的请求出错的响应,具体的可以去百度。然后呢,假设用户请求的文件存在,而且已经被mmap到m_file_address这里了,那么我们就将做如下写操作,将响应写到这个connfd的写缓存m_write_buf中。
首先将状态行写入写缓存,响应头也是要写进connfd的写缓存(HTTP类自己定义的,与socket无关)中的,对于请求的文件,我们已经直接将其映射到m_file_address里面,然后将该connfd文件描述符上修改为EPOLLOUT(可写)事件,然后epoll_wait监测到这一事件后,使用writev来将响应信息和请求文件聚集写到TCP Socket本身定义的发送缓冲区(这个缓冲区大小一般是默认的,但我们也可以通过setsockopt来修改)中,交由内核发送给用户。OVER!

有限状态机

HTTP报文可以拆分为请求行、头部字段、请求体。每个部分之间都通过特殊界限符划分。在我们获得一个数据包(以\r\n结尾的数据包)的时候可以根据状态机的状态变量判断如何处理当前的数据包,并且在执行完相应操作后设置状态变量进行状态转移完成整个报文的解析工作。每个状态都有一系列的转移,每个转移与输入和另一状态相关。当输入进来,如果它与当前状态的某个转移相匹配,机器转换为所指的状态,然后执行相应的代码。

传统应用程序的控制流程基本是按顺序执行的:遵循事先设定的逻辑,从头到尾的执行。简单来说如果想在不同状态下实现代码跳转时,就需要破坏一些代码,会很复杂;

process_read()函数的作用就是将类似上述例子的请求报文进行解析,因为用户的请求内容包含在这个请求报文里面,只有通过解析,知道用户请求的内容是什么,是请求图片,还是视频,或是其他请求,我们根据这些请求返回相应的HTML页面等。
项目中使用主从状态机的模式进行解析,从状态机(parse_line)负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。每解析一部分都会将整个请求的m_check_state状态改变,状态机也就是根据这个状态来进行不同部分的解析跳转的:

parse_request_line(text),解析请求行,也就是GET中的GET /562f25980001b1b106000338.jpg HTTP/1.1这一行,或者POST中的POST / HTTP1.1这一行。通过请求行的解析我们可以判断该HTTP请求的类型(GET/POST),而请求行中最重要的部分就是URL部分,我们会将这部分保存下来用于后面的生成HTTP响应。
parse_headers(text);,解析请求头部,GET和POST中空行以上,请求行以下的部分。
parse_conten数t(text);,解析请求据,对于GET来说这部分是空的,因为这部分内容已经以明文的方式包含在了请求行中的URL部分了;只有POST的这部分是有数据的,项目中的这部分数据为用户名和密码,我们会根据这部分内容做登录和校验,并涉及到与数据库的连接。

得到一个完整的,正确的HTTP请求时,就到了do_request代码部分,我们需要首先对GET请求和不同POST请求(登录,注册,请求图片,视频等等)做不同的预处理,然后分析目标文件的属性,若目标文件存在、对所有用户可读且不是目录时,则使用mmap将其映射到内存地址m_file_address处,并告诉调用者获取文件成功。

抛开mmap这部分,先来看看这些不同请求是怎么来的:

假设你已经搭好了你的HTTP服务器,然后你在本地浏览器中键入localhost:9000,然后回车,这时候你就给你的服务器发送了一个GET请求,什么都没做,然后服务器端就会解析你的这个HTTP请求,然后发现是个GET请求,然后返回给你一个静态HTML页面,也就是项目中的judge.html页面,那POST请求怎么来的呢?这时你会发现,返回的这个judge页面中包含着一些新用户和已有账号这两个button元素,当你用鼠标点击这个button时,你的浏览器就会向你的服务器发送一个POST请求,服务器通过检查action来判断你的POST请求类型是什么,进而做出不同的响应。

缓冲区扩容啥的?

因为缓冲区的大小是固定的大小,而我们通常是一次性将数据全部读取到缓冲区,那么就有可能装不下数据,所以需要临时创建一个缓冲区来缓解,将存不下的放到临时缓冲区,这样就可以一次性将所有的数据读入,这里利用临时缓冲区的技术是一个 分散读 的技术,即将数据分散读取到内存中不同的位置
当固定的缓冲区想要继续写数据的时候,发现剩余的位置不够写的时候那么就可以先把数据写入到临时缓冲中,再将临时数组的数据读取到固定缓冲区来处理
如果我们想要写入数据,那么此时可以利用的空间就是最前面的部分和最后面的部分的位置,但是写入数据一定要连续,所以唯一的办法就是将中间的数据移动到最前面,这样就可以将空闲的区域连接在一起,方便后面的写数据。具体的实现就是将读指针到写指针之间的数据复制到最前面,再更改读指针和写指针的位置,这就是利用一个缓冲实现自动增长的原理

因为读设置的是边沿触发,需要一次性读完所有数据。所以定义一个大小1024的容器,但是有可能放不下,所以在定义一个65535的备用容器,采用分散读的形式,读到这两个容器里。然后整合这两个容器里数据(因为后序要吧数据取出来进行解析请求,所以需要合到一起):如果第一个能放下,写指针向后移动;如果放不下,看看能否凑出来,能凑则凑,凑不出来,第一个容器自动扩容resize。这样所有数据都在第一个容器里了。没必要一开始就用大容器:占内存,影响性能。
解析http请求,生成http响应响应首行,响应头在buffer里,响应体在内存映射里
写数据:边沿触发,一次性把数据从缓冲区buffer、内存映射写到socket中,所以需要分散写

那么如何实现动态扩容的呢?

因为数据的处理只能放到固定大小的缓冲中进行处理,即上述的1024字节的缓冲中,那么如果很多数据都读取到这块区域的话,那么肯定是放不下的,我们就可以利用一个临时的缓冲区,把放不下的数据先放到临时的缓冲的位置,等到1024字节大小的内存有剩余的空间的时候我再将临时缓冲区的数据放入到1024的位置进行处理

HTTPS协议为什么安全?

HTTPS=HTTP+TLS/SSL
TLS/SSL协议位于应用层协议和TCP之间,构建在TCP之上,由TCP协议保证数据传输版的可靠性,任何数据到权达TCP之前,都经过TLS/SSL协议处理。https是加密传输协议,可以保障客户端到服务器端的传输数据安全。用户通过http协议访问网站时,浏览器和服务器之间是明文传输,这就意味着用户填写的密码、帐号、交易记录等机密信息都是明文,随时可能被泄露、窃取、篡改,被第三者加以利用。安装SSL证书后,使用https加密协议访问网站,可激活客户端浏览器到网站服务器之间的"SSL加密通道"(SSL协议),实现高强度双向加密传输,防止传输数据被泄露或篡改。

HTTPS的SSL连接过程
1.客户端提交https请求;
2.服务器响应客户,并把证书公钥发给客户端;
3.客户端验证证书公钥的有效性;
4.有效后,生成一个会话密钥;
5.用证书公钥加密这个会话密钥后,发送给服务器;
6.服务器收到公钥加密的会话密钥后,用私钥解密,回去会话密钥;
7.客户端和服务器利用这个会话密钥加密要传输的数据进行通信;

定时器相关

两种方法:小根堆 升序链表

如果某一用户connect()到服务器之后,长时间不交换数据,一直占用服务器端的文件描述符,导致连接资源的浪费。这时候就应该利用定时器把这些超时的非活动连接释放掉,关闭其占用的文件描述符。这种情况也很常见,当你登录一个网站后长时间没有操作该网站的网页,再次访问的时候你会发现需要重新登录。
项目中使用的是SIGALRM信号来实现定时器,利用alarm函数周期性的触发SIGALRM信号,信号处理函数利用管道通知主循环,主循环接收到该信号后对升序链表上所有定时器进行处理,若该段时间内没有交换数据,则将该连接关闭,释放所占用的资源。alarm函数会定期触发SIGALRM信号,这个信号交由sig_handler来处理,每当监测到有这个信号的时候,都会将这个信号写到pipefd[1]里面,传递给主循环,在主循环中处理啥的;

但基于升序链表存在缺点:
每次遍历添加和修改定时器的效率偏低(O(n)),使用最小堆结构可以降低时间复杂度降至(O(logn))。
每次以固定的时间间隔触发SIGALRM信号,调用tick函数处理超时连接会造成一定的触发浪费,举个例子,若当前的TIMESLOT=5,即每隔5ms触发一次SIGALRM,跳出循环执行tick函数,这时如果当前即将超时的任务距离现在还有20ms,那么在这个期间,SIGALRM信号被触发了4次,tick函数也被执行了4次,可是在这4次中,前三次触发都是无意义的。对此,我们可以动态的设置TIMESLOT的值,每次将其值设置为当前最先超时的定时器与当前时间的时间差,这样每次调用tick函数,超时时间最小的定时器必然到期,并被处理,然后在从时间堆中取一个最先超时的定时器的时间与当前时间做时间差,更新TIMESLOT的值。

除了小根堆实现之外,还有使用时间轮和基于升序链表实现的定时器结构。

基于升序链表实现的定时器结构按照超时时间作为升序做排序,每个结点都链接到下一个结点,由于链表的有序性以及不支持随机访问的特性,每次插入定时器都需要遍历寻找合适的位置,而且每次定时器调整超时时间时也需要往后遍历寻找合适的位置进行挪动,遍历操作效率较低。同时也需要通过多次计时(通过信号中断实现)来检测链表中的定时器是否超时并进行处理。 优化方式可以通过利用IO复用的超时选项。

不同于采用单条链表实现的定时器每次插入更新进行遍历来寻找合适的位置进行操作,时间轮利用哈希思想,将相差整个计时周期整数倍的定时器散列到不同的时间槽中,减少链表上的定时器数量避免过多的顺序遍历操作。时间轮通过提高时间槽的数量来提高查找效率【使每个时间槽里的链表长度尽可能短】,通过减少计时间隔来提高定时器的精度【使定时时间尽可能准确】,设计时需要考虑这两个因素,时间槽多但是定时器数量少则会造成效率低下,可以通过多级时间轮优化,但是实现起来复杂。

小根堆实现的定时器结构,每次取堆头都是最短超时时间,能够利用IO复用的超时选项,每次的计时间隔动态调整为最短超时时间,确保每次调用IO复用系统调用返回时都至少有一个定时事件的超时发生或者监听事件的到达,有效地减少了多余的计时中断(利用信号中断进行计时)。最主要是确保每次定时器插入、更新、删除都能实现稳定的logn时间复杂度【该时间复杂度是调整堆的代价,定时器的定位利用哈希表实现O1查找】,而不像链表一样依赖于定时器数量的大小以及时间轮需要兼顾时间精度和效率的问题。

Webbench原理

Webbench 首先 fork 出多个子进程,每个子进程都循环做 web 访问测试。子进程把访问的结果通过pipe写端告诉父进程,父进程做最终的统计结果。

项目效果及压力测试

一个服务器项目,你在本地浏览器键入localhost:8000发现可以运行无异常还不够,你需要对他进行压测(即服务器并发量测试),压测过了,才说明你的服务器比较稳定了。 用到了一个压测软件叫做Webbench,可以直接在Gtihub里面下载,解压,然后在解压目录打开终端运行命令(-c表示客户端数, -t表示时间)

虚拟机上创建子进程,同时也在虚拟机上运行服务器(创建子进程会消耗资源,可以在另一机器上fork,来访问此服务器)。

在单核【2.4GHz】2G的云服务器上实现了并发量2W+,QPS8K+的效果。
在满载(开始有连接崩溃的临界)的情况下,内存占用达到60%,这个和缓冲区大小的设置有关,可以通过进一步提高其大小来提高性能。同时CPU占用率较高(45%),推测和单核进行频繁的上下文切换(多线程)有关。
整个项目更偏向于IO密集型。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
对于出现 "Permission denied (publickey)" 错误的情况,通常是由于 SSH 密钥配置问题引起的。以下是一些可能的解决办法: 1. 确保已经生成了 SSH 密钥对:在命令行中运行 `ls -al ~/.ssh`,如果存在 `id_rsa` 和 `id_rsa.pub` 文件,则表示已经生成了密钥对。如果不存在,请执行下一步。 2. 生成 SSH 密钥对:在命令行中运行 `ssh-keygen -t rsa -b 4096 -C "[email protected]"`,其中 "[email protected]" 替换为你的邮箱地址。随后,按照提示完成密钥对的生成过程。 3. 将公钥添加到你的 Git 托管平台账户:复制公钥内容(一般为 `id_rsa.pub` 文件中的内容),登录到你的 Git 托管平台账户,找到 SSH 密钥设置页面,将公钥内容粘贴到相应位置,并保存。 4. 验证 SSH 连接:在命令行中运行 `ssh -T git@github.com` 或 `ssh -T git@git.coding.net`,根据你使用的平台选择相应的命令。如果出现成功连接的提示信息,则说明 SSH 连接已经配置成功。 如果上述方法无法解决问题,你可以尝试以下进一步操作: - 检查 SSH 配置文件:打开 `~/.ssh/config` 文件,确保其中没有针对该 Git 托管平台的特殊配置。 - 检查远程仓库 URL:使用 `git remote -v` 命令查看远程仓库的 URL 是否正确,如果不正确,可以使用 `git remote set-url origin <新的远程仓库 URL>` 命令修改。 - 检查访问权限:确保你有访问远程仓库的权限,尤其是在团队协作项目中。 如果问题仍然存在,你可以尝试搜索相关错误信息,或者联系 Git 托管平台的支持团队以获取帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值