「生活可以更简单, 欢迎来到我的开源世界」
  1. day01
    1. 字节序
    2. 避免time_wait状态
    3. 守护、僵尸、孤儿进程概念
      1. 守护
  2. day02
    1. C++类成员函数的重载、覆盖、隐藏
    2. epoll中ET和LT的区别
    3. 动态链接与静态链接的区别
    4. 如何设计好的散列函数
    5. 哈希表解决冲突的方法及优缺点
  3. day03
  4. day04
    1. 虚函数实现机制
    2. 进程和线程的区别
    3. C++多态及其实现
    4. 堆和栈的区别
    5. 有名管道和无名管道的区别
    6. 哪些函数不能定义为虚函数
  5. day05
    1. 循环与递归的区别
    2. 内联函数的优点以及和宏定义的区别
    3. strcpy 和strncpy 的区别
      1. strcpy()函数用来复制字符串,其原型为:
      2. strncpy()用来复制字符串的前n个字符,其原型为:
    4. 内存对齐及其原因
    5. 死锁的定义,死锁的四个必要条件,如何避免死锁,如何解决死锁?
  6. day06
    1. C++模版是怎么实现的
    2. 一个C++程序从编译到运行都经历了哪些阶段
    3. select/poll/epoll的区别
    4. 进程间调度算法
    5. 多线程锁的种类
    6. 自旋锁与互斥锁区别
  7. day07
    1. 进程的通讯方式
    2. reactor和proactor模式
    3. get与post区别
    4. 为什么构造函数不能声明为虚函数,析构函数可以
    5. 滑动窗口协议的概念
  8. day08
    1. vector与list区别
    2. epoll ET下非阻塞读,为什么不能是阻塞
    3. extern C作用,为什么需要?
    4. 内存泄漏和内存溢出的区别和联系
    5. 类型转换有哪些?(四种类型转换,分别举例说明)
  9. day09
    1. HTTP跟HTTPS的区别
    2. struct跟class的区别
    3. 生产者,消费者模式
    4. 虚拟地址空间有什么好处?
  10. day10
    1. 对比vector和list和deque
      1. vector:相当于一个数组
      2. List:双向链表
      3. Deque:双端队列
      4. 使用区别
    2. 局部变量与全局变量
    3. 进程同步与互斥
    4. 输入一个网站,到显示页面的过程
    5. URL的解析过程
    6. HTTP 为什么要用TCP而不用UDP?
  11. day11
    1. C++面向对象的三个特性
    2. DNS是什么,ARP是什么
    3. socket阻塞与非阻塞情况下的recv、send、read、write返回值
    4. http报文的格式,头部包含哪些
  12. day12
    1. const、typedef、define、inline区别
    2. 静态成员函数和静态成员变量有什么意义?
    3. 迭代器删除元素的会发生什么?
    4. 必须在构造函数初始化式里进行初始化的数据成员有哪些?
    5. 模版特化的概念,为什么特化?
  13. day13
    1. C++进程内存空间分布
    2. tcp 如何实时监测断线情况?
    3. Linux下ps命令,以及查看内存当前使用状态的命令
    4. 四个常问命令
  14. day14
    1. 什么时候使用多进程和什么时候使用多线程
    2. 如何定位内存泄露?
    3. Linux 读写锁的作用
    4. 虚拟内存的分布,虚拟内存存在的原因
  15. day15
    1. 子进程继承了父进程的哪些东西
    2. vector如何扩容
    3. 怎么理解线程安全
    4. 如何实现可靠地UDP传输
    5. 若析构函数不声明为虚函数,会有什么后果?为什么?
  16. day16
    1. cookie跟session的区别
    2. 深拷贝浅拷贝
    3. 多态性都有哪些?
    4. STL中仿函数有什么用,和函数指针有什么不同,哪个效率高
    5. Ping的原理与工作过程
  17. day17
    1. 虚函数、纯虚函数
    2. i++是否原子操作?并解释为什么?
    3. TCP、UDP端口扫描的实现方式
    4. 什么是智能指针?写一个模板的智能指针
  18. day18
    1. C++各个容器的实现原理
    2. 讲解一下野指针
    3. 共享内存为什么可以实现进程通信
    4. 什么时候使用线程池(根据项目来问)?
    5. http1.0和1.1和2.0的区别?
  19. day19
    1. 内核态与用户态的区别?从用户态切换到内核态有哪几种方式?
    2. volatile关键字
    3. C++对象的生命周期
  20. day20
    1. connect可能会长时间阻塞,怎么解决?
    2. keepalive 是什么东西?如何使用?
    3. socket什么情况下可读?
    4. udp调用connect有什么作用?
    5. socket编程,如果client断电了,服务器如何快速知道?
2021-03-10

day01

字节序

机器字节序:内存地址存储由低到高,根据数据是先存高位还是低位到内存地址

网络字节序:大端字节序(Big-Endian)

避免time_wait状态

TCP主动关闭端的time_wait状态如何避免:首先服务器可以设置SO_REUSEADDR套接字选项(端口复用)来通知内核,如果端口忙,但TCP连接位于TIME_WAIT状态时可以重用端口。在一个非常有用的场景就是,如果你的服务器程序停止后想立即重启,而新的套接字依旧希望使用同一端口,此时SO_REUSEADDR选项就可以避免TIME_WAIT状态。

守护、僵尸、孤儿进程概念

守护

孤儿进程:

父进程由于某种原因结束了,但其子进程仍在运行,这些子进程就成了孤儿进程。

由init进程收养系统的孤儿进程,并在孤儿进程结束时进行回收资源。

作用:
在现实中用户可能刻意使进程成为孤儿进程,这样就可以让它与父进程会话脱钩,成为后面会介绍的守护进程。

僵尸进程:

什么是僵尸进程?

一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。

僵尸进程通过 ps 命令显示出来的状态为 Z(zombie)。

系统所能使用的进程号是有限的,如果产生大量僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。

要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵尸进程所占有的资源,从而结束僵尸进程

守护进程概念:

守护进程(daemon)是生存期长的一种进程,没有控制终端。它们常常在系统引导装入时启动,仅在系统关闭时才终止。UNIX系统有很多守护进程,守护进程程序的名称通常以字母“d”结尾。

守护进程是在后台运行不受终端控制的进程(如输入、输出等),一般的网络服务都是以守护进程的方式运行。守护进程脱离终端的主要原因有两点:(1)用来启动守护进程的终端在启动守护进程之后,需要执行其他任务。(2)(如其他用户登录该终端后,以前的守护进程的错误信息不应出现)由终端上的一些键所产生的信号(如中断信号),不应对以前从该终端上启动的任何守护进程造成影响。要注意守护进程与后台运行程序(即加&启动的程序)的区别。

需要注意的是,用户层守护进程的父进程是 init进程(进程ID为1),从上面的输出PPID一列也可以看出,内核守护进程的父进程并非是 init进程。对于用户层守护进程, 因为它真正的父进程在 fork 出子进程后就先于子进程 exit 退出了,所以它是一个由 init 继承的孤儿进程。

在创建守护进程之前,需要了解一些基础概念:

进程组 :

会话:会话(session)是一个或多个进程组的集合,进程调用 setsid 函数(原型:pid_t setsid(void) )建立一个会话。

进程调用 setsid 函数建立一个新会话,如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新会话。具体会发生以下3件事:

如果该调用进程已经是一个进程组的组长,则此函数返回出错。为了保证不处于这种情况,通常先调用fork,然后使其父进程终止,而子进程则继续。因为子进程继承了父进程的进程组ID,而其进程ID是重新分配的,两者不可能相等,这就保证了子进程不是一个进程组的组长。

创建守护进程的过程:

  1. 调用fork创建子进程。父进程终止,让子进程在后台继续执行。

  2. 子进程调用setsid产生新会话期并失去控制终端调用setsid()使子进程进程成为新会话组长和新的进程组长,同时失去控制终端。

  3. 忽略SIGHUP信号。会话组长进程终止会向其他进程发该信号,造成其他进程终止。

  4. 调用fork再创建子进程。子进程终止,子子进程继续执行,由于子子进程不再是会话组长,从而禁止进程重新打开控制终端。

  5. 改变当前工作目录为根目录。一般将工作目录改变到根目录,这样进程的启动目录也可以被卸掉。

  6. 关闭打开的文件描述符,打开一个空设备,并复制到标准输出和标准错误上。 避免调用的一些库函数依然向屏幕输出信息。

  7. 重设文件创建掩码清除从父进程那里继承来的文件创建掩码,设为0。

  8. 用openlog函数建立与syslogd的连接。

对于守护进程,需要遵守一些编写规则:

具体可参考以下代码(来自《APUE》一书):

作者:Zyoung
链接:https://www.zhihu.com/question/38609004/answer/529315259
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

void
daemonize(const char *cmd)
{
int i, fd0, fd1, fd2;
pid_t pid;
struct rlimit rl;
struct sigaction sa;

/*
* Clear file creation mask.
*/
umask(0);

/*
* Get maximum number of file descriptors.
*/
if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
err_quit("%s: can't get file limit", cmd);

/*
* Become a session leader to lose controlling TTY.
*/
if ((pid = fork()) < 0)
err_quit("%s: can't fork", cmd);
else if (pid != 0) /* parent */
exit(0);
setsid();

/*
* Ensure future opens won't allocate controlling TTYs.
*/
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGHUP, &sa, NULL) < 0)
err_quit("%s: can't ignore SIGHUP", cmd);
if ((pid = fork()) < 0)
err_quit("%s: can't fork", cmd);
else if (pid != 0) /* parent */
exit(0);

/*
* Change the current working directory to the root so
* we won't prevent file systems from being unmounted.
*/
if (chdir("/") < 0)
err_quit("%s: can't change directory to /", cmd);

/*
* Close all open file descriptors.
*/
if (rl.rlim_max == RLIM_INFINITY)
rl.rlim_max = 1024;
for (i = 0; i < rl.rlim_max; i++)
close(i);

/*
* Attach file descriptors 0, 1, and 2 to /dev/null.
*/
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);

/*
* Initialize the log file.
*/
openlog(cmd, LOG_CONS, LOG_DAEMON);
if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
syslog(LOG_ERR, "unexpected file descriptors %d %d %d",
fd0, fd1, fd2);
exit(1);
}
}

https://www.zhihu.com/question/38609004/answer/529315259

https://www.zhihu.com/question/38609004/answer/77190522

《APUE》一书第13章

day02

C++类成员函数的重载、覆盖、隐藏

  1. 成员函数被重载的特征:

    • 相同的范围(在同一个类中);
    • 函数名字相同;
    • 参数不同;
    • virtual 关键字可有可无。
  2. 覆盖是指派生类函数覆盖基类函数,特征是:

    • 不同的范围(分别位于派生类与基类);
    • 函数名字相同;
    • 参数相同;
    • 基类函数必须有virtual 关键字。
  3. 隐藏是指派生类的函数屏蔽了与其同名的基类函数,规则如下:

    • 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
    • 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

preview

epoll中ET和LT的区别

epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。

LT(水平触发)模式下,只要这个文件描述符还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作;

ET(边缘触发)模式下,在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,如可读,则必须将该文件描述符一直读到空,让 errno 返回 EAGAIN 为止,否则下次的 epoll_wait 不会返回余下的数据,会丢掉事件。如果ET模式不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞。

动态链接与静态链接的区别

静态链接和动态链接两者最大的区别就在于链接的时机不一样,静态链接是在形成可执行程序前,而动态链接的进行则是在程序执行时

静态链接的优缺点:

动态链接的优缺点:

如何设计好的散列函数

一个好的散列函数应该满足简单均匀散列

直接定址法

关键码本身和地址之间存在某个线性函数关系时,散列函数取为关键码的线性函数,即:

$f(key)=a*key+b$,a、b均为常数。

优点:简单、均匀,也不会产生冲突

不足:需要事先知道关键字的分布情况,适合査找表较小且连续的情况。由于这样的限制,在现实应用中,直接定址法虽然简单,但却并不常用。

数字分析法

假设关键码完全已知,且每个关键码都是以某个数r为基数(例以10为基数的十进制数)的值,则关键码中若干位恰能构成分布比较均匀的散列地址空间时,可取关键码的若干位的组合作为散列地址。

平方取中法

将关键码key平方,取$key^2$中间几位作为其散列地址f(key)的值。

假如有以下关键字序列{421,423,436},平方之后的结果为{177241,178929,190096},那么可以取{72,89,00}作为Hash地址。

适合于不知道关键字分布,而位数不是很大的情况

折叠法

折叠法是将关键字从左到右分割成位数相等的几部分(注意组后一部分位数不够的可以短些)

适用于事先不知道关键字分布,关键字位数较多的情况

除留余数法(最常用)

通过选择适当的正整数p,按计算公式$f(key)=key%p$来计算关键码key的散列地址。

若关键码个数为n,散列表表长为m(一般m>=n),通常选p为小于或等于表长m的最大素数或不包含小于20的质因子的合数,一般也要求p>=n。

这种方法计算最简单,也不需根据全部关键码的分布情况研究如何从中析取数据,最常用。

随机数法

选择一个随机数,取关键字的随机函数值为它的散列地址:$f(key)=random(key)$。

这里random是随机函数。当关键字长度不等时,采用这个方法构造散列函数比较合适。

哈希表解决冲突的方法及优缺点

1、开放定址法

开放定址:散列表的地址对任何记录数据都是开放的,即可存储使用。但散列表长度一旦确定,总的可用地址是有限的。闭散列表表长不小于所需存储的记录数,发生冲突总能找到空的散列地址将其存入。查找时,按照一种固定的顺序检索散列表中的相应项,直到找到一个关键字等于k或找到一个空单元将k插入,故是动态查找结构。

1)线性探测法

从发生冲突位置的下一个位置开始寻找空的散列地址。发生冲突时,线性探测下一个散列地址是:

$Hi=(H(key)+di)%m$,di=1,2,3…,m-1,闭散列表长度为m。它实际是按照H(key)+1,H(key)+2,…,m-1,0,1,H(key)-1的顺序探测,一旦探测到空的散列地址,就将关键码记录存入。

该方法会产生堆积现象,即使是同义词也可能会争夺同一个地址空间,今后在其上的查找效率会降低。

2)二次探测法

发生冲突时,下一位置的探测采用公式:

$Hi=(H(key)+di)%m$,($di=1^2,-1^2,2^2,-2^2,…..,q^2,-q^2,q<=\sqrt{m}$)

在一定程度上可解决线性探测中的堆积现象。

3)随机探测法

di为{1,2,3,…,m-1}中的数构成的一个随机数列中顺序取的一个数

4)再散列函数法

除基本散列函数外,事先设计一个散列函数序列,RH1,RH2,…,RHk,k为某个正整数。RHi均为不同的散列函数。对任一关键码,若在某一散列函数上发生冲突,则再用下一个散列函数,直到不发生冲突为止。

5)建立公共溢出区(单链表或顺序表实现)

另外开辟一个存储空间,当发生冲突时,把同义词均顺序放入该空间。若把散列表看成主表或父表,则公共的同义词表就是一个次表或子表。查找时,现在散列表中查,找不到时再去公共同义词子表顺序查找。

2、链地址法

将所有散列地址相同的记录存储在同一个单链表中,该单链表为同义词单链表,或同义词子表。该单链表头指针存储在散列表中。散列表就是个指针数组,下标就是由关键码用散列函数计算出的散列地址。初始,指针数组每个元素为空指针,相当于所有单链表头指针为空,以后每扫描到一条记录,按其关键码的散列地址,在相应的单链表中加入含该记录的节点。

开散列表容量可很大,仅受内存容量的限制。

day03

C++语法基础。

【C++技术面试基础知识总结.md】

day04

虚函数实现机制

  1. 当类中存在虚函数,则编译器会在编译期自动的给该类生成一个虚函数表,并在所有该类的对象中放入一个隐式变量vptr,该变量是一个指针变量,它的值指向那个类中的由编译器生成的虚函数表

  2. 每个类的虚函数的入口都在这张表中维护,调用方法的时候会隐式的传入一个this指针,然后系统会根据this指针找到对应的vptr,进而找到对应的虚函数表,找到真正方法的地址,然后才去调用这个方法,这可以叫动态绑定。

  3. 如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针。当基类的指针指向派生类的对象时,调用虚函数时都会根据vptr来选择虚函数,而基类的虚函数在派生类里已经被改写或者说已经不存在了,所以也就只能调用派生类的虚函数版本了

进程和线程的区别

根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位

在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)

内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。

数据共享:进程数据是分开的,共享复杂,需要用IPC,同步简单;多线程共享进程数据,共享简单,同步复杂

包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

其它(杂乱)

C++多态及其实现

c++的多态性就是通过晚绑定(动态多态)技术来实现的。

多态定义的构成条件

多态是不同继承关系的类对象去调同一函数,产生了不同的行为。就是说,有一对继承关系的两个类,这两个类里面都有一个函数且名字、参数、返回值均相同,然后我们通过调用函数来实现不同类对象完成不同的事件。

从虚函数的实现机制可以看到要想在子类中实现多态需要满足三个重要的条件:

(1)在基类中函数声明为虚函数。

(2)在子类中,对基类的虚函数进行了重写。

(3)基类的指针指向了子类的对象。

每个类用了一个虚表,每个类的对象用了一个虚指针。具体的用法如下:

class A
{
public:
virtual void f();
virtual void g();
private:
int a
};
class B : public A
{
public:
void g();
private:
int b;
};
//A,B的实现省略

因为A有virtual void f()和g(),所以编译器为A类准备了一个虚表vtableA,内容如下:

A::f 的地址
A::g 的地址

B因为继承了A,所以编译器也为B准备了一个虚表vtableB,内容如下:

A::f 的地址
B::g 的地址

注意:因为B::g是重写了的,所以B的虚表的g放的是B::g的入口地址,但是f是从上面的A继承下来的,所以f的地址是A::f的入口地址。
然后某处有语句 B bB;的时候,编译器分配空间时,除了A的int a,B的成员int b;以外,还分配了一个虚指针vptr,指向B的虚表vtableB,bB的布局如下:

vptr : 指向B的虚表vtableB
int a: 继承A的成员
int b: B成员

当如下语句的时候:
A *pa = &bB;
pa的结构就是A的布局(就是说用pa只能访问的到bB对象的前两项,访问不到第三项int b)
那么pa->g()中,编译器知道的是,g是一个声明为virtual的成员函数,而且其入口地址放在表格(无论是vtalbeA表还是vtalbeB表)的第2项,那么编译器编译这条语句的时候就如是转换:call *(pa->vptr)[1](C语言的数组索引从0开始哈~)。

堆和栈的区别

管理方式不同:

空间大小不同:

生长方向不同:

分配方式不同:

分配效率不同:

存放内容不同:

有名管道和无名管道的区别

相同点

open打开管道文件以后,在内存中开辟了一块空间,管道的内容在内存中存放,有两个指针—头指针(指向写的位置)和尾指针(指向读的位置)指向它。读写数据都是在给内存的操作,并且都是半双工通讯。

区别

有名在任意进程之间使用,无名在父子进程之间使用。

哪些函数不能定义为虚函数

  1. 友元函数:它不是类的成员函数
  2. 全局函数
  3. 静态成员函数:它没有this指针
  4. 构造函数,拷贝构造函数:对象未创建完成前没有虚表
  5. 赋值运算符重载(可以但是一般不建议作为虚函数)

day05

循环与递归的区别

递归:在一个函数(或者方法)体内调用这个函数自身,直到某个条件满足(否则会一直执行下去,直到栈内存溢出)。

循环:通过设置一个初始值和终止条件,并在这个范围内重复计算。

在编程中经常会遇到重复计算相同的问题,此时一般会采用递归或者循环来解决。无论是采用递归还是循环,都需要经历如下三步:

  1. 首先需要找出计算问题的规律,用数学计算公式表达出来;

  2. 然后再用代码编程来实现这个数学计算公式;

  3. 最后采用递归或者循环的方式多次运行这个数学计算公式,从而得出计算结果(为了保证程序的健壮性,往往还需要进行一些边界值处理)

内联函数的优点以及和宏定义的区别

内联函数是指用inline关键字修饰的函数。在类内定义的函数被默认成内联函数。内联函数不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处。编译时,类似宏替换,使用函数体替换调用处的函数名。一般在代码中用inline修饰,但是能否形成内联函数,需要看编译器对该函数定义的具体处理。

C++17 起:由于关键词 inline 对于函数的含义已经变为“容许多次定义”而不是“优先内联”,因此这个含义也扩展到了变量。

inline函数的优点

inline函数的缺点

inline与宏的区别

strcpy 和strncpy 的区别

本质区别:strcpy不安全,有可能copy越界

strcpy()函数用来复制字符串,其原型为:
char *strcpy(char *dest, const char *src);

dest 为目标字符串指针,src 为源字符串指针。

成功执行后返回目标数组指针 dest。

strcpy() 把src所指的由NULL结束的字符串复制到dest 所指的数组中,返回指向 dest 字符串的起始地址。

注意:src 和 dest 所指的内存区域不能重叠,且dest 必须有足够的空间放置 src 所包含的字符串(包含结束符NULL)。如果参数 dest 所指的内存空间不够大,可能会造成缓冲溢出(buffer Overflow)的错误情况,在编写程序时请特别留意,或者用strncpy()来取代。

strncpy()用来复制字符串的前n个字符,其原型为:
char * strncpy(char *dest, const char *src, size_t n);

dest 为目标字符串指针,src 为源字符串指针。

返回指向dest的指针(指向dest的最后一个元素)

strncpy()会将字符串src前n个字符拷贝到字符串dest。

不像strcpy(),strncpy()不会向dest追加结束标记’\0’,如果src的前n个字节不含NULL字符,则结果不会以NULL字符结束。

如果src的长度小于n个字节,则以NULL填充dest直到复制完n个字节。

注意:src 和 dest 所指的内存区域不能重叠,且dest 必须有足够的空间放置n个字符。

内存对齐及其原因

数据成员对齐规则

结构体(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储。

结构体作为成员

如果一个结构体里有结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储)

结构体的总大小,也就是sizeof的结果:必须是其内部最大成员的整数倍.不足的要补齐

为什么要内存对齐?

  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

  2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

死锁的定义,死锁的四个必要条件,如何避免死锁,如何解决死锁?

死锁:如果一组进程中的每一个进程都在等待仅由该组进程中的其它进程才能引发的事件,那么该组进程是死锁的。

产生死锁的原因

如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能产生死锁。

产生死锁的四个必要条件

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。因此可以写下如下的预防死锁的方法。

死锁避免的基本思想(安全性检查):系统对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配,这是一种保证系统不进入死锁状态的动态策略。

处理死锁的方法

day06

C++模版是怎么实现的

C++中提供了两种模板机制,分别是函数模板和类模板。

函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表。这个通用函数就称函数模板。凡是函数体相同的函数都可用这个模板来代替,不必定义多个函数,只需在模板中定义一次即可。在调用函数时系统会根据实参类型来取代模板中的虚拟内型,从而实现不同函数功能的调用。

类模板,使程序(算法)可以使逻辑上抽象,把处理的对象类型作为参数传递,凡是类体内的对象相同时可用这个类模板来代替,不必重复定义多个类。

一个C++程序从编译到运行都经历了哪些阶段

对于C/C++编写的程序,从源代码到可执行文件,一般经过下面四个步骤:

  1. 预处理:条件编译,头文件包含,宏替换的处理,生成.i文件。
  2. 编译:将预处理后的文件转换成汇编语言,生成.s文件
  3. 汇编:汇编变为目标代码(机器代码)生成.o的文件
  4. 链接:连接目标代码,生成可执行程序

select/poll/epoll的区别

1、支持一个进程所能打开的最大连接数

select:单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。

poll:poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。

epoll:虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接

2、FD剧增后带来的IO效率问题

select:因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。

poll:同上

epoll:因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

3、 消息传递方式

select:内核需要将消息传递到用户空间,都需要内核拷贝动作

poll:同上

epoll:epoll通过内核和用户空间共享一块内存来实现的。

进程间调度算法

调度算法是指:根据系统的资源分配策略所规定的资源分配算法

先来先服务调度算法

来先服务调度算法是一种最简单的调度算法,也称为先进先出或严格排队方案。当每个进程就绪后,它加入就绪队列。当前正运行的进程停止执行,选择在就绪队列中存在时间最长的进程运行。该算法既可以用于作业调度,也可以用于进程调度。先来先去服务比较适合于常作业(进程),而不利于段作业(进程)。在进程调度中,FCFS调度算法每次从就绪队列中选择最先进入该队列的进程,将处理机分配给它,使之投入运行,直到完成或因某种原因而阻塞时才释放处理机。

时间片轮转调度算法

片轮转调度算法主要适用于分时系统。在这种算法中,系统将所有就绪进程按到达时间的先后次序排成一个队列,进程调度程序总是选择就绪队列中第一个进程执行,即先来先服务的原则,但仅能运行一个时间片,如100ms。在使用完一个时间片后,即使进程并未完成其运行,它也必须释放出(被剥夺)处理机给下一个就绪的进程,而被剥夺的进程返回到就绪队列的末尾重新排队,等候再次运行。

短作业(SJF)优先调度算法

作业(进程)优先调度算法是指对短作业(进程)优先调度的算法。短作业优先(SJF)调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法,则是从就绪队列中选择一个估计运行时间最短的进程,将处理机分配给它,使之立即执行,直到完成或发生某事件而阻塞时,才释放处理机。

最短剩余时间优先

剩余时间是针对最短进程优先增加了抢占机制的版本。在这种情况下,进程调度总是选择预期剩余时间最短的进程。当一个进程加入到就绪队列时,他可能比当前运行的进程具有更短的剩余时间,因此只要新进程就绪,调度程序就能可能抢占当前正在运行的进程。像最短进程优先一样,调度程序正在执行选择函数是必须有关于处理时间的估计,并且存在长进程饥饿的危险。

多线程锁的种类

自旋锁与互斥锁区别

互斥锁(mutexlock):

最常使用于线程同步的锁;标记用来保证在任一时刻,只能有一个线程访问该对象,同一线程多次加锁操作会造成死锁;临界区和互斥量都可用来实现此锁,通常情况下锁操作失败会将该线程睡眠等待锁释放时被唤醒

自旋锁(spinlock):

同样用来标记只能有一个线程访问该对象,在同一线程多次加锁操作会造成死锁;使用硬件提供的swap指令或test_and_set指令实现;同互斥锁不同的是在锁操作需要等待的时候并不是睡眠等待唤醒,而是循环检测保持者已经释放了锁,这样做的好处是节省了线程从睡眠状态到唤醒之间内核会产生的消耗,在加锁时间短暂的环境下这点会提高很大效率(轮询获取锁)

day07

进程的通讯方式

1. 管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。

2.FIFO,也称为命名管道,它是一种文件类型。

3.消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。

4.信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

5.共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。

6.套接字

7.条件变量

reactor和proactor模式

Reactor模式

要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即将事件通知工作线程(逻辑单元)数据的读写,接受新的连接以及处理客户请求均在工作线程中完成;除此之外,逻辑线程不作任何工作。

  1. 主线程往epoll内核事件表中注册socket上的读就绪事件
  2. 主线程调用epoll_wait等待socket上有数据可读
  3. 当socket上有数据可读时,epoll_wait通知主线程,主线程则将socket可读事件放入请求队列。
  4. 睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket上的写就绪事件。
  5. 主线程调用epoll_wait等待socket可写
  6. 当socket可写时,epoll_wait通知主线程,主线程将socket可写事件放入请求队列
  7. 睡眠在请求队列上的某个工作线程(工作线程从请求队列读取事件后,根据事件的类型来决定如何处理它,没有必要区分读工作线程和写工作线程)被唤醒,它往socket上写入服务器处理客户请求的结果

image-20210311170716000

proactor模式

Proactor将所有I/O操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑。使用异步I/O模型(aio_read和aio_write)来实现Proactor模式的工作流程是:

  1. 主线程调用aio_read向内核注册socket上的读完成事件,并告诉内核用户缓冲区的位置,以及读操作完成时如何通知应用程序(可以用信号)
  2. 主线程继续处理其他逻辑
  3. 当socket上的读数据被读入用户缓冲区后,内核向应用进程发送一个信号,已通知应用程序数据已经可用
  4. 应用进程预先定义好的信号处理函数选择一个工作线程来处理处理客户请求,工作线程处理完客户请求之后,调用aio_write向内核注册socket的完成写事件,并告诉内核用户写缓冲区的位置,以及操作完成时如何通知应用程序(可以用信号)
  5. 主线程继续处理其他逻辑
  6. 当用户缓冲区的数据被写入socket之后,内核将向应用程序发送一个信号,已通知应用程序数据已经发送完毕
  7. 应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭socket

image-20210311173141404

get与post区别

  1. url可见性:get传参方式是通过地址栏URL传递,是可以直接看到get传递的参数,post传参方式参数URL不可见,get把请求的数据在URL后通过?连接,通过&进行参数分割。psot将从参数存放在HTTP的包体内
  2. 传输数据大小:get传递数据是通过URL进行传递,对传递的数据长度是受到URL大小的限制,URL最大长度是2048个字符。post没有长度限制
  3. 后退页面:get后退不会有影响,post后退会重新进行提交
  4. 缓存:get请求可以被缓存,post不可以被缓存
  5. 编码方式:get请求只有URL编码,post支持多种编码方式
  6. 历史记录:get请求的记录会留在历史记录中,post请求不会留在历史记录
  7. 字符类型:get只支持ASCII字符,post没有字符类型限制

为什么构造函数不能声明为虚函数,析构函数可以

构造函数不能声明为虚函数的原因是:

析构函数设为虚函数的作用:

滑动窗口协议的概念

滑动窗口协议属于TCP协议的一种应用,用于网络数据传输时的流量控制,以避免拥塞的发生。该协议允许发送方在停止并等待确认前发送多个数据分组。由于发送方不必每发一个分组就停下来等待确认。因此该协议可以加速数据的传输,提高网络吞吐量。

TCP利用一个滑动的窗口来告诉发送端对它所发送的数据能够提供多大的缓冲区,由16位定义,最大为65535个字节。滑动窗口本质上是描述接收方的数据报缓冲区大小的数据,发送方根据这个数据来计算自己最多能发送多长的数据。这个窗口大小为0时,发送方将停止发送数据。启动坚持定时器,等待这个窗口变成非0。

day08

vector与list区别

epoll ET下非阻塞读,为什么不能是阻塞

ET 模式是一种边沿触发模型,在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,对于每一个被通知的文件描述符,如可读,则必须将该文件描述符一直读到空,让 errno 返回 EAGAIN 为止,否则下次的 epoll_wait 不会返回余下的数据,会丢掉事件。而如果你的文件描述符如果不是非阻塞的,那这个一直读或一直写势必会在最后一次阻塞。

extern C作用,为什么需要?

作用:实现c++代码能够调用其他c语言代码,加上extern “C”后,这部分代码编译器以c语言的方式进行编译和链接,而不是按c++方式。

原因:c和c++对同一个函数经过编译后生成的函数名是不同的,由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。如果在c++中调用一个使用c语言编写的模块中的某个函数,那么c++是根据c++的名称修饰方式来查找并链接这个函数,那么就会发生链接错误。

内存泄漏和内存溢出的区别和联系

内存泄漏(memory leak):是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。

内存溢出(out of memory): OOM,即所谓的内存溢出。指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错。

二者的关系

类型转换有哪些?(四种类型转换,分别举例说明)

reinterpret_cast

  1. 该函数将一个类型的指针转换为另一个类型的指针
  2. 这种转换不用修改指针变量值存放格式(不改变指针变量值),只需在编译时重新解释指针的类型就可做到.
  3. reinterpret_cast可以将指针值转换为一个整型数,但不能用于非指针类型的转换。
//基本类型指针的类型转换 
double d=9.2;
double* pd = &d;
int *pi = reinterpret_cast<int*>(pd); //相当于int *pi = (int*)pd;
//不相关的类的指针的类型转换
class A{};
class B{};
A* pa = new A;
B* pb = reinterpret_cast<B*>(pa); //相当于B* pb = (B*)pa;
//指针转换为整数
long l = reinterpret_cast<long>(pi); //相当于long l = (long)pi;

const_cast

  1. 该函数用于去除指针变量的常量属性,将它转换为一个对应指针类型的普通变量。反过来,也可以将一个非常量的指针变量转换为一个常指针变量。
  2. 这种转换是在编译期间做出的类型更改。
const int* pci = 0; 
int* pk = const_cast<int*>(pci); //相当于int* pk = (int*)pci;

const A* pca = new A;
A* pa = const_cast<A*>(pca); //相当于A* pa = (A*)pca;

出于安全性考虑,const_cast无法将非指针的常量转换为普通变量。

static_cast

  1. 该函数主要用于基本类型之间和具有继承关系的类型之间的转换。
  2. 这种转换一般会更改变量的内部表示方式,因此,static_cast应用于指针类型转换没有太大意义。
//基本类型转换 
int i=0;
double d = static_cast<double>(i); //相当于 double d = (double)i;
//转换继承类的对象为基类对象
class Base{};
class Derived : public Base{};
Derived d;
Base b = static_cast<Base>(d); //相当于 Base b = (Base)d;

dynamic_cast

  1. 它与static_cast相对,是动态转换。
  2. 这种转换是在运行时进行转换分析的,并非在编译时进行,明显区别于上面三个类型转换操作。
  3. 该函数只能在继承类对象的指针之间或引用之间进行类型转换。进行转换时,会根据当前运行时类型信息,判断类型对象之间的转换是否合法。dynamic_cast的指针转换失败,可通过是否为null检测,引用转换失败则抛出一个bad_cast异常。
例: class Base{}; 
class Derived : public Base{};
//派生类指针转换为基类指针
Derived *pd = new Derived;
Base *pb = dynamic_cast<Base*>(pd);
if (!pb)
cout << "类型转换失败" << endl;

//没有继承关系,但被转换类有虚函数
class A(virtual ~A();) //有虚函数
class B{}:
A* pa = new A;
B* pb = dynamic_cast<B*>(pa);

如果对无继承关系或者没有虚函数的对象指针进行转换、基本类型指针转换以及基类指针转换为派生类指针,都不能通过编译。

day09

HTTP跟HTTPS的区别

HTTP:是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),用于从WWW服务器传输超文本到本地浏览器的传输协议,它可以使浏览器更加高效,使网络传输减少。

HTTPS:是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。HTTPS协议的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性。

区别:

struct跟class的区别

  1. 内部成员变量及成员函数的默认防控属性:struct默认防控属性是public的,而class默认的防控属性是private的

  2. 继承关系中默认防控属性的区别:在继承关系,struct默认是public的,而class是private

  3. 模板中的使用:class这个关键字还可用于定义模板参数,就像typename。但是strcut不用与定义模板参数

生产者,消费者模式

生产者消费者模式是Controlnet网络中特有的一种传输数据的模式,设置方便,使用安全快捷。

生产者消费者模式为信息传输开辟了一个崭新的概念,因为它的优先级最高,所以即使网络发生堵塞时它也会最先通过,最大程度的保证了设备的安全。也有缺点,就是在网络中的个数是有限制的。生产者消费者模式在设置时比较简单,使用方便安全。

虚拟地址空间有什么好处?

  1. 扩大地址空间;
  2. 内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。
  3. 公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。
  4. 当进程通信时,可采用虚存共享的方式实现。
  5. 当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存
  6. 虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高
  7. 在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片

day10

对比vector和list和deque

vector:相当于一个数组

在内存中分配一块连续的内存空间进行存储。支持不指定vector大小的存储。STL内部实现时,首先分配一个非常大的内存空间预备进行存储,即capacity()函数返回的大小,当超过此分配的空间时再整体重新放分配一块内存存储,这给人以vector可以不指定vector即一个连续内存的大小的感觉。通常此默认的内存分配能完成大部分情况下的存储。

优点:

缺点:

List:双向链表

每一个结点都包括一个信息块Info、一个前驱指针Pre、一个后驱指针Next。可以不分配必须的内存大小方便的进行添加和删除操作。使用的是非连续的内存空间进行存储。

优点:

缺点:

Deque:双端队列

deque是在功能上合并了vector和list。

优点:

缺点:

使用区别

局部变量与全局变量

局部变量

局部变量出现在三种地方

  1. 在函数内定义的变量
  2. 在复合语句内定义的变量
  3. 形式参数

局部变量的作用域在其所定义的语句块中(即花括号包含)

全局变量

一个源文件可以包含若干个函数,在函数之外定义的变量称为全局变量。全局变量可以为本文件中其它的函数所共用,他的有效范围从定义变量的开始位置到本源文件结束。

  1. 全局变量在程序的全部执行过程中都占用存储单元,而不是仅在需要时才开辟单元。
  2. 它使函数的通用性降低,如果在函数中引用了全局变量,那么执行情况会受到有关的外部变量的影响。如果将一个函数移到另一个文件中,还要考虑把有关的外部变量及其值一起弄过去。

总体来说,定义在函数内部的变量为局部变量,定义在函数外部的变量为全局变量。\

进程同步与互斥

同步

同步亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作

例如,输入进程A通过单缓冲向进程B提供数据。当该缓冲区空时,进程B不能获得所需数据而阻塞,一旦进程A将数据送入缓冲区,进程B被唤醒。反之,当缓冲区满时,进程A被阻塞,仅当进程B取走缓冲数据时,才唤醒进程A。

互斥

互斥亦称间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待, 当占用临界资源的进程退出临界区后,另一进程才允许去访问此临界资源。

例如,在仅有一台打印机的系统中,有两个进程A和进程B,如果进程A需要打印时, 系统已将打印机分配给进程B,则进程A必须阻塞。一旦进程B将打印机释放,系统便将进程A唤醒,并将其由阻塞状态变为就绪状态。

为禁止两个进程同时进入临界区,同步机制应遵循以下准则:

输入一个网站,到显示页面的过程

URL的解析过程

  1. 在浏览器地址栏输入url地址,按下回车键
  2. 浏览器获取url进行域名解析,首先从本地DNS缓存查找,如果本地没有则去DNS服务器查找,如果都没有找到,则浏览器返回请求失败
  3. DNS解析出请求地址,浏览器想这个地址发送请求
  4. 进行tcp三次握手建立连接
  5. tcp/ip连接建立后,浏览器向服务器发送http请求,服务处理请求并返回相应的资源(如果有缓存就在缓存中去)
  6. 客户端下载资源,浏览器将内容展示到窗口

HTTP 为什么要用TCP而不用UDP?

  1. udp链接不安全,不可靠,主要应用在不安全性要求不高,效率要求比较高的应用程序,比如聊天程序
  2. http协议只定义了应用层的东西,下层的可靠性要传输层来保证,但是没有说一定要用tcp,只要是可以保证可靠性传输层协议都可以承载http,比如有基于sctp的http实现。
  3. 再分析一下TCP/UDP之间的区别

day11

C++面向对象的三个特性

面向对象的三个基本特征是:

DNS是什么,ARP是什么

DNS(Domain Name System,域名系统),因特网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。通过主机名,最终得到该主机名对应的IP地址的过程叫做域名解析(或主机名解析)。

DNS协议运行在UDP协议之上,使用端口号53。

DNS解析过程涉及将主机名转换为计算机友好的IP地址

ARP即地址解析协议,实现通过IP地址得知其物理地址。在TCP/IP网络环境下,每个主机都分配了一个32位的IP地址,这种互联网地址是在网络范围标识主机的一种逻辑地址。为了让报文在物理网路上传送,必须知道对方目的主机的物理地址。这样就存在把IP地址变换成物理地址的地址转换问题。以以太网环境为例,为了正确地向目的主机传送报文,必须把目的主机的32位IP地址转换成为48位以太网的地址。这就需要在互连层有一组服务将IP地址转换为相应物理地址,这组协议就是ARP协议。

socket阻塞与非阻塞情况下的recv、send、read、write返回值

recv:

阻塞与非阻塞recv返回值没有区分,都是<0出错,=0连接关闭,>0接收数据大小

特别:非阻塞模式下返回值<0时并且(errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情况下认为连接是正常的,继续接收。

阻塞模式下recv会阻塞着接收数据,非阻塞模式下如果没有数据会返回,不会阻塞着读,因此需要循环读取。

send:

阻塞与非阻塞send返回值没有区分,都是<0出错,=0连接关闭,>0发送数据大小

特别:非阻塞模式下返回值<0时并且 (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情况下认为连接是正常的,继续发送。

阻塞模式下send会阻塞着发送数据,非阻塞模式下如果暂时无法发送数据会返回,不会阻塞着 send,因此需要循环发送。

read:

阻塞与非阻塞read返回值没有区分,都是<0出错,=0连接关闭,>0接收数据大小

特别:非阻塞模式下返回值<0时并且 (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情况下认为连接是正常的,继续接收。

阻塞模式下read会阻塞着接收数据,非阻塞模式下如果没有数据会返回,不会阻塞着读,因此需要循环读取。

write:

阻塞与非阻塞write返回值没有区分,都是<0出错,=0连接关闭,>0发送数据大小

特别:非阻塞模式下返回值<0时并且 (errno == EINTR || errno == EWOULDBLOCK || errno == EAGAIN)的情况下认为连接是正常的,继续发送。

阻塞模式下write会阻塞着发送数据,非阻塞模式下如果暂时无法发送数据会返回,不会阻塞着 write,因此需要循环发送。

http报文的格式,头部包含哪些

一个http请求报文由四部分组成:请求行(request line)、消息头部(header)、空行、请求正文。 

image-20210312102919610

首部行由key/value键值对组成,每行一对,key和value用冒号”:”分隔,请求头部通知服务器有关于client端的请求信息,典型的请求头:

day12

const、typedef、define、inline区别

typedef

define

C++ 中推荐使用 const 代替 #define 声明常量

C++ 中推荐使用 inline 代替 #define 声明函数

静态成员函数和静态成员变量有什么意义?

静态成员变量

静态成员函数

迭代器删除元素的会发生什么?

关联容器

对于关联容器(如map,set,multimap,multiset),删除当前的iterator,仅仅会使当前的iterator失效,只要在erase时,递增当前的iterator即可。这是因为map之类的容器,使用了红黑树来实现,插入,删除一个结点不会对其他结点造成影响。使用方式如下例子:

set<int> valset = { 1,2,3,4,5,6 };
set<int>::iterator iter;
for (iter = valset.begin(); iter != valset.end(); )
{
if (3 == *iter)
valset.erase(iter++);
else
++iter;
}

因为传给erase的是iter的一个副本,iter++是下一个有效的迭代器。

序列式容器

对于序列式容器(如vector,deque,list等),删除当前的iterator会使后面所有元素的iterator都失效。这是因为vector,deque使用了连续分配的内存,删除一个元素导致后面所有的元素会向前移动一个位置。不过erase方法可以返回下一个有效的iterator。使用方式如下,例如:

vector<int> val = { 1,2,3,4,5,6 };
vector<int>::iterator iter;
for (iter = val.begin(); iter != val.end(); )
{
if (3 == *iter)
iter = val.erase(iter); //返回下一个有效的迭代器,无需+1
else
++iter;
}

必须在构造函数初始化式里进行初始化的数据成员有哪些?

  1. 常量成员,const修饰的成员变量,因为常量只能初始化不能赋值,所以也要写在初始化列表里面;
  2. 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面;
  3. 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化。

模版特化的概念,为什么特化?

C++中经常为了避免重复的编码而需要使用到模板,这是C++泛型编程不可或缺的利器。然而通常又有一些特殊的情况,不能直接使用泛型模板展开实现,这时就需要针对某个特殊的类型或者是某一类特殊的类型,而实现一个特例模板——即模板特化。通常会使用到模板特化的有类模板和函数模板。

day13

C++进程内存空间分布

img

栈从高到低分配,堆从低到高分配

为什么要将初始化和未初始化的变量分别存放在两个区:

  • 内存是否被分配的区别
  • 未初始化的放在bss区,在程序启动时可以统一调用memset。

tcp 如何实时监测断线情况?

TCP正常的断开,通信双方(服务端和客户端)都是能知道的。但是非正常的断开,比如直接拔掉了网线,就只能靠如下两种方法,实现短时间内的检测。

1、心跳包机制

心跳包机制,是网游设计中的常用机制。从用户层面,自己发包去判断对方连线状态。可以根据情况,很灵活的使用。比如,20秒发送一个最小的数据包(也可以根据实际情况稍带一些其他数据)。如果发送没有回应,就判断对方掉线了。断开后立即关闭socket。

2、利用tcp_keepalive机制

利用TCP的机制,通过设置系统参数,从系统层面,监测tcp的连接状态。在配置socket属性时,使用 keepalive option,一旦有此配置,这些长时间无数据的链接会根据tcp的keepalive内核属性,在大于(tcp_keepalive_time)所对应的时间(单位为秒)之后,断开这些链接。

关于keep alive无论windows,还是linux,keepalive就三个参数

对于一个已经建立的tcp连接,如果在keepalive_time时间内双方没有任何的数据包传输,则开启keepalive功能的一端将发送 keepalive数据包,若没有收到应答,则每隔keepalive_intvl时间再发送该数据包,发送keepalive_probes次。一直没有收到应答,则发送rst包关闭连接。若收到应答,则将计时器清零。

Linux下ps命令,以及查看内存当前使用状态的命令

ps命令就是最基本进程查看命令。使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵尸、哪些进程占用了过多的资源等等。总之大部分信息都是可以通过执行该命令得到。ps是显示瞬间进程的状态,并不动态连续;如果想对进程进行实时监控应该用top命令。

ps参数

-A :所有的进程均显示出来,与 -e 具有同样的效用;
-a :显示现行终端机下的所有进程,包括其他用户的进程;
-u :以用户为主的进程状态 ;
-x :可与 -a,-e参数一起使用,可列出较完整信息。

查看内存当前使用状态的命令

1. free

-b  以Byte为单位显示内存使用情况。
-k  以KB为单位显示内存使用情况。
-m  以MB为单位显示内存使用情况。
-o  不显示缓冲区调节列。
-s<间隔秒数>  持续观察内存使用状况。
-t  显示内存总和列。
-V  显示版本信息。
-h 人性化方式显示数值:单位取 M、G等(这是一个通用参数,很多命令都可以带这个参数。)

2. df

-a :列出所有的文件系统,包括系统特有的 /proc 等文件系统;
-k :以 KBytes 的容量显示各文件系统;
-m :以 MBytes 的容量显示各文件系统;
-h :以人们较易阅读的 GBytes, MBytes, KBytes 等格式自行显示;
-H :以 M=1000K 取代 M=1024K 的进位方式;
-T :显示文件系统类型, 连同该 partition 的 filesystem 名称 (例如 ext3) 也列出;
-i :不用硬盘容量,而以 inode 的数量来显示

3.du

-a :列出所有的文件与目录容量,因为默认仅统计目录底下的文件量而已。
-h :以人们较易读的容量格式 (G/M) 显示;
-s :列出总量而已,而不列出每个各别的目录占用容量;
-S :不包括子目录下的总计,与 -s 有点差别。
-k :以 KBytes 列出容量显示;
-m :以 MBytes 列出容量显示;

四个常问命令

  1. netstat :显示网络状态
  2. tcpdump:主要是截获通过本机网络接口的数据,用以分析。能够截获当前所有通过本机网卡的数据包。它拥有灵活的过滤机制,可以确保得到想要的数据。
  3. ipcs:检查系统上共享内存的分配
  4. ipcrm:手动解除系统上共享内存的分配

day14

什么时候使用多进程和什么时候使用多线程

如何定位内存泄露?

内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的、大小任意的(内存块的大小可以在程序运行期决定)、使用完后必须显示释放的内存。应用程序一般使用malloc、realloc、new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块。否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。

C++程序缺乏相应的手段来检测内存信息,只能使用top指令观察进程的动态内存总额。而且程序退出时,我们无法获知任何内存泄漏信息。

Linux 读写锁的作用

读写锁其实还是一种锁,是给一段临界区代码加锁,但是此加锁是在进行写操作的时候才会互斥,而在进行读的时候是可以共享的进行访问临界区的。

读写锁和互斥量(互斥锁)很类似,是另一种线程同步机制,但不属于POSIX标准,可以用来同步同一进程中的各个线程。

读写锁的使用规则

读写锁本质上是一种自旋锁

互斥锁与读写锁的区别:

  1. 当访问临界区的资源时(访问的含义包括所有的操作),需要上互斥锁;
  2. 当对数据(互斥锁中的临界区资源)进行读取时,需要上读取锁,当对数据进行写入时,需要上写入锁。

读写锁的优点:

对于读数据较修改数据频繁的应用,用读写锁代替互斥锁可以提高效率。因为使用互斥锁时,即使是读出数据(相当于操作临界区资源)都需要上互斥锁;而采用读写锁则允许在任一时刻多个读出者存在,提高了并发性。

为什么需要读写锁?

有时候,在多线程中,有一些公共数据修改的机会比较少,而读的机会却是非常多的,此公共数据的操作基本都是读,如果每次操作都给此段代码加锁,太浪费时间了而且也很浪费资源,降低程序的效率,因为读操作不会修改数据,只是做一些查询,所以在读的时候不用给此段代码加锁,可以共享的访问,只有涉及到写的时候,互斥的访问就好了

虚拟内存的分布,虚拟内存存在的原因

32位系统中,虚拟内存分布:

虚拟内存存在的原因

在系统中所有的进程之间是共享CPU和主存这些内存资源的。当进程数量变多时,所需要的内存资源就会相应的增加。可能会导致部分程序没有主存空间可用。此外,由于资源是共享的,那么就有可能导致某个进程不小心写了另一个进程所使用的内存,进而导致程序运行不符合正常逻辑。

虚拟内存提供了三个重要的能力: 缓存,内存管理,内存保护

  1. 将主存视为一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据
  2. 为每个进程提供了一致的地址空间,简化内存管理
  3. 保护了每个进程的地址空间不被其他进程破坏

day15

子进程继承了父进程的哪些东西

子进程继承父进程:

子进程独有

父进程和子进程拥有独立的地址空间和PID参数。

vector如何扩容

总结

  1. vector在push_back以成倍增长可以在均摊后达到O(1)的事件复杂度,相对于增长指定大小的O(n)时间复杂度更好。
  2. 为了防止申请内存的浪费,现在使用较多的有2倍与1.5倍的增长方式,而1.5倍的增长方式可以更好的实现对内存的重复利用,因而更好。

怎么理解线程安全

当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的

通过同步和互斥可以来保证线程的安全

互斥:通过保证同一时间只有一个执行流可以对临界资源进行访问(一个执行流访问期间,其它执行流不能访问),来保证数据访问的安全性。通过互斥锁实现互斥。

同步:通过一些条件判断来实现多个执行流对临界资源访问的合理性(有资源则访问,没有资源则等待,等有了资源再被唤醒)。条件变量:一个pcb等待队列 + 向外提供一个使pcb等待以及唤醒的接口。条件变量可以通过提供的等待队列和等待唤醒接口实现线程间的同步。

如何实现可靠地UDP传输

UDP不属于连接协议,具有资源消耗少,处理速度快的优点,所以通常音频,视频和普通数据在传送时,使用UDP较多,因为即使丢失少量的包,也不会对接受结果产生较大的影响。

传输层无法保证数据的可靠传输,只能通过应用层来实现了。实现的方式可以参照tcp可靠性传输的方式,只是实现不在传输层,实现转移到了应用层。最简单的方式是在应用层模仿传输层TCP的可靠性传输。

下面不考虑拥塞处理,可靠UDP的简单设计。

  1. 添加seq/ack机制,确保数据发送到对端
  2. 添加发送和接收缓冲区,主要是用户超时重传
  3. 添加超时重传机制

详细说明

若析构函数不声明为虚函数,会有什么后果?为什么?

如果基类的析构函数不是虚函数,在特定情况下会导致派生类无法被析构。

day16

cookie跟session的区别

区别:

  1. cookie数据存放在客户的浏览器上,session数据放在服务器上。
  2. cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗。考虑到安全应当使用session。
  3. session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能。考虑到减轻服务器性能方面,应当使用cookie。
  4. 单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。

深拷贝浅拷贝

深浅拷贝区别:

注意:浅拷贝多个对象共用一个资源,当一个对象销毁时,资源就会释放。如果对另一个对象进行销毁,会因为资源重复释放造成程序崩溃!

多态性都有哪些?

多态是指同样的消息被不同类型的对象接受时导致不同的行为。所谓消息是指对类的成员函数的调用,不同的行为是指不同的实现,也就调用不同的函数。换言之,多态指的就是用同样的接口访问功能不同的函数,从而实现“一个接口,多种方法”。

多态性分类:

STL中仿函数有什么用,和函数指针有什么不同,哪个效率高

在函数对象的方式中,内联inline有效,而作为函数指针时,一般编译器都不会内联函数指针指向的函数,即使指定了inline,使用函数对象一般是裸函数的1.5倍,最多能快2倍多

Ping的原理与工作过程

ping 的原理

ping 程序是用来探测主机到主机之间是否可通信,如果不能 ping到某台主机,表明不能和这台主机建立连接。ping 使用的是ICMP协议,它发送icmp回送请求消息给目的主机。

ICMP协议规定:目的主机必须返回ICMP回送应答消息给源主机。如果源主机在一定时间内收到应答,则认为主机可达。

Ping工作过程

假定主机A的IP地址是192.168.1.1,主机B的IP地址是192.168.1.2,都在同一子网内,则当你在主机A上运行“ Ping 192.168.1.2”后,都发生了些什么呢?

首先, Ping命令会构建一个固定格式的ICMP请求数据包,然后由ICMP协议将这个数据包连同地址“192.168.1.2”一起交给 IP层协议(和ICMP一样,实际上是一组后台运行的进程),IP层协议将以地址“192.168.1.2”作为目的地址,本机IP地址作为源地址,加上一些其他的控制信息,构建一个IP数据包,并在一个映射表中查找出IP地址192.168.1.2所对应的 物理地址(也叫MAC地址,这是数据链路层协议构建数据链路层的传输单元——帧所必需的),一并交给数据链路层。后者构建一个数据帧,目的地址是IP层传过来的物理地址,源地址则是本机的物理地址,还要附加上一些控制信息,依据以太网的介质访问规则,将它们传送出去。

其中映射表由ARP实现。ARP(Address Resolution Protocol)是地址解析协议,是一种将IP地址转化成物理地址的协议。ARP具体说来就是将网络层(IP层,也就是相当于OSI的第三层)地址解析为数据连接层(MAC层,也就是相当于OSI的第二层)的MAC地址。

主机B收到这个数据帧后,先检查它的目的地址,并和本机的物理地址对比,如符合,则接收;否则丢弃。接收后检查该数据帧,将IP数据包从帧中提取出来,交给本机的IP层协议。同样,IP层检查后,将有用的信息提取后交给ICMP协议,后者处理后,马上构建一个ICMP应答包,发送给主机A,其过程和主机A发送ICMP请求包到主机B一模一样。即先由IP地址,在网络层传输,然后再根据mac地址由数据链路层传送到目的主机

day17

虚函数、纯虚函数

  1. 虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类。

  2. 虚函数可以被直接使用,也可以被子类重载以后以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类只有声明而没有定义。

  3. 虚函数和纯虚函数都可以在子类中被重载,以多态的形式被调用。

  4. 虚函数和纯虚函数通常存在于抽象基类之中,被继承的子类重载,目的是提供一个统一的接口。

  5. 虚函数的定义形式:virtual {method body}

    纯虚函数的定义形式:virtual { } = 0;

  6. 虚函数必须实现,如果不实现,编译器将报错。

  7. 对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。

  8. 实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。

  9. 虚函数是C++中用于实现多态的机制。核心理念就是通过基类访问派生类定义的函数

  10. 多态性指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。

    • 编译时多态性:通过重载函数实现
    • 运行时多态性:通过虚函数实现。
  11. 如果一个类中含有纯虚函数,那么任何试图对该类进行实例化的语句都将导致错误的产生,因为抽象基类(ABC)是不能被直接调用的。必须被子类继承重载以后,根据要求调用其子类的方法。

  12. 在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时候要求前期bind,然而虚函数却是动态绑定,而且被两者修饰的函数生命周期也不一样。

i++是否原子操作?并解释为什么?

i++的操作分三步:

  1. 栈中取出i
  2. i自增1
  3. 将i存到栈

所以i++不是原子操作,上面的三个步骤中任何一个步骤同时操作,都可能导致i的值不正确自增

TCP、UDP端口扫描的实现方式

TCP

UDP

什么是智能指针?写一个模板的智能指针

智能指针:实际指行为类似于指针的类对象 ,它的一种通用实现方法是采用引用计数的方法。

引入辅助类示例

//基础对象类,要做一个对Point类的智能指针
class Point
{
public:
Point(int xVal = 0, int yVal = 0):x(xVal),y(yVal) { }
int getX() const { return x; }
int getY() const { return y; }
void setX(int xVal) { x = xVal; }
void setY(int yVal) { y = yVal; }
private:
int x,y;
};

//辅助类,该类成员访问权限全部为private,因为不想让用户直接使用该类
class RefPtr
{
friend class SmartPtr;//定义智能指针类为友元,因为智能指针类需要直接操纵辅助类
RefPtr(Point *ptr):p(ptr), count(1) { }
~RefPtr() { delete p; }

int count; //引用计数
Point *p; //基础对象指针
};

//智能指针类
class SmartPtr
{
public:
SmartPtr(Point *ptr):rp(new RefPtr(ptr)) { } //构造函数
SmartPtr(const SmartPtr &sp):rp(sp.rp) { ++rp->count; } //复制构造函数
SmartPtr& operator=(const SmartPtr& rhs) { //重载赋值操作符
++rhs.rp->count; //首先将右操作数引用计数加1,
if(--rp->count == 0) //然后将引用计数减1,可以应对自赋值
delete rp;
rp = rhs.rp;
return *this;
}
~SmartPtr() { //析构函数
if(--rp->count == 0) //当引用计数减为0时,删除辅助类对象指针,从而删除基础对象
delete rp;
}

private:
RefPtr *rp; //辅助类对象指针
};

day18

C++各个容器的实现原理

讲解一下野指针

1.野指针与垂悬指针的区别:

2.概念

指针指向了一块随机的空间,不受程序控制。

3.野指针产生的原因:

  1. 指针定义时未被初始化:指针在被定义的时候,如果程序不对其进行初始化的话,它会随机指向一个区域,因为任意指针变量(除了static修饰的指针)它的默认值都是随机的
  2. 指针被释放时没有置空:我们在用malloc()开辟空间的时候,要检查返回值是否为空,如果为空,则开辟失败;如果不为空,则指针指向的是开辟的内存空间的首地址。指针指向的内存空间在用free()和delete释放后,如果程序员没有对其进行置空或者其他赋值操作的话,就会成为一个野指针
  3. 指针操作超越变量作用域:不要返回指向栈内存的指针或者引用,因为栈内存在函数结束的时候会被释放。

4.野指针的危害

问题:指针指向的内容已经无效了,而指针没有被置空,解引用一个非空的无效指针是一个未被定义的行为,也就是说不一定导致错误,野指针被定位到是哪里出现问题,在哪里指针就失效了,不好查找错误的原因。

5.规避方法:

共享内存为什么可以实现进程通信

共享内存,顾名思义就是允许两个不相关的进程访问同一个逻辑内存,共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一段物理内存。进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

特别提醒:共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取,所以我们通常需要用其他的机制来同步对共享内存的访问,例如信号量。

在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。

img

当两个进程通过页表将虚拟地址映射到物理地址时,在物理地址中有一块共同的内存区,即共享内存,这块内存可以被两个进程同时看到。这样当一个进程进行写操作,另一个进程读操作就可以实现进程间通信。但是,我们要确保一个进程在写的时候不能被读,因此我们使用信号量来实现同步与互斥。对于一个共享内存,实现采用的是引用计数的原理,当进程脱离共享存储区后,计数器减一,挂架成功时,计数器加一,只有当计数器变为零时,才能被删除。当进程终止时,它所附加的共享存储区都会自动脱离。

什么时候使用线程池(根据项目来问)?

  1. 当服务端处理单个任务时间较短且所需处理任务量较大时。因为线程频繁地创建和销毁会造成服务器性能损耗。
  2. 每一个任务是无状态的,前后请求没有关联。

http1.0和1.1和2.0的区别?

http1.0和1.1的区别

HTTP2.0和HTTP1.X相比的新特性

day19

内核态与用户态的区别?从用户态切换到内核态有哪几种方式?

内核态和用户态的区别

当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核状态。此时处理器处于特权级最高的(0级)内核代码。当进程处于内核态时,执行的内核代码会使用当前的内核栈。每个进程都有自己的内核栈。

当进程在执行用户自己的代码时,则称其处于用户态。即此时处理器在特权级最低的用户代码中运行。当正在执行用户程序而突然中断时,此时用户程序也可以象征性地处于进程的内核态。因为中断处理程序将使用当前进程的内核态。

用户态切换到内核态的3种方式

a.系统调用

这是用户进程主动要求切换到内核态的一种方式,用户进程通过系统调用申请操作系统提供的服务程序完成工作。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的ine 80h中断。

b.异常

当CPU在执行运行在用户态的程序时,发现了某些事件不可知的异常,这是会触发由当前运行进程切换到处理此异常的内核相关程序中,也就到了内核态,比如缺页异常。

c.外围设备的中断

当外围设备完成用户请求的操作之后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条将要执行的指令转而去执行中断信号的处理程序,如果先执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了有用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

volatile关键字

C++对象的生命周期

day20

connect可能会长时间阻塞,怎么解决?

  1. 使用定时器;(最常用也最有效的一种方法)

  2. 采用非阻塞模式:设置非阻塞,返回之后用select检测状态。

keepalive 是什么东西?如何使用?

keepalive,是在TCP中一个可以检测死连接的机制。

  1. 如果主机可达,对方就会响应ACK应答,就认为是存活的。
  2. 如果可达,但应用程序退出,对方就发RST应答,发送TCP撤消连接。
  3. 如果可达,但应用程序崩溃,对方就发FIN消息。
  4. 如果对方主机不响应ack, rst,继续发送直到超时,就撤消连接。默认二个小时。

socket什么情况下可读?

  1. socket接收缓冲区中已经接收的数据的字节数大于等于socket接收缓冲区低潮限度的当前值;对这样的socket的读操作不会阻塞,并返回一个大于0的值(准备好读入的数据的字节数).

  2. 连接的读一半关闭(即:接收到对方发过来的FIN的TCP连接),并且返回0;

  3. socket收到了对方的connect请求已经完成的连接数为非0,这样的soocket处于可读状态;

  4. 异常的情况下socket的读操作将不会阻塞,并且返回一个错误(-1)。

udp调用connect有什么作用?

  1. 因为UDP可以是一对一,多对一,一对多,或者多对多的通信,所以每次调用sendto()/recvfrom()时都必须指定目标IP和端口号。通过调用connect()建立一个端到端的连接,就可以和TCP一样使用send()/recv()传递数据,而不需要每次都指定目标IP和端口号。但是它和TCP不同的是它没有三次握手的过程。

  2. 可以通过在已建立连接的UDP套接字上,调用connect()实现指定新的IP地址和端口号以及断开连接。

socket编程,如果client断电了,服务器如何快速知道?

使用定时器(适合有数据流动的情况);

使用socket选项SO_KEEPALIVE(适合没有数据流动的情况);

自己编写心跳包程序,简单的说就是自己的程序加入一条线程,定时向对端发送数据包,查看是否有ACK,根据ACK的返回情况来管理连接。此方法比较通用,一般使用业务层心跳处理,灵活可控,但改变了现有的协议;

使用TCP的keepalive机制,UNIX网络编程不推荐使用SO_KEEPALIVE来做心)跳检测。

keepalive原理:TCP内嵌有心跳包,以服务端为例,当server检测到超过一定时间(/proc/sys/net/ipv4/tcp_keepalive_time 7200 即2小时)没有数据传输,那么会向client端发送一个keepalive packet。

<⇧>