注:个人学习记录,用于总结和复习,如误引用,万分抱歉
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