libevent实现https服务器

libevent简介

这是一个事件驱动的库,libevent API提供了一种机制,该机制可在文件描述符上发生特定事件或达到超时后执行回调函数。此外,libevent还支持由于信号或定期超时产生的回调。

libevent旨在替换事件驱动的网络服务器中的事件循环。应用程序只需要调用event_dispatch(),然后动态添加或删除事件,而无需更改事件循环。

目前,libevent支持/dev/poll,kqueue,event ports,POSIX select,Windows select,poll和epoll。
内部事件机制完全独立于公开的事件API,libevent的简单更新就可以提供新功能,而无需重新设计应用程序。正因为这样,libevent允许便携式应用程序开发,并提供操作系统上可用的最具扩展性的事件通知机制。
Libevent也可以用于多线程应用程序,方法是隔离每个event_base,以便只有一个线程可以访问它,或者通过锁定对单个共享event_base的访问来实现。 Libevent应该在Linux,* BSD,Mac OS X,Solaris,Windows等平台上编译。

Libevent还提供了用于缓冲​​网络IO的复杂框架,并支持套接字,过滤器,速率限制,SSL,零拷贝文件传输和IOCP。 Libevent包括对几种有用协议的支持,包括DNS,HTTP和最小的RPC框架。

有关网络服务器事件通知机制的更多信息,请参见Dan Kegel的”The C10K problem“ 网页。

HTTP服务器

TCP 三次握手

这是基本的知识,必须掌握,我把它写在另外一篇文章了,点击这里查看。

TCP Server

这里需要掌握socket的一些知识,可以直接查看man手册,通过命令man -f socket找到对应的章节,然后man 2 socket查看即可,篇幅较长,点击这里查看。

HTTP Server

有了上面的tcp socket编程基础后,我们可以尝试加入http协议部分,来完成对收到的http请求进行响应。
具体参考C++实现简单的HTTP Serve

Libevent Server

https://kukuruku.co/post/lightweight-http-server-in-less-than-40-lines-on-libevent-and-c-11/

libevent对socket相关函数进行了进一层的封装,加入了对http协议对处理,并结合本身的事件处理机制,可以快速的建立http服务器。

libevent比libev和boost.asio更好,因为它具有嵌入式HTTP服务器和用于缓冲区操作的抽象。 它还具有大量的辅助功能。 您可以自己编写一个简单的FSN(有限状态机)或通过其他方法来检查HTTP协议。 使用libevent时它已经全部存在。 您也可以进入较低级别,为HTTP编写自己的解析器,并在libevent上使用套接字执行工作。 我喜欢这个库的详细程度。 如果您想快速执行某项操作,将会发现使用高级语言的接口通常不那么灵活的。 如果有更严格的要求,则可以逐级逐步降低。 该库允许执行许多操作,例如:异步输入/输出,与网络配合使用,与计时器配合使用,rpc等。您还可以使用它来创建服务器端和客户端软件。

参考libevent-2.1.11,其中的http.c给出了大部分封装后的api,实际使用这些api的例子可以参考http_server.c文件,我根据其中的api做了一个最简单的服务器,接收到客户端的任何请求,均返回ok,其中的evhttp_set_cb是指定特定的uri,evhttp_set_gencb是指没有被cb过滤的uri均会被gencb处理,如果gencb也没有设置的话,就会返回404页面,查看源代码的http.c里面的evhttp_handle_request函数即可得知,可以看出,经过封装后的服务器代码看起来非常简洁,短短几十行就能达到之前的效果,其实效果还更好。

evhttp其实是bufferevent的http版本,而bufferevent是在之前的event上封装出来的,使用起来更加方便,通过buffevent_setcb就能设置好读写、异常事件的回调函数,里面已经实现了ev_read/ev_write/ev_error,后面只要通过bufferevent_enable/bufferevent_disable就可以将事件event_add起来,如果设置了定时器,就要使用bufferevent_settimeout,这里要特别注意ev_error异常事件的处理,比如连接失败,连接异常,连接超时,客户端主动关闭连接等都是需要在这个里面处理的,通过what参数来判断传进来异常的类型,可以看看bufferevent_event_cb函数指针原型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <evhttp.h>
#include <string.h>

namespace {
const std::string addr = "127.0.0.1";
const int port = 8080;
}

void common(evhttp_request *req, void *arg)
{
std::cout << "common uri: " << req->uri << std::endl;
evhttp_send_reply(req, 200, "OK", NULL);
}

void test(evhttp_request *req, void *arg)
{
std::cout << "test uri: " << req->uri << std::endl;
evhttp_send_reply(req, 200, "OK", NULL);
}

int main(int argc, char *argv[])
{
event_init(); //初始化event_base
struct evhttp *server = evhttp_start(addr.c_str(), port);
evhttp_set_cb(server, "/test", test, nullptr);
evhttp_set_gencb(server, common, nullptr);
event_dispatch(); //开始循环
evhttp_free(server);

return 0;
}

使用event_init函数初始化库的全局对象current_base。你只能将此功能用于单线程处理。为了执行多线程操作,应该为每个线程创建一个单独的对象,http_server.c中的例子就可以。

最后可以使用event_dispatch函数开始事件处理的循环。此函数用于在一个线程中使用全局对象。

可以结合C++11的一些语法将上述代码进行优化,利用智能指针自动释放内存,在一个函数里面定义另外一个函数指针,更加的精简代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <memory>
#include <cstdint>
#include <iostream>
#include <evhttp.h>

int main()
{
if (!event_init())
{
std::cerr << "Failed to init libevent." << std::endl;
return -1;
}
char const SrvAddress[] = "127.0.0.1";
std::uint16_t SrvPort = 8080;
std::unique_ptr<evhttp, decltype(&evhttp_free)> Server(evhttp_start(SrvAddress, SrvPort), &evhttp_free);
if (!Server)
{
std::cerr << "Failed to init http server." << std::endl;
return -1;
}
void (*OnReq)(evhttp_request * req, void *) = [](evhttp_request * req, void *)
{
auto *OutBuf = evhttp_request_get_output_buffer(req);
if (!OutBuf)
return;
evbuffer_add_printf(OutBuf, "<html><body><center><h1>Hello World!</h1></center></body></html>");
evhttp_send_reply(req, HTTP_OK, "", OutBuf);
};
evhttp_set_gencb(Server.get(), OnReq, nullptr);
if (event_dispatch() == -1)
{
std::cerr << "Failed to run messahe loop." << std::endl;
return -1;
}
return 0;
}

多线程的版本需要调用更加底层的api,首先得自己创建event_base,不能再用公共的current_base,然后建立evhttp对象,设置好回调函数再进行事件循环,注意其中的异常处理,销毁机制同样采用智能指针即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <stdexcept>
#include <iostream>
#include <memory>
#include <chrono>
#include <thread>
#include <cstdint>
#include <vector>
#include <evhttp.h>
int main()
{
char const SrvAddress[] = "127.0.0.1";
std::uint16_t const SrvPort = 5555;
int const SrvThreadCount = 4;
try {
void (*OnRequest)(evhttp_request *, void *) = [] (evhttp_request *req, void *) {
auto *OutBuf = evhttp_request_get_output_buffer(req);
if (!OutBuf)
return;
evbuffer_add_printf(OutBuf, "<html><body><center><h1>Hello World!</h1></center></body></html>");
evhttp_send_reply(req, HTTP_OK, "", OutBuf);
};
std::exception_ptr InitExcept;
bool volatile IsRun = true;
evutil_socket_t Socket = -1;
auto ThreadFunc = [&] () {
try {
std::unique_ptr<event_base, decltype(&event_base_free)> EventBase(event_base_new(), &event_base_free);
if (!EventBase)
throw std::runtime_error("Failed to create new base_event.");
std::unique_ptr<evhttp, decltype(&evhttp_free)> EvHttp(evhttp_new(EventBase.get()), &evhttp_free);
if (!EvHttp)
throw std::runtime_error("Failed to create new evhttp.");
evhttp_set_gencb(EvHttp.get(), OnRequest, nullptr);
if (Socket == -1) { //共用一个socket
auto *BoundSock = evhttp_bind_socket_with_handle(EvHttp.get(), SrvAddress, SrvPort);
if (!BoundSock)
throw std::runtime_error("Failed to bind server socket.");
if ((Socket = evhttp_bound_socket_get_fd(BoundSock)) == -1)
throw std::runtime_error("Failed to get server socket for next instance.");
} else {
if (evhttp_accept_socket(EvHttp.get(), Socket) == -1)
throw std::runtime_error("Failed to bind server socket for new instance.");
}
for ( ; IsRun ; ) {
event_base_loop(EventBase.get(), EVLOOP_NONBLOCK); //非阻塞循环性调度
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
catch (...) {
InitExcept = std::current_exception();
}
};
auto ThreadDeleter = [&] (std::thread *t) { IsRun = false; t->join(); delete t; };
typedef std::unique_ptr<std::thread, decltype(ThreadDeleter)> ThreadPtr;
typedef std::vector<ThreadPtr> ThreadPool;
ThreadPool Threads;
for (int i = 0 ; i < SrvThreadCount ; ++i) {
ThreadPtr Thread(new std::thread(ThreadFunc), ThreadDeleter);
std::this_thread::sleep_for(std::chrono::milliseconds(500));
if (InitExcept != std::exception_ptr()) {
IsRun = false;
std::rethrow_exception(InitExcept);
}
Threads.push_back(std::move(Thread));
}
std::cout << "Press Enter fot quit." << std::endl;
std::cin.get();
IsRun = false;
}
catch (std::exception const &e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}

HTTPS服务器

https://segmentfault.com/a/1190000016855991
https://cheapsslsecurity.com/blog/http-vs-https-do-you-really-need-https/

原理上很简单,在原来http的基础上,加入ssl通道,所以在原来http三次握手的基础上还要增加ssl握手协议,建立连接的时间也会变长,可以用tcpdump tcp port加端口查看实际的报文,发现多了一次握手的过程,也可以tcpdump -i any -w file.cap tcp port 加端口打包后结合wireshark进行分析,在实际编程的过程中,accept后会生成一个新的套接字fd,然后新建ssl套接字并绑定之前的fd,利用ssl套接字进行握手监听,后续通信都在ssl套接字上进行。
具体步骤可分为:1、初始化OpenSSL;2、创建CTX;3、创建SSL套接字;4、完成SSL握手;5、 数据传输;6、会话结束;

其中所有的数据传输采用SSL_read、SSL_write接口。

在libevent中,ssl相关的接口均被封装起来了,使得使用openssl的方法更加便捷,但是初始化openssl、释放openssl资源的一些操作还是需要自己动手,见不到SSL_read、SSL_write接口了,全部在bufferevent相关的函数里面,比如bufferevent_openssl_socket_new会生成一个ssl接口的bufferevent对象,如果不使用ssl,初始化bufferevent对象时使用bufferevent_socket_new即可,后续基于该对象bev进行读写操作即可,如bufferevent_write_buffer、bufferevent_read_buffer,可以看出后续有没有使用ssl的操作都是一样的。

初始化时需要用到https的证书和私钥,开发阶段可以自己生成,一般使用单向加密就行,首先生成RSA私钥(使用aes256加密):openssl genrsa -aes256 -out server.key 2048,再根据该私钥生成一个证书:openssl req -new -x509 -days 3650 -key server.key -out server.crt,由于这个证书是自制的,所以一般浏览器不认,调试的时候,通过高级选项让它信任即可,如果使用curl请求的话,加一个-k的参数即可。

关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//初始化
#if (OPENSSL_VERSION_NUMBER < 0x10100000L) || \
(defined(LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x20700000L)
// Initialize OpenSSL
SSL_library_init();
ERR_load_crypto_strings();
SSL_load_error_strings();
OpenSSL_add_all_algorithms();
#endif
/* Create a new OpenSSL context */
ssl_ctx = SSL_CTX_new(SSLv23_method());
if (!ssl_ctx) {
err_openssl("SSL_CTX_new");
goto error;
}
if (!SSL_CTX_use_certificate_file(ctx, CERT_FILE, SSL_FILETYPE_PEM)) {
err_openssl(stdout);
goto error;
}
if (!SSL_CTX_use_PrivateKey_file(ctx, PKEY_FILE, SSL_FILETYPE_PEM)) {
err_openssl(stdout);
goto error;
}
if (!SSL_CTX_check_private_key(ctx)) {
err_openssl(stdout);
goto error;
}
// Create event base
base = event_base_new();
if (!base) {
perror("event_base_new()");
goto error;
}
// Create OpenSSL bufferevent and stack evhttp on top of it
ssl = SSL_new(ssl_ctx);
if (ssl == NULL) {
err_openssl("SSL_new()");
goto error;
}
// 将base绑定到bev上,后续操作bev即可
bufferevent *bev = bufferevent_openssl_socket_new(base, -1, ssl,
BUFFEREVENT_SSL_CONNECTING,
BEV_OPT_CLOSE_ON_FREE);
// 如果不开启ssl
// 参考libevent库http.c evhttp_get_request_connection函数
bev = bufferevent_socket_new(base, -1, 0);
if (bev == NULL) {
fprintf(stderr, "bufferevent_openssl_socket_new() failed\n");
goto error;
}
// bev是和连接con结合在一起使用的
bufferevent_setcb(bev, evhttp_read_cb, evhttp_write_cb, evhttp_error_cb, evcon);
evcon->bufev = bev; // 最后将这个赋值给con保存

如果结合libevent库http.c例子来看的话,只要看evhttp_start函数就行了,服务器大概的运行流程为:

  1. 建立服务器的对象evhttp http;
  2. 绑定bind_socket、监听listen端口,当有请求来时,进入accept_socket_cb事件回调,evhttp_get_request处理该请求,会先生成一个连接evhttp_get_request_connection.
  3. 然后生成基于该连接的请求evhttp_associate_new_request_with_connection.
  4. 最后读取数据evhttp_start_read_,进入evhttp_read_cb函数,这里是基于一个状态机evcon->state的处理,先从第一行开始读evhttp_read_firstline,这里会解析出GET / HTTP/1.0的关键信息,再读头部信息evhttp_read_header,比如content-length/content-type都可以读出来,存到req->input_headers键值对中,因为当前的req->kind为EVHTTP_REQUEST,所以完了之后就会进入到evhttp_connection_done中,最后调用请求的回调函数进行处理(*req->cb)(req, req->cb_arg);,这个回调函数是evhttp_request_new函数使用时带有的参数evhttp_handle_request,在该回调函数中最终处理http设置好的用户自定义回调函数gencb/cb,之前通过evhttp_set_gencb/evhttp_set_cb函数设置的,比如uri对应的业务处理逻辑,除了特定uri的全部有gencb处理,如果gencb没有设置,那就只能回应404了,uri对应的业务处理完了后,在给客户端回复响应的消息,到此完成一个循环。

感觉比较绕,一般可以结合gdb来实际看看代码的流动过程,打一些关键的断点,然后不断的按continue,再用浏览器发请求,此时可以把运行到的断点按先后顺序打印出来了。

目前只能实现一种http或者https,没办法同时兼容两种协议,可能要用到重定向?目前看ngix的源码是这么写的,不知道libevent该怎么改,后面有想到再更新吧!

nephen wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!