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. URL的转义和解析

    在开始python编程之前我们先来看看一个关与url的知识 在url中会有一些特殊字符,如果你写过cgi程序,并且提交一个表单去调用你的cgi,你会很清楚的 像?name=aiqier&age ...

  2. hadoop 端口总结

    localhost:50030/jobtracker.jsp localhost:50060/tasktracker.jsp localhost:50070/dfshealth.jsp 1. Name ...

  3. Scrapy项目注意事项

  4. H3C 递归查询

  5. H3C 链路聚合配置举例

  6. linux 系统挂起

    尽管内核代码的大部分 bug 以 oops 消息结束, 有时候它们可能完全挂起系统. 如果系 统挂起, 没有消息打印. 例如, 如果代码进入一个无限循环, 内核停止调度,[15]15 并且系 统不会响 ...

  7. dotnet 通过 HttpClient 下载文件同时报告进度的方法

    本文告诉大家一个简单的方法通过 HttpClient 下载文件,同时报告下载进度 通过 HttpClient 的 ContentLength 很多时候都可以拿到下载的内容的长度,通过 ReadAsyn ...

  8. 洛谷——P1305 新二叉树(新建二叉树以及遍历)

    题目描述输入一串二叉树,用遍历前序打出. 输入输出格式输入格式: 第一行为二叉树的节点数n.(n \leq 26n≤26) 后面n行,每一个字母为节点,后两个字母分别为其左右儿子. 空节点用*表示 输 ...

  9. U8 EAI实现XML的生成

    /*************************************************************************************************** ...

  10. visio基础

    右下角是一个切换文件的按钮 也可以用ctrl+tab键进行切换 页面底部左边是一个页面的增加与切换的几个按钮 这是切换页面不是切换文件 右上角这个按钮是一个功能隐藏的按钮 左上角这个按钮可以自定义快速 ...