目录
二面算法 一个先升序后降序的无重复元素数组,找出给定数字k的索引,不能遍历,即实现一个 find 函数。找出字符串里面 ,出现的次数最多的字符和其次数.
memset(this,/0,sizeof(*this)。我从有无虚函数,有无对象成员,是否pod的情况来说。不过面试官说我漏考虑一种情况。至今为止不知道漏考虑啥了。
手写单例模式,懒汉式就常用的,双重判断,安全考虑用用new的nothrow版本,再判空指针。又问不准用新特性,安全吗?
粘包是在那一层发生的,怎么解决(我是传输层,他说不是。。。然后讲了好久,但是我并没有听懂)
Tcp四大定时器,client和server如果一方掉线会怎么样,掉线重连会怎么样,进程挂了怎么样,宕机了会怎么样?什么时候发送rst,收不到RST的话重试多久?这地方问的很细。
copyonwrite锁在写的时候是每个线程都复制还是怎么样(我说都复制,但是这么一想和私有变量有什么区别,面试官说写时复制锁其实是读写锁的升级,保证了读和写不互斥)
项目介绍需要复习的点
1、windows原生进程/线程有哪五种
进程管理:https://www.cnblogs.com/LittleHann/p/3458736.html
2、QT槽事件
3、安卓和QTThread/网络编程/Nginx
挑战杯项目-视频监控实现
使用 mjpg_streamer 开源项目将摄像头采集到的 视频数据通过网络传输到客户端,实现视频监控。(原理:从摄像头采集图 像,并把他们已流的形式,通过基于 IP 的网络传输到安卓端)。在 Android stdio 平台开发安卓应用,接收远程服务器的数据,实现远程监控家庭情况;
问题:mjpg_streamer项目介绍/数据怎么传输到服务器/
概念:在Linux上运行的视频服务器,可以将摄像头采集到的视频数据通过网络传输到客户端,实现视频监控
参考:https://blog.csdn.net/yi412/article/details/37649641
输出插件的实现是一个http服务器
主要结构:mjpg_streamer主要由三部分构成,主函数mjpg_streamer.c和输入、输出组件,其中输入、输出组件通常是input_uvc.so和output_http.so,他们是以动态链接库的形式在主函数中调用的。
主函数的主要功能有:
1.解析命令行的各个输入参数。2.判断程序是否需要成为守护进程,如果需要,则执行相应的操作。3.初始化global全局变量。4.打开并配置输入、输出插件。5.运行输入、输出插件。
输入插件将采集到的视频送到全局缓存中,输出插件则从全局缓存中读取数据并发送。输出插件的实现是一个http服务器,这里就不介绍了。输入插件的视频采集功能主要是通过Linux的V4L2接口( https://blog.csdn.net/Jfuck/article/details/8169352)实现的,主要是4个函数input_init()、 input_stop()、 input_run()和 input_cmd()。其中iniput_run()函数创建了线程cma_thread(),这个线程很重要,该函数的作用是抓取一帧的图像,并复制到全局缓冲区。
C++网络编程面试题
参考:https://zhuanlan.zhihu.com/p/81807986
经典面试题:https://blog.csdn.net/weiyuefei/article/details/50413543
nginx面试题
Nginx 是如何实现高并发?常见的优化手段有哪些?
参考:https://juejin.im/post/5cf790985188254c5726a981
经典面试题:https://www.jianshu.com/p/cd4fafd4477a
经典面试题2:https://blog.csdn.net/zhouchunyue/article/details/79271908
4、音视频处理
音视频:https://juejin.im/post/5be9084d6fb9a049f153b94c
算法题
求01矩阵最大正方形面积
参考:https://leetcode-cn.com/problems/maximal-square/solution/leetcode221-xian-xing-dp-by-happy_yuxuan/
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix) {
if (matrix.empty() || matrix[0].empty()) return 0;
int n = matrix.size(), m = matrix[0].size();
vector<vector<int>> dp(n+1, vector<int>(m+1));
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (matrix[i-1][j-1] == '1') {
dp[i][j] = min({dp[i-1][j], dp[i-1][j-1], dp[i][j-1]}) + 1;
ans = max(ans, dp[i][j]);
}
}
}
return ans * ans;
}
};
归并排序
LeetCode第一题,twosum
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> m;
for (int i = 0; i < nums.size(); ++i) {
if (m.count(target - nums[i])) {
return {i, m[target - nums[i]]};
}
m[nums[i]] = i;
}
return {};
}
};
二面算法 一个先升序后降序的无重复元素数组,找出给定数字k的索引,不能遍历,即实现一个 find 函数。找出字符串里面 ,出现的次数最多的字符和其次数.
参考:https://blog.csdn.net/u010232305/article/details/50889714
/********************************************************
Description:求字符串中出现次数最多的字符和次数
********************************************************/
#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void search(char* pData, int len)
{
char counts[1024] = {0}; //存放原始数据作为为索引出现的次数
char bufMax[1024] = {0}; //用于存放出现次数最多的字符
int max = 0; //出现次数最多的字符
for (int i=0; i<len; i++)
{
counts[pData[i]] ++;
}
for (int i=0; i<1024; i++)
{
if (counts[i] > max)
{
max = counts[i];
bufMax[0] = i;
}else if ((counts[i] == max) && (counts[i] !=0))
{
bufMax[strlen(bufMax)] = i;
}
}
printf("出现最多的字符分别为:");
for (int i=0; i<strlen(bufMax); i++)
{
printf("%c ", bufMax[i]);
}
printf("\n");
printf("出现最多的字符的次数:%d\n", max);
}
int main()
{
char* srcData = "aabbbcddddeeffffghijklmnopqrst";
search(srcData, strlen(srcData));
getchar();
return 1;
}
C++
虚析构函数,虚析构得调用顺序。
当你用基类指针指向某个派生类对象,delete该对象时,如果基类析构函数不是虚函数,则该对象析构会异常。
为了能够正确的调用对象的析构函数,一般要求具有层次结构的顶级类定义其析构函数为虚函数。因为在delete一个抽象类指针时候,必须要通过虚函数找到真正的析构函数。如果析构函数不加virtual,delete pb只会执行Base的析构函数,而不是真正的Derived析构函数。
因为不是virtual函数,所以调用的函数依赖于指向静态类型,即Base
构造函数不能声明为虚函数的原因
1 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象 的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类。无法确定。
2 虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初 始化,将无法进行。
extern c
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。
#ifndef __INCvxWorksh /*防止该头文件被重复引用*/
#define __INCvxWorksh
#ifdef __cplusplus //__cplusplus是cpp中自定义的一个宏
extern "C" { //告诉编译器,这部分代码按C语言的格式进行编译,而不是C++的
#endif
/**** some declaration or so *****/
#ifdef __cplusplus
}
#endif
#endif /* __INCvxWorksh */
memset(this,/0,sizeof(*this)。我从有无虚函数,有无对象成员,是否pod的情况来说。不过面试官说我漏考虑一种情况。至今为止不知道漏考虑啥了。
手写单例模式,懒汉式就常用的,双重判断,安全考虑用用new的nothrow版本,再判空指针。又问不准用新特性,安全吗?
我说在外面catch异常,那个锁上加guard_lock。他不说话我突然想起来guard_lock也是新特性。那就手写构造析构自己实现RAII。
单例模式(Singleton Pattern,也称为单件模式),使用最广泛的设计模式之一。其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
定义一个单例类:
私有化它的构造函数,以防止外界创建单例类的对象;
使用类的私有静态指针变量指向类的唯一实例;
使用一个公有的静态方法获取该实例
class Singleton
{
private:
static Singleton* instance;
private:
Singleton() {};
~Singleton() {};
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
public:
static Singleton* getInstance()
{
if(instance == NULL)
instance = new Singleton();
return instance;
}
};
// init static member
Singleton* Singleton::instance = NULL;
智能指针,循环引用,unique_ptr这些东西,然后unique_ptr的删除器简洁写法?
shared_ptr、unique_ptr、weak_ptr
shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
队列和栈的区别
队列(Queue):是限定只能在表的一端进行插入和另一端删除操作的线性表
栈(Stack):是限定之能在表的一端进行插入和删除操作的线性表
队列和栈的规则
队列:先进先出
栈:先进后出
队列和栈的遍历数据速度
队列:基于地址指针进行遍历,而且可以从头部或者尾部进行遍历,但不能同时遍历,无需开辟空间,因为在遍历的过程中不影响数据结构,所以遍历速度要快
栈:只能从顶部取数据,也就是说最先进入栈底的,需要遍历整个栈才能取出来,遍历数据时需要微数据开辟临时空间,保持数据在遍历前的一致性
如何用队列实现栈
思路:
1、当插入时,直接插入 stack1
2、当弹出时,当 stack2 不为空,弹出 stack2 栈顶元素,如果 stack2 为空,将 stack1 中的全部数逐个出栈入栈 stack2,再弹出 stack2 栈顶元素
class Solution
{
public:
void push(int node) {
stack1.push(node);
}
int pop() {
//当弹出时,当 stack2 不为空,弹出 stack2 栈顶元素,
//如果 stack2 为空,将 stack1 中的全部数逐个出栈入栈 stack2,
//再弹出 stack2 栈顶元素
if(stack2.empty())
{
while(!stack1.empty())
{
stack2.push(stack1.top());
stack1.pop();
}
}
int ret = stack2.top();
stack2.pop();
return ret;
}
private:
stack<int> stack1;
stack<int> stack2;
};
lambda表达式。问了值捕获,引用捕获。
参考:https://docs.microsoft.com/zh-cn/cpp/cpp/lambda-expressions-in-cpp?view=vs-2019
ambda 表达式-通常称为lambda— 是定义匿名函数对象的简便方法 (闭包) 坐在调用或作为参数传递位置的位置到函数。 Lambda 通常用于封装传递给算法或异步方法的少量代码行。
volatile关键字的适用场景
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
1). 并行设备的硬件寄存器(如:状态寄存器)
2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3). 多线程应用中被几个任务共享的变量
硬件、中断、RTOS等等打交道,所用这些都要求volatile变量。
读过 STL 源码是吧?是,读过空间配置器和红黑树的?
答:vector,支持下标,内存连续,resize,capacity,还分析push_back
vector特点是:其容量在需要时可以自动分配,本质上是数组形式的存储方式。即在索引可以在常数时间内完成。缺点是在插入或者删除一项时,需要线性时间。但是在尾部插入或者删除,是常数时间的。
list 是双向链表:如果知道位置,在其中进行插入和删除操作时,是常数时间的。索引则需要线性时间(和单链表一样)。
vector 和 list 都支持在常量的时间内在容器的末尾添加或者删除项,vector和list都支持在常量的时间内访问表的前端的项.
vector会一次分配多个元素内存,那么下次增加时,只是改写内存而已,就不会再分配内存了,但是list每次只分配一个元素的内存,每次增加一个元素时,都会执行一次内存分配行为。如果是大量数据追加,建议使用list,因为vector 在有大量元素,并且内存已满,再pushback元素时,需要分配大块内存并把已经有的数据move到新分配的内存中去(vector不能加入不可复制元素)然后再释放原来的内存块,这些操作很耗时。而list的性能而会始终于一的,不会出现vector的性能变化情况,所以对于容器构件,需要用什么类型最好,取决于业务逻辑。
Map也是一种关联容器,它是 键—值对的集合,即它的存储都是以一对键和值进行存储的,Map通常也可以理解为关联数组(associative array),就是每一个值都有一个键与之一一对应,因此,map也是不允许重复元素出现的。
底层数据结构是红黑树。具体理解参考:https://github.com/julycoding/The-Art-Of-Programming-By-July/blob/master/ebook/zh/03.01.md
哈希了解吗?如何处理冲突问题?讲了讲底层实现
TIME_WAIT 相关的,主动端关闭还是被动端关闭?
如何通过多线程解决高并发问题?
操作系统
进程和线程的区别
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
2) 线程的划分尺度小于进程,使得多线程程序的并发性高。
3) 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
4) 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
5) 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
Linux进程和线程的区别
参考:https://my.oschina.net/cnyinlinux/blog/422207
答:没有区别,从内核角度来看调度的单位都一样,都是task struct。讲了一下怎么创建task struct,dofork和clone的时候共享了什么,不共享什么。再问那为什么主线程没了,其他也跟着没了。然后讲了一下linux thread和NPTL线程模型,管理线程的弊端。
进程间的通信方式和线程间的通信方式
参考:https://www.jianshu.com/p/9218692cb209
线程间的同步方式有四种
- 临界区
临界区对应着一个CcriticalSection对象,当线程需要访问保护数据时,调用EnterCriticalSection函数;当对保护数据的操作完成之后,调用LeaveCriticalSection函数释放对临界区对象的拥有权,以使另一个线程可以夺取临界区对象并访问受保护的数据。
PS:关键段对象会记录拥有该对象的线程句柄即其具有“线程所有权”概念,即进入代码段的线程在leave之前,可以重复进入关键代码区域。所以关键段可以用于线程间的互斥,但不可以用于同步(同步需要在一个线程进入,在另一个线程leave) - 互斥量
互斥与临界区很相似,但是使用时相对复杂一些(互斥量为内核对象),不仅可以在同一应用程序的线程间实现同步,还可以在不同的进程间实现同步,从而实现资源的安全共享。
PS:1、互斥量由于也有线程所有权的概念,故也只能进行线程间的资源互斥访问,不能由于线程同步;
2、由于互斥量是内核对象,因此其可以进行进程间通信,同时还具有一个很好的特性,就是在进程间通信时完美的解决了"遗弃"问题 - 信号量
信号量的用法和互斥的用法很相似,不同的是它可以同一时刻允许多个线程访问同一个资源,PV操作
PS:事件可以完美解决线程间的同步问题,同时信号量也属于内核对象,可用于进程间的通信 - 事件
事件分为手动置位事件和自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。由SetEvent()来触发,由ResetEvent()来设成未触发。
PS:事件是内核对象,可以解决线程间同步问题,因此也能解决互斥问题
进程间通信又称IPC(Inter-Process Communication),指多个进程之间相互通信,交换信息的方法。根据进程通信时信息量大小的不同,可以将进程通信划分为两大类型:
1、低级通信,控制信息的通信(主要用于进程之间的同步,互斥,终止和挂起等等控制信息的传递)
2、高级通信,大批数据信息的通信(主要用于进程间数据块数据的交换和共享,常见的高级通信有管道,消息队列,共享内存等).
- 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
- 有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
- 信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。不是用于交换大批数据,而用于多线程之间的同步.常作为一种锁机制,防止某进程在访问资源时其它进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 信号 ( signal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
- [共享内存( shared memory )] :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
- 套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
操作系统里面有哪些锁
参考:https://martian101.github.io/2017/07/14/%E8%81%8A%E8%81%8A%E5%B8%B8%E8%A7%81%E7%9A%84%E9%94%81/
概念:锁是计算机对资源进行并发访问控制的一种机制,多线程情况下来实现对临界资源的同步互斥访问。
操作系统层面
1、自旋锁是一种非阻塞锁,多个线程尝试获取自旋锁时,没有获取到的线程会持续尝试获取直到获取到为止。
2、mutex是阻塞锁,多个线程尝试获取锁时,没有获取到锁的线程会被操作系统调度为阻塞状态直到锁被释放然后才会被重新唤醒。OS线程调度,线程上下文切换带来的开销是很大的,多线程程序如果有大量的线程切换,最坏情况下性能甚至会比单线程运行的代码效率还要差。mutex锁如果被频繁的获取和释放,代价也可想而知。
锁状态
1、死锁是已经进入等待状态的线程相互等待各自锁释放的一种循环等待的状态。
2、活锁和死锁产生的条件一样,只是表现不同。死锁下线程进入等待状态,而活锁下线程仍然处于运行状态尝试获取锁。活锁下线程仍然消耗CPU,这样看来,活锁和死锁的区别也有点类似spinlock和mutex。
锁策略
1. 独占锁
独占锁(exclusive lock),有时候也被称为写锁,排它锁等。被独占锁保护的资源,同一时刻只能有一个线程可以进行读写操作。
2. 共享锁
共享锁(shared lock),有时候也被称为读锁。被共享锁保护的资源,当有线程写时仍然可以被别的线程读取,读线程数并不限定为1。但是同一时刻只能有一个线程写入。
3. 可重入锁
可重入锁可以并且只能被已经持有锁的线程加锁多次,一个线程内有多个地方形成对该锁的嵌套获取时可以防止死锁发生。实现上可重入锁会记录自己被lock的次数,只有unlock的次数和lock次数相等时才完成对锁的释放。
4. 公平锁、非公平锁
公平锁顾名思义,申请锁的线程按照申请顺序来获取锁,遵循先申请先得到的原则。而非公平锁则没有该顺序保障。公平锁通常使用等待队列来实现,申请线程未获取到锁时则进入等待队列,锁被释放时得到通知或者直接由上个拥有者移交锁的所有权。
线程池
参考:https://www.cnblogs.com/lzpong/p/6397997.html
线程池如何管理线程的
参考:https://blog.csdn.net/xiaosong_2016/article/details/80416744
计算机网络
OIS七层网络
每层有哪些协议
五层体系结构包括:应用层、运输层、网络层、数据链路层和物理层。
五层协议只是OSI和TCP/IP的综合,实际应用还是TCP/IP的四层结构。为了方便可以把下两层称为网络接口层
tcp的过程
建立连接需要tcp三次握手
所谓的“三次握手”即对每次发送的数据量是怎样跟踪进行协商使数据段的发送和接收同步,根据所接收到的数据量而确定的数据确认数及数据发送、接收完毕后何时撤消联系,并建立虚连接。为了提供可靠的传送,TCP在发送新的数据之前,以特定的顺序将数据包的序号,并需要这些包传送给目标机之后的确认消息。TCP总是用来发送大批量的数据。当应用程序在收到数据后要做出确认时也要用到TCP。
在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。
第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
完成三次握手,客户端与服务器开始传送数据。
四次挥手
第一次挥手:客户端给服务器发送TCP包,用来关闭客户端到服务器的数据传送。将标志位FIN和ACK置为1,序号为X=1,确认序号为Z=1。
第二次挥手:服务器收到FIN后,发回一个ACK(标志位ACK=1),确认序号为收到的序号加1,即X=X+1=2。序号为收到的确认序号=Z。
第三次挥手:服务器关闭与客户端的连接,发送一个FIN。标志位FIN和ACK置为1,序号为Y=1,确认序号为X=2。
第四次挥手:客户端收到服务器发送的FIN之后,发回ACK确认(标志位ACK=1),确认序号为收到的序号加1,即Y+1=2。序号为收到的确认序号X=2。
三次握手属于传输层。
TCP 有哪些字段
1、ACK 是 TCP 报头的控制位之一,对数据进行确认。确认由目的端发出, 用 它来告诉发送端这个序列号之前的数据段都收到了。 比如确认号为 X,则表示 前 X-1 个数据段都收到了,只有当 ACK=1 时,确认号才有效,当 ACK=0 时,确认 号无效,这时会要求重传数据,保证数据的完整性。
2、SYN 同步序列号,TCP 建立连接时将这个位置 1。
3、FIN 发送端完成发送任务位,当 TCP 完成数据传输需要断开时,,出断开 连接的一方将这位置 1。
Socket 建立网络连接的步骤:服务器监听、客户端请求、连接确认
为什么要三次握手和四次挥手?
TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议,采用全双工通信。
那为什么需要三次握手呢?请看如下的过程:
A向B发起建立连接请求:A——>B;
B收到A的发送信号,并且向A发送确认信息:B——>A;
A收到B的确认信号,并向B发送确认信号:A——>B。
三次握手大概就是这么个过程。
通过第一次握手,B知道A能够发送数据。通过第二次握手,A知道B能发送数据。结合第一次握手和第二次握手,A知道B能接收数据。结合第三次握手,B知道A能够接收数据。
至此,完成了握手过程,A知道B能收能发,B知道A能收能发,通信连接至此建立。三次连接是保证可靠的最小握手次数,再多次握手也不能提高通信成功的概率,反而浪费资源。
那为什么需要四次挥手呢?请看如下过程:
A向B发起请求,表示A没有数据要发送了:A——>B;
B向A发送信号,确认A的断开请求请求:B——>A;
B向A发送信号,请求断开连接,表示B没有数据要发送了:B——>A;
A向B发送确认信号,同意断开:A——>B。
B收到确认信号,断开连接,而A在一段时间内没收到B的信号,表明B已经断开了,于是A也断开了连接。至此,完成挥手过程。
可能有捧油会问,为什么2、3次挥手不能合在一次挥手中?那是因为此时A虽然不再发送数据了,但是还可以接收数据,B可能还有数据要发送给A,所以两次挥手不能合并为一次。
挥手次数比握手多一次,是因为握手过程,通信只需要处理连接。而挥手过程,通信需要处理数据+连接。
粘包是在那一层发生的,怎么解决(我是传输层,他说不是。。。然后讲了好久,但是我并没有听懂)
参考:https://zhuanlan.zhihu.com/p/108822858
UDP 是基于报文发送的,UDP首部采用了 16bit 来指示 UDP 数据报文的长度,因此在应用层能很好的将不同的数据报文区分开,从而避免粘包和拆包的问题。而 TCP 是基于字节流的,虽然应用层和 TCP 传输层之间的数据交互是大小不等的数据块,但是 TCP 并没有把这些数据块区分边界,仅仅是一连串没有结构的字节流;另外从 TCP 的帧结构也可以看出,在 TCP 的首部没有表示数据长度的字段,基于上面两点,在使用 TCP 传输数据时,才有粘包或者拆包现象发生的可能。
什么是粘包、拆包?
假设 Client 向 Server 连续发送了两个数据包,用 packet1 和 packet2 来表示,那么服务端收到的数据可以分为三种情况,现列举如下:
第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象。
第二种情况,接收端只收到一个数据包,但是这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。
第三种情况,这种情况有两种表现形式,如下图。接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。这两种情况如果不加特殊处理,对于接收端同样是不好处理的。
为什么会发生 TCP 粘包、拆包?
要发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包。
待发送数据大于 MSS(最大报文长度),TCP 在传输前将进行拆包。
要发送的数据小于 TCP 发送缓冲区的大小,TCP 将多次写入缓冲区的数据一次发送出去,将会发生粘包。
接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
粘包、拆包解决办法
由于 TCP 本身是面向字节流的,无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案,归纳如下:
消息定长:发送端将每个数据包封装为固定长度(不够的可以通过补 0 填充),这样接收端每次接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
设置消息边界:服务端从网络流中按消息边界分离出消息内容。在包尾增加回车换行符进行分割,例如 FTP 协议。
将消息分为消息头和消息体:消息头中包含表示消息总长度(或者消息体长度)的字段。
更复杂的应用层协议比如 Netty 中实现的一些协议都对粘包、拆包做了很好的处理。
Tcp四大定时器,client和server如果一方掉线会怎么样,掉线重连会怎么样,进程挂了怎么样,宕机了会怎么样?什么时候发送rst,收不到RST的话重试多久?这地方问的很细。
软链接和硬链接的区别,我从inode引用计数的角度来答
Linux下信号量,怎么屏蔽,知道哪些不可屏蔽的信号。
数据库
存储引擎myisam和innodb的区别
关于二者的对比与总结:
count运算上的区别:因为MyISAM缓存有表meta-data(行数等),因此在做COUNT(*)时对于一个结构很好的查询是不需要消耗多少资源的。而对于InnoDB来说,则没有这种缓存。
是否支持事务和崩溃后的安全恢复: MyISAM 强调的是性能,每次查询具有原子性,其执行数度比InnoDB类型更快,但是不提供事务支持。但是InnoDB 提供事务支持事务,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。
是否支持外键: MyISAM不支持,而InnoDB支持。
MyISAM更适合读密集的表,而InnoDB更适合写密集的的表。 在数据库做主从分离的情况下,经常选择MyISAM作为主库的存储引擎。 一般来说,如果需要事务支持,并且有较高的并发读取频率(MyISAM的表锁的粒度太大,所以当该表写并发量较高时,要等待的查询就会很多了),InnoDB是不错的选择。如果你的数据量很大(MyISAM支持压缩特性可以减少磁盘的空间占用),而且不需要支持事务时,MyISAM是最好的选择。
MyISAM 存储引擎的特点
在 5.1 版本之前,MyISAM 是 MySQL 的默认存储引擎,MyISAM 并发性比较差,使用的场景比较少,主要特点是
不支持事务操作,ACID 的特性也就不存在了,这一设计是为了性能和效率考虑的。
不支持外键操作,如果强行增加外键,MySQL 不会报错,只不过外键不起作用。
MyISAM 默认的锁粒度是表级锁,所以并发性能比较差,加锁比较快,锁冲突比较少,不太容易发生死锁的情况。
MyISAM 会在磁盘上存储三个文件,文件名和表名相同,扩展名分别是 .frm(存储表定义)、.MYD(MYData,存储数据)、MYI(MyIndex,存储索引)。这里需要特别注意的是 MyISAM 只缓存索引文件,并不缓存数据文件。
MyISAM 支持的索引类型有 全局索引(Full-Text)、B-Tree 索引、R-Tree 索引
Full-Text 索引:它的出现是为了解决针对文本的模糊查询效率较低的问题。
B-Tree 索引:所有的索引节点都按照平衡树的数据结构来存储,所有的索引数据节点都在叶节点
R-Tree索引:它的存储方式和 B-Tree 索引有一些区别,主要设计用于存储空间和多维数据的字段做索引,目前的 MySQL 版本仅支持 geometry 类型的字段作索引,相对于 BTREE,RTREE 的优势在于范围查找。
数据库所在主机如果宕机,MyISAM 的数据文件容易损坏,而且难以恢复。
增删改查性能方面:SELECT 性能较高,适用于查询较多的情况
InnoDB 存储引擎的特点
自从 MySQL 5.1 之后,默认的存储引擎变成了 InnoDB 存储引擎,相对于 MyISAM,InnoDB 存储引擎有了较大的改变,它的主要特点是
支持事务操作,具有事务 ACID 隔离特性,默认的隔离级别是可重复读(repetable-read)、通过MVCC(并发版本控制)来实现的。能够解决脏读和不可重复读的问题。
InnoDB 支持外键操作。
InnoDB 默认的锁粒度行级锁,并发性能比较好,会发生死锁的情况。
和 MyISAM 一样的是,InnoDB 存储引擎也有 .frm文件存储表结构 定义,但是不同的是,InnoDB 的表数据与索引数据是存储在一起的,都位于 B+ 数的叶子节点上,而 MyISAM 的表数据和索引数据是分开的。
InnoDB 有安全的日志文件,这个日志文件用于恢复因数据库崩溃或其他情况导致的数据丢失问题,保证数据的一致性。
InnoDB 和 MyISAM 支持的索引类型相同,但具体实现因为文件结构的不同有很大差异。
增删改查性能方面,如果执行大量的增删改操作,推荐使用 InnoDB 存储引擎,它在删除操作时是对行删除,不会重建表。
MyISAM 和 InnoDB 存储引擎的对比
锁粒度方面:由于锁粒度不同,InnoDB 比 MyISAM 支持更高的并发;InnoDB 的锁粒度为行锁、MyISAM 的锁粒度为表锁、行锁需要对每一行进行加锁,所以锁的开销更大,但是能解决脏读和不可重复读的问题,相对来说也更容易发生死锁
可恢复性上:由于 InnoDB 是有事务日志的,所以在产生由于数据库崩溃等条件后,可以根据日志文件进行恢复。而 MyISAM 则没有事务日志。
查询性能上:MyISAM 要优于 InnoDB,因为 InnoDB 在查询过程中,是需要维护数据缓存,而且查询过程是先定位到行所在的数据块,然后在从数据块中定位到要查找的行;而 MyISAM 可以直接定位到数据所在的内存地址,可以直接找到数据。
表结构文件上:MyISAM 的表结构文件包括:.frm(表结构定义),.MYI(索引),.MYD(数据);而 InnoDB 的表数据文件为:.ibd和.frm(表结构定义);
数据库有哪些锁(我说读写锁)
在DBMS中,可以按照锁的粒度把数据库锁分为行级锁(INNODB引擎)、表级锁(MYISAM引擎)和页级锁(BDB引擎 )。
行级锁:
行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。
特点: 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
表级锁:
表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。
特点:开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。
页级锁:
页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。
特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
copyonwrite锁在写的时候是每个线程都复制还是怎么样(我说都复制,但是这么一想和私有变量有什么区别,面试官说写时复制锁其实是读写锁的升级,保证了读和写不互斥)
手撕sql,一个表中符合条件的人成绩+1。
select subject,max(score),name from grade GROUP BY subject
SELECT S# FORM SC WHERE C#=“C2” AND GRADE>=ALL (SELECT GRADE FORM SC WHERE C#=“C2”)
手撕sql,一个表中男女性别交换。
update salary
set sex =
case sex
when 'm'
then 'f'
else 'm'
end;
sql优化
参考:
https://database.51cto.com/art/200904/118526.htm
https://www.cnblogs.com/yunfeifei/p/3850440.html
查询语句无论是使用哪种判断条件 等于、小于、大于, WHERE 左侧的条件查询字段不要使用函数或者表达式
使用 EXPLAIN 命令优化你的 SELECT 查询,对于复杂、效率低的 sql 语句,我们通常是使用 explain sql 来分析这条 sql 语句,这样方便我们分析,进行优化。
当你的 SELECT 查询语句只需要使用一条记录时,要使用 LIMIT 1
不要直接使用 SELECT *,而应该使用具体需要查询的表字段,因为使用 EXPLAIN 进行分析时,SELECT * 使用的是全表扫描,也就是 type = all。
为每一张表设置一个 ID 属性
避免在 WHERE 字句中对字段进行 NULL 判断
避免在 WHERE 中使用 != 或 <> 操作符
使用 BETWEEN AND 替代 IN
为搜索字段创建索引
选择正确的存储引擎,InnoDB 、MyISAM 、MEMORY 等
使用 LIKE %abc% 不会走索引,而使用 LIKE abc% 会走索引
对于枚举类型的字段(即有固定罗列值的字段),建议使用ENUM而不是VARCHAR,如性别、星期、类型、类别等
拆分大的 DELETE 或 INSERT 语句
选择合适的字段类型,选择标准是 尽可能小、尽可能定长、尽可能使用整数。
字段设计尽可能使用 NOT NULL
进行水平切割或者垂直分割
输入一个url之后发生了什么
参考:https://zhuanlan.zhihu.com/p/43369093
1.DNS域名解析:浏览器缓存、系统缓存、路由器、ISP的DNS服务器、根域名服务器。把域名转化成IP地址。
2.与IP地址对应的服务器建立TCP连接,经历三次握手:SYN,ACK、SYN,ACK
3.以get,post方式发送HTTP请求,get方式发送主机,用户***,connection属性,cookie等
4.获得服务器的响应,显示页面