百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

使用curl进行http高并发访问

bigegpt 2024-09-03 10:54 3 浏览

本文主要介绍curl异步接口的使用方式,以及获取高性能的一些思路和实践。同时假设读者已经熟悉并且使用过同步接口。

1.curl接口基本介绍

curl一共有三种接口:

  • Easy Interface
  • Multi Interface
  • Share Interface

1.1 Easy Interface

Easy下是同步接口,curl_easy_*的形式,基本处理方式有几个步骤:

  1. curl_easy_init获取easy handle
  2. curl_easy_setopt设置header/cookie/post-filed/网页内容接收回调函数等
  3. curl_easy_perform执行
  4. curl_easy_cleanup清理
    注意在第3步是阻塞的

1.2 Multi Interface

Multi下是异步接口,curl_multi_*的形式,允许在单线程下同时操作多个easy handle,基本处理方式有几个步骤:

  1. curl_multi_init获取multi handle
  2. 调用Easy Interface设置若干easy handle,通过curl_multi_add_handle.加到1里的multi handle
  3. 调用curl_multi_wait/curl_multi_perform等待所有easy handle处理完成
  4. curl_multi_info_read依次读取所有数据
  5. curl_multi/easy_cleanup清理
    注意curl_multi_perform不是阻塞的

1.3 Share Interface

Share是共享接口,curl_shared_*的形式,用于多个easy handle间共享一些数据,例如cookie dns等
注意需要用到锁的情况,比如share了CURL_LOCK_DATA_DNS,如果没有加锁,在curl_multi_perform时会core掉:

Since you can use this share from multiple threads, and libcurl has no internal thread synchronization, you must provide mutex callbacks if you’re using this multi-threaded. You set lock and unlock functions with curl_share_setopt too.

可以参考这两处例子:

  1. http://www.mit.edu/afs.new/sipb/user/ssen/src/curl-7.11.1/tests/libtest/lib506.c
  2. https://curl.haxx.se/mail/lib-2006-01/0218.html

2. 异步接口的例子

curl自带的例子还是介绍的curl_multi_fdset的方法。
实际上已经可以用curl_multi_wait代替了,据说是facebook的工程师提的升级:
Facebook contributes fix to libcurl’s multi interface to overcome problem with more than 1024 file descriptors.
使用方法可以参考这里

通过一个示例来看下multi的工作方式(注意出于简短的目的,有的函数没有判断返回值)

#include "curl/curl.h"
#include <string>

const char* url = "http://www.baidu.com";
const int count = 1000;

size_t write_data(void* buffer, size_t size, size_t count, void* stream) {
    (void)buffer;
    (void)stream;
    return size * count;
}

void curl_multi_demo() {
    CURLM* curlm = curl_multi_init();

    for (int i = 0; i < count; ++i) {
        CURL* easy_handle = curl_easy_init();
        curl_easy_setopt(easy_handle, CURLOPT_NOSIGNAL, 1);
        curl_easy_setopt(easy_handle, CURLOPT_URL, url);
        curl_easy_setopt(easy_handle, CURLOPT_WRITEFUNCTION, write_data);

        curl_multi_add_handle(curlm, easy_handle);
    }

    int running_handlers = 0;
    do {
        curl_multi_wait(curlm, NULL, 0, 2000, NULL);
        curl_multi_perform(curlm, &running_handlers);
    } while (running_handlers > 0);

    int msgs_left = 0;
    CURLMsg* msg = NULL;
    while ((msg = curl_multi_info_read(curlm, &msgs_left)) != NULL) {
        if (msg->msg == CURLMSG_DONE) {
            int http_status_code = 0;
            curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &http_status_code);
            char* effective_url = NULL;
            curl_easy_getinfo(msg->easy_handle, CURLINFO_EFFECTIVE_URL, &effective_url);
            fprintf(stdout, "url:%s status:%d %s\n",
                    effective_url,
                    http_status_code,
                    curl_easy_strerror(msg->data.result));

            curl_multi_remove_handle(curlm, msg->easy_handle);
            curl_easy_cleanup(msg->easy_handle);
        }
    }

    curl_multi_cleanup(curlm);
}

int main() {
    curl_multi_demo();
    return 0;
}

注意L35~36 L37~38不能互换,否则url为空,原因没有继续深追。

可以看到异步处理的方式是通过curl_multi_add_handle接口不断的把待抓的easy handle放到multi handle里。然后通过curl_multi_wait/curl_multi_perform等待所有easy handle处理完毕。
因为是同时在等待所有easy handle处理完毕,耗时比easy方式里逐个同步等待大大减少。其中产生hold作用的是在这段代码里:

//等待所有easy handle处理完毕
do {
    ...
} while (running_handlers > 0);

3.应用

而在实际应用中,我们的使用场景往往是这样的:

某个负责抓取数据的模块,service监听端口接收url,抓取数据后发往下游。

因为不希望所有的线程都处于上面multi示例中的等待(什么都不做)。于是就有了这种想法:接收到一条url后构造对应的easy handle,通过curl_multi_add_handle接口放入curlm后返回。同时两个线程不断的wait/perform和read,如果有完成的url,那么就调用对应的回调函数即可。
相当于将curlm当做一个队列,两个线程分别充当了生产者和消费者。

模型类似于:

CURLM* curlm = curl_multi_init()

//Thread-Consumer
	while true:
		//读取已经完成的url
		curl_multi_info_read
		//通知该url已完成,并且从curlm里删除
		curl_multi_remove_handle
		
//Thread-Producer
	while true:
		//等待可读socket
		curl_multi_wait
		//查看运行中的easy handle数目
		curl_multi_perform
		
//Thread-Other
	//根据url构造CURL easy handle
	CURL* curl = make_curl_easy_handle(url)
	//添加到curlm
	curl_multi_add_handle

做了一下测试很快就放弃了,程序在libcurl内部函数里core掉。
实际上curl是线程安全的,但同时也格外强调了这点:

The first basic rule is that you must never share a libcurl handle (be it easy or multi or whatever) between multiple threads. Only use one handle in one thread at a time.

具体可以参考这里,说明上面的模型是不可行的。

看到这里有一个基于libcurl的单线程I:O多路复用HTTP框架,dispatch部分和chrome源码里的thread模型很像,CURLM*对象在Dispatch::IO线程里统一操作,同时全局唯一,在一个LazyInstance的ConnectionRunner里维护。不过没有找到dispatch_after的实现,所以不太确定。

在StackOverflow上看到了复用curl的想法:curl handler放在一个池子中,需要时从中获取,使用后归还,同样不可行。

因此,标准的写法就是之前的示例的代码,正如这里提到的:

The multi interface is designed for this purpose: you add multiple handles and then process all of them with one call, all in the same thread.

4.优化

接下来就是优化的问题,在不使用curl_multi_socket_*的接口的情况下,是否有办法提升性能呢?
参考了curl的Persistence一节,主要是持久化部分信息来加速(缓存)。
其中提到

Each easy handle will attempt to keep the last few connections alive for a while in case they are to be used again.

这里说到每个easy handle会缓存之前的若干连接来避免重连、缓存DNS等以提高性能。因此一些思路就是easy handle重用、dns全局缓存等。

5.测试&结论

按照上面的思路分别测试抓取1000次baidu首页

  1. 串行使用curl easy接口
  2. 10个线程并行使用curl easy接口
  3. 使用curl multi接口
  4. 使用curl multi接口,并且reuse connection。(方法是第一遍curl easy handle抓取后不cleanup,计算第二次全部抓取完成的时间)
  5. 使用dns cache(使用curl_share_*接口,第一次抓取用于dnscache填充,计算第二次全部抓取完成的时间。效果上打开VERBOSE可以看到hostname found in DNS Cache)

其中处理时间测试结论如下:

methodavgmaxmineasy16.45721.61714.262easy parallel2.3319.4961.723multi0.7348.8950.259multi reuse0.001130.0015570.000898multi reuse cache0.001090.001400.000828

4 5的区别不大,同时不确定重用connection的情况下,dnscache是否还能起到正向作用

对应的测试代码都放到了gist上:1 2 3 4 5


6.补充

关于curl性能的讨论帖子很多,比如这里,其中也讲到了获取网页的一个基本流程:

  1. Request from your DNS server, the IP corresponding to the name of the site you requested
  2. Use the server’s reply to open a socket to that IP, port 80
  3. Send a small HTTP message describing what you want
  4. Receive the html code

这里有一些关于performance的建议,用到了curl_multi_socket_*接口,我没有用到。

相关推荐

当Frida来“敲”门(frida是什么)

0x1渗透测试瓶颈目前,碰到越来越多的大客户都会将核心资产业务集中在统一的APP上,或者对自己比较重要的APP,如自己的主业务,办公APP进行加壳,流量加密,投入了很多精力在移动端的防护上。而现在挖...

服务端性能测试实战3-性能测试脚本开发

前言在前面的两篇文章中,我们分别介绍了性能测试的理论知识以及性能测试计划制定,本篇文章将重点介绍性能测试脚本开发。脚本开发将分为两个阶段:阶段一:了解各个接口的入参、出参,使用Python代码模拟前端...

Springboot整合Apache Ftpserver拓展功能及业务讲解(三)

今日分享每天分享技术实战干货,技术在于积累和收藏,希望可以帮助到您,同时也希望获得您的支持和关注。架构开源地址:https://gitee.com/msxyspringboot整合Ftpserver参...

Linux和Windows下:Python Crypto模块安装方式区别

一、Linux环境下:fromCrypto.SignatureimportPKCS1_v1_5如果导包报错:ImportError:Nomodulenamed'Crypt...

Python 3 加密简介(python des加密解密)

Python3的标准库中是没多少用来解决加密的,不过却有用于处理哈希的库。在这里我们会对其进行一个简单的介绍,但重点会放在两个第三方的软件包:PyCrypto和cryptography上,我...

怎样从零开始编译一个魔兽世界开源服务端Windows

第二章:编译和安装我是艾西,上期我们讲述到编译一个魔兽世界开源服务端环境准备,那么今天跟大家聊聊怎么编译和安装我们直接进入正题(上一章没有看到的小伙伴可以点我主页查看)编译服务端:在D盘新建一个文件夹...

附1-Conda部署安装及基本使用(conda安装教程)

Windows环境安装安装介质下载下载地址:https://www.anaconda.com/products/individual安装Anaconda安装时,选择自定义安装,选择自定义安装路径:配置...

如何配置全世界最小的 MySQL 服务器

配置全世界最小的MySQL服务器——如何在一块IntelEdison为控制板上安装一个MySQL服务器。介绍在我最近的一篇博文中,物联网,消息以及MySQL,我展示了如果Partic...

如何使用Github Action来自动化编译PolarDB-PG数据库

随着PolarDB在国产数据库领域荣膺桂冠并持续获得广泛认可,越来越多的学生和技术爱好者开始关注并涉足这款由阿里巴巴集团倾力打造且性能卓越的关系型云原生数据库。有很多同学想要上手尝试,却卡在了编译数据...

面向NDK开发者的Android 7.0变更(ndk android.mk)

订阅Google官方微信公众号:谷歌开发者。与谷歌一起创造未来!受Android平台其他改进的影响,为了方便加载本机代码,AndroidM和N中的动态链接器对编写整洁且跨平台兼容的本机...

信创改造--人大金仓(Kingbase)数据库安装、备份恢复的问题纪要

问题一:在安装KingbaseES时,安装用户对于安装路径需有“读”、“写”、“执行”的权限。在Linux系统中,需要以非root用户执行安装程序,且该用户要有标准的home目录,您可...

OpenSSH 安全漏洞,修补操作一手掌握

1.漏洞概述近日,国家信息安全漏洞库(CNNVD)收到关于OpenSSH安全漏洞(CNNVD-202407-017、CVE-2024-6387)情况的报送。攻击者可以利用该漏洞在无需认证的情况下,通...

Linux:lsof命令详解(linux lsof命令详解)

介绍欢迎来到这篇博客。在这篇博客中,我们将学习Unix/Linux系统上的lsof命令行工具。命令行工具是您使用CLI(命令行界面)而不是GUI(图形用户界面)运行的程序或工具。lsoflsof代表&...

幻隐说固态第一期:固态硬盘接口类别

前排声明所有信息来源于网络收集,如有错误请评论区指出更正。废话不多说,目前固态硬盘接口按速度由慢到快分有这几类:SATA、mSATA、SATAExpress、PCI-E、m.2、u.2。下面我们来...

新品轰炸 影驰SSD多款产品登Computex

分享泡泡网SSD固态硬盘频道6月6日台北电脑展作为全球第二、亚洲最大的3C/IT产业链专业展,吸引了众多IT厂商和全球各地媒体的热烈关注,全球存储新势力—影驰,也积极参与其中,为广大玩家朋友带来了...