头图

1. 背景

之前Linux网络编程的文章下有小伙帮咨询jni中发送http请求的示例,本文基于libcurl库实现http网络请求发送功能。

2. libcurl库介绍

libcurl是一个免费和易于使用的客户端URL传输库,支持DICT, FILE, FTP, FTPS, GOPHER, gopers, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, MQTT, POP3, POP3S, RTMP, RTMP, RTSP, SCP, SFTP, SMB, SMBS, SMTP, SMTPS, TELNET和TFTP。libcurl支持SSL证书,HTTP POST, HTTP PUT, FTP上传,HTTP表单上传,代理,HTTP/2, HTTP/3, cookie,用户+密码认证(基本,摘要,NTLM,协商,Kerberos),文件传输恢复,HTTP代理隧道等等!

libcurl是高度可移植的,它构建和工作在许多平台上,包括Solaris, NetBSD, FreeBSD, OpenBSD,达尔文,HPUX, IRIX, AIX, Tru64, Linux, UnixWare, HURD, Windows, Amiga, OS/2, BeOs, Mac OS X, Ultrix, QNX, OpenVMS, RISC OS, Novell NetWare, DOS等等。

libcurl是免费的,线程安全的,IPv6兼容的,特性丰富,有着良好,快速,充分的文档,已经被许多知名的,很多大厂都在使用。

官方文档:https://curl.se/libcurl/

3. libcurl库编译

3.1 编译openssl

libcurl支持SSL证书,我们需要支持HTTPS的话需要依赖openssl库,我们先把openssl库编译出来。

我们从 https://github.com/openssl/openssl/archive/OpenSSL_1_1_0h.tar.gz 下载1.1.0h版本的openssl库,解压后执行Configure配置脚本:

Configure" \
"${OPENSSL_TARGET}" \
-DARCH="${OPENSSL_ARCH}" \
-DCROSS_COMPILE="${OPENSSL_CROSS_COMPILE}" \
-DMACHINE="${OPENSSL_MACHINE}" \
-DRELEASE="${OPENSSL_RELEASE}" \
-DSYSTEM="${OPENSSL_SYSTEM}" \
no-asm \
no-comp \
no-dso \
no-dtls \
no-engine \
no-hw \
no-idea \
no-nextprotoneg \
no-psk \
no-srp \
no-ssl3 \
no-weak-ssl-ciphers \
--prefix="${INSTALL_TARGET}" \
--openssldir="${INSTALL_TARGET}/ssl" \
-D_FORTIFY_SOURCE="2" -fstack-protector-strong

由于我们是使用ndk交叉编译,需要配置架构ARCH和跨平台编译器CROSS_COMPILE。

再执行make 进行编译。

3.2 编译nghttp2

如果需要支持HTTP2协议,需要依赖nghttp2库,这里我们下载1.32.0版本:https://github.com/nghttp2/nghttp2/releases/download/v1.32.0/nghttp2-1.32.0.tar.gz,解压后运行configure脚本:

configure" \
${DISABLE_RPATH} \
--prefix="${INSTALL_TARGET}" \
--host="${TOOLCHAIN_HOST}" \
--build="${TOOLCHAIN_BUILD}" \
--enable-static="YES" \
--enable-shared="YES" \
CPPFLAGS="-fPIE -D_FORTIFY_SOURCE=2 -fstack-protector-strong" \
LDFLAGS="-fPIE -pie" \
PKG_CONFIG_LIBDIR="${INSTALL_TARGET_LIB}/pkgconfig"

执行make编译。

3.3 编译curl

下载7.61.0版本curl源码https://github.com/curl/curl/releases/download/curl-7_61_0/curl-7.61.0.tar.gz 后解压,进入源码目录执行:

autoreconf -i
automake
autoconf

配置编译选项:

CFLAGS="-fstack-protector-strong" \
CPPFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -I\"${INSTALL_TARGET_INCLUDE}\"" \
LDFLAGS="-L${INSTALL_TARGET_LIB} -Wl,-rpath=${INSTALL_TARGET_LIB}" 

configure" \
          ${DISABLE_RPATH} \
          --prefix="${INSTALL_TARGET}" \
          --with-sysroot="${SYSROOT}" \
          --host="${TOOLCHAIN_HOST}" \
          --build="${TOOLCHAIN_BUILD}" \
          --enable-optimize \
          --enable-hidden-symbols \
          --disable-largefile \
          --disable-static \
          --disable-ftp \
          --disable-file \
          --disable-ldap \
          --disable-rtsp \
          --disable-proxy \
          --disable-dict \
          --disable-telnet \
          --disable-tftp \
          --disable-pop3 \
          --disable-imap \
          --disable-smb \
          --disable-smtp \
          --disable-gopher \
          --disable-manual \
          --disable-verbose \
          --disable-sspi \
          --disable-crypto-auth \
          --disable-tls-srp \
          --disable-unix-sockets \
          --enable-cookies \
          --without-zlib \
          --with-ssl="${INSTALL_TARGET}" \
          --with-ca-bundle="${CURL_CA_BUNDLE}" \
          --with-nghttp2="${INSTALL_TARGET}"

这里面最后配置了ssl和nghttp2库的路径。

执行make编译。

4. libcurl库API介绍

编译出最终的库后可以开始使用了,使用前我们先了解libcurl库主要API。

官方文档:https://curl.se/libcurl/c/

4.1 全局初始化

应用程序在使用libcurl之前,必须先初始化libcurl。libcurl只需初始化一次。可以使用以下语句进行初始化:

curl_global_init();

curl_global_init()接收一个参数,告诉libcurl如何初始化。参数CURL_GLOBAL_ALL 会使libcurl初始化所有的子模块和一些默认的选项,我们通常使用这个默认值即可。还有两个可选值:

CURL_GLOBAL_WIN32

只能应用于Windows平台。它告诉libcurl初始化winsock库。如果winsock库没有正确地初始化,应用程序就不能使用socket。在应用程序中,只要初始化一次即可。

CURL_GLOBAL_SSL

如果libcurl在编译时被设定支持SSL,那么该参数用于初始化相应的SSL库。同样,在应用程序中,只要初始化一次即可。

libcurl有默认的保护机制,如果在调用curl_easy_perform时它检测到还没有通过curl_global_init进行初始化,libcurl会根据当前的运行时环境,自动调用全局初始化函数。但是,安全起见,我们还是自己来全局初始化一波。当应用程序不再使用libcurl的时候,应该调用curl_global_cleanup来释放相关的资源。

注意:使用过程中应当避免多次调用curl_global_init和curl_global_cleanup,最好是进程启动和进程结束时各调用一次。

4.2 版本信息

在运行时根据libcurl支持的特性来进行开发,通常比编译时更好。可以通过调用curl_version_info函数返回的结构体来获取运行时的具体信息,从而确定当前环境下libcurl支持的一些特性。比如我们查看是否支持HTTP2:

if (!(curl_version_info(CURLVERSION_NOW)->features & CURL_VERSION_HTTP2)) {
    LOGI("curl not support http2");
  }

curl_version_info_data包含以下内容:

  1. age:age of the returned struct
  2. version:LIBCURL_VERSION
  3. version_num:LIBCURL_VERSION_NUM
  4. host:OS/host/cpu/machine when configured
  5. features:bitmask
  6. ssl_version:human readable string
  7. ssl_version_num:not used anymore, always 0

4.3 easy interface

libcurl提供了两种接口:easy interface与multi interface。

  • easy interface是同步的,高效的,快速上手的,许多应用程序都是使用这种方法构建的。
  • multi interface是异步的,它还提供了使用单线程或多线程的多路传输。

easy interface的api函数都是有相同的前缀:curl_easy。

4.3.1 创建easy handle

要使用easy interface,首先必须创建一个easy handle,easy handle用于执行每次操作。下面的函数用于获取一个easy handle :

CURL *easy_handle = curl_easy_init();

每个线程都应该有自己的easy handle用于网络请求。千万不要在多线程之间共享同一个easy handle。

4.3.2 设置属性

在easy handle上可以设置属性和操作(action)。easy handle就像一个逻辑连接,用于接下来要进行的数据传输。

使用curl_easy_setopt函数可以设置easy handle的属性和操作,这些属性和操作控制libcurl如何与远程主机进行数据通信。一旦在easy handle中设置了相应的属性和操作,它们将一直作用与该easy handle。也就是说,重复使用easy hanle向远程主机发出请求,先前设置的属性仍然生效。

easy handle的许多属性使用字符串(以/0结尾的字节数组)来设置。通过curl_easy_setopt函数设置字符串属性时,libcurl内部会自动拷贝这些字符串,所以在设置完相关属性之后,字符串可以直接被释放掉。

easy handle最基本、最常用的属性是URL。你应当通过CURLOPT_URL属性提供适当的URL:

curl_easy_setopt(easy_handle, CURLOPT_URL, "http://baidu.com ");

4.3.3 设置回调函数

我们发起请求后需要获取请求响应,这个时候需要通过curl_easy_setopt来设置回调函数,回调函数的原型如下:

size_t write_data(void *buffer, size_t size, size_t nmemb, void *userp);

使用下面的语句来注册回调函数,回调函数将会在接收到数据的时候被调用:

curl_easy_setopt(easy_handle, CURLOPT_WRITEFUNCTION, write_data);

可以给回调函数提供一个自定义参数(libcurl不处理该参数,只是简单的传递):

curl_easy_setopt(easy_handle, CURLOPT_WRITEDATA, &internal_struct);

如果你没有通过CURLOPT_WRITEFUNCTION属性给easy handle设置回调函数,libcurl会提供一个默认的回调函数,它只是简单的将接收到的数据打印到标准输出。我们可以通过CURLOPT_WRITEDATA属性给默认回调函数传递一个已经打开的文件指针,用于将数据输出到文件里。

4.3.4 执行网络请求

调用curl_easy_perform函数,将执行真正的数据通信:

success = curl_easy_perform(easy_handle);

curl_easy_perfrom将连接到远程主机,执行必要的命令,并接收数据。当接收到数据时,先前设置的回调函数将被调用。libcurl可能一次只接收到1字节的数据,也可能接收到好几K的数据,libcurl会尽可能多、及时的将数据传递给回调函数。回调函数返回接收的数据长度。如果回调函数返回的数据长度与传递给它的长度不一致(即返回长度 != size * nmemb),libcurl将会终止操作,并返回一个错误代码。

当数据传递结束的时候,curl_easy_perform将返回一个代码表示操作成功或失败。如果需要获取更多有关通信细节的信息,你可以设置CURLOPT_ERRORBUFFER属性,让libcurl缓存许多可读的错误信息。

easy handle在完成一次数据通信之后可以被重用,libcurl推荐重用一个已经存在的easy handle。如果在完成数据传输之后,你创建另一个easy handle来执行其他的数据通信,libcurl在内部会尝试着重用上一次创建的连接。

4.3.5 释放easy handle

可以通过curl_easy_cleanup释放easy handle。

4.4 multi interface

上面介绍的easy interface以同步的方式进行数据传输,curl_easy_perform会一直阻塞到数据传输完毕后返回,且一次操作只能发送一次请求,如果要同时发送多个请求,必须使用多线程。 而multi interface以一种简单的、非阻塞的方式进行传输,它允许在一个线程中,同时提交多个相同类型的请求。 multi interface是建立在easy interface基础之上的,它只是简单的将多个easy handler添加到一个multi stack,而后同时传输而已。
使用multi interface很简单,首先使用curl_multi_init()函数创建一个multi handler,然后使用curl_easy_init()创建一个或多个easy handler,并按照上面介绍的接口正常的设置相关的属性,然后通过curl_multi_add_handler将这些easy handler添加到multi handler,最后调用curl_multi_perform进行数据传输。

curl_multi_perform是异步的、非阻塞的函数。如果它返回CURLM_CALL_MULTI_PERFORM,表示数据通信正在进行。

每个easy handler在低层就是一个socket,通过select()来管理这些socket,在有数据可读/可写/异常的时候,通知应用程序,所以通过select()来操作multi interface将会使工作变得简单。在调用select()函数之前,应该使用curl_multi_fdset来初始化fd_set变量。

select()函数返回时,说明受管理的低层socket可以操作相应的操作(接收数据或发送数据,或者连接已经断开),此时应该马上调用curl_multi_perform,libcurl将会执行相应操作。使用select()时,应该设置一个较短的超时时间。在调用select()之前,不要忘记通过curl_multi_fdset来初始化fd_set,因为每次操作,fd_set中的文件描述符可能都不一样。

如果想中止multi stack中某一个easy handle的数据通信,可以调用curl_multi_remove_handle函数将其从multi stack中取出。同事不要忘记释放掉easy handle(通过curl_easy_cleanup()函数)。

当multi stack中的一个eash handle完成数据传输的时候,同时运行的传输任务数量就会减少一个。当数量降到0的时候,说明所有的数据传输已经完成。

curl_multi_info_read用于获取当前已经完成的传输任务信息,它返回每一个easy handle的CURLcode状态码。可以根据这个状态码来判断每个easy handle传输是否成功。

5. 发送网络请求示例

5.1 使用easy interface发送http请求

我们简单在回调结果中打印响应内容:

size_t process_data(void *buffer, size_t size, size_t nmemb, void *user_p) {
  FILE *fp = (FILE *)user_p;
  size_t return_size = fwrite(buffer, size, nmemb, fp);
  LOGI("process_data = %s", buffer);

  return return_size;
}

发送请求:

static jint
_httprequest(JNIEnv *env, jclass cls) {
  CURL *easy_handle = curl_easy_init();
  curl_easy_setopt(easy_handle, CURLOPT_URL, "http://baidu.com");
  curl_easy_setopt(easy_handle, CURLOPT_WRITEFUNCTION, &process_data);
  curl_easy_perform(easy_handle);
  curl_easy_cleanup(easy_handle);
  return 0;
}

打印结果:

process_data = <html>
    <meta http-equiv="refresh" content="0;url=http://www.baidu.com/">
    </html>

5.2 使用multi interface发送http请求

我们创建两个easy handle用来分别向新浪和搜狐网站发送请求并打印响应结果:

size_t save_sina_page(void *buffer, size_t size, size_t count, void *user_p){
  LOGI("save_sina_page = %s", buffer);

  return size;
}
size_t save_sohu_page(void *buffer, size_t size, size_t count, void *user_p){
  LOGI("save_sohu_page = %s", buffer);

  return size;
}
static jint
_httprequest2(JNIEnv *env, jclass cls) {
  CURLM *multi_handle = NULL;
  CURL *easy_handle1 = NULL;
  CURL *easy_handle2 = NULL;

  multi_handle = curl_multi_init();

  // 设置easy handle
  easy_handle1 = curl_easy_init();
  curl_easy_setopt(easy_handle1, CURLOPT_URL, "http://www.sina.com.cn");
  curl_easy_setopt(easy_handle1, CURLOPT_WRITEFUNCTION, &save_sina_page);

  easy_handle2 = curl_easy_init();
  curl_easy_setopt(easy_handle2, CURLOPT_URL, "http://www.sohu.com");
  curl_easy_setopt(easy_handle2, CURLOPT_WRITEFUNCTION, &save_sohu_page);

  // 添加到multi stack
  curl_multi_add_handle(multi_handle, easy_handle1);
  curl_multi_add_handle(multi_handle, easy_handle2);

  //
  int running_handle_count;
  while (CURLM_CALL_MULTI_PERFORM == curl_multi_perform(multi_handle, &running_handle_count))
  {
    LOGI("running_handle_count = %d", running_handle_count);
  }

  while (running_handle_count)
  {
    timeval tv;
    tv.tv_sec = 1;
    tv.tv_usec = 0;

    int max_fd;
    fd_set fd_read;
    fd_set fd_write;
    fd_set fd_except;

    FD_ZERO(&fd_read);
    FD_ZERO(&fd_write);
    FD_ZERO(&fd_except);

    curl_multi_fdset(multi_handle, &fd_read, &fd_write, &fd_except, &max_fd);
    int return_code = select(max_fd + 1, &fd_read, &fd_write, &fd_except, &tv);
    if (-1 == return_code)
    {
      LOGI("select error.");
      break;
    }
    else
    {
      while (CURLM_CALL_MULTI_PERFORM == curl_multi_perform(multi_handle, &running_handle_count))
      {
        LOGI("running_handle_count = %d", running_handle_count);
      }
    }
  }

  // 释放资源
  curl_easy_cleanup(easy_handle1);
  curl_easy_cleanup(easy_handle2);
  curl_multi_cleanup(multi_handle);
  curl_global_cleanup();
  return 0;
}

执行结果:

2022-02-11 16:08:58.213 21853-23488/com.qingkouwei.chttp2 I/JNI_HTTP: [save_sina_page():60]save_sina_page = <html>
    <head><title>302 Found</title></head>
    <body>
    <center><h1>302 Found</h1></center>
    <hr><center>nginx</center>
    </body>
    </html>
2022-02-11 16:08:58.220 21853-23488/com.qingkouwei.chttp2 I/JNI_HTTP: [save_sohu_page():65]save_sohu_page = <html>
    <head><title>307 Temporary Redirect</title></head>
    <body bgcolor="white">
    <center><h1>307 Temporary Redirect</h1></center>
    <hr><center>nginx</center>
    </body>
    </html>

6. 总结

本文介绍了Android在jni中使用libcurl发送http网络请求,libcurl是一个传统的功能强大的客户端网络库,优点是成熟稳定,确定是功能强大带来的臃肿,编译出来的动态库有400多k。稳重介绍了libcurl的跨平台交叉编译方法以及libcurl的API,并提供了基于easy interface与multi interface的http请求示例。


轻口味
16.9k 声望3.9k 粉丝

移动端十年老人,主要做IM、音视频、AI方向,目前在做鸿蒙化适配,欢迎这些方向的同学交流:wodekouwei