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“ 网页。

1
2
3
4
5
6
7
8
9
kqueue() 这是 FreeBSD(以及即将推出的 NetBSD)的推荐边缘触发轮询替代品。
epoll 这是 2.6 Linux 内核的推荐边缘触发轮询替换。
使用异步调用(例如 aio_write()) 启动 I/O,使用完成通知(例如信号或完成端口)来了解 I/O 何时完成。适用于网络和磁盘 I/O。

每个线程为多个客户端提供服务,并使用非阻塞 I/O 和就绪更改通知,这与“水平触发”就绪通知相反。它对编程错误的容忍度要低一些,因为如果你只错过了一个事件,该事件的连接就会永远卡住。

使用每个服务器线程为多个客户端提供服务,并使用异步 I/O,这在 Unix 中还没有流行起来,可能是因为很少有操作系统支持异步 I/O,也可能是因为它(如非阻塞 I/O)需要重新考虑你的应用程序。

为每个服务器线程提供一个客户端服务,缺点是每个客户端都使用整个堆栈帧,这会消耗内存。许多操作系统在处理超过几百个线程时也遇到了麻烦。

libevent:使用了经典的基于 select、poll、epoll、kqueue 等系统调用的事件循环实现,支持跨平台操作系统,包括 Linux、Unix、Windows 等。
libevent:提供了更多高级抽象的接口,包括事件优先级、定时器、信号处理等功能,比如可以用 bufferevent 进行高级的缓冲区管理,支持更多的网络协议层,比如 HTTP、DNS 等。

libev:是一个轻量级的事件循环库,底层实现采用了对特定平台的高效事件通知机制,比如使用 epoll 在 Linux 上。它的设计更加简洁,代码库更小,性能更高。
libev:更加专注于事件循环本身,提供了更简单的接口,没有像 libevent 那样的高级抽象,因此更加轻量级和灵活。

libevent 和 libev 对 window 支持比较差,由此产生了 libuv 库;libuv 基于 libev,在window 平台上更好的封装了 iocp;node.js 基于 libuv;

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
// libev demo
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>

#include <string.h>
#include <time.h>
#include <ev.h>

static void
do_accept(struct ev_loop *reactor, ev_io *w, int events) {
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));
socklen_t len = sizeof(addr);
int clientfd = accept(w->fd, (struct sockaddr *) &addr, &len);
if (clientfd != -1)
close(clientfd);

printf("accept fd = %d\n", clientfd);
}

int socket_listen(uint16_t port) {
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));

addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;

if (bind(listenfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_in)) == -1)
return -1;

if (listen(listenfd, 5) == -1)
return -1;

printf("server start listening port:%d\n", port);
return listenfd;
}

static void
do_timer(struct ev_loop *loop, ev_timer *timer_w, int e) {
time_t now = time(NULL);
printf("do_timer %s", (char *) ctime(&now));
// ev_timer_stop(loop, timer_w);
}

int main() {
struct ev_loop *loop = ev_loop_new(0);

struct ev_timer t1;
ev_timer_init(&t1, do_timer, 1, 1);
ev_timer_start(loop, &t1);

int listenfd = socket_listen(8989);
if (listenfd == -1) return -2;
struct ev_io i1;
ev_io_init(&i1, do_accept, listenfd, EV_READ);
ev_io_start(loop, &i1);
ev_run(loop, 0);

ev_loop_destroy(loop);
return 0;
}

关键函数

event_base_new:
使用libevent 函数之前需要分配一个或者多个 event_base 结构体, 每个event_base结构体持有一个事件集合, 可以检测以确定哪个事件是激活的, event_base结构相当于epoll红黑树的树根节点, 每个event_base都有一种用于检测某种事件已经就绪的 “方法”
event_new:
typedef void (event_callback_fn)(evutil_socket_t fd, short events, void arg);
struct event event_new(struct event_base base, evutil_socket_t fd, short events, event_callback_fn cb, void arg);
event_new负责创建event结构指针, 同时指定对应的base(epfd), 还有对应的文件描述符, 事件, 以及回调函数和回调函数的参数。类似于我们使用epoll的时候定义了struct epoll_event ev并且对ev的两个字段进行了赋值, 但是此时尚未调用epoll_ctl对事件上树.
event_add:
int event_add(struct event
ev, const struct timeval *timeout);
将非未决态事件转为未决态, 相当于调用epoll_ctl函数(EPOLL_CTL_ADD), 开始监听事件是否产生, 相当于epoll的上树操作.

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// 只用libevent事件检测,io操作自己来处理 Demo
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include <netinet/in.h>
#include "event2/event.h"

void socket_read_cb(int fd, short events, void *arg);

void socket_accept_cb(int fd, short events, void *arg) {
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
evutil_socket_t clientfd = accept(fd, (struct sockaddr *) &addr, &len);
evutil_make_socket_nonblocking(clientfd);
printf("accept a client %d\n", clientfd);
struct event_base *base = (struct event_base *) arg;
struct event *ev = event_new(NULL, -1, 0, NULL, NULL);
event_assign(ev, base, clientfd, EV_READ | EV_PERSIST,
socket_read_cb, (void *) ev);
event_add(ev, NULL);
}

void socket_read_cb(int fd, short events, void *arg) {
char msg[4096];
struct event *ev = (struct event *) arg;
int len = read(fd, msg, sizeof(msg) - 1);
if (len <= 0) {
printf("client fd:%d disconnect\n", fd);
event_free(ev);
close(fd);
return;
}

msg[len] = '\0';
printf("recv the client msg: %s", msg);

char reply_msg[4096] = "recvieced msg: ";
strcat(reply_msg + strlen(reply_msg), msg);
write(fd, reply_msg, strlen(reply_msg));
}

int socket_listen(int port) {
int errno_save;

evutil_socket_t listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1)
return -1;

evutil_make_listen_socket_reuseable(listenfd);

struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = 0;
sin.sin_port = htons(port);

if (bind(listenfd, (struct sockaddr *) &sin, sizeof(sin)) < 0) {
evutil_closesocket(listenfd);
return -1;
}

if (listen(listenfd, 5) < 0) {
evutil_closesocket(listenfd);
return -1;
}

evutil_make_socket_nonblocking(listenfd);

return listenfd;
}

int main(int argc, char **argv) {
int listenfd = socket_listen(8080);
if (listenfd == -1) {
printf("socket_listen error\n");
return -1;
}
struct event_base *base = event_base_new();

struct event *ev_listen = event_new(base, listenfd, EV_READ | EV_PERSIST,
socket_accept_cb, base);
/*
event_new 等价于
struct event ev_listen;
event_set(&ev_listen, listenfd, EV_READ | EV_PERSIST, socket_accept_cb, base);
event_base_set(base, &ev_listen);
*/

event_add(ev_listen, NULL);

event_base_dispatch(base);
return 0;
}

自带buffer的事件-bufferevent
bufferevent实际上也是一个event, 只不过比普通的event高级一些, 它的内部有两个缓冲区, 以及一个文件描述符(网络套接字)。一个网络套接字有读和写两个缓冲区, bufferevent同样也带有两个缓冲区, 还有就是libevent事件驱动的核心回调函数
一个bufferevent对应两个缓冲区, 三个回调函数, 分别是写回调, 读回调和事件回调

  • 读回调:当bufferevent将底层读缓冲区的数据读到自身的读缓冲区时触发读事件回调.
  • 写回调:当bufferevent将自身写缓冲的数据写到底层写缓冲区的时候触发写事件回调, 由于数据最终是写入了内核的写缓冲区中, 应用程序已经无法控制, 这个事件对于应用程序来说基本没什么用, 只是通知功能.
  • 事件回调:当bufferevent绑定的socket连接, 断开或者异常的时候触发事件回调.

构建bufferevent对象
struct bufferevent bufferevent_socket_new(struct event_base base, evutil_socket_t fd, int options);
bufferevent_socket_new 对已经存在socket创建bufferevent事件, 可用于后面讲到的连接监听器的回调函数中.
options : bufferevent的选项
BEV_OPT_CLOSE_ON_FREE – 释放bufferevent自动关闭底层接口(当bufferevent被释放以后, 文件描述符也随之被close)
BEV_OPT_THREADSAFE – 使bufferevent能够在多线程下是安全的

连接操作bufferevent_socket_connect
int bufferevent_socket_connect(struct bufferevent bev, struct sockaddr serv, int socklen);
该函数封装了底层的socket与connect接口, 通过调用此函数, 可以将bufferevent事件与通信的socket进行绑定, 参数如下:
bev – 需要提前初始化的bufferevent事件
serv – 对端(一般指服务端)的ip地址, 端口, 协议的结构指针
socklen – 描述serv的长度
说明: 调用此函数以后, 通信的socket与bufferevent缓冲区做了绑定

设置bufferevent回调bufferevent_setcb
对bufferevent缓冲区的读写操作的事件设置回调函数, 当往缓冲区中写数据的时候会触发写回调函数, 当数据从socket的内核缓冲区读到bufferevent读缓冲区中的时候会触发读回调函数.readcb, writecb,eventcb分别对应了读回调, 写回调, 事件回调, cbarg代表回调函数的参数。
读回调函数的触发时机: - 当socket的内核socket读缓冲区中有数据的时候, bufferevent会将内核缓冲区中的数据读到自身的读缓冲区, 会触发bufferevent的读操作, 此时会调用bufferevent的读回调函数.
写回调函数的触发时机: - 当往bufferevent的写缓冲区写入数据的时候, bufferevent底层会把缓冲区中的数据写入到内核的socket的写缓冲区中, 此时会触发bufferevent的写回调函数, 最后由内核的驱动程序将数据发送出去.
事件(异常)回调函数的触发时机: - 客户端关闭连接或者是被信号终止进程会触发事件回调函数

写数据到写缓冲区bufferevent_write
int bufferevent_write(struct bufferevent bufev, const void data, size_t size);
int bufferevent_write_buffer(struct bufferevent bufev, struct evbuffer buf);
bufferevent_write是将data的数据写到bufferevent的写缓冲区,bufferevent_write_buffer 是将数据写到写缓冲区另外一个写法, 实际上bufferevent的内部的两个缓冲区结构就是struct evbuffer。
当我们调用bufferevent_write往写缓冲区写数据时,实际上是调用了evbuffer_add,在写入后libevent自动帮我们写到内核缓冲区,之后会触发写回调函数。
若这个evbuffer中没有一个 chain 可以写入数据,则需要根据写入的数据大小新申请一个 chain 挂到链表末尾,然后往这个chain中写数据,所以每个 chain 的 buffer 大小是不定的。

从读缓冲区读数据bufferevent_read
size_t bufferevent_read(struct bufferevent bufev, void data, size_t size);
int bufferevent_read_buffer(struct bufferevent bufev, struct evbuffer buf);
bufferevent_read 是将bufferevent的读缓冲区数据读到data中, 同时将读到的数据从bufferevent的读缓冲清除。
bufferevent_read_buffer 将bufferevent读缓冲数据读到buf中, 接口的另外一种。
bufferevent_read()底层调用evbuffer_remove,这代表它按照指定长度去读,其又调用了evbuffer_copyout_from、evbuffer_drain

获取读写缓冲区bufferevent_get_input和bufferevent_get_output
struct evbuffer bufferevent_get_input(struct bufferevent bufev)
struct evbuffer bufferevent_get_output(struct bufferevent bufev)
获取bufferevent的读缓冲区和写缓冲区

分割字符读evbuffer_readln,固定长度读evbuffer_remove
char evbuffer_readln(struct evbuffer buffer, size_t n_read_out, enum evbuffer_eol_style eol_style);
int evbuffer_remove(struct evbuffer
buf, void *data, size_t datlen);
分割字符读evbuffer_readln
固定长度读evbuffer_remove

链接监听器-evconnlistener
链接监听器封装了底层的socket通信相关函数, 比如socket, bind, listen, accept这几个函数。链接监听器创建后实际上相当于调用了socket, bind, listen, 此时等待新的客户端连接到来, 如果有新的客户端连接, 那么内部先进行调用accept处理, 然后调用用户指定的回调函数

构建链接监听器evconnlistener_new_bind
struct evconnlistener evconnlistener_new_bind(
struct event_base
base,evconnlistener_cb cb,
void ptr, unsigned flags, int backlog,
const struct sockaddr
sa, int socklen
);
是在当前没有套接字的情况下对链接监听器进行初始化, 看最后2个参数实际上就是bind使用的关键参数,
backlog是listen函数的关键参数(略有不同的是, 如果backlog是-1, 那么监听器会自动选择一个合适的值,
如果填0, 那么监听器会认为listen函数已经被调用过了), ptr是回调函数的参数, cb是有新连接之后的回调函数,
但是注意这个回调函数触发的时候, 链接器已经处理好新连接了, 并将与新连接通信的描述符交给回调函数。
flags 需要参考几个值:
LEV_OPT_LEAVE_SOCKETS_BLOCKING 文件描述符为阻塞的
LEV_OPT_CLOSE_ON_FREE 关闭时自动释放
LEV_OPT_REUSEABLE 端口复用
LEV_OPT_THREADSAFE 分配锁, 线程安全

struct evconnlistener evconnlistener_new(
struct event_base
base,
evconnlistener_cb cb, void *ptr,
unsigned flags, int backlog,
evutil_socket_t fd
);
evconnlistener_new函数与前一个函数不同的地方在与后2个参数, 使用本函数时, 认为socket已经初始化好, 并且bind完成, 甚至也可以做完listen, 所以大多数时候, 我们都可以使用第一个函数。

accept的回调函数evconnlistener_cb
typedef void (evconnlistener_cb)(struct evconnlistener evl, evutil_socket_t fd, struct sockaddr cliaddr, int socklen, void ptr);
回调函数fd参数是与客户端通信的描述符, 并非是等待连接的监听的那个描述符, 所以cliaddr对应的也是新连接的对端地址信息, 已经是accept处理好的。

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// 使用libevent的事件检测与事件操作Demo
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <event.h>
#include <time.h>
#include <signal.h>
#include <event2/listener.h>
#include <event2/bufferevent.h>
#include <event2/buffer.h>

void socket_read_callback(struct bufferevent *bev, void *arg) {
// 操作读缓冲当中的数据
struct evbuffer *evbuf = bufferevent_get_input(bev); // 封装了读缓冲区
char *msg = evbuffer_readln(evbuf, NULL, EVBUFFER_EOL_LF);
// 也可以直接用 bufferevent_read 读数据
// bufferevent_read(struct bufferevent *bufev, void *data, size_t size)
if (!msg) return;

printf("server read the data: %s\n", msg);

char reply[4096] = {0};
sprintf(reply, "recvieced msg: %s\n", msg);//echo
// -WRN: 需要自己释放资源
free(msg);
bufferevent_write(bev, reply, strlen(reply));
}

// stdio标准输入触发读事件时的回调
void stdio_callback(struct bufferevent *bev, void *arg) {
// 获取读缓冲区并操作读缓冲中的数据
struct evbuffer *evbuf = bufferevent_get_input(bev); // 封装了读缓冲区
// 从中读一行,需要指定换行符
char *msg = evbuffer_readln(evbuf, NULL, EVBUFFER_EOL_LF);

if (!msg) return;

if (strcmp(msg, "quit") == 0) {
printf("safe exit!!!\n");
event_base_loopbreak(arg);
}

printf("stdio read the data: %s\n", msg);
}

// socket出现如错误、关闭等异常事件时的回调
void socket_event_callback(struct bufferevent *bev, short events, void *arg) {
if (events & BEV_EVENT_EOF)
printf("connection closed\n");
else if (events & BEV_EVENT_ERROR)
printf("some other error\n");
else if (events & BEV_EVENT_TIMEOUT)
printf("timeout\n");
bufferevent_free(bev); // close
}

// accept的回调函数封装
void listener_callback(struct evconnlistener *listener, evutil_socket_t fd,
struct sockaddr *sock, int socklen, void *arg) {
char ip[32] = {0};
evutil_inet_ntop(AF_INET, sock, ip, sizeof(ip) - 1);
printf("accept a client fd:%d ip:%s\n", fd, ip);
struct event_base *base = (struct event_base *) arg;
//创建一个bufferevent
struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
// 设置读、写、以及异常时的回调函数
bufferevent_setcb(bev, socket_read_callback, NULL, socket_event_callback, NULL);
// 使能这个bufferevent开启读事件
bufferevent_enable(bev, EV_READ | EV_PERSIST);
}

static void
do_timer(int fd, short events, void *arg) {
struct event *timer = (struct event *) arg;
time_t now = time(NULL);
printf("do_timer %s", (char *) ctime(&now));
//event_del(timer);
// struct timeval tv = {1,0};
// event_add(timer, &tv);
}

static void
do_sig_int(int fd, short event, void *arg) {
struct event *si = (struct event *) arg;
event_del(si);
printf("do_sig_int SIGINT\n");//CTRL + C
}

// 建立连接的事件封装
int main() {
struct sockaddr_in sin;
memset(&sin, 0, sizeof(struct sockaddr_in));
sin.sin_family = AF_INET;
sin.sin_port = htons(8080);
/* 底层IO多路复用抽象 */
struct event_base *base = event_base_new();
/* evconnlistener 监听 */
struct evconnlistener *listener =
evconnlistener_new_bind(base, listener_callback, base,
LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE,
10, (struct sockaddr *) &sin,
sizeof(struct sockaddr_in));

/* 普通 fd 的 IO 事件管理,此处以标准输入 stdin 为例 */
struct bufferevent *ioev = bufferevent_socket_new(base, 0, BEV_OPT_CLOSE_ON_FREE);
bufferevent_setcb(ioev, stdio_callback, NULL, NULL, base);
bufferevent_enable(ioev, EV_READ | EV_PERSIST);
/* 定时事件 */
struct event evtimer;
struct timeval tv = {1, 0}; // {秒, 微秒}
event_set(&evtimer, -1, EV_PERSIST, do_timer, &evtimer); // tv 为超时时间
event_base_set(base, &evtimer);
event_add(&evtimer, &tv);
/* 信号事件 */
struct event evint;
event_set(&evint, SIGINT, EV_SIGNAL, do_sig_int, &evint);
event_base_set(base, &evint);
event_add(&evint, NULL);

/* 开启主循环 */
event_base_dispatch(base);
/* 结束释放资源 */
evconnlistener_free(listener);
event_base_free(base);
return 0;
}

evbuffer 是 libevent 底层实现的一种链式缓冲区,当我们使用bufferevent来管理事件时,就会从每个事件的 evbuffer 中读写数据。每个 evbuffer 实质是一个缓冲区链表,其中的每个元素为 struct evbuffer_chain。一个struct evbuffer中的关键成员定义如下:

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
struct evbuffer {
/** The first chain in this buffer's linked list of chains. */
struct evbuffer_chain *first;
/** The last chain in this buffer's linked list of chains. */
struct evbuffer_chain *last;
/** Pointer to the next pointer pointing at the 'last_with_data' chain. */
struct evbuffer_chain **last_with_datap; // 指针指向最后一个可写的 chain
/** Total amount of bytes stored in all chains.*/
size_t total_len;
...... // 以上为关键成员
}
/** A single item in an evbuffer. */
struct evbuffer_chain {
/** points to next buffer in the chain */
struct evbuffer_chain *next; // 指向下一个 evbuffer_chain

/** total allocation available in the buffer field. */
size_t buffer_len; // buffer 的长度

/** unused space at the beginning of buffer or an offset into a file for sendfile buffers. */
ev_misalign_t misalign; // 实际数据在 buffer 中的偏移

/** Offset into buffer + misalign at which to start writing.
* In other words, the total number of bytes actually stored in buffer. */
size_t off; // buffer 中有效数据的末尾,接下来的数据从这个位置开始填入(该位置即 buffer + misalign + off)

/** number of references to this chain */
int refcnt; // 这个 buffer的引用计数

/** Usually points to the read-write memory belonging to this buffer allocated as part of the evbuffer_chain allocation.
* For mmap, this can be a read-only buffer and EVBUFFER_IMMUTABLE will be set in flags. For sendfile, it may point to NULL. */
unsigned char *buffer; // 指向实际数据存储的位置,这是真正的 buffer
};

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. 绑定地址和端口:使用 evhttp_bind_socket 绑定服务器的地址和端口,以便接收客户端的连接请求。
  3. 处理请求:
    • 当有客户端连接到服务器时,会触发 accept_socket_cb 事件回调。
    • 在回调函数中,通过 evhttp_get_request 获取客户端的 HTTP 请求。
    • 该函数会生成一个新的连接对象 evhttp_connection,用于表示与客户端的连接。
    • 接着通过 evhttp_associate_new_request_with_connection 创建一个基于该连接的请求对象 evhttp_request。
  4. 读取请求数据:
    • 调用 evhttp_start_read 开始读取客户端发送的请求数据。
    • 进入 evhttp_read_cb 回调函数,根据 evcon->state 的状态机进行处理。
    • 首先读取请求行(比如 GET / HTTP/1.0),然后读取请求头部信息,将其存储到 req->input_headers 中。
    • 当请求头部信息读取完毕后,进入 evhttp_connection_done 函数,最终调用请求的回调函数来处理请求。
  5. 处理请求:
    • 调用用户设置的回调函数(通过 evhttp_set_gencb 或 evhttp_set_cb 设置),处理具体的业务逻辑。
    • 如果没有设置对应 URI 的回调函数,则返回 404 Not Found。
    • 处理完业务逻辑后,发送响应给客户端,完成一个请求-响应循环。

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

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

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