一面 1个小时
自我介绍
1、讲一下项目
应用场景:
当一个游客去一个景点旅游的时候,进入景点要用身份证刷闸机,这个闸机就相当于我系统里的信息采集机,负责采集游客的身份证号码,传给该景点的客户端,这个客户端主要分为两大部分,一个就是充当这个信息采集机的服务器,接收信息采集机发送过来的游客信息;另外一个就是负责给服务器发送请求,接收服务器的消息。当服务器收到这个客户端发来的请求之后,会根据之前双方约定好的请求类型去进行相应的处理,再将结果发回客户端。客户端接收到服务器返回的消息之后,判断是发给自己的还是对应的信息采集机,如果是发送给信息采集机的消息,直接再发送给信息采集机,以此消息的值来决定游客是否可以进入该景区。
工作流程:
整个系统刚启动的时候,首先要打开负载均衡器,然后打开服务器去连接负载均衡,负载均衡在hash环上添加该服务器的节点,供客户端选择,然后就可以打开客户端进行正常的连接了。一个客户端刚启动的时候,它也会先去连接这个负载均衡器,然后负载均衡根据一致性哈希算法找到一个合适的服务器节点,给客户端返回该服务器的ip和端口,然后客户端再去连接这个服务器,连接成功之后进行工作。
服务器具体实现:
并发模式是一个高效的半同步半异步模式,主线程使用libevent将服务器的套接字监听起来,当有新的客户端连接时,会触发回调,这个回调函数会将连接套接字通过socket_pair给到线程池里的一个当前连接数最小的工作线程,由这个工作线程去处理该客户端的请求。然后每个工作线程也有使用到libevent,服务器刚启动的时候工作线程的libevent监听的文件描述符就是socket_pair的一端,等待主线程为其分配客户端套接字。然后工作线程这块使用mvc对业务进行处理。工作线程接收到具体的操作请求之后,将其传给controller,controller根据不同的操作类型将任务分发给不同的model,然后model在进行对数据库的操作,操作完成后将结果返回给view,view再发送回客户端,这就完成了一次服务器和客户端的交互工作。
负载均衡具体实现:
服务器连接负载均衡器,负载均衡器获取到服务器的ip和port利用MD5值进行一致性哈希的计算,再将其映射到哈希环的一个节点上,并为其增加了20个虚拟节点。
然后客户端连接服务器时,先连接到负载均衡,负载均衡根据其哈希值在哈希环上的位置找到顺时针的第一个节点或者虚拟节点,返回对应的服务器ip和port。
客户端具体实现:
要和服务器通信时,将数据封装成jsoncpp,调用单例模式对象发送给服务器。开辟了一个单独的线程用来接收服务器的响应。当用户登录成功之后,开始监听信息采集机发送的数据,发送给服务器
编写了一个简单的shell脚本,for循环去连接服务器,输出当前循环数
最大连接量达到一万三千多的时候虚拟机就回卡死,必须重启才能运行
2、单例模式在项目中的应用
因为一个客户端在给服务器发送消息的时候不论是信息采集机的消息还是客户端要发送的消息都是要用到同一个套接字去给服务器发送请求,所以在实例化专门发送消息的对象的时候就只需要一个对象就可以了,而不用每发送一次消息就实例化一个对象,这样做的好处是节省资源,降低代码复杂性,提高可读性。
3、单例模式在并发系统中需要进行什么处理吗
需要注意的是当不同线程同时请求该单例模式的对象时,如果不加入线程同步的控制,可能会每个线程都实例化一个对象,这样单例模式就失效了,产生了多个对象
4、你的单例模式如何做线程同步的
用互斥锁,get单例模式的static函数里加了两层判断,第一层判断该单例对象是否为空,不为空时直接返回该对象,如果为空,加锁,然后再判断一次是否为空,还是空的话就调用构造函数构造单例对象,这里第二次判断的原因是,如果有两个线程同时进入了第一层判断,然后一个线程获取了锁资源创建了单例对象,而当另外一个没有获取到锁资源的阻塞线程获取到该锁资源的时候,如果没有第二层判断,他也会创建一个对象,就会同时存在两个对象
5、你知道单例模式的饿汉模式和懒汉模式吗,他们有什么不同
饿汉是在刚进入程序的时候就率先获取一个单例模式的对象,懒汉是当要用到该对象的时候才去获取他。饿汉模式不存在线程安全问题。
6、线程同步除了互斥锁还有什么方式
读写锁和自旋锁
读写锁存在三种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态
一次只有一个线程可以占有写模式的读写锁,但一次可以有多个线程同时占有读模式的读写锁。如果某个线程申请了读锁,那么其他线程还是可以申请读锁,但是不可以申请写锁,如果某个线程申请了写锁,那么其他的线程都不可以再申请读写锁。
自旋锁和互斥锁的功能差不多,当有一个线程加锁之后,其他线程就不能够再对该资源进行加锁。不同点在于自旋锁如果被阻塞的话,它不会陷入休眠状态,而是一直处于一个循环检测,检测是否可以获取锁,而互斥锁如果被阻塞的话,会陷入休眠状态,直到可以获取锁的时候才被唤醒。
7、互斥锁和读写锁的不同,互斥锁和自旋锁的不同
首先,互斥锁和自旋锁都是当一个线程加锁之后,其他线程只能阻塞而不能再加锁,而读写锁的情况不同,如果一个线程加读锁其他线程还是可以加读锁,不能加写锁,如果一个线程加写锁,那么其他线程就不能加任何锁。
其次,互斥锁和读写锁如果获取不到锁的时候会进入睡眠状态,而自旋锁会一直循环检测,处于忙等待的状态。
10、你了解死锁吗,什么情况下会产生死锁
死锁是这样一种状态:多个进程或线程并发执行的过程中,占有着其他进程或线程所需要的资源,同时又请求其他线程所占有的资源,互不释放,一直处于阻塞状态,成为死锁。
产生条件:1、互斥,2、请求和保持,3、不剥夺,4、环路等待
11、如何自己实现一个死锁
进程A,B,资源1,2.进程A占有了资源1,进程B占有了资源2,都互相不会释放自己所占有的资源,同时进程A去请求资源2,进程B去请求资源1,两者都阻塞,成为死锁。
12、负载均衡中的一致性哈希算法
一致性hash:将原来的余数哈希(模上服务器的个数来计算客户端该被映射到哪个服务器上)变为一致性hash(模上2^32-1)可以得到一个哈希环,将每台服务器都根据自己映射的位置看作环上的不同节点,当客户端连接服务器的时候,负载均衡根据客户端的ip和port计算出一个hash值,然后向前查找到第一个节点就是该客户端所要连接的服务器
利用服务器的ip和port进行MD5计算得到一个唯一的散列值,将其加入到hash环上成为一个节点,再根据服务器自己决定的虚拟节点个数为该服务器创建虚拟节点,这里是20个。
13、一致性哈希和普通哈希相比的优点
普通hash的每次扩展和收缩都会导致所有客户端分布的重新计算,而一致性hash不存在这个问题,当我有一台服务器挂掉之后,整个服务器集群不会受太大的影响,只会影响到顺时针放下遇到的下一个服务器,也就是说一致性hash的容错性和可扩展性要好于普通hash
普通hash的查找效率(O(1))要高于一致性hash(O(n)),n为查找的个数。
14、除了单例模式还了解其他设计模式吗,说一下代理模式和装饰模式的不同
工厂模式/抽象工厂模式
工厂模式中,一个工厂可以生产一种类型的多个不同产品,而抽象工厂中,一个抽象工厂可以生产多个实际工厂,每个实际工厂可以生产不同类型的产品
代理模式/装饰者模式区别
篮球运动员和经纪人:代理(帮你处理一些你不关注的事务,签合同谈薪资等)
篮球运动员和教练:装饰(帮助你提升自己的能力)
观察者模式
对象之间的一对多的依赖关系,多个观察者对应一个被观察者对象,当被观察者的状态改变时,观察者会接收到响应的通知,然后去进行一系列的操作
15、http的常用状态码
200:请求成功;300:重定向(302表示资源被转移,要通过get操作来获取);400:客户端错误(404找不到页面);500:服务器错误(503暂时无法访问服务器)
16、http请求GET和POST的区别
get请求的参数跟在url后面,post请求参数跟在request body中。
get请求会在浏览器中缓存,而post请求会进行第二次提交。
get请求参数有长度限制,post参数无长度限制。
get请求在浏览器回退时是无害的,post请求在回退时会被再次提交。
get请求的参数只接受ASII字符,而post无限制。
17、网络OSI7层模型和TCP/IP四层模型,TCP属于哪一层,还有什么其他的传输层协议?
OSI:物理层->数据链路层->网络层->传输层->会话层->表示层->应用层
TCP/IP:网络接口层->网络层->传输层->应用层
18、根据三次握手四次挥手的机制,如何对服务器进行攻击
有一种叫syn洪水攻击
19、syn洪水攻击具体怎么实现
攻击者发送TCP SYN,SYN是TCP三次握手中的第一个数据包,而当服务器返回ACK后,该攻击者就不对其进行再确认,那这个TCP连接就处于挂起状态,也就是所谓的半连接状态,服务器收不到再确认的话,还会重复发送ACK给攻击者。这样更加会浪费服务器的资源。攻击者就对服务器发送非常大量的这种TCP连接,由于每一个都没法完成三次握手,所以在服务器上,这些TCP连接会因为挂起状态而消耗CPU和内存,最后服务器可能死机,就无法为正常用户提供服务了。
解决方案:
1.可缩短 SYN Timeout时间,可以通过缩短从接收到SYN报文到确定这个报文无效并丢弃该连接的时间,可以降低服务器负荷。
2.设置SYN Cookie,给每个请求连接的IP地址分配一个Cookie,如果短时间内收到同一个IP的重复SYN报文,则以后从这个IP地址来的包会被丢弃。
20、常用的数据结构
21、链表的地址是连续的吗,顺序表呢
链表一般情况不是连续的,而顺序表比如数组,队列和栈这些是连续的
22、栈和队列的区别,堆和栈的区别
队列和栈都只能在头或尾进行操作,不能从中间操作
栈是先进后出的结构,队列是先进先出的结构
栈只能再栈顶进行插入和删除,而队列是在队头插,队尾删
堆栈区别
1、管理方式不同:栈由操作系统自动分配和释放,而堆的申请和释放由程序员自己控制,容易产生内存泄漏
2、空间大小不同:理论上程序员可申请的堆大小为虚拟内存的大小,而栈的话,windows下默认1MB,linux下默认10MB
3、生长方向不同:栈从高地址向低地址生长,堆从低地址向高地址生长
4、堆区空间都是动态分配的,没有静态分配的堆,栈区的分配有静态分配和动态分配,静态分配是由操作系统完成的,动态分配有alloca函数进行,但是释放都是由操作系统进行释放
5、栈区分配效率高,堆区效率低
23、二叉树的三种遍历方式
前序遍历:跟->左孩子->右孩子
中序遍历:左孩子->跟->右孩子
后序遍历:左孩子->右孩子->跟
24、红黑树和二叉树的区别
25、红黑树在系统中的应用(我说了epoll,STL中的map和set)
STL中的map和set;linux下用红黑树管理进程控制块PCB,I/O复用中的epoll也用的是红黑树;nginx中用红黑树管理定时器。
26、map和set的区别
两者都是关联容器,底层都是红黑树,都是默认有序的
map的元素是一个key-value的两个数据,而set的元素是只有一个key
map和set都不能存放键值相同的数据,因为红黑树中不存在key值相同的节点
27、set中可以存放相同的值吗,map中如何存放key值相同的数据
map和set无法插入key值相同的元素,multimap和multiset可以插入key值相同的元素,这是由于他们的底层实现的差异,map和set在插入数据的时候底层会调用红黑树的insert_unique(),而multimap和multiset底层调用insert_equal()函数进行插入,然后这两个函数最后真正向红黑树中插入的时候都会调用一个__insert()函数,只是在关于键值相同时的处理方式不同,insert_unique()函数在遇到key值相同的元素时,不进行插入,直接返回,而insert_equal()函数还是会进行插入。
hash_map和hash_set的底层实现是hashtable,因此不会自动排序,他们插入新元素的时候也是有一个insert_unique()函数,而hash_multimap和hash_multiset插入元素的时候有一个insert_equal()函数,这两个函数的底层实现原理和基于红黑树的这两个函数相差不多,只是插入操作没有再去转调另外的函数,直接在自己的函数体内实现,因此hash_map和hash_set不能插入key’值相同的元素,而hash_multimap和hash_multiset可以
28、你开发时使用的编程语言,c和c++的区别
1、c++可以给参数带默认值,声明或定义只能有一处给默认值
2、c++支持inline内联函数
3、c++支持函数重载
4、c中的const是常变量,c++是常量
5、c++支持引用
6、c++申请堆区内存一般使用new,释放内存使用delete,c只能使用malloc,free
29、c++面向对象的三大特性,讲一下多态
封装,继承和多态
多态分为静多态和动多态,静多态是在编译器就已经确定的多态,主要实现方式有函数重载和模板,而动多态是在运行时才确定的多态,主要实现方式是继承和虚函数。
当基类中有虚函数存在时,基类和派生类都会自动生成一个vftable虚表,一个类的所有对象公用一个虚表,而该类实例化的对象中会有一个vfptr虚表指针,当编译器无法确定要调用基类还是派生类当中的函数时,就会去本对象的vfptr所指向的vftable中查找,当我们派生类重写了该虚函数的时候,如果用基类指针指向派生类对象并且用该指针去调用该虚函数时,就会发生多态调用。
30、哪个命令可以查找当前目录下最大的文件
du -s * | sort -nr | head -1
ls -l | awk '/^[^d]/ {print $5,$9}' | sort -nr | head -1
find -type f -exec stat -c "%s %n" {} \; | sort -nr | head -1