写点什么

高并发 HTTP 请求实践

用户头像
namelij
关注
发布于: 2021 年 03 月 15 日
高并发HTTP请求实践

当今,正处于互联网高速发展的时代,每个人的生活都离不开互联网,互联网已经影响了每个人生活的方方面面。我们使用淘宝、京东进行购物,使用微信进行沟通,使用美图秀秀进行拍照美化等等。而这些每一步的操作下面,都离不开一个技术概念 HTTP(Hypertext Transfer Protocol,超文本传输协议)。


举个🌰,当我们打开京东 APP 的时候,首先进入的是开屏页面,然后进入首页。在开屏一般是广告,而首页是内容相关,包括秒杀,商品推荐以及各个 tag 页面,而各个 tag 也有其对应的内容。当我们在进入开屏之前或者开屏之后(这块依赖于各个 app 的技术实现),会向后端服务发送一个 http 请求,这个请求会带上该页面广告位信息,向后端要内容,后端根据广告位的配置,挑选一个合适的广告或者推荐商品返回给 APP 端进行展示。在这里,为了描述方便,后端当做一个简单的整体,实际上,后端会有非常复杂的业务调度,比如获取用户画像,广告定向,获取素材,计算坐标,返回 APP,APP 端根据坐标信息,下载素材,然后进行渲染,从而在用户端进行展示,这一切都是秒级甚至毫秒级响应,一个高效的 HTTP Client 在这里就显得尤为重要,本文主要从业务场景来分析,如何实现一个高效的 HTTP Client。


1 概念



当我们需要模拟发送一个 http 请求的时候,往往有两种方式:

1、通过浏览器

2、通过 curl 命令进行发送请求


如果我们在大规模高并发的业务中,如果使用 curl 来进行 http 请求,其效果以及性能是不能满足业务需求的,这就引入了另外一个概念 libcurl。


curl


  • 利用 URL 语法在命令行方式下工作的开源文件传输工具。

它支持很多协议:DICT, FILE, FTP, FTPS, Gopher, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, POP3, POP3S, RTMP, RTSP, SCP, SFTP, SMTP, SMTPS, Telnet and TFTP。


  • 支持 SSL 证书,HTTP POST, HTTP PUT,FTP 上传,基于表单的 HTTP 上传,代理(proxies)、cookies、用户名/密码认证(Basic, Digest, NTLM 等)、下载文件断点续传,上载文件断点续传(file transfer resume),http 代理服务器管道(proxy tunneling)以及其他特性。


libcurl


  • 一个免费开源的,客户端 url 传输库,支持 DICT, FILE, FTP, FTPS, Gopher, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, POP3, POP3S, RTMP, RTSP, SCP, SFTP, SMTP, SMTPS, Telnet and TFTP 等协议。


  • 支持 SSL 证书,HTTP POST, HTTP PUT,FTP 上传,基于表单的 HTTP 上传,代理(proxies)、cookies、用户名/密码认证(Basic, Digest, NTLM 等)、下载文件断点续传,上载文件断点续传(file transfer resume),http 代理服务器管道(proxy tunneling)等。


  • 高度可移植,可以工作在不同的平台上,支持 Windows,Unix,Linux 等。


  • 免费的,线程安全的,IPV6 兼容的,同事它还有很多其他非常丰富的特性。libcurl 已经被很多知名的大企业以及应用程序所采用。


特点


  • curl 和 libcurl 都可以利用多种多样的协议来传输文件,包括 HTTP, HTTPS, FTP, FTPS, GOPHER, LDAP, DICT, TELNET and FILE 等


  • 支持 SSL 证书,HTTP POST, HTTP PUT,FTP 上传,基于表单的 HTTP 上传,代理(proxies)、cookies、用户名/密码认证(Basic, Digest, NTLM 等)、下载文件断点续传,上载文件断点续传(file transfer resume),http 代理服务器管道(proxy tunneling)等。


  • libcurl 是一个库,通常与别的程序绑定在一起使用,如命令行工具 curl 就是封装了 libcurl 库。所以我们也可以在你自己的程序或项目中使用 libcurl 以获得类似 CURL 的强大功能。


2 实现



在开始实现 client 发送 http 请求之前,我们先理解两个概念:

同步请求:

当客户端向服务器发送同步请求时,服务处理在请求的过程中,客户端会处于等待的状态,一直等待服务器处理完成,客户端将服务端处理后的结果返回给调用方。

异步请求

客户端把请求发送给服务器之后,不会等待服务器返回,而是去做其他事情,待服务器处理完成之后,通知客户端该事件已经完成,客户端在获取到通知后,将服务器处理后的结果返回给调用方。

通过这俩概念,就能看出,异步在实现上,要比同步复杂的多。同步,即我们简单的等待处理结果,待处理结果完成之后,再返回调用方。而对于异步,往往在实现上,需要各种回调机制,各种通知机制,即在处理完成的时候,需要知道是哪个任务完成了,从而通知客户端去处理该任务完成后剩下的逻辑。


下面,我们将从代码实现的角度,来更深一步的理解 libcurl 在实现同步和异步请求操作上的区别,从而更近异步的了解同步和异步的实现原理。

同步

使用 libcurl 完成同步 http 请求,原理和代码都比较简单,主要是分位以下几个步骤:


1、初始化 easy handle


2、在该 easy handle 上设置相关参数,在本例中主要有以下几个参数


  • CURLOPT_URL,即请求的 url


  • CURLOPT_WRITEFUNCTION,即回调函数,将 http server 返回数据写入对应的地方


  • CURLOPT_FOLLOWLOCATION,是否获取 302 跳转后的内容


  • CURLOPT_POSTFIELDSIZE,此次发送的数据大小


  • CURLOPT_POSTFIELDS,此次发送的数据内容


更多的参数设置,请参考 libcurl 官网


3、curleasyperform,调用该函数发送 http 请求,并同步等待返回结果


4、curleasycleanup,释放步骤一中申请的 easy handle 资源


代码实现(easy_curl.cc)

#include <curl/curl.h>#include <iostream>#include <string>
std::string resp;size_t WriteData( char* buffer, size_t size, size_t nmemb, void* userp) { resp.append(buffer, size * nmemb); return size * nmemb;}
int main(void) { CURLcode res; CURL *curl = curl_easy_init(); std::string post = "abc"; if(curl) { // 设置url curl_easy_setopt(curl, CURLOPT_URL, "https://www.baidu.com"); // 设置回调函数,即当有返回的时候,调用回调函数WriteData curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteData); // 抓取302跳转后d额内容 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION,1);
// 设置发送的内容大小 curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, post.size());
// 设置发送的内容 curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post.c_str()); // 开始执行http请求,此处是同步的,即等待http服务器响应 res = curl_easy_perform(curl); /* Check for errors */ if(res != CURLE_OK) fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); /* always cleanup */ curl_easy_cleanup(curl); } std::cout << resp << std::endl; return 0;}
复制代码


编译

g++ --std=c++11 easy_curl.cc -I ../artifacts/include/ -L ../artifacts/lib -lcurl -o easy_curl
复制代码


结果

<!DOCTYPE html><!--STATUS OK--><html><head>    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">    <meta http-equiv="content-type" content="text/html;charset=utf-8">    <meta content="always" name="referrer">    <script src="https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/nocache/imgdata/seErrorRec.js"></script>    <title>页面不存在_百度搜索</title>    <style data-for="result">        body {color: #333; background: #fff; padding: 0; margin: 0; position: relative; min-width: 700px; font-family: arial; font-size: 12px }        p, form, ol, ul, li, dl, dt, dd, h3 {margin: 0; padding: 0; list-style: none }        input {padding-top: 0; padding-bottom: 0; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box } img {border: none; }        .logo {width: 117px; height: 38px; cursor: pointer }         #wrapper {_zoom: 1 }        #head {padding-left: 35px; margin-bottom: 20px; width: 900px }        ...
复制代码

异步

接触过网络编程的读者,都或多或少的了解多路复用的原理。IO 多路复用在 Linux 下包括了三种,select、poll、epoll,抽象来看,他们功能是类似的,但具体细节各有不同:首先都会对一组文件描述符进行相关事件的注册,然后阻塞等待某些事件的发生或等待超时。

在使用 Libcurl 进行异步请求,从上层结构来看,简单来说,就是对 easy handle 和 multi 接口的结合使用。其中,easy handle 底层也是一个 socket,multi 接口,其底层实现也用的是 epoll,那么我们如何使用 easy handle 和 multi 接口,来实现一个高性能的异步 http 请求 client 呢?下面我们将使用代码的形式,使得读者能够进一步了解其实现机制。

multi 接口的使用是在 easy 接口的基础之上,将 easy handle 放到一个队列中(multi handle),然后并发发送请求。与 easy 接口相比,multi 接口是一个异步的,非阻塞的传输方式。

  • multi 接口的使用,主要有以下几个步骤:

  • curlmulti init 初始化一个 multi handler 对象

  • 初始化多个 easy handler 对象,使用 curleasysetopt 进行相关设置

  • 调用 curlmulti add_handle 把 easy handler 添加到 multi curl 对象中

  • 添加完毕后执行 curlmultiperform 方法进行并发的访问

  • 访问结束后 curlmultiremovehandle 移除相关 easy curl 对象,先用 curleasy_cleanup 清除 easy handler 对象,最后 curl_multi_cleanup 清除 multi handler 对象。


http_request.h

/*该类是对easy handle的封装,主要做一些初始化操作,设置url 、发送的内容header以及回调函数*/class HttpRequest {public:  using FinishCallback = std::function<int()>;
HttpRequest(const std::string& url, const std::string& post_data) : url_(url), post_data_(post_data) { } int Init(const std::vector<std::string> &headers);
CURL* GetHandle(); const std::string& Response();
void SetFinishCallback(const FinishCallback& cb);
int OnFinish(int response_ret);
CURLcode Perform();
void AddMultiHandle(CURLM* multi_handle);
void Clear(CURLM* multi_handle);
private: void AppendData(char* buffer, size_t size, size_t nmemb);
static size_t WriteData(char *buffer, size_t size, size_t nmemb, void *userp);
std::string url_;
std::string post_data_;
CURL* handle_ = nullptr;
struct curl_slist *chunk_ = nullptr;
FinishCallback cb_;

int response_ret_;};
复制代码


http_request.cc

/*http_request.h的实现*/int HttpRequest::Init(const std::vector<std::string> &headers) {  handle_ = curl_easy_init();  if (handle_ == nullptr) {    return kCurlEasyInitFailed;  }  CURLcode ret = curl_easy_setopt(handle_, CURLOPT_URL, url_.c_str());  if (ret != CURLE_OK) {    return ret;  }
ret = curl_easy_setopt(handle_, CURLOPT_WRITEFUNCTION, &HttpRequest::WriteData); if (ret != CURLE_OK) { return ret; }
ret = curl_easy_setopt(handle_, CURLOPT_WRITEDATA, this); if (ret != CURLE_OK) { return ret; }
ret = curl_easy_setopt(handle_, CURLOPT_NOSIGNAL, 1); if (ret != CURLE_OK) { return ret; }
ret = curl_easy_setopt(handle_, CURLOPT_PRIVATE, this); if (ret != CURLE_OK) { return ret; }
ret = curl_easy_setopt(handle_, CURLOPT_POSTFIELDSIZE, post_data_.length()); if (ret != CURLE_OK) { return ret; } ret = curl_easy_setopt(handle_, CURLOPT_POSTFIELDS, post_data_.c_str()); if (ret != CURLE_OK) { return ret; }
ret = curl_easy_setopt(handle_, CURLOPT_TIMEOUT_MS, 100); if (ret != CURLE_OK) { return ret; }
// ret = curl_easy_setopt(handle_, CURLOPT_CONNECTTIMEOUT_MS, 10);
ret = curl_easy_setopt(handle_, CURLOPT_DNS_CACHE_TIMEOUT, 600); if (ret != CURLE_OK) { return ret; }
chunk_ = curl_slist_append(chunk_, "Expect:");
for (auto item : headers) { chunk_ = curl_slist_append(chunk_, item.c_str()); }

ret = curl_easy_setopt(handle_, CURLOPT_HTTPHEADER, chunk_);
if (ret != CURLE_OK) { return ret; } // 设置http header if (boost::algorithm::starts_with(url_, "https://")) { curl_easy_setopt(handle_, CURLOPT_SSL_VERIFYPEER, false); curl_easy_setopt(handle_, CURLOPT_SSL_VERIFYHOST, false); }
return kOk;}
//获取easy handleCURL* HttpRequest::GetHandle() { return handle_;}
// 获取http server端的响应const std::string& HttpRequest::Response() { return response_;}
//设置回调函数,当server返回完成之后,调用void HttpRequest::SetFinishCallback(const FinishCallback& cb) { cb_ = cb;}
// libcurl 错误码信息int HttpRequest::OnFinish(int response_ret) { response_ret_ = response_ret;
if (cb_) { return cb_(); }
return kRequestFinishCallbackNull;}
// 执行http请求CURLcode HttpRequest::Perform() { CURLcode ret = curl_easy_perform(handle_); return ret;}
// 将easy handle 加入到被监控的multi handlevoid HttpRequest::AddMultiHandle(CURLM* multi_handle) { if (multi_handle != nullptr) { curl_multi_add_handle(multi_handle, handle_); }}
// 释放资源void HttpRequest::Clear(CURLM* multi_handle) { curl_slist_free_all(chunk_); if (multi_handle != nullptr) { curl_multi_remove_handle(multi_handle, handle_); }
curl_easy_cleanup(handle_);}
// 获取返回void HttpRequest::AppendData(char* buffer, size_t size, size_t nmemb) { response_.append(buffer, size * nmemb);}// 回调函数,获取返回内容size_t HttpRequest::WriteData( char* buffer, size_t size, size_t nmemb, void* userp) { HttpRequest* req = static_cast<HttpRequest*>(userp); req->AppendData(buffer, size, nmemb); return size * nmemb;}
复制代码


main.cc

curl_global_init(CURL_GLOBAL_DEFAULT);auto multi_handle_ = curl_multi_init();int numfds = 0;int running_handles = 0;while (true) {  //此处读者来实现,基本功能如下:  // 1、获取上游的请求内容,从里面获取要发送http的相关信息  // 2、通过步骤1获取的相关信息,来创建HttpRequest对象  // 3、将该HttpRequest对象跟multi_handle_对象关联起来  curl_multi_perform(multi_handle_, &running_handles);  CURLMcode mc = curl_multi_wait(multi_handle_, nullptr, 0,            200, &numfds);    if (mc != CURLM_OK) {      std::cerr << "RequestDispatcher::Run"                << " curl_multi_wait failed, ret: "                << mc;      continue;    }    curl_multi_perform(multi_handle_, &running_handles);
CURLMsg* msg = nullptr; int left = 0;while ((msg = curl_multi_info_read(multi_handle_, &left))) { // msg->msg will always equals to CURLMSG_DONE. // CURLMSG_NONE and CURLMSG_LAST were not used. if (msg->msg != CURLMSG_DONE) { std::cerr << "RequestDispatcher::Run" << " curl_multi_info_read failed, msg: " << msg->msg; continue; }
CURL* easy_handle = msg->easy_handle; CURLcode curl_ret = msg->data.result; int response_ret = kOk; if (curl_ret != CURLE_OK) { std::cerr << "RequestDispatcher::Run" << " msg->data.result != CURLE_OK, curl_ret: " << curl_ret; response_ret = static_cast<int>(curl_ret); }
int http_status_code = 0; curl_ret = curl_easy_getinfo(easy_handle, CURLINFO_RESPONSE_CODE, &http_status_code); if (curl_ret != CURLE_OK) { std::cerr << "RequestDispatcher::Run" << " curl_easy_getinfo failed, curl_ret=" << curl_ret; if (response_ret == kOk) { response_ret = static_cast<int>(curl_ret); } } if (http_status_code != 200) { std::cerr << "RequestDispatcher::Run" << " http_status_code=" << http_status_code; if (response_ret == kOk) { response_ret = http_status_code; } }
HttpRequest* req = nullptr; // In fact curl_easy_getinfo will // always return CURLE_OK if CURLINFO_PRIVATE was set. curl_ret = curl_easy_getinfo(easy_handle, CURLINFO_PRIVATE, &req);if (curl_ret != CURLE_OK) { std::cerr << "RequestDispatcher::Run" << " curl_easy_getinfo failed, curlf_ret=" << curl_ret; if (response_ret == kOk) { response_ret = static_cast<int>(curl_ret); } }
if (req != nullptr) { ret = req->OnFinish(response_ret); } } }
复制代码

至此,我们已经可以使用 libcurl 来实现并发发送 http 请求,当然这个只是一个简单异步实现功能,更多的功能,还需要读者去使用 libcurl 中的其他功能去实现,此处留给读者一个问题(这个问题,也是笔者项目中使用的一个功能,该项目已经线上稳定运行 4 年,日请求量在 20E 图片),业务需要,某一个请求需要并发发送给指定的几家,即该请求,需要并发发送给几个 http server,在一个特定的超时时间内,获取这几个 http server 的返回内容,并进行处理,那么这种功能应该如何使用 libcurl 来实现呢?透露下,可以使用 libcurl 的另外一个参数 CURLOPT_PRIVATE。

3 性能对比



至此,我们已经基本完成了高性能 http 并发功能的设计,那么到底性能如何呢?笔者从 以下几个角度来做了测试:

1、串行发送同步请求

2、多线程情况下,发送同步请求(此处线程为 4 个,笔者测试的服务器为 4C)

3、使用 multi 接口

4、使用 multi 接口,并复用其对应的 easy handle

5、使用 dns cache(对 easy handle 设置 CURLOPTDNSCACHE_TIMEOUT),即不用每次都进行 dns 解析


| 方法 | 平均耗时(ms) | 最大耗时(ms) |

| ------------ | ------------ | ------------ |

|串行同步 | 21.381 | 30.617 |

| 多线程同步 | 4.331 | 16.751 |

| multi 接口 | 1.376 | 11.974 |

| multi 接口 连接复用 | 0.352 | 0.748 |

| multi 接口使用 dns cache | 0.381 | 0.731 |


4 一点心得


libcurl 是一个高性能,较易用的 HTTP client,在使用其直接,一定要对其接口功能进行详细的了解,否则很容易入坑,犹记得在 18 年中的时候,上线了某一个功能,会偶现 coredump(在上线之前,也进行了大量的性能测试,都没有出现过一次 coredump),为了分析这个原因,笔者将服务的代码一直精简精简,然后模拟测试,缩小 coredump 定位范围,最终发现,只有在超时的时候,才会导致 coredump,这就说明了为什么测试环境没有 coredump,而线上会产生 coredump,这是因为线上的超时时间设置的是 5ms,而测试环境超时时间是 20ms,这就基本把原因定位到超时导致的 coredump。

然后,分析 libcurl 源码,发送时一个 libcurl 的参数设置导致 coredump,至此,笔者耗费了 23 个小时,问题才得以解决。


更多内容,请移步

高性能编程探索


发布于: 2021 年 03 月 15 日阅读数: 37
用户头像

namelij

关注

还未添加个人签名 2021.03.12 加入

还未添加个人简介

评论

发布
暂无评论
高并发HTTP请求实践