webserver学习心得

注:个人学习记录,用于总结和复习,如误引用,万分抱歉

1 项目简介

​webserver是一个基于Linux系统的高性能服务器项目,该项目实现了对HTTP协议中GET和POST请求的解析,同时经过webbench压测,可以实现上万的QPS

2 项目实现

2.1 Buffer缓冲区

​实现一个自动增长的缓冲区,有点类似C++中的vector容器,通过引入两个指针(读和写)来记录数据,Buffer类的设计如下:

#ifndef BUFFER_H
#define BUFFER_H
#include <cstring> 
#include <iostream>
#include <unistd.h> 
#include <sys/uio.h> 
#include <vector> 
#include <atomic>
#include <assert.h>
class Buffer {
public:
    // 构造与析构
    Buffer(int initBuffSize = 1024);
    ~Buffer() = default;

    size_t WritableBytes() const;       
    size_t ReadableBytes() const ;
    size_t PrependableBytes() const;

    const char* Peek() const;
    void EnsureWriteable(size_t len);
    void HasWritten(size_t len);

    void Retrieve(size_t len);
    void RetrieveUntil(const char* end);

    void RetrieveAll() ;
    std::string RetrieveAllToStr();

    const char* BeginWriteConst() const;
    char* BeginWrite();

    void Append(const std::string& str);
    void Append(const char* str, size_t len);
    void Append(const void* data, size_t len);
    void Append(const Buffer& buff);

    ssize_t ReadFd(int fd, int* Errno);
    ssize_t WriteFd(int fd, int* Errno);

// 只能在类内使用,防止意外访问
private:
    char* BeginPtr_();
    const char* BeginPtr_() const;
    void MakeSpace_(size_t len);

    std::vector<char> buffer_;
    std::atomic<std::size_t> readPos_; // 读指针
    std::atomic<std::size_t> writePos_; // 写指针
};

#endif 

2.2 Timer定时器

​定时器的作用是为了自动断开超时的连接,释放闲置的系统资源,定时器是基于小顶堆实现,涉及的主要算法就是堆排序算法,同时,需要自定义一个定时器任务结构体,用来存放定时任务的相关属性,定时器类的设计如下:

#ifndef HEAP_TIMER_H
#define HEAP_TIMER_H

#include <queue>
#include <unordered_map>
#include <time.h>
#include <algorithm>
#include <arpa/inet.h> 
#include <functional> 
#include <assert.h> 
#include <chrono>
#include "../log/log.h"

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;
    }
};

class HeapTimer {
public:
    HeapTimer() { heap_.reserve(64); }

    ~HeapTimer() { clear(); }
    
    void adjust(int id, int newExpires);

    void add(int id, int timeOut, const TimeoutCallBack& cb);

    void doWork(int id);

    void clear();

    void tick();

    void pop();

    int GetNextTick();

private:
    void del_(size_t i);
    
    void siftup_(size_t i);

    bool siftdown_(size_t index, size_t n);

    void SwapNode_(size_t i, size_t j);

    std::vector<TimerNode> heap_;

    std::unordered_map<int, size_t> ref_; // <定时任务标识,最小堆下标>
};

#endif

2.3 线程池和数据库连接池

​池在并发编程中是一个非常常见的概念,使用池的本质就是空间换时间,因此,顾名思义,线程池就是在服务器系统初始化时就创建固定数量的线程,用来处理读写任务,使用完后直接归还给线程池,而数据库连接池就是在初始化时创建一定数量的连接,用户一旦执行CURD操作,直接拿出一条连接即可,不需要TCP的连接过程和资源回收过程,使用完该连接后归还给连接池的连接队列,供之后使用。通过使用池,能够大大提高对网络请求的处理效率,提高QPS. 线程池类的设计如下:

#ifndef THREADPOOL_H
#define THREADPOOL_H
#include <bits/stdc++.h> // 万能头文件
#include <mutex>
#include <condition_variable>
#include <queue>
#include <thread>
#include <functional>
class ThreadPool {
public:
    // explicit关键字,必须显示调用,不能隐式调用
    explicit ThreadPool(size_t threadCount);

    ThreadPool() = default;  //默认构造函数

    ThreadPool(ThreadPool&&) = default; // 拷贝构造函数

    ThreadPool& operator = (const ThreadPool& other) = delete;
    
    ~ThreadPool();

    template<class F>
    void AddTask(F&& task);

private:
    struct Pool {
        std::mutex mtx;
        std::condition_variable cond;
        bool isClosed;
        std::queue<std::function<void()>> tasks;
    };
    std::shared_ptr<Pool> pool_;  // 类比于Pool* pool_ 使用智能指针可以更有效的管理内存资源
};
#endif

数据库连接池的设计如下:

#ifndef SQLCONNPOOL_H
#define SQLCONNPOOL_H
#include <mysql/mysql.h>
#include <bits/stdc++.h>
#include <string>
#include <queue>
#include <mutex>
#include <semaphore.h>
#include <thread>
#include "../log/log.h"

class SqlConnPool {
public:
    static SqlConnPool *Instance();

    MYSQL *GetConn();
    void FreeConn(MYSQL * conn);
    int GetFreeConnCount();

    void Init(const char* host, int port,
              const char* user,const char* pwd, 
              const char* dbName, int connSize);
    void ClosePool();

private:
    SqlConnPool();
    ~SqlConnPool();

    int MAX_CONN_; // 最大连接池数
    int useCount_; // 使用连接池数
    int freeCount_; // 空闲连接池数

    std::queue<MYSQL *> connQue_;
    std::mutex mtx_;
    sem_t semId_; // 信号量进行多线程通信
};
#endif

为了实现数据库连接的自动创建和析构,使用C++语言的RALL特性,即在创建一个类时自动调用类的构造函数,在离开作用域时自动调用析构函数(也可使用shared_ptr实现资源的自动创建和回收),具体代码如下:

#ifndef SQLCONNRALL_H
#define SQLCONNRALL_H
#include "sqlconnpool.h"

class sqlconnRAll
{
public:
    sqlconnRAll(MYSQL** sql, SqlConnPool *connpool)
    {
        assert(connpool);
        *sql = connpool->GetConn();
        sql_ = *sql;
        connpool_ = connpool;
    }

    ~sqlconnRAll()
    {
        if(sql_)
        {
            connpool_->FreeConn(sql_);
        }
    }

private:
    MYSQL* sql_;
    SqlConnPool* connpool_;
};

#endif

2.4 阻塞队列和日志系统

​同步写日志会引起阻塞,从而影响服务器性能,阻塞队列的设计是为了实现异步的日志系统,基于生产者-消费者的设计思想,日志异步写入时直接从阻塞队列中取出一条日志信息。日志模块的设计目的是为了监测服务器的运行状态,方便调试和更新,在设计过程中,需要规定日志的形式,即格式化日志内容,同时,要对过大的日志文件分页(分成多个文件),日志模块使用单例模式设计,阻塞队列类的设计如下:

#ifndef BLOCKQUEUE_H
#define BLOCKQUEUE_H

#include <mutex>
#include <deque>
#include <condition_variable>
#include <sys/time.h>

template<class T>
class BlockDeque {
public:
    explicit BlockDeque(size_t MaxCapacity = 1000);

    ~BlockDeque()// 四种宏定义格式化不同日志输出;

    void clear();

    bool empty();

    bool full();

    void Close();

    size_t size();

    size_t capacity();

    T front();

    T back();

    void push_back(const T &item);

    void push_front(const T &item);

    bool pop(T &item);

    bool pop(T &item, int timeout);

    void flush();

private:
    std::deque<T> deq_;

    size_t capacity_;

    std::mutex mtx_;

    bool isClose_;

    std::condition_variable condConsumer_;

    std::condition_variable condProducer_;
};
#endif

日志类的设计如下:

#ifndef LOG_H
#define LOG_H

#include <mutex>
#include <string>
#include <thread>
#include <sys/time.h>
#include <string.h>
#include <stdarg.h>     
#include <assert.h>
#include <sys/stat.h>       
#include "blockqueue.h"
#include "../buffer/buffer.h"

class Log {
public:
    void init(int level, const char* path = "./log", 
                const char* suffix =".log",
                int maxQueueCapacity = 1024);

    static Log* Instance();
    static void FlushLogThread();

    void write(int level, const char *format,...);
    void flush();

    int GetLevel();
    void SetLevel(int level);
    bool IsOpen() { return isOpen_; }
    
private:
    Log();
    void AppendLogLevelTitle_(int level);
    virtual ~Log();
    void AsyncWrite_();

private:
    static const int LOG_PATH_LEN = 256; // 最大路径长度
    static const int LOG_NAME_LEN = 256; // 最长文件名
    static const int MAX_LINES = 50000; // 最长日志条数

    const char* path_;
    const char* suffix_; // 后缀名

    int MAX_LINES_;

    int lineCount_;
    int toDay_;  // 按当天提前区分文件

    bool isOpen_;
 
    Buffer buff_;
    int level_;
    bool isAsync_;

    FILE* fp_; // 打开log的文件指针
    std::unique_ptr<BlockDeque<std::string>> deque_; // 阻塞队列
    std::unique_ptr<std::thread> writeThread_; //写线程
    std::mutex mtx_;
};

#define LOG_BASE(level, format, ...) \
    do {\
        Log* log = Log::Instance();\
        if (log->IsOpen() && log->GetLevel() <= level) {\
            log->write(level, format, ##__VA_ARGS__); \
            log->flush();\
        }\
    } while(0);

// 四种宏定义格式化不同日志输出
#define LOG_DEBUG(format, ...) do {LOG_BASE(0, format, ##__VA_ARGS__)} while(0);
#define LOG_INFO(format, ...) do {LOG_BASE(1, format, ##__VA_ARGS__)} while(0);
#define LOG_WARN(format, ...) do {LOG_BASE(2, format, ##__VA_ARGS__)} while(0);
#define LOG_ERROR(format, ...) do {LOG_BASE(3, format, ##__VA_ARGS__)} while(0);

#endif 

2.5 HTTP

​HTTP分为四个部分,接收请求->解析请求->生成响应->回送响应,接收的请求为GET和POST,利用状态机和正则表达式来解析客户端请求,然后,根据客户端的请求数据生产响应报文,最后将报文写入socket缓冲区中,通过服务器主线程发送到客户端,定义三个类来实现以上功能,代码如下:

  • httprequest类:
#ifndef HTTP_REQUEST_H
#define HTTP_REQUEST_H

#include <unordered_map>
#include <unordered_set>
#include <string>
#include <regex>
#include <errno.h>     
#include <mysql/mysql.h>  //mysql

#include "../buffer/buffer.h"
#include "../log/log.h"
#include "../pool/sqlconnpool.h"
#include "../pool/sqlconnRAll.h"

// using namespace std;

class HttpRequest {
public:
    enum PARSE_STATE {
        REQUEST_LINE,
        HEADERS,
        BODY,
        FINISH,        
    };

    enum HTTP_CODE {
        NO_REQUEST = 0,
        GET_REQUEST,
        BAD_REQUEST,
        NO_RESOURSE,
        FORBIDDENT_REQUEST,
        FILE_REQUEST,
        INTERNAL_ERROR,
        CLOSED_CONNECTION,
    };
    
    HttpRequest() { Init(); }
    ~HttpRequest() = default; // 内置析构函数 default自动生成函数体

    void Init();
    bool parse(Buffer& buff);

    std::string path() const;
    std::string& path();
    std::string method() const;
    std::string version() const;
    std::string GetPost(const std::string& key) const;
    std::string GetPost(const char* key) const;

    bool IsKeepAlive() const;

    /* 
    todo 
    void HttpConn::ParseFormData() {}
    void HttpConn::ParseJson() {}
    */

private:
    bool ParseRequestLine_(const std::string& line);
    void ParseHeader_(const std::string& line);
    void ParseBody_(const std::string& line);

    void ParsePath_();
    void ParsePost_();
    void ParseFromUrlencoded_(); // parse from url encoded

    static bool UserVerify(const std::string& name, const std::string& pwd, bool isLogin); // user verify
    static int ConverHex(char ch);

    PARSE_STATE state_;
    std::string method_, path_, version_, body_;
    std::unordered_map<std::string, std::string> header_;
    std::unordered_map<std::string, std::string> post_;

    static const std::unordered_set<std::string> DEFAULT_HTML;
    static const std::unordered_map<std::string, int> DEFAULT_HTML_TAG;
    
};


#endif
  • httpresponse类:
#ifndef HTTP_RESPONSE_H
#define HTTP_RESPONSE_H

#include <unordered_map>
#include <fcntl.h>       // open
#include <unistd.h>      // close
#include <sys/stat.h>    // stat
#include <sys/mman.h>    // mmap, munmap

#include "../buffer/buffer.h"
#include "../log/log.h"

class HttpResponse {
public:
    HttpResponse();
    ~HttpResponse();

    void Init(const std::string& srcDir, std::string& path, bool isKeepAlive = false, int code = -1);
    void MakeResponse(Buffer& buff);
    void UnmapFile();
    char* File();
    size_t FileLen() const;
    void ErrorContent(Buffer& buff, std::string message);
    int Code() const { return code_; }

private:
    void AddStateLine_(Buffer &buff);
    void AddHeader_(Buffer &buff);
    void AddContent_(Buffer &buff);

    void ErrorHtml_();
    std::string GetFileType_();

    int code_;
    bool isKeepAlive_;

    std::string path_;
    std::string srcDir_;
    
    char* mmFile_; 
    struct stat mmFileStat_; // 保存响应文件的属性

    static const std::unordered_map<std::string, std::string> SUFFIX_TYPE;
    static const std::unordered_map<int, std::string> CODE_STATUS;
    static const std::unordered_map<int, std::string> CODE_PATH;
};


#endif
  • httpconn类:
#ifndef HTTP_CONN_H
#define HTTP_CONN_H

#include <sys/types.h>
#include <sys/uio.h>     // readv/writev
#include <arpa/inet.h>   // sockaddr_in
#include <stdlib.h>      // atoi()
#include <errno.h>      

#include "../log/log.h"
#include "../pool/sqlconnRAll.h"
#include "../buffer/buffer.h"
#include "httprequest.h"
#include "httpresponse.h"

class HttpConn {
public:
    HttpConn();

    ~HttpConn();

    void init(int sockFd, const sockaddr_in& addr);

    ssize_t read(int* saveErrno);

    ssize_t write(int* saveErrno);

    void Close();

    int GetFd() const; // 获取套接字文件描述符

    int GetPort() const; // 获取端口

    const char* GetIP() const; // 获取IP
    
    sockaddr_in GetAddr() const; // 获取客户端地址(总)
    
    bool process(); // 处理客户端连接请求

    int ToWriteBytes() { 
        return iov_[0].iov_len + iov_[1].iov_len; 
    }

    // 判断是否是长期连接
    bool IsKeepAlive() const {
        return request_.IsKeepAlive();
    }

    static bool isET;
    static const char* srcDir;
    static std::atomic<int> userCount;
    
private:
    // 类内访问(私人访问)加_
    int fd_;
    struct  sockaddr_in addr_;

    bool isClose_;
    
    int iovCnt_;
    struct iovec iov_[2];
    
    Buffer readBuff_; // 读缓冲区
    Buffer writeBuff_; // 写缓冲区

    HttpRequest request_;
    HttpResponse response_;
};


#endif 

2.6 服务器实现

​基于Epoll实现的多线程Reactor服务器模型,涉及到主要知识就是Socket和Epoll的使用,这是一个简单服务器设计的基础,高性能服务器就是在一个简单服务器中不断增添新的功能模块(日志、池、定时器、协程等),Epoll类设计如下:

#ifndef EPOLLER_H
#define EPOLLER_H

#include <sys/epoll.h> //epoll_ctl()
#include <fcntl.h>  // fcntl()
#include <unistd.h> // close()
#include <assert.h> // close()
#include <vector>
#include <errno.h>

class Epoller {
public:
    explicit Epoller(int maxEvent = 1024);

    ~Epoller();

    bool AddFd(int fd, uint32_t events);

    bool ModFd(int fd, uint32_t events);

    bool DelFd(int fd);

    int Wait(int timeoutMs = -1);

    int GetEventFd(size_t i) const;

    uint32_t GetEvents(size_t i) const;
        
private:
    int epollFd_;

    std::vector<struct epoll_event> events_;    
};

#endif

最终将各个模块进行整合,得到一个高性能网络服务器,服务器类设计如下:

#ifndef WEBSERVER_H
#define WEBSERVER_H

#include <unordered_map>
#include <fcntl.h>      
#include <unistd.h>   
#include <assert.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "epoller.h"
#include "../log/log.h"
#include "../timer/heaptimer.h"
#include "../pool/sqlconnpool.h"
#include "../pool/threadpool.h"
#include "../pool/sqlconnRAll.h"
#include "../http/httpconn.h"

class WebServer {
public:
    WebServer(
        int port, int trigMode, int timeoutMS, bool OptLinger, 
        int sqlPort, const char* sqlUser, const  char* sqlPwd, 
        const char* dbName, int connPoolNum, int threadNum,
        bool openLog, int logLevel, int logQueSize);

    ~WebServer();
    void Start();

private:
    bool InitSocket_(); 
    void InitEventMode_(int trigMode);
    void AddClient_(int fd, sockaddr_in addr);
  
    void DealListen_();
    void DealWrite_(HttpConn* client);
    void DealRead_(HttpConn* client);

    void SendError_(int fd, const char*info);
    void ExtentTime_(HttpConn* client);
    void CloseConn_(HttpConn* client);

    void OnRead_(HttpConn* client);
    void OnWrite_(HttpConn* client);
    void OnProcess(HttpConn* client);

    static int SetFdNonblock(int fd);
    static const int MAX_FD = 65536;

    int port_;
    bool openLinger_;
    int timeoutMS_;  /* 毫秒MS */
    bool isClose_;
    int listenFd_;
    char* srcDir_;
    
    uint32_t listenEvent_;
    uint32_t connEvent_;
   
    std::unique_ptr<HeapTimer> timer_;
    std::unique_ptr<ThreadPool> threadpool_;
    std::unique_ptr<Epoller> epoller_;
    std::unordered_map<int, HttpConn> users_;
};


#endif 

3 项目总结

​此项目前前后后花费的时间大概有4个月,从最初的走马观花,到阅读源码,再到最后的手撕源码,几个月学习下来,让自己了解了实现一个项目的大致流程(期间还进行了单元测试),虽然这是一个开源的个人项目,但是其中涉及到Linux,计算机网络,数据库,设计模式等许多作为一个程序员必须掌握的基本内容,非常值得一学。最重要的是,通过此项目,让自己具备了一些作为程序员必备的特质和能力,相信这对后续的学习是非常有帮助的。

4 参考

《Linux高性能服务器服务器编程》

https://github.com/qinguoyi/TinyWebServer

https://github.com/markparticle/WebServer

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值