目录:
面向对象设计(OOD)
- 注意:
- 经手的项目要留下文档/类图
- 从我接手开始,出现一些变化,开始有文档,源码也开始有注释
- 持续这样做,别人会非常愿意跟我一起共事,成为别人心目中的大佬
- 必看的书[当成字典来查]:TCP/IP详解:协议(一) 深入理解计算机系统(CSAPP) Unix网络编程(UNP) Unix高级环境编程(APUE) Linux系统编程手册
1. 类与类之间的关系
依赖关系:依赖关系,从语义来说是A use B,偶然的,临时的,非固定的,通过调用成员函数来体现,在代码上,B作为A的成员函数参数、B作为A的成员函数局部变量(返回值)、A的成员函数调用B的静态方法,类图中用虚线加箭头表示。
关联关系:双向跟单向关联关系,实线来表示。
彼此之间并不负责对方的生命周期,语义上是A has B,代码上一般是以指针或引用存在。
聚合关系:比较强的一种关联关系,对象之间的关系表现为整体和局部,整体并不负责局部的生命周期,用空心菱形加直线表示。(A has B)
组合关系:更强的关联关系,对象之间的关系表现为整体和局部,整体负责局部对象的销毁,数据成员以子对象成员形式存在。用实心菱形加直线表示。(A has B)
继承:类图中用空心三角箭头来表示。( A is B)
从上到下耦合度越来越强。
当组合与依赖结合时,可以替代对象。[组合+依赖(基于对象) VS 继承+虚函数(面向对象)]
2. 面向对象设计原则
SOLID的5原则:
单一职责原则:一个类,最好只做一件事
开闭原则:对扩展开放,对修改闭合
里氏替换原则:派生类必须能够替换基类[派生类能扩展基类的功能,但是不能修改基类原有的功能]
接口分离原则:使用多个小的专门的接口,而不要使用一个大的总接口。
依赖倒置原则:高层模块不依赖于低层模块,二者都同依赖于抽象,抽象不依赖于具体,具体依赖于抽象。(调用链上调用者是高层模块,被调用者是低层模块) 且更强调框架的设计,关注点在怎样去写框架,写好后不需要再由程序员来控制执行流。
另外两大原则:
迪米特法则(最少知识原则):一个类应当尽可能少的与其他类发生联系。
组合复用原则:尽可能采用组合、聚合方式而不是继承的关系来达到软件复用的目标。
3. Linux下pthread线程的封装
面向对象:start()函数里创建线程,线程处理函数为threadFunc(要为静态成员函数,否则参数不匹配),参数传this指针,threadFunc里调用纯虚函数run()。
基于对象:
4. 生产者消费者问题
面向对象:
TaskQueue是否可以设计成单例模式?可以,但没必要。
TaskQueue是否可以设计成静态的或全局对象?如果程序中出现大量的静态对象或全局对象,会导致
设计出现问题,因为这些对象难以管理的;尽量要少使用或者不使用全局对象。
可以用RAII来进行锁的管理,创建局部对象,加锁写在构造函数,解锁写在析构函数,当局部对象创建,加锁,当局部对象销毁,解锁。
条件变量的虚假(异常)唤醒:如果pthread_cond_signal出现异常可能唤醒多个wait,防止虚假唤醒的情况判断时要使用while而不是if。
基于对象:
5. 线程池
面向对象:
基于对象:
网络编程:
1. 七层模型
2. 四层模型
3. 五层模型
4. 传输层的TCP和UDP
TCP:面向连接,可靠传输,字节流,全双工
数据偏移指首部长度,以4字节为单位,最大为60字节。
TCP的状态迁移图:
建立连接时候的状态变化:
server: CLOSED -> (1)LISTEN -> (3)SYNRECVD -> (5)ESTABLISHED
client: CLOSED -> (2)SYN_SENT -> (4)ESTABLISHED
断开连接时的状态变化:
server:ESTABLISHED -> (2)CLOSE_WAIT -> (4)LAST_ACK ->(6)CLOSED
client: ESTABLISHED -> (1)FIN-WAIT1 -> (3)FIN_WAIT2 ->(5)TIME_WAIT -> CLOSED
当服务器跟客户端几乎同时发送FIN,二三次挥手合并,进入CLOSING状态,接着进入TIME_WAIT状态。
建立连接:三次握手
Q1:为什么需要三次握手:
1.确保连接能正常建立。
2.对于网络中延迟的syn,可以保证不会进入established状态,造成服务器资源的浪费。
断开连接:四次挥手
Q2:为什么要四次挥手?
TCP是一个全双工的连接,要确保双方都没有数据发送给对方,连接才真正关闭。每两次都关闭一个方向的数据传输。
Q3:服务器IME_WAIT的状态?
确保第四次报文丢失时也能正常断开。
确保网络中的延迟的数据包消失,防止数据串链。
Q4:服务器可以主动断开连接吗?
可以,但是主动断开方会有2MSL的TIME_WAIT时间,对服务器造成资源的浪费。[此时该端口不可以再连接]
可以通过设置服务器套接字的属性(setsocketopt)来设置网络地址可重用,来解决该问题。
Q5(面试题):当发现服务器后台有大量的连接处于TIME_WAIT状态时,该如何解决?
出现该问题,可能会触发一个问题,文件描述符不够用,不能接受新的连接了。出现该问题,是服务器主动断开了连接,这时候首先排查为什么服务器会发生主动断开连接的情况,然后可以通过设置参数,修改TIME_WAIT的时间,使之变小一点。
查看连接的状态:netstat -ano -p tcp |grep 端口号
MTU和MSS
UDP:不可靠,数据包,无连接
5. 其他
SIGPIPE信号:默认处理方式是程序直接崩溃。
当往一个已经断开的连接执行send操作时,对端会返回一个RST报文,会触发SIGPIPE信号,如果不去处理SIGPIPE信号,程序直接崩溃。
解决方案:处理该信号,利用信号处理函数signal或者sigaction。
如:signal(SIGPIPE,SIG_IGN);
设置网络地址可重用:
int on = 1;
if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(int)) < 0) {
perror("setsockopt");
close(listenfd);
return;
}
设置端口可重用:当端口设置为可以重用时, 就实现了系统级别的负载均衡,在一台机器上,对于同一个服务器程序可以同时启动多次
if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(int)) < 0) {
perror("setsockopt");
close(listenfd);
return;
}
Linux命令搜索:https://wangchujiang.com/linux-command/
Linux服务器下的抓包:tcpdump,可以-w然后将抓取的内容放到指定文件,然后sz到windows,然后用wireshark打开分析。
eg : tcpdump -i ens33 -S -w mytest.pcapng
read/write和recv/send的区别:
int recv(int sockfd,void *buf,int len,int flags);
int send(int sockfd,void *buf,int len,int flags);
前面的三个参数和read,write相同,第四个参数能够是0或是以下的组合:
MSG_DONTROUTE--不查找路由表
MSG_OOB--接受或发送带外数据
MSG_PEEK--查看数据,并不从系统缓冲区移走数据
MSG_WAITALL--等待任何数据
如果flags为0,则和read,write一样的操作
在linux中,所有的设备都可以看成是一个文件,所以可以用read/write来读写socket数据。
6. I/O多路复用模型
select:
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
1 单个进程可监视的fd数量被限制
2 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
3 对socket进行扫描时是线性扫描
poll:
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
poll还有一个特点是“水平触发LT”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
epoll:
有水平触发和边沿触发,边缘触发ET提高了编程的复杂程度
还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知
查看能打开多少个文件描述符:
查看机器能打开的最大连接数:cat /proc/sys/fs/file-max,与内存大小有关,1G大概能打开十万个。
还与limits.conf文件有关,查看文件 /etc/security/limits.conf ,查看nofile的数量(soft,hard),修改后重启机器才能生效。
通过命令ulimit查看、修改(查看修改的就是limits.conf里面的值)一个进程最多打开的文件描述符数量:ulimit -n,修改后只有在该会话生效。
真正可以打开的文件描述符是两者的交集,如果没有设置,当文件描述符数量到达上限时会报错。
7. 五种网络I/O模型
阻塞I/O:
非阻塞I/O:
当创建socket时,如果加上SOCK_NONBLOCK的标志位,就变成一个非阻塞的socket
在后续执行accept函数时,不会再阻塞,返回值为-1.
int listenfd = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
//对于一个已经建立连接的套接字,将它设置为非阻塞的[recv函数不再阻塞]:
//1.先获取原有套接字的状态标志
int flags = fcntl(peerfd, F_GETFL, 0);
//2.将非阻塞的标志与原有的标志信息做一个或操作
flags |= O_NONBLOCK;
//3.最后再将该标志位信息写回到socket中
ret = fcntl(peerfd, F_SETFL, flags);
I/O多路复用:
信号驱动I/O:
异步I/O:aio_xxx函数,更高效。
面试常考:阻塞非阻塞、同步异步。
阻塞 VS 非阻塞:
关注的是用户态进程/线程(程序)的状态
其要访问的数据是否就绪,进程/线程是否需要等待;
同步 VS 异步:
关注的是消息通信机制 – 内核与应用程序的交互
访问数据的方式,同步需要主动读写数据,在读写数据的过程中还是会阻塞;
异步只需要关注I/O操作完成的通知,并不主动读写数据,由操作系统内核完成数据的读写。
8. 常见并发服务器方案
网络IO中一个连接的基本处理:
read:获取对端的数据
decode/compute/encode:业务逻辑的处理,重点在设计协议(公有和私有),公司里主要做的事就在这。
write:将处理的结果发送给对端
并发服务器的经典设计:reactor+Threadpool,分工合作,各司其职。
reactor(反应器模型,同步IO):处理网络IO的数据接收和发送。(IO线程,IO密集型)
首先关注的是用户态进程与内核。
反应器模型,Linux下可以理解成对多路IO复用的封装。
三个概念:注册就绪事件(读、写、错误),注册事件处理器,事件分离器。
reactor读取操作过程:
- 应用程序注册读就绪事件和相关联的事件处理器
- 事件分离器等待事件的发生
- 当发生读就绪事件的时候,事件分离器调用第一步注册的事件处理器
- 事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理
threadpool:收到数据之后,对数据进行业务逻辑的处理。(计算线程,计算密集型)
另一种并发服务器方案:Proactor,基于异步。
三个概念:注册IO完成事件、注册事件处理器、事件分离器。
boost库是一个很庞大的库,里面分为很多字库,是一个C++准标准库,其中boost.asio实现的proactor专门用来处理网络IO,并不是真正的异步IO,底层是基于epoll来模拟异步IO。
proactor读取操作过程:
- 应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。
- 事件分离器等待读取操作完成事件。
- 在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作,并将读取的内容放入用户传递过来的缓冲区中。这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓冲区。
- 事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区处理数据,而不需要进行实际的读取操作。
Reactor vs Proactor:
-
Proactor 感兴趣的事件是读写完成事件。
-
Reactor和Proactor模式的主要区别:真正的读取和写入操作是由谁来完成的:
Reactor中需要应用(用户)程序自己读取或者写入数据;
Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,内核会读取缓存区或者写入缓存区到真正的IO设备
-
综上所述,同步和异步是相对于应用和内核的交互方式而言的,同步需要主动去询问,而异步的时候内核在IO事件发生的时候通知应用程序,而阻塞和非阻塞仅仅是系统在调用系统调用的时候函数的实现方式而已。
9. reactor模型封装
-
获取地址
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen); int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
read/write和recv/send区别
int recv(int sockfd,void *buf,int len,int flags); int send(int sockfd,void *buf,int len,int flags);
前面的三个参数和read,write相同,第四个参数能够是0或是以下的组合:
MSG_DONTROUTE–不查找路由表
MSG_OOB–接受或发送带外数据
MSG_PEEK–查看数据,并不从系统缓冲区移走数据
MSG_WAITALL–等待任何数据
如果flags为0,则和read,write一样的操作
在linux中,所有的设备都可以看成是一个文件,所以可以用read/write来读写socket数据。 -
eventfd —— Reactor与线程池融合的组件
#include <sys/eventfd.h> int eventfd(unsigned int initval, int flags); /* initval:初始化计数器值,该值保存在内核。 flags:如果是2.6.26或之后版本的内核,flags 必须设置为0。 flags支持以下标志位: EFD_NONBLOCK 类似于使用O_NONBLOCK标志设置文件描述符。 EFD_CLOEXEC 类似open以O_CLOEXEC标志打开,O_CLOEXEC 应该表示执行exec()时,之前通过open()打开的文件描述符会自动关闭. 返回值:函数返回一个文件描述符,与打开的其他文件一样,可以进行读写操作。 */
eventfd支持的操作:
-
read:如果计数器A的值不为0时,读取成功,获得到该值。如果A的值为0,非阻塞模式时,会直接返回失败,并把error置为EINVAL; 如果为阻塞模式,一直会阻塞到A为非0为止;
-
write:将缓冲区写入的8字节整形值加到内核计数器上,即会增加8字节的整数在计数器A上,如果其值达到0xfffffffffffffffe时,就会阻塞(在阻塞模式下),直到A的值被read;
-
select/poll/epoll:支持被io多路复用监听。当内核计数器的值发生变化时,就会触发事件。
-
-
reactor模型的类图
-
reactor+threadpool
-
最后可用一个类把reactor和threadpool组织起来
class Task { public: Task(const string & msg, const TcpConnectionPtr & conn) : _msg(msg) , _conn(conn) {} //process方法的执行在线程池中的某一个子线程进行 void process() { //1. 执行业务逻辑的处理 //decode //compute //encode //2. 将结果发送给客户端 string response = _msg;//模拟回显服务 _conn->sendInLoop(response); } private: string _msg; TcpConnectionPtr _conn; }; class EchoServer { public: EchoServer(unsigned short port, const string & ip = "127.0.0.1") : _threadpool(4, 10) // 4, 10最好写在配置文件中 , _server(port) {} void start() { _threadpool.start(); _server.setAllCallbacks( std::bind(&EchoServer::onConnection, this, std::placeholders::_1), std::bind(&EchoServer::onMessage, this, std::placeholders::_1), std::bind(&EchoServer::onClose, this, std::placeholders::_1)); _server.start(); } void onConnection(const TcpConnectionPtr & conn) { cout << ">> tcp " << conn->toString() << " has connected!" << endl; } void onMessage(const TcpConnectionPtr & conn) { string msg = conn->receive(); Task task(msg, conn); _threadpool.addTask(std::bind(&Task::process, task)); } void onClose(const TcpConnectionPtr & conn) { cout << ">> tcp " << conn->toString() << " has closed!" << endl; } private: Threadpool _threadpool; TcpServer _server; }; void test() { EchoServer server(8888); server.start(); }
-
定时器timefd,timerfd是Linux提供的一个定时器接口。这个接口基于文件描述符,通过文件描述符的可读事件进行超时通知,所以能够被用于select/poll/epoll的应用场景。timerfd是linux内核2.6.25版本中加入的接口。
#include <sys/timerfd.h> int timerfd_create(int clockid, int flags); /* 功能:该函数生成一个定时器对象,返回与之关联的文件描述符。 参数详解: clockid:可设置为 CLOCK_REALTIME:相对时间,从1970.1.1到目前的时间。更改系统时间 会更改获取的值,它以系统时间为坐标。 CLOCK_MONOTONIC:绝对时间,获取的时间为系统重启到现在的时间,更改系统时间对齐没有影响。 flags: 可设置为 TFD_NONBLOCK(非阻塞), TFD_CLOEXEC(同O_CLOEXEC) linux内核2.6.26版本以上都指定为0 */ int timerfd_settime(int fd, int flags,const struct itimerspec *new_value, struct itimerspec *old_value); /* 功能:该函数能够启动和停止定时器 参数详解: fd: timerfd对应的文件描述符 flags: 0表示是相对定时器,TFD_TIMER_ABSTIME表示是绝对定时器 new_value: 设置超时时间,如果为0则表示停止定时器。 old_value: 一般设为NULL, 不为NULL,则返回定时器这次设置之前的超时时间 */ //数据结构 struct itimerspec{ struct timespec it_interval; struct timespec it_value; } struct timespec{ time_t tv_sec; long tv_nsec; }
timerfd支持的操作:
read: 读取缓冲区中的数据,其占据的存储空间为sizeof(uint64_t),表示超时次数;
select/poll/epoll:当定时器超时时,会触发定时器相对应的文件描述符上的读操作,IO复用操作会返回,然后再去对该读事件进行处理。