libevent是一套轻量级的网络库,基于事件驱动开发。能够实现多线程的多路复用和注册事件响应。本文将介绍libevent的基本功能以及如何利用libevent开发一个线程池。

一. 使用指南

监听服务和注册连接事件

libevent是一个基于事件驱动的网络库,通过在一个事件循环上注册不同的事件以完成线程多路复用。由于libevent采用c语言开发,为了使用方便我们可以将它的功能通过面向对象的设计模式用c++来封装。下面是对常用函数的详细介绍:

(1)event_base_new():创建(初始化)event_base

event_base代表了一个事件循环上下文,所有需要基于这个事件循环的事件都需要注册在它的上面。如果创建成功,最后需要使用event_base_free()来释放资源。

(2)evconnlistener_new_bind():绑定一个本地端口并注册网络监听事件

参数说明:

  • struct event_base* base 前文创建好的base,事件将关联到这个事件循环上
  • evconnlistener_cb cb 事件触发的回调
  • void *ptr 回调函数的参数,这个参数可以由用户任意指定,方便在回调函数中使用
  • unsigned flags 事件的附加标识,代表事件操作
  • int backlog 网络缓存大小
  • const struct sockaddr *sa socket地址
  • int socklen socket地址长度

函数会返回一个新的evconnlistener,如果创建成功,需要使用evconnlistener_free()来释放资源。

(3)event_base_dispatch():启动事件循环和事件分发

这个函数会阻塞当前线程,用户可以在事件回调函数中通过event_base_loopbreak()来中断。如果不希望当前线程被堵塞也可以使用event_base_loop()函数。注意,千万不要在回调函数中清理event_base。

代码示例:

// 创建事件循环
ev_base_ = event_base_new();
if (!ev_base_)
{
return false;
}
sockaddr_in sin;
memset(&sin, , sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(port_);
// 创建socket连接回调
ev_listener_ = evconnlistener_new_bind(
ev_base_,
SConnListenerCb,
this,
LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE,
this->backlog_,
(sockaddr *)&sin,
sizeof(sin));
if (!ev_listener_)
{
return false;
}
while (!quit_)
{
event_base_loop(ev_base_, EVLOOP_NONBLOCK);
this_thread::sleep_for(chrono::milliseconds());
}
evconnlistener_free(ev_listener_);
event_base_free(ev_base_);
static void SConnListenerCb(struct evconnlistener *listen, evutil_socket_t sock, struct sockaddr *addr, int len, void *ctx)
{
// 解析客户端ip
char ip[] = {};
sockaddr_in *addr_in = (sockaddr_in *)addr;
evutil_inet_ntop(AF_INET, &addr_in->sin_addr, ip, sizeof(ip));
stringstream ss;
ss << ip << ":" << addr_in->sin_port << " 连接完成...";
LOG4CPLUS_INFO(SimpleLogger::Get()->LoggerRef(), ss.str());
SmsServer *server = (SmsServer *)ctx;
int s = sock; server->ConnListener(s);
}

创建连接和注册读、写、事件监听

(1)bufferevent_socket_new():创建一个带socket缓存的事件

bufferevent表示一个事件缓存,每当有数据需要读取的时候,它会先将数据从内核态取出再通知用户。顺带提一下,libevent对事件的触发支持两种模式:(ET)边沿触发和(LT)水平触发。如果你设置了水平触发,但是通过bufferevent来读取消息,无论你是否将消息接收完成,都不会被反复触发回调。因此,使用bufferevent来接收消息的时候,需要特别关注TCP粘包和长包。

(2)bufferevent_setcb():设置bufferevent的回调函数

参数说明:

  • struct bufferevent* bufev 关联对象
  • bufferevent_data_cb readcb 读回调 函数原型void (*bufferevent_data_cb)(struct bufferevent *bev, void* ctx)
  • bufferevent_data_cb writecb 写回调 函数原型(同上)
  • bufferevent_event_cb eventcb 事件回调 函数原型void (*bufferevent_event_cb)(struct bufferevent *bev, short what, void *ctx)
  • void *cbarg 回调函数的最后一个参数,由用户指定

读回调顾名思义就是当有数据的时候会触发的函数,可是写回调什么时候触发?有兴趣的朋友可以自己测试一下。特别需要关注事件回调函数。所有可触发的事件包括:BEV_EVENT_READING(读事件),BEV_EVENT_WRITING(写事件),BEV_EVENT_EOF(结束事件),BEV_EVENT_ERROR(错误事件),BEV_EVENT_TIMEOUT(超时事件),BEV_EVENT_CONNECTED(连接事件)。如果你是在开发服务端BEV_EVENT_CONNECTED事件不会被触发,因为连接事件是在bufferevent创建前产生的。BEV_EVENT_READING || BEV_EVENT_TIMEOUT可以用来表示读数据超时,通过这个事件可以侦测心跳代表距离上次读数据已经超时。BEV_EVENT_WRITING || BEV_EVENT_TIMEOUT可以表示写超时,但是这个事件只会在当有数据需要被发送可是超时未发送成功后才会被触发。

此外,当发生超时事件后,相关的读写操作都会被从bufferevent中移除。如果用户希望继续之前的操作,需要重新注册读/写。

(3)bufferevent_set_timeouts():设置读/写超时

只有在通过这个函数设置了读/写超时后,在事件回调函数中BEV_EVENT_TIMEOUT才会生效。

代码示例:

bufferevent *buff_ev_ = bufferevent_socket_new(ev_base_, socket_, BEV_OPT_CLOSE_ON_FREE);
if (!buff_ev_)
{
return false;
}
// 指定参数
bufferevent_setcb(buff_ev_, SReadCb, SWriteCb, SEventCb, this);
bufferevent_enable(buff_ev_, EV_READ | EV_WRITE);
timeval tv = {timeout_, };
bufferevent_set_timeouts(buff_ev_, &tv, NULL);
return true;

读写数据

(1)bufferevent_read():从缓存中接收数据

通常在读回调中使用,通过返回值判断缓存中是否还有数据

(2)bufferevent_write():向缓冲写入数据以通过socket发送

返回值表示有多少数据已经被写入进内核

创建基于管道的事件

libevent除了可以用在网络上,还可以和管道(pipe)结合用来生成管道事件。

(1)event_config_new():创建一个事件配置对象

event_config可以用来创建一个非默认的事件循环,通常使用这个函数配合event_base_new_with_config()创建event_base。最后需要使用event_config_free()来释放资源。

(2)event_new():创建一个读/写事件

和bufferevent的创建不同,event_new()只会创建一个配套的事件,如果在事件中用户没有对数据进行处理,回调会一直被触发。

代码示例:

// 初始化一对管道,只能在linux系统下使用
int pipefd[];
if (pipe(pipefd))
{
return false;
}
// pipefd[0]读取管道 pipefd[1]发送管道
this->pipe_endpoint_ = pipefd[];
// 创建管道事件
event_config *ev_conf = event_config_new();
event_config_set_flag(ev_conf, EVENT_BASE_FLAG_NOLOCK);
this->ev_base_ = event_base_new_with_config(ev_conf);
event_config_free(ev_conf);
if (!ev_base_)
{
return false;
} pipe_ev_ = event_new(this->ev_base_, pipefd[], EV_READ | EV_PERSIST, SEventCb, this);
event_add(pipe_ev_, );

二、实现线程池

线程池实现原理

libevent可以实现对线程的多路复用,因此我们可以在一个线程中完成服务端对多个客户端的同时读写操作。这样做虽然能够最大限度的利用系统资源,可是无法充分发挥cpu多线程的处理能力。开发高可用和适合高负载的服务端我们依然应该考虑启动多个线程来处理数据。关键是我们如何将事件分发到不同的线程中以保持多个线程的负载均衡。

  1. 当服务启动的时候首先创建N条线程。每一个线程对应一个事件循环event_base。
  2. 主线程负责监听指定端口并在连接的回调函数中处理新连接套接字的处理。
  3. 当有新的客户端连接后,主线程会把套接字先保存在一个队列中。扫描当前所有线程的处理量,选择负载最小的线程利用管道发送一个信号(‘c’)。对应线程的事件循环在管道的读事件中从队列中获取这个套接字,并建立对应的bufferevent进行处理。当前线程负载量+1。
  4. 客户端断开后通知bufferevent所在的线程将负载量减一。

smss源码阅读

相关的源码文件为sms_server,work_group,work_thread和socket_manager

服务初始化,注册连接监听事件并初始化线程组

bool SmsServer::Init()
{
// 创建事件循环
ev_base_ = event_base_new();
if (!ev_base_)
{
return false;
}
sockaddr_in sin;
memset(&sin, , sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(port_);
// 创建socket连接回调
ev_listener_ = evconnlistener_new_bind(
ev_base_,
SConnListenerCb,
this,
LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE,
this->backlog_,
(sockaddr *)&sin,
sizeof(sin));
if (!ev_listener_)
{
return false;
}
// 创建线程组管理类
boss_ = new WorkGroup(thread_num_);
boss_->Init();
return true;
}

线程组负责管理所有的线程

bool WorkGroup::Init()
{
// 直接初始化指定的工作线程
for (int i = ; i < num_; i++)
{
int id = group_.size() + ;
WorkThread *work = new WorkThread(this, id, net_bus_);
if (!work->Init())
{
return false;
}
work->Start(); // thread start...
group_.push_back(work);
// 将当前初始化完成的工作线程注册进消息总线
net_bus_->Regist(work); // regist thread to netbus
}
return true;
}

每一个线程在初始化的时候会创建一条管道并在自己的事件循环上注册对应的读回调,对外部暴露Notify方法用来激活事件

bool WorkThread::Init()
{
// 初始化一对管道,只能在linux系统下使用
int pipefd[];
if (pipe(pipefd))
{
return false;
}
// pipefd[0]读取管道 pipefd[1]发送管道
this->pipe_endpoint_ = pipefd[];
// 创建管道事件
event_config *ev_conf = event_config_new();
event_config_set_flag(ev_conf, EVENT_BASE_FLAG_NOLOCK);
this->ev_base_ = event_base_new_with_config(ev_conf);
event_config_free(ev_conf);
if (!ev_base_)
{
return false;
} pipe_ev_ = event_new(this->ev_base_, pipefd[], EV_READ | EV_PERSIST, SEventCb, this);
event_add(pipe_ev_, );
return true;
} void WorkThread::Notify(const char *sign)
{
// 激活
int re = write(this->pipe_endpoint_, sign, );
if (re <= )
{
LOG4CPLUS_ERROR(SimpleLogger::Get()->LoggerRef(), "管道激活失败");
}
}

在读回调中获取套接字,创建连接管理对象SocketManager

void WorkThread::Activated(int fd)
{
char buf[] = {};
int re = read(fd, buf, );
socket_list_mtx_.lock();
if (strcmp(buf, "c") == ) // 通知有新的客户端连接
{
// new client connect, create SocketManager
if (socket_list_.empty())
{
socket_list_mtx_.unlock();
return;
}
// 读取一条套接字
int client_sock = socket_list_.front();
socket_list_.pop_front();
// 创建socketManager
SocketManager *manager = new SocketManager(this, ev_base_, client_sock, AppContext::Get()->client_timeout());
manager->Init();
sock_manager_list_.push_back(manager);
stringstream ss;
ss << "SocketManager:" << client_sock << " 创建完成";
LOG4CPLUS_DEBUG(SimpleLogger::Get()->LoggerRef(), ss.str());
} socket_list_mtx_.unlock();
}

客户端连接后将创建的套接字交给负载最小的线程处理

void WorkGroup::CreateConnection(int sock)
{
int min = -;
WorkThread *work = nullptr;
// 遍历寻找负载最轻的线程
for (auto it = group_.begin(); it != group_.end(); it++)
{
if (min == -)
{
min = (*it)->connect_num();
work = (*it);
}
else if ((*it)->connect_num() < min)
{
min = (*it)->connect_num();
work = (*it);
}
}
// 添加一条socket fd进队列并通过管道激活
work->AddSocket(sock);
work->Notify("c");
}

完整源码已经发布在码云上。

相关文章:《开源项目SMSS开发指南》

开源项目SMSS开发指南(二)——基于libevent的线程池的更多相关文章

  1. 开源项目SMSS开发指南

    SMSS是一个由我个人发起的开源项目,目的是建立一套轻量化,高可用,高安全和方便扩展的业务支撑框架.SMSS面向TCP/IP层开发,适合扩展上层业务接口.数据结构传输序列化通过Protobuf实现.传 ...

  2. [开源项目]可观测、易使用的SpringBoot线程池

    在开发spring boot应用服务的时候,难免会使用到异步任务及线程池.spring boot的线程池是可以自定义的,所以我们经常会在项目里面看到类似于下面这样的代码 @Bean public Ex ...

  3. 开源项目SMSS发开指南(四)——SSL/TLS加密通信详解

    本文将详细介绍如何在Java端.C++端和NodeJs端实现基于SSL/TLS的加密通信,重点分析Java端利用SocketChannel和SSLEngine从握手到数据发送/接收的完整过程.本文也涵 ...

  4. 开源项目SMSS发开指南(五)——SSL/TLS加密通信详解(下)

    继上一篇介绍如何在多种语言之间使用SSL加密通信,今天我们关注Java端的证书创建以及支持SSL的NioSocket服务端开发.完整源码 一.创建keystore文件 网上大多数是通过jdk命令创建秘 ...

  5. 开源项目SMSS开源项目(三)——protobuf协议设计

    本文的第一部分将介绍protobuf使用基础以及如何利用protobuf设计通信协议.第二部分会给出smss项目的协议设计规范和源码讲解. 一.Protobuf使用基础 什么是protobuf pro ...

  6. 开源项目renren-fast-vue开发环境部署(前端部分)

    开源项目renren-fast-vue开发环境部署(前端部分) 说明:renren-fast是一个开源的基于springboot的前后端分离手脚架,当前版本是3.0 开发文档需要付费,官方的开发环境部 ...

  7. 开源项目renren-fast开发环境部署(后端部分)

    开源项目renren-fast开发环境部署(后端部分) 说明:renren-fast是一个开源的基于springboot的前后端分离手脚架,当前版本是3.0 开发文档需要付费,官方的开发环境部署介绍相 ...

  8. 我的开源之路:耗时 6 个月发布线程池框架,GitHub 1.7k Star!

    文章首发在公众号(龙台的技术笔记),之后同步到掘金和个人网站:xiaomage.info Hippo4J 线程池框架经过 6 个多月的版本迭代,2022 年春节当天成功发行了 1.0.0 RELEAS ...

  9. SlickSafe.NET 开源权限框架开发指南

    前言:本文适用于快速搭建权限系统的用户,尤其适用于希望有良好定义的权限模型建立:系统解决方案是在基于角色访问控制(RBAC)策略基础上的权限访问模型实现,主要完成了后台权限验证逻辑和前端权限数据验证的 ...

随机推荐

  1. CODE FESTIVAL 2017 qual A C Palindromic Matrix(补题)

    彩笔看到题目后,除了懵逼,没有啥反应了,唯一想的就是 这是不是dp啊?看了题解才发现,原来是这样啊. 画几个矩阵看看就能看出来规律. 思路:先假设这是个M * N的矩阵 如果M和N都是偶数,则每个出现 ...

  2. Java JDBC学习实战(二): 管理结果集

    在我的上一篇博客<Java JDBC学习实战(一): JDBC的基本操作>中,简要介绍了jdbc开发的基本流程,并详细介绍了Statement和PreparedStatement的使用:利 ...

  3. Python--day72--SweetAlert插件

    引用:http://www.cnblogs.com/liwenzhou/p/8718861.html 补充一个SweetAlert插件示例 点击下载Bootstrap-sweetalert项目. $( ...

  4. 精选Pycharm里6大神器插件

    http://www.sohu.com/a/306693644_752099 上次写了一篇关于Sublime的精品插件推荐,有小伙伴提议再来一篇Pycharm的主题.相比Sublime,Pycharm ...

  5. 深入java面向对象四:Java 内部类种类及使用解析(转)

    内部类Inner Class 将相关的类组织在一起,从而降低了命名空间的混乱. 一个内部类可以定义在另一个类里,可以定义在函数里,甚至可以作为一个表达式的一部分. Java中的内部类共分为四种: 静态 ...

  6. Python--day29--logging模块(日志模块)

    重要程度六颗星,比如一个小窗口的广告如果因为你没有日志的问题导致点击量没有记录下来,几十分钟那就会损失几十万了,这责任谁负得起. 希望离开一个公司是因为有了更好的去处而不是因为各种各样的原因被开掉,那 ...

  7. 2019-10-23-WPF-使用-SharpDx-异步渲染

    title author date CreateTime categories WPF 使用 SharpDx 异步渲染 lindexi 2019-10-23 21:18:38 +0800 2018-0 ...

  8. Linux创建用户、设置密码、修改用户、删除用户命令

    与大家分享下Linux系统中创建用户.设置密码.修改用户.删除用户的命令,希望对你有所帮助. useradd testuser  创建用户testuserpasswd testuser  给已创建的用 ...

  9. P1064 连续自然数和

    题目描述 对一个给定的自然数 M ,求出所有的连续的自然数段,这些连续的自然数段中的全部数之和为 M . 例子:1998+1999+2000+2001+2002=10000 ,所以从 1998 到 2 ...

  10. win10 uwp 使用 Microsoft.Graph 发送邮件

    在 2018 年 10 月 13 号参加了 张队长 的 Office 365 训练营 学习如何开发 Office 365 插件和 OAuth 2.0 开发,于是我就使用 UWP 尝试使用 Micros ...