⾼并发HHTP实践
当今,正处于互联⽹⾼速发展的时代,每个⼈的⽣活都离不开互联⽹,互联⽹已经影响了每个⼈⽣活的⽅⽅⾯⾯。我们使⽤淘宝、京东进⾏购物,使⽤进⾏沟通,使⽤美图秀秀进⾏拍照美化等等。⽽这些每⼀步的操作下⾯,都离不开⼀个技术概念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、curl_easy_perform,调⽤该函数发送http请求,并同步等待返回结果
4、curl_easy_cleanup,释放步骤⼀中申请的easy handle资源
代码实现()
#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, "www.baidu");
// 设置回调函数,即当有返回的时候,调⽤回调函数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 -I ../artifacts/include/ -L ../artifacts/lib -lcurl -o easy_curl
结果
<!DOCTYPE html>
<!--STATUS OK-->
<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="ss1.bdstatic/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接⼝的使⽤,主要有以下⼏个步骤:
curl_multi _init初始化⼀个multi handler对象
初始化多个easy handler对象,使⽤curl_easy_setopt进⾏相关设置
调⽤curl_multi _add_handle把easy handler添加到multi curl对象中
添加完毕后执⾏curl_multi_perform⽅法进⾏并发的访问
访问结束后curl_multi_remove_handle移除相关easy curl对象,先⽤curl_easy_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.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_, "")) {
curl_easy_setopt(handle_, CURLOPT_SSL_VERIFYPEER, false);
curl_easy_setopt(handle_, CURLOPT_SSL_VERIFYHOST, false);
}
return 0;
}
//获取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) {curl命令发送post请求带参数
response_ret_ = response_ret;
if (cb_) {
return cb_();
}
return -1;
}
// 执⾏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;
}
<
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"
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论