2022-3-11 基于libevent/evhttp模块的简易http服务器归纳小结-与前端同事的互侃

项目场景:

背景:在C/C+开发后端服务时,需要开发一个处理http请求的服务器模块。
要求:由于http服务是一个运行在tcp之上的传输协议,并且此次服务器的性能要求并不是很高,并发数在20左右;

基于以上考虑,在项目编写代码之初,我有以下几种想法:一是编写原生socket再做协议的解析(这个无疑是重复造轮子,工作量大);二是利用开源框架快速集成。基于以往的开发经验,我想到了使用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等等。


问题描述

第一版做法,也是网上大多数网友的做法:

这一版本,在实际使用过程中使用时,前端反馈,在接口调用过程中,经常会出现“http接口调用failed”的情况,前端的原话是“你的http服务挂了!!!!!”。当时我很惊讶,因为原因有以下几个:

  1. 服务并没有down掉;
  2. 服务后续的接口任然能够处理并返回;
  3. 主流程看似没有任何影响,日志也在照常输出;
/*我的主流程代码,部分省略;开发环境是qt*/
class cmyhttpservice : public QThread
{
	public:
		...
	private:
		
		const char *find_http_path(struct evhttp_request *req,struct evkeyvalq *params);
		char *find_http_header(struct evhttp_request *req,struct evkeyvalq *params,const char *query_char);
		...
		QString m_strSerIP;
    	int     m_nPort;
	protected:
    	void run() override;
}

const char *cmyhttpservice::find_http_path(struct evhttp_request *req,struct evkeyvalq *params)
{
    if(req == nullptr || params == nullptr)
    {
        return nullptr;
    }

    struct evhttp_uri *decoded = nullptr;
    const char *path = nullptr;
    const char *uri = evhttp_request_get_uri(req);
    if(uri == nullptr)
    {
        qDebug()<<"evhttp_request_get_uri return nullptr\n";
        return nullptr;
    }
    else
    {
        //qDebug()<<"Got a GET request for:"<<uri;
    }
    decoded = evhttp_uri_parse(uri);
    if (!decoded)
    {
        evhttp_send_error(req, HTTP_BADREQUEST, 0);
        return nullptr;
    }

    //获取uri中的path部分
    path = evhttp_uri_get_path(decoded);
    if (path == nullptr)
    {
        path = "/";
    }
    else
    {
        return path;
    }
    return nullptr;
}



char *cmyhttpservice::find_http_header(struct evhttp_request *req,struct evkeyvalq *params,const char *query_char)
{
    if(req == nullptr || params == nullptr || query_char == nullptr)
    {
        qDebug()<<"input params is nullptr.\n";
        return nullptr;
    }

    struct evhttp_uri *decoded = nullptr;
    char *query = nullptr;
    char *query_result = nullptr;
    const char *path;
    const char *uri = evhttp_request_get_uri(req);

    if(uri == nullptr)
    {
        return nullptr;
    }
    else
    {
        //qDebug()<<"Got a GET request for:"<<uri;
    }

    //解码uri
    decoded = evhttp_uri_parse(uri);
    if (!decoded)
    {
        evhttp_send_error(req, HTTP_BADREQUEST, 0);
        return nullptr;
    }

    //获取uri中的path部分
    path = evhttp_uri_get_path(decoded);
    if (path == nullptr)
    {
        path = "/";
    }
    else
    {
        //qDebug()<<"path is:"<<path<<"\n";
    }
    //获取uri中的参数部分
    query = (char*)evhttp_uri_get_query(decoded);
    if(query == nullptr)
    {
        qDebug()<<"evhttp_uri_get_query return nullptr\n";
        return nullptr;
    }
    //查询指定参数的值
    evhttp_parse_query_str(query, params);
    query_result = (char*)evhttp_find_header(params, query_char);
    return query_result;
}

void cmyhttpservice::http_handler_post_address(struct evhttp_request *req,void *arg)
{
    cmyhttpservice* pService = cmyhttpservice::GetInstance();
    QString m_strRet = "null";
    if(req == nullptr)
    {
        qDebug()<<"input params is nullptr.\n";
        return;
    }

    struct evkeyvalq params = {0};
    const char*path = pService->find_http_path(req,&params);
    QString m_strPath = path;
    if(m_strPath != "/getaddress")
    {
         qDebug()<<"wrong path\n";
         return ;
    }
    char * address = pService->find_http_header(req,&params,"address");//获取get请求uri中的address参数
    char * type = pService->find_http_header(req,&params,"type");//获取get请求uri中的type参数
    char * open = pService->find_http_header(req,&params,"open");//获取get请求uri中的open参数
    char * dtu = pService->find_http_header(req,&params,"dtu");//获取get请求uri中的open参数

	/* 获取了接口传入的4个参数,省略其他处理 */
	......

THE_END:
    //回响应
    struct evbuffer *retbuff = nullptr;
    retbuff = evbuffer_new();
    if(retbuff == nullptr)
    {
        qDebug()<<"retbuff is nullptr.\n";
        return;
    }
    qDebug()<<QString("return result:%1\n").arg(m_strRet);
    evhttp_add_header(req->output_headers, "Access-Control-Allow-Origin","*");
    evhttp_add_header(req->output_headers, "Access-Control-Allow-Methods","POST,GET,OPTIONS");
    evhttp_add_header(req->output_headers, "Access-Control-Allow-Credentials","true");
    evhttp_add_header(req->output_headers, "Access-Control-Allow-Headers","authorization,Origin,Content-Type,Accept,token,X-Requested-With");
    evbuffer_add_printf(retbuff,m_strRet.toStdString().c_str());
    evhttp_send_reply(req,HTTP_OK,nullptr,retbuff);
    evbuffer_free(retbuff);

}

void cmyhttpservice::run()
{
	event_init();
    http_server = evhttp_start(m_strSerIP.toUtf8(),m_nPort);
    if(http_server == nullptr)
    {
    	//error
        return ;
    }
    //设置请求超时时间(s)
    evhttp_set_timeout(http_server,10);
    evhttp_set_cb(http_server,"/getaddress",http_handler_post_address,nullptr);
    event_dispatch();
    evhttp_free(http_server);
}

原因分析:

因为经常处理后端开发,想到用抓包的方式分析问题,这也是很常用的方法,一开始想的就是看,到底我的服务给了replay没有,下面一探究竟:

  1. 由于我的接口,使用端口为1888,过滤抓包文件:
    过滤抓包
  2. 初步看数据,发现数据有两种,tcp的SYN/ACK等握手数据(这个不管),以及应用层数据(在wireshark中显示为[PSH,ACK]):
    在这里插入图片描述
  3. 将对应时间端的数据解码成,http协议(decode as http),发现前端几乎在同一时间调用了4次接口,然而确实服务器没有返回,但是根据日志,接口异常应该在sendreplay阶段;
    在这里插入图片描述
  4. 这让我有理由相信,是底层框架的问题。结合以前使用Libevent开发tcp并发服务器的经验,相信只有一个问题,我这版本的做法,并不能处理并发请求!

升级改造:

第二版做法,改成多线程并发处理:

libevent是基于事件循环的;用多线程开发libevent的关键在于,开辟多线程,每个线程加装一个base对象,并进入loop循环:

  1. 于是乎,考虑到本身应用并不复杂,若干数量的并发就可以满足要求,故简单改造如下代码:
void cmyhttpservice::run()
{
	int nfd = 0;
    int ret = 0;
    //省略部分通用代码
    ......
    //创建socket
    ......
    //设置端口可复用setsockopt
    ......
    //绑定本地端口,我代码中是m_nPort
    ......
    //开始listen监听端口
    ......
    //设置端口非阻塞
    ioctl(nfd, FIONBIO, 1);  
	std::vector<std::thread> m_threads;
	for (int i = 0; i < MAX_THREADS; i++)
	{
		struct event_base *base = event_base_new();
		struct evhttp *httpd = evhttp_new(base);
		r = evhttp_accept_socket(httpd, nfd);	//SOCKET 绑定evhttp
		if (r != 0)
		{
			evhttp_free(httpd);
			event_base_free(base);
			continue;
		}
		evhttp_set_timeout(httpd, 10);
		evhttp_set_cb(http_server,"/getaddress",http_handler_post_address,nullptr);
		m_threads.push_back(std::thread(&HttpServer::httpserver_Dispatch, this, base));
	}
	//WLog(LOG_INFO, "http server start at port:%d", httpPort);
 
	for (i = 0; i < threadVec.size(); i++)
	{
		threadVec[i].join();
	}
	return true;
}
//每个线程中执行的函数体,就是进入对应的事件循环;libevent的处理流程,随着这个event_base的loop——exit就会停止,否则会一直处理。
void* cmyhttpservice::httpserver_Dispatch(void *arg)
{
	event_base_dispatch((struct event_base*)arg);
	return NULL;
}

问题小结:

  • 其实问题本身并不复杂,之前我也开发过Libevent的tcp高并发服务器,这一次失误,竟是因为偷懒,没有动脑子。并且代码编写没有经过深思熟虑。
  • 总的来说,把前端调用当成tcp客户端,http服务器当成tcp服务端,那么所有的处理都变得理所应当。
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ShaYQ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值