基于libevent的多线程http server

项目中的业务需要实现最基本的HTTP/1.0版本的web服务器,客户端能够使用GET、POST方法请求资源,项目是运行在嵌入式linux系统中的,并且某一时刻可能有大量并发请求,综合考虑,选择libevent实现一个多线程的http服务器。

本文使用的libevent的版本是libevent-2.1.12-stable。也是目前最新的稳定版本。

程序源代码可以见公众号 xutopia77 文章 基于libevent的多线程http服务器

在开始论述之前,我希望读者对IO多路复用器(尤其是epoll),以及对reactor和proactor模式有个大致的理解。对于这两个方面,网络上已经有大量的文章了,我自己也写过很多,如果读者想要了解这两项内容,可以点击下面链接查阅: select,poll,epoll的区别以及使用方法

首先,libevent是什么,我们看下官方的介绍:

The libevent API provides a mechanism to execute a callback function when a specific event occurs on a file descriptor or after a timeout has been reached. Furthermore, libevent also support callbacks due to signals or regular timeouts.

libevent API提供了一种机制,可以在文件描述符上发生特定事件或超时后执行回调函数。此外,libevent还支持由于信号或常规超时而产生的回调。

同时看一下百度百科对他的介绍

Libevent 是一个用C语言编写的、轻量级的开源高性能事件通知库,主要有以下几个亮点:事件驱动( event-driven),高性能;轻量级,专注于网络,不如 ACE 那么臃肿庞大;源代码相当精炼、易读;跨平台,支持 Windows、 Linux、 *BSD 和 Mac Os;支持多种 I/O 多路复用技术, epoll、 poll、 dev/poll、 select 和 kqueue 等;支持 I/O,定时器和信号等事件;注册事件优先级。

Libevent 已经被广泛的应用,作为底层的网络库;比如 memcached、 Vomit、 Nylon、 Netchat等等。

简单的来讲,libevent是一个网络库,能够帮我们处理大量的网络编程细节,降低我们的网络编程门槛,并且它具备良好的跨平台能力。使用者只需要向它注册一些事件,它就会在合适的时机(比如可读、可写或定时事件触发时)调用我们对应的注册函数。

libevent包括事件管理、缓存管理、DNS、HTTP、缓存事件几大部分。事件管理包括各种IO(socket)、定时器、信号等事件;缓存管理是指evbuffer功能;DNS是libevent提供的一个异步DNS查询功能;

我们要实现http服务器,就是使用他的HTTP组件。HTTP是libevent的一个轻量级http实现,包括服务器和客户端。libevent也支持ssl,这对于有安全需求的网络程序非常的重要,但是其支持不是很完善,比如http server的实现就不支持ssl。

基于libevnet的单线程的使用方法见文章:

基于libevent的http服务器实现

这里基于libevnet实现多线程的http服务器。

libevent的多线程使用注意事项:

对于libevnet是否支持多线程,很多资料都有争议,有的说支持,有的说不支持。对于多线程这个特性是否能够使用,只能说多线程多线程是怎么使用的,是怎么与libevent结合的。

1,对于不同的线程,使用不同的base,是可以的。

2,libevent的信号事件是不支持多线程的,因为里面使用了全局变量。

3,不同的线程使用相同的base,即在不同线程里的事件都注册到一个base上,这是不行的,即使加锁也不行,因为某个事件没有用event_set()设置为EV_PERSIST,当事件发生时,会被自动删除。有可能线程a在删除事件的时候,线程b却在添加事件,这样还是会出现并发问题。最后的结论是——不行!。

如果想要使用多线程,就需要每个线程中对应一个base,将线程里的事件注册到线程的base上。

下面是http多线程使用的使用方法

启动多线程http的服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

int nfd = BindSocket(port);//监听端口

for (int i = 0; i < nthreads; i++)
{
WorkRoom* w = new WorkRoom;//每个线程独有的环境参数,不使用局部变量,防止离开局部作用域后失效

w->base = event_init();//libevnet使用多线程,每个线程中使用一个base

w->httpd = evhttp_new(w->base);//初始化http

r = evhttp_accept_socket(w->httpd, nfd);

evhttp_set_gencb(w->httpd, GenericHandler, this);//设置http路由 404

evhttp_set_cb(w->httpd, "/test", ProcessRequest, this );//设置http路由
event* watchdogEv = event_new(w->base, -1, EV_PERSIST|EV_TIMEOUT, watchdog, w);//每个base设置一个看门狗,可以做一些线程监控的工作
event_add(watchdogEv, &tv);
}

然后就是开启事件分发线程

1
event_base_dispatch( w->base );

可以在看门狗中关闭事件分发循环,event_base_loopexit是可以设置延迟事件的,不设置是可以参数为空。

1
2
struct timeval delay = { 0, 1 };
event_base_loopexit(w->base, &delay);

http服务器停止时,需要注意资源释放,以免内存泄漏

1
2
3
4
5
event_free(w->watchdogEv); 

evhttp_free(w->httpd);

event_base_free(w->base);

程序使用demo,只需要设置监听端口和要启动的线程数量即可,在程序停止的时候,调用stop会释放所有资源,避免内存泄漏。

1
2
3
4
5
6
7
8
servers::HttpSrv s;
s.start(8080, 5);

sleep(10);

LOG_INFO("http to stop\n");
s.stop();
LOG_INFO("http stop\n");

程序源代码可以见公众号 xutopia77 文章 基于libevent的多线程http服务器

程序运行结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
dispatch start
dispatch start
dispatch start
dispatch start
dispatch start
http start port:8080, thNum:5
http to stop
dispatch stop
dispatch stop
dispatch stop
dispatch stop
dispatch stop
http stop

postman请求测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET http://192.168.31.106:8080/test
User-Agent: PostmanRuntime/7.28.4
Accept: */*
Postman-Token: 7a99df83-e2b1-442b-9f07-272cf1b40ef8
Host: 192.168.31.106:8080
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.1 200 OK
Date: Sat, 15 Jan 2022 10:16:26 GMT
Content-Length: 17
Content-Type: text/html; charset=ISO-8859-1

Requested: /test

本次在项目中用到了libevent库,目前程序运行基本稳定,没有出现异常情况, libevent是目前一个非常优秀的跨平台网络库,通过研究学习它,可以知道一个网络库的设计方法,虽然libevent库不是很胖发,但是他的设计思想还是很优秀的,但是短短的时间里想要彻底掌握所有的细节,还是有难度的,这里我只是简单的阐述了libevnet的基本运行机制和使用方法,没有做过多的深入研究,如果本章中有错误的地方,还希望读者朋友们能够指出来。

程序源代码可以见公众号 xutopia77 文章 基于libevent的多线程http服务器