开始使用 LibcURL¶
本节我们会介绍 LibcURL 的基础使用方法。
Hello LibcURL!¶
下面的这个程序将 http://localhost:8000
的内容输出到 stdout
。
任何 libcurl 程序都离不开以下四个函数。
curl/curl.h | |
---|---|
curl/easy.h | |
---|---|
在使用任何 libcurl 函数前,一定要调用 curl_global_init()
。这个函数在程序的整个生命周期内只需调用一次,多次调用的作用与一次调用相同。它的参数告诉 libcurl 应该初始化哪些资源,而且总应该是 CURL_GLOBAL_ALL
,除非你有正当理由打算操纵 libcurl 的内部行为。
Warning
如果没有全局初始化,那么在第一次调用 curl_easy_init()
时会以默认值 CURL_GLOBAL_DEFAULT
全局初始化。虽然这个值目前也被定义为 CURL_GLOBAL_ALL
,但不要依赖于这个行为。某些版本的 libcurl 中,curl_global_init()
不是线程安全的(你可能会很惊讶,但是 macOS 系统自带的 curl 就不是;可以用 curl-config --feature | grep threadsafe
来检查),在多线程程序中可能产生错误。具体请查阅 curl_global_init()
的 manpage。
调用 curl_easy_init()
创建一个 handle。一个 handle 可以用于多个(不同时的)请求。这样也能充分地利用 Keep-Alive 机制。
通过 curl_easy_setopt()
设置请求参数,类似 Socket 编程中的 setsockopt()
。与之根据 option_name
确定参数一样,curl_easy_setopt()
根据 option
确定参数,不同之处在于 setsockopt()
统一使用 const void *
规避类型系统,而 curl_easy_setopt()
采用可变参数。因此在 curl/curl.h
中存在另一个同名宏定义以限制参数数量(看看最末尾)。后文中会详细介绍一些常用设置项。
Note
任何请求都应设置 CURLOPT_URL
或者 CURLOPT_CURLU
。(不然呢?)
对于 CURLOPT_URL
,libcurl 会将提供的 URL 在内部复制一份,因此用户不需要在整个请求周期中保持 URL 指向地址的有效性,这在使用临时 buffer 时很有用。大多数其他字符串型设置都有这个设定,但还是记得查查 manpage。我们以后不会强调这个设定,除了那些不遵循这个设定的设置。
那么,CURLOPT_CURLU
是什么?如果不好马上合成一个完整的 URL(比如你有成吨的 GET 参数),就可以用 CURLU *
来一部分一部分地构造 URL。之后介绍。
Tip
使用 man
(或 info
)可以更详细地查看每个单独选项的说明。
例如,当执行 man CURLOPT_URL
时,manpage 将给出此时 curl_easy_setopt()
第三个参数的正确类型:
CURLOPT_URL(3) | |
---|---|
最后,使用 curl_easy_perform()
实际发送请求。默认情况下,它会包揽创建连接、发送数据、接收数据与处理数据的全部过程(因此,当它返回时,所有数据已经全部处理完毕)。这可以通过 CURLOPT_CONNECT_ONLY
参数改变,我们后面会介绍如何实现。同时,在默认情况下,它会将接收到的数据全部输出到 stdout
。我们马上就会看到,可以通过 CURLOPT_WRITEFUNCTION
自定义此行为。
为什么叫做 curl_easy
¶
libcurl 提供了两套接口:Easy 与 Multi。
Easy 顾名思义是 libcurl 提供的易用接口(并不意味着在功能上有所妥协),它们全部以 curl_easy
打头,handle 类型 CURL *
。Easy 接口是同步(阻塞)的,适用于快速上手,或只需要进行少量请求的情况(指请求个数而非数据量)。
Multi 是用于管理多个 Easy handle 的接口,支持单线程异步(非阻塞)操作,支持并发请求,它们以 curl_multi
打头,handle 类型 CURLM *
。Multi 也分两种编程风格:select()
式与事件驱动式(称作 multi_socket
)。
Note
下文中,我们默认以 Easy 接口举例。Multi 接口将在以后单独介绍。
使用 callback 接收数据¶
总览页中的示例程序演示了如何使用 callback 接收数据。在此我们给出一个更精简的例子。
此处重要的两个参数是 CURLOPT_WRITEDATA
与 CURLOPT_WRITEFUNCTION
。
使用 CURLOPT_WRITEFUNCTION
设置自定义 callback。使用 CURLOPT_WRITEDATA
设置的指针成为 callback 的第四个参数。
在这个程序中,我们的 callback 只是简单的调用 fwrite()
将数据写入文件。实际上你可能已经发现,recv_cb()
的函数签名与 fwrite()
是一样的(除了指针类型,但这对于 callback 不重要)。也就是说,在此例中,可以直接将 fwrite()
作为 CURLOPT_WRITEFUNCTION
的参数。
Warning
callback 的返回值是重要的。需要是 size_t
类型,而且应当等于 size * nmemb
。否则,libcurl 将认为写数据时发生错误,curl_easy_perform()
调用将返回 CURLE_WRITE_ERROR
。(libcurl 保证 size
永远等于 1。)
如果你正在 C++ 中使用 libcurl,请注意在设置 callback 时不能使用 Lambda 表达式与仿函数。即使你的 Lambda 不捕获任何变量,也需要先转换到函数指针。否则在执行 curl_easy_perform()
时程序会崩溃。
实际上,在这个例子中,也可以完全不设置 callback。还记得我们说过,如果没有额外设置,会将数据输出到 stdout
吗?也就是说,默认的 callback 就是 fwrite()
,而默认的第四个参数也就是 stdout
(自己试试输出它的地址)。如果不设置自定义 callback,那么这时候 CURLOPT_WRITEDATA
就必须是一个 FILE *
指针。此时 libcurl 表现得如同用 fwrite()
将数据写到你提供的 FILE *
中。
设置请求标头¶
使用 CURLOPT_HTTPHEADER
设置自定义请求标头。它的参数是类型 struct curl_slist *
的链表指针。
通过调用 curl_slist_append()
向链表中添加字符串。若第一个参数为 NULL
则创建链表。记得用 curl_slist_free_all()
释放链表。
如果 CURLOPT_HTTPHEADER
的参数为 NULL
,会清除自定义标头。
上面的程序会产生如下的请求:
HTTP | |
---|---|
Note
libcurl 还提供了一些设置某些特定标头的选项(如 CURLOPT_USERAGENT
及 CURLOPT_COOKIE
等)。若其对应的标头在链表中同时存在,则使用链表中的标头。
没有专门的用于从链表中删除元素的函数,也不存在诸如 curl_slist_dup()
这种方便的函数。如果你有两个标头非常类似但不同的请求,只能分别构造两个列表。
对链表来说,字符串的有效性不需要保持,链表会复制一份。但是,对于 handle 来说,链表本身是不会复制的:必须在整个 handle 的请求周期中保持标头链表的有效性。
在 curl_slist_append()
的 manpage 中提到,函数会在出错时返回 NULL
。虽然一般来说不会有什么问题,但如果你有这方面的担心,要注意防止返回的 NULL
值覆盖掉原本的指针。
Warning
不要在标头字符串的最后带上 \r\n
。
如果我们观察上面产生的请求,会发现 libcurl 自动帮我们加上了 Accept
标头。当然,这很好,但是假如说我们不希望 libcurl 这样做呢?这也可以。只需给冒号后面留空即可,像这样:
HTTP | |
---|---|
复制请求¶
使用 curl_easy_duphandle()
复制一个 handle。下面的程序将先后请求 URL 两次,而其中的一个会输出详细信息,正如在命令行使用 curl -v
的结果一样。
复制出的 handle 也需要使用 curl_easy_cleanup()
清理。所有 libcurl 内部复制的字符串设置也会复制一份,所有仅指向而没有复制的设置也同样只是复制了指向,它们的指针有效性需要保持。
不仅仅是 GET
¶
使用 POST
方法¶
接下来我们来看看,怎么用 libcurl 向服务端发送数据。
注意 Content-Type
标头需要自己设置。使用 CURLOPT_POSTFIELDS
来设置数据的指针。此时,Content-Length
在内部由 strlen()
计算得出。
Note
CURLOPT_POSTFIELDS
不复制数据,所以指针有效性需要保持。如果需要 libcurl 复制数据,请把它换成 CURLOPT_COPYPOSTFIELDS
。
特别注意在这种情况下,设置数据大小的顺序就是必要的了:如果你先设置 CURLOPT_COPYPOSTFIELDS
,此时会立刻根据 strlen()
的计算结果复制数据,后续再使用 CURLOPT_POSTFIELDSIZE
也就没有作用了。
假设你要发送二进制数据怎么办?很好办:设置 CURLOPT_POSTFIELDSIZE
。设为 -1 则代表使用 strlen()
。如果你的数据量超过了 2 GB,那么应转而使用 CURLOPT_POSTFIELDSIZE_LARGE
。(小于 2 GB 的数据也可以用它,那为什么 libcurl 要这样设计呢?也许是历史原因。)
其他方法的请求¶
对于其他类型的请求,情况会变得稍微棘手一些。
对于 PUT
、DELETE
、OPTIONS
等,请使用 CURLOPT_CUSTOMREQUEST
显式用字符串设置。
但是,对于 HEAD
,正确的设置项是 CURLOPT_NOBODY
。
Note
你可能会注意到,存在一个 CURLOPT_PUT
设置项,并且想用它来反驳我:“这不是有专门设置 PUT
的选项吗?”你说得对,但是你也许有所不知:在 libcurl 7.12.1 版本后,这个设置项被标为已废弃——manpage 是这么说的,并且建议换用 CURLOPT_UPLOAD
。这里涉及到一些比较错综复杂的关系。我们会在下一节理清,目前就先承认它好了。
增量构造 URL¶
我们之前还提到了神秘选项 CURLOPT_CURLU
——其实没什么神秘的,只是把一个 const char *
类型的 URL 换成了 CURLU *
类型的 URL 而已。那么很自然地,我们会问:“这又是什么新东西?”例如如果我们需要发一个查询参数需要进行 URL 编码的请求,如果不想手动处理那些转义,就可以用 CURLU *
把这种工作交给 libcurl,太贴心了。
注意,如果 CURLOPT_URL
与 CURLOPT_CURLU
都设置了,CURLU *
比 const char *
优先级更高。
这份程序产生下面这样的请求。
HTTP | |
---|---|
你已经看到了这里的关键函数 curl_url_set()
。
curl/urlapi.h | |
---|---|
CURLUPart
就是我们看到的诸如 CURLUPART_URL
这样的枚举。flags
控制 libcurl 应该怎么处理这部分设置。例如,如果在上面设置查询参数时不加上 CURLU_APPENDQUERY
的话,那么后设置的参数就会覆盖前面设置的参数。同样,如果不使用 CURLU_URLENCODE
的话,那些 URL 特殊字符就不会被编码。你也已经看到,在处理查询参数这方面 libcurl 足够智能,它需要你直接传入 key=value
的形式,而且这第一个等号不会被编码。
相同的,还有 curl_url_get()
用来从 CURLU *
中解析你所需要的部分出来。在这里通过 part
参数获得的字符串必须由你使用 curl_free()
释放掉。
Note
CURLU
并不是 libcurl 的重点,所以它的这些参数与选项并没有排面到能独占自己的一份 manpage 的地步,而是全部收归 curl_url_set()
和 curl_url_get()
下。
用 curl_url_dup()
可以复制一份 CURLU *
出来。记得释放。
顺便,libcurl 还提供了 curl_easy_escape()
函数,顾名思义它只帮你做 URL 编码的部分,然后你就可以用你最喜欢的 snprintf()
来自己格式化 URL 了。与之相反的,也有 curl_easy_unescape()
。