操作系统:CentOS 7.6。
使用yum源安装
为了方便,直接使用yum安装:yum install libcurl-devel.
Installed:
libcurl-devel.x86_64 0:7.29.0-59.el7_9.2Dependency Updated:
curl.x86_64 0:7.29.0-59.el7_9.2 libcurl.x86_64 0:7.29.0-59.el7_9.2
可以看到安装的版本是7.29, 比最新版本8.71低。
接下来就是测试demo了。
从官网现在examples,下载https.c:
#include <stdio.h>
#include <curl/curl.h>
int main(void)
{
CURL *curl;
CURLcode res;
curl_global_init(CURL_GLOBAL_DEFAULT);
curl = curl_easy_init();
if(curl) {
curl_easy_setopt(curl, CURLOPT_URL, "https://www.baidu.com/");
#ifdef SKIP_PEER_VERIFICATION
/*
* If you want to connect to a site who is not using a certificate that is
* signed by one of the certs in the CA bundle you have, you can skip the
* verification of the server's certificate. This makes the connection
* A LOT LESS SECURE.
*
* If you have a CA cert for the server stored someplace else than in the
* default bundle, then the CURLOPT_CAPATH option might come handy for
* you.
*/
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
#endif
#ifdef SKIP_HOSTNAME_VERIFICATION
/*
* If the site you are connecting to uses a different host name that what
* they have mentioned in their server certificate's commonName (or
* subjectAltName) fields, libcurl refuses to connect. You can skip this
* check, but it makes the connection insecure.
*/
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
#endif
/* cache the CA cert bundle in memory for a week */
// 从7.87版本才开始支持这个选项,先注释掉
//curl_easy_setopt(curl, CURLOPT_CA_CACHE_TIMEOUT, 604800L);
/* Perform the request, res gets the return code */
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);
}
curl_global_cleanup();
return 0;
}
直接在shell中使用命令行编译:
g++ https.c -lssl -lcrypto -lcurl
由于libcurl-devel安装到系统目录下了,所以不需要指定头文件目录和库目录,直接连接就行了。在编译的过程中注释掉了一行代码 ,这是由于linux安装的库版本比example版本更低导致的。
// 从7.87版本才开始支持这个选项,先注释掉
//curl_easy_setopt(curl, CURLOPT_CA_CACHE_TIMEOUT, 604800L);
编译和运行都很顺利。
但是,在实际使用时,用到了curl_multi_poll函数。查看了changelog之后,发现改函数是7.66版本才增加的。虽然使用这个函数不是必要的,考虑到新版有修复的bug以及新的特性,还是决定使用最新的版本。
既然yum源不提供8.71版本的安装包,就采用源码安装。
使用源码安装curl-8.7.1
第一步:从官网https://curl.se/download/curl-8.7.1.zip下载源码包。放到root目录下,并解压。
第二步:由于我需要使用到https,需要用到openssl。我之前一直使用的openssl-1.1.1j的静态库。于是把openssl的头文件和库copy到root目录下。目录结构为:
/root
├─include
│ └─openssl
└─lib└─libssl.a
└─libcrypto.a
目录结构不正确,会导致configure时错误。
第三步:设置、编译、安装。
cd /root/curl-8.7.1
./configure --with-openssl=/root/openssl
make
make install
安装完成之后,动态库和静态库都会安装到/usr/local/lib目录。如下:
ll /usr/local/lib |grep curl
/usr/local/lib/libcurl.a
/usr/local/lib/libcurl.so.4.8.0
/usr/local/lib/libcurl.so.4
/usr/local/lib/libcurl.so
为什么curl-8.7.1编译出来的libcurl动态库是4.8.0版本呢?
头文件会复制到/usr/local/include/curl目录下。
使用动态库
linux默认的LD_LIBRARY_PATH是不包含/usr/local/lib目录的。这样系统里面就有两份libcurl。/usr/lib64目录下还有一份系统自带的libcurl.so.4.3.0版本。
CMakeLists.txt文件中指定链接libcurl.so.4.8.0。
target_link_libraries(${PROJECT_NAME} /usr/local/lib/libcurl.so.4.8.0)
但是运行的时候,又调用了libcurl.so.4.3.0。这是为什么呢?
使用readelf -d 查看libcurl.so.4.3.0和libcurl.so.4.8.0,可以发现,它们的Library soname都是libcrul.so.4:
0x000000000000000e (SONAME) Library soname: [libcurl.so.4]
再用readelf -d查看demo程序,输出如下:
Dynamic section at offset 0x3fed60 contains 31 entries:
标记 类型 名称/值
0x0000000000000001 (NEEDED) 共享库:[libdl.so.2]
0x0000000000000001 (NEEDED) 共享库:[libcurl.so.4]
0x0000000000000001 (NEEDED) 共享库:[libpthread.so.0]
0x0000000000000001 (NEEDED) 共享库:[libstdc++.so.6]
0x0000000000000001 (NEEDED) 共享库:[libm.so.6]
0x0000000000000001 (NEEDED) 共享库:[libgcc_s.so.1]
0x0000000000000001 (NEEDED) 共享库:[libc.so.6]
也就是说,我们的demo程序是依赖libcurl.so.4的,于是ld就会按照一定的顺序去查找soname为libcurl.so.4的动态库,最后找到了/usr/lib64目录中的libcurl.so.4.3.0。
TODO:有时间再研究一下动态库的查找顺序,看看从这上面能否解决。
我首先尝试了一个愚蠢的办法:把/usr/local/lib中的libcurl.so.4.8.0复制到/usr/lib64,并且将/usr/lib64/libcurl.so.4的连接指向了4.8.0,然后将libcurl.so.4.3.0删除掉。结果demo确实能运行起来了。但是,yum不能运行。
我感觉应该是版本兼容性有问题,CRYPTO_num_locks看起来像是openssl库里面的。libcurl.so.4.8.0是我编译的,把openssl1.1.1j静态库链接进去的。使用ldd查看了一下libcurl.so.4.3.0的依赖库:
[root@localhost ~]# ldd /lib64/libcurl.so.4 |grep crypto
libk5crypto.so.3 => /lib64/libk5crypto.so.3 (0x00007fd61a804000)
libcrypto.so.10 => /lib64/libcrypto.so.10 (0x00007fd6194e3000)
可以看到libcurl.so.4.3.0依赖libcrypto.so.10。使用nm -D 查看 libcrypto.so.10是有导出、CRYPTO_num_locks函数的:
[root@localhost ~]# nm -D /usr/lib64/libcrypto.so.10 |grep CRYPTO_num_locks
000000000006d7c0 T CRYPTO_num_locks
再查看1.1.1j版本的libcrypto.a文件:
[root@localhost ~]# nm /root/curl-8.7.1/openssl/lib/libcrypto.a |grep CRYPTO_num_locks [root@localhost ~]#
没有找到这个函数。于是去网上查找资料,其中openssl1.1.1版本的crypto.h头文件的一段代码是这样的:
* On the other hand, the locking callbacks are no longer used. Consequently,
* the callback management functions can be safely replaced with no-op macros.
*/
# define CRYPTO_num_locks() (1)
# define CRYPTO_set_locking_callback(func)
# define CRYPTO_get_locking_callback() (NULL)
# define CRYPTO_set_add_lock_callback(func)
# define CRYPTO_get_add_lock_callback() (NULL)
也就是在1.1.1版本中已经没有这些函数了。
后来,用了LD_PRELOAD来指定加载libcurl.so.4.8.0,运行正常了。命令如下:
export LD_PRELOAD=/usr/local/lib/libcurl.so.4.8.0 && ./demo
至少还有两个方案是可以尝试的:
- 将libcurl编译成静态库,应用程序将libcurl链接进去,这样就跟系统目录下的libcurl.so.4.3.0没关系,就不存在加载错误的动态库的问题了。
- 在编译libcurl.so.4.8.0的时候,将其soname改成libcurl.so.8(本来就是从源码8.7.1安装的)。这样两个版本的soname就不会重复了。但是,不打算这样做,默认的版本号属于一种约定,擅自改了之后,时间长了记不清楚,会造成困扰。
使用静态库
修改demo的CMakeLists.txt文件,链接libcurl.a,同时需要把openssl的库加入:
target_link_libraries(${PROJECT_NAME}
/usr/local/lib/libcurl.a
ssl
crypto)
重新编译,竟然报错:
[ 11%] Linking CXX executable curl_demo
../../../../source/curl/linux/libcurl.a(libcurl_la-content_encoding.o):在函数‘exit_zlib’中:
content_encoding.c:(.text+0x1b4):对‘inflateEnd’未定义的引用
../../../../source/curl/linux/libcurl.a(libcurl_la-content_encoding.o):在函数‘inflate_stream’中:
content_encoding.c:(.text+0x325):对‘inflate’未定义的引用
content_encoding.c:(.text+0x3b3):对‘inflateEnd’未定义的引用
content_encoding.c:(.text+0x3cc):对‘inflateInit2_’未定义的引用
../../../../source/curl/linux/libcurl.a(libcurl_la-content_encoding.o):在函数‘gzip_do_init’中:
content_encoding.c:(.text+0x515):对‘zlibVersion’未定义的引用
content_encoding.c:(.text+0x54b):对‘inflateInit2_’未定义的引用
content_encoding.c:(.text+0x571):对‘inflateInit2_’未定义的引用
../../../../source/curl/linux/libcurl.a(libcurl_la-content_encoding.o):在函数‘deflate_do_init’中:
content_encoding.c:(.text+0x5d3):对‘inflateInit_’未定义的引用
collect2: 错误:ld 返回 1
make[2]: *** [binance_app] 错误 1
make[1]: *** [CMakeFiles/binance_app.dir/all] 错误 2
make: *** [all] 错误 2
从报错内容来看是找不到zlib,于是在CMakeList.txt文件中添加zlib的库:
target_link_libraries(${PROJECT_NAME} z)
重新编译,成功;运行,成功。
使用readelf -d curl_demo,可以看到已经不依赖libcurl.so库了。
问题:为什么demo使用libcurl动态库时,不需要链接zlib;但是使用静态库时,需要链接zlib?
真正要使用的zlib的不是demo,而是libcurl。当使用动态库时,链接libz是在libcurl.so的时候完成的。而使用静态库时,链接libz是在编译demo的时候完成的。可以断定,在编译libcurl.so的脚本中一定有-lz的连接选项。
还有一个小细节,libcurl库必须放在ssl、crypto库前面,否则还是会报找不到openssl函数的问题。这个就是gcc链接静态库的顺序问题,如果A依赖B,A需要放在B的前面。stackoverflow上有个回答讲得很清楚。why-does-the-order-in-which-libraries-are-linked-sometimes-cause-errors-in-gcc
大概的原理是,gcc依次从左向右解析每个库的符号,如果发现一个库中有未定义的符号,就会记录下来,并在后面的库中查找。