libcurl网络协议库使用介绍

  要说到应用层网络协议库,我想没有谁比libcurl更夸张了,一大串的协议支持列表涵盖了绝大多数人碰见到的网络应用协议,虽然之前自己只会使用其命令行工具curl下载文件及进行GET/POST服务端测试。之前看老大在Windows平台下的HTTP请求客户端是用的InternetOpen、InternetConnect这类的库操作的,其实在Linux平台下,大家通常都是使用libcurl这个库来实现各种协议的客户端(比如HTTP、FTP等),而且这货本身是跨平台的哦!
  另外,libcurl还需要赞扬的就是附带了很多例子snippets,对于简单的操作,很多例子拿来改改参数就可以直接用了,大大降低了上手的难度!
libcurl

一、libcurl使用简介

  libcurl使用的时候首选需要进行一次全局的初始化curl_global_init,确保这个初始化在程序的整个声生命周期中只能进行一次,而当确信程序不再使用libcurl的时候,应当调用curl_global_cleanup进行清理工作。正确的情况应该避免两者被多次调用,他们应该只在你的程序中被调用一次,在C++封装的时候应该分别分布在构造函数和析构函数中。
  libcurl可以安全的和C++使用,唯一需要注意的是:callback回调函数不能是非静态的成员函数,可以是非成员函数、类的静态成员函数。

1
2
curl_global_init(CURL_GLOBAL_ALL);
curl_global_cleanup();

  libcurl提供easy和multi两种操作接口,easy模式是以同步和阻塞的方式进行单个传输操作,而multi模式是在单个线程中进行多个同时的传输操作。

1.1 easy interface

  在使用easy interface之前,需要首先创建easy handle,一个handle对应于一个session,该handle应该只在一个线程中使用而绝对不能够被多个线程共享。接下来需要设置handle的属性和选项,用以控制接下来的传输操作,注意的这种设置是持久生效的,意味着一旦设置除非将其改变为其他的值,否则使用该handle进行的多个请求和传输都使用之前设置的属性、选项值,后面通过curl_easy_reset可以将之前对handle的所有设置清空。大部分给libcurl设置的值都是C风格的字符串类型,libcurl会对其值进行一次深拷贝,所以对属性、选项值的生命周期没有要求。
  常见的使用方式是设置其URL,然后获取对应的资源数据,libcurl默认会将所得到的内容打印到标准输出上,可以通过设置WRITEFUNCTION设置写回调函数,libcurl将会把得到的数据通过该函数进行用户自定义写操作。其中还有一个WRITEDATA选项(之前称为FILE),如果设置了WRITEFUNCTION那么该参数可以是任意的数据指针,其可以被用户的回调函数使用而libcurl本身不会修改其内容;如果没有设置WRITEFUNCTION,那么该参数可以是一个FILE*的文件指针供libcurl输出使用。
  接下来调用curl_easy_perform,程序就会连接远端的服务器,进行必要的操作后就从远端向回传递数据,任何时候收到数据都会调用之前设置的回调函数,每次传输的数据可能是1byte,也可能上kbyte,总之libcurl会尽量传输多的数据。此时要求你的回调函数返回的长度值是你使用了的长度,这应该和libcurl传递进去的数据长度相同,否则libcurl会报错。当整个传输完成之后,该函数会有一个返回码表示该次请求是成功还是失败了,如果失败了可以添加ERRORBUFFER参数,以得到相关的调试信息,同时设置VERBOSE=1也会显示整个协议交互过程的详细信息,针对HTTP协议还可以设置HEADER选项,可以将协议的头信息打印出来。

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
static size_t writedate(void* ptr, size_t size, size_t nmemb, void* data) {
size_t len = size * nmemb;
if(data)
memcpy(data, ptr, len);
return len;
}
int HttpClient(const std::string& strUrl) {
try {
CURL *curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_ENCODING , "UTF-8");
curl_easy_setopt(curl, CURLOPT_URL, strUrl.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writedate);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 200);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 800);

CURLcode res = curl_easy_perform(curl);
int nStatusCode = 500;
if (res == CURLE_OK)
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &nStatusCode);
curl_easy_cleanup(curl);
if (res == CURLE_OK && nStatusCode == 200 )
return 0;
else
return -1;
} catch (...) {
return -1;
}
}

  handle可以(而且被推荐)重用来传递其他文件,算是一个比较好的优化机制,称之为Re-cycling。在每次curl_easy_perform操作之后,libcurl将会保留该连接为活动、打开的状态,下次再进行请求可以直接使用该连接,从而降低重建连接的各项消耗。不仅仅是HTTP,FTP的连接也能够享受到该机制带来的好处,对于有并发数限制的系统,保活的连接可以帮组你免被其他用户排挤。

1.2 multi interface

  上面的easy interface都是同步阻塞方式,使用起来简单安全;而multi interface允许程序在一个线程中进行多个文件、多个方向的数据传输,这样在增加了传输效率的同时,还降低了多线程传输的性能消耗、同步工作。其实,其骨子里就是select()的事件驱动机制来实施的,此外还有一种multi socket的接口,可以和其他的异步框架组合使用,而非限制在select()方式。
  使用multi interface,首先需要调用curl_multi_init创建multi handle,该handle将会被后面所有的curl_multi_xx函数接口作为参数使用;后面的所有传输,可以使用上面的easy interface的方式进行创建、选项设置、回调设定等操作,只不过不使用curl_easy_perform()进行实际的操作,而是通过curl_multi_add_handle()将其与multi handle进行关联,也可以使用curl_multi_remove_handle()解除关联。
  通过调用curl_multi_perform()可以驱动传输操作。接下来的操作就跟通常select()开发一样,不过libcurl没有将socket暴露出来,所以需要其他额外的机制和接口配合select()来使用:因为之前已经使用curl_multi_add_handle()将各个handle进行收集,接下来就可以使用curl_multi_fdset()聚集得到maxfd参数;根据这个maxfd显式调用select(),当该调用返回后说明超时或者有就绪的fd了,curl_multi_perform()有一个参数记录了当前仍然在运行的handle数目,通过判断他就可以知道所有的任务是否都完成了。整个事件循环的过程就是不断的调用curl_multi_fdset()-select()-curl_multi_perform()来实现的。
  当curl_multi_perform()的那个输入参数更新为0,就表示整个传输过程结束了,接下去的就是善后工作了。通过curl_multi_info_read()可以遍历读取每个handle的执行状态成功与否。接下来按照顺序调用curl_multi_cleanup()和各个curl_easy_cleanup,释放对应的资源。

二、上传数据到远端

  libcurl设计的时候讲求操作接口和具体协议分开,因此不论使用HTTP PUT还是FTP协议,上传数据的操作接口都是比较类似的。和上面的retrieve不同,这里需要通过READFUNCTION设置read回调函数,同时还可以设置READDATA设置私有数据指针,让libcurl通过该函数获取用户需要上传的数据。
  因为默认libcurl是进行retrieve操作,所以还需要显示指定UPLOAD选项告知进行上传操作。对于用户设置的读回调函数,需要返回每次写入发送缓存的字节数目,返回0表示上传操作结束。

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
static size_t readdata(void* ptr, size_t size, size_t nmemb, void* userp) {
if(!userp || !size || !nmemb)
return 0;
size_t bufSize = size * nmemb;
PtrData* pData = (PtrData *)userp;
size_t copySize = pData->data.size(); // assume std::string
if (copySize > bufSize){
copySize = bufSize;
}
if (copySize){
memcpy(ptr, pData->data.c_str(), copySize);
pData->data.erase(0, copySize);
}
return copySize;
}
int FtpUploadClient(const std::string& strUrl, PtrData* pData) {
try {
CURL *curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_ENCODING , "UTF-8");
curl_easy_setopt(curl, CURLOPT_URL, strUrl.c_str());
curl_easy_setopt(curl, CURLOPT_READFUNCTION, readdata);
curl_easy_setopt(curl, CURLOPT_READDATA, pData);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 200);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 800);

CURLcode res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (res == CURLE_OK)
return 0;
else
return -1;
} catch (...) {
return -1;
}
}

三、HTTP POST数据

3.1 简单方式 simple POST

  因为HTTP协议很多部分是基于文本的,所以在简单情况下POST数据只需要提供字符串指针就可以了,strlen()可以自动计算出需要传输数据的长度,比如:

1
2
3
4
5
std::string message("Hello from server!");
curl_easy_setopt(curl, CURLOPT_POST, 1);
curl_easy_setopt(curl, CURLOPT_URL, "http://taozj.org/api");
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, message.c_str());
curl_easy_perform(curl);

3.2 二进制类型

  对于传输二进制的数据(比如图片、多媒体等),需要设置Content-Type头部信息外,还需要告知需要传输的数据长度:

1
2
3
4
5
6
7
8
9
curl_easy_setopt(curl, CURLOPT_POST, 1);
curl_easy_setopt(curl, CURLOPT_URL, "http://taozj.org/api");
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, ptrImg);
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 1022L);
struct curl_slist *s_headers = NULL;
s_headers = curl_slist_append(s_headers, "Content-Type: img/jpg");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, s_headers);
curl_easy_perform(curl); //do post
curl_slist_free_all(s_headers);

3.3 multi-part传输

  multi-part主要是用来传输大块数据的RFC,他将数据组合一系列的块part表示,然后每一块(每块都有自己的名字和数据)作为单独的单元,对此libcurl提供了curl_formadd接口方便添加part。
  下面是一个例子,包括一个string text的part和一个图像文件的binary part,组合后作为整体上传,在构建form的时候还可以添加CONTENTHEADER头部。还有,libcurl的CURLOPT是sticky的,所以在POST之后如果想使用该handle再次使用GET方式,就必须再显示切换请求方式,安全来说每次操作前最好将参数都重新设置一遍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct curl_httppost *post = NULL; 
struct curl_httppost *last = NULL;
curl_formadd(&post, &last,
CURLFORM_COPYNAME, "name",
CURLFORM_COPYCONTENTS, "nicol tao",
CURLFORM_END);
struct curl_slist *s_headers = NULL;
s_headers = curl_slist_append(s_headers, "Content-Type: text/xml");
curl_formadd(&post, &last,
CURLFORM_COPYNAME, "logotype-image",
CURLFORM_FILECONTENT, "curl.xml",
CURLFORM_CONTENTHEADER, s_headers,
CURLFORM_END);
curl_easy_setopt(easyhandle, CURLOPT_HTTPPOST, post); // form info
curl_easy_perform(easyhandle);
curl_formfree(post);
curl_slist_free_all(s_headers);

四、其他

4.1 用户名和密码

  很多协议为了安全需要用户名和密码进行认证后才允许进行下载或上传文件。多数协议允许将用户名、密码信息编进URL中(如果用户名、密码中含有特殊字符,需要使用URL编码),比如:

1
ftp://user:passsword@taozj.org/upload/data.pdf

  除了上面的方式,libcurl还提供USERPWD方式设置用户名和密码。除此之外,在需要证书的时候,往往需要制定证书的passphase,也可以使用类似的方式指定。

1
2
curl_easy_setopt(easyhandle, CURLOPT_USERPWD, "name:passwd");
curl_easy_setopt(easyhandle, CURLOPT_KEYPASSWD, "passwd");

4.2 HTTP协议头信息

  libcurl在每次请求的时候,都会预置内部头信息Host(如果非标准端口,还带有端口号)和Accept。
  libcurl的HTTP有一个灵活的参数CUSTOMREQUEST,如果HTTP的行为在GET、HEAD、POST等标准语义之外还需要增加请求类型,可以通过设置该参数来达到目的:

1
curl_easy_setopt(easyhandle, CURLOPT_CUSTOMREQUEST, "MYOWNREQUEST"

  修改或者增加HTTP头部参数的方式,上面已经示例用curl_slist的方式实现了(内置的Host、Accept也可以通过这种方式覆盖修改),如果需要删除某个参数就将:后的值置空就可以了。

  HTTP中的Cookie是服务器通过Set-Cookie头发送给客户端的键值对,并且客户端在此后的每次请求中都会带上该Cookie值的头,从而帮助服务端识别对话。libcurl中可以使用COOKIE参数设置其值,但是这几乎没什么用,因为客户端需要真正传递的值是服务端动态生成的,没法再代码中硬编码写死。
  libcurl有个cookie parser功能,当使能该功能后,cookie信息将会保存在内存当中,此后该handle的所有请求都会自动带上相应的Cookie,这是最推荐的使用方式。
  通过设置COOKIEJAR参数指明cookie-jar保存的位置,libcurl会在调用curl_easy_cleanup()的时候将cookie值保存到那个文件中去,如果这个文件刚好是Mozilla等浏览器的cookie-jar文件,那么就可以将cookie分享给浏览器了。

4.4 SSL

  由之前的HTTPS原理可知SSL对上层的应用程序是透明的,因此在libcurl中使用SSL也是极为简单便捷的。

1
2
3
4
5
6
7
8
curl_easy_setopt(curl, CURLOPT_SSLKEYTYPE, "PEM");
curl_easy_setopt(curl, CURLOPT_SSLKEY, pKeyName);
curl_easy_setopt(curl, CURLOPT_SSLKEYPASSWD, pKeyPassphrase); // if
curl_easy_setopt(curl, CURLOPT_SSLCERTTYPE, "PEM");
curl_easy_setopt(curl, CURLOPT_SSLCERT, pCertFile); // Cert
curl_easy_setopt(curl, CURLOPT_KEYPASSWD, pPassphrase); // if
curl_easy_setopt(curl, CURLOPT_CAINFO, pCACertFile); // CACert
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);

五、小结

  通常后台开发的都关注服务端的开发,libcurl作为客户端的开发还是很被忽略的。但是,在比如自己的接口需要调用别人第三方接口的结果后,再进行组合回传的话,libcurl的性能就必须考虑进来,否则就会成为业务的瓶颈所在,此时上面锁说的multi interface就需要得到重视了!

本文完!

参考