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