一、libevent evhttp使用
1、基本流程
http服务端使用到的借口函数及流程如下
1)、创建event_base和evhttp
struct event_base *event_base_new(void);
struct evhttp *evhttp_new(struct event_base *base);
2)、绑定地址和端口
int evhttp_bind_socket(struct evhttp *http, const char *address, ev_uint16_t port);
3)、设置处理函数
void evhttp_set_gencb(struct evhttp *http,
void (*cb)(struct evhttp_request *, void *), void *arg);
4)、派发事件循环
int event_base_dispatch(struct event_base *);
代码:
#include "event2/http.h"
#include "event2/http_struct.h"
#include "event2/event.h"
#include "event2/buffer.h"
#include "event2/dns.h"
#include "event2/thread.h"
#include
#include
void HttpGenericCallback(struct evhttp_request* request, void* arg)
{
const struct evhttp_uri* evhttp_uri = evhttp_request_get_evhttp_uri(request);
char url[8192];
evhttp_uri_join(const_cast(evhttp_uri), url, 8192);
printf("accept request url:%s\n", url);
struct evbuffer* evbuf = evbuffer_new();
if (!evbuf)
{
printf("create evbuffer failed!\n");
return ;
}
//HTTP header
evhttp_add_header(request->output_headers, "Server", "myhttp v1.0");
evhttp_add_header(request->output_headers, "Content-Type", "text/plain; charset=UTF-8");//utf8
//evhttp_add_header(request->output_headers, "Content-Type", "text/html");
evhttp_add_header(request->output_headers, "Access-Control-Allow-Origin", "*");//跨域
evhttp_add_header(request->output_headers, "Connection", "close");
evbuffer_add_printf(evbuf, "Server response. Your request url is %s", url);
evhttp_send_reply(request, HTTP_OK, "OK", evbuf);
evbuffer_free(evbuf);
}
void specific_handler(struct evhttp_request *req, void *arg)
{
}
int main(int argc, char** argv)
{
if (argc != 2)
{
printf("usage:%s port\n", argv[0]);
return 1;
}
int port = atoi(argv[1]);
if (port == 0)
{
printf("port error:%s\n", argv[1]);
return 1;
}
struct event_base* base = event_base_new();
if (!base)
{
printf("create event_base failed!\n");
return 1;
}
struct evhttp* http = evhttp_new(base);
if (!http)
{
printf("create evhttp failed!\n");
return 1;
}
if (evhttp_bind_socket(http, "0.0.0.0", port) != 0)
{
printf("bind socket failed! port:%d\n", port);
return 1;
}
int http_option_timeout = 120; //in seconds
evhttp_set_timeout(http, http_option_timeout);
//evhttp_set_allowed_methods( http , EVHTTP_REQ_GET);
//指定generic callback
evhttp_set_gencb(http, HttpGenericCallback, NULL);
//Set a callback for a specified URI
evhttp_set_cb(http, "/spec", specific_handler, NULL);
event_base_dispatch(base);
evhttp_free(http);
return 0;
}
多线程的Http Server
两种方式:
1、启多个线程,每个子线程持有一个event_base和evhttp,接受请求和处理业务逻辑在同一个子线程中。
2、主线程持有一个event_base和evhttp,主线程接受到请求放入队列,子线程中只处理业务逻辑。
在上面的Http Server中,处理Http请求的回调函数generic_handler和定时器读取文件的回调函数read_file_timer_cb都在同一个event_base的dispatch中,并且都在同一个进程中,使用多线程可以改善程序的性能,下面是一个来自网络的多线程Http Server:
#include
#include
#include
#include
#include
#include
#include
#include
#include
int httpserver_bindsocket(int port, int backlog);
int httpserver_start(int port, int nthreads, int backlog);
void* httpserver_Dispatch(void *arg);
void httpserver_GenericHandler(struct evhttp_request *req, void *arg);
void httpserver_ProcessRequest(struct evhttp_request *req);
int httpserver_bindsocket(int port, int backlog) {
int r;
int nfd;
nfd = socket(AF_INET, SOCK_STREAM, 0);
if (nfd < 0) return -1;
int one = 1;
r = setsockopt(nfd, SOL_SOCKET, SO_REUSEADDR, (char *)&one, sizeof(int));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
r = bind(nfd, (struct sockaddr*)&addr, sizeof(addr));
if (r < 0) return -1;
r = listen(nfd, backlog);
if (r < 0) return -1;
int flags;
if ((flags = fcntl(nfd, F_GETFL, 0)) < 0
|| fcntl(nfd, F_SETFL, flags | O_NONBLOCK) < 0)
return -1;
return nfd;
}
int httpserver_start(int port, int nthreads, int backlog) {
int r, i;
int nfd = httpserver_bindsocket(port, backlog);
if (nfd < 0) return -1;
pthread_t ths[nthreads];
for (i = 0; i < nthreads; i++) {
struct event_base *base = event_init();
if (base == NULL) return -1;
struct evhttp *httpd = evhttp_new(base);
if (httpd == NULL) return -1;
r = evhttp_accept_socket(httpd, nfd);
if (r != 0) return -1;
evhttp_set_gencb(httpd, httpserver_GenericHandler, NULL);
r = pthread_create(&ths[i], NULL, httpserver_Dispatch, base);
if (r != 0) return -1;
}
for (i = 0; i < nthreads; i++) {
pthread_join(ths[i], NULL);
}
}
void* httpserver_Dispatch(void *arg) {
event_base_dispatch((struct event_base*)arg);
return NULL;
}
void httpserver_GenericHandler(struct evhttp_request *req, void *arg) {
httpserver_ProcessRequest(req);
}
void httpserver_ProcessRequest(struct evhttp_request *req) {
struct evbuffer *buf = evbuffer_new();
if (buf == NULL) return;
//here comes the magic
}
int main(void) {
httpserver_start(80, 10, 10240);
}
3)关键函数
获取客户端请求的URI
使用req->uri或使用函数const char *evhttp_request_uri(struct evhttp_request *req);//即(evhttp_request_uri(req);)。
对获取的URI进行解析和其他操作
使用函数void evhttp_parse_query(const char *uri, struct evkeyvalq *args);
可对uri的参数进行解析,结果保存在struct evkeyvalq的key-value pairs中,例如:
char *uri = "http://foo.com/?id=1&infor=hello";
struct evkeyvalq args;
evhttp_parse_query(uri, &args);
//然后通过evhttp_find_header等函数获取各个参数及对应的值
evhttp_find_header(&args, "id"); //得到test
evhttp_find_header(&args, "infor"); //得到some thing
如下两个函数对URI进行encode和decode:
char *evhttp_encode_uri(const char *uri);
char *evhttp_decode_uri(const char *uri);
URI encode的结果是所有非alphanumeric及-_的字符都被类似于%和一个2位16进制字符替换(其中空格被+号替换)。如上两个函数返回的字符串需要free掉。
Escape特殊的HTML字符
char *evhttp_htmlescape(const char *html);
特殊字符:&被替换为&;”被替换为";’被替换为'; <被替换为<;>被替换为>。该函数返回的字符串需要free掉。
处理HTTP headers相关的函数
HTTP headers保存在req的input_headers中,这个是struct evkeyvalq 的结构体(key-value pairs),使用如下函数可对其进行修改:
const char *evhttp_find_header(const struct evkeyvalq *, const char *);
int evhttp_remove_header(struct evkeyvalq *, const char *);
int evhttp_add_header(struct evkeyvalq *, const char *, const char *);
void evhttp_clear_headers(struct evkeyvalq *);
设定只识别get请求,
evhttp_set_allowed_methods( httpd , EVHTTP_REQ_GET);
设置后,只处理get请求,其他请求返回405 Method not allowed,我自己使用的时候发现返回的是501 Not Implemented
请求类型如下:
enum evhttp_cmd_type {
EVHTTP_REQ_GET = 1 << 0,
EVHTTP_REQ_POST = 1 << 1,
EVHTTP_REQ_HEAD = 1 << 2,
EVHTTP_REQ_PUT = 1 << 3,
EVHTTP_REQ_DELETE = 1 << 4,
EVHTTP_REQ_OPTIONS = 1 << 5,
EVHTTP_REQ_TRACE = 1 << 6,
EVHTTP_REQ_CONNECT = 1 << 7,
EVHTTP_REQ_PATCH = 1 << 8
};
#define BUF_MAX 2048 //最大BUF数据长度
//解析post请求数据
void get_post_message(char *buf, struct evhttp_request *req)
{
size_t post_size = 0;
post_size = evbuffer_get_length(req->input_buffer);//获取数据长度
printf("====line:%d,post len:%d\n", __LINE__, post_size);
if (post_size <= 0)
{
printf("====line:%d,post msg is empty!\n", __LINE__);
return;
}
else
{
size_t copy_len = post_size > BUF_MAX ? BUF_MAX : post_size;
printf("====line:%d,post len:%d, copy_len:%d\n", __LINE__, post_size, copy_len);
memcpy(buf, evbuffer_pullup(req->input_buffer, -1), copy_len);
buf[post_size] = '\0';
printf("====line:%d,post msg:%s\n", __LINE__, buf);
}
}
//处理post请求
void http_handler_testpost_msg(struct evhttp_request *req, void *arg)
{
if (req == NULL)
{
printf("====line:%d,%s\n", __LINE__, "input param req is null.\n");
return;
}
char buf[BUF_MAX] = { 0 };
get_post_message(buf, req);//获取请求数据,一般是json格式的数据
if (buf == NULL)
{
printf("====line:%d,%s\n", __LINE__, "get_post_message return null.\n");
return;
}
else
{
//可以使用json库解析需要的数据
printf("====line:%d,request data:%s", __LINE__, buf);
}
//回响应
struct evbuffer *retbuff = NULL;
retbuff = evbuffer_new();
if (retbuff == NULL)
{
printf("====line:%d,%s\n", __LINE__, "retbuff is null.\n");
return;
}
evbuffer_add_printf(retbuff, "Receive post request,Thamks for the request!\n");
evhttp_send_reply(req, HTTP_OK, "Client", retbuff);
evbuffer_free(retbuff);
}
//解析http头,主要用于get请求时解析uri和请求参数
char *find_http_header(struct evhttp_request *req, struct evkeyvalq *params, const char *query_char)
{
if (req == NULL || params == NULL || query_char == NULL)
{
printf("====line:%d,%s\n", __LINE__, "input params is null.\n");
return NULL;
}
struct evhttp_uri *decoded = NULL;
char *query = NULL;
char *query_result = NULL;
const char *path;
const char *uri = evhttp_request_get_uri(req);//获取请求uri
if (uri == NULL)
{
printf("====line:%d,evhttp_request_get_uri return null\n", __LINE__);
return NULL;
}
else
{
printf("====line:%d,Got a GET request for <%s>\n", __LINE__, uri);
}
/*
//解码uri
decoded = evhttp_uri_parse(uri);
if (!decoded)
{
printf("====line:%d,It's not a good URI. Sending BADREQUEST\n", __LINE__);
evhttp_send_error(req, HTTP_BADREQUEST, 0);
return;
}
//获取uri中的path部分
path = evhttp_uri_get_path(decoded);
if (path == NULL)
{
path = "/";
}
else
{
printf("====line:%d,path is:%s\n", __LINE__, path);
}
//获取uri中的参数部分
query = (char*)evhttp_uri_get_query(decoded);
if (query == NULL)
{
printf("====line:%d,evhttp_uri_get_query return null\n", __LINE__);
return NULL;
}*/
//查询指定参数的值
evhttp_parse_query_str(query, params);
query_result = (char*)evhttp_find_header(params, query_char);
return query_result;
}
//处理get请求
void http_handler_testget_msg(struct evhttp_request *req, void *arg)
{
if (req == NULL)
{
printf("====line:%d,%s\n", __LINE__, "input param req is null.\n");
return;
}
char *sign = NULL;
char *data = NULL;
struct evkeyvalq sign_params = { 0 };
sign = find_http_header(req, &sign_params, "sign");//获取get请求uri中的sign参数
if (sign == NULL)
{
printf("====line:%d,%s\n", __LINE__, "request uri no param sign.\n");
}
else
{
printf("====line:%d,get request param: sign=[%s]\n", __LINE__, sign);
}
data = find_http_header(req, &sign_params, "data");//获取get请求uri中的data参数
if (data == NULL)
{
printf("====line:%d,%s\n", __LINE__, "request uri no param data.\n");
}
else
{
printf("====line:%d,get request param: data=[%s]\n", __LINE__, data);
}
printf("\n");
//回响应
struct evbuffer *retbuff = NULL;
retbuff = evbuffer_new();
if (retbuff == NULL)
{
printf("====line:%d,%s\n", __LINE__, "retbuff is null.");
return;
}
evbuffer_add_printf(retbuff, "Receive get request,Thamks for the request!");
evhttp_send_reply(req, HTTP_OK, "Client", retbuff);
evbuffer_free(retbuff);
}
注意:
1)使用event_base_loopbreak或event_base_loopexit无法让event_base_dispatch退出循环。解决方法:
要让libevent使用多线程需要在创建event_base之前调用evthread_use_pthreads()。在windows平台下,使用evthread_use_windows_threads。
2)evhttp_set_cb和evhttp_set_gencb区别
设置事件处理函数,evhttp_set_cb针对每一个事件(请求)注册一个处理函数,evhttp_set_gencb函数,是对所有请求设置一个统一的处理函数。
evhttp_set_cb(http_server, "/post", http_handler_testpost_msg, NULL);
evhttp_set_cb(http_server, "/get", http_handler_testget_msg, NULL);
3)“对不完全的类型‘struct evhttp_request’的非法使用”错误解决方法:增加#include "event2/http_struct.h"
#include "event2/http.h"
#include "event2/http_struct.h"
#include "event2/event.h"
#include "event2/buffer.h"
#include "event2/dns.h"
#include "event2/thread.h"
4)在windows中vs编写代码时,引用windows.h、winsock2.h、evhttp.h会出现sockaddr类型重定义问题。解决方法:
#define _WINSOCKAPI_
#include
#include
//这样写的作用实际上就是, 告诉windows.h不要包含winsock.h文件了
5)使用libevent处理http请求时,带中文的url乱码。
解决方法:使用evhttp_decode_uri函数对uri路径进行解码,解码出来的是utf-8编码的路径,如果需要将utf-8编码转换成gbk编码。
6)evhttp接受POST数据,网上流传的方法是:char *post_data = (char *) EVBUFFER_DATA(req->input_buffer);
这是一个坑,这里因为evhttp的处理机制问题,他读取的是evhttp预留的整个内存空间的内容,这样就会造成你获取到正确内容后面跟着一堆垃圾内容。普通处理还好,遇到加密解密就容易出错。
获取POST数据正确方法:
int buffer_data_len = EVBUFFER_LENGTH(req->input_buffer);
char *post_data = (char *) malloc(buffer_data_len + 1);
memset(post_data, 0, buffer_data_len + 1);
memcpy(post_data, EVBUFFER_DATA(req->input_buffer), buffer_data_len);
//处理业务
free(post_data);
7)get数据、post数据解析。
//解析URI的参数(即GET方法的参数)
char *decode_uri = strdup((char*) evhttp_request_uri(req));
struct evkeyvalq http_query_get;
evhttp_parse_query(decode_uri, &http_query_get);
free(decode_uri);
const char *http_get_name = evhttp_find_header(&http_query_get, "name");
//解析URI的参数(即POST方法的参数)
//获取POST数据(如果数据是json格式),直接用jsoncpp解析。
//获取POST数据(如果数据是name=value格式),使用vhttp_parse_query解析。
struct evkeyvalq http_query_post;
int buffer_data_len = EVBUFFER_LENGTH(req->input_buffer);
char *decode_post_uri = (char *) malloc(buffer_data_len + 3);
memset(decode_post_uri, 0, buffer_data_len + 3);
char *post_data = decode_post_uri + 2;
memcpy(post_data, EVBUFFER_DATA(req->input_buffer), buffer_data_len);
//拼凑成get样式
decode_post_uri[0] = '/';
decode_post_uri[1] = '?';
////////////////////////////////////////
evhttp_parse_query(decode_post_uri, &http_query_post);
const char *http_post_name = evhttp_find_header(&http_query_post, "name");
8)创建evhttp_connection对象,并设置回调函数,这里的回调函数是和连接状态相关的函数。
struct evhttp_connection *evhttp_connection_base_new(struct event_base *base,
struct evdns_base *dnsbase, const char *address, unsigned short port);
void evhttp_connection_set_closecb(struct evhttp_connection *evcon,
void (*)(struct evhttp_connection *, void *), void *);
9)、使用不当导致内存泄露
evhttp当客户端请求到来时, 服务器端调用了evhttp_send_reply_start();表示开始向客户端推送数据(使用 HTTP chunked), 最后当 long-polling 结束时, 应该调用evhttp_send_reply_end();来关闭连接, 释放 libevent 的内存。
不过, 如果客户端提前终止了请求, 会导致什么呢? 会导致连接关闭的回调函数 evhttp_connection_set_closecb() 被调用, 但是, 在这个回调函数里, 我没有调用 evhttp_send_reply_end(), 所以导致了内存泄露。
void On_Close(struct evhttp_connection * conn,void * args)
{
evhttp_send_reply_end((struct evhttp_request *)args);
}
void HttpGenericCallback(struct evhttp_request* req, void* arg)
{
struct bufferevent * bev = evhttp_connection_get_bufferevent(req->evcon);
bufferevent_enable(bev,EV_READ);
evhttp_connection_set_closecb(req->evcon,On_Close,req);
//...
}
非上面原因导致内存泄露解决方法:调用malloc_trim(0); 回收资源。
二、libevent编译
1、Windows下编译libevent及使用
1)下载地址:http://libevent.org/ ,下载版本:libevent-2.0.22-stable.tar.gz
解压, 目录为...\libevent-2.0.22-stable(自己的目录)
2)修改以下三个文件,添加宏定义:
在以下3个文件开头添加“#define _WIN32_WINNT 0x0500”
libevent-2.0.21-stable\event_iocp.c
libevent-2.0.21-stable\evthread_win32.c
libevent-2.0.21-stable\listener.c
3)如果出现libevent-2.0.22-stable\minheap-internal.h(76) : error C2065: “UINT32_MAX”: 未声明的标识符
在该文件中添加#include “stdint.h”
4)在Makefile.nmake中的CFLAGS中加入/ZI选项,同时去掉/Ox优化选项,这样生成的lib库会带有调试信息。
打开VS2015命令工具,切换到解压后的libevent目录,然后执行nmake /f Makefile.nmake命令进行编译。
先输入cd/d D:\aa_zhj\a_work\projects\libevent\libevent-2.0.22-stable切换目录,然后输入nmake /f Makefile.nmake进行编译。
libevent默认编译为32位lib,若需要编译为64位,则需修改Makefile.nmake文件,在LIBFLAGS处添加MACHINE:X64(LIBFLAGS=/nologo/MACHINE:X64 )。
编译成功后再libevent-2.0.22-stable目录下生成三个lib文件:
libevent.lib、libevent_core.lib、libevent_extras.lib
VS2015下使用lib
环境配置:
项目下建一个Lib目录,将上面三个lib文件copy到该目录下。
新建一个Include目录,将libevent-2.0.21-stable\include文件夹下的所有内容和WIN32-Code文件夹下的所有内容拷贝到新建的include目录下,两个event2目录下的文件可合并在一起。
项目属性设置
包含目录,添加上面的Include目录;
库目录,添加上面的Lib目录;
连接器输入:ws2_32.lib;wsock32.lib;libevent.lib;libevent_core.lib;libevent_extras.lib;
//初始化winsocket
#ifdef WIN32
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2) , &wsaData) != 0)
{
return -1;
}
#endif
//和释放
#ifdef WIN32
WSACleanup();
#endif
2、linux下编译libevent
https://blog.csdn.net/littlebeat123/article/details/74010877
参考文章:
https://blog.csdn.net/xuleisdjn/article/details/78500055