TinyWebServer源码阅读(一)

主程序的步骤

在本部分,主要去解析WebServer的四个主要组成部分

  • Webserver类:供main函数使用的webserver主类,通过该类可以进行网络通信的各种连接
  • Epoll类:通过epoll函数进行系统调用
  • Thread类:负责线程池的管理
  • Sql资源管理部分
    main.cpp中,我们可以看到,主程序做了两件事
  • 创建一个webServer
  • 调用了server.start() 启动创建的server

1 WebServer类

下面进入到WebServer类的代码中一探究竟

1.1 初始化

在列表初始化中,初始化了port,Timer,ThreadPool,Epoller
在构造函数中主要包含了以下步骤

  1. 初始化HttpConn的静态变量,包括
    • userCount: 默认为0
    • srcDir: getcwd + '/resources/'
  2. 初始化SqlConnPool
  3. 初始化WebservereventMode_
  4. InitSocket_:
    • 主要包含初始化socket的各种配置,创建listenFd
  5. 初始化log

InitSocket_具体包含:

  • 创建socket
  • 设置数据没发送完毕的时候容许逗留(setsockopt用法介绍
    setsockopt(listenFd_, SOL_SOCKET, SO_LINGER, &optLinger, sizeof(optLinger));
  • 设置端口复用 setsockopt(listenFd_, SOL_SOCKET, SO_REUSEADDR, (const void*)&optval, sizeof(int));
  • bind + listen
  • 添加fd到监听红黑树上
    • 实际通过Epoller类进行操作,核心代码为epoll_ctl(epollFd_, EPOLL_CTL_ADD, fd, &ev)
    • 修改listenFd_为非阻塞: SetFdNonBlock(listenFd_)

1.2 WebServer的启动

涉及到的是WebServer Start部分的代码,timeMS表示事件等待超时的时间

  1. 判断isClose_是否为true,不为true时一直停留在循环内部(步骤2-步骤4)
  2. 通过timer更新timeMS
  3. 调用int eventCnt = epoller_->Wait(timeMS);获取满足监听事件的总数
  4. 遍历获取的事件对事件进行处理
    • 如果是listenfd_,调用DealListen_()
    • 如果EPOLLRDHUP | EPOLLHUP | EPOLLERROR调用CloseConn_()
    • 如果是EPOLLIN,调用DealRead_()
    • 如果是EPOLLOUT,调用DealWrite_()
  5. 跳转至步骤1
  • DealListen_()的处理
  • 调用accept
    • 出错处理
  • 添加client
    • AddClient_中执行
    • 包括添加到timer,调用epoller_->AddFd添加到监听红黑树上,设置nonblock等一系列步骤
  • CloseConn_()的处理
  • epoller_->DelFd()
  • client关闭连接
  • DealRead_()/DealWrite_()的处理
  • 延长时间
  • 加入threadPool

2 Epoller类

Epoller非常简单,主要是对epoll操作的封装,内部用vector<struct epoll_event> events存储监听树上的事件
主要方法如下

  • AddFd(int fd, uint32_t event)
  • ModFd(int fd, uint32_t event)
  • DelFd(int fd)
  • Wait(int timeoutMs)
  • GetEventFd(size_t i)
  • GetEvents(size_t i)
    这里可以直接参考epoll的用法

3 Thread类

核心代码在pool/threadPool中,在ThreadPool成员变量为Pool

   struct Pool {
       std::mutex mtx;
       std::condition_variable cond;
       bool isClosed;
       std::queue<std::function<void()>> tasks;
    };
    std::shared_ptr<Pool> pool_;

对其进行分析,里面主要含有锁mutex,条件变量cond,是否关闭isClosed,函数组成的队列queue<std::function<void()>> tasks
pool是多个线程之间的共享资源,因为对pool的所有操作都需要先获取锁

3.1 线程池

首先来到构造函数,它做的事是

  • 初始化pool_: 这里采用make_shared进行初始化(c11 make_shared),构造函数建议使用make_shared而不是new分配对象
  • 创建threadCount个线程:用detach方法作为后台线程,线程的使用可以参考joining-and-detaching-threads

创建的后台线程做的事如下:
- 使用std::unique_lock创建一个锁(使用方法),该构造方法会直接对mutex对象加锁
- while循环中
- 当tasks不为空时,可以看到已经获取了lock,取出第一个task,释放lock,执行task,最后获取锁进到下一次循环中
- 当tasks为空时,如果有停止标志,退出循环,否则等待
- 当 std::condition_variable 对象的某个 wait 函数被调用的时候,它使用 std::unique_lock(通过 std::mutex) 来锁住当前线程。当前线程会一直被阻塞,直到另外一个线程在相同的 std::condition_variable 对象上调用了 notification 函数来唤醒当前线程
- 观察到在addTask方法中, pool_->cond.notify_one()会将其唤醒

   explicit ThreadPool(size_t threadCount = 8): pool_(std::make_shared<Pool>()) {
           assert(threadCount > 0);
           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();
           }
   }

3.2 回调函数的放入

std::function<void()>是可调用对象包装器,可用于callback,参考why use std::function, std::function and std::bind, std::bind

整个放入到tasks到流程如下

threadpool_->AddTask(std::bind(&WebServer::OnWrite_, this, client))
void AddTask(F&& task) {
    {
        std::lock_guard<std::mutex> locker(pool_->mtx);
        pool_->tasks.emplace(std::forward<F>(task));
    }
    pool_->cond.notify_one();
}
std::queue<std::function<void()>> tasks;		

std::bindclient参数绑定到WebServer::OnWrite_方法,这样返回的functor就变为void(),放入到std::function<void()>可调用对象包装器中。

3.3 析构函数

在析构函数中,需要唤醒阻塞的后台进程,使其结束。

~ThreadPool() {
    if(static_cast<bool>(pool_)) {
        {
            std::lock_guard<std::mutex> locker(pool_->mtx);
            pool_->isClosed = true;
        }
        pool_->cond.notify_all();
    }
}

4 Sql资源管理

主要包含以下文件:sqlConnRAII.h, sqlconnpool.h, sqlconnpool.c

4.1 RAII机制

RAII(Resource Acquisition Is Initialization)为资源获取即初始化,sqlConnRAII.h专门负责了sql资源的连接,关键代码为在构造函数中调用*sql = connpool->GetConn();,在析构函数中调用connpool_->FreeConn(sql_);
内部成员有两个:

  • MYSQL *sql_;
  • SqlConnPool* connpool_;

4.2 SqlConnPool

sqlconnpool.h中,静态方法为instance: 它用static声明了一个SqlConnPool对象并返回了一个地址
成员方法包括

  • MYSQL *GetConn()
  • void FreeConn(MYSQL conn)
  • int GetFreeConnCount()
  • void Init(args)
  • void ClosePool()
    成员变量包括
  • std::queue<MYSQL *> connQue_;
  • std::mutex mtx_;
  • sem_t semId_;
  • int MAX_CONN_;
4.2.1 Init和ClosePool

首先来到init方法,它调用connSize
- mysql_init
- mysql_real_connect
- 并MYSQL对象添加到connQue_
之后初始化MAC_CONN_和初始化信号量semId_MAX_CONN_
ClosePool中,主要做的事情是清空connQue_,调用mysql_library_end结束mysql

4.2.2 GetConn和FreeConn

这里采用sem和lock_guard对多线程并发访问进行控制,sem_waitsem_post对连接数进行原子操作,lock_guard在构造时会自动获取锁,析构时会释放锁,用来取出sql对象

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;
}
void SqlConnPool::FreeConn(MYSQL* sql) {
    assert(sql);
    lock_guard<mutex> locker(mtx_);
    connQue_.push(sql);
    sem_post(&semId_);
}

参考资料

  1. c11语法: Cplusplus-A-Glance-Part-of-n
  2. c++多线程编程:C++11 并发指南
  3. writev和readv:高级I/O之readv和writev函数
  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值