项目场景:
背景:在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服务挂了!!!!!”。当时我很惊讶,因为原因有以下几个:
- 服务并没有down掉;
- 服务后续的接口任然能够处理并返回;
- 主流程看似没有任何影响,日志也在照常输出;
/*我的主流程代码,部分省略;开发环境是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,¶ms);
QString m_strPath = path;
if(m_strPath != "/getaddress")
{
qDebug()<<"wrong path\n";
return ;
}
char * address = pService->find_http_header(req,¶ms,"address");//获取get请求uri中的address参数
char * type = pService->find_http_header(req,¶ms,"type");//获取get请求uri中的type参数
char * open = pService->find_http_header(req,¶ms,"open");//获取get请求uri中的open参数
char * dtu = pService->find_http_header(req,¶ms,"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没有,下面一探究竟:
- 由于我的接口,使用端口为1888,过滤抓包文件:
- 初步看数据,发现数据有两种,tcp的SYN/ACK等握手数据(这个不管),以及应用层数据(在wireshark中显示为[PSH,ACK]):
- 将对应时间端的数据解码成,http协议(decode as http),发现前端几乎在同一时间调用了4次接口,然而确实服务器没有返回,但是根据日志,接口异常应该在sendreplay阶段;
- 这让我有理由相信,是底层框架的问题。结合以前使用Libevent开发tcp并发服务器的经验,相信只有一个问题,我这版本的做法,并不能处理并发请求!
升级改造:
第二版做法,改成多线程并发处理:
libevent是基于事件循环的;用多线程开发libevent的关键在于,开辟多线程,每个线程加装一个base对象,并进入loop循环:
- 于是乎,考虑到本身应用并不复杂,若干数量的并发就可以满足要求,故简单改造如下代码:
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服务端,那么所有的处理都变得理所应当。