WebServer 高性能服务器

本文基于《Linux 高性能服务器编程》和 markparticle 大佬的代码 markparticle--GitHub,将 WebServer 高性能服务器各部分解耦,从实现一个最基本的基于 epoll 的 HTTP 服务器开始,逐步添加后续模块,包括线程池和定时器、数据库连接池和注册登录功能、及日志系统。根据对项目模块的拆分,进一步加深对项目的理解,也方便自己后面复习。

本系列文章:

WebServer 高性能服务器

WebServer 高性能服务器v2

WebServer 高性能服务器 v3

WebServer 高性能服务器v4


实现一个基于 epoll 的 HTTP 服务器

第一部分代码在基于epoll的简单HTTP服务器

利用标准库容器封装char,实现自动增长的缓冲区

缓冲区底层基本的数据结构为std::vector<char>,在向缓冲区中读数据时采用分散读的方式,保证数据全部读完。下面是 Buffer 类的基本结构,部分函数实现重载是为了后续其他模块的使用。特别注意的是这里的读是指向缓冲区中读数据(其实就是将数据写入缓冲区)写是指将缓冲区中的数据写出(同理其实就是把数据从缓冲区取出来),要搞清楚这个逻辑关系。

// buffer.h 
class Buffer {
public:
     Buffer(int initBufferSize=1024);
     ~Buffer() =default;
 ​
     size_t WriteableBytes() const;              // 缓冲区中可写的字节数
     size_t ReadableBytes() const;               // 缓冲区中未读的字节数
     size_t PrependableBytes() const;            // 缓冲区中已读过的字节数
 ​
     const char*Peek() const;                   // 返回要取出数据的起始位置
     void EnsureWriteable(size_tlen);           // 判断缓冲区是否够用,不够就创造空间(调用 MakeSpace_ 函数)
     void HasWritten(size_tlen);                // 写入 len 长度的数据,更新 writePos_
 ​
     void Retrieve(size_tlen);                  // 取出 len 长度的未读数据,更新 readPos_
     void RetrieveUntil(constchar*end);        // 取出到指定位置之间的未读数据,更新 readPos_
     void RetrieveAll();                         // 清空缓冲区
     std::string RetrieveAllToStr();             // 将未读数据转为字符串返回,清空缓冲区
 ​
     const char*BeginWriteConst() const;        // 返回要写入数据的起始位置
     char*BeginWrite();
 ​
     void Append(conststd::string&str);
     void Append(constchar*str, size_tlen);   // 向指定位置写入数据
     void Append(constvoid*data, size_tlen);
     void Append(constBuffer&buff);
 ​
     ssize_t ReadFd(intfd, int*Errno);         // 向缓冲区中读入数据
     ssize_t WriteFd(intfd, int*Errno);        // 从缓冲区中取出数据
 ​
 private:
     char*BeginPtr_();                          // 缓冲区起始地址
     const char*BeginPtr_() const;
     voidMakeSpace_(size_tlen);                // 如果可写+已读空间不够直接resize,否则将未读取数据移动到起始地址
 ​
     std::atomic<std::size_t>readPos_;          // 已经取出数据的末尾
     std::atomic<std::size_t>writePos_;         // 已经写入数据的末尾
     std::vector<char>buffer_;                  // 缓冲区
 ​
 };

比较重要的三个函数是 ReadFd WriteFd MakeSpace_,下面分别看一下他们的具体实现。

 ssize_t Buffer::ReadFd(int fd, int*saveErrno) {
     char buff[65535];
     struct iovec iov[2];
     const size_t writable=WriteableBytes();
     /* 分散读, 保证数据全部读完 */
     iov[0].iov_base=BeginPtr_() +writePos_;
     iov[0].iov_len=writable;
     iov[1].iov_base=buff;
     iov[1].iov_len=sizeof(buff);
 ​
     const ssize_t len=readv(fd, iov, 2);
     if(len<0) {
         *saveErrno=errno;
     }
     elseif(static_cast<size_t>(len) <=writable) { // 判断是否超出可写的字节数
         writePos_+=len;
     }
     else {                                      // 超过就用Append进行分散读
         writePos_=buffer_.size();
         Append(buff, len-writable);
     }
     returnlen;
 }
 ssize_t Buffer::WriteFd(intfd, int*saveErrno) {
     size_t readSize=ReadableBytes();
     ssize_t len=write(fd, Peek(), readSize);
     if(len<0) {
         *saveErrno=errno;
         return len;
     }
     readPos_+=len;
     return len;
 }
 void Buffer::MakeSpace_(size_tlen) {
     if(WriteableBytes() +PrependableBytes() <len) {   // 可用空间不够直接扩展
         buffer_.resize(writePos_+len+1);
     }
     else {                                          // 将未读的数据拷贝到起始位置,更新readPos_和writePos_
         size_treadable=ReadableBytes();
         std::copy(BeginPtr_() +readPos_, BeginPtr_() +writePos_, BeginPtr_());
         readPos_=0;
         writePos_=readPos_+readable;
         assert(readable==ReadableBytes());
     }
 }

ReadFd函数最关键的地方就是采用分散读,通过判断缓冲区剩余字节数确定存储的位置。

MakeSpace_函数主要来实现自动增长的功能,具体通过resize()函数实现。

利用状态机解析 HTTP 请求报文,处理静态资源的请求

在处理 HTTP 请求报文时,采用有限状态机对 HTTP 请求报文的不同部分进行解析,在处理完当前状态的逻辑后,将状态更新方便处理接下来的状态。在解析报文时采用正则表达式进行匹配,提高效率。

 // http_request.cpp
 bool HttpRequest::parse(Buffer&buff) {
     const char CRLF[] ="\r\n";
     if(buff.ReadableBytes() <=0) {
         return false;
     }
     while(buff.ReadableBytes() &&state_!=FINISH) {
         const char* lineEnd=search(buff.Peek(), buff.BeginWriteConst(), CRLF, CRLF+2);
         std::stringline(buff.Peek(), lineEnd);
         switch(state_)
         {
             case REQUEST_LINE:
                 if(!ParseRequestLine_(line)) {          // 解析请求行
                     returnfalse;
                 }
                 ParsePath_();                           // 解析要请求的资源
                 break;
             case HEADERS:
                 ParseHeader_(line);                     // 解析请求头
                 if(buff.ReadableBytes() <=2) {         // 可能没有请求体
                     state_=FINISH;
                 }
                 break;
             case BODY:
                 ParseBody_(line);                       // 解析请求体
                 break;
             default:
                 break;
         }
         if(lineEnd==buff.BeginWrite()) {
             if(method_=="POST"&&state_==FINISH) {
                 buff.RetrieveUntil(lineEnd);
             }
             break;
         }
         buff.RetrieveUntil(lineEnd+2);
     } 
     return true;
 }
 ​
 bool HttpRequest::ParseRequestLine_(conststring&line) {
     regex patten("^([^ ]*) ([^ ]*) HTTP/([^ ]*)$");     // 采用正则表达式进行匹配
     smatch subMatch;
     if(regex_match(line, subMatch, patten)) {
         method_=subMatch[1];
         path_=subMatch[2];
         version_=subMatch[3];
         state_=HEADERS;
         return true;
     }
     return false;
 }

封装HttpConn类,每个客户端套接字对应一个HttpConn类

HttpConn::read函数直接调用缓冲区的读函数。与readv分散读相对应,writev函数以顺序iov[0]、iov[1]至iov[iovcnt-1]从各缓冲区中聚集输出数据到fd,称为集中写。由下面的process函数可以看到,通常状态行、头部字段和空行放在一块内存,而文档内容通常被放到另外一块内存,使用writev函数可以将它们同时写出。process函数包括两步:1.从读缓冲区中取出请求报文并解析,2.根据请求报文制作相应的响应报文并放入写缓冲区中。

 // http_connect.cpp
 ssize_t HttpConn::read(int*saveErrno) {
     ssize_t len=-1;
     do {
         len=readBuff_.ReadFd(fd_, saveErrno);
         if (len<=0) {
             break;
         }
     } while (isET);
     return len;
 }
 ​
 ssize_t HttpConn::write(int*saveErrno) {
     ssize_t len=-1;
     do {
         len=writev(fd_, iov_, iovCnt_);
         if(len<=0) {
             *saveErrno=errno;
             break;
         }
         if(iov_[0].iov_len+iov_[1].iov_len  ==0) { break; } /* 传输结束 */
         else if(static_cast<size_t>(len) >iov_[0].iov_len) {
             iov_[1].iov_base= (uint8_t*) iov_[1].iov_base+ (len-iov_[0].iov_len);
             iov_[1].iov_len-= (len-iov_[0].iov_len);
             if(iov_[0].iov_len) {
                 writeBuff_.RetrieveAll();
                 iov_[0].iov_len=0;
             }
         }
         else {
             iov_[0].iov_base= (uint8_t*)iov_[0].iov_base+len;
             iov_[0].iov_len-=len;
             writeBuff_.Retrieve(len);
         }
     } while(isET||ToWriteBytes() >10240);
     return len;
 }
 ​
 bool HttpConn::process() {
     request_.Init();
     if(readBuff_.ReadableBytes() <=0) {
         returnfalse;
     }
     elseif(request_.parse(readBuff_)) {
         response_.Init(srcDir, request_.path(), request_.IsKeepAlive(), 200);
     } else {
         response_.Init(srcDir, request_.path(), false, 400);
     }
 ​
     response_.MakeResponse(writeBuff_);
     /* 响应头 */
     iov_[0].iov_base=const_cast<char*>(writeBuff_.Peek());
     iov_[0].iov_len=writeBuff_.ReadableBytes();
     iovCnt_=1;
 ​
     /* 文件 */
     if(response_.FileLen() >0 &&response_.File()) {
         iov_[1].iov_base=response_.File();
         iov_[1].iov_len=response_.FileLen();
         iovCnt_=2;
     }
     return true;
 }

I/O 复用技术 epoll

epoll 接口是为解决 Linux 内核处理大量文件描述符而提出的方案。该接口属于 Linux 下多路 I/O 复用接口中 select/poll 的增强。其经常应用于 Linux 下高并发服务型程序,特别是在大量并发连接中只有少部分连接处于活跃下的情况 (通常是这种情况),在该情况下能显著的提高程序的 CPU 利用率。

将 epoll 的接口封装起来,方便后续多次调用。

// epoller.cpp
boolEpoller::AddFd(intfd, uint32_tevents) {      //注册事件
     if(fd<0) return false;
     epoll_event ev= {0};
     ev.data.fd=fd;
     ev.events=events;
     return 0==epoll_ctl(epollFd_, EPOLL_CTL_ADD, fd, &ev);
 }
 ​
 bool Epoller::ModFd(intfd, uint32_tevents) {      //修改已经注册的fd的监听事件
     if(fd<0) return false;
     epoll_event ev= {0};
     ev.data.fd=fd;
     ev.events=events;
     return 0==epoll_ctl(epollFd_, EPOLL_CTL_MOD, fd, &ev);
 }
 ​
 bool Epoller::DelFd(intfd) {                       //从epfd中删除一个fd
     if(fd<0) return false;
     epoll_event ev= {0};
     return 0==epoll_ctl(epollFd_, EPOLL_CTL_DEL, fd, &ev);
 }
 ​
 int Epoller::Wait(inttimeoutMs) {              // 将就绪的事件从内核事件表中复制到它的第二个参数 events 指向的数组
     returnepoll_wait(epollFd_, &events_[0], static_cast<int>(events_.size()), timeoutMs);
 }
 ​
 int Epoller::GetEventFd(size_ti) const {
     assert(i<events_.size() &&i>=0);
     return events_[i].data.fd;
 }
 ​
 uint32_t Epoller::GetEvents(size_ti) const {
     assert(i<events_.size() &&i>=0);
     return events_[i].events;
 }

epoll 对文件描述符的操作有两种模式:LT(电平触发)和 ET(边沿触发),ET 模式更为高效,因此这里选择使用 ET 模式。

 epoll_eventevent;
 event.data.fd=fd;
 event.events=EPOLLIN|EPOLLET;
 epoll_ctl(epollFd, EPOLL_CTL_ADD, fd, &event);

WebServer类封装服务器网络编程的主要逻辑

WebServer::InitSocket_函数主要用于创建套接字,完成基本的网络编程创建逻辑。

WebServer::Start是服务器运行的主函数,对不同的事件进行处理

// webserver.cpp
boolWebServer::InitSocket_() {
     int ret;
     struct sockaddr_in addr{};
     if(port_>65535||port_<1024) {
         return false;
     }
     // 分配地址信息
     addr.sin_family=AF_INET;
     addr.sin_addr.s_addr=htonl(INADDR_ANY);
     addr.sin_port=htons(port_);
     structlingeroptLinger= { 0 };
     if(openLinger_) {
         /* 优雅关闭: 直到所剩数据发送完毕或超时 */
         optLinger.l_onoff=1;
         optLinger.l_linger=1;
     }
     
     listenFd_=socket(AF_INET, SOCK_STREAM, 0);        // 创建套接字
     if(listenFd_<0) {
         returnfalse;
     }
 ​
     ret=setsockopt(listenFd_, SOL_SOCKET, SO_LINGER, &optLinger, sizeof(optLinger));  // 设置套接字优雅关闭
     if(ret<0) {
         close(listenFd_);
         returnfalse;
     }
 ​
     intoptval=1;
     /* 端口复用 */
     /* 只有最后一个套接字会正常接收数据。 */
     ret=setsockopt(listenFd_, SOL_SOCKET, SO_REUSEADDR, (constvoid*)&optval, sizeof(int));
     if(ret==-1) {
         close(listenFd_);
         returnfalse;
     }
 ​
     ret=bind(listenFd_, (structsockaddr*)&addr, sizeof(addr));      // 将地址信息绑定到套接字
     if(ret<0) {
         close(listenFd_);
         returnfalse;
     }
 ​
     ret=listen(listenFd_, 6);                                     // 监听套接字
     if(ret<0) {
         close(listenFd_);
         returnfalse;
     }
     ret=epoller_->AddFd(listenFd_,  listenEvent_|EPOLLIN);      // 将套接字注册到epoll
     if(ret==0) {
         close(listenFd_);
         returnfalse;
     }
     SetFdNonblock(listenFd_);                                   // 设置为非阻塞
     returntrue;
 }
 ​
 void WebServer::Start() {
     int timeMS=-1;  /* epoll wait timeout == -1 无事件将阻塞 */
     while(!isClose_) {
         int eventCnt=epoller_->Wait(timeMS);      // 在一段超时时间上等待一组文件描述符上的事件
         for(int i=0; i < eventCnt; i++) {
             /* 处理事件 */
             int fd = epoller_->GetEventFd(i);
             uint32_t events = epoller_->GetEvents(i);
             if(fd==listenFd_) {
                 DealListen_();
             }
             elseif(events& (EPOLLRDHUP|EPOLLHUP|EPOLLERR)) {
                 assert(users_.count(fd) >0);
                 CloseConn_(&users_[fd]);
             }
             elseif(events&EPOLLIN) {
                 assert(users_.count(fd) >0);
                 DealRead_(&users_[fd]);
             }
             elseif(events&EPOLLOUT) {
                 assert(users_.count(fd) >0);
                 DealWrite_(&users_[fd]);
             } else {
                 std::cout<<"Unexpected event"<<std::endl;
             }
         }
     }
 }

持续更新......

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值