当今,正处于互联网高速发展的时代,每个人的生活都离不开互联网,互联网已经影响了每个人生活的方方面面。我们使用淘宝、京东进行购物,使用微信进行沟通,使用美图秀秀进行拍照美化等等。而这些每一步的操作下面,都离不开一个技术概念 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
它支持很多协议: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)等。
特点
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)等。
2 实现
在开始实现 client 发送 http 请求之前,我们先理解两个概念:
同步请求:
当客户端向服务器发送同步请求时,服务处理在请求的过程中,客户端会处于等待的状态,一直等待服务器处理完成,客户端将服务端处理后的结果返回给调用方。
异步请求
客户端把请求发送给服务器之后,不会等待服务器返回,而是去做其他事情,待服务器处理完成之后,通知客户端该事件已经完成,客户端在获取到通知后,将服务器处理后的结果返回给调用方。
通过这俩概念,就能看出,异步在实现上,要比同步复杂的多。同步,即我们简单的等待处理结果,待处理结果完成之后,再返回调用方。而对于异步,往往在实现上,需要各种回调机制,各种通知机制,即在处理完成的时候,需要知道是哪个任务完成了,从而通知客户端去处理该任务完成后剩下的逻辑。
下面,我们将从代码实现的角度,来更深一步的理解 libcurl 在实现同步和异步请求操作上的区别,从而更近异步的了解同步和异步的实现原理。
同步
使用 libcurl 完成同步 http 请求,原理和代码都比较简单,主要是分位以下几个步骤:
1、初始化 easy handle
2、在该 easy handle 上设置相关参数,在本例中主要有以下几个参数
更多的参数设置,请参考 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 handle
CURL* 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 handle
void 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 个小时,问题才得以解决。
更多内容,请移步
高性能编程探索
评论