这是为了掌握c++11的一些新特性和网络编程的知识而做的一个项目.
github:https://github.com/viktorika/Webserver
模型:
参考muduo部分代码,采用Reactor模型+EPOLL(ET)非阻塞IO模式.外加线程池提高服务器的并发性能.主线程accept,其他线程读-解析-写.
Reactor模型运行过程
事件循环loop->封装EPOLL_WAIT的poll->返回事件集handles->对每个handle执行事件handleEvent->handleEvent通过回调函数执行
线程池的实现:
muduo线程池的代码,讲道理我读得并不是很懂,等有空再仔细读读,但是它的one loop per thread思想起码我还是做到了,基本上就简单实现这个思想,一个线程一个loop.
RAII封装锁:
如果在使用锁的时候突然发生意外退出去了,那么该锁就永远不可能释放,如果有其他的线程或者当前线程还需要获取这个锁,那么很容易就会造成死锁,为了解决这个情况我们用RAII机制来封装锁,即资源获取即初始化,离开作用域后释放.
HTTP解析问题:
因为采用的是ET模式,故每次都读完全部的数据再处理,处理的时候用状态机处理,我这里只用了4种状态,PARSE_ERROR,PARSE_METHOD,PARSE_HEADER,PARSE_SUCCESS,每一种状态都被我用bind绑定了实现方法,故不需要判断当前是哪种状态,写法比较优雅.每次当待分析串有一行时才进行分析,即发现"\r\n"才进行解析.主流程代码如下:
void Http_conn::parse(){
bool zero=false;
int readsum=readn(channel->getFd(),inbuffer,zero);
if(readsum<0||zero){
initmsg();
channel->setDeleted(true);
channel->getLoop().lock()->addTimer(channel,0);
return;
//读到RST和FIN默认FIN处理方式,这里的话因为我不太清楚读到RST该怎么处理,就一起这样处理好了
}
while(inbuffer.length()&&~inbuffer.find("\r\n",pos))
parsestate=handleparse[parsestate]();
}
对于每一个状态都被我绑定了对应的事件处理函数:
handleparse[0]=bind(&Http_conn::parseError,this);
handleparse[1]=bind(&Http_conn::parseMethod,this);
handleparse[2]=bind(&Http_conn::parseHeader,this);
handleparse[3]=bind(&Http_conn::parseSuccess,this);
剩下的细节请看我的github里的代码.
计时器:
这个其实算是Reactor的一部分吧,就是用来处理超时连接的.timer的处理逻辑如下:
Timer里有两个数据结构:unordered_map和priority_queue,优先队列存放着TimerNode节点,只要节点的时间到了,或者该节点对应的channel事件关闭了那么该节点就可以删除,同时unorder_map是fd映射TimerNode节点,此TimerNode节点为fd对应的真正的节点,如果priority_queue删除的节点对应的不是unorder_map映射的节点,则证明当前的priority_queue里的节点并非真正的节点,故把堆顶的节点删除后要把unordered_map节点放进堆里。
Log日志实现逻辑(参考muduo):
首先是Logger类,Logger类里面有Impl类,其实具体实现是Impl类,我也不懂muduo为何要再封装一层,那么我们来说说Impl干了什么,在初始化的时候Impl会把时间信息存到LogStream的缓冲区里,在我们实际用Log的时候,实际写入的缓冲区也是LogStream,在析构的时候Impl会把当前文件和行数等信息写入到LogStream,再把LogStream里的内容写到AsyncLogging的缓冲区中,当然这时候我们要先开启一个后端线程用于把缓冲区的信息写到文件里.
然后来说说LogStream类,里面其实就一个Buffer缓冲区,是用来暂时存放我们写入的信息的.还有就是重载运算符,因为我们采用的是C++ Stream的风格
再来说说AsyncLogging类,这个是最核心的部分,我们知道在多线程程序中写Log无非就是前端往后端写,后端往硬盘写,首先前面提到了将LogStream的内容写到了AsyncLogging缓冲区里,也就是前端往后端写,这个过程通过append函数实现,后端实现通过threadfunc函数,两个线程的同步和等待通过互斥锁和条件变量来实现,具体实现使用了双缓冲技术,双缓冲技术的基本思路:准备两块buffer,A和B,前端往A写数据,后端从B里面往硬盘写数据,当A写满后,交换A和B,如此反复.不过实际的实现的话和这个还是有点区别,具体看代码吧
剩下的LogFile类和FileUtil类其实没什么好说的,就是把文件用RAII机制封装了,LogFile在FileUtil的基础上再封装增加了点功能罢了.
内存池
结合STL的实现和github上第一个memorypool的实现,我创建了64个块,里面维护了一个free_list,每一块都是8的倍数,比如下标为0的块free_list维护的是8B的链表,下标为1的是16B的链表,根据需要去找不同的块,当块内的空闲内存不足时通过malloc申请一大块内存,大致实现思路是这样,详细的可以去看github上的第一个memorypool的实现,我只不过在其基础上多了64个块罢了。
LFU缓存
具体实现请看我另一篇博客,https://blog.csdn.net/qq_34262582/article/details/80358388
涉及的知识点:
getopt():这个函数是Linux的c程序接口,用于转换命令行参数作为Linux命令选项和参数。
函数原型:int getopt(int argc,char * const argv[],const char * optstring);
前两个参数就是命令行参数,第三个就是选项字符串。
举个例子:选项字符串为"ab:c:d"
那么该程序有四个命令选项-a,-b,-c,-d。其中带冒号的b和c后面带参数,参数默认保存在optarg里。返回值为命令选项,返回-1证明没有参数了。
std::bind和std::function:这两个是C++11的新特性,用于函数调用。
std::bind,这个可以绑定函数参数,返回一个可用的临时变量,用于延迟调用。
std::function,作用类似函数指针,但是它比函数指针要安全,无需释放,可以认为是函数指针的智能指针。
通常两者结合使用,通过bind绑定参数返回给function,在需要的时候调用function就好了。
std::move:也是C++11的东西,用于赋值
当你一个变量a要赋值给另一个变量b的时候,如果赋值完后变量a就要销毁,那么这一步赋值的代价就不划算,故提供了一种新的方式,权限转移机制,你也可以理解为让这个变量a变成临时变量,也就是左值变成了右值。通过这种操作赋值完后a的值会变得未知。这里又谈到了左值和右值的问题,那么我们下面谈谈右值引用。
右值引用&&:这个顾名思义就是引用右值的。还是C++11的特性。
先来说说左值和右值
左值:可以出现在赋值号的左边或右边的变量。
右值:只能出现在赋值号右边的常量或临时变量。
举个例子a=3。a是变量可以出现在等号的左边和右边,所以是左值,3是常量只能出现在等号的右边是右值。
再有:a=a+b,a+b返回的是临时变量,只能出现在右边,故是右值。
然后右值引用就是用来处理无法引用右值的问题,在C++11以前,右值是没法引用的。
智能指针:虽然以前也有智能指针auto_ptr,但是到了C++11后已经摒弃这个东西了。
首先讲讲智能指针的原理,因为一般指针使用都是要new/malloc,然后离开作用域前要delete/free,那么你很有可能会忘了delete/free,而导致内存泄露。这时候智能指针就诞生了,通过把指针封装在模板类里面,调用构造函数分配内存,析构函数释放内存,那么只要离开作用域就会自动调用析构函数释放内存,就可以解决你忘了delete/free而导致的内存泄露问题了。其实本质上就是RAII机制.
shared_ptr:采用引用计数原理,每次复制都会把引用计数加1,销毁一个就减一,当引用计数为0时释放内存。
unique_ptr:控制权唯一,同一时间一个对象只能由一个unique_ptr指向它,离开作用域后释放内存,unique_ptr若要赋值给另一个unique_ptr,只能通过std::move变成右值,释放掉控制权才可以赋值给另一个unique_ptr。
weak_ptr:shared_ptr的辅助指针,只获得观测权,并不会使引用计数增加或减少。他的作用主要是解决循环引用问题,例如有两个类a和b,a的成员变量是shared_ptr <类b>,b的成员变量是shared_ptr<类a>,然后创建两个智能指针类,互相对成员变量赋值,最后导致a包含b,b包含a,那么他们的引用计数是2,当离开作用域后,假设b先销毁,那么b的引用计数减1,所以b的内存空间不释放,b的内存空间不释放意味着b里面的成员变量不释放,所以a的引用计数还是2,然后到a离开作用域,a也和b的结果一样,引用计数变成1,故两个都不能释放资源。为了解决这种循环引用问题,引入了weak_ptr来解决。
for循环语句:C++11新特性帮助你简化for语句
知道这个之前首先得知道auto变量,还是C++11的新特性,编译器自动根据上下文推断应该是什么类型,有些时候例如你要描述一个迭代器,那么你得写很长,这样看起来很难受,写起来也麻烦,直接用auto它会自己推断为相应的类型,例如auto i=1,那么i为int类型.不要以为这是弱类型,其实是不一样的.
然后for的新用法,直接举例比较快:
int main()
{
int numbers[] = { 1,2,3,4,5 };
std::cout << "numbers:" << std::endl;
for (auto &number : numbers)
{
std::cout << number << std::endl;
}
}
代码的意思是遍历numbers,对于每一个numbers的元素赋值给&number,当然你也可以写成auto number,这样的话就无法修改numbers里面的元素.
pthread_once_t:控制变量
其实这个我觉得也没什么好讲的,就是用来使某个函数在整个进程里只执行一次,所以叫控制变量。
这里如果有看过设计模式的朋友们会想起单例模式,没错单例模式使用控制变量的话就可以很优雅的实现了。不需要互斥锁+条件变量的使用。
用法:pthread_once(函数地址,控制变量地址);
mmap:内存映射
把文件或一个Posix共享内存区对象映射到进程的地址空间。
一般来说使用这个有三个目的:
(1)使用普通文件以提供内存映射I/O.
(2)使用特殊文件以提供匿名内存映射.
(3)使用shm_open以提供无亲缘关系进程间的Posix共享内存区.
项目中使用mmap的目的是就是第一个,通过映射可以不需要调用read和write系统调用,也可以释放fd资源。
gettimeofday:精确的获取时间
使用方法就不贴了,看github里的代码一看就懂的,要注意的问题是值都是32位的,所以*1000之后有可能溢出,这里我改成long long 以防溢出。