Bootstrap

网络编程之http服务器(C/C++)(基于libevent实现)

目录

目录

前言

一、安装libevent

二、创建http服务器服务示例

1、evhttp_server.h

2、evhttp_server.c

3、evhttp_server_demo.c

4、编译运行,手动单独编译:


前言

libevent 是一个轻量级的开源高性能事件通知库,支持多种 I/O 多路复用技术,内部使用select、epoll、kqueue、IOCP等系统调用管理事件机制。支持 I/O,定时器和信号等事件,支持注册事件优先级。可用来构建高并发的http服务端、tcp服务...

在此,基于libevent实现一个http服务端服务,并给出一个处理客户端GET、POST、PUT、DELETE请求的示例(至于接收表单多文件上传的接口,后续有空再实现)


一、安装libevent

ubuntu本机环境自动下载安装:apt-get install libevent-dev

如需为linux系统环境交叉编译,参照下面流程:

#下载:
get https://github.com/libevent/libevent/releases/download/release-2.1.12-stable/libevent-2.1.12-stable.tar.gz
tar -zxf libevent-2.1.12-stable.tar.gz
cd libevent-2.1.12-stable/
mkdir my_build

#编译安装到指定目录,需指定编译工具链路径(CC、)和安装路径(--prefix)
#如果不考虑openssl,所以选择参数--disable-openssl
./configure --prefix=$(pwd)/my_build \
--host=arm-linux-gnueabihf \
CC=/.../bin/arm-linux-gnueabihf-gcc \
--disable-openssl


#如需要openssl依赖,需指定openssl路径, 命令改为:
./configure --host=arm-linux-gnueabihf \
CC=/.../bin/arm-linux-gnueabihf-gcc \
CPPFLAGS="-I/home/zhouyihui/openssl-1.1.1a/my_build/include" \
LDFLAGS="-L/home/zhouyihui/openssl-1.1.1a/my_build/lib -lssl -lcrypto"

#编译安装
make -j4
make install

二、创建http服务器服务示例

在此,封装了一个http服务器接口,可直接使用

1、evhttp_server.h

/********************************
func:基于libevent封装的http服务器
author:zyh
date:2023.09.15
*********************************/
#ifndef _EV_HTTP_SERVER_H_
#define _EV_HTTP_SERVER_H_

#ifdef __cplusplus
extern "C" {
#endif

#include <pthread.h>
//#include <event.h>
//#include <evhttp.h>
#include "event.h"
#include "evhttp.h"

/*------------------http服务器接口-----------------------*/
typedef struct {
	const char *host;//地址,"0.0.0.0" or "127.0.0.1"  (127.0.0.1服务端只能在本机上内部通信,无法接受来自其他机器的连接请求,不受外部影响)
	int port;//端口
	int timeout_sec;//设置超时时间(s),默认60
	int max_work_pthread;//服务端处理客户端请求的最大子线程数,大于0
	
	void (*cb)(struct evhttp_request *, void *);//接收处理消息的回调函数
	void *arg;//预留给外部的自定义传参,可传到回调函数cb里面
	
	/* *内部参数,外部中途不要修改 **/
	pthread_t thread_id;
	int thread_run_flag;//内部线程状态,0未运行,1运行
	struct event_base **bases;
} evhttp_server_t;
//启动一个http服务器,同一进程可启动多个不同端口的http服务,返回:0成功,-1失败
int start_evhttp_server(evhttp_server_t *server);
int close_evhttp_server(evhttp_server_t *server);
int restart_evhttp_server(evhttp_server_t *server);
void http_cb_demo(struct evhttp_request *req, void *arg);//一个cb回调函数示例

#ifdef __cplusplus
}
#endif

#endif

2、evhttp_server.c

/********************************
func:基于libevent封装的接口服务示例
author:zyh
date:2023.09.15
*********************************/
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>
#include <stdbool.h>  
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <pthread.h>
#include <arpa/inet.h>//socket 
//for http
#include "event.h"
#include "evhttp.h"
#include "event2/thread.h"
//
#include "evhttp_server.h"

/***-------------------
event_base_loopexit():
给定计时器到期后的下一次event_base_loop()迭代将正常完成(处理所有排队的事件),然后退出。
如果时间为NULL,则运行所有当前活动事件后退出


event_base_loopbreak():
让event_base立即退出循环。它与event_base_loopexit(base, NULL)的不同在于, 
如果event_base当前正在执行某个激活事件,它将在执行完当前正在处理的事件后立即退出。
***/


//响应客户端下载文件的接口
int process_download_file(struct evhttp_request *req, char *filePath)
{
	int ret = -1;
	struct stat st;
	FILE *fp = NULL;
	struct evbuffer *out_buf = NULL;
	
	//添加返回的内容类型,二进制文件
	//evhttp_add_header(req->output_headers, "Content-Type", "application/octet-stream");

	//输出缓冲区
	out_buf = evbuffer_new();
	if (!out_buf) {
		evhttp_send_error(req, HTTP_INTERNAL, "Could not allocate buffer");
		goto done;
	}

	if (stat(filePath, &st) < 0) {
		fprintf(stderr, "<%s:%d>, stat %s failed\n", __FILE__, __LINE__, filePath);
		evhttp_send_error(req, HTTP_NOTFOUND, "file is not exist");
		goto done;
	}
	
	if (S_ISDIR(st.st_mode)) {//如果是文件夹,则出错
		fprintf(stderr, "<%s:%d>, %s is directory\n", __FILE__, __LINE__, filePath);
		evhttp_send_error(req, HTTP_NOTFOUND, "file is directory");
		goto done;
    }
	
	fp = fopen(filePath, "rb");
	if (!fp) {
		fprintf(stderr, "<%s:%d>, Could not open %s\n", __FILE__, __LINE__, filePath);
		evhttp_send_error(req, HTTP_NOTFOUND, "file is not exist");
		goto done;
	}
	
	evbuffer_add_file(out_buf, fileno(fp), 0, st.st_size);//添加文件至evbuffer
	evhttp_send_reply(req, HTTP_OK, "OK", out_buf);
	
	ret = 0;
done:
	if (fp) {
		fclose(fp);
	}
	if (out_buf) {
		evbuffer_free(out_buf);
	}

	return ret;
}



//响应客户端通过表单方式上传多个文件的接口
int process_upload_files(struct evhttp_request *req, const char *upload_dir)
{
	return 0;
}



//http后端数据处理模块
void http_cb_demo(struct evhttp_request *req, void *arg) 
{
	const char *uri = evhttp_request_get_uri(req);//http的get和delete请求不能带body,只能通过uri带参
	int method = evhttp_request_get_command(req);
	const char *content_type = evhttp_find_header(req->input_headers, (const char*)"Content-Type");
	//int length = evhttp_find_header(req->input_headers, "Content-Length")
	int length = evbuffer_get_length(req->input_buffer);
	evbuffer_add(req->input_buffer, "\0", 1);//需要添加这个,否者body会出现之前其它请求的内容
	char *body = (char*)EVBUFFER_DATA(req->input_buffer);
	//char *body = (char*)evbuffer_pullup(req->input_buffer, length);

	printf("\nuri=%s\n", uri);
	if (EVHTTP_REQ_GET == method) {
		printf("method=GET\n");
	} else if (EVHTTP_REQ_POST == method) {
		printf("method=POST\n");
	} else if (EVHTTP_REQ_PUT == method) {
		printf("method=PUT\n");
	} else if (EVHTTP_REQ_DELETE == method) {
		printf("method=DELETE\n");
	}
	printf("content_type=%s\n", content_type);
	printf("length=%d\n", length);
	printf("body=%s\n", body);

	if (NULL == uri) {
		evhttp_send_error(req, HTTP_BADREQUEST, "Bab Request");
		return;
	}

	/*响应返回内容,输出到客户端*/
	//设置HTTP头信息
	evhttp_add_header(req->output_headers, "Server", "myhttpd/v1.0.0");
	evhttp_add_header(req->output_headers, "Connection", "keep-alive");

	
	//输出缓冲区
	const int reply_buf_size = 1024 * 10;
	char *reply = NULL;
	char filePath[128] = {0};
	struct evbuffer *out_buf = NULL;
	out_buf = evbuffer_new();
	if (!out_buf) {
		evhttp_send_error(req, HTTP_INTERNAL, "Could not allocate buffer");
		goto done;
	}
	reply = (char *)calloc(reply_buf_size, sizeof(char));
	if (!reply) {
		evhttp_send_error(req, HTTP_INTERNAL, "Could not calloc reply buf");
		goto done;
	}

	//处理具体内容	
	if (strstr(uri, "/download/")) {//下载文件接口"/download/.../xxx.x"
		//添加返回的内容类型,二进制文件
		evhttp_add_header(req->output_headers, "Content-Type", "application/octet-stream");
		
		snprintf(filePath, sizeof(filePath), "./%s", uri + strlen("/download/"));
		process_download_file(req, filePath);
	} else if ( (0 == strcmp(uri, "/upload")) && strstr(content_type, "application/json")) {
		process_upload_files(req, "./upload");
	} else {
		//添加返回的内容类型
		//evhttp_add_header(req->output_headers, "Content-Type", "application/json");
		evhttp_add_header(req->output_headers, "Content-Type", "application/json;charset=UTF-8");
		
		//判断一下数据类型是否正确
		if ( (NULL == content_type) || (NULL == strstr(content_type, "application/json")) ) {
			snprintf(reply, reply_buf_size, "%s", "{\"code\":400,\"message\":\"Content-Type is not application/json\"}");
		} else {
			//处理一个个请求
			if (0 == strcmp(uri, "/test")) {
				snprintf(reply, reply_buf_size, "%s", "{\"code\":200,\"message\":\"ok\"}");
			} else if (0 == strcmp(uri, "/test1")) {
				snprintf(reply, reply_buf_size, "%s", "{\"code\":200,\"message\":\"ok\"}");
			} else {
				snprintf(reply, reply_buf_size, "%s", "{\"code\":400,\"message\":\"Bad Request, has no uri\"}");
			}
		}
		
		printf("reply=%s\n", reply);

		//添加数据到缓冲区
		//evbuffer_add_printf(buf, "%s", reply);
		evbuffer_add(out_buf, reply, strlen(reply));//如果reply不是字符串,而是二进制流,请输入实际长度
		//发送响应
		evhttp_send_reply(req, HTTP_OK, "OK", out_buf);
	}

done:	
  	/* Release the memory  */
	if (out_buf) {
		evbuffer_free(out_buf);
	}
	if (reply) {
		free(reply);
		reply = NULL;
	}
}

//单线程方式,直接使用libevent的http模块,此模块是为单线程设计*/
#if 0//不安全,已被其它函数和方式取代
int main(void)
{  
	evthread_use_pthreads();//必须,否则调用event_base_loopexit或者event_base_loopbreak终止不了循环,导致最终释放不了资源
	
	//初始化event API
	event_init();//创建的是一个全局的事件集合,event_init( )函数实现是由event_base_new( )封装而成的

	//创建一个http server
	struct evhttp *httpd;
	httpd = evhttp_start("0.0.0.0", 1088);
	evhttp_set_timeout(httpd, 64);

	//指定generic callback
	//evhttp_set_gencb(httpd, httpd_handler, NULL);
	//也可以为特定的URI指定callback
	//evhttp_set_cb(httpd, "/", httpd_handler, NULL);
	evhttp_set_cb(httpd, "/test", httpd_handler, NULL);
	//evhttp_set_cb(httpd, "/test1", httpd_handler1, NULL);
	
	printf("http server start OK!\n");
	//循环处理events
	event_dispatch();// event_loopbreak()可终止侦听event_dispatch()的事件侦听循环; 
	
	//
	evhttp_free(httpd);
	printf("http server quit!\n");
	
	return 0;
}
#endif


//单线程方式,直接使用libevent的http模块,此模块是为单线程设计*/
#if 0
//安全的方式
int main(int argc, char *argv[])
{  
	evthread_use_pthreads();//必须,否则调用event_base_loopexit或者event_base_loopbreak终止不了循环
	
	struct event_base *base;
	//初始化event_init
	base = event_base_new();//创建的是一个局部的事件集合
	
	/* 创建一个http server,使用libevent的http模块,此模块是为单线程设计*/
	struct evhttp *ev_http;
	ev_http = evhttp_new(base);
	evhttp_bind_socket(ev_http, "0.0.0.0", 1088);
	evhttp_set_timeout(ev_http, 60);//设置请求超时时间(s)

	//指定generic callback
   	evhttp_set_gencb(ev_http, httpd_handler, NULL);
	//evhttp_set_cb(ev_http, "/test", httpd_handler, NULL);//也可以为特定的URI指定callback

	printf("http server start OK!\n");
   	//循环处理events
	event_base_dispatch(base);//外部调用event_base_loopbreak() /event_base_loopexit()可终止循环; 

	//释放资源
	if (ev_http) {
		evhttp_free(ev_http);
	}
	if (base) {
		event_base_free(base);
	}
	printf("http server quit!\n");

}
#endif

/**
多线程方式,需要自行设计线程池以便提高吞吐量,每个工作线程中都要运行一个event_base_loop和一个evhttp实例
(这些evhttp实例需要用evhttp_bind_socket绑定到相同的端口上),具体参考官方的sample,
https://github.com/libevent/libevent
**/
static int http_init(const char *host, int port) 
{
	int nfd = socket(AF_INET, SOCK_STREAM, 0);
	if (nfd < 0) {
		return -1;
	}
 
	const int one = 1;
	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_port = htons(port);
	if (0 == strcmp(host, "127.0.0.1")) {//服务端程序只能在本机上测试,无法接受来自其他机器的连接请求
		addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	} else {//服务端程序可以接受任何机器的连接请求,即绑定到此机器的所有网卡上
		addr.sin_addr.s_addr = INADDR_ANY;
	}
	
	//printf("nfd=%d\n", nfd);
	if (bind(nfd, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
		close(nfd);
		return -1;
	}
 
	if (listen(nfd, 128) < 0) {
		close(nfd);
		return -1;
	}
 
	int flags = fcntl(nfd, F_GETFL, 0);//设置socket为非阻塞
	if (0 > flags) {
		close(nfd);
		return -1;
	}
	if (0 > fcntl(nfd, F_SETFL, flags | O_NONBLOCK)) {
		close(nfd);
		return -1;
	}

	return nfd;
}

static void *worker_func(void *arg) 
{
//	printf("http server start pthread [%ld]\n", pthread_self());
	event_base_dispatch((struct event_base *) arg);
//	printf("http server exit pthread [%ld]\n", pthread_self());
	return NULL;
}

static void *evhttp_server_loop(void *arg)
{
	evhttp_server_t *server = (evhttp_server_t *)arg;

	const char *httpd_listen_host = server->host;
	int port = server->port;
   	int i = 0;
	int socket_fd = -1;
	
	int max_work_pthread = server->max_work_pthread;
	server->bases = (struct event_base **)calloc(max_work_pthread, sizeof(struct event_base *));
	//struct evhttp *httpds[max_work_pthread];
	//pthread_t thread_list[max_work_pthread];
	struct evhttp **httpds = (struct evhttp **)calloc(max_work_pthread, sizeof(struct evhttp *));
	pthread_t *thread_list = (pthread_t *)calloc(max_work_pthread, sizeof(pthread_t));

	evthread_use_pthreads();//必须,使用多线程,需要先初始化线程支持, 否则调用event_base_loopexit或者event_base_loopbreak终止不了循环
	while (server->thread_run_flag) {
		//socket
		socket_fd = http_init(httpd_listen_host, port);
		if (socket_fd < 0) {
			fprintf(stderr, "<%s:%d>, http_init fail, %d端口已被占用,重新创建...\n", __func__, __LINE__, port);
			sleep(2);
			continue;
		}

		//http
		for (i = 0; i < max_work_pthread; i++) {
			server->bases[i] = event_base_new();
			httpds[i] = evhttp_new(server->bases[i]);
			evhttp_accept_socket(httpds[i], socket_fd);
			//
			if (0 >= server->timeout_sec) {
				evhttp_set_timeout(httpds[i], 60);//设置超时时间(s)
			} else {
				evhttp_set_timeout(httpds[i], server->timeout_sec);//设置超时时间(s),一般60
			}
			//evhttp_set_gencb(httpds[i], httpd_handler, NULL);
			evhttp_set_gencb(httpds[i], server->cb, server->arg);
			pthread_create(&thread_list[i], NULL, worker_func, (void *)server->bases[i]);
		}
		
		usleep(100*1000);
		printf("<%s:%d>, http server start OK! (ip:%s port:%d)\n", __func__, __LINE__, httpd_listen_host, port);
		for (i = 0; i < max_work_pthread; i++) {//堵塞等待,直到服务关闭
			pthread_join(thread_list[i], NULL);
		}
		
		//关闭释放
		if (socket_fd > 0) {
			close(socket_fd);
		}
		for (i = 0; i < max_work_pthread; i++) {
			if (httpds[i]) {
				evhttp_free(httpds[i]);//有时会出现堵塞,测试发现必须先在event_base_free之前调用才不会堵塞
				httpds[i] = NULL;
			}
			if (server->bases[i]) {
				event_base_free(server->bases[i]);
				server->bases[i] = NULL;
			}
		}
		free(server->bases);
		server->bases = NULL;
		free(httpds);
		httpds = NULL;
		free(thread_list);
		thread_list = NULL;
		printf("<%s:%d>, http server exit success!\n", __func__, __LINE__);
		break;
	}
	
	return NULL;
}

//启动一个http服务器,同一进程可启动多个不同端口的服务
//返回:0成功,-1失败
int start_evhttp_server(evhttp_server_t *server)
{
	int ret = 0;
	ret = pthread_create(&server->thread_id, NULL, evhttp_server_loop, server);
	if (ret < 0) {
		fprintf(stderr, "<%s:%d>, pthread_create evhttp_server_loop is failed\n", __func__, __LINE__);
		return -1;
	}
	server->thread_run_flag = 1;
	return 0;
}
int close_evhttp_server(evhttp_server_t *server)
{
	if (1 == server->thread_run_flag) {
		server->thread_run_flag = 0;
		for (int i = 0; i < server->max_work_pthread; i++) {
			event_base_loopbreak(server->bases[i]);//先断开事件循环
		}
		pthread_join(server->thread_id, NULL);
	}
	sleep(1);//延时一下
	return 0;
}
int restart_evhttp_server(evhttp_server_t *server)
{
	close_evhttp_server(server);
	return start_evhttp_server(server);
}

3、evhttp_server_demo.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>
#include <string.h>
#include "libevent_api.h"

//创建http服务器
int main(int argc, char *argv[])
{
	evhttp_server_t server1 = {
		.host = "0.0.0.0",
		.port = 1080,
		.timeout_sec = 60,
		.max_work_pthread = 4,
		.cb = http_cb_demo
	};
	start_evhttp_server(&server1);

	sleep(10*60);

	printf("关闭\n");
	close_evhttp_server(&server1);
	return 0;
}

4、编译运行,手动单独编译:

创建并复制代码文件,手动 gcc/g++ 编译

g++ -o http_server_test evhttp_server_demo.c evhttp_server.c evhttp_server.h -lpthread -levent -levent_pthreads -Wall

 

;