WHCSRL 技术网

curl使用小记(三)——获取远端数据到内存缓冲区

1. 概述

我在博文《curl使用小记(二)——远程下载一张图片》中介绍了如何通过Curl获取远端的文件。不过在那个例子中,将获取远端数据与写入数据的步骤混杂到一起了。在多线程的场景下,这样做可能会造成读写冲突的问题。理论上,远端访问数据是先保存到内存中,在写出到文件中。而远端访问数据到内存可以看作是读操作,是不会读冲突的。所以一个很好的策略是,一次性将数据读取到内存Buf中,再写出到文件。

2. 实现

《curl使用小记(二)——远程下载一张图片》中的代码改进一下,具体的代码实例如下:

#include <iostream>
#include <curl/curl.h>

using namespace std;

//内存块结构体
struct MemoryStruct
{
	char *memory;
	size_t size;

	MemoryStruct()
	{
		memory = (char *)malloc(1);
		size = 0;
	}

	~MemoryStruct()
	{
		free(memory);
		memory = NULL;
	}
};

//回调函数实现:一次请求可能多次调回调函数
size_t HttpPostWriteBack(void *contents, size_t size, size_t nmemb, void *userp)
{
	size_t realsize = size * nmemb;//一次回调返回的数据量
	struct MemoryStruct *mem = (struct MemoryStruct *)userp;

	char *ptr = (char *)realloc(mem->memory, mem->size + realsize);
	if (ptr == NULL)
	{
		printf("not enough memory (realloc returned NULL)
");
		return 0;
	}

	mem->memory = ptr;
	memcpy(&(mem->memory[mem->size]), contents, realsize);
	mem->size += realsize;
	return realsize;//必须返回真实的数据
}

int main()
{
	const char *netlink = "http://cn.bing.com/th?id=OHR.GrandsCausses_EN-CN3335882379_800x480.jpg";
	const char *output = "D:/dst1.jpg";

	curl_global_init(CURL_GLOBAL_ALL);		//初始化全局资源

	CURL *curl = curl_easy_init();		//初始化句柄

	//需要的话,可以设置代理
	//curl_easy_setopt(curl, CURLOPT_PROXY, "127.0.0.1:7890");
	
	//访问网址
	curl_easy_setopt(curl, CURLOPT_URL, netlink);

	//设置用户代理
	curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36");
	   
	//获取数据
	MemoryStruct chunk;	
	curl_easy_setopt(curl, CURLOPT_WRITEDATA, &chunk);
	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, HttpPostWriteBack);

	实现下载进度
	//curl_easy_setopt(curl, CURLOPT_NOPROGRESS, false);
	//curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, progress_callback);
	//curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, nullptr);

	//运行
	curl_easy_perform(curl);

	curl_easy_cleanup(curl);			//释放句柄

	curl_global_cleanup(); //释放全局资源

	//写出数据
	FILE *fp = nullptr;
	if (fopen_s(&fp, output, "wb") != 0)
	{
		curl_easy_cleanup(curl);
		return 0;
	}
	fwrite(chunk.memory, chunk.size, 1, fp);
	fclose(fp);

	return 1;
}    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90

这段代码其中一个关键改进在于,通过自定义结构体MemoryStruct,实现了一个类似于动态数组的设计。由于远端访问文件的数据量在一开始并不能确定,所以需要先访问一部分,然后将容器扩容,再访问一部分,再扩容。这个申请内存的扩容操作是通过C的realloc()函数来实现的。这个结构体MemoryStruct还利用了C++的RAII机制做内存管理。

另外一个关键就是CURLOPT_WRITEDATA于CURLOPT_WRITEFUNCTION的配合使用了。CURLOPT_WRITEFUNCTION用来设置回调函数,CURLOPT_WRITEDATA用来设置回调函数的出参,这个其实是C的编程思维,万物皆指针,所有的操作都被抽象成同一个函数接口,其实不是同一个东西。

3. 参考

  1. curl CURLOPT_WRITEDATA CURLOPT_WRITEFUNCTION 回调函数
  2. libcurl中CURLOPT_WRITEFUNCTION设置回调函数
推荐阅读