Android C++系列:JNI中发送Http网络请求

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天, 点击查看活动详情

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兼容的,特性丰富,有着良好,快速,充分的文档,已经被许多知名的,很多大厂都在使用。

官方文档: curl.se/libcurl/

3. libcurl库编译

3.1 编译openssl

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

我们从 github.com/openssl/ope… 下载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版本:github.com/nghttp2/ngh…

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源码github.com/curl/curl/r… 后解压,进入源码目录执行:

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。

官方文档: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包含以下内容:

  • age:age of the returned struct
  • version:LIBCURL_VERSION
  • version_num:LIBCURL_VERSION_NUM
  • host:OS/host/cpu/machine when configured
  • features:bitmask
  • ssl_version:human readable string
  • 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, "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;
          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>
        <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>