C++经典面试问题

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++经典面试问题相关的知识,希望对你有一定的参考价值。

参考技术A

C++经典面试问题

   C++经典面试问题分享

  1,关于动态申请内存

  答:内存分配方式三种:

  (1)从静态存储区域分配:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。

  全局变量,static变量。

  (2)在栈上创建:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,

  函数执行结束时这些存储单元自动被释放。

  栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

  (3)用malloc或new申请内存之后,应该立即检查指针值是否为NULL.防止使用指针值为NULL的内存,

  不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。避免数组或指针的下标越界,

  特别要当心发生“多1”或者“少1”操作。动态内存的申请与释放必须配对,防止内存泄漏。

  用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。从堆上分配,亦称动态内存分配。

  程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。

  动态内存的生存期由程序员决定,使用非常灵活。(int *pArray; int MyArray[6]; pArray = &MyArray[0];)

  如果在申请动态内存时找不到足够大的内存块,malloc和new将返回NULL指针,

  判断指针是否为NULL,如果是则马上用return语句终止本函数,

  或者马上用exit(1)终止整个程序的运行,为new和malloc设置异常处理函数。

  2,C++指针攻破

  答案:指针是一个变量,专门存放内存地址,特点是能访问所指向的内存

  指针本身占据了4个字节的长度

  int **ptr; //指针的类型是 int **

  int (*ptr)[3]; //指针的类型是 int(*)[3]

  int *(*ptr)[4]; //指针的类型是 int *(*)[4]

  ptr++:指针ptr的值加上了sizeof(int)

  ptr+=5:将指针ptr的值加上5*sizeof(int)

  指针的赋值:

  把一个变量的地址赋予指向相同数据类型的指针变量( int a; int *ip; ip=&a; )

  把一个指针变量的值赋予指向相同类型变量的另一个指针变量(int a; int *pa=&a; int *pb; pb=pa; )

  把数组的首地址赋予指向数组的指针变量(int a[5],*pa; pa=a; 也可写为:pa=&a[0];)

  如果给指针加1或减1 ,实际上是加上或减去指针所指向的数据类型大小。

  当给指针加上一个整数值或减去一个整数值时,表达式返回一个新地址。

  相同类型的两个指针可以相减,减后返回的整数代表两个地址间该类型的实例个数。

  int ** cc=new (int*)[10]; 声明一个10个元素的数组,数组每个元素都是一个int *指针,

  每个元素还可以单独申请空间,因为cc的类型是int*型的指针,所以你要在堆里申请的话就要用int *来申请;

  int ** a= new int * [2];     //申请两个int * 型的空间

  a[0] = new int[4];        ////为a的第一个元素申请了4个int 型空间,a[0] 指向了此空间的首地址处

  a[1] = new int[3];        //为a的第二个元素又申请了3个int 型空间,a[1]指向了此空间首地址处

  指针数组初始化赋值:

  一维指针开辟空间:char *str;int *arr; scanf("%d",&N);

  str=(char*)malloc(sizeof(char)*N);

  arr=(int*)malloc(sizeof(int)*N);

  二维指针开辟空间:int **arr, i; scanf("%d%d",&row,&col);

  arr=(int**)malloc(sizeof(int)*row);

  for(i=0;i

  arr[i]=(int*)malloc(sizeof(int)*col);

  结构体指针数组,例如typedef struct char x; int y; Quan,*QQuan;

  定义一个结构体指针数组如:QQuan a[MAX]

  for(i=0;i

  

  a[i]=(QQuan)malloc(sizeof(Quan));

  memset(a[i],0,sizeof(Quan));

  

  指针数组赋值

  float a[]=100,200,300,400,500;

  float *p[5]=&a[0],&a[1],&a[2],&a[3],&a[4];

  char *units[1000];

  char get_unit[250];

  for(int i=0;i

  scanf("%s", get_unit); strcpy(units[i],get_unit);

  3,复杂指针解析:

  (1)int (*func)(int *p);

  (*func)()是一个函数,func是一个指向这类函数的指针,就是一个函数指针,这类函数具有int*类型的形参,返回值类型是 int。

  (2)int (*func)(int *p, int (*f)(int*));

  func是一个指向函数的\'指针,这类函数具有int *和int (*)(int*)这样的形参。形参int (*f)(int*),f也是一个函数指针

  (3)int (*func[5])(int *p);

  func数组的元素是函数类型的指针,它所指向的函数具有int*类型的形参,返回值类型为int。

  (4)int (*(*func)[5])(int *p);

  func是一个指向数组的指针,这个数组的元素是函数指针,这些指针指向具有int*形参,返回值为int类型的函数。

  (5)int (*(*func)(int *p))[5];

  func是一个函数指针,这类函数具有int*类型的形参,返回值是指向数组的指针,所指向的数组的元素是具有5个int元素的数组。

  注意:

  需要声明一个复杂指针时,如果把整个声明写成上面所示的形式,对程序可读性是一大损害。

  应该用typedef来对声明逐层,分解,增强可读性,例如对于声明:int (*(*func)(int *p))[5];

  这样分解:typedef int (*PARA)[5]; typedef PARA (*func)(int *);

  例如:int (*(*func)[5][6])[7][8];

  func是一个指向数组的指针,这类数组的元素是一个具有5X6个int元素的二维数组,而这个二维数组的元素又是一个二维数组。

  typedef int (*PARA)[7][8];

  typedef PARA (*func)[5][6];

  例如:int (*(*(*func)(int *))[5])(int *);

  func是一个函数指针,这类函数的返回值是一个指向数组的指针,

  所指向数组的元素也是函数指针,指向的函数具有int*形参,返回值为int。

  typedef int (*PARA1)(int*);

  typedef PARA1 (*PARA2)[5];

  typedef PARA2 (*func)(int*);

  4,函数指针详解

  答:函数指针是指向一个函数入口的指针

  一个函数指针只能指向一种类型的函数,即具有相同的返回值和相同的参数的函数。

  函数指针数组定义:void(*fun[3])(void*); 相应指向类A的成员函数的指针:void (A::*pmf)(char *, const char *);

  指向外部函数的指针:void (*pf)(char *, const char *); void strcpy(char * dest, const char * source); pf=strcpy;

  5,野指针

  答:“野指针”是很危险的,if语句对它不起作用。“野指针”的成因主要有两种:

  (1)指针变量没有被初始化。指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

  char *p = NULL; char *str = (char *) malloc(100);

  (2)指针p被free或者delete之后,没有置为NULL

  (3)指针操作超越了变量的作用范围。所指向的内存值对象生命期已经被销毁

  6,引用和指针有什么区别?

  答:引用必须初始化,指针则不必;引用初始化以后不能改变,指针可以改变其指向的对象;

  不存在指向空值的引用,但存在指向控制的指针;

  引用是某个对象的别名,主要用来描述函数和参数和返回值。而指针与一般的变量是一样的,会在内存中开辟一块内存。

  如果函数的参数或返回值是类的对象的话,采用引用可以提高程序的效率。

  7,C++中的Const用法

  答:char * const p; // 指针不可改,也就说指针只能指向一个地址,不能更改为其他地址,修饰指针本身

  char const * p; // 所指内容不可改,也就是说*p是常量字符串,修饰指针所指向的变量

  const char * const p 和 char const * const p; // 内容和指针都不能改

  const修饰函数参数是它最广泛的一种用途,它表示函数体中不能修改参数的值,

  传递过来的参数在函数内不可以改变,参数指针所指内容为常量不可变,参数指针本身为常量不可变

  在引用或者指针参数的时候使用const限制是有意义的,而对于值传递的参数使用const则没有意义

  const修饰类对象表示该对象为常量对象,其中的任何成员都不能被修改。

  const修饰的对象,该对象的任何非const成员函数都不能被调用,因为任何非const成员函数会有修改成员变量的企图。

  const修饰类的成员变量,表示成员常量,不能被修改,同时它只能在初始化列表中赋值。static const 的成员需在声明的地方直接初始。

  const修饰类的成员函数,则该成员函数不能修改类中任何非const成员。一般写在函数的最后来修饰。

  在函数实现部分也要带const关键字.

  对于const类对象/指针/引用,只能调用类的const成员函数,因此,const修饰成员函数的最重要作用就是限制对于const对象的使用

  使用const的一些建议:在参数中使用const应该使用引用或指针,而不是一般的对象实例

  const在成员函数中的三种用法(参数、返回值、函数)要很好的使用;

  const在成员函数中的三种用法(参数、返回值、函数)要很好的使用;

  不要轻易的将函数的返回值类型定为const;除了重载操作符外一般不要将返回值类型定为对某个对象的const引用;

  8,const常量与define宏定义的区别

  答:(1) 编译器处理方式不同。define宏是在预处理阶段展开,生命周期止于编译期。

  只是一个常数、一个命令中的参数,没有实际的存在。

  #define常量存在于程序的代码段。const常量是编译运行阶段使用,const常量存在于程序的数据段.

  (2)类型和安全检查不同。define宏没有类型,不做任何类型检查,仅仅是展开。

  const常量有具体的类型,在编译阶段会执行类型检查。

  (3) 存储方式不同。define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。

  const常量会在内存中分配(可以是堆中也可以是栈中)

  9,解释堆和栈的区别

  答:1、栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

  由系统自动分配。声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间 。

  只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

  在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域,栈的大小是2M。

  如果申请的空间超过栈的剩余空间时,将提示overflow。

  栈由系统自动分配,速度较快。但程序员是无法控制的。

  函数调用时,第一个进栈的是主函数中后的下一条指令,的地址,然后是函数的各个参数。

  在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。

  堆区(heap) — 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收 。

  注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,需要程序员自己申请,并指明大小,在c中malloc函数

  在C++中用new运算符。首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,

  另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

  堆是向高地址扩展的数据结构,是不连续的内存区域。而链表的遍历方向是由低地址向高地址。

  堆的大小受限于计算机系统中有效的虚拟内存。

  堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便

  一般是在堆的头部用一个字节存放堆的大小。

  10,论述含参数的宏和函数的优缺点

  (1)函数调用时,先求出实参表达式的值,然后代入形参。而使用带参的宏只是进行简单的字符替换

  (2)函数调用是在程序运行时处理的,分配临时的内存单元;而宏展开是在编译时进行的,在展开时不进行

  内存分配,不进行值得传递处理,没有“返回值”概念

  (3)对函数中的形参和实参都要定义类型,类型要求一致,如不一致则进行类型转换。而宏不存在类型问题

  (4)调用函数只可得到一个返回值,而用宏则可以设法得到几个结果

  (5)实用宏次数多时,宏展开后源程序变长,没展开一次源程序增长,函数调用则不会

  (6)宏替换不占用运行时间,只占编译时间,而函数调用占用运行时间

  11,C++的空类,默认产生哪些类成员函数?

  答:class Empty

  

  public:

  Empty(); //缺省构造函数

  Empty(const Empty& ); //拷贝构造函数

  ~Empty(); //虚构函数

  Empty& operator(const Empty& ) //赋值运算符

  Empty& operator&(); //取址运算符

  const Empty* operator&() const; // 取址运算符 const

  

  12,谈谈类和结构体的区别

  答:结构体在默认情况下的成员都是public的,而类在默认情况下的成员是private的。结构体和类都必须使用new创建,

  struct保证成员按照声明顺序在内存在存储,而类不保证。

  13,C++四种强制类型转换

  答:(1)const_cast

  字面上理解就是去const属性,去掉类型的const或volatile属性。

  struct SA int k; const SA ra;

  ra.k = 10; //直接修改const类型,编译错误 SA& rb = const_cast(ra); rb.k = 10; //可以修改

  (2)static_cast

  主要用于基本类型之间和具有继承关系的类型之间的转换。用于指针类型的转换没有太大的意义

  static_cast是无条件和静态类型转换,可用于基类和子类的转换,基本类型转换,把空指针转换为目标类型的空指针,

  把任何类型的表达式转换成void类型,static_cast不能进行无关类型(如非基类和子类)指针之间的转换。

  int a; double d = static_cast(a); //基本类型转换

  int &pn = &a; void *p = static_cast(pn); //任意类型转换为void

  (3)dynamic_cast

  你可以用它把一个指向基类的指针或引用对象转换成继承类的对象

  动态类型转换,运行时类型安全检查(转换失败返回NULL)

  基类必须有虚函数,保持多态特性才能用dynamic_cast

  只能在继承类对象的指针之间或引用之间进行类型转换

  class BaseClasspublic: int m_iNum; virtual void foo();;

  class DerivedClass:BaseClasspublic: char* szName[100]; void bar();;

  BaseClass* pb = new DerivedClass();

  DerivedClass *p2 = dynamic_cast(pb);

  BaseClass* pParent = dynamic_cast(p2);

  //子类->父类,动态类型转换,正确

  (4)reinterpreter_cast

  转换的类型必须是一个指针、引用、算术类型、函数指针或者成员指针。

  主要是将一个类型的指针,转换为另一个类型的指针

  不同类型的指针类型转换用reinterpreter_cast

  最普通的用途就是在函数指针类型之间进行转换

  int DoSomething()return 0;;

  typedef void(*FuncPtr)();

  FuncPtr funcPtrArray[10];

  funcPtrArray[0] = reinterpreter_cast(&DoSomething);

  14,C++函数中值的传递方式有哪几种?

  答:函数的三种传递方式为:值传递、指针传递和引用传递。

  15,将“引用”作为函数参数有哪些特点

  答:(1)传递引用给函数与传递指针的效果是一样的,这时,被调函数的形参就成为原来主调函数的实参变量或者

  对象的一个别名来使用,所以在被调函数中形参的操作就是对相应的目标对象的操作

  (2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作,当参数数据较大时,引用

  传递参数的效率和所占空间都好

  (3)如果使用指针要分配内存单元,需要重复使用“*指针变量名”形式进行计算,容易出错且阅读性较差。

;

手把手写C++服务器(17):自测!TCP协议面试经典十连问

前言:前面一篇文章《手把手写C++服务器(15):网络编程入门第一个TCP项目》介绍了一个简单入门级的TCP项目,这一篇文章重点讲一讲面试常见的TCP协议相关的十个问题,都是后端开发程序员必知必会的经典知识点。

目录

问题一:讲一下TCP三次握手四次挥手的过程

三次握手:

四次挥手:

问题二:TCP和UDP之间有什么区别?

问题三:TCP拥塞控制有哪几种方法?什么是拥塞避免?什么是快速恢复?什么是拥塞发生?

拥塞控制常用方法:

拥塞窗口

拥塞避免

快速恢复

问题四:什么是慢启动算法?

问题五:什么是TCP黏包和拆包问题?为什么会产生?如何解决?

问题六:什么是滑动窗口机制?

问题七:什么是拥塞控制?什么是超时重传机制?

拥塞控制

超时重传机制

问题八:什么是SACK选择性确认?

问题九:什么是SYN泛洪攻击?如何防御SYN泛洪攻击?

问题十:怎样保证TCP可靠性?

参考:


问题一:讲一下TCP三次握手四次挥手的过程

三次握手:

 

开始客户端和服务器都处于CLOSED状态,然后服务端开始监听某个端口,进入LISTEN状态

  • 第一次握手(SYN=1, seq=x),发送完毕后,客户端进入 SYN_SEND 状态

  • 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1), 发送完毕后,服务器端进入 SYN_RCVD 状态。

  • 第三次握手(ACK=1,ACKnum=y+1),发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手,即可以开始数据传输。

四次挥手:

  1. 第一次挥手(FIN=1,seq=u),发送完毕后,客户端进入FIN_WAIT_1 状态

  2. 第二次挥手(ACK=1,ack=u+1,seq =v),发送完毕后,服务器端进入CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态

  3. 第三次挥手(FIN=1,ACK=1,seq=w,ack=u+1),发送完毕后,服务器端进入LAST_ACK 状态,等待来自客户端的最后一个ACK。

  4. 第四次挥手(ACK=1,seq=u+1,ack=w+1),客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。

问题二:TCP和UDP之间有什么区别?

TCP提供面向连接的、可靠的数据流传输,而UDP提供的是非面向连接的、不可靠的数据流传输。
TCP传输单位称为TCP报文段,UDP传输单位称为用户数据报。
TCP有拥塞控制和流量控制保证数据传输的安全性;UDP没有拥塞控制,网络阻塞不会影响源主机的发送效率。
TCP是点对点的两点间服务,UDP支持一对一、一对多、多对一、多对多的交互通信。
TCP首部开销大,20字节;UDP首部开销小,8字节。
TCP对应的协议和UDP对应的协议

    TCP对应的协议:
    (1) FTP:定义了文件传输协议,使用21端口。
    (2) Telnet:一种用于远程登陆的端口,使用23端口,用户可以以自己的身份远程连接到计算机上,可提供基于DOS模式下的通信服务。
    (3) SMTP:邮件传送协议,用于发送邮件。服务器开放的是25号端口。
    (4) POP3:它是和SMTP对应,POP3用于接收邮件。POP3协议所用的是110端口。
    (5)HTTP:是从Web服务器传输超文本到本地浏览器的传送协议。
    UDP对应的协议:
    (1) DNS:用于域名解析服务,将域名地址转换为IP地址。DNS用的是53号端口。
    (2) SNMP:简单网络管理协议,使用161号端口,是用来管理网络设备的。由于网络设备很多,无连接的服务就体现出其优势。
    (3) TFTP(Trivial File Transfer Protocal),简单文件传输协议,该协议在熟知端口69上使用UDP服务。

问题三:TCP拥塞控制有哪几种方法?什么是拥塞避免?什么是快速恢复?什么是拥塞发生?

拥塞控制常用方法:

  • 慢启动

  • 拥塞避免

  • 拥塞发生

  • 快速恢复

拥塞窗口

发送方维持一个叫做拥塞窗口cwnd(congestion window)的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。发送方让自己的发送窗口等于拥塞窗口,另外考虑到接受方的接收能力,发送窗口小于拥塞窗口。

拥塞避免

拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口按线性规律缓慢增长。

无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理),就把慢开始门限设置为出现拥塞时的发送窗口大小的一半。然后把拥塞窗口设置为1,执行慢开始算法。

快速恢复

1.当收到3个重复ACK时,把ssthresh设置为cwnd的一半,把cwnd设置为ssthresh的值加3,然后重传丢失的报文段,加3的原因是因为收到3个重复的ACK,表明有3个“老”的数据包离开了网络。

2.再收到重复的ACK时,拥塞窗口增加1。

3.当收到新的数据包的ACK时,把cwnd设置为第一步中的ssthresh的值。原因是因为该ACK确认了新的数据,说明从重复ACK时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态。

问题四:什么是慢启动算法?

在 TCP 刚刚连接好,开始发送 TCP 报文段时,先让拥塞窗口 cwnd = 1,即一个最大报文段长度 MSS。而在每收到一个对新的报文段的确认后,将 cwnd 加倍,即刚开始会增大一个 MSS。用这样的方法逐步增大发送方的拥塞窗口 cwnd,可以使分组注入到网络的速率更加合理。例如,A 向 B 发送数据,当发送时 A 的拥塞窗口为 2,那么 A 一次可以发送两个 TCP 报文段,当经过一个 RTT 后,A 收到 B 对刚才两个报文的确认,于是就把拥塞窗口调整为 4,即下一次发送时就可以发送 4 个报文段。

使用慢开始算法后,每经过一个传输轮次,拥塞窗口 cwnd 就会加倍,即 cwnd 的大小呈指数形式增长。这样慢开始一直把拥塞窗口 cwnd 增大到一个规定的慢开始门限 ssthresh(阈值),然后改用拥塞避免算法。

问题五:什么是TCP黏包和拆包问题?为什么会产生?如何解决?

TCP是面向流,没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。

为什么会产生粘包和拆包呢?

  • 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包;

  • 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包;

  • 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包;

  • 待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。即TCP报文长度-TCP头部长度>MSS。

解决方案:

  • 发送端将每个数据包封装为固定长度

  • 在数据尾部增加特殊字符进行分割

  • 将数据分为两部分,一部分是头部,一部分是内容体;其中头部结构大小固定,且有一个字段声明内容体的大小。

问题六:什么是滑动窗口机制?

滑动窗口协议的基本工作流程就是由接收方通告窗口的大小,这个窗口称为提出窗口,也就是接收方窗口。接收方提出的窗口则是被接收缓冲区所影响的,如果数据没有被用户进程使用那么接收方通告的窗口就会相应得到减小,发送窗口取决于接收方窗口的大小。可用窗口的大小等于接收方窗口减去发送但是没有被确认的数据包大小。

问题七:什么是拥塞控制?什么是超时重传机制?

拥塞控制

所谓的拥塞控制就是为了防止过多的数据注入网络中,这样可以使网络中的路由器或链路不会过载。当出现拥塞时,端点并不能了解到拥塞发生的细节,对通信连接的端点来说,拥塞往往表现为通信时延的增加。拥塞控制和流量控制相似的地方是通过控制发送方发送数据的速率来达到效果。

拥塞控制与流量控制的区别:拥塞控制是让网络能够承受现有的网络负荷,它是一个全局性的过程,涉及所有的主机、所有的路由器,以及与降低网络传输性能有关的所有因素。而流量控制往往使指点对点通信量的控制,即接收端控制发送端,它所做的就是抑制发送端发送数据的速率,以便使接收端来得及接收。

为了更好地对传输层进行拥塞控制,有以下四种算法:慢开始、拥塞避免、快重传、快恢复。发送方在确定发送报文段的速率时,既要根据接收方的接收能力,又要从全局考虑不要使网络发生拥塞。因此,TCP 协议要求发送方维护以下两个窗口:

(1) 接收窗口 rwnd,接收方根据目前接收缓存大小所许诺的最新的窗口值,反映了接收方的容量。有接收方根据其放在 TCP 报文的首部的 "窗口" 字段通知发送方。

(2) 拥塞窗口 cwnd,发送方根据自己估算的网络拥塞程度而设置的窗口值,反映了网络当前容量。只要网络没有出现拥塞,拥塞窗口就再增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减少一些,以减少注入网络中的分组数。发送窗口的上限值应当取接收窗口 rwnd 和拥塞窗口 cwnd 中较小的一个,即:Min[rwnd, cwnd]。

超时重传机制

TCP 每发送一个报文段,就对这个报文段设置一次计时器。只要计时器设置的重传时间到期但还没有收到确认,就要重传这一报文段。当检测到接收数据有错误时,会采取直接丢弃出错的数据,发送端等待接收端的确认超时后,会自动重发该报文段。

由于 IP 数据报在传输的时候选择的路由变化很大,因此传输层的往返时延的方差很大。为了计算超时计时器的重传时间,TCP 采用一种自适应算法,它记录一个报文段发出的时间,以及收到相应确认的时间,这两个时间之差叫做报文段的往返时间(RTT)。TCP 保留了 RTT 的一个加权平均往返时间 RTTs,当第一次测量 RTT 样本时,RTTs 值就为所测量到的 RTT 样本的值,但之后每测量一个新的 RTT 样本,就按下式重新计算一次 RTTs:

新的 RTTs = ( 1- a ) * (旧的 RTTs) + a(新的RTT样本)

在上式中 0 <= a < 1。若 a 很接近于零,表示新的 RTTs 值和旧的 RTTs 值相比变化不大,而受新的 RTT 样本影响不大(RTT值更新较慢)。若 a 接近于 1,则表示新的 RTTs 值受新的 RTT 样本的影响较大(RTT值更新较快)。[RFC 2988] 推荐的 a 值为 0.125。

所以超时计时器设置的超时重传时间(RTO)应略大于上面得出的加权平均往返时间 RTTs。即 RTO = RTTs + 4RTTd。其中 RTTd 是 RTT 的偏差的加权平均值,它与 RTTs 和新的 RTT 样本之差有关。当第一次测量时,RTTd 取为测量到的 RTT 样本值的一半,以后测量中,使用下式计算:新的 RTTd = (1-β) *(旧的 RTTd) + β*|RTTs - 新的 RTT 样本|,其中 β 是个小于 1 的系数,它的推荐值是 0.25。

问题八:什么是SACK选择性确认?

在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据

如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK 信息发现只有 200~299 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。

问题九:什么是SYN泛洪攻击?如何防御SYN泛洪攻击?

SYN Flood是一种典型的DoS (Denial of Service,拒绝服务) 攻击,它在短时间内,伪造不存在的IP地址,向服务器大量发起SYN报文。当服务器回复SYN+ACK报文后,不会收到ACK回应报文,导致服务器上建立大量的半连接半连接队列满了,这就无法处理正常的TCP请求啦。

主要有 syn cookieSYN Proxy防火墙等方案应对。

  • syn cookie:在收到SYN包后,服务器根据一定的方法,以数据包的源地址、端口等信息为参数计算出一个cookie值作为自己的SYNACK包的序列号,回复SYN+ACK后,服务器并不立即分配资源进行处理,等收到发送方的ACK包后,重新根据数据包的源地址、端口计算该包中的确认序列号是否正确,如果正确则建立连接,否则丢弃该包。

  • SYN Proxy防火墙:服务器防火墙会对收到的每一个SYN报文进行代理和回应,并保持半连接。等发送方将ACK包返回后,再重新构造SYN包发到服务器,建立真正的TCP连接。

问题十:怎样保证TCP可靠性?

这是一个综合性的问题,涉及上述讲好几个问题。

从以下几个方面回答:

  1. 首先要说,三次握手四次挥手是可靠性的基础
  2. 字节编号机制
  3. 滑动窗口机制
  4. 超时重传机制
  5. 快速恢复机制
  6. 拥塞避免机制
  7. 面试说这么多够了,会说的还能再扯一点其他的……

参考:

 

以上是关于C++经典面试问题的主要内容,如果未能解决你的问题,请参考以下文章

手把手写C++服务器(17):自测!TCP协议面试经典十连问

网易校园招聘历年经典面试题汇总:C++研发岗

腾讯校招历年经典面试汇总:C++研发岗

C++经典面试题汇总

今日头条校园招聘历年经典面试题汇总:C++研发岗

c++程序猿经典面试题