libcurl上手笔记-Linux

本文介绍了在CentOS7.6系统中通过yum源安装libcurl-devel的过程,注意到版本较低后,作者选择源码安装curl-8.7.1,并处理了动态库与静态库编译和链接时遇到的兼容性和依赖问题,包括zlib和openssl的链接顺序问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

操作系统:CentOS 7.6。

使用yum源安装

为了方便,直接使用yum安装:yum install libcurl-devel.

Installed:
  libcurl-devel.x86_64 0:7.29.0-59.el7_9.2                                                                                  

Dependency 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

至少还有两个方案是可以尝试的:

  1. 将libcurl编译成静态库,应用程序将libcurl链接进去,这样就跟系统目录下的libcurl.so.4.3.0没关系,就不存在加载错误的动态库的问题了。
  2. 在编译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依次从左向右解析每个库的符号,如果发现一个库中有未定义的符号,就会记录下来,并在后面的库中查找。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值