WebServer笔记

WebServer 笔记

学习开源项目WebServer的笔记,主要是复习回顾,写的比较简单

pool

该部分分为线程池threadpool和数据库连接池sqlconnpool两部分

ThreaPool

这部分线程池是写死的大小,使用for循环直接创建然后直接detach分离线程

for (size_t i = 0; i < threadCount; i++) {
    std::thread([pool = pool_] {
        // 任务为临界资源,上锁防止多个线程执行同一个任务
        std::unique_lock <std::mutex> locker(pool->mtx);
        while (true) {
            if (!pool->tasks.empty()) {
                // 获得对应的任务函数,这个阶段上了锁
                auto task = std::move(pool->tasks.front());
                pool->tasks.pop();
                locker.unlock();
                // 获得任务后解锁并执行该任务
                task();
                locker.lock();
            } else if (pool->isClosed) {
                break;
            } else {
                // 若没有获得任务,则阻塞等待
                pool->cond.wait(locker);
            }
        }
    }).detach();
}

其使用队列存储可执行任务std::queue<std::function<void()>> tasks,并给队列的pushpop操作都加了锁

SqlConnPool

数据库连接池同样使用了一个队列存储连接std::queue<MYSQL *> connQue_,只通过接口获取连接和放回连接


/**
 * 从数据库连接池获得新的连接
 * @return
 */
MYSQL *SqlConnPool::GetConn() {
    MYSQL *sql = nullptr;
    if (connQue_.empty()) {
        LOG_WARN("SqlConnPool busy!");
        return nullptr;
    }
    sem_wait(&semId_);
    {
        lock_guard <mutex> locker(mtx_);
        sql = connQue_.front();
        connQue_.pop();
    }
    return sql;
}

/**
 * 将连接放回连接池中
 * @param sql
 */
void SqlConnPool::FreeConn(MYSQL *sql) {
    assert(sql);
    lock_guard <mutex> locker(mtx_);
    connQue_.push(sql);
    sem_post(&semId_);
}

连接池使用单例模式获取,在整个项目中,只存在一个SqlConnPool实例对象

SqlConnPool *SqlConnPool::Instance() {
    static SqlConnPool connPool;
    return &connPool;
}

但是不直接操作数据库连接池,而是通过RAII模式,设计了SqlConnRAII类来管理连接

class SqlConnRAII {
public:
    SqlConnRAII(MYSQL** sql, SqlConnPool *connpool) {
        assert(connpool);
        *sql = connpool->GetConn();
        sql_ = *sql;
        connpool_ = connpool;
    }
    
    ~SqlConnRAII() {
        if(sql_) {
            connpool_->FreeConn(sql_);
        }
    }
    
private:
    MYSQL *sql_;
    SqlConnPool* connpool_;
};

在每次创建SqlConnRAII对象时,获取连接,而在SqlConnRAII对象被销毁时释放连接。
但在代码中有一个bug,就是他在HttpRequest类中使用了该方法获取连接(而且在结尾居然调手动调用FreeConn释放连接),但是是以匿名对象的方式,这样的话该匿名对象会直接被销毁导致连接立刻被放回连接池中

HttpConn

HttpConn用于处理Http请求和响应,其包含以下参数

// 客户端socket描述符
int fd_;
struct  sockaddr_in addr_;

bool isClose_;

int iovCnt_;
struct iovec iov_[2];

Buffer readBuff_; // 读缓冲区
Buffer writeBuff_; // 写缓冲区

HttpRequest request_; // 处理请求
HttpResponse response_; // 处理相应

在接收到请求时调用init方法,初始化读写缓冲区

void HttpConn::init(int fd, const sockaddr_in &addr) {
    assert(fd > 0);
    userCount++;
    addr_ = addr;
    fd_ = fd;
    writeBuff_.RetrieveAll();
    readBuff_.RetrieveAll();
    isClose_ = false;
    LOG_INFO("Client[%d](%s:%d) in, userCount:%d", fd_, GetIP(), GetPort(), (int) userCount);
}

收到请求时调用read方法,将数据写入读缓冲区

/**
 * 读取客户端发送的数据到读缓冲区
 * @param saveErrno 传出读取时发生的错误
 * @return 成功返回大于1,失败返回-1,连接关闭返回0
 */
ssize_t HttpConn::read(int *saveErrno) {
    ssize_t len = -1;
    do {
        len = readBuff_.ReadFd(fd_, saveErrno);
        if (len <= 0) {
            break;
        }
    } while (isET);
    return len;
}

process方法用于解析请求和制作响应,响应报文写入到写缓冲区中

/**
 * 处理请求
 * @return 若读缓冲区满则返回false表示不可读
 */
bool HttpConn::process() {
    request_.Init();
    // 读缓冲区是否为空,是否可读
    if (readBuff_.ReadableBytes() <= 0) {
        return false;
    } else if (request_.parse(readBuff_)) {
        // 若HTTP请求报文正常被解读
        LOG_DEBUG("%s", request_.path().c_str());
        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;
    }
    LOG_DEBUG("filesize:%d, %d  to %d", response_.FileLen(), iovCnt_, ToWriteBytes());
    return true;
}

write方法用于将写缓冲区的数据发给客户端

/**
 * 写入读缓冲区的数据
 * @param saveErrno 传出读写入发生的错误
 * @return
 */
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;
}

该部分的流程就是read读数据到读缓冲区–>process解析读缓冲区的数据,并制作响应放入写缓冲区–>write发送写缓冲区的数据

HttpRequest

该类用于HTTP请求解析
在解析请求时,其定义了元组PARSE_STATE来记录其解析到了哪一步

enum PARSE_STATE {
    REQUEST_LINE, // 请求行
    HEADERS, // 请求头
    BODY, // 请求体
    FINISH, // 解析完成
};
/**
 * 解析HTTP报文
 * @param buff
 * @return
 */
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);
        // 读出一行,用CRLF隔开
        std::string line(buff.Peek(), lineEnd);
        switch (state_) {
            case REQUEST_LINE:
                // 解析请求行
                if (!ParseRequestLine_(line)) {
                    return false;
                }
                ParsePath_();
                break;
            case HEADERS:
                // 解析请求头
                ParseHeader_(line);
                if (buff.ReadableBytes() <= 2) {
                    state_ = FINISH;
                }
                break;
            case BODY:
                // 解析请求体
                ParseBody_(line);
                break;
            default:
                break;
        }
        // 文件是否被读完
        if (lineEnd == buff.BeginWrite()) {
            break;
        }
        // 移动读指针,跳过CRLF
        buff.RetrieveUntil(lineEnd + 2);
    }
    LOG_DEBUG("[%s], [%s], [%s]", method_.c_str(), path_.c_str(), version_.c_str());
    return true;
}

使用正则表达式来解析请求行和请求头

bool HttpRequest::ParseRequestLine_(const string &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;
    }
    LOG_ERROR("RequestLine Error");
    return false;
}

void HttpRequest::ParseHeader_(const string &line) {
    regex patten("^([^:]*): ?(.*)$");
    smatch subMatch;
    if (regex_match(line, subMatch, patten)) {
        header_[subMatch[1]] = subMatch[2];
    } else {
        state_ = BODY;
    }
}

该类只支持GET和POST两种请求,针对两种请求的不同,其定义不同的解析请求体的方式

void HttpRequest::ParsePost_() {
    // 如果时post请求
    if (method_ == "POST" && header_["Content-Type"] == "application/x-www-form-urlencoded") {
        // 接续URL中的信息
        ParseFromUrlencoded_();
        // 如果是注册或者登录
        if (DEFAULT_HTML_TAG.count(path_)) {
            int tag = DEFAULT_HTML_TAG.find(path_)->second;
            LOG_DEBUG("Tag:%d", tag);
            if (tag == 0 || tag == 1) {
                bool isLogin = (tag == 1);
                // 如果是登录请求且登录成功
                if (UserVerify(post_["username"], post_["password"], isLogin)) {
                    path_ = "/welcome.html";
                } else {
                    //登陆失败
                    path_ = "/error.html";
                }
            }
        }
    }
}

在这个函数中,我觉得ParseFromUrlencoded_()不该在这里调用,因为从其名字和函数本体上来看,它应该是用来解析URL中的信息,而这时GET请求发送信息的方式


void HttpRequest::ParseFromUrlencoded_() {
    if (body_.size() == 0) {
        return;
    }

    string key, value;
    int num = 0;
    int n = body_.size();
    int i = 0, j = 0;

    for (; i < n; i++) {
        char ch = body_[i];
        switch (ch) {
            case '=':
                key = body_.substr(j, i - j);
                j = i + 1;
                break;
            case '+':
                body_[i] = ' ';
                break;
            case '%':
                num = ConverHex(body_[i + 1]) * 16 + ConverHex(body_[i + 2]);
                body_[i + 2] = num % 10 + '0';
                body_[i + 1] = num / 10 + '0';
                i += 2;
                break;
            case '&':
                value = body_.substr(j, i - j);
                j = i + 1;
                post_[key] = value;
                LOG_DEBUG("%s = %s", key.c_str(), value.c_str());
                break;
            default:
                break;
        }
    }
    assert(j <= i);
    if (post_.count(key) == 0 && j < i) {
        value = body_.substr(j, i - j);
        post_[key] = value;
    }
}

HttpResponse

HttpResponse用于进行HTTP响应的处理
响应码

const unordered_map<int, string> HttpResponse::CODE_STATUS = {
        {200, "OK"},
        {400, "Bad Request"},
        {403, "Forbidden"},
        {404, "Not Found"},
};

const unordered_map<int, string> HttpResponse::CODE_PATH = {
        {400, "/400.html"},
        {403, "/403.html"},
        {404, "/404.html"},
};

使用MakePesponse制作响应

/**
 * 对HTTP请求做出响应
 * @param buff
 */
void HttpResponse::MakeResponse(Buffer &buff) {
    /* 判断请求的资源文件 */
    // 获取的文件属性<0 或者文件路径错误
    if (stat((srcDir_ + path_).data(), &mmFileStat_) < 0 || S_ISDIR(mmFileStat_.st_mode)) {
        // 404 not fund
        code_ = 404;
        // 是否由可读权限
    } else if (!(mmFileStat_.st_mode & S_IROTH)) {
        // 403 资源不可用
        code_ = 403;
    } else if (code_ == -1) {
        // 正常响应
        code_ = 200;
    }
    ErrorHtml_();
    AddStateLine_(buff);
    AddHeader_(buff);
    AddContent_(buff);
}

该部分并不是根据响应码来判断是否调用响应的处理函数,而是先调用处理函数,再在处理函数内部进行判断,这个地方我觉得做的不好

/**
 * 根据错误码得到对应的html页面,若状态码不是错误码则不执行
 */
void HttpResponse::ErrorHtml_() {
    // 如果状态码为错误
    if (CODE_PATH.count(code_) == 1) {
        // 得到状态码对应的html文件名
        path_ = CODE_PATH.find(code_)->second;
        // 得到文件状态
        stat((srcDir_ + path_).data(), &mmFileStat_);
    }
}
/**
 * 制作响应报文行
 * @param buff 写缓存
 */
void HttpResponse::AddStateLine_(Buffer &buff) {
    string status;
    if (CODE_STATUS.count(code_) == 1) {
        status = CODE_STATUS.find(code_)->second;
    } else {
        code_ = 400;
        status = CODE_STATUS.find(400)->second;
    }
    buff.Append("HTTP/1.1 " + to_string(code_) + " " + status + "\r\n");
}
/**
 * 制作相应报文头
 * @param buff
 */
void HttpResponse::AddHeader_(Buffer &buff) {
    buff.Append("Connection: ");
    if (isKeepAlive_) {
        buff.Append("keep-alive\r\n");
        buff.Append("keep-alive: max=6, timeout=120\r\n");
    } else {
        buff.Append("close\r\n");
    }
    buff.Append("Content-type: " + GetFileType_() + "\r\n");
}

响应报文体使用文件映射的方式得到html等文件的内容

/**
 * 制作相应报文体
 * @param buff
 */
void HttpResponse::AddContent_(Buffer &buff) {
    int srcFd = open((srcDir_ + path_).data(), O_RDONLY);
    if (srcFd < 0) {
        ErrorContent(buff, "File NotFound!");
        return;
    }

    /* 将文件映射到内存提高文件的访问速度 
        MAP_PRIVATE 建立一个写入时拷贝的私有映射*/
    LOG_DEBUG("file path %s", (srcDir_ + path_).data());
    int *mmRet = (int *) mmap(0, mmFileStat_.st_size, PROT_READ, MAP_PRIVATE, srcFd, 0);
    if (*mmRet == -1) {
        ErrorContent(buff, "File NotFound!");
        return;
    }
    mmFile_ = (char *) mmRet;
    close(srcFd);
    buff.Append("Content-length: " + to_string(mmFileStat_.st_size) + "\r\n\r\n");
}

Buffer

在Buffer类中,其使用vector作为缓冲区,并定义了读指针readPos_和写指针readPos_,该类主要用于在HttpRequestHttResponse两个类中使用

std::vector<char> buffer_;
std::atomic<std::size_t> readPos_;
std::atomic<std::size_t> writePos_;

在初始化时,读指针和写指针都为0,读的时候读指针递增,写的时候写指针递增

ssize_t Buffer::ReadFd(int fd, int *saveErrno) {
    char buff[65535];
    struct iovec iov[2];
    const size_t writable = WritableBytes();
    /* 分散读, 保证数据全部读完 */
    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;
    } else if (static_cast<size_t>(len) <= writable) {
        writePos_ += len;
    } else {
        writePos_ = buffer_.size();
        Append(buff, len - writable);
    }
    return len;
}
ssize_t Buffer::WriteFd(int fd, int *saveErrno) {
    size_t readSize = ReadableBytes();
    ssize_t len = write(fd, Peek(), readSize);
    if (len < 0) {
        *saveErrno = errno;
        return len;
    }
    readPos_ += len;
    return len;
}

通过缓冲区大小和读写指针的大小来判断是否可读或可写等

size_t Buffer::ReadableBytes() const {
    return writePos_ - readPos_;
}

size_t Buffer::WritableBytes() const {
    return buffer_.size() - writePos_;
}

Timer

该部分负责定时任务,HeapTimer使用堆的方式存储定时任务,其包含两个属性

// 用于存储定时事件节点
std::vector <TimerNode> heap_;
// 用于判断是否存在id的节点,key为事件节点的id,value为事件节点在head_中的位置
std::unordered_map<int, size_t> ref_;

节点定义为

typedef std::function<void()> TimeoutCallBack;
typedef std::chrono::high_resolution_clock Clock;
typedef std::chrono::milliseconds MS;
typedef Clock::time_point TimeStamp;

struct TimerNode {
    int id;
    TimeStamp expires;
    TimeoutCallBack cb;

    bool operator<(const TimerNode &t) {
        return expires < t.expires;
    }
};

节点在插入时按照触发事件排序

/**
 * 添加新的定时任务,按照结束时间升序,使用堆排序
 * @param id
 * @param timeout
 * @param cb
 */
void HeapTimer::add(int id, int timeout, const TimeoutCallBack &cb) {
    assert(id >= 0);
    size_t i;
    if (ref_.count(id) == 0) {
        /* 新节点:堆尾插入,调整堆 */
        i = heap_.size();
        ref_[id] = i;
        heap_.push_back({id, Clock::now() + MS(timeout), cb});
        siftup_(i);
    } else {
        /* 已有结点:调整堆 */
        i = ref_[id];
        heap_[i].expires = Clock::now() + MS(timeout);
        heap_[i].cb = cb;
        if (!siftdown_(i, heap_.size())) {
            siftup_(i);
        }
    }
}

通过调用tick函数来触发定时任务

/**
 * 触发定时任务
 */
void HeapTimer::tick() {
    /* 清除超时结点 */
    if (heap_.empty()) {
        return;
    }
    while (!heap_.empty()) {
        TimerNode node = heap_.front();
        // 若为达到超时时间
        if (std::chrono::duration_cast<MS>(node.expires - Clock::now()).count() > 0) {
            break;
        }
        // 执行定时任务
        node.cb();
        // 删除超时节点
        pop();
    }
}

后面可以看到,他是通过获取最早的定时任务时间和当前时间进行对比然后主动调用tick函数进行触发的,感觉这样定时任务的触发不够准确

Server

WebServer的主干,其定义了WebServer的主要逻辑,其包含的参数如下

// 端口
int port_;
// 是否优雅退出
bool openLinger_;
// 超市时间
int timeoutMS_;  /* 毫秒MS */
// 监听是否关闭
bool isClose_;
// 监听用socket描述符
int listenFd_;
// 当前工作目录的绝对路径
char *srcDir_;
// 监听事件
uint32_t listenEvent_;
//连接事件
uint32_t connEvent_;
// 设置定时
std::unique_ptr<HeapTimer> timer_;
// 线程池
std::unique_ptr<ThreadPool> threadpool_;
// epoll多路复用
std::unique_ptr<Epoller> epoller_;
// 记录socket
std::unordered_map<int, HttpConn> users_;

对于定时任务,它不是用设置定时任务的方式,而是在循环中主动判断是否超时

/**
 * webserver 主程序
 */
void WebServer::Start() {
    int timeMS = -1;  /* epoll wait timeout == -1 无事件将阻塞 */
    if (!isClose_) {
        LOG_INFO("========== Server start ==========");
    }
    while (!isClose_) {
        if (timeoutMS_ > 0) {
            // 若达到超时时间,寻找超时的节点
            timeMS = timer_->GetNextTick();
        }
        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_) {
                // 若是监听socket发生事件,即有新连接
                DealListen_();
            } else if (events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) {
                // 若socket发生异常或者读端关闭或读写都关闭
                assert(users_.count(fd) > 0);
                CloseConn_(&users_[fd]);
            } else if (events & EPOLLIN) {
                // 若socket收到输入数据
                assert(users_.count(fd) > 0);
                DealRead_(&users_[fd]);
            } else if (events & EPOLLOUT) {
                // 若socket可以进行写操作,即写缓冲区从满变为不满
                assert(users_.count(fd) > 0);
                DealWrite_(&users_[fd]);
            } else {
                LOG_ERROR("Unexpected event");
            }
        }
    }
}
/**
 * 当监听socket发现有新的连接
 */
void WebServer::DealListen_() {
    struct sockaddr_in addr;
    socklen_t len = sizeof(addr);
    do {
        int fd = accept(listenFd_, (struct sockaddr *) &addr, &len);
        if (fd <= 0) {
            return;
        } else if (HttpConn::userCount >= MAX_FD) {
            // 若连接已满
            SendError_(fd, "Server busy!");
            LOG_WARN("Clients is full!");
            return;
        }
        AddClient_(fd, addr);
    } while (listenEvent_ & EPOLLET);
}

/**
 * 读事件处理
 * @param client
 */
void WebServer::DealRead_(HttpConn *client) {
    assert(client);
    // 连接被激活,超时时间重新计时
    ExtentTime_(client);
    // 将读任务交给线程池中的线程处理
    threadpool_->AddTask(std::bind(&WebServer::OnRead_, this, client));
}

/**
 * 对socket进行写操作
 * @param client
 */
void WebServer::DealWrite_(HttpConn *client) {
    assert(client);
    // 连接被激活,超时时间重新计时
    ExtentTime_(client);
    // 将写任务交给线程池处理
    threadpool_->AddTask(std::bind(&WebServer::OnWrite_, this, client));
}

Epoller

将epoll多路复用的内容封装成类,只包含两个属性

int epollFd_;
std::vector<struct epoll_event> events_;

该类只要就是将epoll的操作函数都进行封装

/**
 * 添加epoll需要监听的socket描述符
 * @param fd 需要监听的描述符
 * @param events 监听事件
 * @return
 */
bool Epoller::AddFd(int fd, uint32_t events) {
    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);
}

/**
 * 修改epoll监听事件
 * @param fd
 * @param events
 * @return
 */
bool Epoller::ModFd(int fd, uint32_t events) {
    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);
}

/**
 * 删除epoll监听事件
 * @param fd
 * @return
 */
bool Epoller::DelFd(int fd) {
    if (fd < 0) return false;
    epoll_event ev = {0};
    return 0 == epoll_ctl(epollFd_, EPOLL_CTL_DEL, fd, &ev);
}

/**
 * 添加监听定时任务
 * @param timeoutMs
 * @return
 */
int Epoller::Wait(int timeoutMs) {
    return epoll_wait(epollFd_, &events_[0], static_cast<int>(events_.size()), timeoutMs);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值