NextLoop高并发服务器组件
项目背景
学习Linux操作系统的时候,学习到,一个进程通过中断、异常、主动调用陷入内核,以及磁盘、网卡等IO操作会大大拖慢程序的运行效率。如果想要高并发就要减少系统调用的次数和减少IO等待带来的无意义的时间消耗。学习到了一些更高级的IO模型,比如事件驱动的Reactor模型,阻塞非阻塞IO,异步IO等。但是纸上得来终觉浅,还是想看看真正的高并发是怎么设计的。所以仿照了muduo(木铎)库,写了这个项目。
muduo设计前,传统的网络服务器开发是怎样的?
在此之前,传统的网络服务器开发主要面临三大问题:
- One Thread One Connection,代表的是Apache,主要带来的是资源消耗型问题,每到来一个连接就开一个新线程/进程,带来了服务器内存的消耗,以及进程/线程调度带来的CPU消耗
- 过度设计+滥用锁,代表的是ACE,主要带来的是复杂度问题以及锁竞争带来的性能损耗。
- 单Reactor模型,代表的是libevent,单Reactor没法利用多核CPU并行解决问题。
除此之外还有设计上的不足,比如定时器设计libevent的定时器设计是维护一个最小堆定时器,循环之前取出最近到期时间,然后阻塞等待。但是这种阻塞会被epoll监控的事件打断,导致时间到了的时候定时任务没能被执行。
解决方案:
- 针对One Thread One Connection,muduo库采取了Reactor模式,一个线程监控多个连接描述符事件。
- 针对过度设计+滥用锁,muduo库只在必须的场景(线程间任务投递)使用锁,采用One Thread One Loop模式,极大地简化了设计和锁的使用
- 针对单Reactor模型无法利用多核CPU的优势,muduo库使用了多Reactor多线程的模式,提高CPU的利用率
- 针对循环处理前先取出定时器等待的不足,muduo库把timerfd也加入到了epoll的监控中,定时器时间到,马上通知epoll,马上处理定时事件,不会有定时任务被延迟的问题。
核心目标
了解到了上述的背景,那么本项目的核心目标也就清晰了:
采用One Thread One Loop的多Reactor模式。实现一个支持快速搭建一个网络服务器的组件。
为了更好的测试性能以及提供上层使用,还额外添加了HTTP协议支持的模块。
一个高并发服务器想要运转起来,需要经过以下步骤:
-
http服务器给tcp服务器注册连接到来需要做的事情以及message到来需要做的事情
-
http服务器注册不同请求的回调函数------(GET、POST等)注册到一个_ get_route或者 _ post_route数组中,数组元素是<regex,handler>的pair
-
tcp服务器启动,创建reactor线程池,启动主reactor线程(该线程存在,但是还没进入循环事件,之所以放在后面启动是因为放在前面启动进入循环后就没办法创建从属Reactor线程池了)
-
主线程epoll监控监听套接字的事件,一旦有新连接到来构建一个connection对象绑定一个loop,主线程调用runInloop把新连接初始化的任务压到线程池的任务队列中。
-
从属Reactor线程池监控连接套接字事件,读缓冲区触发就进行业务处理。
所以目标就是描述http服务器,描述tcp服务器,描述EventLoop事件循环,描述一个连接Connection,描述一个文件描述符的事件。
具体实现
自顶向下理解
http服务器:
一个HttpServer服务器,管理着以下资源:
- GET、POST、PUT、DELETE等方法的路由
- 根目录资源
- Tcp服务器对象
c++
class HttpServer {
private:
using Handler = std::function<void(const HttpRequest &, HttpResponse *)>;
using Handlers = std::vector<std::pair<std::regex, Handler>>;
Handlers _get_route;//get方法的路由
Handlers _post_route;//...
Handlers _put_route;
Handlers _delete_route;
std::string _basedir; //静态资源根目录
TcpServer _server;
}
http服务器实例化的时候对Tcp服务器做了这些工作:
启动超时连接销毁、设置连接到来要做的回调和数据到来要做的回调。
c++
HttpServer(int port, int timeout = DEFALT_TIMEOUT):_server(port) {
_server.EnableInactiveRelease(timeout);
_server.SetConnectedCallback(std::bind(&HttpServer::OnConnected, this, std::placeholders::_1));
_server.SetMessageCallback(std::bind(&HttpServer::OnMessage, this, std::placeholders::_1, std::placeholders::_2));
}
连接到来回调:其实就是给连接设置一个上下文,设置成HttpContext()类型
c++
//设置上下文
void OnConnected(const PtrConnection &conn) {
conn->SetContext(HttpContext());
DBG_LOG("NEW CONNECTION %p", conn.get());
}
数据到来回调:核心就是解析出Http请求报文,然后进行业务处理
c++
void OnMessage(const PtrConnection &conn, Buffer *buffer)
{
while(buffer->ReadAbleSize() > 0)//循环读取缓冲区,一次事件全部读完
{
HttpContext *context = conn->GetContext()->get<HttpContext>();//构造HttpContext协议对象
context->RecvHttpRequest(buffer);
HttpRequest &req = context->Request();//协议对象读取缓冲区内容,提取数据构造一个HttpRequest对象
HttpResponse rsp(context->RespStatu());//构造一个默认的HttpResponse对象
if (context->RecvStatu() != RECV_HTTP_OVER)//对不完整的请求判断
//3. 请求路由 + 业务处理
Route(req, &rsp);
//4. 对HttpResponse进行组织发送
WriteReponse(conn, req, rsp);
//...
}
}
附:HttpContext是怎么解析Http请求报文的?(TCP粘包解包问题)
本质上是根据一个状态机,判断当前解析到请求行、请求头、还是请求体.
c++
void RecvHttpRequest(Buffer *buf) {
//不同的状态,做不同的事情,但是这里不要break, 因为处理完请求行后,应该立即处理头部,而不是退出等新数据
switch(_recv_statu) {
case RECV_HTTP_LINE: RecvHttpLine(buf);
case RECV_HTTP_HEAD: RecvHttpHead(buf);
case RECV_HTTP_BODY: RecvHttpBody(buf);
}
return;
}
开始监听新连接
c++
server.Listen();
//这里调用tcpserver的Start接口
void Listen() {
_server.Start();
}
tcp服务器:
一个Tcp服务器管理什么资源?
- 自增长连接ID,用来分配给新到来的连接
- 端口号
- 超时时间
- 主线程循环体
- 监听套接字的封装对象
- Reactor线程池
- 连接到来、数据到来、任意事件、关闭事件等等回调函数
c++
class TcpServer {
private:
uint64_t _next_id; //这是⼀个⾃动增⻓的连接ID,
int _port;
int _timeout; //这是⾮活跃连接的统计时间--多⻓时间⽆通信就是⾮活跃连接
bool _enable_inactive_release;//是否启动了⾮活跃连接超时销毁的判断标志
EventLoop _baseloop; //这是主线程的EventLoop对象,负责监听事件的处理
Acceptor _acceptor; //这是监听套接字的管理对象
LoopThreadPool _pool; //这是从属EventLoop线程池
//保存管理所有连接对应的shared_ptr对象
std::unordered_map<uint64_t, PtrConnection> _conns;
using ConnectedCallback = std::function<void(const PtrConnection&)>;
using MessageCallback = std::function<void(const PtrConnection&, Buffer *)>;
using ClosedCallback = std::function<void(const PtrConnection&)>;
using AnyEventCallback = std::function<void(const PtrConnection&)>;
using Functor = std::function<void()>;
ConnectedCallback _connected_callback;
MessageCallback _message_callback;
ClosedCallback _closed_callback;
AnyEventCallback _event_callback;
}
tcp服务器初始化做了什么工作?
初始化连接ID、端口号、主线程循环体(没写在构造列表中)、监听套接字的封装对象、从属Reactor线程池。
给监听套接字的封装对象设置新连接到来的回调函数。监听套接字的封装对象将自己绑定到主线程循环体的poller(监控器)中。
c++
TcpServer(int port):
_next_id(0),
_port(port),
_enable_inactive_release(false),
_acceptor(&_baseloop, port),
_pool(&_baseloop) {//_pool绑定了一个_baseloop,线程池和主线程绑定...
//_acceptro设置了一个新连接到来后的回调函数.
//然后acceptor开始监听新的连接...,并不是这样,acceptor的监听只是给监听套接字的读事件回调开启了.
_acceptor.SetAcceptCallback(std::bind(&TcpServer::NewConnection, this, std::placeholders::_1));
_acceptor.Listen();//channel绑定了loop,loop又和poller绑定,所以每个channel都可以把自己挂在到对应的poll中监控.
}
**Start接口:**这里是对上面Http服务器启动监听的承接
这里之所以要先创建从属线程池而不是先启动主线程循环,是因为主线程循环一旦启动就会死循环监控监听套接字的事件,处理。没办法创建从属线程池。
c++
void Start() { _pool.Create(); _baseloop.Start(); }
LoopThreadPool:从属Reactor线程池
一个从属Reactor线程池管理什么资源
- 需要创建的线程数量
- 主线程循环体
- 线程数组
- 循环数组
c++
class LoopThreadPool
{
private:
int _thread_count;
int _next_idx;
EventLoop* _baseloop;
std::vector<LoopThread*> _threads;
std::vector<EventLoop*> _loops;
}
怎么创建从属线程池?
其实就是新建线程和循环体将他们加入到线程数组和循环体数组中。
c++
void Create()
{
//根据_thread_count的个数来创建线程池
if(_thread_count > 0)
{
_threads.resize(_thread_count);//_threads是一个容器,用来装LoopThread类型的,此时在初始化这个容器的大小
_loops.resize(_thread_count);
for(int i = 0;i < _thread_count;i++)
{
_threads[i] = new LoopThread();//新建一个Loop线程把它插入到threads中管理
//如果此时线程切换了,会不会出现EventLoop还没有实例化就调用了GetLoop()函数呢?
_loops[i] = _threads[i]->GetLoop();//loops也是一样,一个LoopThread对应一个EventLoop
}
}
return;
}
LoopThread:Reactor线程
一个从属线程管理什么资源?
一个锁、一个条件变量、一个循环体、和一个std::thread(标准库线程)
c++
class LoopThread
{
private:
std::mutex _mutex;
std::condition_variable _cond;
EventLoop * _loop;
std::thread _thread;
}
为什么需要锁和条件变量呢?
来看看一个Reactor线程是怎么实例化的
c++
LoopThread():_loop(NULL),_thread(std::thread(&LoopThread::ThreadEntry,this)){}
void ThreadEntry()
{
EventLoop loop;
{
//这把锁在保护什么?
std::unique_lock<std::mutex> _lock(_mutex);
_loop = &loop;
//这把锁在保护_cond信号量...,cond信号量是什么?谁等待在这上面?
//GetLoop函数等待在这上面,为什么?
//这个信号量是为了实现EventLoop实例化->EventLoop实例的获取的同步关系,防止EventLoop还没创建,就有人通过GetLoop获取这个对象
_cond.notify_all();
}
//跳出临界区后这个执行流开始自己的循环...
loop.Start();
}
给std::thread(标准库线程)一个入口函数。创建一个线程循环体。到这里似乎还是不理解为什么需要锁
那来看看它提供的接口:
c++
EventLoop* GetLoop()
{
EventLoop* loop = NULL;
{
std::unique_lock<std::mutex> _lock(_mutex);
//条件变量会阻塞到loop实例化
_cond.wait(_lock,[&](){ return _loop != NULL;});//捕捉了this
loop = _loop;
}
return loop;
}
这把锁和条件变量原来是为了同步。实现从创建std::thread()到返回线程循环体(EventLoop)的严格同步。如果我们在std::thread()创建完成之前,先调用了GetLoop函数,那么会返回一个空指针。这时候就会发生错误。
EventLoop:线程循环体
**一个线程循环体管理什么资源?**一个EventLoop应该管理一批连接,但是是通过Poller监控和定时器销毁,而没有真的管理在类中。所有的连接都在TcpServer中被管理着。
- 线程id(标准库的)
- epoll唤醒器(一个文件描述符)
- 唤醒器的事件管理对象
- epoll监控器(核心,用来监控连接描述符和唤醒器以及定时器)
- 任务池
- 定时器对象
c++
class EventLoop
{
private:
using Functor = std::function<void()>;
std::thread::id _thread_id;//线程id
int _event_fd;//eventfd唤醒IO事件有可能导致的阻塞,epoll是阻塞监控,每次向任务队列添加任务都应该唤醒一下epoll。
//按理说顺序应该是epoll监控到事件,任务添加到时间轮中,然后执行任务。
std::unique_ptr<Channel> _event_channel;
Poller _poller;//进行所有描述符的事件监控
std::vector<Functor> _tasks;//任务池
std::mutex _mutex;//实现任务池操作的线程安全
TimerWheel _timer_wheel;//定时器
}
事件循环体实例化:
创建一个唤醒器,唤醒器的事件管理对象,定时器对象。
c++
//创建event_fd,管理event_fd的事件(添加可读事件回调,启动读监控),创建时间轮
EventLoop():_thread_id(std::this_thread::get_id())
,_event_fd(CreateEventFd())
,_event_channel(new Channel(this,_event_fd))
,_timer_wheel(this)
{
//给eventfd添加可读事件回调函数,读取eventfd事件通知次数
_event_channel->SetReadCallback(std::bind(&EventLoop::ReadEventFd,this));
//启动读事件监控
_event_channel->EnableRead();
}
事件循环体运行:
监控------批处理到来事件------处理任务池任务(可能由主线程塞进来)
c++
void Start()
{
//一个线程对应一个EventLoop,线程池也共享这个EventLoop,那主线程呢?
while(1)
{
//事件监控
std::vector<Channel*> actives;
//_poller在监控的时候为什么要传递这个局部变量?
//actives是一个输出型参数..
//epoll_wait返回的时候构建channel对象返回给这个容器中,告诉这个Loop,什么事件就绪了.
_poller.Poll(&actives);
//主线程的_poller已经在监听listen套接字了.
//事件处理
for(const auto& channel: actives)
{
channel->HandleEvent();
}
//执行任务
//RunAllTask在运行什么任务,这个任务和我们用epoll_wait监控到的事件有什么区别?
RunAllTask();
}
}
其他线程把任务压入到本线程循环体的任务池中:
c++
void RunInLoop(const Functor& cb)
{
//
if(IsInLoop())
{
return cb();
}
return QueueInLoop(cb);
}
//将任务压入任务池
void QueueInLoop(const Functor& cb)
{
{
std::unique_lock<std::mutex> _lock(_mutex);
_tasks.push_back(cb);
}
//唤醒epoll
WeakUpEventFd();
}
这里的WeakUpEventFd其实就是往唤醒器里边写,这样epoll就能监控到唤醒器的事件到来。
TimerWheel时间轮定时器
_ wheel轮示意图:(表盘大小是60个)这里只画三个意思一下。

c++
class TimerWheel
{
private:
using WeakTask = std::weak_ptr<TimerTask>;//定义TimerTask的weak指针
using PtrTask = std::shared_ptr<TimerTask>;//定义TimerTask的share指针
int _tick;//秒针
int _capacity;//表盘,最大延迟时间
std::vector<std::vector<PtrTask>> _wheel;//二维数组?
std::unordered_map<uint64_t,WeakTask> _timers;//id和任务weak指针的对应关系
EventLoop* _loop;//工作在哪个loop中
int _timerfd;//定时器文件描述符
std::unique_ptr<Channel> _timer_channel;//定时器文件的channel对象,管理定时器的事件
}
实例化:
秒针设为0,表盘大小为60,循环数组大小为60,
c++
TimerWheel(EventLoop* loop)
:_tick(0),_capacity(60),_wheel(_capacity),_loop(loop),_timerfd(CreateTimerfd()),_timer_channel(new Channel(_loop,_timerfd))
{
_timer_channel->SetReadCallback(std::bind(&TimerWheel::OnTime,this));//设置回调
_timer_channel->EnableRead();//启动读事件监控
}
这里主要看看定时器读事件触发做什么:
c++
//timefd读事件回调
void OnTime()
{
//根据超时次数,执行超时任务
int times = ReadTimefd();//这里读取timerfd返回的是超时的次数,这里设计的是1秒算超时1次
for(int i = 0;i < times;i++)
{
RunTimerTask();
}
}
void RunTimerTask()
{
_tick = (_tick + 1) % _capacity;//秒针向后走
_wheel[_tick].clear();//这秒的任务清空,释放掉之后就会执行析构函数,析构函数内就要执行任务
}
那么问题来了,wheel轮中的任务是谁在什么时候添加的呢?
第一次添加定时销毁任务,构造一个TimerTask的Share智能指针对象。设置这个对象的析构函数中的回调函数(将这个id的从哈希表中移除)。
计算在循环数组中的位置,添加到销毁列表中。
下面的刷新定时器就体现了为什么需要一个id------Task对象的Weak指针,因为刷新需要构造新的Shared指针。
c++
void TimerAddInLoop(uint64_t id,uint32_t delay,const TaskFunc& cb)
{
PtrTask pt(new TimerTask(id,delay,cb));
pt->SetRelease(std::bind(&TimerWheel::RemoveTimer,this,id));
int pos = (_tick + delay) % _capacity;
_wheel[pos].push_back(pt);
_timers[id] = WeakTask(pt);//如果timers里的是sharedptr,那就不能在clear的时候销毁Task对象了。
//_timers里管理的Task对象是在relase的时候清理的.我们希望wheel在clear的时候,task的计数清0,调用析构函数
}
void TimerRefreshInLoop(uint64_t id)
{
//构造Task的shared指针,这样引用计数增加了
auto it = _timers.find(id);
if(it == _timers.end())
{
return;
}
PtrTask pt = it->second.lock();//weak的lock获取一个shared指针
int delay = pt->DelayTime();//获取超时时间
int pos = (_tick + delay) % _capacity;
_wheel[pos].push_back(pt);
}
来看一个新连接是怎么把自己添加到定时器任务中的
c++
TcpServer:
void NewConnection(int fd)
{
if (_enable_inactive_release) conn->EnableInactiveRelease(_timeout);
}
Connection:
//启动⾮活跃连接超时释放规则
void EnableInactiveReleaseInLoop(int sec) {
//1. 将判断标志 _enable_inactive_release 置为true
_enable_inactive_release = true;
//2. 如果当前定时销毁任务已经存在,那就刷新延迟⼀下即可
if (_loop->HasTimer(_conn_id)) {
return _loop->TimerRefresh(_conn_id);
}
//3. 如果不存在定时销毁任务,则新增
_loop->TimerAdd(_conn_id, sec, std::bind(&Connection::Release, this));
}
EventLoop:
void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb)
{ return _timer_wheel.TimerAdd(id, delay, cb); }
因为连接绑定了EventLoop,EventLoop又绑定一个定时器对象,所以自然可以通过EventLoop找到定时器对象,将连接销毁的回调函数传给定时器对象,让定时器对象析构的时候调用这个回调函数。
Poller监控器
一个事件循环体绑定一个Poller监控器(对epoll的封装)它的构造也就是创建一个epoll句柄
Poller监控器管理的资源很简单,一个epoll文件描述符,返回型参数返回就绪事件,一个哈希表key为:描述符,value为:Channel事件管理对象。
c++
class Poller
{
private:
int _epfd;
struct epoll_event _evs[MAX_EPOLLEVENTS];//结构体数组
std::unordered_map<int ,Channel*> _channels;//客户端连接和事件管理类的绑定
}
那么最核心就是Poll接口了,这个监控返回函数做了什么呢?
epoll_wait等待,epoll返回的时候把对应的channel添加到返回型参数中。
c++
//开始监控,返回活跃连接
void Poll(std::vector<Channel*> *active)//输出型参数
{
int nfds = epoll_wait(_epfd,_evs,MAX_EPOLLEVENTS,-1);
//判断返回值nfds表活跃数量
if(nfds < 0)
{
if(errno == EINTR)
{
return;//被信号中断
}
ERR_LOG("epoll wait error:%s \n",strerror(errno));
abort();
}
for(int i = 0;i < nfds;i++)
{
//找
auto it = _channels.find(_evs[i].data.fd);
assert(it != _channels.end());
it->second->SetREvents(_evs[i].events);//给channel设置好就绪的事件
active->push_back(it->second);//给输出型参数push
}
return;
}
等等,新连接到来的时候是怎么把连接描述符添加到Poller中监控的?
如果一个描述符同时有多个事件到来,channel能够完整处理吗?
Channel:描述符事件管理对象
channel对象管理什么资源?
- 描述符ID
- 线程循环体
- 需要监控的事件
- 已经触发的事件(由Poller设置)
- 事件触发的回调
c++
class Channel
{
private:
int _fd;//什么描述符
EventLoop *_loop;//在哪个线程中
uint32_t _events; // 当前需要监控的事件
uint32_t _revents; // 当前连接触发的事件
using EventCallback = std::function<void()>;
EventCallback _read_callback; //可读事件被触发的回调函数
EventCallback _write_callback; //可写事件被触发的回调函数
EventCallback _error_callback; //错误事件被触发的回调函数
EventCallback _close_callback; //连接断开事件被触发的回调函数
EventCallback _event_callback; //任意事件被触发的回调函数
}
实例化一个Channel对象:
绑定线程循环体和描述符
c++
Channel(EventLoop *loop, int fd):_fd(fd), _events(0), _revents(0), _loop(loop) {}
下面两个接口解答了上一个模块的问题:新连接到来是怎么把自己添加到Poller的监控中的?
c++
Channel:
//channel启动读事件监控除了设置自己的_event属性还调用了Update
void EnableRead() { _events |= EPOLLIN; Update(); }
//启动写事件监控
void EnableWrite() { _events |= EPOLLOUT; Update(); }
EventLoop:
void Channel::Update() { return _loop->UpdateEvent(this); }
void UpdateEvent(Channel *channel) { return _poller.UpdateEvent(channel); }
Poller:
void Update(Channel* channel,int op)
{
int ret = epoll_ctl(_epfd,op,fd,&ev);
}
答案是通过Channel,设置需要监控的事件,通过绑定的线程事件循环体找到对应的Poller,将自己添加到Poller的监控中。
我们可以看TcpServer服务器设置的新连接到来回调:
本质上是主线程将这个初始化连接的任务压到了某个从属Reactor线程的任务池中了
c++
void NewConnection(int fd)
{
PtrConnection conn(new Connection(_pool.NextLoop(), _next_id, fd));
conn->Established();//就绪初始化
}
Connection:
void EstablishedInLoop()
{
_channel.EnableRead();
}
一个连接对应一个Channel那么一个连接又是怎么描述的呢?
Connection:连接对象
上面的的内容已经告诉我们,一个Connection对象构造初始化的时机是监听套接字事件到来,调用TcpServer传入的NewConnection回调,构造一个连接对象,设置它的各种事件回调,然后启动超时销毁和将自己添加到Poller监控。
Connection管理什么资源:
- 线程循环体
- 描述符事件管理对象
- 输入输出缓冲区和上下文
- 以及TcpServer传下来的回调函数
c++
class Connection : public std::enable_shared_from_this<Connection>
{
private:
uint64_t _conn_id;//连接id,上面还有任务id
int _sockfd;//连接关联的文件描述符
bool _enable_inactive_release;//连接是否启动非活跃销毁
EventLoop* _loop;//连接关联的loop
ConnStatu _statu;//连接状态
Socket _socket;//套接字管理
Channel _channel;//连接的事件管理
Buffer _in_buffer;//输入缓冲区,存放从socket中读到的信息
Buffer _out_buffer;//输出缓冲区,存放要发给socket的信息
Any _context;//协议,上下文
//组件使用者设置的回调函数,应该是业务处理函数
using ConnectedCallback = std::function<void(const PtrConnection&)>;
using MessageCallback = std::function<void(const PtrConnection&,Buffer*)>;
using ClosedCallback = std::function<void(const PtrConnection&)>;
using AnyEventCallback = std::function<void(const PtrConnection&)>;
ConnectedCallback _connected_callback;
MessageCallback _message_callback;
ClosedCallback _closed_callback;
AnyEventCallback _event_callback;
//服务器组件会管理所有的connection所以还需要一个组件内使用的连接关闭回调
ClosedCallback _server_closed_callback;
}
实例化连接发生了什么?
除了初始化自己所处的循环体以及连接ID等信息,最主要的就是设置给Channel(描述符事件管理对象)各种回调函数,而这些回调函数内部,比如HandleRead内部调用了TcpServer传下来的message_callback。而这些回调函数的调用时机就是Poller监控到描述符事件到来,返回的时候,循环体内部调用Channel的HandleEvent()函数内部调用。
c++
Connection(EventLoop *loop, uint64_t conn_id, int sockfd):_conn_id(conn_id), _sockfd(sockfd),
_enable_inactive_release(false), _loop(loop), _statu(CONNECTING), _socket(_sockfd),
_channel(loop, _sockfd) {
_channel.SetCloseCallback(std::bind(&Connection::HandleClose, this));
_channel.SetEventCallback(std::bind(&Connection::HandleEvent, this));
_channel.SetReadCallback(std::bind(&Connection::HandleRead, this));
_channel.SetWriteCallback(std::bind(&Connection::HandleWrite, this));
_channel.SetErrorCallback(std::bind(&Connection::HandleError, this));
}
channel:
void HandleEvent() {
if ((_revents & EPOLLIN) || (_revents & EPOLLRDHUP) || (_revents & EPOLLPRI)) {
/*不管任何事件,都调⽤的回调函数*/
if (_read_callback) _read_callback();
}
/*有可能会释放连接的操作事件,⼀次只处理⼀个*/
if (_revents & EPOLLOUT) {
if (_write_callback) _write_callback();
}else if (_revents & EPOLLERR) {
if (_error_callback) _error_callback();
}else if (_revents & EPOLLHUP) {
if (_close_callback) _close_callback();
}
if (_event_callback) _event_callback();
}
http.hpp:
httpserver:
void OnMessage(const PtrConnection &conn, Buffer *buffer)
{
}
HandleRead,读事件到来回调:
由于外层需要使用Connection对象,所以通过一个Shared_ptr传出去,这个连接对象是HandleRead内部传递出去的,为什么不通过this指针构造一个Shared_ptr而是通过share_from_this()返回呢?
因为一开始的一个Connection的Shared_ptr是在TcpServer的回调函数上构造的,也就是在主线程上,如果我这里通过this指针再构造一个shared_ptr(this)没有经过拷贝构造或者拷贝赋值,是不会共享引用计数的。
c++
void HandleRead()
{
//接收socket数据放到缓冲区
char buf[65535];
ssize_t ret = _socket.NoneBlockRecv(buf,65535);
if(ret <= 0)
{
return ShutdownInLoop();
}
//ret 等于0表示没有读到数据,-1表示连接断开
//将数据放入输入缓冲区
_in_buffer.WriteAndPush(buf,ret);
if(_in_buffer.ReadAbleSize()>0)
{
return _message_callback(shared_from_this(),&_in_buffer);//shared_f_t防止构造shared_ptr(this)
//外部有shared引用这个类,如果再构造一个新的就和他没形成引用计数,double delete
}
}
使用和测试:
先看测试的服务器配置:
| CPU 型号 | Intel Xeon Gold 6148 @ 2.40GHz | 服务器级 CPU(Skylake-SP 架构,2017 年发布),20 核/40 线程的型号,但这里只分配了 4 核 |
|---|---|---|
| CPU 核心数 | 4 核 4 线程 | 单核单线程,无超线程,典型的 4C 云实例 |
| 内存 | 总计 3.6 GiB,可用约 1.1 GiB | 总内存不到 4G,已用 2.2G,压测时要注意内存余量偏紧 |
| 内核版本 | 5.15.0-151-generic | Ubuntu 22.04 的 LTS 内核,稳定但不算新 |
| 系统版本 | Ubuntu 22.04 LTS | 长期支持版,2027 年停止支持 |

这里使用http协议支持,重写一下之前的HistoFind搜索引擎。
c++
HttpServer server(8081);
server.SetThreadCount(3);
server.SetBaseDir(root_path.c_str());
server.Post("/register",RegisterHandler);
server.Post("/login", LoginHandler);
server.Get("/history", GetHistoryHandler);
server.Get("/s", [&](const HttpRequest& req, HttpResponse* res) {
SearchHandler(req, res, &searcher);
});
std::cout << "Server started on port 8081" << std::endl;
server.Listen();

使用高并发服务器组件后测试性能:

发现QPS是cpp-httplib的三分之一。
检查发现http.hpp中ParseHttpLine每次都编译regex,高并发场景下带来严重的性能损耗,修改后测试性能已经超过cpp-httplib
c++
bool ParseHttpLine(const std::string &line) {
std::smatch matches;
//静态,防止每次都编译
static const std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?", std::regex::icase);
}

QPS已经比cpp-httplib库的高了
达到了102万/分钟也就是16000多的每秒并发量。
这里加上业务逻辑再测试一下:

27万/分钟的并发量,和cpp-httplib库的差不多
提高客户端数量再次测试一下
3000客户端,因为服务器资源有限,没办法提的太多。

5000客户端

这个并发量相比于使用cpp-httplib库1000客户端并发请求时还高。
cGpF-1784655028749)]
使用高并发服务器组件后测试性能:
外链图片转存中...(img-FXkkrdkX-1784655028750)
发现QPS是cpp-httplib的三分之一。
检查发现http.hpp中ParseHttpLine每次都编译regex,高并发场景下带来严重的性能损耗,修改后测试性能已经超过cpp-httplib
c++
bool ParseHttpLine(const std::string &line) {
std::smatch matches;
//静态,防止每次都编译
static const std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?", std::regex::icase);
}
外链图片转存中...(img-ttjTCPdk-1784655028750)
QPS已经比cpp-httplib库的高了
达到了102万/分钟也就是16000多的每秒并发量。
这里加上业务逻辑再测试一下:
外链图片转存中...(img-EmCdCnZk-1784655028750)
27万/分钟的并发量,和cpp-httplib库的差不多
提高客户端数量再次测试一下
3000客户端,因为服务器资源有限,没办法提的太多。
外链图片转存中...(img-vHKE8j20-1784655028750)
5000客户端
外链图片转存中...(img-OROOF7iu-1784655028750)
这个并发量相比于使用cpp-httplib库1000客户端并发请求时还高。